source: sasview/src/sas/qtgui/DataExplorer.py @ 0ba0774

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