diff --git a/gerrit-gwtexpui/BUCK b/gerrit-gwtexpui/BUCK index b266e12625..8e531c199c 100644 --- a/gerrit-gwtexpui/BUCK +++ b/gerrit-gwtexpui/BUCK @@ -7,6 +7,7 @@ gwt_module( resources = [ SRC + 'clippy/client/clippy.css', SRC + 'clippy/client/clippy.swf', + SRC + 'clippy/client/CopyableLabelText.properties', ], provided_deps = ['//lib/gwt:user'], deps = [ @@ -84,6 +85,7 @@ gwt_module( name = 'UserAgent', srcs = glob([SRC + 'user/client/*.java']), gwt_xml = SRC + 'user/User.gwt.xml', + resources = [SRC + 'user/client/tooltip.css'], provided_deps = ['//lib/gwt:user'], visibility = ['PUBLIC'], ) diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java index 68495e8e45..05a1861f8c 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/ClippyCss.java @@ -18,5 +18,6 @@ import com.google.gwt.resources.client.CssResource; public interface ClippyCss extends CssResource { String label(); - String control(); + String copier(); + String swf(); } diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java index a9f437a00d..5da8b1ef37 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java @@ -16,6 +16,7 @@ package com.google.gwtexpui.clippy.client; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; @@ -24,9 +25,12 @@ import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; 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.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HasText; @@ -35,6 +39,7 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TextBox; import com.google.gwtexpui.safehtml.client.SafeHtml; import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; +import com.google.gwtexpui.user.client.Tooltip; import com.google.gwtexpui.user.client.UserAgent; /** @@ -71,6 +76,7 @@ public class CopyableLabel extends Composite implements HasText { private int visibleLen; private Label textLabel; private TextBox textBox; + private Button copier; private Element swf; public CopyableLabel() { @@ -111,7 +117,32 @@ public class CopyableLabel extends Composite implements HasText { }); content.add(textLabel); } - embedMovie(); + + if (UserAgent.hasJavaScriptClipboard()) { + copier = new Button("📋"); // CLIPBOARD + copier.setStyleName(ClippyResources.I.css().copier()); + Tooltip.addStyle(copier); + Tooltip.setLabel(copier, CopyableLabelText.I.tooltip()); + copier.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + copy(); + } + }); + copier.addMouseOutHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Tooltip.setLabel(copier, CopyableLabelText.I.tooltip()); + } + }); + + FlowPanel p = new FlowPanel(); + p.getElement().getStyle().setDisplay(Display.INLINE_BLOCK); + p.add(copier); + content.add(p); + } else { + embedMovie(); + } } /** @@ -127,12 +158,13 @@ public class CopyableLabel extends Composite implements HasText { } private void embedMovie() { - if (flashEnabled && !text.isEmpty() && UserAgent.Flash.isInstalled()) { + if (copier == null && flashEnabled && !text.isEmpty() + && UserAgent.Flash.isInstalled()) { final String flashVars = "text=" + URL.encodeQueryString(getText()); final SafeHtmlBuilder h = new SafeHtmlBuilder(); h.openElement("div"); - h.setStyleName(ClippyResources.I.css().control()); + h.setStyleName(ClippyResources.I.css().swf()); h.openElement("object"); h.setWidth(SWF_WIDTH); @@ -236,4 +268,35 @@ public class CopyableLabel extends Composite implements HasText { } textLabel.setVisible(true); } + + private void copy() { + TextBox t = new TextBox(); + try { + t.setText(getText()); + content.add(t); + t.selectAll(); + + boolean ok = execCommand("copy"); + Tooltip.setLabel(copier, ok + ? CopyableLabelText.I.copied() + : CopyableLabelText.I.failed()); + if (!ok) { + // Disable JavaScript clipboard and try flash movie in another instance. + UserAgent.disableJavaScriptClipboard(); + } + } finally { + t.removeFromParent(); + } + } + + private static boolean execCommand(String command) { + try { + return nativeExec(command); + } catch (Exception e) { + return false; + } + } + + private static native boolean nativeExec(String c) + /*-{ return !! $doc.execCommand(c) }-*/; } diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java new file mode 100644 index 0000000000..4d1b837331 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.java @@ -0,0 +1,26 @@ +// Copyright (C) 2015 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.clippy.client; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.i18n.client.Constants; + +interface CopyableLabelText extends Constants { + static final CopyableLabelText I = GWT.create(CopyableLabelText.class); + + String tooltip(); + String copied(); + String failed(); +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties new file mode 100644 index 0000000000..cf93bfa4b8 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabelText.properties @@ -0,0 +1,3 @@ +tooltip = Copy to clipboard +copied = Copied +failed = Failed ! diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css index b962df304d..b25e00694b 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/clippy.css @@ -16,10 +16,24 @@ .label { vertical-align: top; } -.control { +.swf, .copier { margin-left: 5px; - display: inline-block !important; height: 14px; width: 14px; +} +.swf { + display: inline-block !important; overflow: hidden; } +.copier { + display: inline-block; + font-size: 12px; + vertical-align: top; + padding: 0; + border: 0; + background-color: inherit; + cursor: pointer; +} +.copier:focus { + outline: none; +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java new file mode 100644 index 0000000000..e3ab034318 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/Tooltip.java @@ -0,0 +1,80 @@ +// Copyright (C) 2015 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.dom.client.Element; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.user.client.ui.UIObject; + +/** Displays custom tooltip message below an element. */ +public class Tooltip { + interface Resources extends ClientBundle { + static final Resources I = GWT.create(Resources.class); + + @Source("tooltip.css") + Css css(); + } + + interface Css extends CssResource { + String tooltip(); + } + + static { + Resources.I.css().ensureInjected(); + } + + /** + * Add required supporting style to enable custom tooltip rendering. + * + * @param o widget whose element should display a tooltip on hover. + */ + public static void addStyle(UIObject o) { + addStyle(o.getElement()); + } + + /** + * Add required supporting style to enable custom tooltip rendering. + * + * @param e element that should display a tooltip on hover. + */ + public static void addStyle(Element e) { + e.addClassName(Resources.I.css().tooltip()); + } + + /** + * Set the text displayed on hover. + * + * @param o widget whose hover text is being set. + * @param text message to display on hover. + */ + public static void setLabel(UIObject o, String text) { + setLabel(o.getElement(), text); + } + + /** + * Set the text displayed on hover. + * + * @param e element whose hover text is being set. + * @param text message to display on hover. + */ + public static void setLabel(Element e, String text) { + e.setAttribute("aria-label", text != null ? text : ""); + } + + private Tooltip() { + } +} diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java index 15983050fe..2ffa7c5d9e 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/UserAgent.java @@ -27,6 +27,65 @@ import com.google.gwt.user.client.Window; * trivial compared to the time developers lose building their application. */ public class UserAgent { + private static boolean jsClip = guessJavaScriptClipboard(); + + public static boolean hasJavaScriptClipboard() { + return jsClip; + } + + public static void disableJavaScriptClipboard() { + jsClip = false; + } + + private static native boolean nativeHasCopy() + /*-{ return $doc['queryCommandSupported'] && $doc.queryCommandSupported('copy') }-*/; + + private static boolean guessJavaScriptClipboard() { + String ua = Window.Navigator.getUserAgent(); + int chrome = major(ua, "Chrome/"); + if (chrome > 0) { + return 42 <= chrome; + } + + int ff = major(ua, "Firefox/"); + if (ff > 0) { + return 41 <= ff; + } + + int opera = major(ua, "OPR/"); + if (opera > 0) { + return 29 <= opera; + } + + int msie = major(ua, "MSIE "); + if (msie > 0) { + return 9 <= msie; + } + + if (nativeHasCopy()) { + // Firefox 39.0 lies and says it supports copy, then fails. + // So we try this after the browser specific test above. + return true; + } + + // Safari is not planning to support document.execCommand('copy'). + // Assume the browser does not have the feature. + return false; + } + + private static int major(String ua, String product) { + int entry = ua.indexOf(product); + if (entry >= 0) { + String s = ua.substring(entry + product.length()); + String p = s.split("[ /;,.)]", 2)[0]; + try { + return Integer.parseInt(p); + } catch (NumberFormatException nan) { + } + } + return -1; + } + public static class Flash { private static boolean checked; private static boolean installed; diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/tooltip.css b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/tooltip.css new file mode 100644 index 0000000000..1aeb0155a8 --- /dev/null +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/tooltip.css @@ -0,0 +1,54 @@ +/* Copyright (C) 2015 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. + */ + +.tooltip { + position: relative; +} + +.tooltip:hover:before { + position: absolute; + z-index: 51; + border: solid; + border-color: #333 transparent; + border-width: 0 4px 4px 4px; + pointer-events: none; + content: ""; + + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; +} + +.tooltip:hover:after { + position: absolute; + z-index: 50; + font: normal normal 11px/1.5 Helvetica, arial, sans-serif; + text-align: center; + white-space: pre; + pointer-events: none; + background: rgba(0,0,0,.7); + color: #fff; + border-radius: 3px; + padding: 5px; + content: attr(aria-label); + + top: 100%; + right: 50%; + margin-top: 5px; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%) +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java index 803ac55197..a64ba57e03 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java @@ -37,6 +37,7 @@ import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.ListBox; +import com.google.gwtexpui.user.client.UserAgent; import java.util.ArrayList; import java.util.Arrays; @@ -135,16 +136,19 @@ public class MyPreferencesScreen extends SettingsScreen { legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable()); muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes()); - final Grid formGrid = new Grid(11, 2); + boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled(); + final Grid formGrid = new Grid(10 + (flashClippy ? 1 : 0), 2); int row = 0; formGrid.setText(row, labelIdx, ""); formGrid.setWidget(row, fieldIdx, showSiteHeader); row++; - formGrid.setText(row, labelIdx, ""); - formGrid.setWidget(row, fieldIdx, useFlashClipboard); - row++; + if (flashClippy) { + formGrid.setText(row, labelIdx, ""); + formGrid.setWidget(row, fieldIdx, useFlashClipboard); + row++; + } formGrid.setText(row, labelIdx, ""); formGrid.setWidget(row, fieldIdx, copySelfOnEmails);