source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 2eeda93

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

Working version of Save/Load? Analysis. SASVIEW-983.
Changed the default behaviour of Category/Model? combos:
Selecting a category does not pre-select the first model now.

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