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
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# SAS
15from sas.sascalc.dataloader.loader import Loader
16from sas.sasgui.guiframe.data_manager import DataManager
17from sas.sasgui.guiframe.dataFitting import Data1D
18from sas.sasgui.guiframe.dataFitting import Data2D
19
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
28
29class DataExplorerWindow(DroppableDataLoadWidget):
30    # The controller which is responsible for managing signal slots connections
31    # for the gui and providing an interface to the data model.
32
33    def __init__(self, parent=None, guimanager=None, manager=None):
34        super(DataExplorerWindow, self).__init__(parent, guimanager)
35
36        # Main model for keeping loaded data
37        self.model = QtGui.QStandardItemModel(self)
38
39        # Secondary model for keeping frozen data sets
40        self.theory_model = QtGui.QStandardItemModel(self)
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()
46        self.manager = manager if manager is not None else DataManager()
47        self.txt_widget = QtGui.QTextEdit(None)
48
49        # Be careful with twisted threads.
50        self.mutex = QMutex()
51
52        # Active plots
53        self.active_plots = {}
54
55        # Connect the buttons
56        self.cmdLoad.clicked.connect(self.loadFile)
57        self.cmdDeleteData.clicked.connect(self.deleteFile)
58        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
59        self.cmdFreeze.clicked.connect(self.freezeTheory)
60        self.cmdSendTo.clicked.connect(self.sendData)
61        self.cmdNew.clicked.connect(self.newPlot)
62        self.cmdNew_2.clicked.connect(self.newPlot)
63        self.cmdAppend.clicked.connect(self.appendPlot)
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()
69
70        # Fill in the perspectives combo
71        self.initPerspectives()
72
73        # Custom context menu
74        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
75        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
76        self.contextMenu()
77
78        # Same menus for the theory view
79        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
80        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
81
82        # Connect the comboboxes
83        self.cbSelect.currentIndexChanged.connect(self.selectData)
84
85        #self.closeEvent.connect(self.closeEvent)
86        self.currentChanged.connect(self.onTabSwitch)
87        self.communicator = self.parent.communicator()
88        self.communicator.fileReadSignal.connect(self.loadFromURL)
89        self.communicator.activeGraphsSignal.connect(self.updateGraphCombo)
90        self.communicator.activeGraphName.connect(self.updatePlotName)
91        self.communicator.plotUpdateSignal.connect(self.updatePlot)
92        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
93        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
94
95        # Proxy model for showing a subset of Data1D/Data2D content
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"[^()]")
101
102        # The Data viewer is QTreeView showing the proxy model
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"[^()]")
111
112        # Theory model view
113        self.freezeView.setModel(self.theory_proxy)
114
115        self.enableGraphCombo(None)
116
117        # Current view on model
118        self.current_view = self.treeView
119
120    def closeEvent(self, event):
121        """
122        Overwrite the close event - no close!
123        """
124        event.ignore()
125
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
133    def displayHelp(self):
134        """
135        Show the "Loading data" section of help
136        """
137        tree_location = self.parent.HELP_DIRECTORY_LOCATION +\
138            "/user/sasgui/guiframe/data_explorer_help.html"
139        self._helpView.load(QtCore.QUrl(tree_location))
140        self._helpView.show()
141
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
149    def initPerspectives(self):
150        """
151        Populate the Perspective combobox and define callbacks
152        """
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)
157        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
158        # Set the index so we see the default (Fitting)
159        self.updatePerspectiveCombo(0)
160
161    def _perspective(self):
162        """
163        Returns the current perspective
164        """
165        return self.parent.perspective()
166
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)
173
174    def loadFile(self, event=None):
175        """
176        Called when the "Load" button pressed.
177        Opens the Qt "Open File..." dialog
178        """
179        path_str = self.chooseFiles()
180        if not path_str:
181            return
182        self.loadFromURL(path_str)
183
184    def loadFolder(self, event=None):
185        """
186        Called when the "File/Load Folder" menu item chosen.
187        Opens the Qt "Open Folder..." dialog
188        """
189        folder = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
190              QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog)
191        if folder is None:
192            return
193
194        folder = str(folder)
195
196        if not os.path.isdir(folder):
197            return
198
199        # get content of dir into a list
200        path_str = [os.path.join(os.path.abspath(folder), filename)
201                    for filename in os.listdir(folder)]
202
203        self.loadFromURL(path_str)
204
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
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?"
266        reply = QtGui.QMessageBox.question(self,
267                                           'Warning',
268                                           delete_msg,
269                                           QtGui.QMessageBox.Yes,
270                                           QtGui.QMessageBox.No)
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
288        pass
289
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?"
297        reply = QtGui.QMessageBox.question(self,
298                                           'Warning',
299                                           delete_msg,
300                                           QtGui.QMessageBox.Yes,
301                                           QtGui.QMessageBox.No)
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
321    def sendData(self, event):
322        """
323        Send selected item data to the current perspective and set the relevant notifiers
324        """
325        # Set the signal handlers
326        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
327
328        def isItemReady(index):
329            item = self.model.item(index)
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)]
336
337        if len(selected_items) < 1:
338            return
339
340        # Which perspective has been selected?
341        if len(selected_items) > 1 and not self._perspective().allowBatch():
342            msg = self._perspective().title() + " does not allow multiple data."
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
349
350        # Notify the GuiManager about the send request
351        self._perspective().setData(data_item=selected_items)
352
353    def freezeTheory(self, event):
354        """
355        Freeze selected theory rows.
356
357        "Freezing" means taking the plottable data from the Theory item
358        and copying it to a separate top-level item in Data.
359        """
360        # Figure out which rows are checked
361        # Use 'while' so the row count is forced at every iteration
362        outer_index = -1
363        theories_copied = 0
364        while outer_index < self.theory_model.rowCount():
365            outer_index += 1
366            outer_item = self.theory_model.item(outer_index)
367            if not outer_item:
368                continue
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()
379
380        freeze_msg = ""
381        if theories_copied == 0:
382            return
383        elif theories_copied == 1:
384            freeze_msg = "1 theory copied from the Theory tab as a data set"
385        elif theories_copied > 1:
386            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
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
404
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
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()
420        #graph_titles= [str(graph) for graph in graph_list]
421
422        #self.cbgraph.insertItems(0, graph_titles)
423        self.cbgraph.insertItems(0, graph_list)
424        ind = self.cbgraph.findText(orig_text)
425        if ind > 0:
426            self.cbgraph.setCurrentIndex(ind)
427
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())
433        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
434
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]
441        if plot_to_show.id in PlotHelper.currentPlots():
442            return
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)
448        plots = [(None, plot) for plot in plots]
449        self.plotData(plots)
450
451    def addDataPlot2D(self, plot_set, item):
452        """
453        Create a new 2D plot and add it to the workspace
454        """
455        plot2D = Plotter2D(self)
456        plot2D.item = item
457        plot2D.plot(plot_set)
458        self.addPlot(plot2D)
459        #============================================
460        # Experimental hook for silx charts
461        #============================================
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)
476        """
477        # Call show on requested plots
478        # All same-type charts in one plot
479        new_plot = Plotter(self)
480
481        for item, plot_set in plots:
482            if isinstance(plot_set, Data1D):
483                new_plot.plot(plot_set)
484            elif isinstance(plot_set, Data2D):
485                self.addDataPlot2D(plot_set, item)
486            else:
487                msg = "Incorrect data type passed to Plotting"
488                raise AttributeError, msg
489
490        if plots and \
491            hasattr(new_plot, 'data') and \
492            isinstance(new_plot.data, Data1D):
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)
506
507    def addPlot(self, new_plot):
508        """
509        Helper method for plot bookkeeping
510        """
511        # Update the global plot counter
512        title = str(PlotHelper.idOfPlot(new_plot))
513        new_plot.setWindowTitle(title)
514
515        # Add the plot to the workspace
516        self.parent.workspace().addWindow(new_plot)
517
518        # Show the plot
519        new_plot.show()
520
521        # Update the active chart list
522        self.active_plots[new_plot.data.id] = new_plot
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
532        plot_id = str(self.cbgraph.currentText())
533
534        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
535
536        old_plot = PlotHelper.plotById(plot_id)
537
538        # Add new data to the old plot, if data type is the same.
539        for _, plot_set in new_plots:
540            if type(plot_set) is type(old_plot._data):
541                old_plot.data = plot_set
542                old_plot.plot()
543
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
555    def chooseFiles(self):
556        """
557        Shows the Open file dialog and returns the chosen path(s)
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
563        # But only with Qt built-in dialog (non-platform native)
564        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
565                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
566        if paths is None:
567            return
568
569        if isinstance(paths, QtCore.QStringList):
570            paths = [str(f) for f in paths]
571
572        if not isinstance(paths, list):
573            paths = [paths]
574
575        return paths
576
577    def readData(self, path):
578        """
579        verbatim copy-paste from
580           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
581        slightly modified for clarity
582        """
583        message = ""
584        log_msg = ''
585        output = {}
586        any_error = False
587        data_error = False
588        error_message = ""
589        number_of_files = len(path)
590        self.communicator.progressBarUpdateSignal.emit(0.0)
591
592        for index, p_file in enumerate(path):
593            basename = os.path.basename(p_file)
594            _, extension = os.path.splitext(basename)
595            if extension.lower() in GuiUtils.EXTENSIONS:
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:
606                message = "Loading Data... " + str(basename) + "\n"
607
608                # change this to signal notification in GuiManager
609                self.communicator.statusBarUpdateSignal.emit(message)
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:
619                    # cast sascalc.dataloader.data_info.Data1D into
620                    # sasgui.guiframe.dataFitting.Data1D
621                    # TODO : Fix it
622                    new_data = self.manager.create_gui_data(item, p_file)
623                    output[new_data.id] = new_data
624
625                    # Model update should be protected
626                    self.mutex.lock()
627                    self.updateModel(new_data, p_file)
628                    self.model.reset()
629                    QtGui.qApp.processEvents()
630                    self.mutex.unlock()
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:
637
638                        logging.error("Loader returned an invalid object:\n %s" % str(item))
639                        data_error = True
640
641            except Exception as ex:
642                logging.error(sys.exc_value)
643
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)
661
662            current_percentage = int(100.0* index/number_of_files)
663            self.communicator.progressBarUpdateSignal.emit(current_percentage)
664
665        if any_error or error_message:
666            logging.error(error_message)
667            status_bar_message = "Errors occurred while loading %s" % format(basename)
668            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
669
670        else:
671            message = "Loading Data Complete! "
672        message += log_msg
673        # Notify the progress bar that the updates are over.
674        self.communicator.progressBarUpdateSignal.emit(-1)
675        self.communicator.statusBarUpdateSignal.emit(message)
676
677        return output, message
678
679    def getWlist(self):
680        """
681        Wildcards of files we know the format of.
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
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
707            for index in range(self.model.rowCount()):
708                item = self.model.item(index)
709                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
710                    item.setCheckState(QtCore.Qt.Checked)
711        elif index == 1:
712            # De-select All
713            for index in range(self.model.rowCount()):
714                item = self.model.item(index)
715                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
716                    item.setCheckState(QtCore.Qt.Unchecked)
717
718        elif index == 2:
719            # Select All 1-D
720            for index in range(self.model.rowCount()):
721                item = self.model.item(index)
722                item.setCheckState(QtCore.Qt.Unchecked)
723
724                try:
725                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
726                except AttributeError:
727                    msg = "Bad structure of the data model."
728                    raise RuntimeError, msg
729
730                if is1D:
731                    item.setCheckState(QtCore.Qt.Checked)
732
733        elif index == 3:
734            # Unselect All 1-D
735            for index in range(self.model.rowCount()):
736                item = self.model.item(index)
737
738                try:
739                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
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:
745                    item.setCheckState(QtCore.Qt.Unchecked)
746
747        elif index == 4:
748            # Select All 2-D
749            for index in range(self.model.rowCount()):
750                item = self.model.item(index)
751                item.setCheckState(QtCore.Qt.Unchecked)
752                try:
753                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
754                except AttributeError:
755                    msg = "Bad structure of the data model."
756                    raise RuntimeError, msg
757
758                if is2D:
759                    item.setCheckState(QtCore.Qt.Checked)
760
761        elif index == 5:
762            # Unselect All 2-D
763            for index in range(self.model.rowCount()):
764                item = self.model.item(index)
765
766                try:
767                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
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:
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
780    def contextMenu(self):
781        """
782        Define actions and layout of the right click context menu
783        """
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)
799
800    def onCustomContextMenu(self, position):
801        """
802        Show the right-click context menu in the data treeview
803        """
804        index = self.current_view.indexAt(position)
805        proxy = self.current_view.model()
806        model = proxy.sourceModel()
807
808        if index.isValid():
809            model_item = model.itemFromIndex(proxy.mapToSource(index))
810            # Find the mapped index
811            orig_index = model_item.isCheckable()
812            if orig_index:
813                # Check the data to enable/disable actions
814                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
815                self.actionQuick3DPlot.setEnabled(is_2D)
816                self.actionEditMask.setEnabled(is_2D)
817                # Fire up the menu
818                self.context_menu.exec_(self.current_view.mapToGlobal(position))
819
820    def showDataInfo(self):
821        """
822        Show a simple read-only text edit with data information.
823        """
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
829        data = GuiUtils.dataFromItem(model_item)
830        if isinstance(data, Data1D):
831            text_to_show = GuiUtils.retrieveData1d(data)
832            # Hardcoded sizes to enable full width rendering with default font
833            self.txt_widget.resize(420,600)
834        else:
835            text_to_show = GuiUtils.retrieveData2d(data)
836            # Hardcoded sizes to enable full width rendering with default font
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()
846        # Move the slider all the way up, if present
847        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
848        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
849
850    def saveDataAs(self):
851        """
852        Save the data points as either txt or xml
853        """
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
859        data = GuiUtils.dataFromItem(model_item)
860        if isinstance(data, Data1D):
861            GuiUtils.saveData1D(data)
862        else:
863            GuiUtils.saveData2D(data)
864
865    def quickDataPlot(self):
866        """
867        Frozen plot - display an image of the plot
868        """
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
874        data = GuiUtils.dataFromItem(model_item)
875
876        method_name = 'Plotter'
877        if isinstance(data, Data2D):
878            method_name='Plotter2D'
879
880        new_plot = globals()[method_name](self, quickplot=True)
881        new_plot.data = data
882        #new_plot.plot(marker='o')
883        new_plot.plot()
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()
891
892    def quickData3DPlot(self):
893        """
894        Slowish 3D plot
895        """
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
901        data = GuiUtils.dataFromItem(model_item)
902
903        new_plot = Plotter2D(self, quickplot=True, dimension=3)
904        new_plot.data = data
905        new_plot.plot()
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()
913
914    def showEditDataMask(self):
915        """
916        Mask Editor for 2D plots
917        """
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
923        data = GuiUtils.dataFromItem(model_item)
924
925        mask_editor = MaskEditor(self, data)
926        # Modal dialog here.
927        mask_editor.exec_()
928
929    def loadComplete(self, output):
930        """
931        Post message to status bar and update the data manager
932        """
933        assert isinstance(output, tuple)
934
935        # Reset the model so the view gets updated.
936        self.model.reset()
937        self.communicator.progressBarUpdateSignal.emit(-1)
938
939        output_data = output[0]
940        message = output[1]
941        # Notify the manager of the new data available
942        self.communicator.statusBarUpdateSignal.emit(message)
943        self.communicator.fileDataReceivedSignal.emit(output_data)
944        self.manager.add_data(data_list=output_data)
945
946    def updateModel(self, data, p_file):
947        """
948        Add data and Info fields to the model item
949        """
950        # Structure of the model
951        # checkbox + basename
952        #     |-------> Data.D object
953        #     |-------> Info
954        #                 |----> Title:
955        #                 |----> Run:
956        #                 |----> Type:
957        #                 |----> Path:
958        #                 |----> Process
959        #                          |-----> process[0].name
960        #     |-------> THEORIES
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
972        checkbox_item.setChild(0, object_item)
973
974        # Add rows for display in the view
975        info_item = GuiUtils.infoFromData(data)
976
977        # Set info_item as the first child
978        checkbox_item.setChild(1, info_item)
979
980        # Caption for the theories
981        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
982
983        # New row in the model
984        self.model.appendRow(checkbox_item)
985
986
987    def updateModelFromPerspective(self, model_item):
988        """
989        Receive an update model item from a perspective
990        Make sure it is valid and if so, replace it in the model
991        """
992        # Assert the correct type
993        if not isinstance(model_item, QtGui.QStandardItem):
994            msg = "Wrong data type returned from calculations."
995            raise AttributeError, msg
996
997        # TODO: Assert other properties
998
999        # Reset the view
1000        self.model.reset()
1001        # Pass acting as a debugger anchor
1002        pass
1003
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
1016        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1017        #
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
1033
1034if __name__ == "__main__":
1035    app = QtGui.QApplication([])
1036    dlg = DataExplorerWindow()
1037    dlg.show()
1038    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.