Use 'f' in a patch to browse the list of files in the patch set

When the user presses 'f' inside of a patch viewer we now open up
the list of patches from the same patch set, and let the user move
through them with the keyboard.  Selecting a file opens it in the
viewer, but leaves the dialog open until they trigger it to close.

Bug: GERRIT-136
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gerrit/client/Link.java b/src/main/java/com/google/gerrit/client/Link.java
index 6ec5a2a..545fbe9 100644
--- a/src/main/java/com/google/gerrit/client/Link.java
+++ b/src/main/java/com/google/gerrit/client/Link.java
@@ -32,8 +32,7 @@
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.data.AccountInfo;
 import com.google.gerrit.client.data.ChangeInfo;
-import com.google.gerrit.client.patches.PatchSideBySideScreen;
-import com.google.gerrit.client.patches.PatchUnifiedScreen;
+import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.reviewdb.Account;
 import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.client.reviewdb.Change;
@@ -182,11 +181,11 @@
     if (token.startsWith("patch,")) {
       p = "patch,sidebyside,";
       if (token.startsWith(p))
-        return new PatchSideBySideScreen(Patch.Key.parse(skip(p, token)));
+        return new PatchScreen.SideBySide(Patch.Key.parse(skip(p, token)), null);
 
       p = "patch,unified,";
       if (token.startsWith(p))
-        return new PatchUnifiedScreen(Patch.Key.parse(skip(p, token)));
+        return new PatchScreen.Unified(Patch.Key.parse(skip(p, token)), null);
     }
 
     p = "change,publish,";
diff --git a/src/main/java/com/google/gerrit/client/SearchPanel.java b/src/main/java/com/google/gerrit/client/SearchPanel.java
index ec5b4ca..6a9aa8e 100644
--- a/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -90,15 +90,15 @@
     super.onLoad();
     if (regFocus == null) {
       regFocus =
-          GlobalKey
-              .addApplication(new KeyCommand(0, '/', Gerrit.C.keySearch()) {
-                @Override
-                public void onKeyPress(final KeyPressEvent event) {
-                  event.preventDefault();
-                  searchBox.setFocus(true);
-                  searchBox.selectAll();
-                }
-              });
+          GlobalKey.addApplication(this, new KeyCommand(0, '/', Gerrit.C
+              .keySearch()) {
+            @Override
+            public void onKeyPress(final KeyPressEvent event) {
+              event.preventDefault();
+              searchBox.setFocus(true);
+              searchBox.selectAll();
+            }
+          });
     }
   }
 
diff --git a/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 9facb85..1d8b1e0 100644
--- a/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -132,8 +132,8 @@
     }
 
     @Override
