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

Last change on this file since d32a594 was 091eee64, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Modified the open tutorial action - it now opens a webpage with tutorial
links. Just like in 4.2. SASVIEW-1255.
Also, modified copyright year in yet another place.

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