source: sasview/src/sas/qtgui/DataExplorer.py @ 39551a68

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

More context menu functionality + tests

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