source: sasview/src/sas/qtgui/DataExplorer.py @ 0268aed

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

Plotting residuals in fitting.
PlotHelper? updates.
Minor refactoring.

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