From 3b6904b1d92b987e308f5fb3308fec215ba1f1ae Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Tue, 13 Dec 2011 16:51:36 -0500
Subject: [PATCH] Integrated Clippy for a better copy-to-clipboard experience

---
 resources/clippy.swf                                  |    0 
 docs/04_releases.mkd                                  |    4 
 src/com/gitblit/wicket/panels/RepositoryUrlPanel.html |   23 ++
 docs/04_design.mkd                                    |    1 
 src/com/gitblit/wicket/panels/ObjectContainer.java    |  160 ++++++++++++++++++++
 distrib/gitblit.properties                            |    7 
 src/com/gitblit/wicket/panels/ShockWaveComponent.java |  205 +++++++++++++++++++++++++
 src/com/gitblit/wicket/panels/RepositoryUrlPanel.java |   25 ++
 8 files changed, 418 insertions(+), 7 deletions(-)

diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties
index 39e4788..537f9b6 100644
--- a/distrib/gitblit.properties
+++ b/distrib/gitblit.properties
@@ -121,6 +121,13 @@
 # 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 921bc8b..3fd13d4 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 bf70918..9a4e4a8 100644
--- a/docs/04_releases.mkd
+++ b/docs/04_releases.mkd
@@ -15,7 +15,9 @@
    **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 0000000..e46886c
--- /dev/null
+++ b/resources/clippy.swf
Binary files differ
diff --git a/src/com/gitblit/wicket/panels/ObjectContainer.java b/src/com/gitblit/wicket/panels/ObjectContainer.java
new file mode 100644
index 0000000..79bd3a7
--- /dev/null
+++ b/src/com/gitblit/wicket/panels/ObjectContainer.java
@@ -0,0 +1,160 @@
+/*
+ * 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.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ResourceReference;
+import org.apache.wicket.Response;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.protocol.http.ClientProperties;
+import org.apache.wicket.protocol.http.WebRequestCycle;
+import org.apache.wicket.protocol.http.WebSession;
+import org.apache.wicket.protocol.http.request.WebClientInfo;
+import org.apache.wicket.request.ClientInfo;
+import org.apache.wicket.util.value.IValueMap;
+
+import com.gitblit.wicket.WicketUtils;
+
+/**
+ * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html
+ */
+public abstract class ObjectContainer extends WebMarkupContainer {
+
+	private static final long serialVersionUID = 1L;
+
+	// Some general attributes for the object tag:
+	private static final String ATTRIBUTE_CONTENTTYPE = "type";
+	private static final String ATTRIBUTE_CLASSID = "classid";
+	private static final String ATTRIBUTE_CODEBASE = "codebase";
+
+	// This is used for browser specific adjustments
+	private ClientProperties clientProperties = null;
+
+	public ObjectContainer(String id) {
+		super(id);
+	}
+
+	// Set an attribute/property
+	public abstract void setValue(String name, String value);
+
+	// Get an attribute/property
+	public abstract String getValue(String name);
+
+	// Set the object's content type
+	protected abstract String getContentType();
+
+	// Set the object's clsid (for IE)
+	protected abstract String getClsid();
+
+	// Where to get the browser plugin (for IE)
+	protected abstract String getCodebase();
+
+	// Object's valid attribute names
+	protected abstract List<String> getAttributeNames();
+
+	// Object's valid parameter names
+	protected abstract List<String> 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("<param name=\"");
+				response.write(name);
+				response.write("\" value=\"");
+				response.write(value);
+				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 25ba155..2b7be0a 100644
--- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html
+++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.html
@@ -8,6 +8,25 @@
 </wicket:head>
 
 <wicket:panel>
-	<span wicket:id="repositoryUrl" style="color: blue;">[repository url]</span><span style="padding-left:5px;"><span class="btn" style="padding:0px 3px;vertical-align:middle;"><img wicket:id="copyIcon" style="padding-top:1px;"></img></span></span>	
-</wicket:panel>	
+	<span wicket:id="repositoryUrl" style="color: blue;">[repository url]</span><span wicket:id="copyFunction"></span>
+    
+    <!-- Plain JavaScript manual copy & paste -->
+    <wicket:fragment wicket:id="jsPanel">
+    	<span class="btn" style="padding:0px 3px 0px 3px;vertical-align:middle;">
+    		<img wicket:id="copyIcon" style="padding-top:1px;"></img>
+    	</span>
+    </wicket:fragment>
+    
+    <!-- flash-based button-press copy & paste -->
+    <wicket:fragment wicket:id="clippyPanel">
+   		<object style="padding:0px 2px;vertical-align:middle;"
+   			wicket:id="clippy"
+   			width="110" 
+   			height="14"
+   			bgcolor="#ffffff" 
+       		quality="high"
+       		wmode="transparent"
+       		allowScriptAccess="always"></object>
+	</wicket:fragment>
+</wicket:panel>
 </html>
\ 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 bfb02f1..a98e40a 100644
--- a/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java
+++ b/src/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -17,7 +17,11 @@
 
 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 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 0000000..fa98945
--- /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<String> attributeNames = Arrays.asList(new String[] { "classid",
+			"width", "height", "codebase", "align", "base", "data", "flashvars" });
+	// valid parameters
+	private static final List<String> 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<String> optionNames = new ArrayList<String>(attributeNames.size()
+			+ parameterNames.size());
+	static {
+		optionNames.addAll(attributeNames);
+		optionNames.addAll(parameterNames);
+	}
+
+	private Map<String, String> attributes;
+	private Map<String, String> parameters;
+
+	public ShockWaveComponent(String id) {
+		super(id);
+
+		attributes = new HashMap<String, String>();
+		parameters = new HashMap<String, String>();
+	}
+	
+	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("<embed");
+		addParameter(response, "type", CONTENTTYPE);
+		for (String name : getParameterNames()) {
+			String value = getValue(name);
+			if (value != null) {
+				name = "movie".equals(name) ? "src" : name;
+				addParameter(response, name, value);
+			}
+		}
+		for (String name : getAttributeNames()) {
+			if ("width".equals(name) || "height".equals(name)) {
+				String value = getValue(name);
+				if (value != null) {
+					addParameter(response, name, value);
+				}
+			}
+		}
+		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<String> getAttributeNames() {
+		return attributeNames;
+	}
+
+	@Override
+	protected List<String> getParameterNames() {
+		return parameterNames;
+	}
+}
\ No newline at end of file

--
Gitblit v1.9.1