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

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

Startup time improvements - hiding expensive imports and such

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