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

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

Code cleanup and minor refactoring

  • Property mode set to 100644
File size: 36.8 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        plot2D = Plotter2D(self)
453        plot2D.item = item
454        plot2D.plot(plot_set)
455        self.addPlot(plot2D)
456        #============================================
457        ## Attach silx
458        #from silx.gui import qt
459        #from silx.gui.plot import StackView
460        #sv = StackView()
461        #sv.setColormap("jet", autoscale=True)
462        #sv.setStack(plot_set.data.reshape(1,100,100))
463        ##sv.setLabels(["x: -10 to 10 (200 samples)",
464        ##              "y: -10 to 5 (150 samples)"])
465        #sv.show()
466        #============================================
467
468    def plotData(self, plots):
469        """
470        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
471        """
472        # Call show on requested plots
473        # All same-type charts in one plot
474        new_plot = Plotter(self)
475
476        for item, plot_set in plots:
477            if isinstance(plot_set, Data1D):
478                new_plot.plot(plot_set)
479            elif isinstance(plot_set, Data2D):
480                self.addDataPlot2D(plot_set, item)
481            else:
482                msg = "Incorrect data type passed to Plotting"
483                raise AttributeError, msg
484
485        if plots and \
486            hasattr(new_plot, 'data') and \
487            isinstance(new_plot.data, Data1D):
488                self.addPlot(new_plot)
489
490    def newPlot(self):
491        """
492        Select checked data and plot it
493        """
494        # Check which tab is currently active
495        if self.current_view == self.treeView:
496            plots = GuiUtils.plotsFromCheckedItems(self.model)
497        else:
498            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
499
500        self.plotData(plots)
501
502    def addPlot(self, new_plot):
503        """
504        Helper method for plot bookkeeping
505        """
506        # Update the global plot counter
507        title = str(PlotHelper.idOfPlot(new_plot))
508        new_plot.setWindowTitle(title)
509
510        # Add the plot to the workspace
511        self.parent.workspace().addWindow(new_plot)
512
513        # Show the plot
514        new_plot.show()
515
516        # Update the active chart list
517        self.active_plots[new_plot.data.id] = new_plot
518
519    def appendPlot(self):
520        """
521        Add data set(s) to the existing matplotlib chart
522        """
523        # new plot data
524        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
525
526        # old plot data
527        plot_id = str(self.cbgraph.currentText())
528
529        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
530
531        old_plot = PlotHelper.plotById(plot_id)
532
533        # Add new data to the old plot, if data type is the same.
534        for _, plot_set in new_plots:
535            if type(plot_set) is type(old_plot._data):
536                old_plot.data = plot_set
537                old_plot.plot()
538
539    def updatePlot(self, new_data):
540        """
541        Modify existing plot for immediate response
542        """
543        data = new_data[0]
544        assert type(data).__name__ in ['Data1D', 'Data2D']
545
546        id = data.id
547        if data.id in self.active_plots.keys():
548            self.active_plots[id].replacePlot(id, data)
549
550    def chooseFiles(self):
551        """
552        Shows the Open file dialog and returns the chosen path(s)
553        """
554        # List of known extensions
555        wlist = self.getWlist()
556
557        # Location is automatically saved - no need to keep track of the last dir
558        # But only with Qt built-in dialog (non-platform native)
559        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
560                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
561        if paths is None:
562            return
563
564        if isinstance(paths, QtCore.QStringList):
565            paths = [str(f) for f in paths]
566
567        if not isinstance(paths, list):
568            paths = [paths]
569
570        return paths
571
572    def readData(self, path):
573        """
574        verbatim copy-paste from
575           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
576        slightly modified for clarity
577        """
578        message = ""
579        log_msg = ''
580        output = {}
581        any_error = False
582        data_error = False
583        error_message = ""
584        number_of_files = len(path)
585        self.communicator.progressBarUpdateSignal.emit(0.0)
586
587        for index, p_file in enumerate(path):
588            basename = os.path.basename(p_file)
589            _, extension = os.path.splitext(basename)
590            if extension.lower() in GuiUtils.EXTENSIONS:
591                any_error = True
592                log_msg = "Data Loader cannot "
593                log_msg += "load: %s\n" % str(p_file)
594                log_msg += """Please try to open that file from "open project" """
595                log_msg += """or "open analysis" menu\n"""
596                error_message = log_msg + "\n"
597                logging.info(log_msg)
598                continue
599
600            try:
601                message = "Loading Data... " + str(basename) + "\n"
602
603                # change this to signal notification in GuiManager
604                self.communicator.statusBarUpdateSignal.emit(message)
605
606                output_objects = self.loader.load(p_file)
607
608                # Some loaders return a list and some just a single Data1D object.
609                # Standardize.
610                if not isinstance(output_objects, list):
611                    output_objects = [output_objects]
612
613                for item in output_objects:
614                    # cast sascalc.dataloader.data_info.Data1D into
615                    # sasgui.guiframe.dataFitting.Data1D
616                    # TODO : Fix it
617                    new_data = self.manager.create_gui_data(item, p_file)
618                    output[new_data.id] = new_data
619
620                    # Model update should be protected
621                    self.mutex.lock()
622                    self.updateModel(new_data, p_file)
623                    self.model.reset()
624                    QtGui.qApp.processEvents()
625                    self.mutex.unlock()
626
627                    if hasattr(item, 'errors'):
628                        for error_data in item.errors:
629                            data_error = True
630                            message += "\tError: {0}\n".format(error_data)
631                    else:
632
633                        logging.error("Loader returned an invalid object:\n %s" % str(item))
634                        data_error = True
635
636            except Exception as ex:
637                logging.error(sys.exc_value)
638
639                any_error = True
640            if any_error or error_message != "":
641                if error_message == "":
642                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
643                    error += "while loading Data: \n%s\n" % str(basename)
644                    error_message += "The data file you selected could not be loaded.\n"
645                    error_message += "Make sure the content of your file"
646                    error_message += " is properly formatted.\n\n"
647                    error_message += "When contacting the SasView team, mention the"
648                    error_message += " following:\n%s" % str(error)
649                elif data_error:
650                    base_message = "Errors occurred while loading "
651                    base_message += "{0}\n".format(basename)
652                    base_message += "The data file loaded but with errors.\n"
653                    error_message = base_message + error_message
654                else:
655                    error_message += "%s\n" % str(p_file)
656
657            current_percentage = int(100.0* index/number_of_files)
658            self.communicator.progressBarUpdateSignal.emit(current_percentage)
659
660        if any_error or error_message:
661            logging.error(error_message)
662            status_bar_message = "Errors occurred while loading %s" % format(basename)
663            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
664
665        else:
666            message = "Loading Data Complete! "
667        message += log_msg
668        # Notify the progress bar that the updates are over.
669        self.communicator.progressBarUpdateSignal.emit(-1)
670        self.communicator.statusBarUpdateSignal.emit(message)
671
672        return output, message
673
674    def getWlist(self):
675        """
676        Wildcards of files we know the format of.
677        """
678        # Display the Qt Load File module
679        cards = self.loader.get_wildcards()
680
681        # get rid of the wx remnant in wildcards
682        # TODO: modify sasview loader get_wildcards method, after merge,
683        # so this kludge can be avoided
684        new_cards = []
685        for item in cards:
686            new_cards.append(item[:item.find("|")])
687        wlist = ';;'.join(new_cards)
688
689        return wlist
690
691    def selectData(self, index):
692        """
693        Callback method for modifying the TreeView on Selection Options change
694        """
695        if not isinstance(index, int):
696            msg = "Incorrect type passed to DataExplorer.selectData()"
697            raise AttributeError, msg
698
699        # Respond appropriately
700        if index == 0:
701            # Select All
702            for index in range(self.model.rowCount()):
703                item = self.model.item(index)
704                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
705                    item.setCheckState(QtCore.Qt.Checked)
706        elif index == 1:
707            # De-select All
708            for index in range(self.model.rowCount()):
709                item = self.model.item(index)
710                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
711                    item.setCheckState(QtCore.Qt.Unchecked)
712
713        elif index == 2:
714            # Select All 1-D
715            for index in range(self.model.rowCount()):
716                item = self.model.item(index)
717                item.setCheckState(QtCore.Qt.Unchecked)
718
719                try:
720                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
721                except AttributeError:
722                    msg = "Bad structure of the data model."
723                    raise RuntimeError, msg
724
725                if is1D:
726                    item.setCheckState(QtCore.Qt.Checked)
727
728        elif index == 3:
729            # Unselect All 1-D
730            for index in range(self.model.rowCount()):
731                item = self.model.item(index)
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 item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
740                    item.setCheckState(QtCore.Qt.Unchecked)
741
742        elif index == 4:
743            # Select All 2-D
744            for index in range(self.model.rowCount()):
745                item = self.model.item(index)
746                item.setCheckState(QtCore.Qt.Unchecked)
747                try:
748                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
749                except AttributeError:
750                    msg = "Bad structure of the data model."
751                    raise RuntimeError, msg
752
753                if is2D:
754                    item.setCheckState(QtCore.Qt.Checked)
755
756        elif index == 5:
757            # Unselect All 2-D
758            for index in range(self.model.rowCount()):
759                item = self.model.item(index)
760
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 item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
768                    item.setCheckState(QtCore.Qt.Unchecked)
769
770        else:
771            msg = "Incorrect value in the Selection Option"
772            # Change this to a proper logging action
773            raise Exception, msg
774
775    def contextMenu(self):
776        """
777        Define actions and layout of the right click context menu
778        """
779        # Create a custom menu based on actions defined in the UI file
780        self.context_menu = QtGui.QMenu(self)
781        self.context_menu.addAction(self.actionDataInfo)
782        self.context_menu.addAction(self.actionSaveAs)
783        self.context_menu.addAction(self.actionQuickPlot)
784        self.context_menu.addSeparator()
785        self.context_menu.addAction(self.actionQuick3DPlot)
786        self.context_menu.addAction(self.actionEditMask)
787
788        # Define the callbacks
789        self.actionDataInfo.triggered.connect(self.showDataInfo)
790        self.actionSaveAs.triggered.connect(self.saveDataAs)
791        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
792        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
793        self.actionEditMask.triggered.connect(self.showEditDataMask)
794
795    def onCustomContextMenu(self, position):
796        """
797        Show the right-click context menu in the data treeview
798        """
799        index = self.current_view.indexAt(position)
800        proxy = self.current_view.model()
801        model = proxy.sourceModel()
802
803        if index.isValid():
804            model_item = model.itemFromIndex(proxy.mapToSource(index))
805            # Find the mapped index
806            orig_index = model_item.isCheckable()
807            if orig_index:
808                # Check the data to enable/disable actions
809                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
810                self.actionQuick3DPlot.setEnabled(is_2D)
811                self.actionEditMask.setEnabled(is_2D)
812                # Fire up the menu
813                self.context_menu.exec_(self.current_view.mapToGlobal(position))
814
815    def showDataInfo(self):
816        """
817        Show a simple read-only text edit with data information.
818        """
819        index = self.current_view.selectedIndexes()[0]
820        proxy = self.current_view.model()
821        model = proxy.sourceModel()
822        model_item = model.itemFromIndex(proxy.mapToSource(index))
823
824        data = GuiUtils.dataFromItem(model_item)
825        if isinstance(data, Data1D):
826            text_to_show = GuiUtils.retrieveData1d(data)
827            # Hardcoded sizes to enable full width rendering with default font
828            self.txt_widget.resize(420,600)
829        else:
830            text_to_show = GuiUtils.retrieveData2d(data)
831            # Hardcoded sizes to enable full width rendering with default font
832            self.txt_widget.resize(700,600)
833
834        self.txt_widget.setReadOnly(True)
835        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
836        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
837        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
838        self.txt_widget.insertPlainText(text_to_show)
839
840        self.txt_widget.show()
841        # Move the slider all the way up, if present
842        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
843        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
844
845    def saveDataAs(self):
846        """
847        Save the data points as either txt or xml
848        """
849        index = self.current_view.selectedIndexes()[0]
850        proxy = self.current_view.model()
851        model = proxy.sourceModel()
852        model_item = model.itemFromIndex(proxy.mapToSource(index))
853
854        data = GuiUtils.dataFromItem(model_item)
855        if isinstance(data, Data1D):
856            GuiUtils.saveData1D(data)
857        else:
858            GuiUtils.saveData2D(data)
859
860    def quickDataPlot(self):
861        """
862        Frozen plot - display an image of the plot
863        """
864        index = self.current_view.selectedIndexes()[0]
865        proxy = self.current_view.model()
866        model = proxy.sourceModel()
867        model_item = model.itemFromIndex(proxy.mapToSource(index))
868
869        data = GuiUtils.dataFromItem(model_item)
870
871        method_name = 'Plotter'
872        if isinstance(data, Data2D):
873            method_name='Plotter2D'
874
875        new_plot = globals()[method_name](self, quickplot=True)
876        new_plot.data = data
877        #new_plot.plot(marker='o')
878        new_plot.plot()
879
880        # Update the global plot counter
881        title = "Plot " + data.name
882        new_plot.setWindowTitle(title)
883
884        # Show the plot
885        new_plot.show()
886
887    def quickData3DPlot(self):
888        """
889        Slowish 3D plot
890        """
891        index = self.current_view.selectedIndexes()[0]
892        proxy = self.current_view.model()
893        model = proxy.sourceModel()
894        model_item = model.itemFromIndex(proxy.mapToSource(index))
895
896        data = GuiUtils.dataFromItem(model_item)
897
898        new_plot = Plotter2D(self, quickplot=True, dimension=3)
899        new_plot.data = data
900        new_plot.plot()
901
902        # Update the global plot counter
903        title = "Plot " + data.name
904        new_plot.setWindowTitle(title)
905
906        # Show the plot
907        new_plot.show()
908
909    def showEditDataMask(self):
910        """
911        Mask Editor for 2D plots
912        """
913        index = self.current_view.selectedIndexes()[0]
914        proxy = self.current_view.model()
915        model = proxy.sourceModel()
916        model_item = model.itemFromIndex(proxy.mapToSource(index))
917
918        data = GuiUtils.dataFromItem(model_item)
919
920        mask_editor = MaskEditor(self, data)
921        # Modal dialog here.
922        mask_editor.exec_()
923
924    def loadComplete(self, output):
925        """
926        Post message to status bar and update the data manager
927        """
928        assert isinstance(output, tuple)
929
930        # Reset the model so the view gets updated.
931        self.model.reset()
932        self.communicator.progressBarUpdateSignal.emit(-1)
933
934        output_data = output[0]
935        message = output[1]
936        # Notify the manager of the new data available
937        self.communicator.statusBarUpdateSignal.emit(message)
938        self.communicator.fileDataReceivedSignal.emit(output_data)
939        self.manager.add_data(data_list=output_data)
940
941    def updateModel(self, data, p_file):
942        """
943        Add data and Info fields to the model item
944        """
945        # Structure of the model
946        # checkbox + basename
947        #     |-------> Data.D object
948        #     |-------> Info
949        #                 |----> Title:
950        #                 |----> Run:
951        #                 |----> Type:
952        #                 |----> Path:
953        #                 |----> Process
954        #                          |-----> process[0].name
955        #     |-------> THEORIES
956
957        # Top-level item: checkbox with label
958        checkbox_item = QtGui.QStandardItem(True)
959        checkbox_item.setCheckable(True)
960        checkbox_item.setCheckState(QtCore.Qt.Checked)
961        checkbox_item.setText(os.path.basename(p_file))
962
963        # Add the actual Data1D/Data2D object
964        object_item = QtGui.QStandardItem()
965        object_item.setData(QtCore.QVariant(data))
966
967        checkbox_item.setChild(0, object_item)
968
969        # Add rows for display in the view
970        info_item = GuiUtils.infoFromData(data)
971
972        # Set info_item as the first child
973        checkbox_item.setChild(1, info_item)
974
975        # Caption for the theories
976        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
977
978        # New row in the model
979        self.model.appendRow(checkbox_item)
980
981
982    def updateModelFromPerspective(self, model_item):
983        """
984        Receive an update model item from a perspective
985        Make sure it is valid and if so, replace it in the model
986        """
987        # Assert the correct type
988        if not isinstance(model_item, QtGui.QStandardItem):
989            msg = "Wrong data type returned from calculations."
990            raise AttributeError, msg
991
992        # TODO: Assert other properties
993
994        # Reset the view
995        self.model.reset()
996        # Pass acting as a debugger anchor
997        pass
998
999    def updateTheoryFromPerspective(self, model_item):
1000        """
1001        Receive an update theory item from a perspective
1002        Make sure it is valid and if so, replace/add in the model
1003        """
1004        # Assert the correct type
1005        if not isinstance(model_item, QtGui.QStandardItem):
1006            msg = "Wrong data type returned from calculations."
1007            raise AttributeError, msg
1008
1009        # Check if there are any other items for this tab
1010        # If so, delete them
1011        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1012        #
1013        current_tab_name = model_item.text()[:2]
1014        for current_index in xrange(self.theory_model.rowCount()):
1015            if current_tab_name in self.theory_model.item(current_index).text():
1016                self.theory_model.removeRow(current_index)
1017                break
1018
1019        # Reset the view
1020        self.model.reset()
1021
1022        # Reset the view
1023        self.theory_model.appendRow(model_item)
1024
1025        # Pass acting as a debugger anchor
1026        pass
1027
1028
1029if __name__ == "__main__":
1030    app = QtGui.QApplication([])
1031    dlg = DataExplorerWindow()
1032    dlg.show()
1033    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.