Add settings screen for editing GPG public keys

The UI is almost identical to the UI for editing SSH keys (although
implemented with UiBinder).

Change-Id: Ic6cf4dc9d7f71b00efea00a86498660d113ebb2b
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index c80d867..ff2121d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -26,6 +26,7 @@
   public static final String SETTINGS = "/settings/";
   public static final String SETTINGS_PREFERENCES = "/settings/preferences";
   public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
+  public static final String SETTINGS_GPGKEYS = "/settings/gpg-keys";
   public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
   public static final String SETTINGS_WEBIDENT = "/settings/web-identities";
   public static final String SETTINGS_MYGROUPS = "/settings/group-memberships";
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
index 7087888..dcd96da 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -91,6 +91,20 @@
     return arr;
   }
 
+  public static JsArrayString arrayOf(Iterable<String> elements) {
+    JsArrayString arr = JavaScriptObject.createArray().cast();
+    for (String elem : elements) {
+      arr.push(elem);
+    }
+    return arr;
+  }
+
+  public static JsArrayString arrayOf(String element) {
+    JsArrayString arr = JavaScriptObject.createArray().cast();
+    arr.push(element);
+    return arr;
+  }
+
   private Natives() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index e7381f8..946888d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -29,6 +29,7 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_CONTACT;
 import static com.google.gerrit.common.PageLinks.SETTINGS_EXTENSION;
+import static com.google.gerrit.common.PageLinks.SETTINGS_GPGKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_HTTP_PASSWORD;
 import static com.google.gerrit.common.PageLinks.SETTINGS_MYGROUPS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_NEW_AGREEMENT;
@@ -40,6 +41,7 @@
 
 import com.google.gerrit.client.account.MyAgreementsScreen;
 import com.google.gerrit.client.account.MyContactInformationScreen;
+import com.google.gerrit.client.account.MyGpgKeysScreen;
 import com.google.gerrit.client.account.MyGroupsScreen;
 import com.google.gerrit.client.account.MyIdentitiesScreen;
 import com.google.gerrit.client.account.MyPasswordScreen;
@@ -536,6 +538,10 @@
           return new MySshKeysScreen();
         }
 
+        if (matchExact(SETTINGS_GPGKEYS, token)) {
+          return new MyGpgKeysScreen();
+        }
+
         if (matchExact(SETTINGS_WEBIDENT, token)) {
           return new MyIdentitiesScreen();
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index c6eb2de..c2a7637 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -184,6 +184,7 @@
   String sshHostKeyPanelKnownHostEntry();
   String sshKeyPanelEncodedKey();
   String sshKeyPanelInvalid();
+  String sshKeyTable();
   String stringListPanelButtons();
   String topMostCell();
   String topmenu();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index a796f94..367644f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -17,10 +17,13 @@
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
+import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.Set;
@@ -147,4 +150,42 @@
     protected UsernameInput() {
     }
   }
