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

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

Default datasets for fitting SASVIEW-498

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