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

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

Allow for multiple datasets to be opened by the fitting perspective

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