Add CopyableLabel to support copying text in one action

This was pulled out of Gerrit (fa6ee5de8ef0573c65be9aaa3edc566c24aadd5a)
and is based on Tom Preston-Werner's clippy.swf, a Flash movie which can
be used to copy text onto the system clipboard.

See also http://github.com/mojombo/clippy/tree/master

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml b/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
new file mode 100644
index 0000000..f211033
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/clippy/Clippy.gwt.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2009 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 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.safehtml.SafeHtml"/>
+  <inherits name="com.google.gwtexpui.user.User"/>
+  <stylesheet src='gwtexpui_clippy1.cache.css' />
+</module>
diff --git a/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
new file mode 100644
index 0000000..46f83e3
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -0,0 +1,197 @@
+// Copyright 2009 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 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.clippy.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusListener;
+import com.google.gwt.user.client.ui.HasText;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.KeyboardListenerAdapter;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtexpui.user.client.UserAgent;
+
+/**
+ * Label which permits the user to easily copy the complete content.
+ * <p>
+ * If the Flash plugin is available a "movie" is embedded that provides
+ * one-click copying of the content onto the system clipboard. The label (if
+ * visible) can also be clicked, switching from a label to an input box,
+ * allowing the user to copy the text with a keyboard shortcut.
+ * <p>
+ * Style name: <code>gwtexpui-Clippy</code>
+ */
+public class CopyableLabel extends Composite implements HasText {
+  private static final int SWF_WIDTH = 110;
+  private static final int SWF_HEIGHT = 14;
+  private static String swfUrl;
+
+  private static String swfUrl() {
+    if (swfUrl == null) {
+      swfUrl = GWT.getModuleBaseURL() + "gwtexpui_clippy1.cache.swf";
+    }
+    return swfUrl;
+  }
+
+  private final FlowPanel content;
+  private String text;
+  private Label textLabel;
+  private TextBox textBox;
+  private Element swf;
+
+  /**
+   * Create a new label
+   * 
+   * @param str initial content
+   */
+  public CopyableLabel(final String str) {
+    this(str, true);
+  }
+
+  /**
+   * Create a new label
+   * 
+   * @param str initial content
+   * @param showLabel if true, the content is shown, if false it is hidden from
+   *        view and only the copy icon is displayed.
+   */
+  public CopyableLabel(final String str, final boolean showLabel) {
+    content = new FlowPanel();
+    content.setStyleName("gwtexpui-Clippy");
+    initWidget(content);
+
+    text = str;
+    if (showLabel) {
+      textLabel = new InlineLabel(getText());
+      textLabel.setStyleName("gwtexpui-Clippy-Label");
+      textLabel.addClickListener(new ClickListener() {
+        public void onClick(final Widget sender) {
+          showTextBox();
+        }
+      });
+      content.add(textLabel);
+    }
+    embedMovie();
+  }
+
+  private void embedMovie() {
+    if (UserAgent.hasFlash) {
+      final String flashVars = "text=" + URL.encodeComponent(getText());
+      final SafeHtmlBuilder h = new SafeHtmlBuilder();
+
+      h.openElement("span");
+      h.setStyleName("gwtexpui-Clippy-Control");
+
+      h.openElement("object");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("classid", "clsid:d27cdb6e-ae6d-11cf-96b8-444553540000");
+      h.paramElement("movie", swfUrl());
+      h.paramElement("FlashVars", flashVars);
+
+      h.openElement("embed");
+      h.setWidth(SWF_WIDTH);
+      h.setHeight(SWF_HEIGHT);
+      h.setAttribute("type", "application/x-shockwave-flash");
+      h.setAttribute("src", swfUrl());
+      h.setAttribute("FlashVars", flashVars);
+      h.closeSelf();
+
+      h.closeElement("object");
+      h.closeElement("span");
+
+      if (swf != null) {
+        DOM.removeChild(getElement(), swf);
+      }
+      DOM.appendChild(getElement(), swf = SafeHtml.parse(h));
+    }
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public void setText(final String newText) {
+    text = newText;
+
+    if (textLabel != null) {
+      textLabel.setText(getText());
+    }
+    if (textBox != null) {
+      textBox.setText(getText());
+      textBox.selectAll();
+    }
+    embedMovie();
+  }
+
+  private void showTextBox() {
+    if (textBox == null) {
+      textBox = new TextBox();
+      textBox.setText(getText());
+      textBox.setVisibleLength(getText().length());
+      textBox.addKeyboardListener(new KeyboardListenerAdapter() {
+        @Override
+        public void onKeyPress(final Widget sender, final char kc, final int mod) {
+          if ((mod & MODIFIER_CTRL) == MODIFIER_CTRL
+              || (mod & MODIFIER_META) == MODIFIER_META) {
+            switch (kc) {
+              case 'c':
+              case 'x':
+                DeferredCommand.addCommand(new Command() {
+                  public void execute() {
+                    hideTextBox();
+                  }
+                });
+                break;
+            }
+          }
+        }
+      });
+      textBox.addFocusListener(new FocusListener() {
+        public void onFocus(Widget arg0) {
+        }
+
+        public void onLostFocus(Widget arg0) {
+          hideTextBox();
+        }
+      });
+      content.insert(textBox, 1);
+    }
+
+    textLabel.setVisible(false);
+    textBox.setVisible(true);
+    textBox.selectAll();
+    textBox.setFocus(true);
+  }
+
+  private void hideTextBox() {
+    if (textBox != null) {
+      textBox.removeFromParent();
+      textBox = null;
+    }
+    textLabel.setVisible(true);
+  }
+}
diff --git a/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.css b/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.css
new file mode 100644
index 0000000..25ca9e8
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.css
@@ -0,0 +1,8 @@
+.gwtexpui-Clippy {
+}
+.gwtexpui-Clippy-Label {
+  vertical-align: top;
+}
+.gwtexpui-Clippy-Control {
+  margin-left: 5px;
+}
diff --git a/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf b/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
new file mode 100644
index 0000000..e46886c
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/clippy/public/gwtexpui_clippy1.cache.swf
Binary files differ
diff --git a/src/main/java/com/google/gwtexpui/user/User.gwt.xml b/src/main/java/com/google/gwtexpui/user/User.gwt.xml
new file mode 100644
index 0000000..9a94484
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/user/User.gwt.xml
@@ -0,0 +1,18 @@
+<!--
+ Copyright 2009 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 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.gwt.user.User"/>
+</module>
diff --git a/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
new file mode 100644
index 0000000..e03b668
--- /dev/null
+++ b/src/main/java/com/google/gwtexpui/user/client/UserAgent.java
@@ -0,0 +1,50 @@
+// Copyright 2009 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 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;
+
+/**
+ * User agent feature tests we don't create permutations for.
+ * <p>
+ * Some features aren't worth creating full permutations in GWT for, as each new
+ * boolean permutation (only two settings) doubles the compile time required. If
+ * the setting only affects a couple of lines of JavaScript code, the slightly
+ * larger cache files for user agents that lack the functionality requested is
+ * trivial compared to the time developers lose building their application.
+ */
+public class UserAgent {
+  /** Does the browser have ShockwaveFlash plugin enabled? */
+  public static final boolean hasFlash = hasFlash();
+
+  private static native boolean hasFlash()
+  /*-{
+    if (navigator.plugins && navigator.plugins.length) {
+      if (navigator.plugins['Shockwave Flash'])     return true;
+      if (navigator.plugins['Shockwave Flash 2.0']) return true;
+
+    } else if (navigator.mimeTypes && navigator.mimeTypes.length) {
+      var mimeType = navigator.mimeTypes['application/x-shockwave-flash'];
+      if (mimeType && mimeType.enabledPlugin) return true;
+
+    } else {
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); return true; } catch (e) {}
+      try { new ActiveXObject('ShockwaveFlash.ShockwaveFlash');   return true; } catch (e) {}
+    }
+    return false;
+  }-*/;
+
+  private UserAgent() {
+  }
+}