source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 3ae9179

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 3ae9179 was 3ae9179, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

intermediate P(Q), S(Q) values are now saved in Data Explorer and are plottable

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