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

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

Reword the ordering label

  • Property mode set to 100644
File size: 42.9 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        # Add FileDialog widget as docked
124        self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager)
125        ObjectLibrary.addObject('DataExplorer', self.filesWidget)
126
127        self.dockedFilesWidget = QDockWidget("Data Explorer", self._workspace)
128        self.dockedFilesWidget.setFloating(False)
129        self.dockedFilesWidget.setWidget(self.filesWidget)
130
131        # Modify menu items on widget visibility change
132        self.dockedFilesWidget.visibilityChanged.connect(self.updateContextMenus)
133
134        self._workspace.addDockWidget(Qt.LeftDockWidgetArea, self.dockedFilesWidget)
135        self._workspace.resizeDocks([self.dockedFilesWidget], [305], Qt.Horizontal)
136
137        # Add the console window as another docked widget
138        self.logDockWidget = QDockWidget("Log Explorer", self._workspace)
139        self.logDockWidget.setObjectName("LogDockWidget")
140        self.logDockWidget.visibilityChanged.connect(self.updateLogContextMenus)
141
142
143        self.listWidget = QTextBrowser()
144        self.logDockWidget.setWidget(self.listWidget)
145        self._workspace.addDockWidget(Qt.BottomDockWidgetArea, self.logDockWidget)
146
147        # Add other, minor widgets
148        self.ackWidget = Acknowledgements()
149        self.aboutWidget = AboutBox()
150        self.categoryManagerWidget = CategoryManager(self._parent, manager=self)
151
152        self.grid_window = None
153        self.grid_window = BatchOutputPanel(parent=self)
154        if sys.platform == "darwin":
155            self.grid_window.menubar.setNativeMenuBar(False)
156        self.grid_subwindow = self._workspace.workspace.addSubWindow(self.grid_window)
157        self.grid_subwindow.setVisible(False)
158        self.grid_window.windowClosedSignal.connect(lambda: self.grid_subwindow.setVisible(False))
159
160        self.results_panel = ResultPanel(parent=self._parent, manager=self)
161        self.results_frame = self._workspace.workspace.addSubWindow(self.results_panel)
162        self.results_frame.setVisible(False)
163        self.results_panel.windowClosedSignal.connect(lambda: self.results_frame.setVisible(False))
164
165        self._workspace.toolBar.setVisible(LocalConfig.TOOLBAR_SHOW)
166        self._workspace.actionHide_Toolbar.setText("Show Toolbar")
167
168        # Add calculators - floating for usability
169        self.SLDCalculator = SldPanel(self)
170        self.DVCalculator = DensityPanel(self)
171        self.KIESSIGCalculator = KiessigPanel(self)
172        self.SlitSizeCalculator = SlitSizeCalculator(self)
173        self.GENSASCalculator = GenericScatteringCalculator(self)
174        self.ResolutionCalculator = ResolutionCalculatorPanel(self)
175        self.DataOperation = DataOperationUtilityPanel(self)
176
177    def addCategories(self):
178        """
179        Make sure categories.json exists and if not compile it and install in ~/.sasview
180        """
181        try:
182            from sas.sascalc.fit.models import ModelManager
183            from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
184            model_list = ModelManager().cat_model_list()
185            CategoryInstaller.check_install(model_list=model_list)
186        except Exception:
187            import traceback
188            logger.error("%s: could not load SasView models")
189            logger.error(traceback.format_exc())
190
191    def updateLogContextMenus(self, visible=False):
192        """
193        Modify the View/Data Explorer menu item text on widget visibility
194        """
195        if visible:
196            self._workspace.actionHide_LogExplorer.setText("Hide Log Explorer")
197        else:
198            self._workspace.actionHide_LogExplorer.setText("Show Log Explorer")
199
200    def updateContextMenus(self, visible=False):
201        """
202        Modify the View/Data Explorer menu item text on widget visibility
203        """
204        if visible:
205            self._workspace.actionHide_DataExplorer.setText("Hide Data Explorer")
206        else:
207            self._workspace.actionHide_DataExplorer.setText("Show Data Explorer")
208
209    def statusBarSetup(self):
210        """
211        Define the status bar.
212        | <message label> .... | Progress Bar |
213
214        Progress bar invisible until explicitly shown
215        """
216        self.progress = QProgressBar()
217        self._workspace.statusbar.setSizeGripEnabled(False)
218
219        self.statusLabel = QLabel()
220        self.statusLabel.setText("Welcome to SasView")
221        self._workspace.statusbar.addPermanentWidget(self.statusLabel, 1)
222        self._workspace.statusbar.addPermanentWidget(self.progress, stretch=0)
223        self.progress.setRange(0, 100)
224        self.progress.setValue(0)
225        self.progress.setTextVisible(True)
226        self.progress.setVisible(False)
227
228    def fileWasRead(self, data):
229        """
230        Callback for fileDataReceivedSignal
231        """
232        pass
233
234    def showHelp(self, url):
235        """
236        Open a local url in the default browser
237        """
238        GuiUtils.showHelp(url)
239
240    def workspace(self):
241        """
242        Accessor for the main window workspace
243        """
244        return self._workspace.workspace
245
246    def perspectiveChanged(self, perspective_name):
247        """
248        Respond to change of the perspective signal
249        """
250        # Close the previous perspective
251        self.clearPerspectiveMenubarOptions(self._current_perspective)
252        if self._current_perspective:
253            self._current_perspective.setClosable()
254            self._current_perspective.close()
255            self._workspace.workspace.removeSubWindow(self._current_perspective)
256        # Default perspective
257        self._current_perspective = Perspectives.PERSPECTIVES[str(perspective_name)](parent=self)
258
259        self.setupPerspectiveMenubarOptions(self._current_perspective)
260
261        subwindow = self._workspace.workspace.addSubWindow(self._current_perspective)
262
263        # Resize to the workspace height
264        workspace_height = self._workspace.workspace.sizeHint().height()
265        perspective_size = self._current_perspective.sizeHint()
266        perspective_width = perspective_size.width()
267        self._current_perspective.resize(perspective_width, workspace_height-10)
268
269        self._current_perspective.show()
270
271    def updatePerspective(self, data):
272        """
273        Update perspective with data sent.
274        """
275        assert isinstance(data, list)
276        if self._current_perspective is not None:
277            self._current_perspective.setData(list(data.values()))
278        else:
279            msg = "No perspective is currently active."
280            logging.info(msg)
281
282    def communicator(self):
283        """ Accessor for the communicator """
284        return self.communicate
285
286    def perspective(self):
287        """ Accessor for the perspective """
288        return self._current_perspective
289
290    def updateProgressBar(self, value):
291        """
292        Update progress bar with the required value (0-100)
293        """
294        assert -1 <= value <= 100
295        if value == -1:
296            self.progress.setVisible(False)
297            return
298        if not self.progress.isVisible():
299            self.progress.setTextVisible(True)
300            self.progress.setVisible(True)
301
302        self.progress.setValue(value)
303
304    def updateStatusBar(self, text):
305        """
306        Set the status bar text
307        """
308        self.statusLabel.setText(text)
309
310    def appendLog(self, msg):
311        """Appends a message to the list widget in the Log Explorer. Use this
312        instead of listWidget.insertPlainText() to facilitate auto-scrolling"""
313        self.listWidget.append(msg.strip())
314
315    def createGuiData(self, item, p_file=None):
316        """
317        Access the Data1D -> plottable Data1D conversion
318        """
319        return self._data_manager.create_gui_data(item, p_file)
320
321    def setData(self, data):
322        """
323        Sends data to current perspective
324        """
325        if self._current_perspective is not None:
326            self._current_perspective.setData(list(data.values()))
327        else:
328            msg = "Guiframe does not have a current perspective"
329            logging.info(msg)
330
331    def findItemFromFilename(self, filename):
332        """
333        Queries the data explorer for the index corresponding to the filename within
334        """
335        return self.filesWidget.itemFromFilename(filename)
336
337    def quitApplication(self):
338        """
339        Close the reactor and exit nicely.
340        """
341        # Display confirmation messagebox
342        quit_msg = "Are you sure you want to exit the application?"
343        reply = QMessageBox.question(
344            self._parent,
345            'Information',
346            quit_msg,
347            QMessageBox.Yes,
348            QMessageBox.No)
349
350        # Exit if yes
351        if reply == QMessageBox.Yes:
352            # save the paths etc.
353            self.saveCustomConfig()
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={}
573        perspective = self.perspective()
574        if hasattr(perspective, 'isSerializable') and perspective.isSerializable():
575            params = perspective.serializeAllFitpage()
576
577        # project dictionary structure:
578        # analysis[data.id] = [{"fit_data":[data, checkbox, child data],
579        #                       "fit_params":[fitpage_state]}
580        # "fit_params" not present if dataset not sent to fitting
581        analysis = {}
582
583        for id, data in all_data.items():
584            if id=='is_batch':
585                analysis['is_batch'] = data
586                continue
587            data_content = {"fit_data":data}
588            if id in params.keys():
589                # this dataset is represented also by the fit tab. Add to it.
590                data_content["fit_params"] = params[id]
591            analysis[id] = data_content
592
593        # standalone constraint pages
594        for keys, values in params.items():
595            if not 'is_constraint' in values[0]:
596                continue
597            analysis[keys] = values[0]
598
599        with open(filename, 'w') as outfile:
600            GuiUtils.saveData(outfile, analysis)
601
602    def actionSave_Analysis(self):
603        """
604        Menu File/Save Analysis
605        """
606        per = self.perspective()
607        if not isinstance(per, FittingWindow):
608            return
609        # get fit page serialization
610        params = per.serializeCurrentFitpage()
611        # Find dataset ids for the current tab
612        # (can be multiple, if batch)
613        data_id = per.currentTabDataId()
614        tab_id = per.currentTab.tab_id
615        analysis = {}
616        for id in data_id:
617            an = {}
618            data_for_id = self.filesWidget.getDataForID(id)
619            an['fit_data'] = data_for_id
620            an['fit_params'] = [params]
621            analysis[id] = an
622
623        self.filesWidget.saveAnalysis(analysis, tab_id)
624
625    def actionQuit(self):
626        """
627        Close the reactor, exit the application.
628        """
629        self.quitApplication()
630
631    #============ EDIT =================
632    def actionUndo(self):
633        """
634        """
635        print("actionUndo TRIGGERED")
636        pass
637
638    def actionRedo(self):
639        """
640        """
641        print("actionRedo TRIGGERED")
642        pass
643
644    def actionCopy(self):
645        """
646        Send a signal to the fitting perspective so parameters
647        can be saved to the clipboard
648        """
649        self.communicate.copyFitParamsSignal.emit("")
650        self._workspace.actionPaste.setEnabled(True)
651        pass
652
653    def actionPaste(self):
654        """
655        Send a signal to the fitting perspective so parameters
656        from the clipboard can be used to modify the fit state
657        """
658        self.communicate.pasteFitParamsSignal.emit()
659
660    def actionReport(self):
661        """
662        Show the Fit Report dialog.
663        """
664        report_list = None
665        if getattr(self._current_perspective, "currentTab"):
666            try:
667                report_list = self._current_perspective.currentTab.getReport()
668            except Exception as ex:
669                logging.error("Report generation failed with: " + str(ex))
670
671        if report_list is not None:
672            self.report_dialog = ReportDialog(parent=self, report_list=report_list)
673            self.report_dialog.show()
674
675    def actionReset(self):
676        """
677        """
678        logging.warning(" *** actionOpen_Analysis logging *******")
679        print("actionReset print TRIGGERED")
680        sys.stderr.write("STDERR - TRIGGERED")
681        pass
682
683    def actionExcel(self):
684        """
685        Send a signal to the fitting perspective so parameters
686        can be saved to the clipboard
687        """
688        self.communicate.copyExcelFitParamsSignal.emit("Excel")
689
690    def actionLatex(self):
691        """
692        Send a signal to the fitting perspective so parameters
693        can be saved to the clipboard
694        """
695        self.communicate.copyLatexFitParamsSignal.emit("Latex")
696
697    #============ VIEW =================
698    def actionShow_Grid_Window(self):
699        """
700        """
701        self.showBatchOutput(None)
702
703    def showBatchOutput(self, output_data):
704        """
705        Display/redisplay the batch fit viewer
706        """
707        self.grid_subwindow.setVisible(True)
708        if output_data:
709            self.grid_window.addFitResults(output_data)
710
711    def actionHide_Toolbar(self):
712        """
713        Toggle toolbar vsibility
714        """
715        if self._workspace.toolBar.isVisible():
716            self._workspace.actionHide_Toolbar.setText("Show Toolbar")
717            self._workspace.toolBar.setVisible(False)
718        else:
719            self._workspace.actionHide_Toolbar.setText("Hide Toolbar")
720            self._workspace.toolBar.setVisible(True)
721        pass
722
723    def actionHide_DataExplorer(self):
724        """
725        Toggle Data Explorer vsibility
726        """
727        if self.dockedFilesWidget.isVisible():
728            self.dockedFilesWidget.setVisible(False)
729        else:
730            self.dockedFilesWidget.setVisible(True)
731        pass
732
733    def actionHide_LogExplorer(self):
734        """
735        Toggle Data Explorer vsibility
736        """
737        if self.logDockWidget.isVisible():
738            self.logDockWidget.setVisible(False)
739        else:
740            self.logDockWidget.setVisible(True)
741        pass
742
743    def actionStartup_Settings(self):
744        """
745        """
746        print("actionStartup_Settings TRIGGERED")
747        pass
748
749    def actionCategory_Manager(self):
750        """
751        """
752        self.categoryManagerWidget.show()
753
754    #============ TOOLS =================
755    def actionData_Operation(self):
756        """
757        """
758        self.communicate.sendDataToPanelSignal.emit(self._data_manager.get_all_data())
759
760        self.DataOperation.show()
761
762    def actionSLD_Calculator(self):
763        """
764        """
765        self.SLDCalculator.show()
766
767    def actionDensity_Volume_Calculator(self):
768        """
769        """
770        self.DVCalculator.show()
771
772    def actionKiessig_Calculator(self):
773        """
774        """
775        self.KIESSIGCalculator.show()
776
777    def actionSlit_Size_Calculator(self):
778        """
779        """
780        self.SlitSizeCalculator.show()
781
782    def actionSAS_Resolution_Estimator(self):
783        """
784        """
785        try:
786            self.ResolutionCalculator.show()
787        except Exception as ex:
788            logging.error(str(ex))
789            return
790
791    def actionGeneric_Scattering_Calculator(self):
792        """
793        """
794        try:
795            self.GENSASCalculator.show()
796        except Exception as ex:
797            logging.error(str(ex))
798            return
799
800    def actionPython_Shell_Editor(self):
801        """
802        Display the Jupyter console as a docked widget.
803        """
804        # Import moved here for startup performance reasons
805        from sas.qtgui.Utilities.IPythonWidget import IPythonWidget
806        terminal = IPythonWidget()
807
808        # Add the console window as another docked widget
809        self.ipDockWidget = QDockWidget("IPython", self._workspace)
810        self.ipDockWidget.setObjectName("IPythonDockWidget")
811        self.ipDockWidget.setWidget(terminal)
812        self._workspace.addDockWidget(Qt.RightDockWidgetArea, self.ipDockWidget)
813
814    def actionFreeze_Theory(self):
815        """
816        Convert a child index with data into a separate top level dataset
817        """
818        self.filesWidget.freezeCheckedData()
819
820    def actionOrientation_Viewer(self):
821        """
822        Make sasmodels orientation & jitter viewer available
823        """
824        from sasmodels.jitter import run as orientation_run
825        try:
826            orientation_run()
827        except Exception as ex:
828            logging.error(str(ex))
829
830    def actionImage_Viewer(self):
831        """
832        """
833        try:
834            self.image_viewer = ImageViewer(self)
835            if sys.platform == "darwin":
836                self.image_viewer.menubar.setNativeMenuBar(False)
837            self.image_viewer.show()
838        except Exception as ex:
839            logging.error(str(ex))
840            return
841
842    #============ FITTING =================
843    def actionNew_Fit_Page(self):
844        """
845        Add a new, empty Fit page in the fitting perspective.
846        """
847        # Make sure the perspective is correct
848        per = self.perspective()
849        if not isinstance(per, FittingWindow):
850            return
851        per.addFit(None)
852
853    def actionConstrained_Fit(self):
854        """
855        Add a new Constrained and Simult. Fit page in the fitting perspective.
856        """
857        per = self.perspective()
858        if not isinstance(per, FittingWindow):
859            return
860        per.addConstraintTab()
861
862    def actionCombine_Batch_Fit(self):
863        """
864        """
865        print("actionCombine_Batch_Fit TRIGGERED")
866        pass
867
868    def actionFit_Options(self):
869        """
870        """
871        if getattr(self._current_perspective, "fit_options_widget"):
872            self._current_perspective.fit_options_widget.show()
873        pass
874
875    def actionGPU_Options(self):
876        """
877        Load the OpenCL selection dialog if the fitting perspective is active
878        """
879        if hasattr(self._current_perspective, "gpu_options_widget"):
880            self._current_perspective.gpu_options_widget.show()
881        pass
882
883    def actionFit_Results(self):
884        """
885        """
886        self.showFitResults(None)
887
888    def showFitResults(self, output_data):
889        """
890        Show bumps convergence plots
891        """
892        self.results_frame.setVisible(True)
893        if output_data:
894            self.results_panel.onPlotResults(output_data, optimizer=self.perspective().optimizer)
895
896    def actionAdd_Custom_Model(self):
897        """
898        """
899        self.model_editor = TabbedModelEditor(self)
900        self.model_editor.show()
901
902    def actionEdit_Custom_Model(self):
903        """
904        """
905        self.model_editor = TabbedModelEditor(self, edit_only=True)
906        self.model_editor.show()
907
908    def actionManage_Custom_Models(self):
909        """
910        """
911        self.model_manager = PluginManager(self)
912        self.model_manager.show()
913
914    def actionAddMult_Models(self):
915        """
916        """
917        # Add Simple Add/Multiply Editor
918        self.add_mult_editor = AddMultEditor(self)
919        self.add_mult_editor.show()
920
921    def actionEditMask(self):
922
923        self.communicate.extMaskEditorSignal.emit()
924
925    #============ ANALYSIS =================
926    def actionFitting(self):
927        """
928        Change to the Fitting perspective
929        """
930        self.perspectiveChanged("Fitting")
931        # Notify other widgets
932        self.filesWidget.onAnalysisUpdate("Fitting")
933
934    def actionInversion(self):
935        """
936        Change to the Inversion perspective
937        """
938        self.perspectiveChanged("Inversion")
939        self.filesWidget.onAnalysisUpdate("Inversion")
940
941    def actionInvariant(self):
942        """
943        Change to the Invariant perspective
944        """
945        self.perspectiveChanged("Invariant")
946        self.filesWidget.onAnalysisUpdate("Invariant")
947
948    def actionCorfunc(self):
949        """
950        Change to the Corfunc perspective
951        """
952        self.perspectiveChanged("Corfunc")
953        self.filesWidget.onAnalysisUpdate("Corfunc")
954
955    #============ WINDOW =================
956    def actionCascade(self):
957        """
958        Arranges all the child windows in a cascade pattern.
959        """
960        self._workspace.workspace.cascadeSubWindows()
961
962    def actionTile(self):
963        """
964        Tile workspace windows
965        """
966        self._workspace.workspace.tileSubWindows()
967
968    def actionArrange_Icons(self):
969        """
970        Arranges all iconified windows at the bottom of the workspace
971        """
972        self._workspace.workspace.arrangeIcons()
973
974    def actionNext(self):
975        """
976        Gives the input focus to the next window in the list of child windows.
977        """
978        self._workspace.workspace.activateNextSubWindow()
979
980    def actionPrevious(self):
981        """
982        Gives the input focus to the previous window in the list of child windows.
983        """
984        self._workspace.workspace.activatePreviousSubWindow()
985
986    def actionClosePlots(self):
987        """
988        Closes all Plotters and Plotter2Ds.
989        """
990        self.filesWidget.closeAllPlots()
991        pass
992
993    #============ HELP =================
994    def actionDocumentation(self):
995        """
996        Display the documentation
997
998        TODO: use QNetworkAccessManager to assure _helpLocation is valid
999        """
1000        helpfile = "/index.html"
1001        self.showHelp(helpfile)
1002
1003    def actionTutorial(self):
1004        """
1005        Open the tutorial PDF file with default PDF renderer
1006        """
1007        # Not terribly safe here. Shell injection warning.
1008        # isfile() helps but this probably needs a better solution.
1009        if os.path.isfile(self._tutorialLocation):
1010            result = subprocess.Popen([self._tutorialLocation], shell=True)
1011
1012    def actionAcknowledge(self):
1013        """
1014        Open the Acknowledgements widget
1015        """
1016        self.ackWidget.show()
1017
1018    def actionAbout(self):
1019        """
1020        Open the About box
1021        """
1022        # Update the about box with current version and stuff
1023
1024        # TODO: proper sizing
1025        self.aboutWidget.show()
1026
1027    def actionCheck_for_update(self):
1028        """
1029        Menu Help/Check for Update
1030        """
1031        self.checkUpdate()
1032
1033    def updateTheoryFromPerspective(self, index):
1034        """
1035        Catch the theory update signal from a perspective
1036        Send the request to the DataExplorer for updating the theory model.
1037        """
1038        self.filesWidget.updateTheoryFromPerspective(index)
1039
1040    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1041        """
1042        Catch the signal to delete items in the Theory item model which correspond to a model ID.
1043        Send the request to the DataExplorer for updating the theory model.
1044        """
1045        self.filesWidget.deleteIntermediateTheoryPlotsByModelID(model_id)
1046
1047    def updateModelFromDataOperationPanel(self, new_item, new_datalist_item):
1048        """
1049        :param new_item: item to be added to list of loaded files
1050        :param new_datalist_item:
1051        """
1052        if not isinstance(new_item, QStandardItem) or \
1053                not isinstance(new_datalist_item, dict):
1054            msg = "Wrong data type returned from calculations."
1055            raise AttributeError(msg)
1056
1057        self.filesWidget.model.appendRow(new_item)
1058        self._data_manager.add_data(new_datalist_item)
1059
1060    def showPlotFromFilename(self, filename):
1061        """
1062        Pass the show plot request to the data explorer
1063        """
1064        if hasattr(self, "filesWidget"):
1065            self.filesWidget.displayFile(filename=filename, is_data=True)
1066
1067    def showPlot(self, plot, id):
1068        """
1069        Pass the show plot request to the data explorer
1070        """
1071        if hasattr(self, "filesWidget"):
1072            self.filesWidget.displayData(plot, id)
1073
1074    def uncheckAllMenuItems(self, menuObject):
1075        """
1076        Uncheck all options in a given menu
1077        """
1078        menuObjects = menuObject.actions()
1079
1080        for menuItem in menuObjects:
1081            menuItem.setChecked(False)
1082
1083    def checkAnalysisOption(self, analysisMenuOption):
1084        """
1085        Unchecks all the items in the analysis menu and checks the item passed
1086        """
1087        self.uncheckAllMenuItems(self._workspace.menuAnalysis)
1088        analysisMenuOption.setChecked(True)
1089
1090    def clearPerspectiveMenubarOptions(self, perspective):
1091        """
1092        When closing a perspective, clears the menu bar
1093        """
1094        for menuItem in self._workspace.menuAnalysis.actions():
1095            menuItem.setChecked(False)
1096
1097        if isinstance(self._current_perspective, Perspectives.PERSPECTIVES["Fitting"]):
1098            self._workspace.menubar.removeAction(self._workspace.menuFitting.menuAction())
1099
1100    def setupPerspectiveMenubarOptions(self, perspective):
1101        """
1102        When setting a perspective, sets up the menu bar
1103        """
1104        self._workspace.actionReport.setEnabled(False)
1105        self._workspace.actionOpen_Analysis.setEnabled(False)
1106        self._workspace.actionSave_Analysis.setEnabled(False)
1107        if hasattr(perspective, 'isSerializable') and perspective.isSerializable():
1108            self._workspace.actionOpen_Analysis.setEnabled(True)
1109            self._workspace.actionSave_Analysis.setEnabled(True)
1110
1111        if isinstance(perspective, Perspectives.PERSPECTIVES["Fitting"]):
1112            self.checkAnalysisOption(self._workspace.actionFitting)
1113            # Put the fitting menu back in
1114            # This is a bit involved but it is needed to preserve the menu ordering
1115            self._workspace.menubar.removeAction(self._workspace.menuWindow.menuAction())
1116            self._workspace.menubar.removeAction(self._workspace.menuHelp.menuAction())
1117            self._workspace.menubar.addAction(self._workspace.menuFitting.menuAction())
1118            self._workspace.menubar.addAction(self._workspace.menuWindow.menuAction())
1119            self._workspace.menubar.addAction(self._workspace.menuHelp.menuAction())
1120            self._workspace.actionReport.setEnabled(True)
1121
1122        elif isinstance(perspective, Perspectives.PERSPECTIVES["Invariant"]):
1123            self.checkAnalysisOption(self._workspace.actionInvariant)
1124        elif isinstance(perspective, Perspectives.PERSPECTIVES["Inversion"]):
1125            self.checkAnalysisOption(self._workspace.actionInversion)
1126        elif isinstance(perspective, Perspectives.PERSPECTIVES["Corfunc"]):
1127            self.checkAnalysisOption(self._workspace.actionCorfunc)
1128
1129    def saveCustomConfig(self):
1130        """
1131        Save the config file based on current session values
1132        """
1133        # Load the current file
1134        config_content = GuiUtils.custom_config
1135
1136        changed = self.customSavePaths(config_content)
1137        changed = changed or self.customSaveOpenCL(config_content)
1138
1139        if changed:
1140            self.writeCustomConfig(config_content)
1141
1142    def customSavePaths(self, config_content):
1143        """
1144        Update the config module with current session paths
1145        Returns True if update was done, False, otherwise
1146        """
1147        changed = False
1148        # Find load path
1149        open_path = GuiUtils.DEFAULT_OPEN_FOLDER
1150        defined_path = self.filesWidget.default_load_location
1151        if open_path != defined_path:
1152            # Replace the load path
1153            config_content.DEFAULT_OPEN_FOLDER = defined_path
1154            changed = True
1155        return changed
1156
1157    def customSaveOpenCL(self, config_content):
1158        """
1159        Update the config module with current session OpenCL choice
1160        Returns True if update was done, False, otherwise
1161        """
1162        changed = False
1163        # Find load path
1164        file_value = GuiUtils.SAS_OPENCL
1165        session_value = os.environ.get("SAS_OPENCL", "")
1166        if file_value != session_value:
1167            # Replace the load path
1168            config_content.SAS_OPENCL = session_value
1169            changed = True
1170        return changed
1171
1172    def writeCustomConfig(self, config):
1173        """
1174        Write custom configuration
1175        """
1176        from sas import make_custom_config_path
1177        path = make_custom_config_path()
1178        # Just clobber the file - we already have its content read in
1179        with open(path, 'w') as out_f:
1180            out_f.write("#Application appearance custom configuration\n")
1181            for key, item in config.__dict__.items():
1182                if key[:2] == "__":
1183                    continue
1184                if isinstance(item, str):
1185                    item = '"' + item + '"'
1186                out_f.write("%s = %s\n" % (key, str(item)))
1187        pass # debugger anchor
Note: See TracBrowser for help on using the repository browser.