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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9463ca2 was 9463ca2, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 14 months ago

plotting fixes: appended plots now update; Show Plot no longer breaks if file's plot(s) have been appended to

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