Create a global keyboard focus in the document level

The GWT FocusPanel class can be very unreliable, especially
when dealing with older browsers like WebKit that have to
use a hidden input trick to receive keyboard events.  Its
much cleaner to install a onkeypress listener directly onto
the document object, but this is a window level listener and
it gets every key event processed, and there is only one of
them for the entire window.

This new globalkey module supports creating sets of keys and
paging them on or off as the application needs.  A popup is
also available, bound to '?', showing the keyboard actions
that are currently available.  The keyboard table is created
on the fly, based on the current bindings.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml b/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
new file mode 100644
index 0000000..8a6190a
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/GlobalKey.gwt.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright (C) 2009 The Android Open Source Project
+
+ 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 the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+  <inherits name='com.google.gwtexpui.user.User'/>
+  <inherits name='com.google.gwtexpui.safehtml.SafeHtml'/>
+  <stylesheet src='gwtexpui_globalkey1.cache.css' />
+</module>
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java b/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
new file mode 100644
index 0000000..d680a72
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/DocWidget.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+class DocWidget extends Widget implements HasKeyPressHandlers {
+  private static DocWidget me;
+
+  static DocWidget get() {
+    if (me == null) {
+      me = new DocWidget();
+    }
+    return me;
+  }
+
+  private DocWidget() {
+    setElement((Element) docnode());
+    onAttach();
+    RootPanel.detachOnWindowClose(this);
+  }
+
+  @Override
+  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+    return addDomHandler(handler, KeyPressEvent.getType());
+  }
+
+  private static Node docnode() {
+    return Document.get();
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
new file mode 100644
index 0000000..90eb0fc
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+
+
+public class GlobalKey {
+  private static KeyCommandSet keyApplication;
+  static KeyCommandSet keys;
+
+  private static void init() {
+    if (keys == null) {
+      keys = new KeyCommandSet();
+      DocWidget.get().addKeyPressHandler(keys);
+
+      keyApplication = new KeyCommandSet(Util.C.applicationSection());
+      keyApplication.add(new ShowHelpCommand());
+      keys.add(keyApplication);
+    }
+  }
+
+  public static HandlerRegistration addApplication(final KeyCommand key) {
+    init();
+    keys.add(key);
+    keyApplication.add(key);
+
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        keys.remove(key);
+        keyApplication.add(key);
+      }
+    };
+  }
+
+  public static HandlerRegistration add(final KeyCommandSet set) {
+    init();
+    keys.add(set);
+
+    return new HandlerRegistration() {
+      @Override
+      public void removeHandler() {
+        keys.remove(set);
+      }
+    };
+  }
+
+  public static void filter(final KeyCommandFilter filter) {
+    keys.filter(filter);
+  }
+
+  private GlobalKey() {
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
new file mode 100644
index 0000000..7ba7b27
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+
+public abstract class KeyCommand implements KeyPressHandler {
+  public static final int M_CTRL = 1 << 16;
+  public static final int M_ALT = 2 << 16;
+  public static final int M_META = 4 << 16;
+  private static final String KS = "gwtexpui-globalkey-KeyboardShortcuts-Key";
+
+  public static boolean same(final KeyCommand a, final KeyCommand b) {
+    return a.getClass() == b.getClass() && a.helpText.equals(b.helpText);
+  }
+
+  final int keyMask;
+  private final String helpText;
+
+  public KeyCommand(final int mask, final int key, final String help) {
+    this(mask, (char) key, help);
+  }
+
+  public KeyCommand(final int mask, final char key, final String help) {
+    assert help != null;
+    keyMask = mask | key;
+    helpText = help;
+  }
+
+  public String getHelpText() {
+    return helpText;
+  }
+
+  SafeHtml describeKeyStroke() {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+
+    if ((keyMask & M_CTRL) == M_CTRL) {
+      modifier(b, Util.C.keyCtrl());
+    }
+    if ((keyMask & M_ALT) == M_ALT) {
+      modifier(b, Util.C.keyAlt());
+    }
+    if ((keyMask & M_META) == M_META) {
+      modifier(b, Util.C.keyMeta());
+    }
+
+    final char c = (char) (keyMask & 0xffff);
+    switch (c) {
+      case KeyCodes.KEY_ENTER:
+        namedKey(b, Util.C.keyEnter());
+        break;
+      case KeyCodes.KEY_ESCAPE:
+        namedKey(b, Util.C.keyEsc());
+        break;
+      default:
+        b.openSpan();
+        b.setStyleName(KS);
+        b.append(String.valueOf(c));
+        b.closeSpan();
+        break;
+    }
+
+    return b;
+  }
+
+  private void modifier(final SafeHtmlBuilder b, final String name) {
+    namedKey(b, name);
+    b.append(" + ");
+  }
+
+  private void namedKey(final SafeHtmlBuilder b, final String name) {
+    b.append('<');
+    b.openSpan();
+    b.setStyleName(KS);
+    b.append(name);
+    b.closeSpan();
+    b.append(">");
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
new file mode 100644
index 0000000..05f41d4
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandFilter.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+public interface KeyCommandFilter {
+  public boolean include(KeyCommand key);
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
new file mode 100644
index 0000000..e0508e0
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -0,0 +1,128 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class KeyCommandSet implements KeyPressHandler {
+  private final Map<Integer, KeyCommand> map;
+  private List<KeyCommandSet> sets;
+  private String name;
+
+  public KeyCommandSet() {
+    this("");
+  }
+
+  public KeyCommandSet(final String setName) {
+    map = new HashMap<Integer, KeyCommand>();
+    name = setName;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(final String setName) {
+    assert setName != null;
+    name = setName;
+  }
+
+  public boolean isEmpty() {
+    return map.isEmpty();
+  }
+
+  public void add(final KeyCommand k) {
+    assert !map.containsKey(k.keyMask);
+    map.put(k.keyMask, k);
+  }
+
+  public void remove(final KeyCommand k) {
+    assert map.get(k.keyMask) == k;
+    map.remove(k.keyMask);
+  }
+
+  public void add(final KeyCommandSet set) {
+    if (sets == null) {
+      sets = new ArrayList<KeyCommandSet>();
+    }
+    assert !sets.contains(set);
+    sets.add(set);
+    for (final KeyCommand k : set.map.values()) {
+      add(k);
+    }
+  }
+
+  public void remove(final KeyCommandSet set) {
+    assert sets != null;
+    assert sets.contains(set);
+    sets.remove(set);
+    for (final KeyCommand k : set.map.values()) {
+      remove(k);
+    }
+  }
+
+  public void filter(final KeyCommandFilter filter) {
+    if (sets != null) {
+      for (final KeyCommandSet s : sets) {
+        s.filter(filter);
+      }
+    }
+    for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext();) {
+      if (!filter.include(i.next())) {
+        i.remove();
+      }
+    }
+  }
+
+  public Collection<KeyCommand> getKeys() {
+    return map.values();
+  }
+
+  public Collection<KeyCommandSet> getSets() {
+    return sets != null ? sets : Collections.<KeyCommandSet> emptyList();
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    final KeyCommand k = map.get(toMask(event));
+    if (k != null) {
+      event.stopPropagation();
+      k.onKeyPress(event);
+    }
+  }
+
+  private static int toMask(final KeyPressEvent event) {
+    int mask = event.getCharCode();
+    if (event.isAltKeyDown()) {
+      mask |= KeyCommand.M_ALT;
+    }
+    if (event.isControlKeyDown()) {
+      mask |= KeyCommand.M_CTRL;
+    }
+    if (event.isMetaKeyDown()) {
+      mask |= KeyCommand.M_META;
+    }
+    return mask;
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java b/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
new file mode 100644
index 0000000..07bc719
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.i18n.client.Constants;
+
+public interface KeyConstants extends Constants {
+  String applicationSection();
+  String showHelp();
+
+  String keyboardShortcuts();
+  String closeButton();
+  String orOtherKey();
+
+  String keyCtrl();
+  String keyAlt();
+  String keyMeta();
+  String keyEnter();
+  String keyEsc();
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties b/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
new file mode 100644
index 0000000..4b0c74e
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyConstants.properties
@@ -0,0 +1,12 @@
+applicationSection = Application
+showHelp = Open shortcut help
+
+keyboardShortcuts = Keyboard Shortcuts
+closeButton = Close
+orOtherKey = or
+
+keyCtrl = Ctrl
+keyAlt = Alt
+keyMeta = Meta
+keyEnter = Enter
+keyEsc = Esc
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
new file mode 100644
index 0000000..146f118
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+
+public class KeyHelpPopup extends PluginSafePopupPanel implements
+    KeyPressHandler {
+  private static final String S = "gwtexpui-globalkey-KeyboardShortcuts";
+
+  private final FocusPanel focus;
+
+  public KeyHelpPopup() {
+    super(true/* autohide */, true/* modal */);
+    setStyleName(S);
+
+    final Anchor closer = new Anchor(Util.C.closeButton());
+    closer.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        hide();
+      }
+    });
+
+    final Grid header = new Grid(1, 3);
+    header.setStyleName(S + "-Header");
+    header.setText(0, 0, Util.C.keyboardShortcuts());
+    header.setWidget(0, 2, closer);
+
+    final CellFormatter fmt = header.getCellFormatter();
+    fmt.addStyleName(0, 1, S + "-HeaderGlue");
+    fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT);
+
+    final Grid lists = new Grid(0, 7);
+    lists.setStyleName(S + "-Table");
+    populate(lists);
+    lists.getCellFormatter().addStyleName(0, 3, S + "-TableGlue");
+
+    final FlowPanel body = new FlowPanel();
+    body.add(header);
+    DOM.appendChild(body.getElement(), DOM.createElement("hr"));
+    body.add(lists);
+
+    focus = new FocusPanel(body);
+    DOM.setStyleAttribute(focus.getElement(), "outline", "0px");
+    DOM.setElementAttribute(focus.getElement(), "hideFocus", "true");
+    focus.addKeyPressHandler(this);
+    add(focus);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    super.setVisible(show);
+    if (show) {
+      focus.setFocus(true);
+    }
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    hide();
+  }
+
+  private void populate(final Grid lists) {
+    final Iterator<KeyCommandSet> setitr = GlobalKey.keys.getSets().iterator();
+    int end[] = new int[5];
+    int column = 0;
+    while (setitr.hasNext()) {
+      final KeyCommandSet set = setitr.next();
+      int row = end[column];
+      row = populate(lists, row, column, set);
+      end[column] = row;
+      if (column == 0) {
+        column = 4;
+      } else {
+        column = 0;
+      }
+    }
+  }
+
+  private int populate(final Grid lists, int row, final int col,
+      final KeyCommandSet set) {
+    final List<KeyCommand> keys = new ArrayList<KeyCommand>(set.getKeys());
+    Collections.sort(keys, new Comparator<KeyCommand>() {
+      @Override
+      public int compare(KeyCommand arg0, KeyCommand arg1) {
+        if (arg0.keyMask < arg1.keyMask) {
+          return -1;
+        } else if (arg0.keyMask > arg1.keyMask) {
+          return 1;
+        }
+        return 0;
+      }
+    });
+    if (keys.isEmpty()) {
+      return row;
+    }
+
+    if (lists.getRowCount() < row + 1 + keys.size()) {
+      lists.resizeRows(row + 1 + keys.size());
+    }
+
+    final CellFormatter fmt = lists.getCellFormatter();
+    lists.setText(row, col + 2, set.getName());
+    fmt.addStyleName(row, col + 2, S + "-GroupTitle");
+    row++;
+
+    final int initialRow = row;
+    FORMAT_KEYS: for (int i = 0; i < keys.size(); i++) {
+      final KeyCommand k = keys.get(i);
+
+      for (int prior = 0, r = initialRow; prior < i; prior++) {
+        if (KeyCommand.same(keys.get(prior), k)) {
+          final SafeHtmlBuilder b = new SafeHtmlBuilder();
+          b.append(SafeHtml.get(lists, r, col + 0));
+          b.append(" ");
+          b.append(Util.C.orOtherKey());
+          b.append(" ");
+          b.append(k.describeKeyStroke());
+          SafeHtml.set(lists, r, col + 0, b);
+          continue FORMAT_KEYS;
+        }
+      }
+
+      SafeHtml.set(lists, row, col + 0, k.describeKeyStroke());
+      lists.setText(row, col + 1, ":");
+      lists.setText(row, col + 2, k.getHelpText());
+
+      fmt.addStyleName(row, col + 0, S + "-TableKeyStroke");
+      fmt.addStyleName(row, col + 1, S + "-TableSeparator");
+      row++;
+    }
+
+    return row;
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
new file mode 100644
index 0000000..c9e3650
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+
+public class ShowHelpCommand extends KeyCommand {
+  public ShowHelpCommand() {
+    super(0, '?', Util.C.showHelp());
+  }
+
+  @Override
+  public void onKeyPress(final KeyPressEvent event) {
+    final KeyHelpPopup help = new KeyHelpPopup();
+    help.setPopupPositionAndShow(new PositionCallback() {
+      @Override
+      public void setPosition(final int pWidth, final int pHeight) {
+        final int left = (Window.getClientWidth() - pWidth) >> 1;
+        final int wLeft = Window.getScrollLeft();
+        final int wTop = Window.getScrollTop();
+        help.setPopupPosition(wLeft + left, wTop + 50);
+      }
+    });
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/client/Util.java b/src/main/java/com/google/gwtexpui/globalkey/client/Util.java
new file mode 100644
index 0000000..50280db
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/client/Util.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.globalkey.client;
+
+import com.google.gwt.core.client.GWT;
+
+public class Util {
+  public static final KeyConstants C = GWT.create(KeyConstants.class);
+
+  private Util() {
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/globalkey/public/gwtexpui_globalkey1.cache.css b/src/main/java/com/google/gwtexpui/globalkey/public/gwtexpui_globalkey1.cache.css
new file mode 100644
index 0000000..df9ad44
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/globalkey/public/gwtexpui_globalkey1.cache.css
@@ -0,0 +1,68 @@
+.gwtexpui-globalkey-KeyboardShortcuts {
+  background: #000000 none repeat scroll 0 50%;
+  color: #ffffff;
+  font-family: arial,sans-serif;
+  font-weight: bold;
+  overflow: hidden;
+  text-align: left;
+  text-shadow: 1px 1px 7px #000000;
+  width: 92%;
+  z-index: 1002;
+  opacity: 0.85;
+  -moz-border-radius-bottomleft: 10px;
+  -moz-border-radius-bottomright: 10px;
+  -moz-border-radius-topleft: 10px;
+  -moz-border-radius-topright: 10px;
+}
+
+.gwtexpui-globalkey-KeyboardShortcuts .popupContent {
+  margin: 10px;
+}
+
+.gwtexpui-globalkey-KeyboardShortcuts hr {
+  width: 100%;
+}
+
+.gwtexpui-globalkey-KeyboardShortcuts-Header {
+  width: 100%;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-Header td {
+  white-space: nowrap;
+  color: #ffffff;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-Header a,
+.gwtexpui-globalkey-KeyboardShortcuts-Header a:visited,
+.gwtexpui-globalkey-KeyboardShortcuts-Header a:hover {
+  color: #dddd00;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-HeaderGlue {
+  width: 100%;
+}
+
+.gwtexpui-globalkey-KeyboardShortcuts-GroupTitle {
+  color: #dddd00;
+  padding-top: 0.8em;
+  text-align: left;
+}
+
+.gwtexpui-globalkey-KeyboardShortcuts-Table {
+  width: 90%;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-Table td {
+  vertical-align: top;
+  white-space: nowrap;
+}
+td.gwtexpui-globalkey-KeyboardShortcuts-TableKeyStroke {
+  text-align: right;
+}
+td.gwtexpui-globalkey-KeyboardShortcuts-TableSeparator {
+  width: 0.5em;
+  text-align: center;
+  font-weight: bold;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-TableGlue {
+  width: 25px;
+}
+.gwtexpui-globalkey-KeyboardShortcuts-Key {
+  color: #dddd00;
+}
diff --git a/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java b/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
new file mode 100644
index 0000000..7d9c9fc
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/user/client/PluginSafePopupPanel.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// 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 the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gwtexpui.user.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/**
+ * A PopupPanel that can appear over Flash movies and Java applets.
+ * <p>
+ * Some browsers have issues with placing a &lt;div&gt; (such as that used by
+ * the PopupPanel implementation) over top of native UI such as that used by the
+ * Flash plugin. Often the native UI leaks over top of the &lt;div&gt;, which is
+ * not the desired behavior for a dialog box.
+ * <p>
+ * This implementation hides the native resources by setting their display
+ * property to 'none' when the dialog is shown, and restores them back to their
+ * prior setting when the dialog is hidden.
+ * */
+public class PluginSafePopupPanel extends PopupPanel {
+  private final PluginSafeDialogBoxImpl impl =
+      GWT.create(PluginSafeDialogBoxImpl.class);
+
+  public PluginSafePopupPanel() {
+    this(false);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide) {
+    this(autoHide, true);
+  }
+
+  public PluginSafePopupPanel(final boolean autoHide, final boolean modal) {
+    super(autoHide, modal);
+  }
+
+  @Override
+  public void setVisible(final boolean show) {
+    impl.visible(show);
+    super.setVisible(show);
+  }
+
+  @Override
+  public void show() {
+    impl.visible(true);
+    super.show();
+  }
+
+  @Override
+  public void hide(final boolean autoClosed) {
+    impl.visible(false);
+    super.hide(autoClosed);
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/user/client/View.java b/src/main/java/com/google/gwtexpui/user/client/View.java
index c50625c..eed4333 100644
--- a/src/main/java/com/google/gwtexpui/user/client/View.java
+++ b/src/main/java/com/google/gwtexpui/user/client/View.java
@@ -15,6 +15,7 @@
 package com.google.gwtexpui.user.client;
 
 import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Widget;
 
 /**
  * Widget to display within a {@link ViewSite}.
@@ -35,6 +36,12 @@
     super.onUnload();
   }
 
+  /** true if this is the current view of its parent view site */
+  public final boolean isCurrentView() {
+    final Widget p = getParent();
+    return p instanceof ViewSite && ((ViewSite<?>) p).getView() == this;
+  }
+
   /** Replace the current view in the parent ViewSite with this view. */
   public final void display() {
     if (site != null) {