source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 490e230

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

Working batch page in project save/load

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