source: sasview/src/sas/qtgui/GuiManager.py @ 811bec1

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 811bec1 was 811bec1, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Started with unit tests for fitting widget SASVIEW-499

  • Property mode set to 100644
File size: 24.0 KB
Line 
1import sys
2import os
3import subprocess
4import logging
5import json
6import webbrowser
7
8from PyQt4 import QtCore
9from PyQt4 import QtGui
10from PyQt4 import QtWebKit
11
12from twisted.internet import reactor
13# General SAS imports
14
15from sas.sasgui.guiframe.data_manager import DataManager
16from sas.sasgui.guiframe.proxy import Connection
17from sas.qtgui.SasviewLogger import XStream
18import sas.qtgui.LocalConfig as LocalConfig
19import sas.qtgui.GuiUtils as GuiUtils
20from sas.qtgui.UI.AcknowledgementsUI import Ui_Acknowledgements
21from sas.qtgui.AboutBox import AboutBox
22from sas.qtgui.IPythonWidget import IPythonWidget
23from sas.qtgui.WelcomePanel import WelcomePanel
24
25from sas.qtgui.SldPanel import SldPanel
26from sas.qtgui.DensityPanel import DensityPanel
27from sas.qtgui.KiessigPanel import KiessigPanel
28from sas.qtgui.SlitSizeCalculator import SlitSizeCalculator
29
30# Perspectives
31import Perspectives
32from sas.qtgui.Perspectives.Invariant.InvariantPerspective import InvariantWindow
33from sas.qtgui.Perspectives.Fitting.FittingPerspective import FittingWindow
34from sas.qtgui.DataExplorer import DataExplorerWindow
35
36class Acknowledgements(QtGui.QDialog, Ui_Acknowledgements):
37    def __init__(self, parent=None):
38        QtGui.QDialog.__init__(self, parent)
39        self.setupUi(self)
40
41class GuiManager(object):
42    """
43    Main SasView window functionality
44    """
45    ## TODO: CHANGE FOR SHIPPED PATH IN RELEASE
46    HELP_DIRECTORY_LOCATION = "docs/sphinx-docs/build/html"
47
48    def __init__(self, mainWindow=None, reactor=None, parent=None):
49        """
50        Initialize the manager as a child of MainWindow.
51        """
52        self._workspace = mainWindow
53        self._parent = parent
54
55        # Reactor passed from above
56        self.setReactor(reactor)
57
58        # Add signal callbacks
59        self.addCallbacks()
60
61        # Create the data manager
62        # TODO: pull out all required methods from DataManager and reimplement
63        self._data_manager = DataManager()
64
65        # Create action triggers
66        self.addTriggers()
67
68        # Populate menus with dynamic data
69        #
70        # Analysis/Perspectives - potentially
71        # Window/current windows
72        #
73        # Widgets
74        #
75        # Current displayed perspective
76        self._current_perspective = None
77
78        # Invoke the initial perspective
79        self.perspectiveChanged("Fitting")
80
81        self.addWidgets()
82
83        # Fork off logging messages to the Log Window
84        XStream.stdout().messageWritten.connect(self.listWidget.insertPlainText)
85        XStream.stderr().messageWritten.connect(self.listWidget.insertPlainText)
86
87        # Log the start of the session
88        logging.info(" --- SasView session started ---")
89        # Log the python version
90        logging.info("Python: %s" % sys.version)
91
92        # Set up the status bar
93        self.statusBarSetup()
94
95        # Show the Welcome panel
96        self.welcomePanel = WelcomePanel()
97        self._workspace.workspace.addWindow(self.welcomePanel)
98
99        # Current help file
100        self._helpView = QtWebKit.QWebView()
101        # Needs URL like path, so no path.join() here
102        self._helpLocation = self.HELP_DIRECTORY_LOCATION + "/index.html"
103
104        # Current tutorial location
105        self._tutorialLocation = os.path.abspath(os.path.join(self.HELP_DIRECTORY_LOCATION,
106                                              "_downloads",
107                                              "Tutorial.pdf"))
108    def addWidgets(self):
109        """
110        Populate the main window with widgets
111
112        TODO: overwrite close() on Log and DR widgets so they can be hidden/shown
113        on request
114        """
115        # Add FileDialog widget as docked
116        self.filesWidget = DataExplorerWindow(self._parent, self, manager=self._data_manager)
117
118        self.dockedFilesWidget = QtGui.QDockWidget("Data Explorer", self._workspace)
119        self.dockedFilesWidget.setWidget(self.filesWidget)
120
121        # Disable maximize/minimize and close buttons
122        self.dockedFilesWidget.setFeatures(QtGui.QDockWidget.NoDockWidgetFeatures)
123        self._workspace.addDockWidget(QtCore.Qt.LeftDockWidgetArea,
124                                      self.dockedFilesWidget)
125
126        # Add the console window as another docked widget
127        self.logDockWidget = QtGui.QDockWidget("Log Explorer", self._workspace)
128        self.logDockWidget.setObjectName("LogDockWidget")
129        self.listWidget = QtGui.QTextBrowser()
130        self.logDockWidget.setWidget(self.listWidget)
131        self._workspace.addDockWidget(QtCore.Qt.BottomDockWidgetArea,
132                                      self.logDockWidget)
133
134        # Add other, minor widgets
135        self.ackWidget = Acknowledgements()
136        self.aboutWidget = AboutBox()
137
138        # Add calculators - floating for usability
139        self.SLDCalculator = SldPanel(self)
140        self.DVCalculator = DensityPanel(self)
141        #self.KIESSIGCalculator = DensityPanel(self)#KiessigPanel(self)
142        self.KIESSIGCalculator = KiessigPanel(self)
143        self.SlitSizeCalculator = SlitSizeCalculator(self)
144
145    def statusBarSetup(self):
146        """
147        Define the status bar.
148        | <message label> .... | Progress Bar |
149
150        Progress bar invisible until explicitly shown
151        """
152        self.progress = QtGui.QProgressBar()
153        self._workspace.statusbar.setSizeGripEnabled(False)
154
155        self.statusLabel = QtGui.QLabel()
156        self.statusLabel.setText("Welcome to SasView")
157        self._workspace.statusbar.addPermanentWidget(self.statusLabel, 1)
158        self._workspace.statusbar.addPermanentWidget(self.progress, stretch=0)
159        self.progress.setRange(0, 100)
160        self.progress.setValue(0)
161        self.progress.setTextVisible(True)
162        self.progress.setVisible(False)
163
164    def fileRead(self, data):
165        """
166        Callback for fileDataReceivedSignal
167        """
168        pass
169
170    def workspace(self):
171        """
172        Accessor for the main window workspace
173        """
174        return self._workspace.workspace
175
176    def perspectiveChanged(self, perspective_name):
177        """
178        Respond to change of the perspective signal
179        """
180        # Close the previous perspective
181        if self._current_perspective:
182            self._current_perspective.close()
183        # Default perspective
184        self._current_perspective = Perspectives.PERSPECTIVES[str(perspective_name)](parent=self)
185        self._workspace.workspace.addWindow(self._current_perspective)
186        self._current_perspective.show()
187
188    def updatePerspective(self, data):
189        """
190        """
191        assert isinstance(data, list)
192        if self._current_perspective is not None:
193            self._current_perspective.setData(data.values())
194        else:
195            msg = "No perspective is currently active."
196            logging.info(msg)
197
198
199    def communicator(self):
200        """ Accessor for the communicator """
201        return self.communicate
202
203    def reactor(self):
204        """ Accessor for the reactor """
205        return self._reactor
206
207    def setReactor(self, reactor):
208        """ Reactor setter """
209        self._reactor = reactor
210
211    def perspective(self):
212        """ Accessor for the perspective """
213        return self._current_perspective
214
215    def updateProgressBar(self, value):
216        """
217        Update progress bar with the required value (0-100)
218        """
219        assert -1 <= value <= 100
220        if value == -1:
221            self.progress.setVisible(False)
222            return
223        if not self.progress.isVisible():
224            self.progress.setTextVisible(True)
225            self.progress.setVisible(True)
226
227        self.progress.setValue(value)
228
229    def updateStatusBar(self, text):
230        """
231        """
232        #self._workspace.statusbar.showMessage(text)
233        self.statusLabel.setText(text)
234
235    def createGuiData(self, item, p_file=None):
236        """
237        Access the Data1D -> plottable Data1D conversion
238        """
239        return self._data_manager.create_gui_data(item, p_file)
240
241    def setData(self, data):
242        """
243        Sends data to current perspective
244        """
245        if self._current_perspective is not None:
246            self._current_perspective.setData(data.values())
247        else:
248            msg = "Guiframe does not have a current perspective"
249            logging.info(msg)
250
251    def quitApplication(self):
252        """
253        Close the reactor and exit nicely.
254        """
255        # Display confirmation messagebox
256        quit_msg = "Are you sure you want to exit the application?"
257        reply = QtGui.QMessageBox.question(
258            self._parent,
259            'Information',
260            quit_msg,
261            QtGui.QMessageBox.Yes,
262            QtGui.QMessageBox.No)
263
264        # Exit if yes
265        if reply == QtGui.QMessageBox.Yes:
266            reactor.callFromThread(reactor.stop)
267            return True
268
269        return False
270
271    def checkUpdate(self):
272        """
273        Check with the deployment server whether a new version
274        of the application is available.
275        A thread is started for the connecting with the server. The thread calls
276        a call-back method when the current version number has been obtained.
277        """
278        version_info = {"version": "0.0.0"}
279        c = Connection(LocalConfig.__update_URL__, LocalConfig.UPDATE_TIMEOUT)
280        response = c.connect()
281        if response is not None:
282            try:
283                content = response.read().strip()
284                logging.info("Connected to www.sasview.org. Latest version: %s"
285                             % (content))
286                version_info = json.loads(content)
287            except ValueError, ex:
288                logging.info("Failed to connect to www.sasview.org:", ex)
289        self.processVersion(version_info)
290
291    def processVersion(self, version_info):
292        """
293        Call-back method for the process of checking for updates.
294        This methods is called by a VersionThread object once the current
295        version number has been obtained. If the check is being done in the
296        background, the user will not be notified unless there's an update.
297
298        :param version: version string
299        """
300        try:
301            version = version_info["version"]
302            if version == "0.0.0":
303                msg = "Could not connect to the application server."
304                msg += " Please try again later."
305                #self.SetStatusText(msg)
306                self.communicate.statusBarUpdateSignal.emit(msg)
307
308            elif cmp(version, LocalConfig.__version__) > 0:
309                msg = "Version %s is available! " % str(version)
310                if "download_url" in version_info:
311                    webbrowser.open(version_info["download_url"])
312                else:
313                    webbrowser.open(LocalConfig.__download_page__)
314                self.communicate.statusBarUpdateSignal.emit(msg)
315            else:
316                msg = "You have the latest version"
317                msg += " of %s" % str(LocalConfig.__appname__)
318                self.communicate.statusBarUpdateSignal.emit(msg)
319        except:
320            msg = "guiframe: could not get latest application"
321            msg += " version number\n  %s" % sys.exc_value
322            logging.error(msg)
323            msg = "Could not connect to the application server."
324            msg += " Please try again later."
325            self.communicate.statusBarUpdateSignal.emit(msg)
326
327    def addCallbacks(self):
328        """
329        Method defining all signal connections for the gui manager
330        """
331        self.communicate = GuiUtils.Communicate()
332        self.communicate.fileDataReceivedSignal.connect(self.fileRead)
333        self.communicate.statusBarUpdateSignal.connect(self.updateStatusBar)
334        self.communicate.updatePerspectiveWithDataSignal.connect(self.updatePerspective)
335        self.communicate.progressBarUpdateSignal.connect(self.updateProgressBar)
336        self.communicate.perspectiveChangedSignal.connect(self.perspectiveChanged)
337        self.communicate.updateTheoryFromPerspectiveSignal.connect(self.updateTheoryFromPerspective)
338
339    def addTriggers(self):
340        """
341        Trigger definitions for all menu/toolbar actions.
342        """
343        # File
344        self._workspace.actionLoadData.triggered.connect(self.actionLoadData)
345        self._workspace.actionLoad_Data_Folder.triggered.connect(self.actionLoad_Data_Folder)
346        self._workspace.actionOpen_Project.triggered.connect(self.actionOpen_Project)
347        self._workspace.actionOpen_Analysis.triggered.connect(self.actionOpen_Analysis)
348        self._workspace.actionSave.triggered.connect(self.actionSave)
349        self._workspace.actionSave_Analysis.triggered.connect(self.actionSave_Analysis)
350        self._workspace.actionQuit.triggered.connect(self.actionQuit)
351        # Edit
352        self._workspace.actionUndo.triggered.connect(self.actionUndo)
353        self._workspace.actionRedo.triggered.connect(self.actionRedo)
354        self._workspace.actionCopy.triggered.connect(self.actionCopy)
355        self._workspace.actionPaste.triggered.connect(self.actionPaste)
356        self._workspace.actionReport.triggered.connect(self.actionReport)
357        self._workspace.actionReset.triggered.connect(self.actionReset)
358        self._workspace.actionExcel.triggered.connect(self.actionExcel)
359        self._workspace.actionLatex.triggered.connect(self.actionLatex)
360
361        # View
362        self._workspace.actionShow_Grid_Window.triggered.connect(self.actionShow_Grid_Window)
363        self._workspace.actionHide_Toolbar.triggered.connect(self.actionHide_Toolbar)
364        self._workspace.actionStartup_Settings.triggered.connect(self.actionStartup_Settings)
365        self._workspace.actionCategry_Manager.triggered.connect(self.actionCategry_Manager)
366        # Tools
367        self._workspace.actionData_Operation.triggered.connect(self.actionData_Operation)
368        self._workspace.actionSLD_Calculator.triggered.connect(self.actionSLD_Calculator)
369        self._workspace.actionDensity_Volume_Calculator.triggered.connect(self.actionDensity_Volume_Calculator)
370        self._workspace.actionKeissig_Calculator.triggered.connect(self.actionKiessig_Calculator)
371        #self._workspace.actionKIESSING_Calculator.triggered.connect(self.actionKIESSING_Calculator)
372        self._workspace.actionSlit_Size_Calculator.triggered.connect(self.actionSlit_Size_Calculator)
373        self._workspace.actionSAS_Resolution_Estimator.triggered.connect(self.actionSAS_Resolution_Estimator)
374        self._workspace.actionGeneric_Scattering_Calculator.triggered.connect(self.actionGeneric_Scattering_Calculator)
375        self._workspace.actionPython_Shell_Editor.triggered.connect(self.actionPython_Shell_Editor)
376        self._workspace.actionImage_Viewer.triggered.connect(self.actionImage_Viewer)
377        # Fitting
378        self._workspace.actionNew_Fit_Page.triggered.connect(self.actionNew_Fit_Page)
379        self._workspace.actionConstrained_Fit.triggered.connect(self.actionConstrained_Fit)
380        self._workspace.actionCombine_Batch_Fit.triggered.connect(self.actionCombine_Batch_Fit)
381        self._workspace.actionFit_Options.triggered.connect(self.actionFit_Options)
382        self._workspace.actionFit_Results.triggered.connect(self.actionFit_Results)
383        self._workspace.actionChain_Fitting.triggered.connect(self.actionChain_Fitting)
384        self._workspace.actionEdit_Custom_Model.triggered.connect(self.actionEdit_Custom_Model)
385        # Window
386        self._workspace.actionCascade.triggered.connect(self.actionCascade)
387        self._workspace.actionTile.triggered.connect(self.actionTile)
388        self._workspace.actionArrange_Icons.triggered.connect(self.actionArrange_Icons)
389        self._workspace.actionNext.triggered.connect(self.actionNext)
390        self._workspace.actionPrevious.triggered.connect(self.actionPrevious)
391        # Analysis
392        self._workspace.actionFitting.triggered.connect(self.actionFitting)
393        self._workspace.actionInversion.triggered.connect(self.actionInversion)
394        self._workspace.actionInvariant.triggered.connect(self.actionInvariant)
395        # Help
396        self._workspace.actionDocumentation.triggered.connect(self.actionDocumentation)
397        self._workspace.actionTutorial.triggered.connect(self.actionTutorial)
398        self._workspace.actionAcknowledge.triggered.connect(self.actionAcknowledge)
399        self._workspace.actionAbout.triggered.connect(self.actionAbout)
400        self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update)
401
402    #============ FILE =================
403    def actionLoadData(self):
404        """
405        Menu File/Load Data File(s)
406        """
407        self.filesWidget.loadFile()
408
409    def actionLoad_Data_Folder(self):
410        """
411        Menu File/Load Data Folder
412        """
413        self.filesWidget.loadFolder()
414
415    def actionOpen_Project(self):
416        """
417        Menu Open Project
418        """
419        self.filesWidget.loadProject()
420
421    def actionOpen_Analysis(self):
422        """
423        """
424        print("actionOpen_Analysis TRIGGERED")
425        pass
426
427    def actionSave(self):
428        """
429        Menu Save Project
430        """
431        self.filesWidget.saveProject()
432
433    def actionSave_Analysis(self):
434        """
435        """
436        print("actionSave_Analysis TRIGGERED")
437
438        pass
439
440    def actionQuit(self):
441        """
442        Close the reactor, exit the application.
443        """
444        self.quitApplication()
445
446    #============ EDIT =================
447    def actionUndo(self):
448        """
449        """
450        print("actionUndo TRIGGERED")
451        pass
452
453    def actionRedo(self):
454        """
455        """
456        print("actionRedo TRIGGERED")
457        pass
458
459    def actionCopy(self):
460        """
461        """
462        print("actionCopy TRIGGERED")
463        pass
464
465    def actionPaste(self):
466        """
467        """
468        print("actionPaste TRIGGERED")
469        pass
470
471    def actionReport(self):
472        """
473        """
474        print("actionReport TRIGGERED")
475        pass
476
477    def actionReset(self):
478        """
479        """
480        logging.warning(" *** actionOpen_Analysis logging *******")
481        print("actionReset print TRIGGERED")
482        sys.stderr.write("STDERR - TRIGGERED")
483        pass
484
485    def actionExcel(self):
486        """
487        """
488        print("actionExcel TRIGGERED")
489        pass
490
491    def actionLatex(self):
492        """
493        """
494        print("actionLatex TRIGGERED")
495        pass
496
497    #============ VIEW =================
498    def actionShow_Grid_Window(self):
499        """
500        """
501        print("actionShow_Grid_Window TRIGGERED")
502        pass
503
504    def actionHide_Toolbar(self):
505        """
506        Toggle toolbar vsibility
507        """
508        if self._workspace.toolBar.isVisible():
509            self._workspace.actionHide_Toolbar.setText("Show Toolbar")
510            self._workspace.toolBar.setVisible(False)
511        else:
512            self._workspace.actionHide_Toolbar.setText("Hide Toolbar")
513            self._workspace.toolBar.setVisible(True)
514        pass
515
516    def actionStartup_Settings(self):
517        """
518        """
519        print("actionStartup_Settings TRIGGERED")
520        pass
521
522    def actionCategry_Manager(self):
523        """
524        """
525        print("actionCategry_Manager TRIGGERED")
526        pass
527
528    #============ TOOLS =================
529    def actionData_Operation(self):
530        """
531        """
532        print("actionData_Operation TRIGGERED")
533        pass
534
535    def actionSLD_Calculator(self):
536        """
537        """
538        self.SLDCalculator.show()
539
540    def actionDensity_Volume_Calculator(self):
541        """
542        """
543        self.DVCalculator.show()
544
545    def actionKiessig_Calculator(self):
546        """
547        """
548        #self.DVCalculator.show()
549        self.KIESSIGCalculator.show()
550
551    def actionSlit_Size_Calculator(self):
552        """
553        """
554        self.SlitSizeCalculator.show()
555
556    def actionSAS_Resolution_Estimator(self):
557        """
558        """
559        print("actionSAS_Resolution_Estimator TRIGGERED")
560        pass
561
562    def actionGeneric_Scattering_Calculator(self):
563        """
564        """
565        print("actionGeneric_Scattering_Calculator TRIGGERED")
566        pass
567
568    def actionPython_Shell_Editor(self):
569        """
570        Display the Jupyter console as a docked widget.
571        """
572        terminal = IPythonWidget()
573
574        # Add the console window as another docked widget
575        self.ipDockWidget = QtGui.QDockWidget("IPython", self._workspace)
576        self.ipDockWidget.setObjectName("IPythonDockWidget")
577        self.ipDockWidget.setWidget(terminal)
578        self._workspace.addDockWidget(QtCore.Qt.RightDockWidgetArea,
579                                      self.ipDockWidget)
580
581    def actionImage_Viewer(self):
582        """
583        """
584        print("actionImage_Viewer TRIGGERED")
585        pass
586
587    #============ FITTING =================
588    def actionNew_Fit_Page(self):
589        """
590        Add a new, empty Fit page in the fitting perspective.
591        """
592        # Make sure the perspective is correct
593        per = self.perspective()
594        if not isinstance(per, FittingWindow):
595            return
596        per.addFit(None)
597
598    def actionConstrained_Fit(self):
599        """
600        """
601        print("actionConstrained_Fit TRIGGERED")
602        pass
603
604    def actionCombine_Batch_Fit(self):
605        """
606        """
607        print("actionCombine_Batch_Fit TRIGGERED")
608        pass
609
610    def actionFit_Options(self):
611        """
612        """
613        print("actionFit_Options TRIGGERED")
614        pass
615
616    def actionFit_Results(self):
617        """
618        """
619        print("actionFit_Results TRIGGERED")
620        pass
621
622    def actionChain_Fitting(self):
623        """
624        """
625        print("actionChain_Fitting TRIGGERED")
626        pass
627
628    def actionEdit_Custom_Model(self):
629        """
630        """
631        print("actionEdit_Custom_Model TRIGGERED")
632        pass
633
634    #============ ANALYSIS =================
635    def actionFitting(self):
636        """
637        """
638        print("actionFitting TRIGGERED")
639        pass
640
641    def actionInversion(self):
642        """
643        """
644        print("actionInversion TRIGGERED")
645        pass
646
647    def actionInvariant(self):
648        """
649        """
650        print("actionInvariant TRIGGERED")
651        pass
652
653    #============ WINDOW =================
654    def actionCascade(self):
655        """
656        Arranges all the child windows in a cascade pattern.
657        """
658        self._workspace.workspace.cascade()
659
660    def actionTile(self):
661        """
662        Tile workspace windows
663        """
664        self._workspace.workspace.tile()
665
666    def actionArrange_Icons(self):
667        """
668        Arranges all iconified windows at the bottom of the workspace
669        """
670        self._workspace.workspace.arrangeIcons()
671
672    def actionNext(self):
673        """
674        Gives the input focus to the next window in the list of child windows.
675        """
676        self._workspace.workspace.activateNextWindow()
677
678    def actionPrevious(self):
679        """
680        Gives the input focus to the previous window in the list of child windows.
681        """
682        self._workspace.workspace.activatePreviousWindow()
683
684    #============ HELP =================
685    def actionDocumentation(self):
686        """
687        Display the documentation
688
689        TODO: use QNetworkAccessManager to assure _helpLocation is valid
690        """
691        self._helpView.load(QtCore.QUrl(self._helpLocation))
692        self._helpView.show()
693
694    def actionTutorial(self):
695        """
696        Open the tutorial PDF file with default PDF renderer
697        """
698        # Not terribly safe here. Shell injection warning.
699        # isfile() helps but this probably needs a better solution.
700        if os.path.isfile(self._tutorialLocation):
701            result = subprocess.Popen([self._tutorialLocation], shell=True)
702
703    def actionAcknowledge(self):
704        """
705        Open the Acknowledgements widget
706        """
707        self.ackWidget.show()
708
709    def actionAbout(self):
710        """
711        Open the About box
712        """
713        # Update the about box with current version and stuff
714
715        # TODO: proper sizing
716        self.aboutWidget.show()
717
718    def actionCheck_for_update(self):
719        """
720        Menu Help/Check for Update
721        """
722        self.checkUpdate()
723
724    def updateTheoryFromPerspective(self, index):
725        """
726        Catch the theory update signal from a perspective
727        Send the request to the DataExplorer for updating the theory model.
728        """
729        self.filesWidget.updateTheoryFromPerspective(index)
730
731
732
Note: See TracBrowser for help on using the repository browser.