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

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

Add "delete" to the data explorer context menu

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