source: src-qt4/PCDM/src/pcdm-gui.cpp @ d9b0b9e8

9.2-releasereleng/10.0releng/10.0.1
Last change on this file since d9b0b9e8 was d9b0b9e8, checked in by Ken Moore <ken@…>, 9 months ago

Add in a "simple" option for the PCDM desktop switcher.
Change the DESKTOP_ORIENTATION value to "simple" to use a small QComboBox on the toolbar for selecting a desktop environment instead of using the full selector

  • Property mode set to 100644
File size: 19.9 KB
Line 
1/* PCDM Login Manager:
2*  Written by Ken Moore (ken@pcbsd.org) 2012/2013
3*  Copyright(c) 2013 by the PC-BSD Project
4*  Available under the 3-clause BSD license
5*/
6
7#include <QProcess>
8#include <QTimer>
9#include <QGraphicsPixmapItem>
10#include <QStyle>
11#include <pcbsd-utils.h>
12
13#include "pcdm-gui.h"
14#include "pcdm-backend.h"
15#include "fancySwitcher.h"
16
17bool DEBUG_MODE=FALSE;
18
19PCDMgui::PCDMgui() : QMainWindow()
20{
21    lastUser.clear();
22    lastDE.clear();
23    //Load the Theme
24    loadTheme();
25    //Create the GUI based upon the current Theme
26    createGUIfromTheme();
27    //Now make sure that the login widget has keyboard focus
28    loginW->resetFocus();
29    this->setObjectName("PCDM-background");
30}
31
32PCDMgui::~PCDMgui()
33{
34    //delete ui;
35}
36
37void PCDMgui::loadTheme(){
38  currentTheme = new ThemeStruct();
39  QString themeFile = Config::themeFile();
40  if(!QFile::exists(themeFile)){ themeFile = ":samples/themes/default/default.theme"; }
41  currentTheme->loadThemeFile( themeFile );
42  //Check the Theme, using default values as necessary
43  QStringList invalid = currentTheme->invalidItems();
44  if( !invalid.isEmpty() ){
45    ThemeStruct* defaultTheme = new ThemeStruct();
46    defaultTheme->loadThemeFile( ":samples/themes/default/default.theme" );
47    for( int i=0; i<invalid.length(); i++){
48      //Replace the invalid items with the defaults
49      qDebug() << "Invalid Theme Item, Using defaults:" << invalid[i];
50      currentTheme->importItem( invalid[i] , defaultTheme->exportItem(invalid[i]) );
51    }
52  }
53  //Load the data from the last successful login
54  loadLastUser();
55 
56}
57
58void PCDMgui::createGUIfromTheme(){
59  QString style;
60  QString tmpIcon; //used for checking image files before loading them
61  //Set the background image
62  if( currentTheme->itemIsEnabled("background") ){
63    tmpIcon = currentTheme->itemIcon("background");
64    if( tmpIcon.isEmpty() || !QFile::exists(tmpIcon) ){ tmpIcon = ":/images/backgroundimage.jpg"; }
65    //use "border-image" instead of "background-image" to stretch rather than tile the image
66    QString bgstyle = "QMainWindow#PCDM-background {border-image: url(BGIMAGE) stretch;}"; 
67    bgstyle.replace("BGIMAGE", tmpIcon);
68    style.append(bgstyle);
69  }
70  //Set the application style sheet
71  style.append(" "+ currentTheme->styleSheet() );
72  this->setStyleSheet( style.simplified() );
73
74  //Check for whether the desktop switcher is on the toolbar or not
75  simpleDESwitcher = (currentTheme->itemValue("desktop") == "simple");
76 
77  //get the default translation directory
78  translationDir = QApplication::applicationDirPath() + "/i18n/";
79  //Fill the translator
80  m_translator = new QTranslator();
81  //Create the Toolbar
82  toolbar = new QToolBar();
83  //Add the Toolbar to the window
84    //use the theme location   
85    QString tarea = currentTheme->itemValue("toolbar");
86    if(tarea == "left"){
87      this->addToolBar( Qt::LeftToolBarArea, toolbar );             
88    }else if( tarea=="top"){
89      this->addToolBar( Qt::TopToolBarArea, toolbar );             
90    }else if(tarea=="right"){
91      this->addToolBar( Qt::RightToolBarArea, toolbar );           
92    }else{ //bottom is default
93      this->addToolBar( Qt::BottomToolBarArea, toolbar );       
94    }
95    //Set toolbar flags
96    toolbar->setVisible(TRUE);
97    toolbar->setMovable(FALSE);
98    toolbar->setFloatable(FALSE);
99    //Set the default style and icon sizes
100    QString tstyle = currentTheme->itemIcon("toolbar").toLower(); //use the theme style
101    if(tstyle=="textonly"){ toolbar->setToolButtonStyle(Qt::ToolButtonTextOnly); }
102    else if(tstyle=="textbesideicon"){ toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); }
103    else if(tstyle=="textundericon"){ toolbar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); }
104    else{ toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); } //default to icon only
105   
106    toolbar->setIconSize( currentTheme->itemIconSize("toolbar") ); //use theme size
107    toolbar->setFocusPolicy( Qt::NoFocus );
108  //Populate the Toolbar with items (starts at leftmost/topmost)
109    //----Virtual Keyboard
110    tmpIcon = currentTheme->itemIcon("vkeyboard");
111    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/input-keyboard.png"; }
112    virtkeyboardButton = new QAction( QIcon(tmpIcon),tr("Virtual Keyboard"),this );
113    toolbar->addAction(virtkeyboardButton);
114    connect( virtkeyboardButton, SIGNAL(triggered()), this, SLOT(slotPushVirtKeyboard()) );
115
116    //----Locale Switcher
117    tmpIcon = currentTheme->itemIcon("locale");
118    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/language.png"; }
119    localeButton = new QAction( QIcon(tmpIcon),tr("Locale"),this );
120    toolbar->addAction(localeButton);
121    connect( localeButton, SIGNAL(triggered()), this, SLOT(slotChangeLocale()) );
122   
123    //----Keyboard Layout Switcher
124    tmpIcon = currentTheme->itemIcon("keyboard");
125    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/keyboard.png"; }
126    keyboardButton = new QAction( QIcon(tmpIcon),tr("Keyboard Layout"),this );
127    toolbar->addAction(keyboardButton);
128    connect( keyboardButton, SIGNAL(triggered()), this, SLOT(slotChangeKeyboardLayout()) );
129   
130    //----Add a spacer
131    QWidget* spacer = new QWidget();
132    spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
133    toolbar->addWidget(spacer);
134   
135    if(simpleDESwitcher){
136      //Create the simple DE Switcher
137      sdeSwitcher = new QComboBox(this); 
138      toolbar->addWidget(sdeSwitcher);
139      //Add an additional spacer
140      QWidget* spacer2 = new QWidget();
141      spacer2->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
142      toolbar->addWidget(spacer2);
143     
144    }
145   
146    //----System Shutdown/Restart
147    tmpIcon = currentTheme->itemIcon("system");
148    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/system.png"; }
149    QAction* act = new QAction( QIcon(tmpIcon),tr("System"),this );
150    systemButton = new QToolButton();
151    systemButton->setDefaultAction(act);
152    systemMenu = new QMenu();
153    toolbar->addWidget(systemButton);
154    systemButton->setPopupMode( QToolButton::InstantPopup );
155    systemButton->setFocusPolicy( Qt::NoFocus );
156   
157  //Create the grid layout
158  QGridLayout* grid = new QGridLayout;
159  //Populate the grid with items
160    //----Header Image
161    QLabel* header = new QLabel; 
162    if( currentTheme->itemIsEnabled("header") ){
163      tmpIcon = currentTheme->itemIcon("header");
164      if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/banner.png"; }
165      QPixmap tmp( tmpIcon );
166      header->setPixmap( tmp.scaled( currentTheme->itemIconSize("header") ) );
167      grid->addWidget( header, currentTheme->itemLocation("header","row"), \
168                      currentTheme->itemLocation("header","col"), \
169                      currentTheme->itemLocation("header","rowspan"), \
170                      currentTheme->itemLocation("header","colspan"), Qt::AlignCenter);
171    }
172   
173    //Username/Password/Login widget
174    loginW = new LoginWidget;
175    loginW->setUsernames(Backend::getSystemUsers()); //add in the detected users
176    if(!lastUser.isEmpty()){ //set the previously used user
177        loginW->setCurrentUser(lastUser); 
178        loadLastDE(lastUser); //make sure the DE switcher reflects the last user choice
179    } 
180    //Set Icons from theme
181    tmpIcon = currentTheme->itemIcon("login");
182    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/next.png"; }
183    loginW->changeButtonIcon("login",tmpIcon, currentTheme->itemIconSize("login"));
184    tmpIcon = currentTheme->itemIcon("user");
185    slotUserChanged(loginW->currentUsername()); //Make sure that we have the correct user icon
186    tmpIcon = currentTheme->itemIcon("password");
187    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/password.png"; }
188    loginW->changeButtonIcon("pwview",tmpIcon, currentTheme->itemIconSize("password"));
189    //Enable/disable the password view functionality
190    loginW->allowPasswordView( Config::allowPasswordView() );
191    //Add item to the grid
192    grid->addWidget( loginW, currentTheme->itemLocation("login","row"), \
193                      currentTheme->itemLocation("login","col"), \
194                      currentTheme->itemLocation("login","rowspan"), \
195                      currentTheme->itemLocation("login","colspan"), Qt::AlignCenter);
196    //Connect the signals/slots
197    connect(loginW,SIGNAL(loginRequested(QString,QString)),this,SLOT(slotStartLogin(QString,QString)));
198    connect(loginW,SIGNAL(escapePressed()),this,SLOT(slotShutdownComputer()));
199    connect(loginW,SIGNAL(UserSelected(QString)), this, SLOT(slotUserSelected(QString)) );
200    connect(loginW,SIGNAL(UserChanged(QString)), this, SLOT(slotUserChanged(QString)) );
201   
202    //----Desktop Environment Switcher
203    if(!simpleDESwitcher){
204      //Create the switcher
205      deSwitcher = new FancySwitcher(this, !currentTheme->itemIsVertical("desktop") );
206      QSize deSize = currentTheme->itemIconSize("desktop");
207      deSwitcher->setIconSize(deSize.height());
208      tmpIcon = currentTheme->itemIcon("nextde");
209      if( !tmpIcon.isEmpty() && QFile::exists(tmpIcon) ){ deSwitcher->changeButtonIcon("forward", tmpIcon); }
210      tmpIcon = currentTheme->itemIcon("previousde");
211      if( !tmpIcon.isEmpty() && QFile::exists(tmpIcon) ){ deSwitcher->changeButtonIcon("back", tmpIcon); }
212      //Figure out if we need to smooth out the animation
213      deSwitcher->setNumberAnimationFrames("4"); 
214      //NOTE: A transparent widget background slows the full animation to a crawl with a stretched background image
215
216      grid->addWidget( deSwitcher, currentTheme->itemLocation("desktop","row"), \
217                      currentTheme->itemLocation("desktop","col"), \
218                      currentTheme->itemLocation("desktop","rowspan"), \
219                      currentTheme->itemLocation("desktop","colspan"), Qt::AlignCenter);
220    }
221    //----WINDOW SPACERS
222    QStringList spacers = currentTheme->getSpacers();
223    for(int i=0; i<spacers.length(); i++){
224      bool isVertical = (spacers[i].section("::",0,0) == "true");
225      int row = spacers[i].section("::",1,1).toInt();
226      int col = spacers[i].section("::",2,2).toInt();
227      //qDebug() << "Add Spacer:" << isVertical << row << col;
228      if(isVertical){
229        grid->setRowStretch(row,1);
230      }else{ //horizontal
231        grid->setColumnStretch(col,1);
232      }
233    }
234
235  //Connect the grid to the Window
236    QWidget* widget = new QWidget;
237    widget->setLayout(grid);
238    this->setCentralWidget(widget);
239   
240  //Now translate the UI and set all the text
241  retranslateUi();
242
243}
244
245void PCDMgui::slotStartLogin(QString displayname, QString password){
246  //Get user inputs
247  QString username = Backend::getUsernameFromDisplayname(displayname);
248  QString binary;
249  if(simpleDESwitcher){
250    binary = Backend::getDesktopBinary(sdeSwitcher->currentText());
251  }else{
252    binary = Backend::getDesktopBinary(deSwitcher->currentItem());
253  }
254  QString homedir = Backend::getUserHomeDir(username);
255  //Disable user input while confirming login
256  loginW->setEnabled(FALSE);
257  if(!simpleDESwitcher){ deSwitcher->setEnabled(FALSE); }
258  toolbar->setEnabled(FALSE);
259  //Try to login
260  emit xLoginAttempt(username, password, homedir, binary);
261  //Return signals are connected to the slotLogin[Success/Failure] functions
262 
263}
264
265void PCDMgui::slotLoginSuccess(){
266  QString de;
267  if(simpleDESwitcher){ de = sdeSwitcher->currentText(); }
268  else{ de = deSwitcher->currentItem(); }
269  saveLastLogin( loginW->currentUsername(), de );
270  slotClosePCDM(); //now start to close down the PCDM GUI
271}
272
273void PCDMgui::slotLoginFailure(){
274  //Display an info box that the login failed
275  QMessageBox notice(this);
276    notice.setWindowTitle(tr("Invalid Username/Password"));
277    notice.setIcon(QMessageBox::Warning);
278    notice.setText(tr("Username/Password combination is invalid, please try again."));
279    notice.setInformativeText("("+tr("Tip: Make sure that caps-lock is turned off.")+")");
280    notice.setStandardButtons(QMessageBox::Ok);
281    notice.setDefaultButton(QMessageBox::Ok);
282    notice.exec();
283       
284  //Re-Enable user input
285  loginW->setEnabled(TRUE);
286  if(!simpleDESwitcher){ deSwitcher->setEnabled(TRUE); }
287  toolbar->setEnabled(TRUE);
288}
289
290void PCDMgui::slotUserChanged(QString newuser){
291  if( !newuser.isEmpty() ){
292    //Try to load the custom user icon
293    QString tmpIcon = Backend::getUserHomeDir(newuser) + "/.loginIcon.png";
294    if(!QFile::exists(tmpIcon) ){ tmpIcon= currentTheme->itemIcon("user"); }
295    if(!QFile::exists(tmpIcon) || tmpIcon.isEmpty() ){ tmpIcon=":/images/user.png"; }
296    loginW->changeButtonIcon("display",tmpIcon, currentTheme->itemIconSize("user"));
297  }
298}
299
300void PCDMgui::slotUserSelected(QString newuser){
301  if(newuser.isEmpty()){
302    if(simpleDESwitcher){ sdeSwitcher->setVisible(FALSE); }
303    else{ deSwitcher->setVisible(FALSE); }
304  }else{
305    if(simpleDESwitcher){ sdeSwitcher->setVisible(TRUE); }
306    else{ deSwitcher->setVisible(TRUE); }
307    //Try to load the user's last DE
308    loadLastDE(newuser);
309    //Try to load the custom user icon
310    slotUserChanged(newuser);
311  }
312  retranslateUi();
313}
314
315void PCDMgui::slotShutdownComputer(){
316  QMessageBox verify;
317  verify.setWindowTitle(tr("System Shutdown"));
318  verify.setText(tr("You are about to shut down the system."));
319  verify.setInformativeText(tr("Are you sure?"));
320  verify.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
321  verify.setDefaultButton(QMessageBox::No);
322  int ret = verify.exec();
323
324  if(ret == QMessageBox::Yes){
325    Backend::log("PCDM: Shutting down computer");
326    system("shutdown -p now");
327  }
328}
329
330void PCDMgui::slotRestartComputer(){
331  QMessageBox verify;
332  verify.setWindowTitle(tr("System Restart"));
333  verify.setText(tr("You are about to restart the system."));
334  verify.setInformativeText(tr("Are you sure?"));
335  verify.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
336  verify.setDefaultButton(QMessageBox::No);
337  int ret = verify.exec();
338
339  if(ret == QMessageBox::Yes){
340    Backend::log("PCDM: Restarting computer");
341    system("shutdown -r now");
342  }
343}
344
345void PCDMgui::slotClosePCDM(){
346  system("killall -9 xvkbd"); //be sure to close the virtual keyboard
347  QCoreApplication::exit(0);
348  close();
349}
350
351void PCDMgui::slotChangeLocale(){
352  //Open the selector
353  wLoc = new widgetLocale();
354  QLocale currLocale = this->locale();
355  Backend::log("Current PCDM Locale: " + currLocale.name() );
356  wLoc->setCurrentLocale(currLocale.name());
357  wLoc->setWindowModality(Qt::ApplicationModal);
358  wLoc->show();
359  wLoc->raise();
360
361  //Connect the language changed signal
362  connect(wLoc,SIGNAL(languageChanged(QString)),this,SLOT(slotLocaleChanged(QString)) );
363}
364
365void PCDMgui::slotLocaleChanged(QString langCode){
366  //Get the valid Locale code
367  QString translationFile;
368  if(!QFile::exists(translationDir+"PCDM_"+langCode+".qm")){
369    langCode = langCode.section("_",0,0);
370    if(!QFile::exists(translationDir+"PCDM_"+langCode+".qm")){
371      if(!QFile::exists(translationDir+"qt_"+langCode+".qm")){
372        Backend::log("Desired locale is not a valid translation: " + langCode);
373        return;
374      }else{
375        translationFile = translationDir+"qt_"+langCode+".qm";     
376      }
377    }else{
378      translationFile = translationDir+"PCDM_"+langCode+".qm";     
379    }
380  }else{
381    translationFile = translationDir+"PCDM_"+langCode+".qm";
382  }     
383       
384  Backend::log("Changing localization to " + langCode);
385
386  //Alternate method for changing Locale
387  QLocale locale(langCode);
388  this->setLocale(locale);
389  //this->setDefaultLocale(locale);
390  //Change the translator
391  if( !m_translator->isEmpty() ){
392    Backend::log("Remove the translator");
393    QCoreApplication::removeTranslator(m_translator);
394  }
395
396  if(m_translator->load(translationFile)){
397    Backend::log("Install the new translator");
398    QCoreApplication::installTranslator(m_translator);   
399  }
400  //Re-draw the interface
401  retranslateUi();
402 
403  Backend::log("Current Locale after change: " + this->locale().name() );
404}
405
406void PCDMgui::slotChangeKeyboardLayout(){
407  //Fill a couple global variables
408  QStringList kModels = Backend::keyModels();
409  QStringList kLayouts = Backend::keyLayouts();
410  //Startup the GUI
411  wKey = new widgetKeyboard();
412  wKey->programInit(kModels, kLayouts);
413  wKey->setWindowModality(Qt::ApplicationModal);
414  wKey->show();
415  wKey->raise();
416}
417
418// Start xvkbd
419void PCDMgui::slotPushVirtKeyboard(){
420   system("killall -9 xvkbd; xvkbd -compact &");
421   loginW->resetFocus("password");
422}
423
424void PCDMgui::retranslateUi(){
425  //All the text-setting for the main interface needs to be done here
426  //virtual keyboard button
427  virtkeyboardButton->setToolTip(tr("Virtual Keyboard"));
428  virtkeyboardButton->setText(tr("Virtual Keyboard"));
429  //locale switcher button
430  localeButton->setToolTip(tr("Change locale"));
431  localeButton->setText(tr("Locale"));
432  //keyboard layout button
433  keyboardButton->setToolTip(tr("Change Keyboard Layout"));
434  keyboardButton->setText(tr("Keyboard Layout"));
435  //system button
436  systemButton->defaultAction()->setToolTip(tr("Shutdown the computer"));
437  systemButton->defaultAction()->setText(tr("System"));
438  //Menu entries for system button
439    systemMenu->clear();       
440    systemMenu->addAction( tr("Restart"),this, SLOT(slotRestartComputer()) );
441    systemMenu->addAction( tr("Shut Down"), this, SLOT(slotShutdownComputer()) );
442    if(DEBUG_MODE){systemMenu->addAction( tr("Close PCDM"), this, SLOT(slotClosePCDM()) ); }
443    systemButton->setMenu(systemMenu);
444  //The main login widget
445  if(hostname.isEmpty()){
446    //Find the system hostname
447    hostname = pcbsd::Utils::runShellCommand("hostname").join(" ").simplified();
448    loginW->displayHostName(hostname);   
449  }
450  loginW->retranslateUi();
451  //The desktop switcher
452  if(simpleDESwitcher){ sdeSwitcher->clear(); }
453  else{ deSwitcher->removeAllItems(); }
454 
455    //Get the new desktop list (translated)
456    QStringList deList = Backend::getAvailableDesktops();
457    for(int i=0; i<deList.length(); i++){
458      QString deIcon = Backend::getDesktopIcon(deList[i]);
459      if( deIcon.isEmpty() ){ deIcon = currentTheme->itemIcon("desktop"); } //set the default icon if none given
460      if( !QFile::exists(deIcon) ){ deIcon = ":/images/desktop.png"; }
461      //Now add the item back to the widget
462      if(simpleDESwitcher){ sdeSwitcher->addItem(QIcon(deIcon), deList[i]); }
463      else{ deSwitcher->addItem( deList[i], deIcon, Backend::getDesktopComment(deList[i]) ); }
464    }
465    //Set the switcher to the last used desktop environment
466    if( !lastDE.isEmpty() ){ 
467      if(simpleDESwitcher){ 
468        int index = deList.indexOf(lastDE);
469        if(index != -1){ sdeSwitcher->setCurrentIndex(index); }
470        else{ sdeSwitcher->setCurrentIndex(0); }
471      }else{ 
472        deSwitcher->setCurrentItem(lastDE); 
473      }
474    }
475
476}
477
478void PCDMgui::loadLastUser(){
479  lastUser.clear();
480  if(!QFile::exists("/usr/local/share/PCDM/.lastlogin")){
481    Backend::log("PCDM: No previous login data found");
482  }else{
483    //Load the previous login data
484    QFile file("/usr/local/share/PCDM/.lastlogin");
485    if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){
486      Backend::log("PCDM: Unable to open previous login data file");   
487    }else{
488      QTextStream in(&file);
489      lastUser= in.readLine();
490      file.close();
491    }
492  }
493}
494
495void PCDMgui::loadLastDE(QString user){
496  lastDE.clear();
497  QString LLpath = Backend::getUserHomeDir(user) + "/.lastlogin";
498  if(!QFile::exists(LLpath)){
499    Backend::log("PCDM: No previous user login data found for user: "+user);
500  }else{
501    //Load the previous login data
502    QFile file(LLpath);
503    if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){
504      Backend::log("PCDM: Unable to open previous user login file: "+user);   
505    }else{
506      QTextStream in(&file);
507      lastDE= in.readLine();
508      file.close();
509    }
510  }
511
512}
513
514void PCDMgui::saveLastLogin(QString USER, QString DE){
515  QFile file1("/usr/local/share/PCDM/.lastlogin");
516  if(!file1.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)){
517    Backend::log("PCDM: Unable to save last login data");         
518  }else{
519    QTextStream out(&file1);
520    out << USER;
521    file1.close();
522  }
523  QFile file2( Backend::getUserHomeDir(USER) + "/.lastlogin" );
524  if(!file2.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)){
525    Backend::log("PCDM: Unable to save last login data for user:"+USER);         
526  }else{
527    QTextStream out(&file2);
528    out << DE;
529    file2.close();
530  }
531}
532
Note: See TracBrowser for help on using the repository browser.