source: sasview/src/sas/qtgui/DataExplorer.py @ 8696721

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 8696721 was 8696721, checked in by wojciech, 8 years ago

Merged with ESS_GUI

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