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

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

Compute/Show? Plot button logic: SASVIEW-271
Unit tests for plotting in fitting: SASVIEW-501

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