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

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

Added freeze action for inner main model datasets. SASVIEW-1002

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