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

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

Code review fixes

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