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

Last change on this file since a7067ef2 was a7067ef2, checked in by Adam Washington <adam.washington@…>, 7 years ago

Make Fitting the default perspective

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