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

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 ecfe6b6 was ecfe6b6, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

Merge branch 'ESS_GUI_iss976' into ESS_GUI.

SASVIEW-976: Make intermediate data from product models (i.e. P(Q) and S(Q)) plottable.

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