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

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 7fb471d was 7fb471d, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Update for unit tests and minor functionality quirks

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