summaryrefslogtreecommitdiffstats
path: root/panels/user-accounts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:36:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:36:24 +0000
commit9b6d8e63db85c30007b463e91f91a791969fa83f (patch)
tree0899af51d73c1bf986f73ae39a03c4436083018a /panels/user-accounts
parentInitial commit. (diff)
downloadgnome-control-center-9b6d8e63db85c30007b463e91f91a791969fa83f.tar.xz
gnome-control-center-9b6d8e63db85c30007b463e91f91a791969fa83f.zip
Adding upstream version 1:3.38.4.upstream/1%3.38.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'panels/user-accounts')
-rw-r--r--panels/user-accounts/cc-add-user-dialog.c1772
-rw-r--r--panels/user-accounts/cc-add-user-dialog.h34
-rw-r--r--panels/user-accounts/cc-add-user-dialog.ui860
-rw-r--r--panels/user-accounts/cc-avatar-chooser.c663
-rw-r--r--panels/user-accounts/cc-avatar-chooser.h39
-rw-r--r--panels/user-accounts/cc-avatar-chooser.ui58
-rw-r--r--panels/user-accounts/cc-carousel.c438
-rw-r--r--panels/user-accounts/cc-carousel.h50
-rw-r--r--panels/user-accounts/cc-carousel.ui118
-rw-r--r--panels/user-accounts/cc-crop-area.c819
-rw-r--r--panels/user-accounts/cc-crop-area.h40
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.c1454
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.h37
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.ui462
-rw-r--r--panels/user-accounts/cc-fingerprint-manager.c597
-rw-r--r--panels/user-accounts/cc-fingerprint-manager.h74
-rw-r--r--panels/user-accounts/cc-login-history-dialog.c350
-rw-r--r--panels/user-accounts/cc-login-history-dialog.h33
-rw-r--r--panels/user-accounts/cc-login-history-dialog.ui117
-rw-r--r--panels/user-accounts/cc-password-dialog.c538
-rw-r--r--panels/user-accounts/cc-password-dialog.h33
-rw-r--r--panels/user-accounts/cc-password-dialog.ui305
-rw-r--r--panels/user-accounts/cc-realm-manager.c812
-rw-r--r--panels/user-accounts/cc-realm-manager.h97
-rw-r--r--panels/user-accounts/cc-user-image.c137
-rw-r--r--panels/user-accounts/cc-user-image.h32
-rw-r--r--panels/user-accounts/cc-user-panel.c1678
-rw-r--r--panels/user-accounts/cc-user-panel.h29
-rw-r--r--panels/user-accounts/cc-user-panel.ui668
-rw-r--r--panels/user-accounts/data/carousel.css30
-rw-r--r--panels/user-accounts/data/cc-fingerprint-dialog.css83
-rw-r--r--panels/user-accounts/data/faces/bicycle.jpgbin0 -> 164797 bytes
-rw-r--r--panels/user-accounts/data/faces/book.jpgbin0 -> 107001 bytes
-rw-r--r--panels/user-accounts/data/faces/calculator.jpgbin0 -> 71432 bytes
-rw-r--r--panels/user-accounts/data/faces/cat.jpgbin0 -> 84614 bytes
-rw-r--r--panels/user-accounts/data/faces/coffee2.jpgbin0 -> 59609 bytes
-rw-r--r--panels/user-accounts/data/faces/flower2.jpgbin0 -> 37545 bytes
-rw-r--r--panels/user-accounts/data/faces/gamepad.jpgbin0 -> 48885 bytes
-rw-r--r--panels/user-accounts/data/faces/guitar2.jpgbin0 -> 39121 bytes
-rw-r--r--panels/user-accounts/data/faces/headphones.jpgbin0 -> 50165 bytes
-rw-r--r--panels/user-accounts/data/faces/hummingbird.jpgbin0 -> 44924 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/astronaut.jpgbin0 -> 3034 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/baseball.pngbin0 -> 12985 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/butterfly.pngbin0 -> 17171 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/cat-eye.jpgbin0 -> 5495 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/chess.jpgbin0 -> 3346 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/coffee.jpgbin0 -> 3467 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/dice.jpgbin0 -> 2807 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/energy-arc.jpgbin0 -> 2041 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/fish.jpgbin0 -> 3225 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/flake.jpgbin0 -> 4216 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/flower.jpgbin0 -> 3572 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/grapes.jpgbin0 -> 4045 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/guitar.jpgbin0 -> 2281 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/launch.jpgbin0 -> 3121 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/leaf.jpgbin0 -> 2627 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/lightning.jpgbin0 -> 2621 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/penguin.jpgbin0 -> 2192 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/puppy.jpgbin0 -> 3461 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sky.jpgbin0 -> 2964 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/soccerball.pngbin0 -> 9267 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sunflower.jpgbin0 -> 4105 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sunset.jpgbin0 -> 2752 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/tennis-ball.pngbin0 -> 13432 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/yellow-rose.jpgbin0 -> 2727 bytes
-rw-r--r--panels/user-accounts/data/faces/mountain.jpgbin0 -> 42261 bytes
-rw-r--r--panels/user-accounts/data/faces/plane.jpgbin0 -> 65115 bytes
-rw-r--r--panels/user-accounts/data/faces/surfer.jpgbin0 -> 101022 bytes
-rw-r--r--panels/user-accounts/data/faces/tomatoes.jpgbin0 -> 103768 bytes
-rw-r--r--panels/user-accounts/data/faces/tree.jpgbin0 -> 99009 bytes
-rw-r--r--panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in19
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg3
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg3
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg3
-rw-r--r--panels/user-accounts/data/icons/left-index-finger.svg177
-rw-r--r--panels/user-accounts/data/icons/left-little-finger.svg180
-rw-r--r--panels/user-accounts/data/icons/left-middle-finger.svg180
-rw-r--r--panels/user-accounts/data/icons/left-ring-finger.svg180
-rw-r--r--panels/user-accounts/data/icons/left-thumb.svg180
-rw-r--r--panels/user-accounts/data/icons/print_error.svg525
-rw-r--r--panels/user-accounts/data/icons/print_ok.svg310
-rw-r--r--panels/user-accounts/data/icons/right-index-finger.svg179
-rw-r--r--panels/user-accounts/data/icons/right-little-finger.svg182
-rw-r--r--panels/user-accounts/data/icons/right-middle-finger.svg182
-rw-r--r--panels/user-accounts/data/icons/right-ring-finger.svg182
-rw-r--r--panels/user-accounts/data/icons/right-thumb.svg182
-rw-r--r--panels/user-accounts/data/join-dialog.ui238
-rw-r--r--panels/user-accounts/data/net.reactivated.Fprint.Device.xml585
-rw-r--r--panels/user-accounts/data/net.reactivated.Fprint.Manager.xml50
-rw-r--r--panels/user-accounts/data/org.freedesktop.realmd.xml666
-rw-r--r--panels/user-accounts/data/user-accounts-dialog.css28
-rw-r--r--panels/user-accounts/fingerprint-strings.h172
-rw-r--r--panels/user-accounts/meson.build207
-rw-r--r--panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in21
-rw-r--r--panels/user-accounts/pw-utils.c177
-rw-r--r--panels/user-accounts/pw-utils.h31
-rw-r--r--panels/user-accounts/run-passwd.c770
-rw-r--r--panels/user-accounts/run-passwd.h54
-rw-r--r--panels/user-accounts/user-accounts.gresource.xml22
-rw-r--r--panels/user-accounts/user-utils.c772
-rw-r--r--panels/user-accounts/user-utils.h59
101 files changed, 18976 insertions, 0 deletions
diff --git a/panels/user-accounts/cc-add-user-dialog.c b/panels/user-accounts/cc-add-user-dialog.c
new file mode 100644
index 0000000..7a99b09
--- /dev/null
+++ b/panels/user-accounts/cc-add-user-dialog.c
@@ -0,0 +1,1772 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+#include "cc-add-user-dialog.h"
+#include "cc-realm-manager.h"
+#include "user-utils.h"
+#include "pw-utils.h"
+
+#define PASSWORD_CHECK_TIMEOUT 600
+#define DOMAIN_DEFAULT_HINT _("Should match the web address of your login provider.")
+
+typedef enum {
+ MODE_LOCAL,
+ MODE_ENTERPRISE,
+ MODE_OFFLINE
+} AccountMode;
+
+static void mode_change (CcAddUserDialog *self,
+ AccountMode mode);
+
+static void dialog_validate (CcAddUserDialog *self);
+
+static void on_join_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void on_realm_joined (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void add_button_clicked_cb (CcAddUserDialog *self);
+
+struct _CcAddUserDialog {
+ GtkDialog parent_instance;
+
+ GtkButton *add_button;
+ GtkToggleButton *enterprise_button;
+ GtkComboBox *enterprise_domain_combo;
+ GtkEntry *enterprise_domain_entry;
+ GtkLabel *enterprise_domain_hint_label;
+ GtkGrid *enterprise_grid;
+ GtkLabel *enterprise_hint_label;
+ GtkEntry *enterprise_login_entry;
+ GtkEntry *enterprise_password_entry;
+ GtkListStore *enterprise_realm_model;
+ GtkRadioButton *local_account_type_standard;
+ GtkGrid *local_grid;
+ GtkLabel *local_hint_label;
+ GtkEntry *local_name_entry;
+ GtkComboBoxText *local_username_combo;
+ GtkListStore *local_username_model;
+ GtkEntry *local_password_entry;
+ GtkRadioButton *local_password_radio;
+ GtkEntry *local_username_entry;
+ GtkLabel *local_username_hint_label;
+ GtkLevelBar *local_strength_indicator;
+ GtkEntry *local_verify_entry;
+ GtkLabel *local_verify_hint_label;
+ GtkGrid *offline_grid;
+ GtkSpinner *spinner;
+ GtkStack *stack;
+
+ GCancellable *cancellable;
+ GPermission *permission;
+ AccountMode mode;
+ ActUser *user;
+
+ gboolean has_custom_username;
+ gint local_name_timeout_id;
+ gint local_username_timeout_id;
+ ActUserPasswordMode local_password_mode;
+ gint local_password_timeout_id;
+ gboolean local_valid_username;
+
+ guint realmd_watch;
+ CcRealmManager *realm_manager;
+ CcRealmObject *selected_realm;
+ gboolean enterprise_check_credentials;
+ gint enterprise_domain_timeout_id;
+ gboolean enterprise_domain_chosen;
+
+ /* Join credential dialog */
+ GtkDialog *join_dialog;
+ GtkLabel *join_domain;
+ GtkEntry *join_name;
+ GtkEntry *join_password;
+ gboolean join_prompted;
+};
+
+G_DEFINE_TYPE (CcAddUserDialog, cc_add_user_dialog, GTK_TYPE_DIALOG);
+
+static void
+show_error_dialog (CcAddUserDialog *self,
+ const gchar *message,
+ GError *error)
+{
+ GtkWidget *dialog;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (self),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_USE_HEADER_BAR,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s", message);
+
+ if (error != NULL) {
+ g_dbus_error_strip_remote_error (error);
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ "%s", error->message);
+ }
+
+ g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+begin_action (CcAddUserDialog *self)
+{
+ g_debug ("Beginning action, disabling dialog controls");
+
+ if (self->enterprise_check_credentials) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->stack), FALSE);
+ }
+ gtk_widget_set_sensitive (GTK_WIDGET (self->enterprise_button), FALSE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+
+ gtk_widget_show (GTK_WIDGET (self->spinner));
+ gtk_spinner_start (self->spinner);
+}
+
+static void
+finish_action (CcAddUserDialog *self)
+{
+ g_debug ("Completed domain action");
+
+ if (self->enterprise_check_credentials) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->stack), TRUE);
+ }
+ gtk_widget_set_sensitive (GTK_WIDGET (self->enterprise_button), TRUE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), TRUE);
+
+ gtk_widget_hide (GTK_WIDGET (self->spinner));
+ gtk_spinner_stop (self->spinner);
+}
+
+static void
+user_loaded_cb (CcAddUserDialog *self,
+ GParamSpec *pspec,
+ ActUser *user)
+{
+ const gchar *password;
+
+ finish_action (self);
+
+ /* Set a password for the user */
+ password = gtk_entry_get_text (self->local_password_entry);
+ act_user_set_password_mode (user, self->local_password_mode);
+ if (self->local_password_mode == ACT_USER_PASSWORD_MODE_REGULAR)
+ act_user_set_password (user, password, "");
+
+ self->user = g_object_ref (user);
+ gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CLOSE);
+}
+
+static void
+create_user_done (ActUserManager *manager,
+ GAsyncResult *res,
+ CcAddUserDialog *self)
+{
+ ActUser *user;
+ GError *error;
+
+ /* Note that user is returned without an extra reference */
+
+ error = NULL;
+ user = act_user_manager_create_user_finish (manager, res, &error);
+
+ if (user == NULL) {
+ finish_action (self);
+ g_debug ("Failed to create user: %s", error->message);
+ if (!g_error_matches (error, ACT_USER_MANAGER_ERROR, ACT_USER_MANAGER_ERROR_PERMISSION_DENIED))
+ show_error_dialog (self, _("Failed to add account"), error);
+ g_error_free (error);
+ gtk_widget_grab_focus (GTK_WIDGET (self->local_name_entry));
+ } else {
+ g_debug ("Created user: %s", act_user_get_user_name (user));
+
+ /* Check if the returned object is fully loaded before returning it */
+ if (act_user_is_loaded (user))
+ user_loaded_cb (self, NULL, user);
+ else
+ g_signal_connect_object (user, "notify::is-loaded", G_CALLBACK (user_loaded_cb), self, G_CONNECT_SWAPPED);
+ }
+}
+
+static void
+local_create_user (CcAddUserDialog *self)
+{
+ ActUserManager *manager;
+ const gchar *username;
+ const gchar *name;
+ gint account_type;
+
+ begin_action (self);
+
+ name = gtk_entry_get_text (self->local_name_entry);
+ username = gtk_combo_box_text_get_active_text (self->local_username_combo);
+ account_type = (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->local_account_type_standard)) ? ACT_USER_ACCOUNT_TYPE_STANDARD : ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR);
+
+ g_debug ("Creating local user: %s", username);
+
+ manager = act_user_manager_get_default ();
+ act_user_manager_create_user_async (manager,
+ username,
+ name,
+ account_type,
+ self->cancellable,
+ (GAsyncReadyCallback)create_user_done,
+ self);
+}
+
+static gint
+update_password_strength (CcAddUserDialog *self)
+{
+ const gchar *password;
+ const gchar *username;
+ const gchar *hint;
+ const gchar *verify;
+ gint strength_level;
+
+ password = gtk_entry_get_text (self->local_password_entry);
+ username = gtk_combo_box_text_get_active_text (self->local_username_combo);
+
+ pw_strength (password, NULL, username, &hint, &strength_level);
+
+ gtk_label_set_label (self->local_hint_label, hint);
+ gtk_level_bar_set_value (self->local_strength_indicator, strength_level);
+
+ if (strength_level > 1) {
+ set_entry_validation_checkmark (self->local_password_entry);
+ } else if (strlen (password) == 0) {
+ set_entry_generation_icon (self->local_password_entry);
+ } else {
+ clear_entry_validation_error (self->local_password_entry);
+ }
+
+ verify = gtk_entry_get_text (self->local_verify_entry);
+ if (strlen (verify) == 0) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_verify_entry), strength_level > 1);
+ }
+
+ return strength_level;
+}
+
+static gboolean
+local_validate (CcAddUserDialog *self)
+{
+ gboolean valid_name;
+ gboolean valid_password;
+ const gchar *name;
+ const gchar *password;
+ const gchar *verify;
+ gint strength;
+
+ if (self->local_valid_username) {
+ set_entry_validation_checkmark (self->local_username_entry);
+ }
+
+ name = gtk_entry_get_text (self->local_name_entry);
+ valid_name = is_valid_name (name);
+ if (valid_name) {
+ set_entry_validation_checkmark (self->local_name_entry);
+ }
+
+ password = gtk_entry_get_text (self->local_password_entry);
+ verify = gtk_entry_get_text (self->local_verify_entry);
+ if (self->local_password_mode == ACT_USER_PASSWORD_MODE_REGULAR) {
+ strength = update_password_strength (self);
+ valid_password = strength > 1 && strcmp (password, verify) == 0;
+ } else {
+ valid_password = TRUE;
+ }
+
+ return valid_name && self->local_valid_username && valid_password;
+}
+
+static void local_username_is_valid_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *tip = NULL;
+ g_autofree gchar *name = NULL;
+ g_autofree gchar *username = NULL;
+ gboolean valid;
+
+ valid = is_valid_username_finish (result, &tip, &username, &error);
+ if (error != NULL) {
+ g_warning ("Could not check username by usermod: %s", error->message);
+ valid = TRUE;
+ }
+
+ name = gtk_combo_box_text_get_active_text (self->local_username_combo);
+ if (g_strcmp0 (name, username) == 0) {
+ self->local_valid_username = valid;
+ gtk_label_set_label (self->local_username_hint_label, tip);
+ dialog_validate (self);
+ }
+
+ g_object_unref (self);
+}
+
+static gboolean
+local_username_timeout (CcAddUserDialog *self)
+{
+ g_autofree gchar *name = NULL;
+
+ self->local_username_timeout_id = 0;
+
+ name = gtk_combo_box_text_get_active_text (self->local_username_combo);
+ is_valid_username_async (name, NULL, local_username_is_valid_cb, g_object_ref (self));
+
+ return FALSE;
+}
+
+static gboolean
+local_username_combo_focus_out_event_cb (CcAddUserDialog *self)
+{
+ if (self->local_username_timeout_id != 0) {
+ g_source_remove (self->local_username_timeout_id);
+ self->local_username_timeout_id = 0;
+ }
+
+ local_username_timeout (self);
+
+ return FALSE;
+}
+
+static void
+local_username_combo_changed_cb (CcAddUserDialog *self)
+{
+ const gchar *username;
+
+ username = gtk_entry_get_text (self->local_username_entry);
+ if (*username == '\0')
+ self->has_custom_username = FALSE;
+ else if (gtk_widget_has_focus (GTK_WIDGET (self->local_username_entry)) ||
+ gtk_combo_box_get_active (GTK_COMBO_BOX (self->local_username_combo)) > 0)
+ self->has_custom_username = TRUE;
+
+ if (self->local_username_timeout_id != 0) {
+ g_source_remove (self->local_username_timeout_id);
+ self->local_username_timeout_id = 0;
+ }
+
+ clear_entry_validation_error (self->local_username_entry);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+
+ self->local_valid_username = FALSE;
+ self->local_username_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_username_timeout, self);
+}
+
+static gboolean
+local_name_timeout (CcAddUserDialog *self)
+{
+ self->local_name_timeout_id = 0;
+
+ dialog_validate (self);
+
+ return FALSE;
+}
+
+static gboolean
+local_name_entry_focus_out_event_cb (CcAddUserDialog *self)
+{
+ if (self->local_name_timeout_id != 0) {
+ g_source_remove (self->local_name_timeout_id);
+ self->local_name_timeout_id = 0;
+ }
+
+ local_name_timeout (self);
+
+ return FALSE;
+}
+
+static void
+generate_username_choices (const gchar *name,
+ GtkListStore *store)
+{
+ gboolean in_use, same_as_initial;
+ char *lc_name, *ascii_name, *stripped_name;
+ char **words1;
+ char **words2 = NULL;
+ char **w1, **w2;
+ char *c;
+ char *unicode_fallback = "?";
+ GString *first_word, *last_word;
+ GString *item0, *item1, *item2, *item3, *item4;
+ int len;
+ int nwords1, nwords2, i;
+ GHashTable *items;
+ GtkTreeIter iter;
+ gsize max_name_length;
+
+ gtk_list_store_clear (store);
+
+ ascii_name = g_convert_with_fallback (name, -1, "ASCII//TRANSLIT", "UTF-8",
+ unicode_fallback, NULL, NULL, NULL);
+
+ lc_name = g_ascii_strdown (ascii_name, -1);
+
+ /* Remove all non ASCII alphanumeric chars from the name,
+ * apart from the few allowed symbols.
+ *
+ * We do remove '.', even though it is usually allowed,
+ * since it often comes in via an abbreviated middle name,
+ * and the dot looks just wrong in the proposals then.
+ */
+ stripped_name = g_strnfill (strlen (lc_name) + 1, '\0');
+ i = 0;
+ for (c = lc_name; *c; c++) {
+ if (!(g_ascii_isdigit (*c) || g_ascii_islower (*c) ||
+ *c == ' ' || *c == '-' || *c == '_' ||
+ /* used to track invalid words, removed below */
+ *c == '?') )
+ continue;
+
+ stripped_name[i] = *c;
+ i++;
+ }
+
+ if (strlen (stripped_name) == 0) {
+ g_free (ascii_name);
+ g_free (lc_name);
+ g_free (stripped_name);
+ return;
+ }
+
+ /* we split name on spaces, and then on dashes, so that we can treat
+ * words linked with dashes the same way, i.e. both fully shown, or
+ * both abbreviated
+ */
+ words1 = g_strsplit_set (stripped_name, " ", -1);
+ len = g_strv_length (words1);
+
+ /* The default item is a concatenation of all words without ? */
+ item0 = g_string_sized_new (strlen (stripped_name));
+
+ g_free (ascii_name);
+ g_free (lc_name);
+ g_free (stripped_name);
+
+ /* Concatenate the whole first word with the first letter of each
+ * word (item1), and the last word with the first letter of each
+ * word (item2). item3 and item4 are symmetrical respectively to
+ * item1 and item2.
+ *
+ * Constant 5 is the max reasonable number of words we may get when
+ * splitting on dashes, since we can't guess it at this point,
+ * and reallocating would be too bad.
+ */
+ item1 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5);
+ item3 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5);
+
+ item2 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5);
+ item4 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5);
+
+ /* again, guess at the max size of names */
+ first_word = g_string_sized_new (20);
+ last_word = g_string_sized_new (20);
+
+ nwords1 = 0;
+ nwords2 = 0;
+ for (w1 = words1; *w1; w1++) {
+ if (strlen (*w1) == 0)
+ continue;
+
+ /* skip words with string '?', most likely resulting
+ * from failed transliteration to ASCII
+ */
+ if (strstr (*w1, unicode_fallback) != NULL)
+ continue;
+
+ nwords1++; /* count real words, excluding empty string */
+
+ item0 = g_string_append (item0, *w1);
+
+ words2 = g_strsplit_set (*w1, "-", -1);
+ /* reset last word if a new non-empty word has been found */
+ if (strlen (*words2) > 0)
+ last_word = g_string_set_size (last_word, 0);
+
+ for (w2 = words2; *w2; w2++) {
+ if (strlen (*w2) == 0)
+ continue;
+
+ nwords2++;
+
+ /* part of the first "toplevel" real word */
+ if (nwords1 == 1) {
+ item1 = g_string_append (item1, *w2);
+ first_word = g_string_append (first_word, *w2);
+ }
+ else {
+ item1 = g_string_append_unichar (item1,
+ g_utf8_get_char (*w2));
+ item3 = g_string_append_unichar (item3,
+ g_utf8_get_char (*w2));
+ }
+
+ /* not part of the last "toplevel" word */
+ if (w1 != words1 + len - 1) {
+ item2 = g_string_append_unichar (item2,
+ g_utf8_get_char (*w2));
+ item4 = g_string_append_unichar (item4,
+ g_utf8_get_char (*w2));
+ }
+
+ /* always save current word so that we have it if last one reveals empty */
+ last_word = g_string_append (last_word, *w2);
+ }
+
+ g_strfreev (words2);
+ }
+ item2 = g_string_append (item2, last_word->str);
+ item3 = g_string_append (item3, first_word->str);
+ item4 = g_string_prepend (item4, last_word->str);
+
+ max_name_length = get_username_max_length ();
+
+ g_string_truncate (first_word, max_name_length);
+ g_string_truncate (last_word, max_name_length);
+
+ g_string_truncate (item0, max_name_length);
+ g_string_truncate (item1, max_name_length);
+ g_string_truncate (item2, max_name_length);
+ g_string_truncate (item3, max_name_length);
+ g_string_truncate (item4, max_name_length);
+
+ items = g_hash_table_new (g_str_hash, g_str_equal);
+
+ in_use = is_username_used (item0->str);
+ if (!in_use && !g_ascii_isdigit (item0->str[0])) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, item0->str, -1);
+ g_hash_table_insert (items, item0->str, item0->str);
+ }
+
+ in_use = is_username_used (item1->str);
+ same_as_initial = (g_strcmp0 (item0->str, item1->str) == 0);
+ if (!same_as_initial && nwords2 > 0 && !in_use && !g_ascii_isdigit (item1->str[0])) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, item1->str, -1);
+ g_hash_table_insert (items, item1->str, item1->str);
+ }
+
+ /* if there's only one word, would be the same as item1 */
+ if (nwords2 > 1) {
+ /* add other items */
+ in_use = is_username_used (item2->str);
+ if (!in_use && !g_ascii_isdigit (item2->str[0]) &&
+ !g_hash_table_lookup (items, item2->str)) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, item2->str, -1);
+ g_hash_table_insert (items, item2->str, item2->str);
+ }
+
+ in_use = is_username_used (item3->str);
+ if (!in_use && !g_ascii_isdigit (item3->str[0]) &&
+ !g_hash_table_lookup (items, item3->str)) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, item3->str, -1);
+ g_hash_table_insert (items, item3->str, item3->str);
+ }
+
+ in_use = is_username_used (item4->str);
+ if (!in_use && !g_ascii_isdigit (item4->str[0]) &&
+ !g_hash_table_lookup (items, item4->str)) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, item4->str, -1);
+ g_hash_table_insert (items, item4->str, item4->str);
+ }
+
+ /* add the last word */
+ in_use = is_username_used (last_word->str);
+ if (!in_use && !g_ascii_isdigit (last_word->str[0]) &&
+ !g_hash_table_lookup (items, last_word->str)) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, last_word->str, -1);
+ g_hash_table_insert (items, last_word->str, last_word->str);
+ }
+
+ /* ...and the first one */
+ in_use = is_username_used (first_word->str);
+ if (!in_use && !g_ascii_isdigit (first_word->str[0]) &&
+ !g_hash_table_lookup (items, first_word->str)) {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, 0, first_word->str, -1);
+ g_hash_table_insert (items, first_word->str, first_word->str);
+ }
+ }
+
+ g_hash_table_destroy (items);
+ g_strfreev (words1);
+ g_string_free (first_word, TRUE);
+ g_string_free (last_word, TRUE);
+ g_string_free (item0, TRUE);
+ g_string_free (item1, TRUE);
+ g_string_free (item2, TRUE);
+ g_string_free (item3, TRUE);
+ g_string_free (item4, TRUE);
+}
+
+static void
+local_name_entry_changed_cb (CcAddUserDialog *self)
+{
+ const char *name;
+
+ gtk_list_store_clear (self->local_username_model);
+
+ name = gtk_entry_get_text (self->local_name_entry);
+ if ((name == NULL || strlen (name) == 0) && !self->has_custom_username) {
+ gtk_entry_set_text (self->local_username_entry, "");
+ } else if (name != NULL && strlen (name) != 0) {
+ generate_username_choices (name, self->local_username_model);
+ if (!self->has_custom_username)
+ gtk_combo_box_set_active (GTK_COMBO_BOX (self->local_username_combo), 0);
+ }
+
+ if (self->local_name_timeout_id != 0) {
+ g_source_remove (self->local_name_timeout_id);
+ self->local_name_timeout_id = 0;
+ }
+
+ clear_entry_validation_error (self->local_name_entry);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+
+ self->local_name_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_name_timeout, self);
+}
+
+static void
+update_password_match (CcAddUserDialog *self)
+{
+ const gchar *password;
+ const gchar *verify;
+ const gchar *message = "";
+
+ password = gtk_entry_get_text (self->local_password_entry);
+ verify = gtk_entry_get_text (self->local_verify_entry);
+ if (strlen (verify) != 0) {
+ if (strcmp (password, verify) != 0) {
+ message = _("The passwords do not match.");
+ } else {
+ set_entry_validation_checkmark (self->local_verify_entry);
+ }
+ }
+ gtk_label_set_label (self->local_verify_hint_label, message);
+}
+
+static void
+local_password_entry_icon_press_cb (CcAddUserDialog *self)
+{
+ gchar *pwd;
+
+ pwd = pw_generate ();
+ if (pwd == NULL)
+ return;
+
+ gtk_entry_set_text (self->local_password_entry, pwd);
+ gtk_entry_set_text (self->local_verify_entry, pwd);
+ gtk_entry_set_visibility (self->local_password_entry, TRUE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_verify_entry), TRUE);
+
+ g_free (pwd);
+}
+
+static gboolean
+local_password_timeout (CcAddUserDialog *self)
+{
+ self->local_password_timeout_id = 0;
+
+ dialog_validate (self);
+ update_password_match (self);
+
+ return FALSE;
+}
+
+static gboolean
+password_focus_out_event_cb (CcAddUserDialog *self)
+{
+ if (self->local_password_timeout_id != 0) {
+ g_source_remove (self->local_password_timeout_id);
+ self->local_password_timeout_id = 0;
+ }
+
+ local_password_timeout (self);
+
+ return FALSE;
+}
+
+static gboolean
+local_password_entry_key_press_event_cb (CcAddUserDialog *self,
+ GdkEvent *event)
+{
+ GdkEventKey *key = (GdkEventKey *)event;
+
+ if (key->keyval == GDK_KEY_Tab)
+ local_password_timeout (self);
+
+ return FALSE;
+}
+
+static void
+recheck_password_match (CcAddUserDialog *self)
+{
+ const char *password;
+
+ if (self->local_password_timeout_id != 0) {
+ g_source_remove (self->local_password_timeout_id);
+ self->local_password_timeout_id = 0;
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+
+ password = gtk_entry_get_text (self->local_password_entry);
+ if (strlen (password) == 0) {
+ gtk_entry_set_visibility (self->local_password_entry, FALSE);
+ }
+
+ self->local_password_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_password_timeout, self);
+}
+
+static void
+local_password_entry_changed_cb (CcAddUserDialog *self)
+{
+ clear_entry_validation_error (self->local_password_entry);
+ clear_entry_validation_error (self->local_verify_entry);
+ recheck_password_match (self);
+}
+
+static void
+local_verify_entry_changed_cb (CcAddUserDialog *self)
+{
+ clear_entry_validation_error (self->local_verify_entry);
+ recheck_password_match (self);
+}
+
+static void
+local_password_radio_changed_cb (CcAddUserDialog *self)
+{
+ gboolean active;
+
+ active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->local_password_radio));
+ self->local_password_mode = active ? ACT_USER_PASSWORD_MODE_REGULAR : ACT_USER_PASSWORD_MODE_SET_AT_LOGIN;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_password_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_verify_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_strength_indicator), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->local_hint_label), active);
+
+ dialog_validate (self);
+}
+
+static gboolean
+enterprise_validate (CcAddUserDialog *self)
+{
+ const gchar *name;
+ gboolean valid_name;
+ gboolean valid_domain;
+ GtkTreeIter iter;
+
+ name = gtk_entry_get_text (self->enterprise_login_entry);
+ valid_name = is_valid_name (name);
+
+ if (gtk_combo_box_get_active_iter (self->enterprise_domain_combo, &iter)) {
+ gtk_tree_model_get (GTK_TREE_MODEL (self->enterprise_realm_model),
+ &iter, 0, &name, -1);
+ } else {
+ name = gtk_entry_get_text (self->enterprise_domain_entry);
+ }
+
+ valid_domain = is_valid_name (name) && self->selected_realm != NULL;
+ return valid_name && valid_domain;
+}
+
+static void
+enterprise_add_realm (CcAddUserDialog *self,
+ CcRealmObject *realm)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ CcRealmCommon *common;
+ const gchar *realm_name;
+ gboolean match;
+ gboolean ret;
+ gchar *name;
+
+ common = cc_realm_object_get_common (realm);
+ g_return_if_fail (common != NULL);
+
+ realm_name = cc_realm_common_get_name (common);
+
+ /*
+ * Don't add a second realm if we already have one with this name.
+ * Sometimes realmd returns to realms for the same name, if it has
+ * different ways to use that realm. The first one that realmd
+ * returns is the one it prefers.
+ */
+
+ model = GTK_TREE_MODEL (self->enterprise_realm_model);
+ ret = gtk_tree_model_get_iter_first (model, &iter);
+ while (ret) {
+ gtk_tree_model_get (model, &iter, 0, &name, -1);
+ match = (g_strcmp0 (name, realm_name) == 0);
+ g_free (name);
+ if (match) {
+ g_debug ("ignoring duplicate realm: %s", realm_name);
+ g_object_unref (common);
+ return;
+ }
+ ret = gtk_tree_model_iter_next (model, &iter);
+ }
+
+ gtk_list_store_append (self->enterprise_realm_model, &iter);
+ gtk_list_store_set (self->enterprise_realm_model, &iter,
+ 0, realm_name,
+ 1, realm,
+ -1);
+
+ /* Prefill domain entry by the existing one */
+ if (!self->enterprise_domain_chosen && cc_realm_is_configured (realm)) {
+ gtk_entry_set_text (self->enterprise_domain_entry, realm_name);
+ }
+
+ g_debug ("added realm to drop down: %s %s", realm_name,
+ g_dbus_object_get_object_path (G_DBUS_OBJECT (realm)));
+
+ g_object_unref (common);
+}
+
+static void
+on_manager_realm_added (CcAddUserDialog *self,
+ CcRealmObject *realm)
+{
+ enterprise_add_realm (self, realm);
+}
+
+
+static void
+on_register_user (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+ ActUser *user;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ user = act_user_manager_cache_user_finish (ACT_USER_MANAGER (source), result, &error);
+
+ /* This is where we're finally done */
+ if (user != NULL) {
+ g_debug ("Successfully cached remote user: %s", act_user_get_user_name (user));
+ finish_action (self);
+ self->user = g_object_ref (user);
+ gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CLOSE);
+ } else {
+ show_error_dialog (self, _("Failed to register account"), error);
+ g_message ("Couldn't cache user account: %s", error->message);
+ finish_action (self);
+ g_error_free (error);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+on_permit_user_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ CcRealmCommon *common;
+ ActUserManager *manager;
+ GError *error = NULL;
+ gchar *login;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ common = CC_REALM_COMMON (source);
+ cc_realm_common_call_change_login_policy_finish (common, result, &error);
+ if (error == NULL) {
+
+ /*
+ * Now tell the account service about this user. The account service
+ * should also lookup information about this via the realm and make
+ * sure all that is functional.
+ */
+ manager = act_user_manager_get_default ();
+ login = cc_realm_calculate_login (common, gtk_entry_get_text (self->enterprise_login_entry));
+ g_return_if_fail (login != NULL);
+
+ g_debug ("Caching remote user: %s", login);
+
+ act_user_manager_cache_user_async (manager, login, self->cancellable,
+ on_register_user, g_object_ref (self));
+
+ g_free (login);
+
+ } else {
+ show_error_dialog (self, _("Failed to register account"), error);
+ g_message ("Couldn't permit logins on account: %s", error->message);
+ finish_action (self);
+ g_error_free (error);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+enterprise_permit_user_login (CcAddUserDialog *self)
+{
+ CcRealmCommon *common;
+ gchar *login;
+ const gchar *add[2];
+ const gchar *remove[1];
+ GVariant *options;
+
+ common = cc_realm_object_get_common (self->selected_realm);
+ if (common == NULL) {
+ g_debug ("Failed to register account: failed to get d-bus interface");
+ show_error_dialog (self, _("Failed to register account"), NULL);
+ finish_action (self);
+ return;
+ }
+
+ login = cc_realm_calculate_login (common, gtk_entry_get_text (self->enterprise_login_entry));
+ g_return_if_fail (login != NULL);
+
+ add[0] = login;
+ add[1] = NULL;
+ remove[0] = NULL;
+
+ g_debug ("Permitting login for: %s", login);
+ options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
+
+ cc_realm_common_call_change_login_policy (common, "",
+ add, remove, options,
+ self->cancellable,
+ on_permit_user_login,
+ g_object_ref (self));
+
+ g_object_unref (common);
+ g_free (login);
+}
+
+static void
+on_join_response (CcAddUserDialog *self,
+ gint response,
+ GtkDialog *dialog)
+{
+ gtk_widget_hide (GTK_WIDGET (dialog));
+ if (response != GTK_RESPONSE_OK) {
+ finish_action (self);
+ return;
+ }
+
+ g_debug ("Logging in as admin user: %s", gtk_entry_get_text (self->join_name));
+
+ /* Prompted for some admin credentials, try to use them to log in */
+ cc_realm_login (self->selected_realm,
+ gtk_entry_get_text (self->join_name),
+ gtk_entry_get_text (self->join_password),
+ self->cancellable,
+ on_join_login,
+ g_object_ref (self));
+}
+
+static void
+join_show_prompt (CcAddUserDialog *self,
+ GError *error)
+{
+ CcRealmKerberosMembership *membership;
+ CcRealmKerberos *kerberos;
+ const gchar *name;
+
+ gtk_entry_set_text (self->join_password, "");
+ gtk_widget_grab_focus (GTK_WIDGET (self->join_password));
+
+ kerberos = cc_realm_object_get_kerberos (self->selected_realm);
+ membership = cc_realm_object_get_kerberos_membership (self->selected_realm);
+
+ gtk_label_set_text (self->join_domain,
+ cc_realm_kerberos_get_domain_name (kerberos));
+
+ clear_entry_validation_error (self->join_name);
+ clear_entry_validation_error (self->join_password);
+
+ if (!self->join_prompted) {
+ name = cc_realm_kerberos_membership_get_suggested_administrator (membership);
+ if (name && !g_str_equal (name, "")) {
+ g_debug ("Suggesting admin user: %s", name);
+ gtk_entry_set_text (self->join_name, name);
+ } else {
+ gtk_widget_grab_focus (GTK_WIDGET (self->join_name));
+ }
+
+ } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) {
+ g_debug ("Bad admin password: %s", error->message);
+ set_entry_validation_error (self->join_password, error->message);
+
+ } else {
+ g_debug ("Admin login failure: %s", error->message);
+ g_dbus_error_strip_remote_error (error);
+ set_entry_validation_error (self->join_name, error->message);
+ }
+
+ g_debug ("Showing admin password dialog");
+ gtk_window_set_transient_for (GTK_WINDOW (self->join_dialog), GTK_WINDOW (self));
+ gtk_window_set_modal (GTK_WINDOW (self->join_dialog), TRUE);
+ gtk_window_present (GTK_WINDOW (self->join_dialog));
+
+ self->join_prompted = TRUE;
+ g_object_unref (kerberos);
+ g_object_unref (membership);
+
+ /* And now we wait for on_join_response() */
+}
+
+static void
+on_join_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+ GBytes *creds;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ creds = cc_realm_login_finish (result, &error);
+
+ /* Logged in as admin successfully, use creds to join domain */
+ if (error == NULL) {
+ if (!cc_realm_join_as_admin (self->selected_realm,
+ gtk_entry_get_text (self->join_name),
+ gtk_entry_get_text (self->join_password),
+ creds, self->cancellable, on_realm_joined,
+ g_object_ref (self))) {
+ show_error_dialog (self, _("No supported way to authenticate with this domain"), NULL);
+ g_message ("Authenticating as admin is not supported by the realm");
+ finish_action (self);
+ }
+
+ g_bytes_unref (creds);
+
+ /* Couldn't login as admin, show prompt again */
+ } else {
+ join_show_prompt (self, error);
+ g_message ("Couldn't log in as admin to join domain: %s", error->message);
+ g_error_free (error);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+join_init (CcAddUserDialog *self)
+{
+ GtkBuilder *builder;
+ GError *error = NULL;
+
+ builder = gtk_builder_new ();
+
+ if (!gtk_builder_add_from_resource (builder,
+ "/org/gnome/control-center/user-accounts/join-dialog.ui",
+ &error)) {
+ g_error ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ self->join_dialog = GTK_DIALOG (gtk_builder_get_object (builder, "join-dialog"));
+ self->join_domain = GTK_LABEL (gtk_builder_get_object (builder, "join-domain"));
+ self->join_name = GTK_ENTRY (gtk_builder_get_object (builder, "join-name"));
+ self->join_password = GTK_ENTRY (gtk_builder_get_object (builder, "join-password"));
+
+ g_signal_connect_object (self->join_dialog, "response",
+ G_CALLBACK (on_join_response), self, G_CONNECT_SWAPPED);
+
+ g_object_unref (builder);
+}
+
+static void
+on_realm_joined (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ cc_realm_join_finish (self->selected_realm,
+ result, &error);
+
+ /* Yay, joined the domain, register the user locally */
+ if (error == NULL) {
+ g_debug ("Joining realm completed successfully");
+ enterprise_permit_user_login (self);
+
+ /* Credential failure while joining domain, prompt for admin creds */
+ } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN) ||
+ g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) {
+ g_debug ("Joining realm failed due to credentials");
+ join_show_prompt (self, error);
+
+ /* Other failure */
+ } else {
+ show_error_dialog (self, _("Failed to join domain"), error);
+ g_message ("Failed to join the domain: %s", error->message);
+ finish_action (self);
+ }
+
+ g_clear_error (&error);
+ g_object_unref (self);
+}
+
+static void
+on_realm_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+ GBytes *creds = NULL;
+ const gchar *message;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ creds = cc_realm_login_finish (result, &error);
+
+ /*
+ * User login is valid, but cannot authenticate right now (eg: user needs
+ * to change password at next login etc.)
+ */
+ if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_CANNOT_AUTH)) {
+ g_clear_error (&error);
+ creds = NULL;
+ }
+
+ if (error == NULL) {
+
+ /* Already joined to the domain, just register this user */
+ if (cc_realm_is_configured (self->selected_realm)) {
+ g_debug ("Already joined to this realm");
+ enterprise_permit_user_login (self);
+
+ /* Join the domain, try using the user's creds */
+ } else if (creds == NULL ||
+ !cc_realm_join_as_user (self->selected_realm,
+ gtk_entry_get_text (self->enterprise_login_entry),
+ gtk_entry_get_text (self->enterprise_password_entry),
+ creds, self->cancellable,
+ on_realm_joined,
+ g_object_ref (self))) {
+
+ /* If we can't do user auth, try to authenticate as admin */
+ g_debug ("Cannot join with user credentials");
+ join_show_prompt (self, NULL);
+ }
+
+ g_bytes_unref (creds);
+
+ /* A problem with the user's login name or password */
+ } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN)) {
+ g_debug ("Problem with the user's login: %s", error->message);
+ message = _("That login name didn’t work.\nPlease try again.");
+ gtk_label_set_text (self->enterprise_hint_label, message);
+ finish_action (self);
+ gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_login_entry));
+
+ } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) {
+ g_debug ("Problem with the user's password: %s", error->message);
+ message = _("That login password didn’t work.\nPlease try again.");
+ gtk_label_set_text (self->enterprise_hint_label, message);
+ finish_action (self);
+ gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_password_entry));
+
+ /* Other login failure */
+ } else {
+ g_dbus_error_strip_remote_error (error);
+ show_error_dialog (self, _("Failed to log into domain"), error);
+ g_message ("Couldn't log in as user: %s", error->message);
+ finish_action (self);
+ }
+
+ g_clear_error (&error);
+ g_object_unref (self);
+}
+
+static void
+enterprise_check_login (CcAddUserDialog *self)
+{
+ g_assert (self->selected_realm);
+
+ cc_realm_login (self->selected_realm,
+ gtk_entry_get_text (self->enterprise_login_entry),
+ gtk_entry_get_text (self->enterprise_password_entry),
+ self->cancellable,
+ on_realm_login,
+ g_object_ref (self));
+}
+
+static void
+on_realm_discover_input (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+ GList *realms;
+ gchar *message;
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ realms = cc_realm_manager_discover_finish (self->realm_manager,
+ result, &error);
+
+ /* Found a realm, log user into domain */
+ if (error == NULL) {
+ g_assert (realms != NULL);
+ self->selected_realm = g_object_ref (realms->data);
+
+ if (self->enterprise_check_credentials) {
+ enterprise_check_login (self);
+ }
+ set_entry_validation_checkmark (self->enterprise_domain_entry);
+ gtk_label_set_text (self->enterprise_domain_hint_label, DOMAIN_DEFAULT_HINT);
+ g_list_free_full (realms, g_object_unref);
+
+ /* The domain is likely invalid*/
+ } else {
+ g_message ("Couldn't discover domain: %s", error->message);
+ g_dbus_error_strip_remote_error (error);
+
+ if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC)) {
+ message = g_strdup (_("Unable to find the domain. Maybe you misspelled it?"));
+ } else {
+ message = g_strdup_printf ("%s.", error->message);
+ }
+ gtk_label_set_text (self->enterprise_domain_hint_label, message);
+
+ g_free (message);
+ g_error_free (error);
+
+ if (self->enterprise_check_credentials) {
+ finish_action (self);
+ self->enterprise_check_credentials = FALSE;
+ }
+ }
+
+ if (!self->enterprise_check_credentials) {
+ finish_action (self);
+ dialog_validate (self);
+ }
+
+ g_object_unref (self);
+}
+
+static void
+enterprise_check_domain (CcAddUserDialog *self)
+{
+ const gchar *domain;
+
+ domain = gtk_entry_get_text (self->enterprise_domain_entry);
+ if (strlen (domain) == 0) {
+ gtk_label_set_text (self->enterprise_domain_hint_label, DOMAIN_DEFAULT_HINT);
+ return;
+ }
+
+ begin_action (self);
+
+ self->join_prompted = FALSE;
+ cc_realm_manager_discover (self->realm_manager,
+ domain,
+ self->cancellable,
+ on_realm_discover_input,
+ g_object_ref (self));
+}
+
+static void
+enterprise_add_user (CcAddUserDialog *self)
+{
+ self->join_prompted = FALSE;
+ self->enterprise_check_credentials = TRUE;
+ begin_action (self);
+ enterprise_check_login (self);
+
+}
+
+static void
+clear_realm_manager (CcAddUserDialog *self)
+{
+ if (self->realm_manager) {
+ g_signal_handlers_disconnect_by_func (self->realm_manager,
+ on_manager_realm_added,
+ self);
+ g_object_unref (self->realm_manager);
+ self->realm_manager = NULL;
+ }
+}
+
+static void
+on_realm_manager_created (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+ GList *realms, *l;
+
+ clear_realm_manager (self);
+
+ self->realm_manager = cc_realm_manager_new_finish (result, &error);
+ if (error != NULL) {
+ g_warning ("Couldn't contact realmd service: %s", error->message);
+ g_object_unref (self);
+ g_error_free (error);
+ return;
+ }
+
+ if (g_cancellable_is_cancelled (self->cancellable)) {
+ g_object_unref (self);
+ return;
+ }
+
+ /* Lookup all the realm objects */
+ realms = cc_realm_manager_get_realms (self->realm_manager);
+ for (l = realms; l != NULL; l = g_list_next (l))
+ enterprise_add_realm (self, l->data);
+ g_list_free (realms);
+ g_signal_connect_object (self->realm_manager, "realm-added",
+ G_CALLBACK (on_manager_realm_added), self, G_CONNECT_SWAPPED);
+
+ /* When no realms try to discover a sensible default, triggers realm-added signal */
+ cc_realm_manager_discover (self->realm_manager, "", self->cancellable,
+ NULL, NULL);
+
+ /* Show the 'Enterprise Login' stuff, and update mode */
+ gtk_widget_show (GTK_WIDGET (self->enterprise_button));
+ mode_change (self, self->mode);
+ g_object_unref (self);
+}
+
+static void
+on_realmd_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ cc_realm_manager_new (self->cancellable, on_realm_manager_created,
+ g_object_ref (self));
+}
+
+static void
+on_realmd_disappeared (GDBusConnection *unused1,
+ const gchar *unused2,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+
+ clear_realm_manager (self);
+ gtk_list_store_clear (self->enterprise_realm_model);
+ gtk_widget_hide (GTK_WIDGET (self->enterprise_button));
+ mode_change (self, MODE_LOCAL);
+}
+
+static void
+on_network_changed (GNetworkMonitor *monitor,
+ gboolean available,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+
+ if (self->mode != MODE_LOCAL)
+ mode_change (self, MODE_ENTERPRISE);
+}
+
+static gboolean
+enterprise_domain_timeout (CcAddUserDialog *self)
+{
+ GtkTreeIter iter;
+
+ self->enterprise_domain_timeout_id = 0;
+
+ if (gtk_combo_box_get_active_iter (self->enterprise_domain_combo, &iter)) {
+ gtk_tree_model_get (GTK_TREE_MODEL (self->enterprise_realm_model), &iter, 1, &self->selected_realm, -1);
+ set_entry_validation_checkmark (self->enterprise_domain_entry);
+ gtk_label_set_text (self->enterprise_domain_hint_label, DOMAIN_DEFAULT_HINT);
+ }
+ else {
+ enterprise_check_domain (self);
+ }
+
+ return FALSE;
+}
+
+static void
+enterprise_domain_combo_changed_cb (CcAddUserDialog *self)
+{
+ if (self->enterprise_domain_timeout_id != 0) {
+ g_source_remove (self->enterprise_domain_timeout_id);
+ self->enterprise_domain_timeout_id = 0;
+ }
+
+ g_clear_object (&self->selected_realm);
+ clear_entry_validation_error (self->enterprise_domain_entry);
+ self->enterprise_domain_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) enterprise_domain_timeout, self);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+
+ self->enterprise_domain_chosen = TRUE;
+ dialog_validate (self);
+}
+
+static gboolean
+enterprise_domain_combo_focus_out_event_cb (CcAddUserDialog *self)
+{
+ if (self->enterprise_domain_timeout_id != 0) {
+ g_source_remove (self->enterprise_domain_timeout_id);
+ self->enterprise_domain_timeout_id = 0;
+ }
+
+ if (self->selected_realm == NULL) {
+ enterprise_check_domain (self);
+ }
+
+ return FALSE;
+}
+
+static void
+enterprise_login_entry_changed_cb (CcAddUserDialog *self)
+{
+ dialog_validate (self);
+ clear_entry_validation_error (self->enterprise_login_entry);
+ clear_entry_validation_error (self->enterprise_password_entry);
+}
+
+static void
+enterprise_password_entry_changed_cb (CcAddUserDialog *self)
+{
+ dialog_validate (self);
+ clear_entry_validation_error (self->enterprise_password_entry);
+}
+
+static void
+dialog_validate (CcAddUserDialog *self)
+{
+ gboolean valid = FALSE;
+
+ switch (self->mode) {
+ case MODE_LOCAL:
+ valid = local_validate (self);
+ break;
+ case MODE_ENTERPRISE:
+ valid = enterprise_validate (self);
+ break;
+ default:
+ valid = FALSE;
+ break;
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), valid);
+}
+
+static void
+mode_change (CcAddUserDialog *self,
+ AccountMode mode)
+{
+ gboolean available;
+ GNetworkMonitor *monitor;
+
+ if (mode != MODE_LOCAL) {
+ monitor = g_network_monitor_get_default ();
+ available = g_network_monitor_get_network_available (monitor);
+ mode = available ? MODE_ENTERPRISE : MODE_OFFLINE;
+ }
+
+ switch (mode) {
+ default:
+ case MODE_LOCAL:
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->local_grid));
+ gtk_widget_grab_focus (GTK_WIDGET (self->local_name_entry));
+ gtk_toggle_button_set_active (self->enterprise_button, FALSE);
+ break;
+ case MODE_ENTERPRISE:
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->enterprise_grid));
+ gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_domain_entry));
+ gtk_toggle_button_set_active (self->enterprise_button, TRUE);
+ break;
+ case MODE_OFFLINE:
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->offline_grid));
+ gtk_toggle_button_set_active (self->enterprise_button, TRUE);
+ break;
+ }
+
+ self->mode = mode;
+ dialog_validate (self);
+}
+
+static void
+enterprise_button_toggled_cb (CcAddUserDialog *self)
+{
+ AccountMode mode;
+
+ mode = gtk_toggle_button_get_active (self->enterprise_button) ? MODE_ENTERPRISE : MODE_LOCAL;
+ mode_change (self, mode);
+}
+
+static void
+cc_add_user_dialog_init (CcAddUserDialog *self)
+{
+ GNetworkMonitor *monitor;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->cancellable = g_cancellable_new ();
+
+ self->local_password_mode = ACT_USER_PASSWORD_MODE_SET_AT_LOGIN;
+ dialog_validate (self);
+ update_password_strength (self);
+ local_username_timeout (self);
+
+ enterprise_check_domain (self);
+
+ self->realmd_watch = g_bus_watch_name (G_BUS_TYPE_SYSTEM, "org.freedesktop.realmd",
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ on_realmd_appeared, on_realmd_disappeared,
+ self, NULL);
+
+ monitor = g_network_monitor_get_default ();
+ g_signal_connect_object (monitor, "network-changed", G_CALLBACK (on_network_changed), self, 0);
+
+ join_init (self);
+
+ mode_change (self, MODE_LOCAL);
+}
+
+static void
+on_permission_acquired (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data);
+ GError *error = NULL;
+
+ /* Paired with begin_action in cc_add_user_dialog_response () */
+ finish_action (self);
+
+ if (g_permission_acquire_finish (self->permission, res, &error)) {
+ g_return_if_fail (g_permission_get_allowed (self->permission));
+ add_button_clicked_cb (self);
+ } else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_warning ("Failed to acquire permission: %s", error->message);
+ }
+
+ g_clear_error (&error);
+ g_object_unref (self);
+}
+
+static void
+add_button_clicked_cb (CcAddUserDialog *self)
+{
+ /* We don't (or no longer) have necessary permissions */
+ if (self->permission && !g_permission_get_allowed (self->permission)) {
+ begin_action (self);
+ g_permission_acquire_async (self->permission, self->cancellable,
+ on_permission_acquired, g_object_ref (self));
+ return;
+ }
+
+ switch (self->mode) {
+ case MODE_LOCAL:
+ local_create_user (self);
+ break;
+ case MODE_ENTERPRISE:
+ enterprise_add_user (self);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+cc_add_user_dialog_dispose (GObject *obj)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (obj);
+
+ if (self->cancellable)
+ g_cancellable_cancel (self->cancellable);
+
+ g_clear_object (&self->user);
+
+ if (self->realmd_watch)
+ g_bus_unwatch_name (self->realmd_watch);
+ self->realmd_watch = 0;
+
+ if (self->realm_manager) {
+ g_signal_handlers_disconnect_by_func (self->realm_manager,
+ on_manager_realm_added,
+ self);
+ g_object_unref (self->realm_manager);
+ self->realm_manager = NULL;
+ }
+
+ if (self->local_password_timeout_id != 0) {
+ g_source_remove (self->local_password_timeout_id);
+ self->local_password_timeout_id = 0;
+ }
+
+ if (self->local_name_timeout_id != 0) {
+ g_source_remove (self->local_name_timeout_id);
+ self->local_name_timeout_id = 0;
+ }
+
+ if (self->local_username_timeout_id != 0) {
+ g_source_remove (self->local_username_timeout_id);
+ self->local_username_timeout_id = 0;
+ }
+
+ if (self->enterprise_domain_timeout_id != 0) {
+ g_source_remove (self->enterprise_domain_timeout_id);
+ self->enterprise_domain_timeout_id = 0;
+ }
+
+ g_clear_pointer ((GtkWidget **)&self->join_dialog, gtk_widget_destroy);
+
+ G_OBJECT_CLASS (cc_add_user_dialog_parent_class)->dispose (obj);
+}
+
+static void
+cc_add_user_dialog_finalize (GObject *obj)
+{
+ CcAddUserDialog *self = CC_ADD_USER_DIALOG (obj);
+
+ if (self->cancellable)
+ g_object_unref (self->cancellable);
+ g_clear_object (&self->permission);
+
+ G_OBJECT_CLASS (cc_add_user_dialog_parent_class)->finalize (obj);
+}
+
+static void
+cc_add_user_dialog_class_init (CcAddUserDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_add_user_dialog_dispose;
+ object_class->finalize = cc_add_user_dialog_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-add-user-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, add_button);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_button);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_combo);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_grid);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_login_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_password_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_realm_model);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_account_type_standard);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_grid);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_name_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_combo);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_model);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_radio);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_strength_indicator);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_verify_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_verify_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, offline_grid);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, spinner);
+ gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, stack);
+
+ gtk_widget_class_bind_template_callback (widget_class, add_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, dialog_validate);
+ gtk_widget_class_bind_template_callback (widget_class, enterprise_button_toggled_cb);
+ gtk_widget_class_bind_template_callback (widget_class, enterprise_domain_combo_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, enterprise_domain_combo_focus_out_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, enterprise_login_entry_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, enterprise_password_entry_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_name_entry_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_name_entry_focus_out_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_password_entry_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_password_entry_icon_press_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_password_entry_key_press_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_password_radio_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_username_combo_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_username_combo_focus_out_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, local_verify_entry_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, password_focus_out_event_cb);
+}
+
+CcAddUserDialog *
+cc_add_user_dialog_new (GPermission *permission)
+{
+ CcAddUserDialog *self;
+
+ self = g_object_new (CC_TYPE_ADD_USER_DIALOG, "use-header-bar", TRUE, NULL);
+
+ if (permission != NULL)
+ self->permission = g_object_ref (permission);
+
+ return self;
+}
+
+ActUser *
+cc_add_user_dialog_get_user (CcAddUserDialog *self)
+{
+ g_return_val_if_fail (CC_IS_ADD_USER_DIALOG (self), NULL);
+ return self->user;
+}
diff --git a/panels/user-accounts/cc-add-user-dialog.h b/panels/user-accounts/cc-add-user-dialog.h
new file mode 100644
index 0000000..c666d4d
--- /dev/null
+++ b/panels/user-accounts/cc-add-user-dialog.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <act/act.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_ADD_USER_DIALOG (cc_add_user_dialog_get_type ())
+G_DECLARE_FINAL_TYPE (CcAddUserDialog, cc_add_user_dialog, CC, ADD_USER_DIALOG, GtkDialog)
+
+CcAddUserDialog *cc_add_user_dialog_new (GPermission *permission);
+ActUser *cc_add_user_dialog_get_user (CcAddUserDialog *dialog);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-add-user-dialog.ui b/panels/user-accounts/cc-add-user-dialog.ui
new file mode 100644
index 0000000..12a253a
--- /dev/null
+++ b/panels/user-accounts/cc-add-user-dialog.ui
@@ -0,0 +1,860 @@
+<?xml version="1.0"?>
+<interface>
+ <!-- interface-requires gtk+ 3.8 -->
+ <!-- interface-naming-policy toplevel-contextual -->
+ <object class="GtkListStore" id="local_username_model">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ </columns>
+ </object>
+ <template class="CcAddUserDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="title" translatable="yes">Add User</property>
+ <property name="icon_name">system-users</property>
+ <property name="use_header_bar">1</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="show_close_button">False</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="text-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="label" translatable="yes">_Add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="add_button_clicked_cb" object="CcAddUserDialog" swapped="yes"/>
+ <style>
+ <class name="text-button"/>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner" id="spinner">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="transition-type">none</property>
+ <child>
+ <object class="GtkGrid" id="local_grid">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="column_spacing">6</property>
+ <property name="row_spacing">8</property>
+ <property name="border_width">20</property>
+ <property name="margin_end">20</property>
+ <child>
+ <object class="GtkComboBoxText" id="local_username_combo">
+ <property name="visible">True</property>
+ <property name="has_entry">True</property>
+ <property name="entry_text_column">0</property>
+ <property name="model">local_username_model</property>
+ <property name="hexpand">True</property>
+ <signal name="changed" handler="local_username_combo_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="local_username_combo_focus_out_event_cb" after="yes" object="CcAddUserDialog" swapped="yes"/>
+ <child internal-child="entry">
+ <object class="GtkEntry" id="local_username_entry">
+ <property name="activates_default">True</property>
+ <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_username_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="height-request">50</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_username_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Username</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">local_username_combo</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="local_name_entry">
+ <property name="visible">True</property>
+ <property name="max-length">255</property>
+ <property name="can_focus">True</property>
+ <property name="activates_default">True</property>
+ <property name="hexpand">True</property>
+ <property name="has_focus">True</property>
+ <signal name="changed" handler="local_name_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="local_name_entry_focus_out_event_cb" after="yes" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_name_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Full Name</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">local_name_entry</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="local_account_type_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkRadioButton" id="local_account_type_standard">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Standard</property>
+ <property name="draw_indicator">False</property>
+ <property name="height_request">35</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="account_type_admin">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="label" translatable="yes">Administrator</property>
+ <property name="draw_indicator">False</property>
+ <property name="height_request">35</property>
+ <property name="group">local_account_type_standard</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_account_type_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Account _Type</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">local_account_type_box</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Password</property>
+ <property name="margin_top">12</property>
+ <property name="halign">start</property>
+ <property name="margin_start">20</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="margin_start">20</property>
+ <child>
+ <object class="GtkRadioButton" id="local_password_login_radio">
+ <property name="label" translatable="yes">Allow user to set a password when they next _login</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="local_password_radio">
+ <property name="label" translatable="yes">Set a password _now</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">local_password_login_radio</property>
+ <property name="use_underline">True</property>
+ <signal name="toggled" handler="local_password_radio_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">5</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_password_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">local_password_entry</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="local_password_entry">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="has_tooltip">True</property>
+ <property name="visibility">False</property>
+ <property name="invisible_char">●</property>
+ <property name="invisible_char_set">True</property>
+ <property name="hexpand">True</property>
+ <property name="activates_default">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="notify::text" handler="local_password_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="icon-press" handler="local_password_entry_icon_press_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="password_focus_out_event_cb" after="yes" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="key-press-event" handler="local_password_entry_key_press_event_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLevelBar" id="local_strength_indicator">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="mode">discrete</property>
+ <property name="max-value">5</property>
+ <property name="hexpand">True</property>
+ <offsets>
+ <offset name="strength-weak" value="1"/>
+ <offset name="strength-low" value="2"/>
+ <offset name="strength-medium" value="3"/>
+ <offset name="strength-good" value="4"/>
+ <offset name="strength-high" value="5"/>
+ </offsets>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">7</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_hint_label">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="height-request">50</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">8</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_verify_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Confirm</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">local_verify_entry</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">9</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="local_verify_entry">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="invisible_char">●</property>
+ <property name="invisible_char_set">True</property>
+ <property name="hexpand">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="notify::text" handler="local_verify_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="password_focus_out_event_cb" after="yes" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">9</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="local_verify_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">10</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">_local</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="enterprise_grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="row_spacing">8</property>
+ <property name="column_spacing">6</property>
+ <property name="border_width">20</property>
+ <property name="margin_end">20</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Enterprise login allows an existing centrally managed user account to be used on this device. You can also use this account to access company resources on the internet.</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="margin_bottom">20</property>
+ <property name="max_width_chars">55</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enterprise_domain_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Domain</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">enterprise_domain_combo</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enterprise_login_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Username</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">enterprise_login_entry</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enterprise_password_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">enterprise_password_entry</property>
+ <property name="margin_start">20</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enterprise_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="height-request">50</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="enterprise_domain_combo">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="has_entry">True</property>
+ <property name="entry_text_column">0</property>
+ <property name="model">enterprise_realm_model</property>
+ <signal name="changed" handler="enterprise_domain_combo_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="enterprise_domain_combo_focus_out_event_cb" after="yes" object="CcAddUserDialog" swapped="yes"/>
+ <child internal-child="entry">
+ <object class="GtkEntry" id="enterprise_domain_entry">
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="enterprise_domain_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="label"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="height-request">50</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkEntry" id="enterprise_login_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="has_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="invisible_char_set">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="changed" handler="enterprise_login_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="enterprise_password_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="visibility">False</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="invisible_char_set">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="changed" handler="enterprise_password_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="offline_grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="column_spacing">6</property>
+ <property name="row_spacing">8</property>
+ <property name="border_width">20</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Enterprise login allows an existing centrally managed user account to be used on this device. You can also use this account to access company resources on the internet.</property>
+ <property name="wrap">True</property>
+ <property name="wrap_mode">word-char</property>
+ <property name="margin_bottom">20</property>
+ <property name="margin_end">20</property>
+ <property name="max_width_chars">55</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">network-offline-symbolic</property>
+ <property name="pixel_size">160</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="margin_bottom">6</property>
+ <property name="valign">end</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">You are Offline</property>
+ <property name="yalign">0</property>
+ <property name="justify">center</property>
+ <property name="hexpand">True</property>
+ <attributes>
+ <attribute name="scale" value="1.6"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">You must be online in order to add enterprise users.</property>
+ <property name="yalign">0</property>
+ <property name="justify">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <attributes>
+ <attribute name="scale" value="1.2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkToggleButton" id="enterprise_button">
+ <property name="label" translatable="yes">_Enterprise Login</property>
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <property name="border_width">20</property>
+ <signal name="toggled" handler="enterprise_button_toggled_cb" object="CcAddUserDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+ <object class="GtkSizeGroup">
+ <widgets>
+ <widget name="local_username_label"/>
+ <widget name="local_name_label"/>
+ <widget name="local_account_type_label"/>
+ <widget name="enterprise_domain_label"/>
+ <widget name="enterprise_login_label"/>
+ <widget name="enterprise_password_label"/>
+ <widget name="local_password_label"/>
+ <widget name="local_verify_label"/>
+ </widgets>
+ </object>
+ <object class="GtkSizeGroup">
+ <widgets>
+ <widget name="local_username_combo"/>
+ <widget name="local_username_hint_label"/>
+ <widget name="local_name_entry"/>
+ <widget name="local_account_type_box"/>
+ <widget name="local_password_entry"/>
+ <widget name="local_hint_label"/>
+ <widget name="local_verify_entry"/>
+ <widget name="local_verify_hint_label"/>
+ <widget name="enterprise_domain_combo"/>
+ <widget name="enterprise_domain_hint_label"/>
+ <widget name="enterprise_login_entry"/>
+ <widget name="enterprise_password_entry"/>
+ <widget name="enterprise_hint_label"/>
+ </widgets>
+ </object>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="local_account_type_standard"/>
+ <widget name="account_type_admin"/>
+ </widgets>
+ </object>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="add_button"/>
+ <widget name="cancel_button"/>
+ </widgets>
+ </object>
+ <object class="GtkListStore" id="enterprise_realm_model">
+ <columns>
+ <!-- column-name title -->
+ <column type="gchararray"/>
+ <!-- column-name realm -->
+ <column type="GObject"/>
+ </columns>
+ </object>
+</interface>
diff --git a/panels/user-accounts/cc-avatar-chooser.c b/panels/user-accounts/cc-avatar-chooser.c
new file mode 100644
index 0000000..d0d4e1b
--- /dev/null
+++ b/panels/user-accounts/cc-avatar-chooser.c
@@ -0,0 +1,663 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include <gio/gunixoutputstream.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gtk/gtk.h>
+#include <act/act.h>
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-desktop-thumbnail.h>
+
+#ifdef HAVE_CHEESE
+#include <cheese-avatar-chooser.h>
+#include <cheese-camera-device.h>
+#include <cheese-camera-device-monitor.h>
+#endif /* HAVE_CHEESE */
+
+#include "cc-avatar-chooser.h"
+#include "cc-crop-area.h"
+#include "user-utils.h"
+
+#define ROW_SPAN 5
+#define AVATAR_CHOOSER_PIXEL_SIZE 80
+#define PIXEL_SIZE 512
+
+struct _CcAvatarChooser {
+ GtkPopover parent;
+
+ GtkWidget *popup_button;
+ GtkWidget *crop_area;
+ GtkWidget *user_flowbox;
+ GtkWidget *flowbox;
+ GtkWidget *take_picture_button;
+
+#ifdef HAVE_CHEESE
+ CheeseCameraDeviceMonitor *monitor;
+ GCancellable *cancellable;
+ guint num_cameras;
+#endif /* HAVE_CHEESE */
+
+ GnomeDesktopThumbnailFactory *thumb_factory;
+ GListStore *faces;
+
+ ActUser *user;
+};
+
+G_DEFINE_TYPE (CcAvatarChooser, cc_avatar_chooser, GTK_TYPE_POPOVER)
+
+static void
+crop_dialog_response (CcAvatarChooser *self,
+ gint response_id,
+ GtkWidget *dialog)
+{
+ GdkPixbuf *pb, *pb2;
+
+ if (response_id != GTK_RESPONSE_ACCEPT) {
+ self->crop_area = NULL;
+ gtk_widget_destroy (dialog);
+ return;
+ }
+
+ pb = cc_crop_area_get_picture (CC_CROP_AREA (self->crop_area));
+ pb2 = gdk_pixbuf_scale_simple (pb, PIXEL_SIZE, PIXEL_SIZE, GDK_INTERP_BILINEAR);
+
+ set_user_icon_data (self->user, pb2);
+
+ g_object_unref (pb2);
+ g_object_unref (pb);
+
+ self->crop_area = NULL;
+ gtk_widget_destroy (dialog);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+static void
+cc_avatar_chooser_crop (CcAvatarChooser *self,
+ GdkPixbuf *pixbuf)
+{
+ GtkWidget *dialog;
+
+ dialog = gtk_dialog_new_with_buttons ("",
+ GTK_WINDOW (gtk_widget_get_toplevel (self->popup_button)),
+ GTK_DIALOG_USE_HEADER_BAR,
+ _("_Cancel"),
+ GTK_RESPONSE_CANCEL,
+ _("Select"),
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+
+ gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users");
+
+ g_signal_connect_object (G_OBJECT (dialog), "response",
+ G_CALLBACK (crop_dialog_response), self, G_CONNECT_SWAPPED);
+
+ /* Content */
+ self->crop_area = cc_crop_area_new ();
+ gtk_widget_show (self->crop_area);
+ cc_crop_area_set_min_size (CC_CROP_AREA (self->crop_area), 48, 48);
+ cc_crop_area_set_constrain_aspect (CC_CROP_AREA (self->crop_area), TRUE);
+ cc_crop_area_set_picture (CC_CROP_AREA (self->crop_area), pixbuf);
+ gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
+ self->crop_area,
+ TRUE, TRUE, 8);
+
+ gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 300);
+
+ gtk_widget_show (dialog);
+}
+
+static void
+file_chooser_response (CcAvatarChooser *self,
+ gint response,
+ GtkDialog *chooser)
+{
+ gchar *filename;
+ GError *error;
+ GdkPixbuf *pixbuf, *pixbuf2;
+
+ if (response != GTK_RESPONSE_ACCEPT) {
+ gtk_widget_destroy (GTK_WIDGET (chooser));
+ return;
+ }
+
+ filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (chooser));
+
+ error = NULL;
+ pixbuf = gdk_pixbuf_new_from_file (filename, &error);
+ if (pixbuf == NULL) {
+ g_warning ("Failed to load %s: %s", filename, error->message);
+ g_error_free (error);
+ }
+ g_free (filename);
+
+ pixbuf2 = gdk_pixbuf_apply_embedded_orientation (pixbuf);
+ g_object_unref (pixbuf);
+
+ gtk_widget_destroy (GTK_WIDGET (chooser));
+
+ cc_avatar_chooser_crop (self, pixbuf2);
+ g_object_unref (pixbuf2);
+}
+
+static void
+update_preview (GtkFileChooser *chooser,
+ GnomeDesktopThumbnailFactory *thumb_factory)
+{
+ gchar *uri;
+
+ uri = gtk_file_chooser_get_uri (chooser);
+
+ if (uri) {
+ GdkPixbuf *pixbuf = NULL;
+ char *mime_type = NULL;
+ GFile *file;
+ GFileInfo *file_info;
+ GtkWidget *preview;
+
+ preview = gtk_file_chooser_get_preview_widget (chooser);
+
+ file = g_file_new_for_uri (uri);
+ file_info = g_file_query_info (file,
+ "standard::*",
+ G_FILE_QUERY_INFO_NONE,
+ NULL, NULL);
+ g_object_unref (file);
+
+ if (file_info != NULL &&
+ g_file_info_get_file_type (file_info) != G_FILE_TYPE_DIRECTORY) {
+ mime_type = g_strdup (g_file_info_get_content_type (file_info));
+ g_object_unref (file_info);
+ }
+
+ if (mime_type) {
+ pixbuf = gnome_desktop_thumbnail_factory_generate_thumbnail (thumb_factory,
+ uri,
+ mime_type);
+ g_free (mime_type);
+ }
+
+ gtk_dialog_set_response_sensitive (GTK_DIALOG (chooser),
+ GTK_RESPONSE_ACCEPT,
+ (pixbuf != NULL));
+
+ if (pixbuf != NULL) {
+ gtk_image_set_from_pixbuf (GTK_IMAGE (preview), pixbuf);
+ g_object_unref (pixbuf);
+ }
+ else {
+ gtk_image_set_from_icon_name (GTK_IMAGE (preview),
+ "dialog-question",
+ GTK_ICON_SIZE_DIALOG);
+ }
+
+ g_free (uri);
+ }
+
+ gtk_file_chooser_set_preview_widget_active (chooser, TRUE);
+}
+
+static void
+cc_avatar_chooser_select_file (CcAvatarChooser *self)
+{
+ GtkWidget *chooser;
+ const gchar *folder;
+ GtkWidget *preview;
+ GtkFileFilter *filter;
+
+ chooser = gtk_file_chooser_dialog_new (_("Browse for more pictures"),
+ GTK_WINDOW (gtk_widget_get_toplevel (self->popup_button)),
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ _("_Open"), GTK_RESPONSE_ACCEPT,
+ NULL);
+
+ gtk_window_set_modal (GTK_WINDOW (chooser), TRUE);
+
+ preview = gtk_image_new ();
+ gtk_widget_set_size_request (preview, 128, -1);
+ gtk_file_chooser_set_preview_widget (GTK_FILE_CHOOSER (chooser), preview);
+ gtk_file_chooser_set_use_preview_label (GTK_FILE_CHOOSER (chooser), FALSE);
+ gtk_widget_show (preview);
+
+ /* Preview has to be generated after default handler of "selection-changed"
+ * signal, otherwise dialog response sensitivity is rewritten (Bug 547988).
+ * Preview also has to be generated on "selection-changed" signal to reflect
+ * all changes (Bug 660877). */
+ g_signal_connect_after (chooser, "selection-changed",
+ G_CALLBACK (update_preview), self->thumb_factory);
+
+ folder = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES);
+ if (folder)
+ gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (chooser),
+ folder);
+
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_add_pixbuf_formats (filter);
+ gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (chooser), filter);
+
+ g_signal_connect_object (chooser, "response",
+ G_CALLBACK (file_chooser_response), self, G_CONNECT_SWAPPED);
+
+ gtk_window_present (GTK_WINDOW (chooser));
+}
+
+#ifdef HAVE_CHEESE
+static gboolean
+destroy_chooser (GtkWidget *chooser)
+{
+ gtk_widget_destroy (chooser);
+ return FALSE;
+}
+
+static void
+webcam_response_cb (CcAvatarChooser *self,
+ int response,
+ GtkDialog *dialog)
+{
+ if (response == GTK_RESPONSE_ACCEPT) {
+ GdkPixbuf *pb, *pb2;
+
+ g_object_get (G_OBJECT (dialog), "pixbuf", &pb, NULL);
+ pb2 = gdk_pixbuf_scale_simple (pb, PIXEL_SIZE, PIXEL_SIZE, GDK_INTERP_BILINEAR);
+
+ set_user_icon_data (self->user, pb2);
+
+ g_object_unref (pb2);
+ g_object_unref (pb);
+ }
+ if (response != GTK_RESPONSE_DELETE_EVENT &&
+ response != GTK_RESPONSE_NONE)
+ g_idle_add ((GSourceFunc) destroy_chooser, dialog);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+static void
+webcam_icon_selected (CcAvatarChooser *self)
+{
+ GtkWidget *window;
+
+ window = cheese_avatar_chooser_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (window),
+ GTK_WINDOW (gtk_widget_get_toplevel (self->popup_button)));
+ gtk_window_set_modal (GTK_WINDOW (window), TRUE);
+ g_signal_connect_object (G_OBJECT (window), "response",
+ G_CALLBACK (webcam_response_cb), self, G_CONNECT_SWAPPED);
+ gtk_widget_show (window);
+}
+
+static void
+update_photo_menu_status (CcAvatarChooser *self)
+{
+ if (self->num_cameras == 0)
+ gtk_widget_set_visible (self->take_picture_button, FALSE);
+ else
+ gtk_widget_set_sensitive (self->take_picture_button, TRUE);
+}
+
+static void
+device_added (CcAvatarChooser *self)
+{
+ self->num_cameras++;
+ update_photo_menu_status (self);
+}
+
+static void
+device_removed (CcAvatarChooser *self)
+{
+ self->num_cameras--;
+ update_photo_menu_status (self);
+}
+
+#endif /* HAVE_CHEESE */
+
+static void
+face_widget_activated (CcAvatarChooser *self,
+ GtkFlowBoxChild *child)
+{
+ const gchar *filename;
+ GtkWidget *image;
+
+ image = gtk_bin_get_child (GTK_BIN (child));
+ filename = g_object_get_data (G_OBJECT (image), "filename");
+
+ act_user_set_icon_file (self->user, filename);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+static GtkWidget *
+create_face_widget (gpointer item,
+ gpointer user_data)
+{
+ g_autofree gchar *image_path = NULL;
+ g_autoptr(GdkPixbuf) source_pixbuf = NULL;
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+ GtkWidget *image;
+
+ image_path = g_file_get_path (G_FILE (item));
+
+ source_pixbuf = gdk_pixbuf_new_from_file_at_size (image_path,
+ AVATAR_CHOOSER_PIXEL_SIZE,
+ AVATAR_CHOOSER_PIXEL_SIZE,
+ NULL);
+ if (source_pixbuf == NULL)
+ return NULL;
+
+ pixbuf = round_image (source_pixbuf);
+ image = gtk_image_new_from_pixbuf (pixbuf);
+ gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_CHOOSER_PIXEL_SIZE);
+ gtk_widget_show (image);
+
+ g_object_set_data_full (G_OBJECT (image),
+ "filename", g_steal_pointer (&image_path), g_free);
+
+ return image;
+}
+
+#ifdef HAVE_CHEESE
+static void
+setup_cheese_camera_device_monitor (CcAvatarChooser *self)
+{
+ g_signal_connect_object (G_OBJECT (self->monitor), "added", G_CALLBACK (device_added), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (G_OBJECT (self->monitor), "removed", G_CALLBACK (device_removed), self, G_CONNECT_SWAPPED);
+ cheese_camera_device_monitor_coldplug (self->monitor);
+ update_photo_menu_status (self);
+}
+
+static void
+cheese_camera_device_monitor_new_cb (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ CcAvatarChooser *self = user_data;
+ GObject *ret;
+
+ ret = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, NULL);
+ if (ret == NULL)
+ return;
+
+ self->monitor = CHEESE_CAMERA_DEVICE_MONITOR (ret);
+ setup_cheese_camera_device_monitor (self);
+}
+#endif /* HAVE_CHEESE */
+
+static GStrv
+get_settings_facesdirs (void)
+{
+ g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface");
+ g_auto(GStrv) settings_dirs = g_settings_get_strv (settings, "avatar-directories");
+ GPtrArray *facesdirs = g_ptr_array_new ();
+
+ if (settings_dirs != NULL) {
+ int i;
+ for (i = 0; settings_dirs[i] != NULL; i++) {
+ char *path = settings_dirs[i];
+ if (g_strcmp0 (path, "") != 0)
+ g_ptr_array_add (facesdirs, g_strdup (path));
+ }
+ }
+ g_ptr_array_add (facesdirs, NULL);
+
+ return (GStrv) g_ptr_array_steal (facesdirs, NULL);
+}
+
+static GStrv
+get_system_facesdirs (void)
+{
+ const char * const * data_dirs;
+ GPtrArray *facesdirs;
+ int i;
+
+ facesdirs = g_ptr_array_new ();
+
+ data_dirs = g_get_system_data_dirs ();
+ for (i = 0; data_dirs[i] != NULL; i++) {
+ char *path = g_build_filename (data_dirs[i], "pixmaps", "faces", NULL);
+ g_ptr_array_add (facesdirs, path);
+ }
+ g_ptr_array_add (facesdirs, NULL);
+ return (GStrv) g_ptr_array_steal (facesdirs, NULL);
+}
+
+static gboolean
+add_faces_from_dirs (GListStore *faces, GStrv facesdirs, gboolean add_all)
+{
+ GFile *file, *dir;
+ GFileInfo *info;
+ GFileEnumerator *enumerator;
+ GFileType type;
+ const gchar *target;
+ guint i;
+ gboolean added_faces = FALSE;
+
+ for (i = 0; facesdirs[i] != NULL; i++) {
+ dir = g_file_new_for_path (facesdirs[i]);
+
+ enumerator = g_file_enumerate_children (dir,
+ G_FILE_ATTRIBUTE_STANDARD_NAME ","
+ G_FILE_ATTRIBUTE_STANDARD_TYPE ","
+ G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK ","
+ G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET,
+ G_FILE_QUERY_INFO_NONE,
+ NULL, NULL);
+ if (enumerator == NULL) {
+ g_object_unref (dir);
+ continue;
+ }
+
+ while ((info = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) {
+ type = g_file_info_get_file_type (info);
+ if (type != G_FILE_TYPE_REGULAR &&
+ type != G_FILE_TYPE_SYMBOLIC_LINK) {
+ g_object_unref (info);
+ continue;
+ }
+
+ target = g_file_info_get_symlink_target (info);
+ if (target != NULL && g_str_has_prefix (target , "legacy/")) {
+ g_object_unref (info);
+ continue;
+ }
+
+ file = g_file_get_child (dir, g_file_info_get_name (info));
+ g_list_store_append (faces, file);
+
+ g_object_unref (info);
+ added_faces = TRUE;
+ }
+
+ g_file_enumerator_close (enumerator, NULL, NULL);
+ g_object_unref (enumerator);
+ g_object_unref (dir);
+
+ if (added_faces && !add_all)
+ break;
+ }
+ return added_faces;
+}
+
+
+static void
+setup_photo_popup (CcAvatarChooser *self)
+{
+ g_auto(GStrv) settings_facesdirs = NULL;
+
+ self->faces = g_list_store_new (G_TYPE_FILE);
+ gtk_flow_box_bind_model (GTK_FLOW_BOX (self->flowbox),
+ G_LIST_MODEL (self->faces),
+ create_face_widget,
+ self,
+ NULL);
+
+ g_signal_connect_object (self->flowbox, "child-activated",
+ G_CALLBACK (face_widget_activated), self, G_CONNECT_SWAPPED);
+
+ settings_facesdirs = get_settings_facesdirs ();
+
+ if (!add_faces_from_dirs (self->faces, settings_facesdirs, TRUE)) {
+ g_auto(GStrv) system_facesdirs = get_system_facesdirs ();
+ add_faces_from_dirs (self->faces, system_facesdirs, FALSE);
+ }
+
+#ifdef HAVE_CHEESE
+ gtk_widget_set_visible (self->take_picture_button, TRUE);
+
+ self->cancellable = g_cancellable_new ();
+ g_async_initable_new_async (CHEESE_TYPE_CAMERA_DEVICE_MONITOR,
+ G_PRIORITY_DEFAULT,
+ self->cancellable,
+ cheese_camera_device_monitor_new_cb,
+ self,
+ NULL);
+#endif /* HAVE_CHEESE */
+}
+
+static void
+popup_icon_menu (CcAvatarChooser *self)
+{
+ gtk_popover_popup (GTK_POPOVER (self));
+}
+
+static gboolean
+on_popup_button_button_pressed (CcAvatarChooser *self,
+ GdkEventButton *event)
+{
+ if (event->button == 1) {
+ if (!gtk_widget_get_visible (GTK_WIDGET (self))) {
+ popup_icon_menu (self);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->popup_button), TRUE);
+ } else {
+ gtk_popover_popdown (GTK_POPOVER (self));
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->popup_button), FALSE);
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+CcAvatarChooser *
+cc_avatar_chooser_new (GtkWidget *button)
+{
+ CcAvatarChooser *self;
+
+ self = g_object_new (CC_TYPE_AVATAR_CHOOSER,
+ "relative-to", button,
+ NULL);
+
+ self->thumb_factory = gnome_desktop_thumbnail_factory_new (GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL);
+
+ /* Set up the popup */
+ self->popup_button = button;
+ setup_photo_popup (self);
+ g_signal_connect_object (button, "toggled",
+ G_CALLBACK (popup_icon_menu), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (button, "button-press-event",
+ G_CALLBACK (on_popup_button_button_pressed), self, G_CONNECT_SWAPPED);
+
+ return self;
+}
+
+static void
+cc_avatar_chooser_dispose (GObject *object)
+{
+ CcAvatarChooser *self = CC_AVATAR_CHOOSER (object);
+
+ g_clear_object (&self->thumb_factory);
+#ifdef HAVE_CHEESE
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+ g_clear_object (&self->monitor);
+#endif
+ g_clear_object (&self->user);
+
+ G_OBJECT_CLASS (cc_avatar_chooser_parent_class)->dispose (object);
+}
+
+static void
+cc_avatar_chooser_init (CcAvatarChooser *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+cc_avatar_chooser_class_init (CcAvatarChooserClass *klass)
+{
+ GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
+ GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (wclass, "/org/gnome/control-center/user-accounts/cc-avatar-chooser.ui");
+
+ gtk_widget_class_bind_template_child (wclass, CcAvatarChooser, user_flowbox);
+ gtk_widget_class_bind_template_child (wclass, CcAvatarChooser, flowbox);
+ gtk_widget_class_bind_template_child (wclass, CcAvatarChooser, take_picture_button);
+
+ gtk_widget_class_bind_template_callback (wclass, cc_avatar_chooser_select_file);
+#ifdef HAVE_CHEESE
+ gtk_widget_class_bind_template_callback (wclass, webcam_icon_selected);
+#endif
+
+ oclass->dispose = cc_avatar_chooser_dispose;
+}
+
+static void
+user_flowbox_activated (CcAvatarChooser *self)
+{
+ set_default_avatar (self->user);
+
+ gtk_popover_popdown (GTK_POPOVER (self));
+}
+
+void
+cc_avatar_chooser_set_user (CcAvatarChooser *self,
+ ActUser *user)
+{
+ g_autoptr(GdkPixbuf) source_pixbuf = NULL;
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+ GtkWidget *image;
+
+ g_return_if_fail (self != NULL);
+
+ if (self->user) {
+ gtk_container_foreach (GTK_CONTAINER (self->user_flowbox), (GtkCallback) gtk_widget_destroy, NULL);
+ g_object_unref (self->user);
+ self->user = NULL;
+ }
+ self->user = g_object_ref (user);
+
+ source_pixbuf = generate_default_avatar (user, AVATAR_CHOOSER_PIXEL_SIZE);
+ pixbuf = round_image (source_pixbuf);
+ image = gtk_image_new_from_pixbuf (pixbuf);
+ gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_CHOOSER_PIXEL_SIZE);
+ gtk_widget_show (image);
+ gtk_container_add (GTK_CONTAINER (self->user_flowbox), image);
+ g_signal_connect_object (self->user_flowbox, "child-activated", G_CALLBACK (user_flowbox_activated), self, G_CONNECT_SWAPPED);
+}
+
diff --git a/panels/user-accounts/cc-avatar-chooser.h b/panels/user-accounts/cc-avatar-chooser.h
new file mode 100644
index 0000000..56a1699
--- /dev/null
+++ b/panels/user-accounts/cc-avatar-chooser.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_AVATAR_CHOOSER (cc_avatar_chooser_get_type())
+
+G_DECLARE_FINAL_TYPE (CcAvatarChooser, cc_avatar_chooser, CC, AVATAR_CHOOSER, GtkPopover)
+
+typedef struct _CcAvatarChooser CcAvatarChooser;
+
+CcAvatarChooser *cc_avatar_chooser_new (GtkWidget *button);
+void cc_avatar_chooser_free (CcAvatarChooser *dialog);
+void cc_avatar_chooser_set_user (CcAvatarChooser *dialog,
+ ActUser *user);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-avatar-chooser.ui b/panels/user-accounts/cc-avatar-chooser.ui
new file mode 100644
index 0000000..45bcbab
--- /dev/null
+++ b/panels/user-accounts/cc-avatar-chooser.ui
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<interface>
+ <!-- interface-requires gtk+ 3.8 -->
+ <template class="CcAvatarChooser" parent="GtkPopover">
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="border-width">10</property>
+ <child>
+ <object class="GtkFlowBox" id="user_flowbox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="selection-mode">none</property>
+ <property name="homogeneous">True</property>
+ <property name="max-children-per-line">5</property>
+ <property name="column-spacing">10</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFlowBox" id="flowbox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="selection-mode">none</property>
+ <property name="homogeneous">True</property>
+ <property name="max-children-per-line">5</property>
+ <property name="column-spacing">10</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_CENTER</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkButton" id="take_picture_button">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Take a Picture…</property>
+ <signal name="clicked" handler="webcam_icon_selected" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Select a File…</property>
+ <signal name="clicked" handler="cc_avatar_chooser_select_file" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack-type">GTK_PACK_END</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/user-accounts/cc-carousel.c b/panels/user-accounts/cc-carousel.c
new file mode 100644
index 0000000..2c3cd99
--- /dev/null
+++ b/panels/user-accounts/cc-carousel.c
@@ -0,0 +1,438 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2016 (c) Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Felipe Borges <felipeborges@gnome.org>
+ */
+
+#include "cc-carousel.h"
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#define ARROW_SIZE 20
+
+struct _CcCarouselItem {
+ GtkRadioButton parent;
+
+ gint page;
+};
+
+G_DEFINE_TYPE (CcCarouselItem, cc_carousel_item, GTK_TYPE_RADIO_BUTTON)
+
+GtkWidget *
+cc_carousel_item_new (void)
+{
+ return g_object_new (CC_TYPE_CAROUSEL_ITEM, NULL);
+}
+
+static void
+cc_carousel_item_class_init (CcCarouselItemClass *klass)
+{
+}
+
+static void
+cc_carousel_item_init (CcCarouselItem *self)
+{
+ gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE);
+ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
+ "carousel-item");
+}
+
+struct _CcCarousel {
+ GtkRevealer parent;
+
+ GList *children;
+ gint visible_page;
+ CcCarouselItem *selected_item;
+ GtkWidget *last_box;
+ GtkWidget *arrow;
+ gint arrow_start_x;
+
+ /* Widgets */
+ GtkStack *stack;
+ GtkWidget *go_back_button;
+ GtkWidget *go_next_button;
+
+ GtkStyleProvider *provider;
+};
+
+G_DEFINE_TYPE (CcCarousel, cc_carousel, GTK_TYPE_REVEALER)
+
+enum {
+ ITEM_ACTIVATED,
+ NUM_SIGNALS
+};
+
+static guint signals[NUM_SIGNALS] = { 0, };
+
+#define ITEMS_PER_PAGE 3
+
+static gint
+cc_carousel_item_get_x (CcCarouselItem *item,
+ CcCarousel *carousel)
+{
+ GtkWidget *widget, *parent;
+ gint width;
+ gint dest_x = 0;
+
+ parent = GTK_WIDGET (carousel->stack);
+ widget = GTK_WIDGET (item);
+
+ width = gtk_widget_get_allocated_width (widget);
+ if (!gtk_widget_translate_coordinates (widget,
+ parent,
+ width / 2,
+ 0,
+ &dest_x,
+ NULL))
+ return 0;
+
+ return CLAMP (dest_x - ARROW_SIZE,
+ 0,
+ gtk_widget_get_allocated_width (parent));
+}
+
+static void
+cc_carousel_move_arrow (CcCarousel *self)
+{
+ GtkStyleContext *context;
+ gchar *css;
+ gint end_x;
+ GtkSettings *settings;
+ gboolean animations;
+
+ if (!self->selected_item)
+ return;
+
+ end_x = cc_carousel_item_get_x (self->selected_item, self);
+
+ context = gtk_widget_get_style_context (self->arrow);
+ if (self->provider)
+ gtk_style_context_remove_provider (context, self->provider);
+ g_clear_object (&self->provider);
+
+ settings = gtk_widget_get_settings (GTK_WIDGET (self));
+ g_object_get (settings, "gtk-enable-animations", &animations, NULL);
+
+ /* Animate the arrow movement if animations are enabled. Otherwise,
+ * jump the arrow to the right location instantly. */
+ if (animations)
+ {
+ css = g_strdup_printf ("@keyframes arrow_keyframes-%d {\n"
+ " from { margin-left: %dpx; }\n"
+ " to { margin-left: %dpx; }\n"
+ "}\n"
+ "* {\n"
+ " animation-name: arrow_keyframes-%d;\n"
+ "}\n",
+ end_x, self->arrow_start_x, end_x, end_x);
+ }
+ else
+ {
+ css = g_strdup_printf ("* { margin-left: %dpx }", end_x);
+ }
+
+ self->provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
+ gtk_css_provider_load_from_data (GTK_CSS_PROVIDER (self->provider), css, -1, NULL);
+ gtk_style_context_add_provider (context, self->provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ g_free (css);
+}
+
+static gint
+get_last_page_number (CcCarousel *self)
+{
+ if (g_list_length (self->children) == 0)
+ return 0;
+
+ return ((g_list_length (self->children) - 1) / ITEMS_PER_PAGE);
+}
+
+static void
+update_buttons_visibility (CcCarousel *self)
+{
+ gtk_widget_set_visible (self->go_back_button, (self->visible_page > 0));
+ gtk_widget_set_visible (self->go_next_button, (self->visible_page < get_last_page_number (self)));
+}
+
+/**
+ * cc_carousel_find_item:
+ * @carousel: an CcCarousel instance
+ * @data: user data passed to the comparison function
+ * @func: the function to call for each element.
+ * It should return 0 when the desired element is found
+ *
+ * Finds an CcCarousel item using the supplied function to find the
+ * desired element.
+ * Ideally useful for matching a model object and its correspondent
+ * widget.
+ *
+ * Returns: the found CcCarouselItem, or %NULL if it is not found
+ */
+CcCarouselItem *
+cc_carousel_find_item (CcCarousel *self,
+ gconstpointer data,
+ GCompareFunc func)
+{
+ GList *list;
+
+ list = self->children;
+ while (list != NULL)
+ {
+ if (!func (list->data, data))
+ return list->data;
+ list = list->next;
+ }
+
+ return NULL;
+}
+
+static void
+on_item_toggled (CcCarousel *self,
+ GdkEvent *event,
+ CcCarouselItem *item)
+{
+ cc_carousel_select_item (self, item);
+}
+
+void
+cc_carousel_select_item (CcCarousel *self,
+ CcCarouselItem *item)
+{
+ gboolean page_changed = TRUE;
+ GList *children;
+
+ /* Select first user if none is specified */
+ if (item == NULL)
+ {
+ if (self->children != NULL)
+ item = self->children->data;
+ else
+ return;
+ }
+
+ if (self->selected_item != NULL)
+ {
+ page_changed = (self->selected_item->page != item->page);
+ self->arrow_start_x = cc_carousel_item_get_x (self->selected_item, self);
+ }
+
+ self->selected_item = item;
+ self->visible_page = item->page;
+ g_signal_emit (self, signals[ITEM_ACTIVATED], 0, item);
+
+ if (!page_changed)
+ {
+ cc_carousel_move_arrow (self);
+ return;
+ }
+
+ children = gtk_container_get_children (GTK_CONTAINER (self->stack));
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (g_list_nth_data (children, self->visible_page)));
+
+ update_buttons_visibility (self);
+
+ /* cc_carousel_move_arrow is called from on_transition_running */
+}
+
+static void
+cc_carousel_select_item_at_index (CcCarousel *self,
+ gint index)
+{
+ GList *l = NULL;
+
+ l = g_list_nth (self->children, index);
+ cc_carousel_select_item (self, l->data);
+}
+
+static void
+cc_carousel_goto_previous_page (GtkWidget *button,
+ gpointer user_data)
+{
+ CcCarousel *self = CC_CAROUSEL (user_data);
+
+ self->visible_page--;
+ if (self->visible_page < 0)
+ self->visible_page = 0;
+
+ /* Select first item of the page */
+ cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
+}
+
+static void
+cc_carousel_goto_next_page (GtkWidget *button,
+ gpointer user_data)
+{
+ CcCarousel *self = CC_CAROUSEL (user_data);
+ gint last_page;
+
+ last_page = get_last_page_number (self);
+
+ self->visible_page++;
+ if (self->visible_page > last_page)
+ self->visible_page = last_page;
+
+ /* Select first item of the page */
+ cc_carousel_select_item_at_index (self, self->visible_page * ITEMS_PER_PAGE);
+}
+
+static void
+cc_carousel_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ CcCarousel *self = CC_CAROUSEL (container);
+ gboolean last_box_is_full;
+
+ if (!CC_IS_CAROUSEL_ITEM (widget)) {
+ GTK_CONTAINER_CLASS (cc_carousel_parent_class)->add (container, widget);
+ return;
+ }
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (widget), "menu");
+ gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE);
+
+ self->children = g_list_append (self->children, widget);
+ CC_CAROUSEL_ITEM (widget)->page = get_last_page_number (self);
+ if (self->selected_item != NULL)
+ gtk_radio_button_join_group (GTK_RADIO_BUTTON (widget), GTK_RADIO_BUTTON (self->selected_item));
+ g_signal_connect_object (widget, "button-press-event", G_CALLBACK (on_item_toggled), self, G_CONNECT_SWAPPED);
+
+ last_box_is_full = ((g_list_length (self->children) - 1) % ITEMS_PER_PAGE == 0);
+ if (last_box_is_full) {
+ self->last_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show (self->last_box);
+ gtk_widget_set_valign (self->last_box, GTK_ALIGN_CENTER);
+ gtk_container_add (GTK_CONTAINER (self->stack), self->last_box);
+ }
+
+ gtk_widget_show_all (widget);
+ gtk_box_pack_start (GTK_BOX (self->last_box), widget, TRUE, FALSE, 10);
+
+ update_buttons_visibility (self);
+}
+
+void
+cc_carousel_purge_items (CcCarousel *self)
+{
+ gtk_container_forall (GTK_CONTAINER (self->stack),
+ (GtkCallback) gtk_widget_destroy,
+ NULL);
+
+ g_list_free (self->children);
+ self->children = NULL;
+ self->visible_page = 0;
+ self->selected_item = NULL;
+}
+
+CcCarousel *
+cc_carousel_new (void)
+{
+ return g_object_new (CC_TYPE_CAROUSEL, NULL);
+}
+
+static void
+cc_carousel_dispose (GObject *object)
+{
+ CcCarousel *self = CC_CAROUSEL (object);
+
+ g_clear_object (&self->provider);
+ if (self->children != NULL) {
+ g_list_free (self->children);
+ self->children = NULL;
+ }
+
+ G_OBJECT_CLASS (cc_carousel_parent_class)->dispose (object);
+}
+
+static void
+cc_carousel_class_init (CcCarouselClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (wclass,
+ "/org/gnome/control-center/user-accounts/cc-carousel.ui");
+
+ gtk_widget_class_bind_template_child (wclass, CcCarousel, stack);
+ gtk_widget_class_bind_template_child (wclass, CcCarousel, go_back_button);
+ gtk_widget_class_bind_template_child (wclass, CcCarousel, go_next_button);
+ gtk_widget_class_bind_template_child (wclass, CcCarousel, arrow);
+
+ gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_previous_page);
+ gtk_widget_class_bind_template_callback (wclass, cc_carousel_goto_next_page);
+
+ object_class->dispose = cc_carousel_dispose;
+
+ container_class->add = cc_carousel_add;
+
+ signals[ITEM_ACTIVATED] = g_signal_new ("item-activated",
+ CC_TYPE_CAROUSEL,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE, 1,
+ CC_TYPE_CAROUSEL_ITEM);
+}
+
+static void
+on_size_allocate (CcCarousel *self)
+{
+ if (self->selected_item == NULL)
+ return;
+
+ if (gtk_stack_get_transition_running (self->stack))
+ return;
+
+ self->arrow_start_x = cc_carousel_item_get_x (self->selected_item, self);
+ cc_carousel_move_arrow (self);
+}
+
+static void
+on_transition_running (CcCarousel *self)
+{
+ if (!gtk_stack_get_transition_running (self->stack))
+ cc_carousel_move_arrow (self);
+}
+
+static void
+cc_carousel_init (CcCarousel *self)
+{
+ GtkStyleProvider *provider;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
+ gtk_css_provider_load_from_resource (GTK_CSS_PROVIDER (provider),
+ "/org/gnome/control-center/user-accounts/carousel.css");
+
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ provider,
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ g_object_unref (provider);
+
+ g_signal_connect_object (self->stack, "size-allocate", G_CALLBACK (on_size_allocate), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "notify::transition-running", G_CALLBACK (on_transition_running), self, G_CONNECT_SWAPPED);
+}
+
+guint
+cc_carousel_get_item_count (CcCarousel *self)
+{
+ return g_list_length (self->children);
+}
diff --git a/panels/user-accounts/cc-carousel.h b/panels/user-accounts/cc-carousel.h
new file mode 100644
index 0000000..8cd3f9a
--- /dev/null
+++ b/panels/user-accounts/cc-carousel.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2016 (c) Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Author: Felipe Borges <felipeborges@gnome.org>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_CAROUSEL_ITEM (cc_carousel_item_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcCarouselItem, cc_carousel_item, CC, CAROUSEL_ITEM, GtkRadioButton)
+
+#define CC_TYPE_CAROUSEL (cc_carousel_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcCarousel, cc_carousel, CC, CAROUSEL, GtkRevealer)
+
+GtkWidget *cc_carousel_item_new (void);
+
+CcCarousel *cc_carousel_new (void);
+
+void cc_carousel_purge_items (CcCarousel *self);
+
+CcCarouselItem *cc_carousel_find_item (CcCarousel *self,
+ gconstpointer data,
+ GCompareFunc func);
+
+void cc_carousel_select_item (CcCarousel *self,
+ CcCarouselItem *item);
+
+guint cc_carousel_get_item_count (CcCarousel *self);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-carousel.ui b/panels/user-accounts/cc-carousel.ui
new file mode 100644
index 0000000..77ba44b
--- /dev/null
+++ b/panels/user-accounts/cc-carousel.ui
@@ -0,0 +1,118 @@
+<?xml version="1.0"?>
+<interface>
+ <!-- interface-requires gtk+ 3.8 -->
+ <template class="CcCarousel" parent="GtkRevealer">
+ <property name="transition_duration">400</property>
+ <property name="reveal-child">True</property>
+ <child>
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="border_width">16</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="transition_duration">400</property>
+ <property name="transition_type">GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT</property>
+ <style>
+ <class name="location-bar"/>
+ </style>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
+ <property name="border_width">12</property>
+ <child>
+ <object class="GtkButton" id="go_back_button">
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="circular"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon-size">4</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ </object>
+ </child>
+ <signal name="clicked" handler="cc_carousel_goto_previous_page" object="CcCarousel" swapped="no"/>
+ </object>
+ <packing>
+ <property name="pack_type">GTK_PACK_START</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="go_next_button">
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="circular"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon-size">4</property>
+ <property name="icon_name">go-next-symbolic</property>
+ </object>
+ </child>
+ <signal name="clicked" handler="cc_carousel_goto_next_page" object="CcCarousel" swapped="no"/>
+ </object>
+ <packing>
+ <property name="pack_type">GTK_PACK_END</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="valign">GTK_ALIGN_END</property>
+ <style>
+ <class name="carousel-arrow-container"/>
+ </style>
+ <child>
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="arrow">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="carousel-arrow"/>
+ </style>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="carousel-inner-arrow"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pass-through">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="pass-through">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/user-accounts/cc-crop-area.c b/panels/user-accounts/cc-crop-area.c
new file mode 100644
index 0000000..c4a04a6
--- /dev/null
+++ b/panels/user-accounts/cc-crop-area.c
@@ -0,0 +1,819 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "cc-crop-area.h"
+
+struct _CcCropArea {
+ GtkDrawingArea parent_instance;
+
+ GdkPixbuf *browse_pixbuf;
+ GdkPixbuf *pixbuf;
+ GdkPixbuf *color_shifted;
+ gdouble scale;
+ GdkRectangle image;
+ GdkCursorType current_cursor;
+ GdkRectangle crop;
+ gint active_region;
+ gint last_press_x;
+ gint last_press_y;
+ gint base_width;
+ gint base_height;
+ gdouble aspect;
+};
+
+G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_DRAWING_AREA);
+
+static inline guchar
+shift_color_byte (guchar b,
+ int shift)
+{
+ return CLAMP(b + shift, 0, 255);
+}
+
+static void
+shift_colors (GdkPixbuf *pixbuf,
+ gint red,
+ gint green,
+ gint blue,
+ gint alpha)
+{
+ gint x, y, offset, y_offset, rowstride, width, height;
+ guchar *pixels;
+ gint channels;
+
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+ rowstride = gdk_pixbuf_get_rowstride (pixbuf);
+ pixels = gdk_pixbuf_get_pixels (pixbuf);
+ channels = gdk_pixbuf_get_n_channels (pixbuf);
+
+ for (y = 0; y < height; y++) {
+ y_offset = y * rowstride;
+ for (x = 0; x < width; x++) {
+ offset = y_offset + x * channels;
+ if (red != 0)
+ pixels[offset] = shift_color_byte (pixels[offset], red);
+ if (green != 0)
+ pixels[offset + 1] = shift_color_byte (pixels[offset + 1], green);
+ if (blue != 0)
+ pixels[offset + 2] = shift_color_byte (pixels[offset + 2], blue);
+ if (alpha != 0 && channels >= 4)
+ pixels[offset + 3] = shift_color_byte (pixels[offset + 3], blue);
+ }
+ }
+}
+
+static void
+update_pixbufs (CcCropArea *area)
+{
+ gint width;
+ gint height;
+ GtkAllocation allocation;
+ gdouble scale;
+ gint dest_width, dest_height;
+ GtkWidget *widget;
+
+ widget = GTK_WIDGET (area);
+ gtk_widget_get_allocation (widget, &allocation);
+
+ width = gdk_pixbuf_get_width (area->browse_pixbuf);
+ height = gdk_pixbuf_get_height (area->browse_pixbuf);
+
+ scale = allocation.height / (gdouble)height;
+ if (scale * width > allocation.width)
+ scale = allocation.width / (gdouble)width;
+
+ dest_width = width * scale;
+ dest_height = height * scale;
+
+ if (area->pixbuf == NULL ||
+ gdk_pixbuf_get_width (area->pixbuf) != allocation.width ||
+ gdk_pixbuf_get_height (area->pixbuf) != allocation.height) {
+ g_clear_object (&area->pixbuf);
+ area->pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
+ gdk_pixbuf_get_has_alpha (area->browse_pixbuf),
+ 8,
+ dest_width, dest_height);
+ gdk_pixbuf_fill (area->pixbuf, 0x0);
+
+ gdk_pixbuf_scale (area->browse_pixbuf,
+ area->pixbuf,
+ 0, 0,
+ dest_width, dest_height,
+ 0, 0,
+ scale, scale,
+ GDK_INTERP_BILINEAR);
+
+ g_clear_object (&area->color_shifted);
+ area->color_shifted = gdk_pixbuf_copy (area->pixbuf);
+ shift_colors (area->color_shifted, -32, -32, -32, 0);
+
+ if (area->scale == 0.0) {
+ gdouble scale_to_80, scale_to_image, crop_scale;
+
+ /* Scale the crop rectangle to 80% of the area, or less to fit the image */
+ scale_to_80 = MIN ((gdouble)gdk_pixbuf_get_width (area->pixbuf) * 0.8 / area->base_width,
+ (gdouble)gdk_pixbuf_get_height (area->pixbuf) * 0.8 / area->base_height);
+ scale_to_image = MIN ((gdouble)dest_width / area->base_width,
+ (gdouble)dest_height / area->base_height);
+ crop_scale = MIN (scale_to_80, scale_to_image);
+
+ area->crop.width = crop_scale * area->base_width / scale;
+ area->crop.height = crop_scale * area->base_height / scale;
+ area->crop.x = (gdk_pixbuf_get_width (area->browse_pixbuf) - area->crop.width) / 2;
+ area->crop.y = (gdk_pixbuf_get_height (area->browse_pixbuf) - area->crop.height) / 2;
+ }
+
+ area->scale = scale;
+ area->image.x = (allocation.width - dest_width) / 2;
+ area->image.y = (allocation.height - dest_height) / 2;
+ area->image.width = dest_width;
+ area->image.height = dest_height;
+ }
+}
+
+static void
+crop_to_widget (CcCropArea *area,
+ GdkRectangle *crop)
+{
+ crop->x = area->image.x + area->crop.x * area->scale;
+ crop->y = area->image.y + area->crop.y * area->scale;
+ crop->width = area->crop.width * area->scale;
+ crop->height = area->crop.height * area->scale;
+}
+
+typedef enum {
+ OUTSIDE,
+ INSIDE,
+ TOP,
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT,
+ LEFT,
+ RIGHT
+} Location;
+
+static gboolean
+cc_crop_area_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GdkRectangle crop;
+ gint width, height, ix, iy;
+ CcCropArea *uarea = CC_CROP_AREA (widget);
+
+ if (uarea->browse_pixbuf == NULL)
+ return FALSE;
+
+ update_pixbufs (uarea);
+
+ width = gdk_pixbuf_get_width (uarea->pixbuf);
+ height = gdk_pixbuf_get_height (uarea->pixbuf);
+ crop_to_widget (uarea, &crop);
+
+ ix = uarea->image.x;
+ iy = uarea->image.y;
+
+ gdk_cairo_set_source_pixbuf (cr, uarea->color_shifted, ix, iy);
+ cairo_rectangle (cr, ix, iy, width, crop.y - iy);
+ cairo_rectangle (cr, ix, crop.y, crop.x - ix, crop.height);
+ cairo_rectangle (cr, crop.x + crop.width, crop.y, width - crop.width - (crop.x - ix), crop.height);
+ cairo_rectangle (cr, ix, crop.y + crop.height, width, height - crop.height - (crop.y - iy));
+ cairo_fill (cr);
+
+ gdk_cairo_set_source_pixbuf (cr, uarea->pixbuf, ix, iy);
+ cairo_rectangle (cr, crop.x, crop.y, crop.width, crop.height);
+ cairo_fill (cr);
+
+ if (uarea->active_region != OUTSIDE) {
+ gint x1, x2, y1, y2;
+ cairo_set_source_rgb (cr, 1, 1, 1);
+ cairo_set_line_width (cr, 1.0);
+ x1 = crop.x + crop.width / 3.0;
+ x2 = crop.x + 2 * crop.width / 3.0;
+ y1 = crop.y + crop.height / 3.0;
+ y2 = crop.y + 2 * crop.height / 3.0;
+
+ cairo_move_to (cr, x1 + 0.5, crop.y);
+ cairo_line_to (cr, x1 + 0.5, crop.y + crop.height);
+
+ cairo_move_to (cr, x2 + 0.5, crop.y);
+ cairo_line_to (cr, x2 + 0.5, crop.y + crop.height);
+
+ cairo_move_to (cr, crop.x, y1 + 0.5);
+ cairo_line_to (cr, crop.x + crop.width, y1 + 0.5);
+
+ cairo_move_to (cr, crop.x, y2 + 0.5);
+ cairo_line_to (cr, crop.x + crop.width, y2 + 0.5);
+ cairo_stroke (cr);
+ }
+
+ cairo_set_source_rgb (cr, 0, 0, 0);
+ cairo_set_line_width (cr, 1.0);
+ cairo_rectangle (cr,
+ crop.x + 0.5,
+ crop.y + 0.5,
+ crop.width - 1.0,
+ crop.height - 1.0);
+ cairo_stroke (cr);
+
+ cairo_set_source_rgb (cr, 1, 1, 1);
+ cairo_set_line_width (cr, 2.0);
+ cairo_rectangle (cr,
+ crop.x + 2.0,
+ crop.y + 2.0,
+ crop.width - 4.0,
+ crop.height - 4.0);
+ cairo_stroke (cr);
+
+ return FALSE;
+}
+
+typedef enum {
+ BELOW,
+ LOWER,
+ BETWEEN,
+ UPPER,
+ ABOVE
+} Range;
+
+static Range
+find_range (gint x,
+ gint min,
+ gint max)
+{
+ gint tolerance = 12;
+
+ if (x < min - tolerance)
+ return BELOW;
+ if (x <= min + tolerance)
+ return LOWER;
+ if (x < max - tolerance)
+ return BETWEEN;
+ if (x <= max + tolerance)
+ return UPPER;
+ return ABOVE;
+}
+
+static Location
+find_location (GdkRectangle *rect,
+ gint x,
+ gint y)
+{
+ Range x_range, y_range;
+ Location location[5][5] = {
+ { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE },
+ { OUTSIDE, TOP_LEFT, TOP, TOP_RIGHT, OUTSIDE },
+ { OUTSIDE, LEFT, INSIDE, RIGHT, OUTSIDE },
+ { OUTSIDE, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, OUTSIDE },
+ { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE }
+ };
+
+ x_range = find_range (x, rect->x, rect->x + rect->width);
+ y_range = find_range (y, rect->y, rect->y + rect->height);
+
+ return location[y_range][x_range];
+}
+
+static void
+update_cursor (CcCropArea *area,
+ gint x,
+ gint y)
+{
+ gint cursor_type;
+ GdkRectangle crop;
+ gint region;
+
+ region = area->active_region;
+ if (region == OUTSIDE) {
+ crop_to_widget (area, &crop);
+ region = find_location (&crop, x, y);
+ }
+
+ switch (region) {
+ case OUTSIDE:
+ cursor_type = GDK_LEFT_PTR;
+ break;
+ case TOP_LEFT:
+ cursor_type = GDK_TOP_LEFT_CORNER;
+ break;
+ case TOP:
+ cursor_type = GDK_TOP_SIDE;
+ break;
+ case TOP_RIGHT:
+ cursor_type = GDK_TOP_RIGHT_CORNER;
+ break;
+ case LEFT:
+ cursor_type = GDK_LEFT_SIDE;
+ break;
+ case INSIDE:
+ cursor_type = GDK_FLEUR;
+ break;
+ case RIGHT:
+ cursor_type = GDK_RIGHT_SIDE;
+ break;
+ case BOTTOM_LEFT:
+ cursor_type = GDK_BOTTOM_LEFT_CORNER;
+ break;
+ case BOTTOM:
+ cursor_type = GDK_BOTTOM_SIDE;
+ break;
+ case BOTTOM_RIGHT:
+ cursor_type = GDK_BOTTOM_RIGHT_CORNER;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (cursor_type != area->current_cursor) {
+ GdkCursor *cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (area)),
+ cursor_type);
+ gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (area)), cursor);
+ g_object_unref (cursor);
+ area->current_cursor = cursor_type;
+ }
+}
+
+static int
+eval_radial_line (gdouble center_x, gdouble center_y,
+ gdouble bounds_x, gdouble bounds_y,
+ gdouble user_x)
+{
+ gdouble decision_slope;
+ gdouble decision_intercept;
+
+ decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
+ decision_intercept = -(decision_slope * bounds_x);
+
+ return (int) (decision_slope * user_x + decision_intercept);
+}
+
+static gboolean
+cc_crop_area_motion_notify_event (GtkWidget *widget,
+ GdkEventMotion *event)
+{
+ CcCropArea *area = CC_CROP_AREA (widget);
+ gint x, y;
+ gint delta_x, delta_y;
+ gint width, height;
+ gint adj_width, adj_height;
+ gint pb_width, pb_height;
+ GdkRectangle damage;
+ gint left, right, top, bottom;
+ gdouble new_width, new_height;
+ gdouble center_x, center_y;
+ gint min_width, min_height;
+
+ if (area->browse_pixbuf == NULL)
+ return FALSE;
+
+ update_cursor (area, event->x, event->y);
+
+ crop_to_widget (area, &damage);
+ gtk_widget_queue_draw_area (widget,
+ damage.x - 1, damage.y - 1,
+ damage.width + 2, damage.height + 2);
+
+ pb_width = gdk_pixbuf_get_width (area->browse_pixbuf);
+ pb_height = gdk_pixbuf_get_height (area->browse_pixbuf);
+
+ x = (event->x - area->image.x) / area->scale;
+ y = (event->y - area->image.y) / area->scale;
+
+ delta_x = x - area->last_press_x;
+ delta_y = y - area->last_press_y;
+ area->last_press_x = x;
+ area->last_press_y = y;
+
+ left = area->crop.x;
+ right = area->crop.x + area->crop.width - 1;
+ top = area->crop.y;
+ bottom = area->crop.y + area->crop.height - 1;
+
+ center_x = (left + right) / 2.0;
+ center_y = (top + bottom) / 2.0;
+
+ switch (area->active_region) {
+ case INSIDE:
+ width = right - left + 1;
+ height = bottom - top + 1;
+
+ left += delta_x;
+ right += delta_x;
+ top += delta_y;
+ bottom += delta_y;
+
+ if (left < 0)
+ left = 0;
+ if (top < 0)
+ top = 0;
+ if (right > pb_width)
+ right = pb_width;
+ if (bottom > pb_height)
+ bottom = pb_height;
+
+ adj_width = right - left + 1;
+ adj_height = bottom - top + 1;
+ if (adj_width != width) {
+ if (delta_x < 0)
+ right = left + width - 1;
+ else
+ left = right - width + 1;
+ }
+ if (adj_height != height) {
+ if (delta_y < 0)
+ bottom = top + height - 1;
+ else
+ top = bottom - height + 1;
+ }
+
+ break;
+
+ case TOP_LEFT:
+ if (area->aspect < 0) {
+ top = y;
+ left = x;
+ }
+ else if (y < eval_radial_line (center_x, center_y, left, top, x)) {
+ top = y;
+ new_width = (bottom - top) * area->aspect;
+ left = right - new_width;
+ }
+ else {
+ left = x;
+ new_height = (right - left) / area->aspect;
+ top = bottom - new_height;
+ }
+ break;
+
+ case TOP:
+ top = y;
+ if (area->aspect > 0) {
+ new_width = (bottom - top) * area->aspect;
+ right = left + new_width;
+ }
+ break;
+
+ case TOP_RIGHT:
+ if (area->aspect < 0) {
+ top = y;
+ right = x;
+ }
+ else if (y < eval_radial_line (center_x, center_y, right, top, x)) {
+ top = y;
+ new_width = (bottom - top) * area->aspect;
+ right = left + new_width;
+ }
+ else {
+ right = x;
+ new_height = (right - left) / area->aspect;
+ top = bottom - new_height;
+ }
+ break;
+
+ case LEFT:
+ left = x;
+ if (area->aspect > 0) {
+ new_height = (right - left) / area->aspect;
+ bottom = top + new_height;
+ }
+ break;
+
+ case BOTTOM_LEFT:
+ if (area->aspect < 0) {
+ bottom = y;
+ left = x;
+ }
+ else if (y < eval_radial_line (center_x, center_y, left, bottom, x)) {
+ left = x;
+ new_height = (right - left) / area->aspect;
+ bottom = top + new_height;
+ }
+ else {
+ bottom = y;
+ new_width = (bottom - top) * area->aspect;
+ left = right - new_width;
+ }
+ break;
+
+ case RIGHT:
+ right = x;
+ if (area->aspect > 0) {
+ new_height = (right - left) / area->aspect;
+ bottom = top + new_height;
+ }
+ break;
+
+ case BOTTOM_RIGHT:
+ if (area->aspect < 0) {
+ bottom = y;
+ right = x;
+ }
+ else if (y < eval_radial_line (center_x, center_y, right, bottom, x)) {
+ right = x;
+ new_height = (right - left) / area->aspect;
+ bottom = top + new_height;
+ }
+ else {
+ bottom = y;
+ new_width = (bottom - top) * area->aspect;
+ right = left + new_width;
+ }
+ break;
+
+ case BOTTOM:
+ bottom = y;
+ if (area->aspect > 0) {
+ new_width = (bottom - top) * area->aspect;
+ right= left + new_width;
+ }
+ break;
+
+ default:
+ return FALSE;
+ }
+
+ min_width = area->base_width / area->scale;
+ min_height = area->base_height / area->scale;
+
+ width = right - left + 1;
+ height = bottom - top + 1;
+ if (area->aspect < 0) {
+ if (left < 0)
+ left = 0;
+ if (top < 0)
+ top = 0;
+ if (right > pb_width)
+ right = pb_width;
+ if (bottom > pb_height)
+ bottom = pb_height;
+
+ width = right - left + 1;
+ height = bottom - top + 1;
+
+ switch (area->active_region) {
+ case LEFT:
+ case TOP_LEFT:
+ case BOTTOM_LEFT:
+ if (width < min_width)
+ left = right - min_width;
+ break;
+ case RIGHT:
+ case TOP_RIGHT:
+ case BOTTOM_RIGHT:
+ if (width < min_width)
+ right = left + min_width;
+ break;
+
+ default: ;
+ }
+
+ switch (area->active_region) {
+ case TOP:
+ case TOP_LEFT:
+ case TOP_RIGHT:
+ if (height < min_height)
+ top = bottom - min_height;
+ break;
+ case BOTTOM:
+ case BOTTOM_LEFT:
+ case BOTTOM_RIGHT:
+ if (height < min_height)
+ bottom = top + min_height;
+ break;
+
+ default: ;
+ }
+ }
+ else {
+ if (left < 0 || top < 0 ||
+ right > pb_width || bottom > pb_height ||
+ width < min_width || height < min_height) {
+ left = area->crop.x;
+ right = area->crop.x + area->crop.width - 1;
+ top = area->crop.y;
+ bottom = area->crop.y + area->crop.height - 1;
+ }
+ }
+
+ area->crop.x = left;
+ area->crop.y = top;
+ area->crop.width = right - left + 1;
+ area->crop.height = bottom - top + 1;
+
+ crop_to_widget (area, &damage);
+ gtk_widget_queue_draw_area (widget,
+ damage.x - 1, damage.y - 1,
+ damage.width + 2, damage.height + 2);
+
+ return FALSE;
+}
+
+static gboolean
+cc_crop_area_button_press_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ CcCropArea *area = CC_CROP_AREA (widget);
+ GdkRectangle crop;
+
+ if (area->browse_pixbuf == NULL)
+ return FALSE;
+
+ crop_to_widget (area, &crop);
+
+ area->last_press_x = (event->x - area->image.x) / area->scale;
+ area->last_press_y = (event->y - area->image.y) / area->scale;
+ area->active_region = find_location (&crop, event->x, event->y);
+
+ gtk_widget_queue_draw_area (widget,
+ crop.x - 1, crop.y - 1,
+ crop.width + 2, crop.height + 2);
+
+ return FALSE;
+}
+
+static gboolean
+cc_crop_area_button_release_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ CcCropArea *area = CC_CROP_AREA (widget);
+ GdkRectangle crop;
+
+ if (area->browse_pixbuf == NULL)
+ return FALSE;
+
+ crop_to_widget (area, &crop);
+
+ area->last_press_x = -1;
+ area->last_press_y = -1;
+ area->active_region = OUTSIDE;
+
+ gtk_widget_queue_draw_area (widget,
+ crop.x - 1, crop.y - 1,
+ crop.width + 2, crop.height + 2);
+
+ return FALSE;
+}
+
+static void
+cc_crop_area_set_size_request (CcCropArea *area)
+{
+ gtk_widget_set_size_request (GTK_WIDGET (area),
+ area->base_width,
+ area->base_height);
+}
+
+static void
+cc_crop_area_finalize (GObject *object)
+{
+ CcCropArea *area = CC_CROP_AREA (object);
+
+ g_clear_object (&area->browse_pixbuf);
+ g_clear_object (&area->pixbuf);
+ g_clear_object (&area->color_shifted);
+
+ G_OBJECT_CLASS (cc_crop_area_parent_class)->finalize (object);
+}
+
+static void
+cc_crop_area_class_init (CcCropAreaClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = cc_crop_area_finalize;
+ widget_class->draw = cc_crop_area_draw;
+ widget_class->button_press_event = cc_crop_area_button_press_event;
+ widget_class->button_release_event = cc_crop_area_button_release_event;
+ widget_class->motion_notify_event = cc_crop_area_motion_notify_event;
+}
+
+static void
+cc_crop_area_init (CcCropArea *area)
+{
+ gtk_widget_add_events (GTK_WIDGET (area), GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK);
+
+ area->scale = 0.0;
+ area->image.x = 0;
+ area->image.y = 0;
+ area->image.width = 0;
+ area->image.height = 0;
+ area->active_region = OUTSIDE;
+ area->base_width = 48;
+ area->base_height = 48;
+ area->aspect = 1;
+
+ cc_crop_area_set_size_request (area);
+}
+
+GtkWidget *
+cc_crop_area_new (void)
+{
+ return g_object_new (CC_TYPE_CROP_AREA, NULL);
+}
+
+GdkPixbuf *
+cc_crop_area_get_picture (CcCropArea *area)
+{
+ gint width, height;
+
+ width = gdk_pixbuf_get_width (area->browse_pixbuf);
+ height = gdk_pixbuf_get_height (area->browse_pixbuf);
+ width = MIN (area->crop.width, width - area->crop.x);
+ height = MIN (area->crop.height, height - area->crop.y);
+
+ return gdk_pixbuf_new_subpixbuf (area->browse_pixbuf,
+ area->crop.x,
+ area->crop.y,
+ width, height);
+}
+
+void
+cc_crop_area_set_picture (CcCropArea *area,
+ GdkPixbuf *pixbuf)
+{
+ int width;
+ int height;
+
+ if (area->browse_pixbuf) {
+ g_object_unref (area->browse_pixbuf);
+ area->browse_pixbuf = NULL;
+ }
+ if (pixbuf) {
+ area->browse_pixbuf = g_object_ref (pixbuf);
+ width = gdk_pixbuf_get_width (pixbuf);
+ height = gdk_pixbuf_get_height (pixbuf);
+ } else {
+ width = 0;
+ height = 0;
+ }
+
+ area->crop.width = 2 * area->base_width;
+ area->crop.height = 2 * area->base_height;
+ area->crop.x = (width - area->crop.width) / 2;
+ area->crop.y = (height - area->crop.height) / 2;
+
+ area->scale = 0.0;
+ area->image.x = 0;
+ area->image.y = 0;
+ area->image.width = 0;
+ area->image.height = 0;
+
+ gtk_widget_queue_draw (GTK_WIDGET (area));
+}
+
+void
+cc_crop_area_set_min_size (CcCropArea *area,
+ gint width,
+ gint height)
+{
+ area->base_width = width;
+ area->base_height = height;
+
+ cc_crop_area_set_size_request (area);
+
+ if (area->aspect > 0) {
+ area->aspect = area->base_width / (gdouble)area->base_height;
+ }
+}
+
+void
+cc_crop_area_set_constrain_aspect (CcCropArea *area,
+ gboolean constrain)
+{
+ if (constrain) {
+ area->aspect = area->base_width / (gdouble)area->base_height;
+ }
+ else {
+ area->aspect = -1;
+ }
+}
+
diff --git a/panels/user-accounts/cc-crop-area.h b/panels/user-accounts/cc-crop-area.h
new file mode 100644
index 0000000..d2bfab5
--- /dev/null
+++ b/panels/user-accounts/cc-crop-area.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2009 Bastien Nocera <hadess@hadess.net>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_CROP_AREA (cc_crop_area_get_type ())
+G_DECLARE_FINAL_TYPE (CcCropArea, cc_crop_area, CC, CROP_AREA, GtkDrawingArea)
+
+GtkWidget *cc_crop_area_new (void);
+GdkPixbuf *cc_crop_area_get_picture (CcCropArea *area);
+void cc_crop_area_set_picture (CcCropArea *area,
+ GdkPixbuf *pixbuf);
+void cc_crop_area_set_min_size (CcCropArea *area,
+ gint width,
+ gint height);
+void cc_crop_area_set_constrain_aspect (CcCropArea *area,
+ gboolean constrain);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-fingerprint-dialog.c b/panels/user-accounts/cc-fingerprint-dialog.c
new file mode 100644
index 0000000..b7894c0
--- /dev/null
+++ b/panels/user-accounts/cc-fingerprint-dialog.c
@@ -0,0 +1,1454 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authors: Marco Trevisan <marco.trevisan@canonical.com>
+ */
+
+#include <glib/gi18n.h>
+#include <cairo/cairo.h>
+
+#include "cc-fingerprint-dialog.h"
+
+#include "cc-fingerprint-manager.h"
+#include "cc-fprintd-generated.h"
+#include "cc-list-row.h"
+
+#include "config.h"
+
+/* Translate fprintd strings */
+#define TR(s) dgettext ("fprintd", s)
+#include "fingerprint-strings.h"
+
+struct _CcFingerprintDialog
+{
+ GtkWindow parent_instance;
+
+ GtkButton *back_button;
+ GtkButton *cancel_button;
+ GtkButton *delete_prints_button;
+ GtkButton *done_button;
+ GtkContainer *add_print_popover_box;
+ GtkEntry *enroll_print_entry;
+ GtkFlowBox *prints_gallery;
+ GtkHeaderBar *titlebar;
+ GtkImage *enroll_result_image;
+ GtkLabel *enroll_message;
+ GtkLabel *enroll_result_message;
+ GtkLabel *infobar_error;
+ GtkLabel *title;
+ GtkListBox *devices_list;
+ GtkPopoverMenu *add_print_popover;
+ GtkPopoverMenu *print_popover;
+ GtkSpinner *spinner;
+ GtkStack *stack;
+ GtkWidget *add_print_icon;
+ GtkWidget *delete_confirmation_infobar;
+ GtkWidget *device_selector;
+ GtkWidget *enroll_print_bin;
+ GtkWidget *enroll_result_icon;
+ GtkWidget *enrollment_view;
+ GtkWidget *error_infobar;
+ GtkWidget *no_devices_found;
+ GtkWidget *prints_manager;
+
+ CcFingerprintManager *manager;
+ CcFprintdDevice *device;
+ gboolean claiming;
+ gboolean device_claimed;
+ gulong device_signal_id;
+ gulong device_name_owner_id;
+ GCancellable *cancellable;
+ GStrv enrolled_fingers;
+ const char *enrolling_finger;
+ guint enroll_stages_passed;
+ guint enroll_stage_passed_id;
+ gdouble enroll_progress;
+};
+
+/* TODO - fprintd and API changes required:
+ - Identify the finger when the enroll dialog is visible
+ + Only if device supports identification
+ · And only in such case support enrolling more than one finger
+ - Delete a single fingerprint | and remove the "Delete all" button
+ - Highlight the finger when the sensor is touched during enrollment
+ - Add customized labels to fingerprints
+ - Devices hotplug (object manager)
+ */
+
+G_DEFINE_TYPE (CcFingerprintDialog, cc_fingerprint_dialog, GTK_TYPE_WINDOW)
+
+enum {
+ PROP_0,
+ PROP_MANAGER,
+ N_PROPS
+};
+
+#define N_VALID_FINGERS G_N_ELEMENTS (FINGER_IDS) - 1
+/* The order of the fingers here will affect the UI order */
+const char * FINGER_IDS[] = {
+ "right-index-finger",
+ "left-index-finger",
+ "right-thumb",
+ "right-middle-finger",
+ "right-ring-finger",
+ "right-little-finger",
+ "left-thumb",
+ "left-middle-finger",
+ "left-ring-finger",
+ "left-little-finger",
+ "any",
+};
+
+typedef enum {
+ ENROLL_STATE_NORMAL,
+ ENROLL_STATE_RETRY,
+ ENROLL_STATE_SUCCESS,
+ ENROLL_STATE_WARNING,
+ ENROLL_STATE_ERROR,
+ ENROLL_STATE_COMPLETED,
+ N_ENROLL_STATES,
+} EnrollState;
+
+const char * ENROLL_STATE_CLASSES[N_ENROLL_STATES] = {
+ "",
+ "retry",
+ "success",
+ "warning",
+ "error",
+ "completed",
+};
+
+static GParamSpec *properties[N_PROPS];
+
+CcFingerprintDialog *
+cc_fingerprint_dialog_new (CcFingerprintManager *manager)
+{
+ return g_object_new (CC_TYPE_FINGERPRINT_DIALOG,
+ "fingerprint-manager", manager,
+ NULL);
+}
+
+static void
+disconnect_device_signals (CcFingerprintDialog *self)
+{
+ if (!self->device)
+ return;
+
+ if (self->device_signal_id)
+ {
+ g_signal_handler_disconnect (self->device, self->device_signal_id);
+ self->device_signal_id = 0;
+ }
+
+ if (self->device_name_owner_id)
+ {
+ g_signal_handler_disconnect (self->device, self->device_name_owner_id);
+ self->device_name_owner_id = 0;
+ }
+}
+
+static void
+cc_fingerprint_dialog_dispose (GObject *object)
+{
+ CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object);
+
+ g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove);
+
+ if (self->device && self->device_claimed)
+ {
+ disconnect_device_signals (self);
+
+ if (self->enrolling_finger)
+ cc_fprintd_device_call_enroll_stop_sync (self->device, NULL, NULL);
+ cc_fprintd_device_call_release (self->device, NULL, NULL, NULL);
+ }
+
+ g_clear_object (&self->manager);
+ g_clear_object (&self->device);
+ g_clear_pointer (&self->enrolled_fingers, g_strfreev);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ G_OBJECT_CLASS (cc_fingerprint_dialog_parent_class)->dispose (object);
+}
+
+static void
+cc_fingerprint_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_value_set_object (value, self->manager);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_fingerprint_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_MANAGER:
+ g_set_object (&self->manager, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+notify_error (CcFingerprintDialog *self,
+ const char *error_message)
+{
+ if (error_message)
+ gtk_label_set_label (self->infobar_error, error_message);
+
+ gtk_widget_set_visible (self->error_infobar, error_message != NULL);
+}
+
+static gboolean
+fingerprint_icon_draw (GtkWidget *widget,
+ cairo_t *cr,
+ gdouble *progress_data)
+{
+ gdouble progress = 0.0f;
+
+ if (progress_data)
+ progress = *progress_data;
+
+ if (G_APPROX_VALUE (progress, 0.f, FLT_EPSILON) || progress > 1)
+ return FALSE;
+
+ GTK_WIDGET_GET_CLASS (widget)->draw (widget, cr);
+
+ if (progress > 0)
+ {
+ g_autoptr(GdkRGBA) outline_color = NULL;
+ GtkStyleContext *context;
+ GtkStateFlags state;
+ int outline_width;
+ int outline_offset;
+ int width;
+ int height;
+ int radius;
+ int delta;
+
+ context = gtk_widget_get_style_context (widget);
+ gtk_style_context_save (context);
+
+ state = gtk_style_context_get_state (context);
+
+ gtk_style_context_add_class (context, "progress");
+ gtk_style_context_get (context, state,
+ "outline-width", &outline_width,
+ "outline-offset", &outline_offset,
+ "outline-color", &outline_color,
+ NULL);
+
+ width = gtk_widget_get_allocated_width (widget);
+ height = gtk_widget_get_allocated_height (widget);
+ radius = MIN (width / 2, height / 2) + outline_offset;
+ delta = radius - outline_width / 2;
+
+ cairo_arc (cr, width / 2., height / 2., delta,
+ 1.5 * G_PI, (1.5 + progress * 2) * G_PI);
+ gdk_cairo_set_source_rgba (cr, outline_color);
+
+ cairo_set_line_width (cr, MIN (outline_width, radius));
+ cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT);
+ cairo_stroke (cr);
+
+ gtk_style_context_restore (context);
+ }
+
+ return TRUE;
+}
+
+static GtkWidget *
+fingerprint_icon_new (const char *icon_name,
+ const char *label_text,
+ GType icon_widget_type,
+ gpointer progress_data,
+ GtkWidget **out_icon,
+ GtkWidget **out_label)
+{
+ GtkStyleContext *context;
+ GtkWidget *box;
+ GtkWidget *label;
+ GtkWidget *image;
+ GtkWidget *icon_widget;
+
+ g_return_val_if_fail (g_type_is_a (icon_widget_type, GTK_TYPE_WIDGET), NULL);
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+ gtk_widget_set_name (box, "fingerprint-box");
+ gtk_widget_set_hexpand (box, TRUE);
+
+ image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_DND);
+
+ if (icon_widget_type == GTK_TYPE_IMAGE)
+ icon_widget = image;
+ else
+ icon_widget = g_object_new (icon_widget_type, NULL);
+
+ if (progress_data)
+ g_signal_connect (image, "draw", G_CALLBACK (fingerprint_icon_draw),
+ progress_data);
+
+ if (g_type_is_a (icon_widget_type, GTK_TYPE_BUTTON))
+ {
+ gtk_button_set_image (GTK_BUTTON (icon_widget), image);
+ gtk_button_set_relief (GTK_BUTTON (icon_widget), GTK_RELIEF_NONE);
+ gtk_widget_set_can_focus (icon_widget, FALSE);
+ }
+
+ gtk_widget_set_halign (icon_widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign (icon_widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_name (icon_widget, "fingerprint-image");
+
+ gtk_container_add (GTK_CONTAINER (box), icon_widget);
+
+ context = gtk_widget_get_style_context (icon_widget);
+ gtk_style_context_add_class (context, "fingerprint-image");
+
+ label = gtk_label_new_with_mnemonic (label_text);
+ gtk_container_add (GTK_CONTAINER (box), label);
+
+ context = gtk_widget_get_style_context (box);
+ gtk_style_context_add_class (context, "fingerprint-icon");
+
+ if (out_icon)
+ *out_icon = icon_widget;
+
+ if (out_label)
+ *out_label = label;
+
+ return box;
+}
+
+static GtkWidget *
+fingerprint_menu_button (const char *icon_name,
+ const char *label_text)
+{
+ GtkWidget *flowbox_child;
+ GtkWidget *button;
+ GtkWidget *label;
+ GtkWidget *box;
+
+ box = fingerprint_icon_new (icon_name, label_text, GTK_TYPE_MENU_BUTTON, NULL,
+ &button, &label);
+
+ flowbox_child = gtk_flow_box_child_new ();
+ gtk_widget_set_focus_on_click (flowbox_child, FALSE);
+ gtk_widget_set_name (flowbox_child, "fingerprint-flowbox");
+
+ gtk_container_add (GTK_CONTAINER (flowbox_child), box);
+
+ g_object_set_data (G_OBJECT (flowbox_child), "button", button);
+ g_object_set_data (G_OBJECT (flowbox_child), "icon",
+ gtk_button_get_image (GTK_BUTTON (button)));
+ g_object_set_data (G_OBJECT (flowbox_child), "label", label);
+ g_object_set_data (G_OBJECT (button), "flowbox-child", flowbox_child);
+
+ return flowbox_child;
+}
+
+static gboolean
+prints_visibility_filter (GtkFlowBoxChild *child,
+ gpointer user_data)
+{
+ CcFingerprintDialog *self = user_data;
+ const char *finger_id;
+
+ if (gtk_stack_get_visible_child (self->stack) != self->prints_manager)
+ return FALSE;
+
+ finger_id = g_object_get_data (G_OBJECT (child), "finger-id");
+
+ if (!finger_id)
+ return TRUE;
+
+ if (!self->enrolled_fingers)
+ return FALSE;
+
+ return g_strv_contains ((const gchar **) self->enrolled_fingers, finger_id);
+}
+
+static void
+update_prints_to_add_visibility (CcFingerprintDialog *self)
+{
+ g_autoptr(GList) print_buttons = NULL;
+ GList *l;
+ guint i;
+
+ print_buttons = gtk_container_get_children (self->add_print_popover_box);
+
+ for (i = 0, l = print_buttons; i < N_VALID_FINGERS && l; ++i, l = l->next)
+ {
+ GtkWidget *button = l->data;
+ gboolean enrolled;
+
+ enrolled = self->enrolled_fingers &&
+ g_strv_contains ((const gchar **) self->enrolled_fingers,
+ FINGER_IDS[i]);
+
+ gtk_widget_set_visible (button, !enrolled);
+ }
+}
+
+static void
+update_prints_visibility (CcFingerprintDialog *self)
+{
+ update_prints_to_add_visibility (self);
+
+ gtk_flow_box_invalidate_filter (self->prints_gallery);
+}
+
+static void
+list_enrolled_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_auto(GStrv) enrolled_fingers = NULL;
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+ guint n_enrolled_fingers = 0;
+
+ cc_fprintd_device_call_list_enrolled_fingers_finish (fprintd_device,
+ &enrolled_fingers,
+ res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ gtk_spinner_stop (self->spinner);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), TRUE);
+
+ if (self->device_claimed)
+ gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), TRUE);
+
+ if (error)
+ {
+ g_autofree char *dbus_error = g_dbus_error_get_remote_error (error);
+
+ if (!dbus_error || !g_str_has_suffix (dbus_error, ".Error.NoEnrolledPrints"))
+ {
+ g_autofree char *error_message = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to list fingerprints: %s"),
+ error->message);
+ g_warning ("Listing of fingerprints on device %s failed: %s",
+ cc_fprintd_device_get_name (self->device), error->message);
+ notify_error (self, error_message);
+ return;
+ }
+ }
+ else
+ {
+ n_enrolled_fingers = g_strv_length (enrolled_fingers);
+ }
+
+ self->enrolled_fingers = g_steal_pointer (&enrolled_fingers);
+ gtk_flow_box_set_max_children_per_line (self->prints_gallery,
+ MIN (3, n_enrolled_fingers + 1));
+
+ update_prints_visibility (self);
+
+ if (n_enrolled_fingers == N_VALID_FINGERS)
+ gtk_widget_set_sensitive (self->add_print_icon, FALSE);
+
+ if (n_enrolled_fingers > 0)
+ gtk_widget_show (GTK_WIDGET (self->delete_prints_button));
+}
+
+static void
+update_prints_store (CcFingerprintDialog *self)
+{
+ ActUser *user;
+
+ g_assert_true (CC_FPRINTD_IS_DEVICE (self->device));
+
+ gtk_spinner_start (self->spinner);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), FALSE);
+ gtk_widget_hide (GTK_WIDGET (self->delete_prints_button));
+
+ g_clear_pointer (&self->enrolled_fingers, g_strfreev);
+
+ user = cc_fingerprint_manager_get_user (self->manager);
+ cc_fprintd_device_call_list_enrolled_fingers (self->device,
+ act_user_get_user_name (user),
+ self->cancellable,
+ list_enrolled_cb,
+ self);
+}
+
+static void
+delete_prints_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+
+ cc_fprintd_device_call_delete_enrolled_fingers2_finish (fprintd_device, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ if (error)
+ {
+ g_autofree char *error_message = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to delete saved fingerprints: %s"),
+ error->message);
+ g_warning ("Deletion of fingerprints on device %s failed: %s",
+ cc_fprintd_device_get_name (self->device), error->message);
+ notify_error (self, error_message);
+ }
+
+ update_prints_store (self);
+ cc_fingerprint_manager_update_state (self->manager, NULL, NULL);
+}
+
+static void
+delete_enrolled_prints (CcFingerprintDialog *self)
+{
+ g_return_if_fail (self->device_claimed);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), FALSE);
+ gtk_spinner_start (self->spinner);
+
+ cc_fprintd_device_call_delete_enrolled_fingers2 (self->device,
+ self->cancellable,
+ delete_prints_cb,
+ self);
+}
+
+static const char *
+get_finger_name (const char *finger_id)
+{
+ if (g_str_equal (finger_id, "left-thumb"))
+ return _("Left thumb");
+ if (g_str_equal (finger_id, "left-middle-finger"))
+ return _("Left middle finger");
+ if (g_str_equal (finger_id, "left-index-finger"))
+ return _("_Left index finger");
+ if (g_str_equal (finger_id, "left-ring-finger"))
+ return _("Left ring finger");
+ if (g_str_equal (finger_id, "left-little-finger"))
+ return _("Left little finger");
+ if (g_str_equal (finger_id, "right-thumb"))
+ return _("Right thumb");
+ if (g_str_equal (finger_id, "right-middle-finger"))
+ return _("Right middle finger");
+ if (g_str_equal (finger_id, "right-index-finger"))
+ return _("_Right index finger");
+ if (g_str_equal (finger_id, "right-ring-finger"))
+ return _("Right ring finger");
+ if (g_str_equal (finger_id, "right-little-finger"))
+ return _("Right little finger");
+
+ g_return_val_if_reached (_("Unknown Finger"));
+}
+
+static gboolean
+have_multiple_devices (CcFingerprintDialog *self)
+{
+ g_autoptr(GList) devices_rows = NULL;
+
+ devices_rows = gtk_container_get_children (GTK_CONTAINER (self->devices_list));
+
+ return devices_rows && devices_rows->next;
+}
+
+static void
+set_enroll_result_message (CcFingerprintDialog *self,
+ EnrollState enroll_state,
+ const char *message)
+{
+ GtkStyleContext *style_context;
+ const char *icon_name;
+ guint i;
+
+ g_return_if_fail (enroll_state >= 0 && enroll_state < N_ENROLL_STATES);
+
+ style_context = gtk_widget_get_style_context (self->enroll_result_icon);
+
+ switch (enroll_state)
+ {
+ case ENROLL_STATE_WARNING:
+ case ENROLL_STATE_ERROR:
+ icon_name = "fingerprint-detection-warning-symbolic";
+ break;
+ case ENROLL_STATE_COMPLETED:
+ icon_name = "fingerprint-detection-complete-symbolic";
+ break;
+ default:
+ icon_name = "fingerprint-detection-symbolic";
+ }
+
+ for (i = 0; i < N_ENROLL_STATES; ++i)
+ gtk_style_context_remove_class (style_context, ENROLL_STATE_CLASSES[i]);
+
+ gtk_style_context_add_class (style_context, ENROLL_STATE_CLASSES[enroll_state]);
+
+ gtk_image_set_from_icon_name (self->enroll_result_image, icon_name, GTK_ICON_SIZE_DND);
+ gtk_label_set_label (self->enroll_result_message, message);
+}
+
+static gboolean
+stage_passed_timeout_cb (gpointer user_data)
+{
+ CcFingerprintDialog *self = user_data;
+ const char *current_message;
+
+ current_message = gtk_label_get_label (self->enroll_result_message);
+ set_enroll_result_message (self, ENROLL_STATE_NORMAL, current_message);
+ self->enroll_stage_passed_id = 0;
+
+ return FALSE;
+}
+
+static void
+handle_enroll_signal (CcFingerprintDialog *self,
+ const char *result,
+ gboolean done)
+{
+ gboolean completed;
+
+ g_return_if_fail (self->enrolling_finger);
+
+ g_debug ("Device enroll result message: %s, done: %d", result, done);
+
+ completed = g_str_equal (result, "enroll-completed");
+ g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove);
+
+ if (g_str_equal (result, "enroll-stage-passed") || completed)
+ {
+ guint enroll_stages;
+
+ enroll_stages = cc_fprintd_device_get_num_enroll_stages (self->device);
+
+ self->enroll_stages_passed++;
+
+ if (enroll_stages > 0)
+ self->enroll_progress =
+ MIN (1.0f, self->enroll_stages_passed / (double) enroll_stages);
+ else
+ g_warning ("The device %s requires an invalid number of enroll stages (%u)",
+ cc_fprintd_device_get_name (self->device), enroll_stages);
+
+ g_debug ("Enroll state passed, %u/%u (%.2f%%)",
+ self->enroll_stages_passed, (guint) enroll_stages,
+ self->enroll_progress);
+
+ if (!completed)
+ {
+ set_enroll_result_message (self, ENROLL_STATE_SUCCESS, NULL);
+
+ self->enroll_stage_passed_id =
+ g_timeout_add (750, stage_passed_timeout_cb, self);
+ }
+ else
+ {
+ if (!G_APPROX_VALUE (self->enroll_progress, 1.0f, FLT_EPSILON))
+ {
+ g_warning ("Device marked enroll as completed, but progress is at %.2f",
+ self->enroll_progress);
+ self->enroll_progress = 1.0f;
+ }
+ }
+ }
+ else if (!done)
+ {
+ const char *scan_type;
+ const char *message;
+ gboolean is_swipe;
+
+ scan_type = cc_fprintd_device_get_scan_type (self->device);
+ is_swipe = g_str_equal (scan_type, "swipe");
+
+ message = TR (enroll_result_str_to_msg (result, is_swipe));
+ set_enroll_result_message (self, ENROLL_STATE_RETRY, message);
+
+ self->enroll_stage_passed_id =
+ g_timeout_add (850, stage_passed_timeout_cb, self);
+ }
+
+ if (done)
+ {
+ if (completed)
+ {
+ /* TRANSLATORS: This is the message shown when the fingerprint
+ * enrollment has been completed successfully */
+ set_enroll_result_message (self, ENROLL_STATE_COMPLETED,
+ C_("Fingerprint enroll state", "Complete"));
+ gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), FALSE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), TRUE);
+ gtk_widget_grab_focus (GTK_WIDGET (self->done_button));
+ }
+ else
+ {
+ const char *message;
+
+ if (g_str_equal (result, "enroll-disconnected"))
+ message = _("Fingerprint device disconnected");
+ else if (g_str_equal (result, "enroll-data-full"))
+ message = _("Fingerprint device storage is full");
+ else
+ message = _("Failed to enroll new fingerprint");
+
+ set_enroll_result_message (self, ENROLL_STATE_WARNING, message);
+ }
+ }
+}
+
+static void
+enroll_start_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+
+ cc_fprintd_device_call_enroll_start_finish (fprintd_device, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ gtk_spinner_stop (self->spinner);
+
+ if (error)
+ {
+ g_autofree char *error_message = NULL;
+
+ self->enrolling_finger = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to start enrollment: %s"),
+ error->message);
+ g_warning ("Enrollment on device %s failed: %s",
+ cc_fprintd_device_get_name (self->device), error->message);
+ notify_error (self, error_message);
+
+ set_enroll_result_message (self, ENROLL_STATE_ERROR,
+ C_("Fingerprint enroll state",
+ "Failed to enroll new fingerprint"));
+ gtk_widget_set_sensitive (self->enrollment_view, FALSE);
+
+ return;
+ }
+}
+
+static void
+enroll_stop_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+
+ cc_fprintd_device_call_enroll_stop_finish (fprintd_device, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ self->enrolling_finger = NULL;
+ gtk_spinner_stop (self->spinner);
+ gtk_widget_set_sensitive (self->enrollment_view, TRUE);
+ gtk_stack_set_visible_child (self->stack, self->prints_manager);
+
+ if (error)
+ {
+ g_autofree char *error_message = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to stop enrollment: %s"),
+ error->message);
+ g_warning ("Stopping enrollment on device %s failed: %s",
+ cc_fprintd_device_get_name (self->device), error->message);
+ notify_error (self, error_message);
+
+ return;
+ }
+
+ cc_fingerprint_manager_update_state (self->manager, NULL, NULL);
+}
+
+static void
+enroll_stop (CcFingerprintDialog *self)
+{
+ g_return_if_fail (self->enrolling_finger);
+
+ gtk_spinner_start (self->spinner);
+ gtk_widget_set_sensitive (self->enrollment_view, FALSE);
+ cc_fprintd_device_call_enroll_stop (self->device, self->cancellable,
+ enroll_stop_cb, self);
+}
+
+static char *
+get_enrollment_string (CcFingerprintDialog *self,
+ const char *finger_id)
+{
+ char *ret;
+ const char *scan_type;
+ const char *device_name;
+ gboolean is_swipe;
+
+ device_name = NULL;
+ scan_type = cc_fprintd_device_get_scan_type (self->device);
+ is_swipe = g_str_equal (scan_type, "swipe");
+
+ if (have_multiple_devices (self))
+ device_name = cc_fprintd_device_get_name (self->device);
+
+ ret = finger_str_to_msg (finger_id, device_name, is_swipe);
+
+ if (ret)
+ return ret;
+
+ return g_strdup (_("Repeatedly lift and place your finger on the reader to enroll your fingerprint"));
+}
+
+static void
+enroll_finger (CcFingerprintDialog *self,
+ const char *finger_id)
+{
+ g_auto(GStrv) tmp_finger_name = NULL;
+ g_autofree char *finger_name = NULL;
+ g_autofree char *enroll_message = NULL;
+
+ g_return_if_fail (finger_id);
+
+ self->enrolling_finger = finger_id;
+ self->enroll_progress = 0;
+ self->enroll_stages_passed = 0;
+
+ g_debug ("Enrolling finger %s", finger_id);
+
+ enroll_message = get_enrollment_string (self, finger_id);
+ tmp_finger_name = g_strsplit (get_finger_name (finger_id), "_", -1);
+ finger_name = g_strjoinv ("", tmp_finger_name);
+
+ set_enroll_result_message (self, ENROLL_STATE_NORMAL, NULL);
+ gtk_stack_set_visible_child (self->stack, self->enrollment_view);
+ gtk_label_set_label (self->enroll_message, enroll_message);
+ gtk_entry_set_text (self->enroll_print_entry, finger_name);
+ gtk_spinner_start (self->spinner);
+
+ cc_fprintd_device_call_enroll_start (self->device, finger_id, self->cancellable,
+ enroll_start_cb, self);
+}
+
+static void
+populate_enrollment_view (CcFingerprintDialog *self)
+{
+ GtkStyleContext *style_context;
+
+ self->enroll_result_icon =
+ fingerprint_icon_new ("fingerprint-detection-symbolic",
+ NULL,
+ GTK_TYPE_IMAGE,
+ &self->enroll_progress,
+ (GtkWidget **) &self->enroll_result_image,
+ (GtkWidget **) &self->enroll_result_message);
+
+ gtk_container_add (GTK_CONTAINER (self->enroll_print_bin), self->enroll_result_icon);
+
+ style_context = gtk_widget_get_style_context (self->enroll_result_icon);
+ gtk_style_context_add_class (style_context, "enroll-status");
+
+ gtk_widget_show_all (self->enroll_print_bin);
+}
+
+static void
+reenroll_finger_cb (CcFingerprintDialog *self)
+{
+ GtkWidget *button;
+ GtkWidget *flowbox_child;
+ const char *finger_id;
+
+ button = gtk_popover_get_relative_to (GTK_POPOVER (self->print_popover));
+ flowbox_child = g_object_get_data (G_OBJECT (button), "flowbox-child");
+ finger_id = g_object_get_data (G_OBJECT (flowbox_child), "finger-id");
+
+ enroll_finger (self, finger_id);
+}
+
+static void
+on_print_activated_cb (GtkFlowBox *flowbox,
+ GtkFlowBoxChild *child,
+ CcFingerprintDialog *self)
+{
+ GtkWidget *selected_button;
+
+ selected_button = g_object_get_data (G_OBJECT (child), "button");
+ gtk_button_clicked (GTK_BUTTON (selected_button));
+}
+
+static void
+on_enroll_cb (CcFingerprintDialog *self,
+ GtkModelButton *button)
+{
+ const char *finger_id;
+
+ finger_id = g_object_get_data (G_OBJECT (button), "finger-id");
+ enroll_finger (self, finger_id);
+}
+
+static void
+populate_add_print_popover (CcFingerprintDialog *self)
+{
+ guint i;
+
+ for (i = 0; i < N_VALID_FINGERS; ++i)
+ {
+ GtkWidget *finger_item;
+
+ finger_item = gtk_model_button_new ();
+ gtk_button_set_label (GTK_BUTTON (finger_item), get_finger_name (FINGER_IDS[i]));
+ gtk_button_set_use_underline (GTK_BUTTON (finger_item), TRUE);
+ g_object_set_data (G_OBJECT (finger_item), "finger-id", (gpointer) FINGER_IDS[i]);
+ gtk_container_add (self->add_print_popover_box, finger_item);
+
+ g_signal_connect_object (finger_item, "clicked", G_CALLBACK (on_enroll_cb),
+ self, G_CONNECT_SWAPPED);
+ }
+}
+
+static void
+populate_prints_gallery (CcFingerprintDialog *self)
+{
+ const char *add_print_label;
+ GtkWidget *button;
+ GtkStyleContext *style_context;
+ guint i;
+
+ g_return_if_fail (!GTK_IS_WIDGET (self->add_print_icon));
+
+ for (i = 0; i < N_VALID_FINGERS; ++i)
+ {
+ GtkWidget *flowbox_child;
+
+ flowbox_child = fingerprint_menu_button ("fingerprint-detection-symbolic",
+ get_finger_name (FINGER_IDS[i]));
+
+ button = g_object_get_data (G_OBJECT (flowbox_child), "button");
+
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (button),
+ GTK_WIDGET (self->print_popover));
+ /* Move the popover on click, so we can just reuse the same instance */
+ g_signal_connect_object (button, "clicked",
+ G_CALLBACK (gtk_popover_set_relative_to),
+ self->print_popover, G_CONNECT_SWAPPED);
+
+ g_object_set_data (G_OBJECT (flowbox_child), "finger-id",
+ (gpointer) FINGER_IDS[i]);
+
+ gtk_flow_box_insert (self->prints_gallery, flowbox_child, i);
+ }
+
+ /* TRANSLATORS: This is the label for the button to enroll a new finger */
+ add_print_label = _("Scan new fingerprint");
+ self->add_print_icon = fingerprint_menu_button ("list-add-symbolic",
+ add_print_label);
+ style_context = gtk_widget_get_style_context (self->add_print_icon);
+ gtk_style_context_add_class (style_context, "fingerprint-print-add");
+
+ populate_add_print_popover (self);
+ button = g_object_get_data (G_OBJECT (self->add_print_icon), "button");
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (button),
+ GTK_WIDGET (self->add_print_popover));
+
+ gtk_flow_box_insert (self->prints_gallery, self->add_print_icon, -1);
+ gtk_flow_box_set_max_children_per_line (self->prints_gallery, 1);
+
+ gtk_widget_show_all (GTK_WIDGET (self->prints_gallery));
+ gtk_flow_box_set_filter_func (self->prints_gallery, prints_visibility_filter,
+ self, NULL);
+
+ update_prints_visibility (self);
+}
+
+static void
+release_device_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+
+ cc_fprintd_device_call_release_finish (fprintd_device, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ if (error)
+ {
+ g_autofree char *error_message = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to release fingerprint device %s: %s"),
+ cc_fprintd_device_get_name (fprintd_device),
+ error->message);
+ g_warning ("%s", error_message);
+
+ notify_error (self, error_message);
+ return;
+ }
+
+ self->device_claimed = FALSE;
+}
+
+static void
+release_device (CcFingerprintDialog *self)
+{
+ if (!self->device || !self->device_claimed)
+ return;
+
+ disconnect_device_signals (self);
+
+ cc_fprintd_device_call_release (self->device,
+ self->cancellable,
+ release_device_cb,
+ self);
+}
+
+static void
+on_device_signal (CcFingerprintDialog *self,
+ gchar *sender_name,
+ gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ if (g_str_equal (signal_name, "EnrollStatus"))
+ {
+ const char *result;
+ gboolean done;
+
+ if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sb)")))
+ {
+ g_warning ("Unexpected enroll parameters type %s",
+ g_variant_get_type_string (parameters));
+ return;
+ }
+
+ g_variant_get (parameters, "(&sb)", &result, &done);
+ handle_enroll_signal (self, result, done);
+ }
+}
+
+static void claim_device (CcFingerprintDialog *self);
+
+static void
+on_device_owner_changed (CcFprintdDevice *device,
+ GParamSpec *spec,
+ CcFingerprintDialog *self)
+{
+ g_autofree char *name_owner = NULL;
+
+ name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (device));
+
+ if (!name_owner)
+ {
+ if (self->device_claimed)
+ {
+ disconnect_device_signals (self);
+
+ if (self->enrolling_finger)
+ {
+ set_enroll_result_message (self, ENROLL_STATE_ERROR,
+ C_("Fingerprint enroll state",
+ "Problem Reading Device"));
+ self->enrolling_finger = NULL;
+ }
+
+ self->device_claimed = FALSE;
+ claim_device (self);
+ }
+ }
+}
+
+static void
+claim_device_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GError) error = NULL;
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ CcFingerprintDialog *self = user_data;
+
+ cc_fprintd_device_call_claim_finish (fprintd_device, res, &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ self->claiming = FALSE;
+
+ if (error)
+ {
+ g_autofree char *dbus_error = g_dbus_error_get_remote_error (error);
+ g_autofree char *error_message = NULL;
+
+ if (dbus_error && g_str_has_suffix (dbus_error, ".Error.AlreadyInUse") &&
+ self->device_claimed)
+ return;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to claim fingerprint device %s: %s"),
+ cc_fprintd_device_get_name (self->device),
+ error->message);
+ g_warning ("%s", error_message);
+ notify_error (self, error_message);
+ return;
+ }
+
+ gtk_widget_set_sensitive (self->prints_manager, TRUE);
+ self->device_claimed = TRUE;
+ self->device_signal_id = g_signal_connect_object (self->device, "g-signal",
+ G_CALLBACK (on_device_signal),
+ self, G_CONNECT_SWAPPED);
+ self->device_name_owner_id = g_signal_connect_object (self->device, "notify::g-name-owner",
+ G_CALLBACK (on_device_owner_changed),
+ self, 0);
+}
+
+static void
+claim_device (CcFingerprintDialog *self)
+{
+ ActUser *user;
+
+ g_return_if_fail (!self->device_claimed);
+
+ if (self->claiming)
+ return;
+
+ user = cc_fingerprint_manager_get_user (self->manager);
+ gtk_widget_set_sensitive (self->prints_manager, FALSE);
+ self->claiming = TRUE;
+
+ cc_fprintd_device_call_claim (self->device,
+ act_user_get_user_name (user),
+ self->cancellable,
+ claim_device_cb,
+ self);
+}
+
+static void
+on_stack_child_changed (CcFingerprintDialog *self)
+{
+ GtkWidget *visible_child = gtk_stack_get_visible_child (self->stack);
+
+ g_debug ("Fingerprint dialog child changed: %s",
+ gtk_stack_get_visible_child_name (self->stack));
+
+ gtk_widget_hide (GTK_WIDGET (self->back_button));
+ gtk_widget_hide (GTK_WIDGET (self->cancel_button));
+ gtk_widget_hide (GTK_WIDGET (self->done_button));
+
+ gtk_header_bar_set_show_close_button (self->titlebar, TRUE);
+ gtk_flow_box_invalidate_filter (self->prints_gallery);
+
+ if (visible_child == self->prints_manager)
+ {
+ gtk_widget_set_visible (GTK_WIDGET (self->back_button),
+ have_multiple_devices (self));
+ notify_error (self, NULL);
+ update_prints_store (self);
+
+ if (!self->device_claimed)
+ claim_device (self);
+ }
+ else if (visible_child == self->enrollment_view)
+ {
+ gtk_header_bar_set_show_close_button (self->titlebar, FALSE);
+
+ gtk_widget_show (GTK_WIDGET (self->cancel_button));
+ gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), TRUE);
+
+ gtk_widget_show (GTK_WIDGET (self->done_button));
+ gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), FALSE);
+ }
+ else
+ {
+ release_device (self);
+ g_clear_object (&self->device);
+ }
+}
+
+static void
+cc_fingerprint_dialog_init (CcFingerprintDialog *self)
+{
+ g_autoptr(GtkCssProvider) provider = NULL;
+
+ self->cancellable = g_cancellable_new ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider,
+ "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.css");
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ on_stack_child_changed (self);
+ g_signal_connect_object (self->stack, "notify::visible-child",
+ G_CALLBACK (on_stack_child_changed), self,
+ G_CONNECT_SWAPPED);
+
+ g_object_bind_property (self->stack, "visible-child-name",
+ self->title, "label", G_BINDING_SYNC_CREATE);
+
+ populate_prints_gallery (self);
+ populate_enrollment_view (self);
+}
+
+static void
+select_device_row (CcFingerprintDialog *self,
+ GtkListBoxRow *row,
+ GtkListBox *listbox)
+{
+ CcFprintdDevice *device = g_object_get_data (G_OBJECT (row), "device");
+
+ g_return_if_fail (CC_FPRINTD_DEVICE (device));
+
+ g_set_object (&self->device, device);
+ gtk_stack_set_visible_child (self->stack, self->prints_manager);
+}
+
+static void
+on_devices_list (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autolist (CcFprintdDevice) fprintd_devices = NULL;
+ g_autoptr(GError) error = NULL;
+ CcFingerprintManager *fingerprint_manager = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (user_data);
+
+ fprintd_devices = cc_fingerprint_manager_get_devices_finish (fingerprint_manager,
+ res, &error);
+ gtk_spinner_stop (self->spinner);
+
+ if (fprintd_devices == NULL)
+ {
+ if (error)
+ {
+ g_autofree char *error_message = NULL;
+
+ g_dbus_error_strip_remote_error (error);
+ error_message = g_strdup_printf (_("Failed to get fingerprint devices: %s"),
+ error->message);
+ g_warning ("%s", error_message);
+ notify_error (self, error_message);
+ }
+
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->no_devices_found));
+ }
+ else if (fprintd_devices->next == NULL)
+ {
+ /* We have just one device... Skip devices selection */
+ self->device = g_object_ref (fprintd_devices->data);
+ gtk_stack_set_visible_child (self->stack, self->prints_manager);
+ }
+ else
+ {
+ GList *l;
+
+ for (l = fprintd_devices; l; l = l->next)
+ {
+ CcFprintdDevice *device = l->data;
+ CcListRow *device_row;
+
+ device_row = g_object_new (CC_TYPE_LIST_ROW,
+ "visible", TRUE,
+ "icon-name", "go-next-symbolic",
+ "title", cc_fprintd_device_get_name (device),
+ NULL);
+
+ gtk_list_box_insert (self->devices_list, GTK_WIDGET (device_row), -1);
+ g_object_set_data_full (G_OBJECT (device_row), "device",
+ g_object_ref (device), g_object_unref);
+ }
+
+ gtk_stack_set_visible_child (self->stack, self->device_selector);
+ }
+}
+
+static void
+cc_fingerprint_dialog_constructed (GObject *object)
+{
+ CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object);
+
+ bindtextdomain ("fprintd", GNOMELOCALEDIR);
+ bind_textdomain_codeset ("fprintd", "UTF-8");
+
+ gtk_spinner_start (self->spinner);
+ cc_fingerprint_manager_get_devices (self->manager, self->cancellable,
+ on_devices_list, self);
+}
+
+static void
+back_button_clicked_cb (CcFingerprintDialog *self)
+{
+ if (gtk_stack_get_visible_child (self->stack) == self->prints_manager)
+ {
+ notify_error (self, NULL);
+ gtk_stack_set_visible_child (self->stack, self->device_selector);
+ return;
+ }
+
+ g_return_if_reached ();
+}
+
+static void
+confirm_deletion_button_clicked_cb (CcFingerprintDialog *self)
+{
+ gtk_widget_hide (self->delete_confirmation_infobar);
+ delete_enrolled_prints (self);
+}
+
+static void
+cancel_deletion_button_clicked_cb (CcFingerprintDialog *self)
+{
+ gtk_widget_set_sensitive (self->prints_manager, TRUE);
+ gtk_widget_hide (self->delete_confirmation_infobar);
+}
+
+static void
+delete_prints_button_clicked_cb (CcFingerprintDialog *self)
+{
+ gtk_widget_set_sensitive (self->prints_manager, FALSE);
+ gtk_widget_show (self->delete_confirmation_infobar);
+}
+
+static void
+cancel_button_clicked_cb (CcFingerprintDialog *self)
+{
+ if (self->enrolling_finger)
+ {
+ g_cancellable_cancel (self->cancellable);
+ g_set_object (&self->cancellable, g_cancellable_new ());
+
+ g_debug ("Cancelling enroll operation");
+ enroll_stop (self);
+ }
+ else
+ {
+ gtk_stack_set_visible_child (self->stack, self->prints_manager);
+ }
+}
+
+static void
+done_button_clicked_cb (CcFingerprintDialog *self)
+{
+ g_return_if_fail (self->enrolling_finger);
+
+ g_debug ("Completing enroll operation");
+ enroll_stop (self);
+}
+
+static void
+fingerprint_dialog_delete_cb (CcFingerprintDialog *self)
+{
+ cc_fingerprint_manager_update_state (self->manager, NULL, NULL);
+ gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+cc_fingerprint_dialog_class_init (CcFingerprintDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.ui");
+
+ object_class->constructed = cc_fingerprint_dialog_constructed;
+ object_class->dispose = cc_fingerprint_dialog_dispose;
+ object_class->get_property = cc_fingerprint_dialog_get_property;
+ object_class->set_property = cc_fingerprint_dialog_set_property;
+
+ properties[PROP_MANAGER] =
+ g_param_spec_object ("fingerprint-manager",
+ "FingerprintManager",
+ "The CC fingerprint manager",
+ CC_TYPE_FINGERPRINT_MANAGER,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover_box);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, back_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, cancel_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_confirmation_infobar);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_prints_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, device_selector);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, devices_list);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, done_button);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_message);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_bin);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enrollment_view);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, error_infobar);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, infobar_error);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, no_devices_found);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, print_popover);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_gallery);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_manager);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, spinner);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, stack);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, title);
+ gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, titlebar);
+
+ gtk_widget_class_bind_template_callback (widget_class, back_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, cancel_deletion_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, confirm_deletion_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, delete_prints_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, done_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, fingerprint_dialog_delete_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_print_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, reenroll_finger_cb);
+ gtk_widget_class_bind_template_callback (widget_class, select_device_row);
+}
diff --git a/panels/user-accounts/cc-fingerprint-dialog.h b/panels/user-accounts/cc-fingerprint-dialog.h
new file mode 100644
index 0000000..9afac0b
--- /dev/null
+++ b/panels/user-accounts/cc-fingerprint-dialog.h
@@ -0,0 +1,37 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authors: Marco Trevisan <marco.trevisan@canonical.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "cc-fingerprint-manager.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_FINGERPRINT_DIALOG (cc_fingerprint_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcFingerprintDialog, cc_fingerprint_dialog,
+ CC, FINGERPRINT_DIALOG, GtkWindow)
+
+CcFingerprintDialog *cc_fingerprint_dialog_new (CcFingerprintManager *manager);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-fingerprint-dialog.ui b/panels/user-accounts/cc-fingerprint-dialog.ui
new file mode 100644
index 0000000..2613d5e
--- /dev/null
+++ b/panels/user-accounts/cc-fingerprint-dialog.ui
@@ -0,0 +1,462 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcFingerprintDialog" parent="GtkWindow">
+ <style>
+ <class name="fingerprint" />
+ </style>
+ <property name="name">fingerprint-dialog</property>
+ <property name="title" translatable="yes">Fingerprint Manager</property>
+ <property name="type-hint">dialog</property>
+ <property name="window-position">center-on-parent</property>
+ <property name="destroy-with-parent">True</property>
+ <property name="default-width">600</property>
+ <property name="default-height">400</property>
+ <property name="modal">True</property>
+ <signal name="delete-event" handler="fingerprint_dialog_delete_cb"/>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="titlebar">
+ <property name="visible">True</property>
+ <property name="show-close-button">True</property>
+ <child type="title">
+ <object class="GtkLabel" id="title">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Fingerprint</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="label" translatable="yes">_Cancel</property>
+ <signal name="clicked" handler="cancel_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" />
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="back_button">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="back_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" />
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-back">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkButton" id="done_button">
+ <property name="use-underline">True</property>
+ <property name="can-default">True</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="label" translatable="yes">_Done</property>
+ <signal name="clicked" handler="done_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" />
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkSpinner" id="spinner">
+ <property name="visible">True</property>
+ <property name="active">False</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkInfoBar" id="delete_confirmation_infobar">
+ <property name="visible">False</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">0</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton">
+ <signal name="clicked" handler="cancel_deletion_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">_No</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <signal name="clicked" handler="confirm_deletion_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="label" translatable="yes">_Yes</property>
+ <property name="use-underline">True</property>
+ <style>
+ <class name="destructive-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child internal-child="content_area">
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="spacing">16</property>
+ <property name="margin-start">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="hexpand">False</property>
+ <property name="wrap">True</property>
+ <property name="label" translatable="yes">Do you want to delete your registered fingerprints so fingerprint login is disabled?</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkInfoBar" id="error_infobar">
+ <property name="name">error_infobar</property>
+ <property name="visible">False</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">0</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <style>
+ <class name="error"/>
+ </style>
+ <child internal-child="content_area">
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="spacing">16</property>
+ <child>
+ <object class="GtkLabel" id="infobar_error">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="hexpand">False</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="propagate-natural-width">True</property>
+ <property name="can-focus">False</property>
+ <property name="hscrollbar-policy">never</property>
+
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="transition_duration">300</property>
+ <property name="margin-start">20</property>
+ <property name="margin-end">20</property>
+ <property name="margin-top">30</property>
+ <property name="margin-bottom">30</property>
+ <property name="width_request">360</property>
+ <property name="halign">center</property>
+
+ <child>
+ <object class="GtkBox" id="no_devices_found">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="spacing">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">fingerprint-detection-symbolic</property>
+ <property name="pixel_size">192</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes" comments="Translators: This is the empty state page label which states that there are no devices ready.">No Fingerprint device</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.6"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Ensure the device is properly connected.</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name" translatable="yes">No fingerprint device</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkBox" id="device_selector">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="spacing">10</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Choose the fingerprint device you want to configure</property>
+ <property name="halign">start</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">True</property>
+ <child>
+ <object class="GtkListBox" id="devices_list">
+ <property name="visible">True</property>
+ <property name="selection-mode">none</property>
+ <property name="valign">center</property>
+ <signal name="row-activated" handler="select_device_row" object="CcFingerprintDialog" swapped="yes"/>
+ <style>
+ <class name="frame" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name" translatable="yes">Fingerprint Device</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkBox" id="prints_manager">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">fill</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Fingerprint login allows you to unlock and log into your computer with your finger</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFlowBox" id="prints_gallery">
+ <style>
+ <class name="prints-gallery" />
+ </style>
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ <property name="column-spacing">12</property>
+ <property name="row-spacing">12</property>
+ <property name="homogeneous">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="min-children-per-line">1</property>
+ <property name="max-children-per-line">3</property>
+ <property name="activate-on-single-click">True</property>
+ <property name="selection-mode">none</property>
+ <signal name="child-activated" handler="on_print_activated_cb" object="CcFingerprintDialog" swapped="no" />
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkButton" id="delete_prints_button">
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="halign">end</property>
+ <property name="use-underline">True</property>
+ <property name="label" translatable="yes">_Delete Fingerprints</property>
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <signal name="clicked" handler="delete_prints_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/>
+ <style>
+ <class name="destructive-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name" translatable="yes">Fingerprint Login</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkBox" id="enrollment_view">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">fill</property>
+ <property name="spacing">12</property>
+ <style>
+ <class name="enrollment" />
+ </style>
+ <child>
+ <object class="GtkLabel" id="enroll_message">
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ <property name="spacing">12</property>
+ <property name="halign">fill</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="enroll_print_bin">
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="enroll_print_entry">
+ <property name="valign">end</property>
+ <property name="visible">True</property>
+ <property name="halign">center</property>
+ <property name="editable">False</property>
+ <property name="sensitive">False</property>
+ <property name="width-request">200</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name" translatable="yes">Fingerprint Enroll</property>
+ </packing>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+
+ <object class="GtkPopoverMenu" id="print_popover">
+ <property name="position">bottom</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">12</property>
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkModelButton">
+ <property name="label" translatable="yes">_Re-enroll this finger…</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">0.0</property>
+ <signal name="clicked" handler="reenroll_finger_cb" object="CcFingerprintDialog" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+
+ <object class="GtkPopoverMenu" id="add_print_popover">
+ <property name="position">bottom</property>
+ <child>
+ <object class="GtkBox" id="add_print_popover_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">12</property>
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+ </object>
+ </child>
+ </object>
+
+</interface>
diff --git a/panels/user-accounts/cc-fingerprint-manager.c b/panels/user-accounts/cc-fingerprint-manager.c
new file mode 100644
index 0000000..07a50e7
--- /dev/null
+++ b/panels/user-accounts/cc-fingerprint-manager.c
@@ -0,0 +1,597 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authors: Marco Trevisan <marco.trevisan@canonical.com>
+ */
+
+#include "cc-fingerprint-manager.h"
+
+#include "cc-fprintd-generated.h"
+#include "cc-user-accounts-enum-types.h"
+
+#define CC_FPRINTD_NAME "net.reactivated.Fprint"
+#define CC_FPRINTD_MANAGER_PATH "/net/reactivated/Fprint/Manager"
+
+struct _CcFingerprintManager
+{
+ GObject parent_instance;
+};
+
+typedef struct
+{
+ ActUser *user;
+ GTask *current_task;
+ CcFingerprintState state;
+ GList *cached_devices;
+} CcFingerprintManagerPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (CcFingerprintManager, cc_fingerprint_manager, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_USER,
+ PROP_STATE,
+ N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+static void cleanup_cached_devices (CcFingerprintManager *self);
+
+CcFingerprintManager *
+cc_fingerprint_manager_new (ActUser *user)
+{
+ return g_object_new (CC_TYPE_FINGERPRINT_MANAGER, "user", user, NULL);
+}
+
+static void
+cc_fingerprint_manager_dispose (GObject *object)
+{
+ CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ if (priv->current_task)
+ {
+ g_cancellable_cancel (g_task_get_cancellable (priv->current_task));
+ priv->current_task = NULL;
+ }
+
+ g_clear_object (&priv->user);
+ cleanup_cached_devices (self);
+
+ G_OBJECT_CLASS (cc_fingerprint_manager_parent_class)->dispose (object);
+}
+
+static void
+cc_fingerprint_manager_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_STATE:
+ g_value_set_enum (value, priv->state);
+ break;
+
+ case PROP_USER:
+ g_value_set_object (value, priv->user);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_fingerprint_manager_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_USER:
+ g_set_object (&priv->user, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_fingerprint_manager_constructed (GObject *object)
+{
+ cc_fingerprint_manager_update_state (CC_FINGERPRINT_MANAGER (object), NULL, NULL);
+}
+
+static void
+cc_fingerprint_manager_class_init (CcFingerprintManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = cc_fingerprint_manager_constructed;
+ object_class->dispose = cc_fingerprint_manager_dispose;
+ object_class->get_property = cc_fingerprint_manager_get_property;
+ object_class->set_property = cc_fingerprint_manager_set_property;
+
+ properties[PROP_USER] =
+ g_param_spec_object ("user",
+ "User",
+ "The user account we manage the fingerprint for",
+ ACT_TYPE_USER,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ properties[PROP_STATE] =
+ g_param_spec_enum ("state",
+ "State",
+ "The state of the fingerprint for the user",
+ CC_TYPE_FINGERPRINT_STATE, CC_FINGERPRINT_STATE_NONE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+cc_fingerprint_manager_init (CcFingerprintManager *self)
+{
+}
+
+typedef struct
+{
+ guint waiting_devices;
+ GList *devices;
+} DeviceListData;
+
+static void
+object_list_destroy_notify (gpointer data)
+{
+ GList *list = data;
+ g_list_free_full (list, g_object_unref);
+}
+
+static void
+on_device_owner_changed (CcFingerprintManager *self,
+ GParamSpec *spec,
+ CcFprintdDevice *device)
+{
+ g_autofree char *name_owner = NULL;
+
+ name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (device));
+
+ if (!name_owner)
+ {
+ g_debug ("Fprintd daemon disappeared, cleaning cache...");
+ cleanup_cached_devices (self);
+ }
+}
+
+static void
+cleanup_cached_devices (CcFingerprintManager *self)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ CcFprintdDevice *target_device;
+
+ if (!priv->cached_devices)
+ return;
+
+ g_return_if_fail (CC_FPRINTD_IS_DEVICE (priv->cached_devices->data));
+
+ target_device = CC_FPRINTD_DEVICE (priv->cached_devices->data);
+
+ g_signal_handlers_disconnect_by_func (target_device, on_device_owner_changed, self);
+ g_list_free_full (g_steal_pointer (&priv->cached_devices), g_object_unref);
+}
+
+static void
+cache_devices (CcFingerprintManager *self,
+ GList *devices)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ CcFprintdDevice *target_device;
+
+ g_return_if_fail (devices && CC_FPRINTD_IS_DEVICE (devices->data));
+
+ cleanup_cached_devices (self);
+ priv->cached_devices = g_list_copy_deep (devices, (GCopyFunc) g_object_ref, NULL);
+
+ /* We can monitor just the first device name, as the owner is just the same */
+ target_device = CC_FPRINTD_DEVICE (priv->cached_devices->data);
+
+ g_signal_connect_object (target_device, "notify::g-name-owner",
+ G_CALLBACK (on_device_owner_changed), self,
+ G_CONNECT_SWAPPED);
+}
+
+static void
+on_device_proxy (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ g_autoptr(CcFprintdDevice) fprintd_device = NULL;
+ g_autoptr(GTask) task = G_TASK (user_data);
+ g_autoptr(GError) error = NULL;
+ CcFingerprintManager *self = g_task_get_source_object (task);
+ DeviceListData *list_data = g_task_get_task_data (task);
+
+ fprintd_device = cc_fprintd_device_proxy_new_for_bus_finish (res, &error);
+ list_data->waiting_devices--;
+
+ if (error)
+ {
+ if (list_data->waiting_devices == 0)
+ g_task_return_error (task, g_steal_pointer (&error));
+ else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Impossible to ge the device proxy: %s", error->message);
+
+ return;
+ }
+
+ g_debug ("Got fingerprint device %s", cc_fprintd_device_get_name (fprintd_device));
+
+ list_data->devices = g_list_append (list_data->devices, g_steal_pointer (&fprintd_device));
+
+ if (list_data->waiting_devices == 0)
+ {
+ cache_devices (self, list_data->devices);
+ g_task_return_pointer (task, g_steal_pointer (&list_data->devices), object_list_destroy_notify);
+ }
+}
+
+static void
+on_devices_list (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ CcFprintdManager *fprintd_manager = CC_FPRINTD_MANAGER (object);
+ g_autoptr(GTask) task = G_TASK (user_data);
+ g_autoptr(GError) error = NULL;
+ g_auto(GStrv) devices_list = NULL;
+ DeviceListData *list_data;
+ guint i;
+
+ cc_fprintd_manager_call_get_devices_finish (fprintd_manager, &devices_list, res, &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ if (!devices_list || !devices_list[0])
+ {
+ g_task_return_pointer (task, NULL, NULL);
+ return;
+ }
+
+ list_data = g_new0 (DeviceListData, 1);
+ g_task_set_task_data (task, list_data, g_free);
+
+ g_debug ("Fprintd replied with %u device(s)", g_strv_length (devices_list));
+
+ for (i = 0; devices_list[i] != NULL; ++i)
+ {
+ const char *device_path = devices_list[i];
+
+ list_data->waiting_devices++;
+
+ cc_fprintd_device_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ CC_FPRINTD_NAME,
+ device_path,
+ g_task_get_cancellable (task),
+ on_device_proxy,
+ g_object_ref (task));
+ }
+}
+
+static void
+on_manager_proxy (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ g_autoptr(GTask) task = G_TASK (user_data);
+ g_autoptr(CcFprintdManager) fprintd_manager = NULL;
+ g_autoptr(GError) error = NULL;
+
+ fprintd_manager = cc_fprintd_manager_proxy_new_for_bus_finish (res, &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ g_debug ("Fprintd manager connected");
+
+ cc_fprintd_manager_call_get_devices (fprintd_manager,
+ g_task_get_cancellable (task),
+ on_devices_list,
+ g_object_ref (task));
+}
+
+static void
+fprintd_manager_connect (CcFingerprintManager *self,
+ GAsyncReadyCallback callback,
+ GTask *task)
+{
+ g_assert (G_IS_TASK (task));
+
+ cc_fprintd_manager_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE,
+ CC_FPRINTD_NAME, CC_FPRINTD_MANAGER_PATH,
+ g_task_get_cancellable (task),
+ callback,
+ task);
+}
+
+void
+cc_fingerprint_manager_get_devices (CcFingerprintManager *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, cc_fingerprint_manager_get_devices);
+
+ if (priv->cached_devices)
+ {
+ GList *devices;
+
+ devices = g_list_copy_deep (priv->cached_devices, (GCopyFunc) g_object_ref, NULL);
+ g_task_return_pointer (task, devices, object_list_destroy_notify);
+ return;
+ }
+
+ fprintd_manager_connect (self, on_manager_proxy, g_steal_pointer (&task));
+}
+
+/**
+ * cc_fingerprint_manager_get_devices_finish:
+ * @self: The #CcFingerprintManager
+ * @result: A #GAsyncResult
+ * @error: Return location for errors, or %NULL to ignore
+ *
+ * Finish an asynchronous operation to list all devices.
+ *
+ * Returns: (element-type CcFprintdDevice) (transfer full): List of prints or %NULL on error
+ */
+GList *
+cc_fingerprint_manager_get_devices_finish (CcFingerprintManager *self,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (res, self), NULL);
+
+ return g_task_propagate_pointer (G_TASK (res), error);
+}
+
+static void
+set_state (CcFingerprintManager *self,
+ CcFingerprintState state)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ if (priv->state == state)
+ return;
+
+ g_debug ("Fingerprint manager state changed to %d", state);
+
+ priv->state = state;
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
+}
+
+typedef struct
+{
+ guint waiting_devices;
+ CcFingerprintStateUpdated callback;
+ gpointer user_data;
+} UpdateStateData;
+
+static void
+update_state_callback (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ g_autoptr(GError) error = NULL;
+ CcFingerprintState state;
+ UpdateStateData *data;
+ GTask *task;
+
+ g_return_if_fail (g_task_is_valid (res, self));
+
+ task = G_TASK (res);
+ g_assert (g_steal_pointer (&priv->current_task) == task);
+
+ state = g_task_propagate_int (task, &error);
+ data = g_task_get_task_data (task);
+
+ if (error)
+ {
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ g_warning ("Impossible to update fingerprint manager state: %s",
+ error->message);
+
+ state = CC_FINGERPRINT_STATE_NONE;
+ }
+
+ set_state (self, state);
+
+ if (data->callback)
+ data->callback (self, state, data->user_data, error);
+}
+
+static void
+on_device_list_enrolled (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object);
+ g_autoptr(GTask) task = G_TASK (user_data);
+ g_autoptr(GError) error = NULL;
+ g_auto(GStrv) enrolled_fingers = NULL;
+ UpdateStateData *data = g_task_get_task_data (task);
+ guint num_enrolled_fingers;
+
+ cc_fprintd_device_call_list_enrolled_fingers_finish (fprintd_device,
+ &enrolled_fingers,
+ res, &error);
+
+ if (data->waiting_devices == 0)
+ return;
+
+ data->waiting_devices--;
+
+ if (error)
+ {
+ g_autofree char *dbus_error = g_dbus_error_get_remote_error (error);
+
+ if (!g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints"))
+ {
+ if (data->waiting_devices == 0)
+ g_task_return_error (task, g_steal_pointer (&error));
+ else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Impossible to list enrolled fingers: %s", error->message);
+
+ return;
+ }
+ }
+
+ num_enrolled_fingers = enrolled_fingers ? g_strv_length (enrolled_fingers) : 0;
+
+ g_debug ("Device %s has %u enrolled fingers",
+ cc_fprintd_device_get_name (fprintd_device),
+ num_enrolled_fingers);
+
+ if (num_enrolled_fingers > 0)
+ {
+ data->waiting_devices = 0;
+ g_task_return_int (task, CC_FINGERPRINT_STATE_ENABLED);
+ }
+ else if (data->waiting_devices == 0)
+ {
+ g_task_return_int (task, CC_FINGERPRINT_STATE_DISABLED);
+ }
+}
+
+static void
+on_manager_devices_list (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object);
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ g_autolist(CcFprintdDevice) fprintd_devices = NULL;
+ g_autoptr(GTask) task = G_TASK (user_data);
+ g_autoptr(GError) error = NULL;
+ UpdateStateData *data = g_task_get_task_data (task);
+ const char *user_name;
+ GList *l;
+
+ fprintd_devices = cc_fingerprint_manager_get_devices_finish (self, res, &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ if (fprintd_devices == NULL)
+ {
+ g_debug ("No fingerprint devices found");
+ g_task_return_int (task, CC_FINGERPRINT_STATE_NONE);
+ return;
+ }
+
+ user_name = act_user_get_user_name (priv->user);
+
+ for (l = fprintd_devices; l; l = l->next)
+ {
+ CcFprintdDevice *device = l->data;
+
+ g_debug ("Connected to device %s, looking for enrolled fingers",
+ cc_fprintd_device_get_name (device));
+
+ data->waiting_devices++;
+ cc_fprintd_device_call_list_enrolled_fingers (device, user_name,
+ g_task_get_cancellable (task),
+ on_device_list_enrolled,
+ g_object_ref (task));
+ }
+}
+
+void
+cc_fingerprint_manager_update_state (CcFingerprintManager *self,
+ CcFingerprintStateUpdated callback,
+ gpointer user_data)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+ g_autoptr(GCancellable) cancellable = NULL;
+ UpdateStateData *data;
+
+ g_return_if_fail (priv->current_task == NULL);
+
+ if (act_user_get_uid (priv->user) != getuid () ||
+ !act_user_is_local_account (priv->user))
+ {
+ set_state (self, CC_FINGERPRINT_STATE_NONE);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ data = g_new0 (UpdateStateData, 1);
+ data->callback = callback;
+ data->user_data = user_data;
+
+ priv->current_task = g_task_new (self, cancellable, update_state_callback, NULL);
+ g_task_set_source_tag (priv->current_task, cc_fingerprint_manager_update_state);
+ g_task_set_task_data (priv->current_task, data, g_free);
+
+ set_state (self, CC_FINGERPRINT_STATE_UPDATING);
+
+ cc_fingerprint_manager_get_devices (self, cancellable, on_manager_devices_list,
+ priv->current_task);
+}
+
+CcFingerprintState
+cc_fingerprint_manager_get_state (CcFingerprintManager *self)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ g_return_val_if_fail (CC_IS_FINGERPRINT_MANAGER (self), CC_FINGERPRINT_STATE_NONE);
+
+ return priv->state;
+}
+
+ActUser *
+cc_fingerprint_manager_get_user (CcFingerprintManager *self)
+{
+ CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self);
+
+ g_return_val_if_fail (CC_IS_FINGERPRINT_MANAGER (self), NULL);
+
+ return priv->user;
+}
diff --git a/panels/user-accounts/cc-fingerprint-manager.h b/panels/user-accounts/cc-fingerprint-manager.h
new file mode 100644
index 0000000..d12f52c
--- /dev/null
+++ b/panels/user-accounts/cc-fingerprint-manager.h
@@ -0,0 +1,74 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authors: Marco Trevisan <marco.trevisan@canonical.com>
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <act/act.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_FINGERPRINT_MANAGER (cc_fingerprint_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcFingerprintManager, cc_fingerprint_manager, CC, FINGERPRINT_MANAGER, GObject)
+
+/**
+ * CcFingerprintManager:
+ * @CC_FINGERPRINT_STATE_NONE: Fingerprint recognition is not available
+ * @CC_FINGERPRINT_STATE_UPDATING: Fingerprint recognition is being fetched
+ * @CC_FINGERPRINT_STATE_ENABLED: Fingerprint recognition is enabled
+ * @CC_FINGERPRINT_STATE_DISABLED: Fingerprint recognition is disabled
+ *
+ * The status of the fingerprint support.
+ */
+typedef enum {
+ CC_FINGERPRINT_STATE_NONE,
+ CC_FINGERPRINT_STATE_UPDATING,
+ CC_FINGERPRINT_STATE_ENABLED,
+ CC_FINGERPRINT_STATE_DISABLED,
+} CcFingerprintState;
+
+typedef void (*CcFingerprintStateUpdated) (CcFingerprintManager *fp_manager,
+ CcFingerprintState state,
+ gpointer user_data,
+ GError *error);
+
+CcFingerprintManager * cc_fingerprint_manager_new (ActUser *user);
+
+CcFingerprintState cc_fingerprint_manager_get_state (CcFingerprintManager *fp_manager);
+
+ActUser * cc_fingerprint_manager_get_user (CcFingerprintManager *fp_manager);
+
+void cc_fingerprint_manager_update_state (CcFingerprintManager *fp_manager,
+ CcFingerprintStateUpdated callback,
+ gpointer user_data);
+
+void cc_fingerprint_manager_get_devices (CcFingerprintManager *fp_manager,
+ GCancellable *cancellable,
+ GAsyncReadyCallback res,
+ gpointer user_data);
+
+GList *cc_fingerprint_manager_get_devices_finish (CcFingerprintManager *fp_manager,
+ GAsyncResult *res,
+ GError **error);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-login-history-dialog.c b/panels/user-accounts/cc-login-history-dialog.c
new file mode 100644
index 0000000..6670f75
--- /dev/null
+++ b/panels/user-accounts/cc-login-history-dialog.c
@@ -0,0 +1,350 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Ondrej Holy <oholy@redhat.com>
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+#include "cc-login-history-dialog.h"
+#include "cc-user-accounts-resources.h"
+#include "cc-util.h"
+#include "user-utils.h"
+
+struct _CcLoginHistoryDialog
+{
+ GtkDialog parent_instance;
+
+ GtkHeaderBar *header_bar;
+ GtkListBox *history_box;
+ GtkButton *next_button;
+ GtkButton *previous_button;
+
+ GDateTime *week;
+ GDateTime *current_week;
+
+ ActUser *user;
+};
+
+G_DEFINE_TYPE (CcLoginHistoryDialog, cc_login_history_dialog, GTK_TYPE_DIALOG)
+
+typedef struct {
+ gint64 login_time;
+ gint64 logout_time;
+ const gchar *type;
+} CcLoginHistory;
+
+static void
+show_week_label (CcLoginHistoryDialog *self)
+{
+ g_autofree gchar *label = NULL;
+ GTimeSpan span;
+
+ span = g_date_time_difference (self->current_week, self->week);
+ if (span == 0) {
+ label = g_strdup (_("This Week"));
+ }
+ else if (span == G_TIME_SPAN_DAY * 7) {
+ label = g_strdup (_("Last Week"));
+ }
+ else {
+ g_autofree gchar *from = NULL;
+ g_autofree gchar *to = NULL;
+ g_autoptr(GDateTime) date = NULL;
+
+ date = g_date_time_add_days (self->week, 6);
+ /* Translators: This is a date format string in the style of "Feb 18",
+ shown as the first day of a week on login history dialog. */
+ from = g_date_time_format (self->week, C_("login history week label","%b %e"));
+ if (g_date_time_get_year (self->week) == g_date_time_get_year (self->current_week)) {
+ /* Translators: This is a date format string in the style of "Feb 24",
+ shown as the last day of a week on login history dialog. */
+ to = g_date_time_format (date, C_("login history week label","%b %e"));
+ }
+ else {
+ /* Translators: This is a date format string in the style of "Feb 24, 2013",
+ shown as the last day of a week on login history dialog. */
+ to = g_date_time_format (date, C_("login history week label","%b %e, %Y"));
+ }
+
+ /* Translators: This indicates a week label on a login history.
+ The first %s is the first day of a week, and the second %s the last day. */
+ label = g_strdup_printf(C_("login history week label", "%s — %s"), from, to);
+ }
+
+ gtk_header_bar_set_subtitle (self->header_bar, label);
+}
+
+static void
+clear_history (CcLoginHistoryDialog *self)
+{
+ g_autoptr(GList) list = NULL;
+ GList *it;
+
+ list = gtk_container_get_children (GTK_CONTAINER (self->history_box));
+ for (it = list; it != NULL; it = it->next) {
+ gtk_container_remove (GTK_CONTAINER (self->history_box), GTK_WIDGET (it->data));
+ }
+}
+
+static GArray *
+get_login_history (ActUser *user)
+{
+ GArray *login_history;
+ GVariantIter *iter, *iter2;
+ GVariant *variant;
+ const GVariant *value;
+ const gchar *key;
+ CcLoginHistory history;
+
+ login_history = NULL;
+ value = act_user_get_login_history (user);
+ g_variant_get ((GVariant *) value, "a(xxa{sv})", &iter);
+ while (g_variant_iter_loop (iter, "(xxa{sv})", &history.login_time, &history.logout_time, &iter2)) {
+ while (g_variant_iter_loop (iter2, "{&sv}", &key, &variant)) {
+ if (g_strcmp0 (key, "type") == 0) {
+ history.type = g_variant_get_string (variant, NULL);
+ }
+ }
+
+ if (login_history == NULL) {
+ login_history = g_array_new (FALSE, TRUE, sizeof (CcLoginHistory));
+ }
+
+ g_array_append_val (login_history, history);
+ }
+
+ return login_history;
+}
+
+static void
+set_sensitivity (CcLoginHistoryDialog *self)
+{
+ g_autoptr(GArray) login_history = NULL;
+ CcLoginHistory history;
+ gboolean sensitive = FALSE;
+
+ login_history = get_login_history (self->user);
+ if (login_history != NULL) {
+ history = g_array_index (login_history, CcLoginHistory, 0);
+ sensitive = g_date_time_to_unix (self->week) > history.login_time;
+ }
+ gtk_widget_set_sensitive (GTK_WIDGET (self->previous_button), sensitive);
+
+ sensitive = (g_date_time_compare (self->current_week, self->week) == 1);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->next_button), sensitive);
+}
+
+static void
+add_record (CcLoginHistoryDialog *self, GDateTime *datetime, gchar *record_string, gint line)
+{
+ g_autofree gchar *date = NULL;
+ g_autofree gchar *time = NULL;
+ g_autofree gchar *str = NULL;
+ GtkWidget *label, *row;
+
+ date = cc_util_get_smart_date (datetime);
+ /* Translators: This is a time format string in the style of "22:58".
+ It indicates a login time which follows a date. */
+ time = g_date_time_format (datetime, C_("login date-time", "%k:%M"));
+ /* Translators: This indicates a login date-time.
+ The first %s is a date, and the second %s a time. */
+ str = g_strdup_printf(C_("login date-time", "%s, %s"), date, time);
+
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_show (row);
+ gtk_box_set_homogeneous (GTK_BOX (row), TRUE);
+ gtk_container_set_border_width (GTK_CONTAINER (row), 6);
+
+ label = gtk_label_new (record_string);
+ gtk_widget_show (label);
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+ gtk_box_pack_start (GTK_BOX (row), label, TRUE, TRUE, 0);
+
+ label = gtk_label_new (str);
+ gtk_widget_show (label);
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+ gtk_box_pack_start (GTK_BOX (row), label, TRUE, TRUE, 0);
+
+ gtk_list_box_insert (self->history_box, row, line);
+}
+
+static void
+show_week (CcLoginHistoryDialog *self)
+{
+ g_autoptr(GArray) login_history = NULL;
+ g_autoptr(GDateTime) datetime = NULL;
+ g_autoptr(GDateTime) temp = NULL;
+ gint64 from, to;
+ gint i, line;
+ CcLoginHistory history;
+
+ show_week_label (self);
+ clear_history (self);
+ set_sensitivity (self);
+
+ login_history = get_login_history (self->user);
+ if (login_history == NULL) {
+ return;
+ }
+
+ /* Find first record for week */
+ from = g_date_time_to_unix (self->week);
+ temp = g_date_time_add_weeks (self->week, 1);
+ to = g_date_time_to_unix (temp);
+ for (i = login_history->len - 1; i >= 0; i--) {
+ history = g_array_index (login_history, CcLoginHistory, i);
+ if (history.login_time < to) {
+ break;
+ }
+ }
+
+ /* Add new session records */
+ line = 0;
+ for (;i >= 0; i--) {
+ history = g_array_index (login_history, CcLoginHistory, i);
+
+ /* Display only x-session and tty records */
+ if (!g_str_has_prefix (history.type, ":") &&
+ !g_str_has_prefix (history.type, "tty")) {
+ continue;
+ }
+
+ if (history.logout_time > 0 && history.logout_time < from) {
+ break;
+ }
+
+ if (history.logout_time > 0 && history.logout_time < to) {
+ datetime = g_date_time_new_from_unix_local (history.logout_time);
+ add_record (self, datetime, _("Session Ended"), line);
+ line++;
+ }
+
+ if (history.login_time >= from) {
+ datetime = g_date_time_new_from_unix_local (history.login_time);
+ add_record (self, datetime, _("Session Started"), line);
+ line++;
+ }
+ }
+}
+
+static void
+previous_button_clicked_cb (CcLoginHistoryDialog *self)
+{
+ g_autoptr(GDateTime) temp = NULL;
+
+ temp = self->week;
+ self->week = g_date_time_add_weeks (self->week, -1);
+
+ show_week (self);
+}
+
+static void
+next_button_clicked_cb (CcLoginHistoryDialog *self)
+{
+ g_autoptr(GDateTime) temp = NULL;
+
+ temp = self->week;
+ self->week = g_date_time_add_weeks (self->week, 1);
+
+ show_week (self);
+}
+
+static void
+cc_login_history_dialog_dispose (GObject *object)
+{
+ CcLoginHistoryDialog *self = CC_LOGIN_HISTORY_DIALOG (object);
+
+ g_clear_object (&self->user);
+ g_clear_pointer (&self->week, g_date_time_unref);
+ g_clear_pointer (&self->current_week, g_date_time_unref);
+
+ G_OBJECT_CLASS (cc_login_history_dialog_parent_class)->dispose (object);
+}
+
+void
+cc_login_history_dialog_class_init (CcLoginHistoryDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_login_history_dialog_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-login-history-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, header_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, history_box);
+ gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, next_button);
+ gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, previous_button);
+
+ gtk_widget_class_bind_template_callback (widget_class, next_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, previous_button_clicked_cb);
+}
+
+void
+cc_login_history_dialog_init (CcLoginHistoryDialog *self)
+{
+ g_resources_register (cc_user_accounts_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+CcLoginHistoryDialog *
+cc_login_history_dialog_new (ActUser *user)
+{
+ CcLoginHistoryDialog *self;
+ g_autoptr(GDateTime) temp = NULL;
+ g_autoptr(GDateTime) local = NULL;
+ g_autofree gchar *title = NULL;
+
+ g_return_val_if_fail (ACT_IS_USER (user), NULL);
+
+ self = g_object_new (CC_TYPE_LOGIN_HISTORY_DIALOG,
+ "use-header-bar", 1,
+ NULL);
+
+ self->user = g_object_ref (user);
+
+ /* Set the first day of this week */
+ local = g_date_time_new_now_local ();
+ temp = g_date_time_new_local (g_date_time_get_year (local),
+ g_date_time_get_month (local),
+ g_date_time_get_day_of_month (local),
+ 0, 0, 0);
+ self->week = g_date_time_add_days (temp, 1 - g_date_time_get_day_of_week (temp));
+ self->current_week = g_date_time_ref (self->week);
+
+ /* Translators: This is the title of the "Account Activity" dialog.
+ The %s is the user real name. */
+ title = g_strdup_printf (_("%s — Account Activity"),
+ act_user_get_real_name (self->user));
+ gtk_header_bar_set_title (self->header_bar, title);
+
+ show_week (self);
+
+ return self;
+}
diff --git a/panels/user-accounts/cc-login-history-dialog.h b/panels/user-accounts/cc-login-history-dialog.h
new file mode 100644
index 0000000..e71f160
--- /dev/null
+++ b/panels/user-accounts/cc-login-history-dialog.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Ondrej Holy <oholy@redhat.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <act/act-user.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_LOGIN_HISTORY_DIALOG (cc_login_history_dialog_get_type ())
+G_DECLARE_FINAL_TYPE (CcLoginHistoryDialog, cc_login_history_dialog, CC, LOGIN_HISTORY_DIALOG, GtkDialog)
+
+CcLoginHistoryDialog *cc_login_history_dialog_new (ActUser *user);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-login-history-dialog.ui b/panels/user-accounts/cc-login-history-dialog.ui
new file mode 100644
index 0000000..95b24e7
--- /dev/null
+++ b/panels/user-accounts/cc-login-history-dialog.ui
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.8 -->
+ <template class="CcLoginHistoryDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="icon_name">system-users</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar" id="header_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <style>
+ <class name="linked"/>
+ </style>
+ <child>
+ <object class="GtkButton" id="previous_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="previous_button_clicked_cb" object="CcLoginHistoryDialog" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="pixel_size">16</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="next_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="next_button_clicked_cb" object="CcLoginHistoryDialog" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-next-symbolic</property>
+ <property name="pixel_size">16</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="border_width">0</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="width_request">350</property>
+ <property name="height_request">300</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="shadow_type">none</property>
+ <child>
+ <object class="GtkListBox" id="history_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="border_width">12</property>
+ <property name="selection_mode">none</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/user-accounts/cc-password-dialog.c b/panels/user-accounts/cc-password-dialog.c
new file mode 100644
index 0000000..b199999
--- /dev/null
+++ b/panels/user-accounts/cc-password-dialog.c
@@ -0,0 +1,538 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+#include "cc-password-dialog.h"
+#include "cc-user-accounts-resources.h"
+#include "pw-utils.h"
+#include "run-passwd.h"
+#include "user-utils.h"
+
+#define PASSWORD_CHECK_TIMEOUT 600
+
+struct _CcPasswordDialog
+{
+ GtkDialog parent_instance;
+
+ GtkBox *action_radio_box;
+ GtkRadioButton *action_now_radio;
+ GtkRadioButton *action_login_radio;
+ GtkButton *ok_button;
+ GtkLabel *old_password_label;
+ GtkEntry *old_password_entry;
+ GtkEntry *password_entry;
+ GtkLabel *password_hint_label;
+ GtkLevelBar *strength_indicator;
+ GtkEntry *verify_entry;
+ GtkLabel *verify_hint_label;
+
+ gint password_entry_timeout_id;
+
+ ActUser *user;
+ ActUserPasswordMode password_mode;
+
+ gboolean old_password_ok;
+ gint old_password_entry_timeout_id;
+
+ PasswdHandler *passwd_handler;
+};
+
+G_DEFINE_TYPE (CcPasswordDialog, cc_password_dialog, GTK_TYPE_DIALOG)
+
+static gint
+update_password_strength (CcPasswordDialog *self)
+{
+ const gchar *password;
+ const gchar *old_password;
+ const gchar *username;
+ gint strength_level;
+ const gchar *hint;
+ const gchar *verify;
+
+ password = gtk_entry_get_text (self->password_entry);
+ old_password = gtk_entry_get_text (self->old_password_entry);
+ username = act_user_get_user_name (self->user);
+
+ pw_strength (password, old_password, username,
+ &hint, &strength_level);
+
+ gtk_level_bar_set_value (self->strength_indicator, strength_level);
+ gtk_label_set_label (self->password_hint_label, hint);
+
+ if (strength_level > 1) {
+ set_entry_validation_checkmark (self->password_entry);
+ } else if (strlen (password) == 0) {
+ set_entry_generation_icon (self->password_entry);
+ } else {
+ clear_entry_validation_error (self->password_entry);
+ }
+
+ verify = gtk_entry_get_text (self->verify_entry);
+ if (strlen (verify) == 0) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), strength_level > 1);
+ }
+
+ return strength_level;
+}
+
+static void
+password_changed_cb (PasswdHandler *handler,
+ GError *error,
+ CcPasswordDialog *self)
+{
+ GtkWidget *dialog;
+ const gchar *primary_text;
+ const gchar *secondary_text;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE);
+ gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (self)), NULL);
+
+ if (!error) {
+ gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT);
+ return;
+ }
+
+ if (error->code == PASSWD_ERROR_REJECTED) {
+ primary_text = error->message;
+ secondary_text = _("Please choose another password.");
+
+ gtk_entry_set_text (self->password_entry, "");
+ gtk_widget_grab_focus (GTK_WIDGET (self->password_entry));
+
+ gtk_entry_set_text (self->verify_entry, "");
+ }
+ else if (error->code == PASSWD_ERROR_AUTH_FAILED) {
+ primary_text = error->message;
+ secondary_text = _("Please type your current password again.");
+
+ gtk_entry_set_text (self->old_password_entry, "");
+ gtk_widget_grab_focus (GTK_WIDGET (self->old_password_entry));
+ }
+ else {
+ primary_text = _("Password could not be changed");
+ secondary_text = error->message;
+ }
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (self),
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s", primary_text);
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ "%s", secondary_text);
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (dialog);
+}
+
+static void
+ok_button_clicked_cb (CcPasswordDialog *self)
+{
+ const gchar *password;
+
+ password = gtk_entry_get_text (self->password_entry);
+
+ switch (self->password_mode) {
+ case ACT_USER_PASSWORD_MODE_REGULAR:
+ if (act_user_get_uid (self->user) == getuid ()) {
+ GdkDisplay *display;
+ g_autoptr(GdkCursor) cursor = NULL;
+
+ /* When setting a password for the current user,
+ * use passwd directly, to preserve the audit trail
+ * and to e.g. update the keyring password.
+ */
+ passwd_change_password (self->passwd_handler, password,
+ (PasswdCallback) password_changed_cb, self);
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+ display = gtk_widget_get_display (GTK_WIDGET (self));
+ cursor = gdk_cursor_new_for_display (display, GDK_WATCH);
+ gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (self)), cursor);
+ gdk_display_flush (display);
+ return;
+ }
+
+ act_user_set_password_mode (self->user, ACT_USER_PASSWORD_MODE_REGULAR);
+ act_user_set_password (self->user, password, "");
+ break;
+
+ case ACT_USER_PASSWORD_MODE_SET_AT_LOGIN:
+ act_user_set_password_mode (self->user, self->password_mode);
+ act_user_set_automatic_login (self->user, FALSE);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT);
+}
+
+static void
+update_sensitivity (CcPasswordDialog *self)
+{
+ const gchar *password, *verify;
+ gboolean can_change;
+ int strength;
+
+ password = gtk_entry_get_text (self->password_entry);
+ verify = gtk_entry_get_text (self->verify_entry);
+
+ if (self->password_mode == ACT_USER_PASSWORD_MODE_REGULAR) {
+ strength = update_password_strength (self);
+ can_change = strength > 1 && strcmp (password, verify) == 0 &&
+ (self->old_password_ok || !gtk_widget_get_visible (GTK_WIDGET (self->old_password_entry)));
+ }
+ else {
+ can_change = TRUE;
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), can_change);
+}
+
+static void
+mode_change (CcPasswordDialog *self,
+ ActUserPasswordMode mode)
+{
+ gboolean active;
+
+ active = (mode == ACT_USER_PASSWORD_MODE_REGULAR);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->password_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->old_password_entry), active);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->password_hint_label), active);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->action_now_radio), active);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->action_login_radio), !active);
+
+ self->password_mode = mode;
+ update_sensitivity (self);
+}
+
+static void
+action_now_radio_toggled_cb (CcPasswordDialog *self)
+{
+ gint active;
+ ActUserPasswordMode mode;
+
+ active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->action_now_radio));
+ mode = active ? ACT_USER_PASSWORD_MODE_REGULAR : ACT_USER_PASSWORD_MODE_SET_AT_LOGIN;
+ mode_change (self, mode);
+}
+
+static void
+update_password_match (CcPasswordDialog *self)
+{
+ const gchar *password;
+ const gchar *verify;
+ const gchar *message = "";
+
+ password = gtk_entry_get_text (self->password_entry);
+ verify = gtk_entry_get_text (self->verify_entry);
+
+ if (strlen (verify) > 0) {
+ if (strcmp (password, verify) != 0) {
+ message = _("The passwords do not match.");
+ }
+ else {
+ set_entry_validation_checkmark (self->verify_entry);
+ }
+ }
+ gtk_label_set_label (self->verify_hint_label, message);
+}
+
+static gboolean
+password_entry_timeout (CcPasswordDialog *self)
+{
+ update_password_strength (self);
+ update_sensitivity (self);
+ update_password_match (self);
+
+ self->password_entry_timeout_id = 0;
+
+ return FALSE;
+}
+
+static void
+recheck_password_match (CcPasswordDialog *self)
+{
+ const gchar *password;
+
+ if (self->password_entry_timeout_id != 0) {
+ g_source_remove (self->password_entry_timeout_id);
+ self->password_entry_timeout_id = 0;
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), FALSE);
+
+ password = gtk_entry_get_text (self->password_entry);
+ if (strlen (password) == 0) {
+ gtk_entry_set_visibility (self->password_entry, FALSE);
+ }
+
+ self->password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT,
+ (GSourceFunc) password_entry_timeout,
+ self);
+}
+
+static void
+password_entry_changed (CcPasswordDialog *self)
+{
+ clear_entry_validation_error (self->password_entry);
+ clear_entry_validation_error (self->verify_entry);
+ recheck_password_match (self);
+}
+
+static void
+verify_entry_changed (CcPasswordDialog *self)
+{
+ clear_entry_validation_error (self->verify_entry);
+ recheck_password_match (self);
+}
+
+static gboolean
+password_entry_focus_out_cb (CcPasswordDialog *self)
+{
+ if (self->password_entry_timeout_id != 0) {
+ g_source_remove (self->password_entry_timeout_id);
+ self->password_entry_timeout_id = 0;
+ }
+
+ if (self->user != NULL)
+ password_entry_timeout (self);
+
+ return FALSE;
+}
+
+static gboolean
+password_entry_key_press_cb (CcPasswordDialog *self,
+ GdkEvent *event)
+{
+ GdkEventKey *key = (GdkEventKey *)event;
+
+ if (self->password_entry_timeout_id != 0) {
+ g_source_remove (self->password_entry_timeout_id);
+ self->password_entry_timeout_id = 0;
+ }
+
+ if (key->keyval == GDK_KEY_Tab)
+ password_entry_timeout (self);
+
+ return FALSE;
+}
+
+static void
+auth_cb (PasswdHandler *handler,
+ GError *error,
+ CcPasswordDialog *self)
+{
+ if (error) {
+ self->old_password_ok = FALSE;
+ }
+ else {
+ self->old_password_ok = TRUE;
+ set_entry_validation_checkmark (self->old_password_entry);
+ }
+
+ update_sensitivity (self);
+}
+
+static gboolean
+old_password_entry_timeout (CcPasswordDialog *self)
+{
+ const gchar *text;
+
+ update_sensitivity (self);
+
+ text = gtk_entry_get_text (self->old_password_entry);
+ if (!self->old_password_ok) {
+ passwd_authenticate (self->passwd_handler, text, (PasswdCallback)auth_cb, self);
+ }
+
+ self->old_password_entry_timeout_id = 0;
+
+ return FALSE;
+}
+
+static gboolean
+old_password_entry_focus_out_cb (CcPasswordDialog *self)
+{
+ if (self->old_password_entry_timeout_id != 0) {
+ g_source_remove (self->old_password_entry_timeout_id);
+ self->old_password_entry_timeout_id = 0;
+ }
+
+ if (self->user != NULL)
+ old_password_entry_timeout (self);
+
+ return FALSE;
+}
+
+static void
+old_password_entry_changed (CcPasswordDialog *self)
+{
+ if (self->old_password_entry_timeout_id != 0) {
+ g_source_remove (self->old_password_entry_timeout_id);
+ self->old_password_entry_timeout_id = 0;
+ }
+
+ clear_entry_validation_error (self->old_password_entry);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), FALSE);
+
+ self->old_password_ok = FALSE;
+ self->old_password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT,
+ (GSourceFunc) old_password_entry_timeout,
+ self);
+}
+
+static void
+password_entry_icon_press_cb (CcPasswordDialog *self)
+{
+ g_autofree gchar *pwd = NULL;
+
+ pwd = pw_generate ();
+ if (pwd == NULL)
+ return;
+
+ gtk_entry_set_text (self->password_entry, pwd);
+ gtk_entry_set_text (self->verify_entry, pwd);
+ gtk_entry_set_visibility (self->password_entry, TRUE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), TRUE);
+}
+
+static void
+cc_password_dialog_dispose (GObject *object)
+{
+ CcPasswordDialog *self = CC_PASSWORD_DIALOG (object);
+
+ g_clear_object (&self->user);
+
+ if (self->passwd_handler) {
+ passwd_destroy (self->passwd_handler);
+ self->passwd_handler = NULL;
+ }
+
+ if (self->old_password_entry_timeout_id != 0) {
+ g_source_remove (self->old_password_entry_timeout_id);
+ self->old_password_entry_timeout_id = 0;
+ }
+
+ if (self->password_entry_timeout_id != 0) {
+ g_source_remove (self->password_entry_timeout_id);
+ self->password_entry_timeout_id = 0;
+ }
+
+ G_OBJECT_CLASS (cc_password_dialog_parent_class)->dispose (object);
+}
+
+static void
+cc_password_dialog_class_init (CcPasswordDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_password_dialog_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-password-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, action_radio_box);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, action_now_radio);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, action_login_radio);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, ok_button);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, old_password_label);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, old_password_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_hint_label);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, strength_indicator);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, verify_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, verify_hint_label);
+
+ gtk_widget_class_bind_template_callback (widget_class, action_now_radio_toggled_cb);
+ gtk_widget_class_bind_template_callback (widget_class, old_password_entry_changed);
+ gtk_widget_class_bind_template_callback (widget_class, old_password_entry_focus_out_cb);
+ gtk_widget_class_bind_template_callback (widget_class, ok_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, password_entry_changed);
+ gtk_widget_class_bind_template_callback (widget_class, password_entry_focus_out_cb);
+ gtk_widget_class_bind_template_callback (widget_class, password_entry_icon_press_cb);
+ gtk_widget_class_bind_template_callback (widget_class, password_entry_key_press_cb);
+ gtk_widget_class_bind_template_callback (widget_class, verify_entry_changed);
+}
+
+static void
+cc_password_dialog_init (CcPasswordDialog *self)
+{
+ g_resources_register (cc_user_accounts_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+CcPasswordDialog *
+cc_password_dialog_new (ActUser *user)
+{
+ CcPasswordDialog *self;
+
+ g_return_val_if_fail (ACT_IS_USER (user), NULL);
+
+ self = g_object_new (CC_TYPE_PASSWORD_DIALOG,
+ "use-header-bar", 1,
+ NULL);
+
+ self->user = g_object_ref (user);
+
+ if (act_user_get_uid (self->user) == getuid ()) {
+ gboolean visible;
+
+ mode_change (self, ACT_USER_PASSWORD_MODE_REGULAR);
+ gtk_widget_hide (GTK_WIDGET (self->action_radio_box));
+
+ visible = (act_user_get_password_mode (user) != ACT_USER_PASSWORD_MODE_NONE);
+ gtk_widget_set_visible (GTK_WIDGET (self->old_password_label), visible);
+ gtk_widget_set_visible (GTK_WIDGET (self->old_password_entry), visible);
+ self->old_password_ok = !visible;
+
+ self->passwd_handler = passwd_init ();
+ }
+ else {
+ mode_change (self, ACT_USER_PASSWORD_MODE_SET_AT_LOGIN);
+ gtk_widget_show (GTK_WIDGET (self->action_radio_box));
+
+ gtk_widget_hide (GTK_WIDGET (self->old_password_label));
+ gtk_widget_hide (GTK_WIDGET (self->old_password_entry));
+ self->old_password_ok = TRUE;
+ }
+
+ if (self->old_password_ok == FALSE)
+ gtk_widget_grab_focus (GTK_WIDGET (self->old_password_entry));
+ else
+ gtk_widget_grab_focus (GTK_WIDGET (self->password_entry));
+
+ gtk_widget_grab_default (GTK_WIDGET (self->ok_button));
+
+ return self;
+}
diff --git a/panels/user-accounts/cc-password-dialog.h b/panels/user-accounts/cc-password-dialog.h
new file mode 100644
index 0000000..958366b
--- /dev/null
+++ b/panels/user-accounts/cc-password-dialog.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_PASSWORD_DIALOG (cc_password_dialog_get_type ())
+G_DECLARE_FINAL_TYPE (CcPasswordDialog, cc_password_dialog, CC, PASSWORD_DIALOG, GtkDialog)
+
+CcPasswordDialog *cc_password_dialog_new (ActUser *user);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-password-dialog.ui b/panels/user-accounts/cc-password-dialog.ui
new file mode 100644
index 0000000..bfcc758
--- /dev/null
+++ b/panels/user-accounts/cc-password-dialog.ui
@@ -0,0 +1,305 @@
+<?xml version="1.0"?>
+<interface>
+ <!-- interface-requires gtk+ 2.12 -->
+ <!-- interface-naming-policy toplevel-contextual -->
+ <template class="CcPasswordDialog" parent="GtkDialog">
+ <property name="border_width">6</property>
+ <property name="title" translatable="yes">Change Password</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-on-parent</property>
+ <property name="icon_name">system-users</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar" id="dialog-header-bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="show_close_button">False</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="text-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ok_button">
+ <property name="label" translatable="yes">Ch_ange</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="ok_button_clicked_cb" object="CcPasswordDialog" swapped="yes"/>
+ <style>
+ <class name="text-button"/>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">6</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="column_spacing">6</property>
+ <property name="row_spacing">6</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkEntry" id="verify_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="hexpand">True</property>
+ <property name="activates_default">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="notify::text" handler="verify_entry_changed" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="activate" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="password_entry_focus_out_cb" after="yes" object="CcPasswordDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="password_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="height-request">50</property>
+ <property name="wrap">True</property>
+ <property name="hexpand">True</property>
+ <property name="wrap_mode">word-char</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="verify_hint_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes"></property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="wrap">True</property>
+ <property name="hexpand">True</property>
+ <property name="wrap_mode">word-char</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Confirm New Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">verify_entry</property>
+ <property name="margin_start">25</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_New Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">password_entry</property>
+ <property name="margin_start">25</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="hexpand">True</property>
+ <property name="activates_default">True</property>
+ <property name="input_purpose">password</property>
+ <signal name="notify::text" handler="password_entry_changed" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="activate" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="password_entry_focus_out_cb" after="yes" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="key-press-event" handler="password_entry_key_press_cb" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="icon-press" handler="password_entry_icon_press_cb" object="CcPasswordDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLevelBar" id="strength_indicator">
+ <property name="visible">True</property>
+ <property name="mode">discrete</property>
+ <property name="max-value">5</property>
+ <offsets>
+ <offset name="strength-weak" value="1"/>
+ <offset name="strength-low" value="2"/>
+ <offset name="strength-medium" value="3"/>
+ <offset name="strength-good" value="4"/>
+ <offset name="strength-high" value="5"/>
+ </offsets>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="old_password_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Current _Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">old_password_entry</property>
+ <property name="margin_start">25</property>
+ <property name="margin_bottom">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="old_password_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="visibility">False</property>
+ <property name="hexpand">True</property>
+ <property name="activates_default">True</property>
+ <property name="margin_bottom">12</property>
+ <property name="input_purpose">password</property>
+ <signal name="notify::text" handler="old_password_entry_changed" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="activate" handler="old_password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/>
+ <signal name="focus-out-event" handler="old_password_entry_focus_out_cb" after="yes" object="CcPasswordDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="action_radio_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRadioButton" id="action_login_radio">
+ <property name="label" translatable="yes">Allow user to change their password on next login</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="action_now_radio">
+ <property name="label" translatable="yes">Set a password now</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">action_login_radio</property>
+ <signal name="toggled" handler="action_now_radio_toggled_cb" object="CcPasswordDialog" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="0">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/panels/user-accounts/cc-realm-manager.c b/panels/user-accounts/cc-realm-manager.c
new file mode 100644
index 0000000..9b8077e
--- /dev/null
+++ b/panels/user-accounts/cc-realm-manager.c
@@ -0,0 +1,812 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Stef Walter <stefw@gnome.org>
+ */
+
+#include "config.h"
+
+#include "cc-realm-manager.h"
+
+#include <krb5/krb5.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <fcntl.h>
+
+
+struct _CcRealmManager {
+ CcRealmObjectManagerClient parent_instance;
+
+ CcRealmProvider *provider;
+ guint diagnostics_sig;
+};
+
+enum {
+ REALM_ADDED,
+ NUM_SIGNALS,
+};
+
+static gint signals[NUM_SIGNALS] = { 0, };
+
+G_DEFINE_TYPE (CcRealmManager, cc_realm_manager, CC_REALM_TYPE_OBJECT_MANAGER_CLIENT);
+
+GQuark
+cc_realm_error_get_quark (void)
+{
+ static GQuark quark = 0;
+ if (quark == 0)
+ quark = g_quark_from_static_string ("cc-realm-error");
+ return quark;
+}
+
+static gboolean
+is_realm_with_kerberos_and_membership (gpointer object)
+{
+ GDBusInterface *interface;
+
+ if (!G_IS_DBUS_OBJECT (object))
+ return FALSE;
+
+ interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.Kerberos");
+ if (interface == NULL)
+ return FALSE;
+ g_object_unref (interface);
+
+ interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.KerberosMembership");
+ if (interface == NULL)
+ return FALSE;
+ g_object_unref (interface);
+
+ return TRUE;
+}
+
+static void
+on_interface_added (CcRealmManager *self,
+ GDBusObject *object,
+ GDBusInterface *interface)
+{
+ g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (interface), G_MAXINT);
+}
+
+static void
+on_object_added (CcRealmManager *self,
+ GDBusObject *object)
+{
+ GList *interfaces, *l;
+
+ interfaces = g_dbus_object_get_interfaces (object);
+ for (l = interfaces; l != NULL; l = g_list_next (l))
+ on_interface_added (self, object, l->data);
+ g_list_free_full (interfaces, g_object_unref);
+
+ if (is_realm_with_kerberos_and_membership (object)) {
+ g_debug ("Saw realm: %s", g_dbus_object_get_object_path (object));
+ g_signal_emit (self, signals[REALM_ADDED], 0, object);
+ }
+}
+
+static void
+cc_realm_manager_init (CcRealmManager *self)
+{
+ g_signal_connect (self, "object-added", G_CALLBACK (on_object_added), NULL);
+ g_signal_connect (self, "interface-added", G_CALLBACK (on_interface_added), NULL);
+}
+
+static void
+cc_realm_manager_dispose (GObject *obj)
+{
+ CcRealmManager *self = CC_REALM_MANAGER (obj);
+ GDBusConnection *connection;
+
+ g_clear_object (&self->provider);
+
+ if (self->diagnostics_sig) {
+ connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (self));
+ if (connection != NULL)
+ g_dbus_connection_signal_unsubscribe (connection, self->diagnostics_sig);
+ self->diagnostics_sig = 0;
+ }
+
+ G_OBJECT_CLASS (cc_realm_manager_parent_class)->dispose (obj);
+}
+
+static void
+cc_realm_manager_class_init (CcRealmManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = cc_realm_manager_dispose;
+
+ signals[REALM_ADDED] = g_signal_new ("realm-added", CC_TYPE_REALM_MANAGER,
+ G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, CC_REALM_TYPE_OBJECT);
+}
+
+static void
+on_realm_diagnostics (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ const gchar *message;
+ const gchar *unused;
+
+ if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ss)"))) {
+ /* Data is already formatted appropriately for stderr */
+ g_variant_get (parameters, "(&s&s)", &message, &unused);
+ g_printerr ("%s", message);
+ }
+}
+
+static void
+on_provider_new (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ CcRealmManager *manager = g_task_get_task_data (task);
+ GError *error = NULL;
+
+ manager->provider = cc_realm_provider_proxy_new_finish (result, &error);
+ if (error == NULL) {
+ g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (manager->provider), -1);
+ g_debug ("Created realm manager");
+ g_task_return_pointer (task, g_object_ref (manager), g_object_unref);
+ } else {
+ g_task_return_error (task, error);
+ }
+
+ g_object_unref (task);
+}
+
+static void
+on_manager_new (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ CcRealmManager *manager;
+ GDBusConnection *connection;
+ GError *error = NULL;
+ GObject *object;
+ guint sig;
+
+ object = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, &error);
+ if (error == NULL) {
+ manager = CC_REALM_MANAGER (object);
+ connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (object));
+
+ g_debug ("Connected to realmd");
+
+ sig = g_dbus_connection_signal_subscribe (connection,
+ "org.freedesktop.realmd",
+ "org.freedesktop.realmd.Service",
+ "Diagnostics",
+ NULL,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ on_realm_diagnostics,
+ NULL,
+ NULL);
+ manager->diagnostics_sig = sig;
+
+ g_task_set_task_data (task, manager, g_object_unref);
+
+ cc_realm_provider_proxy_new (connection,
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
+ "org.freedesktop.realmd",
+ "/org/freedesktop/realmd",
+ g_task_get_cancellable (task),
+ on_provider_new, task);
+ } else {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+ }
+}
+
+void
+cc_realm_manager_new (GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task;
+
+ g_debug ("Connecting to realmd...");
+
+ task = g_task_new (NULL, cancellable, callback, user_data);
+ g_task_set_source_tag (task, cc_realm_manager_new);
+
+ g_async_initable_new_async (CC_TYPE_REALM_MANAGER, G_PRIORITY_DEFAULT,
+ cancellable, on_manager_new, task,
+ "flags", G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
+ "name", "org.freedesktop.realmd",
+ "bus-type", G_BUS_TYPE_SYSTEM,
+ "object-path", "/org/freedesktop/realmd",
+ "get-proxy-type-func", cc_realm_object_manager_client_get_proxy_type,
+ NULL);
+}
+
+CcRealmManager *
+cc_realm_manager_new_finish (GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, NULL), NULL);
+ g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_manager_new), NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+realms_free (gpointer data)
+{
+ g_list_free_full (data, g_object_unref);
+}
+
+static void
+on_provider_discover (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ CcRealmManager *manager = g_task_get_source_object (task);
+ GDBusObject *object;
+ GError *error = NULL;
+ gboolean no_membership = FALSE;
+ gchar **realms;
+ gint relevance;
+ gint i;
+ GList *kerberos_realms = NULL;
+
+ cc_realm_provider_call_discover_finish (CC_REALM_PROVIDER (source), &relevance,
+ &realms, result, &error);
+ if (error == NULL) {
+ for (i = 0; realms[i]; i++) {
+ object = g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (manager), realms[i]);
+ if (object == NULL) {
+ g_warning ("Realm is not in object manager: %s", realms[i]);
+ } else {
+ if (is_realm_with_kerberos_and_membership (object)) {
+ g_debug ("Discovered realm: %s", realms[i]);
+ kerberos_realms = g_list_prepend (kerberos_realms, object);
+ } else {
+ g_debug ("Realm does not support kerberos membership: %s", realms[i]);
+ no_membership = TRUE;
+ g_object_unref (object);
+ }
+ }
+ }
+ g_strfreev (realms);
+
+ if (!kerberos_realms && no_membership) {
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC,
+ _("Cannot automatically join this type of domain"));
+ } else if (!kerberos_realms) {
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC,
+ _("No such domain or realm found"));
+ } else {
+ kerberos_realms = g_list_reverse (kerberos_realms);
+ g_task_return_pointer (task, kerberos_realms, realms_free);
+ }
+ } else {
+ g_task_return_error (task, error);
+ }
+
+ g_object_unref (task);
+}
+
+void
+cc_realm_manager_discover (CcRealmManager *self,
+ const gchar *input,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task;
+ GVariant *options;
+
+ g_return_if_fail (CC_IS_REALM_MANAGER (self));
+ g_return_if_fail (input != NULL);
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ g_debug ("Discovering realms for: %s", input);
+
+ task = g_task_new (G_OBJECT (self), cancellable, callback, user_data);
+ g_task_set_source_tag (task, cc_realm_manager_discover);
+
+ options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
+
+ cc_realm_provider_call_discover (self->provider, input, options, cancellable,
+ on_provider_discover, task);
+}
+
+GList *
+cc_realm_manager_discover_finish (CcRealmManager *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (CC_IS_REALM_MANAGER (self), NULL);
+ g_return_val_if_fail (g_task_is_valid (result, G_OBJECT (self)), NULL);
+ g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_manager_discover), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+GList *
+cc_realm_manager_get_realms (CcRealmManager *self)
+{
+ GList *objects;
+ GList *realms = NULL;
+ GList *l;
+
+ g_return_val_if_fail (CC_IS_REALM_MANAGER (self), NULL);
+
+ objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self));
+ for (l = objects; l != NULL; l = g_list_next (l)) {
+ if (is_realm_with_kerberos_and_membership (l->data))
+ realms = g_list_prepend (realms, g_object_ref (l->data));
+ }
+
+ g_list_free_full (objects, g_object_unref);
+ return realms;
+}
+
+static void
+string_replace (GString *string,
+ const gchar *find,
+ const gchar *replace)
+{
+ const gchar *at;
+ gssize pos;
+
+ at = strstr (string->str, find);
+ if (at != NULL) {
+ pos = at - string->str;
+ g_string_erase (string, pos, strlen (find));
+ g_string_insert (string, pos, replace);
+ }
+}
+
+gchar *
+cc_realm_calculate_login (CcRealmCommon *realm,
+ const gchar *username)
+{
+ GString *string;
+ const gchar *const *formats;
+ gchar *login = NULL;
+
+ formats = cc_realm_common_get_login_formats (realm);
+ if (formats[0] != NULL) {
+ string = g_string_new (formats[0]);
+ string_replace (string, "%U", username);
+ string_replace (string, "%D", cc_realm_common_get_name (realm));
+ login = g_string_free (string, FALSE);
+ }
+
+ return login;
+
+}
+
+gboolean
+cc_realm_is_configured (CcRealmObject *realm)
+{
+ CcRealmCommon *common;
+ const gchar *configured;
+ gboolean is = FALSE;
+
+ common = cc_realm_object_get_common (realm);
+ if (common != NULL) {
+ configured = cc_realm_common_get_configured (common);
+ is = configured != NULL && !g_str_equal (configured, "");
+ g_object_unref (common);
+ }
+
+ return is;
+}
+
+static const gchar *
+find_supported_credentials (CcRealmKerberosMembership *membership,
+ const gchar *owner)
+{
+ const gchar *cred_owner;
+ const gchar *cred_type;
+ GVariant *supported;
+ GVariantIter iter;
+
+ supported = cc_realm_kerberos_membership_get_supported_join_credentials (membership);
+ g_return_val_if_fail (supported != NULL, NULL);
+
+ g_variant_iter_init (&iter, supported);
+ while (g_variant_iter_loop (&iter, "(&s&s)", &cred_type, &cred_owner)) {
+ if (g_str_equal (owner, cred_owner)) {
+ if (g_str_equal (cred_type, "ccache") ||
+ g_str_equal (cred_type, "password")) {
+ return g_intern_string (cred_type);
+ }
+ }
+ }
+
+ return NULL;
+}
+
+static gboolean
+realm_join_as_owner (CcRealmObject *realm,
+ const gchar *owner,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ CcRealmKerberosMembership *membership;
+ GVariant *contents;
+ GVariant *options;
+ GVariant *option;
+ GVariant *creds;
+ const gchar *type;
+
+ membership = cc_realm_object_get_kerberos_membership (realm);
+ g_return_val_if_fail (membership != NULL, FALSE);
+
+ type = find_supported_credentials (membership, owner);
+ if (type == NULL) {
+ g_debug ("Couldn't find supported credential type for owner: %s", owner);
+ g_object_unref (membership);
+ return FALSE;
+ }
+
+ if (g_str_equal (type, "ccache")) {
+ g_debug ("Using a kerberos credential cache to join the realm");
+ contents = g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
+ g_bytes_get_data (credentials, NULL),
+ g_bytes_get_size (credentials),
+ TRUE, (GDestroyNotify)g_bytes_unref, credentials);
+
+ } else if (g_str_equal (type, "password")) {
+ g_debug ("Using a user/password to join the realm");
+ contents = g_variant_new ("(ss)", login, password);
+
+ } else {
+ g_assert_not_reached ();
+ }
+
+ creds = g_variant_new ("(ssv)", type, owner, contents);
+ option = g_variant_new ("{sv}", "manage-system", g_variant_new_boolean (FALSE));
+ options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), &option, 1);
+
+ g_debug ("Calling the Join() method with %s credentials", owner);
+
+ cc_realm_kerberos_membership_call_join (membership, creds, options,
+ cancellable, callback, user_data);
+ g_object_unref (membership);
+
+ return TRUE;
+}
+
+gboolean
+cc_realm_join_as_user (CcRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE);
+ g_return_val_if_fail (credentials != NULL, FALSE);
+ g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
+ g_return_val_if_fail (login != NULL, FALSE);
+ g_return_val_if_fail (password != NULL, FALSE);
+ g_return_val_if_fail (credentials != NULL, FALSE);
+
+ return realm_join_as_owner (realm, "user", login, password,
+ credentials, cancellable, callback, user_data);
+}
+
+gboolean
+cc_realm_join_as_admin (CcRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE);
+ g_return_val_if_fail (credentials != NULL, FALSE);
+ g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
+ g_return_val_if_fail (login != NULL, FALSE);
+ g_return_val_if_fail (password != NULL, FALSE);
+ g_return_val_if_fail (credentials != NULL, FALSE);
+
+ return realm_join_as_owner (realm, "administrator", login, password, credentials,
+ cancellable, callback, user_data);
+}
+
+gboolean
+cc_realm_join_finish (CcRealmObject *realm,
+ GAsyncResult *result,
+ GError **error)
+{
+ CcRealmKerberosMembership *membership;
+ GError *call_error = NULL;
+ gchar *dbus_error;
+
+ g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ membership = cc_realm_object_get_kerberos_membership (realm);
+ g_return_val_if_fail (membership != NULL, FALSE);
+
+ cc_realm_kerberos_membership_call_join_finish (membership, result, &call_error);
+ g_object_unref (membership);
+
+ if (call_error == NULL) {
+ g_debug ("Completed Join() method call");
+ return TRUE;
+ }
+
+ dbus_error = g_dbus_error_get_remote_error (call_error);
+ if (dbus_error == NULL) {
+ g_debug ("Join() failed because of %s", call_error->message);
+ g_propagate_error (error, call_error);
+ return FALSE;
+ }
+
+ g_dbus_error_strip_remote_error (call_error);
+
+ if (g_str_equal (dbus_error, "org.freedesktop.realmd.Error.AuthenticationFailed")) {
+ g_debug ("Join() failed because of invalid/insufficient credentials");
+ g_set_error (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN,
+ "%s", call_error->message);
+ g_error_free (call_error);
+ } else {
+ g_debug ("Join() failed because of %s", call_error->message);
+ g_propagate_error (error, call_error);
+ }
+
+ g_free (dbus_error);
+ return FALSE;
+}
+
+typedef struct {
+ gchar *domain;
+ gchar *realm;
+ gchar *user;
+ gchar *password;
+} LoginClosure;
+
+static void
+login_closure_free (gpointer data)
+{
+ LoginClosure *login = data;
+ g_free (login->domain);
+ g_free (login->realm);
+ g_free (login->user);
+ g_free (login->password);
+ g_slice_free (LoginClosure, login);
+}
+
+static krb5_error_code
+login_perform_kinit (krb5_context k5,
+ const gchar *realm,
+ const gchar *login,
+ const gchar *password,
+ const gchar *filename)
+{
+ krb5_get_init_creds_opt *opts;
+ krb5_error_code code;
+ krb5_principal principal;
+ krb5_ccache ccache;
+ krb5_creds creds;
+ gchar *name;
+
+ name = g_strdup_printf ("%s@%s", login, realm);
+ code = krb5_parse_name (k5, name, &principal);
+
+ if (code != 0) {
+ g_debug ("Couldn't parse principal name: %s: %s",
+ name, krb5_get_error_message (k5, code));
+ g_free (name);
+ return code;
+ }
+
+ g_debug ("Using principal name to kinit: %s", name);
+ g_free (name);
+
+ if (filename == NULL)
+ code = krb5_cc_default (k5, &ccache);
+ else
+ code = krb5_cc_resolve (k5, filename, &ccache);
+
+ if (code != 0) {
+ krb5_free_principal (k5, principal);
+ g_debug ("Couldn't open credential cache: %s: %s",
+ filename ? filename : "<default>",
+ krb5_get_error_message (k5, code));
+ return code;
+ }
+
+ code = krb5_get_init_creds_opt_alloc (k5, &opts);
+ g_return_val_if_fail (code == 0, code);
+
+ code = krb5_get_init_creds_opt_set_out_ccache (k5, opts, ccache);
+ g_return_val_if_fail (code == 0, code);
+
+ code = krb5_get_init_creds_password (k5, &creds, principal,
+ (char *)password,
+ NULL, 0, 0, NULL, opts);
+
+ krb5_get_init_creds_opt_free (k5, opts);
+ krb5_cc_close (k5, ccache);
+ krb5_free_principal (k5, principal);
+
+ if (code == 0) {
+ g_debug ("kinit succeeded");
+ krb5_free_cred_contents (k5, &creds);
+ } else {
+ g_debug ("kinit failed: %s", krb5_get_error_message (k5, code));
+ }
+
+ return code;
+}
+
+static void
+kinit_thread_func (GTask *task,
+ gpointer object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ LoginClosure *login = task_data;
+ krb5_context k5 = NULL;
+ krb5_error_code code;
+ GError *error = NULL;
+ gchar *filename = NULL;
+ gchar *contents;
+ gsize length;
+ gint temp_fd;
+
+ filename = g_build_filename (g_get_user_runtime_dir (),
+ "um-krb5-creds.XXXXXX", NULL);
+ temp_fd = g_mkstemp_full (filename, O_RDWR, S_IRUSR | S_IWUSR);
+ if (temp_fd == -1) {
+ g_warning ("Couldn't create credential cache file: %s: %s",
+ filename, g_strerror (errno));
+ g_free (filename);
+ filename = NULL;
+ } else {
+ close (temp_fd);
+ }
+
+ code = krb5_init_context (&k5);
+ if (code == 0) {
+ code = login_perform_kinit (k5, login->realm, login->user,
+ login->password, filename);
+ }
+
+ switch (code) {
+ case 0:
+ if (filename != NULL) {
+ g_file_get_contents (filename, &contents, &length, &error);
+ if (error == NULL) {
+ g_debug ("Read in credential cache: %s", filename);
+ } else {
+ g_warning ("Couldn't read credential cache: %s: %s",
+ filename, error->message);
+ g_error_free (error);
+ }
+
+ g_task_return_pointer (task, g_bytes_new_take (contents, length), (GDestroyNotify) g_bytes_unref);
+ }
+ break;
+
+ case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
+ case KRB5KDC_ERR_POLICY:
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN,
+ _("Cannot log in as %s at the %s domain"),
+ login->user, login->domain);
+ break;
+ case KRB5KDC_ERR_PREAUTH_FAILED:
+ case KRB5KRB_AP_ERR_BAD_INTEGRITY:
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD,
+ _("Invalid password, please try again"));
+ break;
+ case KRB5_PREAUTH_FAILED:
+ case KRB5KDC_ERR_KEY_EXP:
+ case KRB5KDC_ERR_CLIENT_REVOKED:
+ case KRB5KDC_ERR_ETYPE_NOSUPP:
+ case KRB5_PROG_ETYPE_NOSUPP:
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_CANNOT_AUTH,
+ _("Cannot log in as %s at the %s domain"),
+ login->user, login->domain);
+ break;
+ default:
+ g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC,
+ _("Couldn’t connect to the %s domain: %s"),
+ login->domain, krb5_get_error_message (k5, code));
+ break;
+ }
+
+ if (filename) {
+ g_unlink (filename);
+ g_debug ("Deleted credential cache: %s", filename);
+ g_free (filename);
+ }
+
+ if (k5)
+ krb5_free_context (k5);
+
+ g_object_unref (task);
+}
+
+void
+cc_realm_login (CcRealmObject *realm,
+ const gchar *user,
+ const gchar *password,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task;
+ LoginClosure *login;
+ CcRealmKerberos *kerberos;
+
+ g_return_if_fail (CC_REALM_IS_OBJECT (realm));
+ g_return_if_fail (user != NULL);
+ g_return_if_fail (password != NULL);
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ kerberos = cc_realm_object_get_kerberos (realm);
+ g_return_if_fail (kerberos != NULL);
+
+ task = g_task_new (NULL, cancellable, callback, user_data);
+ g_task_set_source_tag (task, cc_realm_login);
+
+ login = g_slice_new0 (LoginClosure);
+ login->domain = g_strdup (cc_realm_kerberos_get_domain_name (kerberos));
+ login->realm = g_strdup (cc_realm_kerberos_get_realm_name (kerberos));
+ login->user = g_strdup (user);
+ login->password = g_strdup (password);
+ g_task_set_task_data (task, login, login_closure_free);
+
+ g_task_set_return_on_cancel (task, TRUE);
+ g_task_run_in_thread (task, kinit_thread_func);
+
+ g_object_unref (kerberos);
+}
+
+GBytes *
+cc_realm_login_finish (GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
+ g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_login), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/panels/user-accounts/cc-realm-manager.h b/panels/user-accounts/cc-realm-manager.h
new file mode 100644
index 0000000..7e68e8e
--- /dev/null
+++ b/panels/user-accounts/cc-realm-manager.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Stef Walter <stefw@gnome.org>
+ */
+
+#pragma once
+
+#include "cc-realm-generated.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ CC_REALM_ERROR_BAD_LOGIN,
+ CC_REALM_ERROR_BAD_PASSWORD,
+ CC_REALM_ERROR_CANNOT_AUTH,
+ CC_REALM_ERROR_GENERIC,
+} CcRealmErrors;
+
+#define CC_REALM_ERROR (cc_realm_error_get_quark ())
+
+GQuark cc_realm_error_get_quark (void) G_GNUC_CONST;
+
+#define CC_TYPE_REALM_MANAGER (cc_realm_manager_get_type ())
+G_DECLARE_FINAL_TYPE (CcRealmManager, cc_realm_manager, CC, REALM_MANAGER, CcRealmObjectManagerClient)
+
+void cc_realm_manager_new (GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+CcRealmManager * cc_realm_manager_new_finish (GAsyncResult *result,
+ GError **error);
+
+void cc_realm_manager_discover (CcRealmManager *self,
+ const gchar *input,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GList * cc_realm_manager_discover_finish (CcRealmManager *self,
+ GAsyncResult *result,
+ GError **error);
+
+GList * cc_realm_manager_get_realms (CcRealmManager *self);
+
+void cc_realm_login (CcRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GBytes * cc_realm_login_finish (GAsyncResult *result,
+ GError **error);
+
+gboolean cc_realm_join_as_user (CcRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+gboolean cc_realm_join_as_admin (CcRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+gboolean cc_realm_join_finish (CcRealmObject *realm,
+ GAsyncResult *result,
+ GError **error);
+
+gboolean cc_realm_is_configured (CcRealmObject *realm);
+
+gchar * cc_realm_calculate_login (CcRealmCommon *realm,
+ const gchar *username);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-user-image.c b/panels/user-accounts/cc-user-image.c
new file mode 100644
index 0000000..8dc6389
--- /dev/null
+++ b/panels/user-accounts/cc-user-image.c
@@ -0,0 +1,137 @@
+/*
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2015 Red Hat, Inc.
+ */
+
+#include "cc-user-image.h"
+
+#include <gtk/gtk.h>
+#include <act/act.h>
+#include <sys/stat.h>
+
+#include "user-utils.h"
+
+struct _CcUserImage {
+ GtkImage parent_instance;
+
+ ActUser *user;
+};
+
+G_DEFINE_TYPE (CcUserImage, cc_user_image, GTK_TYPE_IMAGE)
+
+static cairo_surface_t *
+render_user_icon (ActUser *user,
+ gint icon_size,
+ gint scale)
+{
+ g_autoptr(GdkPixbuf) source_pixbuf = NULL;
+ GdkPixbuf *pixbuf = NULL;
+ const gchar *icon_file;
+ cairo_surface_t *surface = NULL;
+
+ g_return_val_if_fail (ACT_IS_USER (user), NULL);
+ g_return_val_if_fail (icon_size > 12, NULL);
+
+ icon_file = act_user_get_icon_file (user);
+ pixbuf = NULL;
+ if (icon_file) {
+ source_pixbuf = gdk_pixbuf_new_from_file_at_size (icon_file,
+ icon_size * scale,
+ icon_size * scale,
+ NULL);
+ if (source_pixbuf)
+ pixbuf = round_image (source_pixbuf);
+ }
+
+ if (pixbuf != NULL) {
+ goto out;
+ }
+
+ if (source_pixbuf != NULL) {
+ g_object_unref (source_pixbuf);
+ }
+
+ source_pixbuf = generate_default_avatar (user, icon_size * scale);
+ if (source_pixbuf)
+ pixbuf = round_image (source_pixbuf);
+ out:
+
+ if (pixbuf != NULL) {
+ surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL);
+ g_object_unref (pixbuf);
+ }
+
+ return surface;
+}
+
+static void
+render_image (CcUserImage *image)
+{
+ cairo_surface_t *surface;
+ gint scale, pixel_size;
+
+ if (image->user == NULL)
+ return;
+
+ pixel_size = gtk_image_get_pixel_size (GTK_IMAGE (image));
+ scale = gtk_widget_get_scale_factor (GTK_WIDGET (image));
+ surface = render_user_icon (image->user,
+ pixel_size > 0 ? pixel_size : 48,
+ scale);
+ gtk_image_set_from_surface (GTK_IMAGE (image), surface);
+ cairo_surface_destroy (surface);
+}
+
+void
+cc_user_image_set_user (CcUserImage *image,
+ ActUser *user)
+{
+ g_clear_object (&image->user);
+ image->user = g_object_ref (user);
+
+ render_image (image);
+}
+
+static void
+cc_user_image_finalize (GObject *object)
+{
+ CcUserImage *image = CC_USER_IMAGE (object);
+
+ g_clear_object (&image->user);
+
+ G_OBJECT_CLASS (cc_user_image_parent_class)->finalize (object);
+}
+
+static void
+cc_user_image_class_init (CcUserImageClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = cc_user_image_finalize;
+}
+
+static void
+cc_user_image_init (CcUserImage *image)
+{
+ g_signal_connect_swapped (image, "notify::scale-factor", G_CALLBACK (render_image), image);
+ g_signal_connect_swapped (image, "notify::pixel-size", G_CALLBACK (render_image), image);
+}
+
+GtkWidget *
+cc_user_image_new (void)
+{
+ return g_object_new (CC_TYPE_USER_IMAGE, NULL);
+}
diff --git a/panels/user-accounts/cc-user-image.h b/panels/user-accounts/cc-user-image.h
new file mode 100644
index 0000000..a7f69a8
--- /dev/null
+++ b/panels/user-accounts/cc-user-image.h
@@ -0,0 +1,32 @@
+/*
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright 2015 Red Hat, Inc.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_USER_IMAGE (cc_user_image_get_type ())
+G_DECLARE_FINAL_TYPE (CcUserImage, cc_user_image, CC, USER_IMAGE, GtkImage)
+
+GtkWidget *cc_user_image_new (void);
+void cc_user_image_set_user (CcUserImage *image, ActUser *user);
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-user-panel.c b/panels/user-accounts/cc-user-panel.c
new file mode 100644
index 0000000..5a9b5c2
--- /dev/null
+++ b/panels/user-accounts/cc-user-panel.c
@@ -0,0 +1,1678 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include "cc-user-panel.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <locale.h>
+#include <errno.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <polkit/polkit.h>
+#include <act/act.h>
+#include <cairo-gobject.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#ifdef HAVE_MALCONTENT
+#include <libmalcontent/malcontent.h>
+#endif
+
+#include "cc-add-user-dialog.h"
+#include "cc-avatar-chooser.h"
+#include "cc-carousel.h"
+#include "cc-language-chooser.h"
+#include "cc-login-history-dialog.h"
+#include "cc-password-dialog.h"
+#include "cc-realm-manager.h"
+#include "cc-user-accounts-resources.h"
+#include "cc-user-image.h"
+#include "cc-fingerprint-manager.h"
+#include "cc-fingerprint-dialog.h"
+#include "user-utils.h"
+
+#include "cc-common-language.h"
+#include "cc-permission-infobar.h"
+#include "cc-util.h"
+#include "list-box-helper.h"
+
+#define USER_ACCOUNTS_PERMISSION "org.gnome.controlcenter.user-accounts.administration"
+
+struct _CcUserPanel {
+ CcPanel parent_instance;
+
+ ActUserManager *um;
+ GSettings *login_screen_settings;
+
+ GtkBox *accounts_box;
+ GtkBox *account_settings_box;
+ GtkListBox *account_settings_listbox;
+ GtkListBox *authentication_and_login_listbox;
+ GtkListBoxRow *account_type_row;
+ GtkSwitch *account_type_switch;
+ GtkButton *add_user_button;
+ GtkListBoxRow *autologin_row;
+ GtkSwitch *autologin_switch;
+ CcCarousel *carousel;
+ GtkLabel *fingerprint_state_label;
+ GtkListBoxRow *fingerprint_row;
+ GtkStack *full_name_stack;
+ GtkLabel *full_name_label;
+ GtkToggleButton *full_name_edit_button;
+ GtkEntry *full_name_entry;
+ GtkLabel *language_button_label;
+ GtkListBoxRow *language_row;
+ GtkLabel *last_login_button_label;
+ GtkListBoxRow *last_login_row;
+ GtkBox *no_users_box;
+ GtkRevealer *notification_revealer;
+ GtkLabel *password_button_label;
+#ifdef HAVE_MALCONTENT
+ GtkLabel *parental_controls_button_label;
+ GtkImage *parental_control_go_next;
+ GtkListBoxRow *parental_controls_row;
+#endif
+ GtkListBoxRow *password_row;
+ CcPermissionInfobar *permission_infobar;
+ GtkButton *remove_user_button;
+ GtkStack *stack;
+ GtkToggleButton *user_icon_button;
+ CcUserImage *user_icon_image;
+ CcUserImage *user_icon_image2;
+ GtkStack *user_icon_stack;
+ GtkOverlay *users_overlay;
+
+ ActUser *selected_user;
+ GPermission *permission;
+ CcLanguageChooser *language_chooser;
+
+ CcAvatarChooser *avatar_chooser;
+
+ CcFingerprintManager *fingerprint_manager;
+
+ gint other_accounts;
+};
+
+CC_PANEL_REGISTER (CcUserPanel, cc_user_panel)
+
+static void show_restart_notification (CcUserPanel *self, const gchar *locale);
+static gint user_compare (gconstpointer i, gconstpointer u);
+
+typedef struct {
+ CcUserPanel *self;
+ GCancellable *cancellable;
+ gchar *login;
+} AsyncDeleteData;
+
+static void
+async_delete_data_free (AsyncDeleteData *data)
+{
+ g_object_unref (data->self);
+ g_object_unref (data->cancellable);
+ g_free (data->login);
+ g_slice_free (AsyncDeleteData, data);
+}
+
+static void
+show_error_dialog (CcUserPanel *self,
+ const gchar *message,
+ GError *error)
+{
+ GtkWidget *dialog;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_USE_HEADER_BAR,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "%s", message);
+
+ if (error != NULL) {
+ g_dbus_error_strip_remote_error (error);
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ "%s", error->message);
+ }
+
+ g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static ActUser *
+get_selected_user (CcUserPanel *self)
+{
+ return self->selected_user;
+}
+
+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 show_user (ActUser *user, CcUserPanel *self);
+
+static void
+set_selected_user (CcUserPanel *self, CcCarouselItem *item)
+{
+ uid_t uid;
+
+ uid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "uid"));
+ g_set_object (&self->selected_user,
+ act_user_manager_get_user_by_id (self->um, uid));
+
+ if (self->selected_user != NULL) {
+ show_user (self->selected_user, self);
+ }
+}
+
+static GtkWidget *
+create_carousel_entry (CcUserPanel *self, ActUser *user)
+{
+ GtkWidget *box, *widget;
+ gchar *label;
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+
+ widget = cc_user_image_new ();
+ cc_user_image_set_user (CC_USER_IMAGE (widget), user);
+ gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
+
+ label = g_markup_printf_escaped ("<b>%s</b>",
+ 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, TRUE, 0);
+ g_free (label);
+
+ if (act_user_get_uid (user) == getuid ())
+ label = g_strdup_printf ("<small>%s</small>", _("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, TRUE, 0);
+ gtk_style_context_add_class (gtk_widget_get_style_context (widget),
+ "dim-label");
+
+ return box;
+}
+
+static void
+user_added (CcUserPanel *self, ActUser *user)
+{
+ GtkWidget *item, *widget;
+ gboolean show_carousel;
+
+ if (act_user_is_system_account (user)) {
+ return;
+ }
+
+ g_debug ("user added: %d %s\n", act_user_get_uid (user), get_real_or_user_name (user));
+
+ widget = create_carousel_entry (self, user);
+ item = cc_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);
+
+ if (act_user_get_uid (user) != getuid ()) {
+ self->other_accounts++;
+ }
+
+ /* Show heading for other accounts if new one have been added. */
+ show_carousel = (self->other_accounts > 0);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->carousel), show_carousel);
+
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->users_overlay));
+}
+
+static gint
+sort_users (gconstpointer a, gconstpointer b)
+{
+ ActUser *ua, *ub;
+ gchar *name1, *name2;
+ 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_MAXINT32;
+ }
+ else if (act_user_get_uid (ub) == getuid ()) {
+ result = G_MAXINT32;
+ }
+ else {
+ 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);
+
+ g_free (name1);
+ g_free (name2);
+ }
+
+ return result;
+}
+
+static void
+reload_users (CcUserPanel *self, ActUser *selected_user)
+{
+ ActUser *user;
+ GSList *list, *l;
+ CcCarouselItem *item = NULL;
+ GtkSettings *settings;
+ gboolean animations;
+ guint users_count;
+
+ 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);
+
+ cc_carousel_purge_items (self->carousel);
+ self->other_accounts = 0;
+
+ list = act_user_manager_list_users (self->um);
+ users_count = g_slist_length (list);
+ g_debug ("Got %d users\n", users_count);
+
+ list = g_slist_sort (list, (GCompareFunc) sort_users);
+ for (l = list; l; l = l->next) {
+ user = l->data;
+ g_debug ("adding user %s\n", get_real_or_user_name (user));
+ user_added (self, user);
+ }
+ g_slist_free (list);
+
+ if (cc_carousel_get_item_count (self->carousel) == 0)
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->no_users_box));
+ if (self->other_accounts == 0)
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->carousel), FALSE);
+
+ if (selected_user)
+ item = cc_carousel_find_item (self->carousel, selected_user, user_compare);
+ cc_carousel_select_item (self->carousel, item);
+
+ g_object_set (settings, "gtk-enable-animations", animations, NULL);
+#ifdef HAVE_MALCONTENT
+ /* Parental Controls row not to be shown for single user setups. */
+ gtk_widget_set_visible (GTK_WIDGET (self->parental_controls_row), users_count > 1);
+#endif
+}
+
+static gint
+user_compare (gconstpointer i,
+ gconstpointer u)
+{
+ CcCarouselItem *item;
+ ActUser *user;
+ gint uid_a, uid_b;
+ gint result;
+
+ item = (CcCarouselItem *) 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
+user_changed (CcUserPanel *self, ActUser *user)
+{
+ reload_users (self, self->selected_user);
+}
+
+static void
+add_user (CcUserPanel *self)
+{
+ CcAddUserDialog *dialog;
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+ GtkWindow *toplevel;
+ ActUser *user;
+
+ dialog = cc_add_user_dialog_new (self->permission);
+ toplevel = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), toplevel);
+
+ gtk_dialog_run (GTK_DIALOG (dialog));
+
+ user = cc_add_user_dialog_get_user (dialog);
+ if (user != NULL) {
+ set_default_avatar (user);
+ reload_users (self, user);
+ }
+
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+delete_user_done (ActUserManager *manager,
+ GAsyncResult *res,
+ CcUserPanel *self)
+{
+ GError *error;
+
+ error = NULL;
+ if (!act_user_manager_delete_user_finish (manager, res, &error)) {
+ if (!g_error_matches (error, ACT_USER_MANAGER_ERROR,
+ ACT_USER_MANAGER_ERROR_PERMISSION_DENIED))
+ show_error_dialog (self, _("Failed to delete user"), error);
+
+ g_error_free (error);
+ }
+}
+
+static void
+delete_user_response (CcUserPanel *self,
+ gint response_id,
+ GtkWidget *dialog)
+{
+ ActUser *user;
+ gboolean remove_files;
+
+ gtk_widget_destroy (dialog);
+
+ if (response_id == GTK_RESPONSE_CANCEL) {
+ return;
+ }
+ else if (response_id == GTK_RESPONSE_NO) {
+ remove_files = TRUE;
+ }
+ else {
+ remove_files = FALSE;
+ }
+
+ user = get_selected_user (self);
+
+ /* remove autologin */
+ if (act_user_get_automatic_login (user)) {
+ act_user_set_automatic_login (user, FALSE);
+ }
+
+ act_user_manager_delete_user_async (self->um,
+ user,
+ remove_files,
+ NULL,
+ (GAsyncReadyCallback)delete_user_done,
+ self);
+}
+
+static void
+enterprise_user_revoked (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncDeleteData *data = user_data;
+ CcUserPanel *self = data->self;
+ CcRealmCommon *common = CC_REALM_COMMON (source);
+ GError *error = NULL;
+
+ if (g_cancellable_is_cancelled (data->cancellable)) {
+ async_delete_data_free (data);
+ return;
+ }
+
+ cc_realm_common_call_change_login_policy_finish (common, result, &error);
+ if (error != NULL) {
+ show_error_dialog (self, _("Failed to revoke remotely managed user"), error);
+ g_error_free (error);
+ }
+
+ async_delete_data_free (data);
+}
+
+static CcRealmCommon *
+find_matching_realm (CcRealmManager *realm_manager, const gchar *login)
+{
+ CcRealmCommon *common = NULL;
+ GList *realms, *l;
+
+ realms = cc_realm_manager_get_realms (realm_manager);
+ for (l = realms; l != NULL; l = g_list_next (l)) {
+ const gchar * const *permitted_logins;
+ gint i;
+
+ common = cc_realm_object_get_common (l->data);
+ if (common == NULL)
+ continue;
+
+ permitted_logins = cc_realm_common_get_permitted_logins (common);
+ for (i = 0; permitted_logins[i] != NULL; i++) {
+ if (g_strcmp0 (permitted_logins[i], login) == 0)
+ break;
+ }
+
+ if (permitted_logins[i] != NULL)
+ break;
+
+ g_clear_object (&common);
+ }
+ g_list_free_full (realms, g_object_unref);
+
+ return common;
+}
+
+static void
+realm_manager_found (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ AsyncDeleteData *data = user_data;
+ CcUserPanel *self = data->self;
+ CcRealmCommon *common;
+ CcRealmManager *realm_manager;
+ const gchar *add[1];
+ const gchar *remove[2];
+ GVariant *options;
+ GError *error = NULL;
+
+ if (g_cancellable_is_cancelled (data->cancellable)) {
+ async_delete_data_free (data);
+ return;
+ }
+
+ realm_manager = cc_realm_manager_new_finish (result, &error);
+ if (error != NULL) {
+ show_error_dialog (self, _("Failed to revoke remotely managed user"), error);
+ g_error_free (error);
+ async_delete_data_free (data);
+ return;
+ }
+
+ /* Find matching realm */
+ common = find_matching_realm (realm_manager, data->login);
+ if (common == NULL) {
+ /* The realm was probably left */
+ async_delete_data_free (data);
+ return;
+ }
+
+ /* Remove the user from permitted logins */
+ g_debug ("Denying future login for: %s", data->login);
+
+ add[0] = NULL;
+ remove[0] = data->login;
+ remove[1] = NULL;
+
+ options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
+ cc_realm_common_call_change_login_policy (common, "",
+ add, remove, options,
+ data->cancellable,
+ enterprise_user_revoked,
+ data);
+
+ g_object_unref (common);
+}
+
+static void
+enterprise_user_uncached (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ AsyncDeleteData *data = user_data;
+ CcUserPanel *self = data->self;
+ ActUserManager *manager = ACT_USER_MANAGER (source);
+ GError *error = NULL;
+
+ if (g_cancellable_is_cancelled (data->cancellable)) {
+ async_delete_data_free (data);
+ return;
+ }
+
+ act_user_manager_uncache_user_finish (manager, res, &error);
+ if (error == NULL) {
+ /* Find realm manager */
+ cc_realm_manager_new (cc_panel_get_cancellable (CC_PANEL (self)), realm_manager_found, data);
+ }
+ else {
+ show_error_dialog (self, _("Failed to revoke remotely managed user"), error);
+ g_error_free (error);
+ async_delete_data_free (data);
+ }
+}
+
+static void
+delete_enterprise_user_response (CcUserPanel *self,
+ gint response_id,
+ GtkWidget *dialog)
+{
+ AsyncDeleteData *data;
+ ActUser *user;
+
+ gtk_widget_destroy (dialog);
+
+ if (response_id != GTK_RESPONSE_ACCEPT) {
+ return;
+ }
+
+ user = get_selected_user (self);
+
+ data = g_slice_new (AsyncDeleteData);
+ data->self = g_object_ref (self);
+ data->cancellable = g_object_ref (cc_panel_get_cancellable (CC_PANEL (self)));
+ data->login = g_strdup (act_user_get_user_name (user));
+
+ /* Uncache the user account from the accountsservice */
+ g_debug ("Uncaching remote user: %s", data->login);
+
+ act_user_manager_uncache_user_async (self->um, data->login,
+ data->cancellable,
+ enterprise_user_uncached,
+ data);
+}
+
+static void
+delete_user (CcUserPanel *self)
+{
+ ActUser *user;
+ GtkWidget *dialog;
+
+ user = get_selected_user (self);
+ if (user == NULL) {
+ return;
+ }
+ else if (act_user_get_uid (user) == getuid ()) {
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ 0,
+ GTK_MESSAGE_INFO,
+ GTK_BUTTONS_CLOSE,
+ _("You cannot delete your own account."));
+ g_signal_connect (dialog, "response",
+ G_CALLBACK (gtk_widget_destroy), NULL);
+ }
+ else if (act_user_is_logged_in_anywhere (user)) {
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ 0,
+ GTK_MESSAGE_INFO,
+ GTK_BUTTONS_CLOSE,
+ _("%s is still logged in"),
+ get_real_or_user_name (user));
+
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ _("Deleting a user while they are logged in can leave the system in an inconsistent state."));
+ g_signal_connect (dialog, "response",
+ G_CALLBACK (gtk_widget_destroy), NULL);
+ }
+ else if (act_user_is_local_account (user)) {
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ 0,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_NONE,
+ _("Do you want to keep %s’s files?"),
+ get_real_or_user_name (user));
+
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ _("It is possible to keep the home directory, mail spool and temporary files around when deleting a user account."));
+
+ gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+ _("_Delete Files"), GTK_RESPONSE_NO,
+ _("_Keep Files"), GTK_RESPONSE_YES,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+
+ gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users");
+
+ g_signal_connect_object (dialog, "response",
+ G_CALLBACK (delete_user_response), self, G_CONNECT_SWAPPED);
+ }
+ else {
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ 0,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_NONE,
+ _("Are you sure you want to revoke remotely managed %s’s account?"),
+ get_real_or_user_name (user));
+
+ gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+ _("_Delete"), GTK_RESPONSE_ACCEPT,
+ _("_Cancel"), GTK_RESPONSE_CANCEL,
+ NULL);
+
+ gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users");
+
+ g_signal_connect_object (dialog, "response",
+ G_CALLBACK (delete_enterprise_user_response), self, G_CONNECT_SWAPPED);
+ }
+
+ g_signal_connect (dialog, "close",
+ G_CALLBACK (gtk_widget_destroy), NULL);
+
+ gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
+
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static const gchar *
+get_invisible_text (void)
+{
+ GtkWidget *entry;
+ gunichar invisible_char;
+ static gchar invisible_text[40];
+ gchar *p;
+ gint i;
+
+ entry = gtk_entry_new ();
+ invisible_char = gtk_entry_get_invisible_char (GTK_ENTRY (entry));
+ if (invisible_char == 0)
+ invisible_char = 0x2022;
+
+ g_object_ref_sink (entry);
+ g_object_unref (entry);
+
+ /* five bullets */
+ p = invisible_text;
+ for (i = 0; i < 5; i++)
+ p += g_unichar_to_utf8 (invisible_char, p);
+ *p = 0;
+
+ return invisible_text;
+}
+
+static const gchar *
+get_password_mode_text (ActUser *user)
+{
+ const gchar *text;
+
+ if (act_user_get_locked (user)) {
+ text = C_("Password mode", "Account disabled");
+ }
+ else {
+ switch (act_user_get_password_mode (user)) {
+ case ACT_USER_PASSWORD_MODE_REGULAR:
+ text = get_invisible_text ();
+ break;
+ case ACT_USER_PASSWORD_MODE_SET_AT_LOGIN:
+ text = C_("Password mode", "To be set at next login");
+ break;
+ case ACT_USER_PASSWORD_MODE_NONE:
+ text = C_("Password mode", "None");
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ return text;
+}
+
+static void
+autologin_changed (CcUserPanel *self)
+{
+ gboolean active;
+ ActUser *user;
+
+ active = gtk_switch_get_active (self->autologin_switch);
+ user = get_selected_user (self);
+
+ if (active != act_user_get_automatic_login (user)) {
+ act_user_set_automatic_login (user, active);
+ if (act_user_get_automatic_login (user)) {
+ GSList *list;
+ GSList *l;
+ list = act_user_manager_list_users (self->um);
+ for (l = list; l != NULL; l = l->next) {
+ ActUser *u = l->data;
+ if (act_user_get_uid (u) != act_user_get_uid (user)) {
+ act_user_set_automatic_login (user, FALSE);
+ }
+ }
+ g_slist_free (list);
+ }
+ }
+}
+
+static gchar *
+get_login_time_text (ActUser *user)
+{
+ gchar *text, *date_str, *time_str;
+ GDateTime *date_time;
+ gint64 time;
+
+ time = act_user_get_login_time (user);
+ if (act_user_is_logged_in (user)) {
+ text = g_strdup (_("Logged in"));
+ }
+ else if (time > 0) {
+ date_time = g_date_time_new_from_unix_local (time);
+ date_str = cc_util_get_smart_date (date_time);
+ /* Translators: This is a time format string in the style of "22:58".
+ It indicates a login time which follows a date. */
+ time_str = g_date_time_format (date_time, C_("login date-time", "%k:%M"));
+
+ /* Translators: This indicates a login date-time.
+ The first %s is a date, and the second %s a time. */
+ text = g_strdup_printf(C_("login date-time", "%s, %s"), date_str, time_str);
+
+ g_date_time_unref (date_time);
+ g_free (date_str);
+ g_free (time_str);
+ }
+ else {
+ text = g_strdup ("—");
+ }
+
+ return text;
+}
+
+static gboolean
+get_autologin_possible (ActUser *user)
+{
+ gboolean locked;
+ gboolean set_password_at_login;
+
+ locked = act_user_get_locked (user);
+ set_password_at_login = (act_user_get_password_mode (user) == ACT_USER_PASSWORD_MODE_SET_AT_LOGIN);
+
+ return !(locked || set_password_at_login);
+}
+
+static void on_permission_changed (CcUserPanel *self);
+static void full_name_edit_button_toggled (CcUserPanel *self);
+
+#ifdef HAVE_MALCONTENT
+static gboolean
+is_parental_controls_enabled_for_user (ActUser *user)
+{
+ g_autoptr(MctManager) manager = NULL;
+ g_autoptr(MctAppFilter) app_filter = NULL;
+ g_autoptr(GDBusConnection) system_bus = NULL;
+ g_autoptr(GError) error = NULL;
+
+ /* FIXME: should become asynchronous */
+ system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error);
+ if (system_bus == NULL) {
+ g_warning ("Error getting system bus while trying to show user details: %s", error->message);
+ return FALSE;
+ }
+
+ manager = mct_manager_new (system_bus);
+ app_filter = mct_manager_get_app_filter (manager,
+ act_user_get_uid (user),
+ MCT_GET_APP_FILTER_FLAGS_NONE,
+ NULL,
+ &error);
+ if (error) {
+ if (!g_error_matches (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED))
+ g_warning ("Error retrieving app filter for user %s: %s",
+ act_user_get_user_name (user),
+ error->message);
+
+ return FALSE;
+ }
+
+ return mct_app_filter_is_enabled (app_filter);
+}
+#endif
+
+static void
+update_fingerprint_row_state (CcUserPanel *self, GParamSpec *spec, CcFingerprintManager *fingerprint_manager)
+{
+ CcFingerprintState state = cc_fingerprint_manager_get_state (fingerprint_manager);
+
+ if (state != CC_FINGERPRINT_STATE_UPDATING) {
+ gtk_widget_set_visible (GTK_WIDGET (self->fingerprint_row),
+ state != CC_FINGERPRINT_STATE_NONE);
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row),
+ state != CC_FINGERPRINT_STATE_UPDATING);
+
+ if (state == CC_FINGERPRINT_STATE_ENABLED)
+ gtk_label_set_text (self->fingerprint_state_label, _("Enabled"));
+ else if (state == CC_FINGERPRINT_STATE_DISABLED)
+ gtk_label_set_text (self->fingerprint_state_label, _("Disabled"));
+}
+
+static void
+show_user (ActUser *user, CcUserPanel *self)
+{
+ gchar *lang, *text, *name;
+ gboolean show, enable;
+ ActUser *current;
+
+ self->selected_user = user;
+
+ cc_user_image_set_user (self->user_icon_image, user);
+ cc_user_image_set_user (self->user_icon_image2, user);
+
+ cc_avatar_chooser_set_user (self->avatar_chooser, user);
+
+ gtk_label_set_label (self->full_name_label, act_user_get_real_name (user));
+ gtk_entry_set_text (self->full_name_entry, act_user_get_real_name (user));
+ gtk_widget_set_tooltip_text (GTK_WIDGET (self->full_name_label), act_user_get_user_name (user));
+
+ g_signal_handlers_block_by_func (self->full_name_edit_button, full_name_edit_button_toggled, self);
+ gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_label));
+ gtk_toggle_button_set_active (self->full_name_edit_button, FALSE);
+ g_signal_handlers_unblock_by_func (self->full_name_edit_button, full_name_edit_button_toggled, self);
+
+ enable = (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR);
+ gtk_switch_set_active (self->account_type_switch, enable);
+
+ /* Do not show the "Account Type" option when there's a single user account. */
+ show = (self->other_accounts != 0);
+ gtk_widget_set_visible (GTK_WIDGET (self->account_settings_box), show);
+
+ gtk_label_set_label (self->password_button_label, get_password_mode_text (user));
+ enable = act_user_is_local_account (user);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->password_button_label), enable);
+
+ g_signal_handlers_block_by_func (self->autologin_switch, autologin_changed, self);
+ gtk_switch_set_active (self->autologin_switch, act_user_get_automatic_login (user));
+ g_signal_handlers_unblock_by_func (self->autologin_switch, autologin_changed, self);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_switch), get_autologin_possible (user));
+
+ name = NULL;
+ lang = g_strdup (act_user_get_language (user));
+
+ if (lang && *lang != '\0') {
+ name = gnome_get_language_from_locale (lang, NULL);
+ } else {
+ name = g_strdup ("—");
+ }
+
+ gtk_label_set_label (self->language_button_label, name);
+ g_free (lang);
+ g_free (name);
+
+ /* Fingerprint: show when self, local, enabled, and possible */
+ show = (act_user_get_uid (user) == getuid() &&
+ act_user_is_local_account (user) &&
+ (self->login_screen_settings &&
+ g_settings_get_boolean (self->login_screen_settings,
+ "enable-fingerprint-authentication")));
+
+ if (show) {
+ if (!self->fingerprint_manager) {
+ self->fingerprint_manager = cc_fingerprint_manager_new (user);
+ g_signal_connect_object (self->fingerprint_manager,
+ "notify::state",
+ G_CALLBACK (update_fingerprint_row_state),
+ self, G_CONNECT_SWAPPED);
+ }
+
+ update_fingerprint_row_state (self, NULL, self->fingerprint_manager);
+ } else {
+ gtk_widget_set_visible (GTK_WIDGET (self->fingerprint_row), FALSE);
+ }
+
+ /* Autologin: show when local account */
+ show = act_user_is_local_account (user);
+ gtk_widget_set_visible (GTK_WIDGET (self->autologin_row), show);
+
+#ifdef HAVE_MALCONTENT
+ /* Parental Controls: Unavailable if user is admin */
+ if (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) {
+ gtk_widget_hide (GTK_WIDGET (self->parental_control_go_next));
+ /* TRANSLATORS: Status of Parental Controls setup */
+ gtk_label_set_text (self->parental_controls_button_label, _("Unavailable"));
+ } else {
+ if (is_parental_controls_enabled_for_user (user))
+ /* TRANSLATORS: Status of Parental Controls setup */
+ gtk_label_set_text (self->parental_controls_button_label, _("Enabled"));
+ else
+ /* TRANSLATORS: Status of Parental Controls setup */
+ gtk_label_set_text (self->parental_controls_button_label, _("Disabled"));
+
+ gtk_widget_show (GTK_WIDGET (self->parental_control_go_next));
+ }
+#endif
+
+ /* Language: do not show for current user */
+ show = act_user_get_uid (user) != getuid();
+ gtk_widget_set_visible (GTK_WIDGET (self->language_row), show);
+
+ /* Last login: show when administrator or current user */
+ current = act_user_manager_get_user_by_id (self->um, getuid ());
+ show = act_user_get_uid (user) == getuid () ||
+ act_user_get_account_type (current) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR;
+ if (show) {
+ text = get_login_time_text (user);
+ gtk_label_set_label (self->last_login_button_label, text);
+ g_free (text);
+ }
+ gtk_widget_set_visible (GTK_WIDGET (self->last_login_row), show);
+
+ enable = act_user_get_login_history (user) != NULL;
+ gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), enable);
+
+ if (self->permission != NULL)
+ on_permission_changed (self);
+}
+
+static void
+full_name_entry_activate (CcUserPanel *self)
+{
+ const gchar *text;
+ ActUser *user;
+
+ user = get_selected_user (self);
+ text = gtk_entry_get_text (self->full_name_entry);
+ if (g_strcmp0 (text, act_user_get_real_name (user)) != 0 &&
+ is_valid_name (text)) {
+ act_user_set_real_name (user, text);
+ }
+
+ gtk_toggle_button_set_active (self->full_name_edit_button, FALSE);
+}
+
+static void
+full_name_edit_button_toggled (CcUserPanel *self)
+{
+ if (gtk_stack_get_visible_child (self->full_name_stack) == GTK_WIDGET (self->full_name_label)) {
+ gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_entry));
+
+ gtk_widget_grab_focus (GTK_WIDGET (self->full_name_entry));
+ } else {
+ gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_label));
+
+ full_name_entry_activate (self);
+ }
+}
+
+static gboolean
+full_name_entry_key_press_cb (CcUserPanel *self,
+ GdkEvent *event)
+{
+ GdkEventKey *key = (GdkEventKey *)event;
+
+ if (key->keyval == GDK_KEY_Escape) {
+ gtk_entry_set_text (self->full_name_entry, act_user_get_real_name (self->selected_user));
+
+ full_name_entry_activate (self);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+account_type_changed (CcUserPanel *self)
+{
+ ActUser *user;
+ gboolean self_selected;
+ gboolean is_admin;
+ ActUserAccountType account_type;
+
+ user = get_selected_user (self);
+ self_selected = act_user_get_uid (user) == geteuid ();
+ is_admin = gtk_switch_get_active (self->account_type_switch);
+
+ account_type = is_admin ? ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR : ACT_USER_ACCOUNT_TYPE_STANDARD;
+ if (account_type != act_user_get_account_type (user)) {
+ act_user_set_account_type (user, account_type);
+
+ if (self_selected)
+ show_restart_notification (self, NULL);
+ }
+}
+
+static void
+dismiss_notification (CcUserPanel *self)
+{
+ gtk_revealer_set_reveal_child (self->notification_revealer, FALSE);
+}
+
+static void
+restart_now (CcUserPanel *self)
+{
+ GDBusConnection *bus;
+
+ gtk_revealer_set_reveal_child (self->notification_revealer, FALSE);
+
+ bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
+ g_dbus_connection_call (bus,
+ "org.gnome.SessionManager",
+ "/org/gnome/SessionManager",
+ "org.gnome.SessionManager",
+ "Logout",
+ g_variant_new ("(u)", 0),
+ NULL, 0, G_MAXINT,
+ NULL, NULL, NULL);
+ g_object_unref (bus);
+}
+
+static void
+show_restart_notification (CcUserPanel *self, const gchar *locale)
+{
+ locale_t current_locale;
+ locale_t new_locale;
+
+ if (locale) {
+ new_locale = newlocale (LC_MESSAGES_MASK, locale, (locale_t) 0);
+ if (new_locale == (locale_t) 0)
+ g_warning ("Failed to create locale %s: %s", locale, g_strerror (errno));
+ else
+ current_locale = uselocale (new_locale);
+ }
+
+ gtk_revealer_set_reveal_child (self->notification_revealer, TRUE);
+
+ if (locale && new_locale != (locale_t) 0) {
+ uselocale (current_locale);
+ freelocale (new_locale);
+ }
+}
+
+static void
+language_response (CcUserPanel *self,
+ gint response_id,
+ GtkDialog *dialog)
+{
+ ActUser *user;
+ const gchar *lang, *account_language;
+
+ if (response_id != GTK_RESPONSE_OK) {
+ gtk_widget_hide (GTK_WIDGET (dialog));
+ return;
+ }
+
+ user = get_selected_user (self);
+ account_language = act_user_get_language (user);
+
+ lang = cc_language_chooser_get_language (CC_LANGUAGE_CHOOSER (dialog));
+ if (lang) {
+ g_autofree gchar *name = NULL;
+ if (g_strcmp0 (lang, account_language) != 0) {
+ act_user_set_language (user, lang);
+ }
+
+ name = gnome_get_language_from_locale (lang, NULL);
+ gtk_label_set_label (self->language_button_label, name);
+ }
+
+ gtk_widget_hide (GTK_WIDGET (dialog));
+}
+
+static void
+change_language (CcUserPanel *self)
+{
+ const gchar *current_language;
+ ActUser *user;
+
+ user = get_selected_user (self);
+ current_language = act_user_get_language (user);
+
+ if (self->language_chooser) {
+ cc_language_chooser_clear_filter (self->language_chooser);
+ cc_language_chooser_set_language (self->language_chooser, NULL);
+ }
+ else {
+ self->language_chooser = cc_language_chooser_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (self->language_chooser),
+ GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))));
+
+ g_signal_connect_object (self->language_chooser, "response",
+ G_CALLBACK (language_response), self, G_CONNECT_SWAPPED);
+ g_signal_connect (self->language_chooser, "delete-event",
+ G_CALLBACK (gtk_widget_hide_on_delete), NULL);
+
+ gdk_window_set_cursor (gtk_widget_get_window (gtk_widget_get_toplevel (GTK_WIDGET (self))), NULL);
+ }
+
+ if (current_language && *current_language != '\0')
+ cc_language_chooser_set_language (self->language_chooser, current_language);
+ gtk_window_present (GTK_WINDOW (self->language_chooser));
+}
+
+static void
+change_password (CcUserPanel *self)
+{
+ ActUser *user;
+ CcPasswordDialog *dialog;
+ GtkWindow *parent;
+
+ user = get_selected_user (self);
+ dialog = cc_password_dialog_new (user);
+
+ parent = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), parent);
+
+ gtk_dialog_run (GTK_DIALOG (dialog));
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+change_fingerprint (CcUserPanel *self)
+{
+ ActUser *user;
+ GtkWindow *top_level;
+ CcFingerprintDialog *dialog;
+
+ user = get_selected_user (self);
+ top_level = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+
+ g_assert (g_strcmp0 (g_get_user_name (), act_user_get_user_name (user)) == 0);
+
+ dialog = cc_fingerprint_dialog_new (self->fingerprint_manager);
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), top_level);
+ gtk_widget_show (GTK_WIDGET (dialog));
+}
+
+static void
+show_history (CcUserPanel *self)
+{
+ CcLoginHistoryDialog *dialog;
+ ActUser *user;
+ GtkWindow *parent;
+ gint parent_width;
+
+ user = get_selected_user (self);
+ dialog = cc_login_history_dialog_new (user);
+
+ parent = GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+ gtk_window_get_size (parent, &parent_width, NULL);
+ gtk_window_set_default_size (GTK_WINDOW (dialog), parent_width * 0.6, -1);
+ gtk_window_set_transient_for (GTK_WINDOW (dialog), parent);
+
+ gtk_dialog_run (GTK_DIALOG (dialog));
+
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+#ifdef HAVE_MALCONTENT
+static void
+spawn_malcontent_control (CcUserPanel *self)
+{
+ ActUser *user;
+
+ user = get_selected_user (self);
+
+ /* no-op if the user is administrator */
+ if (act_user_get_account_type (user) != ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) {
+ const gchar *argv[] = { "malcontent-control", NULL };
+ g_spawn_async (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
+ }
+}
+#endif
+
+static void
+activate_row (GtkListBox *box, GtkListBoxRow *row, CcUserPanel *self)
+{
+ if (!gtk_widget_get_sensitive (GTK_WIDGET (row)))
+ return;
+
+ if (row == self->language_row) {
+ change_language (self);
+ } else if (row == self->password_row) {
+ change_password (self);
+ } else if (row == self->fingerprint_row) {
+ change_fingerprint (self);
+ } else if (row == self->last_login_row) {
+ show_history (self);
+ }
+
+#ifdef HAVE_MALCONTENT
+ if (row == self->parental_controls_row) {
+ spawn_malcontent_control (self);
+ }
+#endif
+}
+
+static void
+users_loaded (CcUserPanel *self)
+{
+ GtkWidget *dialog;
+
+ if (act_user_manager_no_service (self->um)) {
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+ GTK_DIALOG_MODAL,
+ GTK_MESSAGE_OTHER,
+ GTK_BUTTONS_CLOSE,
+ _("Failed to contact the accounts service"));
+ gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+ _("Please make sure that the AccountService is installed and enabled."));
+ g_signal_connect (dialog, "response",
+ G_CALLBACK (gtk_widget_destroy),
+ NULL);
+ gtk_widget_show (dialog);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->accounts_box), FALSE);
+ }
+
+ g_signal_connect_object (self->um, "user-changed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->um, "user-is-logged-in-changed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->um, "user-added", G_CALLBACK (user_added), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->um, "user-removed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED);
+
+ reload_users (self, NULL);
+}
+
+static void
+add_unlock_tooltip (GtkWidget *widget)
+{
+ gchar *names[3];
+ GIcon *icon;
+
+ names[0] = "changes-allow-symbolic";
+ names[1] = "changes-allow";
+ names[2] = NULL;
+ icon = (GIcon *)g_themed_icon_new_from_names (names, -1);
+ setup_tooltip_with_embedded_icon (widget,
+ /* Translator comments:
+ * We split the line in 2 here to "make it look good", as there's
+ * no good way to do this in GTK+ for tooltips. See:
+ * https://bugzilla.gnome.org/show_bug.cgi?id=657168 */
+ _("To make changes,\nclick the * icon first"),
+ "*",
+ icon);
+ g_object_unref (icon);
+ g_signal_connect (widget, "button-release-event",
+ G_CALLBACK (show_tooltip_now), NULL);
+}
+
+static void
+remove_unlock_tooltip (GtkWidget *widget)
+{
+ setup_tooltip_with_embedded_icon (widget, NULL, NULL, NULL);
+ g_signal_handlers_disconnect_by_func (widget,
+ G_CALLBACK (show_tooltip_now), NULL);
+}
+
+static guint
+get_num_active_admin (ActUserManager *um)
+{
+ GSList *list;
+ GSList *l;
+ guint num_admin = 0;
+
+ list = act_user_manager_list_users (um);
+ for (l = list; l != NULL; l = l->next) {
+ ActUser *u = l->data;
+ if (act_user_get_account_type (u) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR && !act_user_get_locked (u)) {
+ num_admin++;
+ }
+ }
+ g_slist_free (list);
+
+ return num_admin;
+}
+
+static gboolean
+would_demote_only_admin (ActUser *user)
+{
+ ActUserManager *um = act_user_manager_get_default ();
+
+ /* Prevent the user from demoting the only admin account.
+ * Returns TRUE when user is an administrator and there is only
+ * one enabled administrator. */
+
+ if (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_STANDARD ||
+ act_user_get_locked (user))
+ return FALSE;
+
+ if (get_num_active_admin (um) > 1)
+ return FALSE;
+
+ return TRUE;
+}
+
+static void
+on_permission_changed (CcUserPanel *self)
+{
+ gboolean is_authorized;
+ gboolean self_selected;
+ ActUser *user;
+
+ is_authorized = g_permission_get_allowed (G_PERMISSION (self->permission));
+
+ gtk_widget_set_visible (GTK_WIDGET (self->add_user_button), is_authorized);
+
+ user = get_selected_user (self);
+ if (!user) {
+ return;
+ }
+
+ self_selected = act_user_get_uid (user) == geteuid ();
+ gtk_widget_set_sensitive (GTK_WIDGET (self->remove_user_button), is_authorized && !self_selected
+ && !would_demote_only_admin (user));
+ if (is_authorized) {
+ setup_tooltip_with_embedded_icon (GTK_WIDGET (self->remove_user_button), _("Delete the selected user account"), NULL, NULL);
+ }
+ else {
+ gchar *names[3];
+ GIcon *icon;
+
+ names[0] = "changes-allow-symbolic";
+ names[1] = "changes-allow";
+ names[2] = NULL;
+ icon = (GIcon *)g_themed_icon_new_from_names (names, -1);
+
+ setup_tooltip_with_embedded_icon (GTK_WIDGET (self->remove_user_button),
+ _("To delete the selected user account,\nclick the * icon first"),
+ "*",
+ icon);
+ g_object_unref (icon);
+ }
+
+ if (!act_user_is_local_account (user)) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->account_type_row), FALSE);
+ remove_unlock_tooltip (GTK_WIDGET (self->account_type_row));
+ gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_row), FALSE);
+ remove_unlock_tooltip (GTK_WIDGET (self->autologin_row));
+
+ } else if (is_authorized && act_user_is_local_account (user)) {
+ if (would_demote_only_admin (user)) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->account_type_row), FALSE);
+ } else {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->account_type_row), TRUE);
+ }
+ remove_unlock_tooltip (GTK_WIDGET (self->account_type_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_row), get_autologin_possible (user));
+ remove_unlock_tooltip (GTK_WIDGET (self->autologin_row));
+ }
+ else {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->account_type_row), FALSE);
+ if (would_demote_only_admin (user)) {
+ remove_unlock_tooltip (GTK_WIDGET (self->account_type_row));
+ } else {
+ add_unlock_tooltip (GTK_WIDGET (self->account_type_row));
+ }
+ gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->autologin_row));
+ }
+
+ /* The full name entry: insensitive if remote or not authorized and not self */
+ if (!act_user_is_local_account (user)) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), FALSE);
+ remove_unlock_tooltip (GTK_WIDGET (self->full_name_stack));
+
+ } else if (is_authorized || self_selected) {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), TRUE);
+ remove_unlock_tooltip (GTK_WIDGET (self->full_name_stack));
+
+ } else {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->full_name_stack));
+ }
+
+ if (is_authorized || self_selected) {
+ CcFingerprintState fingerprint_state = CC_FINGERPRINT_STATE_NONE;
+
+ if (self->fingerprint_manager)
+ fingerprint_state = cc_fingerprint_manager_get_state (self->fingerprint_manager);
+
+ gtk_stack_set_visible_child (self->user_icon_stack, GTK_WIDGET (self->user_icon_button));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->language_row), TRUE);
+ remove_unlock_tooltip (GTK_WIDGET (self->language_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->password_row), TRUE);
+ remove_unlock_tooltip (GTK_WIDGET (self->password_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row),
+ fingerprint_state != CC_FINGERPRINT_STATE_UPDATING);
+ remove_unlock_tooltip (GTK_WIDGET (self->fingerprint_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), TRUE);
+ remove_unlock_tooltip (GTK_WIDGET (self->last_login_row));
+#ifdef HAVE_MALCONTENT
+ gtk_widget_set_sensitive (GTK_WIDGET (self->parental_controls_row), TRUE);
+ remove_unlock_tooltip (GTK_WIDGET (self->parental_controls_row));
+#endif
+ }
+ else {
+ gtk_stack_set_visible_child (self->user_icon_stack, GTK_WIDGET (self->user_icon_image));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->language_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->language_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->password_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->password_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->fingerprint_row));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->last_login_row));
+#ifdef HAVE_MALCONTENT
+ gtk_widget_set_sensitive (GTK_WIDGET (self->parental_controls_row), FALSE);
+ add_unlock_tooltip (GTK_WIDGET (self->parental_controls_row));
+#endif
+ }
+}
+
+static void
+setup_main_window (CcUserPanel *self)
+{
+ GIcon *icon;
+ GError *error = NULL;
+ gchar *names[3];
+ gboolean loaded;
+
+ self->other_accounts = 0;
+
+ add_unlock_tooltip (GTK_WIDGET (self->user_icon_image));
+
+ self->permission = (GPermission *)polkit_permission_new_sync (USER_ACCOUNTS_PERMISSION, NULL, NULL, &error);
+ if (self->permission != NULL) {
+ g_signal_connect_object (self->permission, "notify",
+ G_CALLBACK (on_permission_changed), self, G_CONNECT_SWAPPED);
+ on_permission_changed (self);
+ } else {
+ g_warning ("Cannot create '%s' permission: %s", USER_ACCOUNTS_PERMISSION, error->message);
+ g_error_free (error);
+ }
+
+ names[0] = "changes-allow-symbolic";
+ names[1] = "changes-allow";
+ names[2] = NULL;
+ icon = (GIcon *)g_themed_icon_new_from_names (names, -1);
+ setup_tooltip_with_embedded_icon (GTK_WIDGET (self->remove_user_button),
+ _("To delete the selected user account,\nclick the * icon first"),
+ "*",
+ icon);
+ g_object_unref (icon);
+
+ g_object_get (self->um, "is-loaded", &loaded, NULL);
+ if (loaded)
+ users_loaded (self);
+ else
+ g_signal_connect_object (self->um, "notify::is-loaded", G_CALLBACK (users_loaded), self, G_CONNECT_SWAPPED);
+
+ gtk_list_box_set_header_func (self->account_settings_listbox,
+ cc_list_box_update_header_func,
+ NULL, NULL);
+ gtk_list_box_set_header_func (self->authentication_and_login_listbox,
+ cc_list_box_update_header_func,
+ NULL, NULL);
+}
+
+static GSettings *
+settings_or_null (const gchar *schema)
+{
+ GSettingsSchemaSource *source = NULL;
+ gchar **non_relocatable = NULL;
+ gchar **relocatable = NULL;
+ GSettings *settings = NULL;
+
+ source = g_settings_schema_source_get_default ();
+ if (!source)
+ return NULL;
+
+ g_settings_schema_source_list_schemas (source, TRUE, &non_relocatable, &relocatable);
+
+ if (g_strv_contains ((const gchar * const *)non_relocatable, schema) ||
+ g_strv_contains ((const gchar * const *)relocatable, schema))
+ settings = g_settings_new (schema);
+
+ g_strfreev (non_relocatable);
+ g_strfreev (relocatable);
+ return settings;
+}
+
+static void
+cc_user_panel_constructed (GObject *object)
+{
+ CcUserPanel *self = CC_USER_PANEL (object);
+ CcShell *shell;
+
+ G_OBJECT_CLASS (cc_user_panel_parent_class)->constructed (object);
+
+ shell = cc_panel_get_shell (CC_PANEL (self));
+ cc_shell_embed_widget_in_header (shell, GTK_WIDGET (self->add_user_button), GTK_POS_RIGHT);
+
+ cc_permission_infobar_set_permission (self->permission_infobar, self->permission);
+}
+
+static void
+cc_user_panel_init (CcUserPanel *self)
+{
+ volatile GType type G_GNUC_UNUSED;
+ GtkCssProvider *provider;
+
+ g_resources_register (cc_user_accounts_get_resource ());
+
+ /* register types that the builder might need */
+ type = cc_user_image_get_type ();
+ type = cc_carousel_get_type ();
+ type = cc_permission_infobar_get_type ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->um = act_user_manager_get_default ();
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/user-accounts/user-accounts-dialog.css");
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ g_object_unref (provider);
+
+ self->login_screen_settings = settings_or_null ("org.gnome.login-screen");
+
+ self->avatar_chooser = cc_avatar_chooser_new (GTK_WIDGET (self->user_icon_button));
+ setup_main_window (self);
+}
+
+static void
+cc_user_panel_dispose (GObject *object)
+{
+ CcUserPanel *self = CC_USER_PANEL (object);
+
+ g_clear_object (&self->selected_user);
+
+ g_clear_object (&self->login_screen_settings);
+
+ g_clear_pointer ((GtkWidget **)&self->language_chooser, gtk_widget_destroy);
+ g_clear_object (&self->permission);
+ G_OBJECT_CLASS (cc_user_panel_parent_class)->dispose (object);
+}
+
+static const char *
+cc_user_panel_get_help_uri (CcPanel *panel)
+{
+ return "help:gnome-help/user-accounts";
+}
+
+static void
+cc_user_panel_class_init (CcUserPanelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ CcPanelClass *panel_class = CC_PANEL_CLASS (klass);
+
+ object_class->dispose = cc_user_panel_dispose;
+ object_class->constructed = cc_user_panel_constructed;
+
+ panel_class->get_help_uri = cc_user_panel_get_help_uri;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-user-panel.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, accounts_box);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_settings_box);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_settings_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, authentication_and_login_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_type_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_type_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, add_user_button);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, autologin_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, autologin_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, carousel);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, fingerprint_state_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, fingerprint_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_edit_button);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, language_button_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, language_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, last_login_button_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, last_login_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, no_users_box);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, notification_revealer);
+#ifdef HAVE_MALCONTENT
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, parental_controls_button_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, parental_control_go_next);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, parental_controls_row);
+#endif
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, password_button_label);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, password_row);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, permission_infobar);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, remove_user_button);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, stack);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_icon_button);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_icon_image);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_icon_image2);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_icon_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcUserPanel, users_overlay);
+
+ gtk_widget_class_bind_template_callback (widget_class, account_type_changed);
+ gtk_widget_class_bind_template_callback (widget_class, activate_row);
+ gtk_widget_class_bind_template_callback (widget_class, add_user);
+ gtk_widget_class_bind_template_callback (widget_class, autologin_changed);
+ gtk_widget_class_bind_template_callback (widget_class, change_fingerprint);
+ gtk_widget_class_bind_template_callback (widget_class, change_language);
+ gtk_widget_class_bind_template_callback (widget_class, full_name_edit_button_toggled);
+ gtk_widget_class_bind_template_callback (widget_class, full_name_entry_activate);
+ gtk_widget_class_bind_template_callback (widget_class, full_name_entry_key_press_cb);
+ gtk_widget_class_bind_template_callback (widget_class, change_password);
+ gtk_widget_class_bind_template_callback (widget_class, delete_user);
+ gtk_widget_class_bind_template_callback (widget_class, dismiss_notification);
+ gtk_widget_class_bind_template_callback (widget_class, restart_now);
+ gtk_widget_class_bind_template_callback (widget_class, set_selected_user);
+}
diff --git a/panels/user-accounts/cc-user-panel.h b/panels/user-accounts/cc-user-panel.h
new file mode 100644
index 0000000..6f0aa5a
--- /dev/null
+++ b/panels/user-accounts/cc-user-panel.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <shell/cc-panel.h>
+
+G_BEGIN_DECLS
+
+G_DECLARE_FINAL_TYPE (CcUserPanel, cc_user_panel, CC, USER_PANEL, CcPanel)
+
+G_END_DECLS
diff --git a/panels/user-accounts/cc-user-panel.ui b/panels/user-accounts/cc-user-panel.ui
new file mode 100644
index 0000000..b965280
--- /dev/null
+++ b/panels/user-accounts/cc-user-panel.ui
@@ -0,0 +1,668 @@
+<interface>
+ <object class="GtkButton" id="add_user_button">
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">_Add User…</property>
+ <property name="use_underline">True</property>
+ <property name="has_tooltip">True</property>
+ <property name="tooltip_text" translatable="yes">Create a user account</property>
+ <signal name="clicked" handler="add_user" object="CcUserPanel" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <object class="GtkListStore" id="shortname-model">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ </columns>
+ </object>
+ <object class="GtkListStore" id="language-model">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ </columns>
+ </object>
+ <template class="CcUserPanel" parent="CcPanel">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="CcPermissionInfobar" id="permission_infobar">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="visible-child">no_users_box</property>
+ <child>
+ <object class="GtkOverlay" id="users_overlay">
+ <property name="visible">True</property>
+ <child type="overlay">
+ <object class="GtkRevealer" id="notification_revealer">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_CENTER</property>
+ <property name="valign">GTK_ALIGN_START</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="spacing">6</property>
+ <style>
+ <class name="app-notification"/>
+ </style>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ <property name="max_width_chars">30</property>
+ <property name="label" translatable="yes">Your session needs to be restarted for changes to take effect</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <property name="label" translatable="yes">Restart Now</property>
+ <signal name="clicked" handler="restart_now" object="CcUserPanel" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="dismiss_button">
+ <property name="visible">True</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <signal name="clicked" handler="dismiss_notification" object="CcUserPanel" swapped="yes"/>
+ <style>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">window-close-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="accounts_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="border_width">0</property>
+
+ <child>
+ <object class="CcCarousel" id="carousel">
+ <property name="visible">True</property>
+ <signal name="item-activated" handler="set_selected_user" object="CcUserPanel" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="fill">False</property>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="hscrollbar-policy">GTK_POLICY_NEVER</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_CENTER</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="spacing">20</property>
+ <property name="margin-top">30</property>
+ <property name="margin-bottom">30</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <property name="spacing">10</property>
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <child>
+ <object class="GtkStack" id="user_icon_stack">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="user-icon-button"/>
+ </style>
+ <child>
+ <object class="CcUserImage" id="user_icon_image">
+ <property name="visible">True</property>
+ <property name="icon_name">avatar-default</property>
+ <property name="pixel_size">96</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="user-icon-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">User Icon</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="user_icon_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <style>
+ <class name="user-icon-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">User Icon</property>
+ </object>
+ </child>
+ <child>
+ <object class="CcUserImage" id="user_icon_image2">
+ <property name="visible">True</property>
+ <property name="icon_name">avatar-default</property>
+ <property name="pixel_size">96</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkStack" id="full_name_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="full_name_label">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_END</property>
+ <property name="width-chars">18</property>
+ <property name="max-width-chars">30</property>
+ <property name="xalign">0</property>
+ <attributes>
+ <attribute name="scale" value="1.2"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="full_name_entry">
+ <property name="visible">True</property>
+ <property name="max-length">255</property>
+ <property name="width-chars">18</property>
+ <property name="max-width-chars">30</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <signal name="activate" handler="full_name_entry_activate" object="CcUserPanel" swapped="yes"/>
+ <signal name="key-press-event" handler="full_name_entry_key_press_cb" object="CcUserPanel" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="full_name_edit_button">
+ <property name="visible">True</property>
+ <signal name="toggled" handler="full_name_edit_button_toggled" object="CcUserPanel" swapped="yes"/>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <style>
+ <class name="circular"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="margin">5</property>
+ <property name="icon-name">document-edit-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkBox" id="account_settings_box">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Account Settings</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="account_settings_listbox">
+ <property name="visible">True</property>
+ <property name="selection-mode">GTK_SELECTION_NONE</property>
+ <signal name="row-activated" handler="activate_row"/>
+ <style>
+ <class name="frame"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow" id="account_type_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border-width">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <property name="label" translatable="yes">_Administrator</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">account_type_switch</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="account_type_switch">
+ <property name="visible">True</property>
+ <signal name="notify::active" handler="account_type_changed" object="CcUserPanel" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <property name="max-width-chars">48</property>
+ <property name="label" translatable="yes">Administrators can add and remove other users, and can change settings for all users.</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.9"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkListBoxRow" id="parental_controls_row">
+ <property name="visible">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Parental Controls</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">parental_controls_button_label</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="parental_control_go_next">
+ <property name="visible">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="parental_controls_button_label">
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkListBoxRow" id="language_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Language</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">language_button_label</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="language_button_label">
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkBox" id="authentication_and_login_box">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="spacing">10</property>
+ <property name="margin-top">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Authentication &amp; Login</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="authentication_and_login_listbox">
+ <property name="visible">True</property>
+ <property name="selection-mode">GTK_SELECTION_NONE</property>
+ <signal name="row-activated" handler="activate_row"/>
+ <style>
+ <class name="frame"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow" id="password_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">password_button_label</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="password_button_label">
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="fingerprint_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">_Fingerprint Login</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">fingerprint_state_label</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="fingerprint_state_label">
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="autologin_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">A_utomatic Login</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">autologin_switch</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="autologin_switch">
+ <property name="visible">True</property>
+ <signal name="notify::active" handler="autologin_changed" object="CcUserPanel" swapped="yes"/>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="last_login_row">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Account Activity</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">last_login_button_label</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="last_login_button_label">
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_user_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <property name="label" translatable="yes">Remove User…</property>
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <signal name="clicked" handler="delete_user" object="CcUserPanel" swapped="yes"/>
+ <style>
+ <class name="destructive-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="no_users_box">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ <property name="spacing">12</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon_name">avatar-default-symbolic</property>
+ <property name="pixel_size">192</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes" comments="Translators: This is the empty state page label which states that there are no users to show in the panel.">No Users Found</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.6"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Unlock to add a user account.</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">both</property>
+ <widgets>
+ <widget name="user_icon_button"/>
+ <widget name="user_icon_image"/>
+ </widgets>
+ </object>
+ <object class="GtkSizeGroup">
+ <property name="mode">both</property>
+ <widgets>
+ <widget name="language_row"/>
+ <widget name="password_row"/>
+ <widget name="fingerprint_row"/>
+ <widget name="autologin_row"/>
+ <widget name="last_login_row"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/panels/user-accounts/data/carousel.css b/panels/user-accounts/data/carousel.css
new file mode 100644
index 0000000..738562c
--- /dev/null
+++ b/panels/user-accounts/data/carousel.css
@@ -0,0 +1,30 @@
+.carousel-arrow-container {
+ border-bottom: 1px solid @borders;
+}
+
+.carousel-arrow,
+.carousel-inner-arrow {
+ border-width: 20px; /* ARROW_SIZE */
+ border-style: solid;
+ border-color: transparent;
+}
+
+.carousel-arrow {
+ border-bottom-color: @borders;
+ margin-bottom: -1px;
+ animation-duration: 200ms;
+ animation-timing-function: ease-in-out;
+ animation-fill-mode: forwards;
+}
+
+.carousel-inner-arrow {
+ border-bottom-color: @theme_bg_color;
+ margin-bottom: -2px;
+}
+
+.carousel-item {
+ background: transparent;
+ box-shadow: none;
+ border: none;
+ color: @theme_fg_color;
+}
diff --git a/panels/user-accounts/data/cc-fingerprint-dialog.css b/panels/user-accounts/data/cc-fingerprint-dialog.css
new file mode 100644
index 0000000..800d658
--- /dev/null
+++ b/panels/user-accounts/data/cc-fingerprint-dialog.css
@@ -0,0 +1,83 @@
+.fingerprint-icon {
+ padding: 3px;
+}
+
+.fingerprint-icon > button,
+.fingerprint-icon > image {
+ padding: 15px;
+ min-width: 32px;
+ min-height: 32px;
+ border-radius: 64px;
+ border: 1px solid @borders;
+ background-color: @theme_base_color;
+ color: @insensitive_fg_color;
+}
+
+.fingerprint-print-add image:not(:disabled):not(:backdrop),
+.fingerprint-print-add button:not(:disabled):not(:backdrop) {
+ color: @theme_fg_color;
+}
+
+.fingerprint-icon.enroll-status image {
+ outline-color: @theme_selected_bg_color;
+ outline-offset: 0px;
+ outline-width: 4px;
+}
+
+.fingerprint-icon.enroll-status image:backdrop {
+ outline-color: @theme_unfocused_selected_bg_color;
+}
+
+.fingerprint-icon.enroll-status {
+ font-weight: bold;
+}
+
+.fingerprint-icon.enroll-status.completed image {
+ outline-color: @success_color;
+}
+
+.fingerprint-icon.enroll-status.warning image {
+ outline-color: @warning_color;
+}
+
+.fingerprint-icon.enroll-status.error image {
+ outline-color: @error_color;
+ /* Given we don't have an error image, we can just recolorize the warning one */
+ -gtk-icon-palette: warning @error_color;
+}
+
+.fingerprint-icon.enroll-status.success image:not(:backdrop) {
+ color: @theme_selected_bg_color;
+}
+
+.fingerprint-icon.enroll-status.warning image:not(:backdrop),
+.fingerprint-icon.enroll-status.warning label:not(:backdrop) {
+ color: @warning_color;
+}
+
+.fingerprint-icon.enroll-status.error image:not(:backdrop),
+.fingerprint-icon.enroll-status.error label:not(:backdrop) {
+ color: @error_color;
+}
+
+@keyframes wiggle {
+ /* Unfortunately we can't use translation or xalign, so here's the workaround */
+ 0% { padding-left: 0; padding-right: 0; }
+ 10% { padding-left: 0; padding-right: 2px; }
+ 20% { padding-left: 4px; padding-right: 0; }
+ 30% { padding-left: 0; padding-right: 8px; }
+ 40% { padding-left: 8px; padding-right: 0; }
+ 50% { padding-left: 0; padding-right: 8px; }
+ 60% { padding-left: 8px; padding-right: 0; }
+ 70% { padding-left: 0; padding-right: 8px; }
+ 80% { padding-left: 4px; padding-right: 0; }
+ 90% { padding-left: 0; padding-right: 2px; }
+ 100% { padding-left: 0; padding-right: 0; }
+}
+
+.fingerprint-icon.enroll-status.retry label {
+ animation-name: wiggle;
+ animation-duration: 850ms;
+ animation-timing-function: ease-in;
+ animation-iteration-count: 1;
+}
diff --git a/panels/user-accounts/data/faces/bicycle.jpg b/panels/user-accounts/data/faces/bicycle.jpg
new file mode 100644
index 0000000..c598251
--- /dev/null
+++ b/panels/user-accounts/data/faces/bicycle.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/book.jpg b/panels/user-accounts/data/faces/book.jpg
new file mode 100644
index 0000000..abda4b7
--- /dev/null
+++ b/panels/user-accounts/data/faces/book.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/calculator.jpg b/panels/user-accounts/data/faces/calculator.jpg
new file mode 100644
index 0000000..43ece1e
--- /dev/null
+++ b/panels/user-accounts/data/faces/calculator.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/cat.jpg b/panels/user-accounts/data/faces/cat.jpg
new file mode 100644
index 0000000..99275b2
--- /dev/null
+++ b/panels/user-accounts/data/faces/cat.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/coffee2.jpg b/panels/user-accounts/data/faces/coffee2.jpg
new file mode 100644
index 0000000..4be830d
--- /dev/null
+++ b/panels/user-accounts/data/faces/coffee2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/flower2.jpg b/panels/user-accounts/data/faces/flower2.jpg
new file mode 100644
index 0000000..b77e717
--- /dev/null
+++ b/panels/user-accounts/data/faces/flower2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/gamepad.jpg b/panels/user-accounts/data/faces/gamepad.jpg
new file mode 100644
index 0000000..9843758
--- /dev/null
+++ b/panels/user-accounts/data/faces/gamepad.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/guitar2.jpg b/panels/user-accounts/data/faces/guitar2.jpg
new file mode 100644
index 0000000..f1ad374
--- /dev/null
+++ b/panels/user-accounts/data/faces/guitar2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/headphones.jpg b/panels/user-accounts/data/faces/headphones.jpg
new file mode 100644
index 0000000..2ad9175
--- /dev/null
+++ b/panels/user-accounts/data/faces/headphones.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/hummingbird.jpg b/panels/user-accounts/data/faces/hummingbird.jpg
new file mode 100644
index 0000000..d06a3cf
--- /dev/null
+++ b/panels/user-accounts/data/faces/hummingbird.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/astronaut.jpg b/panels/user-accounts/data/faces/legacy/astronaut.jpg
new file mode 100644
index 0000000..4b79f0e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/astronaut.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/baseball.png b/panels/user-accounts/data/faces/legacy/baseball.png
new file mode 100644
index 0000000..0d6dfdb
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/baseball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/butterfly.png b/panels/user-accounts/data/faces/legacy/butterfly.png
new file mode 100644
index 0000000..66b813c
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/butterfly.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/cat-eye.jpg b/panels/user-accounts/data/faces/legacy/cat-eye.jpg
new file mode 100644
index 0000000..c818bd5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/cat-eye.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/chess.jpg b/panels/user-accounts/data/faces/legacy/chess.jpg
new file mode 100644
index 0000000..7abb8a4
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/chess.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/coffee.jpg b/panels/user-accounts/data/faces/legacy/coffee.jpg
new file mode 100644
index 0000000..46e8fc5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/coffee.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/dice.jpg b/panels/user-accounts/data/faces/legacy/dice.jpg
new file mode 100644
index 0000000..641b124
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/dice.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/energy-arc.jpg b/panels/user-accounts/data/faces/legacy/energy-arc.jpg
new file mode 100644
index 0000000..9f4c892
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/energy-arc.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/fish.jpg b/panels/user-accounts/data/faces/legacy/fish.jpg
new file mode 100644
index 0000000..fc363d6
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/fish.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/flake.jpg b/panels/user-accounts/data/faces/legacy/flake.jpg
new file mode 100644
index 0000000..5546d7e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/flake.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/flower.jpg b/panels/user-accounts/data/faces/legacy/flower.jpg
new file mode 100644
index 0000000..3e41ba4
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/flower.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/grapes.jpg b/panels/user-accounts/data/faces/legacy/grapes.jpg
new file mode 100644
index 0000000..3d31daf
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/grapes.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/guitar.jpg b/panels/user-accounts/data/faces/legacy/guitar.jpg
new file mode 100644
index 0000000..9e8834f
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/guitar.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/launch.jpg b/panels/user-accounts/data/faces/legacy/launch.jpg
new file mode 100644
index 0000000..7c7bf43
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/launch.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/leaf.jpg b/panels/user-accounts/data/faces/legacy/leaf.jpg
new file mode 100644
index 0000000..5354103
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/leaf.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/lightning.jpg b/panels/user-accounts/data/faces/legacy/lightning.jpg
new file mode 100644
index 0000000..736ccd5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/lightning.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/penguin.jpg b/panels/user-accounts/data/faces/legacy/penguin.jpg
new file mode 100644
index 0000000..2a8dfd6
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/penguin.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/puppy.jpg b/panels/user-accounts/data/faces/legacy/puppy.jpg
new file mode 100644
index 0000000..ab55a8b
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/puppy.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sky.jpg b/panels/user-accounts/data/faces/legacy/sky.jpg
new file mode 100644
index 0000000..841f90e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sky.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/soccerball.png b/panels/user-accounts/data/faces/legacy/soccerball.png
new file mode 100644
index 0000000..56588a9
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/soccerball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sunflower.jpg b/panels/user-accounts/data/faces/legacy/sunflower.jpg
new file mode 100644
index 0000000..6102b8b
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sunflower.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sunset.jpg b/panels/user-accounts/data/faces/legacy/sunset.jpg
new file mode 100644
index 0000000..48b6223
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sunset.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/tennis-ball.png b/panels/user-accounts/data/faces/legacy/tennis-ball.png
new file mode 100644
index 0000000..a1beb50
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/tennis-ball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/yellow-rose.jpg b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg
new file mode 100644
index 0000000..7f1de96
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/mountain.jpg b/panels/user-accounts/data/faces/mountain.jpg
new file mode 100644
index 0000000..8425e0d
--- /dev/null
+++ b/panels/user-accounts/data/faces/mountain.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/plane.jpg b/panels/user-accounts/data/faces/plane.jpg
new file mode 100644
index 0000000..a0c4506
--- /dev/null
+++ b/panels/user-accounts/data/faces/plane.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/surfer.jpg b/panels/user-accounts/data/faces/surfer.jpg
new file mode 100644
index 0000000..47f19b0
--- /dev/null
+++ b/panels/user-accounts/data/faces/surfer.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/tomatoes.jpg b/panels/user-accounts/data/faces/tomatoes.jpg
new file mode 100644
index 0000000..ebdb6ba
--- /dev/null
+++ b/panels/user-accounts/data/faces/tomatoes.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/tree.jpg b/panels/user-accounts/data/faces/tree.jpg
new file mode 100644
index 0000000..8a619d3
--- /dev/null
+++ b/panels/user-accounts/data/faces/tree.jpg
Binary files differ
diff --git a/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in b/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in
new file mode 100644
index 0000000..c2e361d
--- /dev/null
+++ b/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in
@@ -0,0 +1,19 @@
+[Desktop Entry]
+Name=Users
+Comment=Add or remove users and change your password
+Exec=gnome-control-center user-accounts
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=system-users
+Terminal=false
+Type=Application
+NoDisplay=true
+StartupNotify=true
+Categories=GNOME;GTK;Settings;X-GNOME-Settings-Panel;X-GNOME-DetailsSettings;
+OnlyShowIn=GNOME;Unity;
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-control-center
+X-GNOME-Bugzilla-Component=user-accounts
+X-GNOME-Bugzilla-Version=@VERSION@
+X-GNOME-Settings-Panel=user-accounts
+# Translators: Search terms to find the Users panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon!
+Keywords=Login;Name;Fingerprint;Avatar;Logo;Face;Password;
diff --git a/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg
new file mode 100644
index 0000000..a225384
--- /dev/null
+++ b/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467">
+ <path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M 16.132812,0.99023438 C 13.902514,0.96755721 11.66847,1.5168081 9.65625,2.6425781 8.4831917,3.288913 9.4616217,5.0473362 10.628906,4.390625 14.036766,2.4839667 18.198267,2.527856 21.566406,4.5039062 24.934547,6.4799188 27,10.08815 27,13.994141 v 2 c -5.97e-4,1.33435 1.999403,1.33435 2,0 v -2 c 3.54e-4,-0.03325 -9.56e-4,-0.06649 -0.0039,-0.09961 C 28.961052,9.320245 26.52689,5.092052 22.578125,2.7753906 20.589365,1.6086127 18.363092,1.0130627 16.132812,0.99023438 Z M 5.7617188,6.640625 C 5.4310747,6.6594699 5.1312075,6.840853 4.9609375,7.125 3.6959444,9.158537 3.0222407,11.500805 3.0039062,13.894531 3.00095,13.927649 2.9996462,13.960893 3,13.994141 v 10.128906 c 0,1.333754 2,1.333754 2,0 V 13.994141 C 5,11.939136 5.5747962,9.9244686 6.6601562,8.1796875 7.1014645,7.4921558 6.5771855,6.5940433 5.7617188,6.640625 Z M 16,6.9921875 c -3.813165,0 -6.9283277,3.0816025 -6.9941406,6.8808595 -0.0044,0.04021 -0.00636,0.08065 -0.00586,0.121094 v 2 c 5.96e-4,1.333157 2.000596,1.333157 2,0 v -2 c 0,-2.774325 2.22666,-5.0019535 5,-5.0019535 2.77334,0 5,2.2276285 5,5.0019535 V 22.125 c 0.426667,0.161975 0.81076,0.41915 1.123047,0.751953 L 23,23.753906 v -9.759765 c 2.65e-4,-0.03849 -0.0017,-0.07697 -0.0059,-0.115235 C 22.931551,10.076928 19.815145,6.9921875 16,6.9921875 Z m -0.01563,5.9863285 C 15.43218,12.98705 14.991449,13.441767 15,13.994141 v 10.003906 c 0,0 -5.87e-4,1.09432 0.269531,2.445312 0.2701,1.350992 0.787778,3.027578 2.023438,4.263672 0.942205,0.981983 2.395438,-0.47125 1.414062,-1.414062 -0.690333,-0.690533 -1.138586,-1.835412 -1.390625,-2.898438 l -0.002,-0.002 C 16.951575,25.742041 16.843618,24.979658 17.011719,24.253906 17.009401,24.177001 17,23.998047 17,23.998047 V 13.994141 c 0.0087,-0.564623 -0.451183,-1.024549 -1.015625,-1.015625 z m -6,8.001953 C 9.4321797,20.989003 8.9914495,21.44372 9,21.996094 v 6.001953 c -5.966e-4,1.33435 1.999403,1.33435 2,0 v -6.001953 c 0.0087,-0.564623 -0.451183,-1.024549 -1.015625,-1.015625 z m 20.998047,0.0059 a 1.0001,1.0001 0 0 0 -0.6875,0.302734 l -6.296875,6.289063 -3.291016,-3.292969 a 1.0001,1.0001 0 1 0 -1.410156,1.417969 l 4.701172,4.703125 7.707031,-7.707031 a 1.0001,1.0001 0 0 0 -0.722656,-1.712891 z" transform="scale(.26458)" class="success" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#33d17a"/>
+</svg>
diff --git a/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg
new file mode 100644
index 0000000..00e31cc
--- /dev/null
+++ b/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467">
+ <path d="m 4.2684727,288.79533 c -0.5901,-0.006 -1.1811905,0.13932 -1.7135904,0.43718 a 0.26457931,0.26466281 0 1 0 0.2573486,0.46251 c 0.9016629,-0.50447 2.0027268,-0.49283 2.8938802,0.03 0.8911538,0.52282 1.4376385,1.4775 1.4376385,2.51096 v 0.52916 a 0.26458335,0.26466684 0 1 0 0.5291667,0 v -0.52916 a 0.26456806,0.26465154 0 0 0 -0.00103,-0.0264 c -0.00927,-1.21028 -0.6533118,-2.32899 -1.6980893,-2.94194 -0.5261927,-0.30871 -1.1152275,-0.46628 -1.7053224,-0.47232 z m -2.7440185,1.495 a 0.26456806,0.26465154 0 0 0 -0.2118734,0.12816 c -0.3346961,0.53804 -0.51294685,1.15776 -0.51779785,1.7911 a 0.26456806,0.26465154 0 0 0 -0.001034,0.0264 v 2.67994 a 0.26458334,0.26466683 0 0 0 0.52916665,0 v -2.67994 c 0,-0.54372 0.1520815,-1.07677 0.4392497,-1.53841 a 0.26456806,0.26465154 0 0 0 -0.2377116,-0.40721 z m 2.7088787,0.093 c -1.0089,0 -1.8331201,0.81534 -1.8505331,1.82056 a 0.26456806,0.26465154 0 0 0 -0.00155,0.032 v 0.52916 a 0.26458335,0.26466684 0 1 0 0.5291667,0 v -0.52916 c 0,-0.73404 0.5891371,-1.32344 1.3229167,-1.32344 0.7337795,0 1.3229167,0.5894 1.3229167,1.32344 v 2.64686 c 0,0 0.00212,0.18248 0.079582,0.41445 0.077301,0.23197 0.2313432,0.5351 0.5270998,0.83096 a 0.26456806,0.26465154 0 1 0 0.3741372,-0.37414 c -0.2334101,-0.23348 -0.3439509,-0.45923 -0.3989419,-0.62425 -0.054991,-0.16503 -0.05271,-0.24702 -0.05271,-0.24702 v -2.64686 a 0.26456806,0.26465154 0 0 0 -0.00156,-0.0305 c -0.016571,-1.00594 -0.8410985,-1.82211 -1.8505223,-1.82211 z m -0.00414,1.58388 a 0.26456806,0.26465154 0 0 0 -0.2604479,0.26872 v 2.64686 c 0,0 -1.558e-4,0.28954 0.071313,0.64699 0.071464,0.35745 0.208433,0.80105 0.535368,1.1281 a 0.26456806,0.26465154 0 1 0 0.3741373,-0.37414 c -0.2022317,-0.20229 -0.329846,-0.55309 -0.3906737,-0.85731 -0.060823,-0.30422 -0.060978,-0.54364 -0.060978,-0.54364 v -2.64686 a 0.26456806,0.26465154 0 0 0 -0.2687175,-0.26872 z m -1.5875,2.11718 a 0.26456806,0.26465154 0 0 0 -0.2604479,0.26872 v 1.58802 a 0.26458335,0.26466684 0 1 0 0.5291667,0 v -1.58802 a 0.26456806,0.26465154 0 0 0 -0.2687175,-0.26872 z m 4.7625001,0 a 0.26456806,0.26465154 0 0 0 -0.2604479,0.26769 v 0.52968 a 0.26458335,0.26466684 0 1 0 0.5291667,0 v -0.52968 a 0.26456806,0.26465154 0 0 0 -0.2687175,-0.26769 z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#3d3846" transform="translate(0 -288.533)"/>
+</svg>
diff --git a/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg
new file mode 100644
index 0000000..5b3fa81
--- /dev/null
+++ b/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467">
+ <path overflow="visible" font-weight="400" d="m 4.2684194,0.26199621 c -0.5900922,-0.006 -1.1811756,0.13932088 -1.7135688,0.4371771 A 0.26457597,0.26465948 0 1 0 2.8121959,1.1616716 C 3.7138475,0.65720791 4.8148975,0.66882014 5.7060397,1.1916435 6.5971824,1.7144569 7.14366,2.6691227 7.14366,3.7025698 v 0.52916 a 0.26458003,0.2646635 0 1 0 0.52916,0 v -0.52916 A 0.26456472,0.26464821 0 0 0 7.6717881,3.676215 C 7.6625167,2.4659504 7.0184846,1.3472551 5.9737203,0.73431284 5.4475342,0.42560675 4.8585069,0.26803613 4.2684194,0.26199621 Z M 1.5244356,1.7569766 A 0.26456472,0.26464821 0 0 0 1.3125648,1.8851325 C 0.97787297,2.4231657 0.79962444,3.042883 0.7947735,3.676215 A 0.26456472,0.26464821 0 0 0 0.79374,3.7025698 v 2.679906 a 0.26458,0.2646635 0 0 0 0.52916,0 v -2.679906 c 0,-0.5437132 0.1520796,-1.0767539 0.4392441,-1.5383881 A 0.26456472,0.26464821 0 0 0 1.5244356,1.7569766 Z M 4.23328,1.849993 c -1.0088872,0 -1.8330969,0.8153304 -1.8505097,1.8205378 a 0.26456472,0.26464821 0 0 0 -0.00155,0.032039 v 0.52916 a 0.26458003,0.2646635 0 0 0 0.52916,0 v -0.52916 c 0,-0.7340309 0.5891297,-1.3234168 1.3229,-1.3234168 0.7337703,0 1.3229,0.5893859 1.3229,1.3234168 V 4.4110447 C 5.6854821,4.2368203 5.8715938,4.0894366 6.08534,4.0260602 V 3.7025698 A 0.26456472,0.26464821 0 0 0 6.083779,3.6720809 C 6.0672189,2.6661536 5.2426911,1.849993 4.23328,1.849993 Z M 4.22914,3.4338558 A 0.26456472,0.26464821 0 0 0 3.9687,3.7025698 v 2.6468335 c 0,0 -1.553e-4,0.2895352 0.071312,0.6469806 0.00877,0.043848 0.022106,0.091907 0.033073,0.1379745 L 4.49786,6.3468194 V 3.7025698 A 0.26456472,0.26464821 0 0 0 4.2291459,3.4338558 Z M 6.3251097,4.5226644 C 6.1860729,4.5146644 6.0504148,4.5975684 5.944776,4.7789762 L 4.2911556,7.8464506 C 4.1500114,8.1013375 4.3039147,8.46656 4.5805412,8.46656 h 3.4808807 c 0.2597435,0 0.5035248,-0.3067533 0.3224568,-0.6201094 L 6.7137175,4.7955125 C 6.6082242,4.6245446 6.4641524,4.5302343 6.3251156,4.5226644 Z M 2.6416659,5.5510125 A 0.26456472,0.26464821 0 0 0 2.38122,5.8197266 v 1.5879967 a 0.26458003,0.2646635 0 1 0 0.52916,0 V 5.8197266 A 0.26456472,0.26464821 0 0 0 2.6416659,5.5510125 Z m 3.699986,0.013436 C 6.4857104,5.5594481 6.6190614,5.693238 6.6145,5.8372962 V 6.87908 C 6.61635,7.0188682 6.4897161,7.14366 6.34992,7.14366 6.2101265,7.14366 6.0833636,7.0188682 6.08534,6.87908 V 5.8372962 c -0.00212,-0.1234583 0.093688,-0.2414303 0.2149712,-0.26458 0.01352,-0.004 0.027371,-0.00675 0.041341,-0.00775 z M 6.34992,7.40824 c 0.1461236,0 0.26458,0.1184617 0.26458,0.26458 0,0.1461281 -0.1184564,0.26458 -0.26458,0.26458 -0.1461236,0 -0.26458,-0.1184519 -0.26458,-0.26458 0,-0.1461183 0.1184564,-0.26458 0.26458,-0.26458 z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" class="warning" color="#000" font-family="sans-serif" fill="#ff7800"/>
+</svg>
diff --git a/panels/user-accounts/data/icons/left-index-finger.svg b/panels/user-accounts/data/icons/left-index-finger.svg
new file mode 100644
index 0000000..3c36aea
--- /dev/null
+++ b/panels/user-accounts/data/icons/left-index-finger.svg
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="left-index-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="36"
+ inkscape:window-y="91"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1605241,0,0,1.3370602,-4.3871473,-0.7984997)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="translate(-52.466647,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/left-little-finger.svg b/panels/user-accounts/data/icons/left-little-finger.svg
new file mode 100644
index 0000000..0835854
--- /dev/null
+++ b/panels/user-accounts/data/icons/left-little-finger.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="left-pinky-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-ring-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="122"
+ inkscape:window-y="443"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1074589,0,0,1.2726911,-25.531655,5.5330271)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="translate(-52.466647,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/left-middle-finger.svg b/panels/user-accounts/data/icons/left-middle-finger.svg
new file mode 100644
index 0000000..1082da2
--- /dev/null
+++ b/panels/user-accounts/data/icons/left-middle-finger.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="left-middle-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-index-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="122"
+ inkscape:window-y="443"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1824583,0,0,1.3363867,-12.845608,-2.0066594)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="translate(-52.466647,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/left-ring-finger.svg b/panels/user-accounts/data/icons/left-ring-finger.svg
new file mode 100644
index 0000000..50ace80
--- /dev/null
+++ b/panels/user-accounts/data/icons/left-ring-finger.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="left-ring-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-middle-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="122"
+ inkscape:window-y="443"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1824583,0,0,1.3363867,-20.636466,-0.7947482)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="translate(-52.466647,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/left-thumb.svg b/panels/user-accounts/data/icons/left-thumb.svg
new file mode 100644
index 0000000..fd0f582
--- /dev/null
+++ b/panels/user-accounts/data/icons/left-thumb.svg
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="left-thumb.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-pinky-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="116"
+ inkscape:window-y="498"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1916623,0,0,1.4021101,4.5265732,14.334323)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="translate(-52.466647,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/print_error.svg b/panels/user-accounts/data/icons/print_error.svg
new file mode 100644
index 0000000..4ad6bee
--- /dev/null
+++ b/panels/user-accounts/data/icons/print_error.svg
@@ -0,0 +1,525 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 36.184 43.865"
+ enable-background="new 0 0 36.184 43.865"
+ xml:space="preserve"
+ id="svg2419"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="print_error.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/print_error.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata2435"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs2433"><linearGradient
+ inkscape:collect="always"
+ id="linearGradient84104"><stop
+ style="stop-color:#fffffc;stop-opacity:1;"
+ offset="0"
+ id="stop84106" /><stop
+ style="stop-color:#fffffc;stop-opacity:0;"
+ offset="1"
+ id="stop84108" /></linearGradient><linearGradient
+ id="linearGradient84076"><stop
+ style="stop-color:#73d216;stop-opacity:1;"
+ offset="0"
+ id="stop84078" /><stop
+ id="stop84090"
+ offset="0.31459025"
+ style="stop-color:#73d216;stop-opacity:1;" /><stop
+ style="stop-color:#4e9a06;stop-opacity:1;"
+ offset="1"
+ id="stop84080" /></linearGradient><linearGradient
+ id="linearGradient3531"><stop
+ id="stop3533"
+ offset="0"
+ style="stop-color:#9b9b9b;stop-opacity:1;" /><stop
+ id="stop3535"
+ offset="1"
+ style="stop-color:#414141;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient3483"><stop
+ id="stop3485"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" /><stop
+ id="stop3487"
+ offset="1"
+ style="stop-color:#787878;stop-opacity:1" /></linearGradient><linearGradient
+ id="linearGradient3263"><stop
+ id="stop3265"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop3267"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient3247"><stop
+ style="stop-color:#52a714;stop-opacity:1;"
+ offset="0"
+ id="stop3249" /><stop
+ style="stop-color:#398800;stop-opacity:1;"
+ offset="1"
+ id="stop3251" /></linearGradient><linearGradient
+ id="linearGradient3233"><stop
+ id="stop3235"
+ offset="0"
+ style="stop-color:#398800;stop-opacity:1;" /><stop
+ id="stop3237"
+ offset="1"
+ style="stop-color:#84c706;stop-opacity:1;" /></linearGradient><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 21.932501 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="36.183998 : 21.932501 : 1"
+ inkscape:persp3d-origin="18.091999 : 14.621667 : 1"
+ id="perspective2437" />
+
+
+
+
+
+ <filter
+ inkscape:collect="always"
+ id="filter3471"><feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.057808254"
+ id="feGaussianBlur3473" /></filter><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3233"
+ id="linearGradient3517"
+ gradientUnits="userSpaceOnUse"
+ x1="25.144751"
+ y1="43.865002"
+ x2="25.144751"
+ y2="23.838018" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3247"
+ id="linearGradient3519"
+ gradientUnits="userSpaceOnUse"
+ x1="30.691881"
+ y1="23.365002"
+ x2="30.691881"
+ y2="44.365963" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient3521"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="55.692348"
+ x2="18.072493"
+ y1="29.205048"
+ x1="21.55229"
+ id="linearGradient5138"
+ xlink:href="#linearGradient5132"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient5132"
+ inkscape:collect="always"><stop
+ id="stop5134"
+ offset="0"
+ style="stop-color:white;stop-opacity:1;" /><stop
+ id="stop5136"
+ offset="1"
+ style="stop-color:white;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient1913"><stop
+ id="stop1915"
+ offset="0"
+ style="stop-color:#73d216;stop-opacity:1" /><stop
+ id="stop1917"
+ offset="1"
+ style="stop-color:#8ae234;stop-opacity:1" /></linearGradient><inkscape:perspective
+ id="perspective84036"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84076"
+ id="radialGradient84088"
+ cx="26.183998"
+ cy="40.111427"
+ fx="26.183998"
+ fy="40.111427"
+ r="10.5"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.9228377,7.3282173e-8,-7.9001009e-8,0.8924238,2.0204218,4.4167191)" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84092"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84104"
+ id="linearGradient84110"
+ x1="28.185518"
+ y1="22.649143"
+ x2="27.596079"
+ y2="42.648415"
+ gradientUnits="userSpaceOnUse" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84076"
+ id="radialGradient84134"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0862379,-2.5925308e-8,1.5464794e-8,0.8186283,-2.2580516,7.302016)"
+ cx="26.183998"
+ cy="39.098457"
+ fx="26.183998"
+ fy="39.098457"
+ r="10.5" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84136"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84104"
+ id="linearGradient84138"
+ gradientUnits="userSpaceOnUse"
+ x1="21.515692"
+ y1="23.09075"
+ x2="34.488232"
+ y2="40.661182" /><filter
+ inkscape:collect="always"
+ id="filter84266"
+ x="-0.07103052"
+ width="1.142061"
+ y="-0.5276553"
+ height="2.0553105"><feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.45756194"
+ id="feGaussianBlur84268" /></filter><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84277"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ id="linearGradient5171"><stop
+ style="stop-color:#fe3a00;stop-opacity:1"
+ offset="0"
+ id="stop5173" /><stop
+ style="stop-color:#c00;stop-opacity:1;"
+ offset="1"
+ id="stop5175" /></linearGradient><inkscape:perspective
+ id="perspective7871"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective7973"
+ inkscape:persp3d-origin="14 : 9.3333333 : 1"
+ inkscape:vp_z="28 : 14 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 14 : 1"
+ sodipodi:type="inkscape:persp3d" />
+
+<radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5171"
+ id="radialGradient8812"
+ cx="26.184002"
+ cy="39.797016"
+ fx="26.184002"
+ fy="39.797016"
+ r="10.65866"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.9444304,0,0,0.7220468,1.4550326,11.504981)" /><linearGradient
+ y2="40.661182"
+ x2="34.488232"
+ y1="23.09075"
+ x1="21.515692"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient84279"
+ xlink:href="#linearGradient84104"
+ inkscape:collect="always" /><linearGradient
+ y2="30.466549"
+ x2="26.455547"
+ y1="24.322035"
+ x1="26.455547"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9095"
+ xlink:href="#linearGradient3263"
+ inkscape:collect="always" /><linearGradient
+ y2="40.661182"
+ x2="34.488232"
+ y1="23.09075"
+ x1="21.515692"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9089"
+ xlink:href="#linearGradient84104"
+ inkscape:collect="always" /><linearGradient
+ y2="30.466549"
+ x2="26.455547"
+ y1="24.322035"
+ x1="26.455547"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9087"
+ xlink:href="#linearGradient3263"
+ inkscape:collect="always" /><radialGradient
+ r="10.5"
+ fy="39.098457"
+ fx="26.183998"
+ cy="39.098457"
+ cx="26.183998"
+ gradientTransform="matrix(1.0862379,-2.5925308e-8,1.5464794e-8,0.8186283,-2.2580516,7.302016)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient9085"
+ xlink:href="#linearGradient84076"
+ inkscape:collect="always" /><linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="42.648415"
+ x2="27.596079"
+ y1="22.649143"
+ x1="28.185518"
+ id="linearGradient9083"
+ xlink:href="#linearGradient84104"
+ inkscape:collect="always" /><linearGradient
+ y2="30.466549"
+ x2="26.455547"
+ y1="24.322035"
+ x1="26.455547"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9081"
+ xlink:href="#linearGradient3263"
+ inkscape:collect="always" /><radialGradient
+ gradientTransform="matrix(0.9228377,7.3282173e-8,-7.9001009e-8,0.8924238,2.0204218,4.4167191)"
+ gradientUnits="userSpaceOnUse"
+ r="10.5"
+ fy="40.111427"
+ fx="26.183998"
+ cy="40.111427"
+ cx="26.183998"
+ id="radialGradient9079"
+ xlink:href="#linearGradient84076"
+ inkscape:collect="always" /><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective9077" /><linearGradient
+ id="linearGradient9071"><stop
+ style="stop-color:#73d216;stop-opacity:1"
+ offset="0"
+ id="stop9073" /><stop
+ style="stop-color:#8ae234;stop-opacity:1"
+ offset="1"
+ id="stop9075" /></linearGradient><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient5132"
+ id="linearGradient9063"
+ x1="21.55229"
+ y1="29.205048"
+ x2="18.072493"
+ y2="55.692348"
+ gradientUnits="userSpaceOnUse" /><linearGradient
+ y2="30.466549"
+ x2="26.455547"
+ y1="24.322035"
+ x1="26.455547"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9061"
+ xlink:href="#linearGradient3263"
+ inkscape:collect="always" /><linearGradient
+ y2="44.365963"
+ x2="30.691881"
+ y1="23.365002"
+ x1="30.691881"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9059"
+ xlink:href="#linearGradient3247"
+ inkscape:collect="always" /><linearGradient
+ y2="23.838018"
+ x2="25.144751"
+ y1="43.865002"
+ x1="25.144751"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient9057"
+ xlink:href="#linearGradient3233"
+ inkscape:collect="always" />
+
+
+
+
+
+ <inkscape:perspective
+ id="perspective9051"
+ inkscape:persp3d-origin="18.091999 : 14.621667 : 1"
+ inkscape:vp_z="36.183998 : 21.932501 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 21.932501 : 1"
+ sodipodi:type="inkscape:persp3d" /><linearGradient
+ id="linearGradient9045"><stop
+ style="stop-color:#398800;stop-opacity:1;"
+ offset="0"
+ id="stop9047" /><stop
+ style="stop-color:#84c706;stop-opacity:1;"
+ offset="1"
+ id="stop9049" /></linearGradient><linearGradient
+ id="linearGradient9039"><stop
+ id="stop9041"
+ offset="0"
+ style="stop-color:#52a714;stop-opacity:1;" /><stop
+ id="stop9043"
+ offset="1"
+ style="stop-color:#398800;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient9033"><stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop9035" /><stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop9037" /></linearGradient><linearGradient
+ id="linearGradient9027"><stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop9029" /><stop
+ style="stop-color:#787878;stop-opacity:1"
+ offset="1"
+ id="stop9031" /></linearGradient><linearGradient
+ id="linearGradient9021"><stop
+ style="stop-color:#9b9b9b;stop-opacity:1;"
+ offset="0"
+ id="stop9023" /><stop
+ style="stop-color:#414141;stop-opacity:1;"
+ offset="1"
+ id="stop9025" /></linearGradient><linearGradient
+ id="linearGradient9013"><stop
+ id="stop9015"
+ offset="0"
+ style="stop-color:#73d216;stop-opacity:1;" /><stop
+ style="stop-color:#73d216;stop-opacity:1;"
+ offset="0.31459025"
+ id="stop9017" /><stop
+ id="stop9019"
+ offset="1"
+ style="stop-color:#4e9a06;stop-opacity:1;" /></linearGradient><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient9115"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /></defs><sodipodi:namedview
+ inkscape:window-height="733"
+ inkscape:window-width="1263"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="10.958333"
+ inkscape:cx="16.307224"
+ inkscape:cy="24"
+ inkscape:window-x="6"
+ inkscape:window-y="140"
+ inkscape:current-layer="svg2419" />
+<path
+ id="path2424"
+ d="M 11.485207,8.6743869 C 11.872117,8.5219533 18.066562,7.5772471 17.547442,14.819684 C 16.836811,24.751336 10.199071,21.863582 8.4570051,28.091683 C 9.1530536,27.855507 9.3319265,27.184615 9.77036,26.633222 C 11.09052,24.971517 11.912946,24.144427 13.199081,23.591154 C 17.777838,21.620819 20.261644,13.237019 16.405184,9.3659791 C 14.958646,7.9131637 12.270692,8.0514823 11.485207,8.6743869 z M -0.41567362,30.622819 C -0.62079412,30.073309 -0.81036052,29.508743 -0.98534482,28.932886 C 0.28134733,30.510848 2.408639,29.511665 3.7800623,29.851546 C 7.7792565,30.842672 10.201988,29.700696 12.4972,27.002072 C 14.538683,24.600785 15.730521,26.026314 17.692291,22.415916 C 18.187107,21.506024 19.527682,20.515211 20.049718,16.478567 C 20.335526,14.26641 21.73087,14.007651 21.560747,12.407106 C 21.300215,9.95501 21.209476,9.6012157 20.182901,7.9460967 C 18.706228,5.5636294 16.443158,4.9249489 14.396731,4.9187631 C 12.674334,4.9140584 11.84411,5.6088349 11.260829,5.8365431 C 15.390232,4.8356399 18.977454,6.2618089 19.977781,8.6743869 C 20.486207,9.9004346 20.744795,10.273048 20.835204,11.578136 C 20.978108,13.644446 20.307334,14.416019 20.263588,13.790293 C 20.079855,11.11331 19.152438,7.8049552 16.050354,6.738867 C 14.094417,6.0670338 11.302445,6.5779657 9.6303729,7.8454158 C 7.5908326,9.7988133 6.4952361,12.616937 6.4952361,14.474358 C 6.4952361,18.037708 6.4689884,19.168722 5.854599,22.494014 C 5.596011,23.89696 4.6627611,25.357303 5.2022963,27.278709 C 5.9975029,26.985134 6.7110502,25.878585 6.9949136,25.111717 C 8.9868188,19.727642 10.292396,20.99227 12.343602,18.829042 C 12.831614,18.314346 13.669595,16.922691 13.771669,16.409877 C 14.018591,15.167833 14.629092,12.200099 12.557472,12.269729 C 10.832904,13.36875 11.557145,15.649595 10.237957,17.872102 C 12.271664,16.963152 12.138481,13.127869 12.885082,13.037538 C 13.533495,12.95944 13.493638,14.504468 13.351707,15.328734 C 13.046456,17.100529 12.422345,18.101693 11.019554,19.120734 C 9.9006261,19.933709 8.8604412,20.065441 7.8484484,21.773252 C 7.6579099,22.073413 6.3027534,25.346012 6.134574,25.654641 C 5.2858998,27.216607 5.3879741,25.564311 6.3212239,22.946607 C 6.3212239,22.946607 7.2311425,20.110606 7.1115699,17.014904 C 7.0571303,15.581848 7.1368454,9.9201951 10.843597,7.9150456 C 12.438871,7.052201 14.9149,6.5102179 16.620025,7.776727 C 21.95802,11.740919 19.47227,21.463682 15.405828,24.214058 C 13.770697,25.320606 12.413596,25.389296 11.019554,27.516768 C 9.932706,29.17565 6.8500655,30.289726 4.7074794,29.321496 C 5.5649027,28.845379 6.8957558,28.803036 7.2787772,27.73883 C 10.085332,19.931827 15.530262,23.306048 16.183536,14.966471 C 16.414904,12.009088 14.877959,9.6953099 12.69746,9.6953099 C 10.466409,9.6953099 8.9664042,12.675276 8.6281009,15.440706 C 8.4657544,16.774023 8.3782621,18.528881 8.2713273,19.450064 C 10.056167,18.413145 8.4851971,11.162239 12.484562,10.747283 C 15.963834,10.386902 15.66636,14.964589 15.26973,16.96127 C 14.967396,18.483716 14.270374,19.795389 11.270365,21.516375 C 9.690645,22.422502 8.643655,23.573275 7.8426157,25.457043 C 7.3730743,26.56171 6.4126046,28.452065 4.547077,28.632726 C 4.3332073,28.653426 3.9268547,26.573943 3.8004772,25.47304 C 3.6138271,23.848031 4.453752,22.765006 4.7327549,21.015793 C 5.0438382,19.072746 5.2615964,17.525836 5.2781228,14.197721 C 4.4936096,9.0893427 5.2917327,7.5791289 7.4226532,5.1562011 C 9.9142358,2.3220811 12.045219,2.0705493 13.508882,2.0570557 C 15.652565,2.0393053 16.899028,3.5509526 16.263252,3.4276891 C 14.62326,3.1105913 11.630055,2.6909308 9.0577845,5.2945196 C 7.6229131,6.7454536 6.0402768,8.6122845 6.2084562,10.886543 C 6.8802017,7.6195895 10.334508,3.7516736 13.747676,3.7516736 C 17.577206,3.7516736 19.976808,5.1712562 21.120039,7.0155042 C 22.280769,8.8889209 22.761976,10.297513 22.761976,12.822062 C 22.761976,14.686071 21.730541,15.451057 21.262943,16.962211 C 21.083098,17.542773 20.463848,19.045459 20.191651,20.211286 C 19.097998,24.896241 18.309596,27.152622 14.271346,30.220096 C 12.511782,31.556235 11.111907,31.646565 10.179629,31.285243 C 10.187406,31.13281 12.366934,30.934272 13.724035,28.937591 C 15.002392,27.055705 16.650162,27.007717 17.396761,24.840726 C 16.622941,25.778846 15.419438,26.673683 14.41425,27.115925 C 14.130387,27.241071 13.618072,27.886557 13.421701,28.116148 C 11.368551,30.531548 9.713004,31.103642 7.3458545,31.373692 C 7.7220709,31.550589 8.110925,31.687026 8.5231103,31.739719 C 11.53187,32.124565 13.387675,32.163143 15.774267,30.055431 C 19.142717,27.08111 19.696834,25.201106 20.047775,22.967308 C 20.307334,21.313131 21.669648,17.436727 22.261326,15.868175 C 23.094574,13.65922 22.570465,18.105457 22.332292,18.688842 C 20.93825,22.115755 21.500144,26.735784 17.955739,30.380997 C 15.523456,32.882964 12.32416,33.811675 7.1358733,32.637379 C 5.7525248,32.324044 6.253216,31.857336 4.671552,31.373692 C 4.1971499,31.159157 1.6540024,31.013311 1.3623619,31.011429 C 0.72269676,31.007665 0.11997297,30.875933 -0.41567362,30.622819 z M 4.2680743,5.1477326 C 2.8681995,6.7162844 3.1442858,7.9310416 2.2158966,9.2445983 C 0.39119867,11.826545 1.4517984,15.393659 1.0658607,17.99913 C 0.56618308,21.378997 0.52283784,20.110089 0.0065640818,17.990301 C -0.75463862,14.864849 -0.20180392,13.731954 -0.076398418,12.407106 C 0.6186783,9.1298026 1.2262629,7.7560263 2.8691715,5.6887748 C 4.2253003,3.9828454 6.149156,3.0371977 4.2680743,5.1477326 z M 24.036445,36.450079 C 23.488161,37.083333 22.896131,37.673305 22.262298,38.211523 L 21.475841,38.506979 C 18.241547,39.711387 16.58114,39.821477 11.857534,39.082837 C 9.932706,38.781735 7.3050248,38.411944 5.4978253,39.277612 C 6.5839482,39.510065 10.395401,38.361053 11.576587,39.986142 C 11.459931,40.156453 9.2113823,39.88358 7.0960157,40.347465 C 6.5837004,40.058595 6.0888836,39.731146 5.6115651,39.367002 C 5.1527173,39.009443 4.8173306,38.720573 4.4926375,38.413826 C 6.9570004,37.030641 10.904842,37.507699 13.34879,37.820091 C 17.581468,38.362075 19.260345,38.181414 21.964825,37.188718 C 22.696843,36.919609 23.388031,36.670259 24.036445,36.450079 z M 26.094456,7.8981086 C 27.151167,9.9813566 28.257457,11.575313 28.257457,14.734999 C 28.257457,18.212845 25.759069,19.908303 25.360494,22.223022 C 24.987194,24.390014 24.927894,26.193802 24.599312,26.193802 C 24.132687,26.193802 23.98395,24.125609 24.054915,22.674675 C 24.126854,21.205864 24.314124,20.404861 24.655344,19.310545 C 25.039337,18.081673 25.62266,15.331577 25.504059,13.251152 C 25.348517,10.505481 25.590559,11.089486 26.303134,12.745546 C 26.769759,13.828571 26.261622,15.950138 26.303134,17.484815 C 27.329709,15.648996 27.135261,12.882863 26.349775,10.877714 C 26.087298,10.20494 25.726989,8.8446969 26.094456,7.8981086 z M 24.622001,9.2587722 C 24.485902,8.654687 23.723397,6.4935811 22.726959,5.2026076 C 21.232786,3.2689699 22.578201,3.6726143 23.649494,4.846911 C 24.696483,5.9948613 25.472289,7.3598893 25.501453,8.680032 C 25.560753,11.39277 25.122651,11.470929 24.622001,9.2587722 z M 28.324534,16.874703 C 28.602565,16.515264 28.874762,18.765998 28.371196,19.556391 C 27.026733,21.665043 27.509885,25.128655 26.639823,27.894085 C 25.254531,32.293934 20.848813,35.618286 15.998831,35.528896 C 19.543236,34.354599 22.714341,33.813557 24.183237,26.840229 C 24.312532,26.225793 25.174816,26.557005 24.397107,29.459814 C 25.254531,28.223415 25.999186,25.124891 26.111954,22.623864 C 26.186809,20.930167 27.729588,18.502534 28.324534,16.874703 z M 0.35036898,7.0776065 C 3.1160939,2.0115701 8.6912897,0.54275818 13.214636,0.54275818 C 16.041605,0.54275818 18.507912,1.0273437 20.818678,2.3550142 C 21.174479,2.6147144 21.713042,2.9355759 22.002738,3.2253863 C 22.938905,4.1653883 21.369626,3.7445623 19.405193,2.8753556 C 15.80217,1.2811182 13.224356,1.5241817 11.057467,1.8713897 C 9.4544161,2.1282671 8.4327017,3.4107521 7.629718,4.1889118 C 4.6705382,7.05126 4.2097461,9.6106249 4.5655476,12.822062 C 5.0068971,16.806955 4.2680743,21.015793 3.3367686,23.309812 C 2.8604223,24.48505 2.9236112,26.011258 3.1365088,26.978548 C 3.5214743,28.723056 5.0173803,29.448829 3.1481744,29.208582 C -0.12595652,28.787761 -0.12403302,24.289333 0.99392257,21.109887 C 2.1361815,17.860811 1.50895,15.966631 1.7492717,13.57764 C 2.8163869,2.8893489 3.1267874,15.841548 3.0655428,16.408937 C 2.7077971,19.726701 2.2340211,20.608302 1.7385783,22.084704 C 0.6727661,25.260787 1.9314335,27.1104 2.2661583,27.366598 C 0.91096007,23.04949 3.2794126,21.655634 3.4660626,16.648877 C 3.5117529,15.433179 3.5311956,14.688893 3.3455178,13.046948 C 2.7748744,7.9799706 4.0814242,5.0574021 6.9725545,2.7097496 C 5.2188225,2.7266866 3.8724152,3.9950776 2.6825216,5.1477326 C 1.376944,6.4114188 0.16469114,9.2935264 -0.21249732,10.402898 C -0.77633582,12.06178 -1.2711526,10.046281 0.35036898,7.0776065 z M -1.1097781,28.513226 C -2.0128918,25.368594 -2.4649347,21.861701 -2.3900803,18.216487 C -2.3609163,16.765553 -2.2306501,15.171597 -1.7834679,13.305707 C -0.91243472,9.6699043 -0.68197602,11.586122 -0.74034592,12.032491 C -1.892347,20.8421 1.6144968,19.138912 -0.52455272,25.744091 C -1.2711526,25.202108 -0.48304042,21.510409 -1.656139,19.312946 C -1.3508469,21.734954 -2.0167804,25.74309 0.62937185,28.271403 C 1.3856931,28.994988 2.8681995,29.389244 0.72269676,29.208582 C -0.014181718,29.14648 -0.61787772,28.901835 -1.1097781,28.513226 z M 0.48744008,32.752173 C 0.31731635,32.398379 0.15496979,32.036116 -0.001544018,31.666325 C 1.0075324,31.547766 3.0567937,31.442381 4.827052,32.187607 C 5.8555711,32.620441 3.7965886,32.40967 2.0000826,32.51976 C 1.3565291,32.558339 0.82574314,32.712653 0.48744008,32.752173 z M 1.3409749,34.366831 C 1.1446035,34.029973 0.95600926,33.683706 0.77422,33.328971 C 5.2207668,32.537638 7.1728144,33.319562 10.628756,33.606549 C 14.190659,33.902946 18.050036,32.097277 19.263261,30.020616 C 19.886399,28.953587 20.061384,28.694827 20.186789,28.414427 C 20.295669,28.171663 20.367606,27.912904 20.662164,27.098048 C 21.315439,25.292378 21.284331,27.753885 21.120039,28.360793 C 20.008889,32.4586 19.076611,31.735955 17.211084,33.542565 C 21.502088,32.007888 22.154392,28.937591 21.595413,24.661006 C 21.461259,23.63726 22.257437,20.869947 22.622961,19.880074 C 22.622961,19.880074 23.390948,17.397867 23.395808,12.997078 C 23.398724,10.126261 22.460614,5.7019479 18.621652,4.0505932 C 17.335517,3.4973188 15.549704,1.9767551 15.549704,1.9767551 C 16.345283,2.4005401 18.263906,2.6674072 20.120685,3.773956 C 20.191651,3.8162985 24.211429,6.0576243 24.612921,11.923462 C 24.802488,14.69548 24.823213,14.824989 24.636563,16.630658 C 24.636563,16.630658 24.522513,18.48748 23.87021,20.022157 C 23.555238,20.876533 23.217907,23.396378 23.404558,24.661006 C 23.836185,27.588279 22.063983,34.896582 14.845879,34.896582 C 11.657275,34.896582 9.7168927,34.487272 8.1906401,34.251096 C 6.8082637,34.036561 4.0143469,33.91612 1.3409749,34.366831 z M 3.9297711,37.852084 C 3.5603597,37.465356 3.2055303,37.055105 2.865283,36.622271 C 3.9113006,36.029477 4.8299684,36.020068 6.6935517,36.070879 C 8.8283608,36.070879 12.045156,36.916786 11.057467,37.195306 C 11.009832,37.207538 6.4806541,36.578988 3.9297711,37.852084 z M 14.733111,41.653493 C 14.310232,41.716536 13.88152,41.763583 13.44406,41.791811 C 11.361746,41.927308 9.4310844,41.515174 7.6851297,40.656094 C 8.5367202,40.538475 10.183517,40.482019 11.744767,40.7022 C 12.669268,40.832991 13.723062,41.288407 14.733111,41.653493 z M 20.761322,39.340655 C 19.332282,40.284421 17.738953,40.992951 16.005636,41.406965 C 14.734083,41.029648 11.984449,40.585503 12.005589,39.75495 C 12.021421,39.132891 15.493321,40.331468 17.489114,40.076472 C 18.855937,39.902398 19.892232,39.635171 20.761322,39.340655 z M 26.76134,32.003183 C 27.309624,31.472491 26.651489,34.623708 23.573708,35.937265 C 21.48945,36.675905 18.394173,38.324437 13.07076,36.917727 C 12.417485,36.744594 10.347808,36.196965 9.8870164,36.103812 C 6.2998369,35.382109 4.4372258,35.597584 2.4064351,36.01254 C 2.1702062,35.685092 1.9417545,35.345412 1.7210798,34.995381 C 2.3753268,34.730976 4.0192075,34.628413 5.200352,34.626531 C 8.9615437,34.619945 14.003037,35.980548 15.996887,36.070879 C 20.670913,36.282591 23.680644,33.000582 24.053944,33.000582 C 24.986221,33.000582 23.179994,34.632178 22.678372,35.082889 C 22.251605,35.467734 24.361138,34.516441 26.76134,32.003183 z M 28.531599,25.021387 C 28.408138,27.148859 28.247735,30.101537 26.467755,31.577877 C 23.628148,33.933056 24.799572,32.277938 26.039044,30.634111 C 28.194268,27.774586 27.297959,21.332891 28.708527,20.0852 C 28.725053,21.437335 28.621035,23.498941 28.531599,25.021387 z"
+ style="fill:#a1a1a1;fill-opacity:1"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"
+ sodipodi:nodetypes="cscssscccssssssscsssscsscsssccssscscssssscsssscssssssscsssscsssssscscsscssssssscsccssscsccccscccscssccssssssscsccsssccsscscsccscsssssssssssscsscssccsssccsscccsscccssssscscsscssccsssccccsccscscccsscccssccssssccsscc" /><path
+ sodipodi:type="arc"
+ style="opacity:0.24299999;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter84266)"
+ id="path84140"
+ sodipodi:cx="27.093658"
+ sodipodi:cy="38.810692"
+ sodipodi:rx="7.7301183"
+ sodipodi:ry="1.0405928"
+ d="M 34.823777,38.810692 A 7.7301183,1.0405928 0 1 1 19.36354,38.810692 A 7.7301183,1.0405928 0 1 1 34.823777,38.810692 z"
+ transform="matrix(1.1911672,0,0,2.1266149,-5.0625748,-41.775272)" /><g
+ id="Background">
+</g>
+<g
+ id="Guides">
+</g>
+
+<circle
+ clip-rule="evenodd"
+ cx="26.184"
+ cy="33.865002"
+ r="10"
+ id="circle2428"
+ sodipodi:cx="26.184"
+ sodipodi:cy="33.865002"
+ sodipodi:rx="10"
+ sodipodi:ry="10"
+ style="fill:url(#radialGradient8812);fill-opacity:1;fill-rule:evenodd;stroke:#a40000;stroke-width:1.31732059000000001;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1263785,0,0,1.1511056,-2.2713556,-9.1997707)" /><path
+ sodipodi:type="arc"
+ style="opacity:0.5;fill:url(#linearGradient84277);fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path3253"
+ sodipodi:cx="26.455547"
+ sodipodi:cy="27.394291"
+ sodipodi:rx="6.1445141"
+ sodipodi:ry="3.072257"
+ d="M 32.600061,27.394291 A 6.1445141,3.072257 0 1 1 20.311033,27.394291 A 6.1445141,3.072257 0 1 1 32.600061,27.394291 z"
+ transform="matrix(1.1246822,0,0,1.4387643,-2.5144268,-14.969086)" /><path
+ sodipodi:type="arc"
+ style="opacity:0.48;fill:none;fill-opacity:1;stroke:#e64837;stroke-width:1.21842730000000010;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path84102"
+ sodipodi:cx="28.185518"
+ sodipodi:cy="31.917336"
+ sodipodi:rx="10.027505"
+ sodipodi:ry="10.240856"
+ d="M 38.213023,31.917336 A 10.027505,10.240856 0 1 1 18.158013,31.917336 A 10.027505,10.240856 0 1 1 38.213023,31.917336 z"
+ transform="matrix(0.9962424,0,0,0.9957004,-0.8393988,-2.0505383)" /><g
+ id="g7978"
+ transform="translate(103.26268,8.6771365)">
+</g><g
+ style="display:none"
+ id="g7980"
+ display="none"
+ transform="translate(103.26268,8.6771365)">
+</g><path
+ id="path7993"
+ style="opacity:0.55;fill:#a40000;fill-opacity:1;fill-rule:evenodd;stroke:#a40000;stroke-width:1.14231765000000007;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 27.143847,37.652725 C 26.708227,37.652725 26.326837,37.518032 25.998788,37.249507 C 25.677852,36.974118 25.516939,36.592349 25.516939,36.10334 C 25.516939,35.676959 25.66985,35.315778 25.977452,35.018942 C 26.291276,34.7161 26.672666,34.565107 27.122511,34.56425 C 27.571466,34.56425 27.953745,34.7161 28.26757,35.018942 C 28.588505,35.31492 28.749418,35.676959 28.749418,36.10334 C 28.749418,36.585484 28.588505,36.963823 28.26757,37.239212 C 27.946633,37.515458 27.571466,37.652725 27.143847,37.652725 z M 25.956115,30.858079 L 25.613842,25.912229 C 25.549833,24.94794 25.517828,24.256464 25.517828,23.836088 C 25.517828,23.26472 25.670739,22.82118 25.978341,22.504613 C 26.292165,22.18118 26.702004,22.019893 27.208746,22.019035 C 27.82217,22.019035 28.232008,22.225791 28.439151,22.638445 C 28.645403,23.045095 28.749418,23.633622 28.749418,24.404025 C 28.749418,24.858717 28.724526,25.320272 28.674741,25.787834 L 28.214228,30.878669 C 28.164443,31.484354 28.056871,31.94934 27.893292,32.272773 C 27.728822,32.596205 27.457671,32.758349 27.079838,32.758349 C 26.694892,32.758349 26.427296,32.603067 26.277052,32.293362 C 26.126807,31.976794 26.020125,31.498939 25.956115,30.858079 z" /><path
+ id="path8814"
+ style="fill:#ffffff;fill-rule:evenodd"
+ d="M 27.143847,36.992527 C 26.708227,36.992527 26.326837,36.857834 25.998788,36.589309 C 25.677852,36.31392 25.516939,35.932151 25.516939,35.443142 C 25.516939,35.016761 25.66985,34.65558 25.977452,34.358744 C 26.291276,34.055902 26.672666,33.904909 27.122511,33.904052 C 27.571466,33.904052 27.953745,34.055902 28.26757,34.358744 C 28.588505,34.654722 28.749418,35.016761 28.749418,35.443142 C 28.749418,35.925286 28.588505,36.303625 28.26757,36.579014 C 27.946633,36.85526 27.571466,36.992527 27.143847,36.992527 z M 25.956115,30.197881 L 25.613842,25.252031 C 25.549833,24.287742 25.517828,23.596266 25.517828,23.17589 C 25.517828,22.604522 25.670739,22.160982 25.978341,21.844415 C 26.292165,21.520982 26.702004,21.359695 27.208746,21.358837 C 27.82217,21.358837 28.232008,21.565593 28.439151,21.978247 C 28.645403,22.384897 28.749418,22.973424 28.749418,23.743827 C 28.749418,24.198519 28.724526,24.660074 28.674741,25.127636 L 28.214228,30.218471 C 28.164443,30.824156 28.056871,31.289142 27.893292,31.612575 C 27.728822,31.936007 27.457671,32.098151 27.079838,32.098151 C 26.694892,32.098151 26.427296,31.942869 26.277052,31.633164 C 26.126807,31.316596 26.020125,30.838741 25.956115,30.197881 z" /><rect
+ style="fill:#666666;fill-opacity:0.75;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="rect9126"
+ width="1.8060035"
+ height="42.650608"
+ x="-26.038683"
+ y="-24.284592"
+ transform="matrix(-0.6420845,-0.7666339,0.7632254,-0.6461324,0,0)"
+ ry="0"
+ rx="0.018515259" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/print_ok.svg b/panels/user-accounts/data/icons/print_ok.svg
new file mode 100644
index 0000000..ba821ef
--- /dev/null
+++ b/panels/user-accounts/data/icons/print_ok.svg
@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 36.184 43.865"
+ enable-background="new 0 0 36.184 43.865"
+ xml:space="preserve"
+ id="svg2419"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="print_ok.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/print_ok.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata2435"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs2433"><linearGradient
+ inkscape:collect="always"
+ id="linearGradient84104"><stop
+ style="stop-color:#fffffc;stop-opacity:1;"
+ offset="0"
+ id="stop84106" /><stop
+ style="stop-color:#fffffc;stop-opacity:0;"
+ offset="1"
+ id="stop84108" /></linearGradient><linearGradient
+ id="linearGradient84076"><stop
+ style="stop-color:#73d216;stop-opacity:1;"
+ offset="0"
+ id="stop84078" /><stop
+ id="stop84090"
+ offset="0.31459025"
+ style="stop-color:#73d216;stop-opacity:1;" /><stop
+ style="stop-color:#4e9a06;stop-opacity:1;"
+ offset="1"
+ id="stop84080" /></linearGradient><linearGradient
+ id="linearGradient3531"><stop
+ id="stop3533"
+ offset="0"
+ style="stop-color:#9b9b9b;stop-opacity:1;" /><stop
+ id="stop3535"
+ offset="1"
+ style="stop-color:#414141;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient3483"><stop
+ id="stop3485"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" /><stop
+ id="stop3487"
+ offset="1"
+ style="stop-color:#787878;stop-opacity:1" /></linearGradient><linearGradient
+ id="linearGradient3263"><stop
+ id="stop3265"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop3267"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient3247"><stop
+ style="stop-color:#52a714;stop-opacity:1;"
+ offset="0"
+ id="stop3249" /><stop
+ style="stop-color:#398800;stop-opacity:1;"
+ offset="1"
+ id="stop3251" /></linearGradient><linearGradient
+ id="linearGradient3233"><stop
+ id="stop3235"
+ offset="0"
+ style="stop-color:#398800;stop-opacity:1;" /><stop
+ id="stop3237"
+ offset="1"
+ style="stop-color:#84c706;stop-opacity:1;" /></linearGradient><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 21.932501 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="36.183998 : 21.932501 : 1"
+ inkscape:persp3d-origin="18.091999 : 14.621667 : 1"
+ id="perspective2437" />
+
+
+
+
+
+ <filter
+ inkscape:collect="always"
+ id="filter3471"><feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.057808254"
+ id="feGaussianBlur3473" /></filter><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3233"
+ id="linearGradient3517"
+ gradientUnits="userSpaceOnUse"
+ x1="25.144751"
+ y1="43.865002"
+ x2="25.144751"
+ y2="23.838018" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3247"
+ id="linearGradient3519"
+ gradientUnits="userSpaceOnUse"
+ x1="30.691881"
+ y1="23.365002"
+ x2="30.691881"
+ y2="44.365963" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient3521"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="55.692348"
+ x2="18.072493"
+ y1="29.205048"
+ x1="21.55229"
+ id="linearGradient5138"
+ xlink:href="#linearGradient5132"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient5132"
+ inkscape:collect="always"><stop
+ id="stop5134"
+ offset="0"
+ style="stop-color:white;stop-opacity:1;" /><stop
+ id="stop5136"
+ offset="1"
+ style="stop-color:white;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient1913"><stop
+ id="stop1915"
+ offset="0"
+ style="stop-color:#73d216;stop-opacity:1" /><stop
+ id="stop1917"
+ offset="1"
+ style="stop-color:#8ae234;stop-opacity:1" /></linearGradient><inkscape:perspective
+ id="perspective84036"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84076"
+ id="radialGradient84088"
+ cx="26.183998"
+ cy="40.111427"
+ fx="26.183998"
+ fy="40.111427"
+ r="10.5"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.9228377,7.3282173e-8,-7.9001009e-8,0.8924238,2.0204218,4.4167191)" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84092"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84104"
+ id="linearGradient84110"
+ x1="28.185518"
+ y1="22.649143"
+ x2="27.596079"
+ y2="42.648415"
+ gradientUnits="userSpaceOnUse" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84076"
+ id="radialGradient84134"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0862379,-2.5925308e-8,1.5464794e-8,0.8186283,-2.2580516,7.302016)"
+ cx="26.183998"
+ cy="39.098457"
+ fx="26.183998"
+ fy="39.098457"
+ r="10.5" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84136"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84104"
+ id="linearGradient84138"
+ gradientUnits="userSpaceOnUse"
+ x1="21.515692"
+ y1="23.09075"
+ x2="34.488232"
+ y2="40.661182" /><filter
+ inkscape:collect="always"
+ id="filter84266"
+ x="-0.07103052"
+ width="1.142061"
+ y="-0.5276553"
+ height="2.0553105"><feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.45756194"
+ id="feGaussianBlur84268" /></filter><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3263"
+ id="linearGradient84277"
+ gradientUnits="userSpaceOnUse"
+ x1="26.455547"
+ y1="24.322035"
+ x2="26.455547"
+ y2="30.466549" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient84104"
+ id="linearGradient84279"
+ gradientUnits="userSpaceOnUse"
+ x1="21.515692"
+ y1="23.09075"
+ x2="34.488232"
+ y2="40.661182" /></defs><sodipodi:namedview
+ inkscape:window-height="713"
+ inkscape:window-width="1222"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="4.399364"
+ inkscape:cx="39.372624"
+ inkscape:cy="7.0437262"
+ inkscape:window-x="15"
+ inkscape:window-y="165"
+ inkscape:current-layer="svg2419" />
+<path
+ sodipodi:type="arc"
+ style="opacity:0.24299999;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter84266)"
+ id="path84140"
+ sodipodi:cx="27.093658"
+ sodipodi:cy="38.810692"
+ sodipodi:rx="7.7301183"
+ sodipodi:ry="1.0405928"
+ d="M 34.823777,38.810692 A 7.7301183,1.0405928 0 1 1 19.36354,38.810692 A 7.7301183,1.0405928 0 1 1 34.823777,38.810692 z"
+ transform="matrix(1.1911672,0,0,2.1266149,-5.0625748,-41.775272)" /><g
+ id="Background">
+</g>
+<g
+ id="Guides">
+</g>
+<path
+ id="path2424"
+ d="M 11.4715,8.6587828 C 11.85841,8.5063492 18.052855,7.561643 17.533735,14.80408 C 16.823104,24.735732 10.185364,21.847978 8.4432981,28.076079 C 9.1393466,27.839903 9.3182195,27.169011 9.756653,26.617618 C 11.076813,24.955913 11.899239,24.128823 13.185374,23.57555 C 17.764131,21.605215 20.247937,13.221415 16.391477,9.350375 C 14.944939,7.8975596 12.256985,8.0358782 11.4715,8.6587828 z M -0.42938057,30.607215 C -0.63450107,30.057705 -0.82406747,29.493139 -0.99905177,28.917282 C 0.26764038,30.495244 2.394932,29.496061 3.7663553,29.835942 C 7.7655495,30.827068 10.188281,29.685092 12.483493,26.986468 C 14.524976,24.585181 15.716814,26.01071 17.678584,22.400312 C 18.1734,21.49042 19.513975,20.499607 20.036011,16.462963 C 20.321819,14.250806 21.717163,13.992047 21.54704,12.391502 C 21.286508,9.9394059 21.195769,9.5856116 20.169194,7.9304926 C 18.692521,5.5480253 16.429451,4.9093448 14.383024,4.903159 C 12.660627,4.8984543 11.830403,5.5932308 11.247122,5.820939 C 15.376525,4.8200358 18.963747,6.2462048 19.964074,8.6587828 C 20.4725,9.8848305 20.731088,10.257444 20.821497,11.562532 C 20.964401,13.628842 20.293627,14.400415 20.249881,13.774689 C 20.066148,11.097706 19.138731,7.7893511 16.036647,6.7232629 C 14.08071,6.0514297 11.288738,6.5623616 9.6166659,7.8298117 C 7.5771256,9.7832092 6.4815291,12.601333 6.4815291,14.458754 C 6.4815291,18.022104 6.4552814,19.153118 5.840892,22.47841 C 5.582304,23.881356 4.6490541,25.341699 5.1885893,27.263105 C 5.9837959,26.96953 6.6973432,25.862981 6.9812066,25.096113 C 8.9731118,19.712038 10.278689,20.976666 12.329895,18.813438 C 12.817907,18.298742 13.655888,16.907087 13.757962,16.394273 C 14.004884,15.152229 14.615385,12.184495 12.543765,12.254125 C 10.819197,13.353146 11.543438,15.633991 10.22425,17.856498 C 12.257957,16.947548 12.124774,13.112265 12.871375,13.021934 C 13.519788,12.943836 13.479931,14.488864 13.338,15.31313 C 13.032749,17.084925 12.408638,18.086089 11.005847,19.10513 C 9.8869191,19.918105 8.8467342,20.049837 7.8347414,21.757648 C 7.6442029,22.057809 6.2890464,25.330408 6.120867,25.639037 C 5.2721928,27.201003 5.3742671,25.548707 6.3075169,22.931003 C 6.3075169,22.931003 7.2174355,20.095002 7.0978629,16.9993 C 7.0434233,15.566244 7.1231384,9.904591 10.82989,7.8994415 C 12.425164,7.0365969 14.901193,6.4946138 16.606318,7.7611229 C 21.944313,11.725315 19.458563,21.448078 15.392121,24.198454 C 13.75699,25.305002 12.399889,25.373692 11.005847,27.501164 C 9.918999,29.160046 6.8363585,30.274122 4.6937724,29.305892 C 5.5511957,28.829775 6.8820488,28.787432 7.2650702,27.723226 C 10.071625,19.916223 15.516555,23.290444 16.169829,14.950867 C 16.401197,11.993484 14.864252,9.6797058 12.683753,9.6797058 C 10.452702,9.6797058 8.9526972,12.659672 8.6143939,15.425102 C 8.4520474,16.758419 8.3645551,18.513277 8.2576203,19.43446 C 10.04246,18.397541 8.4714901,11.146635 12.470855,10.731679 C 15.950127,10.371298 15.652653,14.948985 15.256023,16.945666 C 14.953689,18.468112 14.256667,19.779785 11.256658,21.500771 C 9.676938,22.406898 8.629948,23.557671 7.8289087,25.441439 C 7.3593673,26.546106 6.3988976,28.436461 4.53337,28.617122 C 4.3195003,28.637822 3.9131477,26.558339 3.7867702,25.457436 C 3.6001201,23.832427 4.440045,22.749402 4.7190479,21.000189 C 5.0301312,19.057142 5.2478894,17.510232 5.2644158,14.182117 C 4.4799026,9.0737386 5.2780257,7.5635248 7.4089462,5.140597 C 9.9005288,2.306477 12.031512,2.0549452 13.495175,2.0414516 C 15.638858,2.0237012 16.885321,3.5353485 16.249545,3.412085 C 14.609553,3.0949872 11.616348,2.6753267 9.0440775,5.2789155 C 7.6092061,6.7298495 6.0265698,8.5966804 6.1947492,10.870939 C 6.8664947,7.6039854 10.320801,3.7360695 13.733969,3.7360695 C 17.563499,3.7360695 19.963101,5.1556521 21.106332,6.9999001 C 22.267062,8.8733168 22.748269,10.281909 22.748269,12.806458 C 22.748269,14.670467 21.716834,15.435453 21.249236,16.946607 C 21.069391,17.527169 20.450141,19.029855 20.177944,20.195682 C 19.084291,24.880637 18.295889,27.137018 14.257639,30.204492 C 12.498075,31.540631 11.0982,31.630961 10.165922,31.269639 C 10.173699,31.117206 12.353227,30.918668 13.710328,28.921987 C 14.988685,27.040101 16.636455,26.992113 17.383054,24.825122 C 16.609234,25.763242 15.405731,26.658079 14.400543,27.100321 C 14.11668,27.225467 13.604365,27.870953 13.407994,28.100544 C 11.354844,30.515944 9.699297,31.088038 7.3321475,31.358088 C 7.7083639,31.534985 8.097218,31.671422 8.5094033,31.724115 C 11.518163,32.108961 13.373968,32.147539 15.76056,30.039827 C 19.12901,27.065506 19.683127,25.185502 20.034068,22.951704 C 20.293627,21.297527 21.655941,17.421123 22.247619,15.852571 C 23.080867,13.643616 22.556758,18.089853 22.318585,18.673238 C 20.924543,22.100151 21.486437,26.72018 17.942032,30.365393 C 15.509749,32.86736 12.310453,33.796071 7.1221663,32.621775 C 5.7388178,32.30844 6.239509,31.841732 4.657845,31.358088 C 4.1834429,31.143553 1.6402954,30.997707 1.3486549,30.995825 C 0.70898981,30.992061 0.10626602,30.860329 -0.42938057,30.607215 z M 4.2543673,5.1321285 C 2.8544925,6.7006803 3.1305788,7.9154375 2.2021896,9.2289942 C 0.37749172,11.810941 1.4380914,15.378055 1.0521537,17.983526 C 0.55247613,21.363393 0.50913089,20.094485 -0.0071428722,17.974697 C -0.76834557,14.849245 -0.21551087,13.71635 -0.090105372,12.391502 C 0.60497135,9.1141985 1.2125559,7.7404222 2.8554645,5.6731707 C 4.2115933,3.9672413 6.135449,3.0215936 4.2543673,5.1321285 z M 24.022738,36.434475 C 23.474454,37.067729 22.882424,37.657701 22.248591,38.195919 L 21.462134,38.491375 C 18.22784,39.695783 16.567433,39.805873 11.843827,39.067233 C 9.918999,38.766131 7.2913178,38.39634 5.4841183,39.262008 C 6.5702412,39.494461 10.381694,38.345449 11.56288,39.970538 C 11.446224,40.140849 9.1976753,39.867976 7.0823087,40.331861 C 6.5699934,40.042991 6.0751766,39.715542 5.5978581,39.351398 C 5.1390103,38.993839 4.8036236,38.704969 4.4789305,38.398222 C 6.9432934,37.015037 10.891135,37.492095 13.335083,37.804487 C 17.567761,38.346471 19.246638,38.16581 21.951118,37.173114 C 22.683136,36.904005 23.374324,36.654655 24.022738,36.434475 z M 26.080749,7.8825045 C 27.13746,9.9657525 28.24375,11.559709 28.24375,14.719395 C 28.24375,18.197241 25.745362,19.892699 25.346787,22.207418 C 24.973487,24.37441 24.914187,26.178198 24.585605,26.178198 C 24.11898,26.178198 23.970243,24.110005 24.041208,22.659071 C 24.113147,21.19026 24.300417,20.389257 24.641637,19.294941 C 25.02563,18.066069 25.608953,15.315973 25.490352,13.235548 C 25.33481,10.489877 25.576852,11.073882 26.289427,12.729942 C 26.756052,13.812967 26.247915,15.934534 26.289427,17.469211 C 27.316002,15.633392 27.121554,12.867259 26.336068,10.86211 C 26.073591,10.189336 25.713282,8.8290928 26.080749,7.8825045 z M 24.608294,9.2431681 C 24.472195,8.6390829 23.70969,6.477977 22.713252,5.1870035 C 21.219079,3.2533658 22.564494,3.6570102 23.635787,4.8313069 C 24.682776,5.9792572 25.458582,7.3442852 25.487746,8.6644279 C 25.547046,11.377166 25.108944,11.455325 24.608294,9.2431681 z M 28.310827,16.859099 C 28.588858,16.49966 28.861055,18.750394 28.357489,19.540787 C 27.013026,21.649439 27.496178,25.113051 26.626116,27.878481 C 25.240824,32.27833 20.835106,35.602682 15.985124,35.513292 C 19.529529,34.338995 22.700634,33.797953 24.16953,26.824625 C 24.298825,26.210189 25.161109,26.541401 24.3834,29.44421 C 25.240824,28.207811 25.985479,25.109287 26.098247,22.60826 C 26.173102,20.914563 27.715881,18.48693 28.310827,16.859099 z M 0.33666203,7.0620024 C 3.1023869,1.995966 8.6775827,0.52715408 13.200929,0.52715408 C 16.027898,0.52715408 18.494205,1.0117396 20.804971,2.3394101 C 21.160772,2.5991103 21.699335,2.9199718 21.989031,3.2097822 C 22.925198,4.1497842 21.355919,3.7289582 19.391486,2.8597515 C 15.788463,1.2655141 13.210649,1.5085776 11.04376,1.8557856 C 9.4407091,2.112663 8.4189947,3.395148 7.616011,4.1733077 C 4.6568312,7.0356559 4.1960391,9.5950208 4.5518406,12.806458 C 4.9931901,16.791351 4.2543673,21.000189 3.3230616,23.294208 C 2.8467153,24.469446 2.9099042,25.995654 3.1228018,26.962944 C 3.5077673,28.707452 5.0036733,29.433225 3.1344674,29.192978 C -0.13966347,28.772157 -0.13773997,24.273729 0.98021562,21.094283 C 2.1224745,17.845207 1.495243,15.951027 1.7355647,13.562036 C 2.8026799,2.8737448 3.1130804,15.825944 3.0518358,16.393333 C 2.6940901,19.711097 2.2203141,20.592698 1.7248713,22.0691 C 0.65905915,25.245183 1.9177265,27.094796 2.2524513,27.350994 C 0.89725312,23.033886 3.2657056,21.64003 3.4523556,16.633273 C 3.4980459,15.417575 3.5174886,14.673289 3.3318108,13.031344 C 2.7611674,7.9643665 4.0677172,5.041798 6.9588475,2.6941455 C 5.2051155,2.7110825 3.8587082,3.9794735 2.6688146,5.1321285 C 1.363237,6.3958147 0.15098419,9.2779223 -0.22620427,10.387294 C -0.79004277,12.046176 -1.2848596,10.030677 0.33666203,7.0620024 z M -1.1234851,28.497622 C -2.0265988,25.35299 -2.4786417,21.846097 -2.4037873,18.200883 C -2.3746233,16.749949 -2.2443571,15.155993 -1.7971749,13.290103 C -0.92614167,9.6543002 -0.69568297,11.570518 -0.75405287,12.016887 C -1.906054,20.826496 1.6007898,19.123308 -0.53825967,25.728487 C -1.2848596,25.186504 -0.49674737,21.494805 -1.669846,19.297342 C -1.3645539,21.71935 -2.0304874,25.727486 0.6156649,28.255799 C 1.3719861,28.979384 2.8544925,29.37364 0.70898981,29.192978 C -0.027888672,29.130876 -0.63158467,28.886231 -1.1234851,28.497622 z M 0.47373313,32.736569 C 0.3036094,32.382775 0.14126284,32.020512 -0.015250972,31.650721 C 0.99382545,31.532162 3.0430867,31.426777 4.813345,32.172003 C 5.8418641,32.604837 3.7828816,32.394066 1.9863756,32.504156 C 1.3428221,32.542735 0.81203619,32.697049 0.47373313,32.736569 z M 1.3272679,34.351227 C 1.1308965,34.014369 0.94230231,33.668102 0.76051305,33.313367 C 5.2070598,32.522034 7.1591074,33.303958 10.615049,33.590945 C 14.176952,33.887342 18.036329,32.081673 19.249554,30.005012 C 19.872692,28.937983 20.047677,28.679223 20.173082,28.398823 C 20.281962,28.156059 20.353899,27.8973 20.648457,27.082444 C 21.301732,25.276774 21.270624,27.738281 21.106332,28.345189 C 19.995182,32.442996 19.062904,31.720351 17.197377,33.526961 C 21.488381,31.992284 22.140685,28.921987 21.581706,24.645402 C 21.447552,23.621656 22.24373,20.854343 22.609254,19.86447 C 22.609254,19.86447 23.377241,17.382263 23.382101,12.981474 C 23.385017,10.110657 22.446907,5.6863438 18.607945,4.0349891 C 17.32181,3.4817147 15.535997,1.961151 15.535997,1.961151 C 16.331576,2.384936 18.250199,2.6518031 20.106978,3.7583519 C 20.177944,3.8006944 24.197722,6.0420202 24.599214,11.907858 C 24.788781,14.679876 24.809506,14.809385 24.622856,16.615054 C 24.622856,16.615054 24.508806,18.471876 23.856503,20.006553 C 23.541531,20.860929 23.2042,23.380774 23.390851,24.645402 C 23.822478,27.572675 22.050276,34.880978 14.832172,34.880978 C 11.643568,34.880978 9.7031857,34.471668 8.1769331,34.235492 C 6.7945567,34.020957 4.0006399,33.900516 1.3272679,34.351227 z M 3.9160641,37.83648 C 3.5466527,37.449752 3.1918233,37.039501 2.851576,36.606667 C 3.8975936,36.013873 4.8162614,36.004464 6.6798447,36.055275 C 8.8146538,36.055275 12.031449,36.901182 11.04376,37.179702 C 10.996125,37.191934 6.4669471,36.563384 3.9160641,37.83648 z M 14.719404,41.637889 C 14.296525,41.700932 13.867813,41.747979 13.430353,41.776207 C 11.348039,41.911704 9.4173774,41.49957 7.6714227,40.64049 C 8.5230132,40.522871 10.16981,40.466415 11.73106,40.686596 C 12.655561,40.817387 13.709355,41.272803 14.719404,41.637889 z M 20.747615,39.325051 C 19.318575,40.268817 17.725246,40.977347 15.991929,41.391361 C 14.720376,41.014044 11.970742,40.569899 11.991882,39.739346 C 12.007714,39.117287 15.479614,40.315864 17.475407,40.060868 C 18.84223,39.886794 19.878525,39.619567 20.747615,39.325051 z M 26.747633,31.987579 C 27.295917,31.456887 26.637782,34.608104 23.560001,35.921661 C 21.475743,36.660301 18.380466,38.308833 13.057053,36.902123 C 12.403778,36.72899 10.334101,36.181361 9.8733094,36.088208 C 6.2861299,35.366505 4.4235188,35.58198 2.3927281,35.996936 C 2.1564992,35.669488 1.9280475,35.329808 1.7073728,34.979777 C 2.3616198,34.715372 4.0055005,34.612809 5.186645,34.610927 C 8.9478367,34.604341 13.98933,35.964944 15.98318,36.055275 C 20.657206,36.266987 23.666937,32.984978 24.040237,32.984978 C 24.972514,32.984978 23.166287,34.616574 22.664665,35.067285 C 22.237898,35.45213 24.347431,34.500837 26.747633,31.987579 z M 28.517892,25.005783 C 28.394431,27.133255 28.234028,30.085933 26.454048,31.562273 C 23.614441,33.917452 24.785865,32.262334 26.025337,30.618507 C 28.180561,27.758982 27.284252,21.317287 28.69482,20.069596 C 28.711346,21.421731 28.607328,23.483337 28.517892,25.005783 z"
+ style="fill:#a1a1a1;fill-opacity:1"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"
+ sodipodi:nodetypes="cscssscccssssssscsssscsscsssccssscscssssscsssscssssssscsssscsssssscscsscssssssscsccssscsccccscccscssccssssssscsccsssccsscscsccscsssssssssssscsscssccsssccsscccsscccssssscscsscssccsssccccsccscscccsscccssccssssccsscc" />
+<circle
+ clip-rule="evenodd"
+ cx="26.184"
+ cy="33.865002"
+ r="10"
+ id="circle2428"
+ sodipodi:cx="26.184"
+ sodipodi:cy="33.865002"
+ sodipodi:rx="10"
+ sodipodi:ry="10"
+ style="fill:url(#radialGradient84134);fill-opacity:1;fill-rule:evenodd;stroke:#448c00;stroke-width:1.31732059;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(1.1263785,0,0,1.1511056,-2.2713556,-9.1997707)" /><path
+ sodipodi:type="arc"
+ style="opacity:0.50746268;fill:url(#linearGradient84277);fill-opacity:1;stroke:none;stroke-width:1;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path3253"
+ sodipodi:cx="26.455547"
+ sodipodi:cy="27.394291"
+ sodipodi:rx="6.1445141"
+ sodipodi:ry="3.072257"
+ d="M 32.600061,27.394291 A 6.1445141,3.072257 0 1 1 20.311033,27.394291 A 6.1445141,3.072257 0 1 1 32.600061,27.394291 z"
+ transform="matrix(1.1111101,0,0,1.2758999,-2.2387648,-10.924499)" /><path
+ clip-rule="evenodd"
+ d="M 21.777021,33.575871 C 22.428021,34.222871 23.330021,35.596871 24.012021,36.209871 C 25.264021,34.898871 27.599021,31.912871 31.077021,29.499871 C 31.754021,29.029871 33.458021,29.462871 32.557021,30.487871 C 29.846021,33.287871 27.332021,36.692871 25.385021,39.387871 C 24.468021,40.656871 23.706021,39.994871 22.908021,38.978871 C 21.912021,37.682871 20.897021,36.071871 20.509021,35.011871 C 20.282021,34.392871 20.974021,32.785871 21.777021,33.575871 z"
+ id="path2430"
+ style="opacity:0.42786069;fill:#398800;fill-opacity:1;fill-rule:evenodd;stroke:#398800;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3471)"
+ transform="matrix(1.1263785,0,0,1.1511056,-2.2713556,-9.1997707)" /><path
+ clip-rule="evenodd"
+ d="M 21.969435,28.185201 C 22.702707,28.929966 23.718701,30.511585 24.486892,31.217213 C 25.897117,29.708113 28.527211,26.270911 32.444755,23.493294 C 33.207314,22.952275 35.126663,23.450703 34.111795,24.630586 C 31.058183,27.853682 28.226468,31.773196 26.033409,34.875426 C 25.00052,36.33618 24.142219,35.574148 23.243369,34.404624 C 22.121497,32.912791 20.978222,31.05836 20.541188,29.838188 C 20.2855,29.125654 21.064954,27.275827 21.969435,28.185201 z"
+ id="path3469"
+ style="fill:#ffffff;fill-rule:evenodd" /><path
+ sodipodi:type="arc"
+ style="opacity:0.36815945;fill:none;fill-opacity:1;stroke:url(#linearGradient84279);stroke-width:1.2184273;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path84102"
+ sodipodi:cx="28.185518"
+ sodipodi:cy="31.917336"
+ sodipodi:rx="10.027505"
+ sodipodi:ry="10.240856"
+ d="M 38.213023,31.917336 A 10.027505,10.240856 0 1 1 18.158013,31.917336 A 10.027505,10.240856 0 1 1 38.213023,31.917336 z"
+ transform="matrix(1.0116992,0,0,1.0189783,-1.4418411,-2.7101136)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/right-index-finger.svg b/panels/user-accounts/data/icons/right-index-finger.svg
new file mode 100644
index 0000000..5a621a2
--- /dev/null
+++ b/panels/user-accounts/data/icons/right-index-finger.svg
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="right-index-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="922"
+ inkscape:window-width="1302"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="15.672125"
+ inkscape:cy="30.299841"
+ inkscape:window-x="36"
+ inkscape:window-y="91"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(-1.1605241,0,0,1.3370602,44.823901,-0.7984997)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background"
+ transform="matrix(-1,0,0,1,40.436754,0)">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1"
+ transform="matrix(-1,0,0,1,40.436754,0)">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="matrix(-1,0,0,1,92.903401,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/right-little-finger.svg b/panels/user-accounts/data/icons/right-little-finger.svg
new file mode 100644
index 0000000..9fcec2a
--- /dev/null
+++ b/panels/user-accounts/data/icons/right-little-finger.svg
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="right-pinky-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-ring-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="346"
+ inkscape:window-y="109"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(-1.1074589,0,0,1.2726911,65.968411,5.5330271)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="matrix(-1,0,0,1,92.903403,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/right-middle-finger.svg b/panels/user-accounts/data/icons/right-middle-finger.svg
new file mode 100644
index 0000000..b33a654
--- /dev/null
+++ b/panels/user-accounts/data/icons/right-middle-finger.svg
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="right-middle-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-index-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="362"
+ inkscape:window-y="121"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(-1.1824583,0,0,1.3363867,53.282364,-2.0066594)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="matrix(-1,0,0,1,92.903403,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/right-ring-finger.svg b/panels/user-accounts/data/icons/right-ring-finger.svg
new file mode 100644
index 0000000..9e264fe
--- /dev/null
+++ b/panels/user-accounts/data/icons/right-ring-finger.svg
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="right-ring-finger.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-middle-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="424"
+ inkscape:window-y="91"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(-1.1824583,0,0,1.3363867,61.073222,-0.7947482)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="matrix(-1,0,0,1,92.903403,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/icons/right-thumb.svg b/panels/user-accounts/data/icons/right-thumb.svg
new file mode 100644
index 0000000..0aa0f2e
--- /dev/null
+++ b/panels/user-accounts/data/icons/right-thumb.svg
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.0"
+ x="0px"
+ y="0px"
+ width="48"
+ height="48"
+ viewBox="0 0 40.425 46.214"
+ enable-background="new 0 0 40.425 46.214"
+ xml:space="preserve"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docname="right-thumb.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/Users/mlanglie/Desktop/Fingerprint Enrollment Icons/Vector/left-pinky-finger.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90"><metadata
+ id="metadata44"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs42"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 23.107 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="40.424999 : 23.107 : 1"
+ inkscape:persp3d-origin="20.2125 : 15.404667 : 1"
+ id="perspective46" />
+
+
+
+
+
+
+<radialGradient
+ r="8.341651"
+ fy="9.3411446"
+ fx="38.658855"
+ cy="9.3411446"
+ cx="38.658855"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2479"
+ xlink:href="#linearGradient2378"
+ inkscape:collect="always" /><linearGradient
+ id="linearGradient2378"><stop
+ id="stop2386"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ style="stop-color:#27dc16;stop-opacity:1;"
+ offset="1"
+ id="stop2382" /></linearGradient><linearGradient
+ id="linearGradient3702"><stop
+ id="stop3704"
+ offset="0"
+ style="stop-color:black;stop-opacity:0;" /><stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0.5"
+ id="stop3710" /><stop
+ id="stop3706"
+ offset="1"
+ style="stop-color:black;stop-opacity:0;" /></linearGradient><linearGradient
+ id="linearGradient6732"><stop
+ id="stop6734"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" /><stop
+ id="stop6736"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:1;" /></linearGradient><linearGradient
+ id="linearGradient4585"><stop
+ id="stop4587"
+ offset="0"
+ style="stop-color:#9e9e9e;stop-opacity:1;" /><stop
+ id="stop4589"
+ offset="1"
+ style="stop-color:#dddddd;stop-opacity:0;" /></linearGradient><inkscape:perspective
+ id="perspective2516"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 24 : 1"
+ sodipodi:type="inkscape:persp3d" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86956"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86964"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /><radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2378"
+ id="radialGradient86966"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0059869,5.3782029e-7,-9.45767e-7,1.1767077,-0.1585918,-0.9842761)"
+ cx="26.49"
+ cy="5.5700002"
+ fx="26.49"
+ fy="5.5700002"
+ r="4.0552225" /></defs><sodipodi:namedview
+ inkscape:window-height="933"
+ inkscape:window-width="1054"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ showgrid="false"
+ inkscape:zoom="11.122171"
+ inkscape:cx="22.316511"
+ inkscape:cy="30.299841"
+ inkscape:window-x="116"
+ inkscape:window-y="117"
+ inkscape:current-layer="svg2" />
+<g
+ id="g35"
+ style="fill:url(#radialGradient86956);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ transform="matrix(-1.1916623,0,0,1.4021101,35.910183,14.334323)">
+ <circle
+ style="fill:url(#radialGradient86964);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:ry="3.829"
+ sodipodi:rx="3.829"
+ sodipodi:cy="5.5700002"
+ sodipodi:cx="26.49"
+ id="circle37"
+ r="3.829"
+ cy="5.5700002"
+ cx="26.49"
+ stroke-miterlimit="3.8637" />
+ <path
+ style="fill:url(#radialGradient86966);fill-opacity:1;stroke:#31ae00;stroke-width:0.45244551;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path39"
+ stroke-miterlimit="3.8637"
+ d="" />
+ </g><g
+ id="Background"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+</g>
+<g
+ id="Guides"
+ display="none"
+ style="display:none">
+</g>
+<g
+ id="g7"
+ style="fill:#2f2f2f;fill-opacity:1"
+ transform="matrix(-1,0,0,1,40.436756,0)">
+ <path
+ style="fill:#2f2f2f;fill-opacity:1;fill-rule:evenodd"
+ id="path9"
+ d="M 20.569,13.286 C 20.843,15.459 20.791,17.834 20.82,18.542 C 20.865,18.917 21.913,19.241 22.094,18.877 C 22.301,17.514 22.586,14.847 22.92,13.455 C 23.372,9.917 23.886,8.247 24.072,7.257 C 24.58,4.567 25.014,3.12 26.762,3.12 C 28.384,3.12 28.623,5.167 28.153,8.165 C 28.025,9.379 28.036,12.24 27.433,14.894 C 27.165,16.691 26.962,21.278 27.139,24.981 C 27.189,26.045 27.669,27.315 28.242,27.945 C 29.462,27.773 30.379,24.377 31.988,22.847 C 32.809,21.464 34.883,19.299 36.951,19.299 C 39.243,19.299 39.292,21 38.345,22.262 C 37.187,23.806 36.525,24.528 36.079,25.802 C 35.633,27.076 33.758,29.778 33.471,31.209 C 32.902,34.054 32.471,35.105 29.284,37.661 C 27.783,38.865 27.302,39.637 26.855,40.401 C 26.347,41.273 26.409,44.721 26.409,46.212 L 10.64,46.212 C 10.64,46.212 10.767,43.587 10.513,42.058 C 10.461,41.746 10.105,39.613 9.176,38.231 C 8.317,36.954 5.902,32.306 5.902,26.836 C 5.902,24.026 4.39,21.718 3.854,19.533 C 3.591,17.833 2.638,15.423 2.57,14.709 C 1.684,11.559 2.132,10.118 3.741,10.118 C 5.132,10.118 5.528,11.927 6.093,13.622 C 6.258,14.52 7.505,16.934 7.788,18.233 C 8.038,19.088 8.897,21.556 9.437,21.727 C 9.75,21.784 10.151,21.523 10.291,21.043 C 10.52,20.26 9.818,18.945 9.539,15.26 C 9.087,12.343 9.17,10.139 9.116,8.949 C 8.653,5.424 9.133,3.598 10.693,3.598 C 12.176,3.598 12.568,4.455 13.02,7.936 C 13.189,9.518 13.754,12.095 14.037,14.864 C 14.213,16.364 14.639,18.601 14.98,19.17 C 15.285,19.503 15.919,19.198 15.9,18.848 C 15.894,16.377 15.732,14.977 15.958,12.999 C 15.845,10.287 16.219,7.823 16.219,6.467 C 16.219,3.494 16.502,1.473 18.358,1.473 C 20.333,1.473 20.287,3.776 20.287,6.806 C 20.399,7.545 20.456,10.087 20.569,13.286 z"
+ clip-rule="evenodd" />
+ </g>
+<g
+ style="display:inline"
+ inkscape:label="Base"
+ id="layer1"
+ transform="matrix(-1,0,0,1,92.903403,2.6102791)" /></svg> \ No newline at end of file
diff --git a/panels/user-accounts/data/join-dialog.ui b/panels/user-accounts/data/join-dialog.ui
new file mode 100644
index 0000000..0fda6f7
--- /dev/null
+++ b/panels/user-accounts/data/join-dialog.ui
@@ -0,0 +1,238 @@
+<?xml version="1.0"?>
+<interface>
+ <!-- interface-requires gtk+ 3.8 -->
+ <object class="GtkDialog" id="join-dialog">
+ <property name="can_focus">False</property>
+ <property name="border_width">10</property>
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="type_hint">dialog</property>
+ <property name="title" translatable="yes">Add User</property>
+ <property name="use_header_bar">1</property>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar" id="join-dialog-header-bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="show_close_button">False</property>
+ <child>
+ <object class="GtkButton" id="button1">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="text-button"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="button2">
+ <property name="label" translatable="yes" comments="Translators: This button enrolls the computer in the domain in order to use enterprise logins.">_Enroll</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="has_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_action_appearance">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="text-button"/>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child>
+ <object class="GtkBox" id="box2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">5</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label71">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Domain Administrator Login</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="label" translatable="yes">In order to use enterprise logins, this computer needs to be
+enrolled in the domain. Please have your network administrator
+type their domain password here.</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">12</property>
+ <property name="hexpand">True</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Domain</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">join-domain</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="join-domain">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Administrator _Name</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">join-name</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="join-name">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="invisible_char">●</property>
+ <property name="invisible_char_set">True</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">Administrator Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">join-password</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="join-password">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="visibility">False</property>
+ <property name="invisible_char">●</property>
+ <property name="activates_default">True</property>
+ <property name="invisible_char_set">True</property>
+ <property name="input_purpose">password</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-6">button1</action-widget>
+ <action-widget response="-5">button2</action-widget>
+ </action-widgets>
+ </object>
+</interface>
diff --git a/panels/user-accounts/data/net.reactivated.Fprint.Device.xml b/panels/user-accounts/data/net.reactivated.Fprint.Device.xml
new file mode 100644
index 0000000..786d89c
--- /dev/null
+++ b/panels/user-accounts/data/net.reactivated.Fprint.Device.xml
@@ -0,0 +1,585 @@
+<!DOCTYPE node PUBLIC
+"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [
+<!ENTITY ERROR_CLAIM_DEVICE "net.reactivated.Fprint.Error.ClaimDevice">
+<!ENTITY ERROR_ALREADY_IN_USE "net.reactivated.Fprint.Error.AlreadyInUse">
+<!ENTITY ERROR_INTERNAL "net.reactivated.Fprint.Error.Internal">
+<!ENTITY ERROR_PERMISSION_DENIED "net.reactivated.Fprint.Error.PermissionDenied">
+<!ENTITY ERROR_NO_ENROLLED_PRINTS "net.reactivated.Fprint.Error.NoEnrolledPrints">
+<!ENTITY ERROR_NO_ACTION_IN_PROGRESS "net.reactivated.Fprint.Error.NoActionInProgress">
+<!ENTITY ERROR_INVALID_FINGERNAME "net.reactivated.Fprint.Error.InvalidFingername">
+]>
+
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <interface name="net.reactivated.Fprint.Device">
+ value="fprint_device" />
+
+ <doc:doc>
+ <doc:title id="polkit-integration">
+ PolicyKit integration
+ </doc:title>
+ <doc:para>
+ fprintd uses PolicyKit to check whether users are allowed to access fingerprint data, or the
+ fingerprint readers itself.
+ <doc:list>
+ <doc:item>
+ <doc:term>net.reactivated.fprint.device.verify</doc:term>
+ <doc:definition>
+ Whether the user is allowed to verify fingers against saved fingerprints.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>net.reactivated.fprint.device.enroll</doc:term>
+ <doc:definition>
+ Whether the user is allowed to enroll new fingerprints.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>net.reactivated.fprint.device.setusername</doc:term>
+ <doc:definition>
+ Whether the user is allowed to query, verify, or enroll fingerprints for users other than itself.
+ </doc:definition>
+ </doc:item>
+ </doc:list>
+ </doc:para>
+
+ <doc:title id="usernames">
+ Usernames
+ </doc:title>
+ <doc:para>
+ When a username argument is used for a method, a PolicyKit check is done on the
+ <doc:tt>net.reactivated.fprint.device.setusername</doc:tt> PolicyKit
+ action to see whether the user the client is running as is allowed to access data from other users.
+ </doc:para>
+ <doc:para>
+ By default, only root is allowed to access fingerprint data for users other than itself. For a normal user,
+ it is recommended that you use an empty string for the username, which will mean "the client the user is
+ running as".
+ </doc:para>
+ <doc:para>
+ See <doc:ref type="description" to="polkit-integration">PolicyKit integration</doc:ref>.
+ </doc:para>
+
+ <doc:title id="fingerprint-names">
+ Fingerprint names
+ </doc:title>
+ <doc:para>
+ When a finger name argument is used for a method, it refers to either a single finger, or
+ "any" finger. See the list of possible values below:
+ <doc:list>
+ <doc:item>
+ <doc:term>left-thumb</doc:term>
+ <doc:definition>
+ Left thumb
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>left-index-finger</doc:term>
+ <doc:definition>
+ Left index finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>left-middle-finger</doc:term>
+ <doc:definition>
+ Left middle finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>left-ring-finger</doc:term>
+ <doc:definition>
+ Left ring finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>left-little-finger</doc:term>
+ <doc:definition>
+ Left little finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>right-thumb</doc:term>
+ <doc:definition>
+ Right thumb
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>right-index-finger</doc:term>
+ <doc:definition>
+ Right index finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>right-middle-finger</doc:term>
+ <doc:definition>
+ Right middle finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>right-ring-finger</doc:term>
+ <doc:definition>
+ Right ring finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>right-little-finger</doc:term>
+ <doc:definition>
+ Right little finger
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>any</doc:term>
+ <doc:definition>
+ Any finger. This is only used for <doc:ref type="method" to="Device.VerifyStart">Device.VerifyStart</doc:ref>
+ (select the first finger with a fingerprint associated, or all the fingerprints available for the user when
+ the device supports it) and <doc:ref type="signal" to="Device::VerifyFingerSelected">Device::VerifyFingerSelected</doc:ref>
+ (any finger with an associated fingerprint can be used).
+ </doc:definition>
+ </doc:item>
+ </doc:list>
+ </doc:para>
+
+ <doc:title id="verify-statuses">
+ Verify Statuses
+ </doc:title>
+ <doc:para>
+ <doc:list>
+ Possible values for the result passed through <doc:ref type="signal" to="Device::VerifyResult">Device::VerifyResult</doc:ref> are:
+ <doc:item>
+ <doc:term>verify-no-match</doc:term>
+ <doc:definition>
+ The verification did not match, <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-match</doc:term>
+ <doc:definition>
+ The verification succeeded, <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-retry-scan</doc:term>
+ <doc:definition>
+ The user should retry scanning their finger, the verification is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-swipe-too-short</doc:term>
+ <doc:definition>
+ The user's swipe was too short. The user should retry scanning their finger, the verification is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-finger-not-centered</doc:term>
+ <doc:definition>
+ The user's finger was not centered on the reader. The user should retry scanning their finger, the verification is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-remove-and-retry</doc:term>
+ <doc:definition>
+ The user should remove their finger from the reader and retry scanning their finger, the verification is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-disconnected</doc:term>
+ <doc:definition>
+ The device was disconnected during the verification, no other actions should be taken, and you shouldn't use the device any more.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>verify-unknown-error</doc:term>
+ <doc:definition>
+ An unknown error occurred (usually a driver problem), <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called.
+ </doc:definition>
+ </doc:item>
+ </doc:list>
+ </doc:para>
+
+ <doc:title id="enroll-statuses">
+ Enroll Statuses
+ </doc:title>
+ <doc:para>
+ <doc:list>
+ Possible values for the result passed through <doc:ref type="signal" to="Device::EnrollResult">Device::EnrollResult</doc:ref> are:
+ <doc:item>
+ <doc:term>enroll-completed</doc:term>
+ <doc:definition>
+ The enrollment successfully completed, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-failed</doc:term>
+ <doc:definition>
+ The enrollment failed, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-stage-passed</doc:term>
+ <doc:definition>
+ One stage of the enrollment passed, the enrollment is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-retry-scan</doc:term>
+ <doc:definition>
+ The user should retry scanning their finger, the enrollment is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-swipe-too-short</doc:term>
+ <doc:definition>
+ The user's swipe was too short. The user should retry scanning their finger, the enrollment is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-finger-not-centered</doc:term>
+ <doc:definition>
+ The user's finger was not centered on the reader. The user should retry scanning their finger, the enrollment is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-remove-and-retry</doc:term>
+ <doc:definition>
+ The user should remove their finger from the reader and retry scanning their finger, the enrollment is still ongoing.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-data-full</doc:term>
+ <doc:definition>
+ No further prints can be enrolled on this device, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called.
+
+ <doc:ref type="method" to="DeleteEnrolledFingers2">Delete other prints</doc:ref> from the device first to continue
+ (e.g. from other users). Note that old prints or prints from other operating systems may be deleted automatically
+ to resolve this error without any notification.
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-disconnected</doc:term>
+ <doc:definition>
+ The device was disconnected during the enrollment, no other actions should be taken, and you shouldn't use the device any more.
+
+ </doc:definition>
+ </doc:item>
+ <doc:item>
+ <doc:term>enroll-unknown-error</doc:term>
+ <doc:definition>
+ An unknown error occurred (usually a driver problem), <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called.
+
+ </doc:definition>
+ </doc:item>
+ </doc:list>
+ </doc:para>
+ </doc:doc>
+
+ <!-- ************************************************************ -->
+
+ <method name="ListEnrolledFingers">
+ <arg type="s" name="username" direction="in">
+ <doc:doc><doc:summary>The username for whom to list the enrolled fingerprints. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc>
+ </arg>
+ <arg type="as" name="enrolled_fingers" direction="out">
+ <doc:doc><doc:summary>An array of strings representing the enrolled fingerprints. See <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ List all the enrolled fingerprints for the chosen user.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_NO_ENROLLED_PRINTS;">if the chosen user doesn't have any fingerprints enrolled</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="DeleteEnrolledFingers">
+ <arg type="s" name="username" direction="in">
+ <doc:doc><doc:summary>The username for whom to delete the enrolled fingerprints. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Delete all the enrolled fingerprints for the chosen user.
+ </doc:para>
+ <doc:para>
+ This call only exists for compatibility reasons, you should instead claim the device using
+ <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> and then call
+ <doc:ref type="method" to="DeleteEnrolledFingers2">DeleteEnrolledFingers2</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="DeleteEnrolledFingers2">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Delete all the enrolled fingerprints for the user currently claiming the device with <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="Claim">
+ <arg type="s" name="username" direction="in">
+ <doc:doc><doc:summary>The username for whom to claim the device. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Claim the device for the chosen user.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_ALREADY_IN_USE;">if the device is already claimed</doc:error>
+ <doc:error name="&ERROR_INTERNAL;">if the device couldn't be claimed</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="Release">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Release a device claimed with <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="VerifyStart">
+ <arg type="s" name="finger_name" direction="in">
+ <doc:doc><doc:summary>A string representing the finger to verify. See <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Check the chosen finger against a saved fingerprint. You need to have claimed the device using
+ <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>. The finger selected is sent to the front-end
+ using <doc:ref type="signal" to="Device::VerifyFingerSelected">Device::VerifyFingerSelected</doc:ref> and
+ verification status through <doc:ref type="signal" to="Device::VerifyStatus">Device::VerifyStatus</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error>
+ <doc:error name="&ERROR_ALREADY_IN_USE;">if the device was already being used</doc:error>
+ <doc:error name="&ERROR_NO_ENROLLED_PRINTS;">if there are no enrolled prints for the chosen user</doc:error>
+ <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="VerifyStop">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Stop an on-going fingerprint verification started with <doc:ref type="method" to="Device.VerifyStart">Device.VerifyStart</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error>
+ <doc:error name="&ERROR_NO_ACTION_IN_PROGRESS;">if there was no ongoing verification</doc:error>
+ <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <signal name="VerifyFingerSelected">
+ <arg type="s" name="finger_name">
+ <doc:doc>
+ <doc:summary>
+ <doc:para>
+ A string representing the finger select to be verified.
+ </doc:para>
+ </doc:summary>
+ </doc:doc>
+ </arg>
+ <doc:doc>
+ <doc:seealso>
+ <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.
+ </doc:seealso>
+ </doc:doc>
+ </signal>
+
+ <!-- ************************************************************ -->
+
+ <signal name="VerifyStatus">
+ <arg type="s" name="result">
+ <doc:doc>
+ <doc:summary>
+ A string representing the status of the verification.
+ </doc:summary>
+ </doc:doc>
+ </arg>
+
+ <arg type="b" name="done">
+ <doc:doc>
+ <doc:summary>
+ Whether the verification finished and can be stopped.
+ </doc:summary>
+ </doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:seealso>
+ <doc:ref type="description" to="verify-statuses">Verify Statuses</doc:ref> and <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref>.
+ </doc:seealso>
+ </doc:doc>
+ </signal>
+
+ <!-- ************************************************************ -->
+
+ <method name="EnrollStart">
+ <arg type="s" name="finger_name" direction="in">
+ <doc:doc><doc:summary>A string representing the finger to enroll. See
+ <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.
+ Note that "any" is not a valid finger name for this method.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Start enrollment for the selected finger. You need to have claimed the device using
+ <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> before calling
+ this method. Enrollment status is sent through <doc:ref type="signal" to="Device::EnrollStatus">Device::EnrollStatus</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error>
+ <doc:error name="&ERROR_ALREADY_IN_USE;">if the device was already being used</doc:error>
+ <doc:error name="&ERROR_INVALID_FINGERNAME;">if the finger name passed is invalid</doc:error>
+ <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error>
+ </doc:errors>
+
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="EnrollStop">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Stop an on-going fingerprint enrollment started with <doc:ref type="method" to="Device.EnrollStart">Device.EnrollStart</doc:ref>.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error>
+ <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error>
+ <doc:error name="&ERROR_NO_ACTION_IN_PROGRESS;">if there was no ongoing verification</doc:error>
+ <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <signal name="EnrollStatus">
+ <arg type="s" name="result">
+ <doc:doc>
+ <doc:summary>
+ A string representing the status of the enrollment.
+ </doc:summary>
+ </doc:doc>
+ </arg>
+
+ <arg type="b" name="done">
+ <doc:doc>
+ <doc:summary>
+ Whether the enrollment finished and can be stopped.
+ </doc:summary>
+ </doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:seealso>
+ <doc:ref type="description" to="enroll-statuses">Enrollment Statuses</doc:ref> and <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref>.
+ </doc:seealso>
+ </doc:doc>
+ </signal>
+
+ <!-- ************************************************************ -->
+
+ <property name="name" type="s" access="read">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ The product name of the device.
+ </doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ <!-- ************************************************************ -->
+
+ <property name="num-enroll-stages" type="i" access="read">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ The number of enrollment stages for the device. This is only available when the device has been claimed, otherwise it will be undefined (-1).
+ </doc:para>
+ <doc:seealso>
+ <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> and <doc:ref type="method" to="Device.EnrollStart">Device.EnrollStart</doc:ref>.
+ </doc:seealso>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ <!-- ************************************************************ -->
+
+ <property name="scan-type" type="s" access="read">
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ The scan type of the device, either "press" if you place your finger on the device, or "swipe" if you have to swipe your finger.
+ </doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ </interface>
+</node>
+
diff --git a/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml b/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml
new file mode 100644
index 0000000..f4a38c7
--- /dev/null
+++ b/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml
@@ -0,0 +1,50 @@
+<!DOCTYPE node PUBLIC
+"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [
+<!ENTITY ERROR_NO_SUCH_DEVICE "net.reactivated.Fprint.Error.NoSuchDevice">
+]>
+<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <interface name="net.reactivated.Fprint.Manager">
+ <annotation name="org.freedesktop.DBus.GLib.CSymbol"
+ value="fprint_manager" />
+
+ <!-- ************************************************************ -->
+
+ <method name="GetDevices">
+ <arg type="ao" name="devices" direction="out">
+ <doc:doc><doc:summary>An array of object paths for devices.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Enumerate all the fingerprint readers attached to the system. If there are
+ no devices available, an empty array is returned.
+ </doc:para>
+ </doc:description>
+ </doc:doc>
+ </method>
+
+ <!-- ************************************************************ -->
+
+ <method name="GetDefaultDevice">
+ <arg type="o" name="device" direction="out">
+ <doc:doc><doc:summary>The object path for the default device.</doc:summary></doc:doc>
+ </arg>
+
+ <doc:doc>
+ <doc:description>
+ <doc:para>
+ Returns the default fingerprint reader device.
+ </doc:para>
+ </doc:description>
+
+ <doc:errors>
+ <doc:error name="&ERROR_NO_SUCH_DEVICE;">if the device does not exist</doc:error>
+ </doc:errors>
+ </doc:doc>
+ </method>
+
+ </interface>
+</node>
+
diff --git a/panels/user-accounts/data/org.freedesktop.realmd.xml b/panels/user-accounts/data/org.freedesktop.realmd.xml
new file mode 100644
index 0000000..316213a
--- /dev/null
+++ b/panels/user-accounts/data/org.freedesktop.realmd.xml
@@ -0,0 +1,666 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node name="/">
+
+ <!--
+ org.freedesktop.realmd.Provider:
+ @short_description: a realm provider
+
+ Various realm providers represent different software implementations
+ that provide access to realms or domains.
+
+ This interface is implemented by individual providers, but is
+ aggregated globally at the system bus name
+ <literal>org.freedesktop.realmd</literal>
+ with the object path <literal>/org/freedesktop/realmd</literal>
+ -->
+ <interface name="org.freedesktop.realmd.Provider">
+
+ <!--
+ Name: the name of the provider
+
+ The name of the provider. This is not normally displayed
+ to the user, but may be useful for diagnostics or debugging.
+ -->
+ <property name="Name" type="s" access="read"/>
+
+ <!--
+ Version: the version of the provider
+
+ The version of the provider. This is not normally used in
+ logic, but may be useful for diagnostics or debugging.
+ -->
+ <property name="Version" type="s" access="read"/>
+
+ <!--
+ Realms: a list of realms
+
+ A list of known, enrolled or discovered realms. All realms
+ that this provider knows about are listed here. As realms
+ are discovered they are added to this list.
+
+ Each realm is represented by the DBus object path of the
+ realm object.
+ -->
+ <property name="Realms" type="ao" access="read"/>
+
+ <!--
+ Discover:
+ @string: an input string to discover realms for
+ @options: options for the discovery operation
+ @relevance: the relevance of the returned results
+ @realm: a list of realms discovered
+
+ Discover realms for the given string. The input @string is
+ usually a domain or realm name, perhaps typed by a user. If
+ an empty string is provided the realm provider should try to
+ discover a default realm if possible (eg: from DHCP).
+
+ @options can contain, but is not limited to, the following values:
+ <itemizedlist>
+ <listitem><para><literal>operation</literal>: a string
+ identifier chosen by the client, which can then later be
+ passed to org.freedesktop.realmd.Service.Cancel() in order
+ to cancel the operation</para></listitem>
+ </itemizedlist>
+
+ The @relevance returned can be used to rank results from
+ different discover calls to different providers. Implementors
+ should return a positive number if the provider highly
+ recommends that the realms be handled by this provider,
+ or a zero if it can possibly handle the realms. Negative
+ should be returned if no realms are found.
+
+ This method does not return an error when no realms are
+ discovered. It simply returns an @realm list.
+
+ To see diagnostic information about the discovery process
+ connect to the org.freedesktop.realmd.Service::Diagnostics
+ signal.
+
+ This method requires authorization for the PolicyKit action
+ called <literal>org.freedesktop.realmd.discover-realm</literal>.
+
+ In addition to common DBus error results, this method may
+ return:
+ <itemizedlist>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>:
+ may be returned if the discovery could not be run for some reason.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>:
+ returned if the operation was cancelled.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>:
+ returned if the calling client is not permitted to perform a discovery
+ operation.</para></listitem>
+ </itemizedlist>
+ -->
+ <method name="Discover">
+ <arg name="string" type="s" direction="in"/>
+ <arg name="options" type="a{sv}" direction="in"/>
+ <arg name="relevance" type="i" direction="out"/>
+ <arg name="realm" type="ao" direction="out"/>
+ </method>
+
+ </interface>
+
+ <!--
+ org.freedesktop.realmd.Service:
+ @short_description: the realmd service
+
+ Global calls for managing the realmd service. Usually you'll want
+ to use #org.freedesktop.realmd.Provider instead.
+
+ This interface is implemented by the realmd service, and is always
+ available at the object path <literal>/org/freedesktop/realmd</literal>
+
+ The service also implements the
+ <literal>org.freedesktop.DBus.ObjectManager</literal> interface which
+ makes it easy to retrieve all realmd objects and properties in one go.
+ -->
+ <interface name="org.freedesktop.realmd.Service">
+
+ <!--
+ Cancel:
+ @operation: the operation to cancel
+
+ Cancel a realmd operation. To be able to cancel an operation
+ pass a uniquely chosen <literal>operation</literal> string
+ identifier as an option in the methods <literal>options</literal>
+ argument.
+
+ These operation string identifiers should be unique per client
+ calling the realmd service.
+
+ It is not guaranteed that the service can or will cancel the
+ operation. For example the operation may have already completed
+ by the time this method is handled. The caller of the operation
+ method will receive a
+ <literal>org.freedesktop.realmd.Error.Cancelled</literal>
+ if the operation was cancelled.
+ -->
+ <method name="Cancel">
+ <arg name="operation" type="s" direction="in"/>
+ </method>
+
+ <!--
+ SetLocale:
+ @locale: the locale for the client
+
+ Set the language @locale for the client. This locale is used
+ for error messages. The locale is used until the next time
+ this method is called, the client disconnects, or the client
+ calls #org.freedesktop.realmd.Service.Release().
+ -->
+ <method name="SetLocale">
+ <arg name="locale" type="s" direction="in"/>
+ </method>
+
+ <!--
+ Diagnostics:
+ @data: diagnostic data
+ @operation: the operation this data resulted from
+
+ This signal is fired when diagnostics result from an operation
+ in the provider or one of its realms.
+
+ It is not guaranteed that this signal is emitted once per line.
+ More than one line may be contained in @data, or a partial
+ line. New line characters are embedded in @data.
+
+ This signal is sent explicitly to the client which invoked
+ operation method. In order to tell which operation this
+ diagnostic data results from, pass a unique
+ <literal>operation</literal> string identifier in the
+ <literal>options</literal> argument of the operation method.
+ That same identifier will be passed back via the @operation
+ argument of this signal.
+ -->
+ <signal name="Diagnostics">
+ <arg name="data" type="s"/>
+ <arg name="operation" type="s"/>
+ </signal>
+
+ <!--
+ Release:
+
+ Normally realmd waits until all clients have disconnected
+ before exiting itself, sometime later. For long lived clients
+ they can call this method to allow the realmd service to quit.
+ This is an optimization. The daemon will not exit immediately.
+ It is safe to call this multiple times.
+ -->
+ <method name="Release">
+ <!-- no arguments -->
+ </method>
+
+ </interface>
+
+ <!--
+ org.freedesktop.realmd.Realm:
+ @short_description: a realm
+
+ Represents one realm.
+
+ Contains generic information about a realm, and useful properties for
+ introspecting what kind of realm this is and how to work with
+ the realm.
+
+ Use #org.freedesktop.realmd.Provider:Realms or
+ #org.freedesktop.realmd.Provider.Discover() to get access to some
+ kerberos realm objects.
+
+ Realms will always implement additional interfaces, such as
+ #org.freedesktop.realmd.Kerberos. Do not assume that all realms
+ implement that kerberos interface. Use the
+ #org.freedesktop.realmd.Realm:SupportedInterfaces property to see
+ which interfaces are set.
+
+ Different realms support various ways to configure them on the
+ system. Use the #org.freedesktop.realmd.Realm:Configured property
+ to determine if a realm is configured. If it is configured the
+ property will be set to the interface of the mechanism that was
+ used to configure it.
+
+ To configure a realm, look in the
+ #org.freedesktop.realmd.Realm:SupportedInterfaces property for a
+ recognized purpose specific interface that can be used for
+ configuration, such as the
+ #org.freedesktop.realmd.KerberosMembership interface and its
+ #org.freedesktop.realmd.KerberosMembership.Join() method.
+
+ To deconfigure a realm from the current system, you can use the
+ #org.freedesktop.realmd.Realm.Deconfigure() method. In additon some
+ of the configuration specific interfaces provide methods to
+ deconfigure a realm in a specific way, such as
+ #org.freedesktop.realmd.KerberosMembership.Leave() method.
+
+ The various properties are guaranteed to have been updated before
+ the operation methods return, if they change state.
+ -->
+ <interface name="org.freedesktop.realmd.Realm">
+
+ <!--
+ Name: the realm name
+
+ This is the name of the realm, appropriate for display to
+ end users where necessary.
+ -->
+ <property name="Name" type="s" access="read"/>
+
+ <!--
+ Configured: whether this domain is configured and how
+
+ If this property is an empty string, then the realm is not
+ configured. Otherwise the realm is configured, and contains
+ a string which is the interface that represents how it was
+ configured, for example #org.freedesktop.realmd.KerberosMembership.
+ -->
+ <property name="Configured" type="s" access="read"/>
+
+ <!--
+ Deconfigure: deconfigure this realm
+
+ Deconfigure this realm from the local machine with standard
+ default behavior.
+
+ The behavior of this method depends on the which configuration
+ interface is present in the
+ #org.freedesktop.realmd.Realm.Configured property. It does not
+ always delete membership accounts in the realm, but just
+ reconfigures the local machine so it no longer is configured
+ for the given realm. In some cases the implementation may try
+ to update membership accounts, but this is not guaranteed.
+
+ Various configuration interfaces may support more specific ways
+ to deconfigure a realm in a specific way, such as the
+ #org.freedesktop.realmd.KerberosMembership.Leave() method.
+
+ @options can contain, but is not limited to, the following values:
+ <itemizedlist>
+ <listitem><para><literal>operation</literal>: a string
+ identifier chosen by the client, which can then later be
+ passed to org.freedesktop.realmd.Service.Cancel() in order
+ to cancel the operation</para></listitem>
+ </itemizedlist>
+
+ This method requires authorization for the PolicyKit action
+ called <literal>org.freedesktop.realmd.deconfigure-realm</literal>.
+
+ In addition to common DBus error results, this method may return:
+ <itemizedlist>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>:
+ may be returned if the deconfigure failed for a generic reason.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>:
+ returned if the operation was cancelled.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>:
+ returned if the calling client is not permitted to deconfigure a
+ realm.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotConfigured</literal>:
+ returned if this realm is not configured on the machine.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>:
+ returned if the service is currently performing another operation like
+ join or leave.</para></listitem>
+ </itemizedlist>
+ -->
+ <method name="Deconfigure">
+ <arg name="options" type="a{sv}" direction="in"/>
+ </method>
+
+ <!--
+ SupportedInterfaces:
+
+ Additional supported interfaces of this realm. This includes
+ interfaces that contain more information about the realm,
+ such as #org.freedesktop.realmd.Kerberos and interfaces
+ which contain methods for configuring a realm, such as
+ #org.freedesktop.realmd.KerberosMembership.
+ -->
+ <property name="SupportedInterfaces" type="as" access="read"/>
+
+ <!--
+ Details: informational details about the realm
+
+ Informational details about the realm. The following values
+ should be present:
+ <itemizedlist>
+ <listitem><para><literal>server-software</literal>:
+ identifier of the software running on the server (eg:
+ <literal>active-directory</literal>).</para></listitem>
+ <listitem><para><literal>client-software</literal>:
+ identifier of the software running on the client (eg:
+ <literal>sssd</literal>).</para></listitem>
+ </itemizedlist>
+ -->
+ <property name="Details" type="a(ss)" access="read"/>
+
+ <!--
+ LoginFormats: supported formats for login names
+
+ Supported formats for login to this realm. This is only
+ relevant once the realm has been enrolled. The formats
+ will contain a <literal>%U</literal> in the string, which
+ indicate where the user name should be placed. The formats
+ may contain a <literal>%D</literal> in the string which
+ indicate where a domain name should be placed.
+
+ The first format in the list is the preferred format for
+ login names.
+ -->
+ <property name="LoginFormats" type="as" access="read"/>
+
+ <!--
+ LoginPolicy: the policy for logins using this realm
+
+ The policy for logging into this computer using this realm.
+
+ The policy can be changed using the
+ #org.freedesktop.realmd.Realm.ChangeLoginPolicy() method.
+
+ The following policies are predefined. Not all providers
+ support all these policies and there may be provider specific
+ policies or multiple policies represented in the string:
+ <itemizedlist>
+ <listitem><para><literal>allow-any-login</literal>: allow
+ login by any authenticated user present in this
+ realm.</para></listitem>
+ <listitem><para><literal>allow-permitted-logins</literal>:
+ only allow the logins permitted in the
+ #org.freedesktop.realmd.Realm:PermittedLogins
+ property.</para></listitem>
+ <listitem><para><literal>deny-any-login</literal>:
+ don't allow any logins via authenticated users of this
+ realm.</para></listitem>
+ </itemizedlist>
+ -->
+ <property name="LoginPolicy" type="s" access="read"/>
+
+ <!--
+ PermittedLogins: the permitted login names
+
+ The list of permitted authenticated users allowed to login
+ into this computer. This is only relevant if the
+ #org.freedesktop.realmd.Realm:LoginPolicy property
+ contains the <literal>allow-permitted-logins</literal>
+ string.
+ -->
+ <property name="PermittedLogins" type="as" access="read"/>
+
+ <!--
+ ChangeLoginPolicy:
+ @login_policy: the new login policy, or an empty string
+ @permitted_add: a list of logins to permit
+ @permitted_remove: a list of logins to not permit
+ @options: options for this operation
+
+ Change the login policy and/or permitted logins for this realm.
+
+ Not all realms support the all the various login policies. An
+ error will be returned if the new login policy is not supported.
+ You may specify an empty string for the @login_policy argument
+ which will cause no change in the policy itself. If the policy
+ is changed, it will be reflected in the
+ #org.freedesktop.realmd.Realm:LoginPolicy property.
+
+ The @permitted_add and @permitted_remove arguments represent
+ lists of login names that should be added and removed from
+ the #org.freedesktop.realmd.Kerberos:PermittedLogins property.
+
+ @options can contain, but is not limited to, the following values:
+ <itemizedlist>
+ <listitem><para><literal>operation</literal>: a string
+ identifier chosen by the client, which can then later be
+ passed to org.freedesktop.realmd.Service.Cancel() in order
+ to cancel the operation</para></listitem>
+ </itemizedlist>
+
+ This method requires authorization for the PolicyKit action
+ called <literal>org.freedesktop.realmd.login-policy</literal>.
+
+ In addition to common DBus error results, this method may return:
+ <itemizedlist>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>:
+ may be returned if the policy change failed for a generic reason.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>:
+ returned if the operation was cancelled.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>:
+ returned if the calling client is not permitted to change login policy
+ operation.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotConfigured</literal>:
+ returned if the realm is not configured.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>:
+ returned if the service is currently performing another operation like
+ join or leave.</para></listitem>
+ </itemizedlist>
+ -->
+ <method name="ChangeLoginPolicy">
+ <arg name="login_policy" type="s" direction="in"/>
+ <arg name="permitted_add" type="as" direction="in"/>
+ <arg name="permitted_remove" type="as" direction="in"/>
+ <arg name="options" type="a{sv}" direction="in"/>
+ </method>
+
+ </interface>
+
+ <!--
+ org.freedesktop.realmd.Kerberos:
+ @short_description: a kerberos realm
+
+ An interface that describes a kerberos realm in more detail. This
+ is always implemented on an DBus object path that also implements
+ the #org.freedesktop.realmd.Realm interface.
+ -->
+ <interface name="org.freedesktop.realmd.Kerberos">
+
+ <!--
+ RealmName: the kerberos realm name
+
+ The kerberos name for this realm. This is usually in upper
+ case.
+ -->
+ <property name="RealmName" type="s" access="read"/>
+
+ <!--
+ DomainName: the DNS domain name
+
+ The DNS domain name for this realm.
+ -->
+ <property name="DomainName" type="s" access="read"/>
+
+ </interface>
+
+ <!--
+ org.freedesktop.realmd.KerberosMembership:
+
+ An interface used to configure this machine by joining a realm.
+
+ It sets up a computer/host account in the realm for this machine
+ and a keytab to track the credentials for that account.
+
+ The various properties are guaranteed to have been updated before
+ the operation methods return, if they change state.
+ -->
+ <interface name="org.freedesktop.realmd.KerberosMembership">
+
+ <!--
+ SuggestedAdministrator: common administrator name
+
+ The common administrator name for this type of realm. This
+ can be used by clients as a hint when prompting the user for
+ administrative authentication.
+ -->
+ <property name="SuggestedAdministrator" type="s" access="read"/>
+
+ <!--
+ SupportedJoinCredentials: credentials supported for joining
+
+ Various kinds of credentials that are supported when calling the
+ #org.freedesktop.realmd.Kerberos.Join() method.
+
+ Each credential is represented by a type, and an owner. The type
+ denotes which kind of credential is passed to the method. The
+ owner indicates to the client how to prompt the user or obtain
+ the credential, and to the service how to use the credential.
+
+ The various types are:
+ <itemizedlist>
+ <listitem><para><literal>ccache</literal>:
+ the credentials should contain an array of bytes as a
+ <literal>ay</literal> containing the data from a kerberos
+ credential cache file.</para></listitem>
+ <listitem><para><literal>password</literal>:
+ the credentials should contain a pair of strings as a
+ <literal>(ss)</literal> representing a name and
+ password. The name may contain a realm in the standard
+ kerberos format. If missing, it will default to this
+ realm. The name may be empty for a computer or one time
+ password.</para></listitem>
+ <listitem><para><literal>automatic</literal>:
+ the credentials should contain an empty string as a
+ <literal>s</literal>. Using <literal>automatic</literal>
+ indicates that default or system credentials are to be
+ used.</para></listitem>
+ </itemizedlist>
+
+ The various owners are:
+ <itemizedlist>
+ <listitem><para><literal>administrator</literal>:
+ the credentials belong to a kerberos user principal.
+ The caller may use this as a hint to prompt the user
+ for administrative credentials.</para></listitem>
+ <listitem><para><literal>user</literal>:
+ the credentials belong to a kerberos user principal.
+ The caller may use this as a hint to prompt the user
+ for his (possibly non-administrative)
+ credentials.</para></listitem>
+ <listitem><para><literal>computer</literal>:
+ the credentials belong to the computer realmd is
+ being run on.</para></listitem>
+ <listitem><para><literal>secret</literal>:
+ the credentials are a one time password or other secret
+ used to join or leave the computer.</para></listitem>
+ </itemizedlist>
+ -->
+ <property name="SupportedJoinCredentials" type="a(ss)" access="read"/>
+
+ <!--
+ SupportedLeaveCredentials: credentials supported for leaving
+
+ Various kinds of credentials that are supported when calling the
+ #org.freedesktop.realmd.Kerberos.Leave() method.
+
+ See #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials for
+ a discussion of what the values represent.
+ -->
+ <property name="SupportedLeaveCredentials" type="a(ss)" access="read"/>
+
+ <!--
+ Join:
+
+ Join this machine to the realm and enroll the machine.
+
+ If this method returns successfully then the machine will be
+ joined to the realm. It is not necessary to restart services or the
+ machine afterward. Relevant properties on the realm will be updated
+ before the method returns.
+
+ The @credentials should be set according to one of the
+ supported credentials returned by
+ #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials.
+ The first string in the tuple is the type, the second string
+ is the owner, and the variant contains the credential contents
+ See the discussion at
+ #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials
+ for more information.
+
+ @options can contain, but is not limited to, the following values:
+ <itemizedlist>
+ <listitem><para><literal>operation</literal>: a string
+ identifier chosen by the client, which can then later be
+ passed to org.freedesktop.realmd.Service.Cancel() in order
+ to cancel the operation</para></listitem>
+ </itemizedlist>
+
+ This method requires authorization for the PolicyKit action
+ called <literal>org.freedesktop.realmd.configure-realm</literal>.
+
+ In addition to common DBus error results, this method may return:
+ <itemizedlist>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>:
+ may be returned if the join failed for a generic reason.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>:
+ returned if the operation was cancelled.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>:
+ returned if the calling client is not permitted to perform an join
+ operation.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.AuthenicationFailed</literal>:
+ returned if the credentials passed did not authenticate against the realm
+ correctly. It is appropriate to prompt the user again.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.AlreadyEnrolled</literal>:
+ returned if already enrolled in this realm, or another realm and enrolling
+ in multiple realms is not supported.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>:
+ returned if the service is currently performing another operation like
+ join or leave.</para></listitem>
+ </itemizedlist>
+ -->
+ <method name="Join">
+ <arg name="credentials" type="(ssv)" direction="in"/>
+ <arg name="options" type="a{sv}" direction="in"/>
+ </method>
+
+ <!--
+ Leave:
+
+ Leave the realm and unenroll the machine.
+
+ If this method returns successfully then the machine will have
+ left the domain and been unenrolled. It is not necessary to restart
+ services or the machine afterward. Relevant properties on the realm
+ will be updated before the method returns.
+
+ The @credentials should be set according to one of the
+ supported credentials returned by
+ #org.freedesktop.realmd.Kerberos:SupportedUnenrollCredentials.
+ The first string in the tuple is the type, the second string
+ is the owner, and the variant contains the credential contents
+ See the discussion at
+ #org.freedesktop.realmd.Kerberos:SupportedEnrollCredentials
+ for more information.
+
+ @options can contain, but is not limited to, the following values:
+ <itemizedlist>
+ <listitem><para><literal>operation</literal>: a string
+ identifier chosen by the client, which can then later be
+ passed to org.freedesktop.realmd.Service.Cancel() in order
+ to cancel the operation</para></listitem>
+ </itemizedlist>
+
+ This method requires authorization for the PolicyKit action
+ called <literal>org.freedesktop.realmd.deconfigure-realm</literal>.
+
+ In addition to common DBus error results, this method may return:
+ <itemizedlist>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>:
+ may be returned if the unenroll failed for a generic reason.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>:
+ returned if the operation was cancelled.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>:
+ returned if the calling client is not permitted to perform an unenroll
+ operation.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.AuthenicationFailed</literal>:
+ returned if the credentials passed did not authenticate against the realm
+ correctly. It is appropriate to prompt the user again.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.NotEnrolled</literal>:
+ returned if not enrolled in this realm.</para></listitem>
+ <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>:
+ returned if the service is currently performing another operation like
+ enroll or unenroll.</para></listitem>
+ </itemizedlist>
+ -->
+ <method name="Leave">
+ <arg name="credentials" type="(ssv)" direction="in"/>
+ <arg name="options" type="a{sv}" direction="in"/>
+ </method>
+
+ </interface>
+
+</node>
diff --git a/panels/user-accounts/data/user-accounts-dialog.css b/panels/user-accounts/data/user-accounts-dialog.css
new file mode 100644
index 0000000..49be443
--- /dev/null
+++ b/panels/user-accounts/data/user-accounts-dialog.css
@@ -0,0 +1,28 @@
+levelbar .strength-weak {
+ background-color: #cc0000;
+ border-color: #cc0000;
+}
+
+levelbar .strength-low {
+ background-color: #f5ce00;
+ border-color: #f5ce00;
+}
+
+levelbar .strength-medium,
+levelbar .strength-good,
+levelbar .strength-high {
+ background-color: #73d216;
+ border-color: #73d216;
+}
+
+.user-icon-button {
+ background: transparent;
+ box-shadow: none;
+ border: 1px solid @borders;
+ border-radius: 50%;
+ padding: 3px;
+}
+
+.user-icon-button > .user-icon-button {
+ padding: 0;
+}
diff --git a/panels/user-accounts/fingerprint-strings.h b/panels/user-accounts/fingerprint-strings.h
new file mode 100644
index 0000000..4336130
--- /dev/null
+++ b/panels/user-accounts/fingerprint-strings.h
@@ -0,0 +1,172 @@
+/*
+ * Helper functions to translate statuses and actions to strings
+ * Copyright (C) 2008 Bastien Nocera <hadess@hadess.net>
+ *
+ * Experimental code. This will be moved out of fprintd into it's own
+ * package once the system has matured.
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+struct {
+ const char *dbus_name;
+ const char *place_str_generic;
+ const char *place_str_specific;
+ const char *swipe_str_generic;
+ const char *swipe_str_specific;
+} fingers[] = {
+ { "any",
+ N_("Place your finger on the fingerprint reader"),
+ N_("Place your finger on %s"),
+ N_("Swipe your finger across the fingerprint reader"),
+ N_("Swipe your finger across %s") },
+ { "left-thumb",
+ N_("Place your left thumb on the fingerprint reader"),
+ N_("Place your left thumb on %s"),
+ N_("Swipe your left thumb across the fingerprint reader"),
+ N_("Swipe your left thumb across %s") },
+ { "left-index-finger",
+ N_("Place your left index finger on the fingerprint reader"),
+ N_("Place your left index finger on %s"),
+ N_("Swipe your left index finger across the fingerprint reader"),
+ N_("Swipe your left index finger across %s") },
+ { "left-middle-finger",
+ N_("Place your left middle finger on the fingerprint reader"),
+ N_("Place your left middle finger on %s"),
+ N_("Swipe your left middle finger across the fingerprint reader"),
+ N_("Swipe your left middle finger across %s") },
+ { "left-ring-finger",
+ N_("Place your left ring finger on the fingerprint reader"),
+ N_("Place your left ring finger on %s"),
+ N_("Swipe your left ring finger across the fingerprint reader"),
+ N_("Swipe your left ring finger across %s") },
+ { "left-little-finger",
+ N_("Place your left little finger on the fingerprint reader"),
+ N_("Place your left little finger on %s"),
+ N_("Swipe your left little finger across the fingerprint reader"),
+ N_("Swipe your left little finger across %s") },
+ { "right-thumb",
+ N_("Place your right thumb on the fingerprint reader"),
+ N_("Place your right thumb on %s"),
+ N_("Swipe your right thumb across the fingerprint reader"),
+ N_("Swipe your right thumb across %s") },
+ { "right-index-finger",
+ N_("Place your right index finger on the fingerprint reader"),
+ N_("Place your right index finger on %s"),
+ N_("Swipe your right index finger across the fingerprint reader"),
+ N_("Swipe your right index finger across %s") },
+ { "right-middle-finger",
+ N_("Place your right middle finger on the fingerprint reader"),
+ N_("Place your right middle finger on %s"),
+ N_("Swipe your right middle finger across the fingerprint reader"),
+ N_("Swipe your right middle finger across %s") },
+ { "right-ring-finger",
+ N_("Place your right ring finger on the fingerprint reader"),
+ N_("Place your right ring finger on %s"),
+ N_("Swipe your right ring finger across the fingerprint reader"),
+ N_("Swipe your right ring finger across %s") },
+ { "right-little-finger",
+ N_("Place your right little finger on the fingerprint reader"),
+ N_("Place your right little finger on %s"),
+ N_("Swipe your right little finger across the fingerprint reader"),
+ N_("Swipe your right little finger across %s") },
+ { NULL, NULL, NULL, NULL, NULL }
+};
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+
+G_GNUC_UNUSED static char *finger_str_to_msg(const char *finger_name, const char *driver_name, gboolean is_swipe)
+{
+ int i;
+
+ if (finger_name == NULL)
+ return NULL;
+
+ for (i = 0; fingers[i].dbus_name != NULL; i++) {
+ if (g_str_equal (fingers[i].dbus_name, finger_name)) {
+ if (is_swipe == FALSE) {
+ if (driver_name)
+ return g_strdup_printf (TR (fingers[i].place_str_specific), driver_name);
+ else
+ return g_strdup (TR (fingers[i].place_str_generic));
+ } else {
+ if (driver_name)
+ return g_strdup_printf (TR (fingers[i].swipe_str_specific), driver_name);
+ else
+ return g_strdup (TR (fingers[i].swipe_str_generic));
+ }
+ }
+ }
+
+ return NULL;
+}
+
+#pragma GCC diagnostic pop
+
+/* Cases not handled:
+ * verify-no-match
+ * verify-match
+ * verify-unknown-error
+ */
+G_GNUC_UNUSED static const char *verify_result_str_to_msg(const char *result, gboolean is_swipe)
+{
+ if (result == NULL)
+ return NULL;
+
+ if (strcmp (result, "verify-retry-scan") == 0) {
+ if (is_swipe == FALSE)
+ return N_("Place your finger on the reader again");
+ else
+ return N_("Swipe your finger again");
+ }
+ if (strcmp (result, "verify-swipe-too-short") == 0)
+ return N_("Swipe was too short, try again");
+ if (strcmp (result, "verify-finger-not-centered") == 0)
+ return N_("Your finger was not centered, try swiping your finger again");
+ if (strcmp (result, "verify-remove-and-retry") == 0)
+ return N_("Remove your finger, and try swiping your finger again");
+
+ return NULL;
+}
+
+/* Cases not handled:
+ * enroll-completed
+ * enroll-failed
+ * enroll-unknown-error
+ */
+G_GNUC_UNUSED static const char *enroll_result_str_to_msg(const char *result, gboolean is_swipe)
+{
+ if (result == NULL)
+ return NULL;
+
+ if (strcmp (result, "enroll-retry-scan") == 0 || strcmp (result, "enroll-stage-passed") == 0) {
+ if (is_swipe == FALSE)
+ return N_("Place your finger on the reader again");
+ else
+ return N_("Swipe your finger again");
+ }
+ if (strcmp (result, "enroll-swipe-too-short") == 0)
+ return N_("Swipe was too short, try again");
+ if (strcmp (result, "enroll-finger-not-centered") == 0)
+ return N_("Your finger was not centered, try swiping your finger again");
+ if (strcmp (result, "enroll-remove-and-retry") == 0)
+ return N_("Remove your finger, and try swiping your finger again");
+
+ return NULL;
+}
+
diff --git a/panels/user-accounts/meson.build b/panels/user-accounts/meson.build
new file mode 100644
index 0000000..b8ee9d9
--- /dev/null
+++ b/panels/user-accounts/meson.build
@@ -0,0 +1,207 @@
+panels_list += cappletname
+desktop = 'gnome-@0@-panel.desktop'.format(cappletname)
+
+desktop_in = configure_file(
+ input: 'data/' + desktop + '.in.in',
+ output: desktop + '.in',
+ configuration: desktop_conf
+)
+
+i18n.merge_file(
+ desktop,
+ type: 'desktop',
+ input: desktop_in,
+ output: desktop,
+ po_dir: po_dir,
+ install: true,
+ install_dir: control_center_desktopdir
+)
+
+image_data = files(
+ 'data/faces/bicycle.jpg',
+ 'data/faces/book.jpg',
+ 'data/faces/calculator.jpg',
+ 'data/faces/cat.jpg',
+ 'data/faces/coffee2.jpg',
+ 'data/faces/flower2.jpg',
+ 'data/faces/gamepad.jpg',
+ 'data/faces/guitar2.jpg',
+ 'data/faces/headphones.jpg',
+ 'data/faces/hummingbird.jpg',
+ 'data/faces/mountain.jpg',
+ 'data/faces/plane.jpg',
+ 'data/faces/surfer.jpg',
+ 'data/faces/tomatoes.jpg',
+ 'data/faces/tree.jpg',
+)
+
+legacy_image_data = files(
+ 'data/faces/legacy/astronaut.jpg',
+ 'data/faces/legacy/baseball.png',
+ 'data/faces/legacy/butterfly.png',
+ 'data/faces/legacy/cat-eye.jpg',
+ 'data/faces/legacy/chess.jpg',
+ 'data/faces/legacy/coffee.jpg',
+ 'data/faces/legacy/dice.jpg',
+ 'data/faces/legacy/energy-arc.jpg',
+ 'data/faces/legacy/fish.jpg',
+ 'data/faces/legacy/flake.jpg',
+ 'data/faces/legacy/flower.jpg',
+ 'data/faces/legacy/grapes.jpg',
+ 'data/faces/legacy/guitar.jpg',
+ 'data/faces/legacy/launch.jpg',
+ 'data/faces/legacy/leaf.jpg',
+ 'data/faces/legacy/lightning.jpg',
+ 'data/faces/legacy/penguin.jpg',
+ 'data/faces/legacy/puppy.jpg',
+ 'data/faces/legacy/sky.jpg',
+ 'data/faces/legacy/soccerball.png',
+ 'data/faces/legacy/sunflower.jpg',
+ 'data/faces/legacy/sunset.jpg',
+ 'data/faces/legacy/tennis-ball.png',
+ 'data/faces/legacy/yellow-rose.jpg',
+)
+
+image_dir = join_paths(control_center_datadir, 'pixmaps', 'faces')
+
+install_data(
+ image_data,
+ install_dir: image_dir
+)
+
+legacy_image_dir = join_paths(image_dir, 'legacy')
+
+install_data(
+ legacy_image_data,
+ install_dir: legacy_image_dir
+)
+
+# create symlinks for legacy images to not break current images for people
+meson.add_install_script('sh', '-c',
+ '''for f in $DESTDIR@0@/*; do
+ ln -sf legacy/$(basename $f) $DESTDIR@1@/$(basename $f);
+ done'''.format(legacy_image_dir, image_dir))
+
+polkit = 'org.gnome.controlcenter.@0@.policy'.format(cappletname)
+
+i18n.merge_file(
+ polkit,
+ input: polkit + '.in',
+ output: polkit,
+ po_dir: po_dir,
+ install: true,
+ install_dir: join_paths(control_center_datadir, 'polkit-1', 'actions')
+)
+
+common_sources = files(
+ 'cc-add-user-dialog.c',
+ 'cc-realm-manager.c',
+ 'pw-utils.c',
+ 'user-utils.c',
+)
+
+resource_data = files(
+ 'cc-add-user-dialog.ui',
+ 'cc-avatar-chooser.ui',
+ 'cc-carousel.ui',
+ 'cc-login-history-dialog.ui',
+ 'cc-password-dialog.ui',
+ 'cc-user-panel.ui',
+ 'cc-fingerprint-dialog.ui',
+ 'data/icons/fingerprint-detection-complete-symbolic.svg',
+ 'data/icons/fingerprint-detection-symbolic.svg',
+ 'data/icons/fingerprint-detection-warning-symbolic.svg',
+ 'data/carousel.css',
+ 'data/join-dialog.ui',
+ 'data/user-accounts-dialog.css',
+ 'data/cc-fingerprint-dialog.css',
+)
+
+common_sources += gnome.compile_resources(
+ 'cc-' + cappletname + '-resources',
+ cappletname + '.gresource.xml',
+ c_name: 'cc_' + cappletname.underscorify(),
+ dependencies: resource_data,
+ export: true
+)
+
+realmd_namespace = 'org.freedesktop.realmd'
+
+common_sources += gnome.gdbus_codegen(
+ 'cc-realm-generated',
+ 'data/' + realmd_namespace + '.xml',
+ interface_prefix: realmd_namespace + '.',
+ namespace: 'CcRealm',
+ object_manager: true,
+ annotations: ['org.freedesktop.realmd.Realm', 'org.gtk.GDBus.C.Name', 'Common']
+)
+
+fprintd_namespace = 'net.reactivated.Fprint'
+common_sources += gnome.gdbus_codegen(
+ 'cc-fprintd-generated',
+ sources: [
+ 'data' / fprintd_namespace + '.Manager.xml',
+ 'data' / fprintd_namespace + '.Device.xml',
+ ],
+ interface_prefix: fprintd_namespace + '.',
+ namespace: 'CcFprintd',
+ autocleanup: 'all',
+)
+
+enum_headers = [
+ 'cc-fingerprint-manager.h',
+]
+
+sources = common_sources + files(
+ 'cc-avatar-chooser.c',
+ 'cc-carousel.c',
+ 'cc-crop-area.c',
+ 'cc-fingerprint-manager.c',
+ 'cc-fingerprint-dialog.c',
+ 'cc-login-history-dialog.c',
+ 'cc-password-dialog.c',
+ 'cc-user-image.c',
+ 'cc-user-panel.c',
+ 'run-passwd.c',
+)
+
+sources += gnome.mkenums_simple(
+ 'cc-user-accounts-enum-types',
+ sources: files(enum_headers))
+
+# Kerberos support
+krb_dep = dependency('krb5', required: false)
+assert(krb_dep.found(), 'kerberos libraries not found in your path')
+
+deps = common_deps + [
+ accounts_dep,
+ gdk_pixbuf_dep,
+ gnome_desktop_dep,
+ liblanguage_dep,
+ krb_dep,
+ m_dep,
+ polkit_gobject_dep,
+ dependency('pwquality', version: '>= 1.2.2')
+]
+
+if enable_cheese
+ deps += cheese_deps
+endif
+
+if enable_malcontent
+ deps += malcontent_dep
+endif
+
+cflags += [
+ '-DGNOMELOCALEDIR="@0@"'.format(control_center_localedir),
+ '-DHAVE_LIBPWQUALITY',
+ '-DUM_PIXMAP_DIR="@0@"'.format(join_paths(control_center_pkgdatadir, 'pixmaps'))
+]
+
+panels_libs += static_library(
+ cappletname,
+ sources: sources,
+ include_directories: [top_inc, shell_inc],
+ dependencies: deps,
+ c_args: cflags
+)
diff --git a/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in b/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in
new file mode 100644
index 0000000..7d9e686
--- /dev/null
+++ b/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
+
+<policyconfig>
+ <vendor>The GNOME Project</vendor>
+ <vendor_url>http://www.gnome.org/</vendor_url>
+
+ <action id="org.gnome.controlcenter.user-accounts.administration">
+ <description>Manage user accounts</description>
+ <message>Authentication is required to change user data</message>
+ <defaults>
+ <allow_any>no</allow_any>
+ <allow_inactive>no</allow_inactive>
+ <allow_active>auth_admin_keep</allow_active>
+ </defaults>
+ <annotate key="org.freedesktop.policykit.imply">org.freedesktop.accounts.user-administration org.freedesktop.realmd.configure-realm org.freedesktop.realmd.login-policy</annotate>
+ </action>
+
+</policyconfig>
diff --git a/panels/user-accounts/pw-utils.c b/panels/user-accounts/pw-utils.c
new file mode 100644
index 0000000..0f4dfd8
--- /dev/null
+++ b/panels/user-accounts/pw-utils.c
@@ -0,0 +1,177 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include "pw-utils.h"
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include <pwquality.h>
+
+static pwquality_settings_t *
+get_pwq (void)
+{
+ static pwquality_settings_t *settings;
+
+ if (settings == NULL) {
+ gchar *err = NULL;
+ gint rv = 0;
+
+ settings = pwquality_default_settings ();
+ pwquality_set_int_value (settings, PWQ_SETTING_MAX_SEQUENCE, 4);
+
+ rv = pwquality_read_config (settings, NULL, (gpointer)&err);
+ if (rv < 0) {
+ g_warning ("failed to read pwquality configuration: %s\n",
+ pwquality_strerror (NULL, 0, rv, err));
+ pwquality_free_settings (settings);
+
+ /* Load just default settings in case of failure. */
+ settings = pwquality_default_settings ();
+ pwquality_set_int_value (settings, PWQ_SETTING_MAX_SEQUENCE, 4);
+ }
+ }
+
+ return settings;
+}
+
+gint
+pw_min_length (void)
+{
+ gint value = 0;
+ gint rv;
+
+ rv = pwquality_get_int_value (get_pwq (), PWQ_SETTING_MIN_LENGTH, &value);
+ if (rv < 0) {
+ g_warning ("Failed to read pwquality setting: %s\n",
+ pwquality_strerror (NULL, 0, rv, NULL));
+ }
+
+ return value;
+}
+
+gchar *
+pw_generate (void)
+{
+ gchar *res;
+ gint rv;
+
+ rv = pwquality_generate (get_pwq (), 0, &res);
+
+ if (rv < 0) {
+ g_warning ("Password generation failed: %s\n",
+ pwquality_strerror (NULL, 0, rv, NULL));
+ return NULL;
+ }
+
+ return res;
+}
+
+static const gchar *
+pw_error_hint (gint error)
+{
+ switch (error) {
+ case PWQ_ERROR_SAME_PASSWORD:
+ return C_("Password hint", "The new password needs to be different from the old one.");
+ case PWQ_ERROR_CASE_CHANGES_ONLY:
+ return C_("Password hint", "Try changing some letters and numbers.");
+ case PWQ_ERROR_TOO_SIMILAR:
+ return C_("Password hint", "Try changing the password a bit more.");
+ case PWQ_ERROR_USER_CHECK:
+ return C_("Password hint", "A password without your user name would be stronger.");
+ case PWQ_ERROR_GECOS_CHECK:
+ return C_("Password hint", "Try to avoid using your name in the password.");
+ case PWQ_ERROR_BAD_WORDS:
+ return C_("Password hint", "Try to avoid some of the words included in the password.");
+ case PWQ_ERROR_ROTATED:
+ return C_("Password hint", "Try changing the password a bit more.");
+ case PWQ_ERROR_CRACKLIB_CHECK:
+ return C_("Password hint", "Try to avoid common words.");
+ case PWQ_ERROR_PALINDROME:
+ return C_("Password hint", "Try to avoid reordering existing words.");
+ case PWQ_ERROR_MIN_DIGITS:
+ return C_("Password hint", "Try to use more numbers.");
+ case PWQ_ERROR_MIN_UPPERS:
+ return C_("Password hint", "Try to use more uppercase letters.");
+ case PWQ_ERROR_MIN_LOWERS:
+ return C_("Password hint", "Try to use more lowercase letters.");
+ case PWQ_ERROR_MIN_OTHERS:
+ return C_("Password hint", "Try to use more special characters, like punctuation.");
+ case PWQ_ERROR_MIN_CLASSES:
+ return C_("Password hint", "Try to use a mixture of letters, numbers and punctuation.");
+ case PWQ_ERROR_MAX_CONSECUTIVE:
+ return C_("Password hint", "Try to avoid repeating the same character.");
+ case PWQ_ERROR_MAX_CLASS_REPEAT:
+ return C_("Password hint", "Try to avoid repeating the same type of character: you need to mix up letters, numbers and punctuation.");
+ case PWQ_ERROR_MAX_SEQUENCE:
+ return C_("Password hint", "Try to avoid sequences like 1234 or abcd.");
+ case PWQ_ERROR_MIN_LENGTH:
+ return C_("Password hint", "Password needs to be longer. Try to add more letters, numbers and punctuation.");
+ case PWQ_ERROR_EMPTY_PASSWORD:
+ return C_("Password hint", "Mix uppercase and lowercase and try to use a number or two.");
+ default:
+ return C_("Password hint", "Adding more letters, numbers and punctuation will make the password stronger.");
+ }
+}
+
+gdouble
+pw_strength (const gchar *password,
+ const gchar *old_password,
+ const gchar *username,
+ const gchar **hint,
+ gint *strength_level)
+{
+ gint rv, level, length = 0;
+ gdouble strength = 0.0;
+ void *auxerror;
+
+ rv = pwquality_check (get_pwq (),
+ password, old_password, username,
+ &auxerror);
+
+ if (password != NULL)
+ length = strlen (password);
+
+ strength = CLAMP (0.01 * rv, 0.0, 1.0);
+ if (rv < 0) {
+ level = (length > 0) ? 1 : 0;
+ }
+ else if (strength < 0.50) {
+ level = 2;
+ } else if (strength < 0.75) {
+ level = 3;
+ } else if (strength < 0.90) {
+ level = 4;
+ } else {
+ level = 5;
+ }
+
+ if (length && length < pw_min_length())
+ *hint = pw_error_hint (PWQ_ERROR_MIN_LENGTH);
+ else
+ *hint = pw_error_hint (rv);
+
+ if (strength_level)
+ *strength_level = level;
+
+ return strength;
+}
diff --git a/panels/user-accounts/pw-utils.h b/panels/user-accounts/pw-utils.h
new file mode 100644
index 0000000..d7df491
--- /dev/null
+++ b/panels/user-accounts/pw-utils.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2012 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <glib.h>
+
+gint pw_min_length (void);
+gchar *pw_generate (void);
+gdouble pw_strength (const gchar *password,
+ const gchar *old_password,
+ const gchar *username,
+ const gchar **hint,
+ gint *strength_level);
diff --git a/panels/user-accounts/run-passwd.c b/panels/user-accounts/run-passwd.c
new file mode 100644
index 0000000..56eea9f
--- /dev/null
+++ b/panels/user-accounts/run-passwd.c
@@ -0,0 +1,770 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* run-passwd.c: this file is part of users-admin, a gnome-system-tools frontend
+ * for user administration.
+ *
+ * Copyright (C) 2002 Diego Gonzalez
+ * Copyright (C) 2006 Johannes H. Jensen
+ * Copyright (C) 2010 Milan Bouchet-Valat
+ *
+ * Written by: Diego Gonzalez <diego@pemas.net>
+ * Modified by: Johannes H. Jensen <joh@deworks.net>,
+ * Milan Bouchet-Valat <nalimilan@club.fr>.
+ *
+ * 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, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Most of this code originally comes from gnome-about-me-password.c,
+ * from gnome-control-center.
+ */
+
+#include <config.h>
+#include <glib/gi18n.h>
+
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/wait.h>
+
+#if __sun
+#include <sys/types.h>
+#include <signal.h>
+#endif
+
+#include "run-passwd.h"
+
+/* Passwd states */
+typedef enum {
+ PASSWD_STATE_NONE, /* Passwd is not asking for anything */
+ PASSWD_STATE_AUTH, /* Passwd is asking for our current password */
+ PASSWD_STATE_NEW, /* Passwd is asking for our new password */
+ PASSWD_STATE_RETYPE, /* Passwd is asking for our retyped new password */
+ PASSWD_STATE_DONE, /* Passwd succeeded but has not yet exited */
+ PASSWD_STATE_ERR /* Passwd reported an error but has not yet exited */
+} PasswdState;
+
+struct PasswdHandler {
+ const char *current_password;
+ const char *new_password;
+
+ /* Communication with the passwd program */
+ GPid backend_pid;
+
+ GIOChannel *backend_stdin;
+ GIOChannel *backend_stdout;
+
+ GQueue *backend_stdin_queue; /* Write queue to backend_stdin */
+
+ /* GMainLoop IDs */
+ guint backend_child_watch_id; /* g_child_watch_add (PID) */
+ guint backend_stdout_watch_id; /* g_io_add_watch (stdout) */
+
+ /* State of the passwd program */
+ PasswdState backend_state;
+ gboolean changing_password;
+
+ PasswdCallback auth_cb;
+ gpointer auth_cb_data;
+
+ PasswdCallback chpasswd_cb;
+ gpointer chpasswd_cb_data;
+};
+
+/* Buffer size for backend output */
+#define BUFSIZE 64
+
+
+static GQuark
+passwd_error_quark (void)
+{
+ static GQuark q = 0;
+
+ if (q == 0) {
+ q = g_quark_from_static_string("passwd_error");
+ }
+
+ return q;
+}
+
+/* Error handling */
+#define PASSWD_ERROR (passwd_error_quark ())
+
+
+static void
+stop_passwd (PasswdHandler *passwd_handler);
+
+static void
+free_passwd_resources (PasswdHandler *passwd_handler);
+
+static gboolean
+io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler);
+
+
+/*
+ * Spawning and closing of backend {{
+ */
+
+/* Child watcher */
+static void
+child_watch_cb (GPid pid, gint status, PasswdHandler *passwd_handler)
+{
+ if (WIFEXITED (status)) {
+ if (WEXITSTATUS (status) >= 255) {
+ g_warning ("Child exited unexpectedly");
+ }
+ if (WEXITSTATUS (status) == 0) {
+ if (passwd_handler->backend_state == PASSWD_STATE_RETYPE) {
+ passwd_handler->backend_state = PASSWD_STATE_DONE;
+ if (passwd_handler->chpasswd_cb)
+ passwd_handler->chpasswd_cb (passwd_handler,
+ NULL,
+ passwd_handler->chpasswd_cb_data);
+ }
+ }
+ }
+
+ free_passwd_resources (passwd_handler);
+}
+
+static void
+ignore_sigpipe (gpointer data)
+{
+ signal (SIGPIPE, SIG_IGN);
+}
+
+/* Spawn passwd backend
+ * Returns: TRUE on success, FALSE otherwise and sets error appropriately */
+static gboolean
+spawn_passwd (PasswdHandler *passwd_handler, GError **error)
+{
+ gchar *argv[2];
+ gchar **envp;
+ gint my_stdin, my_stdout, my_stderr;
+
+ argv[0] = "/usr/bin/passwd"; /* Is it safe to rely on a hard-coded path? */
+ argv[1] = NULL;
+
+ envp = g_get_environ ();
+ envp = g_environ_setenv (envp, "LC_ALL", "C", TRUE);
+
+ if (!g_spawn_async_with_pipes (NULL, /* Working directory */
+ argv, /* Argument vector */
+ envp, /* Environment */
+ G_SPAWN_DO_NOT_REAP_CHILD, /* Flags */
+ ignore_sigpipe, /* Child setup */
+ NULL, /* Data to child setup */
+ &passwd_handler->backend_pid, /* PID */
+ &my_stdin, /* Stdin */
+ &my_stdout, /* Stdout */
+ &my_stderr, /* Stderr */
+ error)) { /* GError */
+
+ /* An error occurred */
+ free_passwd_resources (passwd_handler);
+
+ g_strfreev (envp);
+
+ return FALSE;
+ }
+
+ g_strfreev (envp);
+
+ /* 2>&1 */
+ if (dup2 (my_stderr, my_stdout) == -1) {
+ /* Failed! */
+ g_set_error_literal (error,
+ PASSWD_ERROR,
+ PASSWD_ERROR_BACKEND,
+ strerror (errno));
+
+ /* Clean up */
+ stop_passwd (passwd_handler);
+
+ return FALSE;
+ }
+
+ /* Open IO Channels */
+ passwd_handler->backend_stdin = g_io_channel_unix_new (my_stdin);
+ passwd_handler->backend_stdout = g_io_channel_unix_new (my_stdout);
+
+ /* Set raw encoding */
+ /* Set nonblocking mode */
+ if (g_io_channel_set_encoding (passwd_handler->backend_stdin, NULL, error) != G_IO_STATUS_NORMAL ||
+ g_io_channel_set_encoding (passwd_handler->backend_stdout, NULL, error) != G_IO_STATUS_NORMAL ||
+ g_io_channel_set_flags (passwd_handler->backend_stdin, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ||
+ g_io_channel_set_flags (passwd_handler->backend_stdout, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ) {
+
+ /* Clean up */
+ stop_passwd (passwd_handler);
+ return FALSE;
+ }
+
+ /* Turn off buffering */
+ g_io_channel_set_buffered (passwd_handler->backend_stdin, FALSE);
+ g_io_channel_set_buffered (passwd_handler->backend_stdout, FALSE);
+
+ /* Add IO Channel watcher */
+ passwd_handler->backend_stdout_watch_id = g_io_add_watch (passwd_handler->backend_stdout,
+ G_IO_IN | G_IO_PRI,
+ (GIOFunc) io_watch_stdout, passwd_handler);
+
+ /* Add child watcher */
+ passwd_handler->backend_child_watch_id = g_child_watch_add (passwd_handler->backend_pid, (GChildWatchFunc) child_watch_cb, passwd_handler);
+
+ /* Success! */
+
+ return TRUE;
+}
+
+/* Stop passwd backend */
+static void
+stop_passwd (PasswdHandler *passwd_handler)
+{
+ /* This is the standard way of returning from the dialog with passwd.
+ * If we return this way we can safely kill passwd as it has completed
+ * its task.
+ */
+
+ if (passwd_handler->backend_pid != -1) {
+ kill (passwd_handler->backend_pid, 9);
+ }
+
+ /* We must run free_passwd_resources here and not let our child
+ * watcher do it, since it will access invalid memory after the
+ * dialog has been closed and cleaned up.
+ *
+ * If we had more than a single thread we'd need to remove
+ * the child watch before trying to kill the child.
+ */
+ free_passwd_resources (passwd_handler);
+}
+
+/* Clean up passwd resources */
+static void
+free_passwd_resources (PasswdHandler *passwd_handler)
+{
+ GError *error = NULL;
+
+ /* Remove the child watcher */
+ if (passwd_handler->backend_child_watch_id != 0) {
+
+ g_source_remove (passwd_handler->backend_child_watch_id);
+
+ passwd_handler->backend_child_watch_id = 0;
+ }
+
+
+ /* Close IO channels (internal file descriptors are automatically closed) */
+ if (passwd_handler->backend_stdin != NULL) {
+
+ if (g_io_channel_shutdown (passwd_handler->backend_stdin, TRUE, &error) != G_IO_STATUS_NORMAL) {
+ g_warning ("Could not shutdown backend_stdin IO channel: %s", error->message);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ g_io_channel_unref (passwd_handler->backend_stdin);
+ passwd_handler->backend_stdin = NULL;
+ }
+
+ if (passwd_handler->backend_stdout != NULL) {
+
+ if (g_io_channel_shutdown (passwd_handler->backend_stdout, TRUE, &error) != G_IO_STATUS_NORMAL) {
+ g_warning ("Could not shutdown backend_stdout IO channel: %s", error->message);
+ g_error_free (error);
+ error = NULL;
+ }
+
+ g_io_channel_unref (passwd_handler->backend_stdout);
+
+ passwd_handler->backend_stdout = NULL;
+ }
+
+ /* Remove IO watcher */
+ if (passwd_handler->backend_stdout_watch_id != 0) {
+
+ g_source_remove (passwd_handler->backend_stdout_watch_id);
+
+ passwd_handler->backend_stdout_watch_id = 0;
+ }
+
+ /* Close PID */
+ if (passwd_handler->backend_pid != -1) {
+
+ g_spawn_close_pid (passwd_handler->backend_pid);
+
+ passwd_handler->backend_pid = -1;
+ }
+
+ /* Clear backend state */
+ passwd_handler->backend_state = PASSWD_STATE_NONE;
+}
+
+/*
+ * }} Spawning and closing of backend
+ */
+
+/*
+ * Backend communication code {{
+ */
+
+/* Write the first element of queue through channel */
+static void
+io_queue_pop (GQueue *queue, GIOChannel *channel)
+{
+ gchar *buf;
+ gsize bytes_written;
+ GError *error = NULL;
+
+ buf = g_queue_pop_head (queue);
+
+ if (buf != NULL) {
+
+ if (g_io_channel_write_chars (channel, buf, -1, &bytes_written, &error) != G_IO_STATUS_NORMAL) {
+ g_warning ("Could not write queue element \"%s\" to channel: %s", buf, error->message);
+ g_error_free (error);
+ }
+
+ /* Ensure passwords are cleared from memory */
+ memset (buf, 0, strlen (buf));
+ g_free (buf);
+ }
+}
+
+/* Goes through the argument list, checking if one of them occurs in str
+ * Returns: TRUE as soon as an element is found to match, FALSE otherwise */
+static gboolean
+is_string_complete (gchar *str, ...)
+{
+ va_list ap;
+ gchar *arg;
+
+ if (strlen (str) == 0) {
+ return FALSE;
+ }
+
+ va_start (ap, str);
+
+ while ((arg = va_arg (ap, char *)) != NULL) {
+ if (strstr (str, arg) != NULL) {
+ va_end (ap);
+ return TRUE;
+ }
+ }
+
+ va_end (ap);
+
+ return FALSE;
+}
+
+/*
+ * IO watcher for stdout, called whenever there is data to read from the backend.
+ * This is where most of the actual IO handling happens.
+ */
+static gboolean
+io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler)
+{
+ static GString *str = NULL; /* Persistent buffer */
+
+ gchar buf[BUFSIZE]; /* Temporary buffer */
+ gsize bytes_read;
+ GError *gio_error = NULL; /* Error returned by functions */
+ GError *error = NULL; /* Error sent to callbacks */
+
+ gboolean reinit = FALSE;
+
+ /* Initialize buffer */
+ if (str == NULL) {
+ str = g_string_new ("");
+ }
+
+ if (g_io_channel_read_chars (source, buf, BUFSIZE, &bytes_read, &gio_error)
+ != G_IO_STATUS_NORMAL) {
+ g_warning ("IO Channel read error: %s", gio_error->message);
+ g_error_free (gio_error);
+
+ return TRUE;
+ }
+
+ str = g_string_append_len (str, buf, bytes_read);
+
+ /* In which state is the backend? */
+ switch (passwd_handler->backend_state) {
+ case PASSWD_STATE_AUTH:
+ /* Passwd is asking for our current password */
+
+ if (is_string_complete (str->str, "assword: ", "failure", "wrong", "error", NULL)) {
+
+ if (strstr (str->str, "assword: ") != NULL &&
+ strstr (str->str, "incorrect") == NULL &&
+ strstr (str->str, "urrent") == NULL) {
+ /* Authentication successful */
+
+ passwd_handler->backend_state = PASSWD_STATE_NEW;
+
+ /* Trigger callback to update authentication status */
+ if (passwd_handler->auth_cb)
+ passwd_handler->auth_cb (passwd_handler,
+ NULL,
+ passwd_handler->auth_cb_data);
+
+ } else {
+ /* Authentication failed */
+
+ error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED,
+ _("Authentication failed"));
+
+ passwd_handler->changing_password = FALSE;
+
+ /* This error can happen both while authenticating or while changing password:
+ * if chpasswd_cb is set, this means we're already changing password */
+ if (passwd_handler->chpasswd_cb)
+ passwd_handler->chpasswd_cb (passwd_handler,
+ error,
+ passwd_handler->chpasswd_cb_data);
+ else if (passwd_handler->auth_cb)
+ passwd_handler->auth_cb (passwd_handler,
+ error,
+ passwd_handler->auth_cb_data);
+
+ g_error_free (error);
+ }
+
+ reinit = TRUE;
+ }
+ break;
+ case PASSWD_STATE_NEW:
+ /* Passwd is asking for our new password */
+
+ if (is_string_complete (str->str, "assword: ", NULL)) {
+ /* Advance to next state */
+ passwd_handler->backend_state = PASSWD_STATE_RETYPE;
+
+ /* Pop retyped password from queue and into IO channel */
+ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
+
+ reinit = TRUE;
+ }
+ break;
+ case PASSWD_STATE_RETYPE:
+ /* Passwd is asking for our retyped new password */
+
+ if (is_string_complete (str->str,
+ "successfully",
+ "short",
+ "longer",
+ "palindrome",
+ "dictionary",
+ "simple",
+ "simplistic",
+ "similar",
+ "case",
+ "different",
+ "wrapped",
+ "recovered",
+ "recent",
+ "unchanged",
+ "match",
+ "1 numeric or special",
+ "failure",
+ "DIFFERENT",
+ "BAD PASSWORD",
+ NULL)) {
+
+ if (strstr (str->str, "successfully") != NULL) {
+ /* Hooray! */
+
+ passwd_handler->backend_state = PASSWD_STATE_DONE;
+ /* Trigger callback to update status */
+ if (passwd_handler->chpasswd_cb)
+ passwd_handler->chpasswd_cb (passwd_handler,
+ NULL,
+ passwd_handler->chpasswd_cb_data);
+ }
+ else {
+ /* Ohnoes! */
+
+ if (strstr (str->str, "recovered") != NULL) {
+ /* What does this indicate?
+ * "Authentication information cannot be recovered?" from libpam? */
+ error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN,
+ str->str);
+ } else if (strstr (str->str, "short") != NULL ||
+ strstr (str->str, "longer") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The new password is too short"));
+ } else if (strstr (str->str, "palindrome") != NULL ||
+ strstr (str->str, "simple") != NULL ||
+ strstr (str->str, "simplistic") != NULL ||
+ strstr (str->str, "dictionary") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The new password is too simple"));
+ } else if (strstr (str->str, "similar") != NULL ||
+ strstr (str->str, "different") != NULL ||
+ strstr (str->str, "case") != NULL ||
+ strstr (str->str, "wrapped") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The old and new passwords are too similar"));
+ } else if (strstr (str->str, "recent") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The new password has already been used recently."));
+ } else if (strstr (str->str, "1 numeric or special") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The new password must contain numeric or special characters"));
+ } else if (strstr (str->str, "unchanged") != NULL ||
+ strstr (str->str, "match") != NULL) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The old and new passwords are the same"));
+ } else if (strstr (str->str, "failure") != NULL) {
+ /* Authentication failure */
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED,
+ _("Your password has been changed since you initially authenticated!"));
+ }
+ else if (strstr (str->str, "DIFFERENT")) {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED,
+ _("The new password does not contain enough different characters"));
+ }
+ else {
+ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN,
+ _("Unknown error"));
+ }
+
+ /* At this point, passwd might have exited, in which case
+ * child_watch_cb should clean up for us and remove this watcher.
+ * On some error conditions though, passwd just re-prompts us
+ * for our new password. */
+ passwd_handler->backend_state = PASSWD_STATE_ERR;
+
+ passwd_handler->changing_password = FALSE;
+
+ /* Trigger callback to update status */
+ if (passwd_handler->chpasswd_cb)
+ passwd_handler->chpasswd_cb (passwd_handler,
+ error,
+ passwd_handler->chpasswd_cb_data);
+
+ g_error_free (error);
+
+ }
+
+ reinit = TRUE;
+
+ /* child_watch_cb should clean up for us now */
+ }
+ break;
+ case PASSWD_STATE_NONE:
+ /* Passwd is not asking for anything yet */
+ if (is_string_complete (str->str, "assword: ", NULL)) {
+
+ /* If the user does not have a password set,
+ * passwd will immediately ask for the new password,
+ * so skip the AUTH phase */
+ if (is_string_complete (str->str, "new", "New", NULL)) {
+ gchar *pw;
+
+ passwd_handler->backend_state = PASSWD_STATE_NEW;
+
+ /* since passwd didn't ask for our old password
+ * in this case, simply remove it from the queue */
+ pw = g_queue_pop_head (passwd_handler->backend_stdin_queue);
+ g_free (pw);
+
+ /* Pop the IO queue, i.e. send new password */
+ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
+
+ } else {
+
+ passwd_handler->backend_state = PASSWD_STATE_AUTH;
+
+ /* Pop the IO queue, i.e. send current password */
+ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
+ }
+
+ reinit = TRUE;
+ }
+ break;
+ default:
+ /* Passwd has returned an error */
+ reinit = TRUE;
+ break;
+ }
+
+ if (reinit) {
+ g_string_free (str, TRUE);
+ str = NULL;
+ }
+
+ /* Continue calling us */
+ return TRUE;
+}
+
+/*
+ * }} Backend communication code
+ */
+
+/* Adds the current password to the IO queue */
+static void
+authenticate (PasswdHandler *passwd_handler)
+{
+ gchar *s;
+
+ s = g_strdup_printf ("%s\n", passwd_handler->current_password);
+
+ g_queue_push_tail (passwd_handler->backend_stdin_queue, s);
+}
+
+/* Adds the new password twice to the IO queue */
+static void
+update_password (PasswdHandler *passwd_handler)
+{
+ gchar *s;
+
+ s = g_strdup_printf ("%s\n", passwd_handler->new_password);
+
+ g_queue_push_tail (passwd_handler->backend_stdin_queue, s);
+ /* We need to allocate new space because io_queue_pop() g_free()s
+ * every element of the queue after it's done */
+ g_queue_push_tail (passwd_handler->backend_stdin_queue, g_strdup (s));
+}
+
+
+PasswdHandler *
+passwd_init (void)
+{
+ PasswdHandler *passwd_handler;
+
+ passwd_handler = g_new0 (PasswdHandler, 1);
+
+ /* Initialize backend_pid. -1 means the backend is not running */
+ passwd_handler->backend_pid = -1;
+
+ /* Initialize IO Channels */
+ passwd_handler->backend_stdin = NULL;
+ passwd_handler->backend_stdout = NULL;
+
+ /* Initialize write queue */
+ passwd_handler->backend_stdin_queue = g_queue_new ();
+
+ /* Initialize watchers */
+ passwd_handler->backend_child_watch_id = 0;
+ passwd_handler->backend_stdout_watch_id = 0;
+
+ /* Initialize backend state */
+ passwd_handler->backend_state = PASSWD_STATE_NONE;
+ passwd_handler->changing_password = FALSE;
+
+ return passwd_handler;
+}
+
+void
+passwd_destroy (PasswdHandler *passwd_handler)
+{
+ g_queue_free (passwd_handler->backend_stdin_queue);
+ stop_passwd (passwd_handler);
+ g_free (passwd_handler);
+}
+
+void
+passwd_authenticate (PasswdHandler *passwd_handler,
+ const char *current_password,
+ PasswdCallback cb,
+ const gpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Don't stop if we've already started changing password */
+ if (passwd_handler->changing_password)
+ return;
+
+ /* Clear data from possible previous attempts to change password */
+ passwd_handler->new_password = NULL;
+ passwd_handler->chpasswd_cb = NULL;
+ passwd_handler->chpasswd_cb_data = NULL;
+ g_queue_foreach (passwd_handler->backend_stdin_queue, (GFunc) g_free, NULL);
+ g_queue_clear (passwd_handler->backend_stdin_queue);
+
+ passwd_handler->current_password = current_password;
+ passwd_handler->auth_cb = cb;
+ passwd_handler->auth_cb_data = user_data;
+
+ /* Spawn backend */
+ stop_passwd (passwd_handler);
+
+ if (!spawn_passwd (passwd_handler, &error)) {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+
+ return;
+ }
+
+ authenticate (passwd_handler);
+
+ /* Our IO watcher should now handle the rest */
+}
+
+gboolean
+passwd_change_password (PasswdHandler *passwd_handler,
+ const char *new_password,
+ PasswdCallback cb,
+ const gpointer user_data)
+{
+ GError *error = NULL;
+
+ passwd_handler->changing_password = TRUE;
+
+ passwd_handler->new_password = new_password;
+ passwd_handler->chpasswd_cb = cb;
+ passwd_handler->chpasswd_cb_data = user_data;
+
+ /* Stop passwd if an error occurred and it is still running */
+ if (passwd_handler->backend_state == PASSWD_STATE_ERR) {
+
+ /* Stop passwd, free resources */
+ stop_passwd (passwd_handler);
+ }
+
+ /* Check that the backend is still running, or that an error
+ * has occurred but it has not yet exited */
+ if (passwd_handler->backend_pid == -1) {
+ /* If it is not, re-run authentication */
+
+ /* Spawn backend */
+ stop_passwd (passwd_handler);
+
+ if (!spawn_passwd (passwd_handler, &error)) {
+ g_warning ("%s", error->message);
+ g_error_free (error);
+
+ return FALSE;
+ }
+
+ /* Add current and new passwords to queue */
+ authenticate (passwd_handler);
+ update_password (passwd_handler);
+ } else {
+ /* Only add new passwords to queue */
+ update_password (passwd_handler);
+ }
+
+ /* Pop new password through the backend.
+ * If user has no password, popping the queue would output current
+ * password, while 'passwd' is waiting for the new one. So wait for
+ * io_watch_stdout() to remove current password from the queue,
+ * and output the new one for us.
+ */
+ if (passwd_handler->current_password)
+ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin);
+
+ /* Our IO watcher should now handle the rest */
+
+ return TRUE;
+}
diff --git a/panels/user-accounts/run-passwd.h b/panels/user-accounts/run-passwd.h
new file mode 100644
index 0000000..c0362bd
--- /dev/null
+++ b/panels/user-accounts/run-passwd.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* run-passwd.h: this file is part of users-admin, a gnome-system-tools frontend
+ * for user administration.
+ *
+ * Copyright (C) 2010 Milan Bouchet-Valat
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Milan Bouchet-Valat <nalimilan@club.fr>
+ */
+
+#pragma once
+
+struct PasswdHandler;
+
+typedef struct PasswdHandler PasswdHandler;
+
+typedef void (*PasswdCallback) (PasswdHandler *passwd_handler, GError *error, const gpointer user_data);
+
+/* Error codes */
+typedef enum {
+ PASSWD_ERROR_REJECTED, /* New password is not secure enough */
+ PASSWD_ERROR_AUTH_FAILED, /* Wrong old password, or PAM failure */
+ PASSWD_ERROR_REAUTH_FAILED, /* Password has changed since first authentication */
+ PASSWD_ERROR_BACKEND, /* Backend error */
+ PASSWD_ERROR_UNKNOWN /* General error */
+} PasswdError;
+
+
+PasswdHandler *passwd_init (void);
+
+void passwd_destroy (PasswdHandler *passwd_handler);
+
+void passwd_authenticate (PasswdHandler *passwd_handler,
+ const char *current_password,
+ PasswdCallback cb,
+ gpointer user_data);
+
+gboolean passwd_change_password (PasswdHandler *passwd_handler,
+ const char *new_password,
+ PasswdCallback cb,
+ const gpointer user_data);
+
diff --git a/panels/user-accounts/user-accounts.gresource.xml b/panels/user-accounts/user-accounts.gresource.xml
new file mode 100644
index 0000000..fcd1a7f
--- /dev/null
+++ b/panels/user-accounts/user-accounts.gresource.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/user-accounts">
+ <file preprocess="xml-stripblanks">cc-add-user-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-avatar-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-carousel.ui</file>
+ <file preprocess="xml-stripblanks">cc-login-history-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-password-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-user-panel.ui</file>
+ <file preprocess="xml-stripblanks">cc-fingerprint-dialog.ui</file>
+ <file alias="join-dialog.ui" preprocess="xml-stripblanks">data/join-dialog.ui</file>
+ <file alias="user-accounts-dialog.css">data/user-accounts-dialog.css</file>
+ <file alias="carousel.css">data/carousel.css</file>
+ <file alias="cc-fingerprint-dialog.css">data/cc-fingerprint-dialog.css</file>
+ </gresource>
+
+ <gresource prefix="/org/gnome/ControlCenter/icons/scalable/status">
+ <file preprocess="xml-stripblanks" alias="fingerprint-detection-complete-symbolic.svg">data/icons/fingerprint-detection-complete-symbolic.svg</file>
+ <file preprocess="xml-stripblanks" alias="fingerprint-detection-symbolic.svg">data/icons/fingerprint-detection-symbolic.svg</file>
+ <file preprocess="xml-stripblanks" alias="fingerprint-detection-warning-symbolic.svg">data/icons/fingerprint-detection-warning-symbolic.svg</file>
+ </gresource>
+</gresources>
diff --git a/panels/user-accounts/user-utils.c b/panels/user-accounts/user-utils.c
new file mode 100644
index 0000000..3c17dfe
--- /dev/null
+++ b/panels/user-accounts/user-utils.c
@@ -0,0 +1,772 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <limits.h>
+#include <unistd.h>
+#include <utmpx.h>
+#include <pwd.h>
+
+#ifdef __FreeBSD__
+#include <sysexits.h>
+#endif
+
+#include <gio/gio.h>
+#include <gio/gunixoutputstream.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include "user-utils.h"
+
+#define IMAGE_SIZE 512
+
+typedef struct {
+ gchar *text;
+ gchar *placeholder_str;
+ GIcon *icon;
+ gunichar placeholder;
+ gulong query_id;
+} IconShapeData;
+
+static IconShapeData *
+icon_shape_data_new (const gchar *text,
+ const gchar *placeholder,
+ GIcon *icon)
+{
+ IconShapeData *data;
+
+ data = g_new0 (IconShapeData, 1);
+
+ data->text = g_strdup (text);
+ data->placeholder_str = g_strdup (placeholder);
+ data->placeholder = g_utf8_get_char_validated (placeholder, -1);
+ data->icon = g_object_ref (icon);
+
+ return data;
+}
+
+static void
+icon_shape_data_free (gpointer user_data)
+{
+ IconShapeData *data = user_data;
+
+ g_free (data->text);
+ g_free (data->placeholder_str);
+ g_object_unref (data->icon);
+ g_free (data);
+}
+
+static void
+icon_shape_renderer (cairo_t *cr,
+ PangoAttrShape *attr,
+ gboolean do_path,
+ gpointer user_data)
+{
+ IconShapeData *data = user_data;
+ gdouble x, y;
+
+ cairo_get_current_point (cr, &x, &y);
+ if (GPOINTER_TO_UINT (attr->data) == data->placeholder) {
+ gdouble ascent;
+ gdouble height;
+ GdkPixbuf *pixbuf;
+ GtkIconInfo *info;
+
+ ascent = pango_units_to_double (attr->ink_rect.y);
+ height = pango_units_to_double (attr->ink_rect.height);
+ info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (),
+ data->icon,
+ (gint)height,
+ GTK_ICON_LOOKUP_FORCE_SIZE | GTK_ICON_LOOKUP_USE_BUILTIN);
+ pixbuf = gtk_icon_info_load_icon (info, NULL);
+ g_object_unref (info);
+
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+ cairo_reset_clip (cr);
+ gdk_cairo_set_source_pixbuf (cr, pixbuf, x, y + ascent);
+ cairo_paint (cr);
+ g_object_unref (pixbuf);
+ }
+}
+
+static PangoAttrList *
+create_shape_attr_list_for_layout (PangoLayout *layout,
+ IconShapeData *data)
+{
+ PangoAttrList *attrs;
+ PangoFontMetrics *metrics;
+ gint ascent, descent;
+ PangoRectangle ink_rect, logical_rect;
+ const gchar *p;
+ const gchar *text;
+ gint placeholder_len;
+
+ /* Get font metrics and prepare fancy shape size */
+ metrics = pango_context_get_metrics (pango_layout_get_context (layout),
+ pango_layout_get_font_description (layout),
+ NULL);
+ ascent = pango_font_metrics_get_ascent (metrics);
+ descent = pango_font_metrics_get_descent (metrics);
+ pango_font_metrics_unref (metrics);
+
+ logical_rect.x = 0;
+ logical_rect.y = - ascent;
+ logical_rect.width = ascent + descent;
+ logical_rect.height = ascent + descent;
+
+ ink_rect = logical_rect;
+
+ attrs = pango_attr_list_new ();
+ text = pango_layout_get_text (layout);
+ placeholder_len = strlen (data->placeholder_str);
+ for (p = text; (p = strstr (p, data->placeholder_str)); p += placeholder_len) {
+ PangoAttribute *attr;
+
+ attr = pango_attr_shape_new_with_data (&ink_rect,
+ &logical_rect,
+ GUINT_TO_POINTER (g_utf8_get_char (p)),
+ NULL, NULL);
+
+ attr->start_index = p - text;
+ attr->end_index = attr->start_index + placeholder_len;
+
+ pango_attr_list_insert (attrs, attr);
+ }
+
+ return attrs;
+}
+
+static gboolean
+query_unlock_tooltip (GtkWidget *widget,
+ gint x,
+ gint y,
+ gboolean keyboard_tooltip,
+ GtkTooltip *tooltip)
+{
+ GtkWidget *label;
+ PangoLayout *layout;
+ PangoAttrList *attrs;
+ IconShapeData *data;
+
+ data = g_object_get_data (G_OBJECT (widget), "icon-shape-data");
+ label = g_object_get_data (G_OBJECT (widget), "tooltip-label");
+ if (label == NULL) {
+ label = gtk_label_new (data->text);
+ g_object_ref_sink (label);
+ g_object_set_data_full (G_OBJECT (widget),
+ "tooltip-label", label, g_object_unref);
+ }
+
+ layout = gtk_label_get_layout (GTK_LABEL (label));
+ pango_cairo_context_set_shape_renderer (pango_layout_get_context (layout),
+ icon_shape_renderer,
+ data, NULL);
+
+ attrs = create_shape_attr_list_for_layout (layout, data);
+ gtk_label_set_attributes (GTK_LABEL (label), attrs);
+ pango_attr_list_unref (attrs);
+
+ gtk_tooltip_set_custom (tooltip, label);
+
+ return TRUE;
+}
+
+void
+setup_tooltip_with_embedded_icon (GtkWidget *widget,
+ const gchar *text,
+ const gchar *placeholder,
+ GIcon *icon)
+{
+ IconShapeData *data;
+
+ data = g_object_get_data (G_OBJECT (widget), "icon-shape-data");
+ if (data) {
+ gtk_widget_set_has_tooltip (widget, FALSE);
+ g_signal_handler_disconnect (widget, data->query_id);
+ g_object_set_data (G_OBJECT (widget), "icon-shape-data", NULL);
+ g_object_set_data (G_OBJECT (widget), "tooltip-label", NULL);
+ }
+
+ if (!placeholder) {
+ gtk_widget_set_tooltip_text (widget, text);
+ return;
+ }
+
+ data = icon_shape_data_new (text, placeholder, icon);
+ g_object_set_data_full (G_OBJECT (widget),
+ "icon-shape-data",
+ data,
+ icon_shape_data_free);
+
+ gtk_widget_set_has_tooltip (widget, TRUE);
+ data->query_id = g_signal_connect (widget, "query-tooltip",
+ G_CALLBACK (query_unlock_tooltip), NULL);
+
+}
+
+gboolean
+show_tooltip_now (GtkWidget *widget,
+ GdkEvent *event)
+{
+ GtkSettings *settings;
+ gint timeout;
+
+ settings = gtk_widget_get_settings (widget);
+
+ g_object_get (settings, "gtk-tooltip-timeout", &timeout, NULL);
+ g_object_set (settings, "gtk-tooltip-timeout", 1, NULL);
+ gtk_tooltip_trigger_tooltip_query (gtk_widget_get_display (widget));
+ g_object_set (settings, "gtk-tooltip-timeout", timeout, NULL);
+
+ return FALSE;
+}
+
+static gboolean
+query_tooltip (GtkWidget *widget,
+ gint x,
+ gint y,
+ gboolean keyboard_mode,
+ GtkTooltip *tooltip,
+ gpointer user_data)
+{
+ gchar *tip;
+
+ if (GTK_ENTRY_ICON_SECONDARY == gtk_entry_get_icon_at_pos (GTK_ENTRY (widget), x, y)) {
+ tip = gtk_entry_get_icon_tooltip_text (GTK_ENTRY (widget),
+ GTK_ENTRY_ICON_SECONDARY);
+ gtk_tooltip_set_text (tooltip, tip);
+ g_free (tip);
+
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+}
+
+static void
+icon_released (GtkEntry *entry)
+{
+ GtkSettings *settings;
+ gint timeout;
+
+ settings = gtk_widget_get_settings (GTK_WIDGET (entry));
+
+ g_object_get (settings, "gtk-tooltip-timeout", &timeout, NULL);
+ g_object_set (settings, "gtk-tooltip-timeout", 1, NULL);
+ gtk_tooltip_trigger_tooltip_query (gtk_widget_get_display (GTK_WIDGET (entry)));
+ g_object_set (settings, "gtk-tooltip-timeout", timeout, NULL);
+}
+
+
+
+void
+set_entry_validation_error (GtkEntry *entry,
+ const gchar *text)
+{
+ g_object_set (entry, "caps-lock-warning", FALSE, NULL);
+ gtk_entry_set_icon_from_icon_name (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ "dialog-warning-symbolic");
+ gtk_entry_set_icon_activatable (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ TRUE);
+ g_signal_connect (entry, "icon-release",
+ G_CALLBACK (icon_released), NULL);
+ g_signal_connect (entry, "query-tooltip",
+ G_CALLBACK (query_tooltip), NULL);
+ g_object_set (entry, "has-tooltip", TRUE, NULL);
+ gtk_entry_set_icon_tooltip_text (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ text);
+}
+
+void
+set_entry_generation_icon (GtkEntry *entry)
+{
+ g_object_set (entry, "caps-lock-warning", FALSE, NULL);
+ gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, "system-run-symbolic");
+ gtk_entry_set_icon_activatable (entry, GTK_ENTRY_ICON_SECONDARY, TRUE);
+}
+
+void
+set_entry_validation_checkmark (GtkEntry *entry)
+{
+ g_object_set (entry, "caps-lock-warning", FALSE, NULL);
+ gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, "object-select-symbolic");
+ gtk_entry_set_icon_activatable (entry, GTK_ENTRY_ICON_SECONDARY, FALSE);
+}
+
+void
+clear_entry_validation_error (GtkEntry *entry)
+{
+ gboolean warning;
+
+ g_object_get (entry, "caps-lock-warning", &warning, NULL);
+
+ if (warning)
+ return;
+
+ g_object_set (entry, "has-tooltip", FALSE, NULL);
+ gtk_entry_set_icon_from_pixbuf (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ NULL);
+ g_object_set (entry, "caps-lock-warning", TRUE, NULL);
+}
+
+/* Taken from defines.h in shadow-utils. On Linux, this value is much smaller
+ * than the sysconf limit LOGIN_NAME_MAX, and values larger than this will
+ * result in failure when running useradd. We could check UT_NAMESIZE instead,
+ * but that is nonstandard. Better to use POSIX utmpx.
+ */
+gsize
+get_username_max_length (void)
+{
+ return sizeof (((struct utmpx *)NULL)->ut_user);
+}
+
+gboolean
+is_username_used (const gchar *username)
+{
+ struct passwd *pwent;
+
+ if (username == NULL || username[0] == '\0') {
+ return FALSE;
+ }
+
+ pwent = getpwnam (username);
+
+ return pwent != NULL;
+}
+
+gboolean
+is_valid_name (const gchar *name)
+{
+ gboolean is_empty = TRUE;
+ const gchar *c;
+
+ /* Valid names must contain:
+ * 1) at least one character.
+ * 2) at least one non-"space" character.
+ */
+ for (c = name; *c; c++) {
+ gunichar unichar;
+
+ unichar = g_utf8_get_char_validated (c, -1);
+
+ /* Partial UTF-8 sequence or end of string */
+ if (unichar == (gunichar) -1 || unichar == (gunichar) -2)
+ break;
+
+ /* Check for non-space character */
+ if (!g_unichar_isspace (unichar)) {
+ is_empty = FALSE;
+ break;
+ }
+ }
+
+ return !is_empty;
+}
+
+typedef struct {
+ gchar *username;
+ gchar *tip;
+} isValidUsernameData;
+
+static void
+is_valid_username_data_free (isValidUsernameData *data)
+{
+ g_free (data->username);
+ g_free (data->tip);
+ g_free (data);
+}
+
+#ifdef __FreeBSD__
+/* Taken from pw(8) man page. */
+#define E_SUCCESS EX_OK
+#define E_BAD_ARG EX_DATAERR
+#define E_NOTFOUND EX_NOUSER
+#else
+/* Taken from usermod.c in shadow-utils. */
+#define E_SUCCESS 0
+#define E_BAD_ARG 3
+#define E_NOTFOUND 6
+#endif
+
+static void
+is_valid_username_child_watch_cb (GPid pid,
+ gint status,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+ isValidUsernameData *data = g_task_get_task_data (task);
+ GError *error = NULL;
+ gboolean valid = FALSE;
+ const gchar *tip = NULL;
+
+ if (WIFEXITED (status)) {
+ switch (WEXITSTATUS (status)) {
+ case E_NOTFOUND:
+ valid = TRUE;
+ break;
+ case E_BAD_ARG:
+ tip = _("The username should usually only consist of lower case letters from a-z, digits and the following characters: - _");
+ valid = FALSE;
+ break;
+ case E_SUCCESS:
+ tip = _("Sorry, that user name isn’t available. Please try another.");
+ valid = FALSE;
+ break;
+ }
+ }
+
+ if (valid || tip != NULL) {
+ data->tip = g_strdup (tip);
+ g_task_return_boolean (task, valid);
+ }
+ else {
+ g_spawn_check_exit_status (status, &error);
+ g_task_return_error (task, error);
+ }
+
+ g_spawn_close_pid (pid);
+ g_object_unref (task);
+}
+
+void
+is_valid_username_async (const gchar *username,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer callback_data)
+{
+ GTask *task;
+ isValidUsernameData *data;
+ gchar *argv[6];
+ GPid pid;
+ GError *error = NULL;
+
+ task = g_task_new (NULL, cancellable, callback, callback_data);
+ g_task_set_source_tag (task, is_valid_username_async);
+
+ data = g_new0 (isValidUsernameData, 1);
+ data->username = g_strdup (username);
+ g_task_set_task_data (task, data, (GDestroyNotify) is_valid_username_data_free);
+
+ if (username == NULL || username[0] == '\0') {
+ g_task_return_boolean (task, FALSE);
+ g_object_unref (task);
+
+ return;
+ }
+ else if (strlen (username) > get_username_max_length ()) {
+ data->tip = g_strdup (_("The username is too long."));
+ g_task_return_boolean (task, FALSE);
+ g_object_unref (task);
+
+ return;
+ }
+
+#ifdef __FreeBSD__
+ /* Abuse "pw usershow -n <name>" in the same way as the code below. We
+ * don't use "pw usermod -n <name> -N -l <newname>" here because it has
+ * a special case for "root" to reject changes to the root user.
+ */
+ argv[0] = "pw";
+ argv[1] = "usershow";
+ argv[2] = "-n";
+ argv[3] = data->username;
+ argv[4] = NULL;
+#else
+ /* "usermod --login" is meant to be used to change a username, but the
+ * exit codes can be safely abused to check the validity of username.
+ * However, the current "usermod" implementation may change in the
+ * future, so it would be nice to have some official way for this
+ * instead of relying on the current "--login" implementation.
+ */
+ argv[0] = "/usr/sbin/usermod";
+ argv[1] = "--login";
+ argv[2] = data->username;
+ argv[3] = "--";
+ argv[4] = data->username;
+ argv[5] = NULL;
+#endif
+
+ if (!g_spawn_async (NULL, argv, NULL,
+ G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD |
+ G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL,
+ NULL, NULL, &pid, &error)) {
+ g_task_return_error (task, error);
+ g_object_unref (task);
+
+ return;
+ }
+
+ g_child_watch_add (pid, (GChildWatchFunc) is_valid_username_child_watch_cb, task);
+}
+
+gboolean
+is_valid_username_finish (GAsyncResult *result,
+ gchar **tip,
+ gchar **username,
+ GError **error)
+{
+ GTask *task;
+ isValidUsernameData *data;
+
+ g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
+
+ task = G_TASK (result);
+ data = g_task_get_task_data (task);
+
+ if (tip != NULL) {
+ *tip = g_steal_pointer (&data->tip);
+ if (*tip == NULL)
+ *tip = g_strdup (_("This will be used to name your home folder and can’t be changed."));
+ }
+
+ if (username != NULL)
+ *username = g_steal_pointer (&data->username);
+
+ return g_task_propagate_boolean (task, error);
+}
+
+GdkPixbuf *
+round_image (GdkPixbuf *pixbuf)
+{
+ GdkPixbuf *dest = NULL;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ gint size;
+
+ size = gdk_pixbuf_get_width (pixbuf);
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
+ cr = cairo_create (surface);
+
+ /* Clip a circle */
+ cairo_arc (cr, size/2, size/2, size/2, 0, 2 * G_PI);
+ cairo_clip (cr);
+ cairo_new_path (cr);
+
+ gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0);
+ cairo_paint (cr);
+
+ dest = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size);
+ cairo_surface_destroy (surface);
+ cairo_destroy (cr);
+
+ return dest;
+}
+
+static gchar *
+extract_initials_from_name (const gchar *name)
+{
+ GString *initials;
+ g_autofree gchar *p = NULL;
+ g_autofree gchar *normalized = NULL;
+ gunichar unichar;
+ gpointer q = NULL;
+
+ g_return_val_if_fail (name != NULL, NULL);
+
+ p = g_utf8_strup (name, -1);
+ normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE);
+ if (normalized == NULL) {
+ return NULL;
+ }
+
+ initials = g_string_new ("");
+
+ unichar = g_utf8_get_char (normalized);
+ g_string_append_unichar (initials, unichar);
+
+ q = g_utf8_strrchr (normalized, -1, ' ');
+ if (q != NULL && g_utf8_next_char (q) != NULL) {
+ q = g_utf8_next_char (q);
+
+ unichar = g_utf8_get_char (q);
+ g_string_append_unichar (initials, unichar);
+ }
+
+ return g_string_free (initials, FALSE);
+}
+
+static GdkRGBA
+get_color_for_name (const gchar *name)
+{
+ // https://gitlab.gnome.org/Community/Design/HIG-app-icons/blob/master/GNOME%20HIG.gpl
+ static gdouble gnome_color_palette[][3] = {
+ { 98, 160, 234 },
+ { 53, 132, 228 },
+ { 28, 113, 216 },
+ { 26, 95, 180 },
+ { 87, 227, 137 },
+ { 51, 209, 122 },
+ { 46, 194, 126 },
+ { 38, 162, 105 },
+ { 248, 228, 92 },
+ { 246, 211, 45 },
+ { 245, 194, 17 },
+ { 229, 165, 10 },
+ { 255, 163, 72 },
+ { 255, 120, 0 },
+ { 230, 97, 0 },
+ { 198, 70, 0 },
+ { 237, 51, 59 },
+ { 224, 27, 36 },
+ { 192, 28, 40 },
+ { 165, 29, 45 },
+ { 192, 97, 203 },
+ { 163, 71, 186 },
+ { 129, 61, 156 },
+ { 97, 53, 131 },
+ { 181, 131, 90 },
+ { 152, 106, 68 },
+ { 134, 94, 60 },
+ { 99, 69, 44 }
+ };
+
+ GdkRGBA color = { 255, 255, 255, 1.0 };
+ guint hash;
+ gint number_of_colors;
+ gint idx;
+
+ if (name == NULL || strlen (name) == 0)
+ return color;
+
+ hash = g_str_hash (name);
+ number_of_colors = G_N_ELEMENTS (gnome_color_palette);
+ idx = hash % number_of_colors;
+
+ color.red = gnome_color_palette[idx][0];
+ color.green = gnome_color_palette[idx][1];
+ color.blue = gnome_color_palette[idx][2];
+
+ return color;
+}
+
+static cairo_surface_t *
+generate_user_picture (const gchar *name, gint size)
+{
+ PangoFontDescription *font_desc;
+ g_autofree gchar *initials = extract_initials_from_name (name);
+ g_autofree gchar *font = g_strdup_printf ("Sans %d", (int)ceil (size / 2.5));
+ PangoLayout *layout;
+ GdkRGBA color = get_color_for_name (name);
+ cairo_surface_t *surface;
+ gint width, height;
+ cairo_t *cr;
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
+ size,
+ size);
+ cr = cairo_create (surface);
+ cairo_rectangle (cr, 0, 0, size, size);
+ cairo_set_source_rgb (cr, color.red/255.0, color.green/255.0, color.blue/255.0);
+ cairo_fill (cr);
+
+ /* Draw the initials on top */
+ cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+ layout = pango_cairo_create_layout (cr);
+ pango_layout_set_text (layout, initials, -1);
+ font_desc = pango_font_description_from_string (font);
+ pango_layout_set_font_description (layout, font_desc);
+ pango_font_description_free (font_desc);
+
+ pango_layout_get_size (layout, &width, &height);
+ cairo_translate (cr, size/2, size/2);
+ cairo_move_to (cr, - ((double)width / PANGO_SCALE)/2, - ((double)height/PANGO_SCALE)/2);
+ pango_cairo_show_layout (cr, layout);
+ cairo_destroy (cr);
+
+ return surface;
+}
+
+void
+set_user_icon_data (ActUser *user,
+ GdkPixbuf *pixbuf)
+{
+ gchar *path;
+ gint fd;
+ GOutputStream *stream;
+ GError *error;
+
+ path = g_build_filename (g_get_tmp_dir (), "gnome-control-center-user-icon-XXXXXX", NULL);
+ fd = g_mkstemp (path);
+
+ if (fd == -1) {
+ g_warning ("failed to create temporary file for image data");
+ g_free (path);
+ return;
+ }
+
+ stream = g_unix_output_stream_new (fd, TRUE);
+
+ error = NULL;
+ if (!gdk_pixbuf_save_to_stream (pixbuf, stream, "png", NULL, &error, NULL)) {
+ g_warning ("failed to save image: %s", error->message);
+ g_error_free (error);
+ g_object_unref (stream);
+ return;
+ }
+
+ g_object_unref (stream);
+
+ act_user_set_icon_file (user, path);
+
+ /* if we ever make the dbus call async, the g_remove call needs
+ * to wait for its completion
+ */
+ g_remove (path);
+
+ g_free (path);
+}
+
+GdkPixbuf *
+generate_default_avatar (ActUser *user, gint size)
+{
+ const gchar *name;
+ GdkPixbuf *pixbuf = NULL;
+ cairo_surface_t *surface;
+
+ name = act_user_get_real_name (user);
+ if (name == NULL)
+ name = "";
+ surface = generate_user_picture (name, size);
+
+ pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size);
+ cairo_surface_destroy (surface);
+
+ return pixbuf;
+}
+
+void
+set_default_avatar (ActUser *user)
+{
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+
+ pixbuf = generate_default_avatar (user, IMAGE_SIZE);
+
+ set_user_icon_data (user, pixbuf);
+}
diff --git a/panels/user-accounts/user-utils.h b/panels/user-accounts/user-utils.h
new file mode 100644
index 0000000..09c6cdd
--- /dev/null
+++ b/panels/user-accounts/user-utils.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright 2009-2010 Red Hat, 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 <http://www.gnu.org/licenses/>.
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <act/act.h>
+
+G_BEGIN_DECLS
+
+void setup_tooltip_with_embedded_icon (GtkWidget *widget,
+ const gchar *text,
+ const gchar *placeholder,
+ GIcon *icon);
+gboolean show_tooltip_now (GtkWidget *widget,
+ GdkEvent *event);
+
+void set_entry_generation_icon (GtkEntry *entry);
+void set_entry_validation_checkmark (GtkEntry *entry);
+void set_entry_validation_error (GtkEntry *entry,
+ const gchar *text);
+void clear_entry_validation_error (GtkEntry *entry);
+
+gsize get_username_max_length (void);
+gboolean is_username_used (const gchar *username);
+gboolean is_valid_name (const gchar *name);
+void is_valid_username_async (const gchar *username,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer callback_data);
+gboolean is_valid_username_finish (GAsyncResult *result,
+ gchar **tip,
+ gchar **username,
+ GError **error);
+GdkPixbuf *round_image (GdkPixbuf *pixbuf);
+GdkPixbuf *generate_default_avatar (ActUser *user,
+ gint size);
+void set_default_avatar (ActUser *user);
+void set_user_icon_data (ActUser *user,
+ GdkPixbuf *pixbuf);
+
+G_END_DECLS