]> source.dussan.org Git - gitblit.git/commitdiff
Integrated Clippy for a better copy-to-clipboard experience
authorJames Moger <james.moger@gitblit.com>
Tue, 13 Dec 2011 21:51:36 +0000 (16:51 -0500)
committerJames Moger <james.moger@gitblit.com>
Tue, 13 Dec 2011 21:51:36 +0000 (16:51 -0500)
distrib/gitblit.properties
docs/04_design.mkd
docs/04_releases.mkd
resources/clippy.swf [new file with mode: 0644]
src/com/gitblit/wicket/panels/ObjectContainer.java [new file with mode: 0644]
src/com/gitblit/wicket/panels/RepositoryUrlPanel.html
src/com/gitblit/wicket/panels/RepositoryUrlPanel.java
src/com/gitblit/wicket/panels/ShockWaveComponent.java [new file with mode: 0644]

index 39e478858526b83f88687b2b6de86ede122bde3d..537f9b6781a36eb859869b6281bea72b42362d5a 100644 (file)
@@ -121,6 +121,13 @@ web.allowGravatar = true
 # SINCE 0.5.0   \r
 web.allowZipDownloads = true\r
 \r
+# Use Clippy (Flash solution) to provide a copy-to-clipboard button.\r
+# If false, a button with a more primitive JavaScript-based prompt box will\r
+# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative.\r
+#\r
+# SINCE 0.8.0\r
+web.allowFlashCopyToClipboard = true\r
+\r
 # Default number of entries to include in RSS Syndication links\r
 #\r
 # SINCE 0.5.0\r
