source: sasview/src/sas/qtgui/DataExplorer.py @ d7ff531

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

Allow for multiple datasets to be opened by the fitting perspective

  • Property mode set to 100644
File size: 35.3 KB
Line 
1# global
2import sys
3import os
4import time
5import logging
6
7from PyQt4 import QtCore
8from PyQt4 import QtGui
9from PyQt4 import QtWebKit
10from PyQt4.Qt import QMutex
11
12from twisted.internet import threads
13
14# 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        self.communicator.statusBarUpdateSignal.emit(message)
630
631        return output, message
632
633    def getWlist(self):
634        """
635        Wildcards of files we know the format of.
636        """
637        # Display the Qt Load File module
638        cards = self.loader.get_wildcards()
639
640        # get rid of the wx remnant in wildcards
641        # TODO: modify sasview loader get_wildcards method, after merge,
642        # so this kludge can be avoided
643        new_cards = []
644        for item in cards:
645            new_cards.append(item[:item.find("|")])
646        wlist = ';;'.join(new_cards)
647
648        return wlist
649
650    def selectData(self, index):
651        """
652        Callback method for modifying the TreeView on Selection Options change
653        """
654        if not isinstance(index, int):
655            msg = "Incorrect type passed to DataExplorer.selectData()"
656            raise AttributeError, msg
657
658        # Respond appropriately
659        if index == 0:
660            # Select All
661            for index in range(self.model.rowCount()):
662                item = self.model.item(index)
663                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
664                    item.setCheckState(QtCore.Qt.Checked)
665        elif index == 1:
666            # De-select All
667            for index in range(self.model.rowCount()):
668                item = self.model.item(index)
669                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
670                    item.setCheckState(QtCore.Qt.Unchecked)
671
672        elif index == 2:
673            # Select All 1-D
674            for index in range(self.model.rowCount()):
675                item = self.model.item(index)
676                item.setCheckState(QtCore.Qt.Unchecked)
677
678                try:
679                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
680                except AttributeError:
681                    msg = "Bad structure of the data model."
682                    raise RuntimeError, msg
683
684                if is1D:
685                    item.setCheckState(QtCore.Qt.Checked)
686
687        elif index == 3:
688            # Unselect All 1-D
689            for index in range(self.model.rowCount()):
690                item = self.model.item(index)
691
692                try:
693                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
694                except AttributeError:
695                    msg = "Bad structure of the data model."
696                    raise RuntimeError, msg
697
698                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
699                    item.setCheckState(QtCore.Qt.Unchecked)
700
701        elif index == 4:
702            # Select All 2-D
703            for index in range(self.model.rowCount()):
704                item = self.model.item(index)
705                item.setCheckState(QtCore.Qt.Unchecked)
706                try:
707                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
708                except AttributeError:
709                    msg = "Bad structure of the data model."
710                    raise RuntimeError, msg
711
712                if is2D:
713                    item.setCheckState(QtCore.Qt.Checked)
714
715        elif index == 5:
716            # Unselect All 2-D
717            for index in range(self.model.rowCount()):
718                item = self.model.item(index)
719
720                try:
721                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
722                except AttributeError:
723                    msg = "Bad structure of the data model."
724                    raise RuntimeError, msg
725
726                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
727                    item.setCheckState(QtCore.Qt.Unchecked)
728
729        else:
730            msg = "Incorrect value in the Selection Option"
731            # Change this to a proper logging action
732            raise Exception, msg
733
734    def contextMenu(self):
735        """
736        Define actions and layout of the right click context menu
737        """
738        # Create a custom menu based on actions defined in the UI file
739        self.context_menu = QtGui.QMenu(self)
740        self.context_menu.addAction(self.actionDataInfo)
741        self.context_menu.addAction(self.actionSaveAs)
742        self.context_menu.addAction(self.actionQuickPlot)
743        self.context_menu.addSeparator()
744        self.context_menu.addAction(self.actionQuick3DPlot)
745        self.context_menu.addAction(self.actionEditMask)
746
747        # Define the callbacks
748        self.actionDataInfo.triggered.connect(self.showDataInfo)
749        self.actionSaveAs.triggered.connect(self.saveDataAs)
750        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
751        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
752        self.actionEditMask.triggered.connect(self.showEditDataMask)
753
754    def onCustomContextMenu(self, position):
755        """
756        Show the right-click context menu in the data treeview
757        """
758        index = self.current_view.indexAt(position)
759        proxy = self.current_view.model()
760        model = proxy.sourceModel()
761
762        if index.isValid():
763            model_item = model.itemFromIndex(proxy.mapToSource(index))
764            # Find the mapped index
765            orig_index = model_item.isCheckable()
766            if orig_index:
767                # Check the data to enable/disable actions
768                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
769                self.actionQuick3DPlot.setEnabled(is_2D)
770                self.actionEditMask.setEnabled(is_2D)
771                # Fire up the menu
772                self.context_menu.exec_(self.current_view.mapToGlobal(position))
773
774    def showDataInfo(self):
775        """
776        Show a simple read-only text edit with data information.
777        """
778        index = self.current_view.selectedIndexes()[0]
779        proxy = self.current_view.model()
780        model = proxy.sourceModel()
781        model_item = model.itemFromIndex(proxy.mapToSource(index))
782
783        data = GuiUtils.dataFromItem(model_item)
784        if isinstance(data, Data1D):
785            text_to_show = GuiUtils.retrieveData1d(data)
786            # Hardcoded sizes to enable full width rendering with default font
787            self.txt_widget.resize(420,600)
788        else:
789            text_to_show = GuiUtils.retrieveData2d(data)
790            # Hardcoded sizes to enable full width rendering with default font
791            self.txt_widget.resize(700,600)
792
793        self.txt_widget.setReadOnly(True)
794        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
795        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
796        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
797        self.txt_widget.insertPlainText(text_to_show)
798
799        self.txt_widget.show()
800        # Move the slider all the way up, if present
801        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
802        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
803
804    def saveDataAs(self):
805        """
806        Save the data points as either txt or xml
807        """
808        index = self.current_view.selectedIndexes()[0]
809        proxy = self.current_view.model()
810        model = proxy.sourceModel()
811        model_item = model.itemFromIndex(proxy.mapToSource(index))
812
813        data = GuiUtils.dataFromItem(model_item)
814        if isinstance(data, Data1D):
815            GuiUtils.saveData1D(data)
816        else:
817            GuiUtils.saveData2D(data)
818
819    def quickDataPlot(self):
820        """
821        Frozen plot - display an image of the plot
822        """
823        index = self.current_view.selectedIndexes()[0]
824        proxy = self.current_view.model()
825        model = proxy.sourceModel()
826        model_item = model.itemFromIndex(proxy.mapToSource(index))
827
828        data = GuiUtils.dataFromItem(model_item)
829
830        method_name = 'Plotter'
831        if isinstance(data, Data2D):
832            method_name='Plotter2D'
833
834        new_plot = globals()[method_name](self, quickplot=True)
835        new_plot.data = data
836        #new_plot.plot(marker='o')
837        new_plot.plot()
838
839        # Update the global plot counter
840        title = "Plot " + data.name
841        new_plot.setWindowTitle(title)
842
843        # Show the plot
844        new_plot.show()
845
846    def quickData3DPlot(self):
847        """
848        Slowish 3D plot
849        """
850        index = self.current_view.selectedIndexes()[0]
851        proxy = self.current_view.model()
852        model = proxy.sourceModel()
853        model_item = model.itemFromIndex(proxy.mapToSource(index))
854
855        data = GuiUtils.dataFromItem(model_item)
856
857        new_plot = Plotter2D(self, quickplot=True, dimension=3)
858        new_plot.data = data
859        new_plot.plot()
860
861        # Update the global plot counter
862        title = "Plot " + data.name
863        new_plot.setWindowTitle(title)
864
865        # Show the plot
866        new_plot.show()
867
868    def showEditDataMask(self):
869        """
870        Mask Editor for 2D plots
871        """
872        index = self.current_view.selectedIndexes()[0]
873        proxy = self.current_view.model()
874        model = proxy.sourceModel()
875        model_item = model.itemFromIndex(proxy.mapToSource(index))
876
877        data = GuiUtils.dataFromItem(model_item)
878
879        mask_editor = MaskEditor(self, data)
880        # Modal dialog here.
881        mask_editor.exec_()
882
883    def loadComplete(self, output):
884        """
885        Post message to status bar and update the data manager
886        """
887        assert isinstance(output, tuple)
888
889        # Reset the model so the view gets updated.
890        self.model.reset()
891        self.communicator.progressBarUpdateSignal.emit(-1)
892
893        output_data = output[0]
894        message = output[1]
895        # Notify the manager of the new data available
896        self.communicator.statusBarUpdateSignal.emit(message)
897        self.communicator.fileDataReceivedSignal.emit(output_data)
898        self.manager.add_data(data_list=output_data)
899
900    def updateModel(self, data, p_file):
901        """
902        Add data and Info fields to the model item
903        """
904        # Structure of the model
905        # checkbox + basename
906        #     |-------> Data.D object
907        #     |-------> Info
908        #                 |----> Title:
909        #                 |----> Run:
910        #                 |----> Type:
911        #                 |----> Path:
912        #                 |----> Process
913        #                          |-----> process[0].name
914        #     |-------> THEORIES
915
916        # Top-level item: checkbox with label
917        checkbox_item = QtGui.QStandardItem(True)
918        checkbox_item.setCheckable(True)
919        checkbox_item.setCheckState(QtCore.Qt.Checked)
920        checkbox_item.setText(os.path.basename(p_file))
921
922        # Add the actual Data1D/Data2D object
923        object_item = QtGui.QStandardItem()
924        object_item.setData(QtCore.QVariant(data))
925
926        checkbox_item.setChild(0, object_item)
927
928        # Add rows for display in the view
929        info_item = GuiUtils.infoFromData(data)
930
931        # Set info_item as the first child
932        checkbox_item.setChild(1, info_item)
933
934        # Caption for the theories
935        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
936
937        # New row in the model
938        self.model.appendRow(checkbox_item)
939
940
941    def updateModelFromPerspective(self, model_item):
942        """
943        Receive an update model item from a perspective
944        Make sure it is valid and if so, replace it in the model
945        """
946        # Assert the correct type
947        if not isinstance(model_item, QtGui.QStandardItem):
948            msg = "Wrong data type returned from calculations."
949            raise AttributeError, msg
950
951        # TODO: Assert other properties
952
953        # Reset the view
954        self.model.reset()
955        # Pass acting as a debugger anchor
956        pass
957
958    def updateTheoryFromPerspective(self, model_item):
959        """
960        Receive an update theory item from a perspective
961        Make sure it is valid and if so, replace/add in the model
962        """
963        # Assert the correct type
964        if not isinstance(model_item, QtGui.QStandardItem):
965            msg = "Wrong data type returned from calculations."
966            raise AttributeError, msg
967
968        # Check if there are any other items for this tab
969        # If so, delete them
970        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
971        #
972        current_tab_name = model_item.text()[:2]
973        for current_index in xrange(self.theory_model.rowCount()):
974            if current_tab_name in self.theory_model.item(current_index).text():
975                self.theory_model.removeRow(current_index)
976                break
977
978        # Reset the view
979        self.model.reset()
980
981        # Reset the view
982        self.theory_model.appendRow(model_item)
983
984        # Pass acting as a debugger anchor
985        pass
986
987
988if __name__ == "__main__":
989    app = QtGui.QApplication([])
990    dlg = DataExplorerWindow()
991    dlg.show()
992    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.