James Moger
2011-12-13 3b6904b1d92b987e308f5fb3308fec215ba1f1ae
Integrated Clippy for a better copy-to-clipboard experience
3 files added
5 files modified
417 ■■■■■ changed files
distrib/gitblit.properties 7 ●●●●● patch | view | raw | blame | history
docs/04_design.mkd 1 ●●●● patch | view | raw | blame | history
docs/04_releases.mkd 4 ●●● patch | view | raw | blame | history
resources/clippy.swf patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ObjectContainer.java 160 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoryUrlPanel.html 21 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/RepositoryUrlPanel.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/panels/ShockWaveComponent.java 205 ●●●●● patch | view | raw | blame | history
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
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)
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
resources/clippy.swf
Binary files differ
src/com/gitblit/wicket/panels/ObjectContainer.java
New file
@@ -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);
    }
}
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>
    <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>
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));
        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));
        add(img);
            fragment.add(img);
            add(fragment);
        }
    }
}
src/com/gitblit/wicket/panels/ShockWaveComponent.java
New file
@@ -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;
    }
}