source: sasview/src/sas/qtgui/GuiManager.py @ 15fd91b

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 15fd91b was f51ed67, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Clean up and automate generation of UI and resources

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