source: sasview/src/sas/qtgui/GuiManager.py @ 8548d739

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 8548d739 was 31c5b58, checked in by Piotr Rozyczko <rozyczko@…>, 8 years ago

Refactored to allow running with run.py.
Minor fixes to plotting.

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