source: sasview/src/sas/qtgui/DataExplorer.py @ f82ab8c

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since f82ab8c was f82ab8c, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

More dialogs, drag and drop onto File Load

  • Property mode set to 100755
File size: 19.1 KB
Line 
1# global
2import sys
3import os
4import logging
5
6from PyQt4 import QtCore
7from PyQt4 import QtGui
8from PyQt4 import QtWebKit
9from twisted.internet import threads
10
11# SAS
12from GuiUtils import *
13from Plotter import Plotter
14from sas.sascalc.dataloader.loader import Loader
15from sas.sasgui.guiframe.data_manager import DataManager
16
17from DroppableDataLoadWidget import DroppableDataLoadWidget
18
19# This is how to get data1/2D from the model item
20# data = [selected_items[0].child(0).data().toPyObject()]
21
22class DataExplorerWindow(DroppableDataLoadWidget):
23    # The controller which is responsible for managing signal slots connections
24    # for the gui and providing an interface to the data model.
25
26    def __init__(self, parent=None, guimanager=None):
27        super(DataExplorerWindow, self).__init__(parent, guimanager)
28
29        # Main model for keeping loaded data
30        self.model = QtGui.QStandardItemModel(self)
31
32        # Secondary model for keeping frozen data sets
33        self.theory_model = QtGui.QStandardItemModel(self)
34
35        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
36        # in order to set the widget parentage properly.
37        self.parent = guimanager
38        self.loader = Loader()
39        self.manager = DataManager()
40
41        # Connect the buttons
42        self.cmdLoad.clicked.connect(self.loadFile)
43        self.cmdDeleteData.clicked.connect(self.deleteFile)
44        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
45        self.cmdFreeze.clicked.connect(self.freezeTheory)
46        self.cmdSendTo.clicked.connect(self.sendData)
47        self.cmdNew.clicked.connect(self.newPlot)
48
49        # Connect the comboboxes
50        self.cbSelect.currentIndexChanged.connect(self.selectData)
51
52        #self.closeEvent.connect(self.closeEvent)
53        # self.aboutToQuit.connect(self.closeEvent)
54
55        self.communicator.fileReadSignal.connect(self.loadFromURL)
56
57        # Proxy model for showing a subset of Data1D/Data2D content
58        self.proxy = QtGui.QSortFilterProxyModel(self)
59        self.proxy.setSourceModel(self.model)
60
61        # The Data viewer is QTreeView showing the proxy model
62        self.treeView.setModel(self.proxy)
63
64        # Theory model view
65        #self.freezeView.setModel(self.theory_model)
66
67    def closeEvent(self, event):
68        """
69        Overwrite the close event - no close!
70        """
71        event.ignore()
72
73    def loadFromURL(self, url):
74        """
75        Threaded file load
76        """
77        load_thread = threads.deferToThread(self.readData, url)
78        load_thread.addCallback(self.loadComplete)
79
80    def loadFile(self, event=None):
81        """
82        Called when the "Load" button pressed.
83        Opens the Qt "Open File..." dialog
84        """
85        path_str = self.chooseFiles()
86        if not path_str:
87            return
88        self.loadFromURL(path_str)
89
90    def loadFolder(self, event=None):
91        """
92        Called when the "File/Load Folder" menu item chosen.
93        Opens the Qt "Open Folder..." dialog
94        """
95        dir = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
96              QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog)
97        if dir is None:
98            return
99
100        dir = str(dir)
101
102        if not os.path.isdir(dir):
103            return
104
105        # get content of dir into a list
106        path_str = [os.path.join(os.path.abspath(dir), filename) for filename in os.listdir(dir)]
107
108        self.loadFromURL(path_str)
109
110    def deleteFile(self, event):
111        """
112        Delete selected rows from the model
113        """
114        # Assure this is indeed wanted
115        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
116                     "\nDo you want to continue?"
117        reply = QtGui.QMessageBox.question(self, 'Warning', delete_msg,
118                QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
119
120        if reply == QtGui.QMessageBox.No:
121            return
122
123        # Figure out which rows are checked
124        ind = -1
125        # Use 'while' so the row count is forced at every iteration
126        while ind < self.model.rowCount():
127            ind += 1
128            item = self.model.item(ind)
129            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
130                # Delete these rows from the model
131                self.model.removeRow(ind)
132                # Decrement index since we just deleted it
133                ind -= 1
134
135        # pass temporarily kept as a breakpoint anchor
136        pass
137
138    def deleteTheory(self, event):
139        """
140        Delete selected rows from the theory model
141        """
142        # Assure this is indeed wanted
143        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
144                     "\nDo you want to continue?"
145        reply = QtGui.QMessageBox.question(self, 'Warning', delete_msg,
146                QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
147
148        if reply == QtGui.QMessageBox.No:
149            return
150
151        # Figure out which rows are checked
152        ind = -1
153        # Use 'while' so the row count is forced at every iteration
154        while ind < self.theory_model.rowCount():
155            ind += 1
156            item = self.theory_model.item(ind)
157            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
158                # Delete these rows from the model
159                self.theory_model.removeRow(ind)
160                # Decrement index since we just deleted it
161                ind -= 1
162
163        # pass temporarily kept as a breakpoint anchor
164        pass
165
166    def sendData(self, event):
167        """
168        Send selected item data to the current perspective and set the relevant notifiers
169        """
170        # should this reside on GuiManager or here?
171        self._perspective = self.parent.perspective()
172
173        # Set the signal handlers
174        self.communicator = self._perspective.communicator()
175        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
176
177        # Figure out which rows are checked
178        selected_items = []
179        for index in range(self.model.rowCount()):
180            item = self.model.item(index)
181            if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
182                selected_items.append(item)
183
184        if len(selected_items) < 1:
185            return
186
187        # Which perspective has been selected?
188        if len(selected_items) > 1 and not self._perspective.allowBatch():
189            msg = self._perspective.title() + " does not allow multiple data."
190            msgbox = QtGui.QMessageBox()
191            msgbox.setIcon(QtGui.QMessageBox.Critical)
192            msgbox.setText(msg)
193            msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
194            retval = msgbox.exec_()
195            return
196
197        # Dig up the item
198        data = selected_items
199
200        # TODO
201        # New plot or appended?
202
203        # Notify the GuiManager about the send request
204        self._perspective.setData(data_item=data)
205
206    def freezeTheory(self, event):
207        """
208        Freeze selected theory rows.
209
210        "Freezing" means taking the plottable data from the filename item
211        and copying it to a separate top-level item.
212        """
213        import copy
214        # Figure out which _inner_ rows are checked
215        # Use 'while' so the row count is forced at every iteration
216        outer_index = -1
217        while outer_index < self.model.rowCount():
218            outer_index += 1
219            outer_item = self.model.item(outer_index)
220            if not outer_item:
221                continue
222            for inner_index in xrange(outer_item.rowCount()):
223                subitem = outer_item.child(inner_index)
224                if subitem and subitem.isCheckable() and subitem.checkState() == QtCore.Qt.Checked:
225                    # Update the main model
226                    new_item = subitem.takeRow(inner_index)
227                    #new_item = QtGui.QStandardItem(subitem)
228                    #new_item = subitem.clone()
229                    #new_item = QtGui.QStandardItem()
230                    #new_item = copy.deepcopy(subitem)
231                    #super(new_item, self).__init__()   
232                    self.model.insertRow(0, new_item)
233                    outer_index += 1
234
235        self.model.reset()
236
237    def newPlot(self):
238        """
239        Create a new matplotlib chart from selected data
240
241        TODO: Add 2D-functionality
242        """
243
244        plots = plotsFromCheckedItems(self.model)
245
246        # Call show on requested plots
247        new_plot = Plotter()
248        for plot_set in plots:
249            new_plot.data(plot_set)
250            new_plot.plot()
251
252        new_plot.show()
253
254    def chooseFiles(self):
255        """
256        Shows the Open file dialog and returns the chosen path(s)
257        """
258        # List of known extensions
259        wlist = self.getWlist()
260
261        # Location is automatically saved - no need to keep track of the last dir
262        # But only with Qt built-in dialog (non-platform native)
263        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
264                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
265        if paths is None:
266            return
267
268        if type(paths) == QtCore.QStringList:
269            paths = [str(f) for f in paths]
270
271        if paths.__class__.__name__ != "list":
272            paths = [paths]
273
274        return paths
275
276    def readData(self, path):
277        """
278        verbatim copy/paste from
279            sasgui\guiframe\local_perspectives\data_loader\data_loader.py
280        slightly modified for clarity
281        """
282        message = ""
283        log_msg = ''
284        output = {}
285        any_error = False
286        data_error = False
287        error_message = ""
288       
289        for p_file in path:
290            info = "info"
291            basename = os.path.basename(p_file)
292            _, extension = os.path.splitext(basename)
293            if extension.lower() in EXTENSIONS:
294                any_error = True
295                log_msg = "Data Loader cannot "
296                log_msg += "load: %s\n" % str(p_file)
297                log_msg += """Please try to open that file from "open project" """
298                log_msg += """or "open analysis" menu\n"""
299                error_message = log_msg + "\n"
300                logging.info(log_msg)
301                continue
302
303            try:
304                message = "Loading Data... " + str(basename) + "\n"
305
306                # change this to signal notification in GuiManager
307                self.communicator.statusBarUpdateSignal.emit(message)
308
309                output_objects = self.loader.load(p_file)
310
311                # Some loaders return a list and some just a single Data1D object.
312                # Standardize.
313                if not isinstance(output_objects, list):
314                    output_objects = [output_objects]
315
316                for item in output_objects:
317                    # cast sascalc.dataloader.data_info.Data1D into sasgui.guiframe.dataFitting.Data1D
318                    # TODO : Fix it
319                    new_data = self.manager.create_gui_data(item, p_file)
320                    output[new_data.id] = new_data
321                    self.updateModel(new_data, p_file)
322                    self.model.reset()
323
324                    QtGui.qApp.processEvents()
325
326                    if hasattr(item, 'errors'):
327                        for error_data in item.errors:
328                            data_error = True
329                            message += "\tError: {0}\n".format(error_data)
330                    else:
331
332                        logging.error("Loader returned an invalid object:\n %s" % str(item))
333                        data_error = True
334
335            except Exception as ex:
336                logging.error(sys.exc_value)
337
338                any_error = True
339            if any_error or error_message != "":
340                if error_message == "":
341                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
342                    error += "while loading Data: \n%s\n" % str(basename)
343                    error_message += "The data file you selected could not be loaded.\n"
344                    error_message += "Make sure the content of your file"
345                    error_message += " is properly formatted.\n\n"
346                    error_message += "When contacting the SasView team, mention the"
347                    error_message += " following:\n%s" % str(error)
348                elif data_error:
349                    base_message = "Errors occurred while loading "
350                    base_message += "{0}\n".format(basename)
351                    base_message += "The data file loaded but with errors.\n"
352                    error_message = base_message + error_message
353                else:
354                    error_message += "%s\n" % str(p_file)
355                info = "error"
356       
357        if any_error or error_message:
358            # self.loadUpdate(output=output, message=error_message, info=info)
359            self.communicator.statusBarUpdateSignal.emit(error_message)
360
361        else:
362            message = "Loading Data Complete! "
363        message += log_msg
364        return output, message
365
366    def getWlist(self):
367        """
368        Wildcards of files we know the format of.
369        """
370        # Display the Qt Load File module
371        cards = self.loader.get_wildcards()
372
373        # get rid of the wx remnant in wildcards
374        # TODO: modify sasview loader get_wildcards method, after merge,
375        # so this kludge can be avoided
376        new_cards = []
377        for item in cards:
378            new_cards.append(item[:item.find("|")])
379        wlist = ';;'.join(new_cards)
380
381        return wlist
382
383    def selectData(self, index):
384        """
385        Callback method for modifying the TreeView on Selection Options change
386        """
387        if not isinstance(index, int):
388            msg = "Incorrect type passed to DataExplorer.selectData()"
389            raise AttributeError, msg
390
391        # Respond appropriately
392        if index == 0:
393            # Select All
394            for index in range(self.model.rowCount()):
395                item = self.model.item(index)
396                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
397                    item.setCheckState(QtCore.Qt.Checked)
398        elif index == 1:
399            # De-select All
400            for index in range(self.model.rowCount()):
401                item = self.model.item(index)
402                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
403                    item.setCheckState(QtCore.Qt.Unchecked)
404
405        elif index == 2:
406            # Select All 1-D
407            for index in range(self.model.rowCount()):
408                item = self.model.item(index)
409                item.setCheckState(QtCore.Qt.Unchecked)
410
411                try:
412                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
413                except AttributeError:
414                    msg = "Bad structure of the data model."
415                    raise RuntimeError, msg
416
417                if is1D:
418                    item.setCheckState(QtCore.Qt.Checked)
419
420        elif index == 3:
421            # Unselect All 1-D
422            for index in range(self.model.rowCount()):
423                item = self.model.item(index)
424
425                try:
426                    is1D = item.child(0).data().toPyObject().__class__.__name__ == 'Data1D'
427                except AttributeError:
428                    msg = "Bad structure of the data model."
429                    raise RuntimeError, msg
430
431                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
432                    item.setCheckState(QtCore.Qt.Unchecked)
433
434        elif index == 4:
435            # Select All 2-D
436            for index in range(self.model.rowCount()):
437                item = self.model.item(index)
438                item.setCheckState(QtCore.Qt.Unchecked)
439                try:
440                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
441                except AttributeError:
442                    msg = "Bad structure of the data model."
443                    raise RuntimeError, msg
444
445                if is2D:
446                    item.setCheckState(QtCore.Qt.Checked)
447
448        elif index == 5:
449            # Unselect All 2-D
450            for index in range(self.model.rowCount()):
451                item = self.model.item(index)
452
453                try:
454                    is2D = item.child(0).data().toPyObject().__class__.__name__ == 'Data2D'
455                except AttributeError:
456                    msg = "Bad structure of the data model."
457                    raise RuntimeError, msg
458
459                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
460                    item.setCheckState(QtCore.Qt.Unchecked)
461
462        else:
463            msg = "Incorrect value in the Selection Option"
464            # Change this to a proper logging action
465            raise Exception, msg
466
467
468    def loadComplete(self, output):
469        """
470        Post message to status bar and update the data manager
471        """
472        # Don't show "empty" rows with data objects
473        self.proxy.setFilterRegExp(r"[^()]")
474
475        # Reset the model so the view gets updated.
476        self.model.reset()
477        assert type(output)== tuple
478
479        output_data = output[0]
480        message = output[1]
481        # Notify the manager of the new data available
482        self.communicator.statusBarUpdateSignal.emit(message)
483        self.communicator.fileDataReceivedSignal.emit(output_data)
484        self.manager.add_data(data_list=output_data)
485
486    def updateModel(self, data, p_file):
487        """
488        """
489        # Structure of the model
490        # checkbox + basename
491        #     |-------> Info
492        #                 |----> Data.D object
493        #                 |----> Title:
494        #                 |----> Run:
495        #                 |----> Type:
496        #                 |----> Path:
497        #                 |----> Process
498        #                          |-----> process[0].name
499        #
500
501        # Top-level item: checkbox with label
502        checkbox_item = QtGui.QStandardItem(True)
503        checkbox_item.setCheckable(True)
504        checkbox_item.setCheckState(QtCore.Qt.Checked)
505        checkbox_item.setText(os.path.basename(p_file))
506
507        # Add the actual Data1D/Data2D object
508        object_item = QtGui.QStandardItem()
509        object_item.setData(QtCore.QVariant(data))
510
511        checkbox_item.setChild(0, object_item)
512
513        # Add rows for display in the view
514        info_item = infoFromData(data)
515
516        # Set info_item as the only child
517        checkbox_item.setChild(1, info_item)
518
519        # New row in the model
520        self.model.appendRow(checkbox_item)
521       
522    def updateModelFromPerspective(self, model_item):
523        """
524        Receive an update model item from a perspective
525        Make sure it is valid and if so, replace it in the model
526        """
527        # Assert the correct type
528        if type(model_item) != QtGui.QStandardItem:
529            msg = "Wrong data type returned from calculations."
530            raise AttributeError, msg
531
532        # TODO: Assert other properties
533
534        # Reset the view
535        self.model.reset()
536
537        # Pass acting as a debugger anchor
538        pass
539       
540
541if __name__ == "__main__":
542    app = QtGui.QApplication([])
543    dlg = DataExplorerWindow()
544    dlg.show()
545    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.