source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 30bed93

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 30bed93 was 30bed93, checked in by GitHub <noreply@…>, 6 years ago

Merge pull request #181 from SasView?/ESS_GUI_poly_plot2

plot polydispersity (SASVIEW-1035 and trac ticket 17)

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