source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 17e2d502

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

Batch page serialization/deserialization

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