Mercurial > projects > package-update-indicator
view pui-application.c @ 56:08747f3a718b
Add new setting to hide indicator menu items
The setting hide-menu-items is an array of strings containing the actions
associated with menu items that should be hidden from the indicator menu.
author | Guido Berhoerster <guido+pui@berhoerster.name> |
---|---|
date | Tue, 16 Aug 2022 16:19:02 +0200 |
parents | 24cba399757f |
children |
line wrap: on
line source
/* * Copyright (C) 2018 Guido Berhoerster <guido+pui@berhoerster.name> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include <gio/gdesktopappinfo.h> #include <glib/gi18n.h> #ifndef HAVE_AYATANA_APPINDICATOR #include <libappindicator/app-indicator.h> #else /* !HAVE_AYATANA_APPINDICATOR */ #include <libayatana-appindicator/app-indicator.h> #endif /* !HAVE_AYATANA_APPINDICATOR */ #include <stdlib.h> #include <string.h> #include "pui-application.h" #include "pui-backend.h" #include "pui-common.h" #include "pui-settings.h" #include "pui-types.h" struct _PuiApplication { GApplication parent_instance; GSettings *settings; GCancellable *cancellable; PuiBackend *backend; AppIndicator *indicator; GtkWidget *about_dialog; GIcon *icons[PUI_STATE_LAST]; PuiState state; gchar *update_command; gchar *error_message; gboolean always_active; }; G_DEFINE_TYPE(PuiApplication, pui_application, G_TYPE_APPLICATION) enum { PROP_0, PROP_UPDATE_COMMAND, PROP_ALWAYS_ACTIVE, PROP_LAST }; extern gboolean restart; static GParamSpec *properties[PROP_LAST] = { NULL }; static const gchar *icon_names[PUI_STATE_LAST] = { [PUI_STATE_INITIAL] = "system-software-update", [PUI_STATE_UP_TO_DATE] = "system-software-update", [PUI_STATE_NORMAL_UPDATES_AVAILABLE] = "software-update-available", [PUI_STATE_IMPORTANT_UPDATES_AVAILABLE] = "software-update-urgent", [PUI_STATE_SESSION_RESTART_REQUIRED] = "system-log-out", [PUI_STATE_SYSTEM_RESTART_REQUIRED] = "system-reboot", [PUI_STATE_ERROR] = "dialog-warning" }; static const GOptionEntry cmd_options[] = { { "debug", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Enable debugging messages"), NULL }, { "quit", 'q', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Quit running instance of Package Update Indicator"), NULL }, { "version", 'V', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, N_("Print the version number and quit"), NULL }, { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, NULL, NULL, NULL }, { NULL } }; static void pui_application_show_about_dialog(GSimpleAction *, GVariant *, gpointer); static void pui_application_open_preferences(GSimpleAction *, GVariant *, gpointer); static void pui_application_quit(GSimpleAction *, GVariant *, gpointer); static void pui_application_install_updates(GSimpleAction *, GVariant *, gpointer); static const GActionEntry pui_application_actions[] = { { "about", pui_application_show_about_dialog }, { "preferences", pui_application_open_preferences }, { "quit", pui_application_quit }, { "install-updates", pui_application_install_updates } }; static gboolean program_exists(const gchar *command_line) { gboolean is_program_in_path; gchar **argv = NULL; gchar *program_path; if (!g_shell_parse_argv(command_line, NULL, &argv, NULL)) { return (FALSE); } program_path = g_find_program_in_path(argv[0]); is_program_in_path = (program_path != NULL) ? TRUE : FALSE; g_free(program_path); g_strfreev(argv); return (is_program_in_path); } static void pui_application_show_about_dialog(GSimpleAction *simple, GVariant *parameter, gpointer user_data) { PuiApplication *self = user_data; GtkBuilder *builder; if (self->about_dialog == NULL) { /* get dialog from builder */ builder = gtk_builder_new_from_resource("/org" "/guido-berhoerster/code/package-update-indicator" "/pui-about-dialog.ui"); self->about_dialog = GTK_WIDGET(gtk_builder_get_object(builder, "about-dialog")); g_object_unref(builder); } gtk_dialog_run(GTK_DIALOG(self->about_dialog)); gtk_widget_hide(self->about_dialog); } static void pui_application_open_preferences(GSimpleAction *simple, GVariant *parameter, gpointer user_data) { GDesktopAppInfo *prefs_app_info; GError *error = NULL; prefs_app_info = g_desktop_app_info_new("org.guido-berhoerster.code." "package-update-indicator.preferences.desktop"); if (prefs_app_info == NULL) { g_warning("desktop file \"org.guido-berhoerster.code." "package-update-indicator.preferences.desktop\" not found"); return; } if (!g_app_info_launch(G_APP_INFO(prefs_app_info), NULL, NULL, &error)) { g_warning("failed to launch preferences: %s", error->message); g_error_free(error); } } static void pui_application_quit(GSimpleAction *simple, GVariant *parameter, gpointer user_data) { PuiApplication *self = user_data; /* quit the GTK main loop if the about dialog is running */ if (self->about_dialog != NULL) { gtk_widget_hide(self->about_dialog); } g_application_quit(G_APPLICATION(self)); } static void pui_application_install_updates(GSimpleAction *simple, GVariant *parameter, gpointer user_data) { PuiApplication *self = user_data; GError *error = NULL; if (!g_spawn_command_line_async(self->update_command, &error)) { g_warning("failed to run update command: %s", error->message); g_error_free(error); } } static void update_menu_item(GtkWidget* widget, gpointer user_data) { gchar ** hidden_menu_items = user_data; const gchar * action_name; const gchar * name; g_return_if_fail(GTK_IS_MENU_ITEM(widget)); action_name = gtk_actionable_get_action_name(GTK_ACTIONABLE(widget)); if (!g_str_has_prefix(action_name, "app.")) { return; } name = action_name + strlen("app."); gtk_widget_set_visible(widget, !g_strv_contains((const gchar * const *)hidden_menu_items, name)); } static void update_menu_items(PuiApplication *self) { gchar ** hidden_menu_items; GtkMenu * menu; menu = app_indicator_get_menu(self->indicator); hidden_menu_items = g_settings_get_strv(self->settings, "hide-menu-items"); g_return_if_fail(hidden_menu_items != NULL); g_return_if_fail((menu != NULL) && GTK_IS_CONTAINER(menu)); gtk_container_foreach(GTK_CONTAINER(menu), update_menu_item, hidden_menu_items); g_strfreev(hidden_menu_items); } static void update_indicator(PuiApplication *self, const gchar *title) { switch (self->state) { case PUI_STATE_INITIAL: app_indicator_set_status(self->indicator, APP_INDICATOR_STATUS_PASSIVE); break; case PUI_STATE_UP_TO_DATE: app_indicator_set_status(self->indicator, self->always_active ? APP_INDICATOR_STATUS_ACTIVE : APP_INDICATOR_STATUS_PASSIVE); break; case PUI_STATE_NORMAL_UPDATES_AVAILABLE: /* FALLTHGROUGH */ case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE: /* FALLTHGROUGH */ case PUI_STATE_SESSION_RESTART_REQUIRED: /* FALLTHGROUGH */ case PUI_STATE_SYSTEM_RESTART_REQUIRED: /* FALLTHGROUGH */ case PUI_STATE_ERROR: app_indicator_set_status(self->indicator, APP_INDICATOR_STATUS_ACTIVE); break; } app_indicator_set_icon_full(self->indicator, icon_names[self->state], title); } static void update_notification(PuiApplication *self, const gchar *title, const gchar *body) { GSimpleAction *install_updates_action; GApplication *application = G_APPLICATION(self); GNotification *notification = NULL; install_updates_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(self), "install-updates")); switch (self->state) { case PUI_STATE_INITIAL: /* FALLTHGROUGH */ case PUI_STATE_UP_TO_DATE: /* FALLTHGROUGH */ case PUI_STATE_ERROR: /* withdraw exisiting notification */ g_application_withdraw_notification(application, "package-updates-or-restart-required"); break; case PUI_STATE_NORMAL_UPDATES_AVAILABLE: /* FALLTHGROUGH */ case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE: /* FALLTHGROUGH */ case PUI_STATE_SESSION_RESTART_REQUIRED: /* FALLTHGROUGH */ case PUI_STATE_SYSTEM_RESTART_REQUIRED: /* create notification */ notification = g_notification_new(title); g_notification_set_body(notification, body); g_notification_set_icon(notification, self->icons[self->state]); g_notification_set_priority(notification, G_NOTIFICATION_PRIORITY_NORMAL); if (g_action_get_enabled(G_ACTION(install_updates_action))) { g_notification_add_button(notification, _("Install Updates"), "app.install-updates"); } g_application_send_notification(application, "package-updates-or-restart-required", notification); break; } if (notification != NULL) { g_object_unref(notification); } } static void update_ui(PuiApplication *self) { GSimpleAction *install_updates_action; guint important_updates = 0; guint normal_updates = 0; gchar *title = NULL; gchar *body = NULL; GApplication *application = G_APPLICATION(self); install_updates_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(self), "install-updates")); if ((self->state == PUI_STATE_NORMAL_UPDATES_AVAILABLE) || (self->state == PUI_STATE_IMPORTANT_UPDATES_AVAILABLE)) { g_object_get(self->backend, "important-updates", &important_updates, "normal-updates", &normal_updates, NULL); } /* actions */ switch (self->state) { case PUI_STATE_INITIAL: /* FALLTHGROUGH */ case PUI_STATE_UP_TO_DATE: /* FALLTHGROUGH */ case PUI_STATE_SESSION_RESTART_REQUIRED: /* FALLTHGROUGH */ case PUI_STATE_SYSTEM_RESTART_REQUIRED: /* FALLTHGROUGH */ case PUI_STATE_ERROR: g_simple_action_set_enabled(install_updates_action, FALSE); break; case PUI_STATE_NORMAL_UPDATES_AVAILABLE: /* FALLTHROUGH */ case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE: g_simple_action_set_enabled(install_updates_action, program_exists(self->update_command)); break; } /* title and body for indicator and notification */ switch (self->state) { case PUI_STATE_INITIAL: title = g_strdup(""); body = g_strdup(""); break; case PUI_STATE_UP_TO_DATE: title = g_strdup(_("Up to Date")); body = g_strdup(_("The system is up to date.")); break; case PUI_STATE_NORMAL_UPDATES_AVAILABLE: title = g_strdup(g_dngettext(NULL, "Software Update", "Software Updates", (gulong)normal_updates)); if (normal_updates == 1) { body = g_strdup(_("There is a software update " "available.")); } else { body = g_strdup_printf(_("There are %u " "software updates available."), normal_updates); } break; case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE: title = g_strdup(g_dngettext(NULL, "Important Software Update", "Important Software Updates", (gulong)important_updates)); if ((normal_updates == 0) && (important_updates == 1)) { body = g_strdup(_("There is an important " "software update available.")); } else if ((normal_updates == 0) && (important_updates > 1)) { body = g_strdup_printf(_("There are %u " "important software updates available."), important_updates); } else if ((normal_updates > 0) && (important_updates == 1)) { body = g_strdup_printf(_("There are %u " "software updates available, " "one of them is important."), normal_updates + important_updates); } else { body = g_strdup_printf(_("There are %u " "software updates available, " "%u of them are important."), normal_updates + important_updates, important_updates); } break; case PUI_STATE_SESSION_RESTART_REQUIRED: title = g_strdup(_("Logout Required")); body = g_strdup(_("You need to log out and back in for the " "update to take effect.")); break; case PUI_STATE_SYSTEM_RESTART_REQUIRED: title = g_strdup(_("Restart Required")); body = g_strdup(_("The computer has to be restarted for the " "updates to take effect.")); break; case PUI_STATE_ERROR: title = g_strdup(self->error_message); break; } update_indicator(self, title); update_menu_items(self); update_notification(self, title, body); g_debug("indicator icon: %s, notification title: \"%s\", " "notification body: \"%s\"", icon_names[self->state], title, body); g_free(body); g_free(title); } static void transition_state(PuiApplication *self) { PuiState state = self->state; PuiRestart restart_type; guint important_updates; guint normal_updates; gchar *old_state; gchar *new_state; switch (self->state) { case PUI_STATE_INITIAL: /* FALLTHROUGH */ case PUI_STATE_UP_TO_DATE: /* FALLTHROUGH */ case PUI_STATE_NORMAL_UPDATES_AVAILABLE: /* FALLTHROUGH */ case PUI_STATE_IMPORTANT_UPDATES_AVAILABLE: if (self->error_message != NULL) { state = PUI_STATE_ERROR; break; } g_object_get(self->backend, "restart-type", &restart_type, "important-updates", &important_updates, "normal-updates", &normal_updates, NULL); if (restart_type == PUI_RESTART_SESSION) { state = PUI_STATE_SESSION_RESTART_REQUIRED; } else if (restart_type == PUI_RESTART_SYSTEM) { state = PUI_STATE_SYSTEM_RESTART_REQUIRED; } else if (important_updates > 0) { state = PUI_STATE_IMPORTANT_UPDATES_AVAILABLE; } else if (normal_updates > 0) { state = PUI_STATE_NORMAL_UPDATES_AVAILABLE; } else { state = PUI_STATE_UP_TO_DATE; } break; case PUI_STATE_SESSION_RESTART_REQUIRED: g_object_get(self->backend, "restart-type", &restart_type, NULL); if (restart_type == PUI_RESTART_SYSTEM) { state = PUI_STATE_SYSTEM_RESTART_REQUIRED; } break; case PUI_STATE_SYSTEM_RESTART_REQUIRED: /* FALLTHROUGH */ case PUI_STATE_ERROR: break; } if (state != self->state) { old_state = pui_types_enum_to_string(PUI_TYPE_STATE, self->state); new_state = pui_types_enum_to_string(PUI_TYPE_STATE, state); g_debug("state %s -> %s", old_state, new_state); self->state = state; update_ui(self); g_free(new_state); g_free(old_state); } } static void on_hide_menu_items_changed(GSettings *settings, gchar *key, gpointer user_data) { g_debug("setting hide-menu-items changed"); update_menu_items(user_data); } static void on_backend_restart_required(PuiBackend *backend, gpointer user_data) { PuiApplication *self = user_data; restart = TRUE; g_action_group_activate_action(G_ACTION_GROUP(G_APPLICATION(self)), "quit", NULL); } static void on_backend_state_changed(PuiBackend *backend, gpointer user_data) { PuiApplication *self = user_data; transition_state(self); } static void on_pui_backend_finished(GObject *source_object, GAsyncResult *result, gpointer user_data) { PuiApplication *self = user_data; GError *error = NULL; self->backend = pui_backend_new_finish(result, &error); if (self->backend == NULL) { g_warning("failed to instantiate backend: %s", error->message); g_free(self->error_message); g_error_free(error); self->error_message = g_strdup(_("Update notifications " "are not supported.")); transition_state(self); return; } pui_backend_set_proxy(self->backend, g_getenv("http_proxy"), g_getenv("https_proxy"), g_getenv("ftp_proxy"), g_getenv("socks_proxy"), g_getenv("no_proxy"), NULL); g_settings_bind(self->settings, "refresh-interval", self->backend, "refresh-interval", G_SETTINGS_BIND_GET); g_settings_bind(self->settings, "use-mobile-connection", self->backend, "use-mobile-connection", G_SETTINGS_BIND_GET); transition_state(self); g_signal_connect(self->backend, "restart-required", G_CALLBACK(on_backend_restart_required), self); g_signal_connect(self->backend, "state-changed", G_CALLBACK(on_backend_state_changed), self); } static void pui_application_startup(GApplication *application) { PuiApplication *self = PUI_APPLICATION(application); gsize i; GtkBuilder *builder; GtkWidget *menu; G_APPLICATION_CLASS(pui_application_parent_class)->startup(application); /* create actions */ g_action_map_add_action_entries(G_ACTION_MAP(self), pui_application_actions, G_N_ELEMENTS(pui_application_actions), self); /* load icons */ for (i = 0; i < G_N_ELEMENTS(self->icons); i++) { self->icons[i] = g_themed_icon_new(icon_names[i]); } /* create settings backend */ self->settings = pui_settings_new(); /* start instantiating backend */ pui_backend_new_async(self->cancellable, on_pui_backend_finished, self); /* create indicator */ self->indicator = app_indicator_new(APPLICATION_ID, "system-software-update", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); app_indicator_set_title(self->indicator, _("Package Update Indicator")); /* get menu from builder and add it to the indicator */ builder = gtk_builder_new_from_resource("/org/guido-berhoerster/code/" "package-update-indicator/pui-menu.ui"); menu = GTK_WIDGET(gtk_builder_get_object(builder, "menu")); gtk_widget_insert_action_group(menu, "app", G_ACTION_GROUP(self)); gtk_widget_show_all(menu); app_indicator_set_menu(self->indicator, GTK_MENU(menu)); /* * bind settings to properties after indicator with its menu are * initialized, if the setting "always-active" is set by the user the * corresponding property is set accordingly which in turn invokes * update_ui() so order matters here */ g_settings_bind(self->settings, "update-command", self, "update-command", G_SETTINGS_BIND_GET); g_settings_bind(self->settings, "always-active", self, "always-active", G_SETTINGS_BIND_GET); /* watch hide-menu-items setting in order to update the menu */ g_signal_connect(self->settings, "changed::hide-menu-items", G_CALLBACK(on_hide_menu_items_changed), self); /* ensure the ui reflects the current state */ update_ui(self); /* keep GApplication running */ g_application_hold(application); g_object_unref(builder); } static void pui_application_shutdown(GApplication *application) { GApplicationClass *application_class = G_APPLICATION_CLASS(pui_application_parent_class); application_class->shutdown(application); } static gint pui_application_handle_local_options(GApplication *application, GVariantDict *options) { gchar *messages_debug; gchar **args = NULL; GError *error = NULL; /* filename arguments are not allowed */ if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^a&ay", &args)) { g_printerr("invalid argument: \"%s\"\n", args[0]); g_free(args); return (1); } if (g_variant_dict_contains(options, "version")) { g_print("%s %s\n", PACKAGE, VERSION); /* quit */ return (0); } if (g_variant_dict_contains(options, "debug")) { /* enable debug logging */ messages_debug = g_strjoin(":", G_LOG_DOMAIN, g_getenv("G_MESSAGES_DEBUG"), NULL); g_setenv("G_MESSAGES_DEBUG", messages_debug, TRUE); g_free(messages_debug); } /* * register with the session bus so that it is possible to discern * between remote and primary instance and that remote actions can be * invoked, this causes the startup signal to be emitted which, in case * of the primary instance, starts to instantiate the * backend with the given values */ if (!g_application_register(application, NULL, &error)) { g_critical("g_application_register: %s", error->message); g_error_free(error); return (1); } if (g_variant_dict_contains(options, "quit")) { /* only valid if a remote instance is running */ if (!g_application_get_is_remote(application)) { g_printerr("%s is not running\n", g_get_prgname()); return (1); } /* signal remote instance to quit */ g_action_group_activate_action(G_ACTION_GROUP(application), "quit", NULL); /* quit local instance */ return (0); } /* proceed with default command line processing */ return (-1); } static void pui_application_activate(GApplication *application) { GApplicationClass *application_class = G_APPLICATION_CLASS(pui_application_parent_class); /* do nothing, implementation required by GApplication */ application_class->activate(application); } static void pui_application_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PuiApplication *self = PUI_APPLICATION(object); switch (property_id) { case PROP_UPDATE_COMMAND: g_free(self->update_command); self->update_command = g_value_dup_string(value); g_debug("property \"update-command\" set to \"%s\"", self->update_command); break; case PROP_ALWAYS_ACTIVE: self->always_active = g_value_get_boolean(value); g_debug("property \"always-active\" set to \"%s\"", self->always_active ? "TRUE" : "FALSE"); update_ui(self); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void pui_application_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PuiApplication *self = PUI_APPLICATION(object); switch (property_id) { case PROP_UPDATE_COMMAND: g_value_set_string(value, self->update_command); break; case PROP_ALWAYS_ACTIVE: g_value_set_boolean(value, self->always_active); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void pui_application_dispose(GObject *object) { PuiApplication *self = PUI_APPLICATION(object); gsize i; if (self->settings != NULL) { g_signal_handlers_disconnect_by_data(self->settings, self); g_clear_object(&self->settings); } if (self->cancellable != NULL) { g_cancellable_cancel(self->cancellable); g_clear_object(&self->cancellable); } if (self->backend != NULL) { g_clear_object(&self->backend); } if (self->indicator != NULL) { g_clear_object(&self->indicator); } if (self->about_dialog != NULL) { g_clear_pointer(&self->about_dialog, gtk_widget_destroy); } for (i = 0; i < G_N_ELEMENTS(self->icons); i++) { if (self->icons[i] != NULL) { g_clear_object(&self->icons[i]); } } G_OBJECT_CLASS(pui_application_parent_class)->dispose(object); } static void pui_application_finalize(GObject *object) { PuiApplication *self = PUI_APPLICATION(object); g_free(self->update_command); g_free(self->error_message); G_OBJECT_CLASS(pui_application_parent_class)->finalize(object); } static void pui_application_class_init(PuiApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); GApplicationClass *application_class = G_APPLICATION_CLASS(klass); object_class->set_property = pui_application_set_property; object_class->get_property = pui_application_get_property; object_class->dispose = pui_application_dispose; object_class->finalize = pui_application_finalize; properties[PROP_UPDATE_COMMAND] = g_param_spec_string("update-command", "Update command", "Command for installing updates", NULL, G_PARAM_READWRITE); properties[PROP_ALWAYS_ACTIVE] = g_param_spec_boolean("always-active", "Whether the indicator is always active", "Whether the indicator is active even when packages are up to date", TRUE, G_PARAM_READWRITE); g_object_class_install_properties(object_class, PROP_LAST, properties); application_class->startup = pui_application_startup; application_class->shutdown = pui_application_shutdown; application_class->handle_local_options = pui_application_handle_local_options; application_class->activate = pui_application_activate; } static void pui_application_init(PuiApplication *self) { g_application_add_main_option_entries(G_APPLICATION(self), cmd_options); self->cancellable = g_cancellable_new(); } PuiApplication * pui_application_new(void) { return (g_object_new(PUI_TYPE_APPLICATION, "application-id", APPLICATION_ID, NULL)); }