source: sasview/src/sas/qtgui/DataExplorer.py @ 9f25bce

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

Towards more 1D plots responding to data change.
Minor bug fixes.

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