source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/changeset/ChangesetCacheManager.java@ 8510

Last change on this file since 8510 was 8510, checked in by Don-vip, 9 years ago

checkstyle: enable relevant whitespace checks and fix them

  • Property svn:eol-style set to native
File size: 23.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.changeset;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Container;
8import java.awt.Dimension;
9import java.awt.FlowLayout;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.awt.event.MouseEvent;
13import java.awt.event.WindowAdapter;
14import java.awt.event.WindowEvent;
15import java.util.Collection;
16import java.util.HashSet;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.AbstractAction;
21import javax.swing.DefaultListSelectionModel;
22import javax.swing.ImageIcon;
23import javax.swing.JComponent;
24import javax.swing.JFrame;
25import javax.swing.JOptionPane;
26import javax.swing.JPanel;
27import javax.swing.JPopupMenu;
28import javax.swing.JScrollPane;
29import javax.swing.JSplitPane;
30import javax.swing.JTabbedPane;
31import javax.swing.JTable;
32import javax.swing.JToolBar;
33import javax.swing.KeyStroke;
34import javax.swing.ListSelectionModel;
35import javax.swing.event.ListSelectionEvent;
36import javax.swing.event.ListSelectionListener;
37
38import org.openstreetmap.josm.Main;
39import org.openstreetmap.josm.data.osm.Changeset;
40import org.openstreetmap.josm.data.osm.ChangesetCache;
41import org.openstreetmap.josm.gui.HelpAwareOptionPane;
42import org.openstreetmap.josm.gui.JosmUserIdentityManager;
43import org.openstreetmap.josm.gui.SideButton;
44import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryDialog;
45import org.openstreetmap.josm.gui.dialogs.changeset.query.ChangesetQueryTask;
46import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
47import org.openstreetmap.josm.gui.help.HelpUtil;
48import org.openstreetmap.josm.gui.io.CloseChangesetTask;
49import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
50import org.openstreetmap.josm.io.ChangesetQuery;
51import org.openstreetmap.josm.io.OnlineResource;
52import org.openstreetmap.josm.tools.ImageProvider;
53import org.openstreetmap.josm.tools.WindowGeometry;
54
55/**
56 * ChangesetCacheManager manages the local cache of changesets
57 * retrieved from the OSM API. It displays both a table of the locally cached changesets
58 * and detail information about an individual changeset. It also provides actions for
59 * downloading, querying, closing changesets, in addition to removing changesets from
60 * the local cache.
61 *
62 */
63public class ChangesetCacheManager extends JFrame {
64
65 /** The changeset download icon **/
66 public static final ImageIcon DOWNLOAD_CONTENT_ICON = ImageProvider.get("dialogs/changeset", "downloadchangesetcontent");
67 /** The changeset update icon **/
68 public static final ImageIcon UPDATE_CONTENT_ICON = ImageProvider.get("dialogs/changeset", "updatechangesetcontent");
69
70 /** the unique instance of the cache manager */
71 private static volatile ChangesetCacheManager instance;
72
73 /**
74 * Replies the unique instance of the changeset cache manager
75 *
76 * @return the unique instance of the changeset cache manager
77 */
78 public static ChangesetCacheManager getInstance() {
79 if (instance == null) {
80 instance = new ChangesetCacheManager();
81 }
82 return instance;
83 }
84
85 /**
86 * Hides and destroys the unique instance of the changeset cache
87 * manager.
88 *
89 */
90 public static void destroyInstance() {
91 if (instance != null) {
92 instance.setVisible(true);
93 instance.dispose();
94 instance = null;
95 }
96 }
97
98 private ChangesetCacheManagerModel model;
99 private JSplitPane spContent;
100 private boolean needsSplitPaneAdjustment;
101
102 private RemoveFromCacheAction actRemoveFromCacheAction;
103 private CloseSelectedChangesetsAction actCloseSelectedChangesetsAction;
104 private DownloadSelectedChangesetsAction actDownloadSelectedChangesets;
105 private DownloadSelectedChangesetContentAction actDownloadSelectedContent;
106 private JTable tblChangesets;
107
108 /**
109 * Creates the various models required
110 */
111 protected void buildModel() {
112 DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
113 selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
114 model = new ChangesetCacheManagerModel(selectionModel);
115
116 actRemoveFromCacheAction = new RemoveFromCacheAction();
117 actCloseSelectedChangesetsAction = new CloseSelectedChangesetsAction();
118 actDownloadSelectedChangesets = new DownloadSelectedChangesetsAction();
119 actDownloadSelectedContent = new DownloadSelectedChangesetContentAction();
120 }
121
122 /**
123 * builds the toolbar panel in the heading of the dialog
124 *
125 * @return the toolbar panel
126 */
127 protected JPanel buildToolbarPanel() {
128 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
129
130 SideButton btn = new SideButton(new QueryAction());
131 pnl.add(btn);
132 pnl.add(new SingleChangesetDownloadPanel());
133 pnl.add(new SideButton(new DownloadMyChangesets()));
134
135 return pnl;
136 }
137
138 /**
139 * builds the button panel in the footer of the dialog
140 *
141 * @return the button row pane
142 */
143 protected JPanel buildButtonPanel() {
144 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
145
146 //-- cancel and close action
147 pnl.add(new SideButton(new CancelAction()));
148
149 //-- help action
150 pnl.add(new SideButton(
151 new ContextSensitiveHelpAction(
152 HelpUtil.ht("/Dialog/ChangesetManager"))
153 )
154 );
155
156 return pnl;
157 }
158
159 /**
160 * Builds the panel with the changeset details
161 *
162 * @return the panel with the changeset details
163 */
164 protected JPanel buildChangesetDetailPanel() {
165 JPanel pnl = new JPanel(new BorderLayout());
166 JTabbedPane tp = new JTabbedPane();
167
168 // -- add the details panel
169 ChangesetDetailPanel pnlChangesetDetail = new ChangesetDetailPanel();
170 tp.add(pnlChangesetDetail);
171 model.addPropertyChangeListener(pnlChangesetDetail);
172
173 // -- add the tags panel
174 ChangesetTagsPanel pnlChangesetTags = new ChangesetTagsPanel();
175 tp.add(pnlChangesetTags);
176 model.addPropertyChangeListener(pnlChangesetTags);
177
178 // -- add the panel for the changeset content
179 ChangesetContentPanel pnlChangesetContent = new ChangesetContentPanel();
180 tp.add(pnlChangesetContent);
181 model.addPropertyChangeListener(pnlChangesetContent);
182
183 // -- add the panel for the changeset discussion
184 ChangesetDiscussionPanel pnlChangesetDiscussion = new ChangesetDiscussionPanel();
185 tp.add(pnlChangesetDiscussion);
186 model.addPropertyChangeListener(pnlChangesetDiscussion);
187
188 tp.setTitleAt(0, tr("Properties"));
189 tp.setToolTipTextAt(0, tr("Display the basic properties of the changeset"));
190 tp.setTitleAt(1, tr("Tags"));
191 tp.setToolTipTextAt(1, tr("Display the tags of the changeset"));
192 tp.setTitleAt(2, tr("Content"));
193 tp.setToolTipTextAt(2, tr("Display the objects created, updated, and deleted by the changeset"));
194 tp.setTitleAt(3, tr("Discussion"));
195 tp.setToolTipTextAt(3, tr("Display the public discussion around this changeset"));
196
197 pnl.add(tp, BorderLayout.CENTER);
198 return pnl;
199 }
200
201 /**
202 * builds the content panel of the dialog
203 *
204 * @return the content panel
205 */
206 protected JPanel buildContentPanel() {
207 JPanel pnl = new JPanel(new BorderLayout());
208
209 spContent = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
210 spContent.setLeftComponent(buildChangesetTablePanel());
211 spContent.setRightComponent(buildChangesetDetailPanel());
212 spContent.setOneTouchExpandable(true);
213 spContent.setDividerLocation(0.5);
214
215 pnl.add(spContent, BorderLayout.CENTER);
216 return pnl;
217 }
218
219 /**
220 * Builds the table with actions which can be applied to the currently visible changesets
221 * in the changeset table.
222 *
223 * @return changset actions panel
224 */
225 protected JPanel buildChangesetTableActionPanel() {
226 JPanel pnl = new JPanel(new BorderLayout());
227
228 JToolBar tb = new JToolBar(JToolBar.VERTICAL);
229 tb.setFloatable(false);
230
231 // -- remove from cache action
232 model.getSelectionModel().addListSelectionListener(actRemoveFromCacheAction);
233 tb.add(actRemoveFromCacheAction);
234
235 // -- close selected changesets action
236 model.getSelectionModel().addListSelectionListener(actCloseSelectedChangesetsAction);
237 tb.add(actCloseSelectedChangesetsAction);
238
239 // -- download selected changesets
240 model.getSelectionModel().addListSelectionListener(actDownloadSelectedChangesets);
241 tb.add(actDownloadSelectedChangesets);
242
243 // -- download the content of the selected changesets
244 model.getSelectionModel().addListSelectionListener(actDownloadSelectedContent);
245 tb.add(actDownloadSelectedContent);
246
247 pnl.add(tb, BorderLayout.CENTER);
248 return pnl;
249 }
250
251 /**
252 * Builds the panel with the table of changesets
253 *
254 * @return the panel with the table of changesets
255 */
256 protected JPanel buildChangesetTablePanel() {
257 JPanel pnl = new JPanel(new BorderLayout());
258 tblChangesets = new JTable(
259 model,
260 new ChangesetCacheTableColumnModel(),
261 model.getSelectionModel()
262 );
263 tblChangesets.addMouseListener(new MouseEventHandler());
264 tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "showDetails");
265 tblChangesets.getActionMap().put("showDetails", new ShowDetailAction());
266 model.getSelectionModel().addListSelectionListener(new ChangesetDetailViewSynchronizer());
267
268 // activate DEL on the table
269 tblChangesets.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "removeFromCache");
270 tblChangesets.getActionMap().put("removeFromCache", actRemoveFromCacheAction);
271
272 pnl.add(new JScrollPane(tblChangesets), BorderLayout.CENTER);
273 pnl.add(buildChangesetTableActionPanel(), BorderLayout.WEST);
274 return pnl;
275 }
276
277 protected void build() {
278 setTitle(tr("Changeset Management Dialog"));
279 setIconImage(ImageProvider.get("dialogs/changeset", "changesetmanager").getImage());
280 Container cp = getContentPane();
281
282 cp.setLayout(new BorderLayout());
283
284 buildModel();
285 cp.add(buildToolbarPanel(), BorderLayout.NORTH);
286 cp.add(buildContentPanel(), BorderLayout.CENTER);
287 cp.add(buildButtonPanel(), BorderLayout.SOUTH);
288
289 // the help context
290 HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/ChangesetManager"));
291
292 // make the dialog respond to ESC
293 getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
294 KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancelAndClose");
295 getRootPane().getActionMap().put("cancelAndClose", new CancelAction());
296
297 // install a window event handler
298 addWindowListener(new WindowEventHandler());
299 }
300
301 /**
302 * Constructs a new {@code ChangesetCacheManager}.
303 */
304 public ChangesetCacheManager() {
305 build();
306 }
307
308 @Override
309 public void setVisible(boolean visible) {
310 if (visible) {
311 new WindowGeometry(
312 getClass().getName() + ".geometry",
313 WindowGeometry.centerInWindow(
314 getParent(),
315 new Dimension(1000, 600)
316 )
317 ).applySafe(this);
318 needsSplitPaneAdjustment = true;
319 model.init();
320
321 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
322 model.tearDown();
323 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
324 }
325 super.setVisible(visible);
326 }
327
328 /**
329 * Handler for window events
330 *
331 */
332 class WindowEventHandler extends WindowAdapter {
333 @Override
334 public void windowClosing(WindowEvent e) {
335 new CancelAction().cancelAndClose();
336 }
337
338 @Override
339 public void windowActivated(WindowEvent arg0) {
340 if (needsSplitPaneAdjustment) {
341 spContent.setDividerLocation(0.5);
342 needsSplitPaneAdjustment = false;
343 }
344 }
345 }
346
347 /**
348 * the cancel / close action
349 */
350 static class CancelAction extends AbstractAction {
351 public CancelAction() {
352 putValue(NAME, tr("Close"));
353 putValue(SMALL_ICON, ImageProvider.get("cancel"));
354 putValue(SHORT_DESCRIPTION, tr("Close the dialog"));
355 }
356
357 public void cancelAndClose() {
358 destroyInstance();
359 }
360
361 @Override
362 public void actionPerformed(ActionEvent arg0) {
363 cancelAndClose();
364 }
365 }
366
367 /**
368 * The action to query and download changesets
369 */
370 class QueryAction extends AbstractAction {
371 public QueryAction() {
372 putValue(NAME, tr("Query"));
373 putValue(SMALL_ICON, ImageProvider.get("dialogs", "search"));
374 putValue(SHORT_DESCRIPTION, tr("Launch the dialog for querying changesets"));
375 setEnabled(!Main.isOffline(OnlineResource.OSM_API));
376 }
377
378 @Override
379 public void actionPerformed(ActionEvent evt) {
380 ChangesetQueryDialog dialog = new ChangesetQueryDialog(ChangesetCacheManager.this);
381 dialog.initForUserInput();
382 dialog.setVisible(true);
383 if (dialog.isCanceled())
384 return;
385
386 try {
387 ChangesetQuery query = dialog.getChangesetQuery();
388 if (query == null) return;
389 ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
390 ChangesetCacheManager.getInstance().runDownloadTask(task);
391 } catch (IllegalStateException e) {
392 JOptionPane.showMessageDialog(ChangesetCacheManager.this, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE);
393 }
394 }
395 }
396
397 /**
398 * Removes the selected changesets from the local changeset cache
399 *
400 */
401 class RemoveFromCacheAction extends AbstractAction implements ListSelectionListener{
402 public RemoveFromCacheAction() {
403 putValue(NAME, tr("Remove from cache"));
404 putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
405 putValue(SHORT_DESCRIPTION, tr("Remove the selected changesets from the local cache"));
406 updateEnabledState();
407 }
408
409 @Override
410 public void actionPerformed(ActionEvent arg0) {
411 List<Changeset> selected = model.getSelectedChangesets();
412 ChangesetCache.getInstance().remove(selected);
413 }
414
415 protected void updateEnabledState() {
416 setEnabled(model.hasSelectedChangesets());
417 }
418
419 @Override
420 public void valueChanged(ListSelectionEvent e) {
421 updateEnabledState();
422
423 }
424 }
425
426 /**
427 * Closes the selected changesets
428 *
429 */
430 class CloseSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
431 public CloseSelectedChangesetsAction() {
432 putValue(NAME, tr("Close"));
433 putValue(SMALL_ICON, ImageProvider.get("closechangeset"));
434 putValue(SHORT_DESCRIPTION, tr("Close the selected changesets"));
435 updateEnabledState();
436 }
437
438 @Override
439 public void actionPerformed(ActionEvent arg0) {
440 List<Changeset> selected = model.getSelectedChangesets();
441 Main.worker.submit(new CloseChangesetTask(selected));
442 }
443
444 protected void updateEnabledState() {
445 List<Changeset> selected = model.getSelectedChangesets();
446 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
447 for (Changeset cs: selected) {
448 if (cs.isOpen()) {
449 if (im.isPartiallyIdentified() && cs.getUser() != null && cs.getUser().getName().equals(im.getUserName())) {
450 setEnabled(true);
451 return;
452 }
453 if (im.isFullyIdentified() && cs.getUser() != null && cs.getUser().getId() == im.getUserId()) {
454 setEnabled(true);
455 return;
456 }
457 }
458 }
459 setEnabled(false);
460 }
461
462 @Override
463 public void valueChanged(ListSelectionEvent e) {
464 updateEnabledState();
465 }
466 }
467
468 /**
469 * Downloads the selected changesets
470 *
471 */
472 class DownloadSelectedChangesetsAction extends AbstractAction implements ListSelectionListener{
473 public DownloadSelectedChangesetsAction() {
474 putValue(NAME, tr("Update changeset"));
475 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "updatechangeset"));
476 putValue(SHORT_DESCRIPTION, tr("Updates the selected changesets with current data from the OSM server"));
477 updateEnabledState();
478 }
479
480 @Override
481 public void actionPerformed(ActionEvent arg0) {
482 List<Changeset> selected = model.getSelectedChangesets();
483 ChangesetHeaderDownloadTask task = ChangesetHeaderDownloadTask.buildTaskForChangesets(ChangesetCacheManager.this, selected);
484 ChangesetCacheManager.getInstance().runDownloadTask(task);
485 }
486
487 protected void updateEnabledState() {
488 setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
489 }
490
491 @Override
492 public void valueChanged(ListSelectionEvent e) {
493 updateEnabledState();
494 }
495 }
496
497 /**
498 * Downloads the content of selected changesets from the OSM server
499 *
500 */
501 class DownloadSelectedChangesetContentAction extends AbstractAction implements ListSelectionListener{
502 public DownloadSelectedChangesetContentAction() {
503 putValue(NAME, tr("Download changeset content"));
504 putValue(SMALL_ICON, DOWNLOAD_CONTENT_ICON);
505 putValue(SHORT_DESCRIPTION, tr("Download the content of the selected changesets from the server"));
506 updateEnabledState();
507 }
508
509 @Override
510 public void actionPerformed(ActionEvent arg0) {
511 ChangesetContentDownloadTask task = new ChangesetContentDownloadTask(ChangesetCacheManager.this, model.getSelectedChangesetIds());
512 ChangesetCacheManager.getInstance().runDownloadTask(task);
513 }
514
515 protected void updateEnabledState() {
516 setEnabled(model.hasSelectedChangesets() && !Main.isOffline(OnlineResource.OSM_API));
517 }
518
519 @Override
520 public void valueChanged(ListSelectionEvent e) {
521 updateEnabledState();
522 }
523 }
524
525 class ShowDetailAction extends AbstractAction {
526
527 public void showDetails() {
528 List<Changeset> selected = model.getSelectedChangesets();
529 if (selected.size() != 1) return;
530 model.setChangesetInDetailView(selected.get(0));
531 }
532
533 @Override
534 public void actionPerformed(ActionEvent arg0) {
535 showDetails();
536 }
537 }
538
539 class DownloadMyChangesets extends AbstractAction {
540 public DownloadMyChangesets() {
541 putValue(NAME, tr("My changesets"));
542 putValue(SMALL_ICON, ImageProvider.get("dialogs/changeset", "downloadchangeset"));
543 putValue(SHORT_DESCRIPTION, tr("Download my changesets from the OSM server (max. 100 changesets)"));
544 setEnabled(!Main.isOffline(OnlineResource.OSM_API));
545 }
546
547 protected void alertAnonymousUser() {
548 HelpAwareOptionPane.showOptionDialog(
549 ChangesetCacheManager.this,
550 tr("<html>JOSM is currently running with an anonymous user. It cannot download<br>"
551 + "your changesets from the OSM server unless you enter your OSM user name<br>"
552 + "in the JOSM preferences.</html>"
553 ),
554 tr("Warning"),
555 JOptionPane.WARNING_MESSAGE,
556 HelpUtil.ht("/Dialog/ChangesetManager#CanDownloadMyChangesets")
557 );
558 }
559
560 @Override
561 public void actionPerformed(ActionEvent arg0) {
562 JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
563 if (im.isAnonymous()) {
564 alertAnonymousUser();
565 return;
566 }
567 ChangesetQuery query = new ChangesetQuery();
568 if (im.isFullyIdentified()) {
569 query = query.forUser(im.getUserId());
570 } else {
571 query = query.forUser(im.getUserName());
572 }
573 ChangesetQueryTask task = new ChangesetQueryTask(ChangesetCacheManager.this, query);
574 ChangesetCacheManager.getInstance().runDownloadTask(task);
575 }
576 }
577
578 class MouseEventHandler extends PopupMenuLauncher {
579
580 public MouseEventHandler() {
581 super(new ChangesetTablePopupMenu());
582 }
583
584 @Override
585 public void mouseClicked(MouseEvent evt) {
586 if (isDoubleClick(evt)) {
587 new ShowDetailAction().showDetails();
588 }
589 }
590 }
591
592 class ChangesetTablePopupMenu extends JPopupMenu {
593 public ChangesetTablePopupMenu() {
594 add(actRemoveFromCacheAction);
595 add(actCloseSelectedChangesetsAction);
596 add(actDownloadSelectedChangesets);
597 add(actDownloadSelectedContent);
598 }
599 }
600
601 class ChangesetDetailViewSynchronizer implements ListSelectionListener {
602 @Override
603 public void valueChanged(ListSelectionEvent e) {
604 List<Changeset> selected = model.getSelectedChangesets();
605 if (selected.size() == 1) {
606 model.setChangesetInDetailView(selected.get(0));
607 } else {
608 model.setChangesetInDetailView(null);
609 }
610 }
611 }
612
613 /**
614 * Selects the changesets in <code>changests</code>, provided the
615 * respective changesets are already present in the local changeset cache.
616 *
617 * @param changesets the collection of changesets. If {@code null}, the
618 * selection is cleared.
619 */
620 public void setSelectedChangesets(Collection<Changeset> changesets) {
621 model.setSelectedChangesets(changesets);
622 int idx = model.getSelectionModel().getMinSelectionIndex();
623 if (idx < 0) return;
624 tblChangesets.scrollRectToVisible(tblChangesets.getCellRect(idx, 0, true));
625 repaint();
626 }
627
628 /**
629 * Selects the changesets with the ids in <code>ids</code>, provided the
630 * respective changesets are already present in the local changeset cache.
631 *
632 * @param ids the collection of ids. If null, the selection is cleared.
633 */
634 public void setSelectedChangesetsById(Collection<Integer> ids) {
635 if (ids == null) {
636 setSelectedChangesets(null);
637 return;
638 }
639 Set<Changeset> toSelect = new HashSet<>();
640 ChangesetCache cc = ChangesetCache.getInstance();
641 for (int id: ids) {
642 if (cc.contains(id)) {
643 toSelect.add(cc.get(id));
644 }
645 }
646 setSelectedChangesets(toSelect);
647 }
648
649 /**
650 * Runs the given changeset download task.
651 * @param task The changeset download task to run
652 */
653 public void runDownloadTask(final ChangesetDownloadTask task) {
654 Main.worker.submit(task);
655 Main.worker.submit(new Runnable() {
656 @Override
657 public void run() {
658 if (task.isCanceled() || task.isFailed()) return;
659 setSelectedChangesets(task.getDownloadedChangesets());
660 }
661 });
662 }
663}
Note: See TracBrowser for help on using the repository browser.