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

Last change on this file since 1561 was 1561, checked in by davidb, 3 years ago

A set of changes that spans three things: beat detection, time stretching; and a debug class motivated by the need to look at a canvas redraw issue most notable when a waveform widget is playing

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