source: sasview/src/sas/qtgui/MainWindow/GuiManager.py @ be74751

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since be74751 was be74751, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

More batchpage related functionality for Analysis save/load

  • Property mode set to 100644
File size: 40.3 KB
Line 
1import sys
2import os
3import subprocess
4import logging
5import json
6import webbrowser
7import traceback
8
9from PyQt5.QtWidgets import *
10from PyQt5.QtGui import *
11from PyQt5.QtCore import Qt, QLocale, QUrl
12
13import matplotlib as mpl
14mpl.use("Qt5Agg")
15
16from twisted.internet import reactor
17# General SAS imports
18from sas import get_local_config, get_custom_config
19from sas.qtgui.Utilities.ConnectionProxy import ConnectionProxy
20from sas.qtgui.Utilities.SasviewLogger import setup_qt_logging
21
22import sas.qtgui.Utilities.LocalConfig as LocalConfig
23import sas.qtgui.Utilities.GuiUtils as GuiUtils
24
25import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
26from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor
27from sas.qtgui.Utilities.PluginManager import PluginManager
28from sas.qtgui.Utilities.GridPanel import BatchOutputPanel
29from sas.qtgui.Utilities.ResultPanel import ResultPanel
30
31from sas.qtgui.Utilities.ReportDialog import ReportDialog
32from sas.qtgui.MainWindow.UI.AcknowledgementsUI import Ui_Acknowledgements
33from sas.qtgui.MainWindow.AboutBox import AboutBox
34from sas.qtgui.MainWindow.WelcomePanel import WelcomePanel
35from sas.qtgui.MainWindow.CategoryManager import CategoryManager
36
37from sas.qtgui.MainWindow.DataManager import DataManager
38
39from sas.qtgui.Calculators.SldPanel import SldPanel
40from sas.qtgui.Calculators.DensityPanel import DensityPanel
41from sas.qtgui.Calculators.KiessigPanel import KiessigPanel
42from sas.qtgui.Calculators.SlitSizeCalculator import SlitSizeCalculator
43from sas.qtgui.Calculators.GenericScatteringCalculator import GenericScatteringCalculator
44from sas.qtgui.Calculators.ResolutionCalculatorPanel import ResolutionCalculatorPanel
45from sas.qtgui.Calculators.DataOperationUtilityPanel import DataOperationUtilityPanel
46
47# Perspectives
48import sas.qtgui.Perspectives as Perspectives
49from sas.qtgui.Perspectives.Fitting.FittingPerspective import FittingWindow
50from sas.qtgui.MainWindow.DataExplorer import DataExplorerWindow, DEFAULT_PERSPECTIVE
51
52from sas.qtgui.Utilities.AddMultEditor import AddMultEditor
53
54logger = logging.getLogger(__name__)
55
56class Acknowledgements(QDialog, Ui_Acknowledgements):
57    def __init__(self, parent=None):
58        QDialog.__init__(self, parent)
59        self.setupUi(self)
60
61class GuiManager(object):
62    """
63    Main SasView window functionality
64    """
65    def __init__(self, parent=None):
66        """
67        Initialize the manager as a child of MainWindow.
68        """
69        self._workspace = parent
70        self._parent = parent
71
72        # Decide on a locale
73        QLocale.setDefault(QLocale('en_US'))
74
75        # Redefine exception hook to not explicitly crash the app.
76        sys.excepthook = self.info
77
78        # Add signal callbacks
79        self.addCallbacks()
80
81        # Assure model categories are available
82        self.addCategories()
83
84        # Create the data manager
85        # TODO: pull out all required methods from DataManager and reimplement
86        self._data_manager = DataManager()
87
88        # Create action triggers
89        self.addTriggers()
90
91        # Currently displayed perspective
92        self._current_perspective = None
93
94        # Populate the main window with stuff
95        self.addWidgets()
96
97        # Fork off logging messages to the Log Window
98        handler = setup_qt_logging()
99        handler.messageWritten.connect(self.appendLog)
100
101        # Log the start of the session
102        logging.info(" --- SasView session started ---")
103        # Log the python version
104        logging.info("Python: %s" % sys.version)
105
106        # Set up the status bar
107        self.statusBarSetup()
108
109        # Current tutorial location
110        self._tutorialLocation = os.path.abspath(os.path.join(GuiUtils.HELP_DIRECTORY_LOCATION,
111                                              "_downloads",
112                                              "Tutorial.pdf"))
113
114    def info(self, type, value, tb):
115        logger.error("SasView threw exception: " + str(value))
116        traceback.print_exception(type, value, tb)
117
118    def addWidgets(self):
119        """
120        Populate the main window with widgets
121
122        TODO: overwrite close() on Log and DR widgets so they can be hidden/shown
123        on request
124        """
125        # Add FileDialog widget as docked
126        self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager)
127        ObjectLibrary.addObject('DataExplorer', self.filesWidget)
128
129        self.dockedFilesWidget = QDockWidget("Data Explorer", self._workspace)
130        self.dockedFilesWidget.setFloating(False)
131        self.dockedFilesWidget.setWidget(self.filesWidget)
132
133        # Modify menu items on widget visibility change
134        self.dockedFilesWidget.visibilityChanged.connect(self.updateContextMenus)
135
136        self._workspace.addDockWidget(Qt.LeftDockWidgetArea, self.dockedFilesWidget)
137        self._workspace.resizeDocks([self.dockedFilesWidget], [305], Qt.Horizontal)
138
139        # Add the console window as another docked widget
140        self.logDockWidget = QDockWidget("Log Explorer", self._workspace)
141        self.logDockWidget.setObjectName("LogDockWidget")
142        self.logDockWidget.visibilityChanged.connect(self.updateLogContextMenus)
143
144
145        self.listWidget = QTextBrowser()
146        self.logDockWidget.setWidget(self.listWidget)
147        self._workspace.addDockWidget(Qt.BottomDockWidgetArea, self.logDockWidget)
148
149        # Add other, minor widgets
150        self.ackWidget = Acknowledgements()
151        self.aboutWidget = AboutBox()
152        self.categoryManagerWidget = CategoryManager(self._parent, manager=self)
153
154        self.grid_window = None
155        self.grid_window = BatchOutputPanel(parent=self)
156        if sys.platform == "darwin":
157            self.grid_window.menubar.setNativeMenuBar(False)
158        self.grid_subwindow = self._workspace.workspace.addSubWindow(self.grid_window)
159        self.grid_subwindow.setVisible(False)
160        self.grid_window.windowClosedSignal.connect(lambda: self.grid_subwindow.setVisible(False))
161
162        self.results_panel = ResultPanel(parent=self._parent, manager=self)
163        self.results_frame = self._workspace.workspace.addSubWindow(self.results_panel)
164        self.results_frame.setVisible(False)
165        self.results_panel.windowClosedSignal.connect(lambda: self.results_frame.setVisible(False))
166
167        self._workspace.toolBar.setVisible(LocalConfig.TOOLBAR_SHOW)
168        self._workspace.actionHide_Toolbar.setText("Show Toolbar")
169
170        # Add calculators - floating for usability
171        self.SLDCalculator = SldPanel(self)
172        self.DVCalculator = DensityPanel(self)
173        self.KIESSIGCalculator = KiessigPanel(self)
174        self.SlitSizeCalculator = SlitSizeCalculator(self)
175        self.GENSASCalculator = GenericScatteringCalculator(self)
176        self.ResolutionCalculator = ResolutionCalculatorPanel(self)
177        self.DataOperation = DataOperationUtilityPanel(self)
178
179    def addCategories(self):
180        """
181        Make sure categories.json exists and if not compile it and install in ~/.sasview
182        """
183        try:
184            from sas.sascalc.fit.models import ModelManager
185            from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
186            model_list = ModelManager().cat_model_list()
187            CategoryInstaller.check_install(model_list=model_list)
188        except Exception:
189            import traceback
190            logger.error("%s: could not load SasView models")
191            logger.error(traceback.format_exc())
192
193    def updateLogContextMenus(self, visible=False):
194        """
195        Modify the View/Data Explorer menu item text on widget visibility
196        """
197        if visible:
198            self._workspace.actionHide_LogExplorer.setText("Hide Log Explorer")
199        else:
200            self._workspace.actionHide_LogExplorer.setText("Show Log Explorer")
201
202    def updateContextMenus(self, visible=False):
203        """
204        Modify the View/Data Explorer menu item text on widget visibility
205        """
206        if visible:
207            self._workspace.actionHide_DataExplorer.setText("Hide Data Explorer")
208        else:
209            self._workspace.actionHide_DataExplorer.setText("Show Data Explorer")
210
211    def statusBarSetup(self):
212        """
213        Define the status bar.
214        | <message label> .... | Progress Bar |
215
216        Progress bar invisible until explicitly shown
217        """
218        self.progress = QProgressBar()
219        self._workspace.statusbar.setSizeGripEnabled(False)
220
221        self.statusLabel = QLabel()
222        self.statusLabel.setText("Welcome to SasView")
223        self._workspace.statusbar.addPermanentWidget(self.statusLabel, 1)
224        self._workspace.statusbar.addPermanentWidget(self.progress, stretch=0)
225        self.progress.setRange(0, 100)
226        self.progress.setValue(0)
227        self.progress.setTextVisible(True)
228        self.progress.setVisible(False)
229
230    def fileWasRead(self, data):
231        """
232        Callback for fileDataReceivedSignal
233        """
234        pass
235
236    def showHelp(self, url):
237        """
238        Open a local url in the default browser
239        """
240        GuiUtils.showHelp(url)
241
242    def workspace(self):
243        """
244        Accessor for the main window workspace
245        """
246        return self._workspace.workspace
247
248    def perspectiveChanged(self, perspective_name):
249        """
250        Respond to change of the perspective signal
251        """
252        # Close the previous perspective
253        self.clearPerspectiveMenubarOptions(self._current_perspective)
254        if self._current_perspective:
255            self._current_perspective.setClosable()
256            self._current_perspective.close()
257            self._workspace.workspace.removeSubWindow(self._current_perspective)
258        # Default perspective
259        self._current_perspective = Perspectives.PERSPECTIVES[str(perspective_name)](parent=self)
260
261        self.setupPerspectiveMenubarOptions(self._current_perspective)
262
263        subwindow = self._workspace.workspace.addSubWindow(self._current_perspective)
264
265        # Resize to the workspace height
266        workspace_height = self._workspace.workspace.sizeHint().height()
267        perspective_size = self._current_perspective.sizeHint()
268        perspective_width = perspective_size.width()
269        self._current_perspective.resize(perspective_width, workspace_height-10)
270
271        self._current_perspective.show()
272
273    def updatePerspective(self, data):
274        """
275        Update perspective with data sent.
276        """
277        assert isinstance(data, list)
278        if self._current_perspective is not None:
279            self._current_perspective.setData(list(data.values()))
280        else:
281            msg = "No perspective is currently active."
282            logging.info(msg)
283
284    def communicator(self):
285        """ Accessor for the communicator """
286        return self.communicate
287
288    def perspective(self):
289        """ Accessor for the perspective """
290        return self._current_perspective
291
292    def updateProgressBar(self, value):
293        """
294        Update progress bar with the required value (0-100)
295        """
296        assert -1 <= value <= 100
297        if value == -1:
298            self.progress.setVisible(False)
299            return
300        if not self.progress.isVisible():
301            self.progress.setTextVisible(True)
302            self.progress.setVisible(True)
303
304        self.progress.setValue(value)
305
306    def updateStatusBar(self, text):
307        """
308        Set the status bar text
309        """
310        self.statusLabel.setText(text)
311
312    def appendLog(self, msg):
313        """Appends a message to the list widget in the Log Explorer. Use this
314        instead of listWidget.insertPlainText() to facilitate auto-scrolling"""
315        self.listWidget.append(msg.strip())
316
317    def createGuiData(self, item, p_file=None):
318        """
319        Access the Data1D -> plottable Data1D conversion
320        """
321        return self._data_manager.create_gui_data(item, p_file)
322
323    def setData(self, data):
324        """
325        Sends data to current perspective
326        """
327        if self._current_perspective is not None:
328            self._current_perspective.setData(list(data.values()))
329        else:
330            msg = "Guiframe does not have a current perspective"
331            logging.info(msg)
332
333    def findItemFromFilename(self, filename):
334        """
335        Queries the data explorer for the index corresponding to the filename within
336        """
337        return self.filesWidget.itemFromFilename(filename)
338
339    def quitApplication(self):
340        """
341        Close the reactor and exit nicely.
342        """
343        # Display confirmation messagebox
344        quit_msg = "Are you sure you want to exit the application?"
345        reply = QMessageBox.question(
346            self._parent,
347            'Information',
348            quit_msg,
349            QMessageBox.Yes,
350            QMessageBox.No)
351
352        # Exit if yes
353        if reply == QMessageBox.Yes:
354            reactor.callFromThread(reactor.stop)
355            return True
356
357        return False
358
359    def checkUpdate(self):
360        """
361        Check with the deployment server whether a new version
362        of the application is available.
363        A thread is started for the connecting with the server. The thread calls
364        a call-back method when the current version number has been obtained.
365        """
366        version_info = {"version": "0.0.0"}
367        c = ConnectionProxy(LocalConfig.__update_URL__, LocalConfig.UPDATE_TIMEOUT)
368        response = c.connect()
369        if response is None:
370            return
371        try:
372            content = response.read().strip()
373            logging.info("Connected to www.sasview.org. Latest version: %s"
374                            % (content))
375            version_info = json.loads(content)
376            self.processVersion(version_info)
377        except ValueError as ex:
378            logging.info("Failed to connect to www.sasview.org:", ex)
379
380    def processVersion(self, version_info):
381        """
382        Call-back method for the process of checking for updates.
383        This methods is called by a VersionThread object once the current
384        version number has been obtained. If the check is being done in the
385        background, the user will not be notified unless there's an update.
386
387        :param version: version string
388        """
389        try:
390            version = version_info["version"]
391            if version == "0.0.0":
392                msg = "Could not connect to the application server."
393                msg += " Please try again later."
394                self.communicate.statusBarUpdateSignal.emit(msg)
395
396            elif version.__gt__(LocalConfig.__version__):
397                msg = "Version %s is available! " % str(version)
398                if "download_url" in version_info:
399                    webbrowser.open(version_info["download_url"])
400                else:
401                    webbrowser.open(LocalConfig.__download_page__)
402                self.communicate.statusBarUpdateSignal.emit(msg)
403            else:
404                msg = "You have the latest version"
405                msg += " of %s" % str(LocalConfig.__appname__)
406                self.communicate.statusBarUpdateSignal.emit(msg)
407        except:
408            msg = "guiframe: could not get latest application"
409            msg += " version number\n  %s" % sys.exc_info()[1]
410            logging.error(msg)
411            msg = "Could not connect to the application server."
412            msg += " Please try again later."
413            self.communicate.statusBarUpdateSignal.emit(msg)
414
415    def actionWelcome(self):
416        """ Show the Welcome panel """
417        self.welcomePanel = WelcomePanel()
418        self._workspace.workspace.addSubWindow(self.welcomePanel)
419        self.welcomePanel.show()
420
421    def showWelcomeMessage(self):
422        """ Show the Welcome panel, when required """
423        # Assure the welcome screen is requested
424        show_welcome_widget = True
425        custom_config = get_custom_config()
426        if hasattr(custom_config, "WELCOME_PANEL_SHOW"):
427            if isinstance(custom_config.WELCOME_PANEL_SHOW, bool):
428                show_welcome_widget = custom_config.WELCOME_PANEL_SHOW
429            else:
430                logging.warning("WELCOME_PANEL_SHOW has invalid value in custom_config.py")
431        if show_welcome_widget:
432            self.actionWelcome()
433
434    def addCallbacks(self):
435        """
436        Method defining all signal connections for the gui manager
437        """
438        self.communicate = GuiUtils.Communicate()
439        self.communicate.fileDataReceivedSignal.connect(self.fileWasRead)
440        self.communicate.statusBarUpdateSignal.connect(self.updateStatusBar)
441        self.communicate.updatePerspectiveWithDataSignal.connect(self.updatePerspective)
442        self.communicate.progressBarUpdateSignal.connect(self.updateProgressBar)
443        self.communicate.perspectiveChangedSignal.connect(self.perspectiveChanged)
444        self.communicate.updateTheoryFromPerspectiveSignal.connect(self.updateTheoryFromPerspective)
445        self.communicate.deleteIntermediateTheoryPlotsSignal.connect(self.deleteIntermediateTheoryPlotsByModelID)
446        self.communicate.plotRequestedSignal.connect(self.showPlot)
447        self.communicate.plotFromFilenameSignal.connect(self.showPlotFromFilename)
448        self.communicate.updateModelFromDataOperationPanelSignal.connect(self.updateModelFromDataOperationPanel)
449
450    def addTriggers(self):
451        """
452        Trigger definitions for all menu/toolbar actions.
453        """
454        # disable not yet fully implemented actions
455        self._workspace.actionUndo.setVisible(False)
456        self._workspace.actionRedo.setVisible(False)
457        self._workspace.actionReset.setVisible(False)
458        self._workspace.actionStartup_Settings.setVisible(False)
459        self._workspace.actionImage_Viewer.setVisible(False)
460        self._workspace.actionCombine_Batch_Fit.setVisible(False)
461        # orientation viewer set to invisible SASVIEW-1132
462        self._workspace.actionOrientation_Viewer.setVisible(False)
463
464        # File
465        self._workspace.actionLoadData.triggered.connect(self.actionLoadData)
466        self._workspace.actionLoad_Data_Folder.triggered.connect(self.actionLoad_Data_Folder)
467        self._workspace.actionOpen_Project.triggered.connect(self.actionOpen_Project)
468        self._workspace.actionOpen_Analysis.triggered.connect(self.actionOpen_Analysis)
469        self._workspace.actionSave.triggered.connect(self.actionSave_Project)
470        self._workspace.actionSave_Analysis.triggered.connect(self.actionSave_Analysis)
471        self._workspace.actionQuit.triggered.connect(self.actionQuit)
472        # Edit
473        self._workspace.actionUndo.triggered.connect(self.actionUndo)
474        self._workspace.actionRedo.triggered.connect(self.actionRedo)
475        self._workspace.actionCopy.triggered.connect(self.actionCopy)
476        self._workspace.actionPaste.triggered.connect(self.actionPaste)
477        self._workspace.actionReport.triggered.connect(self.actionReport)
478        self._workspace.actionReset.triggered.connect(self.actionReset)
479        self._workspace.actionExcel.triggered.connect(self.actionExcel)
480        self._workspace.actionLatex.triggered.connect(self.actionLatex)
481        # View
482        self._workspace.actionShow_Grid_Window.triggered.connect(self.actionShow_Grid_Window)
483        self._workspace.actionHide_Toolbar.triggered.connect(self.actionHide_Toolbar)
484        self._workspace.actionStartup_Settings.triggered.connect(self.actionStartup_Settings)
485        self._workspace.actionCategory_Manager.triggered.connect(self.actionCategory_Manager)
486        self._workspace.actionHide_DataExplorer.triggered.connect(self.actionHide_DataExplorer)
487        self._workspace.actionHide_LogExplorer.triggered.connect(self.actionHide_LogExplorer)
488        # Tools
489        self._workspace.actionData_Operation.triggered.connect(self.actionData_Operation)
490        self._workspace.actionSLD_Calculator.triggered.connect(self.actionSLD_Calculator)
491        self._workspace.actionDensity_Volume_Calculator.triggered.connect(self.actionDensity_Volume_Calculator)
492        self._workspace.actionKeissig_Calculator.triggered.connect(self.actionKiessig_Calculator)
493        #self._workspace.actionKIESSING_Calculator.triggered.connect(self.actionKIESSING_Calculator)
494        self._workspace.actionSlit_Size_Calculator.triggered.connect(self.actionSlit_Size_Calculator)
495        self._workspace.actionSAS_Resolution_Estimator.triggered.connect(self.actionSAS_Resolution_Estimator)
496        self._workspace.actionGeneric_Scattering_Calculator.triggered.connect(self.actionGeneric_Scattering_Calculator)
497        self._workspace.actionPython_Shell_Editor.triggered.connect(self.actionPython_Shell_Editor)
498        self._workspace.actionImage_Viewer.triggered.connect(self.actionImage_Viewer)
499        self._workspace.actionOrientation_Viewer.triggered.connect(self.actionOrientation_Viewer)
500        self._workspace.actionFreeze_Theory.triggered.connect(self.actionFreeze_Theory)
501        # Fitting
502        self._workspace.actionNew_Fit_Page.triggered.connect(self.actionNew_Fit_Page)
503        self._workspace.actionConstrained_Fit.triggered.connect(self.actionConstrained_Fit)
504        self._workspace.actionCombine_Batch_Fit.triggered.connect(self.actionCombine_Batch_Fit)
505        self._workspace.actionFit_Options.triggered.connect(self.actionFit_Options)
506        self._workspace.actionGPU_Options.triggered.connect(self.actionGPU_Options)
507        self._workspace.actionFit_Results.triggered.connect(self.actionFit_Results)
508        self._workspace.actionAdd_Custom_Model.triggered.connect(self.actionAdd_Custom_Model)
509        self._workspace.actionEdit_Custom_Model.triggered.connect(self.actionEdit_Custom_Model)
510        self._workspace.actionManage_Custom_Models.triggered.connect(self.actionManage_Custom_Models)
511        self._workspace.actionAddMult_Models.triggered.connect(self.actionAddMult_Models)
512        self._workspace.actionEditMask.triggered.connect(self.actionEditMask)
513
514        # Window
515        self._workspace.actionCascade.triggered.connect(self.actionCascade)
516        self._workspace.actionTile.triggered.connect(self.actionTile)
517        self._workspace.actionArrange_Icons.triggered.connect(self.actionArrange_Icons)
518        self._workspace.actionNext.triggered.connect(self.actionNext)
519        self._workspace.actionPrevious.triggered.connect(self.actionPrevious)
520        self._workspace.actionClosePlots.triggered.connect(self.actionClosePlots)
521        # Analysis
522        self._workspace.actionFitting.triggered.connect(self.actionFitting)
523        self._workspace.actionInversion.triggered.connect(self.actionInversion)
524        self._workspace.actionInvariant.triggered.connect(self.actionInvariant)
525        self._workspace.actionCorfunc.triggered.connect(self.actionCorfunc)
526        # Help
527        self._workspace.actionDocumentation.triggered.connect(self.actionDocumentation)
528        self._workspace.actionTutorial.triggered.connect(self.actionTutorial)
529        self._workspace.actionAcknowledge.triggered.connect(self.actionAcknowledge)
530        self._workspace.actionAbout.triggered.connect(self.actionAbout)
531        self._workspace.actionWelcomeWidget.triggered.connect(self.actionWelcome)
532        self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update)
533
534        self.communicate.sendDataToGridSignal.connect(self.showBatchOutput)
535        self.communicate.resultPlotUpdateSignal.connect(self.showFitResults)
536
537    #============ FILE =================
538    def actionLoadData(self):
539        """
540        Menu File/Load Data File(s)
541        """
542        self.filesWidget.loadFile()
543
544    def actionLoad_Data_Folder(self):
545        """
546        Menu File/Load Data Folder
547        """
548        self.filesWidget.loadFolder()
549
550    def actionOpen_Project(self):
551        """
552        Menu Open Project
553        """
554        self.filesWidget.loadProject()
555
556    def actionOpen_Analysis(self):
557        """
558        """
559        self.filesWidget.loadAnalysis()
560        pass
561
562    def actionSave_Project(self):
563        """
564        Menu Save Project
565        """
566        filename = self.filesWidget.saveProject()
567
568        # datasets
569        all_data = self.filesWidget.getAllData()
570
571        # fit tabs
572        params = self.perspective().serializeAllFitpage()
573
574        # project dictionary structure:
575        # analysis[data.id] = [{"fit_data":[data, checkbox, child data],
576        #                       "fit_params":[fitpage_state]}
577        # "fit_params" not present if dataset not sent to fitting
578        analysis = {}
579
580        for id, data in all_data.items():
581            if id=='is_batch':
582                analysis['is_batch'] = data
583                continue
584            data_content = {"fit_data":data}
585            if id in params.keys():
586                # this dataset is represented also by the fit tab. Add to it.
587                data_content["fit_params"] = params[id]
588            analysis[id] = data_content
589
590        with open(filename, 'w') as outfile:
591            GuiUtils.saveData(outfile, analysis)
592
593    def actionSave_Analysis(self):
594        """
595        Menu File/Save Analysis
596        """
597        per = self.perspective()
598        if not isinstance(per, FittingWindow):
599            return
600        # get fit page serialization
601        params = per.serializeCurrentFitpage()
602        # Find dataset ids for the current tab
603        # (can be multiple, if batch)
604        data_id = per.currentTabDataId()
605        tab_id = per.currentTab.tab_id
606        analysis = {}
607        for id in data_id:
608            an = {}
609            data_for_id = self.filesWidget.getDataForID(id)
610            an['fit_data'] = data_for_id
611            an['fit_params'] = [params]
612            analysis[id] = an
613
614        self.filesWidget.saveAnalysis(analysis, tab_id)
615
616    def actionQuit(self):
617        """
618        Close the reactor, exit the application.
619        """
620        self.quitApplication()
621
622    #============ EDIT =================
623    def actionUndo(self):
624        """
625        """
626        print("actionUndo TRIGGERED")
627        pass
628
629    def actionRedo(self):
630        """
631        """
632        print("actionRedo TRIGGERED")
633        pass
634
635    def actionCopy(self):
636        """
637        Send a signal to the fitting perspective so parameters
638        can be saved to the clipboard
639        """
640        self.communicate.copyFitParamsSignal.emit("")
641        self._workspace.actionPaste.setEnabled(True)
642        pass
643
644    def actionPaste(self):
645        """
646        Send a signal to the fitting perspective so parameters
647        from the clipboard can be used to modify the fit state
648        """
649        self.communicate.pasteFitParamsSignal.emit()
650
651    def actionReport(self):
652        """
653        Show the Fit Report dialog.
654        """
655        report_list = None
656        if getattr(self._current_perspective, "currentTab"):
657            try:
658                report_list = self._current_perspective.currentTab.getReport()
659            except Exception as ex:
660                logging.error("Report generation failed with: " + str(ex))
661
662        if report_list is not None:
663            self.report_dialog = ReportDialog(parent=self, report_list=report_list)
664            self.report_dialog.show()
665
666    def actionReset(self):
667        """
668        """
669        logging.warning(" *** actionOpen_Analysis logging *******")
670        print("actionReset print TRIGGERED")
671        sys.stderr.write("STDERR - TRIGGERED")
672        pass
673
674    def actionExcel(self):
675        """
676        Send a signal to the fitting perspective so parameters
677        can be saved to the clipboard
678        """
679        self.communicate.copyExcelFitParamsSignal.emit("Excel")
680
681    def actionLatex(self):
682        """
683        Send a signal to the fitting perspective so parameters
684        can be saved to the clipboard
685        """
686        self.communicate.copyLatexFitParamsSignal.emit("Latex")
687
688    #============ VIEW =================
689    def actionShow_Grid_Window(self):
690        """
691        """
692        self.showBatchOutput(None)
693
694    def showBatchOutput(self, output_data):
695        """
696        Display/redisplay the batch fit viewer
697        """
698        self.grid_subwindow.setVisible(True)
699        if output_data:
700            self.grid_window.addFitResults(output_data)
701
702    def actionHide_Toolbar(self):
703        """
704        Toggle toolbar vsibility
705        """
706        if self._workspace.toolBar.isVisible():
707            self._workspace.actionHide_Toolbar.setText("Show Toolbar")
708            self._workspace.toolBar.setVisible(False)
709        else:
710            self._workspace.actionHide_Toolbar.setText("Hide Toolbar")
711            self._workspace.toolBar.setVisible(True)
712        pass
713
714    def actionHide_DataExplorer(self):
715        """
716        Toggle Data Explorer vsibility
717        """
718        if self.dockedFilesWidget.isVisible():
719            self.dockedFilesWidget.setVisible(False)
720        else:
721            self.dockedFilesWidget.setVisible(True)
722        pass
723
724    def actionHide_LogExplorer(self):
725        """
726        Toggle Data Explorer vsibility
727        """
728        if self.logDockWidget.isVisible():
729            self.logDockWidget.setVisible(False)
730        else:
731            self.logDockWidget.setVisible(True)
732        pass
733
734    def actionStartup_Settings(self):
735        """
736        """
737        print("actionStartup_Settings TRIGGERED")
738        pass
739
740    def actionCategory_Manager(self):
741        """
742        """
743        self.categoryManagerWidget.show()
744
745    #============ TOOLS =================
746    def actionData_Operation(self):
747        """
748        """
749        self.communicate.sendDataToPanelSignal.emit(self._data_manager.get_all_data())
750
751        self.DataOperation.show()
752
753    def actionSLD_Calculator(self):
754        """
755        """
756        self.SLDCalculator.show()
757
758    def actionDensity_Volume_Calculator(self):
759        """
760        """
761        self.DVCalculator.show()
762
763    def actionKiessig_Calculator(self):
764        """
765        """
766        self.KIESSIGCalculator.show()
767
768    def actionSlit_Size_Calculator(self):
769        """
770        """
771        self.SlitSizeCalculator.show()
772
773    def actionSAS_Resolution_Estimator(self):
774        """
775        """
776        try:
777            self.ResolutionCalculator.show()
778        except Exception as ex:
779            logging.error(str(ex))
780            return
781
782    def actionGeneric_Scattering_Calculator(self):
783        """
784        """
785        try:
786            self.GENSASCalculator.show()
787        except Exception as ex:
788            logging.error(str(ex))
789            return
790
791    def actionPython_Shell_Editor(self):
792        """
793        Display the Jupyter console as a docked widget.
794        """
795        # Import moved here for startup performance reasons
796        from sas.qtgui.Utilities.IPythonWidget import IPythonWidget
797        terminal = IPythonWidget()
798
799        # Add the console window as another docked widget
800        self.ipDockWidget = QDockWidget("IPython", self._workspace)
801        self.ipDockWidget.setObjectName("IPythonDockWidget")
802        self.ipDockWidget.setWidget(terminal)
803        self._workspace.addDockWidget(Qt.RightDockWidgetArea, self.ipDockWidget)
804
805    def actionFreeze_Theory(self):
806        """
807        Convert a child index with data into a separate top level dataset
808        """
809        self.filesWidget.freezeCheckedData()
810
811    def actionOrientation_Viewer(self):
812        """
813        Make sasmodels orientation & jitter viewer available
814        """
815        from sasmodels.jitter import run as orientation_run
816        try:
817            orientation_run()
818        except Exception as ex:
819            logging.error(str(ex))
820
821    def actionImage_Viewer(self):
822        """
823        """
824        print("actionImage_Viewer TRIGGERED")
825        pass
826
827    #============ FITTING =================
828    def actionNew_Fit_Page(self):
829        """
830        Add a new, empty Fit page in the fitting perspective.
831        """
832        # Make sure the perspective is correct
833        per = self.perspective()
834        if not isinstance(per, FittingWindow):
835            return
836        per.addFit(None)
837
838    def actionConstrained_Fit(self):
839        """
840        Add a new Constrained and Simult. Fit page in the fitting perspective.
841        """
842        per = self.perspective()
843        if not isinstance(per, FittingWindow):
844            return
845        per.addConstraintTab()
846
847    def actionCombine_Batch_Fit(self):
848        """
849        """
850        print("actionCombine_Batch_Fit TRIGGERED")
851        pass
852
853    def actionFit_Options(self):
854        """
855        """
856        if getattr(self._current_perspective, "fit_options_widget"):
857            self._current_perspective.fit_options_widget.show()
858        pass
859
860    def actionGPU_Options(self):
861        """
862        Load the OpenCL selection dialog if the fitting perspective is active
863        """
864        if hasattr(self._current_perspective, "gpu_options_widget"):
865            self._current_perspective.gpu_options_widget.show()
866        pass
867
868    def actionFit_Results(self):
869        """
870        """
871        self.showFitResults(None)
872
873    def showFitResults(self, output_data):
874        """
875        Show bumps convergence plots
876        """
877        self.results_frame.setVisible(True)
878        if output_data:
879            self.results_panel.onPlotResults(output_data, optimizer=self.perspective().optimizer)
880
881    def actionAdd_Custom_Model(self):
882        """
883        """
884        self.model_editor = TabbedModelEditor(self)
885        self.model_editor.show()
886
887    def actionEdit_Custom_Model(self):
888        """
889        """
890        self.model_editor = TabbedModelEditor(self, edit_only=True)
891        self.model_editor.show()
892
893    def actionManage_Custom_Models(self):
894        """
895        """
896        self.model_manager = PluginManager(self)
897        self.model_manager.show()
898
899    def actionAddMult_Models(self):
900        """
901        """
902        # Add Simple Add/Multiply Editor
903        self.add_mult_editor = AddMultEditor(self)
904        self.add_mult_editor.show()
905
906    def actionEditMask(self):
907
908        self.communicate.extMaskEditorSignal.emit()
909
910    #============ ANALYSIS =================
911    def actionFitting(self):
912        """
913        Change to the Fitting perspective
914        """
915        self.perspectiveChanged("Fitting")
916        # Notify other widgets
917        self.filesWidget.onAnalysisUpdate("Fitting")
918
919    def actionInversion(self):
920        """
921        Change to the Inversion perspective
922        """
923        self.perspectiveChanged("Inversion")
924        self.filesWidget.onAnalysisUpdate("Inversion")
925
926    def actionInvariant(self):
927        """
928        Change to the Invariant perspective
929        """
930        self.perspectiveChanged("Invariant")
931        self.filesWidget.onAnalysisUpdate("Invariant")
932
933    def actionCorfunc(self):
934        """
935        Change to the Corfunc perspective
936        """
937        self.perspectiveChanged("Corfunc")
938        self.filesWidget.onAnalysisUpdate("Corfunc")
939
940    #============ WINDOW =================
941    def actionCascade(self):
942        """
943        Arranges all the child windows in a cascade pattern.
944        """
945        self._workspace.workspace.cascadeSubWindows()
946
947    def actionTile(self):
948        """
949        Tile workspace windows
950        """
951        self._workspace.workspace.tileSubWindows()
952
953    def actionArrange_Icons(self):
954        """
955        Arranges all iconified windows at the bottom of the workspace
956        """
957        self._workspace.workspace.arrangeIcons()
958
959    def actionNext(self):
960        """
961        Gives the input focus to the next window in the list of child windows.
962        """
963        self._workspace.workspace.activateNextSubWindow()
964
965    def actionPrevious(self):
966        """
967        Gives the input focus to the previous window in the list of child windows.
968        """
969        self._workspace.workspace.activatePreviousSubWindow()
970
971    def actionClosePlots(self):
972        """
973        Closes all Plotters and Plotter2Ds.
974        """
975        self.filesWidget.closeAllPlots()
976        pass
977
978    #============ HELP =================
979    def actionDocumentation(self):
980        """
981        Display the documentation
982
983        TODO: use QNetworkAccessManager to assure _helpLocation is valid
984        """
985        helpfile = "/index.html"
986        self.showHelp(helpfile)
987
988    def actionTutorial(self):
989        """
990        Open the tutorial PDF file with default PDF renderer
991        """
992        # Not terribly safe here. Shell injection warning.
993        # isfile() helps but this probably needs a better solution.
994        if os.path.isfile(self._tutorialLocation):
995            result = subprocess.Popen([self._tutorialLocation], shell=True)
996
997    def actionAcknowledge(self):
998        """
999        Open the Acknowledgements widget
1000        """
1001        self.ackWidget.show()
1002
1003    def actionAbout(self):
1004        """
1005        Open the About box
1006        """
1007        # Update the about box with current version and stuff
1008
1009        # TODO: proper sizing
1010        self.aboutWidget.show()
1011
1012    def actionCheck_for_update(self):
1013        """
1014        Menu Help/Check for Update
1015        """
1016        self.checkUpdate()
1017
1018    def updateTheoryFromPerspective(self, index):
1019        """
1020        Catch the theory update signal from a perspective
1021        Send the request to the DataExplorer for updating the theory model.
1022        """
1023        self.filesWidget.updateTheoryFromPerspective(index)
1024
1025    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1026        """
1027        Catch the signal to delete items in the Theory item model which correspond to a model ID.
1028        Send the request to the DataExplorer for updating the theory model.
1029        """
1030        self.filesWidget.deleteIntermediateTheoryPlotsByModelID(model_id)
1031
1032    def updateModelFromDataOperationPanel(self, new_item, new_datalist_item):
1033        """
1034        :param new_item: item to be added to list of loaded files
1035        :param new_datalist_item:
1036        """
1037        if not isinstance(new_item, QStandardItem) or \
1038                not isinstance(new_datalist_item, dict):
1039            msg = "Wrong data type returned from calculations."
1040            raise AttributeError(msg)
1041
1042        self.filesWidget.model.appendRow(new_item)
1043        self._data_manager.add_data(new_datalist_item)
1044
1045    def showPlotFromFilename(self, filename):
1046        """
1047        Pass the show plot request to the data explorer
1048        """
1049        if hasattr(self, "filesWidget"):
1050            self.filesWidget.displayFile(filename=filename, is_data=True)
1051
1052    def showPlot(self, plot, id):
1053        """
1054        Pass the show plot request to the data explorer
1055        """
1056        if hasattr(self, "filesWidget"):
1057            self.filesWidget.displayData(plot, id)
1058
1059    def uncheckAllMenuItems(self, menuObject):
1060        """
1061        Uncheck all options in a given menu
1062        """
1063        menuObjects = menuObject.actions()
1064
1065        for menuItem in menuObjects:
1066            menuItem.setChecked(False)
1067
1068    def checkAnalysisOption(self, analysisMenuOption):
1069        """
1070        Unchecks all the items in the analysis menu and checks the item passed
1071        """
1072        self.uncheckAllMenuItems(self._workspace.menuAnalysis)
1073        analysisMenuOption.setChecked(True)
1074
1075    def clearPerspectiveMenubarOptions(self, perspective):
1076        """
1077        When closing a perspective, clears the menu bar
1078        """
1079        for menuItem in self._workspace.menuAnalysis.actions():
1080            menuItem.setChecked(False)
1081
1082        if isinstance(self._current_perspective, Perspectives.PERSPECTIVES["Fitting"]):
1083            self._workspace.menubar.removeAction(self._workspace.menuFitting.menuAction())
1084
1085    def setupPerspectiveMenubarOptions(self, perspective):
1086        """
1087        When setting a perspective, sets up the menu bar
1088        """
1089        self._workspace.actionReport.setEnabled(False)
1090        self._workspace.actionOpen_Analysis.setEnabled(False)
1091        self._workspace.actionSave_Analysis.setEnabled(False)
1092        if hasattr(perspective, 'isSerializable') and perspective.isSerializable():
1093            self._workspace.actionOpen_Analysis.setEnabled(True)
1094            self._workspace.actionSave_Analysis.setEnabled(True)
1095
1096        if isinstance(perspective, Perspectives.PERSPECTIVES["Fitting"]):
1097            self.checkAnalysisOption(self._workspace.actionFitting)
1098            # Put the fitting menu back in
1099            # This is a bit involved but it is needed to preserve the menu ordering
1100            self._workspace.menubar.removeAction(self._workspace.menuWindow.menuAction())
1101            self._workspace.menubar.removeAction(self._workspace.menuHelp.menuAction())
1102            self._workspace.menubar.addAction(self._workspace.menuFitting.menuAction())
1103            self._workspace.menubar.addAction(self._workspace.menuWindow.menuAction())
1104            self._workspace.menubar.addAction(self._workspace.menuHelp.menuAction())
1105            self._workspace.actionReport.setEnabled(True)
1106
1107        elif isinstance(perspective, Perspectives.PERSPECTIVES["Invariant"]):
1108            self.checkAnalysisOption(self._workspace.actionInvariant)
1109        elif isinstance(perspective, Perspectives.PERSPECTIVES["Inversion"]):
1110            self.checkAnalysisOption(self._workspace.actionInversion)
1111        elif isinstance(perspective, Perspectives.PERSPECTIVES["Corfunc"]):
1112            self.checkAnalysisOption(self._workspace.actionCorfunc)
Note: See TracBrowser for help on using the repository browser.