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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 685e0e3 was 685e0e3, checked in by piotr, 6 years ago

Fix for frozen theories not showing correct plots. SASVIEW-978
Fix for Linear Fit not working properly after recent changes.

  • Property mode set to 100644
File size: 45.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
10
11from twisted.internet import threads
12
13# SASCALC
14from sas.sascalc.dataloader.loader import Loader
15
16# QTGUI
17import sas.qtgui.Utilities.GuiUtils as GuiUtils
18import sas.qtgui.Plotting.PlotHelper as PlotHelper
19
20from sas.qtgui.Plotting.PlotterData import Data1D
21from sas.qtgui.Plotting.PlotterData import Data2D
22from sas.qtgui.Plotting.Plotter import Plotter
23from sas.qtgui.Plotting.Plotter2D import Plotter2D
24from sas.qtgui.Plotting.MaskEditor import MaskEditor
25
26from sas.qtgui.MainWindow.DataManager import DataManager
27from sas.qtgui.MainWindow.DroppableDataLoadWidget import DroppableDataLoadWidget
28
29import sas.qtgui.Perspectives as Perspectives
30
31DEFAULT_PERSPECTIVE = "Fitting"
32
33logger = logging.getLogger(__name__)
34
35class DataExplorerWindow(DroppableDataLoadWidget):
36    # The controller which is responsible for managing signal slots connections
37    # for the gui and providing an interface to the data model.
38
39    def __init__(self, parent=None, guimanager=None, manager=None):
40        super(DataExplorerWindow, self).__init__(parent, guimanager)
41
42        # Main model for keeping loaded data
43        self.model = QtGui.QStandardItemModel(self)
44
45        # Secondary model for keeping frozen data sets
46        self.theory_model = QtGui.QStandardItemModel(self)
47
48        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
49        # in order to set the widget parentage properly.
50        self.parent = guimanager
51        self.loader = Loader()
52        self.manager = manager if manager is not None else DataManager()
53        self.txt_widget = QtWidgets.QTextEdit(None)
54
55        # Be careful with twisted threads.
56        self.mutex = QtCore.QMutex()
57
58        # Plot widgets {name:widget}, required to keep track of plots shown as MDI subwindows
59        self.plot_widgets = {}
60
61        # Active plots {id:Plotter1D/2D}, required to keep track of currently displayed plots
62        self.active_plots = {}
63
64        # Connect the buttons
65        self.cmdLoad.clicked.connect(self.loadFile)
66        self.cmdDeleteData.clicked.connect(self.deleteFile)
67        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
68        self.cmdFreeze.clicked.connect(self.freezeTheory)
69        self.cmdSendTo.clicked.connect(self.sendData)
70        self.cmdNew.clicked.connect(self.newPlot)
71        self.cmdNew_2.clicked.connect(self.newPlot)
72        self.cmdAppend.clicked.connect(self.appendPlot)
73        self.cmdHelp.clicked.connect(self.displayHelp)
74        self.cmdHelp_2.clicked.connect(self.displayHelp)
75
76        # Fill in the perspectives combo
77        self.initPerspectives()
78
79        # Custom context menu
80        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
81        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
82        self.contextMenu()
83
84        # Same menus for the theory view
85        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
86        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
87
88        # Connect the comboboxes
89        self.cbSelect.currentIndexChanged.connect(self.selectData)
90
91        #self.closeEvent.connect(self.closeEvent)
92        self.currentChanged.connect(self.onTabSwitch)
93        self.communicator = self.parent.communicator()
94        self.communicator.fileReadSignal.connect(self.loadFromURL)
95        self.communicator.activeGraphsSignal.connect(self.updateGraphCount)
96        self.communicator.activeGraphName.connect(self.updatePlotName)
97        self.communicator.plotUpdateSignal.connect(self.updatePlot)
98        self.communicator.maskEditorSignal.connect(self.showEditDataMask)
99
100        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
101        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
102
103        # Proxy model for showing a subset of Data1D/Data2D content
104        self.data_proxy = QtCore.QSortFilterProxyModel(self)
105        self.data_proxy.setSourceModel(self.model)
106
107        # Don't show "empty" rows with data objects
108        self.data_proxy.setFilterRegExp(r"[^()]")
109
110        # The Data viewer is QTreeView showing the proxy model
111        self.treeView.setModel(self.data_proxy)
112
113        # Proxy model for showing a subset of Theory content
114        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
115        self.theory_proxy.setSourceModel(self.theory_model)
116
117        # Don't show "empty" rows with data objects
118        self.theory_proxy.setFilterRegExp(r"[^()]")
119
120        # Theory model view
121        self.freezeView.setModel(self.theory_proxy)
122
123        self.enableGraphCombo(None)
124
125        # Current view on model
126        self.current_view = self.treeView
127
128    def closeEvent(self, event):
129        """
130        Overwrite the close event - no close!
131        """
132        event.ignore()
133
134    def onTabSwitch(self, index):
135        """ Callback for tab switching signal """
136        if index == 0:
137            self.current_view = self.treeView
138        else:
139            self.current_view = self.freezeView
140
141    def displayHelp(self):
142        """
143        Show the "Loading data" section of help
144        """
145        tree_location = "/user/qtgui/MainWindow/data_explorer_help.html"
146        self.parent.showHelp(tree_location)
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_items = []
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_items.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_items)
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.cloneTheory(outer_item)
408                self.model.appendRow(new_item)
409                self.model.endResetModel()
410
411        freeze_msg = ""
412        if theories_copied == 0:
413            return
414        elif theories_copied == 1:
415            freeze_msg = "1 theory copied from the Theory tab as a data set"
416        elif theories_copied > 1:
417            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
418        else:
419            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
420            raise AttributeError(freeze_msg)
421        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
422        # Actively switch tabs
423        self.setCurrentIndex(1)
424
425    def cloneTheory(self, item_from):
426        """
427        Manually clone theory items into a new HashableItem
428        """
429        new_item = GuiUtils.HashableStandardItem()
430        new_item.setCheckable(True)
431        new_item.setCheckState(QtCore.Qt.Checked)
432        info_item = QtGui.QStandardItem("Info")
433        data_item = QtGui.QStandardItem()
434        data_item.setData(item_from.child(0).data())
435        new_item.setText(item_from.text())
436        new_item.setChild(0, data_item)
437        new_item.setChild(1, info_item)
438        # Append a "unique" descriptor to the name
439        time_bit = str(time.time())[7:-1].replace('.', '')
440        new_name = new_item.text() + '_@' + time_bit
441        new_item.setText(new_name)
442        # Change the underlying data so it is no longer a theory
443        try:
444            new_item.child(0).data().is_data = True
445        except AttributeError:
446            #no data here, pass
447            pass
448        return new_item
449
450    def recursivelyCloneItem(self, item):
451        """
452        Clone QStandardItem() object
453        """
454        new_item = item.clone()
455        # clone doesn't do deepcopy :(
456        for child_index in range(item.rowCount()):
457            child_item = self.recursivelyCloneItem(item.child(child_index))
458            new_item.setChild(child_index, child_item)
459        return new_item
460
461    def updatePlotName(self, name_tuple):
462        """
463        Modify the name of the current plot
464        """
465        old_name, current_name = name_tuple
466        ind = self.cbgraph.findText(old_name)
467        self.cbgraph.setCurrentIndex(ind)
468        self.cbgraph.setItemText(ind, current_name)
469
470    def updateGraphCount(self, graph_list):
471        """
472        Modify the graph name combo and potentially remove
473        deleted graphs
474        """
475        self.updateGraphCombo(graph_list)
476
477        if not self.active_plots:
478            return
479        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
480        active_plots_copy = list(self.active_plots.keys())
481        for plot in active_plots_copy:
482            if self.active_plots[plot] in new_plots:
483                continue
484            self.active_plots.pop(plot)
485
486    def updateGraphCombo(self, graph_list):
487        """
488        Modify Graph combo box on graph add/delete
489        """
490        orig_text = self.cbgraph.currentText()
491        self.cbgraph.clear()
492        self.cbgraph.insertItems(0, graph_list)
493        ind = self.cbgraph.findText(orig_text)
494        if ind > 0:
495            self.cbgraph.setCurrentIndex(ind)
496
497    def updatePerspectiveCombo(self, index):
498        """
499        Notify the gui manager about the new perspective chosen.
500        """
501        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
502        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
503
504    def itemFromFilename(self, filename):
505        """
506        Retrieves model item corresponding to the given filename
507        """
508        item = GuiUtils.itemFromFilename(filename, self.model)
509        return item
510
511    def displayFile(self, filename=None, is_data=True):
512        """
513        Forces display of charts for the given filename
514        """
515        model = self.model if is_data else self.theory_model
516        # Now query the model item for available plots
517        plots = GuiUtils.plotsFromFilename(filename, model)
518        ids = [list(self.active_plots.values())[id].data.id for id in range(len(self.active_plots))]
519
520        new_plots = []
521        for item, plot in plots.items():
522            plot_id = plot.id
523            if plot_id in ids:
524                self.active_plots[plot_id].replacePlot(plot_id, plot)
525            else:
526                # 'sophisticated' test to generate standalone plot for residuals
527                if 'esiduals' in plot.title:
528                    self.plotData([(item, plot)])
529                else:
530                    new_plots.append((item, plot))
531
532        if new_plots:
533            self.plotData(new_plots)
534
535    def displayData(self, data_list):
536        """
537        Forces display of charts for the given data set
538        """
539        plot_to_show = data_list[0]
540        # passed plot is used ONLY to figure out its title,
541        # so all the charts related by it can be pulled from
542        # the data explorer indices.
543        filename = plot_to_show.filename
544        self.displayFile(filename=filename, is_data=plot_to_show.is_data)
545
546    def addDataPlot2D(self, plot_set, item):
547        """
548        Create a new 2D plot and add it to the workspace
549        """
550        plot2D = Plotter2D(self)
551        plot2D.item = item
552        plot2D.plot(plot_set)
553        self.addPlot(plot2D)
554        self.active_plots[plot2D.data.id] = plot2D
555        #============================================
556        # Experimental hook for silx charts
557        #============================================
558        ## Attach silx
559        #from silx.gui import qt
560        #from silx.gui.plot import StackView
561        #sv = StackView()
562        #sv.setColormap("jet", autoscale=True)
563        #sv.setStack(plot_set.data.reshape(1,100,100))
564        ##sv.setLabels(["x: -10 to 10 (200 samples)",
565        ##              "y: -10 to 5 (150 samples)"])
566        #sv.show()
567        #============================================
568
569    def plotData(self, plots):
570        """
571        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
572        """
573        # Call show on requested plots
574        # All same-type charts in one plot
575        for item, plot_set in plots:
576            if isinstance(plot_set, Data1D):
577                if not 'new_plot' in locals():
578                    new_plot = Plotter(self)
579                    new_plot.item = item
580                new_plot.plot(plot_set)
581                # active_plots may contain multiple charts
582                self.active_plots[plot_set.id] = new_plot
583            elif isinstance(plot_set, Data2D):
584                self.addDataPlot2D(plot_set, item)
585            else:
586                msg = "Incorrect data type passed to Plotting"
587                raise AttributeError(msg)
588
589        if 'new_plot' in locals() and \
590            hasattr(new_plot, 'data') and \
591            isinstance(new_plot.data, Data1D):
592                self.addPlot(new_plot)
593
594    def newPlot(self):
595        """
596        Select checked data and plot it
597        """
598        # Check which tab is currently active
599        if self.current_view == self.treeView:
600            plots = GuiUtils.plotsFromCheckedItems(self.model)
601        else:
602            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
603
604        self.plotData(plots)
605
606    def addPlot(self, new_plot):
607        """
608        Helper method for plot bookkeeping
609        """
610        # Update the global plot counter
611        title = str(PlotHelper.idOfPlot(new_plot))
612        new_plot.setWindowTitle(title)
613
614        # Set the object name to satisfy the Squish object picker
615        new_plot.setObjectName(title)
616
617        # Add the plot to the workspace
618        plot_widget = self.parent.workspace().addSubWindow(new_plot)
619
620        # Show the plot
621        new_plot.show()
622        new_plot.canvas.draw()
623
624        # Update the plot widgets dict
625        self.plot_widgets[title]=plot_widget
626
627        # Update the active chart list
628        #self.active_plots[new_plot.data.id] = new_plot
629
630    def appendPlot(self):
631        """
632        Add data set(s) to the existing matplotlib chart
633        """
634        # new plot data
635        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
636
637        # old plot data
638        plot_id = str(self.cbgraph.currentText())
639
640        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
641
642        old_plot = PlotHelper.plotById(plot_id)
643
644        # Add new data to the old plot, if data type is the same.
645        for _, plot_set in new_plots:
646            if type(plot_set) is type(old_plot._data):
647                old_plot.data = plot_set
648                old_plot.plot()
649
650    def updatePlot(self, new_data):
651        """
652        Modify existing plot for immediate response
653        """
654        data = new_data[0]
655        assert type(data).__name__ in ['Data1D', 'Data2D']
656
657        id = data.id
658        if data.id in list(self.active_plots.keys()):
659            self.active_plots[id].replacePlot(id, data)
660
661    def chooseFiles(self):
662        """
663        Shows the Open file dialog and returns the chosen path(s)
664        """
665        # List of known extensions
666        wlist = self.getWlist()
667
668        # Location is automatically saved - no need to keep track of the last dir
669        # But only with Qt built-in dialog (non-platform native)
670        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
671                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
672        if not paths:
673            return
674
675        if not isinstance(paths, list):
676            paths = [paths]
677
678        return paths
679
680    def readData(self, path):
681        """
682        verbatim copy-paste from
683           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
684        slightly modified for clarity
685        """
686        message = ""
687        log_msg = ''
688        output = {}
689        any_error = False
690        data_error = False
691        error_message = ""
692        number_of_files = len(path)
693        self.communicator.progressBarUpdateSignal.emit(0.0)
694
695        for index, p_file in enumerate(path):
696            basename = os.path.basename(p_file)
697            _, extension = os.path.splitext(basename)
698            if extension.lower() in GuiUtils.EXTENSIONS:
699                any_error = True
700                log_msg = "Data Loader cannot "
701                log_msg += "load: %s\n" % str(p_file)
702                log_msg += """Please try to open that file from "open project" """
703                log_msg += """or "open analysis" menu\n"""
704                error_message = log_msg + "\n"
705                logging.info(log_msg)
706                continue
707
708            try:
709                message = "Loading Data... " + str(basename) + "\n"
710
711                # change this to signal notification in GuiManager
712                self.communicator.statusBarUpdateSignal.emit(message)
713
714                output_objects = self.loader.load(p_file)
715
716                # Some loaders return a list and some just a single Data1D object.
717                # Standardize.
718                if not isinstance(output_objects, list):
719                    output_objects = [output_objects]
720
721                for item in output_objects:
722                    # cast sascalc.dataloader.data_info.Data1D into
723                    # sasgui.guiframe.dataFitting.Data1D
724                    # TODO : Fix it
725                    new_data = self.manager.create_gui_data(item, p_file)
726                    output[new_data.id] = new_data
727
728                    # Model update should be protected
729                    self.mutex.lock()
730                    self.updateModel(new_data, p_file)
731                    #self.model.reset()
732                    QtWidgets.QApplication.processEvents()
733                    self.mutex.unlock()
734
735                    if hasattr(item, 'errors'):
736                        for error_data in item.errors:
737                            data_error = True
738                            message += "\tError: {0}\n".format(error_data)
739                    else:
740
741                        logging.error("Loader returned an invalid object:\n %s" % str(item))
742                        data_error = True
743
744            except Exception as ex:
745                logging.error(sys.exc_info()[1])
746
747                any_error = True
748            if any_error or error_message != "":
749                if error_message == "":
750                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
751                    error += "while loading Data: \n%s\n" % str(basename)
752                    error_message += "The data file you selected could not be loaded.\n"
753                    error_message += "Make sure the content of your file"
754                    error_message += " is properly formatted.\n\n"
755                    error_message += "When contacting the SasView team, mention the"
756                    error_message += " following:\n%s" % str(error)
757                elif data_error:
758                    base_message = "Errors occurred while loading "
759                    base_message += "{0}\n".format(basename)
760                    base_message += "The data file loaded but with errors.\n"
761                    error_message = base_message + error_message
762                else:
763                    error_message += "%s\n" % str(p_file)
764
765            current_percentage = int(100.0* index/number_of_files)
766            self.communicator.progressBarUpdateSignal.emit(current_percentage)
767
768        if any_error or error_message:
769            logging.error(error_message)
770            status_bar_message = "Errors occurred while loading %s" % format(basename)
771            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
772
773        else:
774            message = "Loading Data Complete! "
775        message += log_msg
776        # Notify the progress bar that the updates are over.
777        self.communicator.progressBarUpdateSignal.emit(-1)
778        self.communicator.statusBarUpdateSignal.emit(message)
779
780        return output, message
781
782    def getWlist(self):
783        """
784        Wildcards of files we know the format of.
785        """
786        # Display the Qt Load File module
787        cards = self.loader.get_wildcards()
788
789        # get rid of the wx remnant in wildcards
790        # TODO: modify sasview loader get_wildcards method, after merge,
791        # so this kludge can be avoided
792        new_cards = []
793        for item in cards:
794            new_cards.append(item[:item.find("|")])
795        wlist = ';;'.join(new_cards)
796
797        return wlist
798
799    def selectData(self, index):
800        """
801        Callback method for modifying the TreeView on Selection Options change
802        """
803        if not isinstance(index, int):
804            msg = "Incorrect type passed to DataExplorer.selectData()"
805            raise AttributeError(msg)
806
807        # Respond appropriately
808        if index == 0:
809            # Select All
810            for index in range(self.model.rowCount()):
811                item = self.model.item(index)
812                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
813                    item.setCheckState(QtCore.Qt.Checked)
814        elif index == 1:
815            # De-select All
816            for index in range(self.model.rowCount()):
817                item = self.model.item(index)
818                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
819                    item.setCheckState(QtCore.Qt.Unchecked)
820
821        elif index == 2:
822            # Select All 1-D
823            for index in range(self.model.rowCount()):
824                item = self.model.item(index)
825                item.setCheckState(QtCore.Qt.Unchecked)
826
827                try:
828                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
829                except AttributeError:
830                    msg = "Bad structure of the data model."
831                    raise RuntimeError(msg)
832
833                if is1D:
834                    item.setCheckState(QtCore.Qt.Checked)
835
836        elif index == 3:
837            # Unselect All 1-D
838            for index in range(self.model.rowCount()):
839                item = self.model.item(index)
840
841                try:
842                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
843                except AttributeError:
844                    msg = "Bad structure of the data model."
845                    raise RuntimeError(msg)
846
847                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
848                    item.setCheckState(QtCore.Qt.Unchecked)
849
850        elif index == 4:
851            # Select All 2-D
852            for index in range(self.model.rowCount()):
853                item = self.model.item(index)
854                item.setCheckState(QtCore.Qt.Unchecked)
855                try:
856                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
857                except AttributeError:
858                    msg = "Bad structure of the data model."
859                    raise RuntimeError(msg)
860
861                if is2D:
862                    item.setCheckState(QtCore.Qt.Checked)
863
864        elif index == 5:
865            # Unselect All 2-D
866            for index in range(self.model.rowCount()):
867                item = self.model.item(index)
868
869                try:
870                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
871                except AttributeError:
872                    msg = "Bad structure of the data model."
873                    raise RuntimeError(msg)
874
875                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
876                    item.setCheckState(QtCore.Qt.Unchecked)
877
878        else:
879            msg = "Incorrect value in the Selection Option"
880            # Change this to a proper logging action
881            raise Exception(msg)
882
883    def contextMenu(self):
884        """
885        Define actions and layout of the right click context menu
886        """
887        # Create a custom menu based on actions defined in the UI file
888        self.context_menu = QtWidgets.QMenu(self)
889        self.context_menu.addAction(self.actionDataInfo)
890        self.context_menu.addAction(self.actionSaveAs)
891        self.context_menu.addAction(self.actionQuickPlot)
892        self.context_menu.addSeparator()
893        self.context_menu.addAction(self.actionQuick3DPlot)
894        self.context_menu.addAction(self.actionEditMask)
895        self.context_menu.addSeparator()
896        self.context_menu.addAction(self.actionDelete)
897
898
899        # Define the callbacks
900        self.actionDataInfo.triggered.connect(self.showDataInfo)
901        self.actionSaveAs.triggered.connect(self.saveDataAs)
902        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
903        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
904        self.actionEditMask.triggered.connect(self.showEditDataMask)
905        self.actionDelete.triggered.connect(self.deleteItem)
906
907    def onCustomContextMenu(self, position):
908        """
909        Show the right-click context menu in the data treeview
910        """
911        index = self.current_view.indexAt(position)
912        proxy = self.current_view.model()
913        model = proxy.sourceModel()
914
915        if index.isValid():
916            model_item = model.itemFromIndex(proxy.mapToSource(index))
917            # Find the mapped index
918            orig_index = model_item.isCheckable()
919            if orig_index:
920                # Check the data to enable/disable actions
921                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
922                self.actionQuick3DPlot.setEnabled(is_2D)
923                self.actionEditMask.setEnabled(is_2D)
924                # Fire up the menu
925                self.context_menu.exec_(self.current_view.mapToGlobal(position))
926
927    def showDataInfo(self):
928        """
929        Show a simple read-only text edit with data information.
930        """
931        index = self.current_view.selectedIndexes()[0]
932        proxy = self.current_view.model()
933        model = proxy.sourceModel()
934        model_item = model.itemFromIndex(proxy.mapToSource(index))
935
936        data = GuiUtils.dataFromItem(model_item)
937        if isinstance(data, Data1D):
938            text_to_show = GuiUtils.retrieveData1d(data)
939            # Hardcoded sizes to enable full width rendering with default font
940            self.txt_widget.resize(420,600)
941        else:
942            text_to_show = GuiUtils.retrieveData2d(data)
943            # Hardcoded sizes to enable full width rendering with default font
944            self.txt_widget.resize(700,600)
945
946        self.txt_widget.setReadOnly(True)
947        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
948        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
949        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
950        self.txt_widget.clear()
951        self.txt_widget.insertPlainText(text_to_show)
952
953        self.txt_widget.show()
954        # Move the slider all the way up, if present
955        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
956        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
957
958    def saveDataAs(self):
959        """
960        Save the data points as either txt or xml
961        """
962        index = self.current_view.selectedIndexes()[0]
963        proxy = self.current_view.model()
964        model = proxy.sourceModel()
965        model_item = model.itemFromIndex(proxy.mapToSource(index))
966
967        data = GuiUtils.dataFromItem(model_item)
968        if isinstance(data, Data1D):
969            GuiUtils.saveData1D(data)
970        else:
971            GuiUtils.saveData2D(data)
972
973    def quickDataPlot(self):
974        """
975        Frozen plot - display an image of the plot
976        """
977        index = self.current_view.selectedIndexes()[0]
978        proxy = self.current_view.model()
979        model = proxy.sourceModel()
980        model_item = model.itemFromIndex(proxy.mapToSource(index))
981
982        data = GuiUtils.dataFromItem(model_item)
983
984        method_name = 'Plotter'
985        if isinstance(data, Data2D):
986            method_name='Plotter2D'
987
988        self.new_plot = globals()[method_name](self, quickplot=True)
989        self.new_plot.data = data
990        #new_plot.plot(marker='o')
991        self.new_plot.plot()
992
993        # Update the global plot counter
994        title = "Plot " + data.name
995        self.new_plot.setWindowTitle(title)
996
997        # Show the plot
998        self.new_plot.show()
999
1000    def quickData3DPlot(self):
1001        """
1002        Slowish 3D plot
1003        """
1004        index = self.current_view.selectedIndexes()[0]
1005        proxy = self.current_view.model()
1006        model = proxy.sourceModel()
1007        model_item = model.itemFromIndex(proxy.mapToSource(index))
1008
1009        data = GuiUtils.dataFromItem(model_item)
1010
1011        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1012        self.new_plot.data = data
1013        self.new_plot.plot()
1014
1015        # Update the global plot counter
1016        title = "Plot " + data.name
1017        self.new_plot.setWindowTitle(title)
1018
1019        # Show the plot
1020        self.new_plot.show()
1021
1022    def showEditDataMask(self, data=None):
1023        """
1024        Mask Editor for 2D plots
1025        """
1026        if data is None or not isinstance(data, Data2D):
1027            index = self.current_view.selectedIndexes()[0]
1028            proxy = self.current_view.model()
1029            model = proxy.sourceModel()
1030            model_item = model.itemFromIndex(proxy.mapToSource(index))
1031
1032            data = GuiUtils.dataFromItem(model_item)
1033
1034        mask_editor = MaskEditor(self, data)
1035        # Modal dialog here.
1036        mask_editor.exec_()
1037
1038    def deleteItem(self):
1039        """
1040        Delete the current item
1041        """
1042        # Assure this is indeed wanted
1043        delete_msg = "This operation will delete the selected data sets " +\
1044                     "and all the dependents." +\
1045                     "\nDo you want to continue?"
1046        reply = QtWidgets.QMessageBox.question(self,
1047                                           'Warning',
1048                                           delete_msg,
1049                                           QtWidgets.QMessageBox.Yes,
1050                                           QtWidgets.QMessageBox.No)
1051
1052        if reply == QtWidgets.QMessageBox.No:
1053            return
1054
1055        # Every time a row is removed, the indices change, so we'll just remove
1056        # rows and keep calling selectedIndexes until it returns an empty list.
1057        indices = self.current_view.selectedIndexes()
1058
1059        proxy = self.current_view.model()
1060        model = proxy.sourceModel()
1061
1062        deleted_items = []
1063        deleted_names = []
1064
1065        while len(indices) > 0:
1066            index = indices[0]
1067            row_index = proxy.mapToSource(index)
1068            item_to_delete = model.itemFromIndex(row_index)
1069            if item_to_delete and item_to_delete.isCheckable():
1070                row = row_index.row()
1071
1072                # store the deleted item details so we can pass them on later
1073                deleted_names.append(item_to_delete.text())
1074                deleted_items.append(item_to_delete)
1075
1076                # Delete corresponding open plots
1077                self.closePlotsForItem(item_to_delete)
1078
1079                if item_to_delete.parent():
1080                    # We have a child item - delete from it
1081                    item_to_delete.parent().removeRow(row)
1082                else:
1083                    # delete directly from model
1084                    model.removeRow(row)
1085            indices = self.current_view.selectedIndexes()
1086
1087        # Let others know we deleted data
1088        self.communicator.dataDeletedSignal.emit(deleted_items)
1089
1090        # update stored_data
1091        self.manager.update_stored_data(deleted_names)
1092
1093    def closePlotsForItem(self, item):
1094        """
1095        Given standard item, close all its currently displayed plots
1096        """
1097        # item - HashableStandardItems of active plots
1098
1099        # {} -> 'Graph1' : HashableStandardItem()
1100        current_plot_items = {}
1101        for plot_name in PlotHelper.currentPlots():
1102            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1103
1104        # item and its hashable children
1105        items_being_deleted = []
1106        if item.rowCount() > 0:
1107            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1108                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1109        items_being_deleted.append(item)
1110        # Add the parent in case a child is selected
1111        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1112            items_being_deleted.append(item.parent())
1113
1114        # Compare plot items and items to delete
1115        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1116
1117        for plot_item in plots_to_close:
1118            for plot_name in current_plot_items.keys():
1119                if plot_item == current_plot_items[plot_name]:
1120                    plotter = PlotHelper.plotById(plot_name)
1121                    # try to delete the plot
1122                    try:
1123                        plotter.close()
1124                        #self.parent.workspace().removeSubWindow(plotter)
1125                        self.plot_widgets[plot_name].close()
1126                        self.plot_widgets.pop(plot_name, None)
1127                    except AttributeError as ex:
1128                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1129
1130        pass # debugger anchor
1131
1132    def onAnalysisUpdate(self, new_perspective=""):
1133        """
1134        Update the perspective combo index based on passed string
1135        """
1136        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1137        self.cbFitting.blockSignals(True)
1138        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1139        self.cbFitting.blockSignals(False)
1140        pass
1141
1142    def loadComplete(self, output):
1143        """
1144        Post message to status bar and update the data manager
1145        """
1146        assert isinstance(output, tuple)
1147
1148        # Reset the model so the view gets updated.
1149        #self.model.reset()
1150        self.communicator.progressBarUpdateSignal.emit(-1)
1151
1152        output_data = output[0]
1153        message = output[1]
1154        # Notify the manager of the new data available
1155        self.communicator.statusBarUpdateSignal.emit(message)
1156        self.communicator.fileDataReceivedSignal.emit(output_data)
1157        self.manager.add_data(data_list=output_data)
1158
1159    def loadFailed(self, reason):
1160        print("File Load Failed with:\n", reason)
1161        pass
1162
1163    def updateModel(self, data, p_file):
1164        """
1165        Add data and Info fields to the model item
1166        """
1167        # Structure of the model
1168        # checkbox + basename
1169        #     |-------> Data.D object
1170        #     |-------> Info
1171        #                 |----> Title:
1172        #                 |----> Run:
1173        #                 |----> Type:
1174        #                 |----> Path:
1175        #                 |----> Process
1176        #                          |-----> process[0].name
1177        #     |-------> THEORIES
1178
1179        # Top-level item: checkbox with label
1180        checkbox_item = GuiUtils.HashableStandardItem()
1181        checkbox_item.setCheckable(True)
1182        checkbox_item.setCheckState(QtCore.Qt.Checked)
1183        checkbox_item.setText(os.path.basename(p_file))
1184
1185        # Add the actual Data1D/Data2D object
1186        object_item = GuiUtils.HashableStandardItem()
1187        object_item.setData(data)
1188
1189        checkbox_item.setChild(0, object_item)
1190
1191        # Add rows for display in the view
1192        info_item = GuiUtils.infoFromData(data)
1193
1194        # Set info_item as the first child
1195        checkbox_item.setChild(1, info_item)
1196
1197        # Caption for the theories
1198        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1199
1200        # New row in the model
1201        self.model.beginResetModel()
1202        self.model.appendRow(checkbox_item)
1203        self.model.endResetModel()
1204
1205    def updateModelFromPerspective(self, model_item):
1206        """
1207        Receive an update model item from a perspective
1208        Make sure it is valid and if so, replace it in the model
1209        """
1210        # Assert the correct type
1211        if not isinstance(model_item, QtGui.QStandardItem):
1212            msg = "Wrong data type returned from calculations."
1213            raise AttributeError(msg)
1214
1215        # TODO: Assert other properties
1216
1217        # Reset the view
1218        ##self.model.reset()
1219        # Pass acting as a debugger anchor
1220        pass
1221
1222    def updateTheoryFromPerspective(self, model_item):
1223        """
1224        Receive an update theory item from a perspective
1225        Make sure it is valid and if so, replace/add in the model
1226        """
1227        # Assert the correct type
1228        if not isinstance(model_item, QtGui.QStandardItem):
1229            msg = "Wrong data type returned from calculations."
1230            raise AttributeError(msg)
1231
1232        # Check if there are any other items for this tab
1233        # If so, delete them
1234        current_tab_name = model_item.text()
1235        for current_index in range(self.theory_model.rowCount()):
1236            #if current_tab_name in self.theory_model.item(current_index).text():
1237            if current_tab_name == self.theory_model.item(current_index).text():
1238                return
1239                self.theory_model.removeRow(current_index)
1240                break
1241
1242        # send in the new item
1243        self.theory_model.appendRow(model_item)
1244
Note: See TracBrowser for help on using the repository browser.