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

Last change on this file since 4a9786d8 was 88e1f57, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Fixed chart update upon consecutive fitting.
Fixed 1D default charts - now only two (combined data/fit + residuals)
Minor text fixes

  • Property mode set to 100644
File size: 38.6 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        plot_to_show = data_list[0]
466
467        # passed plot is used ONLY to figure out its title,
468        # so all the charts related by it can be pulled from
469        # the data explorer indices.
470        filename = plot_to_show.filename
471        model = self.model if plot_to_show.is_data else self.theory_model
472
473        # Now query the model item for available plots
474        plots = GuiUtils.plotsFromFilename(filename, model)
475        item = GuiUtils.itemFromFilename(filename, model)
476
477        new_plots = []
478        for plot in plots:
479            plot_id = plot.id
480            if plot_id in self.active_plots.keys():
481                self.active_plots[plot_id].replacePlot(plot_id, plot)
482            else:
483                # 'sophisticated' test to generate standalone plot for residuals
484                if 'esiduals' in plot.title:
485                    self.plotData([(item, plot)])
486                else:
487                    new_plots.append((item, plot))
488
489        if new_plots:
490            self.plotData(new_plots)
491
492    def addDataPlot2D(self, plot_set, item):
493        """
494        Create a new 2D plot and add it to the workspace
495        """
496        plot2D = Plotter2D(self)
497        plot2D.item = item
498        plot2D.plot(plot_set)
499        self.addPlot(plot2D)
500        self.active_plots[plot2D.data.id] = plot2D
501        #============================================
502        # Experimental hook for silx charts
503        #============================================
504        ## Attach silx
505        #from silx.gui import qt
506        #from silx.gui.plot import StackView
507        #sv = StackView()
508        #sv.setColormap("jet", autoscale=True)
509        #sv.setStack(plot_set.data.reshape(1,100,100))
510        ##sv.setLabels(["x: -10 to 10 (200 samples)",
511        ##              "y: -10 to 5 (150 samples)"])
512        #sv.show()
513        #============================================
514
515    def plotData(self, plots):
516        """
517        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
518        """
519        # Call show on requested plots
520        # All same-type charts in one plot
521        for item, plot_set in plots:
522            if isinstance(plot_set, Data1D):
523                if not 'new_plot' in locals():
524                    new_plot = Plotter(self)
525                new_plot.plot(plot_set)
526                # active_plots may contain multiple charts
527                self.active_plots[plot_set.id] = new_plot
528            elif isinstance(plot_set, Data2D):
529                self.addDataPlot2D(plot_set, item)
530            else:
531                msg = "Incorrect data type passed to Plotting"
532                raise AttributeError, msg
533
534        if 'new_plot' in locals() and \
535            hasattr(new_plot, 'data') and \
536            isinstance(new_plot.data, Data1D):
537                self.addPlot(new_plot)
538
539    def newPlot(self):
540        """
541        Select checked data and plot it
542        """
543        # Check which tab is currently active
544        if self.current_view == self.treeView:
545            plots = GuiUtils.plotsFromCheckedItems(self.model)
546        else:
547            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
548
549        self.plotData(plots)
550
551    def addPlot(self, new_plot):
552        """
553        Helper method for plot bookkeeping
554        """
555        # Update the global plot counter
556        title = str(PlotHelper.idOfPlot(new_plot))
557        new_plot.setWindowTitle(title)
558
559        # Set the object name to satisfy the Squish object picker
560        new_plot.setObjectName(title)
561
562        # Add the plot to the workspace
563        self.parent.workspace().addWindow(new_plot)
564
565        # Show the plot
566        new_plot.show()
567
568        # Update the active chart list
569        #self.active_plots[new_plot.data.id] = new_plot
570
571    def appendPlot(self):
572        """
573        Add data set(s) to the existing matplotlib chart
574        """
575        # new plot data
576        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
577
578        # old plot data
579        plot_id = str(self.cbgraph.currentText())
580
581        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
582
583        old_plot = PlotHelper.plotById(plot_id)
584
585        # Add new data to the old plot, if data type is the same.
586        for _, plot_set in new_plots:
587            if type(plot_set) is type(old_plot._data):
588                old_plot.data = plot_set
589                old_plot.plot()
590
591    def updatePlot(self, new_data):
592        """
593        Modify existing plot for immediate response
594        """
595        data = new_data[0]
596        assert type(data).__name__ in ['Data1D', 'Data2D']
597
598        id = data.id
599        if data.id in self.active_plots.keys():
600            self.active_plots[id].replacePlot(id, data)
601
602    def chooseFiles(self):
603        """
604        Shows the Open file dialog and returns the chosen path(s)
605        """
606        # List of known extensions
607        wlist = self.getWlist()
608
609        # Location is automatically saved - no need to keep track of the last dir
610        # But only with Qt built-in dialog (non-platform native)
611        paths = QtGui.QFileDialog.getOpenFileNames(self, "Choose a file", "",
612                wlist, None, QtGui.QFileDialog.DontUseNativeDialog)
613        if paths is None:
614            return
615
616        if isinstance(paths, QtCore.QStringList):
617            paths = [str(f) for f in paths]
618
619        if not isinstance(paths, list):
620            paths = [paths]
621
622        return paths
623
624    def readData(self, path):
625        """
626        verbatim copy-paste from
627           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
628        slightly modified for clarity
629        """
630        message = ""
631        log_msg = ''
632        output = {}
633        any_error = False
634        data_error = False
635        error_message = ""
636        number_of_files = len(path)
637        self.communicator.progressBarUpdateSignal.emit(0.0)
638
639        for index, p_file in enumerate(path):
640            basename = os.path.basename(p_file)
641            _, extension = os.path.splitext(basename)
642            if extension.lower() in GuiUtils.EXTENSIONS:
643                any_error = True
644                log_msg = "Data Loader cannot "
645                log_msg += "load: %s\n" % str(p_file)
646                log_msg += """Please try to open that file from "open project" """
647                log_msg += """or "open analysis" menu\n"""
648                error_message = log_msg + "\n"
649                logging.info(log_msg)
650                continue
651
652            try:
653                message = "Loading Data... " + str(basename) + "\n"
654
655                # change this to signal notification in GuiManager
656                self.communicator.statusBarUpdateSignal.emit(message)
657
658                output_objects = self.loader.load(p_file)
659
660                # Some loaders return a list and some just a single Data1D object.
661                # Standardize.
662                if not isinstance(output_objects, list):
663                    output_objects = [output_objects]
664
665                for item in output_objects:
666                    # cast sascalc.dataloader.data_info.Data1D into
667                    # sasgui.guiframe.dataFitting.Data1D
668                    # TODO : Fix it
669                    new_data = self.manager.create_gui_data(item, p_file)
670                    output[new_data.id] = new_data
671
672                    # Model update should be protected
673                    self.mutex.lock()
674                    self.updateModel(new_data, p_file)
675                    self.model.reset()
676                    QtGui.qApp.processEvents()
677                    self.mutex.unlock()
678
679                    if hasattr(item, 'errors'):
680                        for error_data in item.errors:
681                            data_error = True
682                            message += "\tError: {0}\n".format(error_data)
683                    else:
684
685                        logging.error("Loader returned an invalid object:\n %s" % str(item))
686                        data_error = True
687
688            except Exception as ex:
689                logging.error(sys.exc_value)
690
691                any_error = True
692            if any_error or error_message != "":
693                if error_message == "":
694                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
695                    error += "while loading Data: \n%s\n" % str(basename)
696                    error_message += "The data file you selected could not be loaded.\n"
697                    error_message += "Make sure the content of your file"
698                    error_message += " is properly formatted.\n\n"
699                    error_message += "When contacting the SasView team, mention the"
700                    error_message += " following:\n%s" % str(error)
701                elif data_error:
702                    base_message = "Errors occurred while loading "
703                    base_message += "{0}\n".format(basename)
704                    base_message += "The data file loaded but with errors.\n"
705                    error_message = base_message + error_message
706                else:
707                    error_message += "%s\n" % str(p_file)
708
709            current_percentage = int(100.0* index/number_of_files)
710            self.communicator.progressBarUpdateSignal.emit(current_percentage)
711
712        if any_error or error_message:
713            logging.error(error_message)
714            status_bar_message = "Errors occurred while loading %s" % format(basename)
715            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
716
717        else:
718            message = "Loading Data Complete! "
719        message += log_msg
720        # Notify the progress bar that the updates are over.
721        self.communicator.progressBarUpdateSignal.emit(-1)
722        self.communicator.statusBarUpdateSignal.emit(message)
723
724        return output, message
725
726    def getWlist(self):
727        """
728        Wildcards of files we know the format of.
729        """
730        # Display the Qt Load File module
731        cards = self.loader.get_wildcards()
732
733        # get rid of the wx remnant in wildcards
734        # TODO: modify sasview loader get_wildcards method, after merge,
735        # so this kludge can be avoided
736        new_cards = []
737        for item in cards:
738            new_cards.append(item[:item.find("|")])
739        wlist = ';;'.join(new_cards)
740
741        return wlist
742
743    def selectData(self, index):
744        """
745        Callback method for modifying the TreeView on Selection Options change
746        """
747        if not isinstance(index, int):
748            msg = "Incorrect type passed to DataExplorer.selectData()"
749            raise AttributeError, msg
750
751        # Respond appropriately
752        if index == 0:
753            # Select All
754            for index in range(self.model.rowCount()):
755                item = self.model.item(index)
756                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
757                    item.setCheckState(QtCore.Qt.Checked)
758        elif index == 1:
759            # De-select All
760            for index in range(self.model.rowCount()):
761                item = self.model.item(index)
762                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
763                    item.setCheckState(QtCore.Qt.Unchecked)
764
765        elif index == 2:
766            # Select All 1-D
767            for index in range(self.model.rowCount()):
768                item = self.model.item(index)
769                item.setCheckState(QtCore.Qt.Unchecked)
770
771                try:
772                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
773                except AttributeError:
774                    msg = "Bad structure of the data model."
775                    raise RuntimeError, msg
776
777                if is1D:
778                    item.setCheckState(QtCore.Qt.Checked)
779
780        elif index == 3:
781            # Unselect All 1-D
782            for index in range(self.model.rowCount()):
783                item = self.model.item(index)
784
785                try:
786                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
787                except AttributeError:
788                    msg = "Bad structure of the data model."
789                    raise RuntimeError, msg
790
791                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
792                    item.setCheckState(QtCore.Qt.Unchecked)
793
794        elif index == 4:
795            # Select All 2-D
796            for index in range(self.model.rowCount()):
797                item = self.model.item(index)
798                item.setCheckState(QtCore.Qt.Unchecked)
799                try:
800                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
801                except AttributeError:
802                    msg = "Bad structure of the data model."
803                    raise RuntimeError, msg
804
805                if is2D:
806                    item.setCheckState(QtCore.Qt.Checked)
807
808        elif index == 5:
809            # Unselect All 2-D
810            for index in range(self.model.rowCount()):
811                item = self.model.item(index)
812
813                try:
814                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
815                except AttributeError:
816                    msg = "Bad structure of the data model."
817                    raise RuntimeError, msg
818
819                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
820                    item.setCheckState(QtCore.Qt.Unchecked)
821
822        else:
823            msg = "Incorrect value in the Selection Option"
824            # Change this to a proper logging action
825            raise Exception, msg
826
827    def contextMenu(self):
828        """
829        Define actions and layout of the right click context menu
830        """
831        # Create a custom menu based on actions defined in the UI file
832        self.context_menu = QtGui.QMenu(self)
833        self.context_menu.addAction(self.actionDataInfo)
834        self.context_menu.addAction(self.actionSaveAs)
835        self.context_menu.addAction(self.actionQuickPlot)
836        self.context_menu.addSeparator()
837        self.context_menu.addAction(self.actionQuick3DPlot)
838        self.context_menu.addAction(self.actionEditMask)
839
840        # Define the callbacks
841        self.actionDataInfo.triggered.connect(self.showDataInfo)
842        self.actionSaveAs.triggered.connect(self.saveDataAs)
843        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
844        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
845        self.actionEditMask.triggered.connect(self.showEditDataMask)
846
847    def onCustomContextMenu(self, position):
848        """
849        Show the right-click context menu in the data treeview
850        """
851        index = self.current_view.indexAt(position)
852        proxy = self.current_view.model()
853        model = proxy.sourceModel()
854
855        if index.isValid():
856            model_item = model.itemFromIndex(proxy.mapToSource(index))
857            # Find the mapped index
858            orig_index = model_item.isCheckable()
859            if orig_index:
860                # Check the data to enable/disable actions
861                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
862                self.actionQuick3DPlot.setEnabled(is_2D)
863                self.actionEditMask.setEnabled(is_2D)
864                # Fire up the menu
865                self.context_menu.exec_(self.current_view.mapToGlobal(position))
866
867    def showDataInfo(self):
868        """
869        Show a simple read-only text edit with data information.
870        """
871        index = self.current_view.selectedIndexes()[0]
872        proxy = self.current_view.model()
873        model = proxy.sourceModel()
874        model_item = model.itemFromIndex(proxy.mapToSource(index))
875
876        data = GuiUtils.dataFromItem(model_item)
877        if isinstance(data, Data1D):
878            text_to_show = GuiUtils.retrieveData1d(data)
879            # Hardcoded sizes to enable full width rendering with default font
880            self.txt_widget.resize(420,600)
881        else:
882            text_to_show = GuiUtils.retrieveData2d(data)
883            # Hardcoded sizes to enable full width rendering with default font
884            self.txt_widget.resize(700,600)
885
886        self.txt_widget.setReadOnly(True)
887        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
888        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
889        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
890        self.txt_widget.clear()
891        self.txt_widget.insertPlainText(text_to_show)
892
893        self.txt_widget.show()
894        # Move the slider all the way up, if present
895        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
896        vertical_scroll_bar.triggerAction(QtGui.QScrollBar.SliderToMinimum)
897
898    def saveDataAs(self):
899        """
900        Save the data points as either txt or xml
901        """
902        index = self.current_view.selectedIndexes()[0]
903        proxy = self.current_view.model()
904        model = proxy.sourceModel()
905        model_item = model.itemFromIndex(proxy.mapToSource(index))
906
907        data = GuiUtils.dataFromItem(model_item)
908        if isinstance(data, Data1D):
909            GuiUtils.saveData1D(data)
910        else:
911            GuiUtils.saveData2D(data)
912
913    def quickDataPlot(self):
914        """
915        Frozen plot - display an image of the plot
916        """
917        index = self.current_view.selectedIndexes()[0]
918        proxy = self.current_view.model()
919        model = proxy.sourceModel()
920        model_item = model.itemFromIndex(proxy.mapToSource(index))
921
922        data = GuiUtils.dataFromItem(model_item)
923
924        method_name = 'Plotter'
925        if isinstance(data, Data2D):
926            method_name='Plotter2D'
927
928        new_plot = globals()[method_name](self, quickplot=True)
929        new_plot.data = data
930        #new_plot.plot(marker='o')
931        new_plot.plot()
932
933        # Update the global plot counter
934        title = "Plot " + data.name
935        new_plot.setWindowTitle(title)
936
937        # Show the plot
938        new_plot.show()
939
940    def quickData3DPlot(self):
941        """
942        Slowish 3D plot
943        """
944        index = self.current_view.selectedIndexes()[0]
945        proxy = self.current_view.model()
946        model = proxy.sourceModel()
947        model_item = model.itemFromIndex(proxy.mapToSource(index))
948
949        data = GuiUtils.dataFromItem(model_item)
950
951        new_plot = Plotter2D(self, quickplot=True, dimension=3)
952        new_plot.data = data
953        new_plot.plot()
954
955        # Update the global plot counter
956        title = "Plot " + data.name
957        new_plot.setWindowTitle(title)
958
959        # Show the plot
960        new_plot.show()
961
962    def showEditDataMask(self):
963        """
964        Mask Editor for 2D plots
965        """
966        index = self.current_view.selectedIndexes()[0]
967        proxy = self.current_view.model()
968        model = proxy.sourceModel()
969        model_item = model.itemFromIndex(proxy.mapToSource(index))
970
971        data = GuiUtils.dataFromItem(model_item)
972
973        mask_editor = MaskEditor(self, data)
974        # Modal dialog here.
975        mask_editor.exec_()
976
977    def loadComplete(self, output):
978        """
979        Post message to status bar and update the data manager
980        """
981        assert isinstance(output, tuple)
982
983        # Reset the model so the view gets updated.
984        self.model.reset()
985        self.communicator.progressBarUpdateSignal.emit(-1)
986
987        output_data = output[0]
988        message = output[1]
989        # Notify the manager of the new data available
990        self.communicator.statusBarUpdateSignal.emit(message)
991        self.communicator.fileDataReceivedSignal.emit(output_data)
992        self.manager.add_data(data_list=output_data)
993
994    def updateModel(self, data, p_file):
995        """
996        Add data and Info fields to the model item
997        """
998        # Structure of the model
999        # checkbox + basename
1000        #     |-------> Data.D object
1001        #     |-------> Info
1002        #                 |----> Title:
1003        #                 |----> Run:
1004        #                 |----> Type:
1005        #                 |----> Path:
1006        #                 |----> Process
1007        #                          |-----> process[0].name
1008        #     |-------> THEORIES
1009
1010        # Top-level item: checkbox with label
1011        checkbox_item = QtGui.QStandardItem(True)
1012        checkbox_item.setCheckable(True)
1013        checkbox_item.setCheckState(QtCore.Qt.Checked)
1014        checkbox_item.setText(os.path.basename(p_file))
1015
1016        # Add the actual Data1D/Data2D object
1017        object_item = QtGui.QStandardItem()
1018        object_item.setData(QtCore.QVariant(data))
1019
1020        checkbox_item.setChild(0, object_item)
1021
1022        # Add rows for display in the view
1023        info_item = GuiUtils.infoFromData(data)
1024
1025        # Set info_item as the first child
1026        checkbox_item.setChild(1, info_item)
1027
1028        # Caption for the theories
1029        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1030
1031        # New row in the model
1032        self.model.appendRow(checkbox_item)
1033
1034    def updateModelFromPerspective(self, model_item):
1035        """
1036        Receive an update model item from a perspective
1037        Make sure it is valid and if so, replace it in the model
1038        """
1039        # Assert the correct type
1040        if not isinstance(model_item, QtGui.QStandardItem):
1041            msg = "Wrong data type returned from calculations."
1042            raise AttributeError, msg
1043
1044        # TODO: Assert other properties
1045
1046        # Reset the view
1047        self.model.reset()
1048        # Pass acting as a debugger anchor
1049        pass
1050
1051    def updateTheoryFromPerspective(self, model_item):
1052        """
1053        Receive an update theory item from a perspective
1054        Make sure it is valid and if so, replace/add in the model
1055        """
1056        # Assert the correct type
1057        if not isinstance(model_item, QtGui.QStandardItem):
1058            msg = "Wrong data type returned from calculations."
1059            raise AttributeError, msg
1060
1061        # Check if there are any other items for this tab
1062        # If so, delete them
1063        # TODO: fix this to resemble GuiUtils.updateModelItemWithPlot
1064        #
1065        current_tab_name = model_item.text()[:2]
1066        for current_index in xrange(self.theory_model.rowCount()):
1067            if current_tab_name in self.theory_model.item(current_index).text():
1068                self.theory_model.removeRow(current_index)
1069                break
1070
1071        # Reset the view
1072        self.model.reset()
1073
1074        # Reset the view
1075        self.theory_model.appendRow(model_item)
1076
1077        # Pass acting as a debugger anchor
1078        pass
1079
1080
1081if __name__ == "__main__":
1082    app = QtGui.QApplication([])
1083    dlg = DataExplorerWindow()
1084    dlg.show()
1085    sys.exit(app.exec_())
Note: See TracBrowser for help on using the repository browser.