Show a popup with user information as tooltip on avatar images

Hovering over an avatar image opens a popup that shows information for
this user. At the moment only the image, the name and the email address
are shown, but this can be extended further. The popup is shown only
after a short delay when keeping the mouse over the image (similar to
how tooltips appear with a delay). By this it's avoided that popups are
unwantedly shown when moving the mouse cursor over the screen. The
popup automatically disappears when the mouse cursor is moved away from
the avatar image, but the popup stays open if the mouse cursor is moved
over the popup to allow text selection for copy & paste or clicking on
links in the popup if we add any later.

Change-Id: I0c17f8bd9c76a05d2c7562579018a4dac3a337e8
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 1784dae..873ab4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -14,28 +14,49 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.event.dom.client.ErrorEvent;
 import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOutHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.UIObject;
 
 public class AvatarImage extends Image {
 
   /** A default sized avatar image. */
-  public AvatarImage(String email) {
-    this(email, 0);
+  public AvatarImage(AccountInfo account) {
+    this(account, 0);
   }
 
   /**
    * An avatar image for the given account using the requested size.
    *
-   * @param email The email address of the account in which we are interested
+   * @param account The account in which we are interested
    * @param size A requested size. Note that the size can be ignored depending
    *        on the avatar provider. A size <= 0 indicates to let the provider
    *        decide a default size.
    */
-  public AvatarImage(String email, int size) {
-    super(url(email, size));
+  public AvatarImage(AccountInfo account, int size) {
+    this(account, size, true);
+  }
+
+  /**
+   * An avatar image for the given account using the requested size.
+   *
+   * @param account The account in which we are interested
+   * @param size A requested size. Note that the size can be ignored depending
+   *        on the avatar provider. A size <= 0 indicates to let the provider
+   *        decide a default size.
+   * @param addPopup show avatar popup with user info on hovering over the
+   *        avatar image
+   */
+  public AvatarImage(AccountInfo account, int size, boolean addPopup) {
+    super(url(account.email(), size));
 
     if (size > 0) {
       // If the provider does not resize the image, force it in the browser.
@@ -50,6 +71,13 @@
         setVisible(false);
       }
     });
+
+    if (addPopup) {
+      UserPopupPanel userPopup = new UserPopupPanel(account, false, false);
+      PopupHandler popupHandler = new PopupHandler(userPopup, this);
+      addMouseOverHandler(popupHandler);
+      addMouseOutHandler(popupHandler);
+    }
   }
 
   private static String url(String email, int size) {
@@ -68,4 +96,79 @@
     }
     return api.url();
   }
+
+  private class PopupHandler implements MouseOverHandler, MouseOutHandler {
+    private final UserPopupPanel popup;
+    private final UIObject target;
+
+    private Timer showTimer;
+    private Timer hideTimer;
+
+    public PopupHandler(UserPopupPanel popup, UIObject target) {
+      this.popup = popup;
+      this.target = target;
+
+      popup.addDomHandler(new MouseOverHandler() {
+        @Override
+        public void onMouseOver(MouseOverEvent event) {
+          scheduleShow();
+        }
+      }, MouseOverEvent.getType());
+      popup.addDomHandler(new MouseOutHandler() {
+        @Override
+        public void onMouseOut(MouseOutEvent event) {
+          scheduleHide();
+        }
+      }, MouseOutEvent.getType());
+    }
+
+    @Override
+    public void onMouseOver(MouseOverEvent event) {
+      scheduleShow();
+    }
+
+    @Override
+    public void onMouseOut(MouseOutEvent event) {
+      scheduleHide();
+    }
+
+    private void scheduleShow() {
+      if (hideTimer != null) {
+        hideTimer.cancel();
+        hideTimer = null;
+      }
+      if ((popup.isShowing() && popup.isVisible()) || showTimer != null) {
+        return;
+      }
+      showTimer = new Timer() {
+        @Override
+        public void run() {
+          if (!popup.isShowing() || !popup.isVisible()) {
+            popup.showRelativeTo(target);
+          }
+
+        }
+      };
+      showTimer.schedule(600);
+    }
+
+    private void scheduleHide() {
+      if (showTimer != null) {
+        showTimer.cancel();
+        showTimer = null;
+      }
+      if (!popup.isShowing() || !popup.isVisible() || hideTimer != null) {
+        return;
+      }
+      hideTimer = new Timer() {
+        @Override
+        public void run() {
+          popup.hide();
+        }
+      };
+      hideTimer.schedule(50);
+    }
+  }
+
+
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 37c3358..2e94db7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -178,7 +178,7 @@
     return nameEmail(ai);
   }
 
