source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 133812c7

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

Merged ESS_GUI

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