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

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

Implemented hash method for certain QStandardItem objects, allowing them to be used as dict keys. SASVIEW-806
Minor inversion perspective fixes. SASVIEW-338

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