source: sasview/src/sas/qtgui/GuiManager.py @ 6c8fb2c

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 6c8fb2c was 6c8fb2c, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Fitting options - connected GUI to logic.
Minor UI redesign of fitting options tab.

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