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

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

Working project load/save. new format only. SASVIEW-983/984

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