From 3b6904b1d92b987e308f5fb3308fec215ba1f1ae Mon Sep 17 00:00:00 2001 From: James Moger Date: Tue, 13 Dec 2011 16:51:36 -0500 Subject: [PATCH] Integrated Clippy for a better copy-to-clipboard experience --- distrib/gitblit.properties | 7 + docs/04_design.mkd | 1 + docs/04_releases.mkd | 4 +- resources/clippy.swf | Bin 0 -> 5380 bytes .../wicket/panels/ObjectContainer.java | 160 ++++++++++++++ .../wicket/panels/RepositoryUrlPanel.html | 23 +- .../wicket/panels/RepositoryUrlPanel.java | 25 ++- .../wicket/panels/ShockWaveComponent.java | 205 ++++++++++++++++++ 8 files changed, 418 insertions(+), 7 deletions(-) create mode 100644 resources/clippy.swf create mode 100644 src/com/gitblit/wicket/panels/ObjectContainer.java create mode 100644 src/com/gitblit/wicket/panels/ShockWaveComponent.java diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index 39e47885..537f9b67 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -121,6 +121,13 @@ web.allowGravatar = true # SINCE 0.5.0 web.allowZipDownloads = true +# Use Clippy (Flash solution) to provide a copy-to-clipboard button. +# If false, a button with a more primitive JavaScript-based prompt box will +# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative. +# +# SINCE 0.8.0 +web.allowFlashCopyToClipboard = true + # Default number of entries to include in RSS Syndication links # # SINCE 0.5.0 diff --git a/docs/04_design.mkd b/docs/04_design.mkd index 921bc8bb..3fd13d47 100644 --- a/docs/04_design.mkd +++ b/docs/04_design.mkd @@ -11,6 +11,7 @@ The following dependencies are bundled with Gitblit. - [Bootstrap](http://twitter.github.com/bootstrap) (Apache 2.0) +- [Clippy](https://github.com/mojombo/clippy) (MIT) - [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0) - [Commons Daemon](http://commons.apache.org/daemon) (Apache 2.0) - magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY) diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd index bf709183..9a4e4a83 100644 --- a/docs/04_releases.mkd +++ b/docs/04_releases.mkd @@ -15,7 +15,9 @@ This user service implementation allows for serialization and deserialization of **New:** *web.timeFormat = HH:mm* **New:** *web.datestampLongFormat = EEEE, MMMM d, yyyy* - fixed: several a bugs in FileUserService related to cleaning up old repository permissions on a rename or delete -- added: primitive technique for manual *copy to clipboard* of the primary repository url +- added: optional flash-based 1-step *copy to clipboard* of the primary repository url +- added: javascript-based 3-step (click, ctrl+c, enter) *copy to clipboard* of the primary repository url + **New:** *web.allowFlashCopyToClipboard = true* - improved: empty repositories now link to the *empty repository* page which gives some direction to the user for the next step in using Gitblit. This page displays the primary push/clone url of the repository and gives sample syntax for the git command-line client. (issue 31) - improved: unit testing framework has been migrated to JUnit4 syntax and the test suite has been redesigned to run all unit tests, including rpc, federation, and git push/clone tests diff --git a/resources/clippy.swf b/resources/clippy.swf new file mode 100644 index 0000000000000000000000000000000000000000..e46886cd1153122af2f216a6a86cbd816615d54d GIT binary patch literal 5380 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 getAttributeNames(); + + // Object's valid parameter names + protected abstract List getParameterNames(); + + // Utility function to get the URL for the object's data + protected String resolveResource(String src) { + // if it's an absolute path, return it: + if (src.startsWith("/") || src.startsWith("http://") || src.startsWith("https://")) + return (src); + + // use the parent container class to resolve the resource reference + Component parent = getParent(); + if (parent instanceof Fragment) { + // must check for fragment, otherwise we end up in Wicket namespace + parent = parent.getParent(); + } + if (parent != null) { + ResourceReference resRef = new ResourceReference(parent.getClass(), src, false); + return (urlFor(resRef).toString()); + } + + return (src); + } + + public void onComponentTag(ComponentTag tag) { + super.onComponentTag(tag); + + // get the attributes from the html-source + IValueMap attributeMap = tag.getAttributes(); + + // set the content type + String contentType = getContentType(); + if (contentType != null && !"".equals(contentType)) + attributeMap.put(ATTRIBUTE_CONTENTTYPE, contentType); + + // set clsid and codebase for IE + if (getClientProperties().isBrowserInternetExplorer()) { + String clsid = getClsid(); + String codeBase = getCodebase(); + + if (clsid != null && !"".equals(clsid)) + attributeMap.put(ATTRIBUTE_CLASSID, clsid); + if (codeBase != null && !"".equals(codeBase)) + attributeMap.put(ATTRIBUTE_CODEBASE, codeBase); + } + + // add all attributes + for (String name : getAttributeNames()) { + String value = getValue(name); + if (value != null) + attributeMap.put(name, value); + } + } + + public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { + Response response = getResponse(); + response.write("\n"); + + // add all object's parameters: + for (String name : getParameterNames()) { + String value = getValue(name); + if (value != null) { + response.write("\n"); + } + } + + super.onComponentTagBody(markupStream, openTag); + } + + // shortcut to the client properties: + protected ClientProperties getClientProperties() { + if (clientProperties == null) { + ClientInfo clientInfo = WebSession.get().getClientInfo(); + + if (clientInfo == null || !(clientInfo instanceof WebClientInfo)) { + clientInfo = new WebClientInfo((WebRequestCycle) getRequestCycle()); + WebSession.get().setClientInfo(clientInfo); + } + + clientProperties = ((WebClientInfo) clientInfo).getProperties(); + } + return (clientProperties); + } +} \ No newline at end of file diff --git a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html index 25ba1559..2b7be0a0 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html +++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html @@ -8,6 +8,25 @@ - [repository url] - + [repository url] + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java index bfb02f13..a98e40ab 100644 --- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java +++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java @@ -17,7 +17,11 @@ package com.gitblit.wicket.panels; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.image.ContextImage; +import org.apache.wicket.markup.html.panel.Fragment; +import com.gitblit.GitBlit; +import com.gitblit.Keys; +import com.gitblit.utils.StringUtils; import com.gitblit.wicket.WicketUtils; public class RepositoryUrlPanel extends BasePanel { @@ -27,9 +31,22 @@ public class RepositoryUrlPanel extends BasePanel { public RepositoryUrlPanel(String wicketId, String url) { super(wicketId); add(new Label("repositoryUrl", url)); - ContextImage img = WicketUtils.newImage("copyIcon", "clipboard_13x13.png"); - WicketUtils.setHtmlTooltip(img, "Manual Copy to Clipboard"); - img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url)); - add(img); + if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) { + // clippy: flash-based copy & paste + Fragment fragment = new Fragment("copyFunction", "clippyPanel", this); + String baseUrl = WicketUtils.getGitblitURL(getRequest()); + ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf"); + clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(url)); + fragment.add(clippy); + add(fragment); + } else { + // javascript: manual copy & paste with modal browser prompt dialog + Fragment fragment = new Fragment("copyFunction", "jsPanel", this); + ContextImage img = WicketUtils.newImage("copyIcon", "clipboard_13x13.png"); + WicketUtils.setHtmlTooltip(img, "Manual Copy to Clipboard"); + img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url)); + fragment.add(img); + add(fragment); + } } } diff --git a/src/com/gitblit/wicket/panels/ShockWaveComponent.java b/src/com/gitblit/wicket/panels/ShockWaveComponent.java new file mode 100644 index 00000000..fa989453 --- /dev/null +++ b/src/com/gitblit/wicket/panels/ShockWaveComponent.java @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.gitblit.wicket.panels; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.wicket.Response; +import org.apache.wicket.markup.ComponentTag; +import org.apache.wicket.markup.MarkupStream; +import org.apache.wicket.util.value.IValueMap; + +/** + * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html + * + * @author Jan Kriesten + * @author manuelbarzi + * @author James Moger + * + */ +public class ShockWaveComponent extends ObjectContainer { + private static final long serialVersionUID = 1L; + + private static final String CONTENTTYPE = "application/x-shockwave-flash"; + private static final String CLSID = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"; + private static final String CODEBASE = "http://fpdownload.adobe.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0"; + + // valid attributes + private static final List attributeNames = Arrays.asList(new String[] { "classid", + "width", "height", "codebase", "align", "base", "data", "flashvars" }); + // valid parameters + private static final List parameterNames = Arrays.asList(new String[] { "devicefont", + "movie", "play", "loop", "quality", "bgcolor", "scale", "salign", "menu", "wmode", + "allowscriptaccess", "seamlesstabbing", "flashvars" }); + + // combined options (to iterate over them) + private static final List optionNames = new ArrayList(attributeNames.size() + + parameterNames.size()); + static { + optionNames.addAll(attributeNames); + optionNames.addAll(parameterNames); + } + + private Map attributes; + private Map parameters; + + public ShockWaveComponent(String id) { + super(id); + + attributes = new HashMap(); + parameters = new HashMap(); + } + + public ShockWaveComponent(String id, String movie) { + this(id); + setValue("movie", movie); + } + + public ShockWaveComponent(String id, String movie, String width, String height) { + this(id); + + setValue("movie", movie); + setValue("width", width); + setValue("height", height); + } + + public void setValue(String name, String value) { + // IE and other browsers handle movie/data differently. So movie is used + // for IE, whereas + // data is used for all other browsers. The class uses movie parameter + // to handle url and + // puts the values to the maps depending on the browser information + String parameter = name.toLowerCase(); + if ("data".equals(parameter)) + parameter = "movie"; + + if ("movie".equals(parameter) && !getClientProperties().isBrowserInternetExplorer()) + attributes.put("data", value); + + if (attributeNames.contains(parameter)) + attributes.put(parameter, value); + else if (parameterNames.contains(parameter)) + parameters.put(parameter, value); + } + + public String getValue(String name) { + String parameter = name.toLowerCase(); + String value = null; + + if ("data".equals(parameter)) { + if (getClientProperties().isBrowserInternetExplorer()) + return null; + parameter = "movie"; + } + + if (attributeNames.contains(parameter)) + value = attributes.get(parameter); + else if (parameterNames.contains(parameter)) + value = parameters.get(parameter); + + // special treatment of movie to resolve to the url + if (value != null && parameter.equals("movie")) + value = resolveResource(value); + + return value; + } + + public void onComponentTag(ComponentTag tag) { + // get options from the markup + IValueMap valueMap = tag.getAttributes(); + + // Iterate over valid options + for (String s : optionNames) { + if (valueMap.containsKey(s)) { + // if option isn't set programmatically, set value from markup + if (!attributes.containsKey(s) && !parameters.containsKey(s)) + setValue(s, valueMap.getString(s)); + // remove attribute - they are added in super.onComponentTag() + // to + // the right place as attribute or param + valueMap.remove(s); + } + } + + super.onComponentTag(tag); + } + + public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) { + + super.onComponentTagBody(markupStream, openTag); + + Response response = getResponse(); + + // add all object's parameters in embed tag too: + response.write("\n"); + + } + + private void addParameter(Response response, String name, String value) { + response.write(" "); + response.write(name); + response.write("=\""); + response.write(value); + response.write("\""); + } + + @Override + protected String getClsid() { + return CLSID; + } + + @Override + protected String getCodebase() { + return CODEBASE; + } + + @Override + protected String getContentType() { + return CONTENTTYPE; + } + + @Override + protected List getAttributeNames() { + return attributeNames; + } + + @Override + protected List getParameterNames() { + return parameterNames; + } +} \ No newline at end of file -- 2.39.5