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

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 8ac3551 was 8ac3551, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Analysis menu properly interacting with the perspective combo.
Fixed menu enablement for Fitting.

  • 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 displayFile(self, filename=None, is_data=True):
479        """
480        Forces display of charts for the given filename
481        """
482        model = self.model if is_data else self.theory_model
483        # Now query the model item for available plots
484        plots = GuiUtils.plotsFromFilename(filename, model)
485        item = GuiUtils.itemFromFilename(filename, model)
486
487        new_plots = []
488        for plot in plots:
489            plot_id = plot.id
490            if plot_id in list(self.active_plots.keys()):
491                self.active_plots[plot_id].replacePlot(plot_id, plot)
492            else:
493                # 'sophisticated' test to generate standalone plot for residuals
494                if 'esiduals' in plot.title:
495                    self.plotData([(item, plot)])
496                else:
497                    new_plots.append((item, plot))
498
499        if new_plots:
500            self.plotData(new_plots)
501
502    def displayData(self, data_list):
503        """
504        Forces display of charts for the given data set
505        """
506        plot_to_show = data_list[0]
507        # passed plot is used ONLY to figure out its title,
508        # so all the charts related by it can be pulled from
509        # the data explorer indices.
510        filename = plot_to_show.filename
511        self.displayFile(filename=filename, is_data=plot_to_show.is_data)
512
513    def addDataPlot2D(self, plot_set, item):
514        """
515        Create a new 2D plot and add it to the workspace
516        """
517        plot2D = Plotter2D(self)
518        plot2D.item = item
519        plot2D.plot(plot_set)
520        self.addPlot(plot2D)
521        self.active_plots[plot2D.data.id] = plot2D
522        #============================================
523        # Experimental hook for silx charts
524        #============================================
525        ## Attach silx
526        #from silx.gui import qt
527        #from silx.gui.plot import StackView
528        #sv = StackView()
529        #sv.setColormap("jet", autoscale=True)
530        #sv.setStack(plot_set.data.reshape(1,100,100))
531        ##sv.setLabels(["x: -10 to 10 (200 samples)",
532        ##              "y: -10 to 5 (150 samples)"])
533        #sv.show()
534        #============================================
535
536    def plotData(self, plots):
537        """
538        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
539        """
540        # Call show on requested plots
541        # All same-type charts in one plot
542        for item, plot_set in plots:
543            if isinstance(plot_set, Data1D):
544                if not 'new_plot' in locals():
545                    new_plot = Plotter(self)
546                new_plot.plot(plot_set)
547                # active_plots may contain multiple charts
548                self.active_plots[plot_set.id] = new_plot
549            elif isinstance(plot_set, Data2D):
550                self.addDataPlot2D(plot_set, item)
551            else:
552                msg = "Incorrect data type passed to Plotting"
553                raise AttributeError(msg)
554
555        if 'new_plot' in locals() and \
556            hasattr(new_plot, 'data') and \
557            isinstance(new_plot.data, Data1D):
558                self.addPlot(new_plot)
559
560    def newPlot(self):
561        """
562        Select checked data and plot it
563        """
564        # Check which tab is currently active
565        if self.current_view == self.treeView:
566            plots = GuiUtils.plotsFromCheckedItems(self.model)
567        else:
568            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
569
570        self.plotData(plots)
571
572    def addPlot(self, new_plot):
573        """
574        Helper method for plot bookkeeping
575        """
576        # Update the global plot counter
577        title = str(PlotHelper.idOfPlot(new_plot))
578        new_plot.setWindowTitle(title)
579
580        # Set the object name to satisfy the Squish object picker
581        new_plot.setObjectName(title)
582
583        # Add the plot to the workspace
584        self.parent.workspace().addSubWindow(new_plot)
585
586        # Show the plot
587        new_plot.show()
588        new_plot.canvas.draw()
589
590        # Update the active chart list
591        #self.active_plots[new_plot.data.id] = new_plot
592
593    def appendPlot(self):
594        """
595        Add data set(s) to the existing matplotlib chart
596        """
597        # new plot data
598        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
599
600        # old plot data
601        plot_id = str(self.cbgraph.currentText())
602
603        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
604
605        old_plot = PlotHelper.plotById(plot_id)
606
607        # Add new data to the old plot, if data type is the same.
608        for _, plot_set in new_plots:
609            if type(plot_set) is type(old_plot._data):
610                old_plot.data = plot_set
611                old_plot.plot()
612
613    def updatePlot(self, new_data):
614        """
615        Modify existing plot for immediate response
616        """
617        data = new_data[0]
618        assert type(data).__name__ in ['Data1D', 'Data2D']
619
620        id = data.id
621        if data.id in list(self.active_plots.keys()):
622            self.active_plots[id].replacePlot(id, data)
623
624    def chooseFiles(self):
625        """
626        Shows the Open file dialog and returns the chosen path(s)
627        """
628        # List of known extensions
629        wlist = self.getWlist()
630
631        # Location is automatically saved - no need to keep track of the last dir
632        # But only with Qt built-in dialog (non-platform native)
633        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
634                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
635        if not paths:
636            return
637
638        if not isinstance(paths, list):
639            paths = [paths]
640
641        return paths
642
643    def readData(self, path):
644        """
645        verbatim copy-paste from
646           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
647        slightly modified for clarity
648        """
649        message = ""
650        log_msg = ''
651        output = {}
652        any_error = False
653        data_error = False
654        error_message = ""
655        number_of_files = len(path)
656        self.communicator.progressBarUpdateSignal.emit(0.0)
657
658        for index, p_file in enumerate(path):
659            basename = os.path.basename(p_file)
660            _, extension = os.path.splitext(basename)
661            if extension.lower() in GuiUtils.EXTENSIONS:
662                any_error = True
663                log_msg = "Data Loader cannot "
664                log_msg += "load: %s\n" % str(p_file)
665                log_msg += """Please try to open that file from "open project" """
666                log_msg += """or "open analysis" menu\n"""
667                error_message = log_msg + "\n"
668                logging.info(log_msg)
669                continue
670
671            try:
672                message = "Loading Data... " + str(basename) + "\n"
673
674                # change this to signal notification in GuiManager
675                self.communicator.statusBarUpdateSignal.emit(message)
676
677                output_objects = self.loader.load(p_file)
678
679                # Some loaders return a list and some just a single Data1D object.
680                # Standardize.
681                if not isinstance(output_objects, list):
682                    output_objects = [output_objects]
683
684                for item in output_objects:
685                    # cast sascalc.dataloader.data_info.Data1D into
686                    # sasgui.guiframe.dataFitting.Data1D
687                    # TODO : Fix it
688                    new_data = self.manager.create_gui_data(item, p_file)
689                    output[new_data.id] = new_data
690
691                    # Model update should be protected
692                    self.mutex.lock()
693                    self.updateModel(new_data, p_file)
694                    #self.model.reset()
695                    QtWidgets.QApplication.processEvents()
696                    self.mutex.unlock()
697
698                    if hasattr(item, 'errors'):
699                        for error_data in item.errors:
700                            data_error = True
701                            message += "\tError: {0}\n".format(error_data)
702                    else:
703
704                        logging.error("Loader returned an invalid object:\n %s" % str(item))
705                        data_error = True
706
707            except Exception as ex:
708                logging.error(sys.exc_info()[1])
709
710                any_error = True
711            if any_error or error_message != "":
712                if error_message == "":
713                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
714                    error += "while loading Data: \n%s\n" % str(basename)
715                    error_message += "The data file you selected could not be loaded.\n"
716                    error_message += "Make sure the content of your file"
717                    error_message += " is properly formatted.\n\n"
718                    error_message += "When contacting the SasView team, mention the"
719                    error_message += " following:\n%s" % str(error)
720                elif data_error:
721                    base_message = "Errors occurred while loading "
722                    base_message += "{0}\n".format(basename)
723                    base_message += "The data file loaded but with errors.\n"
724                    error_message = base_message + error_message
725                else:
726                    error_message += "%s\n" % str(p_file)
727
728            current_percentage = int(100.0* index/number_of_files)
729            self.communicator.progressBarUpdateSignal.emit(current_percentage)
730
731        if any_error or error_message:
732            logging.error(error_message)
733            status_bar_message = "Errors occurred while loading %s" % format(basename)
734            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
735
736        else:
737            message = "Loading Data Complete! "
738        message += log_msg
739        # Notify the progress bar that the updates are over.
740        self.communicator.progressBarUpdateSignal.emit(-1)
741        self.communicator.statusBarUpdateSignal.emit(message)
742
743        return output, message
744
745    def getWlist(self):
746        """
747        Wildcards of files we know the format of.
748        """
749        # Display the Qt Load File module
750        cards = self.loader.get_wildcards()
751
752        # get rid of the wx remnant in wildcards
753        # TODO: modify sasview loader get_wildcards method, after merge,
754        # so this kludge can be avoided
755        new_cards = []
756        for item in cards:
757            new_cards.append(item[:item.find("|")])
758        wlist = ';;'.join(new_cards)
759
760        return wlist
761
762    def selectData(self, index):
763        """
764        Callback method for modifying the TreeView on Selection Options change
765        """
766        if not isinstance(index, int):
767            msg = "Incorrect type passed to DataExplorer.selectData()"
768            raise AttributeError(msg)
769
770        # Respond appropriately
771        if index == 0:
772            # Select All
773            for index in range(self.model.rowCount()):
774                item = self.model.item(index)
775                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
776                    item.setCheckState(QtCore.Qt.Checked)
777        elif index == 1:
778            # De-select All
779            for index in range(self.model.rowCount()):
780                item = self.model.item(index)
781                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
782                    item.setCheckState(QtCore.Qt.Unchecked)
783
784        elif index == 2:
785            # Select All 1-D
786            for index in range(self.model.rowCount()):
787                item = self.model.item(index)
788                item.setCheckState(QtCore.Qt.Unchecked)
789
790                try:
791                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
792                except AttributeError:
793                    msg = "Bad structure of the data model."
794                    raise RuntimeError(msg)
795
796                if is1D:
797                    item.setCheckState(QtCore.Qt.Checked)
798
799        elif index == 3:
800            # Unselect All 1-D
801            for index in range(self.model.rowCount()):
802                item = self.model.item(index)
803
804                try:
805                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
806                except AttributeError:
807                    msg = "Bad structure of the data model."
808                    raise RuntimeError(msg)
809
810                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
811                    item.setCheckState(QtCore.Qt.Unchecked)
812
813        elif index == 4:
814            # Select All 2-D
815            for index in range(self.model.rowCount()):
816                item = self.model.item(index)
817                item.setCheckState(QtCore.Qt.Unchecked)
818                try:
819                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
820                except AttributeError:
821                    msg = "Bad structure of the data model."
822                    raise RuntimeError(msg)
823
824                if is2D:
825                    item.setCheckState(QtCore.Qt.Checked)
826
827        elif index == 5:
828            # Unselect All 2-D
829            for index in range(self.model.rowCount()):
830                item = self.model.item(index)
831
832                try:
833                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
834                except AttributeError:
835                    msg = "Bad structure of the data model."
836                    raise RuntimeError(msg)
837
838                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
839                    item.setCheckState(QtCore.Qt.Unchecked)
840
841        else:
842            msg = "Incorrect value in the Selection Option"
843            # Change this to a proper logging action
844            raise Exception(msg)
845
846    def contextMenu(self):
847        """
848        Define actions and layout of the right click context menu
849        """
850        # Create a custom menu based on actions defined in the UI file
851        self.context_menu = QtWidgets.QMenu(self)
852        self.context_menu.addAction(self.actionDataInfo)
853        self.context_menu.addAction(self.actionSaveAs)
854        self.context_menu.addAction(self.actionQuickPlot)
855        self.context_menu.addSeparator()
856        self.context_menu.addAction(self.actionQuick3DPlot)
857        self.context_menu.addAction(self.actionEditMask)
858        self.context_menu.addSeparator()
859        self.context_menu.addAction(self.actionDelete)
860
861
862        # Define the callbacks
863        self.actionDataInfo.triggered.connect(self.showDataInfo)
864        self.actionSaveAs.triggered.connect(self.saveDataAs)
865        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
866        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
867        self.actionEditMask.triggered.connect(self.showEditDataMask)
868        self.actionDelete.triggered.connect(self.deleteItem)
869
870    def onCustomContextMenu(self, position):
871        """
872        Show the right-click context menu in the data treeview
873        """
874        index = self.current_view.indexAt(position)
875        proxy = self.current_view.model()
876        model = proxy.sourceModel()
877
878        if index.isValid():
879            model_item = model.itemFromIndex(proxy.mapToSource(index))
880            # Find the mapped index
881            orig_index = model_item.isCheckable()
882            if orig_index:
883                # Check the data to enable/disable actions
884                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
885                self.actionQuick3DPlot.setEnabled(is_2D)
886                self.actionEditMask.setEnabled(is_2D)
887                # Fire up the menu
888                self.context_menu.exec_(self.current_view.mapToGlobal(position))
889
890    def showDataInfo(self):
891        """
892        Show a simple read-only text edit with data information.
893        """
894        index = self.current_view.selectedIndexes()[0]
895        proxy = self.current_view.model()
896        model = proxy.sourceModel()
897        model_item = model.itemFromIndex(proxy.mapToSource(index))
898
899        data = GuiUtils.dataFromItem(model_item)
900        if isinstance(data, Data1D):
901            text_to_show = GuiUtils.retrieveData1d(data)
902            # Hardcoded sizes to enable full width rendering with default font
903            self.txt_widget.resize(420,600)
904        else:
905            text_to_show = GuiUtils.retrieveData2d(data)
906            # Hardcoded sizes to enable full width rendering with default font
907            self.txt_widget.resize(700,600)
908
909        self.txt_widget.setReadOnly(True)
910        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
911        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
912        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
913        self.txt_widget.clear()
914        self.txt_widget.insertPlainText(text_to_show)
915
916        self.txt_widget.show()
917        # Move the slider all the way up, if present
918        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
919        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
920
921    def saveDataAs(self):
922        """
923        Save the data points as either txt or xml
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            GuiUtils.saveData1D(data)
933        else:
934            GuiUtils.saveData2D(data)
935
936    def quickDataPlot(self):
937        """
938        Frozen plot - display an image of the plot
939        """
940        index = self.current_view.selectedIndexes()[0]
941        proxy = self.current_view.model()
942        model = proxy.sourceModel()
943        model_item = model.itemFromIndex(proxy.mapToSource(index))
944
945        data = GuiUtils.dataFromItem(model_item)
946
947        method_name = 'Plotter'
948        if isinstance(data, Data2D):
949            method_name='Plotter2D'
950
951        new_plot = globals()[method_name](self, quickplot=True)
952        new_plot.data = data
953        #new_plot.plot(marker='o')
954        new_plot.plot()
955
956        # Update the global plot counter
957        title = "Plot " + data.name
958        new_plot.setWindowTitle(title)
959
960        # Show the plot
961        new_plot.show()
962
963    def quickData3DPlot(self):
964        """
965        Slowish 3D plot
966        """
967        index = self.current_view.selectedIndexes()[0]
968        proxy = self.current_view.model()
969        model = proxy.sourceModel()
970        model_item = model.itemFromIndex(proxy.mapToSource(index))
971
972        data = GuiUtils.dataFromItem(model_item)
973
974        new_plot = Plotter2D(self, quickplot=True, dimension=3)
975        new_plot.data = data
976        new_plot.plot()
977
978        # Update the global plot counter
979        title = "Plot " + data.name
980        new_plot.setWindowTitle(title)
981
982        # Show the plot
983        new_plot.show()
984
985    def showEditDataMask(self):
986        """
987        Mask Editor for 2D plots
988        """
989        index = self.current_view.selectedIndexes()[0]
990        proxy = self.current_view.model()
991        model = proxy.sourceModel()
992        model_item = model.itemFromIndex(proxy.mapToSource(index))
993
994        data = GuiUtils.dataFromItem(model_item)
995
996        mask_editor = MaskEditor(self, data)
997        # Modal dialog here.
998        mask_editor.exec_()
999
1000    def deleteItem(self):
1001        """
1002        Delete the current item
1003        """
1004        # Assure this is indeed wanted
1005        delete_msg = "This operation will delete the selected data sets." +\
1006                     "\nDo you want to continue?"
1007        reply = QtWidgets.QMessageBox.question(self,
1008                                           'Warning',
1009                                           delete_msg,
1010                                           QtWidgets.QMessageBox.Yes,
1011                                           QtWidgets.QMessageBox.No)
1012
1013        if reply == QtWidgets.QMessageBox.No:
1014            return
1015
1016        indices = self.current_view.selectedIndexes()
1017        proxy = self.current_view.model()
1018        model = proxy.sourceModel()
1019
1020        for index in indices:
1021            row_index = proxy.mapToSource(index)
1022            item_to_delete = model.itemFromIndex(row_index)
1023            if item_to_delete and item_to_delete.isCheckable():
1024                row = row_index.row()
1025                if item_to_delete.parent():
1026                    # We have a child item - delete from it
1027                    item_to_delete.parent().removeRow(row)
1028                else:
1029                    # delete directly from model
1030                    model.removeRow(row)
1031        pass
1032
1033    def onAnalysisUpdate(self, new_perspective=""):
1034        """
1035        Update the perspective combo index based on passed string
1036        """
1037        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1038        self.cbFitting.blockSignals(True)
1039        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1040        self.cbFitting.blockSignals(False)
1041        pass
1042
1043    def loadComplete(self, output):
1044        """
1045        Post message to status bar and update the data manager
1046        """
1047        assert isinstance(output, tuple)
1048
1049        # Reset the model so the view gets updated.
1050        #self.model.reset()
1051        self.communicator.progressBarUpdateSignal.emit(-1)
1052
1053        output_data = output[0]
1054        message = output[1]
1055        # Notify the manager of the new data available
1056        self.communicator.statusBarUpdateSignal.emit(message)
1057        self.communicator.fileDataReceivedSignal.emit(output_data)
1058        self.manager.add_data(data_list=output_data)
1059
1060    def loadFailed(self, reason):
1061        print("File Load Failed with:\n", reason)
1062        pass
1063
1064    def updateModel(self, data, p_file):
1065        """
1066        Add data and Info fields to the model item
1067        """
1068        # Structure of the model
1069        # checkbox + basename
1070        #     |-------> Data.D object
1071        #     |-------> Info
1072        #                 |----> Title:
1073        #                 |----> Run:
1074        #                 |----> Type:
1075        #                 |----> Path:
1076        #                 |----> Process
1077        #                          |-----> process[0].name
1078        #     |-------> THEORIES
1079
1080        # Top-level item: checkbox with label
1081        checkbox_item = GuiUtils.HashableStandardItem()
1082        checkbox_item.setCheckable(True)
1083        checkbox_item.setCheckState(QtCore.Qt.Checked)
1084        checkbox_item.setText(os.path.basename(p_file))
1085
1086        # Add the actual Data1D/Data2D object
1087        object_item = GuiUtils.HashableStandardItem()
1088        object_item.setData(data)
1089
1090        checkbox_item.setChild(0, object_item)
1091
1092        # Add rows for display in the view
1093        info_item = GuiUtils.infoFromData(data)
1094
1095        # Set info_item as the first child
1096        checkbox_item.setChild(1, info_item)
1097
1098        # Caption for the theories
1099        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1100
1101        # New row in the model
1102        self.model.beginResetModel()
1103        self.model.appendRow(checkbox_item)
1104        self.model.endResetModel()
1105
1106    def updateModelFromPerspective(self, model_item):
1107        """
1108        Receive an update model item from a perspective
1109        Make sure it is valid and if so, replace it in the model
1110        """
1111        # Assert the correct type
1112        if not isinstance(model_item, QtGui.QStandardItem):
1113            msg = "Wrong data type returned from calculations."
1114            raise AttributeError(msg)
1115
1116        # TODO: Assert other properties
1117
1118        # Reset the view
1119        ##self.model.reset()
1120        # Pass acting as a debugger anchor
1121        pass
1122
1123    def updateTheoryFromPerspective(self, model_item):
1124        """
1125        Receive an update theory item from a perspective
1126        Make sure it is valid and if so, replace/add in the model
1127        """
1128        # Assert the correct type
1129        if not isinstance(model_item, QtGui.QStandardItem):
1130            msg = "Wrong data type returned from calculations."
1131            raise AttributeError(msg)
1132
1133        # Check if there are any other items for this tab
1134        # If so, delete them
1135        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1136        #
1137        ##self.model.beginResetModel()
1138        ##current_tab_name = model_item.text()[:2]
1139        ##for current_index in range(self.theory_model.rowCount()):
1140            #if current_tab_name in self.theory_model.item(current_index).text():
1141            #    return
1142        ##        self.theory_model.removeRow(current_index)
1143        ##        break
1144
1145        ### Reset the view
1146        ##self.model.endResetModel()
1147
1148        # Reset the view
1149        self.theory_model.appendRow(model_item)
1150
1151        # Pass acting as a debugger anchor
1152        pass
1153
1154
1155if __name__ == "__main__":
1156    app = QtWidgets.QApplication([])
1157    dlg = DataExplorerWindow()
1158    dlg.show()
1159    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.