Allow plugins to add menu items in Gerrit's top menu entries

So far plugins could only add own top menu entries. With this change
it is now possible to extend a Gerrit top menu entry and add new
menu items to it.

Some menu items are added only after a callback to the server is
done. To ensure a stable order of the menu items when menu items
are added by plugins, we now ensure in the callback that the menu item
is always inserted at the same position and menu items contributed by
plugins come after it.

Change-Id: Ia3589b02785e5c6172a89116fb17a21954dff5f0
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 531c493..c0c44f3 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -798,6 +798,23 @@
 }
 ----
 
+Plugins can also add additional menu items to Gerrit's top menu entries
+by defining a `MenuEntry` that has the same name as a Gerrit top menu
+entry:
+
+[source,java]
+----
+public class MyTopMenuExtension implements TopMenu {
+
+  @Override
+  public List<MenuEntry> getEntries() {
+    return Lists.newArrayList(
+               new MenuEntry("Projects", Lists.newArrayList(
+                      new MenuItem("Browse Repositories", "https://gerrit.googlesource.com/"))));
+  }
+}
+----
+
 If no Guice modules are declared in the manifest, the top menu extension may use
 auto-registration by providing an `@Listen` annotation:
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 660c233..0923667 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -92,7 +92,9 @@
 import com.google.gwtorm.client.KeyUtil;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class Gerrit implements EntryPoint {
   public static final GerritConstants C = GWT.create(GerritConstants.class);
@@ -111,10 +113,17 @@
   private static AccountDiffPreference myAccountDiffPref;
   private static String xGerritAuth;
 
+  private static final String ALL_BAR = "All";
+  private static final String MY_BAR = "My";
+  private static final String DIFF_BAR = "Differences";
+  private static final String PROJECTS_BAR = "Projects";
+  private static final String PEOPLE_BAR = "People";
+  private static final String PLUGINS_BAR = "Plugins";
+  private static final String DOCUMENTATION_BAR = "Documentation";
+  private static Map<String, LinkMenuBar> menuBars;
+
   private static MorphingTabPanel menuLeft;
   private static LinkMenuBar menuRight;
-  private static LinkMenuBar diffBar;
-  private static LinkMenuBar projectsBar;
   private static RootPanel topMenu;
   private static RootPanel siteHeader;
   private static RootPanel siteFooter;
@@ -201,6 +210,7 @@
    * @param view the loaded view.
    */
   public static void updateMenus(Screen view) {
+    LinkMenuBar diffBar = menuBars.get(DIFF_BAR);
     if (view instanceof PatchScreen) {
       patchScreen = (PatchScreen) view;
       menuLeft.setVisible(diffBar, true);
@@ -654,11 +664,14 @@
     menuLeft.clear();
     menuRight.clear();
 
+    menuBars = new HashMap<String, LinkMenuBar>();
+
     final boolean signedIn = isSignedIn();
     final GerritConfig cfg = getConfig();
     LinkMenuBar m;
 
     m = new LinkMenuBar();
+    menuBars.put(ALL_BAR, m);
     addLink(m, C.menuAllOpen(), PageLinks.toChangeQuery("status:open"));
     addLink(m, C.menuAllMerged(), PageLinks.toChangeQuery("status:merged"));
     addLink(m, C.menuAllAbandoned(), PageLinks.toChangeQuery("status:abandoned"));
@@ -666,6 +679,7 @@
 
     if (signedIn) {
       m = new LinkMenuBar();
+      menuBars.put(MY_BAR, m);
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
       addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
       addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
@@ -678,7 +692,8 @@
     }
 
     patchScreen = null;
-    diffBar = new LinkMenuBar();
+    LinkMenuBar diffBar = new LinkMenuBar();
+    menuBars.put(DIFF_BAR, diffBar);
     menuLeft.addInvisible(diffBar, C.menuDiff());
     addDiffLink(diffBar, CC.patchTableDiffSideBySide(), PatchScreen.Type.SIDE_BY_SIDE);
     addDiffLink(diffBar, CC.patchTableDiffUnified(), PatchScreen.Type.UNIFIED);
@@ -687,7 +702,7 @@
     addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
     addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
 
-    projectsBar = new LinkMenuBar() {
+    final LinkMenuBar projectsBar = new LinkMenuBar() {
       @Override
       public void onScreenLoad(ScreenLoadEvent event) {
         if (event.getScreen() instanceof ProjectScreen) {
@@ -695,30 +710,41 @@
         }
       }
     };
+    menuBars.put(PROJECTS_BAR, projectsBar);
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
     addProjectLink(projectsBar, C.menuProjectsInfo(), ProjectScreen.INFO);
     addProjectLink(projectsBar, C.menuProjectsBranches(), ProjectScreen.BRANCH);
     addProjectLink(projectsBar, C.menuProjectsAccess(), ProjectScreen.ACCESS);
-    addProjectLink(projectsBar, C.menuProjectsDashboards(), ProjectScreen.DASHBOARDS);
+    final LinkMenuItem dashboardsMenuItem =
+        addProjectLink(projectsBar, C.menuProjectsDashboards(),
+            ProjectScreen.DASHBOARDS);
     menuLeft.add(projectsBar, C.menuProjects());
 
     if (signedIn) {
       final LinkMenuBar peopleBar = new LinkMenuBar();
-      addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
+      menuBars.put(PEOPLE_BAR, peopleBar);
+      final LinkMenuItem groupsListMenuItem =
+          addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
       menuLeft.add(peopleBar, C.menuPeople());
 
       final LinkMenuBar pluginsBar = new LinkMenuBar();
+      menuBars.put(PLUGINS_BAR, pluginsBar);
       AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
         @Override
         public void onSuccess(AccountCapabilities result) {
           if (result.canPerform(CREATE_PROJECT)) {
-            addLink(projectsBar, C.menuProjectsCreate(), PageLinks.ADMIN_CREATE_PROJECT);
+            insertLink(projectsBar, C.menuProjectsCreate(),
+                PageLinks.ADMIN_CREATE_PROJECT,
+                projectsBar.getWidgetIndex(dashboardsMenuItem) + 1);
           }
           if (result.canPerform(CREATE_GROUP)) {
-            addLink(peopleBar, C.menuPeopleGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
+            insertLink(peopleBar, C.menuPeopleGroupsCreate(),
+                PageLinks.ADMIN_CREATE_GROUP,
+                peopleBar.getWidgetIndex(groupsListMenuItem) + 1);
           }
           if (result.canPerform(ADMINISTRATE_SERVER)) {
-            addLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS);
+            insertLink(pluginsBar, C.menuPluginsInstalled(),
+                PageLinks.ADMIN_PLUGINS, 0);
             menuLeft.insert(pluginsBar, C.menuPlugins(),
                 menuLeft.getWidgetIndex(peopleBar) + 1);
           }
@@ -728,6 +754,7 @@
 
     if (getConfig().isDocumentationAvailable()) {
       m = new LinkMenuBar();
+      menuBars.put(DOCUMENTATION_BAR, m);
       addDocLink(m, C.menuDocumentationIndex(), "index.html");
       addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
       addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
@@ -799,13 +826,16 @@
       public void onSuccess(TopMenuList result) {
         List<TopMenu> topMenuExtensions = Natives.asList(result);
         for (TopMenu menu : topMenuExtensions) {
-          LinkMenuBar bar = new LinkMenuBar();
+          LinkMenuBar existingBar = menuBars.get(menu.getName());
+          LinkMenuBar bar = existingBar != null ? existingBar : new LinkMenuBar();
           for (TopMenuItem item : Natives.asList(menu.getItems())) {
             addExtensionLink(bar, item);
           }
-          menuLeft.add(bar, menu.getName());
+          if (existingBar == null ) {
+            menuLeft.add(bar, menu.getName());
+          }
         }
-      };
+      }
     });
   }
 
@@ -874,9 +904,16 @@
     return a;
   }
 
-  private static void addLink(final LinkMenuBar m, final String text,
+  private static LinkMenuItem addLink(final LinkMenuBar m, final String text,
       final String historyToken) {
-    m.addItem(new LinkMenuItem(text, historyToken));
+    LinkMenuItem i = new LinkMenuItem(text, historyToken);
+    m.addItem(i);
+    return i;
+  }
+
+  private static void insertLink(final LinkMenuBar m, final String text,
+      final String historyToken, final int beforeIndex) {
+    m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex);
   }
 
   private static void addDiffLink(final LinkMenuBar m, final String text,
@@ -892,9 +929,9 @@
       });
   }
 
