source: trunk/src/org/expeditee/gui/FrameGraphics.java@ 1553

Last change on this file since 1553 was 1553, checked in by bnemhaus, 3 years ago

Debugging work. Attempting to isolate the issue experienced on some computers where Text looks to be getting drawn multiple times. My findings so far is that there are instances were lines get drawn multiple times but other than that I haven't been able to find anything. Asking David to run as we know the problem occurs on his laptop.

File size: 21.0 KB
Line 
1/**
2 * FrameGraphics.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.gui;
20
21import java.util.Collection;
22import java.util.Collections;
23import java.util.Comparator;
24import java.util.HashSet;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.ListIterator;
28
29import org.expeditee.core.Clip;
30import org.expeditee.core.Colour;
31import org.expeditee.core.Dimension;
32import org.expeditee.core.Image;
33import org.expeditee.core.bounds.PolygonBounds;
34import org.expeditee.gio.EcosystemManager;
35import org.expeditee.gio.input.KBMInputEvent.Key;
36import org.expeditee.gio.input.StandardInputEventListeners;
37import org.expeditee.items.Circle;
38import org.expeditee.items.Dot;
39import org.expeditee.items.Item;
40import org.expeditee.items.Item.HighlightMode;
41import org.expeditee.items.Line;
42import org.expeditee.items.UserAppliedPermission;
43import org.expeditee.items.XRayable;
44import org.expeditee.items.widgets.Widget;
45import org.expeditee.items.widgets.WidgetEdge;
46import org.expeditee.settings.UserSettings;
47
48public class FrameGraphics {
49
50 // Final passes to rendering the current frame
51 private static LinkedList<FrameRenderPass> _frameRenderPasses = new LinkedList<FrameRenderPass>();
52
53 private static Item _lastToolTippedItem = null;
54
55 private static HashSet<Item> itemsPaintedRecently = new HashSet<Item>();
56
57 /** Static-only class. */
58 private FrameGraphics()
59 {
60 }
61
62 /**
63 * Gets an image of the given frame that has the given dimensions. If clip
64 * is not null, only the areas inside clip are guaranteed to be drawn.
65 */
66 public static Image getFrameImage(Frame toPaint, Clip clip, Dimension size)
67 {
68 return getFrameImage(toPaint, clip, size, true, true);
69 }
70
71 /**
72 * Gets an image of the given frame that has the given dimensions. If clip
73 * is not null, only the areas inside clip are guaranteed to be drawn.
74 */
75 public static Image getFrameImage(Frame toPaint, Clip clip, Dimension size, boolean isActualFrame, boolean createVolatile)
76 {
77 if (toPaint == null) {
78 return null;
79 }
80
81 // the buffer is not valid, so it must be recreated
82 if (!toPaint.isBufferValid()) {
83
84 Image buffer = toPaint.getBuffer();
85 // Get the size if it hasn't been given
86 if (size == null) {
87 // Can't get the size if there is no buffer
88 if (buffer == null) {
89 return null;
90 } else {
91 size = buffer.getSize();
92 }
93 }
94
95 if (buffer == null || !buffer.getSize().equals(size)) {
96 buffer = Image.createImage(size.width, size.height, createVolatile);
97 toPaint.setBuffer(buffer);
98 clip = null;
99 }
100
101 EcosystemManager.getGraphicsManager().pushDrawingSurface(buffer);
102 EcosystemManager.getGraphicsManager().pushClip(clip);
103 paintFrame(toPaint, isActualFrame, createVolatile);
104 EcosystemManager.getGraphicsManager().popDrawingSurface();
105 }
106
107 return toPaint.getBuffer();
108 }
109
110 /** TODO: Comment. cts16 */
111 public static void paintFrame(Frame toPaint, boolean isActualFrame, boolean createVolitile) {
112
113 Browser.debugMessage("FrameGraphics", "paintFrame", "Debug Job: Trying to detect Items being painted multiple times");
114 itemsPaintedRecently = new HashSet<Item>();
115
116
117 Clip clip = EcosystemManager.getGraphicsManager().peekClip();
118
119 // Prepare render passes
120 if (isActualFrame) {
121 for (FrameRenderPass pass : _frameRenderPasses) {
122 clip = pass.paintStarted(clip);
123 }
124 }
125
126 // TODO: Revise images and clip - VERY IMPORTANT
127
128 // Nicer looking lines, but may be too jerky while
129 // rubber-banding on older machines
130 if (UserSettings.AntiAlias.get()) {
131 EcosystemManager.getGraphicsManager().setAntialiasing(true);
132 }
133
134 // If we are doing @f etc... then have a clear background if its the default background color
135 Colour backgroundColor = null;
136
137 // Need to allow transparency for frameImages
138 if (createVolitile) {
139 backgroundColor = toPaint.getPaintBackgroundColor();
140 } else {
141 backgroundColor = toPaint.getBackgroundColor();
142 if (backgroundColor == null) {
143 backgroundColor = Item.TRANSPARENT;
144 }
145 }
146
147 EcosystemManager.getGraphicsManager().clear(backgroundColor);
148
149 List<Item> itemsToPaintCanditates = new LinkedList<Item>();
150 List<Widget> paintWidgets;
151
152 if (isActualFrame) {
153 // Add all the items for this frame and any other from other
154 // frames
155 itemsToPaintCanditates.addAll(toPaint.getAllItems());
156 paintWidgets = toPaint.getAllOverlayWidgets();
157 paintWidgets.addAll(toPaint.getInteractiveWidgets());
158 } else {
159 itemsToPaintCanditates.addAll(toPaint.getVisibleItems());
160 itemsToPaintCanditates.addAll(toPaint.getVectorItems());
161 paintWidgets = toPaint.getInteractiveWidgets();
162 }
163
164 HashSet<Item> paintedFillsAndLines = new HashSet<Item>();
165 // FIRST: Paint widgets swing gui (not expeditee gui) .
166 // Note that these are the anchored widgets
167 ListIterator<Widget> widgetItor = paintWidgets.listIterator(paintWidgets.size());
168 while (widgetItor.hasPrevious()) {
169 // Paint first-in-last-serve ordering - like swing
170 // If it is done the other way around then widgets are covered up by
171 // the box that is supposed to be underneath
172 Widget iw = widgetItor.previous();
173 if (clip == null || clip.getBounds() == null || clip.getBounds().intersects(iw.getBounds())) {
174 paintedFillsAndLines.addAll(iw.getItems());
175 //iw.paint(bg);
176 //PaintItem(bg, iw.getItems().get(4));
177
178 }
179 }
180
181
182 // Filter out items that do not need to be painted
183 List<Item> paintItems;
184 HashSet<Item> fillOnlyItems = null; // only contains items that do
185 // not need drawing but fills
186 // might
187
188 if (clip == null) {
189 paintItems = itemsToPaintCanditates;
190 } else {
191 fillOnlyItems = new HashSet<Item>();
192 paintItems = new LinkedList<Item>();
193 for (Item i : itemsToPaintCanditates) {
194 if (clip == null || i.isInDrawingArea(clip.getBounds())) {
195 paintItems.add(i);
196 } else if (i.isEnclosed()) {
197 // just add all fill items despite possibility of fills
198 // not being in clip
199 // because it will be faster than having to test twice
200 // for fills that do need
201 // repainting.
202 fillOnlyItems.add(i);
203 }
204 }
205 }
206
207 // Only paint files and lines once ... between anchored AND free items
208 PaintPictures(paintItems, fillOnlyItems, paintedFillsAndLines);
209 PaintLines(itemsToPaintCanditates);
210
211
212 widgetItor = paintWidgets.listIterator(paintWidgets.size());
213 while (widgetItor.hasPrevious()) {
214 // Paint first-in-last-serve ordering - like swing
215 // If it is done the other way around then widgets are covered up by
216 // the box that is supposed to be underneath
217 Widget iw = widgetItor.previous();
218 if (clip == null || clip.isNotClipped() || clip.getBounds().intersects(iw.getClip().getBounds())) {
219 iw.paint();
220 PaintItem(iw.getItems().get(4));
221 }
222 }
223
224
225 // Filter out free items that do not need to be painted
226 // This is efficient in cases with animation while free items exist
227
228 List<Item> freeItemsToPaint = new LinkedList<Item>();
229 // Dont paint the free items for the other frame in twin frames mode
230 // if (toPaint == DisplayIO.getCurrentFrame()) {
231 if (clip == null || clip.isNotClipped()) {
232 freeItemsToPaint = FreeItems.getInstance();
233 } else {
234 freeItemsToPaint = new LinkedList<Item>();
235 fillOnlyItems.clear();
236 for (Item i : FreeItems.getInstance()) {
237 if (i.isInDrawingArea(clip.getBounds())) {
238 freeItemsToPaint.add(i);
239 } else if (i.isEnclosed()) {
240 fillOnlyItems.add(i);
241 }
242 }
243 }
244 // }
245
246 if (isActualFrame && toPaint == DisplayController.getCurrentFrame()) {
247 PaintPictures(freeItemsToPaint, fillOnlyItems, paintedFillsAndLines);
248 }
249
250 // TODO if we can get transparency with FreeItems.getInstance()...
251 // then text can be done before freeItems
252 PaintNonLinesNonPicture(paintItems);
253
254 // toPaint.setBufferValid(true);
255
256 if (isActualFrame && !DisplayController.isAudienceMode()) {
257 PaintItem(toPaint.getNameItem());
258 }
259
260 if (DisplayController.isTwinFramesOn()) {
261 List<Item> lines = new LinkedList<Item>();
262 for (Item i : freeItemsToPaint) {
263 if (i instanceof Line) {
264 Line line = (Line) i;
265
266 if (toPaint == DisplayController.getCurrentFrame()) {
267 // If exactly one end of the line is floating...
268
269 if (line.getEndItem().isFloating()
270 ^ line.getStartItem().isFloating()) {
271 // Line l = TransposeLine(line,
272 // line.getEndItem(),
273 // toPaint, 0, 0);
274 // if (l == null)
275 // l = TransposeLine(line,
276 // line.getStartItem(), toPaint, 0, 0);
277 // if (l == null)
278 // l = line;
279 // lines.add(l);
280 } else {
281 // lines.add(line);
282 }
283 } else {
284 // if (line.getEndItem().isFloating()
285 // ^ line.getStartItem().isFloating()) {
286 // lines.add(TransposeLine(line,
287 // line.getEndItem(), toPaint,
288 // FrameMouseActions.getY(), -DisplayIO
289 // .getMiddle()));
290 // lines.add(TransposeLine(line, line
291 // .getStartItem(), toPaint,
292 // FrameMouseActions.getY(), -DisplayIO
293 // .getMiddle()));
294 // }
295 }
296 }
297 }
298
299 if (isActualFrame) {
300 PaintLines(lines);
301 }
302 } else {
303 if (isActualFrame) {
304 PaintLines(freeItemsToPaint);
305 }
306 }
307
308 if (isActualFrame && toPaint == DisplayController.getCurrentFrame()) {
309 PaintNonLinesNonPicture(freeItemsToPaint);
310 }
311
312 // Repaint popups / drags... As well as final passes
313 if (isActualFrame) {
314 for (FrameRenderPass pass : _frameRenderPasses) {
315 pass.paintPreLayeredPanePass();
316 }
317
318 //if (PopupManager.getInstance() != null) PopupManager.getInstance().paintLayeredPane(clip == null ? null : clip.getBounds());
319
320 for (FrameRenderPass pass : _frameRenderPasses) {
321 pass.paintFinalPass();
322 }
323 }
324
325 // paint tooltip
326 if(!FreeItems.hasItemsAttachedToCursor()) {
327 Item current = FrameUtils.getCurrentItem();
328 if(current != null) {
329 current.paintTooltip();
330 }
331 if (_lastToolTippedItem != null) {
332 _lastToolTippedItem.clearTooltips();
333 }
334 _lastToolTippedItem = current;
335 }
336
337 if (FreeItems.hasCursor() && DisplayController.getCursor() == Item.DEFAULT_CURSOR) {
338 PaintNonLinesNonPicture(FreeItems.getCursor());
339 }
340 }
341
342 private static void PaintNonLinesNonPicture(List<Item> toPaint)
343 {
344 for (Item i : toPaint) {
345 if (!(i instanceof Line) && !(i instanceof XRayable)) {
346 PaintItem(i);
347 }
348 }
349 }
350
351 /**
352 * Paint the lines that are not part of an enclosure.
353 *
354 * @param g
355 * @param toPaint
356 */
357 private static void PaintLines(List<Item> toPaint)
358 {
359 // Use this set to keep track of the items that have been painted
360 Collection<Item> done = new HashSet<Item>();
361 for (Item i : toPaint) {
362 if (i instanceof Line) {
363 Line l = (Line) i;
364 if (done.contains(l)) {
365 l.paintArrows();
366 } else {
367 // When painting a line all connected lines are painted too
368 done.addAll(l.getAllConnected());
369 if (l.getStartItem().getEnclosedArea() == 0) {
370 PaintItem(i);
371 }
372 }
373 }
374 }
375 }
376
377 /**
378 * Paint filled areas and their surrounding lines as well as pictures. Note:
379 * floating widgets are painted as fills
380 *
381 * @param g
382 * @param toPaint
383 */
384 private static void PaintPictures(List<Item> toPaint, HashSet<Item> fillOnlyItems, HashSet<Item> done)
385 {
386
387 List<Item> toFill = new LinkedList<Item>();
388
389 for (Item i : toPaint) {
390 // Ignore items that have already been done!
391 // Also ignore invisible items..
392 // TODO possibly ignore invisible items before coming to this method?
393 if (done.contains(i)) {
394 continue;
395 }
396
397 if (i instanceof XRayable) {
398 toFill.add(i);
399 done.addAll(i.getConnected());
400 } else if (i.hasEnclosures()) {
401 for (Item enclosure : i.getEnclosures()) {
402 if (!toFill.contains(enclosure)) {
403 toFill.add(enclosure);
404 }
405 }
406 done.addAll(i.getConnected());
407 } else if (i.isLineEnd() && (!DisplayController.isAudienceMode() || !i.isConnectedToAnnotation())) {
408 toFill.add(i);
409 done.addAll(i.getAllConnected());
410 }
411 }
412
413 if (fillOnlyItems != null) {
414 for (Item i : fillOnlyItems) {
415 if (done.contains(i)) {
416 continue;
417 } else if (!DisplayController.isAudienceMode() || !i.isConnectedToAnnotation()) {
418 toFill.add(i);
419 }
420 done.addAll(i.getAllConnected());
421 }
422 }
423
424 // Sort the items to fill
425 Collections.sort(toFill, new Comparator<Item>() {
426 @Override
427 public int compare(Item a, Item b) {
428 Double aArea = a.getEnclosedArea();
429 Double bArea = b.getEnclosedArea();
430 int cmp = aArea.compareTo(bArea);
431 if (cmp == 0) {
432 // Shapes to the left go underneath
433 PolygonBounds pA = a.getEnclosedShape();
434 PolygonBounds pB = b.getEnclosedShape();
435 if (pA == null || pB == null) {
436 return 0;
437 }
438 return new Integer(pA.getMinX()).compareTo(pB.getMinX());
439 }
440 return cmp * -1;
441 }
442 });
443
444 for (Item i : toFill) {
445 if (i instanceof XRayable) {
446 PaintItem(i);
447 } else {
448 // Paint the fill and lines
449 i.paintFill();
450 List<Line> lines = i.getLines();
451 if (lines.size() > 0) {
452 PaintItem(lines.get(0));
453 }
454 }
455 }
456 }
457
458 /** Displays the given Item on the screen. */
459 public static void PaintItem(Item i)
460 {
461 if (i == null) {
462 return;
463 }
464
465 // do not paint annotation items in audience mode
466 if (!DisplayController.isAudienceMode() || (!i.isConnectedToAnnotation() && !i.isAnnotation()) || i == FrameUtils.getLastEdited()) {
467 i.paint();
468
469 if (itemsPaintedRecently.contains(i)) {
470 Browser.debugMessage("FrameGraphics", "PaintItem", "Repaining Item Between Clears: " + i);
471 } else {
472 itemsPaintedRecently.add(i);
473 }
474 }
475 }
476
477 /**
478 * Highlights an item on the screen Note: All graphics are handled by the
479 * Item itself.
480 *
481 * @param i
482 * The Item to highlight.
483 * @param val
484 * True if the highlighting is being shown, false if it is being
485 * erased.
486 * @return the item that was highlighted
487 */
488 public static Item Highlight(Item i) {
489 if ((i instanceof Line)) {
490 // Check if within 20% of the end of the line
491 Line l = (Line) i;
492 Item toDisconnect = l.getEndPointToDisconnect(DisplayController.getMousePosition());
493
494 // Brook: Widget Edges do not have such a context
495 if (toDisconnect != null && !(i instanceof WidgetEdge)) {
496 Item.HighlightMode newMode = toDisconnect.getHighlightMode();
497 if (FreeItems.hasItemsAttachedToCursor()) {
498 newMode = Item.HighlightMode.Normal;
499 }
500 // unhighlight all the other dots
501 for (Item conn : toDisconnect.getAllConnected()) {
502 conn.setHighlightMode(Item.HighlightMode.None);
503 conn.setHighlightColorToDefault();
504 }
505 l.setHighlightMode(newMode);
506 l.setHighlightColorToDefault();
507 // highlight the dot that will be in disconnect mode
508 toDisconnect.setHighlightMode(newMode);
509 toDisconnect.setHighlightColorToDefault();
510 i = toDisconnect;
511 } else {
512 if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT)) {
513 for(Item j : i.getAllConnected()) {
514 if(j instanceof Dot && !j.equals(i)) {
515 j.setHighlightMode(HighlightMode.None);
516 j.setHighlightColorToDefault();
517 }
518 }
519 l.getStartItem().setHighlightMode(HighlightMode.Connected);
520 l.getStartItem().setHighlightColorToDefault();
521 l.getEndItem().setHighlightMode(HighlightMode.Connected);
522 l.getEndItem().setHighlightColorToDefault();
523 } else {
524 for(Item j : i.getAllConnected()) {
525 if(j instanceof Dot && !j.equals(i)) {
526 j.setHighlightMode(HighlightMode.Connected);
527 j.setHighlightColorToDefault();
528 }
529 }
530 }
531// Collection<Item> connected = i.getAllConnected();
532// for (Item conn : connected) {
533// conn.setHighlightMode(Item.HighlightMode.Connected);
534// }
535 }
536 } else if (i instanceof Circle) {
537 i.setHighlightMode(Item.HighlightMode.Connected);
538 i.setHighlightColorToDefault();
539 } else if (!i.isVisible()) {
540 changeHighlightMode(i, Item.HighlightMode.Connected, null);
541 } else if (i instanceof Dot) {
542 // highlight the dot
543 if (i.hasPermission(UserAppliedPermission.full)) {
544 changeHighlightMode(i, Item.HighlightMode.Normal, Item.HighlightMode.None);
545 } else {
546 changeHighlightMode(i, Item.HighlightMode.Connected, Item.HighlightMode.Connected);
547 }
548 // highlight connected dots, but only if there aren't items being carried on the cursor
549 if(FreeItems.getInstance().size() == 0) {
550 if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.SHIFT)) {
551 for(Item j : i.getAllConnected()) {
552 if(j instanceof Dot && !j.equals(i)) {
553 j.setHighlightMode(HighlightMode.Connected);
554 j.setHighlightColorToDefault();
555 }
556 }
557 } else {
558 for(Item j : i.getAllConnected()) {
559 if(j instanceof Dot && !j.equals(i)) {
560 j.setHighlightMode(HighlightMode.None);
561 j.setHighlightColorToDefault();
562 }
563 }
564 for(Line l : i.getLines()) {
565 Item j = l.getOppositeEnd(i);
566 j.setHighlightMode(HighlightMode.Connected);
567 j.setHighlightColorToDefault();
568 }
569 }
570 }
571 } else {
572 // FrameGraphics.ChangeSelectionMode(i,
573 // Item.SelectedMode.Normal);
574 // For polygons need to make sure all other endpoints are
575 // unHighlighted
576 if (i.hasPermission(UserAppliedPermission.full)) {
577 changeHighlightMode(i, Item.HighlightMode.Normal, Item.HighlightMode.None);
578 } else {
579 changeHighlightMode(i, Item.HighlightMode.Connected, Item.HighlightMode.Connected);
580 }
581 }
582 DisplayController.requestRefresh(true);
583 return i;
584 }
585
586 public static void changeHighlightMode(Item item, Item.HighlightMode newMode)
587 {
588 changeHighlightMode(item, newMode, newMode);
589 }
590
591 public static void changeHighlightMode(Item item, Item.HighlightMode newMode, Item.HighlightMode connectedNewMode)
592 {
593 if (item == null) {
594 return;
595 }
596
597 if (item.hasVector()) {
598 for (Item i : item.getParentOrCurrentFrame().getVectorItems()) {
599 if (i.getEditTarget() == item) {
600 i.setHighlightMode(newMode);
601 i.setHighlightColorToDefault();
602 }
603 }
604 item.setHighlightMode(newMode);
605 item.setHighlightColorToDefault();
606 } else {
607 // Mike: TODO comment on why the line below is used!!
608 // I forgot already!! Oops
609 boolean freeItem = FreeItems.getInstance().contains(item);
610 for (Item i : item.getAllConnected()) {
611 if (/* freeItem || */!FreeItems.getInstance().contains(i)) {
612 i.setHighlightMode(connectedNewMode);
613 i.setHighlightColorToDefault();
614 }
615 }
616 if (!freeItem && newMode != connectedNewMode) {
617 item.setHighlightMode(newMode);
618 item.setHighlightColorToDefault();
619 }
620 }
621 DisplayController.requestRefresh(true);
622 }
623
624 /*
625 *
626 * FrameRenderPass stuff. (TODO: Not sure if this is used for anything? In Apollo. cts16)
627 *
628 */
629
630 /**
631 * Adds a FinalFrameRenderPass to the frame-render pipeline...
632 *
633 * Note that the last added FinalFrameRenderPass will be rendered at the
634 * very top.
635 *
636 * @param pass
637 * The pass to add. If already added then nothing results in the
638 * call.
639 *
640 * @throws NullPointerException
641 * If pass is null.
642 */
643 public static void addFrameRenderPass(FrameRenderPass pass) {
644 if (pass == null) {
645 throw new NullPointerException("pass");
646 }
647
648 if (!_frameRenderPasses.contains(pass)) {
649 _frameRenderPasses.add(pass);
650 }
651 }
652
653 /**
654 * Adds a FinalFrameRenderPass to the frame-render pipeline...
655 *
656 * Note that the last added FinalFrameRenderPass will be rendered at the
657 * very top.
658 *
659 * @param pass
660 * The pass to remove
661 *
662 */
663 public static void removeFrameRenderPass(FrameRenderPass pass) {
664 _frameRenderPasses.remove(pass);
665 }
666
667 /**
668 * A FinalFrameRenderPass is invoked at the very final stages for rendering
669 * a frame: that is, after the popups are drawn.
670 *
671 * There can be many applications for FinalFrameRenderPass. Such as tool
672 * tips, balloons, or drawing items at the highest Z-order in special
673 * situations.
674 *
675 * Although if there are multiples FinalFrameRenderPasses attach to the
676 * frame painter then it is not guaranteed to be rendered very last.
677 *
678 * @see FrameGraphics#addFinalFrameRenderPass(org.expeditee.gui.FrameGraphics.FrameRenderPass)
679 * @see FrameGraphics#removeFinalFrameRenderPass(org.expeditee.gui.FrameGraphics.FrameRenderPass)
680 *
681 * @author Brook Novak
682 */
683 public interface FrameRenderPass {
684
685 /**
686 *
687 * @param currentClip
688 *
689 * @return The clip that the pass should use instead. i.e. if there are
690 * any effects that cannot invalidate prior to paint call.
691 */
692 Clip paintStarted(Clip currentClip);
693
694 void paintFinalPass();
695
696 void paintPreLayeredPanePass();
697 }
698
699}
Note: See TracBrowser for help on using the repository browser.