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

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

Converted unit tests

  • Property mode set to 100644
File size: 39.6 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
10from PyQt5 import QtWebKitWidgets
11
12from twisted.internet import threads
13
14# SASCALC
15from sas.sascalc.dataloader.loader import Loader
16
17# QTGUI
18import sas.qtgui.Utilities.GuiUtils as GuiUtils
19import sas.qtgui.Plotting.PlotHelper as PlotHelper
20
21from sas.qtgui.Plotting.PlotterData import Data1D
22from sas.qtgui.Plotting.PlotterData import Data2D
23from sas.qtgui.Plotting.Plotter import Plotter
24from sas.qtgui.Plotting.Plotter2D import Plotter2D
25from sas.qtgui.Plotting.MaskEditor import MaskEditor
26
27from sas.qtgui.MainWindow.DataManager import DataManager
28from sas.qtgui.MainWindow.DroppableDataLoadWidget import DroppableDataLoadWidget
29
30import sas.qtgui.Perspectives as Perspectives
31
32class DataExplorerWindow(DroppableDataLoadWidget):
33    # The controller which is responsible for managing signal slots connections
34    # for the gui and providing an interface to the data model.
35
36    def __init__(self, parent=None, guimanager=None, manager=None):
37        super(DataExplorerWindow, self).__init__(parent, guimanager)
38
39        # Main model for keeping loaded data
40        self.model = QtGui.QStandardItemModel(self)
41
42        # Secondary model for keeping frozen data sets
43        self.theory_model = QtGui.QStandardItemModel(self)
44
45        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
46        # in order to set the widget parentage properly.
47        self.parent = guimanager
48        self.loader = Loader()
49        self.manager = manager if manager is not None else DataManager()
50        self.txt_widget = QtWidgets.QTextEdit(None)
51
52        # Be careful with twisted threads.
53        self.mutex = QtCore.QMutex()
54
55        # Active plots
56        self.active_plots = {}
57
58        # Connect the buttons
59        self.cmdLoad.clicked.connect(self.loadFile)
60        self.cmdDeleteData.clicked.connect(self.deleteFile)
61        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
62        self.cmdFreeze.clicked.connect(self.freezeTheory)
63        self.cmdSendTo.clicked.connect(self.sendData)
64        self.cmdNew.clicked.connect(self.newPlot)
65        self.cmdNew_2.clicked.connect(self.newPlot)
66        self.cmdAppend.clicked.connect(self.appendPlot)
67        self.cmdHelp.clicked.connect(self.displayHelp)
68        self.cmdHelp_2.clicked.connect(self.displayHelp)
69
70        # Display HTML content
71        self._helpView = QtWebKitWidgets.QWebView()
72
73        # Fill in the perspectives combo
74        self.initPerspectives()
75
76        # Custom context menu
77        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
78        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
79        self.contextMenu()
80
81        # Same menus for the theory view
82        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
83        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
84
85        # Connect the comboboxes
86        self.cbSelect.currentIndexChanged.connect(self.selectData)
87
88        #self.closeEvent.connect(self.closeEvent)
89        self.currentChanged.connect(self.onTabSwitch)
90        self.communicator = self.parent.communicator()
91        self.communicator.fileReadSignal.connect(self.loadFromURL)
92        self.communicator.activeGraphsSignal.connect(self.updateGraphCount)
93        self.communicator.activeGraphName.connect(self.updatePlotName)
94        self.communicator.plotUpdateSignal.connect(self.updatePlot)
95
96        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
97        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
98
99        # Proxy model for showing a subset of Data1D/Data2D content
100        self.data_proxy = QtCore.QSortFilterProxyModel(self)
101        self.data_proxy.setSourceModel(self.model)
102
103        # Don't show "empty" rows with data objects
104        self.data_proxy.setFilterRegExp(r"[^()]")
105
106        # The Data viewer is QTreeView showing the proxy model
107        self.treeView.setModel(self.data_proxy)
108
109        # Proxy model for showing a subset of Theory content
110        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
111        self.theory_proxy.setSourceModel(self.theory_model)
112
113        # Don't show "empty" rows with data objects
114        self.theory_proxy.setFilterRegExp(r"[^()]")
115
116        # Theory model view
117        self.freezeView.setModel(self.theory_proxy)
118
119        self.enableGraphCombo(None)
120
121        # Current view on model
122        self.current_view = self.treeView
123
124    def closeEvent(self, event):
125        """
126        Overwrite the close event - no close!
127        """
128        event.ignore()
129
130    def onTabSwitch(self, index):
131        """ Callback for tab switching signal """
132        if index == 0:
133            self.current_view = self.treeView
134        else:
135            self.current_view = self.freezeView
136
137    def displayHelp(self):
138        """
139        Show the "Loading data" section of help
140        """
141        tree_location = GuiUtils.HELP_DIRECTORY_LOCATION +\
142            "/user/sasgui/guiframe/data_explorer_help.html"
143        self._helpView.load(QtCore.QUrl(tree_location))
144        self._helpView.show()
145
146    def enableGraphCombo(self, combo_text):
147        """
148        Enables/disables "Assign Plot" elements
149        """
150        self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
151        self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
152
153    def initPerspectives(self):
154        """
155        Populate the Perspective combobox and define callbacks
156        """
157        available_perspectives = sorted([p for p in list(Perspectives.PERSPECTIVES.keys())])
158        if available_perspectives:
159            self.cbFitting.clear()
160            self.cbFitting.addItems(available_perspectives)
161        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
162        # Set the index so we see the default (Fitting)
163        self.updatePerspectiveCombo(0)
164
165    def _perspective(self):
166        """
167        Returns the current perspective
168        """
169        return self.parent.perspective()
170
171    def loadFromURL(self, url):
172        """
173        Threaded file load
174        """
175        load_thread = threads.deferToThread(self.readData, url)
176        load_thread.addCallback(self.loadComplete)
177        load_thread.addErrback(self.loadFailed)
178
179    def loadFile(self, event=None):
180        """
181        Called when the "Load" button pressed.
182        Opens the Qt "Open File..." dialog
183        """
184        path_str = self.chooseFiles()
185        if not path_str:
186            return
187        self.loadFromURL(path_str)
188
189    def loadFolder(self, event=None):
190        """
191        Called when the "File/Load Folder" menu item chosen.
192        Opens the Qt "Open Folder..." dialog
193        """
194        folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
195              QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
196        if folder is None:
197            return
198
199        folder = str(folder)
200
201        if not os.path.isdir(folder):
202            return
203
204        # get content of dir into a list
205        path_str = [os.path.join(os.path.abspath(folder), filename)
206                    for filename in os.listdir(folder)]
207
208        self.loadFromURL(path_str)
209
210    def loadProject(self):
211        """
212        Called when the "Open Project" menu item chosen.
213        """
214        kwargs = {
215            'parent'    : self,
216            'caption'   : 'Open Project',
217            'filter'    : 'Project (*.json);;All files (*.*)',
218            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
219        }
220        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
221        if filename:
222            load_thread = threads.deferToThread(self.readProject, filename)
223            load_thread.addCallback(self.readProjectComplete)
224            load_thread.addErrback(self.readProjectFailed)
225
226    def loadFailed(self, reason):
227        """
228        """
229        print("file load FAILED: ", reason)
230        pass
231
232    def readProjectFailed(self, reason):
233        """
234        """
235        print("readProjectFailed FAILED: ", reason)
236        pass
237
238    def readProject(self, filename):
239        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
240        try:
241            manager = DataManager()
242            with open(filename, 'r') as infile:
243                manager.load_from_readable(infile)
244
245            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
246            return manager
247
248        except:
249            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
250            raise
251
252    def readProjectComplete(self, manager):
253        self.model.clear()
254
255        self.manager.assign(manager)
256        self.model.beginResetModel()
257        for id, item in self.manager.get_all_data().items():
258            self.updateModel(item.data, item.path)
259
260        self.model.endResetModel()
261
262    def saveProject(self):
263        """
264        Called when the "Save Project" menu item chosen.
265        """
266        kwargs = {
267            'parent'    : self,
268            'caption'   : 'Save Project',
269            'filter'    : 'Project (*.json)',
270            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
271        }
272        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
273        filename = name_tuple[0]
274        if filename:
275            _, extension = os.path.splitext(filename)
276            if not extension:
277                filename = '.'.join((filename, 'json'))
278            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
279            with open(filename, 'w') as outfile:
280                self.manager.save_to_writable(outfile)
281
282    def deleteFile(self, event):
283        """
284        Delete selected rows from the model
285        """
286        # Assure this is indeed wanted
287        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
288                     "\nDo you want to continue?"
289        reply = QtWidgets.QMessageBox.question(self,
290                                           'Warning',
291                                           delete_msg,
292                                           QtWidgets.QMessageBox.Yes,
293                                           QtWidgets.QMessageBox.No)
294
295        if reply == QtWidgets.QMessageBox.No:
296            return
297
298        # Figure out which rows are checked
299        ind = -1
300        # Use 'while' so the row count is forced at every iteration
301        deleted_indices = []
302        deleted_names = []
303        while ind < self.model.rowCount():
304            ind += 1
305            item = self.model.item(ind)
306
307            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
308                # Delete these rows from the model
309                deleted_names.append(str(self.model.item(ind).text()))
310                deleted_indices.append(item)
311
312                self.model.removeRow(ind)
313                # Decrement index since we just deleted it
314                ind -= 1
315
316        # Let others know we deleted data
317        self.communicator.dataDeletedSignal.emit(deleted_indices)
318
319        # update stored_data
320        self.manager.update_stored_data(deleted_names)
321
322    def deleteTheory(self, event):
323        """
324        Delete selected rows from the theory model
325        """
326        # Assure this is indeed wanted
327        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
328                     "\nDo you want to continue?"
329        reply = QtWidgets.QMessageBox.question(self,
330                                           'Warning',
331                                           delete_msg,
332                                           QtWidgets.QMessageBox.Yes,
333                                           QtWidgets.QMessageBox.No)
334
335        if reply == QtWidgets.QMessageBox.No:
336            return
337
338        # Figure out which rows are checked
339        ind = -1
340        # Use 'while' so the row count is forced at every iteration
341        while ind < self.theory_model.rowCount():
342            ind += 1
343            item = self.theory_model.item(ind)
344            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
345                # Delete these rows from the model
346                self.theory_model.removeRow(ind)
347                # Decrement index since we just deleted it
348                ind -= 1
349
350        # pass temporarily kept as a breakpoint anchor
351        pass
352
353    def sendData(self, event):
354        """
355        Send selected item data to the current perspective and set the relevant notifiers
356        """
357        # Set the signal handlers
358        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
359
360        def isItemReady(index):
361            item = self.model.item(index)
362            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
363
364        # Figure out which rows are checked
365        selected_items = [self.model.item(index)
366                          for index in range(self.model.rowCount())
367                          if isItemReady(index)]
368
369        if len(selected_items) < 1:
370            return
371
372        # Which perspective has been selected?
373        if len(selected_items) > 1 and not self._perspective().allowBatch():
374            msg = self._perspective().title() + " does not allow multiple data."
375            msgbox = QtWidgets.QMessageBox()
376            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
377            msgbox.setText(msg)
378            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
379            retval = msgbox.exec_()
380            return
381
382        # Notify the GuiManager about the send request
383        self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
384
385    def freezeTheory(self, event):
386        """
387        Freeze selected theory rows.
388
389        "Freezing" means taking the plottable data from the Theory item
390        and copying it to a separate top-level item in Data.
391        """
392        # Figure out which rows are checked
393        # Use 'while' so the row count is forced at every iteration
394        outer_index = -1
395        theories_copied = 0
396        while outer_index < self.theory_model.rowCount():
397            outer_index += 1
398            outer_item = self.theory_model.item(outer_index)
399            if not outer_item:
400                continue
401            if outer_item.isCheckable() and \
402                   outer_item.checkState() == QtCore.Qt.Checked:
403                self.model.beginResetModel()
404                theories_copied += 1
405                new_item = self.recursivelyCloneItem(outer_item)
406                # Append a "unique" descriptor to the name
407                time_bit = str(time.time())[7:-1].replace('.', '')
408                new_name = new_item.text() + '_@' + time_bit
409                new_item.setText(new_name)
410                self.model.appendRow(new_item)
411                self.model.endResetModel()
412            #self.model.reset()
413
414        freeze_msg = ""
415        if theories_copied == 0:
416            return
417        elif theories_copied == 1:
418            freeze_msg = "1 theory copied from the Theory tab as a data set"
419        elif theories_copied > 1:
420            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
421        else:
422            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
423            raise AttributeError(freeze_msg)
424        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
425        # Actively switch tabs
426        self.setCurrentIndex(1)
427
428    def recursivelyCloneItem(self, item):
429        """
430        Clone QStandardItem() object
431        """
432        new_item = item.clone()
433        # clone doesn't do deepcopy :(
434        for child_index in range(item.rowCount()):
435            child_item = self.recursivelyCloneItem(item.child(child_index))
436            new_item.setChild(child_index, child_item)
437        return new_item
438
439    def updatePlotName(self, name_tuple):
440        """
441        Modify the name of the current plot
442        """
443        old_name, current_name = name_tuple
444        ind = self.cbgraph.findText(old_name)
445        self.cbgraph.setCurrentIndex(ind)
446        self.cbgraph.setItemText(ind, current_name)
447
448    def updateGraphCount(self, graph_list):
449        """
450        Modify the graph name combo and potentially remove
451        deleted graphs
452        """
453        self.updateGraphCombo(graph_list)
454
455        if not self.active_plots:
456            return
457        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
458        active_plots_copy = list(self.active_plots.keys())
459        for plot in active_plots_copy:
460            if self.active_plots[plot] in new_plots:
461                continue
462            self.active_plots.pop(plot)
463
464    def updateGraphCombo(self, graph_list):
465        """
466        Modify Graph combo box on graph add/delete
467        """
468        orig_text = self.cbgraph.currentText()
469        self.cbgraph.clear()
470        self.cbgraph.insertItems(0, graph_list)
471        ind = self.cbgraph.findText(orig_text)
472        if ind > 0:
473            self.cbgraph.setCurrentIndex(ind)
474
475    def updatePerspectiveCombo(self, index):
476        """
477        Notify the gui manager about the new perspective chosen.
478        """
479        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.currentText())
480        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
481
482    def displayData(self, data_list):
483        """
484        Forces display of charts for the given filename
485        """
486        plot_to_show = data_list[0]
487
488        # passed plot is used ONLY to figure out its title,
489        # so all the charts related by it can be pulled from
490        # the data explorer indices.
491        filename = plot_to_show.filename
492        model = self.model if plot_to_show.is_data else self.theory_model
493
494        # Now query the model item for available plots
495        plots = GuiUtils.plotsFromFilename(filename, model)
496        item = GuiUtils.itemFromFilename(filename, model)
497
498        new_plots = []
499        for plot in plots:
500            plot_id = plot.id
501            if plot_id in list(self.active_plots.keys()):
502                self.active_plots[plot_id].replacePlot(plot_id, plot)
503            else:
504                # 'sophisticated' test to generate standalone plot for residuals
505                if 'esiduals' in plot.title:
506                    self.plotData([(item, plot)])
507                else:
508                    new_plots.append((item, plot))
509
510        if new_plots:
511            self.plotData(new_plots)
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
859        # Define the callbacks
860        self.actionDataInfo.triggered.connect(self.showDataInfo)
861        self.actionSaveAs.triggered.connect(self.saveDataAs)
862        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
863        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
864        self.actionEditMask.triggered.connect(self.showEditDataMask)
865
866    def onCustomContextMenu(self, position):
867        """
868        Show the right-click context menu in the data treeview
869        """
870        index = self.current_view.indexAt(position)
871        proxy = self.current_view.model()
872        model = proxy.sourceModel()
873
874        if index.isValid():
875            model_item = model.itemFromIndex(proxy.mapToSource(index))
876            # Find the mapped index
877            orig_index = model_item.isCheckable()
878            if orig_index:
879                # Check the data to enable/disable actions
880                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
881                self.actionQuick3DPlot.setEnabled(is_2D)
882                self.actionEditMask.setEnabled(is_2D)
883                # Fire up the menu
884                self.context_menu.exec_(self.current_view.mapToGlobal(position))
885
886    def showDataInfo(self):
887        """
888        Show a simple read-only text edit with data information.
889        """
890        index = self.current_view.selectedIndexes()[0]
891        proxy = self.current_view.model()
892        model = proxy.sourceModel()
893        model_item = model.itemFromIndex(proxy.mapToSource(index))
894
895        data = GuiUtils.dataFromItem(model_item)
896        if isinstance(data, Data1D):
897            text_to_show = GuiUtils.retrieveData1d(data)
898            # Hardcoded sizes to enable full width rendering with default font
899            self.txt_widget.resize(420,600)
900        else:
901            text_to_show = GuiUtils.retrieveData2d(data)
902            # Hardcoded sizes to enable full width rendering with default font
903            self.txt_widget.resize(700,600)
904
905        self.txt_widget.setReadOnly(True)
906        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
907        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
908        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
909        self.txt_widget.clear()
910        self.txt_widget.insertPlainText(text_to_show)
911
912        self.txt_widget.show()
913        # Move the slider all the way up, if present
914        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
915        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
916
917    def saveDataAs(self):
918        """
919        Save the data points as either txt or xml
920        """
921        index = self.current_view.selectedIndexes()[0]
922        proxy = self.current_view.model()
923        model = proxy.sourceModel()
924        model_item = model.itemFromIndex(proxy.mapToSource(index))
925
926        data = GuiUtils.dataFromItem(model_item)
927        if isinstance(data, Data1D):
928            GuiUtils.saveData1D(data)
929        else:
930            GuiUtils.saveData2D(data)
931
932    def quickDataPlot(self):
933        """
934        Frozen plot - display an image of the plot
935        """
936        index = self.current_view.selectedIndexes()[0]
937        proxy = self.current_view.model()
938        model = proxy.sourceModel()
939        model_item = model.itemFromIndex(proxy.mapToSource(index))
940
941        data = GuiUtils.dataFromItem(model_item)
942
943        method_name = 'Plotter'
944        if isinstance(data, Data2D):
945            method_name='Plotter2D'
946
947        new_plot = globals()[method_name](self, quickplot=True)
948        new_plot.data = data
949        #new_plot.plot(marker='o')
950        new_plot.plot()
951
952        # Update the global plot counter
953        title = "Plot " + data.name
954        new_plot.setWindowTitle(title)
955
956        # Show the plot
957        new_plot.show()
958
959    def quickData3DPlot(self):
960        """
961        Slowish 3D plot
962        """
963        index = self.current_view.selectedIndexes()[0]
964        proxy = self.current_view.model()
965        model = proxy.sourceModel()
966        model_item = model.itemFromIndex(proxy.mapToSource(index))
967
968        data = GuiUtils.dataFromItem(model_item)
969
970        new_plot = Plotter2D(self, quickplot=True, dimension=3)
971        new_plot.data = data
972        new_plot.plot()
973
974        # Update the global plot counter
975        title = "Plot " + data.name
976        new_plot.setWindowTitle(title)
977
978        # Show the plot
979        new_plot.show()
980
981    def showEditDataMask(self):
982        """
983        Mask Editor for 2D plots
984        """
985        index = self.current_view.selectedIndexes()[0]
986        proxy = self.current_view.model()
987        model = proxy.sourceModel()
988        model_item = model.itemFromIndex(proxy.mapToSource(index))
989
990        data = GuiUtils.dataFromItem(model_item)
991
992        mask_editor = MaskEditor(self, data)
993        # Modal dialog here.
994        mask_editor.exec_()
995
996    def loadComplete(self, output):
997        """
998        Post message to status bar and update the data manager
999        """
1000        assert isinstance(output, tuple)
1001
1002        # Reset the model so the view gets updated.
1003        #self.model.reset()
1004        self.communicator.progressBarUpdateSignal.emit(-1)
1005
1006        output_data = output[0]
1007        message = output[1]
1008        # Notify the manager of the new data available
1009        self.communicator.statusBarUpdateSignal.emit(message)
1010        self.communicator.fileDataReceivedSignal.emit(output_data)
1011        self.manager.add_data(data_list=output_data)
1012
1013    def loadFailed(self, reason):
1014        print("File Load Failed with:\n", reason)
1015        pass
1016
1017    def updateModel(self, data, p_file):
1018        """
1019        Add data and Info fields to the model item
1020        """
1021        # Structure of the model
1022        # checkbox + basename
1023        #     |-------> Data.D object
1024        #     |-------> Info
1025        #                 |----> Title:
1026        #                 |----> Run:
1027        #                 |----> Type:
1028        #                 |----> Path:
1029        #                 |----> Process
1030        #                          |-----> process[0].name
1031        #     |-------> THEORIES
1032
1033        # Top-level item: checkbox with label
1034        checkbox_item = QtGui.QStandardItem(True)
1035        checkbox_item.setCheckable(True)
1036        checkbox_item.setCheckState(QtCore.Qt.Checked)
1037        checkbox_item.setText(os.path.basename(p_file))
1038
1039        # Add the actual Data1D/Data2D object
1040        object_item = QtGui.QStandardItem()
1041        object_item.setData(data)
1042
1043        checkbox_item.setChild(0, object_item)
1044
1045        # Add rows for display in the view
1046        info_item = GuiUtils.infoFromData(data)
1047
1048        # Set info_item as the first child
1049        checkbox_item.setChild(1, info_item)
1050
1051        # Caption for the theories
1052        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1053
1054        # New row in the model
1055        self.model.beginResetModel()
1056        self.model.appendRow(checkbox_item)
1057        self.model.endResetModel()
1058
1059    def updateModelFromPerspective(self, model_item):
1060        """
1061        Receive an update model item from a perspective
1062        Make sure it is valid and if so, replace it in the model
1063        """
1064        # Assert the correct type
1065        if not isinstance(model_item, QtGui.QStandardItem):
1066            msg = "Wrong data type returned from calculations."
1067            raise AttributeError(msg)
1068
1069        # TODO: Assert other properties
1070
1071        # Reset the view
1072        ##self.model.reset()
1073        # Pass acting as a debugger anchor
1074        pass
1075
1076    def updateTheoryFromPerspective(self, model_item):
1077        """
1078        Receive an update theory item from a perspective
1079        Make sure it is valid and if so, replace/add in the model
1080        """
1081        # Assert the correct type
1082        if not isinstance(model_item, QtGui.QStandardItem):
1083            msg = "Wrong data type returned from calculations."
1084            raise AttributeError(msg)
1085
1086        # Check if there are any other items for this tab
1087        # If so, delete them
1088        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1089        #
1090        ##self.model.beginResetModel()
1091        ##current_tab_name = model_item.text()[:2]
1092        ##for current_index in range(self.theory_model.rowCount()):
1093            #if current_tab_name in self.theory_model.item(current_index).text():
1094            #    return
1095        ##        self.theory_model.removeRow(current_index)
1096        ##        break
1097
1098        ### Reset the view
1099        ##self.model.endResetModel()
1100
1101        # Reset the view
1102        self.theory_model.appendRow(model_item)
1103
1104        # Pass acting as a debugger anchor
1105        pass
1106
1107
1108if __name__ == "__main__":
1109    app = QtWidgets.QApplication([])
1110    dlg = DataExplorerWindow()
1111    dlg.show()
1112    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.