TreeLogger is now an abstract class that supports HelpInfo; this currently allows a client to specify a URL to open that will provide more info.

Review by: jat, bruce


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2313 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/TreeLogger.java b/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
index 44055d9..bf7982b 100644
--- a/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
+++ b/dev/core/src/com/google/gwt/core/ext/TreeLogger.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
  * 
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
@@ -15,16 +15,33 @@
  */
 package com.google.gwt.core.ext;
 
+import java.net.URL;
+
 /**
  * An interface used to log messages in deferred binding generators.
  */
-public interface TreeLogger {
+public abstract class TreeLogger {
+
+  /**
+   * Provides extra information to the user, generally details of what caused
+   * the problem or what the user should do to fix the problem. How this
+   * information is interpreted and displayed is implementation-dependent.
+   */
+  public abstract static class HelpInfo {
+    /**
+     * If non-null, provides a URL containing extra information about the
+     * problem.
+     */
+    public URL getURL() {
+      return null;
+    }
+  }
 
   /**
    * A type-safe enum of all possible logging severity types.
    */
   @SuppressWarnings("hiding")
-  enum Type {
+  public enum Type {
 
     /**
      * Logs an error.
@@ -116,45 +133,47 @@
   /**
    * Logs an error.
    */
-  Type ERROR = Type.ERROR;
+  public static final Type ERROR = Type.ERROR;
 
   /**
    * Logs a warning.
    */
-  Type WARN = Type.WARN;
+  public static final Type WARN = Type.WARN;
 
   /**
    * Logs information.
    */
-  Type INFO = Type.INFO;
+  public static final Type INFO = Type.INFO;
 
   /**
    * Logs information related to lower-level operation.
    */
-  Type TRACE = Type.TRACE;
+  public static final Type TRACE = Type.TRACE;
 
   /**
    * Logs detailed information that could be useful during debugging.
    */
-  Type DEBUG = Type.DEBUG;
+  public static final Type DEBUG = Type.DEBUG;
 
   /**
    * Logs extremely verbose and detailed information that is typically useful
    * only to product implementors.
    */
-  Type SPAM = Type.SPAM;
+  public static final Type SPAM = Type.SPAM;
 
   /**
    * Logs everything -- quite a bit of stuff.
    */
-  Type ALL = Type.ALL;
+  public static final Type ALL = Type.ALL;
 
   /**
    * A valid logger that ignores all messages. Occasionally useful when calling
    * methods that require a logger parameter.
    */
-  TreeLogger NULL = new TreeLogger() {
-    public TreeLogger branch(Type type, String msg, Throwable caught) {
+  public static final TreeLogger NULL = new TreeLogger() {
+    @Override
+    public TreeLogger branch(Type type, String msg, Throwable caught,
+        HelpInfo helpInfo) {
       return this;
     }
 
@@ -162,12 +181,23 @@
       return false;
     }
 
-    public void log(Type type, String msg, Throwable caught) {
+    @Override
+    public void log(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
       // nothing
     }
   };
 
   /**
+   * Calls
+   * {@link #branch(com.google.gwt.core.ext.TreeLogger.Type, String, Throwable, com.google.gwt.core.ext.TreeLogger.HelpInfo)}
+   * with a <code>null</code> <code>helpInfo</code>.
+   */
+  public final TreeLogger branch(TreeLogger.Type type, String msg,
+      Throwable caught) {
+    return branch(type, msg, caught, null);
+  }
+
+  /**
    * Produces a branched logger, which can be used to write messages that are
    * logically grouped together underneath the current logger. The details of
    * how/if the resulting messages are displayed is implementation-dependent.
@@ -191,32 +221,49 @@
    * </p>
    * 
    * @param type
-   * @param msg An optional message to log, which can be <code>null</code> if
+   * @param msg an optional message to log, which can be <code>null</code> if
    *          only an exception is being logged
-   * @param caught An optional exception to log, which can be <code>null</code>
+   * @param caught an optional exception to log, which can be <code>null</code>
    *          if only a message is being logged
+   * @param helpInfo extra information that might be used by the logger to
+   *          provide extended information to the user
    * @return an instance of {@link TreeLogger} representing the new branch of
-   *         the log. May be the same instance on which this method is called
+   *         the log; may be the same instance on which this method is called
    */
-  TreeLogger branch(TreeLogger.Type type, String msg, Throwable caught);
+  public abstract TreeLogger branch(TreeLogger.Type type, String msg,
+      Throwable caught, HelpInfo helpInfo);
 
   /**
    * Determines whether or not a log entry of the specified type would actually
    * be logged. Caller use this method to avoid constructing log messages that
    * would be thrown away.
    */
-  boolean isLoggable(TreeLogger.Type type);
+  public abstract boolean isLoggable(TreeLogger.Type type);
 
   /**
-   * Logs a message and/or an exception. It is also legal to call this method
-   * using <code>null</code> arguments for <i>both</i> <code>msg</code> and
-   * <code>caught</code>, in which case the log event can be ignored.
+   * Calls
+   * {@link #log(com.google.gwt.core.ext.TreeLogger.Type, String, Throwable, com.google.gwt.core.ext.TreeLogger.HelpInfo)
+   * with a <code>null</code> <code>helpInfo</code>.
+   */
+  public final void log(TreeLogger.Type type, String msg, Throwable caught) {
+    log(type, msg, caught, null);
+  }
+
+  /**
+   * Logs a message and/or an exception, with optional help info. It is also
+   * legal to call this method using <code>null</code> arguments for <i>both</i>
+   * <code>msg</code> and <code>caught</code>, in which case the log event
+   * can be ignored. The <code>info</code> can provide extra information to
+   * the logger; a logger may choose to ignore this info.
    * 
    * @param type
-   * @param msg An optional message to log, which can be <code>null</code> if
+   * @param msg an optional message to log, which can be <code>null</code> if
    *          only an exception is being logged
-   * @param caught An optional exception to log, which can be <code>null</code>
+   * @param caught an optional exception to log, which can be <code>null</code>
    *          if only a message is being logged
+   * @param helpInfo extra information that might be used by the logger to
+   *          provide extended information to the user
    */
-  void log(TreeLogger.Type type, String msg, Throwable caught);
+  public abstract void log(TreeLogger.Type type, String msg, Throwable caught,
+      HelpInfo helpInfo);
 }
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
index 1d5448c..9fafca9 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
@@ -153,8 +153,7 @@
     }
   }
 
-  static void launchExternalBrowser(TreeLogger logger, String location) {
-
+  public static void launchExternalBrowser(TreeLogger logger, String location) {
     // check GWT_EXTERNAL_BROWSER first, it overrides everything else
     LowLevel.init();
     String browserCmd = LowLevel.getEnv("GWT_EXTERNAL_BROWSER");
diff --git a/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java
index 0d2b4ab..a36267b 100644
--- a/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java
@@ -27,7 +27,7 @@
  * A {@link TreeLogger} implementation that can be used during JUnit tests to
  * check for a specified sequence of log events.
  */
-public class UnitTestTreeLogger implements TreeLogger {
+public class UnitTestTreeLogger extends TreeLogger {
 
   /**
    * Simplifies the creation of a {@link UnitTestTreeLogger} by providing
@@ -168,7 +168,8 @@
     Assert.assertEquals("Logs do not match", expected, actual);
   }
 
-  public TreeLogger branch(Type type, String msg, Throwable caught) {
+  public TreeLogger branch(Type type, String msg, Throwable caught,
+      HelpInfo helpInfo) {
     log(type, msg, caught);
     return this;
   }
@@ -177,7 +178,7 @@
     return loggableTypes.contains(type);
   }
 
-  public void log(Type type, String msg, Throwable caught) {
+  public void log(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
     if (!isLoggable(type)) {
       return;
     }
diff --git a/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
index 9c9a742..af2f497 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/AbstractTreeLogger.java
@@ -23,26 +23,28 @@
 /**
  * Abstract base class for TreeLoggers.
  */
-public abstract class AbstractTreeLogger implements TreeLogger {
-
-  // This message is package-protected so that the unit test can access it.
-  static final String OUT_OF_MEMORY_MSG = "Out of memory; to increase the "
-      + "amount of memory, use the -Xmx flag at startup (java -Xmx128M ...)";
+public abstract class AbstractTreeLogger extends TreeLogger {
 
   private static class UncommittedBranchData {
 
     public final Throwable caught;
-
     public final String message;
     public final TreeLogger.Type type;
+    private final HelpInfo helpInfo;
 
-    public UncommittedBranchData(Type type, String message, Throwable exception) {
-      caught = exception;
+    public UncommittedBranchData(Type type, String message, Throwable caught,
+        HelpInfo helpInfo) {
+      this.caught = caught;
       this.message = message;
       this.type = type;
+      this.helpInfo = helpInfo;
     }
   }
 
+  // This message is package-protected so that the unit test can access it.
+  static final String OUT_OF_MEMORY_MSG = "Out of memory; to increase the "
+      + "amount of memory, use the -Xmx flag at startup (java -Xmx128M ...)";
+
   public static String getStackTraceAsString(Throwable e) {
     // For each cause, print the requested number of entries of its stack trace,
     // being careful to avoid getting stuck in an infinite loop.
@@ -120,7 +122,7 @@
    * branched loggers.
    */
   public final synchronized TreeLogger branch(TreeLogger.Type type, String msg,
-      Throwable caught) {
+      Throwable caught, HelpInfo helpInfo) {
 
     if (msg == null) {
       msg = "(Null branch message)";
@@ -148,7 +150,8 @@
     // child (or grandchild) tries to log something that is loggable,
     // in which case there will be cascading commits of the parent branches.
     //
-    childLogger.uncommitted = new UncommittedBranchData(type, msg, caught);
+    childLogger.uncommitted = new UncommittedBranchData(type, msg, caught,
+        helpInfo);
 
     // This logic is intertwined with log(). If a log message is associated
     // with an out-of-memory condition, then we turn it into a branch,
@@ -191,7 +194,7 @@
    * parent branches may be lazily created before the log can take place.
    */
   public final synchronized void log(TreeLogger.Type type, String msg,
-      Throwable caught) {
+      Throwable caught, HelpInfo helpInfo) {
 
     if (msg == null) {
       msg = "(Null log message)";
@@ -207,14 +210,14 @@
     int childIndex = allocateNextChildIndex();
     if (isLoggable(type)) {
       commitMyBranchEntryInMyParentLogger();
-      doLog(childIndex, type, msg, caught);
+      doLog(childIndex, type, msg, caught, helpInfo);
     }
   }
 
   /**
    * @param type the log type representing the most detailed level of logging
-   *            that the caller is interested in, or <code>null</code> to
-   *            choose the default level.
+   *          that the caller is interested in, or <code>null</code> to choose
+   *          the default level.
    */
   public final synchronized void setMaxDetail(TreeLogger.Type type) {
     if (type == null) {
@@ -234,20 +237,42 @@
   protected abstract AbstractTreeLogger doBranch();
 
   /**
+   * @deprecated This method has been deprecated; override
+   *             {@link #doCommitBranch(AbstractTreeLogger, com.google.gwt.core.ext.TreeLogger.Type, String, Throwable, com.google.gwt.core.ext.TreeLogger.HelpInfo)}
+   *             instead.
+   */
+  @Deprecated
+  @SuppressWarnings("unused")
+  protected final void doCommitBranch(AbstractTreeLogger childBeingCommitted,
+      TreeLogger.Type type, String msg, Throwable caught) {
+  }
+
+  /**
    * Derived classes should override this method to actually commit the
    * specified message associated with this the root of this branch.
    */
   protected abstract void doCommitBranch(
       AbstractTreeLogger childBeingCommitted, TreeLogger.Type type, String msg,
-      Throwable caught);
+      Throwable caught, HelpInfo helpInfo);
 
   /**
-   * Dervied classes should override this method to actually write a log
+   * @deprecated This method has been deprecated; override
+   *             {@link #branch(com.google.gwt.core.ext.TreeLogger.Type, String, Throwable, com.google.gwt.core.ext.TreeLogger.HelpInfo)
+   *             instead.
+   */
+  @Deprecated
+  @SuppressWarnings("unused")
+  protected final void doLog(int indexOfLogEntryWithinParentLogger,
+      TreeLogger.Type type, String msg, Throwable caught) {
+  }
+
+  /**
+   * Derived classes should override this method to actually write a log
    * message. Note that {@link #isLoggable(TreeLogger.Type)} will have already
    * been called.
    */
   protected abstract void doLog(int indexOfLogEntryWithinParentLogger,
-      TreeLogger.Type type, String msg, Throwable caught);
+      TreeLogger.Type type, String msg, Throwable caught, HelpInfo helpInfo);
 
   private int allocateNextChildIndex() {
     synchronized (nextChildIndexLock) {
@@ -291,7 +316,7 @@
         // Let the subclass do its thing to commit this branch.
         //
         parent.doCommitBranch(this, uncommitted.type, uncommitted.message,
-            uncommitted.caught);
+            uncommitted.caught, uncommitted.helpInfo);
 
         // Release the uncommitted state.
         //
diff --git a/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
index 6460745..cdd7843 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/PrintWriterTreeLogger.java
@@ -16,16 +16,17 @@
 package com.google.gwt.dev.util.log;
 
 import java.io.PrintWriter;
+import java.net.URL;
 
 /**
  * Tree logger that logs to a print writer.
  */
 public final class PrintWriterTreeLogger extends AbstractTreeLogger {
 
-  private final PrintWriter out;
-
   private final String indent;
 
+  private final PrintWriter out;
+
   public PrintWriterTreeLogger() {
     this(new PrintWriter(System.out, true));
   }
@@ -43,13 +44,15 @@
     return new PrintWriterTreeLogger(out, indent + "   ");
   }
 
+  @Override
   protected void doCommitBranch(AbstractTreeLogger childBeingCommitted,
-      Type type, String msg, Throwable caught) {
-    doLog(childBeingCommitted.getBranchedIndex(), type, msg, caught);
+      Type type, String msg, Throwable caught, HelpInfo helpInfo) {
+    doLog(childBeingCommitted.getBranchedIndex(), type, msg, caught, helpInfo);
   }
 
+  @Override
   protected void doLog(int indexOfLogEntryWithinParentLogger, Type type,
-      String msg, Throwable caught) {
+      String msg, Throwable caught, HelpInfo helpInfo) {
     out.print(indent);
     if (type.needsAttention()) {
       out.print("[");
@@ -58,6 +61,13 @@
     }
 
     out.println(msg);
+    if (helpInfo != null) {
+      URL url = helpInfo.getURL();
+      if (url != null) {
+        out.print(indent);
+        out.println("For additional info see: " + url.toString());
+      }
+    }
     if (caught != null) {
       caught.printStackTrace(out);
     }
diff --git a/dev/core/src/com/google/gwt/dev/util/log/ThreadLocalTreeLoggerProxy.java b/dev/core/src/com/google/gwt/dev/util/log/ThreadLocalTreeLoggerProxy.java
index 8b6f218..cd3d510 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/ThreadLocalTreeLoggerProxy.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/ThreadLocalTreeLoggerProxy.java
@@ -24,7 +24,7 @@
  * can be useful for situations where it is not practical to pass in a logger as
  * a parameter, such as when interfacing with third-party classes.
  */
-public final class ThreadLocalTreeLoggerProxy implements TreeLogger {
+public final class ThreadLocalTreeLoggerProxy extends TreeLogger {
 
   private static final ThreadLocal<TreeLogger> perThreadLogger = new ThreadLocal<TreeLogger>();
 
@@ -40,10 +40,12 @@
    * Delegates the branch to the thread-local logger if one is present.
    * Otherwise, the log entry is discarded and <code>this</code> is returned.
    */
-  public TreeLogger branch(Type type, String msg, Throwable caught) {
+  @Override
+  public TreeLogger branch(Type type, String msg, Throwable caught,
+      HelpInfo helpInfo) {
     TreeLogger logger = perThreadLogger.get();
     if (logger != null) {
-      return logger.branch(type, msg, caught);
+      return logger.branch(type, msg, caught, helpInfo);
     } else {
       return this;
     }
@@ -68,10 +70,11 @@
    * Delegates the log to the thread-local logger if one is present. Otherwise,
    * the log entry is discarded.
    */
-  public void log(Type type, String msg, Throwable caught) {
+  @Override
+  public void log(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
     TreeLogger logger = perThreadLogger.get();
     if (logger != null) {
-      logger.log(type, msg, caught);
+      logger.log(type, msg, caught, helpInfo);
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/util/log/TreeItemLogger.java b/dev/core/src/com/google/gwt/dev/util/log/TreeItemLogger.java
index 8523db8..2c48f88 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/TreeItemLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/TreeItemLogger.java
@@ -26,6 +26,7 @@
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URL;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -42,6 +43,8 @@
   public static class LogEvent {
     public final Throwable caught;
 
+    public final HelpInfo helpInfo;
+
     public final int index;
 
     public final boolean isBranchCommit;
@@ -53,13 +56,14 @@
     public final TreeLogger.Type type;
 
     public LogEvent(TreeItemLogger logger, boolean isBranchCommit, int index,
-        Type type, String message, Throwable caught) {
+        Type type, String message, Throwable caught, HelpInfo helpInfo) {
       this.logger = logger;
       this.isBranchCommit = isBranchCommit;
       this.index = index;
       this.type = type;
       this.message = message;
       this.caught = caught;
+      this.helpInfo = helpInfo;
     }
 
     @Override
@@ -167,6 +171,19 @@
       }
       treeItem.setText(label);
 
+      if (helpInfo != null) {
+        URL url = helpInfo.getURL();
+        if (url != null) {
+          TreeItem helpItem = new TreeItem(treeItem, SWT.NONE);
+          helpItem.setImage(imageLink);
+          helpItem.setText("More info: " + url.toString());
+          helpItem.setForeground(helpItem.getDisplay().getSystemColor(
+              SWT.COLOR_BLUE));
+          helpItem.setData(helpInfo);
+          treeItem.setExpanded(true);
+        }
+      }
+
       // This LogEvent object becomes the tree item's custom data.
       //
       treeItem.setData(this);
@@ -306,6 +323,7 @@
   private static final Image imageDebug = tryLoadImage("log-item-debug.gif");
   private static final Image imageError = tryLoadImage("log-item-error.gif");
   private static final Image imageInfo = tryLoadImage("log-item-info.gif");
+  private static final Image imageLink = tryLoadImage("log-link.gif");
   private static final Image imageSpam = tryLoadImage("log-item-spam.gif");
   private static final Image imageTrace = tryLoadImage("log-item-trace.gif");
   private static final Image imageWarning = tryLoadImage("log-item-warning.gif");
@@ -374,24 +392,25 @@
 
   @Override
   protected void doCommitBranch(AbstractTreeLogger childBeingCommitted,
-      Type type, String msg, Throwable caught) {
+      Type type, String msg, Throwable caught, HelpInfo helpInfo) {
     if (isLoggerDead()) {
       return;
     }
 
     TreeItemLogger commitChild = (TreeItemLogger) childBeingCommitted;
     sharedPendingUpdates.add(new LogEvent(commitChild, true,
-        commitChild.getBranchedIndex(), type, msg, caught));
+        commitChild.getBranchedIndex(), type, msg, caught, helpInfo));
   }
 
   @Override
-  protected void doLog(int index, TreeLogger.Type type, String msg,
-      Throwable caught) {
+  protected void doLog(int index, Type type, String msg, Throwable caught,
+      HelpInfo helpInfo) {
     if (isLoggerDead()) {
       return;
     }
 
-    sharedPendingUpdates.add(new LogEvent(this, false, index, type, msg, caught));
+    sharedPendingUpdates.add(new LogEvent(this, false, index, type, msg,
+        caught, helpInfo));
   }
 
   /**
diff --git a/dev/core/src/com/google/gwt/dev/util/log/TreeLoggerWidget.java b/dev/core/src/com/google/gwt/dev/util/log/TreeLoggerWidget.java
index 3d8ca6a..d21aa6e 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/TreeLoggerWidget.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/TreeLoggerWidget.java
@@ -16,6 +16,8 @@
 package com.google.gwt.dev.util.log;
 
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.TreeLogger.HelpInfo;
+import com.google.gwt.dev.shell.BrowserWidget;
 import com.google.gwt.dev.util.log.TreeItemLogger.LogEvent;
 
 import org.eclipse.swt.SWT;
@@ -27,6 +29,8 @@
 import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.events.KeyAdapter;
 import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
 import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.events.SelectionListener;
 import org.eclipse.swt.events.TreeEvent;
@@ -41,6 +45,7 @@
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.net.URL;
 
 /**
  * SWT widget containing a tree logger.
@@ -70,14 +75,24 @@
     tree = new Tree(sash, SWT.BORDER | SWT.SHADOW_IN);
     tree.setLinesVisible(false);
     tree.addSelectionListener(this);
+    tree.addMouseListener(new MouseAdapter() {
+      public void mouseDoubleClick(MouseEvent e) {
+        openHelpOnSelection(tree);
+      }
+    });
     tree.setFocus();
     tree.addKeyListener(new KeyAdapter() {
       @Override
       public void keyPressed(KeyEvent e) {
-        if (e.keyCode == 'c' && e.stateMask == SWT.CTRL) {
-          // Copy subtree to clipboard.
-          //
-          copyTreeSelectionToClipboard(tree);
+        switch (e.keyCode) {
+          case 'c':
+            if (e.stateMask == SWT.CTRL) {
+              copyTreeSelectionToClipboard(tree);
+            }
+            break;
+          case '\r':
+          case '\n':
+            openHelpOnSelection(tree);
         }
       }
     });
@@ -200,6 +215,20 @@
     cb.setContents(cbText, cbFormat);
   }
 
+  protected void openHelpOnSelection(Tree tree) {
+    TreeItem[] selected = tree.getSelection();
+    for (TreeItem item : selected) {
+      Object itemData = item.getData();
+      if (itemData instanceof HelpInfo) {
+        HelpInfo helpInfo = (HelpInfo) itemData;
+        URL url = helpInfo.getURL();
+        if (url != null) {
+          BrowserWidget.launchExternalBrowser(logger, url.toString());
+        }
+      }
+    }
+  }
+
   private void initLogFlushTimer(final Display display) {
     final int flushDelay = 1000;
 
diff --git a/dev/core/src/com/google/gwt/dev/util/log/log-link.gif b/dev/core/src/com/google/gwt/dev/util/log/log-link.gif
new file mode 100644
index 0000000..c23734e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/log-link.gif
Binary files differ
diff --git a/dev/core/test/com/google/gwt/dev/shell/StandardGeneratorContextTest.java b/dev/core/test/com/google/gwt/dev/shell/StandardGeneratorContextTest.java
index f88a339..090a361 100644
--- a/dev/core/test/com/google/gwt/dev/shell/StandardGeneratorContextTest.java
+++ b/dev/core/test/com/google/gwt/dev/shell/StandardGeneratorContextTest.java
@@ -112,20 +112,6 @@
 
   }
 
-  private static class MockTreeLogger implements TreeLogger {
-
-    public TreeLogger branch(Type type, String msg, Throwable caught) {
-      return this;
-    }
-
-    public boolean isLoggable(Type type) {
-      return false;
-    }
-
-    public void log(Type type, String msg, Throwable caught) {
-    }
-  }
-
   private static class MockTypeOracle extends TypeOracle {
   }
 
@@ -142,7 +128,7 @@
   private final File tempGenDir;
   private final File tempOutDir;
   private final CacheManager mockCacheManager = new MockCacheManager();
-  private final TreeLogger mockLogger = new MockTreeLogger();
+  private final TreeLogger mockLogger = TreeLogger.NULL;
   private final StandardGeneratorContext genCtx;
   private int tempFileCounter;
 
diff --git a/user/src/com/google/gwt/i18n/rebind/ResourceFactory.java b/user/src/com/google/gwt/i18n/rebind/ResourceFactory.java
index 1905946..9e3c7fd 100644
--- a/user/src/com/google/gwt/i18n/rebind/ResourceFactory.java
+++ b/user/src/com/google/gwt/i18n/rebind/ResourceFactory.java
@@ -200,11 +200,11 @@
   }
 
   public static AbstractResource getBundle(Class<?> clazz, String locale, boolean isConstants) {
-    return getBundle(createNullTreeLogger(), clazz, locale, isConstants);
+    return getBundle(TreeLogger.NULL, clazz, locale, isConstants);
   }
 
   public static AbstractResource getBundle(String path, String locale, boolean isConstants) {
-    return getBundle(createNullTreeLogger(), path, locale, isConstants);
+    return getBundle(TreeLogger.NULL, path, locale, isConstants);
   }
 
   /**
@@ -281,21 +281,6 @@
     return name;
   }
 
-  private static TreeLogger createNullTreeLogger() {
-    return new TreeLogger() {
-      public TreeLogger branch(Type type, String msg, Throwable caught) {
-        return null;
-      }
-
-      public boolean isLoggable(Type type) {
-        return false;
-      }
-
-      public void log(Type type, String msg, Throwable caught) {
-      }
-    };
-  }
-
   private static List<AbstractResource> findAlternativeParents(TreeLogger logger,
       ResourceFactory.AbstractPathTree tree, JClassType clazz, String locale,
       boolean isConstants) {