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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9d7cb19 was 9d7cb19, checked in by piotr, 6 years ago

File/data selection should respond to activation, not index changed.
SASVIEW-1188

  • Property mode set to 100644
File size: 53.1 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
[9d7cb19]89        self.cbSelect.activated.connect(self.selectData)
[488c49d]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
[e2e5f3d]539    def add_data(self, data_list):
540        """
541        Update the data manager with new items
542        """
543        self.manager.add_data(data_list)
544
[7d8bebf]545    def updateGraphCount(self, graph_list):
546        """
547        Modify the graph name combo and potentially remove
548        deleted graphs
549        """
550        self.updateGraphCombo(graph_list)
551
552        if not self.active_plots:
553            return
554        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
[b3e8629]555        active_plots_copy = list(self.active_plots.keys())
[7d8bebf]556        for plot in active_plots_copy:
557            if self.active_plots[plot] in new_plots:
558                continue
559            self.active_plots.pop(plot)
560
[8cb6cd6]561    def updateGraphCombo(self, graph_list):
562        """
563        Modify Graph combo box on graph add/delete
564        """
565        orig_text = self.cbgraph.currentText()
566        self.cbgraph.clear()
[0268aed]567        self.cbgraph.insertItems(0, graph_list)
[8cb6cd6]568        ind = self.cbgraph.findText(orig_text)
569        if ind > 0:
570            self.cbgraph.setCurrentIndex(ind)
571
[83d6249]572    def updatePerspectiveCombo(self, index):
573        """
574        Notify the gui manager about the new perspective chosen.
575        """
[8ac3551]576        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
[1970780]577        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
[83d6249]578
[d4dac80]579    def itemFromFilename(self, filename):
580        """
581        Retrieves model item corresponding to the given filename
582        """
583        item = GuiUtils.itemFromFilename(filename, self.model)
584        return item
585
[5b144c6]586    def displayFile(self, filename=None, is_data=True, id=None):
[d48cc19]587        """
588        Forces display of charts for the given filename
589        """
[3b3b40b]590        model = self.model if is_data else self.theory_model
[88e1f57]591        # Now query the model item for available plots
[d48cc19]592        plots = GuiUtils.plotsFromFilename(filename, model)
[5b144c6]593        # Each fitpage contains the name based on fit widget number
594        fitpage_name = "" if id is None else "M"+str(id)
[88e1f57]595        new_plots = []
[6ff103a]596        for item, plot in plots.items():
[855e7ad]597            if self.updatePlot(plot):
598                # Don't create plots which are already displayed
[5b144c6]599                continue
600            # Don't plot intermediate results, e.g. P(Q), S(Q)
601            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
[3ae70f9]602            # 2nd match group contains the identifier for the intermediate
603            # result, if present (e.g. "[P(Q)]")
[5b144c6]604            if match and match.groups()[1] != None:
605                continue
[3ae70f9]606            # Don't include plots from different fitpages,
607            # but always include the original data
608            if (fitpage_name in plot.name
609                    or filename in plot.name
610                    or filename == plot.filename):
[a54bbf2b]611                # Residuals get their own plot
612                if plot.plot_role == Data1D.ROLE_RESIDUAL:
[facf4ca]613                    plot.yscale='linear'
614                    self.plotData([(item, plot)])
[88e1f57]615                else:
616                    new_plots.append((item, plot))
617
618        if new_plots:
619            self.plotData(new_plots)
[56b22f9]620
[2b8286c]621    def displayData(self, data_list, id=None):
[3b3b40b]622        """
623        Forces display of charts for the given data set
624        """
[9ce69ec]625        # data_list = [QStandardItem, Data1D/Data2D]
626        plot_to_show = data_list[1]
627        plot_item = data_list[0]
628
629        # plots to show
630        new_plots = []
631
[428c3b2]632        # Get the main data plot
633        main_data = GuiUtils.dataFromItem(plot_item.parent())
634        if main_data is None:
635            # Try the current item
636            main_data = GuiUtils.dataFromItem(plot_item)
637
638        # Make sure main data for 2D is always displayed
639        if main_data and not self.isPlotShown(main_data):
640            if isinstance(main_data, Data2D):
641                self.plotData([(plot_item, main_data)])
642
[9ce69ec]643        # Check if this is merely a plot update
644        if self.updatePlot(plot_to_show):
645            return
646
647        # Residuals get their own plot
648        if plot_to_show.plot_role == Data1D.ROLE_RESIDUAL:
649            plot_to_show.yscale='linear'
650            self.plotData([(plot_item, plot_to_show)])
651        elif plot_to_show.plot_role == Data1D.ROLE_DELETABLE:
652            # No plot
653            return
654        else:
655            # Plots with main data points on the same chart
656            # Get the main data plot
[428c3b2]657            if main_data and not self.isPlotShown(main_data):
[9ce69ec]658                new_plots.append((plot_item, main_data))
659            new_plots.append((plot_item, plot_to_show))
660
661        if new_plots:
662            self.plotData(new_plots)
[3b3b40b]663
[428c3b2]664    def isPlotShown(self, plot):
665        """
666        Checks currently shown plots and returns true if match
667        """
668        if not hasattr(plot, 'name'):
669            return False
670        ids_vals = [val.data.name for val in self.active_plots.values()]
671
672        return plot.name in ids_vals
673
[56b22f9]674    def addDataPlot2D(self, plot_set, item):
[672b8ab]675        """
676        Create a new 2D plot and add it to the workspace
677        """
[56b22f9]678        plot2D = Plotter2D(self)
679        plot2D.item = item
680        plot2D.plot(plot_set)
681        self.addPlot(plot2D)
[0cd98a1]682        self.active_plots[plot2D.data.name] = plot2D
[56b22f9]683        #============================================
[672b8ab]684        # Experimental hook for silx charts
685        #============================================
[56b22f9]686        ## Attach silx
687        #from silx.gui import qt
688        #from silx.gui.plot import StackView
689        #sv = StackView()
690        #sv.setColormap("jet", autoscale=True)
691        #sv.setStack(plot_set.data.reshape(1,100,100))
692        ##sv.setLabels(["x: -10 to 10 (200 samples)",
693        ##              "y: -10 to 5 (150 samples)"])
694        #sv.show()
695        #============================================
696
[f7d39c9]697    def plotData(self, plots, transform=True):
[56b22f9]698        """
699        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
[1042dba]700        """
701        # Call show on requested plots
[31c5b58]702        # All same-type charts in one plot
[3bdbfcc]703        for item, plot_set in plots:
[49e124c]704            if isinstance(plot_set, Data1D):
[7d8bebf]705                if not 'new_plot' in locals():
706                    new_plot = Plotter(self)
[d9150d8]707                    new_plot.item = item
[f7d39c9]708                new_plot.plot(plot_set, transform=transform)
[88e1f57]709                # active_plots may contain multiple charts
[0cd98a1]710                self.active_plots[plot_set.name] = new_plot
[49e124c]711            elif isinstance(plot_set, Data2D):
[56b22f9]712                self.addDataPlot2D(plot_set, item)
[49e124c]713            else:
714                msg = "Incorrect data type passed to Plotting"
[b3e8629]715                raise AttributeError(msg)
[49e124c]716
[7d8bebf]717        if 'new_plot' in locals() and \
[b4b8589]718            hasattr(new_plot, 'data') and \
719            isinstance(new_plot.data, Data1D):
[56b22f9]720                self.addPlot(new_plot)
721
722    def newPlot(self):
723        """
724        Select checked data and plot it
725        """
726        # Check which tab is currently active
727        if self.current_view == self.treeView:
728            plots = GuiUtils.plotsFromCheckedItems(self.model)
729        else:
730            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
731
732        self.plotData(plots)
[1042dba]733
[56b22f9]734    def addPlot(self, new_plot):
[31c5b58]735        """
736        Helper method for plot bookkeeping
737        """
738        # Update the global plot counter
[0268aed]739        title = str(PlotHelper.idOfPlot(new_plot))
[31c5b58]740        new_plot.setWindowTitle(title)
[8cb6cd6]741
[7d8bebf]742        # Set the object name to satisfy the Squish object picker
743        new_plot.setObjectName(title)
744
[31c5b58]745        # Add the plot to the workspace
[d9150d8]746        plot_widget = self.parent.workspace().addSubWindow(new_plot)
[8cb6cd6]747
[31c5b58]748        # Show the plot
749        new_plot.show()
[fbfc488]750        new_plot.canvas.draw()
[f721030]751
[d9150d8]752        # Update the plot widgets dict
753        self.plot_widgets[title]=plot_widget
754
[31c5b58]755        # Update the active chart list
[a54bbf2b]756        self.active_plots[new_plot.data.name] = new_plot
[8cb6cd6]757
758    def appendPlot(self):
759        """
760        Add data set(s) to the existing matplotlib chart
761        """
[c7f259d]762        # new plot data; check which tab is currently active
763        if self.current_view == self.treeView:
764            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
765        else:
766            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
[8cb6cd6]767
768        # old plot data
[0268aed]769        plot_id = str(self.cbgraph.currentText())
[9f4eaeb]770        try:
771            assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
772        except:
773            return
[8cb6cd6]774
775        old_plot = PlotHelper.plotById(plot_id)
776
[14d9c7b]777        # Add new data to the old plot, if data type is the same.
[3bdbfcc]778        for _, plot_set in new_plots:
[14d9c7b]779            if type(plot_set) is type(old_plot._data):
[31c5b58]780                old_plot.data = plot_set
[14d9c7b]781                old_plot.plot()
[9463ca2]782                # need this for lookup - otherwise this plot will never update
[0cd98a1]783                self.active_plots[plot_set.name] = old_plot
[8cb6cd6]784
[60d55a7]785    def updatePlot(self, data):
[7d077d1]786        """
[60d55a7]787        Modify existing plot for immediate response and returns True.
788        Returns false, if the plot does not exist already.
[7d077d1]789        """
[60d55a7]790        try: # there might be a list or a single value being passed
791            data = data[0]
792        except TypeError:
793            pass
[7d077d1]794        assert type(data).__name__ in ['Data1D', 'Data2D']
795
[9463ca2]796        ids_keys = list(self.active_plots.keys())
[0cd98a1]797        ids_vals = [val.data.name for val in self.active_plots.values()]
[9463ca2]798
[0cd98a1]799        data_id = data.name
[9463ca2]800        if data_id in ids_keys:
[a54bbf2b]801            # We have data, let's replace data that needs replacing
802            if data.plot_role != Data1D.ROLE_DATA:
803                self.active_plots[data_id].replacePlot(data_id, data)
[60d55a7]804            return True
[9463ca2]805        elif data_id in ids_vals:
[a54bbf2b]806            if data.plot_role != Data1D.ROLE_DATA:
807                list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
[60d55a7]808            return True
809        return False
[7d077d1]810
[f721030]811    def chooseFiles(self):
812        """
[5032ea68]813        Shows the Open file dialog and returns the chosen path(s)
[f721030]814        """
815        # List of known extensions
816        wlist = self.getWlist()
817
818        # Location is automatically saved - no need to keep track of the last dir
[5032ea68]819        # But only with Qt built-in dialog (non-platform native)
[4992ff2]820        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
[7969b9c]821                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[fbfc488]822        if not paths:
[f721030]823            return
824
[0cd8612]825        if not isinstance(paths, list):
[f721030]826            paths = [paths]
827
[9e426c1]828        return paths
[f721030]829
830    def readData(self, path):
831        """
[481ff26]832        verbatim copy-paste from
833           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
[f721030]834        slightly modified for clarity
835        """
836        message = ""
837        log_msg = ''
838        output = {}
839        any_error = False
840        data_error = False
841        error_message = ""
[e540cd2]842        number_of_files = len(path)
843        self.communicator.progressBarUpdateSignal.emit(0.0)
844
845        for index, p_file in enumerate(path):
[f721030]846            basename = os.path.basename(p_file)
847            _, extension = os.path.splitext(basename)
[0cd8612]848            if extension.lower() in GuiUtils.EXTENSIONS:
[f721030]849                any_error = True
850                log_msg = "Data Loader cannot "
851                log_msg += "load: %s\n" % str(p_file)
852                log_msg += """Please try to open that file from "open project" """
853                log_msg += """or "open analysis" menu\n"""
854                error_message = log_msg + "\n"
855                logging.info(log_msg)
856                continue
857
858            try:
[5032ea68]859                message = "Loading Data... " + str(basename) + "\n"
[f721030]860
861                # change this to signal notification in GuiManager
[f82ab8c]862                self.communicator.statusBarUpdateSignal.emit(message)
[f721030]863
864                output_objects = self.loader.load(p_file)
865
866                # Some loaders return a list and some just a single Data1D object.
867                # Standardize.
868                if not isinstance(output_objects, list):
869                    output_objects = [output_objects]
870
871                for item in output_objects:
[481ff26]872                    # cast sascalc.dataloader.data_info.Data1D into
873                    # sasgui.guiframe.dataFitting.Data1D
[f721030]874                    # TODO : Fix it
875                    new_data = self.manager.create_gui_data(item, p_file)
876                    output[new_data.id] = new_data
[481ff26]877
878                    # Model update should be protected
879                    self.mutex.lock()
[f721030]880                    self.updateModel(new_data, p_file)
[7969b9c]881                    #self.model.reset()
882                    QtWidgets.QApplication.processEvents()
[481ff26]883                    self.mutex.unlock()
[f721030]884
885                    if hasattr(item, 'errors'):
886                        for error_data in item.errors:
887                            data_error = True
888                            message += "\tError: {0}\n".format(error_data)
889                    else:
[5032ea68]890
[f721030]891                        logging.error("Loader returned an invalid object:\n %s" % str(item))
892                        data_error = True
893
[5032ea68]894            except Exception as ex:
[b3e8629]895                logging.error(sys.exc_info()[1])
[5032ea68]896
[f721030]897                any_error = True
898            if any_error or error_message != "":
899                if error_message == "":
900                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
901                    error += "while loading Data: \n%s\n" % str(basename)
902                    error_message += "The data file you selected could not be loaded.\n"
903                    error_message += "Make sure the content of your file"
904                    error_message += " is properly formatted.\n\n"
905                    error_message += "When contacting the SasView team, mention the"
906                    error_message += " following:\n%s" % str(error)
907                elif data_error:
908                    base_message = "Errors occurred while loading "
909                    base_message += "{0}\n".format(basename)
910                    base_message += "The data file loaded but with errors.\n"
911                    error_message = base_message + error_message
912                else:
913                    error_message += "%s\n" % str(p_file)
[481ff26]914
[e540cd2]915            current_percentage = int(100.0* index/number_of_files)
916            self.communicator.progressBarUpdateSignal.emit(current_percentage)
917
[f721030]918        if any_error or error_message:
[0cd8612]919            logging.error(error_message)
920            status_bar_message = "Errors occurred while loading %s" % format(basename)
921            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
[f721030]922
923        else:
924            message = "Loading Data Complete! "
925        message += log_msg
[0cd8612]926        # Notify the progress bar that the updates are over.
[e540cd2]927        self.communicator.progressBarUpdateSignal.emit(-1)
[454670d]928        self.communicator.statusBarUpdateSignal.emit(message)
[481ff26]929
[a281ab8]930        return output, message
[f721030]931
932    def getWlist(self):
933        """
[f82ab8c]934        Wildcards of files we know the format of.
[f721030]935        """
936        # Display the Qt Load File module
937        cards = self.loader.get_wildcards()
938
939        # get rid of the wx remnant in wildcards
940        # TODO: modify sasview loader get_wildcards method, after merge,
941        # so this kludge can be avoided
942        new_cards = []
943        for item in cards:
944            new_cards.append(item[:item.find("|")])
945        wlist = ';;'.join(new_cards)
946
947        return wlist
[488c49d]948
[a24eacf]949    def setItemsCheckability(self, model, dimension=None, checked=False):
950        """
951        For a given model, check or uncheck all items of given dimension
952        """
953        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
954
955        assert isinstance(checked, bool)
956
957        types = (None, Data1D, Data2D)
[63467b6]958        if not dimension in types:
959            return
[a24eacf]960
961        for index in range(model.rowCount()):
962            item = model.item(index)
963            if item.isCheckable() and item.checkState() != mode:
[63467b6]964                data = item.child(0).data()
965                if dimension is None or isinstance(data, dimension):
966                    item.setCheckState(mode)
967
968            items = list(GuiUtils.getChildrenFromItem(item))
969
970            for it in items:
971                if it.isCheckable() and it.checkState() != mode:
972                    data = it.child(0).data()
973                    if dimension is None or isinstance(data, dimension):
974                        it.setCheckState(mode)
[a24eacf]975
[488c49d]976    def selectData(self, index):
977        """
978        Callback method for modifying the TreeView on Selection Options change
979        """
980        if not isinstance(index, int):
981            msg = "Incorrect type passed to DataExplorer.selectData()"
[b3e8629]982            raise AttributeError(msg)
[488c49d]983
984        # Respond appropriately
985        if index == 0:
[a24eacf]986            self.setItemsCheckability(self.model, checked=True)
987
[488c49d]988        elif index == 1:
989            # De-select All
[a24eacf]990            self.setItemsCheckability(self.model, checked=False)
[488c49d]991
992        elif index == 2:
993            # Select All 1-D
[a24eacf]994            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
[488c49d]995
996        elif index == 3:
997            # Unselect All 1-D
[a24eacf]998            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
[488c49d]999
1000        elif index == 4:
1001            # Select All 2-D
[a24eacf]1002            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
[488c49d]1003
1004        elif index == 5:
1005            # Unselect All 2-D
[a24eacf]1006            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
[488c49d]1007
1008        else:
1009            msg = "Incorrect value in the Selection Option"
1010            # Change this to a proper logging action
[b3e8629]1011            raise Exception(msg)
[488c49d]1012
[4b71e91]1013    def contextMenu(self):
[e540cd2]1014        """
[4b71e91]1015        Define actions and layout of the right click context menu
[e540cd2]1016        """
[4b71e91]1017        # Create a custom menu based on actions defined in the UI file
[4992ff2]1018        self.context_menu = QtWidgets.QMenu(self)
[4b71e91]1019        self.context_menu.addAction(self.actionDataInfo)
1020        self.context_menu.addAction(self.actionSaveAs)
1021        self.context_menu.addAction(self.actionQuickPlot)
1022        self.context_menu.addSeparator()
1023        self.context_menu.addAction(self.actionQuick3DPlot)
1024        self.context_menu.addAction(self.actionEditMask)
[16436f18]1025        self.context_menu.addSeparator()
1026        self.context_menu.addAction(self.actionFreezeResults)
[c6fb57c]1027        self.context_menu.addSeparator()
1028        self.context_menu.addAction(self.actionDelete)
1029
[4b71e91]1030
1031        # Define the callbacks
1032        self.actionDataInfo.triggered.connect(self.showDataInfo)
1033        self.actionSaveAs.triggered.connect(self.saveDataAs)
1034        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
1035        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
1036        self.actionEditMask.triggered.connect(self.showEditDataMask)
[c6fb57c]1037        self.actionDelete.triggered.connect(self.deleteItem)
[33b3e4d]1038        self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
[e540cd2]1039
1040    def onCustomContextMenu(self, position):
1041        """
[4b71e91]1042        Show the right-click context menu in the data treeview
[e540cd2]1043        """
[cbcdd2c]1044        index = self.current_view.indexAt(position)
1045        proxy = self.current_view.model()
1046        model = proxy.sourceModel()
1047
[50cafe7]1048        if not index.isValid():
1049            return
1050        model_item = model.itemFromIndex(proxy.mapToSource(index))
1051        # Find the mapped index
1052        orig_index = model_item.isCheckable()
1053        if not orig_index:
1054            return
1055        # Check the data to enable/disable actions
1056        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
1057        self.actionQuick3DPlot.setEnabled(is_2D)
1058        self.actionEditMask.setEnabled(is_2D)
[33b3e4d]1059
1060        # Freezing
1061        # check that the selection has inner items
1062        freeze_enabled = False
1063        if model_item.parent() is not None:
1064            freeze_enabled = True
1065        self.actionFreezeResults.setEnabled(freeze_enabled)
1066
[50cafe7]1067        # Fire up the menu
1068        self.context_menu.exec_(self.current_view.mapToGlobal(position))
[4b71e91]1069
1070    def showDataInfo(self):
1071        """
1072        Show a simple read-only text edit with data information.
1073        """
[cbcdd2c]1074        index = self.current_view.selectedIndexes()[0]
1075        proxy = self.current_view.model()
1076        model = proxy.sourceModel()
1077        model_item = model.itemFromIndex(proxy.mapToSource(index))
1078
[8548d739]1079        data = GuiUtils.dataFromItem(model_item)
[28a84e9]1080        if isinstance(data, Data1D):
[4b71e91]1081            text_to_show = GuiUtils.retrieveData1d(data)
[28a84e9]1082            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]1083            self.txt_widget.resize(420,600)
1084        else:
1085            text_to_show = GuiUtils.retrieveData2d(data)
[28a84e9]1086            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]1087            self.txt_widget.resize(700,600)
1088
1089        self.txt_widget.setReadOnly(True)
1090        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1091        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1092        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
[d5c5d3d]1093        self.txt_widget.clear()
[4b71e91]1094        self.txt_widget.insertPlainText(text_to_show)
1095
1096        self.txt_widget.show()
[28a84e9]1097        # Move the slider all the way up, if present
[4b71e91]1098        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
[7969b9c]1099        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[4b71e91]1100
1101    def saveDataAs(self):
1102        """
[28a84e9]1103        Save the data points as either txt or xml
[4b71e91]1104        """
[cbcdd2c]1105        index = self.current_view.selectedIndexes()[0]
1106        proxy = self.current_view.model()
1107        model = proxy.sourceModel()
1108        model_item = model.itemFromIndex(proxy.mapToSource(index))
1109
[8548d739]1110        data = GuiUtils.dataFromItem(model_item)
[28a84e9]1111        if isinstance(data, Data1D):
1112            GuiUtils.saveData1D(data)
1113        else:
1114            GuiUtils.saveData2D(data)
[4b71e91]1115
1116    def quickDataPlot(self):
[1af348e]1117        """
1118        Frozen plot - display an image of the plot
1119        """
[cbcdd2c]1120        index = self.current_view.selectedIndexes()[0]
1121        proxy = self.current_view.model()
1122        model = proxy.sourceModel()
1123        model_item = model.itemFromIndex(proxy.mapToSource(index))
1124
[8548d739]1125        data = GuiUtils.dataFromItem(model_item)
[39551a68]1126
[ef01be4]1127        method_name = 'Plotter'
1128        if isinstance(data, Data2D):
1129            method_name='Plotter2D'
[1af348e]1130
[fce6c55]1131        self.new_plot = globals()[method_name](self, quickplot=True)
1132        self.new_plot.data = data
[5236449]1133        #new_plot.plot(marker='o')
[fce6c55]1134        self.new_plot.plot()
[39551a68]1135
1136        # Update the global plot counter
1137        title = "Plot " + data.name
[fce6c55]1138        self.new_plot.setWindowTitle(title)
[39551a68]1139
1140        # Show the plot
[fce6c55]1141        self.new_plot.show()
[4b71e91]1142
1143    def quickData3DPlot(self):
1144        """
[55d89f8]1145        Slowish 3D plot
[4b71e91]1146        """
[cbcdd2c]1147        index = self.current_view.selectedIndexes()[0]
1148        proxy = self.current_view.model()
1149        model = proxy.sourceModel()
1150        model_item = model.itemFromIndex(proxy.mapToSource(index))
1151
[55d89f8]1152        data = GuiUtils.dataFromItem(model_item)
1153
[fce6c55]1154        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1155        self.new_plot.data = data
1156        self.new_plot.plot()
[55d89f8]1157
1158        # Update the global plot counter
1159        title = "Plot " + data.name
[fce6c55]1160        self.new_plot.setWindowTitle(title)
[55d89f8]1161
1162        # Show the plot
[fce6c55]1163        self.new_plot.show()
[4b71e91]1164
[339e22b]1165    def extShowEditDataMask(self):
1166        self.showEditDataMask()
1167
[e20870bc]1168    def showEditDataMask(self, data=None):
[4b71e91]1169        """
[cad617b]1170        Mask Editor for 2D plots
[4b71e91]1171        """
[339e22b]1172        try:
1173            if data is None or not isinstance(data, Data2D):
1174                index = self.current_view.selectedIndexes()[0]
1175                proxy = self.current_view.model()
1176                model = proxy.sourceModel()
1177                model_item = model.itemFromIndex(proxy.mapToSource(index))
1178
1179                data = GuiUtils.dataFromItem(model_item)
1180
1181            if data is None or not isinstance(data, Data2D):
1182                msg = QtWidgets.QMessageBox()
1183                msg.setIcon(QtWidgets.QMessageBox.Information)
1184                msg.setText("Error: cannot apply mask. \
1185                                Please select a 2D dataset.")
1186                msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1187                msg.exec_()
1188                return
1189        except:
1190            msg = QtWidgets.QMessageBox()
1191            msg.setIcon(QtWidgets.QMessageBox.Information)
1192            msg.setText("Error: No dataset selected. \
1193                            Please select a 2D dataset.")
1194            msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1195            msg.exec_()
1196            return
[416fa8f]1197
1198        mask_editor = MaskEditor(self, data)
[cad617b]1199        # Modal dialog here.
[416fa8f]1200        mask_editor.exec_()
[f721030]1201
[33b3e4d]1202    def freezeItem(self, item=None):
1203        """
1204        Freeze given item
1205        """
1206        if item is None:
1207            return
1208        self.model.beginResetModel()
1209        new_item = self.cloneTheory(item)
1210        self.model.appendRow(new_item)
1211        self.model.endResetModel()
1212
[e2e5f3d]1213    def freezeDataToItem(self, data=None):
1214        """
1215        Freeze given set of data to main model
1216        """
1217        if data is None:
1218            return
1219        self.model.beginResetModel()
1220        new_item = GuiUtils.createModelItemWithPlot(data, data.name)
1221
1222        self.model.appendRow(new_item)
1223        self.model.endResetModel()
1224
[33b3e4d]1225    def freezeSelectedItems(self):
1226        """
1227        Freeze selected items
1228        """
1229        indices = self.treeView.selectedIndexes()
1230
1231        proxy = self.treeView.model()
1232        model = proxy.sourceModel()
1233
1234        for index in indices:
1235            row_index = proxy.mapToSource(index)
1236            item_to_copy = model.itemFromIndex(row_index)
1237            if item_to_copy and item_to_copy.isCheckable():
1238                self.freezeItem(item_to_copy)
1239
[c6fb57c]1240    def deleteItem(self):
1241        """
1242        Delete the current item
1243        """
1244        # Assure this is indeed wanted
[b1a7a81]1245        delete_msg = "This operation will delete the selected data sets " +\
1246                     "and all the dependents." +\
[c6fb57c]1247                     "\nDo you want to continue?"
1248        reply = QtWidgets.QMessageBox.question(self,
1249                                           'Warning',
1250                                           delete_msg,
1251                                           QtWidgets.QMessageBox.Yes,
1252                                           QtWidgets.QMessageBox.No)
1253
1254        if reply == QtWidgets.QMessageBox.No:
1255            return
1256
[d9150d8]1257        # Every time a row is removed, the indices change, so we'll just remove
1258        # rows and keep calling selectedIndexes until it returns an empty list.
1259        indices = self.current_view.selectedIndexes()
1260
[c6fb57c]1261        proxy = self.current_view.model()
1262        model = proxy.sourceModel()
1263
[515c23df]1264        deleted_items = []
[b1a7a81]1265        deleted_names = []
1266
1267        while len(indices) > 0:
1268            index = indices[0]
[c6fb57c]1269            row_index = proxy.mapToSource(index)
1270            item_to_delete = model.itemFromIndex(row_index)
[cb4d219]1271            if item_to_delete and item_to_delete.isCheckable():
[c6fb57c]1272                row = row_index.row()
[b1a7a81]1273
1274                # store the deleted item details so we can pass them on later
[515c23df]1275                deleted_names.append(item_to_delete.text())
1276                deleted_items.append(item_to_delete)
[b1a7a81]1277
[d9150d8]1278                # Delete corresponding open plots
1279                self.closePlotsForItem(item_to_delete)
1280
[c6fb57c]1281                if item_to_delete.parent():
1282                    # We have a child item - delete from it
1283                    item_to_delete.parent().removeRow(row)
1284                else:
1285                    # delete directly from model
1286                    model.removeRow(row)
[b1a7a81]1287            indices = self.current_view.selectedIndexes()
1288
1289        # Let others know we deleted data
[515c23df]1290        self.communicator.dataDeletedSignal.emit(deleted_items)
[b1a7a81]1291
1292        # update stored_data
1293        self.manager.update_stored_data(deleted_names)
[c6fb57c]1294
[d9150d8]1295    def closePlotsForItem(self, item):
1296        """
1297        Given standard item, close all its currently displayed plots
1298        """
1299        # item - HashableStandardItems of active plots
1300
1301        # {} -> 'Graph1' : HashableStandardItem()
1302        current_plot_items = {}
1303        for plot_name in PlotHelper.currentPlots():
1304            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1305
1306        # item and its hashable children
1307        items_being_deleted = []
1308        if item.rowCount() > 0:
1309            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1310                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1311        items_being_deleted.append(item)
1312        # Add the parent in case a child is selected
1313        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1314            items_being_deleted.append(item.parent())
1315
1316        # Compare plot items and items to delete
1317        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1318
1319        for plot_item in plots_to_close:
1320            for plot_name in current_plot_items.keys():
1321                if plot_item == current_plot_items[plot_name]:
1322                    plotter = PlotHelper.plotById(plot_name)
1323                    # try to delete the plot
1324                    try:
1325                        plotter.close()
1326                        #self.parent.workspace().removeSubWindow(plotter)
1327                        self.plot_widgets[plot_name].close()
1328                        self.plot_widgets.pop(plot_name, None)
1329                    except AttributeError as ex:
1330                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1331
1332        pass # debugger anchor
1333
[8ac3551]1334    def onAnalysisUpdate(self, new_perspective=""):
1335        """
1336        Update the perspective combo index based on passed string
1337        """
1338        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1339        self.cbFitting.blockSignals(True)
1340        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1341        self.cbFitting.blockSignals(False)
1342        pass
1343
[a281ab8]1344    def loadComplete(self, output):
[f721030]1345        """
1346        Post message to status bar and update the data manager
1347        """
[8cb6cd6]1348        assert isinstance(output, tuple)
[e540cd2]1349
[9e426c1]1350        # Reset the model so the view gets updated.
[7969b9c]1351        #self.model.reset()
[e540cd2]1352        self.communicator.progressBarUpdateSignal.emit(-1)
[a281ab8]1353
1354        output_data = output[0]
1355        message = output[1]
[f721030]1356        # Notify the manager of the new data available
[f82ab8c]1357        self.communicator.statusBarUpdateSignal.emit(message)
1358        self.communicator.fileDataReceivedSignal.emit(output_data)
[a281ab8]1359        self.manager.add_data(data_list=output_data)
[f721030]1360
[7969b9c]1361    def loadFailed(self, reason):
[7fb471d]1362        print("File Load Failed with:\n", reason)
1363        pass
1364
[f721030]1365    def updateModel(self, data, p_file):
1366        """
[481ff26]1367        Add data and Info fields to the model item
[f721030]1368        """
1369        # Structure of the model
1370        # checkbox + basename
[481ff26]1371        #     |-------> Data.D object
[f721030]1372        #     |-------> Info
1373        #                 |----> Title:
1374        #                 |----> Run:
1375        #                 |----> Type:
1376        #                 |----> Path:
1377        #                 |----> Process
1378        #                          |-----> process[0].name
[28a84e9]1379        #     |-------> THEORIES
[f721030]1380
1381        # Top-level item: checkbox with label
[6a3e1fe]1382        checkbox_item = GuiUtils.HashableStandardItem()
[f721030]1383        checkbox_item.setCheckable(True)
1384        checkbox_item.setCheckState(QtCore.Qt.Checked)
1385        checkbox_item.setText(os.path.basename(p_file))
1386
1387        # Add the actual Data1D/Data2D object
[6a3e1fe]1388        object_item = GuiUtils.HashableStandardItem()
[b3e8629]1389        object_item.setData(data)
[f721030]1390
[488c49d]1391        checkbox_item.setChild(0, object_item)
1392
[f721030]1393        # Add rows for display in the view
[0cd8612]1394        info_item = GuiUtils.infoFromData(data)
[f721030]1395
[28a84e9]1396        # Set info_item as the first child
[488c49d]1397        checkbox_item.setChild(1, info_item)
[f721030]1398
[28a84e9]1399        # Caption for the theories
[33b3e4d]1400        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
[28a84e9]1401
[f721030]1402        # New row in the model
[7969b9c]1403        self.model.beginResetModel()
[f721030]1404        self.model.appendRow(checkbox_item)
[7969b9c]1405        self.model.endResetModel()
[481ff26]1406
[5032ea68]1407    def updateModelFromPerspective(self, model_item):
1408        """
[a281ab8]1409        Receive an update model item from a perspective
1410        Make sure it is valid and if so, replace it in the model
[5032ea68]1411        """
[a281ab8]1412        # Assert the correct type
[0cd8612]1413        if not isinstance(model_item, QtGui.QStandardItem):
[5032ea68]1414            msg = "Wrong data type returned from calculations."
[b3e8629]1415            raise AttributeError(msg)
[a281ab8]1416
[1042dba]1417        # TODO: Assert other properties
[a281ab8]1418
[5032ea68]1419        # Reset the view
[7969b9c]1420        ##self.model.reset()
[5032ea68]1421        # Pass acting as a debugger anchor
1422        pass
[481ff26]1423
[5236449]1424    def updateTheoryFromPerspective(self, model_item):
1425        """
1426        Receive an update theory item from a perspective
1427        Make sure it is valid and if so, replace/add in the model
1428        """
1429        # Assert the correct type
1430        if not isinstance(model_item, QtGui.QStandardItem):
1431            msg = "Wrong data type returned from calculations."
[b3e8629]1432            raise AttributeError(msg)
[5236449]1433
1434        # Check if there are any other items for this tab
1435        # If so, delete them
[d6e38661]1436        current_tab_name = model_item.text()
1437        for current_index in range(self.theory_model.rowCount()):
1438            if current_tab_name == self.theory_model.item(current_index).text():
1439                self.theory_model.removeRow(current_index)
1440                break
1441        # send in the new item
[5236449]1442        self.theory_model.appendRow(model_item)
1443
[fd7ef36]1444    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1445        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1446        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1447        retained in the list)."""
1448        items_to_delete = []
1449        for r in range(self.theory_model.rowCount()):
1450            item = self.theory_model.item(r, 0)
1451            data = item.child(0).data()
1452            if not hasattr(data, "id"):
1453                return
1454            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1455            if match:
1456                item_model_id = match.groups()[-1]
1457                if item_model_id == model_id:
1458                    # Only delete those identified as an intermediate plot
1459                    if match.groups()[2] not in (None, ""):
1460                        items_to_delete.append(item)
1461
1462        for item in items_to_delete:
1463            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.