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

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

More Qt5 related fixes.

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