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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9463ca2 was 9463ca2, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

plotting fixes: appended plots now update; Show Plot no longer breaks if file's plot(s) have been appended to

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