source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 38eb433

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

SASVIEW-644: mapping between datasets and fit tabs

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