source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 10d57f6

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 10d57f6 was 345b3b3, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Save status of data explorer

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