source: sasview/src/sas/qtgui/DataExplorer.py @ 4d457df

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

QModel items conversion into SasModel? parameters + data display SASVIEW-535

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