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

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

Minor performance impromevements in DE

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