source: sasview/src/sas/qtgui/DataExplorer.py @ 3bdbfcc

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

Reimplementation of the slicer functionality

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