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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_sync_sascalc
Last change on this file since 99f8760 was 99f8760, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 months ago

Improvements to batch save/load SASVIEW-1222.
Project save/load will now recreate the grid window as well as retain
result theories under dataset indices.

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