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

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 e540cd2 was e540cd2, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Status bar, progress bar, initial treeview context menu + minor cleanup

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