From e23740acb29b87d0dbac51c30497f2d4bf46f7c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Blin?=
 <sebastien.blin@savoirfairelinux.com>
Date: Mon, 29 Apr 2019 10:31:14 -0400
Subject: [PATCH] mainwindow: add some accelerators

Add shortcuts to perform a lot of actions without the mouse. Also
add a Keyboard shortcuts window.

Change-Id: If3fe70a27696b03bf4a067d7bbf004116d4ed97c
---
 src/conversationsview.cpp |  19 +++
 src/conversationsview.h   |   1 +
 src/incomingcallview.cpp  |   8 +-
 src/ring_client.cpp       | 147 +++++++++++++++++++++--
 src/ringmainwindow.cpp    | 247 +++++++++++++++++++++++++++++++++++---
 src/ringmainwindow.h      |  14 +++
 ui/help-overlay.ui        | 191 +++++++++++++++++++++++++++++
 ui/incomingcallview.ui    |   2 -
 ui/ringgearsmenu.ui       |   6 +-
 ui/ringmainwindow.ui      |   5 +
 ui/ui.gresource.xml       |   1 +
 11 files changed, 603 insertions(+), 38 deletions(-)
 create mode 100644 ui/help-overlay.ui

diff --git a/src/conversationsview.cpp b/src/conversationsview.cpp
index 2f8ef16a..7cffdafa 100644
--- a/src/conversationsview.cpp
+++ b/src/conversationsview.cpp
@@ -762,3 +762,22 @@ conversations_view_select_conversation(ConversationsView *self, const std::strin
         idx++;
     }
 }
+
+int
+conversations_view_get_current_selected(ConversationsView *self)
+{
+
+    g_return_val_if_fail(IS_CONVERSATIONS_VIEW(self), -1);
+
+    /* we always drag the selected row */
+    auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(self));
+    GtkTreeModel *model = NULL;
+    GtkTreeIter iter;
+
+    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
+        auto path = gtk_tree_model_get_path(model, &iter);
+        auto idx = gtk_tree_path_get_indices(path);
+        return idx[0];
+    }
+    return -1;
+}
diff --git a/src/conversationsview.h b/src/conversationsview.h
index 24db86a2..1cecda7d 100644
--- a/src/conversationsview.h
+++ b/src/conversationsview.h
@@ -38,5 +38,6 @@ typedef struct _ConversationsViewClass ConversationsViewClass;
 GType      conversations_view_get_type            (void) G_GNUC_CONST;
 GtkWidget *conversations_view_new                 (AccountInfoPointer const & accountInfo);
 void       conversations_view_select_conversation (ConversationsView *self, const std::string& uid);
+int        conversations_view_get_current_selected(ConversationsView *self);
 
 G_END_DECLS
diff --git a/src/incomingcallview.cpp b/src/incomingcallview.cpp
index 9b7b318b..c60e45b4 100644
--- a/src/incomingcallview.cpp
+++ b/src/incomingcallview.cpp
@@ -128,7 +128,7 @@ map_boolean_to_orientation(GValue *value, GVariant *variant, G_GNUC_UNUSED gpoin
 }
 
 static void
-reject_incoming_call(G_GNUC_UNUSED GtkWidget *widget, IncomingCallView *self)
+reject_incoming_call(IncomingCallView *self)
 {
     g_return_if_fail(IS_INCOMING_CALL_VIEW(self));
     auto priv = INCOMING_CALL_VIEW_GET_PRIVATE(self);
@@ -136,7 +136,7 @@ reject_incoming_call(G_GNUC_UNUSED GtkWidget *widget, IncomingCallView *self)
 }
 
 static void
-accept_incoming_call(G_GNUC_UNUSED GtkWidget *widget, IncomingCallView *self)
+accept_incoming_call(IncomingCallView *self)
 {
     g_return_if_fail(IS_INCOMING_CALL_VIEW(self));
     auto priv = INCOMING_CALL_VIEW_GET_PRIVATE(self);
@@ -191,8 +191,8 @@ incoming_call_view_init(IncomingCallView *view)
                                  map_boolean_to_orientation,
                                  nullptr, nullptr, nullptr);
 
