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

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 b4d05bd was b4d05bd, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

allow for only P(Q) or S(Q) to be present in intermediate results; improve comments

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