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

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

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

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