source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 96e8e39

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 96e8e39 was 96e8e39, checked in by rozyczko <piotrrozyczko@…>, 6 years ago

Don't assume all perspectives have 'title' attribute. Some may only have
the standard 'windowTitle'

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