-    protected void onOpenItem(final AccountGroup item) {
-      History.newItem(Link.toAccountGroup(item.getId()));
+    protected void onOpenRow(final int row) {
+      History.newItem(Link.toAccountGroup(getRowItem(row).getId()));
     }
 
     void display(final List<AccountGroup> result) {
diff --git a/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index ca97dd8..dfcfbf6 100644
--- a/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -98,8 +98,8 @@
     }
 
     @Override
-    protected void onOpenItem(final Project item) {
-      History.newItem(link(item));
+    protected void onOpenRow(final int row) {
+      History.newItem(link(getRowItem(row)));
     }
 
     private String link(final Project item) {
diff --git a/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index c0edb6a..36b30c97 100644
--- a/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -117,8 +117,8 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
-    regNavigation = GlobalKey.add(keysNavigation);
-    regAction = GlobalKey.add(keysAction);
+    regNavigation = GlobalKey.add(this, keysNavigation);
+    regAction = GlobalKey.add(this, keysAction);
   }
 
   public void refresh() {
diff --git a/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 33f4a82..d07bccd 100644
--- a/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -35,7 +35,6 @@
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Widget;
@@ -140,7 +139,8 @@
   }
 
   @Override
-  protected void onOpenItem(final ChangeInfo c) {
+  protected void onOpenRow(final int row) {
+    final ChangeInfo c = getRowItem(row);
     Gerrit.display(Link.toChange(c), new ChangeScreen(c));
   }
 
@@ -307,9 +307,9 @@
     }
 
     @Override
-    protected void onClick(final Event event) {
+    public void go() {
       movePointerTo(id);
-      super.onClick(event);
+      super.go();
     }
   }
 
diff --git a/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java b/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
index 681c41e..918f5ac 100644
--- a/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
+++ b/src/main/java/com/google/gerrit/client/changes/PatchSetPanel.java
@@ -149,9 +149,7 @@
 
 
     patchTable = new PatchTable();
-    patchTable.setSavePointerId("patchTable "
-        + changeDetail.getChange().getChangeId() + " "
-        + patchSet.getPatchSetId());
+    patchTable.setSavePointerId("PatchTable " + patchSet.getId());
     patchTable.display(info.getKey(), detail.getPatches());
 
     body.add(infoTable);
diff --git a/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index e74dac3..4494067 100644
--- a/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.Link;
 import com.google.gerrit.client.reviewdb.Patch;
 import com.google.gerrit.client.reviewdb.PatchSet;
+import com.google.gerrit.client.ui.DirectScreenLink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.PatchLink;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DeferredCommand;
-import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.IncrementalCommand;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -41,6 +41,8 @@
 public class PatchTable extends Composite {
   private final FlowPanel myBody;
   private PatchSet.Id psid;
+  private Command onLoadCommand;
+  private MyTable myTable;
   private String savePointerId;
 
   public PatchTable() {
@@ -50,6 +52,8 @@
 
   public void display(final PatchSet.Id id, final List<Patch> list) {
     psid = id;
+    myTable = null;
+
     final DisplayCommand cmd = new DisplayCommand(list);
     if (cmd.execute()) {
       cmd.initMeter();
@@ -63,7 +67,37 @@
     savePointerId = id;
   }
 
+  public boolean isLoaded() {
+    return myTable != null;
+  }
+
+  public void onTableLoaded(final Command cmd) {
+    if (myTable != null) {
+      cmd.execute();
+    } else {
+      onLoadCommand = cmd;
+    }
+  }
+
+  public void setRegisterKeys(final boolean on) {
+    myTable.setRegisterKeys(on);
+  }
+
+  public void movePointerTo(final Patch.Key k) {
+    myTable.movePointerTo(k);
+  }
+
+  public void notifyDraftDelta(final Patch.Key k, final int delta) {
+    if (myTable != null) {
+      myTable.notifyDraftDelta(k, delta);
+    }
+  }
+
   private class MyTable extends NavigationTable<Patch> {
+    private static final int C_PATH = 2;
+    private static final int C_DRAFT = 3;
+    private static final int C_SIDEBYSIDE = 4;
+
     MyTable() {
       keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
       keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
@@ -83,14 +117,30 @@
       setSavePointerId(PatchTable.this.savePointerId);
     }
 
+    void notifyDraftDelta(final Patch.Key key, final int delta) {
+      final int row = findRow(key);
+      if (0 <= row) {
+        final Patch p = getRowItem(row);
+        if (p != null) {
+          p.setDraftCount(p.getDraftCount() + delta);
+          final SafeHtmlBuilder m = new SafeHtmlBuilder();
+          appendCommentCount(m, p);
+          SafeHtml.set(table, row, C_DRAFT, m);
+        }
+      }
+    }
+
     @Override
     public void resetHtml(final SafeHtml html) {
       super.resetHtml(html);
     }
 
+    @Override
+    public void movePointerTo(Object oldId) {
+      super.movePointerTo(oldId);
+    }
+
     void initializeRow(final int row, final Patch p) {
-      final int C_PATH = 2;
-      final int C_SIDEBYSIDE = 4;
       setRowItem(row, p);
 
       Widget nameCol;
@@ -184,18 +234,7 @@
       m.openTd();
       m.addStyleName(S_DATA_CELL);
       m.addStyleName("CommentCell");
-      if (p.getCommentCount() > 0) {
-        m.append(Util.M.patchTableComments(p.getCommentCount()));
-      }
-      if (p.getDraftCount() > 0) {
-        if (p.getCommentCount() > 0) {
-          m.append(", ");
-        }
-        m.openSpan();
-        m.setStyleName("Drafts");
-        m.append(Util.M.patchTableDrafts(p.getDraftCount()));
-        m.closeSpan();
-      }
+      appendCommentCount(m, p);
       m.closeTd();
 
       switch (p.getPatchType()) {
@@ -247,6 +286,21 @@
       m.closeTr();
     }
 
+    void appendCommentCount(final SafeHtmlBuilder m, final Patch p) {
+      if (p.getCommentCount() > 0) {
+        m.append(Util.M.patchTableComments(p.getCommentCount()));
+      }
+      if (p.getDraftCount() > 0) {
+        if (p.getCommentCount() > 0) {
+          m.append(", ");
+        }
+        m.openSpan();
+        m.setStyleName("Drafts");
+        m.append(Util.M.patchTableDrafts(p.getDraftCount()));
+        m.closeSpan();
+      }
+    }
+
     private void openlink(final SafeHtmlBuilder m, final int colspan) {
       m.openTd();
       m.addStyleName(S_DATA_CELL);
@@ -274,8 +328,14 @@
     }
 
     @Override
-    protected void onOpenItem(final Patch item) {
-      History.newItem(Link.toPatchSideBySide(item.getKey()));
+    protected void onOpenRow(final int row) {
+      Widget link = table.getWidget(row, C_PATH);
+      if (link instanceof FlowPanel) {
+        link = ((FlowPanel) link).getWidget(0);
+      }
+      if (link instanceof DirectScreenLink) {
+        ((DirectScreenLink) link).go();
+      }
     }
   }
 
@@ -342,16 +402,21 @@
     }
 
     void showTable() {
-      myBody.clear();
-      myBody.add(table);
+      PatchTable.this.myBody.clear();
+      PatchTable.this.myBody.add(table);
+      PatchTable.this.myTable = table;
       table.finishDisplay();
+      if (PatchTable.this.onLoadCommand != null) {
+        PatchTable.this.onLoadCommand.execute();
+        PatchTable.this.onLoadCommand = null;
+      }
     }
 
     void initMeter() {
       if (meter == null) {
         meter = new ProgressBar(Util.M.loadingPatchSet(psid.get()));
-        myBody.clear();
-        myBody.add(meter);
+        PatchTable.this.myBody.clear();
+        PatchTable.this.myBody.add(meter);
       }
       updateMeter();
     }
diff --git a/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index b3bf55b..13a6a64 100644
--- a/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -20,14 +20,17 @@
 import com.google.gerrit.client.SignOutEvent;
 import com.google.gerrit.client.SignOutHandler;
 import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.data.AccountInfoCache;
 import com.google.gerrit.client.data.PatchScript;
+import com.google.gerrit.client.data.PatchSetDetail;
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.client.reviewdb.Patch;
 import com.google.gerrit.client.reviewdb.PatchLineComment;
 import com.google.gerrit.client.reviewdb.PatchSet;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -48,6 +51,7 @@
 
 public abstract class AbstractPatchContentTable extends NavigationTable<Object> {
   private static final long AGE = 7 * 24 * 60 * 60 * 1000L;
+  protected PatchTable fileList;
   protected AccountInfoCache accountCache = AccountInfoCache.empty();
   protected Patch.Key patchKey;
   protected PatchSet.Id idSideA;
@@ -64,6 +68,7 @@
     keysNavigation.add(new NextKeyCommand(0, 'j', PatchUtil.C.lineNext()));
     keysNavigation.add(new PrevChunkKeyCmd(0, 'p', PatchUtil.C.chunkPrev()));
     keysNavigation.add(new NextChunkKeyCmd(0, 'n', PatchUtil.C.chunkNext()));
+    keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C
@@ -84,6 +89,12 @@
     table.setStyleName("gerrit-PatchContentTable");
   }
 
+  void notifyDraftDelta(final int delta) {
+    if (fileList != null) {
+      fileList.notifyDraftDelta(patchKey, delta);
+    }
+  }
+
   @Override
   protected void onLoad() {
     super.onLoad();
@@ -329,7 +340,8 @@
   }
 
   @Override
-  protected void onOpenItem(final Object item) {
+  protected void onOpenRow(final int row) {
+    final Object item = getRowItem(row);
     if (item instanceof CommentList) {
       for (final ComplexDisclosurePanel p : ((CommentList) item).panels) {
         p.setOpen(!p.isOpen());
@@ -543,4 +555,28 @@
       moveToNextChunk(getCurrentRow());
     }
   }
+
+  public class FileListCmd extends KeyCommand {
+    public FileListCmd(int mask, int key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      if (fileList == null || fileList.isAttached()) {
+        final PatchSet.Id psid = patchKey.getParentKey();
+        fileList = new PatchTable();
+        fileList.setSavePointerId("PatchTable " + psid);
+        Util.DETAIL_SVC.patchSetDetail(psid,
+            new GerritCallback<PatchSetDetail>() {
+              public void onSuccess(final PatchSetDetail result) {
+                fileList.display(psid, result.getPatches());
+              }
+            });
+      }
+
+      final PatchBrowserPopup p = new PatchBrowserPopup(patchKey, fileList);
+      p.open();
+    }
+  }
 }
diff --git a/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index b55d5f0..9725ea2 100644
--- a/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -249,6 +249,9 @@
     PatchUtil.DETAIL_SVC.saveDraft(comment,
         new GerritCallback<PatchLineComment>() {
           public void onSuccess(final PatchLineComment result) {
+            if (isNew()) {
+              notifyDraftDelta(1);
+            }
             comment = result;
             text.setReadOnly(false);
             cancel.setEnabled(true);
@@ -267,6 +270,16 @@
         });
   }
 
+  private void notifyDraftDelta(final int delta) {
+    Widget p = getParent();
+    if (p != null) {
+      p = p.getParent();
+      if (p != null) {
+        ((AbstractPatchContentTable) p).notifyDraftDelta(delta);
+      }
+    }
+  }
+
   private void onDiscard() {
     expandTimer.cancel();
     if (isNew()) {
@@ -283,6 +296,7 @@
     PatchUtil.DETAIL_SVC.deleteDraft(comment.getKey(),
         new GerritCallback<VoidResult>() {
           public void onSuccess(final VoidResult result) {
+            notifyDraftDelta(-1);
             removeUI();
           }
 
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java b/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
new file mode 100644
index 0000000..30ec97a
--- /dev/null
+++ b/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2009 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.google.gerrit.client.patches;
+
+import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.reviewdb.Patch;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.user.client.PluginSafeDialogBox;
+
+class PatchBrowserPopup extends PluginSafeDialogBox implements PositionCallback {
+  private final Patch.Key callerKey;
+  private final PatchTable fileList;
+  private final ScrollPanel sp;
+
+  PatchBrowserPopup(final Patch.Key pk, final PatchTable fl) {
+    super(true/* autohide */, false/* modal */);
+
+    callerKey = pk;
+    fileList = fl;
+    sp = new ScrollPanel(fileList);
+
+    final FlowPanel body = new FlowPanel();
+    body.setStyleName("gerrit-PatchBrowserPopup-Body");
+    body.add(sp);
+
+    setText(Util.M.patchSetHeader(callerKey.getParentKey().get()));
+    setWidget(body);
+    addStyleName("gerrit-PatchBrowserPopup");
+  }
+
+  @Override
+  public void setPosition(final int myWidth, int myHeight) {
+    final int dLeft = (Window.getClientWidth() - myWidth) >> 1;
+    final int cHeight = Window.getClientHeight();
+    final int cHeight2 = 2 * cHeight / 3;
+    final int sLeft = Window.getScrollLeft();
+    final int sTop = Window.getScrollTop();
+
+    if (myHeight > cHeight2) {
+      sp.setHeight((cHeight2 - 50) + "px");
+      myHeight = getOffsetHeight();
+    }
+    setPopupPosition(sLeft + dLeft, (sTop + cHeight) - (myHeight + 10));
+  }
+
+  public void open() {
+    sp.setWidth((Window.getClientWidth() - 60) + "px");
+    if (!fileList.isLoaded()) {
+      sp.setHeight("22px");
+    }
+    setPopupPositionAndShow(this);
+    GlobalKey.dialog(this);
+    if (fileList.isLoaded()) {
+      installFileList();
+    } else {
+      fileList.onTableLoaded(new Command() {
+        @Override
+        public void execute() {
+          sp.setHeight("");
+          setPosition(getOffsetWidth(), getOffsetHeight());
+          installFileList();
+        }
+      });
+    }
+  }
+
+  private void installFileList() {
+    fileList.setRegisterKeys(true);
+    fileList.movePointerTo(callerKey);
+  }
+}
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 11c6e0a..2074c9b 100644
--- a/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -36,6 +36,7 @@
   String lineNext();
   String chunkPrev();
   String chunkNext();
+  String fileList();
   String commentInsert();
   String commentSaveDraft();
   String commentDiscard();
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index a61fd53..cd1f6d3 100644
--- a/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -17,6 +17,7 @@
 lineNext = Next line
 chunkPrev = Previous diff chunk or comment
 chunkNext = Next diff chunk or comment
+fileList = Browse files in patch set
 commentInsert = Create a new inline comment
 commentSaveDraft = Save draft comment
 commentDiscard = Discard draft comment
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 24a658e..33d491a 100644
--- a/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.data.PatchScript;
 import com.google.gerrit.client.reviewdb.AccountGeneralPreferences;
 import com.google.gerrit.client.reviewdb.Change;
@@ -29,7 +30,30 @@
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
 public abstract class PatchScreen extends Screen {
+  public static class SideBySide extends PatchScreen {
+    public SideBySide(final Patch.Key id, final PatchTable files) {
+      super(id, files);
+    }
+
+    @Override
+    protected SideBySideTable createContentTable() {
+      return new SideBySideTable();
+    }
+  }
+
+  public static class Unified extends PatchScreen {
+    public Unified(final Patch.Key id, final PatchTable files) {
+      super(id, files);
+    }
+
+    @Override
+    protected UnifiedDiffTable createContentTable() {
+      return new UnifiedDiffTable();
+    }
+  }
+
   protected final Patch.Key patchKey;
+  protected PatchTable fileList;
   protected PatchSet.Id idSideA;
   protected PatchSet.Id idSideB;
   protected int contextLines;
@@ -37,14 +61,15 @@
   private DisclosurePanel historyPanel;
   private HistoryTable historyTable;
   private Label noDifference;
-  private AbstractPatchContentTable patchTable;
+  private AbstractPatchContentTable contentTable;
 
   private int rpcSequence;
   private PatchScript script;
   private CommentDetail comments;
 
-  protected PatchScreen(final Patch.Key id) {
+  protected PatchScreen(final Patch.Key id, final PatchTable files) {
     patchKey = id;
+    fileList = files;
     idSideA = null;
     idSideB = id.getParentKey();
 
@@ -82,12 +107,15 @@
     noDifference = new Label(PatchUtil.C.noDifference());
     noDifference.setStyleName("gerrit-PatchNoDifference");
     noDifference.setVisible(false);
-    patchTable = createPatchTable();
+
+    contentTable = createContentTable();
+    contentTable.fileList = fileList;
+    fileList = null;
 
     final FlowPanel fp = new FlowPanel();
     fp.setStyleName("gerrit-SideBySideScreen-SideBySideTable");
     fp.add(noDifference);
-    fp.add(patchTable);
+    fp.add(contentTable);
     add(fp);
   }
 
@@ -100,10 +128,10 @@
   @Override
   public void registerKeys() {
     super.registerKeys();
-    patchTable.setRegisterKeys(patchTable.isVisible());
+    contentTable.setRegisterKeys(contentTable.isVisible());
   }
 
-  protected abstract AbstractPatchContentTable createPatchTable();
+  protected abstract AbstractPatchContentTable createContentTable();
 
   protected void refresh(final boolean isFirst) {
     final int rpcseq = ++rpcSequence;
@@ -170,9 +198,9 @@
         historyPanel.setVisible(false);
       }
 
-      patchTable.display(patchKey, idSideA, idSideB, script);
-      patchTable.display(comments);
-      patchTable.finishDisplay();
+      contentTable.display(patchKey, idSideA, idSideB, script);
+      contentTable.display(comments);
+      contentTable.finishDisplay();
       showPatch(true);
 
       script = null;
@@ -184,7 +212,7 @@
 
   private void showPatch(final boolean showPatch) {
     noDifference.setVisible(!showPatch);
-    patchTable.setVisible(showPatch);
-    patchTable.setRegisterKeys(isCurrentView() && showPatch);
+    contentTable.setVisible(showPatch);
+    contentTable.setRegisterKeys(isCurrentView() && showPatch);
   }
 }
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchSideBySideScreen.java b/src/main/java/com/google/gerrit/client/patches/PatchSideBySideScreen.java
deleted file mode 100644
index e20ffd0..0000000
--- a/src/main/java/com/google/gerrit/client/patches/PatchSideBySideScreen.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2008 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.google.gerrit.client.patches;
-
-import com.google.gerrit.client.reviewdb.Patch;
-
-public class PatchSideBySideScreen extends PatchScreen {
-  public PatchSideBySideScreen(final Patch.Key id) {
-    super(id);
-  }
-
-  @Override
-  protected  SideBySideTable createPatchTable() {
-    return new SideBySideTable();
-  }
-}
diff --git a/src/main/java/com/google/gerrit/client/patches/PatchUnifiedScreen.java b/src/main/java/com/google/gerrit/client/patches/PatchUnifiedScreen.java
deleted file mode 100644
index dc07996..0000000
--- a/src/main/java/com/google/gerrit/client/patches/PatchUnifiedScreen.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2008 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.google.gerrit.client.patches;
-
-import com.google.gerrit.client.reviewdb.Patch;
-
-public class PatchUnifiedScreen extends PatchScreen {
-  public PatchUnifiedScreen(final Patch.Key id) {
-    super(id);
-  }
-
-  @Override
-  protected UnifiedDiffTable createPatchTable() {
-    return new UnifiedDiffTable();
-  }
-}
diff --git a/src/main/java/com/google/gerrit/client/ui/DirectScreenLink.java b/src/main/java/com/google/gerrit/client/ui/DirectScreenLink.java
index 5493034..fd6a936 100644
--- a/src/main/java/com/google/gerrit/client/ui/DirectScreenLink.java
+++ b/src/main/java/com/google/gerrit/client/ui/DirectScreenLink.java
@@ -44,13 +44,14 @@
   @Override
   public void onBrowserEvent(final Event event) {
     if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) {
-      onClick(event);
+      DOM.eventPreventDefault(event);
+      go();
     }
   }
 
-  protected void onClick(final Event event) {
+  /** Create the screen and start rendering, updating the browser history. */
+  public void go() {
     Gerrit.display(getTargetHistoryToken(), createScreen());
-    DOM.eventPreventDefault(event);
   }
 
   /** Create the screen this link wants to display. */
diff --git a/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 5a2efc8..343eb33 100644
--- a/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -53,7 +53,7 @@
     keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
   }
 
-  protected abstract void onOpenItem(RowItem item);
+  protected abstract void onOpenRow(int row);
 
   protected abstract Object getRowItemKey(RowItem item);
 
@@ -78,9 +78,8 @@
 
   private void onOpen() {
     if (0 <= currentRow && currentRow < table.getRowCount()) {
-      final RowItem item = getRowItem(currentRow);
-      if (item != null) {
-        onOpenItem(item);
+      if (getRowItem(currentRow) != null) {
+        onOpenRow(currentRow);
       }
     }
   }
@@ -114,16 +113,23 @@
   }
 
   protected void movePointerTo(final Object oldId) {
+    final int row = findRow(oldId);
+    if (0 <= row) {
+      movePointerTo(row);
+    }
+  }
+
+  protected int findRow(final Object oldId) {
     if (oldId != null) {
       final int max = table.getRowCount();
       for (int row = 0; row < max; row++) {
         final RowItem c = getRowItem(row);
         if (c != null && oldId.equals(getRowItemKey(c))) {
-          movePointerTo(row);
-          break;
+          return row;
         }
       }
     }
+    return -1;
   }
 
   public void finishDisplay() {
@@ -140,12 +146,12 @@
   }
 
   public void setRegisterKeys(final boolean on) {
-    if (on) {
+    if (on && isAttached()) {
       if (regNavigation == null) {
-        regNavigation = GlobalKey.add(keysNavigation);
+        regNavigation = GlobalKey.add(this, keysNavigation);
       }
       if (regAction == null) {
-        regAction = GlobalKey.add(keysAction);
+        regAction = GlobalKey.add(this, keysAction);
       }
     } else {
       if (regNavigation != null) {
diff --git a/src/main/java/com/google/gerrit/client/ui/PatchLink.java b/src/main/java/com/google/gerrit/client/ui/PatchLink.java
index f60c7b0..ee7f1b0 100644
--- a/src/main/java/com/google/gerrit/client/ui/PatchLink.java
+++ b/src/main/java/com/google/gerrit/client/ui/PatchLink.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Link;
-import com.google.gerrit.client.patches.PatchSideBySideScreen;
-import com.google.gerrit.client.patches.PatchUnifiedScreen;
+import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.reviewdb.Patch;
+import com.google.gwt.user.client.ui.Widget;
 
 public abstract class PatchLink extends DirectScreenLink {
   protected Patch.Key key;
@@ -27,6 +28,17 @@
     key = p;
   }
 
+  protected PatchTable parentPatchTable() {
+    Widget w = getParent();
+    while (w != null) {
+      if (w instanceof PatchTable) {
+        return ((PatchTable) w);
+      }
+      w = w.getParent();
+    }
+    return null;
+  }
+
   public static class SideBySide extends PatchLink {
     public SideBySide(final String text, final Patch.Key p) {
       super(text, p, Link.toPatchSideBySide(p));
@@ -34,7 +46,7 @@
 
     @Override
     protected Screen createScreen() {
-      return new PatchSideBySideScreen(key);
+      return new PatchScreen.SideBySide(key, parentPatchTable());
     }
   }
 
@@ -45,7 +57,7 @@
 
     @Override
     protected Screen createScreen() {
-      return new PatchUnifiedScreen(key);
+      return new PatchScreen.Unified(key, parentPatchTable());
     }
   }
 }
diff --git a/src/main/java/com/google/gerrit/public/gerrit.css b/src/main/java/com/google/gerrit/public/gerrit.css
index 4bffa3e..fc41ba9 100644
--- a/src/main/java/com/google/gerrit/public/gerrit.css
+++ b/src/main/java/com/google/gerrit/public/gerrit.css
@@ -963,3 +963,14 @@
   padding-right: 10px;
   background: #fff1a8;
 }
+
+
+/** PatchBrowserPopup **/
+.gerrit-PatchBrowserPopup {
+  opacity: 0.90;
+}
+.gerrit-PatchBrowserPopup-Body {
+  background: #ffffff;
+  margin: 4px;
+  opacity: 0.90;
+}