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

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

Added subitems to the selection logic - SASVIEW-999

  • Property mode set to 100644
File size: 45.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 setItemsCheckability(self, model, dimension=None, checked=False):
829        """
830        For a given model, check or uncheck all items of given dimension
831        """
832        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
833
834        assert isinstance(checked, bool)
835
836        types = (None, Data1D, Data2D)
837        assert dimension in types
838
839        for index in range(model.rowCount()):
840            item = model.item(index)
841            if dimension is not None and not isinstance(GuiUtils.dataFromItem(item), dimension):
842                continue
843            if item.isCheckable() and item.checkState() != mode:
844                item.setCheckState(mode)
845            # look for all children
846            for inner_index in range(item.rowCount()):
847                child = item.child(inner_index)
848                if child.isCheckable() and child.checkState() != mode:
849                    child.setCheckState(mode)
850
851    def selectData(self, index):
852        """
853        Callback method for modifying the TreeView on Selection Options change
854        """
855        if not isinstance(index, int):
856            msg = "Incorrect type passed to DataExplorer.selectData()"
857            raise AttributeError(msg)
858
859        # Respond appropriately
860        if index == 0:
861            self.setItemsCheckability(self.model, checked=True)
862
863        elif index == 1:
864            # De-select All
865            self.setItemsCheckability(self.model, checked=False)
866
867        elif index == 2:
868            # Select All 1-D
869            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
870
871        elif index == 3:
872            # Unselect All 1-D
873            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
874
875        elif index == 4:
876            # Select All 2-D
877            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
878
879        elif index == 5:
880            # Unselect All 2-D
881            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
882
883        else:
884            msg = "Incorrect value in the Selection Option"
885            # Change this to a proper logging action
886            raise Exception(msg)
887
888    def contextMenu(self):
889        """
890        Define actions and layout of the right click context menu
891        """
892        # Create a custom menu based on actions defined in the UI file
893        self.context_menu = QtWidgets.QMenu(self)
894        self.context_menu.addAction(self.actionDataInfo)
895        self.context_menu.addAction(self.actionSaveAs)
896        self.context_menu.addAction(self.actionQuickPlot)
897        self.context_menu.addSeparator()
898        self.context_menu.addAction(self.actionQuick3DPlot)
899        self.context_menu.addAction(self.actionEditMask)
900        self.context_menu.addSeparator()
901        self.context_menu.addAction(self.actionDelete)
902
903
904        # Define the callbacks
905        self.actionDataInfo.triggered.connect(self.showDataInfo)
906        self.actionSaveAs.triggered.connect(self.saveDataAs)
907        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
908        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
909        self.actionEditMask.triggered.connect(self.showEditDataMask)
910        self.actionDelete.triggered.connect(self.deleteItem)
911
912    def onCustomContextMenu(self, position):
913        """
914        Show the right-click context menu in the data treeview
915        """
916        index = self.current_view.indexAt(position)
917        proxy = self.current_view.model()
918        model = proxy.sourceModel()
919
920        if index.isValid():
921            model_item = model.itemFromIndex(proxy.mapToSource(index))
922            # Find the mapped index
923            orig_index = model_item.isCheckable()
924            if orig_index:
925                # Check the data to enable/disable actions
926                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
927                self.actionQuick3DPlot.setEnabled(is_2D)
928                self.actionEditMask.setEnabled(is_2D)
929                # Fire up the menu
930                self.context_menu.exec_(self.current_view.mapToGlobal(position))
931
932    def showDataInfo(self):
933        """
934        Show a simple read-only text edit with data information.
935        """
936        index = self.current_view.selectedIndexes()[0]
937        proxy = self.current_view.model()
938        model = proxy.sourceModel()
939        model_item = model.itemFromIndex(proxy.mapToSource(index))
940
941        data = GuiUtils.dataFromItem(model_item)
942        if isinstance(data, Data1D):
943            text_to_show = GuiUtils.retrieveData1d(data)
944            # Hardcoded sizes to enable full width rendering with default font
945            self.txt_widget.resize(420,600)
946        else:
947            text_to_show = GuiUtils.retrieveData2d(data)
948            # Hardcoded sizes to enable full width rendering with default font
949            self.txt_widget.resize(700,600)
950
951        self.txt_widget.setReadOnly(True)
952        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
953        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
954        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
955        self.txt_widget.clear()
956        self.txt_widget.insertPlainText(text_to_show)
957
958        self.txt_widget.show()
959        # Move the slider all the way up, if present
960        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
961        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
962
963    def saveDataAs(self):
964        """
965        Save the data points as either txt or xml
966        """
967        index = self.current_view.selectedIndexes()[0]
968        proxy = self.current_view.model()
969        model = proxy.sourceModel()
970        model_item = model.itemFromIndex(proxy.mapToSource(index))
971
972        data = GuiUtils.dataFromItem(model_item)
973        if isinstance(data, Data1D):
974            GuiUtils.saveData1D(data)
975        else:
976            GuiUtils.saveData2D(data)
977
978    def quickDataPlot(self):
979        """
980        Frozen plot - display an image of the plot
981        """
982        index = self.current_view.selectedIndexes()[0]
983        proxy = self.current_view.model()
984        model = proxy.sourceModel()
985        model_item = model.itemFromIndex(proxy.mapToSource(index))
986
987        data = GuiUtils.dataFromItem(model_item)
988
989        method_name = 'Plotter'
990        if isinstance(data, Data2D):
991            method_name='Plotter2D'
992
993        self.new_plot = globals()[method_name](self, quickplot=True)
994        self.new_plot.data = data
995        #new_plot.plot(marker='o')
996        self.new_plot.plot()
997
998        # Update the global plot counter
999        title = "Plot " + data.name
1000        self.new_plot.setWindowTitle(title)
1001
1002        # Show the plot
1003        self.new_plot.show()
1004
1005    def quickData3DPlot(self):
1006        """
1007        Slowish 3D plot
1008        """
1009        index = self.current_view.selectedIndexes()[0]
1010        proxy = self.current_view.model()
1011        model = proxy.sourceModel()
1012        model_item = model.itemFromIndex(proxy.mapToSource(index))
1013
1014        data = GuiUtils.dataFromItem(model_item)
1015
1016        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1017        self.new_plot.data = data
1018        self.new_plot.plot()
1019
1020        # Update the global plot counter
1021        title = "Plot " + data.name
1022        self.new_plot.setWindowTitle(title)
1023
1024        # Show the plot
1025        self.new_plot.show()
1026
1027    def showEditDataMask(self, data=None):
1028        """
1029        Mask Editor for 2D plots
1030        """
1031        if data is None or not isinstance(data, Data2D):
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        mask_editor = MaskEditor(self, data)
1040        # Modal dialog here.
1041        mask_editor.exec_()
1042
1043    def deleteItem(self):
1044        """
1045        Delete the current item
1046        """
1047        # Assure this is indeed wanted
1048        delete_msg = "This operation will delete the selected data sets " +\
1049                     "and all the dependents." +\
1050                     "\nDo you want to continue?"
1051        reply = QtWidgets.QMessageBox.question(self,
1052                                           'Warning',
1053                                           delete_msg,
1054                                           QtWidgets.QMessageBox.Yes,
1055                                           QtWidgets.QMessageBox.No)
1056
1057        if reply == QtWidgets.QMessageBox.No:
1058            return
1059
1060        # Every time a row is removed, the indices change, so we'll just remove
1061        # rows and keep calling selectedIndexes until it returns an empty list.
1062        indices = self.current_view.selectedIndexes()
1063
1064        proxy = self.current_view.model()
1065        model = proxy.sourceModel()
1066
1067        deleted_items = []
1068        deleted_names = []
1069
1070        while len(indices) > 0:
1071            index = indices[0]
1072            row_index = proxy.mapToSource(index)
1073            item_to_delete = model.itemFromIndex(row_index)
1074            if item_to_delete and item_to_delete.isCheckable():
1075                row = row_index.row()
1076
1077                # store the deleted item details so we can pass them on later
1078                deleted_names.append(item_to_delete.text())
1079                deleted_items.append(item_to_delete)
1080
1081                # Delete corresponding open plots
1082                self.closePlotsForItem(item_to_delete)
1083
1084                if item_to_delete.parent():
1085                    # We have a child item - delete from it
1086                    item_to_delete.parent().removeRow(row)
1087                else:
1088                    # delete directly from model
1089                    model.removeRow(row)
1090            indices = self.current_view.selectedIndexes()
1091
1092        # Let others know we deleted data
1093        self.communicator.dataDeletedSignal.emit(deleted_items)
1094
1095        # update stored_data
1096        self.manager.update_stored_data(deleted_names)
1097
1098    def closePlotsForItem(self, item):
1099        """
1100        Given standard item, close all its currently displayed plots
1101        """
1102        # item - HashableStandardItems of active plots
1103
1104        # {} -> 'Graph1' : HashableStandardItem()
1105        current_plot_items = {}
1106        for plot_name in PlotHelper.currentPlots():
1107            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1108
1109        # item and its hashable children
1110        items_being_deleted = []
1111        if item.rowCount() > 0:
1112            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1113                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1114        items_being_deleted.append(item)
1115        # Add the parent in case a child is selected
1116        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1117            items_being_deleted.append(item.parent())
1118
1119        # Compare plot items and items to delete
1120        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1121
1122        for plot_item in plots_to_close:
1123            for plot_name in current_plot_items.keys():
1124                if plot_item == current_plot_items[plot_name]:
1125                    plotter = PlotHelper.plotById(plot_name)
1126                    # try to delete the plot
1127                    try:
1128                        plotter.close()
1129                        #self.parent.workspace().removeSubWindow(plotter)
1130                        self.plot_widgets[plot_name].close()
1131                        self.plot_widgets.pop(plot_name, None)
1132                    except AttributeError as ex:
1133                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1134
1135        pass # debugger anchor
1136
1137    def onAnalysisUpdate(self, new_perspective=""):
1138        """
1139        Update the perspective combo index based on passed string
1140        """
1141        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1142        self.cbFitting.blockSignals(True)
1143        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1144        self.cbFitting.blockSignals(False)
1145        pass
1146
1147    def loadComplete(self, output):
1148        """
1149        Post message to status bar and update the data manager
1150        """
1151        assert isinstance(output, tuple)
1152
1153        # Reset the model so the view gets updated.
1154        #self.model.reset()
1155        self.communicator.progressBarUpdateSignal.emit(-1)
1156
1157        output_data = output[0]
1158        message = output[1]
1159        # Notify the manager of the new data available
1160        self.communicator.statusBarUpdateSignal.emit(message)
1161        self.communicator.fileDataReceivedSignal.emit(output_data)
1162        self.manager.add_data(data_list=output_data)
1163
1164    def loadFailed(self, reason):
1165        print("File Load Failed with:\n", reason)
1166        pass
1167
1168    def updateModel(self, data, p_file):
1169        """
1170        Add data and Info fields to the model item
1171        """
1172        # Structure of the model
1173        # checkbox + basename
1174        #     |-------> Data.D object
1175        #     |-------> Info
1176        #                 |----> Title:
1177        #                 |----> Run:
1178        #                 |----> Type:
1179        #                 |----> Path:
1180        #                 |----> Process
1181        #                          |-----> process[0].name
1182        #     |-------> THEORIES
1183
1184        # Top-level item: checkbox with label
1185        checkbox_item = GuiUtils.HashableStandardItem()
1186        checkbox_item.setCheckable(True)
1187        checkbox_item.setCheckState(QtCore.Qt.Checked)
1188        checkbox_item.setText(os.path.basename(p_file))
1189
1190        # Add the actual Data1D/Data2D object
1191        object_item = GuiUtils.HashableStandardItem()
1192        object_item.setData(data)
1193
1194        checkbox_item.setChild(0, object_item)
1195
1196        # Add rows for display in the view
1197        info_item = GuiUtils.infoFromData(data)
1198
1199        # Set info_item as the first child
1200        checkbox_item.setChild(1, info_item)
1201
1202        # Caption for the theories
1203        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1204
1205        # New row in the model
1206        self.model.beginResetModel()
1207        self.model.appendRow(checkbox_item)
1208        self.model.endResetModel()
1209
1210    def updateModelFromPerspective(self, model_item):
1211        """
1212        Receive an update model item from a perspective
1213        Make sure it is valid and if so, replace it in the model
1214        """
1215        # Assert the correct type
1216        if not isinstance(model_item, QtGui.QStandardItem):
1217            msg = "Wrong data type returned from calculations."
1218            raise AttributeError(msg)
1219
1220        # TODO: Assert other properties
1221
1222        # Reset the view
1223        ##self.model.reset()
1224        # Pass acting as a debugger anchor
1225        pass
1226
1227    def updateTheoryFromPerspective(self, model_item):
1228        """
1229        Receive an update theory item from a perspective
1230        Make sure it is valid and if so, replace/add in the model
1231        """
1232        # Assert the correct type
1233        if not isinstance(model_item, QtGui.QStandardItem):
1234            msg = "Wrong data type returned from calculations."
1235            raise AttributeError(msg)
1236
1237        # Check if there are any other items for this tab
1238        # If so, delete them
1239        current_tab_name = model_item.text()
1240        for current_index in range(self.theory_model.rowCount()):
1241            #if current_tab_name in self.theory_model.item(current_index).text():
1242            if current_tab_name == self.theory_model.item(current_index).text():
1243                self.theory_model.removeRow(current_index)
1244                break
1245
1246        # send in the new item
1247        self.theory_model.appendRow(model_item)
1248
Note: See TracBrowser for help on using the repository browser.