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

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

Slightly modified behaviour of "Show Plot" to correspond to 4.x SASVIEW-588

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