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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since d4dac80 was d4dac80, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Merge branch 'ESS_GUI' into ESS_GUI_better_batch

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