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

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

Minor performance impromevements in DE

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