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

ESS_GUIESS_GUI_InvariantESS_GUI_Pr_fixesESS_GUI_batch_fittingESS_GUI_iss879ESS_GUI_ordering
Last change on this file since 50cafe7 was 50cafe7, checked in by Piotr Rozyczko <rozyczko@…>, 3 months ago

Don't show constraint menu on parameter headers.

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