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

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

Some improvements in plot handling

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