index 921bc8bb80cf3cd35f7f251f5f353d058eb3ab02..3fd13d474ac65e53dd535e901717ac1252c3e9c2 100644 (file)
@@ -11,6 +11,7 @@
 The following dependencies are bundled with Gitblit.\r
 \r
 - [Bootstrap](http://twitter.github.com/bootstrap) (Apache 2.0)\r
+- [Clippy](https://github.com/mojombo/clippy) (MIT)\r
 - [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0)\r
 - [Commons Daemon](http://commons.apache.org/daemon) (Apache 2.0)\r
 - magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY)\r
index bf709183fc3661a98d19559f6ab88c96c83a4c61..9a4e4a83944cd00c2915c75c5b5a80cc4ac24f6d 100644 (file)
@@ -15,7 +15,9 @@ This user service implementation allows for serialization and deserialization of
    **New:** *web.timeFormat = HH:mm*  \r
    **New:** *web.datestampLongFormat = EEEE, MMMM d, yyyy*  \r
 - fixed: several a bugs in FileUserService related to cleaning up old repository permissions on a rename or delete\r
-- added: primitive technique for manual *copy to clipboard* of the primary repository url\r
+- added: optional flash-based 1-step *copy to clipboard* of the primary repository url\r
+- added: javascript-based 3-step (click, ctrl+c, enter) *copy to clipboard* of the primary repository url\r
+   **New:** *web.allowFlashCopyToClipboard = true*  \r
 - 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)\r
 - 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\r
 \r
diff --git a/resources/clippy.swf b/resources/clippy.swf
new file mode 100644 (file)
index 0000000..e46886c
Binary files /dev/null and b/resources/clippy.swf differ
diff --git a/src/com/gitblit/wicket/panels/ObjectContainer.java b/src/com/gitblit/wicket/panels/ObjectContainer.java
new file mode 100644 (file)
index 0000000..79bd3a7
--- /dev/null
@@ -0,0 +1,160 @@
+/*\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements.  See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License.  You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.wicket.panels;\r
+\r
+import java.util.List;\r
+\r
+import org.apache.wicket.Component;\r
+import org.apache.wicket.ResourceReference;\r
+import org.apache.wicket.Response;\r
+import org.apache.wicket.markup.ComponentTag;\r
+import org.apache.wicket.markup.MarkupStream;\r
+import org.apache.wicket.markup.html.WebMarkupContainer;\r
+import org.apache.wicket.markup.html.panel.Fragment;\r
+import org.apache.wicket.protocol.http.ClientProperties;\r
+import org.apache.wicket.protocol.http.WebRequestCycle;\r
+import org.apache.wicket.protocol.http.WebSession;\r
+import org.apache.wicket.protocol.http.request.WebClientInfo;\r
+import org.apache.wicket.request.ClientInfo;\r
+import org.apache.wicket.util.value.IValueMap;\r
+\r
+import com.gitblit.wicket.WicketUtils;\r
+\r
+/**\r
+ * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html\r
+ */\r
+public abstract class ObjectContainer extends WebMarkupContainer {\r
+\r
+       private static final long serialVersionUID = 1L;\r
+\r
+       // Some general attributes for the object tag:\r
+       private static final String ATTRIBUTE_CONTENTTYPE = "type";\r
+       private static final String ATTRIBUTE_CLASSID = "classid";\r
+       private static final String ATTRIBUTE_CODEBASE = "codebase";\r
+\r
+       // This is used for browser specific adjustments\r
+       private ClientProperties clientProperties = null;\r
+\r
+       public ObjectContainer(String id) {\r
+               super(id);\r
+       }\r
+\r
+       // Set an attribute/property\r
+       public abstract void setValue(String name, String value);\r
+\r
+       // Get an attribute/property\r
+       public abstract String getValue(String name);\r
+\r
+       // Set the object's content type\r
+       protected abstract String getContentType();\r
+\r
+       // Set the object's clsid (for IE)\r
+       protected abstract String getClsid();\r
+\r
+       // Where to get the browser plugin (for IE)\r
+       protected abstract String getCodebase();\r
+\r
+       // Object's valid attribute names\r
+       protected abstract List<String> getAttributeNames();\r
+\r
+       // Object's valid parameter names\r
+       protected abstract List<String> getParameterNames();\r
+\r
+       // Utility function to get the URL for the object's data\r
+       protected String resolveResource(String src) {\r
+               // if it's an absolute path, return it:\r
+               if (src.startsWith("/") || src.startsWith("http://") || src.startsWith("https://"))\r
+                       return (src);\r
+\r
+               // use the parent container class to resolve the resource reference\r
+               Component parent = getParent();\r
+               if (parent instanceof Fragment) {\r
+                       // must check for fragment, otherwise we end up in Wicket namespace\r
+                       parent = parent.getParent();\r
+               }               \r
+               if (parent != null) {\r
+                       ResourceReference resRef = new ResourceReference(parent.getClass(), src, false);\r
+                       return (urlFor(resRef).toString());\r
+               }\r
+\r
+               return (src);\r
+       }\r
+\r
+       public void onComponentTag(ComponentTag tag) {\r
+               super.onComponentTag(tag);\r
+\r
+               // get the attributes from the html-source\r
+               IValueMap attributeMap = tag.getAttributes();\r
+\r
+               // set the content type\r
+               String contentType = getContentType();\r
+               if (contentType != null && !"".equals(contentType))\r
+                       attributeMap.put(ATTRIBUTE_CONTENTTYPE, contentType);\r
+\r
+               // set clsid and codebase for IE\r
+               if (getClientProperties().isBrowserInternetExplorer()) {\r
+                       String clsid = getClsid();\r
+                       String codeBase = getCodebase();\r
+\r
+                       if (clsid != null && !"".equals(clsid))\r
+                               attributeMap.put(ATTRIBUTE_CLASSID, clsid);\r
+                       if (codeBase != null && !"".equals(codeBase))\r
+                               attributeMap.put(ATTRIBUTE_CODEBASE, codeBase);\r
+               }\r
+\r
+               // add all attributes\r
+               for (String name : getAttributeNames()) {\r
+                       String value = getValue(name);\r
+                       if (value != null)\r
+                               attributeMap.put(name, value);\r
+               }\r
+       }\r
+\r
+       public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {\r
+               Response response = getResponse();\r
+               response.write("\n");\r
+\r
+               // add all object's parameters:\r
+               for (String name : getParameterNames()) {\r
+                       String value = getValue(name);\r
+                       if (value != null) {\r
+                               response.write("<param name=\"");\r
+                               response.write(name);\r
+                               response.write("\" value=\"");\r
+                               response.write(value);\r
+                               response.write("\"/>\n");\r
+                       }\r
+               }\r
+\r
+               super.onComponentTagBody(markupStream, openTag);\r
+       }\r
+\r
+       // shortcut to the client properties:\r
+       protected ClientProperties getClientProperties() {\r
+               if (clientProperties == null) {\r
+                       ClientInfo clientInfo = WebSession.get().getClientInfo();\r
+\r
+                       if (clientInfo == null || !(clientInfo instanceof WebClientInfo)) {\r
+                               clientInfo = new WebClientInfo((WebRequestCycle) getRequestCycle());\r
+                               WebSession.get().setClientInfo(clientInfo);\r
+                       }\r
+\r
+                       clientProperties = ((WebClientInfo) clientInfo).getProperties();\r
+               }\r
+               return (clientProperties);\r
+       }\r
+}
\ No newline at end of file
index 25ba1559e7f73ce55679b480d556403a78e95833..2b7be0a01c26f6f6024522ba7902d9167c91c108 100644 (file)
@@ -8,6 +8,25 @@
 </wicket:head>\r
 \r
 <wicket:panel>\r
-       <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>    \r
-</wicket:panel>        \r
+       <span wicket:id="repositoryUrl" style="color: blue;">[repository url]</span><span wicket:id="copyFunction"></span>\r
+    \r
+    <!-- Plain JavaScript manual copy & paste -->\r
+    <wicket:fragment wicket:id="jsPanel">\r
+       <span class="btn" style="padding:0px 3px 0px 3px;vertical-align:middle;">\r
+               <img wicket:id="copyIcon" style="padding-top:1px;"></img>\r
+       </span>\r
+    </wicket:fragment>\r
+    \r
+    <!-- flash-based button-press copy & paste -->\r
+    <wicket:fragment wicket:id="clippyPanel">\r
+               <object style="padding:0px 2px;vertical-align:middle;"\r
+                       wicket:id="clippy"\r
+                       width="110" \r
+                       height="14"\r
+                       bgcolor="#ffffff" \r
+                       quality="high"\r
+                       wmode="transparent"\r
+                       allowScriptAccess="always"></object>\r
+       </wicket:fragment>\r
+</wicket:panel>\r
 </html>
\ No newline at end of file
index bfb02f13e21f09e88d63d911c42eb138883cc776..a98e40abd2a42a69976d05130aa59850749a01e6 100644 (file)
@@ -17,7 +17,11 @@ package com.gitblit.wicket.panels;
 \r
 import org.apache.wicket.markup.html.basic.Label;\r
 import org.apache.wicket.markup.html.image.ContextImage;\r
+import org.apache.wicket.markup.html.panel.Fragment;\r
 \r
+import com.gitblit.GitBlit;\r
+import com.gitblit.Keys;\r
+import com.gitblit.utils.StringUtils;\r
 import com.gitblit.wicket.WicketUtils;\r
 \r
 public class RepositoryUrlPanel extends BasePanel {\r
@@ -27,9 +31,22 @@ public class RepositoryUrlPanel extends BasePanel {
        public RepositoryUrlPanel(String wicketId, String url) {\r
                super(wicketId);\r
                add(new Label("repositoryUrl", url));\r
-               ContextImage img = WicketUtils.newImage("copyIcon", "clipboard_13x13.png");\r
-               WicketUtils.setHtmlTooltip(img, "Manual Copy to Clipboard");\r
-               img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url));\r
-               add(img);\r
+               if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {\r
+                       // clippy: flash-based copy & paste\r
+                       Fragment fragment = new Fragment("copyFunction", "clippyPanel", this);\r
+                       String baseUrl = WicketUtils.getGitblitURL(getRequest());\r
+                       ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");\r
+                       clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(url));\r
+                       fragment.add(clippy);\r
+                       add(fragment);\r
+               } else {\r
+                       // javascript: manual copy & paste with modal browser prompt dialog\r
+                       Fragment fragment = new Fragment("copyFunction", "jsPanel", this);\r
+                       ContextImage img = WicketUtils.newImage("copyIcon", "clipboard_13x13.png");\r
+                       WicketUtils.setHtmlTooltip(img, "Manual Copy to Clipboard");\r
+                       img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url));\r
+                       fragment.add(img);\r
+                       add(fragment);\r
+               }\r
        }\r
 }\r