-    g_signal_connect(priv->button_reject_incoming, "clicked", G_CALLBACK(reject_incoming_call), view);
-    g_signal_connect(priv->button_accept_incoming, "clicked", G_CALLBACK(accept_incoming_call), view);
+    g_signal_connect_swapped(priv->button_reject_incoming, "clicked", G_CALLBACK(reject_incoming_call), view);
+    g_signal_connect_swapped(priv->button_accept_incoming, "clicked", G_CALLBACK(accept_incoming_call), view);
 }
 
 static void
diff --git a/src/ring_client.cpp b/src/ring_client.cpp
index 04089bc6..e8fab5a3 100644
--- a/src/ring_client.cpp
+++ b/src/ring_client.cpp
@@ -104,6 +104,7 @@ G_DEFINE_TYPE_WITH_PRIVATE(RingClient, ring_client, GTK_TYPE_APPLICATION);
 
 #define RING_CLIENT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RING_CLIENT_TYPE, RingClientPrivate))
 
+
 static void
 exception_dialog(const char* msg)
 {
@@ -123,10 +124,60 @@ static void
 ring_accelerators(RingClient *client)
 {
 #if GTK_CHECK_VERSION(3,12,0)
-    const gchar *quit_accels[2] = { "<Ctrl>Q", NULL };
+    const gchar *quit_accels[2] = {"<Ctrl>Q", NULL};
     gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.quit", quit_accels);
+
+    const gchar *accounts_accels[2] = {"<Ctrl>A", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.display_account_list", accounts_accels);
+
+    const gchar *search_accels[2] = {"<Ctrl>F", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.search", search_accels);
+
+    const gchar *conversations_list_accels[2] = {"<Ctrl>L", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.conversations_list", conversations_list_accels);
+    const gchar *requests_list_accels[2] = {"<Ctrl>R", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.requests_list", requests_list_accels);
+
+    const gchar *audio_call_accels[2] = {"<Ctrl><Shift>C", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.audio_call", audio_call_accels);
+    const gchar *clear_history_accels[2] = {"<Ctrl><Shift>L", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.clear_history", clear_history_accels);
+    const gchar *remove_conversation_accels[2] = {"<Ctrl><Shift>Delete", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.remove_conversation", remove_conversation_accels);
+    const gchar *block_contact_accels[2] = {"<Ctrl><Shift>B", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.block_contact", block_contact_accels);
+    const gchar *unblock_contact_accels[2] = {"<Ctrl><Shift>U", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.unblock_contact", unblock_contact_accels);
+    const gchar *copy_contact_accels[2] = {"<Ctrl><Shift>J", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.copy_contact", copy_contact_accels);
+    const gchar *add_contact_accels[2] = {"<Ctrl><Shift>A", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.add_contact", add_contact_accels);
+
+    const gchar *accept_call_accels[2] = {"<Ctrl>Y", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.accept_call", accept_call_accels);
+    const gchar *decline_call_accels[2] = {"<Ctrl>D", NULL};
+    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.decline_call", decline_call_accels);
+
 #else
     gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>Q", "app.quit", NULL);
+
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>A", "app.display_account_list", NULL);
+
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>F", "app.search", NULL);
+
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>L", "app.conversations_list", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>R", "app.requests_list", NULL);
+
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>C", "app.audio_call", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>L", "app.clear_history", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>Delete", "app.remove_conversation", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>B", "app.block_contact", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>U", "app.unblock_contact", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>J", "app.copy_contact", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control><Shift>A", "app.add_contact", NULL);
+
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>Y", "app.accept_call", NULL);
+    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>D", "app.decline_call", NULL);
 #endif
 }
 
@@ -156,6 +207,50 @@ action_about(G_GNUC_UNUSED GSimpleAction *simple,
     ring_about_dialog(priv->win);
 }
 
+static void
+exec_action(GSimpleAction *simple,
+            G_GNUC_UNUSED GVariant      *parameter,
+            gpointer user_data)
+{
+    g_return_if_fail(G_IS_APPLICATION(user_data));
+    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(user_data);
+
+    GValue value = G_VALUE_INIT;
+    g_value_init(&value, G_TYPE_STRING);
+    g_object_get_property(G_OBJECT(simple), "name", &value);
+    if (!g_value_get_string(&value)) return;
+    std::string name = g_value_get_string(&value);
+
+    if (name == "display_account_list")
+        ring_main_window_display_account_list(RING_MAIN_WINDOW(priv->win));
+    else if (name == "search")
+        ring_main_window_search(RING_MAIN_WINDOW(priv->win));
+    else if (name == "conversations_list")
+        ring_main_window_conversations_list(RING_MAIN_WINDOW(priv->win));
+    else if (name == "requests_list")
+        ring_main_window_requests_list(RING_MAIN_WINDOW(priv->win));
+    else if (name == "audio_call")
+        ring_main_window_audio_call(RING_MAIN_WINDOW(priv->win));
+    else if (name == "clear_history")
+        ring_main_window_clear_history(RING_MAIN_WINDOW(priv->win));
+    else if (name == "remove_conversation")
+        ring_main_window_remove_conversation(RING_MAIN_WINDOW(priv->win));
+    else if (name == "block_contact")
+        ring_main_window_block_contact(RING_MAIN_WINDOW(priv->win));
+    else if (name == "unblock_contact")
+        ring_main_window_unblock_contact(RING_MAIN_WINDOW(priv->win));
+    else if (name == "copy_contact")
+        ring_main_window_copy_contact(RING_MAIN_WINDOW(priv->win));
+    else if (name == "add_contact")
+        ring_main_window_add_contact(RING_MAIN_WINDOW(priv->win));
+    else if (name == "accept_call")
+        ring_main_window_accept_call(RING_MAIN_WINDOW(priv->win));
+    else if (name == "decline_call")
+        ring_main_window_decline_call(RING_MAIN_WINDOW(priv->win));
+    else
+        g_warning("Missing implementation for this action: %s", name.c_str());
+}
+
 static void
 toggle_smartinfo(GSimpleAction *action, GVariant *parameter, gpointer)
 {
@@ -167,19 +262,45 @@ toggle_smartinfo(GSimpleAction *action, GVariant *parameter, gpointer)
     }
 }
 
-static const GActionEntry ring_actions[] =
+static void
+action_show_shortcuts(G_GNUC_UNUSED GSimpleAction *action, G_GNUC_UNUSED GVariant *parameter, gpointer user_data)
 {
-    { "accept",             NULL,         NULL, NULL,    NULL, {0} },
-    { "hangup",             NULL,         NULL, NULL,    NULL, {0} },
-    { "hold",               NULL,         NULL, "false", NULL, {0} },
-    { "quit",               action_quit,  NULL, NULL,    NULL, {0} },
-    { "about",              action_about, NULL, NULL,    NULL, {0} },
-    { "mute_audio",         NULL,         NULL, "false", NULL, {0} },
-    { "mute_video",         NULL,         NULL, "false", NULL, {0} },
-    { "record",             NULL,         NULL, "false", NULL, {0} },
-    { "display-smartinfo",  NULL,         NULL, "false", toggle_smartinfo, {0} },
-    /* TODO implement the other actions */
-    // { "transfer",   NULL,        NULL, "flase", NULL, {0} },
+    g_return_if_fail(G_IS_APPLICATION(user_data));
+    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(user_data);
+
+    GtkBuilder *builder = gtk_builder_new_from_resource("/net/jami/JamiGnome/help-overlay.ui");
+    GtkWidget *overlay = GTK_WIDGET(gtk_builder_get_object (builder, "help_overlay"));
+
+    gtk_window_set_transient_for(GTK_WINDOW(overlay), GTK_WINDOW(priv->win));
+    gtk_widget_show(overlay);
+
+    g_object_unref(builder);
+}
+
+static const GActionEntry ring_actions[] = {
+    {"accept", NULL, NULL, NULL, NULL, {0}},
+    {"hangup", NULL, NULL, NULL, NULL, {0}},
+    {"hold", NULL, NULL, "false", NULL, {0}},
+    {"quit", action_quit, NULL, NULL, NULL, {0}},
+    {"about", action_about, NULL, NULL, NULL, {0}},
+    {"mute_audio", NULL, NULL, "false", NULL, {0}},
+    {"mute_video", NULL, NULL, "false", NULL, {0}},
+    {"record", NULL, NULL, "false", NULL, {0}},
+    {"display-smartinfo", NULL, NULL, "false", toggle_smartinfo, {0}},
+    {"display_account_list", exec_action, NULL, NULL, NULL, {0}},
+    {"search", exec_action, NULL, NULL, NULL, {0}},
+    {"conversations_list", exec_action, NULL, NULL, NULL, {0}},
+    {"requests_list", exec_action, NULL, NULL, NULL, {0}},
+    {"audio_call", exec_action, NULL, NULL, NULL, {0}},
+    {"clear_history", exec_action, NULL, NULL, NULL, {0}},
+    {"remove_conversation", exec_action, NULL, NULL, NULL, {0}},
+    {"block_contact", exec_action, NULL, NULL, NULL, {0}},
+    {"unblock_contact", exec_action, NULL, NULL, NULL, {0}},
+    {"copy_contact", exec_action, NULL, NULL, NULL, {0}},
+    {"add_contact", exec_action, NULL, NULL, NULL, {0}},
+    {"accept_call", exec_action, NULL, NULL, NULL, {0}},
+    {"decline_call", exec_action, NULL, NULL, NULL, {0}},
+    {"show_shortcuts", action_show_shortcuts, NULL, NULL, NULL, {0}},
 };
 
 static void
diff --git a/src/ringmainwindow.cpp b/src/ringmainwindow.cpp
index ea38fbea..b39530c0 100644
--- a/src/ringmainwindow.cpp
+++ b/src/ringmainwindow.cpp
@@ -315,6 +315,10 @@ public:
     void enterSettingsView();
     void leaveSettingsView();
 
+    int getCurrentUid();
+    void forCurrentConversation(const std::function<void(const lrc::api::conversation::Info&)>& func);
+    bool showOkCancelDialog(const std::string& title, const std::string& text);
+
     lrc::api::conversation::Info getCurrentConversation(GtkWidget* frame_call);
 
     void showAccountSelectorWidget(bool show = true);
@@ -1621,6 +1625,48 @@ CppImpl::leaveSettingsView()
     }
 }
 
+int
+CppImpl::getCurrentUid()
+{
+    const auto &treeview = gtk_notebook_get_current_page(
+        GTK_NOTEBOOK(widgets->notebook_contacts)) == contactRequestsPageNum
+            ? widgets->treeview_contact_requests
+            : widgets->treeview_conversations;
+    return conversations_view_get_current_selected(CONVERSATIONS_VIEW(treeview));
+}
+
+void
+CppImpl::forCurrentConversation(const std::function<void(const lrc::api::conversation::Info&)>& func)
+{
+    const auto current = getCurrentUid();
+    if (current == -1) return;
+    try {
+        auto conversation = accountInfo_->conversationModel->filteredConversation(current);
+        if (conversation.participants.empty()) return;
+        func(conversation);
+    } catch (...) {
+        g_warning("Can't retrieve conversation %d", current);
+    }
+}
+
+bool
+CppImpl::showOkCancelDialog(const std::string &title, const std::string &text)
+{
+    auto *confirm_dialog = gtk_message_dialog_new(
+        GTK_WINDOW(self), GTK_DIALOG_DESTROY_WITH_PARENT,
+        GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
+        "%s", text.c_str());
+    gtk_window_set_title(GTK_WINDOW(confirm_dialog), title.c_str());
+    gtk_dialog_set_default_response(GTK_DIALOG(confirm_dialog),
+                                    GTK_RESPONSE_CANCEL);
+    gtk_widget_show_all(confirm_dialog);
+
+    auto res = gtk_dialog_run(GTK_DIALOG(confirm_dialog));
+
+    gtk_widget_destroy(confirm_dialog);
+    return res == GTK_RESPONSE_OK;
+}
+
 void
 CppImpl::updateLrc(const std::string& id, const std::string& accountIdToFlagFreeable)
 {
@@ -2244,25 +2290,196 @@ ring_main_window_can_close(RingMainWindow* self)
 {
     g_return_val_if_fail(IS_RING_MAIN_WINDOW(self), true);
     auto* priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(self));
-    if (priv->cpp && !lrc::api::Lrc::activeCalls().empty()) {
-        auto* close_dialog = gtk_message_dialog_new(GTK_WINDOW(self),
-            GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
+    if (!lrc::api::Lrc::activeCalls().empty()) {
+        auto res = priv->cpp->showOkCancelDialog(
+            _("Stop current call?"),
             _("A call is currently ongoing. Do you want to close the window and stop all current calls?"));
-        gtk_window_set_title(GTK_WINDOW(close_dialog), _("Stop current call?"));
-        gtk_dialog_set_default_response(GTK_DIALOG(close_dialog), GTK_RESPONSE_CANCEL);
-        gtk_widget_show_all(close_dialog);
+        if (res) lrc::api::NewCallModel::hangupCallsAndConferences();
+        return res;
+    }
+    return true;
+}
 
-        auto res = gtk_dialog_run(GTK_DIALOG(close_dialog));
+void
+ring_main_window_display_account_list(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    gtk_combo_box_popup(GTK_COMBO_BOX(priv->combobox_account_selector));
+}
 
-        gtk_widget_destroy(close_dialog);
-        if (res == GTK_RESPONSE_OK) {
-            lrc::api::NewCallModel::hangupCallsAndConferences();
-            return true;
-        } else {
-            return false;
-        }
+void
+ring_main_window_search(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    gtk_widget_grab_focus(GTK_WIDGET(priv->search_entry));
+}
+
+void
+ring_main_window_conversations_list(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    auto smartViewPageNum = gtk_notebook_page_num(GTK_NOTEBOOK(priv->notebook_contacts),
+                                                               priv->scrolled_window_smartview);
+    gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook_contacts), smartViewPageNum);
+    gtk_widget_grab_focus(GTK_WIDGET(priv->treeview_conversations));
+}
+
+void
+ring_main_window_requests_list(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    if (!priv->cpp->accountInfo_->contactModel->hasPendingRequests()) return;
+    auto contactRequestsPageNum = gtk_notebook_page_num(GTK_NOTEBOOK(priv->notebook_contacts),
+                                                               priv->scrolled_window_contact_requests);
+    gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->notebook_contacts), contactRequestsPageNum);
+    gtk_widget_grab_focus(GTK_WIDGET(priv->treeview_contact_requests));
+}
+
+void
+ring_main_window_audio_call(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp);
+
+    priv->cpp->forCurrentConversation([&](const auto& conversation){
+        priv->cpp->accountInfo_->conversationModel->placeAudioOnlyCall(conversation.uid);
+    });
+}
+
+void
+ring_main_window_clear_history(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    priv->cpp->forCurrentConversation([&](const auto &conversation) {
+        auto res = priv->cpp->showOkCancelDialog(
+            _("Clear history"),
+            _("Do you really want to clear the history of this conversation?"));
+        if (!res) return;
+        priv->cpp->accountInfo_->conversationModel->clearHistory(conversation.uid);
+    });
+}
+
+void
+ring_main_window_remove_conversation(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    priv->cpp->forCurrentConversation([&](const auto& conversation){
+        auto res = priv->cpp->showOkCancelDialog(
+            _("Remove conversation"),
+            _("Do you really want to remove this conversation?"));
+        if (!res) return;
+        priv->cpp->accountInfo_->conversationModel->removeConversation(conversation.uid);
+    });
+}
+
+void
+ring_main_window_block_contact(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    priv->cpp->forCurrentConversation([&](const auto& conversation){
+        auto res = priv->cpp->showOkCancelDialog(
+            _("Block contact"),
+            _("Do you really want to block this contact?"));
+        if (!res) return;
+        priv->cpp->accountInfo_->conversationModel->removeConversation(conversation.uid, true);
+    });
+}
+
+void
+ring_main_window_unblock_contact(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    priv->cpp->forCurrentConversation([&](const auto& conversation){
+        auto& uri = conversation.participants[0];
+
+        auto contactInfo = priv->cpp->accountInfo_->contactModel->getContact(uri);
+        if (!contactInfo.isBanned) return;
+        priv->cpp->accountInfo_->contactModel->addContact(contactInfo);
+    });
+}
+
+void
+ring_main_window_copy_contact(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    priv->cpp->forCurrentConversation([&](const auto& conversation){
+        auto& contact = priv->cpp->accountInfo_->contactModel->getContact(conversation.participants.front());
+        auto bestName = contact.registeredName.empty() ? contact.profileInfo.uri : contact.registeredName;
+        auto text = (gchar *)bestName.c_str();
+        GtkClipboard* clip = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+        gtk_clipboard_set_text(clip, text, -1);
+        clip = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
+        gtk_clipboard_set_text(clip, text, -1);
+    });
+}
+
+void
+ring_main_window_add_contact(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp);
+
+    priv->cpp->forCurrentConversation([&](const auto &conversation) {
+        priv->cpp->accountInfo_->conversationModel->makePermanent(conversation.uid);
+    });
+}
+
+void
+ring_main_window_accept_call(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp && priv->cpp->accountInfo_);
+
+    // Select the first conversation of the list
+    auto current = priv->cpp->getCurrentUid();
+    if (current == -1) return;
+    try {
+        auto conversation = priv->cpp->accountInfo_->conversationModel->filteredConversation(current);
+        if (conversation.participants.empty()) return;
+        auto contactUri = conversation.participants.at(0);
+        auto contact = priv->cpp->accountInfo_->contactModel->getContact(contactUri);
+        // If the contact is pending, we should accept its request
+        if (contact.profileInfo.type == lrc::api::profile::Type::PENDING)
+            priv->cpp->accountInfo_->conversationModel->makePermanent(conversation.uid);
+        // Accept call
+        priv->cpp->accountInfo_->callModel->accept(conversation.callId);
+    } catch (...) {
+        g_warning("Can't retrieve conversation %d", current);
     }
-    return true;
+}
+
+void
+ring_main_window_decline_call(RingMainWindow *win)
+{
+    g_return_if_fail(IS_RING_MAIN_WINDOW(win));
+    auto *priv = RING_MAIN_WINDOW_GET_PRIVATE(RING_MAIN_WINDOW(win));
+    g_return_if_fail(priv && priv->cpp);
+
+    priv->cpp->forCurrentConversation([&](const auto &conversation) {
+        priv->cpp->accountInfo_->callModel->hangUp(conversation.callId);
+    });
 }
 
 //==============================================================================
diff --git a/src/ringmainwindow.h b/src/ringmainwindow.h
index 994abd50..ec5fd300 100644
--- a/src/ringmainwindow.h
+++ b/src/ringmainwindow.h
@@ -39,6 +39,20 @@ GType      ring_main_window_get_type (void) G_GNUC_CONST;
 GtkWidget *ring_main_window_new      (GtkApplication *app);
 void       ring_main_window_reset    (RingMainWindow *win);
 bool       ring_main_window_can_close(RingMainWindow *win);
+void       ring_main_window_display_account_list(RingMainWindow *win);
+void       ring_main_window_search(RingMainWindow *win);
+
+void ring_main_window_conversations_list(RingMainWindow *win);
+void ring_main_window_requests_list(RingMainWindow *win);
+void ring_main_window_audio_call(RingMainWindow *win);
+void ring_main_window_clear_history(RingMainWindow *win);
+void ring_main_window_remove_conversation(RingMainWindow *win);
+void ring_main_window_block_contact(RingMainWindow *win);
+void ring_main_window_unblock_contact(RingMainWindow *win);
+void ring_main_window_copy_contact(RingMainWindow *win);
+void ring_main_window_add_contact(RingMainWindow *win);
+void ring_main_window_accept_call(RingMainWindow *win);
+void ring_main_window_decline_call(RingMainWindow *win);
 
 G_END_DECLS
 
diff --git a/ui/help-overlay.ui b/ui/help-overlay.ui
new file mode 100644
index 00000000..f4163d08
--- /dev/null
+++ b/ui/help-overlay.ui
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkShortcutsWindow" id="help_overlay">
+    <property name="modal">true</property>
+    <child>
+      <object class="GtkShortcutsSection">
+        <property name="visible">true</property>
+        <property name="section-name">shortcuts</property>
+        <property name="max-height">10</property>
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">General</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;m</property>
+                <property name="title" translatable="yes" context="shortcut window">Open application menu</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;a</property>
+                <property name="title" translatable="yes" context="shortcut window">Open account list</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;f</property>
+                <property name="title" translatable="yes" context="shortcut window">Select search bar</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;l</property>
+                <property name="title" translatable="yes" context="shortcut window">Focus the list of conversations</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;r</property>
+                <property name="title" translatable="yes" context="shortcut window">Focus the list pending requests</property>
+              </object>
+            </child>
+          </object>
+        </child>
+
+
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Conversations</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">Return</property>
+                <property name="title" translatable="yes" context="shortcut window">Start a video call</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;c</property>
+                <property name="title" translatable="yes" context="shortcut window">Start an audio call</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;l</property>
+                <property name="title" translatable="yes" context="shortcut window">Clear history</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;a</property>
+                <property name="title" translatable="yes" context="shortcut window">Add conversation</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;Delete</property>
+                <property name="title" translatable="yes" context="shortcut window">Remove conversation</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;b</property>
+                <property name="title" translatable="yes" context="shortcut window">Block contact</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;u</property>
+                <property name="title" translatable="yes" context="shortcut window">Unblock contact</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;&lt;shift&gt;j</property>
+                <property name="title" translatable="yes" context="shortcut window">Copy contact name</property>
+              </object>
+            </child>
+          </object>
+        </child>
+
+
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Call</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;y</property>
+                <property name="title" translatable="yes" context="shortcut window">Accept call</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;d</property>
+                <property name="title" translatable="yes" context="shortcut window">Decline call</property>
+              </object>
+            </child>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Settings</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;s</property>
+                <property name="title" translatable="yes" context="shortcut window">Open/Close settings page</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;g</property>
+                <property name="title" translatable="yes" context="shortcut window">Open general settings</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;m</property>
+                <property name="title" translatable="yes" context="shortcut window">Open media settings</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;ctrl&gt;a</property>
+                <property name="title" translatable="yes" context="shortcut window">Open account settings</property>
+              </object>
+            </child>
+          </object>
+        </child>
+
+
+        <child>
+          <object class="GtkShortcutsGroup">
+            <property name="visible">true</property>
+            <property name="title" translatable="yes" context="shortcut window">Chat view</property>
+            <child>
+              <object class="GtkShortcutsShortcut">
+                <property name="visible">true</property>
+                <property name="accelerator">&lt;shift&gt;Return</property>
+                <property name="title" translatable="yes" context="shortcut window">Write on a new line</property>
+              </object>
+            </child>
+          </object>
+        </child>
+
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/ui/incomingcallview.ui b/ui/incomingcallview.ui
index dd7ed18c..4fb1072c 100644
--- a/ui/incomingcallview.ui
+++ b/ui/incomingcallview.ui
@@ -151,7 +151,6 @@
                         <property name="has_tooltip">True</property>
                         <property name="tooltip-text" translatable="yes">Accept</property>
                         <property name="image">image_accept</property>
-                        <property name="action-name">app.accept</property>
                       </object>
                       <packing>
                         <property name="expand">True</property>
@@ -173,7 +172,6 @@
                         <property name="tooltip-text" translatable="yes">Reject</property>
                         <property name="can_focus">True</property>
                         <property name="image">image_reject</property>
-                        <property name="action-name">app.hangup</property>
                       </object>
                       <packing>
                         <property name="expand">True</property>
diff --git a/ui/ringgearsmenu.ui b/ui/ringgearsmenu.ui
index 4b9c4e67..b0b1b485 100644
--- a/ui/ringgearsmenu.ui
+++ b/ui/ringgearsmenu.ui
@@ -3,12 +3,10 @@
   <!-- interface-requires gtk+ 3.0 -->
   <menu id="menu">
     <section>
-      <!-- TODO: add help
       <item>
-        <attribute name="label" translatable="yes">_Help</attribute>
-        <attribute name="action">app.help</attribute>
+        <attribute name="label" translatable="yes">Keyboard _Shortcuts</attribute>
+        <attribute name="action">app.show_shortcuts</attribute>
       </item>
-      -->
       <item>
         <attribute name="label" translatable="yes">_About</attribute>
         <attribute name="action">app.about</attribute>
diff --git a/ui/ringmainwindow.ui b/ui/ringmainwindow.ui
index 436790bf..9d755705 100644
--- a/ui/ringmainwindow.ui
+++ b/ui/ringmainwindow.ui
@@ -56,6 +56,7 @@
             <property name="receives_default">False</property>
             <property name="tooltip_text" translatable="yes">Menu</property>
             <property name="direction">none</property>
+            <accelerator key="m" signal="activate" modifiers="GDK_CONTROL_MASK"/>
             <child>
               <object class="GtkImage" id="image_ring">
                 <property name="visible">True</property>
@@ -82,6 +83,7 @@
                     <property name="receives_default">False</property>
                     <property name="image">image_general_settings</property>
                     <property name="draw_indicator">False</property>
+                    <accelerator key="g" signal="activate" modifiers="GDK_CONTROL_MASK"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -98,6 +100,7 @@
                     <property name="image">image_media_settings</property>
                     <property name="draw_indicator">False</property>
                     <property name="group">radiobutton_general_settings</property>
+                    <accelerator key="m" signal="activate" modifiers="GDK_CONTROL_MASK"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -114,6 +117,7 @@
                     <property name="image">image_account_settings</property>
                     <property name="draw_indicator">False</property>
                     <property name="group">radiobutton_general_settings</property>
+                    <accelerator key="a" signal="activate" modifiers="GDK_CONTROL_MASK"/>
                   </object>
                   <packing>
                     <property name="expand">False</property>
@@ -144,6 +148,7 @@
                 <property name="can_focus">False</property>
                 <property name="receives_default">False</property>
                 <property name="tooltip_text" translatable="yes">Settings</property>
+                <accelerator key="s" signal="activate" modifiers="GDK_CONTROL_MASK"/>
                 <child>
                   <object class="GtkImage" id="image_settings">
                     <property name="visible">True</property>
diff --git a/ui/ui.gresource.xml b/ui/ui.gresource.xml
index 5a519b84..2b7a446d 100644
--- a/ui/ui.gresource.xml
+++ b/ui/ui.gresource.xml
@@ -15,5 +15,6 @@
     <file preprocess="xml-stripblanks">avatarmanipulation.ui</file>
     <file preprocess="xml-stripblanks">webkitchatcontainer.ui</file>
     <file preprocess="xml-stripblanks">usernameregistrationbox.ui</file>
+    <file preprocess="xml-stripblanks">help-overlay.ui</file>
   </gresource>
 </gresources>
-- 
GitLab