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

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

Save load path and sas_opencl values to the custom config, if changed
during the session.

  • Property mode set to 100644
File size: 40.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        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._workspace.workspace.removeSubWindow(self._current_perspective)
258            self._current_perspective.close()
259            self._workspace.workspace.removeSubWindow(self._current_perspective)
260        # Default perspective
261        self._current_perspective = Perspectives.PERSPECTIVES[str(perspective_name)](parent=self)
262
263        self.setupPerspectiveMenubarOptions(self._current_perspective)
264
265        subwindow = self._workspace.workspace.addSubWindow(self._current_perspective)
266
267        # Resize to the workspace height
268        workspace_height = self._workspace.workspace.sizeHint().height()
269        perspective_size = self._current_perspective.sizeHint()
270        perspective_width = perspective_size.width()
271        self._current_perspective.resize(perspective_width, workspace_height-10)
272
273        self._current_perspective.show()
274
275    def updatePerspective(self, data):
276        """
277        Update perspective with data sent.
278        """
279        assert isinstance(data, list)
280        if self._current_perspective is not None:
281            self._current_perspective.setData(list(data.values()))
282        else:
283            msg = "No perspective is currently active."
284            logging.info(msg)
285
286    def communicator(self):
287        """ Accessor for the communicator """
288        return self.communicate
289
290    def perspective(self):
291        """ Accessor for the perspective """
292        return self._current_perspective
293
294    def updateProgressBar(self, value):
295        """
296        Update progress bar with the required value (0-100)
297        """
298        assert -1 <= value <= 100
299        if value == -1:
300            self.progress.setVisible(False)
301            return
302        if not self.progress.isVisible():
303            self.progress.setTextVisible(True)
304            self.progress.setVisible(True)
305
306        self.progress.setValue(value)
307
308    def updateStatusBar(self, text):
309        """
310        Set the status bar text
311        """
312        self.statusLabel.setText(text)
313
314    def appendLog(self, msg):
315        """Appends a message to the list widget in the Log Explorer. Use this
316        instead of listWidget.insertPlainText() to facilitate auto-scrolling"""
317        self.listWidget.append(msg.strip())
318
319    def createGuiData(self, item, p_file=None):
320        """
321        Access the Data1D -> plottable Data1D conversion
322        """
323        return self._data_manager.create_gui_data(item, p_file)
324
325    def setData(self, data):
326        """
327        Sends data to current perspective
328        """
329        if self._current_perspective is not None:
330            self._current_perspective.setData(list(data.values()))
331        else:
332            msg = "Guiframe does not have a current perspective"
333            logging.info(msg)
334
335    def findItemFromFilename(self, filename):
336        """
337        Queries the data explorer for the index corresponding to the filename within
338        """
339        return self.filesWidget.itemFromFilename(filename)
340
341    def quitApplication(self):
342        """
343        Close the reactor and exit nicely.
344        """
345        # Display confirmation messagebox
346        quit_msg = "Are you sure you want to exit the application?"
347        reply = QMessageBox.question(
348            self._parent,
349            'Information',
350            quit_msg,
351            QMessageBox.Yes,
352            QMessageBox.No)
353
354        # Exit if yes
355        if reply == QMessageBox.Yes:
356            # save the paths etc.
357            self.saveCustomConfig()
358            reactor.callFromThread(reactor.stop)
359            return True
360
361        return False
362
363    def checkUpdate(self):
364        """
365        Check with the deployment server whether a new version
366        of the application is available.
367        A thread is started for the connecting with the server. The thread calls
368        a call-back method when the current version number has been obtained.
369        """
370        version_info = {"version": "0.0.0"}
371        c = ConnectionProxy(LocalConfig.__update_URL__, LocalConfig.UPDATE_TIMEOUT)
372        response = c.connect()
373        if response is None:
374            return
375        try:
376            content = response.read().strip()
377            logging.info("Connected to www.sasview.org. Latest version: %s"
378                            % (content))
379            version_info = json.loads(content)
380            self.processVersion(version_info)
381        except ValueError as ex:
382            logging.info("Failed to connect to www.sasview.org:", ex)
383
384    def processVersion(self, version_info):
385        """
386        Call-back method for the process of checking for updates.
387        This methods is called by a VersionThread object once the current
388        version number has been obtained. If the check is being done in the
389        background, the user will not be notified unless there's an update.
390
391        :param version: version string
392        """
393        try:
394            version = version_info["version"]
395            if version == "0.0.0":
396                msg = "Could not connect to the application server."
397                msg += " Please try again later."
398                self.communicate.statusBarUpdateSignal.emit(msg)
399
400            elif version.__gt__(LocalConfig.__version__):
401                msg = "Version %s is available! " % str(version)
402                if "download_url" in version_info:
403                    webbrowser.open(version_info["download_url"])
404                else:
405                    webbrowser.open(LocalConfig.__download_page__)
406                self.communicate.statusBarUpdateSignal.emit(msg)
407            else:
408                msg = "You have the latest version"
409                msg += " of %s" % str(LocalConfig.__appname__)
410                self.communicate.statusBarUpdateSignal.emit(msg)
411        except:
412            msg = "guiframe: could not get latest application"
413            msg += " version number\n  %s" % sys.exc_info()[1]
414            logging.error(msg)
415            msg = "Could not connect to the application server."
416            msg += " Please try again later."
417            self.communicate.statusBarUpdateSignal.emit(msg)
418
419    def actionWelcome(self):
420        """ Show the Welcome panel """
421        self.welcomePanel = WelcomePanel()
422        self._workspace.workspace.addSubWindow(self.welcomePanel)
423        self.welcomePanel.show()
424
425    def showWelcomeMessage(self):
426        """ Show the Welcome panel, when required """
427        # Assure the welcome screen is requested
428        show_welcome_widget = True
429        custom_config = get_custom_config()
430        if hasattr(custom_config, "WELCOME_PANEL_SHOW"):
431            if isinstance(custom_config.WELCOME_PANEL_SHOW, bool):
432                show_welcome_widget = custom_config.WELCOME_PANEL_SHOW
433            else:
434                logging.warning("WELCOME_PANEL_SHOW has invalid value in custom_config.py")
435        if show_welcome_widget:
436            self.actionWelcome()
437
438    def addCallbacks(self):
439        """
440        Method defining all signal connections for the gui manager
441        """
442        self.communicate = GuiUtils.Communicate()
443        self.communicate.fileDataReceivedSignal.connect(self.fileWasRead)
444        self.communicate.statusBarUpdateSignal.connect(self.updateStatusBar)
445        self.communicate.updatePerspectiveWithDataSignal.connect(self.updatePerspective)
446        self.communicate.progressBarUpdateSignal.connect(self.updateProgressBar)
447        self.communicate.perspectiveChangedSignal.connect(self.perspectiveChanged)
448        self.communicate.updateTheoryFromPerspectiveSignal.connect(self.updateTheoryFromPerspective)
449        self.communicate.deleteIntermediateTheoryPlotsSignal.connect(self.deleteIntermediateTheoryPlotsByModelID)
450        self.communicate.plotRequestedSignal.connect(self.showPlot)
451        self.communicate.plotFromFilenameSignal.connect(self.showPlotFromFilename)
452        self.communicate.updateModelFromDataOperationPanelSignal.connect(self.updateModelFromDataOperationPanel)
453
454    def addTriggers(self):
455        """
456        Trigger definitions for all menu/toolbar actions.
457        """
458        # disable not yet fully implemented actions
459        self._workspace.actionUndo.setVisible(False)
460        self._workspace.actionRedo.setVisible(False)
461        self._workspace.actionReset.setVisible(False)
462        self._workspace.actionStartup_Settings.setVisible(False)
463        #self._workspace.actionImage_Viewer.setVisible(False)
464        self._workspace.actionCombine_Batch_Fit.setVisible(False)
465        # orientation viewer set to invisible SASVIEW-1132
466        self._workspace.actionOrientation_Viewer.setVisible(False)
467
468        # File
469        self._workspace.actionLoadData.triggered.connect(self.actionLoadData)
470        self._workspace.actionLoad_Data_Folder.triggered.connect(self.actionLoad_Data_Folder)
471        self._workspace.actionOpen_Project.triggered.connect(self.actionOpen_Project)
472        self._workspace.actionOpen_Analysis.triggered.connect(self.actionOpen_Analysis)
473        self._workspace.actionSave.triggered.connect(self.actionSave)
474        self._workspace.actionSave_Analysis.triggered.connect(self.actionSave_Analysis)
475        self._workspace.actionQuit.triggered.connect(self.actionQuit)
476        # Edit
477        self._workspace.actionUndo.triggered.connect(self.actionUndo)
478        self._workspace.actionRedo.triggered.connect(self.actionRedo)
479        self._workspace.actionCopy.triggered.connect(self.actionCopy)
480        self._workspace.actionPaste.triggered.connect(self.actionPaste)
481        self._workspace.actionReport.triggered.connect(self.actionReport)
482        self._workspace.actionReset.triggered.connect(self.actionReset)
483        self._workspace.actionExcel.triggered.connect(self.actionExcel)
484        self._workspace.actionLatex.triggered.connect(self.actionLatex)
485        # View
486        self._workspace.actionShow_Grid_Window.triggered.connect(self.actionShow_Grid_Window)
487        self._workspace.actionHide_Toolbar.triggered.connect(self.actionHide_Toolbar)
488        self._workspace.actionStartup_Settings.triggered.connect(self.actionStartup_Settings)
489        self._workspace.actionCategory_Manager.triggered.connect(self.actionCategory_Manager)
490        self._workspace.actionHide_DataExplorer.triggered.connect(self.actionHide_DataExplorer)
491        self._workspace.actionHide_LogExplorer.triggered.connect(self.actionHide_LogExplorer)
492        # Tools
493        self._workspace.actionData_Operation.triggered.connect(self.actionData_Operation)
494        self._workspace.actionSLD_Calculator.triggered.connect(self.actionSLD_Calculator)
495        self._workspace.actionDensity_Volume_Calculator.triggered.connect(self.actionDensity_Volume_Calculator)
496        self._workspace.actionKeissig_Calculator.triggered.connect(self.actionKiessig_Calculator)
497        #self._workspace.actionKIESSING_Calculator.triggered.connect(self.actionKIESSING_Calculator)
498        self._workspace.actionSlit_Size_Calculator.triggered.connect(self.actionSlit_Size_Calculator)
499        self._workspace.actionSAS_Resolution_Estimator.triggered.connect(self.actionSAS_Resolution_Estimator)
500        self._workspace.actionGeneric_Scattering_Calculator.triggered.connect(self.actionGeneric_Scattering_Calculator)
501        self._workspace.actionPython_Shell_Editor.triggered.connect(self.actionPython_Shell_Editor)
502        self._workspace.actionImage_Viewer.triggered.connect(self.actionImage_Viewer)
503        self._workspace.actionOrientation_Viewer.triggered.connect(self.actionOrientation_Viewer)
504        self._workspace.actionFreeze_Theory.triggered.connect(self.actionFreeze_Theory)
505        # Fitting
506        self._workspace.actionNew_Fit_Page.triggered.connect(self.actionNew_Fit_Page)
507        self._workspace.actionConstrained_Fit.triggered.connect(self.actionConstrained_Fit)
508        self._workspace.actionCombine_Batch_Fit.triggered.connect(self.actionCombine_Batch_Fit)
509        self._workspace.actionFit_Options.triggered.connect(self.actionFit_Options)
510        self._workspace.actionGPU_Options.triggered.connect(self.actionGPU_Options)
511        self._workspace.actionFit_Results.triggered.connect(self.actionFit_Results)
512        self._workspace.actionAdd_Custom_Model.triggered.connect(self.actionAdd_Custom_Model)
513        self._workspace.actionEdit_Custom_Model.triggered.connect(self.actionEdit_Custom_Model)
514        self._workspace.actionManage_Custom_Models.triggered.connect(self.actionManage_Custom_Models)
515        self._workspace.actionAddMult_Models.triggered.connect(self.actionAddMult_Models)
516        self._workspace.actionEditMask.triggered.connect(self.actionEditMask)
517
518        # Window
519        self._workspace.actionCascade.triggered.connect(self.actionCascade)
520        self._workspace.actionTile.triggered.connect(self.actionTile)
521        self._workspace.actionArrange_Icons.triggered.connect(self.actionArrange_Icons)
522        self._workspace.actionNext.triggered.connect(self.actionNext)
523        self._workspace.actionPrevious.triggered.connect(self.actionPrevious)
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        print("actionOpen_Analysis TRIGGERED")
564        pass
565
566    def actionSave(self):
567        """
568        Menu Save Project
569        """
570        self.filesWidget.saveProject()
571
572    def actionSave_Analysis(self):
573        """
574        Menu File/Save Analysis
575        """
576        self.communicate.saveAnalysisSignal.emit()
577
578    def actionQuit(self):
579        """
580        Close the reactor, exit the application.
581        """
582        self.quitApplication()
583
584    #============ EDIT =================
585    def actionUndo(self):
586        """
587        """
588        print("actionUndo TRIGGERED")
589        pass
590
591    def actionRedo(self):
592        """
593        """
594        print("actionRedo TRIGGERED")
595        pass
596
597    def actionCopy(self):
598        """
599        Send a signal to the fitting perspective so parameters
600        can be saved to the clipboard
601        """
602        self.communicate.copyFitParamsSignal.emit("")
603        self._workspace.actionPaste.setEnabled(True)
604        pass
605
606    def actionPaste(self):
607        """
608        Send a signal to the fitting perspective so parameters
609        from the clipboard can be used to modify the fit state
610        """
611        self.communicate.pasteFitParamsSignal.emit()
612
613    def actionReport(self):
614        """
615        Show the Fit Report dialog.
616        """
617        report_list = None
618        if getattr(self._current_perspective, "currentTab"):
619            try:
620                report_list = self._current_perspective.currentTab.getReport()
621            except Exception as ex:
622                logging.error("Report generation failed with: " + str(ex))
623
624        if report_list is not None:
625            self.report_dialog = ReportDialog(parent=self, report_list=report_list)
626            self.report_dialog.show()
627
628    def actionReset(self):
629        """
630        """
631        logging.warning(" *** actionOpen_Analysis logging *******")
632        print("actionReset print TRIGGERED")
633        sys.stderr.write("STDERR - TRIGGERED")
634        pass
635
636    def actionExcel(self):
637        """
638        Send a signal to the fitting perspective so parameters
639        can be saved to the clipboard
640        """
641        self.communicate.copyExcelFitParamsSignal.emit("Excel")
642
643    def actionLatex(self):
644        """
645        Send a signal to the fitting perspective so parameters
646        can be saved to the clipboard
647        """
648        self.communicate.copyLatexFitParamsSignal.emit("Latex")
649
650    #============ VIEW =================
651    def actionShow_Grid_Window(self):
652        """
653        """
654        self.showBatchOutput(None)
655
656    def showBatchOutput(self, output_data):
657        """
658        Display/redisplay the batch fit viewer
659        """
660        self.grid_subwindow.setVisible(True)
661        if output_data:
662            self.grid_window.addFitResults(output_data)
663
664    def actionHide_Toolbar(self):
665        """
666        Toggle toolbar vsibility
667        """
668        if self._workspace.toolBar.isVisible():
669            self._workspace.actionHide_Toolbar.setText("Show Toolbar")
670            self._workspace.toolBar.setVisible(False)
671        else:
672            self._workspace.actionHide_Toolbar.setText("Hide Toolbar")
673            self._workspace.toolBar.setVisible(True)
674        pass
675
676    def actionHide_DataExplorer(self):
677        """
678        Toggle Data Explorer vsibility
679        """
680        if self.dockedFilesWidget.isVisible():
681            self.dockedFilesWidget.setVisible(False)
682        else:
683            self.dockedFilesWidget.setVisible(True)
684        pass
685
686    def actionHide_LogExplorer(self):
687        """
688        Toggle Data Explorer vsibility
689        """
690        if self.logDockWidget.isVisible():
691            self.logDockWidget.setVisible(False)
692        else:
693            self.logDockWidget.setVisible(True)
694        pass
695
696    def actionStartup_Settings(self):
697        """
698        """
699        print("actionStartup_Settings TRIGGERED")
700        pass
701
702    def actionCategory_Manager(self):
703        """
704        """
705        self.categoryManagerWidget.show()
706
707    #============ TOOLS =================
708    def actionData_Operation(self):
709        """
710        """
711        self.communicate.sendDataToPanelSignal.emit(self._data_manager.get_all_data())
712
713        self.DataOperation.show()
714
715    def actionSLD_Calculator(self):
716        """
717        """
718        self.SLDCalculator.show()
719
720    def actionDensity_Volume_Calculator(self):
721        """
722        """
723        self.DVCalculator.show()
724
725    def actionKiessig_Calculator(self):
726        """
727        """
728        self.KIESSIGCalculator.show()
729
730    def actionSlit_Size_Calculator(self):
731        """
732        """
733        self.SlitSizeCalculator.show()
734
735    def actionSAS_Resolution_Estimator(self):
736        """
737        """
738        try:
739            self.ResolutionCalculator.show()
740        except Exception as ex:
741            logging.error(str(ex))
742            return
743
744    def actionGeneric_Scattering_Calculator(self):
745        """
746        """
747        try:
748            self.GENSASCalculator.show()
749        except Exception as ex:
750            logging.error(str(ex))
751            return
752
753    def actionPython_Shell_Editor(self):
754        """
755        Display the Jupyter console as a docked widget.
756        """
757        # Import moved here for startup performance reasons
758        from sas.qtgui.Utilities.IPythonWidget import IPythonWidget
759        terminal = IPythonWidget()
760
761        # Add the console window as another docked widget
762        self.ipDockWidget = QDockWidget("IPython", self._workspace)
763        self.ipDockWidget.setObjectName("IPythonDockWidget")
764        self.ipDockWidget.setWidget(terminal)
765        self._workspace.addDockWidget(Qt.RightDockWidgetArea, self.ipDockWidget)
766
767    def actionFreeze_Theory(self):
768        """
769        Convert a child index with data into a separate top level dataset
770        """
771        self.filesWidget.freezeCheckedData()
772
773    def actionOrientation_Viewer(self):
774        """
775        Make sasmodels orientation & jitter viewer available
776        """
777        from sasmodels.jitter import run as orientation_run
778        try:
779            orientation_run()
780        except Exception as ex:
781            logging.error(str(ex))
782
783    def actionImage_Viewer(self):
784        """
785        """
786        try:
787            self.image_viewer = ImageViewer(self)
788            if sys.platform == "darwin":
789                self.image_viewer.menubar.setNativeMenuBar(False)
790            self.image_viewer.show()
791        except Exception as ex:
792            logging.error(str(ex))
793            return
794
795    #============ FITTING =================
796    def actionNew_Fit_Page(self):
797        """
798        Add a new, empty Fit page in the fitting perspective.
799        """
800        # Make sure the perspective is correct
801        per = self.perspective()
802        if not isinstance(per, FittingWindow):
803            return
804        per.addFit(None)
805
806    def actionConstrained_Fit(self):
807        """
808        Add a new Constrained and Simult. Fit page in the fitting perspective.
809        """
810        per = self.perspective()
811        if not isinstance(per, FittingWindow):
812            return
813        per.addConstraintTab()
814
815    def actionCombine_Batch_Fit(self):
816        """
817        """
818        print("actionCombine_Batch_Fit TRIGGERED")
819        pass
820
821    def actionFit_Options(self):
822        """
823        """
824        if getattr(self._current_perspective, "fit_options_widget"):
825            self._current_perspective.fit_options_widget.show()
826        pass
827
828    def actionGPU_Options(self):
829        """
830        Load the OpenCL selection dialog if the fitting perspective is active
831        """
832        if hasattr(self._current_perspective, "gpu_options_widget"):
833            self._current_perspective.gpu_options_widget.show()
834        pass
835
836    def actionFit_Results(self):
837        """
838        """
839        self.showFitResults(None)
840
841    def showFitResults(self, output_data):
842        """
843        Show bumps convergence plots
844        """
845        self.results_frame.setVisible(True)
846        if output_data:
847            self.results_panel.onPlotResults(output_data, optimizer=self.perspective().optimizer)
848
849    def actionAdd_Custom_Model(self):
850        """
851        """
852        self.model_editor = TabbedModelEditor(self)
853        self.model_editor.show()
854
855    def actionEdit_Custom_Model(self):
856        """
857        """
858        self.model_editor = TabbedModelEditor(self, edit_only=True)
859        self.model_editor.show()
860
861    def actionManage_Custom_Models(self):
862        """
863        """
864        self.model_manager = PluginManager(self)
865        self.model_manager.show()
866
867    def actionAddMult_Models(self):
868        """
869        """
870        # Add Simple Add/Multiply Editor
871        self.add_mult_editor = AddMultEditor(self)
872        self.add_mult_editor.show()
873
874    def actionEditMask(self):
875
876        self.communicate.extMaskEditorSignal.emit()
877
878    #============ ANALYSIS =================
879    def actionFitting(self):
880        """
881        Change to the Fitting perspective
882        """
883        self.perspectiveChanged("Fitting")
884        # Notify other widgets
885        self.filesWidget.onAnalysisUpdate("Fitting")
886
887    def actionInversion(self):
888        """
889        Change to the Inversion perspective
890        """
891        self.perspectiveChanged("Inversion")
892        self.filesWidget.onAnalysisUpdate("Inversion")
893
894    def actionInvariant(self):
895        """
896        Change to the Invariant perspective
897        """
898        self.perspectiveChanged("Invariant")
899        self.filesWidget.onAnalysisUpdate("Invariant")
900
901    def actionCorfunc(self):
902        """
903        Change to the Corfunc perspective
904        """
905        self.perspectiveChanged("Corfunc")
906        self.filesWidget.onAnalysisUpdate("Corfunc")
907
908    #============ WINDOW =================
909    def actionCascade(self):
910        """
911        Arranges all the child windows in a cascade pattern.
912        """
913        self._workspace.workspace.cascadeSubWindows()
914
915    def actionTile(self):
916        """
917        Tile workspace windows
918        """
919        self._workspace.workspace.tileSubWindows()
920
921    def actionArrange_Icons(self):
922        """
923        Arranges all iconified windows at the bottom of the workspace
924        """
925        self._workspace.workspace.arrangeIcons()
926
927    def actionNext(self):
928        """
929        Gives the input focus to the next window in the list of child windows.
930        """
931        self._workspace.workspace.activateNextSubWindow()
932
933    def actionPrevious(self):
934        """
935        Gives the input focus to the previous window in the list of child windows.
936        """
937        self._workspace.workspace.activatePreviousSubWindow()
938
939    def actionClosePlots(self):
940        """
941        Closes all Plotters and Plotter2Ds.
942        """
943        self.filesWidget.closeAllPlots()
944        pass
945
946    #============ HELP =================
947    def actionDocumentation(self):
948        """
949        Display the documentation
950
951        TODO: use QNetworkAccessManager to assure _helpLocation is valid
952        """
953        helpfile = "/index.html"
954        self.showHelp(helpfile)
955
956    def actionTutorial(self):
957        """
958        Open the tutorial PDF file with default PDF renderer
959        """
960        # Not terribly safe here. Shell injection warning.
961        # isfile() helps but this probably needs a better solution.
962        if os.path.isfile(self._tutorialLocation):
963            result = subprocess.Popen([self._tutorialLocation], shell=True)
964
965    def actionAcknowledge(self):
966        """
967        Open the Acknowledgements widget
968        """
969        self.ackWidget.show()
970
971    def actionAbout(self):
972        """
973        Open the About box
974        """
975        # Update the about box with current version and stuff
976
977        # TODO: proper sizing
978        self.aboutWidget.show()
979
980    def actionCheck_for_update(self):
981        """
982        Menu Help/Check for Update
983        """
984        self.checkUpdate()
985
986    def updateTheoryFromPerspective(self, index):
987        """
988        Catch the theory update signal from a perspective
989        Send the request to the DataExplorer for updating the theory model.
990        """
991        self.filesWidget.updateTheoryFromPerspective(index)
992
993    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
994        """
995        Catch the signal to delete items in the Theory item model which correspond to a model ID.
996        Send the request to the DataExplorer for updating the theory model.
997        """
998        self.filesWidget.deleteIntermediateTheoryPlotsByModelID(model_id)
999
1000    def updateModelFromDataOperationPanel(self, new_item, new_datalist_item):
1001        """
1002        :param new_item: item to be added to list of loaded files
1003        :param new_datalist_item:
1004        """
1005        if not isinstance(new_item, QStandardItem) or \
1006                not isinstance(new_datalist_item, dict):
1007            msg = "Wrong data type returned from calculations."
1008            raise AttributeError(msg)
1009
1010        self.filesWidget.model.appendRow(new_item)
1011        self._data_manager.add_data(new_datalist_item)
1012
1013    def showPlotFromFilename(self, filename):
1014        """
1015        Pass the show plot request to the data explorer
1016        """
1017        if hasattr(self, "filesWidget"):
1018            self.filesWidget.displayFile(filename=filename, is_data=True)
1019
1020    def showPlot(self, plot, id):
1021        """
1022        Pass the show plot request to the data explorer
1023        """
1024        if hasattr(self, "filesWidget"):
1025            self.filesWidget.displayData(plot, id)
1026
1027    def uncheckAllMenuItems(self, menuObject):
1028        """
1029        Uncheck all options in a given menu
1030        """
1031        menuObjects = menuObject.actions()
1032
1033        for menuItem in menuObjects:
1034            menuItem.setChecked(False)
1035
1036    def checkAnalysisOption(self, analysisMenuOption):
1037        """
1038        Unchecks all the items in the analysis menu and checks the item passed
1039        """
1040        self.uncheckAllMenuItems(self._workspace.menuAnalysis)
1041        analysisMenuOption.setChecked(True)
1042
1043    def clearPerspectiveMenubarOptions(self, perspective):
1044        """
1045        When closing a perspective, clears the menu bar
1046        """
1047        for menuItem in self._workspace.menuAnalysis.actions():
1048            menuItem.setChecked(False)
1049
1050        if isinstance(self._current_perspective, Perspectives.PERSPECTIVES["Fitting"]):
1051            self._workspace.menubar.removeAction(self._workspace.menuFitting.menuAction())
1052
1053    def setupPerspectiveMenubarOptions(self, perspective):
1054        """
1055        When setting a perspective, sets up the menu bar
1056        """
1057        self._workspace.actionReport.setEnabled(False)
1058        if isinstance(perspective, Perspectives.PERSPECTIVES["Fitting"]):
1059            self.checkAnalysisOption(self._workspace.actionFitting)
1060            # Put the fitting menu back in
1061            # This is a bit involved but it is needed to preserve the menu ordering
1062            self._workspace.menubar.removeAction(self._workspace.menuWindow.menuAction())
1063            self._workspace.menubar.removeAction(self._workspace.menuHelp.menuAction())
1064            self._workspace.menubar.addAction(self._workspace.menuFitting.menuAction())
1065            self._workspace.menubar.addAction(self._workspace.menuWindow.menuAction())
1066            self._workspace.menubar.addAction(self._workspace.menuHelp.menuAction())
1067            self._workspace.actionReport.setEnabled(True)
1068
1069        elif isinstance(perspective, Perspectives.PERSPECTIVES["Invariant"]):
1070            self.checkAnalysisOption(self._workspace.actionInvariant)
1071        elif isinstance(perspective, Perspectives.PERSPECTIVES["Inversion"]):
1072            self.checkAnalysisOption(self._workspace.actionInversion)
1073        elif isinstance(perspective, Perspectives.PERSPECTIVES["Corfunc"]):
1074            self.checkAnalysisOption(self._workspace.actionCorfunc)
1075
1076    def saveCustomConfig(self):
1077        """
1078        Save the config file based on current session values
1079        """
1080        # Load the current file
1081        config_content = GuiUtils.custom_config
1082
1083        changed = self.customSavePaths(config_content)
1084        changed = changed or self.customSaveOpenCL(config_content)
1085
1086        if changed:
1087            self.writeCustomConfig(config_content)
1088
1089    def customSavePaths(self, config_content):
1090        """
1091        Update the config module with current session paths
1092        Returns True if update was done, False, otherwise
1093        """
1094        changed = False
1095        # Find load path
1096        open_path = GuiUtils.DEFAULT_OPEN_FOLDER
1097        defined_path = self.filesWidget.default_load_location
1098        if open_path != defined_path:
1099            # Replace the load path
1100            config_content.DEFAULT_OPEN_FOLDER = defined_path
1101            changed = True
1102        return changed
1103
1104    def customSaveOpenCL(self, config_content):
1105        """
1106        Update the config module with current session OpenCL choice
1107        Returns True if update was done, False, otherwise
1108        """
1109        changed = False
1110        # Find load path
1111        file_value = GuiUtils.SAS_OPENCL
1112        session_value = os.environ.get("SAS_OPENCL", "")
1113        if file_value != session_value:
1114            # Replace the load path
1115            config_content.SAS_OPENCL = session_value
1116            changed = True
1117        return changed
1118
1119    def writeCustomConfig(self, config):
1120        """
1121        Write custom configuration
1122        """
1123        from sas import make_custom_config_path
1124        path = make_custom_config_path()
1125        # Just clobber the file - we already have its content read in
1126        with open(path, 'w') as out_f:
1127            out_f.write("#Application appearance custom configuration\n")
1128            for key, item in config.__dict__.items():
1129                if key[:2] != "__":
1130                    if isinstance(item, str):
1131                        item = '"' + item + '"'
1132                    out_f.write("%s = %s\n" % (key, str(item)))
1133        pass # debugger anchor
Note: See TracBrowser for help on using the repository browser.