-  private static AccountInfo asInfo(Account acct) {
+  public static AccountInfo asInfo(Account acct) {
     if (acct == null) {
       return AccountInfo.create(0, null, null);
     }
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 e018978..2a9cb71 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
@@ -249,6 +250,11 @@
     return myAccount;
   }
 
+  /** @return the currently signed in user's account data; empty account data if no account */
+  public static AccountInfo getUserAccountInfo() {
+    return FormatUtil.asInfo(myAccount);
+  }
+
   /** @return access token to prove user identity during REST API calls. */
   public static String getXGerritAuth() {
     return xGerritAuth;
@@ -762,9 +768,9 @@
   }
 
   private static void whoAmI(boolean canLogOut) {
-    Account account = getUserAccount();
-    final CurrentUserPopupPanel userPopup =
-        new CurrentUserPopupPanel(account, canLogOut);
+    AccountInfo account = getUserAccountInfo();
+    final UserPopupPanel userPopup =
+        new UserPopupPanel(account, canLogOut, true);
     final FlowPanel userSummaryPanel = new FlowPanel();
     class PopupHandler implements KeyDownHandler, ClickHandler {
       private void showHidePopup() {
@@ -791,7 +797,7 @@
     final PopupHandler popupHandler = new PopupHandler();
     final InlineLabel l = new InlineLabel(FormatUtil.name(account));
     l.setStyleName(RESOURCES.css().menuBarUserName());
-    final AvatarImage avatar = new AvatarImage(account.getPreferredEmail(), 26);
+    final AvatarImage avatar = new AvatarImage(account, 26, false);
     avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
     userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
     userSummaryPanel.add(l);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
similarity index 74%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index f6636cd..01811a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
@@ -24,8 +24,8 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 
-public class CurrentUserPopupPanel extends PluginSafePopupPanel {
-  interface Binder extends UiBinder<Widget, CurrentUserPopupPanel> {
+public class UserPopupPanel extends PluginSafePopupPanel {
+  interface Binder extends UiBinder<Widget, UserPopupPanel> {
   }
 
   private static final Binder binder = GWT.create(Binder.class);
@@ -41,9 +41,10 @@
   @UiField
   Anchor settings;
 
-  public CurrentUserPopupPanel(Account account, boolean canLogOut) {
+  public UserPopupPanel(AccountInfo account, boolean canLogOut,
+      boolean showSettingsLink) {
     super(/* auto hide */true, /* modal */false);
-    avatar = new AvatarImage(account.getPreferredEmail(), 100);
+    avatar = new AvatarImage(account, 100, false);
     setWidget(binder.createAndBindUi(this));
     // We must show and then hide this popup so that it is part of the DOM.
     // Otherwise the image does not get any events.  Calling hide() would
@@ -51,17 +52,21 @@
     show();
     setVisible(false);
     setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
-    if (account.getFullName() != null) {
-      userName.setText(account.getFullName());
+    if (account.name() != null) {
+      userName.setText(account.name());
     }
-    if (account.getPreferredEmail() != null) {
-      userEmail.setText(account.getPreferredEmail());
+    if (account.email() != null) {
+      userEmail.setText(account.email());
     }
     if (canLogOut) {
       logout.setHref(Gerrit.selfRedirect("/logout"));
     } else {
       logout.setVisible(false);
     }
-    settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    if (showSettingsLink) {
+      settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+    } else {
+      settings.setVisible(false);
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
similarity index 100%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index cae3c71..4fa76dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -360,10 +360,10 @@
         final CommentEditorPanel editor =
             new CommentEditorPanel(c, commentLinkProcessor);
         if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
-          editor.setAuthorNameText(Gerrit.getUserAccount().getPreferredEmail(),
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
               Util.C.fileCommentHeader());
         } else {
-          editor.setAuthorNameText(Gerrit.getUserAccount().getPreferredEmail(),
+          editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
               Util.M.lineHeader(c.getLine()));
         }
         editor.setOpen(true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 289138a..9ed3f18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -65,8 +65,7 @@
     comment = plc;
 
     addStyleName(Gerrit.RESOURCES.css().commentEditorPanel());
-    setAuthorNameText(Gerrit.getUserAccount().getPreferredEmail(),
-        PatchUtil.C.draft());
+    setAuthorNameText(Gerrit.getUserAccountInfo(), PatchUtil.C.draft());
     setMessageText(plc.getMessage());
     addDoubleClickHandler(this);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index d87cdea..05c6b5a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -63,7 +63,7 @@
     this(commentLinkProcessor);
 
     setMessageText(message);
-    setAuthorNameText(author.email(), FormatUtil.name(author));
+    setAuthorNameText(author, FormatUtil.name(author));
     setDateText(FormatUtil.shortFormatDayTime(when));
 
     final CellFormatter fmt = header.getCellFormatter();
@@ -126,8 +126,8 @@
     SafeHtml.set(messageText, buf);
   }
 
-  public void setAuthorNameText(final String authorEmail, final String nameText) {
-    header.setWidget(0, 0, new AvatarImage(authorEmail, 26));
+  public void setAuthorNameText(final AccountInfo author, final String nameText) {
+    header.setWidget(0, 0, new AvatarImage(author, 26));
     header.setText(0, 1, nameText);
   }