source: sasview/src/sas/qtgui/DataExplorer.py @ 3bdbfcc

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

Reimplementation of the slicer functionality

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