/* -*- 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"

#define CC_FPRINTD_NAME "net.reactivated.Fprint"

/* Translate fprintd strings */
#define TR(s) dgettext ("fprintd", s)
#include "fingerprint-strings.h"

typedef enum {
  DIALOG_STATE_NONE                   = 0,
  DIALOG_STATE_DEVICES_LISTING        = (1 << 0),
  DIALOG_STATE_DEVICE_CLAIMING        = (1 << 1),
  DIALOG_STATE_DEVICE_CLAIMED         = (1 << 2),
  DIALOG_STATE_DEVICE_PRINTS_LISTING  = (1 << 3),
  DIALOG_STATE_DEVICE_RELEASING       = (1 << 4),
  DIALOG_STATE_DEVICE_ENROLL_STARTING = (1 << 5),
  DIALOG_STATE_DEVICE_ENROLLING       = (1 << 6),
  DIALOG_STATE_DEVICE_ENROLL_STOPPING = (1 << 7),
  DIALOG_STATE_DEVICE_DELETING        = (1 << 8),

  DIALOG_STATE_IDLE = DIALOG_STATE_DEVICE_CLAIMED | DIALOG_STATE_DEVICE_ENROLLING,
} DialogState;

struct _CcFingerprintDialog
{
  GtkWindow parent_instance;

  GtkButton      *back_button;
  GtkButton      *cancel_button;
  GtkButton      *delete_prints_button;
  GtkButton      *done_button;
  GtkBox         *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;
  GtkPopover     *add_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;
  DialogState           dialog_state;
  CcFprintdDevice      *device;
  gulong                device_signal_id;
  gulong                device_name_owner_id;
  GCancellable         *cancellable;
  GStrv                 enrolled_fingers;
  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 gboolean
update_dialog_state (CcFingerprintDialog *self,
                     DialogState         state)
{
  if (self->dialog_state == state)
    return FALSE;

  self->dialog_state = state;

  if (self->dialog_state == DIALOG_STATE_NONE ||
      self->dialog_state == (self->dialog_state & DIALOG_STATE_IDLE))
    {
      gtk_spinner_stop (self->spinner);
    }
  else
    {
      gtk_spinner_start (self->spinner);
    }

  return TRUE;
}

static gboolean
add_dialog_state (CcFingerprintDialog *self,
                  DialogState          state)
{
  return update_dialog_state (self, (self->dialog_state | state));
}

static gboolean
remove_dialog_state (CcFingerprintDialog *self,
                     DialogState          state)
{
  return update_dialog_state (self, (self->dialog_state & ~state));
}

typedef struct
{
  CcFingerprintDialog *dialog;
  DialogState          state;
} DialogStateRemover;

static DialogStateRemover *
auto_state_remover (CcFingerprintDialog *self,
                    DialogState          state)
{
  DialogStateRemover *state_remover;

  state_remover = g_new0 (DialogStateRemover, 1);
  state_remover->dialog = g_object_ref (self);
  state_remover->state = state;

  return state_remover;
}

static void
auto_state_remover_cleanup (DialogStateRemover *state_remover)
{
  remove_dialog_state (state_remover->dialog, state_remover->state);
  g_clear_object (&state_remover->dialog);
  g_free (state_remover);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (DialogStateRemover, auto_state_remover_cleanup);

static const char *
dbus_error_to_human (CcFingerprintDialog *self,
                     GError              *error)
{
  g_autofree char *dbus_error = g_dbus_error_get_remote_error (error);

  if (dbus_error == NULL)
    { /* Fallback to generic */ }
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.ClaimDevice"))
    return _("the device needs to be claimed to perform this action");
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse"))
    return _("the device is already claimed by another process");
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.PermissionDenied"))
    return _("you do not have permission to perform the action");
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints"))
    return _("no prints have been enrolled");
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoActionInProgress"))
    { /* Fallback to generic */ }
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.InvalidFingername"))
    { /* Fallback to generic */ }
  else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.Internal"))
    { /* Fallback to generic */ }

  if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING)
    return _("Failed to communicate with the device during enrollment");

  if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED ||
      self->dialog_state & DIALOG_STATE_DEVICE_CLAIMING)
    return _("Failed to communicate with the fingerprint reader");

