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

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

Corrected problem with update of loaded files in Data Operation Panel (ESS-GUI-SasView245)

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