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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 8137a02 was 63467b6, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Improved handling of 2d plot children. Refactored model tree search.

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