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

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

Analysis only for Fitting, project save on other perspectives saves
datasets. Fixed tab deletion on data removal for batch tabs.

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