source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 1942f63

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 1942f63 was 1942f63, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Merged ESS_GUI_image_viewer

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