source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 83eb5208

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

Putting files in more ordered fashion

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