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

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

Removed qtgui dependency on sasgui and wx SASVIEW-590

  • Property mode set to 100644
File size: 37.0 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 = self.parent.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        while ind < self.model.rowCount():
282            ind += 1
283            item = self.model.item(ind)
284            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
285                # Delete these rows from the model
286                self.model.removeRow(ind)
287                # Decrement index since we just deleted it
288                ind -= 1
289
290        # pass temporarily kept as a breakpoint anchor
291        pass
292
293    def deleteTheory(self, event):
294        """
295        Delete selected rows from the theory model
296        """
297        # Assure this is indeed wanted
298        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
299                     "\nDo you want to continue?"
300        reply = QtGui.QMessageBox.question(self,
301                                           'Warning',
302                                           delete_msg,
303                                           QtGui.QMessageBox.Yes,
304                                           QtGui.QMessageBox.No)
305
306        if reply == QtGui.QMessageBox.No:
307            return
308
309        # Figure out which rows are checked
310        ind = -1
311        # Use 'while' so the row count is forced at every iteration
312        while ind < self.theory_model.rowCount():
313            ind += 1
314            item = self.theory_model.item(ind)
315            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
316                # Delete these rows from the model
317                self.theory_model.removeRow(ind)
318                # Decrement index since we just deleted it
319                ind -= 1
320
321        # pass temporarily kept as a breakpoint anchor
322        pass
323
324    def sendData(self, event):
325        """
326        Send selected item data to the current perspective and set the relevant notifiers
327        """
328        # Set the signal handlers
329        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
330
331        def isItemReady(index):
332            item = self.model.item(index)
333            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
334
335        # Figure out which rows are checked
336        selected_items = [self.model.item(index)
337                          for index in xrange(self.model.rowCount())
338                          if isItemReady(index)]
339
340        if len(selected_items) < 1:
341            return
342
343        # Which perspective has been selected?
344        if len(selected_items) > 1 and not self._perspective().allowBatch():
345            msg = self._perspective().title() + " does not allow multiple data."
346            msgbox = QtGui.QMessageBox()
347            msgbox.setIcon(QtGui.QMessageBox.Critical)
348            msgbox.setText(msg)
349            msgbox.setStandardButtons(QtGui.QMessageBox.Ok)
350            retval = msgbox.exec_()
351            return
352
353        # Notify the GuiManager about the send request
354        self._perspective().setData(data_item=selected_items)
355
356    def freezeTheory(self, event):
357        """
358        Freeze selected theory rows.
359
360        "Freezing" means taking the plottable data from the Theory item
361        and copying it to a separate top-level item in Data.
362        """
363        # Figure out which rows are checked
364        # Use 'while' so the row count is forced at every iteration
365        outer_index = -1
366        theories_copied = 0
367        while outer_index < self.theory_model.rowCount():
368            outer_index += 1
369            outer_item = self.theory_model.item(outer_index)
370            if not outer_item:
371                continue
372            if outer_item.isCheckable() and \
373                   outer_item.checkState() == QtCore.Qt.Checked:
374                theories_copied += 1
375                new_item = self.recursivelyCloneItem(outer_item)
376                # Append a "unique" descriptor to the name
377                time_bit = str(time.time())[7:-1].replace('.', '')
378                new_name = new_item.text() + '_@' + time_bit
379                new_item.setText(new_name)
380                self.model.appendRow(new_item)
381            self.model.reset()
382
383        freeze_msg = ""
384        if theories_copied == 0:
385            return
386        elif theories_copied == 1:
387            freeze_msg = "1 theory copied from the Theory tab as a data set"
388        elif theories_copied > 1:
389            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
390        else:
391            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
392            raise AttributeError, freeze_msg
393        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
394        # Actively switch tabs
395        self.setCurrentIndex(1)
396
397    def recursivelyCloneItem(self, item):
398        """
399        Clone QStandardItem() object
400        """
401        new_item = item.clone()
402        # clone doesn't do deepcopy :(
403        for child_index in xrange(item.rowCount()):
404            child_item = self.recursivelyCloneItem(item.child(child_index))
405            new_item.setChild(child_index, child_item)
406        return new_item
407
408    def updatePlotName(self, name_tuple):
409        """
410        Modify the name of the current plot
411        """
412        old_name, current_name = name_tuple
413        ind = self.cbgraph.findText(old_name)
414        self.cbgraph.setCurrentIndex(ind)
415        self.cbgraph.setItemText(ind, current_name)
416
417    def updateGraphCombo(self, graph_list):
418        """
419        Modify Graph combo box on graph add/delete
420        """
421        orig_text = self.cbgraph.currentText()
422        self.cbgraph.clear()
423        #graph_titles= [str(graph) for graph in graph_list]
424
425        #self.cbgraph.insertItems(0, graph_titles)
426        self.cbgraph.insertItems(0, graph_list)
427        ind = self.cbgraph.findText(orig_text)
428        if ind > 0:
429            self.cbgraph.setCurrentIndex(ind)
430
431    def updatePerspectiveCombo(self, index):
432        """
433        Notify the gui manager about the new perspective chosen.
434        """
435        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.currentText())
436        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
437
438    def displayData(self, data_list):
439        """
440        Forces display of charts for the given filename
441        """
442        # Assure no multiple plots for the same ID
443        plot_to_show = data_list[0]
444        if plot_to_show.id in PlotHelper.currentPlots():
445            return
446
447        # Now query the model item for available plots
448        filename = plot_to_show.filename
449        model = self.model if plot_to_show.is_data else self.theory_model
450        plots = GuiUtils.plotsFromFilename(filename, model)
451        _ = [self.plotData([(None, plot)]) for plot in plots]
452
453    def addDataPlot2D(self, plot_set, item):
454        """
455        Create a new 2D plot and add it to the workspace
456        """
457        plot2D = Plotter2D(self)
458        plot2D.item = item
459        plot2D.plot(plot_set)
460        self.addPlot(plot2D)
461        #============================================
462        # Experimental hook for silx charts
463        #============================================
464        ## Attach silx
465        #from silx.gui import qt
466        #from silx.gui.plot import StackView
467        #sv = StackView()
468        #sv.setColormap("jet", autoscale=True)
469        #sv.setStack(plot_set.data.reshape(1,100,100))
470        ##sv.setLabels(["x: -10 to 10 (200 samples)",
471        ##              "y: -10 to 5 (150 samples)"])
472        #sv.show()
473        #============================================
474
475    def plotData(self, plots):
476        """
477        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
478        """
479        # Call show on requested plots
480        # All same-type charts in one plot
481        new_plot = Plotter(self)
482
483        for item, plot_set in plots:
484            if isinstance(plot_set, Data1D):
485                new_plot.plot(plot_set)
486            elif isinstance(plot_set, Data2D):
487                self.addDataPlot2D(plot_set, item)
488            else:
489                msg = "Incorrect data type passed to Plotting"
490                raise AttributeError, msg
491
492        if plots and \
493            hasattr(new_plot, 'data') and \
494            isinstance(new_plot.data, Data1D):
495                self.addPlot(new_plot)
496
497    def newPlot(self):
498        """
499        Select checked data and plot it
500        """
501        # Check which tab is currently active
502        if self.current_view == self.treeView:
503            plots = GuiUtils.plotsFromCheckedItems(self.model)
504        else:
505            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
506
507        self.plotData(plots)
508
509    def addPlot(self, new_plot):
510        """
511        Helper method for plot bookkeeping
512        """
513        # Update the global plot counter
514        title = str(PlotHelper.idOfPlot(new_plot))
515        new_plot.setWindowTitle(title)
516
517        # Add the plot to the workspace
518        self.parent.workspace().addWindow(new_plot)
519
520        # Show the plot
521        new_plot.show()
522
523        # Update the active chart list
524        self.active_plots[new_plot.data.id] = new_plot
525
526    def appendPlot(self):
527        """
528        Add data set(s) to the existing matplotlib chart
529        """
530        # new plot data
531        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
532
533        # old plot data
534        plot_id = str(self.cbgraph.currentText())
535
536        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
537
538        old_plot = PlotHelper.plotById(plot_id)
539
540        # Add new data to the old plot, if data type is the same.
541        for _, plot_set in new_plots:
542            if type(plot_set) is type(old_plot._data):
543                old_plot.data = plot_set
544                old_plot.plot()
545
546    def updatePlot(self, new_data):
547        """
548        Modify existing plot for immediate response
549        """
550        data = new_data[0]
551        assert type(data).__name__ in ['Data1D', 'Data2D']
552
553        id = data.id
554        if data.id in self.active_plots.keys():
555            self.active_plots[id].replacePlot(id, data)
556
557    def chooseFiles(self):
558        """
559        Shows the Open file dialog and returns the chosen path(s)
560        """
561        # List of known extensions
562        wlist = self.getWlist()
563
564        # Location is automatically saved - no need to keep track of the last dir
565        # But only with Qt built-in dialog (non-platform native)
566        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
567                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
568        if paths is None:
569            return
570
571        if isinstance(paths, QtCore.QStringList):
572            paths = [str(f) for f in paths]
573
574        if not isinstance(paths, list):
575            paths = [paths]
576
577        return paths
578
579    def readData(self, path):
580        """
581        verbatim copy-paste from
582           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
583        slightly modified for clarity
584        """
585        message = ""
586        log_msg = ''
587        output = {}
588        any_error = False
589        data_error = False
590        error_message = ""
591        number_of_files = len(path)
592        self.communicator.progressBarUpdateSignal.emit(0.0)
593
594        for index, p_file in enumerate(path):
595            basename = os.path.basename(p_file)
596            _, extension = os.path.splitext(basename)
597            if extension.lower() in GuiUtils.EXTENSIONS:
598                any_error = True
599                log_msg = "Data Loader cannot "
600                log_msg += "load: %s\n" % str(p_file)
601                log_msg += """Please try to open that file from "open project" """
602                log_msg += """or "open analysis" menu\n"""
603                error_message = log_msg + "\n"
604                logging.info(log_msg)
605                continue
606
607            try:
608                message = "Loading Data... " + str(basename) + "\n"
609
610                # change this to signal notification in GuiManager
611                self.communicator.statusBarUpdateSignal.emit(message)
612
613                output_objects = self.loader.load(p_file)
614
615                # Some loaders return a list and some just a single Data1D object.
616                # Standardize.
617                if not isinstance(output_objects, list):
618                    output_objects = [output_objects]
619
620                for item in output_objects:
621                    # cast sascalc.dataloader.data_info.Data1D into
622                    # sasgui.guiframe.dataFitting.Data1D
623                    # TODO : Fix it
624                    new_data = self.manager.create_gui_data(item, p_file)
625                    output[new_data.id] = new_data
626
627                    # Model update should be protected
628                    self.mutex.lock()
629                    self.updateModel(new_data, p_file)
630                    self.model.reset()
631                    QtGui.qApp.processEvents()
632                    self.mutex.unlock()
633
634                    if hasattr(item, 'errors'):
635                        for error_data in item.errors:
636                            data_error = True
637                            message += "\tError: {0}\n".format(error_data)
638                    else:
639
640                        logging.error("Loader returned an invalid object:\n %s" % str(item))
641                        data_error = True
642
643            except Exception as ex:
644                logging.error(sys.exc_value)
645
646                any_error = True
647            if any_error or error_message != "":
648                if error_message == "":
649                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
650                    error += "while loading Data: \n%s\n" % str(basename)
651                    error_message += "The data file you selected could not be loaded.\n"
652                    error_message += "Make sure the content of your file"
653                    error_message += " is properly formatted.\n\n"
654                    error_message += "When contacting the SasView team, mention the"
655                    error_message += " following:\n%s" % str(error)
656                elif data_error:
657                    base_message = "Errors occurred while loading "
658                    base_message += "{0}\n".format(basename)
659                    base_message += "The data file loaded but with errors.\n"
660                    error_message = base_message + error_message
661                else:
662                    error_message += "%s\n" % str(p_file)
663
664            current_percentage = int(100.0* index/number_of_files)
665            self.communicator.progressBarUpdateSignal.emit(current_percentage)
666
667        if any_error or error_message:
668            logging.error(error_message)
669            status_bar_message = "Errors occurred while loading %s" % format(basename)
670            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
671
672        else:
673            message = "Loading Data Complete! "
674        message += log_msg
675        # Notify the progress bar that the updates are over.
676        self.communicator.progressBarUpdateSignal.emit(-1)
677        self.communicator.statusBarUpdateSignal.emit(message)
678
679        return output, message
680
681    def getWlist(self):
682        """
683        Wildcards of files we know the format of.
684        """
685        # Display the Qt Load File module
686        cards = self.loader.get_wildcards()
687
688        # get rid of the wx remnant in wildcards
689        # TODO: modify sasview loader get_wildcards method, after merge,
690        # so this kludge can be avoided
691        new_cards = []
692        for item in cards:
693            new_cards.append(item[:item.find("|")])
694        wlist = ';;'.join(new_cards)
695
696        return wlist
697
698    def selectData(self, index):
699        """
700        Callback method for modifying the TreeView on Selection Options change
701        """
702        if not isinstance(index, int):
703            msg = "Incorrect type passed to DataExplorer.selectData()"
704            raise AttributeError, msg
705
706        # Respond appropriately
707        if index == 0:
708            # Select All
709            for index in range(self.model.rowCount()):
710                item = self.model.item(index)
711                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
712                    item.setCheckState(QtCore.Qt.Checked)
713        elif index == 1:
714            # De-select All
715            for index in range(self.model.rowCount()):
716                item = self.model.item(index)
717                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
718                    item.setCheckState(QtCore.Qt.Unchecked)
719
720        elif index == 2:
721            # Select All 1-D
722            for index in range(self.model.rowCount()):
723                item = self.model.item(index)
724                item.setCheckState(QtCore.Qt.Unchecked)
725
726                try:
727                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
728                except AttributeError:
729                    msg = "Bad structure of the data model."
730                    raise RuntimeError, msg
731
732                if is1D:
733                    item.setCheckState(QtCore.Qt.Checked)
734
735        elif index == 3:
736            # Unselect All 1-D
737            for index in range(self.model.rowCount()):
738                item = self.model.item(index)
739
740                try:
741                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
742                except AttributeError:
743                    msg = "Bad structure of the data model."
744                    raise RuntimeError, msg
745
746                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
747                    item.setCheckState(QtCore.Qt.Unchecked)
748
749        elif index == 4:
750            # Select All 2-D
751            for index in range(self.model.rowCount()):
752                item = self.model.item(index)
753                item.setCheckState(QtCore.Qt.Unchecked)
754                try:
755                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
756                except AttributeError:
757                    msg = "Bad structure of the data model."
758                    raise RuntimeError, msg
759
760                if is2D:
761                    item.setCheckState(QtCore.Qt.Checked)
762
763        elif index == 5:
764            # Unselect All 2-D
765            for index in range(self.model.rowCount()):
766                item = self.model.item(index)
767
768                try:
769                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
770                except AttributeError:
771                    msg = "Bad structure of the data model."
772                    raise RuntimeError, msg
773
774                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
775                    item.setCheckState(QtCore.Qt.Unchecked)
776
777        else:
778            msg = "Incorrect value in the Selection Option"
779            # Change this to a proper logging action
780            raise Exception, msg
781
782    def contextMenu(self):
783        """
784        Define actions and layout of the right click context menu
785        """
786        # Create a custom menu based on actions defined in the UI file
787        self.context_menu = QtGui.QMenu(self)
788        self.context_menu.addAction(self.actionDataInfo)
789        self.context_menu.addAction(self.actionSaveAs)
790        self.context_menu.addAction(self.actionQuickPlot)
791        self.context_menu.addSeparator()
792        self.context_menu.addAction(self.actionQuick3DPlot)
793        self.context_menu.addAction(self.actionEditMask)
794
795        # Define the callbacks
796        self.actionDataInfo.triggered.connect(self.showDataInfo)
797        self.actionSaveAs.triggered.connect(self.saveDataAs)
798        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
799        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
800        self.actionEditMask.triggered.connect(self.showEditDataMask)
801
802    def onCustomContextMenu(self, position):
803        """
804        Show the right-click context menu in the data treeview
805        """
806        index = self.current_view.indexAt(position)
807        proxy = self.current_view.model()
808        model = proxy.sourceModel()
809
810        if index.isValid():
811            model_item = model.itemFromIndex(proxy.mapToSource(index))
812            # Find the mapped index
813            orig_index = model_item.isCheckable()
814            if orig_index:
815                # Check the data to enable/disable actions
816                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
817                self.actionQuick3DPlot.setEnabled(is_2D)
818                self.actionEditMask.setEnabled(is_2D)
819                # Fire up the menu
820                self.context_menu.exec_(self.current_view.mapToGlobal(position))
821
822    def showDataInfo(self):
823        """
824        Show a simple read-only text edit with data information.
825        """
826        index = self.current_view.selectedIndexes()[0]
827        proxy = self.current_view.model()
828        model = proxy.sourceModel()
829        model_item = model.itemFromIndex(proxy.mapToSource(index))
830
831        data = GuiUtils.dataFromItem(model_item)
832        if isinstance(data, Data1D):
833            text_to_show = GuiUtils.retrieveData1d(data)
834            # Hardcoded sizes to enable full width rendering with default font
835            self.txt_widget.resize(420,600)
836        else:
837            text_to_show = GuiUtils.retrieveData2d(data)
838            # Hardcoded sizes to enable full width rendering with default font
839            self.txt_widget.resize(700,600)
840
841        self.txt_widget.setReadOnly(True)
842        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
843        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
844        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
845        self.txt_widget.insertPlainText(text_to_show)
846
847        self.txt_widget.show()
848        # Move the slider all the way up, if present
849        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
850        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
851
852    def saveDataAs(self):
853        """
854        Save the data points as either txt or xml
855        """
856        index = self.current_view.selectedIndexes()[0]
857        proxy = self.current_view.model()
858        model = proxy.sourceModel()
859        model_item = model.itemFromIndex(proxy.mapToSource(index))
860
861        data = GuiUtils.dataFromItem(model_item)
862        if isinstance(data, Data1D):
863            GuiUtils.saveData1D(data)
864        else:
865            GuiUtils.saveData2D(data)
866
867    def quickDataPlot(self):
868        """
869        Frozen plot - display an image of the plot
870        """
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
876        data = GuiUtils.dataFromItem(model_item)
877
878        method_name = 'Plotter'
879        if isinstance(data, Data2D):
880            method_name='Plotter2D'
881
882        new_plot = globals()[method_name](self, quickplot=True)
883        new_plot.data = data
884        #new_plot.plot(marker='o')
885        new_plot.plot()
886
887        # Update the global plot counter
888        title = "Plot " + data.name
889        new_plot.setWindowTitle(title)
890
891        # Show the plot
892        new_plot.show()
893
894    def quickData3DPlot(self):
895        """
896        Slowish 3D plot
897        """
898        index = self.current_view.selectedIndexes()[0]
899        proxy = self.current_view.model()
900        model = proxy.sourceModel()
901        model_item = model.itemFromIndex(proxy.mapToSource(index))
902
903        data = GuiUtils.dataFromItem(model_item)
904
905        new_plot = Plotter2D(self, quickplot=True, dimension=3)
906        new_plot.data = data
907        new_plot.plot()
908
909        # Update the global plot counter
910        title = "Plot " + data.name
911        new_plot.setWindowTitle(title)
912
913        # Show the plot
914        new_plot.show()
915
916    def showEditDataMask(self):
917        """
918        Mask Editor for 2D plots
919        """
920        index = self.current_view.selectedIndexes()[0]
921        proxy = self.current_view.model()
922        model = proxy.sourceModel()
923        model_item = model.itemFromIndex(proxy.mapToSource(index))
924
925        data = GuiUtils.dataFromItem(model_item)
926
927        mask_editor = MaskEditor(self, data)
928        # Modal dialog here.
929        mask_editor.exec_()
930
931    def loadComplete(self, output):
932        """
933        Post message to status bar and update the data manager
934        """
935        assert isinstance(output, tuple)
936
937        # Reset the model so the view gets updated.
938        self.model.reset()
939        self.communicator.progressBarUpdateSignal.emit(-1)
940
941        output_data = output[0]
942        message = output[1]
943        # Notify the manager of the new data available
944        self.communicator.statusBarUpdateSignal.emit(message)
945        self.communicator.fileDataReceivedSignal.emit(output_data)
946        self.manager.add_data(data_list=output_data)
947
948    def updateModel(self, data, p_file):
949        """
950        Add data and Info fields to the model item
951        """
952        # Structure of the model
953        # checkbox + basename
954        #     |-------> Data.D object
955        #     |-------> Info
956        #                 |----> Title:
957        #                 |----> Run:
958        #                 |----> Type:
959        #                 |----> Path:
960        #                 |----> Process
961        #                          |-----> process[0].name
962        #     |-------> THEORIES
963
964        # Top-level item: checkbox with label
965        checkbox_item = QtGui.QStandardItem(True)
966        checkbox_item.setCheckable(True)
967        checkbox_item.setCheckState(QtCore.Qt.Checked)
968        checkbox_item.setText(os.path.basename(p_file))
969
970        # Add the actual Data1D/Data2D object
971        object_item = QtGui.QStandardItem()
972        object_item.setData(QtCore.QVariant(data))
973
974        checkbox_item.setChild(0, object_item)
975
976        # Add rows for display in the view
977        info_item = GuiUtils.infoFromData(data)
978
979        # Set info_item as the first child
980        checkbox_item.setChild(1, info_item)
981
982        # Caption for the theories
983        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
984
985        # New row in the model
986        self.model.appendRow(checkbox_item)
987
988
989    def updateModelFromPerspective(self, model_item):
990        """
991        Receive an update model item from a perspective
992        Make sure it is valid and if so, replace it in the model
993        """
994        # Assert the correct type
995        if not isinstance(model_item, QtGui.QStandardItem):
996            msg = "Wrong data type returned from calculations."
997            raise AttributeError, msg
998
999        # TODO: Assert other properties
1000
1001        # Reset the view
1002        self.model.reset()
1003        # Pass acting as a debugger anchor
1004        pass
1005
1006    def updateTheoryFromPerspective(self, model_item):
1007        """
1008        Receive an update theory item from a perspective
1009        Make sure it is valid and if so, replace/add in the model
1010        """
1011        # Assert the correct type
1012        if not isinstance(model_item, QtGui.QStandardItem):
1013            msg = "Wrong data type returned from calculations."
1014            raise AttributeError, msg
1015
1016        # Check if there are any other items for this tab
1017        # If so, delete them
1018        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1019        #
1020        current_tab_name = model_item.text()[:2]
1021        for current_index in xrange(self.theory_model.rowCount()):
1022            if current_tab_name in self.theory_model.item(current_index).text():
1023                self.theory_model.removeRow(current_index)
1024                break
1025
1026        # Reset the view
1027        self.model.reset()
1028
1029        # Reset the view
1030        self.theory_model.appendRow(model_item)
1031
1032        # Pass acting as a debugger anchor
1033        pass
1034
1035
1036if __name__ == "__main__":
1037    app = QtGui.QApplication([])
1038    dlg = DataExplorerWindow()
1039    dlg.show()
1040    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.