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

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 d5c5d3d was d5c5d3d, checked in by celinedurniak <celine.durniak@…>, 7 years ago

Implemented new GUI for data operation panel

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