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

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

fixed selected dataset deletion (see SASVIEW-956)

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