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

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

Further fitpage implementation with tests SASVIEW-570

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