source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 7d8bebf

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

Some improvements in plot handling

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