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

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

Converted unit tests

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