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

Last change on this file since 5dba493 was d9150d8, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

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