source: sasview/src/sas/qtgui/DataExplorer.py @ 71361f0

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

tweaked Data Explorer UI, added prototype/test silx 2D plotting

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