source: sasview/src/sas/qtgui/DataExplorer.py @ 9290b1a

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

Added AddText? to plot, enabled legend drag - SASVIEW-378

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