source: sasview/src/sas/qtgui/DataExplorer.py @ 83d6249

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

Perspectives are now switchable and can be added "dynamically"

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