  return _("Failed to communicate with the fingerprint daemon");
}

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_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 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)
{
  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);

  if (icon_widget_type == GTK_TYPE_IMAGE)
    icon_widget = image;
  else
    icon_widget = g_object_new (icon_widget_type, NULL);

  if (g_type_is_a (icon_widget_type, GTK_TYPE_MENU_BUTTON))
    {
      gtk_menu_button_set_child (GTK_MENU_BUTTON (icon_widget), image);
      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_box_append (GTK_BOX (box), icon_widget);

  gtk_widget_add_css_class (icon_widget, "circular");

  label = gtk_label_new_with_mnemonic (label_text);
  gtk_box_append (GTK_BOX (box), label);

  gtk_widget_add_css_class (box, "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_flow_box_child_set_child (GTK_FLOW_BOX_CHILD (flowbox_child), box);

  g_object_set_data (G_OBJECT (flowbox_child), "button", button);
  g_object_set_data (G_OBJECT (flowbox_child), "icon",
                     GTK_IMAGE (gtk_menu_button_get_child (GTK_MENU_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 GList *
get_container_children (GtkWidget *container)
{
  GtkWidget *child;
  GList *list = NULL;
    
  child = gtk_widget_get_first_child (container);
  while (child) {
      GtkWidget *next = gtk_widget_get_next_sibling (child);

      list = g_list_append (list, child);

      child = next;
  }

  return list;
}

static void
update_prints_to_add_visibility (CcFingerprintDialog *self)
{
  g_autoptr(GList) print_buttons = NULL;
  GList *l;
  guint i;

  print_buttons = get_container_children (GTK_WIDGET (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;
  g_autoptr(DialogStateRemover) state_remover = 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;

  state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_PRINTS_LISTING);

  gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), TRUE);

  if (self->dialog_state & DIALOG_STATE_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_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints"))
        {
          g_autofree char *error_message = NULL;

          error_message = g_strdup_printf (_("Failed to list fingerprints: %s"),
                                           dbus_error_to_human (self, error));
          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));

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_PRINTS_LISTING))
    return;

  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;

      error_message = g_strdup_printf (_("Failed to delete saved fingerprints: %s"),
                                       dbus_error_to_human (self, error));
      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->dialog_state & DIALOG_STATE_DEVICE_CLAIMED);

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_DELETING))
    return;

  gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), FALSE);

  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 = get_container_children (GTK_WIDGET (self->devices_list));

  return devices_rows && devices_rows->next;
}

static void
set_enroll_result_message (CcFingerprintDialog *self,
                           EnrollState          enroll_state,
                           const char          *message)
{
  const char *icon_name;
  guint i;

  g_return_if_fail (enroll_state >= 0 && enroll_state < N_ENROLL_STATES);

  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_widget_remove_css_class (self->enroll_result_icon, ENROLL_STATE_CLASSES[i]);

  gtk_widget_add_css_class (self->enroll_result_icon, ENROLL_STATE_CLASSES[enroll_state]);

  gtk_image_set_from_icon_name (self->enroll_result_image, icon_name);
  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->dialog_state & DIALOG_STATE_DEVICE_ENROLLING);

  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 = 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");
              remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED |
                                         DIALOG_STATE_DEVICE_ENROLLING);
            }
          else if (g_str_equal (result, "enroll-data-full"))
            {
              message = _("Fingerprint device storage is full");
            }
          else if (g_str_equal (result, "enroll-duplicate"))
            {
              message = _("Fingerprint is duplicate");
            }
          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;
  g_autoptr(DialogStateRemover) state_remover = 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;

  state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLL_STARTING);

  if (error)
    {
      g_autofree char *error_message = NULL;

      remove_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING);

      error_message = g_strdup_printf (_("Failed to start enrollment: %s"),
                                       dbus_error_to_human (self, error));
      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;
  g_autoptr(DialogStateRemover) state_remover = 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;

  state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLLING |
                                            DIALOG_STATE_DEVICE_ENROLL_STOPPING);
  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;

      error_message = g_strdup_printf (_("Failed to stop enrollment: %s"),
                                       dbus_error_to_human (self, error));
      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->dialog_state & DIALOG_STATE_DEVICE_ENROLLING);

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLL_STOPPING))
    return;

  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);

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING |
                               DIALOG_STATE_DEVICE_ENROLL_STARTING))
    return;

  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_editable_set_text (GTK_EDITABLE (self->enroll_print_entry), finger_name);

  cc_fprintd_device_call_enroll_start (self->device, finger_id, self->cancellable,
                                       enroll_start_cb, self);
}

static void
populate_enrollment_view (CcFingerprintDialog *self)
{
  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_box_prepend (GTK_BOX (self->enroll_print_bin), self->enroll_result_icon);

  gtk_widget_add_css_class (self->enroll_result_icon,  "enroll-status");
}

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");
  g_signal_emit_by_name (GTK_MENU_BUTTON (selected_button), "activate");
}

