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

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

CR: Deleting a single dataitem child will close the corresponding plot (SASVIEW-958)

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