/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright © 2019 Endless Mobile, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * Authors: * - Philip Withnall */ #include "config.h" #include #include #include #include #include #include #include #include #include "application.h" #include "user-selector.h" static void user_selector_notify_user_cb (GObject *obj, GParamSpec *pspec, gpointer user_data); static void user_manager_notify_is_loaded_cb (GObject *obj, GParamSpec *pspec, gpointer user_data); static void permission_new_cb (GObject *source_object, GAsyncResult *result, gpointer user_data); static void permission_notify_allowed_cb (GObject *obj, GParamSpec *pspec, gpointer user_data); static void user_accounts_panel_button_clicked_cb (GtkButton *button, gpointer user_data); static void about_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data); static void help_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data); static void quit_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data); /** * MctApplication: * * #MctApplication is a top-level object representing the parental controls * application. * * Since: 0.5.0 */ struct _MctApplication { GtkApplication parent_instance; GCancellable *cancellable; /* (owned) */ GDBusConnection *dbus_connection; /* (owned) */ ActUserManager *user_manager; /* (owned) */ GPermission *permission; /* (owned) */ GError *permission_error; /* (nullable) (owned) */ MctUserSelector *user_selector; MctUserControls *user_controls; GtkStack *main_stack; GtkLabel *error_title; GtkLabel *error_message; GtkLockButton *lock_button; GtkButton *user_accounts_panel_button; GtkLabel *help_label; }; G_DEFINE_TYPE (MctApplication, mct_application, GTK_TYPE_APPLICATION) static void mct_application_init (MctApplication *self) { self->cancellable = g_cancellable_new (); } static void mct_application_constructed (GObject *object) { GApplication *application = G_APPLICATION (object); const GOptionEntry options[] = { { "user", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, /* Translators: This documents the --user command line option to malcontent-control: */ N_("User to select in the UI"), /* Translators: This is a placeholder for a command line argument value: */ N_("USERNAME") }, }; g_application_set_application_id (application, "org.freedesktop.MalcontentControl"); g_application_add_main_option_entries (application, options); g_application_set_flags (application, g_application_get_flags (application) | G_APPLICATION_HANDLES_COMMAND_LINE); /* Translators: This is a summary of what the application does, displayed when * it’s run with --help: */ g_application_set_option_context_parameter_string (application, N_("— view and edit parental controls")); /* Localisation */ bindtextdomain ("malcontent", PACKAGE_LOCALE_DIR); bind_textdomain_codeset ("malcontent", "UTF-8"); textdomain ("malcontent"); g_set_application_name (_("Parental Controls")); gtk_window_set_default_icon_name ("org.freedesktop.MalcontentControl"); G_OBJECT_CLASS (mct_application_parent_class)->constructed (object); } static void mct_application_dispose (GObject *object) { MctApplication *self = MCT_APPLICATION (object); g_cancellable_cancel (self->cancellable); if (self->user_manager != NULL) { g_signal_handlers_disconnect_by_func (self->user_manager, user_manager_notify_is_loaded_cb, self); g_clear_object (&self->user_manager); } if (self->permission != NULL) { g_signal_handlers_disconnect_by_func (self->permission, permission_notify_allowed_cb, self); g_clear_object (&self->permission); } g_clear_object (&self->dbus_connection); g_clear_error (&self->permission_error); g_clear_object (&self->cancellable); G_OBJECT_CLASS (mct_application_parent_class)->dispose (object); } static GtkWindow * mct_application_get_main_window (MctApplication *self) { return gtk_application_get_active_window (GTK_APPLICATION (self)); } static void mct_application_activate (GApplication *application) { MctApplication *self = MCT_APPLICATION (application); GtkWindow *window = NULL; window = mct_application_get_main_window (self); if (window == NULL) { g_autoptr(GtkBuilder) builder = NULL; g_autoptr(GError) local_error = NULL; /* Ensure the types used in the UI are registered. */ g_type_ensure (MCT_TYPE_USER_CONTROLS); g_type_ensure (MCT_TYPE_USER_SELECTOR); /* Start loading the permission */ polkit_permission_new ("org.freedesktop.MalcontentControl.administration", NULL, self->cancellable, permission_new_cb, self); builder = gtk_builder_new (); g_assert (self->dbus_connection == NULL); self->dbus_connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, self->cancellable, &local_error); if (self->dbus_connection == NULL) { g_error ("Error getting system bus: %s", local_error->message); return; } g_assert (self->user_manager == NULL); self->user_manager = g_object_ref (act_user_manager_get_default ()); gtk_builder_set_translation_domain (builder, "malcontent"); gtk_builder_expose_object (builder, "user_manager", G_OBJECT (self->user_manager)); gtk_builder_expose_object (builder, "dbus_connection", G_OBJECT (self->dbus_connection)); gtk_builder_add_from_resource (builder, "/org/freedesktop/MalcontentControl/ui/main.ui", &local_error); g_assert (local_error == NULL); /* Set up the main window. */ window = GTK_WINDOW (gtk_builder_get_object (builder, "main_window")); gtk_window_set_application (window, GTK_APPLICATION (application)); self->main_stack = GTK_STACK (gtk_builder_get_object (builder, "main_stack")); self->user_selector = MCT_USER_SELECTOR (gtk_builder_get_object (builder, "user_selector")); self->user_controls = MCT_USER_CONTROLS (gtk_builder_get_object (builder, "user_controls")); self->error_title = GTK_LABEL (gtk_builder_get_object (builder, "error_title")); self->error_message = GTK_LABEL (gtk_builder_get_object (builder, "error_message")); self->lock_button = GTK_LOCK_BUTTON (gtk_builder_get_object (builder, "lock_button")); self->user_accounts_panel_button = GTK_BUTTON (gtk_builder_get_object (builder, "user_accounts_panel_button")); self->help_label = GTK_LABEL (gtk_builder_get_object (builder, "help_label")); /* Connect signals. */ g_signal_connect_object (self->user_selector, "notify::user", G_CALLBACK (user_selector_notify_user_cb), self, 0 /* flags */); g_signal_connect_object (self->user_accounts_panel_button, "clicked", G_CALLBACK (user_accounts_panel_button_clicked_cb), self, 0 /* flags */); g_signal_connect (self->user_manager, "notify::is-loaded", G_CALLBACK (user_manager_notify_is_loaded_cb), self); /* Work out whether to show the loading page or the main page, and show * the controls for the initially selected user. */ user_selector_notify_user_cb (G_OBJECT (self->user_selector), NULL, self); user_manager_notify_is_loaded_cb (G_OBJECT (self->user_manager), NULL, self); gtk_widget_show (GTK_WIDGET (window)); } /* Bring the window to the front. */ gtk_window_present (window); } static void mct_application_startup (GApplication *application) { const GActionEntry app_entries[] = { { "about", about_action_cb, NULL, NULL, NULL, { 0, } }, { "help", help_action_cb, NULL, NULL, NULL, { 0, } }, { "quit", quit_action_cb, NULL, NULL, NULL, { 0, } }, }; /* Chain up. */ G_APPLICATION_CLASS (mct_application_parent_class)->startup (application); g_action_map_add_action_entries (G_ACTION_MAP (application), app_entries, G_N_ELEMENTS (app_entries), application); gtk_application_set_accels_for_action (GTK_APPLICATION (application), "app.help", (const gchar * const[]) { "F1", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (application), "app.quit", (const gchar * const[]) { "q", "w", NULL }); } static gint mct_application_command_line (GApplication *application, GApplicationCommandLine *command_line) { MctApplication *self = MCT_APPLICATION (application); GVariantDict *options = g_application_command_line_get_options_dict (command_line); const gchar *username; /* Show the application. */ g_application_activate (application); /* Select a user if requested. */ if (g_variant_dict_lookup (options, "user", "&s", &username) && !mct_user_selector_select_user_by_username (self->user_selector, username)) g_warning ("Failed to select user ‘%s’", username); return 0; /* exit status */ } static void mct_application_class_init (MctApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *application_class = G_APPLICATION_CLASS (klass); object_class->constructed = mct_application_constructed; object_class->dispose = mct_application_dispose; application_class->activate = mct_application_activate; application_class->startup = mct_application_startup; application_class->command_line = mct_application_command_line; } static void about_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); const gchar *authors[] = { "Philip Withnall ", "Georges Basile Stavracas Neto ", "Andre Moreira Magalhaes ", NULL }; gtk_show_about_dialog (mct_application_get_main_window (self), "version", VERSION, "copyright", _("Copyright © 2019, 2020 Endless Mobile, Inc."), "authors", authors, /* Translators: this should be "translated" to the names of people who have translated Malcontent into this language, one per line. */ "translator-credits", _("translator-credits"), "logo-icon-name", "org.freedesktop.MalcontentControl", "license-type", GTK_LICENSE_GPL_2_0, "wrap-license", TRUE, /* Translators: "Malcontent" is the brand name of this project, so should not be translated. */ "website-label", _("Malcontent Website"), "website", "https://gitlab.freedesktop.org/pwithnall/malcontent", NULL); } static void help_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); g_autoptr(GError) local_error = NULL; if (!gtk_show_uri_on_window (mct_application_get_main_window (self), "help:malcontent", gtk_get_current_event_time (), &local_error)) { GtkWidget *dialog = gtk_message_dialog_new (mct_application_get_main_window (self), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("The help contents could not be displayed")); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", local_error->message); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); } } static void quit_action_cb (GSimpleAction *action, GVariant *parameters, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); g_application_quit (G_APPLICATION (self)); } static void update_main_stack (MctApplication *self) { gboolean is_user_manager_loaded, is_permission_loaded, has_permission; const gchar *new_page_name, *old_page_name; GtkWidget *new_focus_widget; ActUser *selected_user; /* The implementation of #ActUserManager guarantees that once is-loaded is * true, it is never reset to false. */ g_object_get (self->user_manager, "is-loaded", &is_user_manager_loaded, NULL); is_permission_loaded = (self->permission != NULL || self->permission_error != NULL); has_permission = (self->permission != NULL && g_permission_get_allowed (self->permission)); selected_user = mct_user_selector_get_user (self->user_selector); /* Handle any loading errors (including those from getting the permission). */ if ((is_user_manager_loaded && act_user_manager_no_service (self->user_manager)) || self->permission_error != NULL) { gtk_label_set_label (self->error_title, _("Failed to load user data from the system")); gtk_label_set_label (self->error_message, _("Please make sure that the AccountsService is installed and enabled.")); new_page_name = "error"; new_focus_widget = NULL; } else if (is_user_manager_loaded && selected_user == NULL) { new_page_name = "no-other-users"; new_focus_widget = GTK_WIDGET (self->user_accounts_panel_button); } else if (is_permission_loaded && !has_permission) { gtk_lock_button_set_permission (self->lock_button, self->permission); mct_user_controls_set_permission (self->user_controls, self->permission); new_page_name = "unlock"; new_focus_widget = GTK_WIDGET (self->lock_button); } else if (is_permission_loaded && is_user_manager_loaded) { g_autofree gchar *help_label = NULL; /* Translators: Replace the link to commonsensemedia.org with some * localised guidance for parents/carers on how to set restrictions on * their child/caree in a responsible way which is in keeping with the * best practice and culture of the region. If no suitable localised * guidance exists, and if the default commonsensemedia.org link is not * suitable, please file an issue against malcontent so we can discuss * further! * https://gitlab.freedesktop.org/pwithnall/malcontent/-/issues/new */ help_label = g_strdup_printf (_("It’s recommended that restrictions are " "set as part of an ongoing conversation " "with %s. " "Read guidance on what to consider."), act_user_get_real_name (selected_user)); gtk_label_set_markup (self->help_label, help_label); mct_user_controls_set_user (self->user_controls, selected_user); new_page_name = "controls"; new_focus_widget = GTK_WIDGET (self->user_controls); } else { new_page_name = "loading"; new_focus_widget = NULL; } old_page_name = gtk_stack_get_visible_child_name (self->main_stack); gtk_stack_set_visible_child_name (self->main_stack, new_page_name); if (new_focus_widget != NULL && !g_str_equal (old_page_name, new_page_name)) { if (gtk_widget_get_can_focus (new_focus_widget)) gtk_widget_grab_focus (new_focus_widget); else gtk_widget_child_focus (new_focus_widget, GTK_DIR_TAB_FORWARD); } } static void user_selector_notify_user_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); update_main_stack (self); } static void user_manager_notify_is_loaded_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); update_main_stack (self); } static void permission_new_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); g_autoptr(GPermission) permission = NULL; g_autoptr(GError) local_error = NULL; permission = polkit_permission_new_finish (result, &local_error); if (permission == NULL) { g_assert (self->permission_error == NULL); self->permission_error = g_steal_pointer (&local_error); g_debug ("Error getting permission: %s", self->permission_error->message); } else { g_assert (self->permission == NULL); self->permission = g_steal_pointer (&permission); g_signal_connect (self->permission, "notify::allowed", G_CALLBACK (permission_notify_allowed_cb), self); } /* Recalculate the UI. */ update_main_stack (self); } static void permission_notify_allowed_cb (GObject *obj, GParamSpec *pspec, gpointer user_data) { MctApplication *self = MCT_APPLICATION (user_data); update_main_stack (self); } static void user_accounts_panel_button_clicked_cb (GtkButton *button, gpointer user_data) { g_autoptr(GError) local_error = NULL; if (!g_spawn_command_line_async ("gnome-control-center user-accounts", &local_error)) { g_warning ("Error opening GNOME Control Center: %s", local_error->message); return; } } /** * mct_application_new: * * Create a new #MctApplication. * * Returns: (transfer full): a new #MctApplication * Since: 0.5.0 */ MctApplication * mct_application_new (void) { return g_object_new (MCT_TYPE_APPLICATION, NULL); }