static void
on_enroll_cb (CcFingerprintDialog *self,
              GtkMenuButton       *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_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_box_prepend (GTK_BOX (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;
  guint i;

  g_return_if_fail (!GTK_IS_WIDGET (self->add_print_icon));

  for (i = 0; i < N_VALID_FINGERS; ++i)
    {
      GtkWidget *flowbox_child;
      GtkWidget *popover;
      GtkWidget *reenroll_button;

      flowbox_child = fingerprint_menu_button ("fingerprint-detection-symbolic",
                                               get_finger_name (FINGER_IDS[i]));

      button = g_object_get_data (G_OBJECT (flowbox_child), "button");

      popover = gtk_popover_new ();
      reenroll_button = gtk_button_new ();
      gtk_button_set_use_underline (GTK_BUTTON (reenroll_button), TRUE);
      gtk_button_set_label (GTK_BUTTON (reenroll_button), _("_Re-enroll this finger…"));
      g_object_set_data (G_OBJECT (reenroll_button), "finger-id",
                         (gpointer) FINGER_IDS[i]);
      g_signal_connect_object (reenroll_button, "clicked", G_CALLBACK (on_enroll_cb), self, G_CONNECT_SWAPPED);
      gtk_popover_set_child (GTK_POPOVER (popover), reenroll_button);

      gtk_menu_button_set_popover (GTK_MENU_BUTTON (button),
                                   popover);
      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);
  gtk_widget_add_css_class (self->add_print_icon, "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_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;

      error_message = g_strdup_printf (_("Failed to release fingerprint device %s: %s"),
                                       cc_fprintd_device_get_name (fprintd_device),
                                       dbus_error_to_human (self, error));
      g_warning ("Releasing device %s failed: %s",
                 cc_fprintd_device_get_name (self->device), error->message);

      notify_error (self, error_message);
      return;
    }

  remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED);
}

static void
release_device (CcFingerprintDialog *self)
{
  if (!self->device || !(self->dialog_state & DIALOG_STATE_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->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)
        {
          disconnect_device_signals (self);

          if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING)
            {
              set_enroll_result_message (self, ENROLL_STATE_ERROR,
                                         C_("Fingerprint enroll state",
                                            "Problem Reading Device"));
            }

          remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED);
          claim_device (self);
        }
    }
}

static void
claim_device_cb (GObject      *object,
                 GAsyncResult *res,
                 gpointer      user_data)
{
  g_autoptr(GError) error = NULL;
  g_autoptr(DialogStateRemover) state_remover = 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;

  state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_CLAIMING);

  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_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse") &&
          (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED))
         return;

      error_message = g_strdup_printf (_("Failed to claim fingerprint device %s: %s"),
                                       cc_fprintd_device_get_name (self->device),
                                       dbus_error_to_human (self, error));
      g_warning ("Claiming device %s failed: %s",
                 cc_fprintd_device_get_name (self->device), error->message);
      notify_error (self, error_message);
      return;
    }

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED))
    return;

  gtk_widget_set_sensitive (self->prints_manager, 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->dialog_state & DIALOG_STATE_DEVICE_CLAIMED));

  if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMING))
    return;

  user = cc_fingerprint_manager_get_user (self->manager);
  gtk_widget_set_sensitive (self->prints_manager, FALSE);

  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));

  adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), TRUE);
  adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (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->dialog_state & DIALOG_STATE_DEVICE_CLAIMED))
        claim_device (self);
    }
  else if (visible_child == self->enrollment_view)
    {
      adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), FALSE);
      adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (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_display (gdk_display_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(DialogStateRemover) state_remover = 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);

  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
    return;

  state_remover = auto_state_remover (self, DIALOG_STATE_DEVICES_LISTING);

  if (fprintd_devices == NULL)
    {
      if (error)
        {
          g_autofree char *error_message = NULL;

          error_message = g_strdup_printf (_("Failed to get fingerprint devices: %s"),
                                           dbus_error_to_human (self, error));
          g_warning ("Retrieving fingerprint devices failed: %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");

  add_dialog_state (self, DIALOG_STATE_DEVICES_LISTING);
  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->dialog_state & DIALOG_STATE_DEVICE_ENROLLING)
    {
      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->dialog_state & DIALOG_STATE_DEVICE_ENROLLING);

  g_debug ("Completing enroll operation");
  enroll_stop (self);
}

static gboolean
cc_fingerprint_dialog_close_request (GtkWindow *window)
{
  CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (window);

  cc_fingerprint_manager_update_state (self->manager, NULL, NULL);

  g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove);

  if (self->device && (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED))
    {
      disconnect_device_signals (self);

      if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING)
        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);

  return GTK_WINDOW_CLASS (cc_fingerprint_dialog_parent_class)->close_request (window);
}

static void
cc_fingerprint_dialog_class_init (CcFingerprintDialogClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GtkWindowClass *window_class = GTK_WINDOW_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->get_property = cc_fingerprint_dialog_get_property;
  object_class->set_property = cc_fingerprint_dialog_set_property;

  window_class->close_request = cc_fingerprint_dialog_close_request;

  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, 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, on_print_activated_cb);
  gtk_widget_class_bind_template_callback (widget_class, select_device_row);
}