diff --git a/src/com/gitblit/wicket/panels/ShockWaveComponent.java b/src/com/gitblit/wicket/panels/ShockWaveComponent.java
new file mode 100644 (file)
index 0000000..fa98945
--- /dev/null
@@ -0,0 +1,205 @@
+/*\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements.  See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License.  You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.wicket.panels;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.HashMap;\r
+import java.util.List;\r
+import java.util.Map;\r
+\r
+import org.apache.wicket.Response;\r
+import org.apache.wicket.markup.ComponentTag;\r
+import org.apache.wicket.markup.MarkupStream;\r
+import org.apache.wicket.util.value.IValueMap;\r
+\r
+/**\r
+ * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html\r
+ * \r
+ * @author Jan Kriesten\r
+ * @author manuelbarzi\r
+ * @author James Moger\r
+ * \r
+ */\r
+public class ShockWaveComponent extends ObjectContainer {\r
+       private static final long serialVersionUID = 1L;\r
+\r
+       private static final String CONTENTTYPE = "application/x-shockwave-flash";\r
+       private static final String CLSID = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000";\r
+       private static final String CODEBASE = "http://fpdownload.adobe.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0";\r
+\r
+       // valid attributes\r
+       private static final List<String> attributeNames = Arrays.asList(new String[] { "classid",\r
+                       "width", "height", "codebase", "align", "base", "data", "flashvars" });\r
+       // valid parameters\r
+       private static final List<String> parameterNames = Arrays.asList(new String[] { "devicefont",\r
+                       "movie", "play", "loop", "quality", "bgcolor", "scale", "salign", "menu", "wmode",\r
+                       "allowscriptaccess", "seamlesstabbing", "flashvars" });\r
+\r
+       // combined options (to iterate over them)\r
+       private static final List<String> optionNames = new ArrayList<String>(attributeNames.size()\r
+                       + parameterNames.size());\r
+       static {\r
+               optionNames.addAll(attributeNames);\r
+               optionNames.addAll(parameterNames);\r
+       }\r
+\r
+       private Map<String, String> attributes;\r
+       private Map<String, String> parameters;\r
+\r
+       public ShockWaveComponent(String id) {\r
+               super(id);\r
+\r
+               attributes = new HashMap<String, String>();\r
+               parameters = new HashMap<String, String>();\r
+       }\r
+       \r
+       public ShockWaveComponent(String id, String movie) {\r
+               this(id);\r
+               setValue("movie", movie);\r
+       }\r
+\r
+       public ShockWaveComponent(String id, String movie, String width, String height) {\r
+               this(id);\r
+\r
+               setValue("movie", movie);\r
+               setValue("width", width);\r
+               setValue("height", height);\r
+       }\r
+\r
+       public void setValue(String name, String value) {\r
+               // IE and other browsers handle movie/data differently. So movie is used\r
+               // for IE, whereas\r
+               // data is used for all other browsers. The class uses movie parameter\r
+               // to handle url and\r
+               // puts the values to the maps depending on the browser information\r
+               String parameter = name.toLowerCase();\r
+               if ("data".equals(parameter))\r
+                       parameter = "movie";\r
+\r
+               if ("movie".equals(parameter) && !getClientProperties().isBrowserInternetExplorer())\r
+                       attributes.put("data", value);\r
+\r
+               if (attributeNames.contains(parameter))\r
+                       attributes.put(parameter, value);\r
+               else if (parameterNames.contains(parameter))\r
+                       parameters.put(parameter, value);\r
+       }\r
+\r
+       public String getValue(String name) {\r
+               String parameter = name.toLowerCase();\r
+               String value = null;\r
+\r
+               if ("data".equals(parameter)) {\r
+                       if (getClientProperties().isBrowserInternetExplorer())\r
+                               return null;\r
+                       parameter = "movie";\r
+               }\r
+\r
+               if (attributeNames.contains(parameter))\r
+                       value = attributes.get(parameter);\r
+               else if (parameterNames.contains(parameter))\r
+                       value = parameters.get(parameter);\r
+\r
+               // special treatment of movie to resolve to the url\r
+               if (value != null && parameter.equals("movie"))\r
+                       value = resolveResource(value);\r
+\r
+               return value;\r
+       }\r
+\r
+       public void onComponentTag(ComponentTag tag) {\r
+               // get options from the markup\r
+               IValueMap valueMap = tag.getAttributes();\r
+\r
+               // Iterate over valid options\r
+               for (String s : optionNames) {\r
+                       if (valueMap.containsKey(s)) {\r
+                               // if option isn't set programmatically, set value from markup\r
+                               if (!attributes.containsKey(s) && !parameters.containsKey(s))\r
+                                       setValue(s, valueMap.getString(s));\r
+                               // remove attribute - they are added in super.onComponentTag()\r
+                               // to\r
+                               // the right place as attribute or param\r
+                               valueMap.remove(s);\r
+                       }\r
+               }\r
+\r
+               super.onComponentTag(tag);\r
+       }\r
+\r
+       public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {\r
+\r
+               super.onComponentTagBody(markupStream, openTag);\r
+\r
+               Response response = getResponse();\r
+\r
+               // add all object's parameters in embed tag too:\r
+               response.write("<embed");\r
+               addParameter(response, "type", CONTENTTYPE);\r
+               for (String name : getParameterNames()) {\r
+                       String value = getValue(name);\r
+                       if (value != null) {\r
+                               name = "movie".equals(name) ? "src" : name;\r
+                               addParameter(response, name, value);\r
+                       }\r
+               }\r
+               for (String name : getAttributeNames()) {\r
+                       if ("width".equals(name) || "height".equals(name)) {\r
+                               String value = getValue(name);\r
+                               if (value != null) {\r
+                                       addParameter(response, name, value);\r
+                               }\r
+                       }\r
+               }\r
+               response.write(" />\n");\r
+\r
+       }\r
+\r
+       private void addParameter(Response response, String name, String value) {\r
+               response.write(" ");\r
+               response.write(name);\r
+               response.write("=\"");\r
+               response.write(value);\r
+               response.write("\"");\r
+       }\r
+\r
+       @Override\r
+       protected String getClsid() {\r
+               return CLSID;\r
+       }\r
+\r
+       @Override\r
+       protected String getCodebase() {\r
+               return CODEBASE;\r
+       }\r
+\r
+       @Override\r
+       protected String getContentType() {\r
+               return CONTENTTYPE;\r
+       }\r
+\r
+       @Override\r
+       protected List<String> getAttributeNames() {\r
+               return attributeNames;\r
+       }\r
+\r
+       @Override\r
+       protected List<String> getParameterNames() {\r
+               return parameterNames;\r
+       }\r
+}
\ No newline at end of file