Support decoration of image links in Gerrit WebUI

If links to screenshots are posted in review comments a reviewer needs
to open that link in order to see the image. Instead allow to decorate
the image link so that the image can be seen immediately without
opening the link. Two ways of link decoration are supported:

1. Tooltip:
On mouse over a tooltip with the image is shown.

2. Inline:
The image is inlined and shown instead of the URL.

Change-Id: I4b4e420d2d850aacf5898defb05df52c2ebfdcbb
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
index cf2d9b5..52635bb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/GetConfig.java
@@ -37,10 +37,15 @@
     ConfigInfo info = new ConfigInfo();
     info.defaultProject =
         Objects.firstNonNull(cfg.getString("defaultProject"), "All-Projects");
+    info.linkDecoration = cfg.getEnum("linkDecoration", LinkDecoration.NONE);
+    if (LinkDecoration.NONE.equals(info.linkDecoration)) {
+      info.linkDecoration = null;
+    }
     return info;
   }
 
   public class ConfigInfo {
     String defaultProject;
+    LinkDecoration linkDecoration;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
index afb283d..d72589c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.GwtPlugin;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 
@@ -25,5 +26,7 @@
   protected void configureServlets() {
     DynamicSet.bind(binder(), WebUiPlugin.class)
         .toInstance(new GwtPlugin("imagare"));
+    DynamicSet.bind(binder(), WebUiPlugin.class)
+        .toInstance(new JavaScriptPlugin("imagare.js"));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/LinkDecoration.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/LinkDecoration.java
new file mode 100644
index 0000000..d710e0d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/LinkDecoration.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.imagare;
+
+public enum LinkDecoration {
+  NONE, TOOLTIP, INLINE
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
index 38964e4..45b30dc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/PutConfig.java
@@ -38,6 +38,7 @@
 public class PutConfig implements RestModifyView<ConfigResource, Input>{
   public static class Input {
     public String defaultProject;
+    public LinkDecoration linkDecoration;
   }
 
   private final PluginConfigFactory cfgFactory;
@@ -65,6 +66,14 @@
       cfg.setString("plugin", pluginName, "defaultProject",
           Strings.emptyToNull(input.defaultProject));
     }
+    if (input.linkDecoration != null) {
+      if (LinkDecoration.NONE.equals(input.linkDecoration)) {
+        cfg.unset("plugin", pluginName, "linkDecoration");
+      } else {
+        cfg.setEnum("plugin", pluginName, "linkDecoration",
+            input.linkDecoration);
+      }
+    }
     cfg.save();
     cfgFactory.getFromGerritConfig(pluginName, true);
     return Response.<String> ok("OK");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
index dcea642..6bb83a3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ConfigInfo.java
@@ -18,7 +18,17 @@
 
 public class ConfigInfo extends JavaScriptObject {
   final native String getDefaultProject() /*-{ return this.default_project }-*/;
+
+  final LinkDecoration getLinkDecoration() {
+    if (link_decoration() == null) {
+      return LinkDecoration.NONE;
+    }
+    return LinkDecoration.valueOf(link_decoration());
+  }
+  private final native String link_decoration() /*-{ return this.link_decoration; }-*/;
+
   final native void setDefaultProject(String p) /*-{ this.default_project = p; }-*/;
+  final native void setLinkDecoration(String d) /*-{ this.link_decoration = d; }-*/;
 
   static ConfigInfo create() {
     ConfigInfo g = (ConfigInfo) createObject();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
index cab716d..1057a56 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImagareAdminScreen.java
@@ -24,6 +24,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
@@ -38,6 +39,7 @@
   }
 
   private TextBox projectBox;
+  private ListBox linkDecorationBox;
   private Button saveButton;
 
   ImagareAdminScreen() {
@@ -59,13 +61,28 @@
 
   private void display(ConfigInfo info) {
     HorizontalPanel p = new HorizontalPanel();
-    p.setStyleName("imagare-project-panel");
+    p.setStyleName("imagare-label-panel");
     p.add(new Label("Project:"));
     projectBox = new TextBox();
     projectBox.setValue(info.getDefaultProject());
     p.add(projectBox);
     add(p);
 
+    p = new HorizontalPanel();
+    p.setStyleName("imagare-label-panel");
+    p.add(new Label("Link Decoration:"));
+    linkDecorationBox = new ListBox();
+    int i = 0;
+    for (LinkDecoration v : LinkDecoration.values()) {
+      linkDecorationBox.addItem(v.name());
+      if (v.equals(info.getLinkDecoration())) {
+        linkDecorationBox.setSelectedIndex(i);
+      }
+      i++;
+    }
+    p.add(linkDecorationBox);
+    add(p);
+
     HorizontalPanel buttons = new HorizontalPanel();
     add(buttons);
 
@@ -78,7 +95,8 @@
     });
     buttons.add(saveButton);
     saveButton.setEnabled(false);
-    new OnEditEnabler(saveButton, projectBox);
+    OnEditEnabler onEditEnabler = new OnEditEnabler(saveButton, projectBox);
+    onEditEnabler.listenTo(linkDecorationBox);
 
     projectBox.setFocus(true);
     saveButton.setEnabled(false);
@@ -87,6 +105,7 @@
   private void doSave() {
     ConfigInfo in = ConfigInfo.create();
     in.setDefaultProject(projectBox.getValue());
+    in.setLinkDecoration(linkDecorationBox.getValue(linkDecorationBox.getSelectedIndex()));
     new RestApi("config").id("server").view(Plugin.get().getPluginName(), "config")
         .put(in, new AsyncCallback<JavaScriptObject>() {
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImageUploadScreen.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImageUploadScreen.java
index a52372f..4af24b7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImageUploadScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/ImageUploadScreen.java
@@ -40,7 +40,7 @@
     setStyleName("imagare-image-upload-screen");
 
     HorizontalPanel p = new HorizontalPanel();
-    p.setStyleName("imagare-project-panel");
+    p.setStyleName("imagare-label-panel");
     p.add(new Label("Project:"));
     projectBox = new TextBox();
     p.add(projectBox);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/client/LinkDecoration.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/LinkDecoration.java
new file mode 100644
index 0000000..e208af6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/client/LinkDecoration.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2014 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.googlesource.gerrit.plugins.imagare.client;
+
+public enum LinkDecoration {
+  NONE, TOOLTIP, INLINE
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/public/imagare.css b/src/main/java/com/googlesource/gerrit/plugins/imagare/public/imagare.css
index b30fdff..4afd247 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/public/imagare.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/public/imagare.css
@@ -4,15 +4,15 @@
   border-spacing: 0px 5px;
 }
 
-.imagare-project-panel {
+.imagare-label-panel {
   margin-bottom: 10px;
 }
 
-.imagare-project-panel td {
+.imagare-label-panel td {
   vertical-align: middle !important;
 }
 
-.imagare-project-panel td div {
+.imagare-label-panel td div {
   margin-right: 5px;
 }
 
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index c433612..c1fc220 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -12,3 +12,10 @@
 <a id="block">
 `plugin.@PLUGIN@.defaultProject`
 :	The project to which images are uploaded per default.
+
+<a id="link-decoration">
+`plugin.@PLUGIN@.linkDecoration`
+:	Decoration for image links in the Gerrit WebUI.
+    `NONE`: no decoration, `TOOLTIP`: the image is shown as tooltip on
+    mouse over an image link, `INLINE`: the image is inlined instead of
+    the URL.
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index daf8464..dd8e007 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -67,6 +67,10 @@
 
 * _default\_project_: The project to which images should be uploaded by
   default.
+* _link\_decoration_: Decoration for image links in the Gerrit WebUI.
+  `NONE`: no decoration, `TOOLTIP`: the image is shown as tooltip on
+  mouse over an image link, `INLINE`: the image is inlined instead of
+  the URL.
 
 SEE ALSO
 --------
diff --git a/src/main/resources/static/imagare.js b/src/main/resources/static/imagare.js
new file mode 100644
index 0000000..0e5a5bf
--- /dev/null
+++ b/src/main/resources/static/imagare.js
@@ -0,0 +1,63 @@
+// Copyright (C) 2014 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.
+
+Gerrit.install(function(self) {
+    function onHistory(t) {
+      Gerrit.get('/config/server/config', function(r) {
+        if ('TOOLTIP' === r.link_decoration) {
+          addTooltips();
+        } else if ('INLINE' === r.link_decoration) {
+          inlineImages();
+        }
+      });
+    }
+
+    function inlineImages() {
+      var l = document.links;
+      for(var i = 0; i < l.length; i++) {
+        if (isImage(l[i].href)) {
+          var a = document.createElement('a');
+          a.setAttribute('href', l[i].href);
+          var img = document.createElement('img');
+          img.setAttribute('src', l[i].href);
+          img.setAttribute('style', 'border: 1px solid #B3B2B2;');
+          a.appendChild(img);
+          l[i].parentNode.replaceChild(a, l[i]);
+        }
+      }
+    }
+
+    function addTooltips() {
+      var l = document.links;
+      for(var i = 0; i < l.length; i++) {
+        if (isImage(l[i].href)) {
+          l[i].onmouseover = function (evt) {
+            var img = document.createElement('img');
+            img.setAttribute('src', this.href);
+            img.setAttribute('style', 'border: 1px solid #B3B2B2; position: absolute; bottom: ' + (this.offsetHeight + 3) + 'px');
+            this.parentNode.insertBefore(img, this);
+            this.onmouseout = function (evt) {
+              this.parentNode.removeChild(this.previousSibling);
+            }
+          }
+        }
+      }
+    }
+
+    function isImage(href) {
+      return href.match(window.location.hostname + '.*src/.*/rev/.*/.*\.(jpg|jpeg|png|gif|bmp|ico|svg|tif|tiff)')
+    }
+
+    Gerrit.on('history', onHistory);
+  });