source: trunk/src/org/expeditee/actions/Misc.java@ 242

Last change on this file since 242 was 242, checked in by ra33, 16 years ago
File size: 22.1 KB
Line 
1package org.expeditee.actions;
2
3import java.awt.Color;
4import java.awt.Image;
5import java.awt.image.BufferedImage;
6import java.awt.image.VolatileImage;
7import java.io.File;
8import java.io.FileNotFoundException;
9import java.io.IOException;
10import java.lang.reflect.Method;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.LinkedList;
14import java.util.List;
15
16import javax.imageio.ImageIO;
17
18import org.expeditee.gui.DisplayIO;
19import org.expeditee.gui.Frame;
20import org.expeditee.gui.FrameGraphics;
21import org.expeditee.gui.FrameIO;
22import org.expeditee.gui.FrameMouseActions;
23import org.expeditee.gui.MessageBay;
24import org.expeditee.gui.TimeKeeper;
25import org.expeditee.importer.FrameDNDTransferHandler;
26import org.expeditee.items.Item;
27import org.expeditee.items.ItemUtils;
28import org.expeditee.items.Text;
29import org.expeditee.math.ExpediteeJEP;
30import org.expeditee.stats.CometStats;
31import org.expeditee.stats.SessionStats;
32import org.expeditee.stats.StatsLogger;
33import org.expeditee.stats.TreeStats;
34import org.nfunk.jep.Node;
35import org.nfunk.jep.ParseException;
36
37/**
38 * A list of miscellaneous Actions and Actions specific to Expeditee
39 *
40 */
41public class Misc {
42 /**
43 * Causes the system to beep
44 */
45 public static void beep() {
46 java.awt.Toolkit.getDefaultToolkit().beep();
47 }
48
49 /**
50 * Forces a repaint of the current Frame
51 */
52 public static void display() {
53 FrameGraphics.refresh(false);
54 }
55
56 /**
57 * Restores the current frame to the last saved version currently on the
58 * hard disk
59 */
60 public static void restore() {
61 FrameIO.Reload();
62 // MessageBay.displayMessage("Restoration complete.");
63 }
64
65 /**
66 * Toggles AudienceMode on or off
67 */
68 public static void toggleAudienceMode() {
69 FrameGraphics.ToggleAudienceMode();
70 }
71
72 /**
73 * Toggles TwinFrames mode on or off
74 */
75 public static void toggleTwinFramesMode() {
76 DisplayIO.ToggleTwinFrames();
77 }
78
79 /**
80 * If the given Item is a Text Item, then the text of the Item is
81 * interpreted as actions, if not this method does nothing.
82 *
83 * @param current
84 * The Item to read the Actions from
85 */
86 public static void runItem(Item current) throws Exception {
87 if (current instanceof Text) {
88 List<String> actions = ((Text) current).getTextList();
89 for (String action : actions) {
90 if (!action.equalsIgnoreCase("runitem")) {
91 Actions.PerformAction(DisplayIO.getCurrentFrame(), current,
92 action);
93 }
94 }
95 } else {
96 MessageBay.errorMessage("Item must be a text item.");
97 }
98 }
99
100 /**
101 * Prompts the user to confirm deletion of the current Frame, and deletes if
102 * the user chooses. After deletion this action calls back(), to ensure the
103 * deleted frame is not still being shown
104 *
105 */
106 public static void DeleteFrame() {
107 Frame toDelete = DisplayIO.getCurrentFrame();
108 String deletedFrame = toDelete.getName();
109 String deletedFrameNameLowercase = deletedFrame.toLowerCase();
110 String errorMessage = "Error deleting " + deletedFrame;
111 try {
112 String deletedFrameName = FrameIO.DeleteFrame(toDelete);
113 if (deletedFrameName != null) {
114 DisplayIO.Back();
115 // Remove any links on the previous frame to the one being
116 // deleted
117 Frame current = DisplayIO.getCurrentFrame();
118 for (Item i : current.getItems())
119 if (i.getLink() != null
120 && i.getAbsoluteLink().toLowerCase().equals(
121 deletedFrameNameLowercase)) {
122 i.setLink(null);
123 }
124 MessageBay.displayMessage(deletedFrame + " renamed "
125 + deletedFrameName);
126 // FrameGraphics.Repaint();
127 return;
128 }
129 } catch (IOException ioe) {
130 if (ioe.getMessage() != null)
131 errorMessage += ". " + ioe.getMessage();
132 } catch (SecurityException se) {
133 if (se.getMessage() != null)
134 errorMessage += ". " + se.getMessage();
135 } catch (Exception e) {
136 e.printStackTrace();
137 }
138 MessageBay.errorMessage(errorMessage);
139 }
140
141 /**
142 * Loads the Frame linked to by the given Item. The first Item on the Frame
143 * that is not the title or name is then placed on the cursor. If the given
144 * Item has no link, or no item is found then this is a no-op.
145 *
146 * @param current
147 * The Item that links to the Frame that the Item will be loaded
148 * from.
149 */
150 public static Item GetItemFromChildFrame(Item current) {
151 return getFromChildFrame(current, false);
152 }
153
154 public static void GetItemsFromChildFrame(Item current) {
155 getItemsFromChildFrame(current, false);
156 }
157
158 /**
159 * Loads the Frame linked to by the given Item. The first Text Item on the
160 * Frame that is not the title or name is then placed on the cursor. If the
161 * given Item has no link, or no item is found then this is a no-op.
162 *
163 * @param current
164 * The Item that links to the Frame that the Item will be loaded
165 * from.
166 */
167 public static Item GetTextFromChildFrame(Item current) {
168 return getFromChildFrame(current, true);
169 }
170
171 private static Item getFromChildFrame(Item current, boolean textOnly) {
172 Item item = getFirstBodyItemOnChildFrame(current, textOnly);
173 // if no item was found
174 if (item != null) {
175 // copy the item and switch
176 item = item.copy();
177 item.setPosition(DisplayIO.getMouseX(), FrameMouseActions.getY());
178 }
179 return item;
180 }
181
182 private static void getItemsFromChildFrame(Item current, boolean textOnly) {
183 Collection<Item> items = getItemsOnChildFrame(current, textOnly);
184 // if no item was found
185 if (items == null || items.size() == 0) {
186 return;
187 }
188
189 // copy the item and switch
190 Collection<Item> copies = ItemUtils.CopyItems(items);
191 Item first = items.iterator().next();
192 float deltaX = DisplayIO.getMouseX() - first.getX();
193 float deltaY = FrameMouseActions.getY() - first.getY();
194 for (Item i : copies) {
195 if (i.isVisible())
196 i.setXY(i.getX() + deltaX, i.getY() + deltaY);
197 i.setParent(null);
198 }
199 FrameMouseActions.pickup(copies);
200 FrameGraphics.Repaint();
201 }
202
203 /**
204 * Sets the given Item to have the Given Color. Color can be null (for
205 * default)
206 *
207 * @param toChange
208 * The Item to set the Color.
209 * @param toUse
210 * The Color to give the Item.
211 */
212 public static void SetItemBackgroundColor(Item toChange, Color toUse) {
213 if (toChange == null)
214 return;
215
216 toChange.setBackgroundColor(toUse);
217 FrameGraphics.Repaint();
218 }
219
220 /**
221 * Sets the given Item to have the Given Color. Color can be null (for
222 * default)
223 *
224 * @param toChange
225 * The Item to set the Color.
226 * @param toUse
227 * The Color to give the Item.
228 */
229 public static void SetItemColor(Item toChange, Color toUse) {
230 if (toChange == null)
231 return;
232
233 toChange.setColor(toUse);
234 FrameGraphics.Repaint();
235 }
236
237 /**
238 * Creates a new Text Object containing general statistics for the current
239 * session. The newly created Text Object is then attached to the cursor via
240 * FrameMouseActions.pickup(Item)
241 */
242 public static void GetSessionStats() {
243 attachStatsToCursor(SessionStats.getCurrentStats());
244 }
245
246 /**
247 * Creates a new Text Object containing statistics for the current tree.
248 */
249 public static String GetCometStats(Frame frame) {
250 TimeKeeper timer = new TimeKeeper();
251 MessageBay.displayMessage("Computing comet stats...");
252 CometStats cometStats = new CometStats(frame);
253 String result = cometStats.toString();
254 MessageBay.overwriteMessage("Comet stats time: "
255 + timer.getElapsedStringSeconds());
256 return result;
257 }
258
259 public static String GetTreeStats(Frame frame) {
260 TimeKeeper timer = new TimeKeeper();
261 MessageBay.displayMessage("Computing tree stats...");
262
263 TreeStats treeStats = new TreeStats(frame);
264 String result = treeStats.toString();
265 MessageBay.overwriteMessage("Tree stats time: "
266 + timer.getElapsedStringSeconds());
267 return result;
268
269 }
270
271 /**
272 * Creates a text item and attaches it to the cursor.
273 *
274 * @param itemText
275 * the text to attach to the cursor
276 */
277 public static void attachStatsToCursor(String itemText) {
278 SessionStats.CreatedText();
279 Frame current = DisplayIO.getCurrentFrame();
280 Item text = current.getStatsTextItem(itemText);
281 FrameMouseActions.pickup(text);
282 FrameGraphics.Repaint();
283 }
284
285 public static void attachTextToCursor(String itemText) {
286 SessionStats.CreatedText();
287 Frame current = DisplayIO.getCurrentFrame();
288 Item text = current.getTextItem(itemText);
289 FrameMouseActions.pickup(text);
290 FrameGraphics.Repaint();
291 }
292
293 /**
294 * Creates a new Text Object containing statistics for moving, deleting and
295 * creating items in the current session. The newly created Text Object is
296 * then attached to the cursor via FrameMouseActions.pickup(Item)
297 */
298 public static String getItemStats() {
299 return SessionStats.getItemStats();
300 }
301
302 /**
303 * Creates a new Text Object containing statistics for the time between
304 * events triggered by the user through mouse clicks and key presses. The
305 * newly created Text Object is then attached to the cursor via
306 * FrameMouseActions.pickup(Item)
307 */
308 public static String getEventStats() {
309 return SessionStats.getEventStats();
310 }
311
312 /**
313 * Creates a new Text Object containing the contents of the current frames
314 * file.
315 */
316 public static String getFrameFile(Frame frame) {
317 return FrameIO.ForceSaveFrame(frame);
318 }
319
320 /**
321 * Creates a new Text Object containing the available fonts.
322 */
323 public static String getFontNames() {
324 Collection<String> availableFonts = Actions.getFonts().values();
325 StringBuilder fontsList = new StringBuilder();
326 for (String s : availableFonts) {
327 fontsList.append(s).append(Text.LINE_SEPARATOR);
328 }
329 fontsList.deleteCharAt(fontsList.length() - 1);
330
331 return fontsList.toString();
332 }
333
334 public static String getUnicodeCharacters(int start, int finish) {
335 if (start < 0 && finish < 0) {
336 throw new RuntimeException("Parameters must be non negative");
337 }
338 // Swap the start and finish if they are inthe wrong order
339 if (start > finish) {
340 start += finish;
341 finish = start - finish;
342 start = start - finish;
343 }
344 StringBuilder charList = new StringBuilder();
345 int count = 0;
346 charList.append(String.format("Unicode block 0x%x - 0x%x", start,
347 finish));
348 System.out.println();
349 // charList.append("Unicode block: ").append(String.format(format,
350 // args))
351 for (char i = (char) start; i < (char) finish; i++) {
352 if (Character.isDefined(i)) {
353 if (count++ % 64 == 0)
354 charList.append(Text.LINE_SEPARATOR);
355 charList.append(Character.valueOf(i));
356 }
357 }
358 return charList.toString();
359 }
360
361 /**
362 * Gets a single block of Unicode characters.
363 *
364 * @param start
365 * the start of the block
366 */
367 public static String getUnicodeCharacters(int start) {
368 return getUnicodeCharacters(start, start + 256);
369 }
370
371 public static String getMathSymbols() {
372 return getUnicodeCharacters('\u2200', '\u2300');
373 }
374
375 /**
376 * Resets the statistics back to zero.
377 */
378 public static void repaint() {
379 StatsLogger.WriteStatsFile();
380 SessionStats.resetStats();
381 }
382
383 /**
384 * Loads a frame with the given name and saves it as a JPEG image.
385 *
386 * @param framename
387 * The name of the Frame to save
388 */
389 public static void jpegFrame(String framename) {
390 ImageFrame(framename, "JPEG");
391 }
392
393 /**
394 * Saves the current frame as a JPEG image. This is the same as calling
395 * JpegFrame(currentFrame.getName())
396 */
397 public static void jpegFrame() {
398 ImageFrame(DisplayIO.getCurrentFrame().getName(), "JPEG");
399 }
400
401 public static void jpgFrame() {
402 jpegFrame();
403 }
404
405 /**
406 * Loads a frame with the given name and saves it as a PNG image.
407 *
408 * @param framename
409 * The name of the Frame to save
410 */
411 public static void PNGFrame(String framename) {
412 ImageFrame(framename, "PNG");
413 }
414
415 /**
416 * Saves the current frame as a PNG image. This is the same as calling
417 * PNGFrame(currentFrame.getName())
418 */
419 public static void PNGFrame() {
420 ImageFrame(DisplayIO.getCurrentFrame().getName(), "PNG");
421 }
422
423 public static String SaveImage(BufferedImage screen, String format,
424 String directory, String fileName) {
425 // Check if we need to append the suffix
426 if (fileName.indexOf('.') < 0)
427 fileName += "." + format.toLowerCase();
428
429 try {
430 // set up the file for output
431 String fullFileName = directory + fileName;
432 File out = new File(fullFileName);
433 if (!out.getParentFile().exists())
434 out.mkdirs();
435
436 // If the image is successfully written out return the fileName
437 if (ImageIO.write(screen, format, out))
438 return fileName;
439
440 } catch (Exception e) {
441 e.printStackTrace();
442 }
443 return null;
444 }
445
446 public static String ImageFrame(Frame frame, String format, String directory) {
447 assert (frame != null);
448
449 Image oldBuffer = frame.getBuffer();
450 frame.setBuffer(null);
451 // Jpeg only works properly with volitile frames
452 // Png transparency only works with bufferedImage form
453 Image frameBuffer = FrameGraphics.getBuffer(frame, false, format
454 .equalsIgnoreCase("jpeg"));
455 // Make sure overlay stuff doesnt disapear on the frame visible on the
456 // screen
457 frame.setBuffer(oldBuffer);
458 BufferedImage screen = null;
459
460 if (frameBuffer instanceof VolatileImage) {
461 // If its the current frame it will be a volitive image
462 screen = ((VolatileImage) frameBuffer).getSnapshot();
463 } else {
464 assert (frameBuffer instanceof BufferedImage);
465 screen = (BufferedImage) frameBuffer;
466 }
467 return SaveImage(screen, format, directory, frame.getExportFileName());
468 }
469
470 /**
471 * Saves the Frame with the given Framename as an image of the given format.
472 *
473 * @param framename
474 * The name of the Frame to save as an image
475 * @param format
476 * The Image format to use (i.e. "PNG", "BMP", etc)
477 */
478 public static void ImageFrame(String framename, String format) {
479 Frame loaded = FrameIO.LoadFrame(framename);
480
481 // if the frame was loaded successfully
482 if (loaded != null) {
483 String path = FrameIO.IMAGES_PATH;
484 String frameName = ImageFrame(loaded, format, path);
485 if (frameName != null)
486 MessageBay.displayMessage("Frame successfully saved to " + path
487 + frameName);
488 else
489 MessageBay.errorMessage("Could not find image writer for "
490 + format + " format");
491 // if the frame was not loaded successfully, alert the user
492 } else {
493 MessageBay.displayMessage("Frame '" + framename
494 + "' could not be found.");
495 }
496 }
497
498 /**
499 * Displays a message in the message box area.
500 *
501 * @param message
502 * the message to display
503 */
504 public static void MessageLn(String message) {
505 MessageBay.displayMessage(message);
506 }
507
508 public static void MessageLn2(String message, String message2) {
509 MessageBay.displayMessage(message + " " + message2);
510 }
511
512 public static void CopyFile(String existingFile, String newFileName) {
513 try {
514 // TODO is there a built in method which will do this faster?
515
516 MessageBay.displayMessage("Copying file " + existingFile + " to "
517 + newFileName + "...");
518 FrameIO.copyFile(existingFile, newFileName);
519 MessageBay.displayMessage("File copied successfully");
520 } catch (FileNotFoundException e) {
521 MessageBay.displayMessage("Error opening file: " + existingFile);
522 } catch (Exception e) {
523 MessageBay.displayMessage("File could not be copied");
524 }
525 }
526
527 /**
528 * Runs two methods alternatively a specified number of times and reports on
529 * the time spent running each method.
530 *
531 * @param fullMethodNameA
532 * @param fullMethodNameB
533 * @param repsPerTest
534 * the number of time each method is run per test
535 * @param tests
536 * the number of tests to conduct
537 *
538 */
539 public static void CompareMethods(String fullMethodNameA,
540 String fullMethodNameB, int repsPerTest, int tests) {
541 try {
542 String classNameA = getClassName(fullMethodNameA);
543 String classNameB = getClassName(fullMethodNameB);
544 String methodNameA = getMethodName(fullMethodNameA);
545 String methodNameB = getMethodName(fullMethodNameB);
546
547 Class<?> classA = Class.forName(classNameA);
548 Class<?> classB = Class.forName(classNameB);
549 Method methodA = classA.getDeclaredMethod(methodNameA,
550 new Class[] {});
551 Method methodB = classB.getDeclaredMethod(methodNameB,
552 new Class[] {});
553 TimeKeeper timeKeeper = new TimeKeeper();
554 long timeA = 0;
555 long timeB = 0;
556 // Run the tests
557 for (int i = 0; i < tests; i++) {
558 // Test methodA
559 timeKeeper.restart();
560 for (int j = 0; j < repsPerTest; j++) {
561 methodA.invoke((Object) null, new Object[] {});
562 }
563 timeA += timeKeeper.getElapsedMillis();
564 timeKeeper.restart();
565 // Test methodB
566 for (int j = 0; j < repsPerTest; j++) {
567 methodB.invoke((Object) null, new Object[] {});
568 }
569 timeB += timeKeeper.getElapsedMillis();
570 }
571
572 float aveTimeA = timeA * 1000F / repsPerTest / tests;
573 float aveTimeB = timeB * 1000F / repsPerTest / tests;
574 // Display Results
575 MessageBay.displayMessage("Average Execution Time");
576 MessageBay.displayMessage(methodNameA + ": "
577 + TimeKeeper.Formatter.format(aveTimeA) + "us");
578 MessageBay.displayMessage(methodNameB + ": "
579 + TimeKeeper.Formatter.format(aveTimeB) + "us");
580 } catch (Exception e) {
581 MessageBay.errorMessage(e.getClass().getSimpleName() + ": "
582 + e.getMessage());
583 }
584 }
585
586 public static String getClassName(String fullMethodName) {
587 assert (fullMethodName != null);
588 assert (fullMethodName.length() > 0);
589 int lastPeriod = fullMethodName.lastIndexOf('.');
590 if (lastPeriod > 0 && lastPeriod < fullMethodName.length() - 1)
591 return fullMethodName.substring(0, lastPeriod);
592 throw new RuntimeException("Invalid method name: " + fullMethodName);
593 }
594
595 public static String getMethodName(String methodName) {
596 assert (methodName != null);
597 assert (methodName.length() > 0);
598 int lastPeriod = methodName.lastIndexOf('.');
599 if (lastPeriod > 0 && lastPeriod < methodName.length() - 1)
600 return methodName.substring(1 + lastPeriod);
601 throw new RuntimeException("Invalid method name: " + methodName);
602 }
603
604 /**
605 * Loads the Frame linked to by the given Item. The first Item on the Frame
606 * that is not the title or name is then placed on the current frame. The
607 * item that was clicked on is placed on the frame it was linked to and the
608 * link is switched to the item from the child frame. If the given Item has
609 * no link, or no item is found then this is a no-op.
610 *
611 * @param current
612 * The Item that links to the Frame that the Item will be loaded
613 * from.
614 */
615 public static void SwapItemWithItemOnChildFrame(Item current) {
616 Item item = getFirstBodyItemOnChildFrame(current, false);
617 // if no item was found
618 if (item == null) {
619 return;
620 }
621
622 // swap the items parents
623 Frame parentFrame = current.getParent();
624 Frame childFrame = item.getParent();
625 current.setParent(childFrame);
626 item.setParent(parentFrame);
627
628 // swap the items on the frames
629 parentFrame.removeItem(current);
630 childFrame.removeItem(item);
631 parentFrame.addItem(item);
632 childFrame.addItem(current);
633
634 // swap the items links
635 item.setActions(current.getAction());
636 item.setLink(childFrame.getName());
637 current.setLink(parentFrame.getName());
638 // current.setLink(null);
639 current.setActions(null);
640
641 FrameGraphics.Repaint();
642 }
643
644 private static Item getFirstBodyItemOnChildFrame(Item current,
645 boolean textOnly) {
646 // the item must link to a frame
647 if (current.getLink() == null) {
648 MessageBay
649 .displayMessage("Cannot get item from child - this item has no link");
650 return null;
651 }
652
653 Frame child = FrameIO.LoadFrame(current.getAbsoluteLink());
654
655 // if the frame could not be loaded
656 if (child == null) {
657 MessageBay.errorMessage("Could not load child frame.");
658 return null;
659 }
660
661 // find the first non-title and non-name item
662 List<Item> body = new ArrayList<Item>();
663 if (textOnly)
664 body.addAll(child.getBodyTextItems(false));
665 else
666 body.addAll(child.getItems());
667 Item item = null;
668
669 for (Item i : body)
670 if (i != child.getTitleItem() && !i.isAnnotation()) {
671 item = i;
672 break;
673 }
674
675 // if no item was found
676 if (item == null) {
677 MessageBay.displayMessage("No item found to copy");
678 return null;
679 }
680
681 return item;
682 }
683
684 private static Collection<Item> getItemsOnChildFrame(Item current,
685 boolean textOnly) {
686 // the item must link to a frame
687 if (current.getLink() == null) {
688 MessageBay
689 .displayMessage("Cannot get item from child - this item has no link");
690 return null;
691 }
692 Frame child = FrameIO.LoadFrame(current.getAbsoluteLink());
693
694 // if the frame could not be loaded
695 if (child == null) {
696 MessageBay.errorMessage("Could not load child frame.");
697 return null;
698 }
699
700 // find the first non-title and non-name item
701 Collection<Item> body = new ArrayList<Item>();
702 if (textOnly)
703 body.addAll(child.getBodyTextItems(false));
704 else
705 body.addAll(child.getItems());
706
707 return body;
708 }
709
710 public static void calculate(Frame frame, Item toCalculate) {
711 if (toCalculate instanceof Text) {
712 Text text = (Text) toCalculate;
713 ExpediteeJEP myParser = new ExpediteeJEP();
714 myParser.addVariables(frame);
715 String linkedFrame = toCalculate.getAbsoluteLink();
716 if (linkedFrame != null) {
717 myParser.addVariables(FrameIO.LoadFrame(linkedFrame));
718 }
719 myParser.resetObserver();
720
721 // Do the calculation
722 String formulaFullCase = text.getText().replace('\n', ' ');
723 String formula = formulaFullCase.toLowerCase();
724
725 try {
726 Node node = myParser.parse(formula);
727 Object result = myParser.evaluate(node);
728 text.setText(result.toString());
729 text.setFormula(formulaFullCase);
730 if (text.isFloating()) {
731 text.setPosition(FrameMouseActions.MouseX,
732 FrameMouseActions.MouseY);
733 FrameMouseActions.resetOffset();
734 } else {
735 text.getParentOrCurrentFrame().change();
736 }
737 } catch (ParseException e) {
738 MessageBay.errorMessage("Parse error "
739 + e.getMessage().replace("\n", ""));
740 } catch (Exception e) {
741 MessageBay.errorMessage("evaluation error "
742 + e.getMessage().replace("\n", ""));
743 e.printStackTrace();
744 }
745 }
746 }
747
748 /**
749 * Attach an item to the cursor.
750 *
751 * @param item
752 */
753 public static void attachToCursor(Item item) {
754 item.setParent(null);
755 FrameMouseActions.pickup(item);
756 FrameGraphics.Repaint();
757 }
758
759 public static void importFile(Item item) {
760 List<File> files = new LinkedList<File>();
761 for (String s : item.getText().split("\n")) {
762 File file = new File(s);
763 if(file.exists()){
764 files.add(file);
765 }
766 }
767 try {
768 FrameDNDTransferHandler.getInstance().importFileList(files,
769 FrameMouseActions.getPosition());
770 } catch (Exception e) {
771 }
772 }
773
774 public static void importFiles(Item item) {
775 importFile(item);
776 }
777}
Note: See TracBrowser for help on using the repository browser.