-  private static void addProjectLink(final LinkMenuBar m, final String text,
+  private static LinkMenuItem addProjectLink(final LinkMenuBar m, final String text,
       final String panel) {
-    m.addItem(new LinkMenuItem(text, "") {
+    LinkMenuItem i = new LinkMenuItem(text, "") {
         @Override
         public void onScreenLoad(ScreenLoadEvent event) {
           Screen screen = event.getScreen();
@@ -913,7 +950,9 @@
           }
           super.onScreenLoad(event);
         }
-      });
+      };
+    m.addItem(i);
+    return i;
   }
 
   private static void addDiffLink(final LinkMenuBar m, final String text,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index fe7ffe9..bc88eed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -44,6 +44,10 @@
     add(i);
   }
 
+  public void insertItem(final LinkMenuItem i, int beforeIndex) {
+    insert(i, beforeIndex);
+  }
+
   public void clear() {
     body.clear();
   }
@@ -68,6 +72,18 @@
     body.add(i);
   }
 
+  public void insert(final Widget i, int beforeIndex) {
+    if (body.getWidgetCount() == 0 || body.getWidgetCount() <= beforeIndex) {
+      add(i);
+      return;
+    }
+    body.insert(i, beforeIndex);
+  }
+
+  public int getWidgetIndex(Widget i) {
+    return body.getWidgetIndex(i);
+  }
+
   public void onScreenLoad(ScreenLoadEvent event) {
   }
 }