From 6da84dfc79df3e469627f128729b00b7b5c5b8cd Mon Sep 17 00:00:00 2001 From: Jim Nelson Date: Wed, 15 Aug 2012 17:21:03 -0700 Subject: [PATCH] Support Ubuntu messaging menu: Closes #5648 libindicate is supported in this patch (with some framework in place to support others, such as libmessagingmenu if/when it comes down the pipe). libindicate support must be configured with a ./configure switch (which is one-half of #5607) and is off by default. Note that this patch does not fully implement our design spec for how new messages are cleared due to user interaction. That is covered by #5669. --- configure | 7 ++ src/CMakeLists.txt | 24 ++++++- src/client/geary-application.vala | 2 +- src/client/geary-controller.vala | 71 ++++++++++++++----- src/client/notification/libindicate.vala | 68 ++++++++++++++++++ .../notification/new-messages-indicator.vala | 58 +++++++++++++++ .../notification-bubble.vala | 3 +- src/client/notification/null-indicator.vala | 14 ++++ 8 files changed, 225 insertions(+), 22 deletions(-) create mode 100644 src/client/notification/libindicate.vala create mode 100644 src/client/notification/new-messages-indicator.vala rename src/client/{ui => notification}/notification-bubble.vala (99%) create mode 100644 src/client/notification/null-indicator.vala diff --git a/configure b/configure index 002a4f36..330aedf1 100755 --- a/configure +++ b/configure @@ -29,6 +29,9 @@ configure_help() { printf "\t--disable-icon-update\n" printf "\t\t\t\tDisable icon cache update.\n" + printf "\t--enable-libindicate\n" + printf "\t\t\t\tEnable libindicate messaging menu support.\n" + printf "\n" } @@ -72,6 +75,10 @@ do CMDLINE="${CMDLINE} -DDESKTOP_UPDATE=OFF" ;; + --enable-libindicate) + CMDLINE="${CMDLINE} -DENABLE_LIBINDICATE=ON" + ;; + *) abort $option ;; esac diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c5021c1d..9cae315d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -196,6 +196,11 @@ client/geary-config.vala client/geary-controller.vala client/main.vala +client/notification/libindicate.vala +client/notification/new-messages-indicator.vala +client/notification/notification-bubble.vala +client/notification/null-indicator.vala + client/ui/composer-window.vala client/ui/geary-login.vala client/ui/email-entry.vala @@ -212,7 +217,6 @@ client/ui/message-web-view.vala client/ui/password-dialog.vala client/ui/preferences-dialog.vala client/ui/webview-edit-fixer.vala -client/ui/notification-bubble.vala client/ui/sidebar/sidebar-branch.vala client/ui/sidebar/sidebar-common.vala @@ -249,6 +253,20 @@ include(ValaVersion) ensure_vala_version("0.17.4" MINIMUM) include(ValaPrecompile) +option(ENABLE_LIBINDICATE "Enable libindicate messaging menu support." OFF) +if (ENABLE_LIBINDICATE STREQUAL "ON") + message(STATUS "Building with messaging menu support.") + set(EXTRA_CLIENT_PKG_CONFIG + indicate-0.7>=0.6.1 + ) + set(EXTRA_CLIENT_PACKAGES + Dbusmenu-0.4 Indicate-0.7 + ) + set(EXTRA_VALA_OPTIONS + -D HAVE_LIBINDICATE + ) +endif () + # Packages find_package(PkgConfig) pkg_check_modules(DEPS REQUIRED @@ -264,6 +282,7 @@ pkg_check_modules(DEPS REQUIRED gmime-2.6>=2.6.0 gnome-keyring-1>=3.2.2 webkitgtk-3.0>=1.8.0 + ${EXTRA_CLIENT_PKG_CONFIG} ) set(ENGINE_PACKAGES @@ -271,7 +290,7 @@ set(ENGINE_PACKAGES ) set(CLIENT_PACKAGES - gtk+-3.0 gnome-keyring-1 webkitgtk-3.0 libnotify libcanberra + gtk+-3.0 gnome-keyring-1 webkitgtk-3.0 libnotify libcanberra ${EXTRA_CLIENT_PACKAGES} ) set(CONSOLE_PACKAGES @@ -306,6 +325,7 @@ set(VALAC_OPTIONS --enable-checking --debug --fatal-warnings + ${EXTRA_VALA_OPTIONS} ) # Engine (conceptually a library, but not built as a separate library) diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index 1da0e225..89146d90 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -162,7 +162,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., public override void activate(string[] args) { // If Geary is already running, show the main window and return. if (controller != null && controller.main_window != null) { - controller.main_window.present(); + controller.main_window.present_with_time((uint32) TimeVal().tv_sec); handle_args(args); return; } diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 9fabe41a..b7425aea 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -67,6 +67,7 @@ public class GearyController { public MainWindow main_window { get; private set; } + private Geary.EngineAccount? account = null; private Cancellable cancellable_folder = new Cancellable(); private Cancellable cancellable_inbox = new Cancellable(); private Cancellable cancellable_message = new Cancellable(); @@ -79,8 +80,7 @@ public class GearyController { private Geary.Conversation? last_deleted_conversation = null; private Gee.LinkedList composer_windows = new Gee.LinkedList(); private File? last_save_directory = null; - - private Geary.EngineAccount? account { get; private set; } + private NewMessagesIndicator new_messages_indicator; public GearyController() { // Setup actions. @@ -92,6 +92,12 @@ public class GearyController { // Listen for attempts to close the application. GearyApplication.instance.exiting.connect(on_application_exiting); + // New messages indicator (Ubuntuism) + new_messages_indicator = NewMessagesIndicator.create(); + new_messages_indicator.application_activated.connect(on_indicator_activated_application); + new_messages_indicator.composer_activated.connect(on_indicator_activated_composer); + new_messages_indicator.inbox_activated.connect(on_indicator_activated_inbox); + // Create the main window (must be done after creating actions.) main_window = new MainWindow(); @@ -267,6 +273,7 @@ public class GearyController { } inbox_folder.email_locally_appended.disconnect(on_inbox_new_email); + inbox_folder.email_flags_changed.disconnect(on_inbox_email_flags_changed); } try { @@ -435,7 +442,14 @@ public class GearyController { do_notify_new_email.begin(email_ids); } - public async void do_notify_new_email(Gee.Collection email_ids) { + private void on_inbox_email_flags_changed(Gee.Map ids) { + foreach(Geary.EmailIdentifier id in ids.keys) { + if (!ids[id].is_unread()) + new_messages_indicator.not_new_message(id); + } + } + + private async void do_notify_new_email(Gee.Collection email_ids) { try { Gee.List? list = yield inbox_folder.list_email_by_sparse_id_async(email_ids, NotificationBubble.REQUIRED_FIELDS | Geary.Email.Field.FLAGS, Geary.Folder.ListFlags.NONE, @@ -446,38 +460,57 @@ public class GearyController { return; } - int unread = 0; Geary.Email? last_unread = null; foreach (Geary.Email email in list) { if (email.email_flags.is_unread()) { - unread++; last_unread = email; + // notify via new messages indicator (i.e. libindicate, libmessagingmenu when available) + new_messages_indicator.new_message(email.id); } } - debug("do_notify_new_email: %d messages listed, %d unread", list.size, unread); + debug("do_notify_new_email: %d messages listed, %d unread", list.size, new_messages_indicator.count); + // notify via notification bubble (i.e. libnotify) NotificationBubble notification = new NotificationBubble(); notification.invoked.connect(on_notification_bubble_invoked); - if (unread == 1 && last_unread != null) + if (new_messages_indicator.count == 1 && last_unread != null) yield notification.notify_one_message_async(last_unread, cancellable_inbox); - else if (unread > 0) - notification.notify_new_mail(unread); + else if (new_messages_indicator.count > 0) + notification.notify_new_mail(new_messages_indicator.count); } catch (Error err) { debug("Unable to notify of new email: %s", err.message); } } private void on_notification_bubble_invoked(Geary.Email? email) { - if(inbox_folder != null) { + if (email == null || inbox_folder == null) + return; + + main_window.folder_list.select_path(inbox_folder.get_path()); + Geary.Conversation? conversation = current_conversations.get_conversation_for_email(email.id); + if (conversation != null) + main_window.message_list_view.select_conversation(conversation); + } + + private void on_indicator_activated_application(uint32 timestamp) { + main_window.present_with_time(timestamp); + } + + private void on_indicator_activated_composer(uint32 timestamp) { + main_window.present_with_time(timestamp); + on_new_message(); + } + + private void on_indicator_activated_inbox(uint32 timestamp) { + main_window.present_with_time(timestamp); + + // reset new messages + new_messages_indicator.clear_new_messages(); + + // attempt to select Inbox + if (inbox_folder != null) main_window.folder_list.select_path(inbox_folder.get_path()); - if(email != null) { - Geary.Conversation? conversation = current_conversations.get_conversation_for_email(email.id); - if(conversation != null) { - main_window.message_list_view.select_conversation(conversation); - } - } - } } private void on_conversation_appended(Geary.Conversation conversation, @@ -631,10 +664,14 @@ public class GearyController { if (folder.get_special_folder_type() == Geary.SpecialFolderType.INBOX && inbox_folder == null) { inbox_folder = folder; inbox_folder.email_locally_appended.connect(on_inbox_new_email); + inbox_folder.email_flags_changed.connect(on_inbox_email_flags_changed); // select the inbox and get the show started main_window.folder_list.select_path(folder.get_path()); inbox_folder.open_async.begin(false, cancellable_inbox); + + // reset new messages indicator + new_messages_indicator.clear_new_messages(); } folder.special_folder_type_changed.connect(on_special_folder_type_changed); diff --git a/src/client/notification/libindicate.vala b/src/client/notification/libindicate.vala new file mode 100644 index 00000000..b62c295c --- /dev/null +++ b/src/client/notification/libindicate.vala @@ -0,0 +1,68 @@ +/* Copyright 2011-2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class Libindicate : NewMessagesIndicator { +#if HAVE_LIBINDICATE + private Indicate.Server indicator; + private Indicate.Indicator compose; + private Indicate.Indicator inbox; + + public Libindicate() { + debug("Using libindicate for messaging menu support"); + + indicator = Indicate.Server.ref_default(); + indicator.set_type("message.email"); + + // Find the desktop file this app instance is using (running from build dir vs. install dir) + File desktop_file = GearyApplication.instance.get_resource_directory().get_child("geary.desktop"); + if (!desktop_file.query_exists()) + desktop_file = File.new_for_path("/usr/share/applications/geary.desktop"); + + indicator.set_desktop_file(desktop_file.get_path()); + indicator.server_display.connect(on_display_server); + + // Create "Compose Message" option and always display it + compose = new Indicate.Indicator.with_server(indicator); + compose.set_property_variant("name", _("Compose Message")); + compose.user_display.connect(on_activate_composer); + compose.show(); + + // Create "New Messages" option which is only displayed if new messages are available + inbox = new Indicate.Indicator.with_server(indicator); + inbox.set_property_variant("name", _("New Messages")); + inbox.user_display.connect(on_activate_inbox); + + notify["count"].connect(on_new_messages_changed); + + indicator.show(); + } + + private void on_new_messages_changed() { + if (count > 0) { + // count is in fact a string property + inbox.set_property_variant("count", count.to_string()); + inbox.set_property_bool("draw-attention", true); + + inbox.show(); + } else { + inbox.hide(); + } + } + + private void on_display_server(uint timestamp) { + application_activated(timestamp); + } + + private void on_activate_composer(uint timestamp) { + composer_activated(timestamp); + } + + private void on_activate_inbox(uint timestamp) { + inbox_activated(timestamp); + } +#endif +} + diff --git a/src/client/notification/new-messages-indicator.vala b/src/client/notification/new-messages-indicator.vala new file mode 100644 index 00000000..cd7150e7 --- /dev/null +++ b/src/client/notification/new-messages-indicator.vala @@ -0,0 +1,58 @@ +/* Copyright 2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// This is coded this way to allow for libindicate and libmessagingmenu to coexist in code (if not +// compiled at same time) and minimize the exposure of differences to the rest of the application. +// Subclasses should trap the "notify::count" signal and use that to perform whatever magic +// they need for their implementation. + +public abstract class NewMessagesIndicator : Object { + public int count { get; private set; default = 0; } + + private Gee.HashSet new_ids = new Gee.HashSet( + Geary.Hashable.hash_func, Geary.Equalable.equal_func); + + public signal void application_activated(uint32 timestamp); + + public signal void inbox_activated(uint32 timestamp); + + public signal void composer_activated(uint32 timestamp); + + protected NewMessagesIndicator() { + } + + public void new_message(Geary.EmailIdentifier email_id) { + new_ids.add(email_id); + update_count(); + } + + public void not_new_message(Geary.EmailIdentifier email_id) { + new_ids.remove(email_id); + update_count(); + } + + public void clear_new_messages() { + new_ids.clear(); + update_count(); + } + + private void update_count() { + // Documentation for "notify" signal seems to suggest that it's possible for the signal to + // fire even if the value of the property doesn't change. Since this signal can trigger + // big events, want to avoid firing it unless necessary + if (count != new_ids.size) + count = new_ids.size; + } + + public static NewMessagesIndicator create() { +#if HAVE_LIBINDICATE + return new Libindicate(); +#else + return new NullIndicator(); +#endif + } +} + diff --git a/src/client/ui/notification-bubble.vala b/src/client/notification/notification-bubble.vala similarity index 99% rename from src/client/ui/notification-bubble.vala rename to src/client/notification/notification-bubble.vala index 3053dfe0..174ab3d7 100644 --- a/src/client/ui/notification-bubble.vala +++ b/src/client/notification/notification-bubble.vala @@ -45,7 +45,7 @@ public class NotificationBubble : GLib.Object { invoked(email); GearyApplication.instance.activate(new string[0]); } - + public void notify_new_mail(int count) throws GLib.Error { // don't pass email if invoked email = null; @@ -113,7 +113,6 @@ public class NotificationBubble : GLib.Object { notification.set_hint_string("sound-name", sound); else play_sound(sound); - } public static void play_sound(string sound) { diff --git a/src/client/notification/null-indicator.vala b/src/client/notification/null-indicator.vala new file mode 100644 index 00000000..a98a94d4 --- /dev/null +++ b/src/client/notification/null-indicator.vala @@ -0,0 +1,14 @@ +/* Copyright 2012 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +// Do-nothing NewMessagesIndicator, used on non-Ubuntu compiles. + +public class NullIndicator : NewMessagesIndicator { + public NullIndicator() { + debug("No messaging menu support in this build"); + } +} +