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

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

Implemented comments from review for Data Operation Panel (ESS-GUI-SasView?-245)

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