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

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

Added window title GUI for charts

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