Add navigation links to side-by-side and unified diff screens

The navigation links allow to navigate to the previous/next file in
the change and back to the change screen. If a formatter is available
for the previous/next file, the diff is again shown in the HTML diff
view, otherwise it goes back to the normal side-by-side/unified diff
view.

Change-Id: I1ba63b90110e8d41e0ab10083b15ae92bda58134
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocs.gwt.xml b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocs.gwt.xml
index 7855c78..405a81f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocs.gwt.xml
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/XDocs.gwt.xml
@@ -18,6 +18,7 @@
   <!-- Inherit the core Web Toolkit stuff.                        -->
   <inherits name="com.google.gwt.user.User"/>
   <!-- Other module inherits                                      -->
+  <inherits name="com.google.gerrit.GerritGwtUICommon"/>
   <inherits name="com.google.gerrit.Plugin"/>
   <inherits name="com.google.gwt.http.HTTP"/>
   <inherits name="com.google.gwt.json.JSON"/>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeApi.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeApi.java
index 69ecf7d..44d1d49 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeApi.java
@@ -31,6 +31,10 @@
     return call(id, "detail");
   }
 
+  public static RestApi revision(String changeId, int patchSetId) {
+    return change(changeId).view("revisions").id(patchSetId);
+  }
+
   private static RestApi call(String id, String action) {
     return change(id).view(action);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeInfo.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeInfo.java
index caad355..9f28d99 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/ChangeInfo.java
@@ -20,6 +20,7 @@
 public class ChangeInfo extends JavaScriptObject {
   public final native String project() /*-{ return this.project; }-*/;
   public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
+  public final native int _number() /*-{ return this._number; }-*/;
 
   protected ChangeInfo() {
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/DiffApi.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/DiffApi.java
new file mode 100644
index 0000000..7bd9b7f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/DiffApi.java
@@ -0,0 +1,31 @@
+// 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.xdocs.client;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.plugin.client.rpc.RestApi;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class DiffApi {
+
+  public static void list(String changeId, int patchSetId, Integer basePatchSetId,
+      AsyncCallback<NativeMap<FileInfo>> cb) {
+    RestApi api = ChangeApi.revision(changeId, patchSetId).view("files");
+    if (basePatchSetId != null) {
+      api.addParameter("base", basePatchSetId);
+    }
+    api.get(NativeMap.copyKeysIntoChildren("path", cb));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/FileInfo.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/FileInfo.java
new file mode 100644
index 0000000..8fff48b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/FileInfo.java
@@ -0,0 +1,53 @@
+// 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.xdocs.client;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+public class FileInfo extends JavaScriptObject {
+  public final native String path() /*-{ return this.path; }-*/;
+  public final native boolean binary() /*-{ return this.binary || false; }-*/;
+
+  public static void sortFileInfoByPath(JsArray<FileInfo> list) {
+    Collections.sort(Natives.asList(list), new Comparator<FileInfo>() {
+      @Override
+      public int compare(FileInfo a, FileInfo b) {
+        if (Patch.COMMIT_MSG.equals(a.path())) {
+          return -1;
+        } else if (Patch.COMMIT_MSG.equals(b.path())) {
+          return 1;
+        }
+        return a.path().compareTo(b.path());
+      }
+    });
+  }
+
+  public static String getFileName(String path) {
+    String fileName = Patch.COMMIT_MSG.equals(path)
+        ? "Commit Message"
+        : path;
+    int s = fileName.lastIndexOf('/');
+    return s >= 0 ? fileName.substring(s + 1) : fileName;
+  }
+
+  protected FileInfo() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/VoidResult.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/VoidResult.java
new file mode 100644
index 0000000..d381cd6
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/VoidResult.java
@@ -0,0 +1,26 @@
+// 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.xdocs.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public final class VoidResult extends JavaScriptObject {
+  protected VoidResult() {
+  }
+
+  public static VoidResult create() {
+    return createObject().cast();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocApi.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocApi.java
new file mode 100644
index 0000000..e8de4ab
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocApi.java
@@ -0,0 +1,66 @@
+// 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.xdocs.client;
+
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class XDocApi {
+  public static String getUrl(String projectName, String revision,
+      String path) {
+    StringBuilder url = new StringBuilder();
+    url.append("plugins/");
+    url.append(Plugin.get().getName());
+    url.append("/project/");
+    url.append(URL.encodeQueryString(projectName));
+    if (revision != null && !"HEAD".equals(revision)) {
+      url.append("/rev/");
+      url.append(URL.encodeQueryString(revision));
+    }
+    url.append("/");
+    url.append(URL.encodeQueryString(path));
+    return url.toString();
+  }
+
+  public static void checkHtml(String url,
+      final AsyncCallback<VoidResult> callback) {
+    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
+    try {
+      builder.sendRequest(null, new RequestCallback() {
+        public void onResponseReceived(Request request, Response response) {
+          int status = response.getStatusCode();
+          if (200 <= status && status < 300) {
+            callback.onSuccess(VoidResult.create());
+          } else {
+            callback.onFailure(new RequestException(status + " "
+                + response.getStatusText()));
+          }
+        }
+
+        public void onError(Request request, Throwable caught) {
+          callback.onFailure(caught);
+        }
+      });
+    } catch (RequestException e) {
+      callback.onFailure(e);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocDiffScreen.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocDiffScreen.java
index eb05e5a..12606b6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocDiffScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocDiffScreen.java
@@ -14,37 +14,50 @@
 
 package com.googlesource.gerrit.plugins.xdocs.client;
 
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.plugin.client.Plugin;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.InlineHyperlink;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 
 import com.googlesource.gerrit.plugins.xdocs.client.ChangeInfo.RevisionInfo;
 
 public abstract class XDocDiffScreen extends VerticalPanel {
+  private final String changeId;
   private final String path;
   private String revisionA;
   private String revisionB;
+  private int patchSet;
+  private Integer base;
+  private FlowPanel iconPanel;
 
-  XDocDiffScreen(final String change, final String patchSet, String path) {
+  XDocDiffScreen(String changeId, final String patchSet, String path) {
     setStyleName("xdocs-panel");
 
+    this.changeId = changeId;
     this.path = path;
 
-    ChangeApi.getChangeInfo(change, new AsyncCallback<ChangeInfo>() {
+    ChangeApi.getChangeInfo(changeId, new AsyncCallback<ChangeInfo>() {
 
       @Override
       public void onSuccess(ChangeInfo change) {
-        addPathHeader(change);
         setRevisions(change, patchSet);
+        addHeader(change);
         display(change);
       }
 
       @Override
       public void onFailure(Throwable caught) {
-        showError("Unable to load change " + change + ": " + caught.getMessage());
+        showError("Unable to load change " + XDocDiffScreen.this.changeId
+            + ": " + caught.getMessage());
       }
     });
   }
@@ -63,41 +76,163 @@
     return revisionB;
   }
 
-  private void setRevisions(ChangeInfo change, String patchSet) {
-    int i = patchSet.indexOf("..");
+  private void setRevisions(ChangeInfo change, String patchSetString) {
+    int i = patchSetString.indexOf("..");
     if (i > 0) {
-      revisionA = getRevision(change, patchSet.substring(0, i));
-      if (patchSet.length() > i + 2) {
-        revisionB = getRevision(change, patchSet.substring(i + 2));
+      base = parsePatchSet(patchSetString.substring(0, i));
+      revisionA = getRevision(change, base);
+      if (patchSetString.length() > i + 2) {
+        patchSet = parsePatchSet(patchSetString.substring(i + 2));
+        revisionB = getRevision(change, patchSet);
       } else {
-        throw new IllegalArgumentException("Invalid patch set: " + patchSet);
+        throw new IllegalArgumentException("Invalid patch set: " + patchSetString);
       }
     } else {
+      patchSet = parsePatchSet(patchSetString);
       revisionB = getRevision(change, patchSet);
       revisionA = this.revisionB + "^1";
     }
   }
 
-  private static String getRevision(ChangeInfo change, String patchSet) {
+  private static String getRevision(ChangeInfo change, int patchSet) {
     for (RevisionInfo rev : Natives.asList(change.revisions().values())) {
-      try {
-        if (rev._number() == Integer.valueOf(patchSet)) {
-          return rev.ref();
-        }
-      } catch (NumberFormatException e) {
-        throw new IllegalArgumentException("Invalid patch set: " + patchSet);
+      if (rev._number() == patchSet) {
+        return rev.ref();
       }
     }
     throw new IllegalArgumentException("Patch set " + patchSet + " not found.");
   }
 
-  private void addPathHeader(ChangeInfo change) {
+  private static int parsePatchSet(String patchSet) {
+    try {
+      return Integer.valueOf(patchSet);
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("Invalid patch set: " + patchSet);
+    }
+  }
+
+  private void addHeader(ChangeInfo change) {
     HorizontalPanel p = new HorizontalPanel();
     p.setStyleName("xdocs-header");
+    p.add(getPathHeader(change));
+
+    iconPanel = new FlowPanel();
+    iconPanel.setStyleName("xdocs-icon-panel");
+    p.add(iconPanel);
+    addNavigationButtons(change);
+
+    add(p);
+  }
+
+  private Widget getPathHeader(ChangeInfo change) {
+    HorizontalPanel p = new HorizontalPanel();
+    p.setStyleName("xdocs-file-header");
     p.add(new InlineHyperlink(change.project(), "/admin/projects/" + change.project()));
     p.add(new Label("/"));
     p.add(new Label(path));
-    add(p);
+    return p;
+  }
+
+  private void addNavigationButtons(final ChangeInfo change) {
+    DiffApi.list(changeId, patchSet, base,
+        new AsyncCallback<NativeMap<FileInfo>>() {
+      @Override
+      public void onSuccess(NativeMap<FileInfo> result) {
+        JsArray<FileInfo> files = result.values();
+        FileInfo.sortFileInfoByPath(files);
+        int index = 0;
+        for (int i = 0; i < files.length(); i++) {
+          if (path.equals(files.get(i).path())) {
+            index = i;
+            break;
+          }
+        }
+
+        FileInfo prevInfo = index == 0 ? null : files.get(index - 1);
+        if (prevInfo != null) {
+          iconPanel.add(createNavLink(XDocsPlugin.RESOURCES.goPrev(),
+              change, patchSet, base, prevInfo));
+        }
+
+        iconPanel.add(createIcon(XDocsPlugin.RESOURCES.goUp(),
+            "Up to change", toChange(change)));
+
+        FileInfo nextInfo = index == files.length() - 1
+            ? null
+            : files.get(index + 1);
+        if (nextInfo != null) {
+          iconPanel.add(createNavLink(XDocsPlugin.RESOURCES.goNext(),
+              change, patchSet, base, nextInfo));
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        showError("Unable to load files of change " + changeId + ": "
+            + caught.getMessage());
+      }
+    });
+  }
+
+  private InlineHyperlink createNavLink(ImageResource res,
+      final ChangeInfo change, final int patchSet, final Integer base,
+      final FileInfo file) {
+    final InlineHyperlink link = createIcon(
+        res, FileInfo.getFileName(file.path()),
+        toFile(change, patchSet, base, file));
+    XDocApi.checkHtml(XDocApi.getUrl(change.project(),
+        getRevision(change, patchSet), file.path()),
+        new AsyncCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult result) {
+        link.setTargetHistoryToken(
+            toPreview(change, patchSet, base, file));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+      }
+    });
+    return link;
+  }
+
+  private static InlineHyperlink createIcon(ImageResource res, String tooltip, String target) {
+    InlineHyperlink l = new InlineHyperlink(
+        AbstractImagePrototype.create(res).getHTML(), true, target);
+    if (tooltip != null) {
+      l.setTitle(tooltip);
+    }
+    return l;
+  }
+
+  private String toPreview(ChangeInfo change, int patchSet,
+      Integer base, FileInfo file) {
+    String panel = getPanel();
+    return "/x/" + Plugin.get().getName()
+        + toPatchSet(change, patchSet, base)
+        + file.path()
+        + (panel != null ? "," + panel : "");
+  }
+
+  private String toFile(ChangeInfo change, int patchSet, Integer base,
+      FileInfo file) {
+    String panel = file.binary() ? "unified" : getPanel();
+    return toPatchSet(change, patchSet, base)
+        + file.path()
+        + (panel != null ? "," + panel : "");
+  }
+
+  protected String getPanel() {
+    return null;
+  }
+
+  private static String toPatchSet(ChangeInfo change, int patchSet, Integer base) {
+    return toChange(change)
+        + (base != null ? patchSet + ".." + base : patchSet) + "/";
+  }
+
+  private static String toChange(ChangeInfo change) {
+    return "/c/" + change._number() + "/";
   }
 
   protected void showError(String message) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
index 9c3439e..f60e140 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocScreen.java
@@ -14,15 +14,9 @@
 
 package com.googlesource.gerrit.plugins.xdocs.client;
 
-import com.google.gerrit.plugin.client.Plugin;
 import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
 import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Frame;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.InlineHyperlink;
@@ -38,7 +32,7 @@
       String revision = URL.decode(screen.getToken(2));
       String path = URL.decode(screen.getToken(3));
       screen.show(new XDocScreen(projectName, revision, path));
-      screen.setWindowTitle(getFileName(path));
+      screen.setWindowTitle(FileInfo.getFileName(path));
     }
   }
 
@@ -48,7 +42,7 @@
       String projectName = URL.decode(screen.getToken(1));
       String path = URL.decode(screen.getToken(2));
       screen.show(new XDocScreen(projectName, "HEAD", path));
-      screen.setWindowTitle(getFileName(path));
+      screen.setWindowTitle(FileInfo.getFileName(path));
     }
   }
 
@@ -56,37 +50,29 @@
     setStyleName("xdocs-panel");
 
     HorizontalPanel p = new HorizontalPanel();
-    p.setStyleName("xdocs-header");
+    p.setStyleName("xdocs-file-header");
     p.add(new InlineHyperlink(projectName, "/admin/projects/" + projectName));
     p.add(new Label("/"));
     p.add(new Label(path));
     p.add(new Label("(" + revision + ")"));
     add(p);
 
-    final String url = getUrl(projectName, revision, path);
-    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
-    try {
-      builder.sendRequest(null, new RequestCallback() {
-        public void onResponseReceived(Request request, Response response) {
-          int status = response.getStatusCode();
-          if (200 <= status && status < 300) {
-            String frameId = "xdoc_iframe";
-            Frame frame = new Frame(url);
-            frame.getElement().setId(frameId);
-            resize(frame, frameId);
-            add(frame);
-          } else {
-            showError(status + " " + response.getStatusText());
-          }
-        }
+    final String url = XDocApi.getUrl(projectName, revision, path);
+    XDocApi.checkHtml(url, new AsyncCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult result) {
+        String frameId = "xdoc_iframe";
+        Frame frame = new Frame(url);
+        frame.getElement().setId(frameId);
+        resize(frame, frameId);
+        add(frame);
+      }
 
-        public void onError(Request request, Throwable exception) {
-          showError(exception.getMessage());
-        }
-      });
-    } catch (RequestException e) {
-      showError(e.getMessage());
-    }
+      @Override
+      public void onFailure(Throwable caught) {
+        showError(caught.getMessage());
+      }
+    });
   }
 
   private void showError(String message) {
@@ -95,29 +81,6 @@
     add(l);
   }
 
-  public static String getFileName(String path) {
-    String fileName = Patch.COMMIT_MSG.equals(path)
-        ? "Commit Message"
-        : path;
-    int s = fileName.lastIndexOf('/');
-    return s >= 0 ? fileName.substring(s + 1) : fileName;
-  }
-
-  public static String getUrl(String projectName, String revision, String fileName) {
-    StringBuilder url = new StringBuilder();
-    url.append("plugins/");
-    url.append(Plugin.get().getName());
-    url.append("/project/");
-    url.append(URL.encodeQueryString(projectName));
-    if (revision != null && !"HEAD".equals(revision)) {
-      url.append("/rev/");
-      url.append(URL.encodeQueryString(revision));
-    }
-    url.append("/");
-    url.append(URL.encodeQueryString(fileName));
-    return url.toString();
-  }
-
   public static void resize(Widget w, String id) {
     StringBuilder autoResizeScript = new StringBuilder();
     autoResizeScript.append("if (document.getElementById) {");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocSideBySideDiffScreen.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocSideBySideDiffScreen.java
index f46344e..9209a36 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocSideBySideDiffScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocSideBySideDiffScreen.java
@@ -27,7 +27,7 @@
       String patchSet = URL.decode(screen.getToken(2));
       String path = URL.decode(screen.getToken(4));
       screen.show(new XDocSideBySideDiffScreen(change, patchSet, path));
-      screen.setWindowTitle(XDocScreen.getFileName(path));
+      screen.setWindowTitle(FileInfo.getFileName(path));
     }
   }
 
@@ -39,13 +39,13 @@
   protected void display(ChangeInfo change) {
     String frameIdA = "xdoc_sidebyside_diff_a_iframe";
     Frame frameA =
-        new Frame(XDocScreen.getUrl(change.project(), getRevisionSideA(), getPath()));
+        new Frame(XDocApi.getUrl(change.project(), getRevisionSideA(), getPath()));
     frameA.getElement().setId(frameIdA);
     XDocScreen.resize(frameA, frameIdA);
 
     String frameIdB = "xdoc_sidebyside_diff_b_iframe";
     Frame frameB =
-        new Frame(XDocScreen.getUrl(change.project(), getRevisionSideB(), getPath()));
+        new Frame(XDocApi.getUrl(change.project(), getRevisionSideB(), getPath()));
     frameB.getElement().setId(frameIdB);
     XDocScreen.resize(frameB, frameIdB);
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocUnifiedDiffScreen.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocUnifiedDiffScreen.java
index 57d6dcb..551d76e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocUnifiedDiffScreen.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocUnifiedDiffScreen.java
@@ -26,7 +26,7 @@
       String patchSet = URL.decode(screen.getToken(2));
       String path = URL.decode(screen.getToken(4));
       screen.show(new XDocUnifiedDiffScreen(change, patchSet, path));
-      screen.setWindowTitle(XDocScreen.getFileName(path));
+      screen.setWindowTitle(FileInfo.getFileName(path));
     }
   }
 
@@ -38,7 +38,7 @@
   protected void display(ChangeInfo change) {
     String frameId = "xdoc_unified_diff_iframe";
     Frame frame =
-        new Frame(XDocScreen.getUrl(change.project(), getRevision(), getPath()));
+        new Frame(XDocApi.getUrl(change.project(), getRevision(), getPath()));
     frame.getElement().setId(frameId);
     XDocScreen.resize(frame, frameId);
     add(frame);
@@ -47,4 +47,9 @@
   private String getRevision() {
     return getRevisionA() + "<->" + getRevisionB();
   }
+
+  @Override
+  protected String getPanel() {
+    return "unified";
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocsPlugin.java b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocsPlugin.java
index 08507c0..71c2714 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocsPlugin.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/client/XDocsPlugin.java
@@ -14,10 +14,13 @@
 
 package com.googlesource.gerrit.plugins.xdocs.client;
 
+import com.google.gerrit.client.Resources;
 import com.google.gerrit.plugin.client.Plugin;
 import com.google.gerrit.plugin.client.PluginEntryPoint;
+import com.google.gwt.core.client.GWT;
 
 public class XDocsPlugin extends PluginEntryPoint {
+  public static final Resources RESOURCES = GWT.create(Resources.class);
 
   @Override
   public void onPluginLoad() {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css b/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
index f2db809..9372584 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
+++ b/src/main/java/com/googlesource/gerrit/plugins/xdocs/public/xdocs.css
@@ -3,10 +3,21 @@
   width: 100%;
 }
 
-.xdocs-header td {
+.xdocs-file-header td {
   padding-right: 5px;
 }
 
+.xdocs-icon-panel {
+  position: absolute;
+  right: 10px;
+  height: 16px;
+  line-height: 16px;
+}
+
+.xdocs-icon-panel img {
+  padding-right: 3px;
+}
+
 .xdocs-panel iframe {
   width: 100%;
   border-width: 1px;