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

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 facf4ca was facf4ca, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed erroneous error bars. SASVIEW-994

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