source: sasview/src/sas/qtgui/DataExplorer.py @ 363fbfa

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 363fbfa was 8548d739, checked in by Piotr Rozyczko <rozyczko@…>, 8 years ago

Further work on the main QStandardItemModel

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