source: sasview/src/sas/qtgui/DataExplorer.py @ 87cc73a

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

Added Modify Plot Properties functionality. SASVIEW-169

  • 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):
402            plot2D = Plotter2D(self)
403            plot2D.plot(plot_set)
404            self.plotAdd(plot2D)
405
406        for plot_set in plots:
407            if isinstance(plot_set, Data1D):
408                new_plot.plot(plot_set)
409            elif isinstance(plot_set, Data2D):
410                addDataPlot2D(plot_set)
411            else:
412                msg = "Incorrect data type passed to Plotting"
413                raise AttributeError, msg
414
415        if plots and \
416            hasattr(new_plot, 'data') and \
417            isinstance(new_plot.data, Data1D):
418                self.plotAdd(new_plot)
419
420    def plotAdd(self, new_plot):
421        """
422        Helper method for plot bookkeeping
423        """
424        # Update the global plot counter
425        title = "Graph"+str(PlotHelper.idOfPlot(new_plot))
426        new_plot.setWindowTitle(title)
427
428        # Add the plot to the workspace
429        self.parent.workspace().addWindow(new_plot)
430
431        # Show the plot
432        new_plot.show()
433
434        # Update the active chart list
435        self.active_plots.append(title)
436
437    def appendPlot(self):
438        """
439        Add data set(s) to the existing matplotlib chart
440        """
441        # new plot data
442        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
443
444        # old plot data
445        plot_id = self.cbgraph.currentIndex() + 1
446
447        assert plot_id in PlotHelper.currentPlots(), "No such plot: Graph%s"%str(plot_id)
448
449        old_plot = PlotHelper.plotById(plot_id)
450
451        # Add new data to the old plot, if data type is the same.
452        for plot_set in new_plots:
453            if type(plot_set) is type(old_plot._data):
454                old_plot.data = plot_set
455                old_plot.plot()
456
457    def chooseFiles(self):
458        """
459        Shows the Open file dialog and returns the chosen path(s)
460        """
461        # List of known extensions
462        wlist = self.getWlist()
463
464        # Location is automatically saved - no need to keep track of the last dir
465        # But only with Qt built-in dialog (non-platform native)
466        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
467                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
468        if paths is None:
469            return
470
471        if isinstance(paths, QtCore.QStringList):
472            paths = [str(f) for f in paths]
473
474        if not isinstance(paths, list):
475            paths = [paths]
476
477        return paths
478
479    def readData(self, path):
480        """
481        verbatim copy-paste from
482           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
483        slightly modified for clarity
484        """
485        message = ""
486        log_msg = ''
487        output = {}
488        any_error = False
489        data_error = False
490        error_message = ""
491        number_of_files = len(path)
492        self.communicator.progressBarUpdateSignal.emit(0.0)
493
494        for index, p_file in enumerate(path):
495            basename = os.path.basename(p_file)
496            _, extension = os.path.splitext(basename)
497            if extension.lower() in GuiUtils.EXTENSIONS:
498                any_error = True
499                log_msg = "Data Loader cannot "
500                log_msg += "load: %s\n" % str(p_file)
501                log_msg += """Please try to open that file from "open project" """
502                log_msg += """or "open analysis" menu\n"""
503                error_message = log_msg + "\n"
504                logging.info(log_msg)
505                continue
506
507            try:
508                message = "Loading Data... " + str(basename) + "\n"
509
510                # change this to signal notification in GuiManager
511                self.communicator.statusBarUpdateSignal.emit(message)
512
513                output_objects = self.loader.load(p_file)
514
515                # Some loaders return a list and some just a single Data1D object.
516                # Standardize.
517                if not isinstance(output_objects, list):
518                    output_objects = [output_objects]
519
520                for item in output_objects:
521                    # cast sascalc.dataloader.data_info.Data1D into
522                    # sasgui.guiframe.dataFitting.Data1D
523                    # TODO : Fix it
524                    new_data = self.manager.create_gui_data(item, p_file)
525                    output[new_data.id] = new_data
526
527                    # Model update should be protected
528                    self.mutex.lock()
529                    self.updateModel(new_data, p_file)
530                    self.model.reset()
531                    QtGui.qApp.processEvents()
532                    self.mutex.unlock()
533
534                    if hasattr(item, 'errors'):
535                        for error_data in item.errors:
536                            data_error = True
537                            message += "\tError: {0}\n".format(error_data)
538                    else:
539
540                        logging.error("Loader returned an invalid object:\n %s" % str(item))
541                        data_error = True
542
543            except Exception as ex:
544                logging.error(sys.exc_value)
545
546                any_error = True
547            if any_error or error_message != "":
548                if error_message == "":
549                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
550                    error += "while loading Data: \n%s\n" % str(basename)
551                    error_message += "The data file you selected could not be loaded.\n"
552                    error_message += "Make sure the content of your file"
553                    error_message += " is properly formatted.\n\n"
554                    error_message += "When contacting the SasView team, mention the"
555                    error_message += " following:\n%s" % str(error)
556                elif data_error:
557                    base_message = "Errors occurred while loading "
558                    base_message += "{0}\n".format(basename)
559                    base_message += "The data file loaded but with errors.\n"
560                    error_message = base_message + error_message
561                else:
562                    error_message += "%s\n" % str(p_file)
563
564            current_percentage = int(100.0* index/number_of_files)
565            self.communicator.progressBarUpdateSignal.emit(current_percentage)
566
567        if any_error or error_message:
568            logging.error(error_message)
569            status_bar_message = "Errors occurred while loading %s" % format(basename)
570            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
571
572        else:
573            message = "Loading Data Complete! "
574        message += log_msg
575        # Notify the progress bar that the updates are over.
576        self.communicator.progressBarUpdateSignal.emit(-1)
577
578        return output, message
579
580    def getWlist(self):
581        """
582        Wildcards of files we know the format of.
583        """
584        # Display the Qt Load File module
585        cards = self.loader.get_wildcards()
586
587        # get rid of the wx remnant in wildcards
588        # TODO: modify sasview loader get_wildcards method, after merge,
589        # so this kludge can be avoided
590        new_cards = []
591        for item in cards:
592            new_cards.append(item[:item.find("|")])
593        wlist = ';;'.join(new_cards)
594
595        return wlist
596
597    def selectData(self, index):
598        """
599        Callback method for modifying the TreeView on Selection Options change
600        """
601        if not isinstance(index, int):
602            msg = "Incorrect type passed to DataExplorer.selectData()"
603            raise AttributeError, msg
604
605        # Respond appropriately
606        if index == 0:
607            # Select All
608            for index in range(self.model.rowCount()):
609                item = self.model.item(index)
610                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
611                    item.setCheckState(QtCore.Qt.Checked)
612        elif index == 1:
613            # De-select All
614            for index in range(self.model.rowCount()):
615                item = self.model.item(index)
616                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
617                    item.setCheckState(QtCore.Qt.Unchecked)
618
619        elif index == 2:
620            # Select All 1-D
621            for index in range(self.model.rowCount()):
622                item = self.model.item(index)
623                item.setCheckState(QtCore.Qt.Unchecked)
624
625                try:
626                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
627                except AttributeError:
628                    msg = "Bad structure of the data model."
629                    raise RuntimeError, msg
630
631                if is1D:
632                    item.setCheckState(QtCore.Qt.Checked)
633
634        elif index == 3:
635            # Unselect All 1-D
636            for index in range(self.model.rowCount()):
637                item = self.model.item(index)
638
639                try:
640                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
641                except AttributeError:
642                    msg = "Bad structure of the data model."
643                    raise RuntimeError, msg
644
645                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
646                    item.setCheckState(QtCore.Qt.Unchecked)
647
648        elif index == 4:
649            # Select All 2-D
650            for index in range(self.model.rowCount()):
651                item = self.model.item(index)
652                item.setCheckState(QtCore.Qt.Unchecked)
653                try:
654                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
655                except AttributeError:
656                    msg = "Bad structure of the data model."
657                    raise RuntimeError, msg
658
659                if is2D:
660                    item.setCheckState(QtCore.Qt.Checked)
661
662        elif index == 5:
663            # Unselect All 2-D
664            for index in range(self.model.rowCount()):
665                item = self.model.item(index)
666
667                try:
668                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
669                except AttributeError:
670                    msg = "Bad structure of the data model."
671                    raise RuntimeError, msg
672
673                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
674                    item.setCheckState(QtCore.Qt.Unchecked)
675
676        else:
677            msg = "Incorrect value in the Selection Option"
678            # Change this to a proper logging action
679            raise Exception, msg
680
681    def contextMenu(self):
682        """
683        Define actions and layout of the right click context menu
684        """
685        # Create a custom menu based on actions defined in the UI file
686        self.context_menu = QtGui.QMenu(self)
687        self.context_menu.addAction(self.actionDataInfo)
688        self.context_menu.addAction(self.actionSaveAs)
689        self.context_menu.addAction(self.actionQuickPlot)
690        self.context_menu.addSeparator()
691        self.context_menu.addAction(self.actionQuick3DPlot)
692        self.context_menu.addAction(self.actionEditMask)
693
694        # Define the callbacks
695        self.actionDataInfo.triggered.connect(self.showDataInfo)
696        self.actionSaveAs.triggered.connect(self.saveDataAs)
697        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
698        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
699        self.actionEditMask.triggered.connect(self.showEditDataMask)
700
701    def onCustomContextMenu(self, position):
702        """
703        Show the right-click context menu in the data treeview
704        """
705        index = self.treeView.indexAt(position)
706        if index.isValid():
707            model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
708            # Find the mapped index
709            orig_index = model_item.isCheckable()
710            if orig_index:
711                # Check the data to enable/disable actions
712                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
713                self.actionQuick3DPlot.setEnabled(is_2D)
714                self.actionEditMask.setEnabled(is_2D)
715                # Fire up the menu
716                self.context_menu.exec_(self.treeView.mapToGlobal(position))
717
718    def showDataInfo(self):
719        """
720        Show a simple read-only text edit with data information.
721        """
722        index = self.treeView.selectedIndexes()[0]
723        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
724        data = GuiUtils.dataFromItem(model_item)
725        if isinstance(data, Data1D):
726            text_to_show = GuiUtils.retrieveData1d(data)
727            # Hardcoded sizes to enable full width rendering with default font
728            self.txt_widget.resize(420,600)
729        else:
730            text_to_show = GuiUtils.retrieveData2d(data)
731            # Hardcoded sizes to enable full width rendering with default font
732            self.txt_widget.resize(700,600)
733
734        self.txt_widget.setReadOnly(True)
735        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
736        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
737        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
738        self.txt_widget.insertPlainText(text_to_show)
739
740        self.txt_widget.show()
741        # Move the slider all the way up, if present
742        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
743        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
744
745    def saveDataAs(self):
746        """
747        Save the data points as either txt or xml
748        """
749        index = self.treeView.selectedIndexes()[0]
750        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
751        data = GuiUtils.dataFromItem(model_item)
752        if isinstance(data, Data1D):
753            GuiUtils.saveData1D(data)
754        else:
755            GuiUtils.saveData2D(data)
756
757    def quickDataPlot(self):
758        """
759        Frozen plot - display an image of the plot
760        """
761        index = self.treeView.selectedIndexes()[0]
762        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
763        data = GuiUtils.dataFromItem(model_item)
764
765        method_name = 'Plotter'
766        if isinstance(data, Data2D):
767            method_name='Plotter2D'
768
769        new_plot = globals()[method_name](self, quickplot=True)
770        new_plot.data = data
771        new_plot.plot(marker='o')
772
773        # Update the global plot counter
774        title = "Plot " + data.name
775        new_plot.setWindowTitle(title)
776
777        # Show the plot
778        new_plot.show()
779
780    def quickData3DPlot(self):
781        """
782        Slowish 3D plot
783        """
784        index = self.treeView.selectedIndexes()[0]
785        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
786        data = GuiUtils.dataFromItem(model_item)
787
788        new_plot = Plotter2D(self, quickplot=True, dimension=3)
789        new_plot.data = data
790        new_plot.plot(marker='o', linestyle='')
791
792        # Update the global plot counter
793        title = "Plot " + data.name
794        new_plot.setWindowTitle(title)
795
796        # Show the plot
797        new_plot.show()
798
799    def showEditDataMask(self):
800        """
801        Mask Editor for 2D plots
802        """
803        index = self.treeView.selectedIndexes()[0]
804        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
805        data = GuiUtils.dataFromItem(model_item)
806
807        mask_editor = MaskEditor(self, data)
808        # Modal dialog here.
809        mask_editor.exec_()
810
811    def loadComplete(self, output):
812        """
813        Post message to status bar and update the data manager
814        """
815        assert isinstance(output, tuple)
816
817        # Reset the model so the view gets updated.
818        self.model.reset()
819        self.communicator.progressBarUpdateSignal.emit(-1)
820
821        output_data = output[0]
822        message = output[1]
823        # Notify the manager of the new data available
824        self.communicator.statusBarUpdateSignal.emit(message)
825        self.communicator.fileDataReceivedSignal.emit(output_data)
826        self.manager.add_data(data_list=output_data)
827
828    def updateModel(self, data, p_file):
829        """
830        Add data and Info fields to the model item
831        """
832        # Structure of the model
833        # checkbox + basename
834        #     |-------> Data.D object
835        #     |-------> Info
836        #                 |----> Title:
837        #                 |----> Run:
838        #                 |----> Type:
839        #                 |----> Path:
840        #                 |----> Process
841        #                          |-----> process[0].name
842        #     |-------> THEORIES
843
844        # Top-level item: checkbox with label
845        checkbox_item = QtGui.QStandardItem(True)
846        checkbox_item.setCheckable(True)
847        checkbox_item.setCheckState(QtCore.Qt.Checked)
848        checkbox_item.setText(os.path.basename(p_file))
849
850        # Add the actual Data1D/Data2D object
851        object_item = QtGui.QStandardItem()
852        object_item.setData(QtCore.QVariant(data))
853
854        checkbox_item.setChild(0, object_item)
855
856        # Add rows for display in the view
857        info_item = GuiUtils.infoFromData(data)
858
859        # Set info_item as the first child
860        checkbox_item.setChild(1, info_item)
861
862        # Caption for the theories
863        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
864
865        # New row in the model
866        self.model.appendRow(checkbox_item)
867
868
869    def updateModelFromPerspective(self, model_item):
870        """
871        Receive an update model item from a perspective
872        Make sure it is valid and if so, replace it in the model
873        """
874        # Assert the correct type
875        if not isinstance(model_item, QtGui.QStandardItem):
876            msg = "Wrong data type returned from calculations."
877            raise AttributeError, msg
878
879        # TODO: Assert other properties
880
881        # Reset the view
882        self.model.reset()
883
884        # Pass acting as a debugger anchor
885        pass
886
887
888if __name__ == "__main__":
889    app = QtGui.QApplication([])
890    dlg = DataExplorerWindow()
891    dlg.show()
892    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.