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

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

CR: Deleting a single dataitem child will close the corresponding plot (SASVIEW-958)

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