source: sasview/src/sas/qtgui/DataExplorer.py @ 253e7170

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 253e7170 was 253e7170, checked in by wojciech, 7 years ago

Loading data using simple loader

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