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

Last change on this file since c57ecca was 88e1f57, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Fixed chart update upon consecutive fitting.
Fixed 1D default charts - now only two (combined data/fit + residuals)
Minor text fixes

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