source: sasview/src/sas/qtgui/DataExplorer.py @ 8cb6cd6

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

Plot handler prototype + append plot functionality

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