diff --git a/malcontent-control/malcontent-control.gresource.xml b/malcontent-control/malcontent-control.gresource.xml index f74e34e..516a033 100644 --- a/malcontent-control/malcontent-control.gresource.xml +++ b/malcontent-control/malcontent-control.gresource.xml @@ -6,5 +6,6 @@ carousel.ui main.ui user-controls.ui + user-selector.ui diff --git a/malcontent-control/meson.build b/malcontent-control/meson.build index 8abe3b7..4e66a6e 100644 --- a/malcontent-control/meson.build +++ b/malcontent-control/meson.build @@ -23,6 +23,8 @@ malcontent_control = executable('malcontent-control', 'user-controls.h', 'user-image.c', 'user-image.h', + 'user-selector.c', + 'user-selector.h', ] + resources, dependencies: [ dependency('accountsservice'), @@ -89,6 +91,7 @@ if xmllint.found() 'carousel.ui', 'main.ui', 'user-controls.ui', + 'user-selector.ui', ), ], suite: ['malcontent-control'], diff --git a/malcontent-control/user-selector.c b/malcontent-control/user-selector.c new file mode 100644 index 0000000..046fc93 --- /dev/null +++ b/malcontent-control/user-selector.c @@ -0,0 +1,470 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 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 +#include +#include +#include +#include +#include + +#include "carousel.h" +#include "user-image.h" +#include "user-selector.h" + + +static void reload_users (MctUserSelector *self, + ActUser *selected_user); +static void notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data); +static void user_added_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data); +static void user_changed_or_removed_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data); +static void carousel_item_activated (MctCarousel *carousel, + MctCarouselItem *item, + gpointer user_data); + + +/** + * MctUserSelector: + * + * The user selector is a widget which lists available user accounts and allows + * the user to select one. + * + * Since: 0.5.0 + */ +struct _MctUserSelector +{ + GtkBox parent_instance; + + MctCarousel *carousel; + + ActUserManager *user_manager; /* (owned) */ + ActUser *user; /* (owned) */ +}; + +G_DEFINE_TYPE (MctUserSelector, mct_user_selector, GTK_TYPE_BOX) + +typedef enum +{ + PROP_USER = 1, + PROP_USER_MANAGER, +} MctUserSelectorProperty; + +static GParamSpec *properties[PROP_USER_MANAGER + 1]; + +static void +mct_user_selector_constructed (GObject *obj) +{ + MctUserSelector *self = MCT_USER_SELECTOR (obj); + + g_assert (self->user_manager != NULL); + + g_signal_connect (self->user_manager, "user-changed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "user-is-logged-in-changed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "user-added", + G_CALLBACK (user_added_cb), self); + g_signal_connect (self->user_manager, "user-removed", + G_CALLBACK (user_changed_or_removed_cb), self); + g_signal_connect (self->user_manager, "notify::is-loaded", + G_CALLBACK (notify_is_loaded_cb), self); + + /* Start loading the user accounts. */ + notify_is_loaded_cb (G_OBJECT (self->user_manager), NULL, self); + + G_OBJECT_CLASS (mct_user_selector_parent_class)->constructed (obj); +} + +static void +mct_user_selector_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MctUserSelector *self = MCT_USER_SELECTOR (object); + + switch ((MctUserSelectorProperty) prop_id) + { + case PROP_USER: + g_value_set_object (value, self->user); + break; + + case PROP_USER_MANAGER: + g_value_set_object (value, self->user_manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_selector_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MctUserSelector *self = MCT_USER_SELECTOR (object); + + switch ((MctUserSelectorProperty) prop_id) + { + case PROP_USER: + /* Currently read only */ + g_assert_not_reached (); + break; + + case PROP_USER_MANAGER: + g_assert (self->user_manager == NULL); + self->user_manager = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +mct_user_selector_dispose (GObject *object) +{ + MctUserSelector *self = (MctUserSelector *)object; + + g_clear_object (&self->user); + + if (self->user_manager != NULL) + { + g_signal_handlers_disconnect_by_func (self->user_manager, notify_is_loaded_cb, self); + g_signal_handlers_disconnect_by_func (self->user_manager, user_changed_or_removed_cb, self); + g_signal_handlers_disconnect_by_func (self->user_manager, user_added_cb, self); + + g_clear_object (&self->user_manager); + } + + G_OBJECT_CLASS (mct_user_selector_parent_class)->dispose (object); +} + +static void +mct_user_selector_class_init (MctUserSelectorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = mct_user_selector_constructed; + object_class->get_property = mct_user_selector_get_property; + object_class->set_property = mct_user_selector_set_property; + object_class->dispose = mct_user_selector_dispose; + + /** + * MctUserSelector:user: (nullable) + * + * The currently selected user account, or %NULL if no user is selected. + * Currently read only but may become writable in future. + * + * Since: 0.5.0 + */ + properties[PROP_USER] = + g_param_spec_object ("user", + "User", + "The currently selected user account, or %NULL if no user is selected.", + ACT_TYPE_USER, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * MctUserSelector:user-manager: (not nullable) + * + * The user manager providing the data for the widget. + * + * Since: 0.5.0 + */ + properties[PROP_USER_MANAGER] = + g_param_spec_object ("user-manager", + "User Manager", + "The user manager providing the data for the widget.", + ACT_TYPE_USER_MANAGER, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/user-selector.ui"); + + gtk_widget_class_bind_template_child (widget_class, MctUserSelector, carousel); + + gtk_widget_class_bind_template_callback (widget_class, carousel_item_activated); +} + +static void +mct_user_selector_init (MctUserSelector *self) +{ + /* Ensure the types used in the UI are registered. */ + g_type_ensure (MCT_TYPE_CAROUSEL); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static void +notify_is_loaded_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + gboolean is_loaded; + + /* 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_loaded, NULL); + if (is_loaded) + reload_users (self, NULL); +} + +static const gchar * +get_real_or_user_name (ActUser *user) +{ + const gchar *name; + + name = act_user_get_real_name (user); + if (name == NULL) + name = act_user_get_user_name (user); + + return name; +} + +static void +carousel_item_activated (MctCarousel *carousel, + MctCarouselItem *item, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + uid_t uid; + ActUser *user = NULL; + + g_clear_object (&self->user); + + uid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "uid")); + user = act_user_manager_get_user_by_id (self->user_manager, uid); + + if (g_set_object (&self->user, user)) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USER]); +} + +static gint +sort_users (gconstpointer a, + gconstpointer b) +{ + ActUser *ua, *ub; + gint result; + + ua = ACT_USER (a); + ub = ACT_USER (b); + + /* Make sure the current user is shown first */ + if (act_user_get_uid (ua) == getuid ()) + { + result = G_MININT32; + } + else if (act_user_get_uid (ub) == getuid ()) + { + result = G_MAXINT32; + } + else + { + g_autofree gchar *name1 = NULL, *name2 = NULL; + + name1 = g_utf8_collate_key (get_real_or_user_name (ua), -1); + name2 = g_utf8_collate_key (get_real_or_user_name (ub), -1); + + result = strcmp (name1, name2); + } + + return result; +} + +static gint +user_compare (gconstpointer i, + gconstpointer u) +{ + MctCarouselItem *item; + ActUser *user; + gint uid_a, uid_b; + gint result; + + item = (MctCarouselItem *) i; + user = ACT_USER (u); + + uid_a = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "uid")); + uid_b = act_user_get_uid (user); + + result = uid_a - uid_b; + + return result; +} + +static void +reload_users (MctUserSelector *self, + ActUser *selected_user) +{ + ActUser *user; + g_autoptr(GSList) list = NULL; + GSList *l; + MctCarouselItem *item = NULL; + GtkSettings *settings; + gboolean animations; + + settings = gtk_widget_get_settings (GTK_WIDGET (self->carousel)); + + g_object_get (settings, "gtk-enable-animations", &animations, NULL); + g_object_set (settings, "gtk-enable-animations", FALSE, NULL); + + mct_carousel_purge_items (self->carousel); + + list = act_user_manager_list_users (self->user_manager); + g_debug ("Got %u users", g_slist_length (list)); + + list = g_slist_sort (list, (GCompareFunc) sort_users); + for (l = list; l; l = l->next) + { + user = l->data; + g_debug ("Adding user %s", get_real_or_user_name (user)); + user_added_cb (self->user_manager, user, self); + } + + if (selected_user) + item = mct_carousel_find_item (self->carousel, selected_user, user_compare); + mct_carousel_select_item (self->carousel, item); + + g_object_set (settings, "gtk-enable-animations", animations, NULL); + + gtk_revealer_set_reveal_child (GTK_REVEALER (self->carousel), TRUE); +} + +static GtkWidget * +create_carousel_entry (MctUserSelector *self, + ActUser *user) +{ + GtkWidget *box, *widget; + gchar *label; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + + widget = mct_user_image_new (); + mct_user_image_set_user (MCT_USER_IMAGE (widget), user); + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + + label = g_strdup_printf ("%s", + get_real_or_user_name (user)); + widget = gtk_label_new (label); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_label_set_ellipsize (GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_widget_set_margin_top (widget, 5); + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + g_free (label); + + if (act_user_get_uid (user) == getuid ()) + label = g_strdup_printf ("%s", _("Your account")); + else + label = g_strdup (" "); + + widget = gtk_label_new (label); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + g_free (label); + + gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0); + gtk_style_context_add_class (gtk_widget_get_style_context (widget), + "dim-label"); + + return box; +} + +static void +user_added_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + GtkWidget *item, *widget; + + if (act_user_is_system_account (user)) + return; + + g_debug ("User added: %u %s", (guint) act_user_get_uid (user), get_real_or_user_name (user)); + + widget = create_carousel_entry (self, user); + item = mct_carousel_item_new (); + gtk_container_add (GTK_CONTAINER (item), widget); + + g_object_set_data (G_OBJECT (item), "uid", GINT_TO_POINTER (act_user_get_uid (user))); + gtk_container_add (GTK_CONTAINER (self->carousel), item); +} + +static void +user_changed_or_removed_cb (ActUserManager *user_manager, + ActUser *user, + gpointer user_data) +{ + MctUserSelector *self = MCT_USER_SELECTOR (user_data); + + reload_users (self, self->user); +} + +/** + * mct_user_selector_new: + * @user_manager: (transfer none): an #ActUserManager to provide the user data + * + * Create a new #MctUserSelector widget. + * + * Returns: (transfer full): a new user selector + * Since: 0.5.0 + */ +MctUserSelector * +mct_user_selector_new (ActUserManager *user_manager) +{ + g_return_val_if_fail (ACT_IS_USER_MANAGER (user_manager), NULL); + + return g_object_new (MCT_TYPE_USER_SELECTOR, + "user-manager", user_manager, + NULL); +} + +/** + * mct_user_selector_get_user: + * @self: an #MctUserSelector + * + * Get the currently selected user, or %NULL if no user is selected. + * + * Returns: (transfer none) (nullable): the currently selected user + * Since: 0.5.0 + */ +ActUser * +mct_user_selector_get_user (MctUserSelector *self) +{ + g_return_val_if_fail (MCT_IS_USER_SELECTOR (self), NULL); + + return self->user; +} diff --git a/malcontent-control/user-selector.h b/malcontent-control/user-selector.h new file mode 100644 index 0000000..ec30929 --- /dev/null +++ b/malcontent-control/user-selector.h @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright © 2020 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 + */ + +#pragma once + +#include +#include + + +G_BEGIN_DECLS + +#define MCT_TYPE_USER_SELECTOR (mct_user_selector_get_type ()) +G_DECLARE_FINAL_TYPE (MctUserSelector, mct_user_selector, MCT, USER_SELECTOR, GtkBox) + +MctUserSelector *mct_user_selector_new (ActUserManager *user_manager); + +ActUser *mct_user_selector_get_user (MctUserSelector *self); + +G_END_DECLS diff --git a/malcontent-control/user-selector.ui b/malcontent-control/user-selector.ui new file mode 100644 index 0000000..088d72f --- /dev/null +++ b/malcontent-control/user-selector.ui @@ -0,0 +1,14 @@ + + + + + +