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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 515c23df was 515c23df, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

fixed deleteItem behaviour as per SASVIEW-956 discussion

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