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

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 c7f259d was c7f259d, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

implement append-to-plot functionality for the Theory view

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