Revision: 7148 Author: j...@google.com Date: Mon Nov 23 18:34:09 2009 Log: Improve the DevMode UI, both for GPE and Swing: - startup URLs are specified separately from launching them, allowing them to be displayed earlier - if no startup URLs are supplied, they are inferred from the contents of the war directory - the Swing UI is change to better allow launching of the URLs and to give feedback while processing modules - Ctrl-Key mappings are now Command-Key on the Mac
Patch by: jat Review by: rdayal http://code.google.com/p/google-web-toolkit/source/detail?r=7148 Modified: /trunk/dev/core/src/com/google/gwt/dev/BootStrapPlatform.java /trunk/dev/core/src/com/google/gwt/dev/DevMode.java /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java /trunk/dev/core/src/com/google/gwt/dev/GWTShell.java /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java /trunk/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java /trunk/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/BootStrapPlatform.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/BootStrapPlatform.java Mon Nov 23 18:34:09 2009 @@ -48,6 +48,14 @@ public static void initHostedMode() { // nothing required } + + /** + * Return true if we are running on a Mac. + */ + public static boolean isMac() { + String lcOSName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + return lcOSName.startsWith("mac "); + } /** * This works around apple radr:5569300. When -XstartOnFirstThread is passed @@ -61,14 +69,6 @@ BootStrapPlatform.class.getClassLoader()); } } - - /** - * Return true if we are running on a Mac. - */ - private static boolean isMac() { - String lcOSName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); - return lcOSName.startsWith("mac "); - } /** * Sets platform specific system properties. Currently, this disables ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/DevMode.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/DevMode.java Mon Nov 23 18:34:09 2009 @@ -36,6 +36,7 @@ import com.google.gwt.util.tools.Utility; import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; import java.net.BindException; import java.util.HashMap; @@ -227,6 +228,11 @@ } } + /** + * Startup development mode. + * + * @param args command line arguments + */ public static void main(String[] args) { /* * NOTE: main always exits with a call to System.exit to terminate any @@ -302,10 +308,7 @@ } @Override - protected boolean doStartup() { - if (!super.doStartup()) { - return false; - } + protected boolean doSlowStartup() { tempWorkDir = options.getWorkDir() == null; if (tempWorkDir) { try { @@ -390,6 +393,19 @@ protected String getWebServerName() { return options.getServletContainerLauncher().getName(); } + + @Override + protected void inferStartupUrls() { + // Look for any HTML files directly under war + File warDir = options.getWarDir(); + for (File htmlFile : warDir.listFiles(new FilenameFilter() { + public boolean accept(File dir, String name) { + return name.matches(".*\\.html"); + } + })) { + options.addStartupURL(htmlFile.getName()); + } + } @Override protected ModuleDef loadModule(TreeLogger logger, String moduleName, ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/DevModeBase.java Mon Nov 23 18:34:09 2009 @@ -674,6 +674,8 @@ * Gets the base log level recommended by the UI for INFO-level messages. This * method can only be called once {...@link #createUI()} has been called. Please * do not depend on this method, as it is subject to change. + * + * @return the log level to use for INFO-level messages */ public TreeLogger.Type getBaseLogLevelForUI() { if (baseLogLevelForUI == null) { @@ -691,29 +693,6 @@ public TreeLogger getTopLogger() { return topLogger; } - - /** - * Launch the arguments as Urls in separate windows. - * - * @param logger TreeLogger instance to use - */ - public void launchStartupUrls(final TreeLogger logger) { - ensureCodeServerListener(); - String startupURL = ""; - Map<String, URL> startupUrls = new HashMap<String, URL>(); - for (String prenormalized : options.getStartupURLs()) { - startupURL = normalizeURL(prenormalized, getPort(), getHost()); - logger.log(TreeLogger.TRACE, "Starting URL: " + startupURL, null); - try { - URL url = processUrl(startupURL); - startupUrls.put(prenormalized, url); - } catch (UnableToCompleteException e) { - logger.log(TreeLogger.ERROR, "Unable to process startup URL " - + startupURL, null); - } - } - ui.setStartupUrls(startupUrls); - } /** * Callback for the UI to indicate it is done. @@ -733,7 +712,7 @@ if (startUp()) { // The web server is running now, so launch browsers for startup urls. - launchStartupUrls(getTopLogger()); + launchStartupUrls(); } blockUntilDone.acquire(); @@ -786,6 +765,18 @@ } protected abstract void doShutDownServer(); + + /** + * Perform any slower startup tasks, such as loading modules. This is + * separate from {...@link #doStartup()} so that the UI can be updated as + * soon as possible and the web server can be started earlier. + * + * @return false if startup failed + */ + protected boolean doSlowStartup() { + // do nothing by default + return true; + } /** * Perform any startup tasks, including initializing the UI (if any) and the @@ -846,6 +837,14 @@ protected String getHost() { return "localhost"; } + + /** + * Add any plausible HTML files which might be used as startup URLs. Found + * URLs should be added to {...@code options.addStartupUrl(url)}. + */ + protected void inferStartupUrls() { + // do nothing by default + } /** * By default we will open the application window. @@ -967,6 +966,16 @@ getTopLogger().log(TreeLogger.TRACE, "Started web server on port " + resultPort); } + + if (options.getStartupURLs().isEmpty()) { + inferStartupUrls(); + } + + setStartupUrls(topLogger); + + if (!doSlowStartup()) { + return false; + } return true; } @@ -1015,6 +1024,14 @@ return newUI; } + + /** + * Actually launch (or indicate to the user they can be launched) previously + * specified (via {...@link #setStartupUrls(TreeLogger)}) URLs. + */ + private void launchStartupUrls() { + ui.launchStartupUrls(); + } /** * Perform hosted mode relink when new artifacts are generated, without @@ -1035,4 +1052,30 @@ newlyGeneratedArtifacts); produceOutput(linkLogger, linkerContext, artifacts, module, true); } -} + + /** + * Set the set of startup URLs. This is done before launching to allow the + * UI to better present the options to the user, but note that the UI should + * not attempt to launch the URLs until {...@link #launchStartupUrls()} + * is called. + * + * @param logger TreeLogger instance to use + */ + private void setStartupUrls(final TreeLogger logger) { + ensureCodeServerListener(); + Map<String, URL> startupUrls = new HashMap<String, URL>(); + for (String prenormalized : options.getStartupURLs()) { + String startupURL = normalizeURL(prenormalized, getPort(), getHost()); + logger.log(TreeLogger.DEBUG, "URL " + prenormalized + " normalized as " + + startupURL, null); + try { + URL url = processUrl(startupURL); + startupUrls.put(prenormalized, url); + } catch (UnableToCompleteException e) { + logger.log(TreeLogger.ERROR, "Unable to process startup URL " + + startupURL, null); + } + } + ui.setStartupUrls(startupUrls); + } +} ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/GWTShell.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/GWTShell.java Mon Nov 23 18:34:09 2009 @@ -209,6 +209,7 @@ return EmbeddedTomcatServer.getPort(); } + @Override protected synchronized void produceOutput(TreeLogger logger, StandardLinkerContext linkerStack, ArtifactSet artifacts, ModuleDef module, boolean isRelink) throws UnableToCompleteException { ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java Thu Nov 19 12:24:42 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/SwingUI.java Mon Nov 23 18:34:09 2009 @@ -16,7 +16,6 @@ package com.google.gwt.dev; import com.google.gwt.core.ext.TreeLogger; -import com.google.gwt.core.ext.TreeLogger.HelpInfo; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.dev.DevModeBase.HostedModeBaseOptions; import com.google.gwt.dev.WebServerPanel.RestartAction; @@ -36,7 +35,6 @@ import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -266,32 +264,14 @@ maybeInitializeOsXApplication(); } + @Override + public void launchStartupUrls() { + mainWnd.setLaunchable(); + } + @Override public void setStartupUrls(Map<String, URL> urls) { - // TODO(jat): provide UI for selecting URLs and launching - ArrayList<String> keys = new ArrayList<String>(urls.keySet()); - Collections.sort(keys); - for (String url : keys) { - final URL helpInfoUrl = urls.get(url); - getTopLogger().log(TreeLogger.INFO, "Waiting for browser connection to " - + helpInfoUrl.toExternalForm(), null, - new HelpInfo() { - @Override - public String getAnchorText() { - return "Launch default browser"; - } - - @Override - public String getPrefix() { - return ""; - } - - @Override - public URL getURL() { - return helpInfoUrl; - } - }); - } + mainWnd.setStartupUrls(urls); } protected int getNextSessionCounter(File logdir) { ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/shell/ShellMainWindow.java Mon Nov 23 18:34:09 2009 @@ -17,22 +17,150 @@ import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.dev.shell.log.SwingLoggerPanel; +import com.google.gwt.dev.util.BrowserLauncher; import java.awt.BorderLayout; import java.awt.GridLayout; +import java.awt.HeadlessException; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; /** + * Top-level window for the Swing DevMode UI. */ -...@suppresswarnings("deprecation") public class ShellMainWindow extends JPanel { + /** + * A class for implementing different methods of launching a URL. + */ + private abstract static class LaunchMethod { + + private final String displayName; + + /** + * Construct the launch method. + * + * @param displayName the name that will display in the UI for this launch + * method + */ + public LaunchMethod(String displayName) { + this.displayName = displayName; + } + + /** + * Launch the specified URL. + * + * @param url + */ + public abstract void launchUrl(URL url); + + @Override + public String toString() { + return displayName; + } + } + + /** + * Launches a URL using the default browser, as defined by + * {...@link BrowserLauncher}. + */ + private class DefaultBrowserLauncher extends LaunchMethod { + + public DefaultBrowserLauncher() { + super("Default browser"); + } + + @Override + public void launchUrl(URL url) { + Throwable caught = null; + try { + BrowserLauncher.browse(url.toExternalForm()); + return; + } catch (IOException e) { + caught = e; + } catch (URISyntaxException e) { + caught = e; + } + getLogger().log(TreeLogger.ERROR, "Unable to launch default browser", + caught); + } + } + + /** + * Launches a URL by copying it to the clipboard. + */ + private class CopyToClipboardLauncher extends LaunchMethod { + + public CopyToClipboardLauncher() { + super("Copy URL to clipboard"); + } + + @Override + public void launchUrl(URL url) { + // is it better to use SwingUtilities2.canAccessSystemClipboard() here? + Throwable caught = null; + try { + Clipboard clipboard = logWindow.getToolkit().getSystemClipboard(); + StringSelection selection = new StringSelection(url.toExternalForm()); + clipboard.setContents(selection, selection); + return; + } catch (SecurityException e) { + caught = e; + } catch (HeadlessException e) { + caught = e; + } + getLogger().log(TreeLogger.ERROR, "Unable to copy URL to clipboard", + caught); + } + } + + /** + * User-visible URL and complete URL for use in combo box. + */ + private static class UrlComboEntry { + + private final String urlFragment; + private final URL url; + + public UrlComboEntry(String urlFragment, URL url) { + this.urlFragment = urlFragment; + this.url = url; + } + + public URL getUrl() { + return url; + } + + @Override + public String toString() { + return urlFragment; + } + } private SwingLoggerPanel logWindow; - + private JComboBox launchCombo; + private JButton launchButton; + + private JComboBox urlCombo; + + /** + * @param maxLevel + * @param logFile + */ public ShellMainWindow(TreeLogger.Type maxLevel, File logFile) { super(new BorderLayout()); // TODO(jat): add back when we have real options @@ -42,13 +170,30 @@ optionPanel.setBorder(BorderFactory.createTitledBorder("Options")); optionPanel.add(new JLabel("Miscellaneous options here")); panel.add(optionPanel); - JPanel launchPanel = new JPanel(); - launchPanel.setBorder(BorderFactory.createTitledBorder("Launch GWT Module")); - launchPanel.add(new JLabel( - "Selections for launching a new module on a selected browser")); - panel.add(launchPanel); add(panel, BorderLayout.NORTH); } + JPanel launchPanel = new JPanel(); + launchPanel.setBorder(BorderFactory.createTitledBorder( + "Launch GWT Module")); + launchPanel.add(new JLabel("Startup URL:")); + JPanel startupPanel = new JPanel(); + urlCombo = new JComboBox(); + urlCombo.addItem("Computing..."); + startupPanel.add(urlCombo); + launchPanel.add(startupPanel); + launchPanel.add(new JLabel("Launch Method:")); + launchCombo = new JComboBox(); + populateLaunchComboBox(); + launchPanel.add(launchCombo); + launchButton = new JButton("Loading..."); + launchButton.setEnabled(false); + launchButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + launch(); + } + }); + launchPanel.add(launchButton); + add(launchPanel, BorderLayout.NORTH); logWindow = new SwingLoggerPanel(maxLevel, logFile); add(logWindow); } @@ -59,4 +204,48 @@ public TreeLogger getLogger() { return logWindow.getLogger(); } -} + + /** + * Indicate that URLs specified in {...@link #setStartupUrls(Map)} are now + * launchable. + */ + public void setLaunchable() { + if (urlCombo.getItemCount() == 0) { + launchButton.setText("No URLs to Launch"); + urlCombo.addItem("No startup URLs"); + urlCombo.setEnabled(false); + return; + } + launchButton.setText("Launch"); + launchButton.setEnabled(true); + } + + /** + * Create the UI to show available startup URLs. These should not be + * launchable by the user until the {...@link #setLaunchable()} method is called. + * + * @param urls map of user-specified URL fragments to final URLs + */ + public void setStartupUrls(Map<String, URL> urls) { + urlCombo.removeAllItems(); + ArrayList<String> keys = new ArrayList<String>(urls.keySet()); + Collections.sort(keys); + for (String url : keys) { + urlCombo.addItem(new UrlComboEntry(url, urls.get(url))); + } + urlCombo.revalidate(); + } + + protected void launch() { + LaunchMethod launcher = (LaunchMethod) launchCombo.getSelectedItem(); + UrlComboEntry selectedUrl = (UrlComboEntry) urlCombo.getSelectedItem(); + URL url = selectedUrl.getUrl(); + launcher.launchUrl(url); + } + + private void populateLaunchComboBox() { + // TODO(jat): support scanning for other browsers and launching them + launchCombo.addItem(new DefaultBrowserLauncher()); + launchCombo.addItem(new CopyToClipboardLauncher()); + } +} ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java Mon Nov 23 16:21:36 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/shell/log/SwingLoggerPanel.java Mon Nov 23 18:34:09 2009 @@ -17,6 +17,7 @@ import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; +import com.google.gwt.dev.BootStrapPlatform; import com.google.gwt.dev.shell.CloseButton; import com.google.gwt.dev.shell.CloseButton.Callback; import com.google.gwt.dev.shell.log.SwingTreeLogger.LogEvent; @@ -101,15 +102,15 @@ private class FindBox extends JPanel { - private JTextField searchField; - private JLabel searchStatus; - private Popup findPopup; - private String lastSearch; - private int matchNumber; + private ArrayList<DefaultMutableTreeNode> matches; + private int matchNumber; + private JTextField searchField; + private JLabel searchStatus; + public FindBox() { super(new BorderLayout()); JPanel top = new JPanel(new FlowLayout()); @@ -241,39 +242,49 @@ } } - private static final Color DISCONNECTED_COLOR = Color.decode("0xFFDDDD"); - - // package protected for SwingTreeLogger to access - - final JTree tree; - - DefaultTreeModel treeModel; - + /** + * The mask to use for Ctrl -- mapped to Command on Mac. + */ + private static int ctrlKeyDown; + + + private static final Color DISCONNECTED_COLOR = Color.decode("0xFFDDDD"); + + static { + ctrlKeyDown = BootStrapPlatform.isMac() ? InputEvent.ALT_DOWN_MASK + : InputEvent.CTRL_DOWN_MASK; + } + + // package protected for SwingTreeLogger to access Type levelFilter; String regexFilter; - private final JEditorPane details; - - private final TreeLogger logger; - - private DefaultMutableTreeNode root; - - private JTextField regexField; - - private JComboBox levelComboBox; - - private JPanel topPanel; + final JTree tree; + + DefaultTreeModel treeModel; + + private CloseHandler closeHandler; + + private CloseButton closeLogger; + + private final JEditorPane details; + + private boolean disconnected = false; private FindBox findBox; - private JScrollPane treeView; - - private CloseButton closeLogger; - - private CloseHandler closeHandler; - - private boolean disconnected = false; + private JComboBox levelComboBox; + + private final TreeLogger logger; + + private JTextField regexField; + + private DefaultMutableTreeNode root; + + private JPanel topPanel; + + private JScrollPane treeView; /** * Create a Swing-based logger panel, with a tree section and a detail @@ -407,15 +418,14 @@ } } logger = bestLogger; - KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, - InputEvent.CTRL_DOWN_MASK); + KeyStroke key = getCommandKeyStroke(KeyEvent.VK_F); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "find"); getActionMap().put("find", new AbstractAction() { public void actionPerformed(ActionEvent e) { showFindBox(); } }); - key = KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK); + key = getCommandKeyStroke(KeyEvent.VK_C); tree.getInputMap().put(key, "copy"); tree.getActionMap().put("copy", new AbstractAction() { public void actionPerformed(ActionEvent e) { @@ -423,14 +433,14 @@ } }); findBox = new FindBox(); - key = KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK); + key = getCommandKeyStroke(KeyEvent.VK_N); tree.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "findnext"); tree.getActionMap().put("findnext", new AbstractAction() { public void actionPerformed(ActionEvent e) { findBox.nextMatch(); } }); - key = KeyStroke.getKeyStroke(KeyEvent.VK_P, InputEvent.CTRL_DOWN_MASK); + key = getCommandKeyStroke(KeyEvent.VK_P); tree.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(key, "findprev"); tree.getActionMap().put("findprev", new AbstractAction() { public void actionPerformed(ActionEvent e) { @@ -566,7 +576,7 @@ JOptionPane.WARNING_MESSAGE); return response != JOptionPane.YES_OPTION; } - + protected ArrayList<DefaultMutableTreeNode> doFind(String search) { @SuppressWarnings("unchecked") Enumeration<DefaultMutableTreeNode> children = root.preorderEnumeration(); @@ -631,6 +641,17 @@ StringSelection selection = new StringSelection(text.toString()); clipboard.setContents(selection, selection); } + + /** + * Returns a keystroke which adds the appropriate modifier for a command key: + * Command on mac, Ctrl everywhere else. + * + * @param key + * @return KeyStroke of the Ctrl/Command-key + */ + private KeyStroke getCommandKeyStroke(int key) { + return KeyStroke.getKeyStroke(key, ctrlKeyDown); + } private String htmlUnescape(String str) { // TODO(jat): real implementation, needs to correspond to ======================================= --- /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java Thu Nov 19 12:24:42 2009 +++ /trunk/dev/core/src/com/google/gwt/dev/ui/DevModeUI.java Mon Nov 23 18:34:09 2009 @@ -80,7 +80,7 @@ public TreeLogger getTopLogger() { return getConsoleLogger(); } - + /** * Create the web server portion of the UI if not already created, and * return its TreeLogger instance. @@ -109,7 +109,15 @@ public void initialize(Type logLevel) { this.logLevel = logLevel; } - + + /** + * Indicate to the user that URLs may be started, or actually launch browsers + * with the URLs specified in {...@link #setStartupUrls(Map)}. + */ + public void launchStartupUrls() { + // do nothing by default + } + /** * Sets the callback for a given event type.. * @@ -132,7 +140,7 @@ public void setStartupUrls(Map<String, URL> urls) { // do nothing by default } - + /** * Call callback for a given event. * @@ -144,7 +152,7 @@ UiEvent.Type<?> eventType) { return (C) callbacks.get(eventType); } - + /** * @return a console-based logger. */ @@ -155,14 +163,14 @@ } return consoleLogger; } - + /** * @return the log level for all logging. */ protected final Type getLogLevel() { return logLevel; } - + /** * Returns true if a callback has been registered for an event. * -- http://groups.google.com/group/Google-Web-Toolkit-Contributors