source: sasview/src/sas/qtgui/DataExplorer.py @ 6ee82dd

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 6ee82dd was 83d6249, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Perspectives are now switchable and can be added "dynamically"

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