source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 5b144c6

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 5b144c6 was 5b144c6, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Clumsy fix to the single-data, multi-fitpage plotting issue SASVIEW-1018.
Fixed tests after replacing plot_dict indexing from .id to .name

  • Property mode set to 100644
File size: 49.0 KB
Line 
1# global
2import sys
3import os
4import time
5import logging
6
7from PyQt5 import QtCore
8from PyQt5 import QtGui
9from PyQt5 import QtWidgets
10
11from twisted.internet import threads
12
13# SASCALC
14from sas.sascalc.dataloader.loader import Loader
15
16# QTGUI
17import sas.qtgui.Utilities.GuiUtils as GuiUtils
18import sas.qtgui.Plotting.PlotHelper as PlotHelper
19
20from sas.qtgui.Plotting.PlotterData import Data1D
21from sas.qtgui.Plotting.PlotterData import Data2D
22from sas.qtgui.Plotting.Plotter import Plotter
23from sas.qtgui.Plotting.Plotter2D import Plotter2D
24from sas.qtgui.Plotting.MaskEditor import MaskEditor
25
26from sas.qtgui.MainWindow.DataManager import DataManager
27from sas.qtgui.MainWindow.DroppableDataLoadWidget import DroppableDataLoadWidget
28
29import sas.qtgui.Perspectives as Perspectives
30
31DEFAULT_PERSPECTIVE = "Fitting"
32
33logger = logging.getLogger(__name__)
34
35class DataExplorerWindow(DroppableDataLoadWidget):
36    # The controller which is responsible for managing signal slots connections
37    # for the gui and providing an interface to the data model.
38
39    def __init__(self, parent=None, guimanager=None, manager=None):
40        super(DataExplorerWindow, self).__init__(parent, guimanager)
41
42        # Main model for keeping loaded data
43        self.model = QtGui.QStandardItemModel(self)
44        # Secondary model for keeping frozen data sets
45        self.theory_model = QtGui.QStandardItemModel(self)
46
47        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
48        # in order to set the widget parentage properly.
49        self.parent = guimanager
50        self.loader = Loader()
51        self.manager = manager if manager is not None else DataManager()
52        self.txt_widget = QtWidgets.QTextEdit(None)
53
54        # Be careful with twisted threads.
55        self.mutex = QtCore.QMutex()
56
57        # Plot widgets {name:widget}, required to keep track of plots shown as MDI subwindows
58        self.plot_widgets = {}
59
60        # Active plots {id:Plotter1D/2D}, required to keep track of currently displayed plots
61        self.active_plots = {}
62
63        # Connect the buttons
64        self.cmdLoad.clicked.connect(self.loadFile)
65        self.cmdDeleteData.clicked.connect(self.deleteFile)
66        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
67        self.cmdFreeze.clicked.connect(self.freezeTheory)
68        self.cmdSendTo.clicked.connect(self.sendData)
69        self.cmdNew.clicked.connect(self.newPlot)
70        self.cmdNew_2.clicked.connect(self.newPlot)
71        self.cmdAppend.clicked.connect(self.appendPlot)
72        self.cmdAppend_2.clicked.connect(self.appendPlot)
73        self.cmdHelp.clicked.connect(self.displayHelp)
74        self.cmdHelp_2.clicked.connect(self.displayHelp)
75
76        # Fill in the perspectives combo
77        self.initPerspectives()
78
79        # Custom context menu
80        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
81        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
82        self.contextMenu()
83
84        # Same menus for the theory view
85        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
86        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
87
88        # Connect the comboboxes
89        self.cbSelect.currentIndexChanged.connect(self.selectData)
90
91        #self.closeEvent.connect(self.closeEvent)
92        self.currentChanged.connect(self.onTabSwitch)
93        self.communicator = self.parent.communicator()
94        self.communicator.fileReadSignal.connect(self.loadFromURL)
95        self.communicator.activeGraphsSignal.connect(self.updateGraphCount)
96        self.communicator.activeGraphName.connect(self.updatePlotName)
97        self.communicator.plotUpdateSignal.connect(self.updatePlot)
98        self.communicator.maskEditorSignal.connect(self.showEditDataMask)
99        self.communicator.extMaskEditorSignal.connect(self.extShowEditDataMask)
100
101        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
102        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
103
104        # Proxy model for showing a subset of Data1D/Data2D content
105        self.data_proxy = QtCore.QSortFilterProxyModel(self)
106        self.data_proxy.setSourceModel(self.model)
107
108        # Don't show "empty" rows with data objects
109        self.data_proxy.setFilterRegExp(r"[^()]")
110
111        # The Data viewer is QTreeView showing the proxy model
112        self.treeView.setModel(self.data_proxy)
113
114        # Proxy model for showing a subset of Theory content
115        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
116        self.theory_proxy.setSourceModel(self.theory_model)
117
118        # Don't show "empty" rows with data objects
119        self.theory_proxy.setFilterRegExp(r"[^()]")
120
121        # Theory model view
122        self.freezeView.setModel(self.theory_proxy)
123
124        self.enableGraphCombo(None)
125
126        # Current view on model
127        self.current_view = self.treeView
128
129    def closeEvent(self, event):
130        """
131        Overwrite the close event - no close!
132        """
133        event.ignore()
134
135    def onTabSwitch(self, index):
136        """ Callback for tab switching signal """
137        if index == 0:
138            self.current_view = self.treeView
139        else:
140            self.current_view = self.freezeView
141
142    def displayHelp(self):
143        """
144        Show the "Loading data" section of help
145        """
146        tree_location = "/user/qtgui/MainWindow/data_explorer_help.html"
147        self.parent.showHelp(tree_location)
148
149    def enableGraphCombo(self, combo_text):
150        """
151        Enables/disables "Assign Plot" elements
152        """
153        self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
154        self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
155
156    def initPerspectives(self):
157        """
158        Populate the Perspective combobox and define callbacks
159        """
160        available_perspectives = sorted([p for p in list(Perspectives.PERSPECTIVES.keys())])
161        if available_perspectives:
162            self.cbFitting.clear()
163            self.cbFitting.addItems(available_perspectives)
164        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
165        # Set the index so we see the default (Fitting)
166        self.cbFitting.setCurrentIndex(self.cbFitting.findText(DEFAULT_PERSPECTIVE))
167
168    def _perspective(self):
169        """
170        Returns the current perspective
171        """
172        return self.parent.perspective()
173
174    def loadFromURL(self, url):
175        """
176        Threaded file load
177        """
178        load_thread = threads.deferToThread(self.readData, url)
179        load_thread.addCallback(self.loadComplete)
180        load_thread.addErrback(self.loadFailed)
181
182    def loadFile(self, event=None):
183        """
184        Called when the "Load" button pressed.
185        Opens the Qt "Open File..." dialog
186        """
187        path_str = self.chooseFiles()
188        if not path_str:
189            return
190        self.loadFromURL(path_str)
191
192    def loadFolder(self, event=None):
193        """
194        Called when the "File/Load Folder" menu item chosen.
195        Opens the Qt "Open Folder..." dialog
196        """
197        folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
198              QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
199        if folder is None:
200            return
201
202        folder = str(folder)
203
204        if not os.path.isdir(folder):
205            return
206
207        # get content of dir into a list
208        path_str = [os.path.join(os.path.abspath(folder), filename)
209                    for filename in os.listdir(folder)]
210
211        self.loadFromURL(path_str)
212
213    def loadProject(self):
214        """
215        Called when the "Open Project" menu item chosen.
216        """
217        kwargs = {
218            'parent'    : self,
219            'caption'   : 'Open Project',
220            'filter'    : 'Project (*.json);;All files (*.*)',
221            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
222        }
223        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
224        if filename:
225            load_thread = threads.deferToThread(self.readProject, filename)
226            load_thread.addCallback(self.readProjectComplete)
227            load_thread.addErrback(self.readProjectFailed)
228
229    def loadFailed(self, reason):
230        """
231        """
232        print("file load FAILED: ", reason)
233        pass
234
235    def readProjectFailed(self, reason):
236        """
237        """
238        print("readProjectFailed FAILED: ", reason)
239        pass
240
241    def readProject(self, filename):
242        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
243        try:
244            manager = DataManager()
245            with open(filename, 'r') as infile:
246                manager.load_from_readable(infile)
247
248            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
249            return manager
250
251        except:
252            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
253            raise
254
255    def readProjectComplete(self, manager):
256        self.model.clear()
257
258        self.manager.assign(manager)
259        self.model.beginResetModel()
260        for id, item in self.manager.get_all_data().items():
261            self.updateModel(item.data, item.path)
262
263        self.model.endResetModel()
264
265    def saveProject(self):
266        """
267        Called when the "Save Project" menu item chosen.
268        """
269        kwargs = {
270            'parent'    : self,
271            'caption'   : 'Save Project',
272            'filter'    : 'Project (*.json)',
273            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
274        }
275        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
276        filename = name_tuple[0]
277        if filename:
278            _, extension = os.path.splitext(filename)
279            if not extension:
280                filename = '.'.join((filename, 'json'))
281            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
282            with open(filename, 'w') as outfile:
283                self.manager.save_to_writable(outfile)
284
285    def deleteFile(self, event):
286        """
287        Delete selected rows from the model
288        """
289        # Assure this is indeed wanted
290        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
291                     "\nDo you want to continue?"
292        reply = QtWidgets.QMessageBox.question(self,
293                                           'Warning',
294                                           delete_msg,
295                                           QtWidgets.QMessageBox.Yes,
296                                           QtWidgets.QMessageBox.No)
297
298        if reply == QtWidgets.QMessageBox.No:
299            return
300
301        # Figure out which rows are checked
302        ind = -1
303        # Use 'while' so the row count is forced at every iteration
304        deleted_items = []
305        deleted_names = []
306        while ind < self.model.rowCount():
307            ind += 1
308            item = self.model.item(ind)
309
310            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
311                # Delete these rows from the model
312                deleted_names.append(str(self.model.item(ind).text()))
313                deleted_items.append(item)
314
315                self.model.removeRow(ind)
316                # Decrement index since we just deleted it
317                ind -= 1
318
319        # Let others know we deleted data
320        self.communicator.dataDeletedSignal.emit(deleted_items)
321
322        # update stored_data
323        self.manager.update_stored_data(deleted_names)
324
325    def deleteTheory(self, event):
326        """
327        Delete selected rows from the theory model
328        """
329        # Assure this is indeed wanted
330        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
331                     "\nDo you want to continue?"
332        reply = QtWidgets.QMessageBox.question(self,
333                                           'Warning',
334                                           delete_msg,
335                                           QtWidgets.QMessageBox.Yes,
336                                           QtWidgets.QMessageBox.No)
337
338        if reply == QtWidgets.QMessageBox.No:
339            return
340
341        # Figure out which rows are checked
342        ind = -1
343        # Use 'while' so the row count is forced at every iteration
344        while ind < self.theory_model.rowCount():
345            ind += 1
346            item = self.theory_model.item(ind)
347            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
348                # Delete these rows from the model
349                self.theory_model.removeRow(ind)
350                # Decrement index since we just deleted it
351                ind -= 1
352
353        # pass temporarily kept as a breakpoint anchor
354        pass
355
356    def sendData(self, event):
357        """
358        Send selected item data to the current perspective and set the relevant notifiers
359        """
360        # Set the signal handlers
361        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
362
363        def isItemReady(index):
364            item = self.model.item(index)
365            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
366
367        # Figure out which rows are checked
368        selected_items = [self.model.item(index)
369                          for index in range(self.model.rowCount())
370                          if isItemReady(index)]
371
372        if len(selected_items) < 1:
373            return
374
375        # Which perspective has been selected?
376        if len(selected_items) > 1 and not self._perspective().allowBatch():
377            msg = self._perspective().title() + " does not allow multiple data."
378            msgbox = QtWidgets.QMessageBox()
379            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
380            msgbox.setText(msg)
381            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
382            retval = msgbox.exec_()
383            return
384
385        # Notify the GuiManager about the send request
386        self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
387
388    def freezeCheckedData(self):
389        """
390        Convert checked results (fitted model, residuals) into separate dataset.
391        """
392        outer_index = -1
393        theories_copied = 0
394        orig_model_size = self.model.rowCount()
395        while outer_index < orig_model_size:
396            outer_index += 1
397            outer_item = self.model.item(outer_index)
398            if not outer_item:
399                continue
400            if not outer_item.isCheckable():
401                continue
402            # Look for checked inner items
403            inner_index = -1
404            while inner_index < outer_item.rowCount():
405               inner_item = outer_item.child(inner_index)
406               inner_index += 1
407               if not inner_item:
408                   continue
409               if not inner_item.isCheckable():
410                   continue
411               if inner_item.checkState() != QtCore.Qt.Checked:
412                   continue
413               self.model.beginResetModel()
414               theories_copied += 1
415               new_item = self.cloneTheory(inner_item)
416               self.model.appendRow(new_item)
417               self.model.endResetModel()
418
419        freeze_msg = ""
420        if theories_copied == 0:
421            return
422        elif theories_copied == 1:
423            freeze_msg = "1 theory copied to a separate data set"
424        elif theories_copied > 1:
425            freeze_msg = "%i theories copied to separate data sets" % theories_copied
426        else:
427            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
428            raise AttributeError(freeze_msg)
429        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
430
431    def freezeTheory(self, event):
432        """
433        Freeze selected theory rows.
434
435        "Freezing" means taking the plottable data from the Theory item
436        and copying it to a separate top-level item in Data.
437        """
438        # Figure out which rows are checked
439        # Use 'while' so the row count is forced at every iteration
440        outer_index = -1
441        theories_copied = 0
442        while outer_index < self.theory_model.rowCount():
443            outer_index += 1
444            outer_item = self.theory_model.item(outer_index)
445            if not outer_item:
446                continue
447            if outer_item.isCheckable() and \
448                   outer_item.checkState() == QtCore.Qt.Checked:
449                self.model.beginResetModel()
450                theories_copied += 1
451                new_item = self.cloneTheory(outer_item)
452                self.model.appendRow(new_item)
453                self.model.endResetModel()
454
455        freeze_msg = ""
456        if theories_copied == 0:
457            return
458        elif theories_copied == 1:
459            freeze_msg = "1 theory copied from the Theory tab as a data set"
460        elif theories_copied > 1:
461            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
462        else:
463            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
464            raise AttributeError(freeze_msg)
465        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
466        # Actively switch tabs
467        self.setCurrentIndex(1)
468
469    def cloneTheory(self, item_from):
470        """
471        Manually clone theory items into a new HashableItem
472        """
473        new_item = GuiUtils.HashableStandardItem()
474        new_item.setCheckable(True)
475        new_item.setCheckState(QtCore.Qt.Checked)
476        info_item = QtGui.QStandardItem("Info")
477        data_item = QtGui.QStandardItem()
478        data_item.setData(item_from.child(0).data())
479        new_item.setText(item_from.text())
480        new_item.setChild(0, data_item)
481        new_item.setChild(1, info_item)
482        # Append a "unique" descriptor to the name
483        time_bit = str(time.time())[7:-1].replace('.', '')
484        new_name = new_item.text() + '_@' + time_bit
485        new_item.setText(new_name)
486        # Change the underlying data so it is no longer a theory
487        try:
488            new_item.child(0).data().is_data = True
489        except AttributeError:
490            #no data here, pass
491            pass
492        return new_item
493
494    def recursivelyCloneItem(self, item):
495        """
496        Clone QStandardItem() object
497        """
498        new_item = item.clone()
499        # clone doesn't do deepcopy :(
500        for child_index in range(item.rowCount()):
501            child_item = self.recursivelyCloneItem(item.child(child_index))
502            new_item.setChild(child_index, child_item)
503        return new_item
504
505    def updatePlotName(self, name_tuple):
506        """
507        Modify the name of the current plot
508        """
509        old_name, current_name = name_tuple
510        ind = self.cbgraph.findText(old_name)
511        self.cbgraph.setCurrentIndex(ind)
512        self.cbgraph.setItemText(ind, current_name)
513
514    def updateGraphCount(self, graph_list):
515        """
516        Modify the graph name combo and potentially remove
517        deleted graphs
518        """
519        self.updateGraphCombo(graph_list)
520
521        if not self.active_plots:
522            return
523        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
524        active_plots_copy = list(self.active_plots.keys())
525        for plot in active_plots_copy:
526            if self.active_plots[plot] in new_plots:
527                continue
528            self.active_plots.pop(plot)
529
530    def updateGraphCombo(self, graph_list):
531        """
532        Modify Graph combo box on graph add/delete
533        """
534        orig_text = self.cbgraph.currentText()
535        self.cbgraph.clear()
536        self.cbgraph.insertItems(0, graph_list)
537        ind = self.cbgraph.findText(orig_text)
538        if ind > 0:
539            self.cbgraph.setCurrentIndex(ind)
540
541    def updatePerspectiveCombo(self, index):
542        """
543        Notify the gui manager about the new perspective chosen.
544        """
545        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
546        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
547
548    def itemFromFilename(self, filename):
549        """
550        Retrieves model item corresponding to the given filename
551        """
552        item = GuiUtils.itemFromFilename(filename, self.model)
553        return item
554
555    def displayFile(self, filename=None, is_data=True, id=None):
556        """
557        Forces display of charts for the given filename
558        """
559        model = self.model if is_data else self.theory_model
560        # Now query the model item for available plots
561        plots = GuiUtils.plotsFromFilename(filename, model)
562        # Each fitpage contains the name based on fit widget number
563        fitpage_name = "" if id is None else "M"+str(id)
564        new_plots = []
565        for item, plot in plots.items():
566            if self.updatePlot(plot) and filename != plot.name:
567                continue
568            # Don't plot intermediate results, e.g. P(Q), S(Q)
569            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
570            # 2nd match group contains the identifier for the intermediate result, if present (e.g. "[P(Q)]")
571            if match and match.groups()[1] != None:
572                continue
573            # Don't include plots from different fitpages, but always include the original data
574            if fitpage_name in plot.name or filename == plot.name:
575                # 'sophisticated' test to generate standalone plot for residuals
576                if 'esiduals' in plot.title:
577                    plot.yscale='linear'
578                    self.plotData([(item, plot)])
579                else:
580                    new_plots.append((item, plot))
581
582        if new_plots:
583            self.plotData(new_plots)
584
585    def displayData(self, data_list, id):
586        """
587        Forces display of charts for the given data set
588        """
589        plot_to_show = data_list[0]
590        # passed plot is used ONLY to figure out its title,
591        # so all the charts related by it can be pulled from
592        # the data explorer indices.
593        filename = plot_to_show.filename
594        self.displayFile(filename=filename, is_data=plot_to_show.is_data, id=id)
595
596    def addDataPlot2D(self, plot_set, item):
597        """
598        Create a new 2D plot and add it to the workspace
599        """
600        plot2D = Plotter2D(self)
601        plot2D.item = item
602        plot2D.plot(plot_set)
603        self.addPlot(plot2D)
604        self.active_plots[plot2D.data.name] = plot2D
605        #============================================
606        # Experimental hook for silx charts
607        #============================================
608        ## Attach silx
609        #from silx.gui import qt
610        #from silx.gui.plot import StackView
611        #sv = StackView()
612        #sv.setColormap("jet", autoscale=True)
613        #sv.setStack(plot_set.data.reshape(1,100,100))
614        ##sv.setLabels(["x: -10 to 10 (200 samples)",
615        ##              "y: -10 to 5 (150 samples)"])
616        #sv.show()
617        #============================================
618
619    def plotData(self, plots, transform=True):
620        """
621        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
622        """
623        # Call show on requested plots
624        # All same-type charts in one plot
625        for item, plot_set in plots:
626            if isinstance(plot_set, Data1D):
627                if not 'new_plot' in locals():
628                    new_plot = Plotter(self)
629                    new_plot.item = item
630                new_plot.plot(plot_set, transform=transform)
631                # active_plots may contain multiple charts
632                self.active_plots[plot_set.name] = new_plot
633            elif isinstance(plot_set, Data2D):
634                self.addDataPlot2D(plot_set, item)
635            else:
636                msg = "Incorrect data type passed to Plotting"
637                raise AttributeError(msg)
638
639        if 'new_plot' in locals() and \
640            hasattr(new_plot, 'data') and \
641            isinstance(new_plot.data, Data1D):
642                self.addPlot(new_plot)
643
644    def newPlot(self):
645        """
646        Select checked data and plot it
647        """
648        # Check which tab is currently active
649        if self.current_view == self.treeView:
650            plots = GuiUtils.plotsFromCheckedItems(self.model)
651        else:
652            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
653
654        self.plotData(plots)
655
656    def addPlot(self, new_plot):
657        """
658        Helper method for plot bookkeeping
659        """
660        # Update the global plot counter
661        title = str(PlotHelper.idOfPlot(new_plot))
662        new_plot.setWindowTitle(title)
663
664        # Set the object name to satisfy the Squish object picker
665        new_plot.setObjectName(title)
666
667        # Add the plot to the workspace
668        plot_widget = self.parent.workspace().addSubWindow(new_plot)
669
670        # Show the plot
671        new_plot.show()
672        new_plot.canvas.draw()
673
674        # Update the plot widgets dict
675        self.plot_widgets[title]=plot_widget
676
677        # Update the active chart list
678        #self.active_plots[new_plot.data.id] = new_plot
679
680    def appendPlot(self):
681        """
682        Add data set(s) to the existing matplotlib chart
683        """
684        # new plot data; check which tab is currently active
685        if self.current_view == self.treeView:
686            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
687        else:
688            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
689
690        # old plot data
691        plot_id = str(self.cbgraph.currentText())
692
693        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
694
695        old_plot = PlotHelper.plotById(plot_id)
696
697        # Add new data to the old plot, if data type is the same.
698        for _, plot_set in new_plots:
699            if type(plot_set) is type(old_plot._data):
700                old_plot.data = plot_set
701                old_plot.plot()
702                # need this for lookup - otherwise this plot will never update
703                self.active_plots[plot_set.name] = old_plot
704
705    def updatePlot(self, data):
706        """
707        Modify existing plot for immediate response and returns True.
708        Returns false, if the plot does not exist already.
709        """
710        try: # there might be a list or a single value being passed
711            data = data[0]
712        except TypeError:
713            pass
714        assert type(data).__name__ in ['Data1D', 'Data2D']
715
716        ids_keys = list(self.active_plots.keys())
717        ids_vals = [val.data.name for val in self.active_plots.values()]
718
719        data_id = data.name
720        if data_id in ids_keys:
721            self.active_plots[data_id].replacePlot(data_id, data)
722            return True
723        elif data_id in ids_vals:
724            list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
725            return True
726        return False
727
728    def chooseFiles(self):
729        """
730        Shows the Open file dialog and returns the chosen path(s)
731        """
732        # List of known extensions
733        wlist = self.getWlist()
734
735        # Location is automatically saved - no need to keep track of the last dir
736        # But only with Qt built-in dialog (non-platform native)
737        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
738                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
739        if not paths:
740            return
741
742        if not isinstance(paths, list):
743            paths = [paths]
744
745        return paths
746
747    def readData(self, path):
748        """
749        verbatim copy-paste from
750           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
751        slightly modified for clarity
752        """
753        message = ""
754        log_msg = ''
755        output = {}
756        any_error = False
757        data_error = False
758        error_message = ""
759        number_of_files = len(path)
760        self.communicator.progressBarUpdateSignal.emit(0.0)
761
762        for index, p_file in enumerate(path):
763            basename = os.path.basename(p_file)
764            _, extension = os.path.splitext(basename)
765            if extension.lower() in GuiUtils.EXTENSIONS:
766                any_error = True
767                log_msg = "Data Loader cannot "
768                log_msg += "load: %s\n" % str(p_file)
769                log_msg += """Please try to open that file from "open project" """
770                log_msg += """or "open analysis" menu\n"""
771                error_message = log_msg + "\n"
772                logging.info(log_msg)
773                continue
774
775            try:
776                message = "Loading Data... " + str(basename) + "\n"
777
778                # change this to signal notification in GuiManager
779                self.communicator.statusBarUpdateSignal.emit(message)
780
781                output_objects = self.loader.load(p_file)
782
783                # Some loaders return a list and some just a single Data1D object.
784                # Standardize.
785                if not isinstance(output_objects, list):
786                    output_objects = [output_objects]
787
788                for item in output_objects:
789                    # cast sascalc.dataloader.data_info.Data1D into
790                    # sasgui.guiframe.dataFitting.Data1D
791                    # TODO : Fix it
792                    new_data = self.manager.create_gui_data(item, p_file)
793                    output[new_data.id] = new_data
794
795                    # Model update should be protected
796                    self.mutex.lock()
797                    self.updateModel(new_data, p_file)
798                    #self.model.reset()
799                    QtWidgets.QApplication.processEvents()
800                    self.mutex.unlock()
801
802                    if hasattr(item, 'errors'):
803                        for error_data in item.errors:
804                            data_error = True
805                            message += "\tError: {0}\n".format(error_data)
806                    else:
807
808                        logging.error("Loader returned an invalid object:\n %s" % str(item))
809                        data_error = True
810
811            except Exception as ex:
812                logging.error(sys.exc_info()[1])
813
814                any_error = True
815            if any_error or error_message != "":
816                if error_message == "":
817                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
818                    error += "while loading Data: \n%s\n" % str(basename)
819                    error_message += "The data file you selected could not be loaded.\n"
820                    error_message += "Make sure the content of your file"
821                    error_message += " is properly formatted.\n\n"
822                    error_message += "When contacting the SasView team, mention the"
823                    error_message += " following:\n%s" % str(error)
824                elif data_error:
825                    base_message = "Errors occurred while loading "
826                    base_message += "{0}\n".format(basename)
827                    base_message += "The data file loaded but with errors.\n"
828                    error_message = base_message + error_message
829                else:
830                    error_message += "%s\n" % str(p_file)
831
832            current_percentage = int(100.0* index/number_of_files)
833            self.communicator.progressBarUpdateSignal.emit(current_percentage)
834
835        if any_error or error_message:
836            logging.error(error_message)
837            status_bar_message = "Errors occurred while loading %s" % format(basename)
838            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
839
840        else:
841            message = "Loading Data Complete! "
842        message += log_msg
843        # Notify the progress bar that the updates are over.
844        self.communicator.progressBarUpdateSignal.emit(-1)
845        self.communicator.statusBarUpdateSignal.emit(message)
846
847        return output, message
848
849    def getWlist(self):
850        """
851        Wildcards of files we know the format of.
852        """
853        # Display the Qt Load File module
854        cards = self.loader.get_wildcards()
855
856        # get rid of the wx remnant in wildcards
857        # TODO: modify sasview loader get_wildcards method, after merge,
858        # so this kludge can be avoided
859        new_cards = []
860        for item in cards:
861            new_cards.append(item[:item.find("|")])
862        wlist = ';;'.join(new_cards)
863
864        return wlist
865
866    def setItemsCheckability(self, model, dimension=None, checked=False):
867        """
868        For a given model, check or uncheck all items of given dimension
869        """
870        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
871
872        assert isinstance(checked, bool)
873
874        types = (None, Data1D, Data2D)
875        assert dimension in types
876
877        for index in range(model.rowCount()):
878            item = model.item(index)
879            if dimension is not None and not isinstance(GuiUtils.dataFromItem(item), dimension):
880                continue
881            if item.isCheckable() and item.checkState() != mode:
882                item.setCheckState(mode)
883            # look for all children
884            for inner_index in range(item.rowCount()):
885                child = item.child(inner_index)
886                if child.isCheckable() and child.checkState() != mode:
887                    child.setCheckState(mode)
888
889    def selectData(self, index):
890        """
891        Callback method for modifying the TreeView on Selection Options change
892        """
893        if not isinstance(index, int):
894            msg = "Incorrect type passed to DataExplorer.selectData()"
895            raise AttributeError(msg)
896
897        # Respond appropriately
898        if index == 0:
899            self.setItemsCheckability(self.model, checked=True)
900
901        elif index == 1:
902            # De-select All
903            self.setItemsCheckability(self.model, checked=False)
904
905        elif index == 2:
906            # Select All 1-D
907            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
908
909        elif index == 3:
910            # Unselect All 1-D
911            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
912
913        elif index == 4:
914            # Select All 2-D
915            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
916
917        elif index == 5:
918            # Unselect All 2-D
919            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
920
921        else:
922            msg = "Incorrect value in the Selection Option"
923            # Change this to a proper logging action
924            raise Exception(msg)
925
926    def contextMenu(self):
927        """
928        Define actions and layout of the right click context menu
929        """
930        # Create a custom menu based on actions defined in the UI file
931        self.context_menu = QtWidgets.QMenu(self)
932        self.context_menu.addAction(self.actionDataInfo)
933        self.context_menu.addAction(self.actionSaveAs)
934        self.context_menu.addAction(self.actionQuickPlot)
935        self.context_menu.addSeparator()
936        self.context_menu.addAction(self.actionQuick3DPlot)
937        self.context_menu.addAction(self.actionEditMask)
938        self.context_menu.addSeparator()
939        self.context_menu.addAction(self.actionDelete)
940
941
942        # Define the callbacks
943        self.actionDataInfo.triggered.connect(self.showDataInfo)
944        self.actionSaveAs.triggered.connect(self.saveDataAs)
945        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
946        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
947        self.actionEditMask.triggered.connect(self.showEditDataMask)
948        self.actionDelete.triggered.connect(self.deleteItem)
949
950    def onCustomContextMenu(self, position):
951        """
952        Show the right-click context menu in the data treeview
953        """
954        index = self.current_view.indexAt(position)
955        proxy = self.current_view.model()
956        model = proxy.sourceModel()
957
958        if not index.isValid():
959            return
960        model_item = model.itemFromIndex(proxy.mapToSource(index))
961        # Find the mapped index
962        orig_index = model_item.isCheckable()
963        if not orig_index:
964            return
965        # Check the data to enable/disable actions
966        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
967        self.actionQuick3DPlot.setEnabled(is_2D)
968        self.actionEditMask.setEnabled(is_2D)
969        # Fire up the menu
970        self.context_menu.exec_(self.current_view.mapToGlobal(position))
971
972    def showDataInfo(self):
973        """
974        Show a simple read-only text edit with data information.
975        """
976        index = self.current_view.selectedIndexes()[0]
977        proxy = self.current_view.model()
978        model = proxy.sourceModel()
979        model_item = model.itemFromIndex(proxy.mapToSource(index))
980
981        data = GuiUtils.dataFromItem(model_item)
982        if isinstance(data, Data1D):
983            text_to_show = GuiUtils.retrieveData1d(data)
984            # Hardcoded sizes to enable full width rendering with default font
985            self.txt_widget.resize(420,600)
986        else:
987            text_to_show = GuiUtils.retrieveData2d(data)
988            # Hardcoded sizes to enable full width rendering with default font
989            self.txt_widget.resize(700,600)
990
991        self.txt_widget.setReadOnly(True)
992        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
993        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
994        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
995        self.txt_widget.clear()
996        self.txt_widget.insertPlainText(text_to_show)
997
998        self.txt_widget.show()
999        # Move the slider all the way up, if present
1000        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1001        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1002
1003    def saveDataAs(self):
1004        """
1005        Save the data points as either txt or xml
1006        """
1007        index = self.current_view.selectedIndexes()[0]
1008        proxy = self.current_view.model()
1009        model = proxy.sourceModel()
1010        model_item = model.itemFromIndex(proxy.mapToSource(index))
1011
1012        data = GuiUtils.dataFromItem(model_item)
1013        if isinstance(data, Data1D):
1014            GuiUtils.saveData1D(data)
1015        else:
1016            GuiUtils.saveData2D(data)
1017
1018    def quickDataPlot(self):
1019        """
1020        Frozen plot - display an image of the plot
1021        """
1022        index = self.current_view.selectedIndexes()[0]
1023        proxy = self.current_view.model()
1024        model = proxy.sourceModel()
1025        model_item = model.itemFromIndex(proxy.mapToSource(index))
1026
1027        data = GuiUtils.dataFromItem(model_item)
1028
1029        method_name = 'Plotter'
1030        if isinstance(data, Data2D):
1031            method_name='Plotter2D'
1032
1033        self.new_plot = globals()[method_name](self, quickplot=True)
1034        self.new_plot.data = data
1035        #new_plot.plot(marker='o')
1036        self.new_plot.plot()
1037
1038        # Update the global plot counter
1039        title = "Plot " + data.name
1040        self.new_plot.setWindowTitle(title)
1041
1042        # Show the plot
1043        self.new_plot.show()
1044
1045    def quickData3DPlot(self):
1046        """
1047        Slowish 3D plot
1048        """
1049        index = self.current_view.selectedIndexes()[0]
1050        proxy = self.current_view.model()
1051        model = proxy.sourceModel()
1052        model_item = model.itemFromIndex(proxy.mapToSource(index))
1053
1054        data = GuiUtils.dataFromItem(model_item)
1055
1056        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1057        self.new_plot.data = data
1058        self.new_plot.plot()
1059
1060        # Update the global plot counter
1061        title = "Plot " + data.name
1062        self.new_plot.setWindowTitle(title)
1063
1064        # Show the plot
1065        self.new_plot.show()
1066
1067    def extShowEditDataMask(self):
1068        self.showEditDataMask()
1069
1070    def showEditDataMask(self, data=None):
1071        """
1072        Mask Editor for 2D plots
1073        """
1074        try:
1075            if data is None or not isinstance(data, Data2D):
1076                index = self.current_view.selectedIndexes()[0]
1077                proxy = self.current_view.model()
1078                model = proxy.sourceModel()
1079                model_item = model.itemFromIndex(proxy.mapToSource(index))
1080
1081                data = GuiUtils.dataFromItem(model_item)
1082
1083            if data is None or not isinstance(data, Data2D):
1084                msg = QtWidgets.QMessageBox()
1085                msg.setIcon(QtWidgets.QMessageBox.Information)
1086                msg.setText("Error: cannot apply mask. \
1087                                Please select a 2D dataset.")
1088                msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1089                msg.exec_()
1090                return
1091        except:
1092            msg = QtWidgets.QMessageBox()
1093            msg.setIcon(QtWidgets.QMessageBox.Information)
1094            msg.setText("Error: No dataset selected. \
1095                            Please select a 2D dataset.")
1096            msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1097            msg.exec_()
1098            return
1099
1100        mask_editor = MaskEditor(self, data)
1101        # Modal dialog here.
1102        mask_editor.exec_()
1103
1104    def deleteItem(self):
1105        """
1106        Delete the current item
1107        """
1108        # Assure this is indeed wanted
1109        delete_msg = "This operation will delete the selected data sets " +\
1110                     "and all the dependents." +\
1111                     "\nDo you want to continue?"
1112        reply = QtWidgets.QMessageBox.question(self,
1113                                           'Warning',
1114                                           delete_msg,
1115                                           QtWidgets.QMessageBox.Yes,
1116                                           QtWidgets.QMessageBox.No)
1117
1118        if reply == QtWidgets.QMessageBox.No:
1119            return
1120
1121        # Every time a row is removed, the indices change, so we'll just remove
1122        # rows and keep calling selectedIndexes until it returns an empty list.
1123        indices = self.current_view.selectedIndexes()
1124
1125        proxy = self.current_view.model()
1126        model = proxy.sourceModel()
1127
1128        deleted_items = []
1129        deleted_names = []
1130
1131        while len(indices) > 0:
1132            index = indices[0]
1133            row_index = proxy.mapToSource(index)
1134            item_to_delete = model.itemFromIndex(row_index)
1135            if item_to_delete and item_to_delete.isCheckable():
1136                row = row_index.row()
1137
1138                # store the deleted item details so we can pass them on later
1139                deleted_names.append(item_to_delete.text())
1140                deleted_items.append(item_to_delete)
1141
1142                # Delete corresponding open plots
1143                self.closePlotsForItem(item_to_delete)
1144
1145                if item_to_delete.parent():
1146                    # We have a child item - delete from it
1147                    item_to_delete.parent().removeRow(row)
1148                else:
1149                    # delete directly from model
1150                    model.removeRow(row)
1151            indices = self.current_view.selectedIndexes()
1152
1153        # Let others know we deleted data
1154        self.communicator.dataDeletedSignal.emit(deleted_items)
1155
1156        # update stored_data
1157        self.manager.update_stored_data(deleted_names)
1158
1159    def closePlotsForItem(self, item):
1160        """
1161        Given standard item, close all its currently displayed plots
1162        """
1163        # item - HashableStandardItems of active plots
1164
1165        # {} -> 'Graph1' : HashableStandardItem()
1166        current_plot_items = {}
1167        for plot_name in PlotHelper.currentPlots():
1168            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1169
1170        # item and its hashable children
1171        items_being_deleted = []
1172        if item.rowCount() > 0:
1173            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1174                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1175        items_being_deleted.append(item)
1176        # Add the parent in case a child is selected
1177        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1178            items_being_deleted.append(item.parent())
1179
1180        # Compare plot items and items to delete
1181        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1182
1183        for plot_item in plots_to_close:
1184            for plot_name in current_plot_items.keys():
1185                if plot_item == current_plot_items[plot_name]:
1186                    plotter = PlotHelper.plotById(plot_name)
1187                    # try to delete the plot
1188                    try:
1189                        plotter.close()
1190                        #self.parent.workspace().removeSubWindow(plotter)
1191                        self.plot_widgets[plot_name].close()
1192                        self.plot_widgets.pop(plot_name, None)
1193                    except AttributeError as ex:
1194                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1195
1196        pass # debugger anchor
1197
1198    def onAnalysisUpdate(self, new_perspective=""):
1199        """
1200        Update the perspective combo index based on passed string
1201        """
1202        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1203        self.cbFitting.blockSignals(True)
1204        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1205        self.cbFitting.blockSignals(False)
1206        pass
1207
1208    def loadComplete(self, output):
1209        """
1210        Post message to status bar and update the data manager
1211        """
1212        assert isinstance(output, tuple)
1213
1214        # Reset the model so the view gets updated.
1215        #self.model.reset()
1216        self.communicator.progressBarUpdateSignal.emit(-1)
1217
1218        output_data = output[0]
1219        message = output[1]
1220        # Notify the manager of the new data available
1221        self.communicator.statusBarUpdateSignal.emit(message)
1222        self.communicator.fileDataReceivedSignal.emit(output_data)
1223        self.manager.add_data(data_list=output_data)
1224
1225    def loadFailed(self, reason):
1226        print("File Load Failed with:\n", reason)
1227        pass
1228
1229    def updateModel(self, data, p_file):
1230        """
1231        Add data and Info fields to the model item
1232        """
1233        # Structure of the model
1234        # checkbox + basename
1235        #     |-------> Data.D object
1236        #     |-------> Info
1237        #                 |----> Title:
1238        #                 |----> Run:
1239        #                 |----> Type:
1240        #                 |----> Path:
1241        #                 |----> Process
1242        #                          |-----> process[0].name
1243        #     |-------> THEORIES
1244
1245        # Top-level item: checkbox with label
1246        checkbox_item = GuiUtils.HashableStandardItem()
1247        checkbox_item.setCheckable(True)
1248        checkbox_item.setCheckState(QtCore.Qt.Checked)
1249        checkbox_item.setText(os.path.basename(p_file))
1250
1251        # Add the actual Data1D/Data2D object
1252        object_item = GuiUtils.HashableStandardItem()
1253        object_item.setData(data)
1254
1255        checkbox_item.setChild(0, object_item)
1256
1257        # Add rows for display in the view
1258        info_item = GuiUtils.infoFromData(data)
1259
1260        # Set info_item as the first child
1261        checkbox_item.setChild(1, info_item)
1262
1263        # Caption for the theories
1264        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1265
1266        # New row in the model
1267        self.model.beginResetModel()
1268        self.model.appendRow(checkbox_item)
1269        self.model.endResetModel()
1270
1271    def updateModelFromPerspective(self, model_item):
1272        """
1273        Receive an update model item from a perspective
1274        Make sure it is valid and if so, replace it in the model
1275        """
1276        # Assert the correct type
1277        if not isinstance(model_item, QtGui.QStandardItem):
1278            msg = "Wrong data type returned from calculations."
1279            raise AttributeError(msg)
1280
1281        # TODO: Assert other properties
1282
1283        # Reset the view
1284        ##self.model.reset()
1285        # Pass acting as a debugger anchor
1286        pass
1287
1288    def updateTheoryFromPerspective(self, model_item):
1289        """
1290        Receive an update theory item from a perspective
1291        Make sure it is valid and if so, replace/add in the model
1292        """
1293        # Assert the correct type
1294        if not isinstance(model_item, QtGui.QStandardItem):
1295            msg = "Wrong data type returned from calculations."
1296            raise AttributeError(msg)
1297
1298        # Check if there are any other items for this tab
1299        # If so, delete them
1300        current_tab_name = model_item.text()
1301        for current_index in range(self.theory_model.rowCount()):
1302            #if current_tab_name in self.theory_model.item(current_index).text():
1303            if current_tab_name == self.theory_model.item(current_index).text():
1304                self.theory_model.removeRow(current_index)
1305                break
1306
1307        # send in the new item
1308        self.theory_model.appendRow(model_item)
1309
1310    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1311        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1312        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1313        retained in the list)."""
1314        items_to_delete = []
1315        for r in range(self.theory_model.rowCount()):
1316            item = self.theory_model.item(r, 0)
1317            data = item.child(0).data()
1318            if not hasattr(data, "id"):
1319                return
1320            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1321            if match:
1322                item_model_id = match.groups()[-1]
1323                if item_model_id == model_id:
1324                    # Only delete those identified as an intermediate plot
1325                    if match.groups()[2] not in (None, ""):
1326                        items_to_delete.append(item)
1327
1328        for item in items_to_delete:
1329            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.