+
+  public static void addGpgKey(String account, String armored,
+      AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
+    new RestApi("/accounts/")
+      .id(account)
+      .view("gpgkeys")
+      .post(GpgKeysInput.add(armored), cb);
+  }
+
+  public static void removeGpgKeys(String account,
+      Iterable<String> fingerprints, AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
+    new RestApi("/accounts/")
+      .id(account)
+      .view("gpgkeys")
+      .post(GpgKeysInput.remove(fingerprints), cb);
+  }
+
+  private static class GpgKeysInput extends JavaScriptObject {
+    static GpgKeysInput add(String key) {
+      return createAdd(Natives.arrayOf(key));
+    }
+
+    static GpgKeysInput remove(Iterable<String> fingerprints) {
+      return createRemove(Natives.arrayOf(fingerprints));
+    }
+
+    private static native GpgKeysInput createAdd(JsArrayString keys) /*-{
+      return {'add': keys};
+    }-*/;
+
+    private static native GpgKeysInput createRemove(
+        JsArrayString fingerprints) /*-{
+      return {'remove': fingerprints};
+    }-*/;
+
+    protected GpgKeysInput() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 4c3cc29..6234f02 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -50,14 +50,15 @@
   String myMenuReset();
 
   String tabAccountSummary();
-  String tabPreferences();
-  String tabWatchedProjects();
-  String tabContactInformation();
-  String tabSshKeys();
-  String tabHttpAccess();
-  String tabWebIdentities();
-  String tabMyGroups();
   String tabAgreements();
+  String tabContactInformation();
+  String tabGpgKeys();
+  String tabHttpAccess();
+  String tabMyGroups();
+  String tabPreferences();
+  String tabSshKeys();
+  String tabWatchedProjects();
+  String tabWebIdentities();
 
   String buttonShowAddSshKey();
   String buttonCloseAddSshKey();
@@ -94,6 +95,10 @@
   String sshHostKeyFingerprint();
   String sshHostKeyKnownHostEntry();
 
+  String gpgKeyId();
+  String gpgKeyFingerprint();
+  String gpgKeyUserIds();
+
   String webIdStatus();
   String webIdEmail();
   String webIdIdentity();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 36cb765..eee7a60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -36,14 +36,15 @@
 changeScreenNewUi = New Screen
 
 tabAccountSummary = Profile
-tabPreferences = Preferences
-tabWatchedProjects = Watched Projects
-tabContactInformation = Contact Information
-tabSshKeys = SSH Public Keys
-tabHttpAccess = HTTP Password
-tabWebIdentities = Identities
-tabMyGroups = Groups
 tabAgreements = Agreements
+tabContactInformation = Contact Information
+tabGpgKeys = GPG Public Keys
+tabHttpAccess = HTTP Password
+tabMyGroups = Groups
+tabPreferences = Preferences
+tabSshKeys = SSH Public Keys
+tabWatchedProjects = Watched Projects
+tabWebIdentities = Identities
 
 buttonShowAddSshKey = Add Key ...
 buttonCloseAddSshKey = Close
@@ -73,6 +74,10 @@
 sshHostKeyFingerprint = Fingerprint:
 sshHostKeyKnownHostEntry = Entry for <code>~/.ssh/known_hosts</code>:
 
+gpgKeyId = ID
+gpgKeyFingerprint = Fingerprint
+gpgKeyUserIds = User IDs
+
 webIdStatus = Status
 webIdEmail = Email Address
 webIdIdentity = Identity
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
new file mode 100644
index 0000000..d1bb426
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/GpgKeyInfo.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2015 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.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+public class GpgKeyInfo extends JavaScriptObject {
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String fingerprint() /*-{ return this.fingerprint; }-*/;
+  public final native JsArrayString userIds() /*-{ return this.user_ids; }-*/;
+  public final native String key() /*-{ return this.key; }-*/;
+
+  protected GpgKeyInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
new file mode 100644
index 0000000..6d88e38
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.java
@@ -0,0 +1,283 @@
+// Copyright (C) 2015 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.account;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class MyGpgKeysScreen extends SettingsScreen {
+  interface Binder extends UiBinder<HTMLPanel, MyGpgKeysScreen> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true) GpgKeyTable keys;
+  @UiField Button deleteKey;
+  @UiField Button addKey;
+
+  @UiField VerticalPanel addKeyBlock;
+  @UiField NpTextArea keyText;
+
+  @UiField VerticalPanel errorPanel;
+  @UiField Label errorText;
+
+  @UiField Button clearButton;
+  @UiField Button addButton;
+  @UiField Button closeButton;
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    keys = new GpgKeyTable();
+    add(uiBinder.createAndBindUi(this));
+    keys.updateDeleteButton();
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    refreshKeys();
+  }
+
+  @UiHandler("deleteKey")
+  void onDeleteKey(@SuppressWarnings("unused") ClickEvent e) {
+    keys.deleteChecked();
+  }
+
+  @UiHandler("addKey")
+  void onAddKey(@SuppressWarnings("unused") ClickEvent e) {
+    showAddKeyBlock(true);
+  }
+
+  @UiHandler("clearButton")
+  void onClearButton(@SuppressWarnings("unused") ClickEvent e) {
+    keyText.setText("");
+    keyText.setFocus(true);
+    errorPanel.setVisible(false);
+  }
+
+  @UiHandler("closeButton")
+  void onCloseButton(@SuppressWarnings("unused") ClickEvent e) {
+    showAddKeyBlock(false);
+  }
+
+  @UiHandler("addButton")
+  void onAddButton(@SuppressWarnings("unused") ClickEvent e) {
+    doAddKey();
+  }
+
+  private void refreshKeys() {
+    AccountApi.self().view("gpgkeys").get(NativeMap.copyKeysIntoChildren("id",
+        new GerritCallback<NativeMap<GpgKeyInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<GpgKeyInfo> result) {
+            List<GpgKeyInfo> list = Natives.asList(result.values());
+            // TODO(dborowitz): Sort on something more meaningful, like
+            // created date?
+            Collections.sort(list, new Comparator<GpgKeyInfo>() {
+              @Override
+              public int compare(GpgKeyInfo a, GpgKeyInfo b) {
+                return a.id().compareTo(b.id());
+              }
+            });
+            keys.clear();
+            keyText.setText("");
+            errorPanel.setVisible(false);
+            addButton.setEnabled(true);
+            if (!list.isEmpty()) {
+              keys.setVisible(true);
+              for (GpgKeyInfo k : list) {
+                keys.addOneKey(k);
+              }
+              showKeyTable(true);
+              showAddKeyBlock(false);
+            } else {
+              keys.setVisible(false);
+              showAddKeyBlock(true);
+              showKeyTable(false);
+            }
+
+            display();
+          }
+        }));
+  }
+
+  private void showAddKeyBlock(boolean show) {
+    addKey.setVisible(!show);
+    addKeyBlock.setVisible(show);
+  }
+
+  private void showKeyTable(boolean show) {
+    keys.setVisible(show);
+    deleteKey.setVisible(show);
+    addKey.setVisible(show);
+  }
+
+  private void doAddKey() {
+    if (keyText.getText().isEmpty()) {
+      return;
+    }
+    addButton.setEnabled(false);
+    keyText.setEnabled(false);
+    AccountApi.addGpgKey("self", keyText.getText(),
+        new AsyncCallback<NativeMap<GpgKeyInfo>>() {
+          @Override
+          public void onSuccess(NativeMap<GpgKeyInfo> result) {
+            keyText.setEnabled(true);
+            refreshKeys();
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            keyText.setEnabled(true);
+            addButton.setEnabled(true);
+            if (caught instanceof StatusCodeException) {
+              StatusCodeException sce = (StatusCodeException) caught;
+              if (sce.getStatusCode() == Response.SC_CONFLICT
+                  || sce.getStatusCode() == Response.SC_BAD_REQUEST) {
+                errorText.setText(sce.getEncodedResponse());
+              } else {
+                errorText.setText(sce.getMessage());
+              }
+            } else {
+              errorText.setText(
+                  "Unexpected error saving key: " + caught.getMessage());
+            }
+            errorPanel.setVisible(true);
+          }
+        });
+  }
+
+  private class GpgKeyTable extends FancyFlexTable<GpgKeyInfo> {
+    private final ValueChangeHandler<Boolean> updateDeleteHandler;
+
+    GpgKeyTable() {
+      table.setWidth("");
+      table.setText(0, 1, Util.C.gpgKeyId());
+      table.setText(0, 2, Util.C.gpgKeyFingerprint());
+      table.setText(0, 3, Util.C.gpgKeyUserIds());
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+
+      updateDeleteHandler = new ValueChangeHandler<Boolean>() {
+        @Override
+        public void onValueChange(ValueChangeEvent<Boolean> event) {
+          updateDeleteButton();
+        }
+      };
+    }
+
+    private void addOneKey(GpgKeyInfo k) {
+      int row = table.getRowCount();
+      table.insertRow(row);
+      applyDataRowStyle(row);
+
+      CheckBox sel = new CheckBox();
+      sel.addValueChangeHandler(updateDeleteHandler);
+      table.setWidget(row, 0, sel);
+      table.setWidget(row, 1, new CopyableLabel(k.id()));
+      table.setText(row, 2, k.fingerprint());
+
+      VerticalPanel userIds = new VerticalPanel();
+      for (int i = 0; i < k.userIds().length(); i++) {
+        userIds.add(new InlineLabel(k.userIds().get(i)));
+      }
+      table.setWidget(row, 3, userIds);
+
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell());
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, k);
+    }
+
+    private void updateDeleteButton() {
+      for (int row = 1; row < table.getRowCount(); row++) {
+        if (isChecked(row)) {
+          deleteKey.setEnabled(true);
+          return;
+        }
+      }
+      deleteKey.setEnabled(false);
+    }
+
+    private void deleteChecked() {
+      deleteKey.setEnabled(false);
+      List<String> toDelete = new ArrayList<>(table.getRowCount());
+      for (int row = 1; row < table.getRowCount(); row++) {
+        if (isChecked(row)) {
+          toDelete.add(getRowItem(row).fingerprint());
+        }
+      }
+      AccountApi.removeGpgKeys("self", toDelete,
+          new GerritCallback<NativeMap<GpgKeyInfo>>() {
+            @Override
+            public void onSuccess(NativeMap<GpgKeyInfo> result) {
+              refreshKeys();
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              deleteKey.setEnabled(true);
+              super.onFailure(caught);
+            }
+          });
+    }
+
+    private boolean isChecked(int row) {
+      return ((CheckBox) table.getWidget(row, 0)).getValue();
+    }
+
+    private void clear() {
+      while (table.getRowCount() > 1) {
+        table.removeRow(1);
+      }
+      for (int i = table.getRowCount() - 1; i >= 1; i++) {
+        table.removeRow(i);
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
new file mode 100644
index 0000000..dc73736
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGpgKeysScreen.ui.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2015 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.
+-->
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'
+    xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'>
+  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+
+  <ui:style gss='false'>
+    .errorHeader {
+      font-weight: bold;
+    }
+    .errorText {
+      white-space: pre-wrap;
+      padding-bottom: 6px;
+    }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <g:Widget ui:field='keys' addStyleNames='{res.css.sshKeyTable}'/>
+    <g:FlowPanel>
+      <g:Button ui:field='deleteKey'>
+        <div><ui:msg>Delete</ui:msg></div>
+      </g:Button>
+      <g:Button ui:field='addKey'>
+        <div><ui:msg>Add Key ...</ui:msg></div>
+      </g:Button>
+    </g:FlowPanel>
+    <g:VerticalPanel ui:field='addKeyBlock'
+        styleName='{res.css.addSshKeyPanel}'
+        visible='false'>
+      <g:Label>Add GPG Public Key</g:Label>
+      <g:DisclosurePanel>
+        <g:header>How to generate a GPG key</g:header>
+        <g:HTMLPanel>
+          <ol>
+            <li>
+              From the Terminal or Git Bash, run <em>gpg --gen-key</em> and
+              follow the prompts to create the key.
+            </li>
+            <li>
+              Use the default kind. Use the default (or higher) keysize. Choose
+              any value for your expiration.
+            </li>
+            <li>
+              The user ID should contain one of your registered email addresses.
+            </li>
+            <li>Setting a passphrase is strongly recommended.</li>
+            <li>Note the ID of your new key.</li>
+            <li>
+              To export your key, run the following and paste the full output
+              into the text box:
+              <br/>
+              <code>gpg --export -a &lt;key ID&gt;</code>
+            </li>
+          </ol>
+        </g:HTMLPanel>
+      </g:DisclosurePanel>
+      <expui:NpTextArea
+          visibleLines='12'
+          characterWidth='80'
+          spellCheck='false'
+          ui:field='keyText'/>
+      <g:VerticalPanel ui:field='errorPanel' visible='false'>
+        <g:Label styleName='{style.errorHeader}'>Error adding GPG key:</g:Label>
+        <g:Label styleName='{style.errorText}' ui:field='errorText'/>
+      </g:VerticalPanel>
+      <g:FlowPanel>
+        <g:Button ui:field='clearButton'>
+          <div><ui:msg>Clear</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='addButton'>
+          <div><ui:msg>Add</ui:msg></div>
+        </g:Button>
+        <g:Button ui:field='closeButton'>
+          <div><ui:msg>Close</ui:msg></div>
+        </g:Button>
+      </g:FlowPanel>
+    </g:VerticalPanel>
+  </g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index ac140ff..2f3a819 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -45,6 +45,9 @@
     if (Gerrit.info().auth().isHttpPasswordSettingsEnabled()) {
       linkByGerrit(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     }
+    if (Gerrit.info().receive().enableSignedPush()) {
+      linkByGerrit(Util.C.tabGpgKeys(), PageLinks.SETTINGS_GPGKEYS);
+    }
     linkByGerrit(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     linkByGerrit(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
     if (Gerrit.info().auth().useContributorAgreements()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index c26c437..0914efd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1074,6 +1074,10 @@
   width: 100%;
 }
 
+.sshKeyTable td.dataCell, .sshKeyTable td.iconCell {
+  vertical-align: top;
+}
+
 .createProjectPanel {
   margin-bottom: 10px;
   background-color: trimColor;