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

Last change on this file since 8e674ccf was 8e674ccf, checked in by ibressler, 6 years ago

DataExplorerWindow?.displayFile(): actually test each plot for being hidden

  • got lost during cherry-picking
  • Property mode set to 100644
File size: 49.9 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.displayData)
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        # groups plots which move into an own window
575        new_plots = dict(int = [], res = [], pd = [])
576        for item, plot in plots.items():
577            if (self.updatePlot(plot) and filename != plot.name) or plot.hidden:
578                continue
579            # Don't plot intermediate results, e.g. P(Q), S(Q)
580            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
581            # 2nd match group contains the identifier for the intermediate
582            # result, if present (e.g. "[P(Q)]")
583            if match and match.groups()[1] != None:
584                continue
585            # Don't include plots from different fitpages,
586            # but always include the original data
587            if fitpage_name in plot.name or filename == plot.name:
588                # 'sophisticated' test to generate standalone plot for residuals
589                # this should be done by some kind of grouping by lists
590                # which helps to indicate which go into a single plot window
591                if 'esiduals' in plot.title:
592                    plot.yscale = 'linear'
593                    new_plots['res'].append((item, plot))
594                elif 'olydispersity' in plot.title:
595                    plot.yscale = 'linear'
596                    new_plots['pd'].append((item, plot))
597                else:
598                    new_plots['int'].append((item, plot))
599
600        # create entirely new plots for those which could not be updated
601        for plots in new_plots.values():
602            if len(plots):
603                self.plotData(plots)
604
605    def displayData(self, data_list, id=None):
606        """
607        Forces display of charts for the given data set
608        """
609        for plot_to_show in data_list:
610            # may there be duplicates? list(OrderedDict.fromkeys(data_list))
611            # passed plot is used ONLY to figure out its title,
612            # so all the charts related by it can be pulled from
613            # the data explorer indices.
614            filename = plot_to_show.filename
615            self.displayFile(filename=filename, is_data=plot_to_show.is_data, id=id)
616
617    def addDataPlot2D(self, plot_set, item):
618        """
619        Create a new 2D plot and add it to the workspace
620        """
621        plot2D = Plotter2D(self)
622        plot2D.item = item
623        plot2D.plot(plot_set)
624        self.addPlot(plot2D)
625        self.active_plots[plot2D.data.name] = plot2D
626        #============================================
627        # Experimental hook for silx charts
628        #============================================
629        ## Attach silx
630        #from silx.gui import qt
631        #from silx.gui.plot import StackView
632        #sv = StackView()
633        #sv.setColormap("jet", autoscale=True)
634        #sv.setStack(plot_set.data.reshape(1,100,100))
635        ##sv.setLabels(["x: -10 to 10 (200 samples)",
636        ##              "y: -10 to 5 (150 samples)"])
637        #sv.show()
638        #============================================
639
640    def plotData(self, plots, transform=True):
641        """
642        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
643        """
644        # Call show on requested plots
645        # All same-type charts in one plot
646        for item, plot_set in plots:
647            if isinstance(plot_set, Data1D):
648                if not 'new_plot' in locals():
649                    new_plot = Plotter(self)
650                    new_plot.item = item
651                new_plot.plot(plot_set, transform=transform)
652                # active_plots may contain multiple charts
653                self.active_plots[plot_set.name] = new_plot
654            elif isinstance(plot_set, Data2D):
655                self.addDataPlot2D(plot_set, item)
656            else:
657                msg = "Incorrect data type passed to Plotting"
658                raise AttributeError(msg)
659
660        if 'new_plot' in locals() and \
661            hasattr(new_plot, 'data') and \
662            isinstance(new_plot.data, Data1D):
663                self.addPlot(new_plot)
664
665    def newPlot(self):
666        """
667        Select checked data and plot it
668        """
669        # Check which tab is currently active
670        if self.current_view == self.treeView:
671            plots = GuiUtils.plotsFromCheckedItems(self.model)
672        else:
673            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
674
675        self.plotData(plots)
676
677    def addPlot(self, new_plot):
678        """
679        Helper method for plot bookkeeping
680        """
681        # Update the global plot counter
682        title = str(PlotHelper.idOfPlot(new_plot))
683        new_plot.setWindowTitle(title)
684
685        # Set the object name to satisfy the Squish object picker
686        new_plot.setObjectName(title)
687
688        # Add the plot to the workspace
689        plot_widget = self.parent.workspace().addSubWindow(new_plot)
690
691        # Show the plot
692        new_plot.show()
693        new_plot.canvas.draw()
694
695        # Update the plot widgets dict
696        self.plot_widgets[title]=plot_widget
697
698        # Update the active chart list
699        #self.active_plots[new_plot.data.id] = new_plot
700
701    def appendPlot(self):
702        """
703        Add data set(s) to the existing matplotlib chart
704        """
705        # new plot data; check which tab is currently active
706        if self.current_view == self.treeView:
707            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
708        else:
709            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
710
711        # old plot data
712        plot_id = str(self.cbgraph.currentText())
713
714        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
715
716        old_plot = PlotHelper.plotById(plot_id)
717
718        # Add new data to the old plot, if data type is the same.
719        for _, plot_set in new_plots:
720            if type(plot_set) is type(old_plot._data):
721                old_plot.data = plot_set
722                old_plot.plot()
723                # need this for lookup - otherwise this plot will never update
724                self.active_plots[plot_set.name] = old_plot
725
726    def updatePlot(self, data):
727        """
728        Modify existing plot for immediate response and returns True.
729        Returns false, if the plot does not exist already.
730        """
731        try: # there might be a list or a single value being passed
732            data = data[0]
733        except TypeError:
734            pass
735        assert type(data).__name__ in ['Data1D', 'Data2D']
736
737        ids_keys = list(self.active_plots.keys())
738        ids_vals = [val.data.name for val in self.active_plots.values()]
739
740        data_id = data.name
741        if data_id in ids_keys:
742            self.active_plots[data_id].replacePlot(data_id, data)
743            return True
744        elif data_id in ids_vals:
745            list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
746            return True
747        return False
748
749    def chooseFiles(self):
750        """
751        Shows the Open file dialog and returns the chosen path(s)
752        """
753        # List of known extensions
754        wlist = self.getWlist()
755
756        # Location is automatically saved - no need to keep track of the last dir
757        # But only with Qt built-in dialog (non-platform native)
758        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
759                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
760        if not paths:
761            return
762
763        if not isinstance(paths, list):
764            paths = [paths]
765
766        return paths
767
768    def readData(self, path):
769        """
770        verbatim copy-paste from
771           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
772        slightly modified for clarity
773        """
774        message = ""
775        log_msg = ''
776        output = {}
777        any_error = False
778        data_error = False
779        error_message = ""
780        number_of_files = len(path)
781        self.communicator.progressBarUpdateSignal.emit(0.0)
782
783        for index, p_file in enumerate(path):
784            basename = os.path.basename(p_file)
785            _, extension = os.path.splitext(basename)
786            if extension.lower() in GuiUtils.EXTENSIONS:
787                any_error = True
788                log_msg = "Data Loader cannot "
789                log_msg += "load: %s\n" % str(p_file)
790                log_msg += """Please try to open that file from "open project" """
791                log_msg += """or "open analysis" menu\n"""
792                error_message = log_msg + "\n"
793                logging.info(log_msg)
794                continue
795
796            try:
797                message = "Loading Data... " + str(basename) + "\n"
798
799                # change this to signal notification in GuiManager
800                self.communicator.statusBarUpdateSignal.emit(message)
801
802                output_objects = self.loader.load(p_file)
803
804                # Some loaders return a list and some just a single Data1D object.
805                # Standardize.
806                if not isinstance(output_objects, list):
807                    output_objects = [output_objects]
808
809                for item in output_objects:
810                    # cast sascalc.dataloader.data_info.Data1D into
811                    # sasgui.guiframe.dataFitting.Data1D
812                    # TODO : Fix it
813                    new_data = self.manager.create_gui_data(item, p_file)
814                    output[new_data.id] = new_data
815
816                    # Model update should be protected
817                    self.mutex.lock()
818                    self.updateModel(new_data, p_file)
819                    #self.model.reset()
820                    QtWidgets.QApplication.processEvents()
821                    self.mutex.unlock()
822
823                    if hasattr(item, 'errors'):
824                        for error_data in item.errors:
825                            data_error = True
826                            message += "\tError: {0}\n".format(error_data)
827                    else:
828
829                        logging.error("Loader returned an invalid object:\n %s" % str(item))
830                        data_error = True
831
832            except Exception as ex:
833                logging.error(sys.exc_info()[1])
834
835                any_error = True
836            if any_error or error_message != "":
837                if error_message == "":
838                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
839                    error += "while loading Data: \n%s\n" % str(basename)
840                    error_message += "The data file you selected could not be loaded.\n"
841                    error_message += "Make sure the content of your file"
842                    error_message += " is properly formatted.\n\n"
843                    error_message += "When contacting the SasView team, mention the"
844                    error_message += " following:\n%s" % str(error)
845                elif data_error:
846                    base_message = "Errors occurred while loading "
847                    base_message += "{0}\n".format(basename)
848                    base_message += "The data file loaded but with errors.\n"
849                    error_message = base_message + error_message
850                else:
851                    error_message += "%s\n" % str(p_file)
852
853            current_percentage = int(100.0* index/number_of_files)
854            self.communicator.progressBarUpdateSignal.emit(current_percentage)
855
856        if any_error or error_message:
857            logging.error(error_message)
858            status_bar_message = "Errors occurred while loading %s" % format(basename)
859            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
860
861        else:
862            message = "Loading Data Complete! "
863        message += log_msg
864        # Notify the progress bar that the updates are over.
865        self.communicator.progressBarUpdateSignal.emit(-1)
866        self.communicator.statusBarUpdateSignal.emit(message)
867
868        return output, message
869
870    def getWlist(self):
871        """
872        Wildcards of files we know the format of.
873        """
874        # Display the Qt Load File module
875        cards = self.loader.get_wildcards()
876
877        # get rid of the wx remnant in wildcards
878        # TODO: modify sasview loader get_wildcards method, after merge,
879        # so this kludge can be avoided
880        new_cards = []
881        for item in cards:
882            new_cards.append(item[:item.find("|")])
883        wlist = ';;'.join(new_cards)
884
885        return wlist
886
887    def setItemsCheckability(self, model, dimension=None, checked=False):
888        """
889        For a given model, check or uncheck all items of given dimension
890        """
891        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
892
893        assert isinstance(checked, bool)
894
895        types = (None, Data1D, Data2D)
896        assert dimension in types
897
898        for index in range(model.rowCount()):
899            item = model.item(index)
900            if dimension is not None and not isinstance(GuiUtils.dataFromItem(item), dimension):
901                continue
902            if item.isCheckable() and item.checkState() != mode:
903                item.setCheckState(mode)
904            # look for all children
905            for inner_index in range(item.rowCount()):
906                child = item.child(inner_index)
907                if child.isCheckable() and child.checkState() != mode:
908                    child.setCheckState(mode)
909
910    def selectData(self, index):
911        """
912        Callback method for modifying the TreeView on Selection Options change
913        """
914        if not isinstance(index, int):
915            msg = "Incorrect type passed to DataExplorer.selectData()"
916            raise AttributeError(msg)
917
918        # Respond appropriately
919        if index == 0:
920            self.setItemsCheckability(self.model, checked=True)
921
922        elif index == 1:
923            # De-select All
924            self.setItemsCheckability(self.model, checked=False)
925
926        elif index == 2:
927            # Select All 1-D
928            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
929
930        elif index == 3:
931            # Unselect All 1-D
932            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
933
934        elif index == 4:
935            # Select All 2-D
936            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
937
938        elif index == 5:
939            # Unselect All 2-D
940            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
941
942        else:
943            msg = "Incorrect value in the Selection Option"
944            # Change this to a proper logging action
945            raise Exception(msg)
946
947    def contextMenu(self):
948        """
949        Define actions and layout of the right click context menu
950        """
951        # Create a custom menu based on actions defined in the UI file
952        self.context_menu = QtWidgets.QMenu(self)
953        self.context_menu.addAction(self.actionDataInfo)
954        self.context_menu.addAction(self.actionSaveAs)
955        self.context_menu.addAction(self.actionQuickPlot)
956        self.context_menu.addSeparator()
957        self.context_menu.addAction(self.actionQuick3DPlot)
958        self.context_menu.addAction(self.actionEditMask)
959        self.context_menu.addSeparator()
960        self.context_menu.addAction(self.actionDelete)
961
962
963        # Define the callbacks
964        self.actionDataInfo.triggered.connect(self.showDataInfo)
965        self.actionSaveAs.triggered.connect(self.saveDataAs)
966        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
967        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
968        self.actionEditMask.triggered.connect(self.showEditDataMask)
969        self.actionDelete.triggered.connect(self.deleteItem)
970
971    def onCustomContextMenu(self, position):
972        """
973        Show the right-click context menu in the data treeview
974        """
975        index = self.current_view.indexAt(position)
976        proxy = self.current_view.model()
977        model = proxy.sourceModel()
978
979        if not index.isValid():
980            return
981        model_item = model.itemFromIndex(proxy.mapToSource(index))
982        # Find the mapped index
983        orig_index = model_item.isCheckable()
984        if not orig_index:
985            return
986        # Check the data to enable/disable actions
987        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
988        self.actionQuick3DPlot.setEnabled(is_2D)
989        self.actionEditMask.setEnabled(is_2D)
990        # Fire up the menu
991        self.context_menu.exec_(self.current_view.mapToGlobal(position))
992
993    def showDataInfo(self):
994        """
995        Show a simple read-only text edit with data information.
996        """
997        index = self.current_view.selectedIndexes()[0]
998        proxy = self.current_view.model()
999        model = proxy.sourceModel()
1000        model_item = model.itemFromIndex(proxy.mapToSource(index))
1001
1002        data = GuiUtils.dataFromItem(model_item)
1003        if isinstance(data, Data1D):
1004            text_to_show = GuiUtils.retrieveData1d(data)
1005            # Hardcoded sizes to enable full width rendering with default font
1006            self.txt_widget.resize(420,600)
1007        else:
1008            text_to_show = GuiUtils.retrieveData2d(data)
1009            # Hardcoded sizes to enable full width rendering with default font
1010            self.txt_widget.resize(700,600)
1011
1012        self.txt_widget.setReadOnly(True)
1013        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1014        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1015        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
1016        self.txt_widget.clear()
1017        self.txt_widget.insertPlainText(text_to_show)
1018
1019        self.txt_widget.show()
1020        # Move the slider all the way up, if present
1021        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1022        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1023
1024    def saveDataAs(self):
1025        """
1026        Save the data points as either txt or xml
1027        """
1028        index = self.current_view.selectedIndexes()[0]
1029        proxy = self.current_view.model()
1030        model = proxy.sourceModel()
1031        model_item = model.itemFromIndex(proxy.mapToSource(index))
1032
1033        data = GuiUtils.dataFromItem(model_item)
1034        if isinstance(data, Data1D):
1035            GuiUtils.saveData1D(data)
1036        else:
1037            GuiUtils.saveData2D(data)
1038
1039    def quickDataPlot(self):
1040        """
1041        Frozen plot - display an image of the plot
1042        """
1043        index = self.current_view.selectedIndexes()[0]
1044        proxy = self.current_view.model()
1045        model = proxy.sourceModel()
1046        model_item = model.itemFromIndex(proxy.mapToSource(index))
1047
1048        data = GuiUtils.dataFromItem(model_item)
1049
1050        method_name = 'Plotter'
1051        if isinstance(data, Data2D):
1052            method_name='Plotter2D'
1053
1054        self.new_plot = globals()[method_name](self, quickplot=True)
1055        self.new_plot.data = data
1056        #new_plot.plot(marker='o')
1057        self.new_plot.plot()
1058
1059        # Update the global plot counter
1060        title = "Plot " + data.name
1061        self.new_plot.setWindowTitle(title)
1062
1063        # Show the plot
1064        self.new_plot.show()
1065
1066    def quickData3DPlot(self):
1067        """
1068        Slowish 3D plot
1069        """
1070        index = self.current_view.selectedIndexes()[0]
1071        proxy = self.current_view.model()
1072        model = proxy.sourceModel()
1073        model_item = model.itemFromIndex(proxy.mapToSource(index))
1074
1075        data = GuiUtils.dataFromItem(model_item)
1076
1077        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1078        self.new_plot.data = data
1079        self.new_plot.plot()
1080
1081        # Update the global plot counter
1082        title = "Plot " + data.name
1083        self.new_plot.setWindowTitle(title)
1084
1085        # Show the plot
1086        self.new_plot.show()
1087
1088    def extShowEditDataMask(self):
1089        self.showEditDataMask()
1090
1091    def showEditDataMask(self, data=None):
1092        """
1093        Mask Editor for 2D plots
1094        """
1095        try:
1096            if data is None or not isinstance(data, Data2D):
1097                index = self.current_view.selectedIndexes()[0]
1098                proxy = self.current_view.model()
1099                model = proxy.sourceModel()
1100                model_item = model.itemFromIndex(proxy.mapToSource(index))
1101
1102                data = GuiUtils.dataFromItem(model_item)
1103
1104            if data is None or not isinstance(data, Data2D):
1105                msg = QtWidgets.QMessageBox()
1106                msg.setIcon(QtWidgets.QMessageBox.Information)
1107                msg.setText("Error: cannot apply mask. \
1108                                Please select a 2D dataset.")
1109                msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1110                msg.exec_()
1111                return
1112        except:
1113            msg = QtWidgets.QMessageBox()
1114            msg.setIcon(QtWidgets.QMessageBox.Information)
1115            msg.setText("Error: No dataset selected. \
1116                            Please select a 2D dataset.")
1117            msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1118            msg.exec_()
1119            return
1120
1121        mask_editor = MaskEditor(self, data)
1122        # Modal dialog here.
1123        mask_editor.exec_()
1124
1125    def deleteItem(self):
1126        """
1127        Delete the current item
1128        """
1129        # Assure this is indeed wanted
1130        delete_msg = "This operation will delete the selected data sets " +\
1131                     "and all the dependents." +\
1132                     "\nDo you want to continue?"
1133        reply = QtWidgets.QMessageBox.question(self,
1134                                           'Warning',
1135                                           delete_msg,
1136                                           QtWidgets.QMessageBox.Yes,
1137                                           QtWidgets.QMessageBox.No)
1138
1139        if reply == QtWidgets.QMessageBox.No:
1140            return
1141
1142        # Every time a row is removed, the indices change, so we'll just remove
1143        # rows and keep calling selectedIndexes until it returns an empty list.
1144        indices = self.current_view.selectedIndexes()
1145
1146        proxy = self.current_view.model()
1147        model = proxy.sourceModel()
1148
1149        deleted_items = []
1150        deleted_names = []
1151
1152        while len(indices) > 0:
1153            index = indices[0]
1154            row_index = proxy.mapToSource(index)
1155            item_to_delete = model.itemFromIndex(row_index)
1156            if item_to_delete and item_to_delete.isCheckable():
1157                row = row_index.row()
1158
1159                # store the deleted item details so we can pass them on later
1160                deleted_names.append(item_to_delete.text())
1161                deleted_items.append(item_to_delete)
1162
1163                # Delete corresponding open plots
1164                self.closePlotsForItem(item_to_delete)
1165
1166                if item_to_delete.parent():
1167                    # We have a child item - delete from it
1168                    item_to_delete.parent().removeRow(row)
1169                else:
1170                    # delete directly from model
1171                    model.removeRow(row)
1172            indices = self.current_view.selectedIndexes()
1173
1174        # Let others know we deleted data
1175        self.communicator.dataDeletedSignal.emit(deleted_items)
1176
1177        # update stored_data
1178        self.manager.update_stored_data(deleted_names)
1179
1180    def closePlotsForItem(self, item):
1181        """
1182        Given standard item, close all its currently displayed plots
1183        """
1184        # item - HashableStandardItems of active plots
1185
1186        # {} -> 'Graph1' : HashableStandardItem()
1187        current_plot_items = {}
1188        for plot_name in PlotHelper.currentPlots():
1189            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1190
1191        # item and its hashable children
1192        items_being_deleted = []
1193        if item.rowCount() > 0:
1194            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1195                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1196        items_being_deleted.append(item)
1197        # Add the parent in case a child is selected
1198        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1199            items_being_deleted.append(item.parent())
1200
1201        # Compare plot items and items to delete
1202        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1203
1204        for plot_item in plots_to_close:
1205            for plot_name in current_plot_items.keys():
1206                if plot_item == current_plot_items[plot_name]:
1207                    plotter = PlotHelper.plotById(plot_name)
1208                    # try to delete the plot
1209                    try:
1210                        plotter.close()
1211                        #self.parent.workspace().removeSubWindow(plotter)
1212                        self.plot_widgets[plot_name].close()
1213                        self.plot_widgets.pop(plot_name, None)
1214                    except AttributeError as ex:
1215                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1216
1217        pass # debugger anchor
1218
1219    def onAnalysisUpdate(self, new_perspective=""):
1220        """
1221        Update the perspective combo index based on passed string
1222        """
1223        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1224        self.cbFitting.blockSignals(True)
1225        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1226        self.cbFitting.blockSignals(False)
1227        pass
1228
1229    def loadComplete(self, output):
1230        """
1231        Post message to status bar and update the data manager
1232        """
1233        assert isinstance(output, tuple)
1234
1235        # Reset the model so the view gets updated.
1236        #self.model.reset()
1237        self.communicator.progressBarUpdateSignal.emit(-1)
1238
1239        output_data = output[0]
1240        message = output[1]
1241        # Notify the manager of the new data available
1242        self.communicator.statusBarUpdateSignal.emit(message)
1243        self.communicator.fileDataReceivedSignal.emit(output_data)
1244        self.manager.add_data(data_list=output_data)
1245
1246    def loadFailed(self, reason):
1247        print("File Load Failed with:\n", reason)
1248        pass
1249
1250    def updateModel(self, data, p_file):
1251        """
1252        Add data and Info fields to the model item
1253        """
1254        # Structure of the model
1255        # checkbox + basename
1256        #     |-------> Data.D object
1257        #     |-------> Info
1258        #                 |----> Title:
1259        #                 |----> Run:
1260        #                 |----> Type:
1261        #                 |----> Path:
1262        #                 |----> Process
1263        #                          |-----> process[0].name
1264        #     |-------> THEORIES
1265
1266        # Top-level item: checkbox with label
1267        checkbox_item = GuiUtils.HashableStandardItem()
1268        checkbox_item.setCheckable(True)
1269        checkbox_item.setCheckState(QtCore.Qt.Checked)
1270        checkbox_item.setText(os.path.basename(p_file))
1271
1272        # Add the actual Data1D/Data2D object
1273        object_item = GuiUtils.HashableStandardItem()
1274        object_item.setData(data)
1275
1276        checkbox_item.setChild(0, object_item)
1277
1278        # Add rows for display in the view
1279        info_item = GuiUtils.infoFromData(data)
1280
1281        # Set info_item as the first child
1282        checkbox_item.setChild(1, info_item)
1283
1284        # Caption for the theories
1285        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1286
1287        # New row in the model
1288        self.model.beginResetModel()
1289        self.model.appendRow(checkbox_item)
1290        self.model.endResetModel()
1291
1292    def updateModelFromPerspective(self, model_item):
1293        """
1294        Receive an update model item from a perspective
1295        Make sure it is valid and if so, replace it in the model
1296        """
1297        # Assert the correct type
1298        if not isinstance(model_item, QtGui.QStandardItem):
1299            msg = "Wrong data type returned from calculations."
1300            raise AttributeError(msg)
1301
1302        # TODO: Assert other properties
1303
1304        # Reset the view
1305        ##self.model.reset()
1306        # Pass acting as a debugger anchor
1307        pass
1308
1309    def updateTheoryFromPerspective(self, model_item):
1310        """
1311        Receive an update theory item from a perspective
1312        Make sure it is valid and if so, replace/add in the model
1313        """
1314        # Assert the correct type
1315        if not isinstance(model_item, QtGui.QStandardItem):
1316            msg = "Wrong data type returned from calculations."
1317            raise AttributeError(msg)
1318
1319        # Check if there are any other items for this tab
1320        # If so, delete them
1321        current_tab_name = model_item.text()
1322        for current_index in range(self.theory_model.rowCount()):
1323            #if current_tab_name in self.theory_model.item(current_index).text():
1324            if current_tab_name == self.theory_model.item(current_index).text():
1325                self.theory_model.removeRow(current_index)
1326                break
1327
1328        # send in the new item
1329        self.theory_model.appendRow(model_item)
1330
1331    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1332        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1333        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1334        retained in the list)."""
1335        items_to_delete = []
1336        for r in range(self.theory_model.rowCount()):
1337            item = self.theory_model.item(r, 0)
1338            data = item.child(0).data()
1339            if not hasattr(data, "id"):
1340                return
1341            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1342            if match:
1343                item_model_id = match.groups()[-1]
1344                if item_model_id == model_id:
1345                    # Only delete those identified as an intermediate plot
1346                    if match.groups()[2] not in (None, ""):
1347                        items_to_delete.append(item)
1348
1349        for item in items_to_delete:
1350            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.