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

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

Initial, in-progress version. Not really working atm. SASVIEW-787

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