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

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

Show help pages in default browser. Fixed some help links and modified unit tests. SASVIEW-800

  • Property mode set to 100644
File size: 40.9 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.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        self.context_menu.addSeparator()
855        self.context_menu.addAction(self.actionDelete)
856
857
858        # Define the callbacks
859        self.actionDataInfo.triggered.connect(self.showDataInfo)
860        self.actionSaveAs.triggered.connect(self.saveDataAs)
861        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
862        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
863        self.actionEditMask.triggered.connect(self.showEditDataMask)
864        self.actionDelete.triggered.connect(self.deleteItem)
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 deleteItem(self):
997        """
998        Delete the current item
999        """
1000        # Assure this is indeed wanted
1001        delete_msg = "This operation will delete the selected data sets." +\
1002                     "\nDo you want to continue?"
1003        reply = QtWidgets.QMessageBox.question(self,
1004                                           'Warning',
1005                                           delete_msg,
1006                                           QtWidgets.QMessageBox.Yes,
1007                                           QtWidgets.QMessageBox.No)
1008
1009        if reply == QtWidgets.QMessageBox.No:
1010            return
1011
1012        indices = self.current_view.selectedIndexes()
1013        proxy = self.current_view.model()
1014        model = proxy.sourceModel()
1015
1016        for index in indices:
1017            row_index = proxy.mapToSource(index)
1018            item_to_delete = model.itemFromIndex(row_index)
1019            if item_to_delete and item_to_delete.isCheckable():
1020                row = row_index.row()
1021                if item_to_delete.parent():
1022                    # We have a child item - delete from it
1023                    item_to_delete.parent().removeRow(row)
1024                else:
1025                    # delete directly from model
1026                    model.removeRow(row)
1027        pass
1028
1029    def loadComplete(self, output):
1030        """
1031        Post message to status bar and update the data manager
1032        """
1033        assert isinstance(output, tuple)
1034
1035        # Reset the model so the view gets updated.
1036        #self.model.reset()
1037        self.communicator.progressBarUpdateSignal.emit(-1)
1038
1039        output_data = output[0]
1040        message = output[1]
1041        # Notify the manager of the new data available
1042        self.communicator.statusBarUpdateSignal.emit(message)
1043        self.communicator.fileDataReceivedSignal.emit(output_data)
1044        self.manager.add_data(data_list=output_data)
1045
1046    def loadFailed(self, reason):
1047        print("File Load Failed with:\n", reason)
1048        pass
1049
1050    def updateModel(self, data, p_file):
1051        """
1052        Add data and Info fields to the model item
1053        """
1054        # Structure of the model
1055        # checkbox + basename
1056        #     |-------> Data.D object
1057        #     |-------> Info
1058        #                 |----> Title:
1059        #                 |----> Run:
1060        #                 |----> Type:
1061        #                 |----> Path:
1062        #                 |----> Process
1063        #                          |-----> process[0].name
1064        #     |-------> THEORIES
1065
1066        # Top-level item: checkbox with label
1067        checkbox_item = GuiUtils.HashableStandardItem()
1068        checkbox_item.setCheckable(True)
1069        checkbox_item.setCheckState(QtCore.Qt.Checked)
1070        checkbox_item.setText(os.path.basename(p_file))
1071
1072        # Add the actual Data1D/Data2D object
1073        object_item = GuiUtils.HashableStandardItem()
1074        object_item.setData(data)
1075
1076        checkbox_item.setChild(0, object_item)
1077
1078        # Add rows for display in the view
1079        info_item = GuiUtils.infoFromData(data)
1080
1081        # Set info_item as the first child
1082        checkbox_item.setChild(1, info_item)
1083
1084        # Caption for the theories
1085        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1086
1087        # New row in the model
1088        self.model.beginResetModel()
1089        self.model.appendRow(checkbox_item)
1090        self.model.endResetModel()
1091
1092    def updateModelFromPerspective(self, model_item):
1093        """
1094        Receive an update model item from a perspective
1095        Make sure it is valid and if so, replace it in the model
1096        """
1097        # Assert the correct type
1098        if not isinstance(model_item, QtGui.QStandardItem):
1099            msg = "Wrong data type returned from calculations."
1100            raise AttributeError(msg)
1101
1102        # TODO: Assert other properties
1103
1104        # Reset the view
1105        ##self.model.reset()
1106        # Pass acting as a debugger anchor
1107        pass
1108
1109    def updateTheoryFromPerspective(self, model_item):
1110        """
1111        Receive an update theory item from a perspective
1112        Make sure it is valid and if so, replace/add in the model
1113        """
1114        # Assert the correct type
1115        if not isinstance(model_item, QtGui.QStandardItem):
1116            msg = "Wrong data type returned from calculations."
1117            raise AttributeError(msg)
1118
1119        # Check if there are any other items for this tab
1120        # If so, delete them
1121        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1122        #
1123        ##self.model.beginResetModel()
1124        ##current_tab_name = model_item.text()[:2]
1125        ##for current_index in range(self.theory_model.rowCount()):
1126            #if current_tab_name in self.theory_model.item(current_index).text():
1127            #    return
1128        ##        self.theory_model.removeRow(current_index)
1129        ##        break
1130
1131        ### Reset the view
1132        ##self.model.endResetModel()
1133
1134        # Reset the view
1135        self.theory_model.appendRow(model_item)
1136
1137        # Pass acting as a debugger anchor
1138        pass
1139
1140
1141if __name__ == "__main__":
1142    app = QtWidgets.QApplication([])
1143    dlg = DataExplorerWindow()
1144    dlg.show()
1145    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.