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

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 f712bf30 was 2b8286c, checked in by ibressler, 6 years ago

InversionPerspective? fix: plotRequestedSignal requires an id now

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