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

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

Merged with ESS_GUI

  • Property mode set to 100644
File size: 32.2 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)
46        # self.txt_widget = GuiUtils.DisplayWindow()
47
[f721030]48
[481ff26]49        # Be careful with twisted threads.
50        self.mutex = QMutex()
51
[8cb6cd6]52        # Active plots
53        self.active_plots = []
54
[f721030]55        # Connect the buttons
56        self.cmdLoad.clicked.connect(self.loadFile)
[f82ab8c]57        self.cmdDeleteData.clicked.connect(self.deleteFile)
58        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
59        self.cmdFreeze.clicked.connect(self.freezeTheory)
[f721030]60        self.cmdSendTo.clicked.connect(self.sendData)
[1042dba]61        self.cmdNew.clicked.connect(self.newPlot)
[8cb6cd6]62        self.cmdAppend.clicked.connect(self.appendPlot)
[481ff26]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()
[f721030]68
[e540cd2]69        # Custom context menu
70        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
71        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
[4b71e91]72        self.contextMenu()
[e540cd2]73
[488c49d]74        # Connect the comboboxes
75        self.cbSelect.currentIndexChanged.connect(self.selectData)
76
[f82ab8c]77        #self.closeEvent.connect(self.closeEvent)
78        # self.aboutToQuit.connect(self.closeEvent)
[e540cd2]79        self.communicator = self.parent.communicator()
[f82ab8c]80        self.communicator.fileReadSignal.connect(self.loadFromURL)
[8cb6cd6]81        self.communicator.activeGraphsSignal.connect(self.updateGraphCombo)
[27313b7]82        self.communicator.activeGraphName.connect(self.updatePlotName)
[8cb6cd6]83        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
84        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
[f721030]85
86        # Proxy model for showing a subset of Data1D/Data2D content
[481ff26]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"[^()]")
[f721030]92
93        # The Data viewer is QTreeView showing the proxy model
[481ff26]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"[^()]")
[f721030]102
[f82ab8c]103        # Theory model view
[481ff26]104        self.freezeView.setModel(self.theory_proxy)
[f82ab8c]105
[8cb6cd6]106        self.enableGraphCombo(None)
107
[f82ab8c]108    def closeEvent(self, event):
109        """
110        Overwrite the close event - no close!
111        """
112        event.ignore()
[1042dba]113
[481ff26]114    def displayHelp(self):
115        """
116        Show the "Loading data" section of help
117        """
[31c5b58]118        tree_location = self.parent.HELP_DIRECTORY_LOCATION +\
119            "/user/sasgui/guiframe/data_explorer_help.html"
120        self._helpView.load(QtCore.QUrl(tree_location))
[481ff26]121        self._helpView.show()
122
[8cb6cd6]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
[f82ab8c]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)
[5032ea68]136
137    def loadFile(self, event=None):
[f721030]138        """
139        Called when the "Load" button pressed.
[481ff26]140        Opens the Qt "Open File..." dialog
[f721030]141        """
142        path_str = self.chooseFiles()
143        if not path_str:
144            return
[f82ab8c]145        self.loadFromURL(path_str)
[f721030]146
[5032ea68]147    def loadFolder(self, event=None):
[f721030]148        """
[5032ea68]149        Called when the "File/Load Folder" menu item chosen.
[481ff26]150        Opens the Qt "Open Folder..." dialog
[f721030]151        """
[481ff26]152        folder = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
[9e426c1]153              QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog)
[481ff26]154        if folder is None:
[5032ea68]155            return
156
[481ff26]157        folder = str(folder)
[f721030]158
[481ff26]159        if not os.path.isdir(folder):
[5032ea68]160            return
[f721030]161
[5032ea68]162        # get content of dir into a list
[481ff26]163        path_str = [os.path.join(os.path.abspath(folder), filename)
[e540cd2]164                    for filename in os.listdir(folder)]
[f721030]165
[f82ab8c]166        self.loadFromURL(path_str)
[5032ea68]167
[630155bd]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
[5032ea68]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?"
[e540cd2]229        reply = QtGui.QMessageBox.question(self,
230                                           'Warning',
231                                           delete_msg,
232                                           QtGui.QMessageBox.Yes,
233                                           QtGui.QMessageBox.No)
[5032ea68]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
[f721030]251        pass
252
[f82ab8c]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?"
[8cb6cd6]260        reply = QtGui.QMessageBox.question(self,
261                                           'Warning',
262                                           delete_msg,
263                                           QtGui.QMessageBox.Yes,
264                                           QtGui.QMessageBox.No)
[f82ab8c]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
[f721030]284    def sendData(self, event):
285        """
[5032ea68]286        Send selected item data to the current perspective and set the relevant notifiers
[f721030]287        """
[5032ea68]288        # should this reside on GuiManager or here?
289        self._perspective = self.parent.perspective()
[f721030]290
[5032ea68]291        # Set the signal handlers
292        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
[f721030]293
[adf81b8]294        def isItemReady(index):
[5032ea68]295            item = self.model.item(index)
[adf81b8]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)]
[f721030]302
[f82ab8c]303        if len(selected_items) < 1:
304            return
305
[f721030]306        # Which perspective has been selected?
[5032ea68]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
[a281ab8]315
[f721030]316        # Notify the GuiManager about the send request
[469b4622]317        self._perspective.setData(data_item=selected_items)
[5032ea68]318
[f82ab8c]319    def freezeTheory(self, event):
320        """
321        Freeze selected theory rows.
322
[ca8b853]323        "Freezing" means taking the plottable data from the Theory item
324        and copying it to a separate top-level item in Data.
[f82ab8c]325        """
[ca8b853]326        # Figure out which rows are checked
[f82ab8c]327        # Use 'while' so the row count is forced at every iteration
328        outer_index = -1
[481ff26]329        theories_copied = 0
[ca8b853]330        while outer_index < self.theory_model.rowCount():
[f82ab8c]331            outer_index += 1
[ca8b853]332            outer_item = self.theory_model.item(outer_index)
[f82ab8c]333            if not outer_item:
334                continue
[ca8b853]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()
[f82ab8c]345
[481ff26]346        freeze_msg = ""
347        if theories_copied == 0:
348            return
349        elif theories_copied == 1:
[ca8b853]350            freeze_msg = "1 theory copied from the Theory tab as a data set"
[481ff26]351        elif theories_copied > 1:
[ca8b853]352            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
[481ff26]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
[f82ab8c]370
[27313b7]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
[8cb6cd6]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 = []
[adf81b8]387        graph_titles= ["Graph"+str(graph) for graph in graph_list]
388
[8cb6cd6]389        self.cbgraph.insertItems(0, graph_titles)
390        ind = self.cbgraph.findText(orig_text)
391        if ind > 0:
392            self.cbgraph.setCurrentIndex(ind)
393        pass
394
[1042dba]395    def newPlot(self):
396        """
397        Create a new matplotlib chart from selected data
398        """
[0cd8612]399        plots = GuiUtils.plotsFromCheckedItems(self.model)
[1042dba]400
401        # Call show on requested plots
[31c5b58]402        # All same-type charts in one plot
403        new_plot = Plotter(self)
[adf81b8]404
405        def addDataPlot(plot, plot_set):
406            plot.data = plot_set
407            plot.plot()
408
409        def addDataPlot2D(plot_set):
410            plot2D = Plotter2D(self)
411            addDataPlot(plot2D, plot_set)
412            self.plotAdd(plot2D)
413
[1042dba]414        for plot_set in plots:
[49e124c]415            if isinstance(plot_set, Data1D):
[adf81b8]416                addDataPlot(new_plot, plot_set)
[49e124c]417            elif isinstance(plot_set, Data2D):
[adf81b8]418                addDataPlot2D(plot_set)
[49e124c]419            else:
420                msg = "Incorrect data type passed to Plotting"
421                raise AttributeError, msg
422
[b4b8589]423
[31c5b58]424        if plots and \
[b4b8589]425            hasattr(new_plot, 'data') and \
426            isinstance(new_plot.data, Data1D):
427                self.plotAdd(new_plot)
[1042dba]428
[31c5b58]429    def plotAdd(self, new_plot):
430        """
431        Helper method for plot bookkeeping
432        """
433        # Update the global plot counter
434        title = "Graph"+str(PlotHelper.idOfPlot(new_plot))
435        new_plot.setWindowTitle(title)
[8cb6cd6]436
[31c5b58]437        # Add the plot to the workspace
438        self.parent.workspace().addWindow(new_plot)
[8cb6cd6]439
[31c5b58]440        # Show the plot
441        new_plot.show()
[f721030]442
[31c5b58]443        # Update the active chart list
444        self.active_plots.append(title)
[8cb6cd6]445
446    def appendPlot(self):
447        """
448        Add data set(s) to the existing matplotlib chart
449        """
450        # new plot data
451        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
452
453        # old plot data
[27313b7]454        plot_id = self.cbgraph.currentIndex() + 1
[8cb6cd6]455
456        assert plot_id in PlotHelper.currentPlots(), "No such plot: Graph%s"%str(plot_id)
457
458        old_plot = PlotHelper.plotById(plot_id)
459
[14d9c7b]460        # Add new data to the old plot, if data type is the same.
[8cb6cd6]461        for plot_set in new_plots:
[14d9c7b]462            if type(plot_set) is type(old_plot._data):
[31c5b58]463                old_plot.data = plot_set
[14d9c7b]464                old_plot.plot()
[8cb6cd6]465
[f721030]466    def chooseFiles(self):
467        """
[5032ea68]468        Shows the Open file dialog and returns the chosen path(s)
[f721030]469        """
470        # List of known extensions
471        wlist = self.getWlist()
472
473        # Location is automatically saved - no need to keep track of the last dir
[5032ea68]474        # But only with Qt built-in dialog (non-platform native)
[9e426c1]475        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
[5032ea68]476                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
[f721030]477        if paths is None:
478            return
479
[481ff26]480        if isinstance(paths, QtCore.QStringList):
[9e426c1]481            paths = [str(f) for f in paths]
482
[0cd8612]483        if not isinstance(paths, list):
[f721030]484            paths = [paths]
485
[9e426c1]486        return paths
[f721030]487
488    def readData(self, path):
489        """
[481ff26]490        verbatim copy-paste from
491           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
[f721030]492        slightly modified for clarity
493        """
494        message = ""
495        log_msg = ''
496        output = {}
497        any_error = False
498        data_error = False
499        error_message = ""
[e540cd2]500        number_of_files = len(path)
501        self.communicator.progressBarUpdateSignal.emit(0.0)
502
503        for index, p_file in enumerate(path):
[f721030]504            basename = os.path.basename(p_file)
505            _, extension = os.path.splitext(basename)
[0cd8612]506            if extension.lower() in GuiUtils.EXTENSIONS:
[f721030]507                any_error = True
508                log_msg = "Data Loader cannot "
509                log_msg += "load: %s\n" % str(p_file)
510                log_msg += """Please try to open that file from "open project" """
511                log_msg += """or "open analysis" menu\n"""
512                error_message = log_msg + "\n"
513                logging.info(log_msg)
514                continue
515
516            try:
[5032ea68]517                message = "Loading Data... " + str(basename) + "\n"
[f721030]518
519                # change this to signal notification in GuiManager
[f82ab8c]520                self.communicator.statusBarUpdateSignal.emit(message)
[f721030]521
522                output_objects = self.loader.load(p_file)
523
524                # Some loaders return a list and some just a single Data1D object.
525                # Standardize.
526                if not isinstance(output_objects, list):
527                    output_objects = [output_objects]
528
529                for item in output_objects:
[481ff26]530                    # cast sascalc.dataloader.data_info.Data1D into
531                    # sasgui.guiframe.dataFitting.Data1D
[f721030]532                    # TODO : Fix it
533                    new_data = self.manager.create_gui_data(item, p_file)
534                    output[new_data.id] = new_data
[481ff26]535
536                    # Model update should be protected
537                    self.mutex.lock()
[f721030]538                    self.updateModel(new_data, p_file)
[5032ea68]539                    self.model.reset()
540                    QtGui.qApp.processEvents()
[481ff26]541                    self.mutex.unlock()
[f721030]542
543                    if hasattr(item, 'errors'):
544                        for error_data in item.errors:
545                            data_error = True
546                            message += "\tError: {0}\n".format(error_data)
547                    else:
[5032ea68]548
[f721030]549                        logging.error("Loader returned an invalid object:\n %s" % str(item))
550                        data_error = True
551
[5032ea68]552            except Exception as ex:
[f721030]553                logging.error(sys.exc_value)
[5032ea68]554
[f721030]555                any_error = True
556            if any_error or error_message != "":
557                if error_message == "":
558                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
559                    error += "while loading Data: \n%s\n" % str(basename)
560                    error_message += "The data file you selected could not be loaded.\n"
561                    error_message += "Make sure the content of your file"
562                    error_message += " is properly formatted.\n\n"
563                    error_message += "When contacting the SasView team, mention the"
564                    error_message += " following:\n%s" % str(error)
565                elif data_error:
566                    base_message = "Errors occurred while loading "
567                    base_message += "{0}\n".format(basename)
568                    base_message += "The data file loaded but with errors.\n"
569                    error_message = base_message + error_message
570                else:
571                    error_message += "%s\n" % str(p_file)
[481ff26]572
[e540cd2]573            current_percentage = int(100.0* index/number_of_files)
574            self.communicator.progressBarUpdateSignal.emit(current_percentage)
575
[f721030]576        if any_error or error_message:
[0cd8612]577            logging.error(error_message)
578            status_bar_message = "Errors occurred while loading %s" % format(basename)
579            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
[f721030]580
581        else:
582            message = "Loading Data Complete! "
583        message += log_msg
[0cd8612]584        # Notify the progress bar that the updates are over.
[e540cd2]585        self.communicator.progressBarUpdateSignal.emit(-1)
[481ff26]586
[a281ab8]587        return output, message
[f721030]588
589    def getWlist(self):
590        """
[f82ab8c]591        Wildcards of files we know the format of.
[f721030]592        """
593        # Display the Qt Load File module
594        cards = self.loader.get_wildcards()
595
596        # get rid of the wx remnant in wildcards
597        # TODO: modify sasview loader get_wildcards method, after merge,
598        # so this kludge can be avoided
599        new_cards = []
600        for item in cards:
601            new_cards.append(item[:item.find("|")])
602        wlist = ';;'.join(new_cards)
603
604        return wlist
[488c49d]605
606    def selectData(self, index):
607        """
608        Callback method for modifying the TreeView on Selection Options change
609        """
610        if not isinstance(index, int):
611            msg = "Incorrect type passed to DataExplorer.selectData()"
612            raise AttributeError, msg
613
614        # Respond appropriately
615        if index == 0:
616            # Select All
[5032ea68]617            for index in range(self.model.rowCount()):
618                item = self.model.item(index)
619                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
[488c49d]620                    item.setCheckState(QtCore.Qt.Checked)
621        elif index == 1:
622            # De-select All
[5032ea68]623            for index in range(self.model.rowCount()):
624                item = self.model.item(index)
625                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
[488c49d]626                    item.setCheckState(QtCore.Qt.Unchecked)
627
628        elif index == 2:
629            # Select All 1-D
[5032ea68]630            for index in range(self.model.rowCount()):
631                item = self.model.item(index)
[488c49d]632                item.setCheckState(QtCore.Qt.Unchecked)
[5032ea68]633
634                try:
[8548d739]635                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]636                except AttributeError:
637                    msg = "Bad structure of the data model."
638                    raise RuntimeError, msg
639
640                if is1D:
[488c49d]641                    item.setCheckState(QtCore.Qt.Checked)
642
643        elif index == 3:
644            # Unselect All 1-D
[5032ea68]645            for index in range(self.model.rowCount()):
646                item = self.model.item(index)
647
648                try:
[8548d739]649                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]650                except AttributeError:
651                    msg = "Bad structure of the data model."
652                    raise RuntimeError, msg
653
654                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
[488c49d]655                    item.setCheckState(QtCore.Qt.Unchecked)
656
657        elif index == 4:
658            # Select All 2-D
[5032ea68]659            for index in range(self.model.rowCount()):
660                item = self.model.item(index)
661                item.setCheckState(QtCore.Qt.Unchecked)
662                try:
[8548d739]663                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]664                except AttributeError:
665                    msg = "Bad structure of the data model."
666                    raise RuntimeError, msg
667
668                if is2D:
[488c49d]669                    item.setCheckState(QtCore.Qt.Checked)
670
671        elif index == 5:
672            # Unselect All 2-D
[5032ea68]673            for index in range(self.model.rowCount()):
674                item = self.model.item(index)
675
676                try:
[8548d739]677                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]678                except AttributeError:
679                    msg = "Bad structure of the data model."
680                    raise RuntimeError, msg
681
682                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
[488c49d]683                    item.setCheckState(QtCore.Qt.Unchecked)
684
685        else:
686            msg = "Incorrect value in the Selection Option"
687            # Change this to a proper logging action
688            raise Exception, msg
689
[4b71e91]690    def contextMenu(self):
[e540cd2]691        """
[4b71e91]692        Define actions and layout of the right click context menu
[e540cd2]693        """
[4b71e91]694        # Create a custom menu based on actions defined in the UI file
695        self.context_menu = QtGui.QMenu(self)
696        self.context_menu.addAction(self.actionDataInfo)
697        self.context_menu.addAction(self.actionSaveAs)
698        self.context_menu.addAction(self.actionQuickPlot)
699        self.context_menu.addSeparator()
700        self.context_menu.addAction(self.actionQuick3DPlot)
701        self.context_menu.addAction(self.actionEditMask)
702
703        # Define the callbacks
704        self.actionDataInfo.triggered.connect(self.showDataInfo)
705        self.actionSaveAs.triggered.connect(self.saveDataAs)
706        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
707        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
708        self.actionEditMask.triggered.connect(self.showEditDataMask)
[e540cd2]709
710    def onCustomContextMenu(self, position):
711        """
[4b71e91]712        Show the right-click context menu in the data treeview
[e540cd2]713        """
714        index = self.treeView.indexAt(position)
715        if index.isValid():
[4b71e91]716            model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
717            # Find the mapped index
718            orig_index = model_item.isCheckable()
719            if orig_index:
720                # Check the data to enable/disable actions
[8548d739]721                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
[4b71e91]722                self.actionQuick3DPlot.setEnabled(is_2D)
723                self.actionEditMask.setEnabled(is_2D)
724                # Fire up the menu
725                self.context_menu.exec_(self.treeView.mapToGlobal(position))
726
727    def showDataInfo(self):
728        """
729        Show a simple read-only text edit with data information.
730        """
731        index = self.treeView.selectedIndexes()[0]
732        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
[8548d739]733        data = GuiUtils.dataFromItem(model_item)
[28a84e9]734        if isinstance(data, Data1D):
[4b71e91]735            text_to_show = GuiUtils.retrieveData1d(data)
[28a84e9]736            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]737            self.txt_widget.resize(420,600)
738        else:
739            text_to_show = GuiUtils.retrieveData2d(data)
[28a84e9]740            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]741            self.txt_widget.resize(700,600)
742
743        self.txt_widget.setReadOnly(True)
744        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
745        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
746        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
747        self.txt_widget.insertPlainText(text_to_show)
748
749        self.txt_widget.show()
[28a84e9]750        # Move the slider all the way up, if present
[4b71e91]751        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
752        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
753
754    def saveDataAs(self):
755        """
[28a84e9]756        Save the data points as either txt or xml
[4b71e91]757        """
[28a84e9]758        index = self.treeView.selectedIndexes()[0]
759        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
[8548d739]760        data = GuiUtils.dataFromItem(model_item)
[28a84e9]761        if isinstance(data, Data1D):
762            GuiUtils.saveData1D(data)
763        else:
764            GuiUtils.saveData2D(data)
[4b71e91]765
766    def quickDataPlot(self):
[1af348e]767        """
768        Frozen plot - display an image of the plot
769        """
[39551a68]770        index = self.treeView.selectedIndexes()[0]
771        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
[8548d739]772        data = GuiUtils.dataFromItem(model_item)
[39551a68]773
[ef01be4]774        method_name = 'Plotter'
775        if isinstance(data, Data2D):
776            method_name='Plotter2D'
[1af348e]777
[ef01be4]778        new_plot = globals()[method_name](self, quickplot=True)
779        new_plot.data = data
[39551a68]780        new_plot.plot(marker='o', linestyle='')
781
782        # Update the global plot counter
783        title = "Plot " + data.name
784        new_plot.setWindowTitle(title)
785
786        # Show the plot
787        new_plot.show()
[4b71e91]788
789    def quickData3DPlot(self):
790        """
[55d89f8]791        Slowish 3D plot
[4b71e91]792        """
[55d89f8]793        index = self.treeView.selectedIndexes()[0]
794        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
795        data = GuiUtils.dataFromItem(model_item)
796
797        new_plot = Plotter2D(self, quickplot=True, dimension=3)
798        new_plot.data = data
799        new_plot.plot(marker='o', linestyle='')
800
801        # Update the global plot counter
802        title = "Plot " + data.name
803        new_plot.setWindowTitle(title)
804
805        # Show the plot
806        new_plot.show()
[4b71e91]807
808    def showEditDataMask(self):
809        """
[cad617b]810        Mask Editor for 2D plots
[4b71e91]811        """
[416fa8f]812        index = self.treeView.selectedIndexes()[0]
813        model_item = self.model.itemFromIndex(self.data_proxy.mapToSource(index))
814        data = GuiUtils.dataFromItem(model_item)
815
816        mask_editor = MaskEditor(self, data)
[cad617b]817        # Modal dialog here.
[416fa8f]818        mask_editor.exec_()
[f721030]819
[a281ab8]820    def loadComplete(self, output):
[f721030]821        """
822        Post message to status bar and update the data manager
823        """
[8cb6cd6]824        assert isinstance(output, tuple)
[e540cd2]825
[9e426c1]826        # Reset the model so the view gets updated.
[5032ea68]827        self.model.reset()
[e540cd2]828        self.communicator.progressBarUpdateSignal.emit(-1)
[a281ab8]829
830        output_data = output[0]
831        message = output[1]
[f721030]832        # Notify the manager of the new data available
[f82ab8c]833        self.communicator.statusBarUpdateSignal.emit(message)
834        self.communicator.fileDataReceivedSignal.emit(output_data)
[a281ab8]835        self.manager.add_data(data_list=output_data)
[f721030]836
837    def updateModel(self, data, p_file):
838        """
[481ff26]839        Add data and Info fields to the model item
[f721030]840        """
841        # Structure of the model
842        # checkbox + basename
[481ff26]843        #     |-------> Data.D object
[f721030]844        #     |-------> Info
845        #                 |----> Title:
846        #                 |----> Run:
847        #                 |----> Type:
848        #                 |----> Path:
849        #                 |----> Process
850        #                          |-----> process[0].name
[28a84e9]851        #     |-------> THEORIES
[f721030]852
853        # Top-level item: checkbox with label
854        checkbox_item = QtGui.QStandardItem(True)
855        checkbox_item.setCheckable(True)
856        checkbox_item.setCheckState(QtCore.Qt.Checked)
857        checkbox_item.setText(os.path.basename(p_file))
858
859        # Add the actual Data1D/Data2D object
860        object_item = QtGui.QStandardItem()
861        object_item.setData(QtCore.QVariant(data))
862
[488c49d]863        checkbox_item.setChild(0, object_item)
864
[f721030]865        # Add rows for display in the view
[0cd8612]866        info_item = GuiUtils.infoFromData(data)
[f721030]867
[28a84e9]868        # Set info_item as the first child
[488c49d]869        checkbox_item.setChild(1, info_item)
[f721030]870
[28a84e9]871        # Caption for the theories
872        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
873
[f721030]874        # New row in the model
875        self.model.appendRow(checkbox_item)
[481ff26]876
[28a84e9]877
[5032ea68]878    def updateModelFromPerspective(self, model_item):
879        """
[a281ab8]880        Receive an update model item from a perspective
881        Make sure it is valid and if so, replace it in the model
[5032ea68]882        """
[a281ab8]883        # Assert the correct type
[0cd8612]884        if not isinstance(model_item, QtGui.QStandardItem):
[5032ea68]885            msg = "Wrong data type returned from calculations."
886            raise AttributeError, msg
[a281ab8]887
[1042dba]888        # TODO: Assert other properties
[a281ab8]889
[5032ea68]890        # Reset the view
891        self.model.reset()
[1042dba]892
[5032ea68]893        # Pass acting as a debugger anchor
894        pass
[481ff26]895
[f721030]896
897if __name__ == "__main__":
898    app = QtGui.QApplication([])
899    dlg = DataExplorerWindow()
900    dlg.show()
[481ff26]901    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.