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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since d4dac80 was d4dac80, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Merge branch 'ESS_GUI' into ESS_GUI_better_batch

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