summaryrefslogtreecommitdiffstats
path: root/gnome-initial-setup
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:47:04 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:47:04 +0000
commite05fb7b3e36c052baf0dd607ddeb22c0a2b5cbde (patch)
tree7a27d70e96502edf2b5576d3ca403f1b8512f55b /gnome-initial-setup
parentInitial commit. (diff)
downloadgnome-initial-setup-e05fb7b3e36c052baf0dd607ddeb22c0a2b5cbde.tar.xz
gnome-initial-setup-e05fb7b3e36c052baf0dd607ddeb22c0a2b5cbde.zip
Adding upstream version 43.2.upstream/43.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gnome-initial-setup')
-rw-r--r--gnome-initial-setup/cc-common-language.c316
-rw-r--r--gnome-initial-setup/cc-common-language.h37
-rw-r--r--gnome-initial-setup/gis-assistant.c514
-rw-r--r--gnome-initial-setup/gis-assistant.gresource.xml8
-rw-r--r--gnome-initial-setup/gis-assistant.h48
-rw-r--r--gnome-initial-setup/gis-assistant.ui75
-rw-r--r--gnome-initial-setup/gis-driver.c936
-rw-r--r--gnome-initial-setup/gis-driver.h128
-rw-r--r--gnome-initial-setup/gis-keyring.c104
-rw-r--r--gnome-initial-setup/gis-keyring.h35
-rw-r--r--gnome-initial-setup/gis-page-header.c203
-rw-r--r--gnome-initial-setup/gis-page-header.css13
-rw-r--r--gnome-initial-setup/gis-page-header.h36
-rw-r--r--gnome-initial-setup/gis-page-header.ui38
-rw-r--r--gnome-initial-setup/gis-page.c422
-rw-r--r--gnome-initial-setup/gis-page.h92
-rw-r--r--gnome-initial-setup/gis-pkexec.c59
-rw-r--r--gnome-initial-setup/gis-pkexec.h41
-rw-r--r--gnome-initial-setup/gnome-initial-setup-copy-worker.c98
-rw-r--r--gnome-initial-setup/gnome-initial-setup.c387
-rw-r--r--gnome-initial-setup/gnome-initial-setup.h43
-rw-r--r--gnome-initial-setup/meson.build89
-rw-r--r--gnome-initial-setup/pages/account/account.gresource.xml9
-rw-r--r--gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui51
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-enterprise.c863
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-enterprise.h42
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-enterprise.ui280
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-local.c721
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-local.h39
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page-local.ui145
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page.c309
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page.h33
-rw-r--r--gnome-initial-setup/pages/account/gis-account-page.ui56
-rw-r--r--gnome-initial-setup/pages/account/gis-account-pages.c32
-rw-r--r--gnome-initial-setup/pages/account/gis-account-pages.h33
-rw-r--r--gnome-initial-setup/pages/account/meson.build33
-rw-r--r--gnome-initial-setup/pages/account/org.freedesktop.realmd.xml666
-rw-r--r--gnome-initial-setup/pages/account/um-photo-dialog.c314
-rw-r--r--gnome-initial-setup/pages/account/um-photo-dialog.h48
-rw-r--r--gnome-initial-setup/pages/account/um-realm-manager.c878
-rw-r--r--gnome-initial-setup/pages/account/um-realm-manager.h108
-rw-r--r--gnome-initial-setup/pages/account/um-utils.c568
-rw-r--r--gnome-initial-setup/pages/account/um-utils.h50
-rw-r--r--gnome-initial-setup/pages/goa/gis-goa-page.c606
-rw-r--r--gnome-initial-setup/pages/goa/gis-goa-page.css6
-rw-r--r--gnome-initial-setup/pages/goa/gis-goa-page.h55
-rw-r--r--gnome-initial-setup/pages/goa/gis-goa-page.ui54
-rw-r--r--gnome-initial-setup/pages/goa/gnome-initial-setup-goa-helper.c505
-rw-r--r--gnome-initial-setup/pages/goa/goa.gresource.xml8
-rw-r--r--gnome-initial-setup/pages/goa/meson.build38
-rw-r--r--gnome-initial-setup/pages/keyboard/cc-ibus-utils.c43
-rw-r--r--gnome-initial-setup/pages/keyboard/cc-ibus-utils.h29
-rw-r--r--gnome-initial-setup/pages/keyboard/cc-input-chooser.c883
-rw-r--r--gnome-initial-setup/pages/keyboard/cc-input-chooser.h65
-rw-r--r--gnome-initial-setup/pages/keyboard/gis-keyboard-page.c541
-rw-r--r--gnome-initial-setup/pages/keyboard/gis-keyboard-page.h70
-rw-r--r--gnome-initial-setup/pages/keyboard/gis-keyboard-page.ui36
-rw-r--r--gnome-initial-setup/pages/keyboard/input-chooser.ui22
-rw-r--r--gnome-initial-setup/pages/keyboard/keyboard.gresource.xml7
-rw-r--r--gnome-initial-setup/pages/keyboard/meson.build14
-rw-r--r--gnome-initial-setup/pages/language/cc-language-chooser.c597
-rw-r--r--gnome-initial-setup/pages/language/cc-language-chooser.h63
-rw-r--r--gnome-initial-setup/pages/language/cc-util.c105
-rw-r--r--gnome-initial-setup/pages/language/cc-util.h28
-rw-r--r--gnome-initial-setup/pages/language/gis-language-page.c312
-rw-r--r--gnome-initial-setup/pages/language/gis-language-page.h57
-rw-r--r--gnome-initial-setup/pages/language/gis-language-page.ui48
-rw-r--r--gnome-initial-setup/pages/language/gis-welcome-widget.c242
-rw-r--r--gnome-initial-setup/pages/language/gis-welcome-widget.h56
-rw-r--r--gnome-initial-setup/pages/language/gis-welcome-widget.ui14
-rw-r--r--gnome-initial-setup/pages/language/language-chooser.ui22
-rw-r--r--gnome-initial-setup/pages/language/language.gresource.xml10
-rw-r--r--gnome-initial-setup/pages/language/meson.build16
-rw-r--r--gnome-initial-setup/pages/meson.build21
-rw-r--r--gnome-initial-setup/pages/network/gis-network-page.c828
-rw-r--r--gnome-initial-setup/pages/network/gis-network-page.h55
-rw-r--r--gnome-initial-setup/pages/network/gis-network-page.ui99
-rw-r--r--gnome-initial-setup/pages/network/meson.build12
-rw-r--r--gnome-initial-setup/pages/network/network-dialogs.c515
-rw-r--r--gnome-initial-setup/pages/network/network-dialogs.h41
-rw-r--r--gnome-initial-setup/pages/network/network.gresource.xml6
-rw-r--r--gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.c246
-rw-r--r--gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.h38
-rw-r--r--gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.ui31
-rw-r--r--gnome-initial-setup/pages/parental-controls/meson.build10
-rw-r--r--gnome-initial-setup/pages/parental-controls/parental-controls.gresource.xml6
-rw-r--r--gnome-initial-setup/pages/password/account-resources.h7
-rw-r--r--gnome-initial-setup/pages/password/gis-password-page.c521
-rw-r--r--gnome-initial-setup/pages/password/gis-password-page.css16
-rw-r--r--gnome-initial-setup/pages/password/gis-password-page.h58
-rw-r--r--gnome-initial-setup/pages/password/gis-password-page.ui143
-rw-r--r--gnome-initial-setup/pages/password/meson.build13
-rw-r--r--gnome-initial-setup/pages/password/password.gresource.xml7
-rw-r--r--gnome-initial-setup/pages/password/pw-utils.c164
-rw-r--r--gnome-initial-setup/pages/password/pw-utils.h29
-rw-r--r--gnome-initial-setup/pages/privacy/gis-privacy-page.c272
-rw-r--r--gnome-initial-setup/pages/privacy/gis-privacy-page.h57
-rw-r--r--gnome-initial-setup/pages/privacy/gis-privacy-page.ui79
-rw-r--r--gnome-initial-setup/pages/privacy/meson.build10
-rw-r--r--gnome-initial-setup/pages/privacy/privacy.gresource.xml6
-rw-r--r--gnome-initial-setup/pages/software/gis-software-page.c179
-rw-r--r--gnome-initial-setup/pages/software/gis-software-page.h58
-rw-r--r--gnome-initial-setup/pages/software/gis-software-page.ui41
-rw-r--r--gnome-initial-setup/pages/software/gis-software-symbolic.svg1
-rw-r--r--gnome-initial-setup/pages/software/meson.build10
-rw-r--r--gnome-initial-setup/pages/software/software.gresource.xml9
-rw-r--r--gnome-initial-setup/pages/summary/gis-summary-page.c298
-rw-r--r--gnome-initial-setup/pages/summary/gis-summary-page.h55
-rw-r--r--gnome-initial-setup/pages/summary/gis-summary-page.ui23
-rw-r--r--gnome-initial-setup/pages/summary/meson.build10
-rw-r--r--gnome-initial-setup/pages/summary/ready-to-go.svg1
-rw-r--r--gnome-initial-setup/pages/summary/summary.gresource.xml8
-rw-r--r--gnome-initial-setup/pages/timezone/backward128
-rw-r--r--gnome-initial-setup/pages/timezone/cc-timezone-map.c530
-rw-r--r--gnome-initial-setup/pages/timezone/cc-timezone-map.h39
-rw-r--r--gnome-initial-setup/pages/timezone/data/bg.pngbin0 -> 85309 bytes
-rw-r--r--gnome-initial-setup/pages/timezone/data/bg_dim.pngbin0 -> 62521 bytes
-rw-r--r--gnome-initial-setup/pages/timezone/data/pin.pngbin0 -> 447 bytes
-rw-r--r--gnome-initial-setup/pages/timezone/datetime.gresource.xml9
-rw-r--r--gnome-initial-setup/pages/timezone/gis-bubble-widget.c141
-rw-r--r--gnome-initial-setup/pages/timezone/gis-bubble-widget.css8
-rw-r--r--gnome-initial-setup/pages/timezone/gis-bubble-widget.h53
-rw-r--r--gnome-initial-setup/pages/timezone/gis-bubble-widget.ui33
-rw-r--r--gnome-initial-setup/pages/timezone/gis-location-entry.c871
-rw-r--r--gnome-initial-setup/pages/timezone/gis-location-entry.h46
-rw-r--r--gnome-initial-setup/pages/timezone/gis-timezone-page.c539
-rw-r--r--gnome-initial-setup/pages/timezone/gis-timezone-page.h57
-rw-r--r--gnome-initial-setup/pages/timezone/gis-timezone-page.ui59
-rw-r--r--gnome-initial-setup/pages/timezone/meson.build45
-rw-r--r--gnome-initial-setup/pages/timezone/test-gis-location-entry.c54
-rw-r--r--gnome-initial-setup/pages/timezone/timedated1-interface.xml28
-rw-r--r--gnome-initial-setup/pages/timezone/timezone.gresource.xml8
-rw-r--r--gnome-initial-setup/pages/timezone/tz.c492
-rw-r--r--gnome-initial-setup/pages/timezone/tz.h90
-rw-r--r--gnome-initial-setup/pages/welcome/gis-welcome-page.c132
-rw-r--r--gnome-initial-setup/pages/welcome/gis-welcome-page.h37
-rw-r--r--gnome-initial-setup/pages/welcome/gis-welcome-page.ui63
-rw-r--r--gnome-initial-setup/pages/welcome/initial-setup-welcome.svg1816
-rw-r--r--gnome-initial-setup/pages/welcome/meson.build10
-rw-r--r--gnome-initial-setup/pages/welcome/welcome.gresource.xml7
140 files changed, 22830 insertions, 0 deletions
diff --git a/gnome-initial-setup/cc-common-language.c b/gnome-initial-setup/cc-common-language.c
new file mode 100644
index 0000000..16dd28f
--- /dev/null
+++ b/gnome-initial-setup/cc-common-language.c
@@ -0,0 +1,316 @@
+/* -*- 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 <locale.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include <fontconfig/fontconfig.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#include "cc-common-language.h"
+
+static char *get_lang_for_user_object_path (const char *path);
+
+static char *current_language;
+
+gboolean
+cc_common_language_has_font (const gchar *locale)
+{
+ const FcCharSet *charset;
+ FcPattern *pattern;
+ FcObjectSet *object_set;
+ FcFontSet *font_set;
+ gchar *language_code;
+ gboolean is_displayable;
+
+ is_displayable = FALSE;
+ pattern = NULL;
+ object_set = NULL;
+ font_set = NULL;
+
+ if (!gnome_parse_locale (locale, &language_code, NULL, NULL, NULL))
+ return FALSE;
+
+ charset = FcLangGetCharSet ((FcChar8 *) language_code);
+ if (!charset) {
+ /* fontconfig does not know about this language */
+ is_displayable = TRUE;
+ }
+ else {
+ /* see if any fonts support rendering it */
+ pattern = FcPatternBuild (NULL, FC_LANG, FcTypeString, language_code, NULL);
+
+ if (pattern == NULL)
+ goto done;
+
+ object_set = FcObjectSetCreate ();
+
+ if (object_set == NULL)
+ goto done;
+
+ font_set = FcFontList (NULL, pattern, object_set);
+
+ if (font_set == NULL)
+ goto done;
+
+ is_displayable = (font_set->nfont > 0);
+ }
+
+ done:
+ if (font_set != NULL)
+ FcFontSetDestroy (font_set);
+
+ if (object_set != NULL)
+ FcObjectSetDestroy (object_set);
+
+ if (pattern != NULL)
+ FcPatternDestroy (pattern);
+
+ g_free (language_code);
+
+ return is_displayable;
+}
+
+gchar *
+cc_common_language_get_current_language (void)
+{
+ g_assert (current_language != NULL);
+ return g_strdup (current_language);
+}
+
+void
+cc_common_language_set_current_language (const char *locale)
+{
+ g_clear_pointer (&current_language, g_free);
+ current_language = gnome_normalize_locale (locale);
+}
+
+static gboolean
+user_language_has_translations (const char *locale)
+{
+ char *name, *language_code, *territory_code;
+ gboolean ret;
+
+ gnome_parse_locale (locale,
+ &language_code,
+ &territory_code,
+ NULL, NULL);
+ name = g_strdup_printf ("%s%s%s",
+ language_code,
+ territory_code != NULL? "_" : "",
+ territory_code != NULL? territory_code : "");
+ g_free (language_code);
+ g_free (territory_code);
+ ret = gnome_language_has_translations (name);
+ g_free (name);
+
+ return ret;
+}
+
+static char *
+get_lang_for_user_object_path (const char *path)
+{
+ GError *error = NULL;
+ GDBusProxy *user;
+ GVariant *props;
+ char *lang;
+
+ user = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.Accounts",
+ path,
+ "org.freedesktop.Accounts.User",
+ NULL,
+ &error);
+ if (user == NULL) {
+ g_warning ("Failed to get proxy for user '%s': %s",
+ path, error->message);
+ g_error_free (error);
+ return NULL;
+ }
+
+ lang = NULL;
+ props = g_dbus_proxy_get_cached_property (user, "Language");
+ if (props != NULL) {
+ lang = g_variant_dup_string (props, NULL);
+ g_variant_unref (props);
+ }
+
+ g_object_unref (user);
+ return lang;
+}
+
+static void
+add_other_users_language (GHashTable *ht)
+{
+ GVariant *variant;
+ GVariantIter *vi;
+ GError *error = NULL;
+ const char *str;
+ GDBusProxy *proxy;
+
+ proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.Accounts",
+ "/org/freedesktop/Accounts",
+ "org.freedesktop.Accounts",
+ NULL,
+ NULL);
+
+ if (proxy == NULL)
+ return;
+
+ variant = g_dbus_proxy_call_sync (proxy,
+ "ListCachedUsers",
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (variant == NULL) {
+ g_warning ("Failed to list existing users: %s", error->message);
+ g_error_free (error);
+ g_object_unref (proxy);
+ return;
+ }
+ g_variant_get (variant, "(ao)", &vi);
+ while (g_variant_iter_loop (vi, "o", &str)) {
+ char *lang;
+ char *name;
+ char *language;
+
+ lang = get_lang_for_user_object_path (str);
+ if (lang != NULL && *lang != '\0' &&
+ cc_common_language_has_font (lang) &&
+ user_language_has_translations (lang)) {
+ name = gnome_normalize_locale (lang);
+ if (!g_hash_table_lookup (ht, name)) {
+ language = gnome_get_language_from_locale (name, NULL);
+ g_hash_table_insert (ht, name, language);
+ }
+ else {
+ g_free (name);
+ }
+ }
+ g_free (lang);
+ }
+ g_variant_iter_free (vi);
+ g_variant_unref (variant);
+
+ g_object_unref (proxy);
+}
+
+/*
+ * Note that @lang needs to be formatted like the locale strings
+ * returned by gnome_get_all_locales().
+ */
+static void
+insert_language (GHashTable *ht,
+ const char *lang)
+{
+ locale_t locale;
+ char *label_own_lang;
+ char *label_current_lang;
+ char *label_untranslated;
+ char *key;
+
+ locale = newlocale (LC_MESSAGES_MASK, lang, (locale_t) 0);
+ if (locale == (locale_t) 0) {
+ g_debug ("%s: Failed to create locale %s", G_STRFUNC, lang);
+ return;
+ }
+ freelocale (locale);
+
+
+ key = g_strdup (lang);
+
+ label_own_lang = gnome_get_language_from_locale (key, key);
+ label_current_lang = gnome_get_language_from_locale (key, current_language);
+ label_untranslated = gnome_get_language_from_locale (key, "C");
+
+ /* We don't have a translation for the label in
+ * its own language? */
+ if (label_own_lang == NULL || g_strcmp0 (label_own_lang, label_untranslated) == 0) {
+ if (g_strcmp0 (label_current_lang, label_untranslated) == 0)
+ g_hash_table_insert (ht, key, g_strdup (label_untranslated));
+ else
+ g_hash_table_insert (ht, key, g_strdup (label_current_lang));
+ } else {
+ g_hash_table_insert (ht, key, g_strdup (label_own_lang));
+ }
+
+ g_free (label_own_lang);
+ g_free (label_current_lang);
+ g_free (label_untranslated);
+}
+
+static void
+insert_user_languages (GHashTable *ht)
+{
+ g_autofree char *name = NULL;
+
+ /* Add the languages used by other users on the system */
+ add_other_users_language (ht);
+
+ /* Add current locale */
+ name = cc_common_language_get_current_language ();
+ if (g_hash_table_lookup (ht, name) == NULL) {
+ insert_language (ht, name);
+ }
+}
+
+GHashTable *
+cc_common_language_get_initial_languages (void)
+{
+ GHashTable *ht;
+
+ ht = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ insert_language (ht, "en_US.UTF-8");
+#if 0
+ /* Having 9 languages in the list initially makes the window
+ * too high. With 8 languages, we end up exactly 768 pixels
+ * high. Sadly, that means we can't affort to show English
+ * twice.
+ */
+ insert_language (ht, "en_GB.UTF-8");
+#endif
+ insert_language (ht, "de_DE.UTF-8");
+ insert_language (ht, "fr_FR.UTF-8");
+ insert_language (ht, "es_ES.UTF-8");
+ insert_language (ht, "zh_CN.UTF-8");
+ insert_language (ht, "ja_JP.UTF-8");
+ insert_language (ht, "ru_RU.UTF-8");
+ insert_language (ht, "ar_EG.UTF-8");
+
+ insert_user_languages (ht);
+
+ return ht;
+}
diff --git a/gnome-initial-setup/cc-common-language.h b/gnome-initial-setup/cc-common-language.h
new file mode 100644
index 0000000..49ead3a
--- /dev/null
+++ b/gnome-initial-setup/cc-common-language.h
@@ -0,0 +1,37 @@
+/* -*- 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __CC_COMMON_LANGUAGE_H__
+#define __CC_COMMON_LANGUAGE_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+gboolean cc_common_language_has_font (const gchar *locale);
+void cc_common_language_set_current_language (const gchar *locale);
+gchar *cc_common_language_get_current_language (void);
+GHashTable *cc_common_language_get_initial_languages (void);
+
+G_END_DECLS
+
+#endif
diff --git a/gnome-initial-setup/gis-assistant.c b/gnome-initial-setup/gis-assistant.c
new file mode 100644
index 0000000..8b62f25
--- /dev/null
+++ b/gnome-initial-setup/gis-assistant.c
@@ -0,0 +1,514 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* -*- encoding: utf8 -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gis-assistant.h"
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_LAST,
+};
+
+enum {
+ PAGE_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+static GParamSpec *obj_props[PROP_LAST];
+
+struct _GisAssistant
+{
+ GtkBox parent_instance;
+
+ GtkWidget *forward;
+ GtkWidget *accept;
+ GtkWidget *skip;
+ GtkWidget *back;
+ GtkWidget *cancel;
+
+ GtkWidget *spinner;
+ GtkWidget *titlebar;
+ GtkWidget *title;
+ GtkWidget *stack;
+
+ GList *pages;
+ GisPage *current_page;
+};
+
+G_DEFINE_TYPE (GisAssistant, gis_assistant, GTK_TYPE_BOX)
+
+static void
+visible_child_changed (GisAssistant *assistant)
+{
+ g_signal_emit (assistant, signals[PAGE_CHANGED], 0);
+}
+
+static void
+switch_to (GisAssistant *assistant,
+ GisPage *page)
+{
+ g_return_if_fail (page != NULL);
+
+ gtk_stack_set_visible_child (GTK_STACK (assistant->stack), GTK_WIDGET (page));
+}
+
+static inline gboolean
+should_show_page (GisPage *page)
+{
+ return gtk_widget_get_visible (GTK_WIDGET (page));
+}
+
+static GisPage *
+find_next_page (GisAssistant *self,
+ GisPage *page)
+{
+ GList *l = g_list_find (self->pages, page);
+
+ g_return_val_if_fail (l != NULL, NULL);
+
+ /* We need the next page */
+ l = l->next;
+
+ for (; l != NULL; l = l->next)
+ {
+ GisPage *page = GIS_PAGE (l->data);
+
+ if (should_show_page (page))
+ return page;
+ }
+
+ return NULL;
+}
+
+static void
+switch_to_next_page (GisAssistant *assistant)
+{
+ switch_to (assistant, find_next_page (assistant, assistant->current_page));
+}
+
+static void
+on_apply_done (GisPage *page,
+ gboolean valid,
+ gpointer user_data)
+{
+ GisAssistant *assistant = GIS_ASSISTANT (user_data);
+
+ if (valid)
+ switch_to_next_page (assistant);
+}
+
+void
+gis_assistant_next_page (GisAssistant *assistant)
+{
+ if (assistant->current_page)
+ gis_page_apply_begin (assistant->current_page, on_apply_done, assistant);
+ else
+ switch_to_next_page (assistant);
+}
+
+static GisPage *
+find_prev_page (GisAssistant *self,
+ GisPage *page)
+{
+ GList *l = g_list_find (self->pages, page);
+
+ g_return_val_if_fail (l != NULL, NULL);
+
+ /* We need the previous page */
+ l = l->prev;
+
+ for (; l != NULL; l = l->prev)
+ {
+ GisPage *page = GIS_PAGE (l->data);
+
+ if (should_show_page (page))
+ return page;
+ }
+
+ return NULL;
+}
+
+void
+gis_assistant_previous_page (GisAssistant *assistant)
+{
+ g_return_if_fail (assistant->current_page != NULL);
+ switch_to (assistant, find_prev_page (assistant, assistant->current_page));
+}
+
+static void
+set_suggested_action_sensitive (GtkWidget *widget,
+ gboolean sensitive)
+{
+ gtk_widget_set_sensitive (widget, sensitive);
+}
+
+static void
+set_navigation_button (GisAssistant *assistant,
+ GtkWidget *widget)
+{
+ gtk_widget_set_visible (assistant->forward, (widget == assistant->forward));
+ gtk_widget_set_visible (assistant->accept, (widget == assistant->accept));
+ gtk_widget_set_visible (assistant->skip, (widget == assistant->skip));
+}
+
+void
+update_navigation_buttons (GisAssistant *assistant)
+{
+ GisPage *page = assistant->current_page;
+ GList *l;
+ gboolean is_last_page;
+
+ if (page == NULL)
+ return;
+
+ l = g_list_find (assistant->pages, page);
+
+ is_last_page = (l->next == NULL);
+
+ if (is_last_page)
+ {
+ gtk_widget_hide (assistant->back);
+ gtk_widget_hide (assistant->forward);
+ gtk_widget_hide (assistant->skip);
+ gtk_widget_hide (assistant->cancel);
+ gtk_widget_hide (assistant->accept);
+ }
+ else
+ {
+ gboolean is_first_page;
+ GtkWidget *next_widget;
+
+ is_first_page = (l->prev == NULL);
+ gtk_widget_set_visible (assistant->back, !is_first_page);
+
+ if (gis_page_get_needs_accept (page))
+ next_widget = assistant->accept;
+ else
+ next_widget = assistant->forward;
+
+ if (gis_page_get_complete (page)) {
+ set_suggested_action_sensitive (next_widget, TRUE);
+ set_navigation_button (assistant, next_widget);
+ } else if (gis_page_get_skippable (page)) {
+ set_navigation_button (assistant, assistant->skip);
+ } else {
+ set_suggested_action_sensitive (next_widget, FALSE);
+ set_navigation_button (assistant, next_widget);
+ }
+
+ if (gis_page_get_has_forward (page)) {
+ gtk_widget_hide (next_widget);
+ }
+ }
+}
+
+static void
+update_applying_state (GisAssistant *assistant)
+{
+ gboolean applying = FALSE;
+ gboolean is_first_page = FALSE;
+
+ if (assistant->current_page)
+ {
+ applying = gis_page_get_applying (assistant->current_page);
+ is_first_page = assistant->pages->data == assistant->current_page;
+ }
+ gtk_widget_set_sensitive (assistant->forward, !applying);
+ gtk_widget_set_visible (assistant->back, !applying && !is_first_page);
+ gtk_widget_set_visible (assistant->cancel, applying);
+ gtk_widget_set_visible (assistant->spinner, applying);
+
+ if (applying)
+ gtk_spinner_start (GTK_SPINNER (assistant->spinner));
+ else
+ gtk_spinner_stop (GTK_SPINNER (assistant->spinner));
+}
+
+static void
+update_titlebar (GisAssistant *assistant)
+{
+ gtk_label_set_label (GTK_LABEL (assistant->title),
+ gis_assistant_get_title (assistant));
+}
+
+static void
+page_notify (GisPage *page,
+ GParamSpec *pspec,
+ GisAssistant *assistant)
+{
+ if (page != assistant->current_page)
+ return;
+
+ if (strcmp (pspec->name, "title") == 0)
+ {
+ g_object_notify_by_pspec (G_OBJECT (assistant), obj_props[PROP_TITLE]);
+ update_titlebar (assistant);
+ }
+ else if (strcmp (pspec->name, "applying") == 0)
+ {
+ update_applying_state (assistant);
+ }
+ else
+ {
+ update_navigation_buttons (assistant);
+ }
+}
+
+void
+gis_assistant_add_page (GisAssistant *assistant,
+ GisPage *page)
+{
+ GList *link;
+
+ /* Page shouldn't already exist */
+ g_return_if_fail (!g_list_find (assistant->pages, page));
+
+ assistant->pages = g_list_append (assistant->pages, page);
+ link = g_list_last (assistant->pages);
+ link = link->prev;
+
+ g_signal_connect (page, "notify", G_CALLBACK (page_notify), assistant);
+
+ gtk_stack_add_child (GTK_STACK (assistant->stack), GTK_WIDGET (page));
+
+ /* Update buttons if current page is now the second last page */
+ if (assistant->current_page && link &&
+ link->data == assistant->current_page)
+ update_navigation_buttons (assistant);
+}
+
+void
+gis_assistant_remove_page (GisAssistant *assistant,
+ GisPage *page)
+{
+ assistant->pages = g_list_remove (assistant->pages, page);
+ if (page == assistant->current_page)
+ assistant->current_page = NULL;
+
+ gtk_stack_remove (GTK_STACK (assistant->stack), GTK_WIDGET (page));
+}
+
+GisPage *
+gis_assistant_get_current_page (GisAssistant *assistant)
+{
+ return assistant->current_page;
+}
+
+GList *
+gis_assistant_get_all_pages (GisAssistant *assistant)
+{
+ return assistant->pages;
+}
+
+static void
+go_forward (GtkWidget *button,
+ GisAssistant *assistant)
+{
+ gis_assistant_next_page (assistant);
+}
+
+static void
+go_backward (GtkWidget *button,
+ GisAssistant *assistant)
+{
+ gis_assistant_previous_page (assistant);
+}
+
+static void
+do_cancel (GtkWidget *button,
+ GisAssistant *assistant)
+{
+ if (assistant->current_page)
+ gis_page_apply_cancel (assistant->current_page);
+}
+
+const gchar *
+gis_assistant_get_title (GisAssistant *assistant)
+{
+ if (assistant->current_page != NULL)
+ return gis_page_get_title (assistant->current_page);
+ else
+ return "";
+}
+
+GtkWidget *
+gis_assistant_get_titlebar (GisAssistant *assistant)
+{
+ return assistant->titlebar;
+}
+
+static void
+update_current_page (GisAssistant *assistant,
+ GisPage *page)
+{
+ if (assistant->current_page == page)
+ return;
+
+ assistant->current_page = page;
+ g_object_notify_by_pspec (G_OBJECT (assistant), obj_props[PROP_TITLE]);
+
+ update_titlebar (assistant);
+ update_applying_state (assistant);
+ update_navigation_buttons (assistant);
+
+ gtk_widget_grab_focus (assistant->forward);
+
+ if (page)
+ gis_page_shown (page);
+}
+
+static void
+current_page_changed (GObject *gobject,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GisAssistant *assistant = GIS_ASSISTANT (user_data);
+ GtkStack *stack = GTK_STACK (gobject);
+ GtkWidget *new_page = gtk_stack_get_visible_child (stack);
+
+ update_current_page (assistant, GIS_PAGE (new_page));
+}
+
+void
+gis_assistant_locale_changed (GisAssistant *assistant)
+{
+ GList *l;
+
+ gtk_button_set_label (GTK_BUTTON (assistant->forward), _("_Next"));
+ gtk_button_set_label (GTK_BUTTON (assistant->accept), _("_Accept"));
+ gtk_button_set_label (GTK_BUTTON (assistant->skip), _("_Skip"));
+ gtk_button_set_label (GTK_BUTTON (assistant->back), _("_Previous"));
+ gtk_button_set_label (GTK_BUTTON (assistant->cancel), _("_Cancel"));
+
+ for (l = assistant->pages; l != NULL; l = l->next)
+ gis_page_locale_changed (l->data);
+
+ update_titlebar (assistant);
+}
+
+gboolean
+gis_assistant_save_data (GisAssistant *assistant,
+ GError **error)
+{
+ GList *l;
+
+ for (l = assistant->pages; l != NULL; l = l->next)
+ {
+ if (!gis_page_save_data (l->data, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+gis_assistant_init (GisAssistant *assistant)
+{
+ gtk_widget_init_template (GTK_WIDGET (assistant));
+
+ g_signal_connect (assistant->stack, "notify::visible-child",
+ G_CALLBACK (current_page_changed), assistant);
+
+ g_signal_connect (assistant->forward, "clicked", G_CALLBACK (go_forward), assistant);
+ g_signal_connect (assistant->accept, "clicked", G_CALLBACK (go_forward), assistant);
+ g_signal_connect (assistant->skip, "clicked", G_CALLBACK (go_forward), assistant);
+
+ g_signal_connect (assistant->back, "clicked", G_CALLBACK (go_backward), assistant);
+ g_signal_connect (assistant->cancel, "clicked", G_CALLBACK (do_cancel), assistant);
+
+ gis_assistant_locale_changed (assistant);
+ update_applying_state (assistant);
+}
+
+static void
+gis_assistant_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisAssistant *assistant = GIS_ASSISTANT (object);
+
+ switch (prop_id)
+ {
+ case PROP_TITLE:
+ g_value_set_string (value, gis_assistant_get_title (assistant));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_assistant_class_init (GisAssistantClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-assistant.ui");
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, forward);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, accept);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, skip);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, back);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, cancel);
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, spinner);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, titlebar);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, title);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAssistant, stack);
+
+ gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), visible_child_changed);
+
+ gobject_class->get_property = gis_assistant_get_property;
+
+ obj_props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ "", "",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+
+ /**
+ * GisAssistant::page-changed:
+ * @assistant: the #GisAssistant
+ *
+ * The ::page-changed signal is emitted when the visible page
+ * changed.
+ */
+ signals[PAGE_CHANGED] =
+ g_signal_new ("page-changed",
+ G_TYPE_FROM_CLASS (gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+}
diff --git a/gnome-initial-setup/gis-assistant.gresource.xml b/gnome-initial-setup/gis-assistant.gresource.xml
new file mode 100644
index 0000000..041cfb0
--- /dev/null
+++ b/gnome-initial-setup/gis-assistant.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks">gis-assistant.ui</file>
+ <file preprocess="xml-stripblanks">gis-page-header.ui</file>
+ <file>gis-page-header.css</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/gis-assistant.h b/gnome-initial-setup/gis-assistant.h
new file mode 100644
index 0000000..26218c1
--- /dev/null
+++ b/gnome-initial-setup/gis-assistant.h
@@ -0,0 +1,48 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#pragma once
+
+#include "gis-page.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_ASSISTANT (gis_assistant_get_type ())
+
+G_DECLARE_FINAL_TYPE (GisAssistant, gis_assistant, GIS, ASSISTANT, GtkBox)
+
+void gis_assistant_add_page (GisAssistant *assistant,
+ GisPage *page);
+void gis_assistant_remove_page (GisAssistant *assistant,
+ GisPage *page);
+
+void gis_assistant_next_page (GisAssistant *assistant);
+void gis_assistant_previous_page (GisAssistant *assistant);
+GisPage * gis_assistant_get_current_page (GisAssistant *assistant);
+GList * gis_assistant_get_all_pages (GisAssistant *assistant);
+const gchar *gis_assistant_get_title (GisAssistant *assistant);
+GtkWidget *gis_assistant_get_titlebar (GisAssistant *assistant);
+
+void gis_assistant_locale_changed (GisAssistant *assistant);
+gboolean gis_assistant_save_data (GisAssistant *assistant,
+ GError **error);
+
+G_END_DECLS
diff --git a/gnome-initial-setup/gis-assistant.ui b/gnome-initial-setup/gis-assistant.ui
new file mode 100644
index 0000000..99d1e6d
--- /dev/null
+++ b/gnome-initial-setup/gis-assistant.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk30">
+ <object class="GtkHeaderBar" id="titlebar">
+ <child type="title">
+ <object class="GtkLabel" id="title">
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="cancel">
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="back">
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="placeholder">
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkSpinner" id="spinner" />
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="skip">
+ <property name="visible">False</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="forward">
+ <property name="visible">False</property>
+ <property name="use-underline">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="accept">
+ <property name="visible">False</property>
+ <property name="use-underline">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+
+ <object class="GtkSizeGroup" id="headerheight">
+ <property name="mode">vertical</property>
+ <widgets>
+ <widget name="title"/>
+ <widget name="placeholder"/>
+ </widgets>
+ </object>
+
+ <template class="GisAssistant" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="transition-type">slide-left-right</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="hhomogeneous">False</property>
+ <property name="vhomogeneous">False</property>
+ <signal name="notify::visible-child" handler="visible_child_changed" object="GisAssistant" swapped="yes"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/gis-driver.c b/gnome-initial-setup/gis-driver.c
new file mode 100644
index 0000000..1ddf3c6
--- /dev/null
+++ b/gnome-initial-setup/gis-driver.c
@@ -0,0 +1,936 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include "gnome-initial-setup.h"
+
+#include <errno.h>
+#include <locale.h>
+#include <stdlib.h>
+
+#ifdef HAVE_WEBKITGTK_6_0
+#include <webkit/webkit.h>
+#else
+#include <webkit2/webkit2.h>
+#endif
+
+#include "cc-common-language.h"
+#include "gis-assistant.h"
+
+#define GIS_TYPE_DRIVER_MODE (gis_driver_mode_get_type ())
+
+/* Statically include this for now. Maybe later
+ * we'll generate this from glib-mkenums. */
+GType
+gis_driver_mode_get_type (void) {
+ static GType enum_type_id = 0;
+ if (G_UNLIKELY (!enum_type_id))
+ {
+ static const GEnumValue values[] = {
+ { GIS_DRIVER_MODE_NEW_USER, "GIS_DRIVER_MODE_NEW_USER", "new_user" },
+ { GIS_DRIVER_MODE_EXISTING_USER, "GIS_DRIVER_MODE_EXISTING_USER", "existing_user" },
+ { 0, NULL, NULL }
+ };
+ enum_type_id = g_enum_register_static("GisDriverMode", values);
+ }
+ return enum_type_id;
+}
+
+enum {
+ REBUILD_PAGES,
+ LOCALE_CHANGED,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL];
+
+typedef enum {
+ PROP_MODE = 1,
+ PROP_USERNAME,
+ PROP_SMALL_SCREEN,
+ PROP_PARENTAL_CONTROLS_ENABLED,
+ PROP_FULL_NAME,
+ PROP_AVATAR,
+} GisDriverProperty;
+
+static GParamSpec *obj_props[PROP_AVATAR + 1];
+
+struct _GisDriver {
+ AdwApplication parent_instance;
+
+ GtkWindow *main_window;
+ GisAssistant *assistant;
+
+ GdmClient *client;
+ GdmGreeter *greeter;
+ GdmUserVerifier *user_verifier;
+
+ ActUser *user_account;
+ gchar *user_password;
+
+ ActUser *parent_account; /* (owned) (nullable) */
+ gchar *parent_password; /* (owned) (nullable) */
+
+ gboolean parental_controls_enabled;
+
+ gchar *lang_id;
+ gchar *username;
+ gchar *full_name; /* (owned) (nullable) */
+
+ GdkPaintable *avatar; /* (owned) (nullable) */
+
+ GisDriverMode mode;
+ UmAccountMode account_mode;
+ gboolean small_screen;
+
+ locale_t locale;
+
+ const gchar *vendor_conf_file_path;
+ GKeyFile *vendor_conf_file;
+};
+
+G_DEFINE_TYPE (GisDriver, gis_driver, ADW_TYPE_APPLICATION)
+
+static void
+gis_driver_dispose (GObject *object)
+{
+ GisDriver *driver = GIS_DRIVER (object);
+
+ g_clear_object (&driver->user_verifier);
+ g_clear_object (&driver->greeter);
+ g_clear_object (&driver->client);
+
+ G_OBJECT_CLASS (gis_driver_parent_class)->dispose (object);
+}
+
+static void
+gis_driver_finalize (GObject *object)
+{
+ GisDriver *driver = GIS_DRIVER (object);
+
+ g_free (driver->lang_id);
+ g_free (driver->username);
+ g_free (driver->full_name);
+ g_free (driver->user_password);
+
+ g_clear_object (&driver->avatar);
+
+ g_clear_object (&driver->user_account);
+ g_clear_pointer (&driver->vendor_conf_file, g_key_file_free);
+
+ g_clear_object (&driver->parent_account);
+ g_free (driver->parent_password);
+
+ if (driver->locale != (locale_t) 0)
+ {
+ uselocale (LC_GLOBAL_LOCALE);
+ freelocale (driver->locale);
+ }
+
+ G_OBJECT_CLASS (gis_driver_parent_class)->finalize (object);
+}
+
+static void
+assistant_page_changed (GtkScrolledWindow *sw)
+{
+ gtk_adjustment_set_value (gtk_scrolled_window_get_vadjustment (sw), 0);
+}
+
+static void
+prepare_main_window (GisDriver *driver)
+{
+ GtkWidget *child, *sw;
+
+ child = gtk_window_get_child (GTK_WINDOW (driver->main_window));
+ g_object_ref (child);
+ gtk_window_set_child (GTK_WINDOW (driver->main_window), NULL);
+ sw = gtk_scrolled_window_new ();
+ gtk_window_set_child (GTK_WINDOW (driver->main_window), sw);
+ gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), child);
+ g_object_unref (child);
+
+ g_signal_connect_swapped (driver->assistant,
+ "page-changed",
+ G_CALLBACK (assistant_page_changed),
+ sw);
+
+ gtk_window_set_titlebar (driver->main_window,
+ gis_assistant_get_titlebar (driver->assistant));
+}
+
+static void
+rebuild_pages (GisDriver *driver)
+{
+ g_signal_emit (G_OBJECT (driver), signals[REBUILD_PAGES], 0);
+}
+
+GisAssistant *
+gis_driver_get_assistant (GisDriver *driver)
+{
+ return driver->assistant;
+}
+
+static void
+gis_driver_locale_changed (GisDriver *driver)
+{
+ GtkTextDirection direction;
+
+ direction = gtk_get_locale_direction ();
+ gtk_widget_set_default_direction (direction);
+
+ rebuild_pages (driver);
+ gis_assistant_locale_changed (driver->assistant);
+
+ g_signal_emit (G_OBJECT (driver), signals[LOCALE_CHANGED], 0);
+}
+
+void
+gis_driver_set_user_language (GisDriver *driver, const gchar *lang_id, gboolean update_locale)
+{
+ g_free (driver->lang_id);
+ driver->lang_id = g_strdup (lang_id);
+
+ cc_common_language_set_current_language (lang_id);
+
+ if (update_locale)
+ {
+ locale_t locale = newlocale (LC_MESSAGES_MASK, lang_id, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ {
+ g_warning ("Failed to create locale %s: %s", lang_id, g_strerror (errno));
+ return;
+ }
+
+ uselocale (locale);
+
+ if (driver->locale != (locale_t) 0 && driver->locale != LC_GLOBAL_LOCALE)
+ freelocale (driver->locale);
+ driver->locale = locale;
+
+ gis_driver_locale_changed (driver);
+ }
+}
+
+const gchar *
+gis_driver_get_user_language (GisDriver *driver)
+{
+ return driver->lang_id;
+}
+
+void
+gis_driver_set_username (GisDriver *driver, const gchar *username)
+{
+ g_free (driver->username);
+ driver->username = g_strdup (username);
+
+ g_object_notify (G_OBJECT (driver), "username");
+}
+
+const gchar *
+gis_driver_get_username (GisDriver *driver)
+{
+ return driver->username;
+}
+
+/**
+ * gis_driver_set_full_name:
+ * @driver: a #GisDriver
+ * @full_name: (nullable): full name of the main user, or %NULL if not known
+ *
+ * Set the #GisDriver:full-name property.
+ *
+ * Since: 3.36
+ */
+void
+gis_driver_set_full_name (GisDriver *driver,
+ const gchar *full_name)
+{
+ g_return_if_fail (GIS_IS_DRIVER (driver));
+ g_return_if_fail (full_name == NULL ||
+ g_utf8_validate (full_name, -1, NULL));
+
+ if (g_strcmp0 (driver->full_name, full_name) == 0)
+ return;
+
+ g_free (driver->full_name);
+ driver->full_name = g_strdup (full_name);
+
+ g_object_notify_by_pspec (G_OBJECT (driver), obj_props[PROP_FULL_NAME]);
+}
+
+/**
+ * gis_driver_get_full_name:
+ * @driver: a #GisDriver
+ *
+ * Get the #GisDriver:full-name property.
+ *
+ * Returns: (nullable): full name of the main user, or %NULL if not known
+ * Since: 3.36
+ */
+const gchar *
+gis_driver_get_full_name (GisDriver *driver)
+{
+ g_return_val_if_fail (GIS_IS_DRIVER (driver), NULL);
+
+ return driver->full_name;
+}
+
+/**
+ * gis_driver_set_avatar:
+ * @driver: a #GisDriver
+ * @avatar: (nullable) (transfer none): avatar of the main user, or %NULL if not known
+ *
+ * Set the #GisDriver:avatar property.
+ *
+ * Since: 3.36
+ */
+void
+gis_driver_set_avatar (GisDriver *driver,
+ GdkPaintable *avatar)
+{
+ g_return_if_fail (GIS_IS_DRIVER (driver));
+ g_return_if_fail (avatar == NULL || GDK_IS_PAINTABLE (avatar));
+
+ if (g_set_object (&driver->avatar, avatar))
+ g_object_notify_by_pspec (G_OBJECT (driver), obj_props[PROP_AVATAR]);
+}
+
+/**
+ * gis_driver_get_avatar:
+ * @driver: a #GisDriver
+ *
+ * Get the #GisDriver:avatar property.
+ *
+ * Returns: (nullable) (transfer none): avatar of the main user, or %NULL if not known
+ * Since: 3.36
+ */
+GdkPaintable *
+gis_driver_get_avatar (GisDriver *driver)
+{
+ g_return_val_if_fail (GIS_IS_DRIVER (driver), NULL);
+
+ return driver->avatar;
+}
+
+void
+gis_driver_set_user_permissions (GisDriver *driver,
+ ActUser *user,
+ const gchar *password)
+{
+ g_set_object (&driver->user_account, user);
+ g_free (driver->user_password);
+ driver->user_password = g_strdup (password);
+}
+
+void
+gis_driver_get_user_permissions (GisDriver *driver,
+ ActUser **user,
+ const gchar **password)
+{
+ if (user != NULL)
+ *user = driver->user_account;
+
+ if (password != NULL)
+ *password = driver->user_password;
+}
+
+/**
+ * gis_driver_set_parent_permissions:
+ * @driver: a #GisDriver
+ * @parent: (transfer none): user account for the parent
+ * @password: password for the parent
+ *
+ * Stores the parent account details for later use when saving the initial setup
+ * data.
+ *
+ * Since: 3.36
+ */
+void
+gis_driver_set_parent_permissions (GisDriver *driver,
+ ActUser *parent,
+ const gchar *password)
+{
+ g_set_object (&driver->parent_account, parent);
+ g_free (driver->parent_password);
+ driver->parent_password = g_strdup (password);
+}
+
+/**
+ * gis_driver_get_parent_permissions:
+ * @driver: a #GisDriver
+ * @parent: (out) (transfer none) (optional) (nullable): return location for the
+ * user account for the parent, which may be %NULL
+ * @password: (out) (transfer none) (optional) (nullable): return location for
+ * the password for the parent
+ *
+ * Gets the parent account details saved from an earlier step in the initial
+ * setup process. They may be %NULL if not set yet.
+ *
+ * Since: 3.36
+ */
+void
+gis_driver_get_parent_permissions (GisDriver *driver,
+ ActUser **parent,
+ const gchar **password)
+{
+ if (parent != NULL)
+ *parent = driver->parent_account;
+ if (password != NULL)
+ *password = driver->parent_password;
+}
+
+void
+gis_driver_set_account_mode (GisDriver *driver,
+ UmAccountMode mode)
+{
+ driver->account_mode = mode;
+}
+
+UmAccountMode
+gis_driver_get_account_mode (GisDriver *driver)
+{
+ return driver->account_mode;
+}
+
+/**
+ * gis_driver_set_parental_controls_enabled:
+ * @driver: a #GisDriver
+ * @parental_controls_enabled: whether parental controls are enabled for the main user
+ *
+ * Set the #GisDriver:parental-controls-enabled property.
+ *
+ * Since: 3.36
+ */
+void
+gis_driver_set_parental_controls_enabled (GisDriver *driver,
+ gboolean parental_controls_enabled)
+{
+ if (driver->parental_controls_enabled == parental_controls_enabled)
+ return;
+
+ driver->parental_controls_enabled = parental_controls_enabled;
+ rebuild_pages (driver);
+
+ g_object_notify_by_pspec (G_OBJECT (driver), obj_props[PROP_PARENTAL_CONTROLS_ENABLED]);
+}
+
+/**
+ * gis_driver_get_parental_controls_enabled:
+ * @driver: a #GisDriver
+ *
+ * Get the #GisDriver:parental-controls-enabled property.
+ *
+ * Returns: whether parental controls are enabled for the main user
+ * Since: 3.36
+ */
+gboolean
+gis_driver_get_parental_controls_enabled (GisDriver *driver)
+{
+ return driver->parental_controls_enabled;
+}
+
+gboolean
+gis_driver_get_gdm_objects (GisDriver *driver,
+ GdmGreeter **greeter,
+ GdmUserVerifier **user_verifier)
+{
+ if (driver->greeter == NULL || driver->user_verifier == NULL)
+ return FALSE;
+
+ *greeter = driver->greeter;
+ *user_verifier = driver->user_verifier;
+
+ return TRUE;
+}
+
+void
+gis_driver_add_page (GisDriver *driver,
+ GisPage *page)
+{
+ gis_assistant_add_page (driver->assistant, page);
+}
+
+void
+gis_driver_hide_window (GisDriver *driver)
+{
+ gtk_widget_hide (GTK_WIDGET (driver->main_window));
+}
+
+static gboolean
+load_vendor_conf_file_at_path (GisDriver *driver,
+ const char *path)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GKeyFile) vendor_conf_file = g_key_file_new ();
+
+ if (!g_key_file_load_from_file (vendor_conf_file, path, G_KEY_FILE_NONE, &error))
+ {
+ if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_warning ("Could not read file %s: %s:", path, error->message);
+ return FALSE;
+ }
+
+ driver->vendor_conf_file_path = path;
+ driver->vendor_conf_file = g_steal_pointer (&vendor_conf_file);
+ return TRUE;
+}
+
+static void
+load_vendor_conf_file (GisDriver *driver)
+{
+#ifdef VENDOR_CONF_FILE
+ load_vendor_conf_file_at_path (driver, VENDOR_CONF_FILE);
+#else
+ /* If no path was passed at build time, then we have search path:
+ *
+ * - First check $(sysconfdir)/gnome-initial-setup/vendor.conf
+ * - Then check $(datadir)/gnome-initial-setup/vendor.conf
+ *
+ * This allows distributions to provide a default packaged config in a
+ * location that might be managed by ostree, and allows OEMs to
+ * override using an unmanaged location.
+ */
+ if (!load_vendor_conf_file_at_path (driver, PKGSYSCONFDIR "/vendor.conf"))
+ load_vendor_conf_file_at_path (driver, PKGDATADIR "/vendor.conf");
+#endif
+}
+
+static void
+report_conf_error_if_needed (GisDriver *driver,
+ const gchar *group,
+ const gchar *key,
+ const GError *error)
+{
+ if (!g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND) &&
+ !g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND))
+ g_warning ("Error getting the value for key '%s' of group [%s] in %s: %s",
+ group, key, driver->vendor_conf_file_path, error->message);
+}
+
+gboolean
+gis_driver_conf_get_boolean (GisDriver *driver,
+ const gchar *group,
+ const gchar *key,
+ gboolean default_value)
+{
+ if (driver->vendor_conf_file) {
+ g_autoptr(GError) error = NULL;
+ gboolean new_value = g_key_file_get_boolean (driver->vendor_conf_file, group,
+ key, &error);
+ if (error == NULL)
+ return new_value;
+
+ report_conf_error_if_needed (driver, group, key, error);
+ }
+
+ return default_value;
+}
+
+GStrv
+gis_driver_conf_get_string_list (GisDriver *driver,
+ const gchar *group,
+ const gchar *key,
+ gsize *out_length)
+{
+ if (driver->vendor_conf_file) {
+ g_autoptr(GError) error = NULL;
+ GStrv new_value = g_key_file_get_string_list (driver->vendor_conf_file, group,
+ key, out_length, &error);
+ if (error == NULL)
+ return new_value;
+
+ report_conf_error_if_needed (driver, group, key, error);
+ }
+
+ return NULL;
+}
+
+gchar *
+gis_driver_conf_get_string (GisDriver *driver,
+ const gchar *group,
+ const gchar *key)
+{
+ if (driver->vendor_conf_file) {
+ g_autoptr(GError) error = NULL;
+ gchar *new_value = g_key_file_get_string (driver->vendor_conf_file, group,
+ key, &error);
+ if (error == NULL)
+ return new_value;
+
+ report_conf_error_if_needed (driver, group, key, error);
+ }
+
+ return NULL;
+}
+
+GisDriverMode
+gis_driver_get_mode (GisDriver *driver)
+{
+ return driver->mode;
+}
+
+gboolean
+gis_driver_is_small_screen (GisDriver *driver)
+{
+ return driver->small_screen;
+}
+
+static gboolean
+monitor_is_small (GdkMonitor *monitor)
+{
+ GdkRectangle geom;
+
+ if (g_getenv ("GIS_SMALL_SCREEN"))
+ return TRUE;
+
+ gdk_monitor_get_geometry (monitor, &geom);
+ return geom.height < 800;
+}
+
+static void
+gis_driver_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisDriver *driver = GIS_DRIVER (object);
+
+ switch ((GisDriverProperty) prop_id)
+ {
+ case PROP_MODE:
+ g_value_set_enum (value, driver->mode);
+ break;
+ case PROP_USERNAME:
+ g_value_set_string (value, driver->username);
+ break;
+ case PROP_SMALL_SCREEN:
+ g_value_set_boolean (value, driver->small_screen);
+ break;
+ case PROP_PARENTAL_CONTROLS_ENABLED:
+ g_value_set_boolean (value, driver->parental_controls_enabled);
+ break;
+ case PROP_FULL_NAME:
+ g_value_set_string (value, driver->full_name);
+ break;
+ case PROP_AVATAR:
+ g_value_set_object (value, driver->avatar);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_driver_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GisDriver *driver = GIS_DRIVER (object);
+
+ switch ((GisDriverProperty) prop_id)
+ {
+ case PROP_MODE:
+ driver->mode = g_value_get_enum (value);
+ break;
+ case PROP_USERNAME:
+ g_free (driver->username);
+ driver->username = g_value_dup_string (value);
+ break;
+ case PROP_PARENTAL_CONTROLS_ENABLED:
+ gis_driver_set_parental_controls_enabled (driver, g_value_get_boolean (value));
+ break;
+ case PROP_FULL_NAME:
+ gis_driver_set_full_name (driver, g_value_get_string (value));
+ break;
+ case PROP_AVATAR:
+ gis_driver_set_avatar (driver, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_driver_activate (GApplication *app)
+{
+ GisDriver *driver = GIS_DRIVER (app);
+
+ G_APPLICATION_CLASS (gis_driver_parent_class)->activate (app);
+
+ gtk_window_present (GTK_WINDOW (driver->main_window));
+}
+
+/* Recompute driver->small_screen based on the monitor where the window is
+ * located, if the window is actually realized. If not, recompute it based on
+ * the primary monitor of the default display. */
+static void
+recompute_small_screen (GisDriver *driver)
+{
+ GdkMonitor *active_monitor;
+ gboolean old_value = driver->small_screen;
+
+ if (gtk_widget_get_realized (GTK_WIDGET (driver->main_window)))
+ {
+ GdkDisplay *default_display = gdk_display_get_default ();
+ GdkSurface *surface;
+
+ surface = gtk_native_get_surface (GTK_NATIVE (driver->main_window));
+ active_monitor = gdk_display_get_monitor_at_surface (default_display, surface);
+ driver->small_screen = monitor_is_small (active_monitor);
+ }
+
+ if (driver->small_screen != old_value)
+ g_object_notify (G_OBJECT (driver), "small-screen");
+}
+
+static void
+update_screen_size (GisDriver *driver)
+{
+ GtkWidget *sw;
+
+ recompute_small_screen (driver);
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (driver->main_window)))
+ return;
+
+ sw = gtk_window_get_child (GTK_WINDOW (driver->main_window));
+
+ if (driver->small_screen)
+ {
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
+ GTK_POLICY_AUTOMATIC,
+ GTK_POLICY_AUTOMATIC);
+ gtk_window_set_default_size (driver->main_window, -1, -1);
+ gtk_window_set_resizable (driver->main_window, TRUE);
+ gtk_window_maximize (driver->main_window);
+ gtk_window_present (driver->main_window);
+ }
+ else
+ {
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
+ GTK_POLICY_NEVER,
+ GTK_POLICY_NEVER);
+ gtk_window_set_default_size (driver->main_window, 1024, 768);
+ gtk_window_set_resizable (driver->main_window, FALSE);
+ gtk_window_unmaximize (driver->main_window);
+ gtk_window_present (driver->main_window);
+ }
+}
+
+static void
+on_surface_enter_monitor_cb (GdkSurface *surface,
+ GdkMonitor *monitor,
+ GisDriver *driver)
+{
+ update_screen_size (driver);
+}
+
+static void
+window_realize_cb (GtkWidget *widget, gpointer user_data)
+{
+ GdkSurface *surface;
+ GisDriver *driver;
+
+ driver = GIS_DRIVER (user_data);
+
+ surface = gtk_native_get_surface (GTK_NATIVE (widget));
+ g_signal_connect (surface, "enter-monitor", G_CALLBACK (on_surface_enter_monitor_cb), driver);
+
+ update_screen_size (driver);
+}
+
+static void
+connect_to_gdm (GisDriver *driver)
+{
+ g_autoptr(GError) error = NULL;
+
+ driver->client = gdm_client_new ();
+
+ driver->greeter = gdm_client_get_greeter_sync (driver->client, NULL, &error);
+ if (error == NULL)
+ driver->user_verifier = gdm_client_get_user_verifier_sync (driver->client, NULL, &error);
+
+ if (error != NULL) {
+ g_warning ("Failed to open connection to GDM: %s", error->message);
+ g_clear_object (&driver->user_verifier);
+ g_clear_object (&driver->greeter);
+ g_clear_object (&driver->client);
+ }
+}
+
+static void
+gis_driver_startup (GApplication *app)
+{
+ GisDriver *driver = GIS_DRIVER (app);
+ WebKitWebContext *context = webkit_web_context_get_default ();
+
+ G_APPLICATION_CLASS (gis_driver_parent_class)->startup (app);
+
+ webkit_web_context_set_sandbox_enabled (context, TRUE);
+
+ if (driver->mode == GIS_DRIVER_MODE_NEW_USER)
+ connect_to_gdm (driver);
+
+ driver->main_window = g_object_new (GTK_TYPE_APPLICATION_WINDOW,
+ "application", app,
+ "icon-name", "preferences-system",
+ "deletable", FALSE,
+ NULL);
+
+ g_signal_connect (driver->main_window,
+ "realize",
+ G_CALLBACK (window_realize_cb),
+ (gpointer)app);
+
+ driver->assistant = g_object_new (GIS_TYPE_ASSISTANT, NULL);
+ gtk_window_set_child (GTK_WINDOW (driver->main_window),
+ GTK_WIDGET (driver->assistant));
+
+ gis_driver_set_user_language (driver, setlocale (LC_MESSAGES, NULL), FALSE);
+
+ prepare_main_window (driver);
+ rebuild_pages (driver);
+}
+
+static void
+gis_driver_init (GisDriver *driver)
+{
+ load_vendor_conf_file (driver);
+}
+
+static void
+gis_driver_class_init (GisDriverClass *klass)
+{
+ GApplicationClass *application_class = G_APPLICATION_CLASS (klass);
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->get_property = gis_driver_get_property;
+ gobject_class->set_property = gis_driver_set_property;
+ gobject_class->dispose = gis_driver_dispose;
+ gobject_class->finalize = gis_driver_finalize;
+ application_class->startup = gis_driver_startup;
+ application_class->activate = gis_driver_activate;
+
+ signals[REBUILD_PAGES] =
+ g_signal_new ("rebuild-pages",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ signals[LOCALE_CHANGED] =
+ g_signal_new ("locale-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ obj_props[PROP_MODE] =
+ g_param_spec_enum ("mode", "", "",
+ GIS_TYPE_DRIVER_MODE,
+ GIS_DRIVER_MODE_EXISTING_USER,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_USERNAME] =
+ g_param_spec_string ("username", "", "",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_SMALL_SCREEN] =
+ g_param_spec_boolean ("small-screen", "", "",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * GisDriver:parental-controls-enabled:
+ *
+ * Whether parental controls are enabled for the main user. If this is %TRUE,
+ * two user accounts will be created when this page is saved: one for the main
+ * user (a child) which will be a standard account; and one for the parent
+ * which will be an administrative account.
+ *
+ * Since: 3.36
+ */
+ obj_props[PROP_PARENTAL_CONTROLS_ENABLED] =
+ g_param_spec_boolean ("parental-controls-enabled",
+ "Parental Controls Enabled",
+ "Whether parental controls are enabled for the main user.",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GisDriver:full-name: (nullable)
+ *
+ * Full name of the main user. May be %NULL if unknown or not set yet.
+ *
+ * Since: 3.36
+ */
+ obj_props[PROP_FULL_NAME] =
+ g_param_spec_string ("full-name",
+ "Full Name",
+ "Full name of the main user.",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GisDriver:avatar: (nullable)
+ *
+ * Avatar of the main user. May be %NULL if unknown or not set yet.
+ *
+ * Since: 3.36
+ */
+ obj_props[PROP_AVATAR] =
+ g_param_spec_object ("avatar",
+ "Avatar",
+ "Avatar of the main user.",
+ GDK_TYPE_PAINTABLE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (gobject_class, G_N_ELEMENTS (obj_props), obj_props);
+}
+
+gboolean
+gis_driver_save_data (GisDriver *driver,
+ GError **error)
+{
+ if (gis_get_mock_mode ())
+ {
+ g_message ("%s: Skipping saving data due to being in mock mode", G_STRFUNC);
+ return TRUE;
+ }
+
+ return gis_assistant_save_data (driver->assistant, error);
+}
+
+GisDriver *
+gis_driver_new (GisDriverMode mode)
+{
+ return g_object_new (GIS_TYPE_DRIVER,
+ "application-id", "org.gnome.InitialSetup",
+ "mode", mode,
+ NULL);
+}
diff --git a/gnome-initial-setup/gis-driver.h b/gnome-initial-setup/gis-driver.h
new file mode 100644
index 0000000..9b935e2
--- /dev/null
+++ b/gnome-initial-setup/gis-driver.h
@@ -0,0 +1,128 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_DRIVER_H__
+#define __GIS_DRIVER_H__
+
+#include "gis-assistant.h"
+#include "gis-page.h"
+#include <act/act-user-manager.h>
+#include <gdm/gdm-client.h>
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_DRIVER (gis_driver_get_type ())
+
+G_DECLARE_FINAL_TYPE (GisDriver, gis_driver, GIS, DRIVER, AdwApplication)
+
+typedef enum {
+ UM_LOCAL,
+ UM_ENTERPRISE,
+ NUM_MODES,
+} UmAccountMode;
+
+typedef enum {
+ GIS_DRIVER_MODE_NEW_USER,
+ GIS_DRIVER_MODE_EXISTING_USER,
+} GisDriverMode;
+
+GisAssistant *gis_driver_get_assistant (GisDriver *driver);
+
+void gis_driver_set_user_permissions (GisDriver *driver,
+ ActUser *user,
+ const gchar *password);
+
+void gis_driver_get_user_permissions (GisDriver *driver,
+ ActUser **user,
+ const gchar **password);
+
+void gis_driver_set_parent_permissions (GisDriver *driver,
+ ActUser *parent,
+ const gchar *password);
+
+void gis_driver_get_parent_permissions (GisDriver *driver,
+ ActUser **parent,
+ const gchar **password);
+
+void gis_driver_set_account_mode (GisDriver *driver,
+ UmAccountMode mode);
+
+UmAccountMode gis_driver_get_account_mode (GisDriver *driver);
+
+void gis_driver_set_parental_controls_enabled (GisDriver *driver,
+ gboolean parental_controls_enabled);
+
+gboolean gis_driver_get_parental_controls_enabled (GisDriver *driver);
+
+void gis_driver_set_user_language (GisDriver *driver,
+ const gchar *lang_id,
+ gboolean update_locale);
+
+const gchar *gis_driver_get_user_language (GisDriver *driver);
+
+void gis_driver_set_username (GisDriver *driver,
+ const gchar *username);
+const gchar *gis_driver_get_username (GisDriver *driver);
+
+void gis_driver_set_full_name (GisDriver *driver,
+ const gchar *full_name);
+const gchar *gis_driver_get_full_name (GisDriver *driver);
+
+void gis_driver_set_avatar (GisDriver *driver,
+ GdkPaintable *avatar);
+GdkPaintable *gis_driver_get_avatar (GisDriver *driver);
+
+gboolean gis_driver_get_gdm_objects (GisDriver *driver,
+ GdmGreeter **greeter,
+ GdmUserVerifier **user_verifier);
+
+GisDriverMode gis_driver_get_mode (GisDriver *driver);
+
+gboolean gis_driver_is_small_screen (GisDriver *driver);
+
+void gis_driver_add_page (GisDriver *driver,
+ GisPage *page);
+
+void gis_driver_hide_window (GisDriver *driver);
+
+gboolean gis_driver_save_data (GisDriver *driver,
+ GError **error);
+
+gboolean gis_driver_conf_get_boolean (GisDriver *driver,
+ const gchar *group,
+ const gchar *key,
+ gboolean default_value);
+
+GStrv gis_driver_conf_get_string_list (GisDriver *driver,
+ const gchar *group,
+ const gchar *key,
+ gsize *out_length);
+
+gchar *gis_driver_conf_get_string (GisDriver *driver,
+ const gchar *group,
+ const gchar *key);
+
+GisDriver *gis_driver_new (GisDriverMode mode);
+
+G_END_DECLS
+
+#endif /* __GIS_DRIVER_H__ */
diff --git a/gnome-initial-setup/gis-keyring.c b/gnome-initial-setup/gis-keyring.c
new file mode 100644
index 0000000..7035e6f
--- /dev/null
+++ b/gnome-initial-setup/gis-keyring.c
@@ -0,0 +1,104 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2014 Red Hat
+ *
+ * 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 <string.h>
+
+#include <gio/gio.h>
+
+#include "gis-keyring.h"
+
+#include <libsecret/secret.h>
+
+#define DUMMY_PWD "gis"
+
+/* We never want to see a keyring dialog, but we need to make
+ * sure a keyring is present.
+ *
+ * To achieve this, install a prompter for gnome-keyring that
+ * never shows any UI, and create a keyring, if one does not
+ * exist yet.
+ */
+
+void
+gis_ensure_login_keyring ()
+{
+ g_autoptr(GSubprocess) subprocess = NULL;
+ g_autoptr(GSubprocessLauncher) launcher = NULL;
+ g_autoptr(GError) error = NULL;
+
+ g_debug ("launching gnome-keyring-daemon --unlock");
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDIN_PIPE | G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_SILENCE);
+ subprocess = g_subprocess_launcher_spawn (launcher, &error, "gnome-keyring-daemon", "--unlock", NULL);
+ if (subprocess == NULL) {
+ g_warning ("Failed to spawn gnome-keyring-daemon --unlock: %s", error->message);
+ return;
+ }
+
+ if (!g_subprocess_communicate_utf8 (subprocess, DUMMY_PWD, NULL, NULL, NULL, &error)) {
+ g_warning ("Failed to communicate with gnome-keyring-daemon: %s", error->message);
+ return;
+ }
+}
+
+void
+gis_update_login_keyring_password (const gchar *new_)
+{
+ g_autoptr(GDBusConnection) bus = NULL;
+ g_autoptr(SecretService) service = NULL;
+ g_autoptr(SecretValue) old_secret = NULL;
+ g_autoptr(SecretValue) new_secret = NULL;
+ g_autoptr(GError) error = NULL;
+
+ service = secret_service_get_sync (SECRET_SERVICE_OPEN_SESSION, NULL, &error);
+ if (service == NULL) {
+ g_warning ("Failed to get secret service: %s", error->message);
+ return;
+ }
+
+ bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
+ if (bus == NULL) {
+ g_warning ("Failed to get session bus: %s", error->message);
+ return;
+ }
+
+ old_secret = secret_value_new (DUMMY_PWD, strlen (DUMMY_PWD), "text/plain");
+ new_secret = secret_value_new (new_, strlen (new_), "text/plain");
+
+ g_dbus_connection_call_sync (bus,
+ "org.gnome.keyring",
+ "/org/freedesktop/secrets",
+ "org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface",
+ "ChangeWithMasterPassword",
+ g_variant_new ("(o@(oayays)@(oayays))",
+ "/org/freedesktop/secrets/collection/login",
+ secret_service_encode_dbus_secret (service, old_secret),
+ secret_service_encode_dbus_secret (service, new_secret)),
+ NULL,
+ 0,
+ G_MAXINT,
+ NULL, &error);
+
+ if (error != NULL) {
+ g_warning ("Failed to change keyring password: %s", error->message);
+ }
+}
diff --git a/gnome-initial-setup/gis-keyring.h b/gnome-initial-setup/gis-keyring.h
new file mode 100644
index 0000000..764f1e6
--- /dev/null
+++ b/gnome-initial-setup/gis-keyring.h
@@ -0,0 +1,35 @@
+
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2014 Red Hat
+ *
+ * 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>
+ */
+
+#ifndef __GIS_KEYRING_H__
+#define __GIS_KEYRING_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+void gis_ensure_login_keyring ();
+void gis_update_login_keyring_password (const gchar *new_);
+
+G_END_DECLS
+
+#endif /* __GIS_KEYRING_H__ */
diff --git a/gnome-initial-setup/gis-page-header.c b/gnome-initial-setup/gis-page-header.c
new file mode 100644
index 0000000..9b84a0b
--- /dev/null
+++ b/gnome-initial-setup/gis-page-header.c
@@ -0,0 +1,203 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* -*- encoding: utf8 -*- */
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * 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:
+ * Adrien Plazas <kekun.plazas@laposte.net>
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gis-page-header.h"
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_ICON_NAME,
+ PROP_PAINTABLE,
+ PROP_SHOW_ICON,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+struct _GisPageHeader
+{
+ GtkBox parent;
+
+ GtkWidget *box;
+ GtkWidget *icon;
+ GtkWidget *subtitle;
+ GtkWidget *title;
+};
+
+G_DEFINE_TYPE (GisPageHeader, gis_page_header, GTK_TYPE_BOX)
+
+static gboolean
+is_valid_string (const gchar *s)
+{
+ return s != NULL && g_strcmp0 (s, "") != 0;
+}
+
+static void
+update_box_visibility (GisPageHeader *header)
+{
+ gtk_widget_set_visible (header->box, gtk_widget_get_visible (header->subtitle) ||
+ gtk_widget_get_visible (header->title));
+}
+
+static void
+gis_page_header_init (GisPageHeader *header)
+{
+ gtk_widget_init_template (GTK_WIDGET (header));
+
+ g_signal_connect_swapped (header->subtitle, "notify::visible",
+ G_CALLBACK(update_box_visibility), header);
+ g_signal_connect_swapped (header->title, "notify::visible",
+ G_CALLBACK(update_box_visibility), header);
+}
+
+static void
+gis_page_header_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisPageHeader *header = GIS_PAGE_HEADER (object);
+
+ switch (prop_id)
+ {
+ case PROP_TITLE:
+ g_value_set_string (value, gtk_label_get_label (GTK_LABEL (header->title)));
+ break;
+
+ case PROP_SUBTITLE:
+ g_value_set_string (value, gtk_label_get_label (GTK_LABEL (header->subtitle)));
+ break;
+
+ case PROP_ICON_NAME:
+ g_object_get_property (G_OBJECT (header->icon), "icon-name", value);
+ break;
+
+ case PROP_PAINTABLE:
+ g_object_get_property (G_OBJECT (header->icon), "paintable", value);
+ break;
+
+ case PROP_SHOW_ICON:
+ g_value_set_boolean (value, gtk_widget_get_visible (header->icon));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_page_header_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GisPageHeader *header = GIS_PAGE_HEADER (object);
+
+ switch (prop_id)
+ {
+ case PROP_TITLE:
+ gtk_label_set_label (GTK_LABEL (header->title), g_value_get_string (value));
+ gtk_widget_set_visible (header->title, is_valid_string (g_value_get_string (value)));
+ break;
+
+ case PROP_SUBTITLE:
+ gtk_label_set_label (GTK_LABEL (header->subtitle), g_value_get_string (value));
+ gtk_widget_set_visible (header->subtitle, is_valid_string (g_value_get_string (value)));
+ break;
+
+ case PROP_ICON_NAME:
+ g_object_set_property (G_OBJECT (header->icon), "icon-name", value);
+ break;
+
+ case PROP_PAINTABLE:
+ g_object_set_property (G_OBJECT (header->icon), "paintable", value);
+ break;
+
+ case PROP_SHOW_ICON:
+ gtk_widget_set_visible (header->icon, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_page_header_class_init (GisPageHeaderClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-page-header.ui");
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisPageHeader, box);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisPageHeader, icon);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisPageHeader, subtitle);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisPageHeader, title);
+
+ gobject_class->get_property = gis_page_header_get_property;
+ gobject_class->set_property = gis_page_header_set_property;
+
+ obj_props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ "", "",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ "", "",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "", "",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_PAINTABLE] =
+ g_param_spec_object ("paintable",
+ "", "",
+ GDK_TYPE_PAINTABLE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_SHOW_ICON] =
+ g_param_spec_boolean ("show-icon",
+ "", "",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+
+ g_autoptr(GtkCssProvider) provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider, "/org/gnome/initial-setup/gis-page-header.css");
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
diff --git a/gnome-initial-setup/gis-page-header.css b/gnome-initial-setup/gis-page-header.css
new file mode 100644
index 0000000..ce02cc8
--- /dev/null
+++ b/gnome-initial-setup/gis-page-header.css
@@ -0,0 +1,13 @@
+/* Styles borrowed from GTK 4
+ * https://gitlab.gnome.org/GNOME/gtk/blob/672d7f679adf543785042ab45d7e59688103464c/gtk/theme/Adwaita/_common.scss#L287-327
+ */
+.large-title {
+ font-weight: 300;
+ font-size: 24pt;
+ letter-spacing: 0.2rem;
+}
+
+.title-1 {
+ font-weight: 800;
+ font-size: 20pt;
+}
diff --git a/gnome-initial-setup/gis-page-header.h b/gnome-initial-setup/gis-page-header.h
new file mode 100644
index 0000000..2ae6978
--- /dev/null
+++ b/gnome-initial-setup/gis-page-header.h
@@ -0,0 +1,36 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* -*- encoding: utf8 -*- */
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * 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:
+ * Adrien Plazas <kekun.plazas@laposte.net>
+ */
+
+#ifndef __GIS_PAGE_HEADER_H__
+#define __GIS_PAGE_HEADER_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_PAGE_HEADER (gis_page_header_get_type ())
+
+G_DECLARE_FINAL_TYPE (GisPageHeader, gis_page_header, GIS, PAGE_HEADER, GtkBox)
+
+G_END_DECLS
+
+#endif /* __GIS_PAGE_HEADER_H__ */
diff --git a/gnome-initial-setup/gis-page-header.ui b/gnome-initial-setup/gis-page-header.ui
new file mode 100644
index 0000000..215c122
--- /dev/null
+++ b/gnome-initial-setup/gis-page-header.ui
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk30">
+ <template class="GisPageHeader" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="pixel_size">96</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="justify">center</property>
+ <property name="max_width_chars">65</property>
+ <property name="wrap">True</property>
+ <style>
+ <class name="title-1"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="justify">center</property>
+ <property name="max_width_chars">65</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/gis-page.c b/gnome-initial-setup/gis-page.c
new file mode 100644
index 0000000..7c53565
--- /dev/null
+++ b/gnome-initial-setup/gis-page.c
@@ -0,0 +1,422 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include <glib-object.h>
+
+#include "gis-page.h"
+
+struct _GisPagePrivate
+{
+ char *title;
+
+ gboolean applying;
+ GCancellable *apply_cancel;
+ GisPageApplyCallback apply_cb;
+ gpointer apply_data;
+
+ guint complete : 1;
+ guint skippable : 1;
+ guint needs_accept : 1;
+ guint has_forward : 1;
+ guint padding : 5;
+};
+typedef struct _GisPagePrivate GisPagePrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GisPage, gis_page, ADW_TYPE_BIN);
+
+enum
+{
+ PROP_0,
+ PROP_DRIVER,
+ PROP_TITLE,
+ PROP_COMPLETE,
+ PROP_SKIPPABLE,
+ PROP_NEEDS_ACCEPT,
+ PROP_APPLYING,
+ PROP_SMALL_SCREEN,
+ PROP_HAS_FORWARD,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+static void
+gis_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisPage *page = GIS_PAGE (object);
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ switch (prop_id)
+ {
+ case PROP_DRIVER:
+ g_value_set_object (value, page->driver);
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, priv->title);
+ break;
+ case PROP_COMPLETE:
+ g_value_set_boolean (value, priv->complete);
+ break;
+ case PROP_SKIPPABLE:
+ g_value_set_boolean (value, priv->skippable);
+ break;
+ case PROP_NEEDS_ACCEPT:
+ g_value_set_boolean (value, priv->needs_accept);
+ break;
+ case PROP_HAS_FORWARD:
+ g_value_set_boolean (value, priv->has_forward);
+ break;
+ case PROP_APPLYING:
+ g_value_set_boolean (value, gis_page_get_applying (page));
+ break;
+ case PROP_SMALL_SCREEN:
+ if (page->driver)
+ g_object_get_property (G_OBJECT (page->driver), "small-screen", value);
+ else
+ g_value_set_boolean (value, FALSE);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+small_screen_changed (GisPage *page)
+{
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_SMALL_SCREEN]);
+}
+
+static void
+gis_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GisPage *page = GIS_PAGE (object);
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ switch (prop_id)
+ {
+ case PROP_DRIVER:
+ page->driver = g_value_dup_object (value);
+ g_signal_connect_swapped (page->driver, "notify::small-screen",
+ G_CALLBACK (small_screen_changed), page);
+ small_screen_changed (page);
+ break;
+ case PROP_TITLE:
+ gis_page_set_title (page, (char *) g_value_get_string (value));
+ break;
+ case PROP_SKIPPABLE:
+ priv->skippable = g_value_get_boolean (value);
+ break;
+ case PROP_NEEDS_ACCEPT:
+ priv->needs_accept = g_value_get_boolean (value);
+ break;
+ case PROP_HAS_FORWARD:
+ priv->has_forward = g_value_get_boolean (value);
+ break;
+ case PROP_COMPLETE:
+ priv->complete = g_value_get_boolean (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_page_finalize (GObject *object)
+{
+ GisPage *page = GIS_PAGE (object);
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+
+ g_free (priv->title);
+ g_assert (!priv->applying);
+ g_assert (priv->apply_cb == NULL);
+ g_assert (priv->apply_cancel == NULL);
+
+ G_OBJECT_CLASS (gis_page_parent_class)->finalize (object);
+}
+
+static void
+gis_page_dispose (GObject *object)
+{
+ GisPage *page = GIS_PAGE (object);
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+
+ if (priv->apply_cancel)
+ g_cancellable_cancel (priv->apply_cancel);
+
+ if (page->driver)
+ g_signal_handlers_disconnect_by_func (page->driver, small_screen_changed, page);
+ g_clear_object (&page->driver);
+
+ G_OBJECT_CLASS (gis_page_parent_class)->dispose (object);
+}
+
+static void
+gis_page_constructed (GObject *object)
+{
+ GisPage *page = GIS_PAGE (object);
+
+ gis_page_locale_changed (page);
+
+ G_OBJECT_CLASS (gis_page_parent_class)->constructed (object);
+
+}
+
+static gboolean
+gis_page_real_apply (GisPage *page,
+ GCancellable *cancellable)
+{
+ return FALSE;
+}
+
+static void
+gis_page_class_init (GisPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = gis_page_constructed;
+ object_class->dispose = gis_page_dispose;
+ object_class->finalize = gis_page_finalize;
+ object_class->get_property = gis_page_get_property;
+ object_class->set_property = gis_page_set_property;
+
+ klass->apply = gis_page_real_apply;
+
+ obj_props[PROP_DRIVER] =
+ g_param_spec_object ("driver", "", "", GIS_TYPE_DRIVER,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+ obj_props[PROP_TITLE] =
+ g_param_spec_string ("title", "", "", "",
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE);
+ obj_props[PROP_COMPLETE] =
+ g_param_spec_boolean ("complete", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE);
+ obj_props[PROP_SKIPPABLE] =
+ g_param_spec_boolean ("skippable", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE);
+ obj_props[PROP_NEEDS_ACCEPT] =
+ g_param_spec_boolean ("needs-accept", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE);
+ obj_props[PROP_HAS_FORWARD] =
+ g_param_spec_boolean ("has-forward", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE);
+ obj_props[PROP_APPLYING] =
+ g_param_spec_boolean ("applying", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+ obj_props[PROP_SMALL_SCREEN] =
+ g_param_spec_boolean ("small-screen", "", "", FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
+
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+}
+
+static void
+gis_page_init (GisPage *page)
+{
+}
+
+char *
+gis_page_get_title (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ if (priv->title != NULL)
+ return priv->title;
+ else
+ return "";
+}
+
+void
+gis_page_set_title (GisPage *page, char *title)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ g_clear_pointer (&priv->title, g_free);
+ priv->title = g_strdup (title);
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_TITLE]);
+}
+
+gboolean
+gis_page_get_complete (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ return priv->complete;
+}
+
+void
+gis_page_set_complete (GisPage *page, gboolean complete)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ priv->complete = complete;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_COMPLETE]);
+}
+
+gboolean
+gis_page_get_skippable (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ return priv->skippable;
+}
+
+void
+gis_page_set_skippable (GisPage *page, gboolean skippable)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ priv->skippable = skippable;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_SKIPPABLE]);
+}
+
+gboolean
+gis_page_get_needs_accept (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ return priv->needs_accept;
+}
+
+void
+gis_page_set_needs_accept (GisPage *page, gboolean needs_accept)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ priv->needs_accept = needs_accept;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_NEEDS_ACCEPT]);
+}
+
+gboolean
+gis_page_get_has_forward (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ return priv->has_forward;
+}
+
+void
+gis_page_set_has_forward (GisPage *page, gboolean has_forward)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ if (priv->has_forward != has_forward)
+ {
+ priv->has_forward = has_forward;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_HAS_FORWARD]);
+ }
+}
+
+void
+gis_page_locale_changed (GisPage *page)
+{
+ if (GIS_PAGE_GET_CLASS (page)->locale_changed)
+ return GIS_PAGE_GET_CLASS (page)->locale_changed (page);
+}
+
+void
+gis_page_apply_begin (GisPage *page,
+ GisPageApplyCallback callback,
+ gpointer user_data)
+{
+ GisPageClass *klass;
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+
+ g_return_if_fail (GIS_IS_PAGE (page));
+ g_return_if_fail (priv->applying == FALSE);
+
+ klass = GIS_PAGE_GET_CLASS (page);
+
+ priv->apply_cb = callback;
+ priv->apply_data = user_data;
+ priv->apply_cancel = g_cancellable_new ();
+ priv->applying = TRUE;
+
+ if (!klass->apply (page, priv->apply_cancel))
+ {
+ /* Shortcut case where we don't want apply, to avoid flicker */
+ gis_page_apply_complete (page, TRUE);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_APPLYING]);
+}
+
+void
+gis_page_apply_complete (GisPage *page,
+ gboolean valid)
+{
+ GisPageApplyCallback callback;
+ gpointer user_data;
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+
+ g_return_if_fail (GIS_IS_PAGE (page));
+ g_return_if_fail (priv->applying == TRUE);
+
+ callback = priv->apply_cb;
+ priv->apply_cb = NULL;
+ user_data = priv->apply_data;
+ priv->apply_data = NULL;
+
+ g_clear_object (&priv->apply_cancel);
+ priv->applying = FALSE;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_APPLYING]);
+
+ if (callback)
+ (callback) (page, valid, user_data);
+}
+
+gboolean
+gis_page_get_applying (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ return priv->applying;
+}
+
+void
+gis_page_apply_cancel (GisPage *page)
+{
+ GisPagePrivate *priv = gis_page_get_instance_private (page);
+ g_cancellable_cancel (priv->apply_cancel);
+}
+
+gboolean
+gis_page_save_data (GisPage *page,
+ GError **error)
+{
+ if (GIS_PAGE_GET_CLASS (page)->save_data == NULL)
+ {
+ /* Not implemented, which presumably means the page has nothing to save. */
+ return TRUE;
+ }
+
+ return GIS_PAGE_GET_CLASS (page)->save_data (page, error);
+}
+
+void
+gis_page_shown (GisPage *page)
+{
+ if (GIS_PAGE_GET_CLASS (page)->shown)
+ GIS_PAGE_GET_CLASS (page)->shown (page);
+}
+
+void
+gis_page_skip (GisPage *page)
+{
+ if (GIS_PAGE_GET_CLASS (page)->skip)
+ GIS_PAGE_GET_CLASS (page)->skip (page);
+}
diff --git a/gnome-initial-setup/gis-page.h b/gnome-initial-setup/gis-page.h
new file mode 100644
index 0000000..9e46b45
--- /dev/null
+++ b/gnome-initial-setup/gis-page.h
@@ -0,0 +1,92 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_PAGE_H__
+#define __GIS_PAGE_H__
+
+#include "gnome-initial-setup.h"
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_PAGE (gis_page_get_type ())
+#define GIS_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_PAGE, GisPage))
+#define GIS_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_PAGE, GisPageClass))
+#define GIS_IS_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_PAGE))
+#define GIS_IS_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_PAGE))
+#define GIS_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_PAGE, GisPageClass))
+
+typedef struct _GisPage GisPage;
+typedef struct _GisPageClass GisPageClass;
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (GisPage, g_object_unref)
+
+typedef void (* GisPageApplyCallback) (GisPage *page,
+ gboolean valid,
+ gpointer user_data);
+
+struct _GisPage
+{
+ AdwBin parent;
+
+ GisDriver *driver;
+};
+
+struct _GisPageClass
+{
+ AdwBinClass parent_class;
+ char *page_id;
+
+ void (*locale_changed) (GisPage *page);
+ gboolean (*apply) (GisPage *page,
+ GCancellable *cancellable);
+ gboolean (*save_data) (GisPage *page,
+ GError **error);
+ void (*shown) (GisPage *page);
+ void (*skip) (GisPage *page);
+};
+
+GType gis_page_get_type (void);
+
+char * gis_page_get_title (GisPage *page);
+void gis_page_set_title (GisPage *page, char *title);
+gboolean gis_page_get_complete (GisPage *page);
+void gis_page_set_complete (GisPage *page, gboolean complete);
+gboolean gis_page_get_skippable (GisPage *page);
+void gis_page_set_skippable (GisPage *page, gboolean skippable);
+gboolean gis_page_get_needs_accept (GisPage *page);
+void gis_page_set_needs_accept (GisPage *page, gboolean needs_accept);
+gboolean gis_page_get_has_forward (GisPage *page);
+void gis_page_set_has_forward (GisPage *page, gboolean has_forward);
+void gis_page_locale_changed (GisPage *page);
+void gis_page_apply_begin (GisPage *page, GisPageApplyCallback callback, gpointer user_data);
+void gis_page_apply_cancel (GisPage *page);
+void gis_page_apply_complete (GisPage *page, gboolean valid);
+gboolean gis_page_get_applying (GisPage *page);
+gboolean gis_page_save_data (GisPage *page,
+ GError **error);
+void gis_page_shown (GisPage *page);
+void gis_page_skip (GisPage *page);
+
+G_END_DECLS
+
+#endif /* __GIS_PAGE_H__ */
diff --git a/gnome-initial-setup/gis-pkexec.c b/gnome-initial-setup/gis-pkexec.c
new file mode 100644
index 0000000..9e1bfa9
--- /dev/null
+++ b/gnome-initial-setup/gis-pkexec.c
@@ -0,0 +1,59 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015-2016 Red Hat
+ * Copyright (C) 2015-2017 Endless OS Foundation LLC
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ *
+ * Written by:
+ * Dan Nicholson <dbn@endlessos.org>
+ * Will Thompson <wjt@endlessos.org>
+ */
+
+#include "gis-pkexec.h"
+
+gboolean
+gis_pkexec (const gchar *command,
+ const gchar *arg1,
+ const gchar *user,
+ GError **error)
+{
+ g_autoptr(GSubprocessLauncher) launcher = NULL;
+ g_autoptr(GSubprocess) process = NULL;
+ const gchar * const root_argv[] = { "pkexec", command, arg1, NULL };
+ const gchar * const user_argv[] = { "pkexec", "--user", user, command, arg1, NULL };
+ const gchar * const *argv = user == NULL ? root_argv : user_argv;
+
+ launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
+
+ /* pkexec won't let us run the program if $SHELL isn't in /etc/shells,
+ * so remove it from the environment.
+ */
+ g_subprocess_launcher_unsetenv (launcher, "SHELL");
+ process = g_subprocess_launcher_spawnv (launcher, argv, error);
+
+ if (!process) {
+ g_prefix_error (error, "Failed to create %s process: ", command);
+ return FALSE;
+ }
+
+ if (!g_subprocess_wait_check (process, NULL, error)) {
+ g_prefix_error (error, "%s failed: ", command);
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/gnome-initial-setup/gis-pkexec.h b/gnome-initial-setup/gis-pkexec.h
new file mode 100644
index 0000000..ff5a88c
--- /dev/null
+++ b/gnome-initial-setup/gis-pkexec.h
@@ -0,0 +1,41 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015-2016 Red Hat
+ * Copyright (C) 2015-2017 Endless OS Foundation LLC
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ *
+ * Written by:
+ * Dan Nicholson <dbn@endlessos.org>
+ * Will Thompson <wjt@endlessos.org>
+ */
+
+#ifndef __GIS_PKEXEC_H__
+#define __GIS_PKEXEC_H__
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+gboolean
+gis_pkexec (const gchar *command,
+ const gchar *arg1,
+ const gchar *user,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* __GIS_PKEXEC_H__ */
diff --git a/gnome-initial-setup/gnome-initial-setup-copy-worker.c b/gnome-initial-setup/gnome-initial-setup-copy-worker.c
new file mode 100644
index 0000000..5d8bab0
--- /dev/null
+++ b/gnome-initial-setup/gnome-initial-setup-copy-worker.c
@@ -0,0 +1,98 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copies settings installed from gnome-initial-setup and
+ * sticks them in the user's profile */
+
+#include <pwd.h>
+#include <string.h>
+#include <gio/gio.h>
+#include <stdlib.h>
+
+static char *
+get_gnome_initial_setup_home_dir (void)
+{
+ struct passwd pw, *pwp;
+ char buf[4096];
+
+ getpwnam_r ("gnome-initial-setup", &pw, buf, sizeof (buf), &pwp);
+ if (pwp != NULL)
+ return g_strdup (pwp->pw_dir);
+ else
+ return NULL;
+}
+
+static gboolean
+file_is_ours (GFile *file)
+{
+ GFileInfo *info;
+ uid_t uid;
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_UNIX_UID,
+ G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+ NULL,
+ NULL);
+ if (!info)
+ return FALSE;
+
+ uid = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_UNIX_UID);
+ g_object_unref (info);
+
+ return uid == geteuid ();
+}
+
+static void
+move_file_from_homedir (GFile *src_base,
+ GFile *dest_base,
+ const gchar *path)
+{
+ GFile *dest = g_file_get_child (dest_base, path);
+ GFile *dest_parent = g_file_get_parent (dest);
+ GFile *src = g_file_get_child (src_base, path);
+
+ GError *error = NULL;
+
+ g_file_make_directory_with_parents (dest_parent, NULL, NULL);
+
+ if (!g_file_move (src, dest, G_FILE_COPY_NONE,
+ NULL, NULL, NULL, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_warning ("Unable to move %s to %s: %s",
+ g_file_get_path (src),
+ g_file_get_path (dest),
+ error->message);
+ }
+ }
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ GFile *src;
+ GFile *dest;
+ char *initial_setup_homedir;
+
+ initial_setup_homedir = get_gnome_initial_setup_home_dir ();
+ if (initial_setup_homedir == NULL)
+ exit (EXIT_SUCCESS);
+
+ src = g_file_new_for_path (initial_setup_homedir);
+
+ if (!g_file_query_exists (src, NULL) ||
+ !file_is_ours (src))
+ exit (EXIT_SUCCESS);
+
+ dest = g_file_new_for_path (g_get_home_dir ());
+
+#define FILE(path) \
+ move_file_from_homedir (src, dest, path);
+
+ FILE (".config/gnome-initial-setup-done");
+ FILE (".config/dconf/user");
+ FILE (".config/goa-1.0/accounts.conf");
+ FILE (".config/monitors.xml");
+ FILE (".local/share/keyrings/login.keyring");
+
+ return EXIT_SUCCESS;
+}
diff --git a/gnome-initial-setup/gnome-initial-setup.c b/gnome-initial-setup/gnome-initial-setup.c
new file mode 100644
index 0000000..113ded3
--- /dev/null
+++ b/gnome-initial-setup/gnome-initial-setup.c
@@ -0,0 +1,387 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include "gnome-initial-setup.h"
+
+#include <adwaita.h>
+#include <pwd.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <glib/gi18n.h>
+
+#include "pages/welcome/gis-welcome-page.h"
+#include "pages/language/gis-language-page.h"
+#include "pages/keyboard/gis-keyboard-page.h"
+#include "pages/network/gis-network-page.h"
+#include "pages/timezone/gis-timezone-page.h"
+#include "pages/privacy/gis-privacy-page.h"
+#include "pages/software/gis-software-page.h"
+#include "pages/goa/gis-goa-page.h"
+#include "pages/account/gis-account-pages.h"
+#include "pages/parental-controls/gis-parental-controls-page.h"
+#include "pages/password/gis-password-page.h"
+#include "pages/summary/gis-summary-page.h"
+
+#define VENDOR_PAGES_GROUP "pages"
+#define VENDOR_SKIP_KEY "skip"
+#define VENDOR_NEW_USER_ONLY_KEY "new_user_only"
+#define VENDOR_EXISTING_USER_ONLY_KEY "existing_user_only"
+
+static gboolean force_existing_user_mode;
+
+static GPtrArray *skipped_pages;
+
+typedef GisPage *(*PreparePage) (GisDriver *driver);
+
+typedef struct {
+ const gchar *page_id;
+ PreparePage prepare_page_func;
+ gboolean new_user_only;
+} PageData;
+
+#define PAGE(name, new_user_only) { #name, gis_prepare_ ## name ## _page, new_user_only }
+
+static PageData page_table[] = {
+ PAGE (welcome, FALSE),
+ PAGE (language, FALSE),
+ PAGE (keyboard, FALSE),
+ PAGE (network, FALSE),
+ PAGE (privacy, FALSE),
+ PAGE (timezone, TRUE),
+ PAGE (software, TRUE),
+ PAGE (goa, FALSE),
+ PAGE (account, TRUE),
+ PAGE (password, TRUE),
+#ifdef HAVE_PARENTAL_CONTROLS
+ PAGE (parental_controls, TRUE),
+ PAGE (parent_password, TRUE),
+#endif
+ PAGE (summary, FALSE),
+ { NULL },
+};
+
+#undef PAGE
+
+static gboolean
+should_skip_page (const gchar *page_id,
+ gchar **skip_pages)
+{
+ guint i = 0;
+
+ /* special case welcome. We only want to show it if language
+ * is skipped
+ */
+ if (strcmp (page_id, "welcome") == 0)
+ return !should_skip_page ("language", skip_pages);
+
+ /* check through our skip pages list for pages we don't want */
+ if (skip_pages) {
+ while (skip_pages[i]) {
+ if (g_strcmp0 (skip_pages[i], page_id) == 0)
+ return TRUE;
+ i++;
+ }
+ }
+
+ return FALSE;
+}
+
+static gchar **
+strv_append (gchar **a,
+ gchar **b)
+{
+ guint n = g_strv_length (a);
+ guint m = g_strv_length (b);
+
+ a = g_renew (gchar *, a, n + m + 1);
+ for (guint i = 0; i < m; i++)
+ a[n + i] = g_strdup (b[i]);
+ a[n + m] = NULL;
+
+ return a;
+}
+
+static gchar **
+pages_to_skip_from_file (GisDriver *driver,
+ gboolean is_new_user)
+{
+ GStrv skip_pages = NULL;
+ GStrv additional_skip_pages = NULL;
+
+ /* This code will read the keyfile containing vendor customization options and
+ * look for options under the "pages" group, and supports the following keys:
+ * - skip (optional): list of pages to be skipped always
+ * - new_user_only (optional): list of pages to be skipped in existing user mode
+ * - existing_user_only (optional): list of pages to be skipped in new user mode
+ *
+ * This is how this file might look on a vendor image:
+ *
+ * [pages]
+ * skip=timezone
+ * existing_user_only=language;keyboard
+ */
+
+ skip_pages = gis_driver_conf_get_string_list (driver, VENDOR_PAGES_GROUP,
+ VENDOR_SKIP_KEY, NULL);
+ additional_skip_pages =
+ gis_driver_conf_get_string_list (driver, VENDOR_PAGES_GROUP,
+ is_new_user ? VENDOR_EXISTING_USER_ONLY_KEY : VENDOR_NEW_USER_ONLY_KEY,
+ NULL);
+
+ if (!skip_pages && additional_skip_pages) {
+ skip_pages = additional_skip_pages;
+ } else if (skip_pages && additional_skip_pages) {
+ skip_pages = strv_append (skip_pages, additional_skip_pages);
+ g_strfreev (additional_skip_pages);
+ }
+
+ return skip_pages;
+}
+
+static void
+destroy_pages_after (GisAssistant *assistant,
+ GisPage *page)
+{
+ GList *pages, *l, *next;
+
+ pages = gis_assistant_get_all_pages (assistant);
+
+ for (l = pages; l != NULL; l = l->next)
+ if (l->data == page)
+ break;
+
+ l = l->next;
+ for (; l != NULL; l = next) {
+ next = l->next;
+ gis_assistant_remove_page (assistant, l->data);
+ }
+}
+
+static void
+destroy_page (gpointer data)
+{
+ GtkWidget *assistant;
+ GisPage *page;
+
+ page = data;
+ assistant = gtk_widget_get_ancestor (GTK_WIDGET (page), GIS_TYPE_ASSISTANT);
+
+ if (assistant)
+ gis_assistant_remove_page (GIS_ASSISTANT (assistant), page);
+}
+
+static void
+rebuild_pages_cb (GisDriver *driver)
+{
+ PageData *page_data;
+ GisPage *page;
+ GisAssistant *assistant;
+ GisPage *current_page;
+ gchar **skip_pages;
+ gboolean is_new_user, skipped;
+
+ assistant = gis_driver_get_assistant (driver);
+ current_page = gis_assistant_get_current_page (assistant);
+ page_data = page_table;
+
+ g_ptr_array_free (skipped_pages, TRUE);
+ skipped_pages = g_ptr_array_new_with_free_func (destroy_page);
+
+ if (current_page != NULL) {
+ destroy_pages_after (assistant, current_page);
+
+ for (page_data = page_table; page_data->page_id != NULL; ++page_data)
+ if (g_str_equal (page_data->page_id, GIS_PAGE_GET_CLASS (current_page)->page_id))
+ break;
+
+ ++page_data;
+ }
+
+ is_new_user = (gis_driver_get_mode (driver) == GIS_DRIVER_MODE_NEW_USER);
+ skip_pages = pages_to_skip_from_file (driver, is_new_user);
+
+ for (; page_data->page_id != NULL; ++page_data) {
+ skipped = FALSE;
+
+ if ((page_data->new_user_only && !is_new_user) ||
+ (should_skip_page (page_data->page_id, skip_pages)))
+ skipped = TRUE;
+
+ page = page_data->prepare_page_func (driver);
+ if (!page)
+ continue;
+
+ if (skipped) {
+ gis_page_skip (page);
+ g_ptr_array_add (skipped_pages, page);
+ } else {
+ gis_driver_add_page (driver, page);
+ }
+ }
+
+ g_strfreev (skip_pages);
+}
+
+static GisDriverMode
+get_mode (void)
+{
+ if (force_existing_user_mode)
+ return GIS_DRIVER_MODE_EXISTING_USER;
+ else
+ return GIS_DRIVER_MODE_NEW_USER;
+}
+
+static gboolean
+initial_setup_disabled_by_anaconda (void)
+{
+ const gchar *file_name = SYSCONFDIR "/sysconfig/anaconda";
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GKeyFile) key_file = g_key_file_new ();
+
+ if (!g_key_file_load_from_file (key_file, file_name, G_KEY_FILE_NONE, &error)) {
+ if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT) &&
+ !g_error_matches (error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND)) {
+ g_warning ("Could not read %s: %s", file_name, error->message);
+ }
+ return FALSE;
+ }
+
+ return g_key_file_get_boolean (key_file, "General", "post_install_tools_disabled", NULL);
+}
+
+int
+main (int argc, char *argv[])
+{
+ GisDriver *driver;
+ int status;
+ GOptionContext *context;
+ GisDriverMode mode;
+
+ GOptionEntry entries[] = {
+ { "existing-user", 0, 0, G_OPTION_ARG_NONE, &force_existing_user_mode,
+ _("Force existing user mode"), NULL },
+ { NULL }
+ };
+
+ g_unsetenv ("GIO_USE_VFS");
+
+ /* By default, libadwaita reads settings from the Settings portal, which causes
+ * the portal to be started, which causes gnome-keyring to be started. This
+ * interferes with our attempt below to manually start gnome-keyring and set
+ * the login keyring password to a well-known value, which we overwrite with
+ * the user's password once they choose one.
+ */
+ g_setenv ("ADW_DISABLE_PORTAL", "1", TRUE);
+
+ context = g_option_context_new (_("— GNOME initial setup"));
+ g_option_context_add_main_entries (context, entries, NULL);
+
+ g_option_context_parse (context, &argc, &argv, NULL);
+
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ g_message ("Starting gnome-initial-setup");
+ if (gis_get_mock_mode ())
+ g_message ("Mock mode: changes will not be saved to disk");
+ else
+ g_message ("Production mode: changes will be saved to disk");
+
+ skipped_pages = g_ptr_array_new_with_free_func (destroy_page);
+ mode = get_mode ();
+
+ /* When we are running as the gnome-initial-setup user we
+ * dont have a normal user session and need to initialize
+ * the keyring manually so that we can pass the credentials
+ * along to the new user in the handoff.
+ */
+ if (mode == GIS_DRIVER_MODE_NEW_USER && !gis_get_mock_mode ())
+ gis_ensure_login_keyring ();
+
+ driver = gis_driver_new (mode);
+ adw_style_manager_set_color_scheme (adw_style_manager_get_default (),
+ ADW_COLOR_SCHEME_PREFER_LIGHT);
+
+ /* On first login, GNOME Shell offers to run a tour. If we also run Initial
+ * Setup, the two immovable, centred windows will sit atop one another.
+ * Until we have the ability to run Initial Setup in the "kiosk" mode, like
+ * it does in new-user mode, disable Initial Setup for existing users.
+ *
+ * https://gitlab.gnome.org/GNOME/gnome-initial-setup/-/issues/120#note_1019004
+ * https://gitlab.gnome.org/GNOME/gnome-initial-setup/-/issues/12
+ */
+ if (mode == GIS_DRIVER_MODE_EXISTING_USER) {
+ g_message ("Skipping gnome-initial-setup for existing user");
+ gis_ensure_stamp_files (driver);
+ exit (EXIT_SUCCESS);
+ }
+
+ /* We only do this in existing-user mode, because if gdm launches us
+ * in new-user mode and we just exit, gdm's special g-i-s session
+ * never terminates. */
+ if (initial_setup_disabled_by_anaconda () &&
+ mode == GIS_DRIVER_MODE_EXISTING_USER) {
+ gis_ensure_stamp_files (driver);
+ exit (EXIT_SUCCESS);
+ }
+
+ g_signal_connect (driver, "rebuild-pages", G_CALLBACK (rebuild_pages_cb), NULL);
+ status = g_application_run (G_APPLICATION (driver), argc, argv);
+
+ g_ptr_array_free (skipped_pages, TRUE);
+
+ g_object_unref (driver);
+ g_option_context_free (context);
+ return status;
+}
+
+void
+gis_ensure_stamp_files (GisDriver *driver)
+{
+ g_autofree gchar *done_file = NULL;
+ g_autoptr(GError) error = NULL;
+
+ done_file = g_build_filename (g_get_user_config_dir (), "gnome-initial-setup-done", NULL);
+ if (!g_file_set_contents (done_file, "yes", -1, &error)) {
+ g_warning ("Unable to create %s: %s", done_file, error->message);
+ g_clear_error (&error);
+ }
+}
+
+/**
+ * gis_get_mock_mode:
+ *
+ * Gets whether gnome-initial-setup has been built for development, and hence
+ * shouldn’t permanently change any system configuration.
+ *
+ * By default, mock mode is enabled when running in a build environment. This
+ * heuristic may be changed in future.
+ *
+ * Returns: %TRUE if in mock mode, %FALSE otherwise
+ */
+gboolean
+gis_get_mock_mode (void)
+{
+ return (g_getenv ("UNDER_JHBUILD") != NULL);
+}
diff --git a/gnome-initial-setup/gnome-initial-setup.h b/gnome-initial-setup/gnome-initial-setup.h
new file mode 100644
index 0000000..8880634
--- /dev/null
+++ b/gnome-initial-setup/gnome-initial-setup.h
@@ -0,0 +1,43 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GNOME_INITIAL_SETUP_H__
+#define __GNOME_INITIAL_SETUP_H__
+
+#include <gtk/gtk.h>
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+
+typedef struct _GisDriver GisDriver;
+typedef struct _GisAssistant GisAssistant;
+typedef struct _GisPage GisPage;
+
+#include "gis-driver.h"
+#include "gis-assistant.h"
+#include "gis-page.h"
+#include "gis-pkexec.h"
+#include "gis-keyring.h"
+
+void gis_ensure_stamp_files (GisDriver *driver);
+gboolean gis_get_mock_mode (void);
+
+#endif /* __GNOME_INITIAL_SETUP_H__ */
+
diff --git a/gnome-initial-setup/meson.build b/gnome-initial-setup/meson.build
new file mode 100644
index 0000000..a3dd513
--- /dev/null
+++ b/gnome-initial-setup/meson.build
@@ -0,0 +1,89 @@
+sources = []
+
+resources = gnome.compile_resources(
+ 'gis-assistant-resources',
+ files('gis-assistant.gresource.xml'),
+ c_name: 'gis_assistant'
+)
+
+sources += [
+ resources,
+ 'cc-common-language.c',
+ 'gnome-initial-setup.c',
+ 'gis-assistant.c',
+ 'gis-page.c',
+ 'gis-page-header.c',
+ 'gis-pkexec.c',
+ 'gis-driver.c',
+ 'gis-keyring.c',
+ 'gnome-initial-setup.h',
+ 'gis-assistant.h',
+ 'gis-page.h',
+ 'gis-page-header.h',
+ 'gis-pkexec.h',
+ 'gis-driver.h',
+ 'gis-keyring.h'
+]
+
+geocode_glib_2_dep = dependency(
+ 'geocode-glib-2.0',
+ fallback: ['geocode-glib', 'geocode_glib_dep'],
+ default_options: [
+ 'enable-gtk-doc=false',
+ 'enable-installed-tests=false',
+ 'enable-introspection=false',
+ 'soup2=false',
+ ],
+)
+
+gweather_dep = dependency('gweather4')
+
+subdir('pages')
+
+dependencies = [
+ dependency ('libnm', version: '>= 1.2'),
+ dependency ('libnma-gtk4', version: '>= 1.0'),
+ dependency ('polkit-gobject-1', version: '>= 0.103'),
+ dependency ('accountsservice'),
+ geocode_glib_2_dep,
+ dependency ('gnome-desktop-4'),
+ dependency ('gsettings-desktop-schemas', version: '>= 3.37.1'),
+ dependency ('fontconfig'),
+ dependency ('goa-1.0'),
+ dependency ('gtk4', version: '>= 4.6'),
+ dependency ('glib-2.0', version: '>= 2.63.1'),
+ dependency ('gio-unix-2.0', version: '>= 2.53.0'),
+ dependency ('gdm', version: '>= 3.8.3'),
+ gweather_dep,
+ dependency ('libgeoclue-2.0', version: '>= 2.3.1'),
+ cc.find_library('m', required: false),
+ dependency ('pango', version: '>= 1.32.5'),
+ dependency ('json-glib-1.0'),
+ dependency ('krb5'),
+ dependency ('libsecret-1', version: '>= 0.18.8'),
+ dependency ('pwquality'),
+ dependency ('rest-1.0'),
+ ibus_dep,
+ libmalcontent_dep,
+ libmalcontent_ui_dep,
+ libadwaita_dep,
+ webkitgtk_dep
+]
+
+executable(
+ 'gnome-initial-setup',
+ sources,
+ include_directories: config_h_dir,
+ dependencies: dependencies,
+ install: true,
+ install_dir: get_option('libexecdir')
+)
+
+executable(
+ 'gnome-initial-setup-copy-worker',
+ ['gnome-initial-setup-copy-worker.c'],
+ include_directories: config_h_dir,
+ dependencies: dependencies,
+ install: true,
+ install_dir: get_option('libexecdir')
+)
diff --git a/gnome-initial-setup/pages/account/account.gresource.xml b/gnome-initial-setup/pages/account/account.gresource.xml
new file mode 100644
index 0000000..d698ba9
--- /dev/null
+++ b/gnome-initial-setup/pages/account/account.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-account-avatar-chooser.ui">gis-account-avatar-chooser.ui</file>
+ <file preprocess="xml-stripblanks" alias="gis-account-page.ui">gis-account-page.ui</file>
+ <file preprocess="xml-stripblanks" alias="gis-account-page-local.ui">gis-account-page-local.ui</file>
+ <file preprocess="xml-stripblanks" alias="gis-account-page-enterprise.ui">gis-account-page-enterprise.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui b/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui
new file mode 100644
index 0000000..dff58a1
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="UmPhotoDialog" parent="GtkPopover">
+ <property name="height-request">360</property>
+ <property name="width-request">480</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ <child>
+ <object class="GtkFlowBox" id="recent_pictures">
+ <property name="halign">start</property>
+ <property name="margin-top">20</property>
+ <property name="margin-start">20</property>
+ <property name="margin-end">20</property>
+ <property name="margin-bottom">0</property>
+ <property name="selection-mode">none</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFlowBox" id="flowbox">
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="selection-mode">none</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="halign">GTK_ALIGN_CENTER</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkButton" id="take_picture_button">
+ <property name="visible">False</property>
+ <property name="label" translatable="yes">Take a Picture…</property>
+ <signal name="clicked" handler="webcam_icon_selected" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.c b/gnome-initial-setup/pages/account/gis-account-page-enterprise.c
new file mode 100644
index 0000000..02a4f3b
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.c
@@ -0,0 +1,863 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include "gis-account-page-enterprise.h"
+#include "gnome-initial-setup.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include <act/act-user-manager.h>
+
+#include "um-realm-manager.h"
+#include "um-utils.h"
+
+#include "gis-page-header.h"
+
+static void join_show_prompt (GisAccountPageEnterprise *page,
+ GError *error);
+
+static void on_join_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void on_realm_joined (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data);
+
+struct _GisAccountPageEnterprise
+{
+ AdwBin parent;
+
+ GtkWidget *header;
+ GtkWidget *login;
+ GtkWidget *password;
+ GtkWidget *domain;
+ GtkWidget *domain_entry;
+ GtkTreeModel *realms_model;
+
+ GtkWidget *join_dialog;
+ GtkWidget *join_name;
+ GtkWidget *join_password;
+ GtkWidget *join_domain;
+ GtkWidget *join_computer;
+
+ ActUserManager *act_client;
+ ActUser *act_user;
+
+ guint realmd_watch;
+ UmRealmManager *realm_manager;
+ gboolean domain_chosen;
+
+ /* Valid during apply */
+ UmRealmObject *realm;
+ GCancellable *cancellable;
+ gboolean join_prompted;
+
+ GisPageApplyCallback apply_complete_callback;
+ gpointer apply_complete_data;
+};
+
+G_DEFINE_TYPE (GisAccountPageEnterprise, gis_account_page_enterprise, ADW_TYPE_BIN);
+
+enum {
+ VALIDATION_CHANGED,
+ USER_CACHED,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void
+clear_password_validation_error (GtkWidget *entry)
+{
+ gtk_widget_remove_css_class (entry, "error");
+ gtk_widget_set_tooltip_text (entry, NULL);
+}
+
+static void
+set_password_validation_error (GtkWidget *entry,
+ const gchar *text)
+{
+ gtk_widget_add_css_class (entry, "error");
+ gtk_widget_set_tooltip_text (entry, text);
+}
+
+static void
+validation_changed (GisAccountPageEnterprise *page)
+{
+ g_signal_emit (page, signals[VALIDATION_CHANGED], 0);
+}
+
+static void
+apply_complete (GisAccountPageEnterprise *page,
+ gboolean valid)
+{
+ page->apply_complete_callback (NULL, valid, page->apply_complete_data);
+}
+
+static void
+show_error_dialog (GisAccountPageEnterprise *page,
+ const gchar *message,
+ GError *error)
+{
+ GtkWidget *dialog;
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (page))),
+ GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
+ 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_window_destroy),
+ NULL);
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+gboolean
+gis_account_page_enterprise_validate (GisAccountPageEnterprise *page)
+{
+ const gchar *name;
+ gboolean valid_name;
+ gboolean valid_domain;
+ GtkTreeIter iter;
+
+ name = gtk_editable_get_text (GTK_EDITABLE (page->login));
+ valid_name = is_valid_name (name);
+
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (page->domain), &iter)) {
+ gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (page->domain)),
+ &iter, 0, &name, -1);
+ } else {
+ name = gtk_editable_get_text (GTK_EDITABLE (page->domain_entry));
+ }
+
+ valid_domain = is_valid_name (name);
+ return valid_name && valid_domain;
+}
+
+static void
+on_permit_user_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ UmRealmCommon *common;
+ GError *error = NULL;
+ gchar *login;
+
+ common = UM_REALM_COMMON (source);
+ um_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.
+ */
+ login = um_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (page->login)));
+ g_return_if_fail (login != NULL);
+
+ g_debug ("Caching remote user: %s", login);
+
+ page->act_user = act_user_manager_cache_user (page->act_client, login, NULL);
+ act_user_set_account_type (page->act_user, ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR);
+ g_signal_emit (page, signals[USER_CACHED], 0, page->act_user, gtk_editable_get_text (GTK_EDITABLE (page->password)));
+ apply_complete (page, TRUE);
+
+ g_free (login);
+ } else {
+ show_error_dialog (page, _("Failed to register account"), error);
+ g_message ("Couldn't permit logins on account: %s", error->message);
+ g_error_free (error);
+ apply_complete (page, FALSE);
+ }
+}
+
+static void
+enterprise_permit_user_login (GisAccountPageEnterprise *page, UmRealmObject *realm)
+{
+ UmRealmCommon *common;
+ gchar *login;
+ const gchar *add[2];
+ const gchar *remove[1];
+ GVariant *options;
+
+ common = um_realm_object_get_common (realm);
+
+ login = um_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (page->login)));
+ 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);
+
+ um_realm_common_call_change_login_policy (common, "",
+ add, remove, options,
+ page->cancellable,
+ on_permit_user_login,
+ page);
+
+ g_object_unref (common);
+ g_free (login);
+}
+
+static void
+on_set_static_hostname (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+ GVariant *retval;
+
+ retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error);
+ if (error != NULL) {
+ join_show_prompt (page, error);
+ g_error_free (error);
+ return;
+ }
+
+ g_variant_unref (retval);
+
+ /* Prompted for some admin credentials, try to use them to log in */
+ um_realm_login (page->realm,
+ gtk_editable_get_text (GTK_EDITABLE (page->join_name)),
+ gtk_editable_get_text (GTK_EDITABLE (page->join_password)),
+ page->cancellable, on_join_login, page);
+}
+
+static void
+on_join_response (GtkDialog *dialog,
+ gint response,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GDBusConnection *connection;
+ GError *error = NULL;
+ gchar hostname[128];
+ const gchar *name;
+
+ gtk_widget_hide (GTK_WIDGET (dialog));
+ if (response != GTK_RESPONSE_OK) {
+ apply_complete (page, FALSE);
+ return;
+ }
+
+ name = gtk_editable_get_text (GTK_EDITABLE (page->join_computer));
+ if (gethostname (hostname, sizeof (hostname)) == 0 &&
+ !g_str_equal (name, hostname)) {
+ g_debug ("Setting StaticHostname to '%s'", name);
+
+ connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, page->cancellable, &error);
+ if (error != NULL) {
+ apply_complete (page, FALSE);
+ g_warning ("Could not get DBus connection: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_dbus_connection_call (connection, "org.freedesktop.hostname1",
+ "/org/freedesktop/hostname1", "org.freedesktop.hostname1",
+ "SetStaticHostname",
+ g_variant_new ("(sb)", name, TRUE),
+ G_VARIANT_TYPE ("()"),
+ G_DBUS_CALL_FLAGS_NONE,
+ G_MAXINT, NULL, on_set_static_hostname, page);
+
+ } else {
+ name = gtk_editable_get_text (GTK_EDITABLE (page->join_name));
+ g_debug ("Logging in as admin user: %s", name);
+
+ /* Prompted for some admin credentials, try to use them to log in */
+ um_realm_login (page->realm, name,
+ gtk_editable_get_text (GTK_EDITABLE (page->join_password)),
+ NULL, on_join_login, page);
+ }
+}
+
+static void
+join_show_prompt (GisAccountPageEnterprise *page,
+ GError *error)
+{
+ UmRealmKerberosMembership *membership;
+ UmRealmKerberos *kerberos;
+ gchar hostname[128];
+ const gchar *name;
+
+ gtk_editable_set_text (GTK_EDITABLE (page->join_password), "");
+ gtk_widget_grab_focus (GTK_WIDGET (page->join_password));
+
+ kerberos = um_realm_object_get_kerberos (page->realm);
+ membership = um_realm_object_get_kerberos_membership (page->realm);
+
+ gtk_label_set_text (GTK_LABEL (page->join_domain),
+ um_realm_kerberos_get_domain_name (kerberos));
+
+ if (gethostname (hostname, sizeof (hostname)) == 0)
+ gtk_editable_set_text (GTK_EDITABLE (page->join_computer), hostname);
+
+ clear_entry_validation_error (GTK_ENTRY (page->join_name));
+ clear_password_validation_error (page->join_password);
+
+ if (!page->join_prompted) {
+ name = um_realm_kerberos_membership_get_suggested_administrator (membership);
+ if (name && !g_str_equal (name, "")) {
+ g_debug ("Suggesting admin user: %s", name);
+ gtk_editable_set_text (GTK_EDITABLE (page->join_name), name);
+ } else {
+ gtk_widget_grab_focus (GTK_WIDGET (page->join_name));
+ }
+
+ } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME)) {
+ g_debug ("Bad host name: %s", error->message);
+ set_entry_validation_error (GTK_ENTRY (page->join_computer), error->message);
+
+ } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD)) {
+ g_debug ("Bad admin password: %s", error->message);
+ set_password_validation_error (page->join_password, error->message);
+
+ } else {
+ g_debug ("Admin login failure: %s", error->message);
+ g_dbus_error_strip_remote_error (error);
+ set_entry_validation_error (GTK_ENTRY (page->join_name), error->message);
+ }
+
+ g_debug ("Showing admin password dialog");
+ gtk_window_set_transient_for (GTK_WINDOW (page->join_dialog),
+ GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (page))));
+ gtk_window_set_modal (GTK_WINDOW (page->join_dialog), TRUE);
+ gtk_window_present (GTK_WINDOW (page->join_dialog));
+
+ page->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)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+ GBytes *creds;
+
+ um_realm_login_finish (page->realm, result, &creds, &error);
+
+ /* Logged in as admin successfully, use creds to join domain */
+ if (error == NULL) {
+ if (!um_realm_join_as_admin (page->realm,
+ gtk_editable_get_text (GTK_EDITABLE (page->join_name)),
+ gtk_editable_get_text (GTK_EDITABLE (page->join_password)),
+ creds, NULL, on_realm_joined, page)) {
+ show_error_dialog (page, _("No supported way to authenticate with this domain"), NULL);
+ g_message ("Authenticating as admin is not supported by the realm");
+ }
+
+ g_bytes_unref (creds);
+
+ /* Couldn't login as admin, show prompt again */
+ } else {
+ join_show_prompt (page, error);
+ g_message ("Couldn't log in as admin to join domain: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+on_realm_joined (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+
+ um_realm_join_finish (page->realm, result, &error);
+
+ /* Yay, joined the domain, register the user locally */
+ if (error == NULL) {
+ g_debug ("Joining realm completed successfully");
+ enterprise_permit_user_login (page, page->realm);
+
+ /* Credential failure while joining domain, prompt for admin creds */
+ } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN) ||
+ g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD) ||
+ g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME)) {
+ g_debug ("Joining realm failed due to credentials or host name");
+
+ join_show_prompt (page, error);
+
+ /* Other failure */
+ } else {
+ show_error_dialog (page, _("Failed to join domain"), error);
+ g_message ("Failed to join the domain: %s", error->message);
+ apply_complete (page, FALSE);
+ }
+
+ g_clear_error (&error);
+}
+
+static void
+on_realm_login (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+ GBytes *creds = NULL;
+
+ um_realm_login_finish (page->realm, result, &creds, &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, UM_REALM_ERROR, UM_REALM_ERROR_CANNOT_AUTH)) {
+ g_clear_error (&error);
+ creds = NULL;
+ }
+
+ if (error == NULL) {
+
+ /* Already joined to the domain, just register this user */
+ if (um_realm_is_configured (page->realm)) {
+ g_debug ("Already joined to this realm");
+ enterprise_permit_user_login (page, page->realm);
+
+ /* Join the domain, try using the user's creds */
+ } else if (creds == NULL ||
+ !um_realm_join_as_user (page->realm,
+ gtk_editable_get_text (GTK_EDITABLE (page->login)),
+ gtk_editable_get_text (GTK_EDITABLE (page->password)),
+ creds, page->cancellable,
+ on_realm_joined,
+ page)) {
+
+ /* If we can't do user auth, try to authenticate as admin */
+ g_debug ("Cannot join with user credentials");
+
+ join_show_prompt (page, error);
+ }
+
+ g_bytes_unref (creds);
+
+ /* A problem with the user's login name or password */
+ } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN)) {
+ g_debug ("Problem with the user's login: %s", error->message);
+ set_entry_validation_error (GTK_ENTRY (page->login), error->message);
+ gtk_widget_grab_focus (page->login);
+ apply_complete (page, FALSE);
+
+ } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD)) {
+ g_debug ("Problem with the user's password: %s", error->message);
+ set_password_validation_error (page->password, error->message);
+ gtk_widget_grab_focus (page->password);
+ apply_complete (page, FALSE);
+
+ /* Other login failure */
+ } else {
+ show_error_dialog (page, _("Failed to log into domain"), error);
+ g_message ("Couldn't log in as user: %s", error->message);
+ apply_complete (page, FALSE);
+ }
+
+ g_clear_error (&error);
+}
+
+static void
+enterprise_check_login (GisAccountPageEnterprise *page)
+{
+
+ g_assert (page->realm);
+
+ um_realm_login (page->realm,
+ gtk_editable_get_text (GTK_EDITABLE (page->login)),
+ gtk_editable_get_text (GTK_EDITABLE (page->password)),
+ page->cancellable,
+ on_realm_login,
+ page);
+}
+
+static void
+on_realm_discover_input (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+ GList *realms;
+
+ realms = um_realm_manager_discover_finish (page->realm_manager,
+ result, &error);
+
+ /* Found a realm, log user into domain */
+ if (error == NULL) {
+ g_assert (realms != NULL);
+ page->realm = g_object_ref (realms->data);
+ enterprise_check_login (page);
+ g_list_free_full (realms, g_object_unref);
+
+ } else {
+ /* The domain is likely invalid */
+ g_dbus_error_strip_remote_error (error);
+ g_message ("Couldn't discover domain: %s", error->message);
+ gtk_widget_grab_focus (page->domain_entry);
+ set_entry_validation_error (GTK_ENTRY (page->domain_entry), error->message);
+ apply_complete (page, FALSE);
+ g_error_free (error);
+ }
+}
+
+static void
+enterprise_add_user (GisAccountPageEnterprise *page)
+{
+ GtkTreeIter iter;
+
+ page->join_prompted = FALSE;
+ g_clear_object (&page->realm);
+
+ /* Already know about this realm, try to login as user */
+ if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (page->domain), &iter)) {
+ gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (page->domain)),
+ &iter, 1, &page->realm, -1);
+ enterprise_check_login (page);
+
+ /* Something the user typed, we need to discover realm */
+ } else {
+ um_realm_manager_discover (page->realm_manager,
+ gtk_editable_get_text (GTK_EDITABLE (page->domain_entry)),
+ page->cancellable,
+ on_realm_discover_input,
+ page);
+ }
+}
+
+gboolean
+gis_account_page_enterprise_apply (GisAccountPageEnterprise *page,
+ GCancellable *cancellable,
+ GisPageApplyCallback callback,
+ gpointer data)
+{
+ GisPage *account_page = GIS_PAGE (data);
+
+ /* Parental controls are not enterprise ready. It’s possible for them to have
+ * been enabled if the user enabled them, applied the account-local page, and
+ * then went back and decided to go all enterprise instead. */
+ gis_driver_set_parental_controls_enabled (account_page->driver, FALSE);
+
+ page->apply_complete_callback = callback;
+ page->apply_complete_data = data;
+ page->cancellable = g_object_ref (cancellable);
+ enterprise_add_user (page);
+ return TRUE;
+}
+
+static gchar *
+realm_get_name (UmRealmObject *realm)
+{
+ UmRealmCommon *common;
+ gchar *name;
+
+ common = um_realm_object_get_common (realm);
+ name = g_strdup (um_realm_common_get_name (common));
+ g_object_unref (common);
+
+ return name;
+}
+
+static gboolean
+model_contains_realm (GtkTreeModel *model,
+ const gchar *realm_name)
+{
+ gboolean contains = FALSE;
+ GtkTreeIter iter;
+ gboolean match;
+ gchar *name;
+ gboolean ret;
+
+ 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);
+ contains = TRUE;
+ break;
+ }
+ ret = gtk_tree_model_iter_next (model, &iter);
+ }
+
+ return contains;
+}
+
+static void
+enterprise_add_realm (GisAccountPageEnterprise *page,
+ UmRealmObject *realm)
+{
+ GtkTreeIter iter;
+ gchar *name;
+
+ name = realm_get_name (realm);
+
+ /*
+ * Don't add a second realm if we already have one with this name.
+ * Sometimes realmd returns two 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.
+ */
+
+ if (!model_contains_realm (GTK_TREE_MODEL (page->realms_model), name)) {
+ gtk_list_store_append (GTK_LIST_STORE (page->realms_model), &iter);
+ gtk_list_store_set (GTK_LIST_STORE (page->realms_model), &iter,
+ 0, name,
+ 1, realm,
+ -1);
+
+ if (!page->domain_chosen && um_realm_is_configured (realm))
+ gtk_combo_box_set_active_iter (GTK_COMBO_BOX (page->domain), &iter);
+
+ g_debug ("added realm to drop down: %s %s", name,
+ g_dbus_object_get_object_path (G_DBUS_OBJECT (realm)));
+ }
+
+ g_free (name);
+}
+
+static void
+on_manager_realm_added (UmRealmManager *manager,
+ UmRealmObject *realm,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ enterprise_add_realm (page, realm);
+}
+
+static void
+on_realm_manager_created (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ GError *error = NULL;
+ GList *realms, *l;
+
+ g_clear_object (&page->realm_manager);
+ page->realm_manager = um_realm_manager_new_finish (result, &error);
+
+ if (error != NULL) {
+ g_warning ("Couldn't contact realmd service: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* Lookup all the realm objects */
+ realms = um_realm_manager_get_realms (page->realm_manager);
+ for (l = realms; l != NULL; l = g_list_next (l))
+ enterprise_add_realm (page, l->data);
+
+ g_list_free (realms);
+ g_signal_connect (page->realm_manager, "realm-added",
+ G_CALLBACK (on_manager_realm_added), page);
+
+ /* When no realms try to discover a sensible default, triggers realm-added signal */
+ um_realm_manager_discover (page->realm_manager, "", NULL, NULL, NULL);
+ gtk_widget_set_visible (GTK_WIDGET (page), TRUE);
+}
+
+static void
+on_realmd_appeared (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ um_realm_manager_new (NULL, on_realm_manager_created, page);
+}
+
+static void
+on_realmd_disappeared (GDBusConnection *unused1,
+ const gchar *unused2,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+
+ if (page->realm_manager != NULL) {
+ g_signal_handlers_disconnect_by_func (page->realm_manager,
+ on_manager_realm_added,
+ page);
+ g_clear_object (&page->realm_manager);
+ }
+
+ gtk_widget_set_visible (GTK_WIDGET (page), FALSE);
+}
+
+static void
+on_domain_changed (GtkComboBox *widget,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+
+ page->domain_chosen = TRUE;
+ validation_changed (page);
+ clear_entry_validation_error (GTK_ENTRY (gtk_combo_box_get_child (widget)));
+}
+
+static void
+on_entry_changed (GtkEditable *editable,
+ gpointer user_data)
+{
+ GisAccountPageEnterprise *page = user_data;
+ validation_changed (page);
+ clear_entry_validation_error (GTK_ENTRY (editable));
+}
+
+static void
+on_password_changed (GtkEditable *editable,
+ gpointer user_data)
+{
+ clear_password_validation_error (GTK_WIDGET (editable));
+}
+
+static void
+gis_account_page_enterprise_realize (GtkWidget *widget)
+{
+ GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (widget);
+ GtkWidget *gis_page;
+
+ gis_page = gtk_widget_get_ancestor (widget, GIS_TYPE_PAGE);
+ g_object_bind_property (gis_page, "small-screen",
+ page->header, "show-icon",
+ G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+
+ GTK_WIDGET_CLASS (gis_account_page_enterprise_parent_class)->realize (widget);
+}
+
+static void
+gis_account_page_enterprise_constructed (GObject *object)
+{
+ GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (object);
+
+ G_OBJECT_CLASS (gis_account_page_enterprise_parent_class)->constructed (object);
+
+ page->act_client = act_user_manager_get_default ();
+
+ page->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,
+ page, NULL);
+
+ g_signal_connect (page->join_dialog, "response",
+ G_CALLBACK (on_join_response), page);
+ g_signal_connect (page->domain, "changed",
+ G_CALLBACK (on_domain_changed), page);
+ g_signal_connect (page->login, "changed",
+ G_CALLBACK (on_entry_changed), page);
+ g_signal_connect (page->password, "changed",
+ G_CALLBACK (on_password_changed), page);
+ g_signal_connect (page->join_password, "changed",
+ G_CALLBACK (on_password_changed), page);
+}
+
+static void
+gis_account_page_enterprise_dispose (GObject *object)
+{
+ GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (object);
+
+ if (page->realmd_watch)
+ g_bus_unwatch_name (page->realmd_watch);
+
+ page->realmd_watch = 0;
+
+ g_cancellable_cancel (page->cancellable);
+
+ g_clear_object (&page->realm_manager);
+ g_clear_object (&page->realm);
+ g_clear_object (&page->cancellable);
+
+ G_OBJECT_CLASS (gis_account_page_enterprise_parent_class)->dispose (object);
+}
+
+static void
+gis_account_page_enterprise_class_init (GisAccountPageEnterpriseClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = gis_account_page_enterprise_constructed;
+ object_class->dispose = gis_account_page_enterprise_dispose;
+
+ widget_class->realize = gis_account_page_enterprise_realize;
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page-enterprise.ui");
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, login);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, password);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, domain);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, realms_model);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, header);
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_dialog);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_name);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_password);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_domain);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_computer);
+
+ signals[VALIDATION_CHANGED] = g_signal_new ("validation-changed", GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ signals[USER_CACHED] = g_signal_new ("user-cached", GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING);
+}
+
+static void
+gis_account_page_enterprise_init (GisAccountPageEnterprise *page)
+{
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ gtk_widget_init_template (GTK_WIDGET (page));
+
+ page->domain_entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->domain));
+}
+
+void
+gis_account_page_enterprise_shown (GisAccountPageEnterprise *page)
+{
+ gtk_widget_grab_focus (page->domain_entry);
+}
diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.h b/gnome-initial-setup/pages/account/gis-account-page-enterprise.h
new file mode 100644
index 0000000..c156f44
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.h
@@ -0,0 +1,42 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#pragma once
+
+#include <adwaita.h>
+
+/* For GisPageApplyCallback */
+#include "gis-page.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE (gis_account_page_enterprise_get_type ())
+G_DECLARE_FINAL_TYPE (GisAccountPageEnterprise, gis_account_page_enterprise, GIS, ACCOUNT_PAGE_ENTERPRISE, AdwBin)
+
+gboolean gis_account_page_enterprise_validate (GisAccountPageEnterprise *enterprise);
+gboolean gis_account_page_enterprise_apply (GisAccountPageEnterprise *enterprise,
+ GCancellable *cancellable,
+ GisPageApplyCallback callback,
+ gpointer data);
+void gis_account_page_enterprise_shown (GisAccountPageEnterprise *enterprise);
+
+G_END_DECLS
+
diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui b/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui
new file mode 100644
index 0000000..e5ef0db
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui
@@ -0,0 +1,280 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisAccountPageEnterprise" parent="AdwBin">
+ <child>
+ <object class="GtkBox" id="area">
+ <property name="orientation">vertical</property>
+ <property name="valign">fill</property>
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_bottom">26</property>
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Enterprise Login</property>
+ <property name="subtitle" 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="icon_name">dialog-password-symbolic</property>
+ <property name="show_icon">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="form">
+ <property name="row_spacing">12</property>
+ <property name="column_spacing">12</property>
+ <property name="margin_bottom">32</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Domain</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">domain</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Username</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">login</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label9">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">password</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">4</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="login">
+ <property name="hexpand">True</property>
+ <property name="max-length">255</property>
+ <property name="width-chars">25</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPasswordEntry" id="password">
+ <property name="hexpand">True</property>
+ <property name="width-chars">25</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">4</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="domain">
+ <property name="hexpand">True</property>
+ <property name="model">realms_model</property>
+ <property name="has_entry">True</property>
+ <property name="entry_text_column">0</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label10">
+ <property name="margin_bottom">12</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Enterprise domain or realm name</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="filler">
+ <layout>
+ <property name="column">2</property>
+ <property name="row">4</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkDialog" id="join_dialog">
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="title" translatable="yes">Domain Administrator Login</property>
+ <child internal-child="content_area">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel" id="label12">
+ <property name="xalign">0.5</property>
+ <property name="yalign">0</property>
+ <property name="wrap">True</property>
+ <property name="max-width-chars">60</property>
+ <property name="label" translatable="yes">In order to use enterprise logins, this computer needs to be enrolled in a domain. Please have your network administrator type the domain password here, and choose a unique computer name for your computer.</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <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="xalign">1</property>
+ <property name="label" translatable="yes">_Domain</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">join_domain</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="join_domain">
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="xalign">0</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label18">
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Computer</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">join_computer</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="join_computer">
+ <property name="hexpand">True</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <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>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="join_name">
+ <property name="hexpand">True</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label15">
+ <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>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPasswordEntry" id="join_password">
+ <property name="hexpand">True</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="button_cancel">
+ <property name="label">_Cancel</property>
+ <property name="use_underline">True</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="button_ok">
+ <property name="label" translatable="yes">C_ontinue</property>
+ <property name="use_underline">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5" default="true">button_ok</action-widget>
+ <action-widget response="-6">button_cancel</action-widget>
+ </action-widgets>
+ </object>
+ <object class="GtkListStore" id="realms_model">
+ <columns>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gobject -->
+ <column type="GObject"/>
+ </columns>
+ </object>
+</interface>
diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.c b/gnome-initial-setup/pages/account/gis-account-page-local.c
new file mode 100644
index 0000000..21bb597
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-local.c
@@ -0,0 +1,721 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include "gis-page.h"
+#include "gis-account-page-local.h"
+#include "gnome-initial-setup.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include <string.h>
+#include <act/act-user-manager.h>
+#include "um-utils.h"
+#include "um-photo-dialog.h"
+
+#include "gis-page-header.h"
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+
+#include <rest/rest-proxy.h>
+#include <json-glib/json-glib.h>
+
+#define VALIDATION_TIMEOUT 600
+
+struct _GisAccountPageLocal
+{
+ AdwBin parent;
+
+ GtkWidget *avatar_button;
+ GtkWidget *avatar_image;
+ GtkWidget *header;
+ GtkWidget *fullname_entry;
+ GtkWidget *username_combo;
+ GtkWidget *enable_parental_controls_box;
+ GtkWidget *enable_parental_controls_check_button;
+ gboolean has_custom_username;
+ GtkWidget *username_explanation;
+ UmPhotoDialog *photo_dialog;
+
+ gint timeout_id;
+
+ GdkPixbuf *avatar_pixbuf;
+ gchar *avatar_filename;
+
+ ActUserManager *act_client;
+
+ GoaClient *goa_client;
+
+ gboolean valid_name;
+ gboolean valid_username;
+ ActUserAccountType account_type;
+};
+
+G_DEFINE_TYPE (GisAccountPageLocal, gis_account_page_local, ADW_TYPE_BIN);
+
+enum {
+ VALIDATION_CHANGED,
+ MAIN_USER_CREATED,
+ PARENT_USER_CREATED,
+ CONFIRM,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void
+validation_changed (GisAccountPageLocal *page)
+{
+ g_signal_emit (page, signals[VALIDATION_CHANGED], 0);
+}
+
+static gboolean
+get_profile_sync (const gchar *access_token,
+ gchar **out_name,
+ gchar **out_picture,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GError *identity_error;
+ RestProxy *proxy;
+ RestProxyCall *call;
+ JsonParser *parser;
+ JsonObject *json_object;
+ gboolean ret;
+
+ ret = FALSE;
+
+ identity_error = NULL;
+ proxy = NULL;
+ call = NULL;
+ parser = NULL;
+
+ /* TODO: cancellable */
+
+ proxy = rest_proxy_new ("https://www.googleapis.com/oauth2/v2/userinfo", FALSE);
+ call = rest_proxy_new_call (proxy);
+ rest_proxy_call_set_method (call, "GET");
+ rest_proxy_call_add_param (call, "access_token", access_token);
+
+ if (!rest_proxy_call_sync (call, error))
+ goto out;
+
+ if (rest_proxy_call_get_status_code (call) != 200)
+ {
+ g_set_error (error,
+ GOA_ERROR,
+ GOA_ERROR_FAILED,
+ "Expected status 200 when requesting your identity, instead got status %d (%s)",
+ rest_proxy_call_get_status_code (call),
+ rest_proxy_call_get_status_message (call));
+ goto out;
+ }
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser,
+ rest_proxy_call_get_payload (call),
+ rest_proxy_call_get_payload_length (call),
+ &identity_error))
+ {
+ g_warning ("json_parser_load_from_data() failed: %s (%s, %d)",
+ identity_error->message,
+ g_quark_to_string (identity_error->domain),
+ identity_error->code);
+ g_set_error (error,
+ GOA_ERROR,
+ GOA_ERROR_FAILED,
+ "Could not parse response");
+ goto out;
+ }
+
+ ret = TRUE;
+
+ json_object = json_node_get_object (json_parser_get_root (parser));
+ if (out_name != NULL)
+ *out_name = g_strdup (json_object_get_string_member (json_object, "name"));
+
+ if (out_picture != NULL)
+ *out_picture = g_strdup (json_object_get_string_member (json_object, "picture"));
+
+ out:
+ g_clear_error (&identity_error);
+ if (call != NULL)
+ g_object_unref (call);
+ if (proxy != NULL)
+ g_object_unref (proxy);
+
+ return ret;
+}
+
+static void
+prepopulate_account_page (GisAccountPageLocal *page)
+{
+ gchar *name = NULL;
+ gchar *picture = NULL;
+ GdkPixbuf *pixbuf = NULL;
+
+ if (page->goa_client) {
+ GList *accounts, *l;
+ accounts = goa_client_get_accounts (page->goa_client);
+ for (l = accounts; l != NULL; l = l->next) {
+ GoaOAuth2Based *oa2;
+ oa2 = goa_object_get_oauth2_based (GOA_OBJECT (l->data));
+ if (oa2) {
+ gchar *token = NULL;
+ GError *error = NULL;
+ if (!goa_oauth2_based_call_get_access_token_sync (oa2, &token, NULL, NULL, &error))
+ {
+ g_warning ("Couldn't get oauth2 token: %s", error->message);
+ g_error_free (error);
+ }
+ else if (!get_profile_sync (token, &name, &picture, NULL, &error))
+ {
+ g_warning ("Couldn't get profile information: %s", error->message);
+ g_error_free (error);
+ }
+ /* FIXME: collect information from more than one account
+ * and present at least the pictures in the avatar chooser
+ */
+ break;
+ }
+ }
+ g_list_free_full (accounts, (GDestroyNotify) g_object_unref);
+ }
+
+ if (name) {
+ g_object_set (page->header, "subtitle", _("Please check the name and username. You can choose a picture too."), NULL);
+ gtk_editable_set_text (GTK_EDITABLE (page->fullname_entry), name);
+ }
+
+ if (picture) {
+ GFile *file;
+ GFileInputStream *stream;
+ GError *error = NULL;
+ file = g_file_new_for_uri (picture);
+ stream = g_file_read (file, NULL, &error);
+ if (!stream)
+ {
+ g_warning ("Failed to read picture %s: %s", picture, error->message);
+ g_error_free (error);
+ }
+ else
+ {
+ pixbuf = gdk_pixbuf_new_from_stream_at_scale (G_INPUT_STREAM (stream), -1, 96, TRUE, NULL, NULL);
+ g_object_unref (stream);
+ }
+ g_object_unref (file);
+ }
+
+ if (pixbuf) {
+ GdkPixbuf *rounded = round_image (pixbuf);
+
+ gtk_image_set_from_pixbuf (GTK_IMAGE (page->avatar_image), rounded);
+ g_object_unref (rounded);
+ page->avatar_pixbuf = pixbuf;
+ }
+
+ g_free (name);
+ g_free (picture);
+}
+
+static void
+accounts_changed (GoaClient *client, GoaObject *object, gpointer data)
+{
+ GisAccountPageLocal *page = data;
+
+ prepopulate_account_page (page);
+}
+
+static gboolean
+validate (GisAccountPageLocal *page)
+{
+ GtkWidget *entry;
+ const gchar *name, *username;
+ gboolean parental_controls_enabled;
+ gchar *tip;
+
+ g_clear_handle_id (&page->timeout_id, g_source_remove);
+
+ entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo));
+
+ name = gtk_editable_get_text (GTK_EDITABLE (page->fullname_entry));
+ username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (page->username_combo));
+#ifdef HAVE_PARENTAL_CONTROLS
+ parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (page->enable_parental_controls_check_button));
+#else
+ parental_controls_enabled = FALSE;
+#endif
+
+ page->valid_name = is_valid_name (name);
+ if (page->valid_name)
+ set_entry_validation_checkmark (GTK_ENTRY (page->fullname_entry));
+
+ page->valid_username = is_valid_username (username, parental_controls_enabled, &tip);
+ if (page->valid_username)
+ set_entry_validation_checkmark (GTK_ENTRY (entry));
+
+ gtk_label_set_text (GTK_LABEL (page->username_explanation), tip);
+ g_free (tip);
+
+ um_photo_dialog_generate_avatar (page->photo_dialog, name);
+
+ validation_changed (page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+on_focusout (GisAccountPageLocal *page)
+{
+ validate (page);
+
+ return FALSE;
+}
+
+static void
+fullname_changed (GtkWidget *w,
+ GParamSpec *pspec,
+ GisAccountPageLocal *page)
+{
+ GtkWidget *entry;
+ GtkTreeModel *model;
+ const char *name;
+
+ name = gtk_editable_get_text (GTK_EDITABLE (w));
+
+ entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo));
+ model = gtk_combo_box_get_model (GTK_COMBO_BOX (page->username_combo));
+
+ gtk_list_store_clear (GTK_LIST_STORE (model));
+
+ if ((name == NULL || strlen (name) == 0) && !page->has_custom_username) {
+ gtk_editable_set_text (GTK_EDITABLE (entry), "");
+ }
+ else if (name != NULL && strlen (name) != 0) {
+ generate_username_choices (name, GTK_LIST_STORE (model));
+ if (!page->has_custom_username)
+ gtk_combo_box_set_active (GTK_COMBO_BOX (page->username_combo), 0);
+ }
+
+ clear_entry_validation_error (GTK_ENTRY (w));
+
+ page->valid_name = FALSE;
+
+ /* username_changed() is called consequently due to changes */
+}
+
+static void
+username_changed (GtkComboBoxText *combo,
+ GisAccountPageLocal *page)
+{
+ GtkWidget *entry;
+ const gchar *username;
+
+ entry = gtk_combo_box_get_child (GTK_COMBO_BOX (combo));
+ username = gtk_editable_get_text (GTK_EDITABLE (entry));
+ if (*username == '\0')
+ page->has_custom_username = FALSE;
+ else if (gtk_widget_has_focus (entry) ||
+ gtk_combo_box_get_active (GTK_COMBO_BOX (page->username_combo)) > 0)
+ page->has_custom_username = TRUE;
+
+ clear_entry_validation_error (GTK_ENTRY (entry));
+
+ page->valid_username = FALSE;
+ validation_changed (page);
+
+ if (page->timeout_id != 0)
+ g_source_remove (page->timeout_id);
+ page->timeout_id = g_timeout_add (VALIDATION_TIMEOUT, (GSourceFunc)validate, page);
+}
+
+static void
+avatar_callback (GdkPixbuf *pixbuf,
+ const gchar *filename,
+ gpointer user_data)
+{
+ GisAccountPageLocal *page = user_data;
+ g_autoptr(GdkPixbuf) tmp = NULL;
+ g_autoptr(GdkPixbuf) rounded = NULL;
+
+ g_clear_object (&page->avatar_pixbuf);
+ g_clear_pointer (&page->avatar_filename, g_free);
+
+ if (pixbuf) {
+ page->avatar_pixbuf = g_object_ref (pixbuf);
+ rounded = round_image (pixbuf);
+ }
+ else if (filename) {
+ page->avatar_filename = g_strdup (filename);
+ tmp = gdk_pixbuf_new_from_file_at_size (filename, 96, 96, NULL);
+
+ if (tmp != NULL)
+ rounded = round_image (tmp);
+ }
+
+ if (rounded != NULL) {
+ gtk_image_set_from_pixbuf (GTK_IMAGE (page->avatar_image), rounded);
+ }
+ else {
+ /* Fallback. */
+ gtk_image_set_pixel_size (GTK_IMAGE (page->avatar_image), 96);
+ gtk_image_set_from_icon_name (GTK_IMAGE (page->avatar_image), "avatar-default-symbolic");
+ }
+}
+
+static void
+confirm (GisAccountPageLocal *page)
+{
+ if (gis_account_page_local_validate (page))
+ g_signal_emit (page, signals[CONFIRM], 0);
+}
+
+static void
+enable_parental_controls_check_button_toggled_cb (GtkCheckButton *check_button,
+ gpointer user_data)
+{
+ GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (user_data);
+ gboolean parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (page->enable_parental_controls_check_button));
+
+ /* This sets the account type of the main user. When we save_data(), we create
+ * two users if parental controls are enabled: the first user is always an
+ * admin, and the second user is the main user using this @account_type. */
+ page->account_type = parental_controls_enabled ? ACT_USER_ACCOUNT_TYPE_STANDARD : ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR;
+
+ validate (page);
+}
+
+static void
+track_focus_out (GisAccountPageLocal *page,
+ GtkWidget *widget)
+{
+ GtkEventController *focus_controller;
+
+ focus_controller = gtk_event_controller_focus_new ();
+ gtk_widget_add_controller (widget, focus_controller);
+
+ g_signal_connect_swapped (focus_controller, "leave", G_CALLBACK (on_focusout), page);
+}
+
+
+static void
+gis_account_page_local_constructed (GObject *object)
+{
+ GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (object);
+
+ G_OBJECT_CLASS (gis_account_page_local_parent_class)->constructed (object);
+
+ page->act_client = act_user_manager_get_default ();
+
+ g_signal_connect (page->fullname_entry, "notify::text",
+ G_CALLBACK (fullname_changed), page);
+ track_focus_out (page, page->fullname_entry);
+
+ g_signal_connect_swapped (page->fullname_entry, "activate",
+ G_CALLBACK (validate), page);
+ g_signal_connect (page->username_combo, "changed",
+ G_CALLBACK (username_changed), page);
+ track_focus_out (page, page->username_combo);
+
+ g_signal_connect_swapped (gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo)),
+ "activate", G_CALLBACK (confirm), page);
+ g_signal_connect_swapped (page->fullname_entry, "activate",
+ G_CALLBACK (confirm), page);
+ g_signal_connect (page->enable_parental_controls_check_button, "toggled",
+ G_CALLBACK (enable_parental_controls_check_button_toggled_cb), page);
+
+ /* Disable parental controls if support is not compiled in. */
+#ifndef HAVE_PARENTAL_CONTROLS
+ gtk_widget_hide (page->enable_parental_controls_box);
+#endif
+
+ page->valid_name = FALSE;
+ page->valid_username = FALSE;
+
+ /* FIXME: change this for a large deployment scenario; maybe through a GSetting? */
+ page->account_type = ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR;
+
+ g_object_set (page->header, "subtitle", _("We need a few details to complete setup."), NULL);
+ gtk_editable_set_text (GTK_EDITABLE (page->fullname_entry), "");
+ gtk_list_store_clear (GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (page->username_combo))));
+ page->has_custom_username = FALSE;
+
+ gtk_image_set_pixel_size (GTK_IMAGE (page->avatar_image), 96);
+ gtk_image_set_from_icon_name (GTK_IMAGE (page->avatar_image), "avatar-default-symbolic");
+
+ page->goa_client = goa_client_new_sync (NULL, NULL);
+ if (page->goa_client) {
+ g_signal_connect (page->goa_client, "account-added",
+ G_CALLBACK (accounts_changed), page);
+ g_signal_connect (page->goa_client, "account-removed",
+ G_CALLBACK (accounts_changed), page);
+ prepopulate_account_page (page);
+ }
+
+ page->photo_dialog = um_photo_dialog_new (avatar_callback, page);
+ um_photo_dialog_generate_avatar (page->photo_dialog, "");
+ gtk_menu_button_set_popover (GTK_MENU_BUTTON (page->avatar_button),
+ GTK_WIDGET (page->photo_dialog));
+
+ validate (page);
+}
+
+static void
+gis_account_page_local_dispose (GObject *object)
+{
+ GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (object);
+
+ g_clear_object (&page->goa_client);
+ g_clear_object (&page->avatar_pixbuf);
+ g_clear_pointer (&page->avatar_filename, g_free);
+ g_clear_handle_id (&page->timeout_id, g_source_remove);
+
+ G_OBJECT_CLASS (gis_account_page_local_parent_class)->dispose (object);
+}
+
+static void
+set_user_avatar (GisAccountPageLocal *page,
+ ActUser *user)
+{
+ GFile *file = NULL;
+ GFileIOStream *io_stream = NULL;
+ GOutputStream *stream = NULL;
+ GError *error = NULL;
+
+ if (page->avatar_filename != NULL) {
+ act_user_set_icon_file (user, page->avatar_filename);
+ return;
+ }
+
+ if (page->avatar_pixbuf == NULL) {
+ return;
+ }
+
+ file = g_file_new_tmp ("usericonXXXXXX", &io_stream, &error);
+ if (error != NULL)
+ goto out;
+
+ stream = g_io_stream_get_output_stream (G_IO_STREAM (io_stream));
+ if (!gdk_pixbuf_save_to_stream (page->avatar_pixbuf, stream, "png", NULL, &error, NULL))
+ goto out;
+
+ act_user_set_icon_file (user, g_file_get_path (file));
+
+ out:
+ if (error != NULL) {
+ g_warning ("failed to save image: %s", error->message);
+ g_error_free (error);
+ }
+ g_clear_object (&io_stream);
+ g_clear_object (&file);
+}
+
+static gboolean
+local_create_user (GisAccountPageLocal *local,
+ GisPage *page,
+ GError **error)
+{
+ const gchar *username;
+ const gchar *fullname;
+ gboolean parental_controls_enabled;
+ g_autoptr(ActUser) main_user = NULL;
+ g_autoptr(ActUser) parent_user = NULL;
+
+ username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (local->username_combo));
+ fullname = gtk_editable_get_text (GTK_EDITABLE (local->fullname_entry));
+ parental_controls_enabled = gis_driver_get_parental_controls_enabled (page->driver);
+
+ /* Always create the admin user first, in case of failure part-way through
+ * this function, which would leave us with no admin user at all. */
+ if (parental_controls_enabled) {
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GDBusConnection) connection = NULL;
+ const gchar *parent_username = "administrator";
+ const gchar *parent_fullname = _("Administrator");
+
+ parent_user = act_user_manager_create_user (local->act_client, parent_username, parent_fullname, ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR, error);
+ if (parent_user == NULL)
+ {
+ g_prefix_error (error,
+ _("Failed to create user '%s': "),
+ parent_username);
+ return FALSE;
+ }
+
+ /* Make the admin account usable in case g-i-s crashes. If all goes
+ * according to plan a password will be set on it in gis-password-page.c */
+ act_user_set_password_mode (parent_user, ACT_USER_PASSWORD_MODE_SET_AT_LOGIN);
+
+ /* Mark it as the parent user account.
+ * FIXME: This should be async. */
+ connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error);
+ if (connection != NULL) {
+ g_dbus_connection_call_sync (connection,
+ "org.freedesktop.Accounts",
+ act_user_get_object_path (parent_user),
+ "org.freedesktop.DBus.Properties",
+ "Set",
+ g_variant_new ("(ssv)",
+ "com.endlessm.ParentalControls.AccountInfo",
+ "IsParent",
+ g_variant_new_boolean (TRUE)),
+ NULL, /* reply type */
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* default timeout */
+ NULL, /* cancellable */
+ &local_error);
+ }
+ if (local_error != NULL) {
+ /* Make this non-fatal, since the correct accounts-service interface
+ * might not be installed, depending on which version of malcontent is installed. */
+ g_warning ("Failed to mark user as parent: %s", local_error->message);
+ g_clear_error (&local_error);
+ }
+
+ g_signal_emit (local, signals[PARENT_USER_CREATED], 0, parent_user, "");
+ }
+
+ /* Now create the main user. */
+ main_user = act_user_manager_create_user (local->act_client, username, fullname, local->account_type, error);
+ if (main_user == NULL)
+ {
+ g_prefix_error (error,
+ _("Failed to create user '%s': "),
+ username);
+ /* FIXME: Could we delete the @parent_user at this point to reset the state
+ * and allow g-i-s to be run again after a reboot? */
+ return FALSE;
+ }
+
+ set_user_avatar (local, main_user);
+
+ g_signal_emit (local, signals[MAIN_USER_CREATED], 0, main_user, "");
+
+ return TRUE;
+}
+
+static void
+gis_account_page_local_class_init (GisAccountPageLocalClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page-local.ui");
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, avatar_button);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, avatar_image);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, header);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, fullname_entry);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, username_combo);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, username_explanation);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, enable_parental_controls_box);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, enable_parental_controls_check_button);
+
+ object_class->constructed = gis_account_page_local_constructed;
+ object_class->dispose = gis_account_page_local_dispose;
+
+ signals[VALIDATION_CHANGED] = g_signal_new ("validation-changed", GIS_TYPE_ACCOUNT_PAGE_LOCAL,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+
+ signals[MAIN_USER_CREATED] = g_signal_new ("main-user-created", GIS_TYPE_ACCOUNT_PAGE_LOCAL,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING);
+
+ signals[PARENT_USER_CREATED] = g_signal_new ("parent-user-created", GIS_TYPE_ACCOUNT_PAGE_LOCAL,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING);
+
+ signals[CONFIRM] = g_signal_new ("confirm", GIS_TYPE_ACCOUNT_PAGE_LOCAL,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+static void
+gis_account_page_local_init (GisAccountPageLocal *page)
+{
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+gboolean
+gis_account_page_local_validate (GisAccountPageLocal *page)
+{
+ return page->valid_name && page->valid_username;
+}
+
+gboolean
+gis_account_page_local_create_user (GisAccountPageLocal *local,
+ GisPage *page,
+ GError **error)
+{
+ return local_create_user (local, page, error);
+}
+
+gboolean
+gis_account_page_local_apply (GisAccountPageLocal *local, GisPage *page)
+{
+ const gchar *username, *full_name;
+ gboolean parental_controls_enabled;
+
+ username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (local->username_combo));
+ gis_driver_set_username (GIS_PAGE (page)->driver, username);
+
+ full_name = gtk_editable_get_text (GTK_EDITABLE (local->fullname_entry));
+ gis_driver_set_full_name (GIS_PAGE (page)->driver, full_name);
+
+ if (local->avatar_pixbuf != NULL)
+ {
+ g_autoptr(GdkTexture) texture = NULL;
+
+ texture = gdk_texture_new_for_pixbuf (local->avatar_pixbuf);
+ gis_driver_set_avatar (GIS_PAGE (page)->driver, GDK_PAINTABLE (texture));
+ }
+ else if (local->avatar_filename != NULL)
+ {
+ g_autoptr(GdkTexture) texture = NULL;
+ g_autoptr(GError) error = NULL;
+
+ texture = gdk_texture_new_from_filename (local->avatar_filename, &error);
+
+ if (!error)
+ gis_driver_set_avatar (GIS_PAGE (page)->driver, GDK_PAINTABLE (texture));
+ else
+ g_warning ("Error loading avatar: %s", error->message);
+ }
+
+#ifdef HAVE_PARENTAL_CONTROLS
+ parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (local->enable_parental_controls_check_button));
+#else
+ parental_controls_enabled = FALSE;
+#endif
+ gis_driver_set_parental_controls_enabled (GIS_PAGE (page)->driver, parental_controls_enabled);
+
+ return FALSE;
+}
+
+void
+gis_account_page_local_shown (GisAccountPageLocal *local)
+{
+ gtk_widget_grab_focus (local->fullname_entry);
+}
diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.h b/gnome-initial-setup/pages/account/gis-account-page-local.h
new file mode 100644
index 0000000..9d8dea0
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-local.h
@@ -0,0 +1,39 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_ACCOUNT_PAGE_LOCAL (gis_account_page_local_get_type ())
+G_DECLARE_FINAL_TYPE (GisAccountPageLocal, gis_account_page_local, GIS, ACCOUNT_PAGE_LOCAL, AdwBin)
+
+gboolean gis_account_page_local_validate (GisAccountPageLocal *local);
+gboolean gis_account_page_local_apply (GisAccountPageLocal *local, GisPage *page);
+gboolean gis_account_page_local_create_user (GisAccountPageLocal *local,
+ GisPage *page,
+ GError **error);
+void gis_account_page_local_shown (GisAccountPageLocal *local);
+
+G_END_DECLS
+
diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.ui b/gnome-initial-setup/pages/account/gis-account-page-local.ui
new file mode 100644
index 0000000..67aaf9a
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page-local.ui
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisAccountPageLocal" parent="AdwBin">
+ <child>
+ <object class="GtkBox" id="area">
+ <property name="halign">center</property>
+ <property name="valign">fill</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkMenuButton" id="avatar_button">
+ <property name="margin_top">24</property>
+ <property name="halign">center</property>
+ <style>
+ <class name="flat"/>
+ </style>
+ <accessibility>
+ <property name="description" translatable="yes">Avatar image</property>
+ </accessibility>
+ <child>
+ <object class="GtkImage" id="avatar_image">
+ <property name="pixel_size">96</property>
+ <property name="icon_name">avatar-default-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="show-icon">False</property>
+ <property name="margin_top">18</property>
+ <property name="title" translatable="yes">About You</property>
+ <property name="subtitle" translatable="yes">Please provide a name and username. You can choose a picture too.</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="form">
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <property name="margin_top">42</property>
+ <child>
+ <object class="GtkLabel" id="fullname_label">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Full Name</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">fullname_entry</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="fullname_entry">
+ <property name="max_length">255</property>
+ <property name="width-chars">25</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="username_label">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Username</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">username_combo</property>
+ <property name="margin_top">6</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">4</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="username_combo">
+ <property name="has_entry">True</property>
+ <property name="entry_text_column">0</property>
+ <property name="id_column">1</property>
+ <property name="margin_top">6</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">4</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="username_explanation">
+ <property name="yalign">0</property>
+ <property name="xalign">0</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>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">5</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="enable_parental_controls_box">
+ <property name="orientation">vertical</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">6</property>
+ </layout>
+ <child>
+ <object class="GtkCheckButton" id="enable_parental_controls_check_button">
+ <property name="label" translatable="yes">Set up _parental controls for this user</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">For use by a parent or supervisor, who must set up their own password.</property>
+ <property name="yalign">0</property>
+ <property name="xalign">0</property>
+ <property name="margin-start">24</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
+
diff --git a/gnome-initial-setup/pages/account/gis-account-page.c b/gnome-initial-setup/pages/account/gis-account-page.c
new file mode 100644
index 0000000..04a8e54
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page.c
@@ -0,0 +1,309 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Account page {{{1 */
+
+#define PAGE_ID "account"
+
+#include "config.h"
+#include "account-resources.h"
+#include "gis-account-page.h"
+#include "gis-account-page-local.h"
+#include "gis-account-page-enterprise.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+struct _GisAccountPage
+{
+ GisPage parent;
+
+ GtkWidget *page_local;
+ GtkWidget *page_enterprise;
+ GtkWidget *stack;
+
+ GtkWidget *page_toggle;
+ GtkWidget *offline_label;
+ GtkWidget *offline_stack;
+
+ UmAccountMode mode;
+};
+
+G_DEFINE_TYPE (GisAccountPage, gis_account_page, GIS_TYPE_PAGE);
+
+static void
+enterprise_apply_complete (GisPage *dummy,
+ gboolean valid,
+ gpointer user_data)
+{
+ GisAccountPage *page = GIS_ACCOUNT_PAGE (user_data);
+ gis_driver_set_username (GIS_PAGE (page)->driver, NULL);
+ gis_page_apply_complete (GIS_PAGE (page), valid);
+}
+
+static gboolean
+page_validate (GisAccountPage *page)
+{
+ switch (page->mode) {
+ case UM_LOCAL:
+ return gis_account_page_local_validate (GIS_ACCOUNT_PAGE_LOCAL (page->page_local));
+ case UM_ENTERPRISE:
+ return gis_account_page_enterprise_validate (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise));
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+update_page_validation (GisAccountPage *page)
+{
+ gis_page_set_complete (GIS_PAGE (page), page_validate (page));
+}
+
+static void
+on_validation_changed (gpointer page_area,
+ GisAccountPage *page)
+{
+ update_page_validation (page);
+}
+
+static void
+set_mode (GisAccountPage *page,
+ UmAccountMode mode)
+{
+ if (page->mode == mode)
+ return;
+
+ page->mode = mode;
+ gis_driver_set_account_mode (GIS_PAGE (page)->driver, mode);
+
+ switch (mode)
+ {
+ case UM_LOCAL:
+ gtk_stack_set_visible_child (GTK_STACK (page->stack), page->page_local);
+ gis_account_page_local_shown (GIS_ACCOUNT_PAGE_LOCAL (page->page_local));
+ break;
+ case UM_ENTERPRISE:
+ gtk_stack_set_visible_child (GTK_STACK (page->stack), page->page_enterprise);
+ gis_account_page_enterprise_shown (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise));
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ update_page_validation (page);
+}
+
+static void
+toggle_mode (GtkToggleButton *button,
+ gpointer user_data)
+{
+ set_mode (GIS_ACCOUNT_PAGE (user_data),
+ gtk_toggle_button_get_active (button) ? UM_ENTERPRISE : UM_LOCAL);
+}
+
+static gboolean
+gis_account_page_apply (GisPage *gis_page,
+ GCancellable *cancellable)
+{
+ GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page);
+
+ switch (page->mode) {
+ case UM_LOCAL:
+ return gis_account_page_local_apply (GIS_ACCOUNT_PAGE_LOCAL (page->page_local), gis_page);
+ case UM_ENTERPRISE:
+ return gis_account_page_enterprise_apply (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise), cancellable,
+ enterprise_apply_complete, page);
+ default:
+ g_assert_not_reached ();
+ break;
+ }
+}
+
+static gboolean
+gis_account_page_save_data (GisPage *gis_page,
+ GError **error)
+{
+ GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page);
+
+ switch (page->mode) {
+ case UM_LOCAL:
+ return gis_account_page_local_create_user (GIS_ACCOUNT_PAGE_LOCAL (page->page_local), gis_page, error);
+ case UM_ENTERPRISE:
+ /* Nothing to do. */
+ return TRUE;
+ default:
+ g_assert_not_reached ();
+ return FALSE;
+ }
+}
+
+static void
+gis_account_page_shown (GisPage *gis_page)
+{
+ GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page);
+
+ gis_account_page_local_shown (GIS_ACCOUNT_PAGE_LOCAL (page->page_local));
+}
+
+static void
+on_local_main_user_created (GtkWidget *page_local,
+ ActUser *user,
+ const gchar *password,
+ GisAccountPage *page)
+{
+ const gchar *language;
+
+ language = gis_driver_get_user_language (GIS_PAGE (page)->driver);
+ if (language)
+ act_user_set_language (user, language);
+
+ gis_driver_set_user_permissions (GIS_PAGE (page)->driver, user, password);
+}
+
+static void
+on_local_parent_user_created (GtkWidget *page_local,
+ ActUser *user,
+ const gchar *password,
+ GisAccountPage *page)
+{
+ const gchar *language;
+
+ language = gis_driver_get_user_language (GIS_PAGE (page)->driver);
+ if (language)
+ act_user_set_language (user, language);
+
+ gis_driver_set_parent_permissions (GIS_PAGE (page)->driver, user, password);
+}
+
+static void
+on_local_page_confirmed (GisAccountPageLocal *local,
+ GisAccountPage *page)
+{
+ gis_assistant_next_page (gis_driver_get_assistant (GIS_PAGE (page)->driver));
+}
+
+static void
+on_local_user_cached (GtkWidget *page_local,
+ ActUser *user,
+ char *password,
+ GisAccountPage *page)
+{
+ const gchar *language;
+
+ language = gis_driver_get_user_language (GIS_PAGE (page)->driver);
+ if (language)
+ act_user_set_language (user, language);
+
+ gis_driver_set_user_permissions (GIS_PAGE (page)->driver, user, password);
+}
+
+static void
+on_network_changed (GNetworkMonitor *monitor,
+ gboolean available,
+ GisAccountPage *page)
+{
+ if (!available && page->mode != UM_ENTERPRISE)
+ gtk_stack_set_visible_child (GTK_STACK (page->offline_stack), page->offline_label);
+ else
+ gtk_stack_set_visible_child (GTK_STACK (page->offline_stack), page->page_toggle);
+}
+
+static void
+gis_account_page_constructed (GObject *object)
+{
+ GisAccountPage *page = GIS_ACCOUNT_PAGE (object);
+ GNetworkMonitor *monitor;
+ gboolean available;
+
+ G_OBJECT_CLASS (gis_account_page_parent_class)->constructed (object);
+
+ g_signal_connect (page->page_local, "validation-changed",
+ G_CALLBACK (on_validation_changed), page);
+ g_signal_connect (page->page_local, "main-user-created",
+ G_CALLBACK (on_local_main_user_created), page);
+ g_signal_connect (page->page_local, "parent-user-created",
+ G_CALLBACK (on_local_parent_user_created), page);
+ g_signal_connect (page->page_local, "confirm",
+ G_CALLBACK (on_local_page_confirmed), page);
+
+ g_signal_connect (page->page_enterprise, "validation-changed",
+ G_CALLBACK (on_validation_changed), page);
+ g_signal_connect (page->page_enterprise, "user-cached",
+ G_CALLBACK (on_local_user_cached), page);
+
+ update_page_validation (page);
+
+ g_signal_connect (page->page_toggle, "toggled", G_CALLBACK (toggle_mode), page);
+ g_object_bind_property (page, "applying", page->page_toggle, "sensitive", G_BINDING_INVERT_BOOLEAN);
+ g_object_bind_property (page->page_enterprise, "visible", page->offline_stack, "visible", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+ /* force a refresh by setting to an invalid value */
+ page->mode = NUM_MODES;
+ set_mode (page, UM_LOCAL);
+
+ monitor = g_network_monitor_get_default ();
+ available = g_network_monitor_get_network_available (monitor);
+ on_network_changed (monitor, available, page);
+ g_signal_connect_object (monitor, "network-changed", G_CALLBACK (on_network_changed), page, 0);
+
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_account_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("About You"));
+}
+
+static void
+gis_account_page_class_init (GisAccountPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page.ui");
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_local);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_enterprise);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, stack);
+
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_toggle);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, offline_label);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, offline_stack);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_account_page_locale_changed;
+ page_class->apply = gis_account_page_apply;
+ page_class->save_data = gis_account_page_save_data;
+ page_class->shown = gis_account_page_shown;
+ object_class->constructed = gis_account_page_constructed;
+}
+
+static void
+gis_account_page_init (GisAccountPage *page)
+{
+ g_resources_register (account_get_resource ());
+ g_type_ensure (GIS_TYPE_ACCOUNT_PAGE_LOCAL);
+ g_type_ensure (GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
diff --git a/gnome-initial-setup/pages/account/gis-account-page.h b/gnome-initial-setup/pages/account/gis-account-page.h
new file mode 100644
index 0000000..17a86fa
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page.h
@@ -0,0 +1,33 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_ACCOUNT_PAGE (gis_account_page_get_type ())
+G_DECLARE_FINAL_TYPE (GisAccountPage, gis_account_page, GIS, ACCOUNT_PAGE, GisPage)
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/account/gis-account-page.ui b/gnome-initial-setup/pages/account/gis-account-page.ui
new file mode 100644
index 0000000..782e4bd
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-page.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisAccountPage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GisAccountPageLocal" id="page_local" />
+ </child>
+ <child>
+ <object class="GisAccountPageEnterprise" id="page_enterprise" />
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkStack" id="offline_stack">
+ <property name="valign">end</property>
+ <child>
+ <object class="GtkToggleButton" id="page_toggle">
+ <property name="use_underline">True</property>
+ <property name="label" translatable="yes">_Enterprise Login</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="offline_label">
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Go online to set up Enterprise Login.</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/account/gis-account-pages.c b/gnome-initial-setup/pages/account/gis-account-pages.c
new file mode 100644
index 0000000..d9cc8d9
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-pages.c
@@ -0,0 +1,32 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+#include "gis-account-pages.h"
+#include "gis-account-page.h"
+
+GisPage *
+gis_prepare_account_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_ACCOUNT_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/account/gis-account-pages.h b/gnome-initial-setup/pages/account/gis-account-pages.h
new file mode 100644
index 0000000..9cf41bb
--- /dev/null
+++ b/gnome-initial-setup/pages/account/gis-account-pages.h
@@ -0,0 +1,33 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+GisPage *gis_prepare_account_page (GisDriver *driver);
+
+G_END_DECLS
+
diff --git a/gnome-initial-setup/pages/account/meson.build b/gnome-initial-setup/pages/account/meson.build
new file mode 100644
index 0000000..1130465
--- /dev/null
+++ b/gnome-initial-setup/pages/account/meson.build
@@ -0,0 +1,33 @@
+realmd_namespace = 'org.freedesktop.realmd'
+sources += gnome.gdbus_codegen(
+ 'um-realm-generated',
+ realmd_namespace + '.xml',
+ interface_prefix: realmd_namespace + '.',
+ namespace: 'UmRealm',
+ object_manager: true,
+ annotations: ['org.freedesktop.realmd.Realm', 'org.gtk.GDBus.C.Name', 'Common']
+)
+
+sources += gnome.compile_resources(
+ 'account-resources',
+ files('account.gresource.xml'),
+ c_name: 'account'
+)
+
+sources += files(
+ 'gis-account-page.c',
+ 'gis-account-page.h',
+ 'gis-account-pages.c',
+ 'gis-account-pages.h',
+ 'gis-account-page-local.c',
+ 'gis-account-page-local.h',
+ 'gis-account-page-enterprise.c',
+ 'gis-account-page-enterprise.h',
+ 'um-realm-manager.c',
+ 'um-realm-manager.h',
+ 'um-utils.c',
+ 'um-photo-dialog.c',
+ 'um-photo-dialog.h'
+)
+
+account_sources_dir = meson.current_source_dir()
diff --git a/gnome-initial-setup/pages/account/org.freedesktop.realmd.xml b/gnome-initial-setup/pages/account/org.freedesktop.realmd.xml
new file mode 100644
index 0000000..316213a
--- /dev/null
+++ b/gnome-initial-setup/pages/account/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/gnome-initial-setup/pages/account/um-photo-dialog.c b/gnome-initial-setup/pages/account/um-photo-dialog.c
new file mode 100644
index 0000000..383101d
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-photo-dialog.c
@@ -0,0 +1,314 @@
+/* -*- 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA
+ *
+ * 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 "um-photo-dialog.h"
+#include "um-utils.h"
+
+#define ROW_SPAN 5
+#define AVATAR_PIXEL_SIZE 72
+
+struct _UmPhotoDialog {
+ GtkPopover parent;
+
+ GtkWidget *take_picture_button;
+ GtkWidget *flowbox;
+ GtkWidget *recent_pictures;
+
+ GListStore *recent_faces;
+ GListStore *faces;
+ GFile *generated_avatar;
+ gboolean custom_avatar_was_chosen;
+
+ SelectAvatarCallback *callback;
+ gpointer data;
+};
+
+G_DEFINE_TYPE (UmPhotoDialog, um_photo_dialog, GTK_TYPE_POPOVER)
+
+
+static void
+webcam_icon_selected (UmPhotoDialog *um)
+{
+ g_warning ("Webcam icon selected, but compiled without Cheese support");
+}
+
+static void
+face_widget_activated (GtkFlowBox *flowbox,
+ GtkFlowBoxChild *child,
+ UmPhotoDialog *um)
+{
+ const char *filename;
+ GtkWidget *image;
+
+ image = gtk_flow_box_child_get_child (child);
+ filename = g_object_get_data (G_OBJECT (image), "filename");
+
+ um->callback (NULL, filename, um->data);
+ um->custom_avatar_was_chosen = TRUE;
+
+ gtk_popover_popdown (GTK_POPOVER (um));
+}
+
+static void
+generated_avatar_activated (GtkFlowBox *flowbox,
+ GtkFlowBoxChild *child,
+ UmPhotoDialog *um)
+{
+ face_widget_activated (flowbox, child, um);
+ um->custom_avatar_was_chosen = FALSE;
+}
+
+static GtkWidget *
+create_face_widget (gpointer item,
+ gpointer user_data)
+{
+ g_autoptr(GdkPixbuf) pixbuf = NULL;
+ GtkWidget *image;
+ g_autofree gchar *path = g_file_get_path (G_FILE (item));
+
+ pixbuf = gdk_pixbuf_new_from_file_at_size (path,
+ AVATAR_PIXEL_SIZE,
+ AVATAR_PIXEL_SIZE,
+ NULL);
+
+ if (pixbuf != NULL)
+ image = gtk_image_new_from_pixbuf (round_image (pixbuf));
+ else
+ image = gtk_image_new ();
+
+ gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_PIXEL_SIZE);
+
+ gtk_widget_show (image);
+
+ g_object_set_data_full (G_OBJECT (image),
+ "filename", g_steal_pointer (&path),
+ (GDestroyNotify) g_free);
+
+ return image;
+}
+
+static GStrv
+get_settings_facesdirs (void)
+{
+ g_autoptr(GSettingsSchema) schema = NULL;
+ g_autoptr(GPtrArray) facesdirs = g_ptr_array_new ();
+ g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface");
+ g_auto(GStrv) settings_dirs = g_settings_get_strv (settings, "avatar-directories");
+
+ if (settings_dirs != NULL) {
+ int i;
+ for (i = 0; settings_dirs[i] != NULL; i++) {
+ char *path = settings_dirs[i];
+ if (path != NULL && g_strcmp0 (path, "") != 0)
+ g_ptr_array_add (facesdirs, g_strdup (path));
+ }
+ }
+
+ // NULL terminated array
+ g_ptr_array_add (facesdirs, NULL);
+ return (GStrv) g_steal_pointer (&facesdirs->pdata);
+}
+
+static GStrv
+get_system_facesdirs (void)
+{
+ g_autoptr(GPtrArray) facesdirs = NULL;
+ const char * const * data_dirs;
+ 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);
+ }
+
+ // NULL terminated array
+ g_ptr_array_add (facesdirs, NULL);
+ return (GStrv) g_steal_pointer (&facesdirs->pdata);
+}
+
+static gboolean
+add_faces_from_dirs (GListStore *faces, GStrv facesdirs, gboolean add_all)
+{
+ gboolean added_faces = FALSE;
+ const gchar *target;
+ int i;
+ GFileType type;
+
+ for (i = 0; facesdirs[i] != NULL; i++) {
+ g_autoptr(GFileEnumerator) enumerator = NULL;
+ g_autoptr(GFile) dir = NULL;
+ const char *path = facesdirs[i];
+ gpointer infoptr;
+
+ dir = g_file_new_for_path (path);
+ 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)
+ continue;
+
+ while ((infoptr = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) {
+ g_autoptr (GFileInfo) info = infoptr;
+ g_autoptr (GFile) face_file = NULL;
+
+ type = g_file_info_get_file_type (info);
+ if (type != G_FILE_TYPE_REGULAR && type != G_FILE_TYPE_SYMBOLIC_LINK)
+ continue;
+
+ target = g_file_info_get_symlink_target (info);
+ if (target != NULL && g_str_has_prefix (target , "legacy/"))
+ continue;
+
+ face_file = g_file_get_child (dir, g_file_info_get_name (info));
+ g_list_store_append (faces, face_file);
+ added_faces = TRUE;
+ }
+
+ g_file_enumerator_close (enumerator, NULL, NULL);
+
+ if (added_faces && !add_all)
+ break;
+ }
+ return added_faces;
+}
+
+static void
+setup_photo_popup (UmPhotoDialog *um)
+{
+ g_auto(GStrv) facesdirs;
+ gboolean added_faces = FALSE;
+
+ um->faces = g_list_store_new (G_TYPE_FILE);
+ gtk_flow_box_bind_model (GTK_FLOW_BOX (um->flowbox),
+ G_LIST_MODEL (um->faces),
+ create_face_widget,
+ um,
+ NULL);
+
+ g_signal_connect (um->flowbox, "child-activated",
+ G_CALLBACK (face_widget_activated), um);
+
+ um->recent_faces = g_list_store_new (G_TYPE_FILE);
+ gtk_flow_box_bind_model (GTK_FLOW_BOX (um->recent_pictures),
+ G_LIST_MODEL (um->recent_faces),
+ create_face_widget,
+ um,
+ NULL);
+ g_signal_connect (um->recent_pictures, "child-activated",
+ G_CALLBACK (generated_avatar_activated), um);
+ um->custom_avatar_was_chosen = FALSE;
+
+ facesdirs = get_settings_facesdirs ();
+ added_faces = add_faces_from_dirs (um->faces, facesdirs, TRUE);
+
+ if (!added_faces) {
+ facesdirs = get_system_facesdirs ();
+ add_faces_from_dirs (um->faces, facesdirs, FALSE);
+ }
+}
+
+void
+um_photo_dialog_generate_avatar (UmPhotoDialog *um,
+ const gchar *name)
+{
+ cairo_surface_t *surface;
+ gchar *filename;
+
+ surface = generate_user_picture (name);
+
+ /* Save into a tmp file that later gets copied by AccountsService */
+ filename = g_build_filename (g_get_user_runtime_dir (), "avatar.png", NULL);
+ um->generated_avatar = g_file_new_for_path (filename);
+ cairo_surface_write_to_png (surface, g_file_get_path (um->generated_avatar));
+ g_free (filename);
+
+ /* Overwrite the first item */
+ if (g_list_model_get_item (G_LIST_MODEL (um->recent_faces), 0) != NULL)
+ g_list_store_remove (um->recent_faces, 0);
+
+ g_list_store_insert (um->recent_faces, 0,
+ um->generated_avatar);
+
+ if (!um->custom_avatar_was_chosen) {
+ um->callback (NULL, g_file_get_path (um->generated_avatar), um->data);
+ }
+}
+
+UmPhotoDialog *
+um_photo_dialog_new (SelectAvatarCallback callback,
+ gpointer data)
+{
+ UmPhotoDialog *um;
+
+ um = g_object_new (UM_TYPE_PHOTO_DIALOG, NULL);
+
+ setup_photo_popup (um);
+
+ um->callback = callback;
+ um->data = data;
+
+ return um;
+}
+
+void
+um_photo_dialog_dispose (GObject *object)
+{
+ G_OBJECT_CLASS (um_photo_dialog_parent_class)->dispose (object);
+}
+
+static void
+um_photo_dialog_init (UmPhotoDialog *um)
+{
+ gtk_widget_init_template (GTK_WIDGET (um));
+}
+
+static void
+um_photo_dialog_class_init (UmPhotoDialogClass *klass)
+{
+ GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
+ GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (wclass, "/org/gnome/initial-setup/gis-account-avatar-chooser.ui");
+
+ gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, flowbox);
+ gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, recent_pictures);
+ gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, take_picture_button);
+ gtk_widget_class_bind_template_callback (wclass, webcam_icon_selected);
+
+ oclass->dispose = um_photo_dialog_dispose;
+}
diff --git a/gnome-initial-setup/pages/account/um-photo-dialog.h b/gnome-initial-setup/pages/account/um-photo-dialog.h
new file mode 100644
index 0000000..75061ff
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-photo-dialog.h
@@ -0,0 +1,48 @@
+/* -*- 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __UM_PHOTO_DIALOG_H__
+#define __UM_PHOTO_DIALOG_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define UM_TYPE_PHOTO_DIALOG (um_photo_dialog_get_type())
+
+G_DECLARE_FINAL_TYPE (UmPhotoDialog, um_photo_dialog, UM, PHOTO_DIALOG, GtkPopover)
+
+typedef struct _UmPhotoDialog UmPhotoDialog;
+typedef void (SelectAvatarCallback) (GdkPixbuf *pixbuf,
+ const gchar *filename,
+ gpointer data);
+
+UmPhotoDialog *um_photo_dialog_new (SelectAvatarCallback callback,
+ gpointer data);
+void um_photo_dialog_free (UmPhotoDialog *dialog);
+
+void um_photo_dialog_generate_avatar (UmPhotoDialog *dialog,
+ const gchar *name);
+
+G_END_DECLS
+
+#endif
diff --git a/gnome-initial-setup/pages/account/um-realm-manager.c b/gnome-initial-setup/pages/account/um-realm-manager.c
new file mode 100644
index 0000000..bc4fd33
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-realm-manager.c
@@ -0,0 +1,878 @@
+/* -*- 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: Matthias Clasen <mclasen@redhat.com>
+ * Stef Walter <stefw@gnome.org>
+ */
+
+#include "config.h"
+
+#include "um-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 _UmRealmManager {
+ UmRealmObjectManagerClient parent;
+ UmRealmProvider *provider;
+};
+
+typedef struct {
+ UmRealmProviderProxyClass parent_class;
+} UmRealmManagerClass;
+
+enum {
+ REALM_ADDED,
+ NUM_SIGNALS,
+};
+
+static gint signals[NUM_SIGNALS] = { 0, };
+
+G_DEFINE_TYPE (UmRealmManager, um_realm_manager, UM_REALM_TYPE_OBJECT_MANAGER_CLIENT);
+
+GQuark
+um_realm_error_get_quark (void)
+{
+ static GQuark quark = 0;
+ if (quark == 0)
+ quark = g_quark_from_static_string ("um-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 (GDBusObjectManager *manager,
+ GDBusObject *object,
+ GDBusInterface *interface)
+{
+ g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (interface), G_MAXINT);
+}
+
+static void
+on_object_added (GDBusObjectManager *manager,
+ GDBusObject *object,
+ gpointer user_data)
+{
+ GList *interfaces, *l;
+
+ interfaces = g_dbus_object_get_interfaces (object);
+ for (l = interfaces; l != NULL; l = g_list_next (l))
+ on_interface_added (manager, 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 (user_data, signals[REALM_ADDED], 0, object);
+ }
+}
+
+static void
+um_realm_manager_init (UmRealmManager *self)
+{
+ g_signal_connect (self, "object-added", G_CALLBACK (on_object_added), self);
+ g_signal_connect (self, "interface-added", G_CALLBACK (on_interface_added), self);
+}
+
+static void
+um_realm_manager_dispose (GObject *obj)
+{
+ UmRealmManager *self = UM_REALM_MANAGER (obj);
+
+ g_clear_object (&self->provider);
+
+ G_OBJECT_CLASS (um_realm_manager_parent_class)->dispose (obj);
+}
+
+static void
+um_realm_manager_class_init (UmRealmManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = um_realm_manager_dispose;
+
+ signals[REALM_ADDED] = g_signal_new ("realm-added", UM_TYPE_REALM_MANAGER,
+ G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, UM_REALM_TYPE_OBJECT);
+}
+
+typedef struct {
+ GCancellable *cancellable;
+ UmRealmManager *manager;
+} NewClosure;
+
+static void
+new_closure_free (gpointer data)
+{
+ NewClosure *closure = data;
+ g_clear_object (&closure->cancellable);
+ g_clear_object (&closure->manager);
+ g_slice_free (NewClosure, closure);
+}
+
+static void
+on_provider_new (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
+ NewClosure *closure = g_simple_async_result_get_op_res_gpointer (async);
+ GError *error = NULL;
+ UmRealmProvider *provider;
+
+ provider = um_realm_provider_proxy_new_finish (result, &error);
+ closure->manager->provider = provider;
+
+ if (error == NULL) {
+ g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (closure->manager->provider), -1);
+ g_debug ("Created realm manager");
+ } else {
+ g_simple_async_result_take_error (async, error);
+ }
+ g_simple_async_result_complete (async);
+
+ g_object_unref (async);
+}
+
+static void
+on_manager_new (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
+ NewClosure *closure = g_simple_async_result_get_op_res_gpointer (async);
+ GDBusConnection *connection;
+ GError *error = NULL;
+ GObject *object;
+
+ object = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, &error);
+ if (error == NULL) {
+ closure->manager = UM_REALM_MANAGER (object);
+ connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (object));
+
+ g_debug ("Connected to realmd");
+
+ um_realm_provider_proxy_new (connection,
+ G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
+ "org.freedesktop.realmd",
+ "/org/freedesktop/realmd",
+ closure->cancellable,
+ on_provider_new, g_object_ref (async));
+ } else {
+ g_simple_async_result_take_error (async, error);
+ g_simple_async_result_complete (async);
+ }
+
+ g_object_unref (async);
+}
+
+void
+um_realm_manager_new (GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *async;
+ NewClosure *closure;
+
+ g_debug ("Connecting to realmd...");
+
+ async = g_simple_async_result_new (NULL, callback, user_data,
+ um_realm_manager_new);
+ closure = g_slice_new (NewClosure);
+ closure->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+ g_simple_async_result_set_op_res_gpointer (async, closure, new_closure_free);
+
+ g_async_initable_new_async (UM_TYPE_REALM_MANAGER, G_PRIORITY_DEFAULT,
+ cancellable, on_manager_new, g_object_ref (async),
+ "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", um_realm_object_manager_client_get_proxy_type,
+ NULL);
+
+ g_object_unref (async);
+}
+
+UmRealmManager *
+um_realm_manager_new_finish (GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *async;
+ NewClosure *closure;
+
+ g_return_val_if_fail (g_simple_async_result_is_valid (result, NULL,
+ um_realm_manager_new), NULL);
+
+ async = G_SIMPLE_ASYNC_RESULT (result);
+ if (g_simple_async_result_propagate_error (async, error))
+ return NULL;
+
+ closure = g_simple_async_result_get_op_res_gpointer (async);
+ return g_object_ref (closure->manager);
+}
+
+typedef struct {
+ UmRealmManager *manager;
+ GCancellable *cancellable;
+ GList *realms;
+} DiscoverClosure;
+
+static void
+discover_closure_free (gpointer data)
+{
+ DiscoverClosure *discover = data;
+ g_object_unref (discover->manager);
+ g_clear_object (&discover->cancellable);
+ g_list_free_full (discover->realms, g_object_unref);
+ g_slice_free (DiscoverClosure, discover);
+}
+
+static void
+on_provider_discover (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
+ DiscoverClosure *discover = g_simple_async_result_get_op_res_gpointer (async);
+ GDBusObject *object;
+ GError *error = NULL;
+ gboolean no_membership = FALSE;
+ gchar **realms;
+ gint relevance;
+ gint i;
+
+ um_realm_provider_call_discover_finish (UM_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 (discover->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]);
+ discover->realms = g_list_prepend (discover->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 (!discover->realms && no_membership) {
+ g_simple_async_result_set_error (async, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC,
+ _("Cannot automatically join this type of domain"));
+ }
+ } else {
+ g_simple_async_result_take_error (async, error);
+ }
+
+ g_simple_async_result_complete (async);
+ g_object_unref (async);
+}
+
+void
+um_realm_manager_discover (UmRealmManager *self,
+ const gchar *input,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *res;
+ DiscoverClosure *discover;
+ GVariant *options;
+
+ g_return_if_fail (UM_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);
+
+ res = g_simple_async_result_new (G_OBJECT (self), callback, user_data,
+ um_realm_manager_discover);
+ discover = g_slice_new0 (DiscoverClosure);
+ discover->manager = g_object_ref (self);
+ discover->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
+ g_simple_async_result_set_op_res_gpointer (res, discover, discover_closure_free);
+
+ options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0);
+
+ um_realm_provider_call_discover (self->provider, input, options, cancellable,
+ on_provider_discover, g_object_ref (res));
+
+ g_object_unref (res);
+}
+
+GList *
+um_realm_manager_discover_finish (UmRealmManager *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *async;
+ DiscoverClosure *discover;
+ GList *realms;
+
+ g_return_val_if_fail (UM_IS_REALM_MANAGER (self), NULL);
+ g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self),
+ um_realm_manager_discover), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ async = G_SIMPLE_ASYNC_RESULT (result);
+ if (g_simple_async_result_propagate_error (async, error))
+ return NULL;
+
+ discover = g_simple_async_result_get_op_res_gpointer (async);
+ if (!discover->realms) {
+ g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC,
+ _("No such domain or realm found"));
+ return NULL;
+ }
+
+ realms = g_list_reverse (discover->realms);
+ discover->realms = NULL;
+ return realms;
+}
+
+GList *
+um_realm_manager_get_realms (UmRealmManager *self)
+{
+ GList *objects;
+ GList *realms = NULL;
+ GList *l;
+
+ g_return_val_if_fail (UM_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 *
+um_realm_calculate_login (UmRealmCommon *realm,
+ const gchar *username)
+{
+ GString *string;
+ const gchar *const *formats;
+ gchar *login = NULL;
+
+ formats = um_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", um_realm_common_get_name (realm));
+ login = g_string_free (string, FALSE);
+ }
+
+ return login;
+
+}
+
+gboolean
+um_realm_is_configured (UmRealmObject *realm)
+{
+ UmRealmCommon *common;
+ const gchar *configured;
+ gboolean is = FALSE;
+
+ common = um_realm_object_get_common (realm);
+ if (common) {
+ configured = um_realm_common_get_configured (common);
+ is = configured != NULL && !g_str_equal (configured, "");
+ g_object_unref (common);
+ }
+
+ return is;
+}
+
+static const gchar *
+find_supported_credentials (UmRealmKerberosMembership *membership,
+ const gchar *owner)
+{
+ const gchar *cred_owner;
+ const gchar *cred_type;
+ GVariant *supported;
+ GVariantIter iter;
+
+ supported = um_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 void
+on_realm_join_complete (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data);
+
+ g_debug ("Completed Join() method call");
+
+ g_simple_async_result_set_op_res_gpointer (async, g_object_ref (result), g_object_unref);
+ g_simple_async_result_complete_in_idle (async);
+ g_object_unref (async);
+}
+
+static gboolean
+realm_join_as_owner (UmRealmObject *realm,
+ const gchar *owner,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ UmRealmKerberosMembership *membership;
+ GSimpleAsyncResult *async;
+ GVariant *contents;
+ GVariant *options;
+ GVariant *option;
+ GVariant *creds;
+ const gchar *type;
+
+ membership = um_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;
+ }
+
+ async = g_simple_async_result_new (G_OBJECT (realm), callback, user_data,
+ realm_join_as_owner);
+
+ 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);
+
+ um_realm_kerberos_membership_call_join (membership, creds, options,
+ cancellable, on_realm_join_complete,
+ g_object_ref (async));
+
+ g_object_unref (async);
+ g_object_unref (membership);
+
+ return TRUE;
+}
+
+gboolean
+um_realm_join_as_user (UmRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_val_if_fail (UM_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
+um_realm_join_as_admin (UmRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_val_if_fail (UM_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
+um_realm_join_finish (UmRealmObject *realm,
+ GAsyncResult *result,
+ GError **error)
+{
+ UmRealmKerberosMembership *membership;
+ GError *call_error = NULL;
+ gchar *dbus_error;
+ GAsyncResult *async;
+
+ g_return_val_if_fail (UM_REALM_IS_OBJECT (realm), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ membership = um_realm_object_get_kerberos_membership (realm);
+ g_return_val_if_fail (membership != NULL, FALSE);
+
+ async = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
+ um_realm_kerberos_membership_call_join_finish (membership, async, &call_error);
+ g_object_unref (membership);
+
+ if (call_error == NULL)
+ 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, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN,
+ "%s", call_error->message);
+ g_error_free (call_error);
+ } else if (g_str_equal (dbus_error, "org.freedesktop.realmd.Error.BadHostname")) {
+ g_debug ("Join() failed because of invalid/conflicting host name");
+ g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME,
+ "%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;
+ GBytes *credentials;
+} 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_bytes_unref (login->credentials);
+ 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 source_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) {
+ login->credentials = g_bytes_new_take (contents, length);
+ 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_boolean (task, TRUE);
+ break;
+
+ case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN:
+ case KRB5KDC_ERR_POLICY:
+ g_task_return_new_error (task, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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);
+}
+
+void
+um_realm_login (UmRealmObject *realm,
+ const gchar *user,
+ const gchar *password,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GTask *task;
+ LoginClosure *login;
+ UmRealmKerberos *kerberos;
+
+ g_return_if_fail (UM_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 = um_realm_object_get_kerberos (realm);
+ g_return_if_fail (kerberos != NULL);
+
+ task = g_task_new (realm, cancellable, callback, user_data);
+ login = g_slice_new0 (LoginClosure);
+ login->domain = g_strdup (um_realm_kerberos_get_domain_name (kerberos));
+ login->realm = g_strdup (um_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_check_cancellable (task, TRUE);
+ g_task_set_return_on_cancel (task, TRUE);
+
+ g_task_run_in_thread (task, kinit_thread_func);
+
+ g_object_unref (task);
+ g_object_unref (kerberos);
+}
+
+gboolean
+um_realm_login_finish (UmRealmObject *realm,
+ GAsyncResult *result,
+ GBytes **credentials,
+ GError **error)
+{
+ GTask *task;
+ LoginClosure *login;
+
+ g_return_val_if_fail (g_task_is_valid (result, realm), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ task = G_TASK (result);
+ if (!g_task_propagate_boolean (task, error))
+ return FALSE;
+
+ login = g_task_get_task_data (task);
+ if (credentials) {
+ if (login->credentials)
+ *credentials = g_bytes_ref (login->credentials);
+ else
+ *credentials = NULL;
+ }
+
+ return TRUE;
+}
diff --git a/gnome-initial-setup/pages/account/um-realm-manager.h b/gnome-initial-setup/pages/account/um-realm-manager.h
new file mode 100644
index 0000000..952bd2f
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-realm-manager.h
@@ -0,0 +1,108 @@
+/* -*- 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>
+ */
+
+#ifndef __UM_REALM_MANAGER_H__
+#define __UM_REALM_MANAGER_H__
+
+#include "um-realm-generated.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ UM_REALM_ERROR_BAD_LOGIN,
+ UM_REALM_ERROR_BAD_PASSWORD,
+ UM_REALM_ERROR_CANNOT_AUTH,
+ UM_REALM_ERROR_BAD_HOSTNAME,
+ UM_REALM_ERROR_GENERIC,
+} UmRealmErrors;
+
+#define UM_REALM_ERROR (um_realm_error_get_quark ())
+
+GQuark um_realm_error_get_quark (void) G_GNUC_CONST;
+
+#define UM_TYPE_REALM_MANAGER (um_realm_manager_get_type ())
+#define UM_REALM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), UM_TYPE_REALM_MANAGER, UmRealmManager))
+#define UM_IS_REALM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), UM_TYPE_REALM_MANAGER))
+
+typedef struct _UmRealmManager UmRealmManager;
+
+GType um_realm_manager_get_type (void) G_GNUC_CONST;
+
+void um_realm_manager_new (GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+UmRealmManager * um_realm_manager_new_finish (GAsyncResult *result,
+ GError **error);
+
+void um_realm_manager_discover (UmRealmManager *self,
+ const gchar *input,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GList * um_realm_manager_discover_finish (UmRealmManager *self,
+ GAsyncResult *result,
+ GError **error);
+
+GList * um_realm_manager_get_realms (UmRealmManager *self);
+
+void um_realm_login (UmRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean um_realm_login_finish (UmRealmObject *realm,
+ GAsyncResult *result,
+ GBytes **credentials,
+ GError **error);
+
+gboolean um_realm_join_as_user (UmRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+gboolean um_realm_join_as_admin (UmRealmObject *realm,
+ const gchar *login,
+ const gchar *password,
+ GBytes *credentials,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+ G_GNUC_WARN_UNUSED_RESULT;
+
+gboolean um_realm_join_finish (UmRealmObject *realm,
+ GAsyncResult *result,
+ GError **error);
+
+gboolean um_realm_is_configured (UmRealmObject *realm);
+
+gchar * um_realm_calculate_login (UmRealmCommon *realm,
+ const gchar *username);
+
+G_END_DECLS
+
+#endif /* __UM_REALM_H__ */
diff --git a/gnome-initial-setup/pages/account/um-utils.c b/gnome-initial-setup/pages/account/um-utils.c
new file mode 100644
index 0000000..2877d94
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-utils.c
@@ -0,0 +1,568 @@
+/* -*- 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+
+#include <math.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <utmp.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include "um-utils.h"
+
+void
+set_entry_validation_checkmark (GtkEntry *entry)
+{
+ gtk_entry_set_icon_from_icon_name (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ "object-select-symbolic");
+}
+
+void
+set_entry_validation_error (GtkEntry *entry,
+ const gchar *text)
+{
+ gtk_entry_set_icon_from_icon_name (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ "dialog-warning-symbolic");
+ gtk_entry_set_icon_tooltip_text (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ text);
+}
+
+void
+clear_entry_validation_error (GtkEntry *entry)
+{
+ gtk_entry_set_icon_from_paintable (entry,
+ GTK_ENTRY_ICON_SECONDARY,
+ NULL);
+}
+
+#define MAXNAMELEN (UT_NAMESIZE - 1)
+
+static 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;
+}
+
+gboolean
+is_valid_username (const gchar *username, gboolean parental_controls_enabled, gchar **tip)
+{
+ gboolean empty;
+ gboolean in_use;
+ gboolean too_long;
+ gboolean valid;
+ gboolean parental_controls_conflict;
+ const gchar *c;
+
+ if (username == NULL || username[0] == '\0') {
+ empty = TRUE;
+ in_use = FALSE;
+ too_long = FALSE;
+ } else {
+ empty = FALSE;
+ in_use = is_username_used (username);
+ too_long = strlen (username) > MAXNAMELEN;
+ }
+ valid = TRUE;
+
+ if (!in_use && !empty && !too_long) {
+ /* First char must be a lower case letter, and it must only be
+ * composed of lower case letters, digits, '-', and '_'.
+ */
+ for (c = username; *c; c++) {
+ if (c == username) {
+ if (! (*c >= 'a' && *c <= 'z'))
+ valid = FALSE;
+ } else {
+ if (! ((*c >= 'a' && *c <= 'z') ||
+ (*c >= '0' && *c <= '9') ||
+ (*c == '_') || (*c == '-')))
+ valid = FALSE;
+ }
+ }
+ }
+
+ parental_controls_conflict = (parental_controls_enabled && g_strcmp0 (username, "administrator") == 0);
+
+ valid = !empty && !in_use && !too_long && !parental_controls_conflict && valid;
+
+ if (!empty && (in_use || too_long || parental_controls_conflict || !valid)) {
+ if (in_use) {
+ *tip = g_strdup (_("Sorry, that user name isn’t available. Please try another."));
+ }
+ else if (too_long) {
+ *tip = g_strdup_printf (_("The username is too long."));
+ }
+ else if (!(username[0] >= 'a' && username[0] <= 'z')) {
+ *tip = g_strdup (_("The username must start with a lower case letter from a-z."));
+ }
+ else if (parental_controls_conflict) {
+ *tip = g_strdup (_("That username isn’t available. Please try another."));
+ }
+ else {
+ *tip = g_strdup (_("The username should only consist of lower case letters from a-z, digits, and the following characters: - _"));
+ }
+ }
+ else {
+ *tip = g_strdup (_("This will be used to name your home folder and can’t be changed."));
+ }
+
+ return valid;
+}
+
+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;
+
+ 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);
+
+ 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);
+}
+
+#define IMAGE_SIZE 512
+#define IMAGE_BORDER_WIDTH 7.2 /* At least 1px when avatar rendered at 72px */
+
+/* U+1F464 "bust in silhouette"
+ * U+FE0E Variant Selector 15 to force text style (monochrome) emoji
+ */
+#define PLACEHOLDER "\U0001F464\U0000FE0E"
+
+static gchar *
+extract_initials_from_name (const gchar *name)
+{
+ GString *initials = g_string_new ("");
+ gchar *p;
+ gchar *normalized;
+ gunichar unichar;
+
+ if (name == NULL || name[0] == '\0') {
+ g_string_free (initials, TRUE);
+ return g_strdup (PLACEHOLDER);
+ }
+
+ p = g_utf8_strup (name, -1);
+ normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE);
+ g_clear_pointer (&p, g_free);
+ if (normalized == NULL) {
+ g_free (normalized);
+ g_string_free (initials, TRUE);
+ return g_strdup (PLACEHOLDER);
+ }
+
+ unichar = g_utf8_get_char (normalized);
+ g_string_append_unichar (initials, unichar);
+
+ p = g_utf8_strrchr (normalized, -1, ' ');
+ if (p != NULL) {
+ p = g_utf8_next_char (p);
+
+ unichar = g_utf8_get_char (p);
+ g_string_append_unichar (initials, unichar);
+ }
+
+ g_free (normalized);
+
+ return g_string_free (initials, FALSE);
+}
+
+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 || name[0] == '\0') {
+ idx = 5;
+ } else {
+ 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 void
+darken_color (GdkRGBA *color, gdouble fraction) {
+ color->red = CLAMP (color->red + color->red * fraction, 0, 255);
+ color->green = CLAMP (color->green + color->green * fraction, 0, 255);
+ color->blue = CLAMP (color->blue + color->blue * fraction, 0, 255);
+}
+
+cairo_surface_t *
+generate_user_picture (const gchar *name) {
+ PangoFontDescription *font_desc;
+ g_autofree gchar *initials = extract_initials_from_name (name);
+ g_autofree gchar *font = g_strdup_printf ("Sans %d", (int)ceil (IMAGE_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,
+ IMAGE_SIZE,
+ IMAGE_SIZE);
+ cr = cairo_create (surface);
+
+ cairo_arc (cr, IMAGE_SIZE/2, IMAGE_SIZE/2, IMAGE_SIZE/2, 0, 2 * G_PI);
+ cairo_set_source_rgb (cr, color.red/255.0, color.green/255.0, color.blue/255.0);
+ cairo_fill (cr);
+
+ cairo_arc (cr, IMAGE_SIZE / 2, IMAGE_SIZE / 2, IMAGE_SIZE / 2 - IMAGE_BORDER_WIDTH / 2,
+ 0, 2 * G_PI);
+ darken_color (&color, -0.3);
+ cairo_set_source_rgb (cr, color.red / 255.0, color.green / 255.0, color.blue / 255.0);
+ cairo_set_line_width (cr, IMAGE_BORDER_WIDTH);
+ cairo_stroke (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, IMAGE_SIZE/2, IMAGE_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;
+}
+
+GdkPixbuf *
+round_image (GdkPixbuf *image)
+{
+ GdkPixbuf *dest = NULL;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+ gint size;
+
+ size = gdk_pixbuf_get_width (image);
+ 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, image, 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;
+}
diff --git a/gnome-initial-setup/pages/account/um-utils.h b/gnome-initial-setup/pages/account/um-utils.h
new file mode 100644
index 0000000..8a8c7cd
--- /dev/null
+++ b/gnome-initial-setup/pages/account/um-utils.h
@@ -0,0 +1,50 @@
+/* -*- 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA
+ *
+ * Written by: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __UM_UTILS_H__
+#define __UM_UTILS_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void set_entry_validation_error (GtkEntry *entry,
+ const gchar *text);
+void set_entry_validation_checkmark (GtkEntry *entry);
+void clear_entry_validation_error (GtkEntry *entry);
+
+gboolean is_valid_name (const gchar *name);
+gboolean is_valid_username (const gchar *name,
+ gboolean parental_controls_enabled,
+ gchar **tip);
+
+void generate_username_choices (const gchar *name,
+ GtkListStore *store);
+
+cairo_surface_t *generate_user_picture (const gchar *name);
+
+GdkPixbuf *round_image (GdkPixbuf *image);
+
+
+G_END_DECLS
+
+#endif
diff --git a/gnome-initial-setup/pages/goa/gis-goa-page.c b/gnome-initial-setup/pages/goa/gis-goa-page.c
new file mode 100644
index 0000000..b934825
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/gis-goa-page.c
@@ -0,0 +1,606 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Online accounts page {{{1 */
+
+#define PAGE_ID "goa"
+
+#include "config.h"
+#include "gis-goa-page.h"
+#include "goa-resources.h"
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#include <goa/goa.h>
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include "gis-page-header.h"
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/x11/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/wayland/gdkwayland.h>
+#endif
+
+#define VENDOR_GOA_GROUP "goa"
+#define VENDOR_PROVIDERS_KEY "providers"
+
+struct _GisGoaPagePrivate {
+ GtkWidget *accounts_list;
+
+ GoaClient *goa_client;
+ GHashTable *providers;
+ gboolean accounts_exist;
+ char *window_export_handle;
+};
+typedef struct _GisGoaPagePrivate GisGoaPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisGoaPage, gis_goa_page, GIS_TYPE_PAGE);
+
+struct _ProviderWidget {
+ GisGoaPage *page;
+ GVariant *provider;
+ GoaAccount *displayed_account;
+
+ GtkWidget *row;
+ GtkWidget *arrow_icon;
+};
+typedef struct _ProviderWidget ProviderWidget;
+
+G_GNUC_NULL_TERMINATED
+static char *
+run_goa_helper_sync (const char *command,
+ ...)
+{
+ g_autoptr(GPtrArray) argv = NULL;
+ g_autofree char *output = NULL;
+ g_autoptr(GError) error = NULL;
+ const char *param;
+ va_list args;
+ int status;
+
+ argv = g_ptr_array_new_with_free_func (g_free);
+ g_ptr_array_add (argv, g_strdup (LIBEXECDIR "/gnome-initial-setup-goa-helper"));
+ g_ptr_array_add (argv, g_strdup (command));
+
+ va_start (args, command);
+ while ((param = va_arg (args, const char*)) != NULL)
+ g_ptr_array_add (argv, g_strdup (param));
+ va_end (args);
+
+ g_ptr_array_add (argv, NULL);
+
+ if (!g_spawn_sync (NULL,
+ (char **) argv->pdata,
+ NULL,
+ 0,
+ NULL,
+ NULL,
+ &output,
+ NULL,
+ &status,
+ &error))
+ {
+ g_warning ("Failed to run online accounts helper: %s", error->message);
+ return NULL;
+ }
+
+ if (!g_spawn_check_exit_status (status, NULL))
+ return NULL;
+
+ if (output == NULL || *output == '\0')
+ return NULL;
+
+ return g_steal_pointer (&output);
+}
+
+static void
+run_goa_helper_in_thread_func (GTask *task,
+ gpointer source_object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ g_autofree char *output = NULL;
+ g_autoptr(GError) error = NULL;
+ GPtrArray *argv = task_data;
+ int status;
+
+ g_spawn_sync (NULL,
+ (char **) argv->pdata,
+ NULL, 0, NULL, NULL,
+ &output,
+ NULL,
+ &status,
+ &error);
+
+ if (error)
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ if (!g_spawn_check_exit_status (status, &error))
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ g_task_return_pointer (task, g_steal_pointer (&output), g_free);
+}
+
+static void
+run_goa_helper_async (const gchar *command,
+ const gchar *param,
+ const gchar *window_handle,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GPtrArray) argv = NULL;
+ g_autoptr(GTask) task = NULL;
+
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ argv = g_ptr_array_new_with_free_func (g_free);
+ g_ptr_array_add (argv, g_strdup (LIBEXECDIR "/gnome-initial-setup-goa-helper"));
+ g_ptr_array_add (argv, g_strdup (command));
+ g_ptr_array_add (argv, g_strdup (param));
+ g_ptr_array_add (argv, g_strdup (window_handle));
+ g_ptr_array_add (argv, NULL);
+
+ task = g_task_new (NULL, cancellable, callback, user_data);
+ g_task_set_source_tag (task, run_goa_helper_async);
+ g_task_set_task_data (task, g_steal_pointer (&argv), (GDestroyNotify) g_ptr_array_unref);
+ g_task_run_in_thread (task, run_goa_helper_in_thread_func);
+}
+
+static void
+sync_provider_widget (ProviderWidget *provider_widget)
+{
+ gboolean has_account = (provider_widget->displayed_account != NULL);
+
+ gtk_widget_set_visible (provider_widget->arrow_icon, !has_account);
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (provider_widget->row), !has_account);
+
+ adw_action_row_set_subtitle (ADW_ACTION_ROW (provider_widget->row),
+ has_account ? goa_account_get_presentation_identity (provider_widget->displayed_account) : NULL);
+
+}
+
+static void
+on_create_account_finish_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autofree char *new_account_id = NULL;
+ g_autoptr(GError) error = NULL;
+
+ new_account_id = g_task_propagate_pointer (G_TASK (result), &error);
+
+ if (error)
+ g_warning ("Error creating account: %s", error->message);
+}
+
+static void
+add_account_to_provider (ProviderWidget *provider_widget)
+{
+ GisGoaPage *page = provider_widget->page;
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ g_autofree char *provider_type = NULL;
+
+ if (!priv->window_export_handle)
+ return;
+
+ g_variant_get (provider_widget->provider, "(ssviu)", &provider_type, NULL, NULL, NULL, NULL);
+
+ run_goa_helper_async ("create-account",
+ provider_type,
+ priv->window_export_handle,
+ NULL,
+ on_create_account_finish_cb,
+ page);
+}
+
+static gboolean
+is_gicon_symbolic (GtkWidget *widget,
+ GIcon *icon)
+{
+ g_autoptr(GtkIconPaintable) icon_paintable = NULL;
+ GtkIconTheme *icon_theme;
+
+ icon_theme = gtk_icon_theme_get_for_display (gdk_display_get_default ());
+ icon_paintable = gtk_icon_theme_lookup_by_gicon (icon_theme,
+ icon,
+ 32,
+ gtk_widget_get_scale_factor (widget),
+ gtk_widget_get_direction (widget),
+ 0);
+
+ return icon_paintable && gtk_icon_paintable_is_symbolic (icon_paintable);
+}
+
+static void
+add_provider_to_list (GisGoaPage *page, GVariant *provider)
+{
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ g_autoptr(GIcon) provider_icon = NULL;
+ g_autofree char *provider_name = NULL;
+ g_autofree char *provider_type = NULL;
+ g_autoptr(GVariant) icon_variant = NULL;
+ ProviderWidget *provider_widget;
+ GtkWidget *row;
+ GtkWidget *image;
+ GtkWidget *arrow_icon;
+
+ g_variant_get (provider, "(ssviu)",
+ &provider_type,
+ &provider_name,
+ &icon_variant,
+ NULL,
+ NULL);
+
+ row = adw_action_row_new ();
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);
+ adw_preferences_row_set_use_markup (ADW_PREFERENCES_ROW (row), TRUE);
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), provider_name);
+
+ provider_icon = g_icon_deserialize (icon_variant);
+ image = gtk_image_new_from_gicon (provider_icon);
+ adw_action_row_add_prefix (ADW_ACTION_ROW (row), image);
+
+ if (is_gicon_symbolic (GTK_WIDGET (page), provider_icon))
+ {
+ gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_NORMAL);
+ gtk_widget_add_css_class (image, "symbolic-circular");
+ }
+ else
+ {
+ gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
+ gtk_widget_add_css_class (image, "lowres-icon");
+ }
+
+ arrow_icon = gtk_image_new_from_icon_name ("go-next-symbolic");
+ adw_action_row_add_suffix (ADW_ACTION_ROW (row), arrow_icon);
+
+ provider_widget = g_new0 (ProviderWidget, 1);
+ provider_widget->page = page;
+ provider_widget->provider = g_variant_ref (provider);
+ provider_widget->row = row;
+ provider_widget->arrow_icon = arrow_icon;
+
+ g_object_set_data_full (G_OBJECT (row), "widget", provider_widget, g_free);
+
+ g_hash_table_insert (priv->providers, g_steal_pointer (&provider_type), provider_widget);
+
+ gtk_list_box_append (GTK_LIST_BOX (priv->accounts_list), row);
+}
+
+static void
+populate_provider_list (GisGoaPage *page)
+{
+ g_autoptr(GVariant) providers_variant = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autofree char *listed_providers = NULL;
+ GVariantIter iter;
+ GVariant *provider;
+ g_auto(GStrv) conf_providers =
+ gis_driver_conf_get_string_list (GIS_PAGE (page)->driver, VENDOR_GOA_GROUP, VENDOR_PROVIDERS_KEY, NULL);
+ GStrv providers = conf_providers ? conf_providers :
+ (gchar *[]) { "google", "owncloud", "windows_live", "facebook", NULL };
+
+ listed_providers = run_goa_helper_sync ("list-providers", NULL);
+ providers_variant = g_variant_parse (G_VARIANT_TYPE ("a(ssviu)"),
+ listed_providers,
+ NULL,
+ NULL,
+ &error);
+
+ if (error)
+ {
+ g_warning ("Error listing providers: %s", error->message);
+ gtk_widget_hide (GTK_WIDGET (page));
+ return;
+ }
+
+ /* This code will read the keyfile containing vendor customization options and
+ * look for options under the "goa" group, and supports the following keys:
+ * - providers (optional): list of online account providers to offer
+ *
+ * This is how this file might look on a vendor image:
+ *
+ * [goa]
+ * providers=owncloud;imap_smtp
+ */
+
+ g_variant_iter_init (&iter, providers_variant);
+
+ while ((provider = g_variant_iter_next_value (&iter)))
+ {
+ g_autofree gchar *id = NULL;
+
+ g_variant_get (provider, "(ssviu)", &id, NULL, NULL, NULL, NULL);
+
+ if (g_strv_contains ((const char * const *)providers, id))
+ add_provider_to_list (page, provider);
+ }
+}
+
+static void
+sync_visibility (GisGoaPage *page)
+{
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ GisAssistant *assistant = gis_driver_get_assistant (GIS_PAGE (page)->driver);
+ GNetworkMonitor *network_monitor = g_network_monitor_get_default ();
+ gboolean visible;
+
+ if (gis_assistant_get_current_page (assistant) == GIS_PAGE (page))
+ return;
+
+ visible = (priv->accounts_exist || g_network_monitor_get_network_available (network_monitor));
+ gtk_widget_set_visible (GTK_WIDGET (page), visible);
+}
+
+static void
+sync_accounts (GisGoaPage *page)
+{
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ GList *accounts, *l;
+
+ accounts = goa_client_get_accounts (priv->goa_client);
+
+ for (l = accounts; l != NULL; l = l->next) {
+ GoaObject *object = GOA_OBJECT (l->data);
+ GoaAccount *account = goa_object_get_account (object);
+ const char *account_type = goa_account_get_provider_type (account);
+ ProviderWidget *provider_widget;
+
+ provider_widget = g_hash_table_lookup (priv->providers, account_type);
+ if (!provider_widget)
+ continue;
+
+ priv->accounts_exist = TRUE;
+
+ if (provider_widget->displayed_account)
+ continue;
+
+ provider_widget->displayed_account = account;
+ sync_provider_widget (provider_widget);
+ }
+
+ g_list_free_full (accounts, (GDestroyNotify) g_object_unref);
+
+ sync_visibility (page);
+ gis_page_set_skippable (GIS_PAGE (page), !priv->accounts_exist);
+ gis_page_set_complete (GIS_PAGE (page), priv->accounts_exist);
+}
+
+static void
+accounts_changed (GoaClient *client, GoaObject *object, gpointer user_data)
+{
+ GisGoaPage *page = GIS_GOA_PAGE (user_data);
+ sync_accounts (page);
+}
+
+static void
+network_status_changed (GNetworkMonitor *monitor,
+ gboolean available,
+ gpointer user_data)
+{
+ GisGoaPage *page = GIS_GOA_PAGE (user_data);
+ sync_visibility (page);
+}
+
+static void
+row_activated (GtkListBox *box,
+ GtkListBoxRow *row,
+ GisGoaPage *page)
+{
+ ProviderWidget *provider_widget;
+
+ if (row == NULL)
+ return;
+
+ provider_widget = g_object_get_data (G_OBJECT (row), "widget");
+ g_assert (provider_widget != NULL);
+ g_assert (provider_widget->displayed_account == NULL);
+ add_account_to_provider (provider_widget);
+}
+
+#ifdef GDK_WINDOWING_WAYLAND
+static void
+wayland_window_exported_cb (GdkToplevel *toplevel,
+ const char *handle,
+ gpointer data)
+
+{
+ GisGoaPage *page = data;
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+
+ priv->window_export_handle = g_strdup_printf ("wayland:%s", handle);
+}
+#endif
+
+static void
+export_window_handle (GisGoaPage *page)
+{
+ GtkNative *native = gtk_widget_get_native (GTK_WIDGET (page));
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY (gtk_widget_get_display (GTK_WIDGET (native))))
+ {
+ GdkSurface *surface = gtk_native_get_surface (native);
+ guint32 xid = (guint32) gdk_x11_surface_get_xid (surface);
+
+ priv->window_export_handle = g_strdup_printf ("x11:%x", xid);
+ }
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_DISPLAY (gtk_widget_get_display (GTK_WIDGET (native))))
+ {
+ GdkSurface *surface = gtk_native_get_surface (native);
+
+ gdk_wayland_toplevel_export_handle (GDK_TOPLEVEL (surface),
+ wayland_window_exported_cb,
+ page,
+ NULL);
+ }
+#endif
+}
+
+static void
+unexport_window_handle (GisGoaPage *page)
+{
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+
+ if (!priv->window_export_handle)
+ return;
+
+#ifdef GDK_WINDOWING_WAYLAND
+ GtkNative *native = gtk_widget_get_native (GTK_WIDGET (page));
+
+ if (GDK_IS_WAYLAND_DISPLAY (gtk_widget_get_display (GTK_WIDGET (native))))
+ {
+ GdkSurface *surface = gtk_native_get_surface (native);
+ gdk_wayland_toplevel_unexport_handle (GDK_TOPLEVEL (surface));
+ }
+#endif
+}
+
+
+static void
+gis_goa_page_realize (GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS (gis_goa_page_parent_class)->realize (widget);
+
+ export_window_handle (GIS_GOA_PAGE (widget));
+}
+
+static void
+gis_goa_page_unrealize (GtkWidget *widget)
+{
+ unexport_window_handle (GIS_GOA_PAGE (widget));
+
+ GTK_WIDGET_CLASS (gis_goa_page_parent_class)->unrealize (widget);
+}
+
+
+static void
+gis_goa_page_constructed (GObject *object)
+{
+ GisGoaPage *page = GIS_GOA_PAGE (object);
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ GError *error = NULL;
+ GNetworkMonitor *network_monitor = g_network_monitor_get_default ();
+
+ G_OBJECT_CLASS (gis_goa_page_parent_class)->constructed (object);
+
+ gis_page_set_skippable (GIS_PAGE (page), TRUE);
+
+ priv->providers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ priv->goa_client = goa_client_new_sync (NULL, &error);
+
+ if (priv->goa_client == NULL) {
+ g_warning ("Failed to get a GoaClient: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ g_signal_connect (priv->goa_client, "account-added",
+ G_CALLBACK (accounts_changed), page);
+ g_signal_connect (priv->goa_client, "account-removed",
+ G_CALLBACK (accounts_changed), page);
+ g_signal_connect (network_monitor, "network-changed",
+ G_CALLBACK (network_status_changed), page);
+
+ g_signal_connect (priv->accounts_list, "row-activated",
+ G_CALLBACK (row_activated), page);
+
+ populate_provider_list (page);
+ sync_accounts (page);
+}
+
+static void
+gis_goa_page_dispose (GObject *object)
+{
+ GisGoaPage *page = GIS_GOA_PAGE (object);
+ GisGoaPagePrivate *priv = gis_goa_page_get_instance_private (page);
+ GNetworkMonitor *network_monitor = g_network_monitor_get_default ();
+
+ g_clear_object (&priv->goa_client);
+
+ g_signal_handlers_disconnect_by_func (network_monitor, G_CALLBACK (network_status_changed), page);
+
+ G_OBJECT_CLASS (gis_goa_page_parent_class)->dispose (object);
+}
+
+static void
+gis_goa_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Online Accounts"));
+}
+
+static void
+gis_goa_page_class_init (GisGoaPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (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/initial-setup/gis-goa-page.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GisGoaPage, accounts_list);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_goa_page_locale_changed;
+ object_class->constructed = gis_goa_page_constructed;
+ object_class->dispose = gis_goa_page_dispose;
+ widget_class->realize = gis_goa_page_realize;
+ widget_class->unrealize = gis_goa_page_unrealize;
+}
+
+static void
+gis_goa_page_init (GisGoaPage *page)
+{
+ g_autoptr(GtkCssProvider) provider = NULL;
+
+ g_resources_register (goa_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider, "/org/gnome/initial-setup/gis-goa-page.css");
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+GisPage *
+gis_prepare_goa_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_GOA_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/goa/gis-goa-page.css b/gnome-initial-setup/pages/goa/gis-goa-page.css
new file mode 100644
index 0000000..08a60b6
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/gis-goa-page.css
@@ -0,0 +1,6 @@
+image.symbolic-circular {
+ background-color: alpha(currentColor, 0.08);
+ min-width: 32px;
+ min-height: 32px;
+ border-radius: 50%;
+}
diff --git a/gnome-initial-setup/pages/goa/gis-goa-page.h b/gnome-initial-setup/pages/goa/gis-goa-page.h
new file mode 100644
index 0000000..31918bf
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/gis-goa-page.h
@@ -0,0 +1,55 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_GOA_PAGE_H__
+#define __GIS_GOA_PAGE_H__
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_GOA_PAGE (gis_goa_page_get_type ())
+#define GIS_GOA_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_GOA_PAGE, GisGoaPage))
+#define GIS_GOA_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_GOA_PAGE, GisGoaPageClass))
+#define GIS_IS_GOA_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_GOA_PAGE))
+#define GIS_IS_GOA_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_GOA_PAGE))
+#define GIS_GOA_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_GOA_PAGE, GisGoaPageClass))
+
+typedef struct _GisGoaPage GisGoaPage;
+typedef struct _GisGoaPageClass GisGoaPageClass;
+
+struct _GisGoaPage
+{
+ GisPage parent;
+};
+
+struct _GisGoaPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_goa_page_get_type (void);
+
+GisPage *gis_prepare_goa_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_GOA_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/goa/gis-goa-page.ui b/gnome-initial-setup/pages/goa/gis-goa-page.ui
new file mode 100644
index 0000000..96dab4e
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/gis-goa-page.ui
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisGoaPage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Connect Your Online Accounts</property>
+ <property name="subtitle" translatable="yes">Connect your accounts to easily access your email, online calendar, contacts, documents and photos.</property>
+ <property name="icon_name">goa-panel-symbolic</property>
+ <property name="show_icon" bind-source="GisGoaPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkListBox" id="accounts_list">
+ <property name="selection_mode">none</property>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel" id="footer_label">
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">Accounts can be added and removed at any time from the Settings application.</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/goa/gnome-initial-setup-goa-helper.c b/gnome-initial-setup/pages/goa/gnome-initial-setup-goa-helper.c
new file mode 100644
index 0000000..5af33db
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/gnome-initial-setup-goa-helper.c
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2022 Endless OS Foundation, LLC
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author:
+ * Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
+ */
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+
+#define GOA_API_IS_SUBJECT_TO_CHANGE
+#define GOA_BACKEND_API_IS_SUBJECT_TO_CHANGE
+#include <goabackend/goabackend.h>
+
+#ifdef HAVE_GTK_X11
+#include <gdk/gdkx.h>
+#endif
+#ifdef HAVE_GTK_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+static GdkDisplay *
+get_wayland_display (void)
+{
+ static GdkDisplay *wayland_display = NULL;
+
+ if (wayland_display)
+ return wayland_display;
+
+ gdk_set_allowed_backends ("wayland");
+ wayland_display = gdk_display_open (NULL);
+ gdk_set_allowed_backends (NULL);
+ if (!wayland_display)
+ g_warning ("Failed to open Wayland display");
+
+ return wayland_display;
+}
+
+static GdkDisplay *
+get_x11_display (void)
+{
+ static GdkDisplay *x11_display = NULL;
+
+ if (x11_display)
+ return x11_display;
+
+ gdk_set_allowed_backends ("x11");
+ x11_display = gdk_display_open (NULL);
+ gdk_set_allowed_backends (NULL);
+ if (!x11_display)
+ g_warning ("Failed to open X11 display");
+
+ return x11_display;
+}
+
+static void
+set_external_parent_from_handle (GtkApplication *application,
+ GtkWindow *dialog,
+ const char *handle_str)
+{
+ GdkDisplay *display;
+ GtkWindow *fake_parent;
+ GdkScreen *screen;
+
+#ifdef HAVE_GTK_X11
+ {
+ const char *x11_prefix = "x11:";
+ if (g_str_has_prefix (handle_str, x11_prefix))
+ {
+ display = get_x11_display ();
+ if (!display)
+ {
+ g_warning ("No X display connection, ignoring X11 parent");
+ return;
+ }
+ }
+ }
+#endif
+#ifdef HAVE_GTK_WAYLAND
+ {
+ const char *wayland_prefix = "wayland:";
+
+ if (g_str_has_prefix (handle_str, wayland_prefix))
+ {
+ display = get_wayland_display ();
+ if (!display)
+ {
+ g_warning ("No Wayland display connection, ignoring Wayland parent");
+ return;
+ }
+ }
+ }
+#endif
+
+ screen = gdk_display_get_default_screen (gdk_display_get_default ());
+ fake_parent = g_object_new (GTK_TYPE_APPLICATION_WINDOW,
+ "application", application,
+ "type", GTK_WINDOW_TOPLEVEL,
+ "screen", screen,
+ NULL);
+ g_object_ref_sink (fake_parent);
+
+ gtk_window_set_transient_for (dialog, GTK_WINDOW (fake_parent));
+ gtk_window_set_modal (dialog, TRUE);
+ gtk_widget_realize (GTK_WIDGET (dialog));
+
+#ifdef HAVE_GTK_X11
+ {
+ const char *x11_prefix = "x11:";
+ if (g_str_has_prefix (handle_str, x11_prefix))
+ {
+ GdkWindow *foreign_gdk_window;
+ int xid;
+
+ errno = 0;
+ xid = strtol (handle_str + strlen (x11_prefix), NULL, 16);
+ if (errno != 0)
+ {
+ g_warning ("Failed to reference external X11 window, invalid XID %s", handle_str);
+ return;
+ }
+
+ foreign_gdk_window = gdk_x11_window_foreign_new_for_display (display, xid);
+ if (!foreign_gdk_window)
+ {
+ g_warning ("Failed to create foreign window for XID %d", xid);
+ return;
+ }
+
+ gdk_window_set_transient_for (gtk_widget_get_window (GTK_WIDGET (dialog)),
+ foreign_gdk_window);
+ }
+ }
+#endif
+#ifdef HAVE_GTK_WAYLAND
+ {
+ const char *wayland_prefix = "wayland:";
+
+ if (g_str_has_prefix (handle_str, wayland_prefix))
+ {
+ const char *wayland_handle_str = handle_str + strlen (wayland_prefix);
+
+ if (!gdk_wayland_window_set_transient_for_exported (gtk_widget_get_window (GTK_WIDGET (dialog)),
+ (char *) wayland_handle_str))
+ {
+ g_warning ("Failed to set window transient for external parent");
+ return;
+ }
+ }
+ }
+#endif
+
+ gtk_window_present (dialog);
+}
+
+/* create-account */
+
+static void
+on_application_activate_create_account_cb (GtkApplication *application,
+ char **argv)
+{
+ g_autoptr(GoaProvider) provider = NULL;
+ g_autoptr(GoaClient) client = NULL;
+ g_autoptr(GError) error = NULL;
+ GoaAccount *account;
+ GtkWidget *content_area;
+ GtkWidget *dialog;
+ GoaObject *object;
+
+ client = goa_client_new_sync (NULL, &error);
+ if (error)
+ {
+ g_printerr ("Error retrieving online accounts client");
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+
+ /* Find the provider with a matching type */
+ provider = goa_provider_get_for_provider_type (argv[2]);
+ if (!provider)
+ {
+ g_printerr ("Provider type not supported");
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+ dialog = g_object_new (GTK_TYPE_DIALOG,
+ "use-header-bar", 1,
+ "default-width", 500,
+ "default-height", 350,
+ NULL);
+ g_signal_connect_swapped (dialog, "response", G_CALLBACK (g_application_quit), application);
+ set_external_parent_from_handle (application, GTK_WINDOW (dialog), argv[3]);
+
+ content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ gtk_container_set_border_width (GTK_CONTAINER (content_area), 0);
+
+ object = goa_provider_add_account (provider,
+ client,
+ GTK_DIALOG (dialog),
+ GTK_BOX (content_area),
+ &error);
+ if (error)
+ {
+ g_printerr ("Failed to create account: %s", error->message);
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+ account = goa_object_peek_account (object);
+ g_print ("%s", goa_account_get_id (account));
+}
+
+static int
+create_account (int argc,
+ char **argv)
+{
+ g_autoptr(GtkApplication) application = NULL;
+
+ gtk_init (&argc, &argv);
+
+ if (argc != 4)
+ {
+ g_printerr ("Not enough arguments");
+ return EXIT_FAILURE;
+ }
+
+ application = gtk_application_new ("org.gnome.Settings.GoaHelper",
+ G_APPLICATION_FLAGS_NONE);
+ g_signal_connect (application, "activate", G_CALLBACK (on_application_activate_create_account_cb), argv);
+
+ return g_application_run (G_APPLICATION (application), 0, NULL);
+}
+
+/* list-providers */
+
+typedef struct {
+ GMainLoop *mainloop;
+ GList *providers;
+ GError *error;
+} GetAllProvidersData;
+
+static void
+get_all_providers_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autolist(GoaProvider) providers = NULL;
+ GetAllProvidersData *data;
+
+ data = user_data;
+
+ goa_provider_get_all_finish (&providers, res, &data->error);
+ if (data->error)
+ goto out;
+
+ data->providers = g_steal_pointer (&providers);
+
+out:
+ g_main_loop_quit (data->mainloop);
+}
+
+static GList *
+get_all_providers (GError **error)
+{
+ GetAllProvidersData data = (GetAllProvidersData) {
+ .mainloop = g_main_loop_new (NULL, FALSE),
+ .providers = NULL,
+ .error = NULL,
+ };
+
+ goa_provider_get_all (get_all_providers_cb, &data);
+
+ g_main_loop_run (data.mainloop);
+
+ if (data.error)
+ g_propagate_error (error, data.error);
+
+ return data.providers;
+}
+
+static int
+list_providers (int argc,
+ char **argv)
+{
+ g_autofree char *serialized_result = NULL;
+ g_autolist(GoaProvider) providers = NULL;
+ g_autoptr(GVariant) result = NULL;
+ g_autoptr(GError) error = NULL;
+ GVariantBuilder b;
+ GList *l;
+
+ providers = get_all_providers (&error);
+
+ if (error)
+ {
+ g_printerr ("%s", error->message);
+ return EXIT_FAILURE;
+ }
+
+ g_variant_builder_init (&b, G_VARIANT_TYPE ("a(ssviu)"));
+ for (l = providers; l; l = l->next)
+ {
+ GoaProvider *provider = l->data;
+ g_autofree char *name = NULL;
+ g_autoptr(GVariant) icon_variant = NULL;
+ g_autoptr(GIcon) icon = NULL;
+
+ name = goa_provider_get_provider_name (provider, NULL);
+ icon = goa_provider_get_provider_icon (provider, NULL);
+ icon_variant = g_icon_serialize (icon);
+
+ g_variant_builder_add (&b, "(ssviu)",
+ goa_provider_get_provider_type (provider),
+ name,
+ icon_variant,
+ goa_provider_get_provider_features (provider),
+ goa_provider_get_credentials_generation (provider));
+ }
+ result = g_variant_builder_end (&b);
+
+ serialized_result = g_variant_print (result, TRUE);
+ g_print ("%s", serialized_result);
+
+ return EXIT_SUCCESS;
+}
+
+/* show-account */
+
+static void
+on_remove_button_clicked_cb (GtkButton *button,
+ GApplication *application)
+{
+ g_print ("remove");
+ g_application_quit (application);
+}
+
+static void
+on_application_activate_show_account_cb (GtkApplication *application,
+ char **argv)
+{
+ g_autoptr(GoaProvider) provider = NULL;
+ g_autoptr(GoaObject) object = NULL;
+ g_autoptr(GoaClient) client = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autofree char *title = NULL;
+ GoaAccount *account;
+ GtkWidget *content_area;
+ GtkWidget *button;
+ GtkWidget *dialog;
+ GtkWidget *box;
+ const char *provider_type;
+
+ client = goa_client_new_sync (NULL, &error);
+ if (error)
+ {
+ g_printerr ("Error retrieving online accounts client");
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+ object = goa_client_lookup_by_id (client, argv[2]);
+ if (!object)
+ {
+ g_printerr ("Online account does not exist");
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+ /* Find the provider with a matching type */
+ account = goa_object_get_account (object);
+ provider_type = goa_account_get_provider_type (account);
+ provider = goa_provider_get_for_provider_type (provider_type);
+ if (!provider)
+ {
+ g_printerr ("Provider type not supported");
+ exit (EXIT_FAILURE);
+ return;
+ }
+
+ dialog = g_object_new (GTK_TYPE_DIALOG,
+ "use-header-bar", 1,
+ NULL);
+ /* Keep account alive so that the switches are still bound to it */
+ g_object_set_data_full (G_OBJECT (dialog), "goa-account", account, g_object_unref);
+ g_signal_connect_swapped (dialog, "response", G_CALLBACK (g_application_quit), application);
+ set_external_parent_from_handle (application, GTK_WINDOW (dialog), argv[3]);
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 42);
+ gtk_widget_set_margin_bottom (box, 24);
+
+ content_area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
+ gtk_container_set_border_width (GTK_CONTAINER (content_area), 0);
+ gtk_container_add (GTK_CONTAINER (content_area), box);
+
+ goa_provider_show_account (provider,
+ client,
+ object,
+ GTK_BOX (box),
+ NULL,
+ NULL);
+
+ /*
+ * The above call doesn't set any widgets to visible, so we have to do that.
+ * https://gitlab.gnome.org/GNOME/gnome-online-accounts/issues/56
+ */
+ gtk_widget_show_all (box);
+
+ /* translators: This is the title of the "Show Account" dialog. The
+ * %s is the name of the provider. e.g., 'Google'. */
+ title = g_strdup_printf (_("%s Account"), goa_account_get_provider_name (account));
+ gtk_window_set_title (GTK_WINDOW (dialog), title);
+
+ button = gtk_button_new_with_label (_("Remove Account"));
+ gtk_widget_set_margin_start (box, 24);
+ gtk_widget_set_margin_end (box, 24);
+ gtk_widget_set_halign (button, GTK_ALIGN_END);
+ gtk_widget_set_valign (button, GTK_ALIGN_END);
+ gtk_widget_set_visible (button, !goa_account_get_is_locked (account));
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "destructive-action");
+ gtk_container_add (GTK_CONTAINER (box), button);
+ g_signal_connect (button, "clicked", G_CALLBACK (on_remove_button_clicked_cb), application);
+}
+
+static int
+show_account (int argc,
+ char **argv)
+{
+ g_autoptr(GtkApplication) application = NULL;
+
+ gtk_init (&argc, &argv);
+
+ if (argc != 4)
+ {
+ g_printerr ("Not enough arguments");
+ return EXIT_FAILURE;
+ }
+
+ application = gtk_application_new ("org.gnome.Settings.GoaHelper",
+ G_APPLICATION_FLAGS_NONE);
+ g_signal_connect (application, "activate", G_CALLBACK (on_application_activate_show_account_cb), argv);
+
+ return g_application_run (G_APPLICATION (application), 0, NULL);
+}
+
+struct {
+ const char *command_name;
+ int (*command_func) (int argc,
+ char **argv);
+} commands[] = {
+ { "create-account", create_account, },
+ { "list-providers", list_providers, },
+ { "show-account", show_account, },
+};
+
+
+static void
+log_handler (const gchar *domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ g_printerr ("%s: %s\n", domain, message);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ gsize i;
+
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ if (argc < 2)
+ return EXIT_FAILURE;
+
+ g_log_set_default_handler (log_handler, NULL);
+
+ for (i = 0; i < G_N_ELEMENTS (commands); i++)
+ {
+ if (g_strcmp0 (commands[i].command_name, argv[1]) == 0)
+ return commands[i].command_func (argc, argv);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/gnome-initial-setup/pages/goa/goa.gresource.xml b/gnome-initial-setup/pages/goa/goa.gresource.xml
new file mode 100644
index 0000000..045858c
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/goa.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-goa-page.ui">gis-goa-page.ui</file>
+ <file alias="gis-goa-page.css">gis-goa-page.css</file>
+ </gresource>
+</gresources>
+
diff --git a/gnome-initial-setup/pages/goa/meson.build b/gnome-initial-setup/pages/goa/meson.build
new file mode 100644
index 0000000..b19fcc3
--- /dev/null
+++ b/gnome-initial-setup/pages/goa/meson.build
@@ -0,0 +1,38 @@
+sources += gnome.compile_resources(
+ 'goa-resources',
+ files('goa.gresource.xml'),
+ c_name: 'goa'
+)
+
+sources += files(
+ 'gis-goa-page.c',
+ 'gis-goa-page.h'
+)
+
+goa_helper_deps = [
+ dependency('goa-backend-1.0'),
+]
+
+goa_helper_cflags = []
+
+gtk_x11_dep = dependency('gtk+-x11-3.0', required: false)
+if gtk_x11_dep.found()
+ goa_helper_deps += [ gtk_x11_dep ]
+ goa_helper_cflags += ['-DHAVE_GTK_X11']
+endif
+
+gtk_wayland_dep = dependency('gtk+-wayland-3.0', required: false)
+if gtk_wayland_dep.found()
+ goa_helper_deps += [ gtk_wayland_dep ]
+ goa_helper_cflags += ['-DHAVE_GTK_WAYLAND']
+endif
+
+executable(
+ 'gnome-initial-setup-goa-helper',
+ 'gnome-initial-setup-goa-helper.c',
+ include_directories: config_h_dir,
+ dependencies: goa_helper_deps,
+ c_args: goa_helper_cflags,
+ install: true,
+ install_dir: libexec_dir,
+) \ No newline at end of file
diff --git a/gnome-initial-setup/pages/keyboard/cc-ibus-utils.c b/gnome-initial-setup/pages/keyboard/cc-ibus-utils.c
new file mode 100644
index 0000000..424c69e
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/cc-ibus-utils.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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/>.
+ */
+
+#include <config.h>
+
+#ifdef HAVE_IBUS
+#include "cc-ibus-utils.h"
+
+gchar *
+engine_get_display_name (IBusEngineDesc *engine_desc)
+{
+ const gchar *name;
+ const gchar *language_code;
+ const gchar *language;
+ const gchar *textdomain;
+ gchar *display_name;
+
+ name = ibus_engine_desc_get_longname (engine_desc);
+ language_code = ibus_engine_desc_get_language (engine_desc);
+ language = ibus_get_language_name (language_code);
+ textdomain = ibus_engine_desc_get_textdomain (engine_desc);
+ if (*textdomain != '\0' && *name != '\0')
+ name = g_dgettext (textdomain, name);
+ display_name = g_strdup_printf ("%s (%s)", language, name);
+
+ return display_name;
+}
+
+#endif /* HAVE_IBUS */
diff --git a/gnome-initial-setup/pages/keyboard/cc-ibus-utils.h b/gnome-initial-setup/pages/keyboard/cc-ibus-utils.h
new file mode 100644
index 0000000..da3d996
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/cc-ibus-utils.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2013 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/>.
+ */
+
+#ifndef __GIS_IBUS_UTILS_H__
+#define __GIS_IBUS_UTILS_H__
+
+#include <ibus.h>
+
+G_BEGIN_DECLS
+
+gchar *engine_get_display_name (IBusEngineDesc *engine_desc);
+
+G_END_DECLS
+
+#endif /* __GIS_IBUS_UTILS_H__ */
diff --git a/gnome-initial-setup/pages/keyboard/cc-input-chooser.c b/gnome-initial-setup/pages/keyboard/cc-input-chooser.c
new file mode 100644
index 0000000..efba249
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/cc-input-chooser.c
@@ -0,0 +1,883 @@
+/*
+ * Copyright (C) 2013 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ * Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+#include "cc-input-chooser.h"
+
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include <gtk/gtk.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#include "cc-ibus-utils.h"
+#endif
+
+#include "cc-common-language.h"
+
+#include <glib-object.h>
+
+#define INPUT_SOURCE_TYPE_XKB "xkb"
+#define INPUT_SOURCE_TYPE_IBUS "ibus"
+
+#define MIN_ROWS 5
+
+struct _CcInputChooserPrivate
+{
+ GtkWidget *filter_entry;
+ GtkWidget *input_list;
+ GHashTable *inputs;
+
+ GtkWidget *no_results;
+ GtkWidget *more_item;
+
+ gboolean showing_extra;
+ gchar *locale;
+ gchar *id;
+ gchar *type;
+ GnomeXkbInfo *xkb_info;
+#ifdef HAVE_IBUS
+ IBusBus *ibus;
+ GHashTable *ibus_engines;
+ GCancellable *ibus_cancellable;
+#endif
+};
+typedef struct _CcInputChooserPrivate CcInputChooserPrivate;
+G_DEFINE_TYPE_WITH_PRIVATE (CcInputChooser, cc_input_chooser, GTK_TYPE_BOX);
+
+enum {
+ PROP_0,
+ PROP_SHOWING_EXTRA,
+ PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+enum {
+ CHANGED,
+ CONFIRM,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+typedef struct {
+ GtkWidget *box;
+ GtkWidget *label;
+ GtkWidget *checkmark;
+
+ gchar *id;
+ gchar *type;
+ gchar *name;
+ gboolean is_extra;
+} InputWidget;
+
+static InputWidget *
+get_input_widget (GtkWidget *widget)
+{
+ return g_object_get_data (G_OBJECT (widget), "input-widget");
+}
+
+static GtkWidget *
+padded_label_new (char *text)
+{
+ GtkWidget *widget;
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (widget, 10);
+ gtk_widget_set_margin_bottom (widget, 10);
+ gtk_box_append (GTK_BOX (widget), gtk_label_new (text));
+ return widget;
+}
+
+static void
+input_widget_free (gpointer data)
+{
+ InputWidget *widget = data;
+
+ g_free (widget->id);
+ g_free (widget->type);
+ g_free (widget->name);
+ g_free (widget);
+}
+
+static gboolean
+get_layout (CcInputChooser *chooser,
+ const gchar *type,
+ const gchar *id,
+ const gchar **layout,
+ const gchar **variant)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ if (g_strcmp0 (type, INPUT_SOURCE_TYPE_XKB) == 0) {
+ gnome_xkb_info_get_layout_info (priv->xkb_info,
+ id, NULL, NULL,
+ layout, variant);
+ return TRUE;
+ }
+#ifdef HAVE_IBUS
+ if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) == 0) {
+ IBusEngineDesc *engine_desc = NULL;
+
+ if (priv->ibus_engines)
+ engine_desc = g_hash_table_lookup (priv->ibus_engines, id);
+
+ if (!engine_desc)
+ return FALSE;
+
+ *layout = ibus_engine_desc_get_layout (engine_desc);
+ *variant = "";
+ return TRUE;
+ }
+#endif
+ return FALSE;
+}
+
+static gboolean
+preview_cb (GtkLabel *label,
+ const gchar *uri,
+ CcInputChooser *chooser)
+{
+ GtkWidget *row;
+ InputWidget *widget;
+ const gchar *layout;
+ const gchar *variant;
+ gchar *commandline;
+
+ row = gtk_widget_get_parent (GTK_WIDGET (label));
+ widget = get_input_widget (row);
+
+ if (!get_layout (chooser, widget->type, widget->id, &layout, &variant))
+ return TRUE;
+
+ if (variant[0])
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l \"%s\t%s\"", layout, variant);
+ else
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l %s", layout);
+ g_spawn_command_line_async (commandline, NULL);
+ g_free (commandline);
+
+ return TRUE;
+}
+
+static GtkWidget *
+input_widget_new (CcInputChooser *chooser,
+ const char *type,
+ const char *id,
+ gboolean is_extra)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ GtkWidget *label;
+ InputWidget *widget = g_new0 (InputWidget, 1);
+ const gchar *name;
+ gchar *text;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB)) {
+ gnome_xkb_info_get_layout_info (priv->xkb_info, id, &name, NULL, NULL, NULL);
+ }
+#ifdef HAVE_IBUS
+ else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS)) {
+ if (priv->ibus_engines)
+ name = engine_get_display_name (g_hash_table_lookup (priv->ibus_engines, id));
+ else
+ name = id;
+ }
+#endif
+ else {
+ name = "ERROR";
+ }
+
+ widget->id = g_strdup (id);
+ widget->type = g_strdup (type);
+ widget->name = g_strdup (name);
+ widget->is_extra = is_extra;
+
+ widget->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_widget_set_halign (widget->box, GTK_ALIGN_FILL);
+ gtk_widget_set_margin_top (widget->box, 10);
+ gtk_widget_set_margin_bottom (widget->box, 10);
+ gtk_widget_set_margin_start (widget->box, 10);
+ gtk_widget_set_margin_end (widget->box, 10);
+
+ widget->label = gtk_label_new (name);
+ gtk_label_set_xalign (GTK_LABEL (widget->label), 0);
+ gtk_label_set_yalign (GTK_LABEL (widget->label), 0.5);
+ gtk_label_set_ellipsize (GTK_LABEL (widget->label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_max_width_chars (GTK_LABEL (widget->label), 40);
+ gtk_label_set_width_chars (GTK_LABEL (widget->label), 40);
+ gtk_box_append (GTK_BOX (widget->box), widget->label);
+
+
+ widget->checkmark = gtk_image_new_from_icon_name ("object-select-symbolic");
+ gtk_box_append (GTK_BOX (widget->box), widget->checkmark);
+
+ gtk_widget_set_margin_start (widget->checkmark, 10);
+ gtk_widget_set_margin_end (widget->checkmark, 10);
+ gtk_widget_set_halign (widget->box, GTK_ALIGN_START);
+
+ text = g_strdup_printf ("<a href='preview'>%s</a>", _("Preview"));
+ label = gtk_label_new ("");
+ gtk_label_set_markup (GTK_LABEL (label), text);
+ g_free (text);
+ g_signal_connect (label, "activate-link",
+ G_CALLBACK (preview_cb), chooser);
+ gtk_box_append (GTK_BOX (widget->box), label);
+
+ g_object_set_data_full (G_OBJECT (widget->box), "input-widget", widget,
+ input_widget_free);
+
+ return widget->box;
+}
+
+static void
+sync_all_checkmarks (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv;
+ GtkWidget *row;
+
+ priv = cc_input_chooser_get_instance_private (chooser);
+ row = gtk_widget_get_first_child (priv->input_list);
+ while (row) {
+ InputWidget *widget;
+ GtkWidget *child;
+ gboolean should_be_visible;
+
+ child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row));
+ widget = get_input_widget (child);
+
+ if (widget == NULL)
+ return;
+
+ if (priv->id == NULL || priv->type == NULL)
+ should_be_visible = FALSE;
+ else
+ should_be_visible = g_strcmp0 (widget->id, priv->id) == 0 &&
+ g_strcmp0 (widget->type, priv->type) == 0;
+ gtk_widget_set_opacity (widget->checkmark, should_be_visible ? 1.0 : 0.0);
+
+ if (widget->is_extra && should_be_visible)
+ widget->is_extra = FALSE;
+
+ row = gtk_widget_get_next_sibling (row);
+ }
+
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
+}
+
+static GtkWidget *
+more_widget_new (void)
+{
+ GtkWidget *widget;
+ GtkWidget *arrow;
+
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_widget_set_tooltip_text (widget, _("More…"));
+
+ arrow = gtk_image_new_from_icon_name ("view-more-symbolic");
+ gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
+ gtk_widget_set_margin_top (widget, 12);
+ gtk_widget_set_margin_bottom (widget, 12);
+ gtk_widget_set_hexpand (arrow, TRUE);
+ gtk_widget_set_halign (arrow, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign (arrow, GTK_ALIGN_CENTER);
+ gtk_box_append (GTK_BOX (widget), arrow);
+
+ return widget;
+}
+
+static GtkWidget *
+no_results_widget_new (void)
+{
+ GtkWidget *widget;
+
+ /* Translators: a search for input methods or keyboard layouts
+ * did not yield any results
+ */
+ widget = padded_label_new (_("No inputs found"));
+ gtk_widget_set_sensitive (widget, FALSE);
+ return widget;
+}
+
+static void
+choose_non_extras (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv;
+ GtkWidget *row;
+ guint count = 0;
+
+ priv = cc_input_chooser_get_instance_private (chooser);
+ row = gtk_widget_get_first_child (priv->input_list);
+ while (row) {
+ InputWidget *widget;
+ GtkWidget *child;
+
+ if (++count > MIN_ROWS)
+ break;
+
+ child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row));
+ widget = get_input_widget (child);
+ if (widget == NULL)
+ break;
+
+ widget->is_extra = FALSE;
+
+ row = gtk_widget_get_next_sibling (row);
+ }
+}
+
+static void
+add_rows_to_list (CcInputChooser *chooser,
+ GList *list,
+ const gchar *type,
+ const gchar *default_id)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ const gchar *id;
+ GtkWidget *widget;
+ gchar *key;
+
+ for (; list; list = list->next) {
+ id = (const gchar *) list->data;
+
+ if (g_strcmp0 (id, default_id) == 0)
+ continue;
+
+ key = g_strdup_printf ("%s::%s", type, id);
+ if (g_hash_table_contains (priv->inputs, key)) {
+ g_free (key);
+ continue;
+ }
+ g_hash_table_add (priv->inputs, key);
+
+ widget = input_widget_new (chooser, type, id, TRUE);
+ gtk_list_box_append (GTK_LIST_BOX (priv->input_list), widget);
+ }
+}
+
+static void
+add_row_to_list (CcInputChooser *chooser,
+ const gchar *type,
+ const gchar *id)
+{
+ GList tmp = { 0 };
+ tmp.data = (gpointer)id;
+ add_rows_to_list (chooser, &tmp, type, NULL);
+}
+
+static void
+get_locale_infos (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ const gchar *type = NULL;
+ const gchar *id = NULL;
+ gchar *lang, *country;
+ GList *list;
+
+ if (gnome_get_input_source_from_locale (priv->locale, &type, &id)) {
+ add_row_to_list (chooser, type, id);
+ if (!priv->id) {
+ priv->id = g_strdup (id);
+ priv->type = g_strdup (type);
+ }
+ }
+
+ if (!gnome_parse_locale (priv->locale, &lang, &country, NULL, NULL))
+ goto out;
+
+ list = gnome_xkb_info_get_layouts_for_language (priv->xkb_info, lang);
+ add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
+ g_list_free (list);
+
+ if (country != NULL) {
+ list = gnome_xkb_info_get_layouts_for_country (priv->xkb_info, country);
+ add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
+ g_list_free (list);
+ }
+
+ choose_non_extras (chooser);
+
+ list = gnome_xkb_info_get_all_layouts (priv->xkb_info);
+ add_rows_to_list (chooser, list, INPUT_SOURCE_TYPE_XKB, id);
+ g_list_free (list);
+
+out:
+ g_free (lang);
+ g_free (country);
+}
+
+static gboolean
+input_visible (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcInputChooser *chooser = user_data;
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ InputWidget *widget;
+ gboolean visible;
+ GtkWidget *child;
+ const char *search_term;
+
+ child = gtk_list_box_row_get_child (row);
+ if (child == priv->more_item)
+ return !priv->showing_extra && g_hash_table_size (priv->inputs) > MIN_ROWS;
+
+ widget = get_input_widget (child);
+
+ if (!priv->showing_extra && widget->is_extra)
+ return FALSE;
+
+ search_term = gtk_editable_get_text (GTK_EDITABLE (priv->filter_entry));
+ if (!search_term || !*search_term)
+ return TRUE;
+
+ visible = g_str_match_string (search_term, widget->name, TRUE);
+ return visible;
+}
+
+static gint
+sort_inputs (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ InputWidget *la, *lb;
+
+ la = get_input_widget (gtk_list_box_row_get_child (a));
+ lb = get_input_widget (gtk_list_box_row_get_child (b));
+
+ if (la == NULL)
+ return 1;
+
+ if (lb == NULL)
+ return -1;
+
+ if (la->is_extra && !lb->is_extra)
+ return 1;
+
+ if (!la->is_extra && lb->is_extra)
+ return -1;
+
+ return strcmp (la->name, lb->name);
+}
+
+static void
+filter_changed (GtkEntry *entry,
+ CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
+}
+
+static void
+show_more (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ if (g_hash_table_size (priv->inputs) <= MIN_ROWS)
+ return;
+
+ gtk_widget_grab_focus (priv->filter_entry);
+
+ gtk_widget_set_valign (GTK_WIDGET (chooser), GTK_ALIGN_FILL);
+
+ priv->showing_extra = TRUE;
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->input_list));
+ g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_SHOWING_EXTRA]);
+}
+
+static void
+set_input (CcInputChooser *chooser,
+ const gchar *id,
+ const gchar *type)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ if (g_strcmp0 (priv->id, id) == 0 &&
+ g_strcmp0 (priv->type, type) == 0)
+ return;
+
+ g_free (priv->id);
+ g_free (priv->type);
+ priv->id = g_strdup (id);
+ priv->type = g_strdup (type);
+
+ sync_all_checkmarks (chooser);
+
+ g_signal_emit (chooser, signals[CHANGED], 0);
+}
+
+static gboolean
+confirm_choice (gpointer data)
+{
+ GtkWidget *widget = data;
+
+ g_signal_emit (widget, signals[CONFIRM], 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+row_activated (GtkListBox *box,
+ GtkListBoxRow *row,
+ CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ GtkWidget *child;
+ InputWidget *widget;
+
+ if (row == NULL)
+ return;
+
+ child = gtk_list_box_row_get_child (row);
+ if (child == priv->more_item) {
+ show_more (chooser);
+ } else {
+ widget = get_input_widget (child);
+ if (widget == NULL)
+ return;
+ if (g_strcmp0 (priv->id, widget->id) == 0 &&
+ g_strcmp0 (priv->type, widget->type) == 0)
+ confirm_choice (chooser);
+ else
+ set_input (chooser, widget->id, widget->type);
+ }
+}
+
+#ifdef HAVE_IBUS
+static void
+update_ibus_active_sources (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv;
+ IBusEngineDesc *engine_desc;
+ GtkWidget *child;
+ const gchar *type;
+ const gchar *id;
+ gchar *name;
+
+ priv = cc_input_chooser_get_instance_private (chooser);
+ child = gtk_widget_get_first_child (priv->input_list);
+ while (child) {
+ InputWidget *row;
+
+ row = get_input_widget (child);
+ child = gtk_widget_get_next_sibling (child);
+
+ if (row == NULL)
+ continue;
+
+ type = row->type;
+ id = row->id;
+ if (g_strcmp0 (type, INPUT_SOURCE_TYPE_IBUS) != 0)
+ continue;
+
+ engine_desc = g_hash_table_lookup (priv->ibus_engines, id);
+ if (engine_desc) {
+ name = engine_get_display_name (engine_desc);
+ gtk_label_set_text (GTK_LABEL (row->label), name);
+ g_free (name);
+ }
+ }
+}
+
+static void
+get_ibus_locale_infos (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ GHashTableIter iter;
+ const gchar *engine_id;
+ IBusEngineDesc *engine;
+
+ if (!priv->ibus_engines)
+ return;
+
+ g_hash_table_iter_init (&iter, priv->ibus_engines);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine))
+ add_row_to_list (chooser, INPUT_SOURCE_TYPE_IBUS, engine_id);
+}
+
+static void
+fetch_ibus_engines_result (GObject *object,
+ GAsyncResult *result,
+ CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv;
+ GList *list, *l;
+ GError *error;
+
+ error = NULL;
+ list = ibus_bus_list_engines_async_finish (IBUS_BUS (object), result, &error);
+ if (!list && error) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Couldn't finish IBus request: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ priv = cc_input_chooser_get_instance_private (chooser);
+ g_clear_object (&priv->ibus_cancellable);
+
+ /* Maps engine ids to engine description objects */
+ priv->ibus_engines = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
+
+ for (l = list; l; l = l->next) {
+ IBusEngineDesc *engine = l->data;
+ const gchar *engine_id;
+
+ engine_id = ibus_engine_desc_get_name (engine);
+ if (g_str_has_prefix (engine_id, "xkb:"))
+ g_object_unref (engine);
+ else
+ g_hash_table_replace (priv->ibus_engines, (gpointer)engine_id, engine);
+ }
+ g_list_free (list);
+
+ update_ibus_active_sources (chooser);
+ get_ibus_locale_infos (chooser);
+
+ sync_all_checkmarks (chooser);
+}
+
+static void
+fetch_ibus_engines (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ priv->ibus_cancellable = g_cancellable_new ();
+
+ ibus_bus_list_engines_async (priv->ibus,
+ -1,
+ priv->ibus_cancellable,
+ (GAsyncReadyCallback)fetch_ibus_engines_result,
+ chooser);
+
+ /* We've got everything we needed, don't want to be called again. */
+ g_signal_handlers_disconnect_by_func (priv->ibus, fetch_ibus_engines, chooser);
+}
+
+static void
+maybe_start_ibus (void)
+{
+ /* IBus doesn't export API in the session bus. The only thing
+ * we have there is a well known name which we can use as a
+ * sure-fire way to activate it.
+ */
+ g_bus_unwatch_name (g_bus_watch_name (G_BUS_TYPE_SESSION,
+ IBUS_SERVICE_IBUS,
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ NULL,
+ NULL,
+ NULL,
+ NULL));
+}
+#endif
+
+static void
+cc_input_chooser_constructed (GObject *object)
+{
+ CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ G_OBJECT_CLASS (cc_input_chooser_parent_class)->constructed (object);
+
+ priv->xkb_info = gnome_xkb_info_new ();
+
+#ifdef HAVE_IBUS
+ ibus_init ();
+ if (!priv->ibus) {
+ priv->ibus = ibus_bus_new_async ();
+ if (ibus_bus_is_connected (priv->ibus))
+ fetch_ibus_engines (chooser);
+ else
+ g_signal_connect_swapped (priv->ibus, "connected",
+ G_CALLBACK (fetch_ibus_engines), chooser);
+ }
+ maybe_start_ibus ();
+#endif
+
+ priv->inputs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ priv->more_item = more_widget_new ();
+ priv->no_results = no_results_widget_new ();
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->input_list),
+ sort_inputs, chooser, NULL);
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->input_list),
+ input_visible, chooser, NULL);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->input_list),
+ GTK_SELECTION_NONE);
+
+ if (priv->locale == NULL) {
+ priv->locale = cc_common_language_get_current_language ();
+ }
+
+ get_locale_infos (chooser);
+#ifdef HAVE_IBUS
+ get_ibus_locale_infos (chooser);
+#endif
+
+ gtk_list_box_append (GTK_LIST_BOX (priv->input_list), priv->more_item);
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->input_list), priv->no_results);
+
+ g_signal_connect (priv->filter_entry, "changed",
+ G_CALLBACK (filter_changed),
+ chooser);
+
+ g_signal_connect (priv->input_list, "row-activated",
+ G_CALLBACK (row_activated), chooser);
+
+ sync_all_checkmarks (chooser);
+}
+
+static void
+cc_input_chooser_finalize (GObject *object)
+{
+ CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ g_clear_object (&priv->xkb_info);
+ g_hash_table_unref (priv->inputs);
+#ifdef HAVE_IBUS
+ g_clear_object (&priv->ibus);
+ if (priv->ibus_cancellable)
+ g_cancellable_cancel (priv->ibus_cancellable);
+ g_clear_object (&priv->ibus_cancellable);
+ g_clear_pointer (&priv->ibus_engines, g_hash_table_destroy);
+#endif
+
+ G_OBJECT_CLASS (cc_input_chooser_parent_class)->finalize (object);
+}
+
+static void
+cc_input_chooser_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcInputChooser *chooser = CC_INPUT_CHOOSER (object);
+ switch (prop_id) {
+ case PROP_SHOWING_EXTRA:
+ g_value_set_boolean (value, cc_input_chooser_get_showing_extra (chooser));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_input_chooser_class_init (CcInputChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/input-chooser.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, filter_entry);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcInputChooser, input_list);
+
+ object_class->finalize = cc_input_chooser_finalize;
+ object_class->get_property = cc_input_chooser_get_property;
+ object_class->constructed = cc_input_chooser_constructed;
+
+ obj_props[PROP_SHOWING_EXTRA] =
+ g_param_spec_string ("showing-extra", "", "", "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ signals[CHANGED] =
+ g_signal_new ("changed",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ signals[CONFIRM] =
+ g_signal_new ("confirm",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+}
+
+static void
+cc_input_chooser_init (CcInputChooser *chooser)
+{
+ gtk_widget_init_template (GTK_WIDGET (chooser));
+}
+
+void
+cc_input_chooser_clear_filter (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ gtk_editable_set_text (GTK_EDITABLE (priv->filter_entry), "");
+}
+
+const gchar *
+cc_input_chooser_get_input_id (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ return priv->id;
+}
+
+const gchar *
+cc_input_chooser_get_input_type (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ return priv->type;
+}
+
+void
+cc_input_chooser_get_layout (CcInputChooser *chooser,
+ const gchar **layout,
+ const gchar **variant)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+
+ if (!get_layout (chooser, priv->type, priv->id, layout, variant)) {
+ if (layout != NULL)
+ *layout = NULL;
+ if (variant != NULL)
+ *variant = NULL;
+ }
+}
+
+void
+cc_input_chooser_set_input (CcInputChooser *chooser,
+ const gchar *id,
+ const gchar *type)
+{
+ set_input (chooser, id, type);
+}
+
+gboolean
+cc_input_chooser_get_showing_extra (CcInputChooser *chooser)
+{
+ CcInputChooserPrivate *priv = cc_input_chooser_get_instance_private (chooser);
+ return priv->showing_extra;
+}
diff --git a/gnome-initial-setup/pages/keyboard/cc-input-chooser.h b/gnome-initial-setup/pages/keyboard/cc-input-chooser.h
new file mode 100644
index 0000000..dfd6a28
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/cc-input-chooser.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ * Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __GIS_INPUT_CHOOSER_H__
+#define __GIS_INPUT_CHOOSER_H__
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+#define CC_TYPE_INPUT_CHOOSER (cc_input_chooser_get_type ())
+#define CC_INPUT_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CC_TYPE_INPUT_CHOOSER, CcInputChooser))
+#define CC_INPUT_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CC_TYPE_INPUT_CHOOSER, CcInputChooserClass))
+#define CC_IS_INPUT_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CC_TYPE_INPUT_CHOOSER))
+#define CC_IS_INPUT_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CC_TYPE_INPUT_CHOOSER))
+#define CC_INPUT_CHOOSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CC_TYPE_INPUT_CHOOSER, CcInputChooserClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CcInputChooser CcInputChooser;
+typedef struct _CcInputChooserClass CcInputChooserClass;
+
+struct _CcInputChooser
+{
+ GtkBox parent;
+};
+
+struct _CcInputChooserClass
+{
+ GtkBoxClass parent_class;
+};
+
+GType cc_input_chooser_get_type (void);
+
+void cc_input_chooser_clear_filter (CcInputChooser *chooser);
+const gchar * cc_input_chooser_get_input_id (CcInputChooser *chooser);
+const gchar * cc_input_chooser_get_input_type (CcInputChooser *chooser);
+void cc_input_chooser_set_input (CcInputChooser *chooser,
+ const gchar *id,
+ const gchar *type);
+void cc_input_chooser_get_layout (CcInputChooser *chooser,
+ const gchar **layout,
+ const gchar **variant);
+gboolean cc_input_chooser_get_showing_extra (CcInputChooser *chooser);
+
+G_END_DECLS
+
+#endif /* __GIS_INPUT_CHOOSER_H__ */
diff --git a/gnome-initial-setup/pages/keyboard/gis-keyboard-page.c b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.c
new file mode 100644
index 0000000..3adfd66
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.c
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2010 Intel, 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: Sergey Udaltsov <svu@gnome.org>
+ * Michael Wood <michael.g.wood@intel.com>
+ *
+ * Based on gnome-control-center cc-region-panel.c
+ */
+
+#define PAGE_ID "keyboard"
+
+#include "config.h"
+
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <polkit/polkit.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#include "gis-keyboard-page.h"
+#include "keyboard-resources.h"
+#include "cc-input-chooser.h"
+
+#include "cc-common-language.h"
+
+#include "gis-page-header.h"
+
+#define GNOME_DESKTOP_INPUT_SOURCES_DIR "org.gnome.desktop.input-sources"
+#define KEY_CURRENT_INPUT_SOURCE "current"
+#define KEY_INPUT_SOURCES "sources"
+
+struct _GisKeyboardPagePrivate {
+ GtkWidget *input_chooser;
+
+ GDBusProxy *localed;
+ GCancellable *cancellable;
+ GPermission *permission;
+ GSettings *input_settings;
+
+ GSList *system_sources;
+};
+typedef struct _GisKeyboardPagePrivate GisKeyboardPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisKeyboardPage, gis_keyboard_page, GIS_TYPE_PAGE);
+
+static void
+gis_keyboard_page_finalize (GObject *object)
+{
+ GisKeyboardPage *self = GIS_KEYBOARD_PAGE (object);
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+
+ if (priv->cancellable)
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+
+ g_clear_object (&priv->permission);
+ g_clear_object (&priv->localed);
+ g_clear_object (&priv->input_settings);
+
+ g_slist_free_full (priv->system_sources, g_free);
+
+ G_OBJECT_CLASS (gis_keyboard_page_parent_class)->finalize (object);
+}
+
+static void
+set_input_settings (GisKeyboardPage *self)
+{
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+ const gchar *type;
+ const gchar *id;
+ GVariantBuilder builder;
+ GSList *l;
+ gboolean is_xkb_source = FALSE;
+
+ type = cc_input_chooser_get_input_type (CC_INPUT_CHOOSER (priv->input_chooser));
+ id = cc_input_chooser_get_input_id (CC_INPUT_CHOOSER (priv->input_chooser));
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)"));
+
+ if (g_str_equal (type, "xkb")) {
+ g_variant_builder_add (&builder, "(ss)", type, id);
+ is_xkb_source = TRUE;
+ }
+
+ for (l = priv->system_sources; l; l = l->next) {
+ const gchar *sid = l->data;
+
+ if (g_str_equal (id, sid) && g_str_equal (type, "xkb"))
+ continue;
+
+ g_variant_builder_add (&builder, "(ss)", "xkb", sid);
+ }
+
+ if (!is_xkb_source)
+ g_variant_builder_add (&builder, "(ss)", type, id);
+
+ g_settings_set_value (priv->input_settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder));
+ g_settings_set_uint (priv->input_settings, KEY_CURRENT_INPUT_SOURCE, 0);
+
+ g_settings_apply (priv->input_settings);
+}
+
+static void
+set_localed_input (GisKeyboardPage *self)
+{
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+ const gchar *layout, *variant;
+ GString *layouts;
+ GString *variants;
+ GSList *l;
+
+ if (!priv->localed)
+ return;
+
+ cc_input_chooser_get_layout (CC_INPUT_CHOOSER (priv->input_chooser), &layout, &variant);
+ if (layout == NULL)
+ layout = "";
+ if (variant == NULL)
+ variant = "";
+
+ layouts = g_string_new (layout);
+ variants = g_string_new (variant);
+
+#define LAYOUT(a) (a[0])
+#define VARIANT(a) (a[1] ? a[1] : "")
+ for (l = priv->system_sources; l; l = l->next) {
+ const gchar *sid = l->data;
+ gchar **lv = g_strsplit (sid, "+", -1);
+
+ if (!g_str_equal (LAYOUT (lv), layout) ||
+ !g_str_equal (VARIANT (lv), variant)) {
+ if (layouts->str[0]) {
+ g_string_append_c (layouts, ',');
+ g_string_append_c (variants, ',');
+ }
+ g_string_append (layouts, LAYOUT (lv));
+ g_string_append (variants, VARIANT (lv));
+ }
+ g_strfreev (lv);
+ }
+#undef LAYOUT
+#undef VARIANT
+
+ g_dbus_proxy_call (priv->localed,
+ "SetX11Keyboard",
+ g_variant_new ("(ssssbb)", layouts->str, "", variants->str, "", TRUE, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+ g_string_free (layouts, TRUE);
+ g_string_free (variants, TRUE);
+}
+
+static void
+change_locale_permission_acquired (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GisKeyboardPage *page = GIS_KEYBOARD_PAGE (data);
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (page);
+ GError *error = NULL;
+ gboolean allowed;
+
+ allowed = g_permission_acquire_finish (priv->permission, res, &error);
+ if (error) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to acquire permission: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ if (allowed)
+ set_localed_input (GIS_KEYBOARD_PAGE (data));
+}
+
+static void
+update_input (GisKeyboardPage *self)
+{
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+
+ set_input_settings (self);
+
+ if (gis_driver_get_mode (GIS_PAGE (self)->driver) == GIS_DRIVER_MODE_NEW_USER) {
+ if (g_permission_get_allowed (priv->permission)) {
+ set_localed_input (self);
+ } else if (g_permission_get_can_acquire (priv->permission)) {
+ g_permission_acquire_async (priv->permission,
+ NULL,
+ change_locale_permission_acquired,
+ self);
+ }
+ }
+}
+
+static gboolean
+gis_keyboard_page_apply (GisPage *page,
+ GCancellable *cancellable)
+{
+ update_input (GIS_KEYBOARD_PAGE (page));
+ return FALSE;
+}
+
+static GSList *
+get_localed_input (GDBusProxy *proxy)
+{
+ GVariant *v;
+ const gchar *s;
+ gchar *id;
+ guint i, n;
+ gchar **layouts = NULL;
+ gchar **variants = NULL;
+ GSList *sources = NULL;
+
+ v = g_dbus_proxy_get_cached_property (proxy, "X11Layout");
+ if (v) {
+ s = g_variant_get_string (v, NULL);
+ layouts = g_strsplit (s, ",", -1);
+ g_variant_unref (v);
+ }
+
+ v = g_dbus_proxy_get_cached_property (proxy, "X11Variant");
+ if (v) {
+ s = g_variant_get_string (v, NULL);
+ if (s && *s)
+ variants = g_strsplit (s, ",", -1);
+ g_variant_unref (v);
+ }
+
+ if (variants && variants[0])
+ n = MIN (g_strv_length (layouts), g_strv_length (variants));
+ else if (layouts && layouts[0])
+ n = g_strv_length (layouts);
+ else
+ n = 0;
+
+ for (i = 0; i < n && layouts[i][0]; i++) {
+ if (variants && variants[i] && variants[i][0])
+ id = g_strdup_printf ("%s+%s", layouts[i], variants[i]);
+ else
+ id = g_strdup (layouts[i]);
+ sources = g_slist_prepend (sources, id);
+ }
+
+ g_strfreev (variants);
+ g_strfreev (layouts);
+
+ return sources;
+}
+
+static void
+add_default_keyboard_layout (GDBusProxy *proxy,
+ GVariantBuilder *builder)
+{
+ GSList *sources = get_localed_input (proxy);
+ sources = g_slist_reverse (sources);
+
+ for (; sources; sources = sources->next)
+ g_variant_builder_add (builder, "(ss)", "xkb",
+ (const gchar *) sources->data);
+
+ g_slist_free_full (sources, g_free);
+}
+
+static void
+add_default_input_sources (GisKeyboardPage *self,
+ GDBusProxy *proxy)
+{
+ const gchar *type;
+ const gchar *id;
+ gchar *language;
+ GVariantBuilder builder;
+ GSettings *input_settings;
+
+ input_settings = g_settings_new (GNOME_DESKTOP_INPUT_SOURCES_DIR);
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)"));
+
+ add_default_keyboard_layout (proxy, &builder);
+
+ /* add other input sources */
+ language = cc_common_language_get_current_language ();
+ if (gnome_get_input_source_from_locale (language, &type, &id)) {
+ if (!g_str_equal (type, "xkb"))
+ g_variant_builder_add (&builder, "(ss)", type, id);
+ }
+ g_free (language);
+
+ g_settings_delay (input_settings);
+ g_settings_set_value (input_settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder));
+ g_settings_set_uint (input_settings, KEY_CURRENT_INPUT_SOURCE, 0);
+ g_settings_apply (input_settings);
+
+ g_object_unref (input_settings);
+}
+
+static void
+skip_proxy_ready (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GisKeyboardPage *self = data;
+ GDBusProxy *proxy;
+ GError *error = NULL;
+
+ proxy = g_dbus_proxy_new_finish (res, &error);
+
+ if (!proxy) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to contact localed: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ add_default_input_sources (self, proxy);
+
+ g_object_unref (proxy);
+}
+
+static void
+gis_keyboard_page_skip (GisPage *page)
+{
+ GisKeyboardPage *self = GIS_KEYBOARD_PAGE (page);
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+ NULL,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ priv->cancellable,
+ (GAsyncReadyCallback) skip_proxy_ready,
+ self);
+}
+
+static void
+preselect_input_source (GisKeyboardPage *self)
+{
+ const gchar *type;
+ const gchar *id;
+ gchar *language;
+ gboolean desktop_got_something;
+ gboolean desktop_got_input_method;
+
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+ GSList *sources = get_localed_input (priv->localed);
+
+ /* These will be added silently after the user selection when
+ * writing out the settings. */
+ g_slist_free_full (priv->system_sources, g_free);
+ priv->system_sources = g_slist_reverse (sources);
+
+ /* We have two potential sources of information as to which
+ * source to pre-select here: the keyboard layout that is
+ * configured system-wide (read from priv->system_sources),
+ * and a gnome-desktop function that lets us look up a default
+ * input source for a given language.
+ *
+ * An important limitation here is that there is no system-wide
+ * configuration for input methods, so if the best choice for the
+ * language is an input method, we will only find it from the
+ * gnome-desktop lookup. But if both sources give us keyboard layouts,
+ * we want to prefer the one that's configured system-wide over the one
+ * from gnome-desktop.
+ *
+ * So we first do the gnome-desktop lookup, and keep track of what we
+ * got.
+ *
+ * - If we got an input method, we preselect that, and we're done.
+ * - If we got a keyboard layout, and there's no system-wide keyboard
+ * layout set, we preselect the layout we got from gnome-desktop.
+ * - If we didn't get an input method from gnome-desktop and there
+ * is a system-wide keyboard layout set, we preselect that.
+ * - If we got nothing from gnome-desktop and there's no system-wide
+ * keyboard layout set, we don't preselect anything.
+ *
+ * See:
+ * - https://bugzilla.gnome.org/show_bug.cgi?id=776189
+ * - https://gitlab.gnome.org/GNOME/gnome-initial-setup/-/issues/104
+ */
+ language = cc_common_language_get_current_language ();
+
+ desktop_got_something = gnome_get_input_source_from_locale (language, &type, &id);
+ desktop_got_input_method = (desktop_got_something && g_strcmp0 (type, "xkb") != 0);
+
+ if (desktop_got_something && (desktop_got_input_method || !priv->system_sources)) {
+ cc_input_chooser_set_input (CC_INPUT_CHOOSER (priv->input_chooser),
+ id, type);
+ } else if (priv->system_sources) {
+ cc_input_chooser_set_input (CC_INPUT_CHOOSER (priv->input_chooser),
+ (const gchar *) priv->system_sources->data,
+ "xkb");
+ }
+
+ g_free (language);
+}
+
+static void
+update_page_complete (GisKeyboardPage *self)
+{
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+ gboolean complete;
+
+ complete = (priv->localed != NULL &&
+ cc_input_chooser_get_input_id (CC_INPUT_CHOOSER (priv->input_chooser)) != NULL);
+ gis_page_set_complete (GIS_PAGE (self), complete);
+}
+
+static void
+localed_proxy_ready (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GisKeyboardPage *self = data;
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+ GDBusProxy *proxy;
+ GError *error = NULL;
+
+ proxy = g_dbus_proxy_new_finish (res, &error);
+
+ if (!proxy) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to contact localed: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ priv->localed = proxy;
+
+ preselect_input_source (self);
+ update_page_complete (self);
+}
+
+static void
+input_confirmed (CcInputChooser *chooser,
+ GisKeyboardPage *self)
+{
+ gis_assistant_next_page (gis_driver_get_assistant (GIS_PAGE (self)->driver));
+}
+
+static void
+input_changed (CcInputChooser *chooser,
+ GisKeyboardPage *self)
+{
+ update_page_complete (self);
+}
+
+static void
+gis_keyboard_page_constructed (GObject *object)
+{
+ GisKeyboardPage *self = GIS_KEYBOARD_PAGE (object);
+ GisKeyboardPagePrivate *priv = gis_keyboard_page_get_instance_private (self);
+
+ g_type_ensure (CC_TYPE_INPUT_CHOOSER);
+
+ G_OBJECT_CLASS (gis_keyboard_page_parent_class)->constructed (object);
+
+ g_signal_connect (priv->input_chooser, "confirm",
+ G_CALLBACK (input_confirmed), self);
+ g_signal_connect (priv->input_chooser, "changed",
+ G_CALLBACK (input_changed), self);
+
+ priv->input_settings = g_settings_new (GNOME_DESKTOP_INPUT_SOURCES_DIR);
+ g_settings_delay (priv->input_settings);
+
+ priv->cancellable = g_cancellable_new ();
+
+ g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+ NULL,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ priv->cancellable,
+ (GAsyncReadyCallback) localed_proxy_ready,
+ self);
+
+ /* If we're in new user mode then we're manipulating system settings */
+ if (gis_driver_get_mode (GIS_PAGE (self)->driver) == GIS_DRIVER_MODE_NEW_USER)
+ priv->permission = polkit_permission_new_sync ("org.freedesktop.locale1.set-keyboard", NULL, NULL, NULL);
+
+ update_page_complete (self);
+
+ gtk_widget_show (GTK_WIDGET (self));
+}
+
+static void
+gis_keyboard_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Typing"));
+}
+
+static void
+gis_keyboard_page_class_init (GisKeyboardPageClass * klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GisPageClass * page_class = GIS_PAGE_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-keyboard-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisKeyboardPage, input_chooser);
+
+ page_class->page_id = PAGE_ID;
+ page_class->apply = gis_keyboard_page_apply;
+ page_class->skip = gis_keyboard_page_skip;
+ page_class->locale_changed = gis_keyboard_page_locale_changed;
+ object_class->constructed = gis_keyboard_page_constructed;
+ object_class->finalize = gis_keyboard_page_finalize;
+}
+
+static void
+gis_keyboard_page_init (GisKeyboardPage *self)
+{
+ g_resources_register (keyboard_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ g_type_ensure (CC_TYPE_INPUT_CHOOSER);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GisPage *
+gis_prepare_keyboard_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_KEYBOARD_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/keyboard/gis-keyboard-page.h b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.h
new file mode 100644
index 0000000..d5710a0
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 Intel, 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: Sergey Udaltsov <svu@gnome.org>
+ *
+ */
+
+
+#ifndef _GIS_KEYBOARD_PAGE_H
+#define _GIS_KEYBOARD_PAGE_H
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_KEYBOARD_PAGE gis_keyboard_page_get_type()
+
+#define GIS_KEYBOARD_PAGE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ GIS_TYPE_KEYBOARD_PAGE, GisKeyboardPage))
+
+#define GIS_KEYBOARD_PAGE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), \
+ GIS_TYPE_KEYBOARD_PAGE, GisKeyboardPageClass))
+
+#define GIS_IS_KEYBOARD_PAGE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ GIS_TYPE_KEYBOARD_PAGE))
+
+#define GIS_IS_KEYBOARD_PAGE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), \
+ GIS_TYPE_KEYBOARD_PAGE))
+
+#define GIS_KEYBOARD_PAGE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), \
+ GIS_TYPE_KEYBOARD_PAGE, GisKeyboardPageClass))
+
+typedef struct _GisKeyboardPage GisKeyboardPage;
+typedef struct _GisKeyboardPageClass GisKeyboardPageClass;
+
+struct _GisKeyboardPage
+{
+ GisPage parent;
+};
+
+struct _GisKeyboardPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_keyboard_page_get_type (void) G_GNUC_CONST;
+
+GisPage *gis_prepare_keyboard_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* _GIS_KEYBOARD_PAGE_H */
diff --git a/gnome-initial-setup/pages/keyboard/gis-keyboard-page.ui b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.ui
new file mode 100644
index 0000000..a47d8a1
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/gis-keyboard-page.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisKeyboardPage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+
+ <child>
+ <object class="GtkBox" id="page">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Typing</property>
+ <property name="subtitle" translatable="yes">Select your keyboard layout or an input method.</property>
+ <property name="icon_name">input-keyboard-symbolic</property>
+ <property name="show_icon" bind-source="GisKeyboardPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="CcInputChooser" id="input_chooser">
+ <property name="margin_top">18</property>
+ <property name="margin_bottom">18</property>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/keyboard/input-chooser.ui b/gnome-initial-setup/pages/keyboard/input-chooser.ui
new file mode 100644
index 0000000..5fe2229
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/input-chooser.ui
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="CcInputChooser" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkSearchEntry" id="filter_entry">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="input_list">
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/keyboard/keyboard.gresource.xml b/gnome-initial-setup/pages/keyboard/keyboard.gresource.xml
new file mode 100644
index 0000000..103b3f1
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/keyboard.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks">gis-keyboard-page.ui</file>
+ <file preprocess="xml-stripblanks">input-chooser.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/keyboard/meson.build b/gnome-initial-setup/pages/keyboard/meson.build
new file mode 100644
index 0000000..69d6de8
--- /dev/null
+++ b/gnome-initial-setup/pages/keyboard/meson.build
@@ -0,0 +1,14 @@
+sources += gnome.compile_resources(
+ 'keyboard-resources',
+ files('keyboard.gresource.xml'),
+ c_name: 'keyboard'
+)
+
+sources += files(
+ 'cc-input-chooser.c',
+ 'cc-input-chooser.h',
+ 'cc-ibus-utils.c',
+ 'cc-ibus-utils.h',
+ 'gis-keyboard-page.c',
+ 'gis-keyboard-page.h'
+)
diff --git a/gnome-initial-setup/pages/language/cc-language-chooser.c b/gnome-initial-setup/pages/language/cc-language-chooser.c
new file mode 100644
index 0000000..309704c
--- /dev/null
+++ b/gnome-initial-setup/pages/language/cc-language-chooser.c
@@ -0,0 +1,597 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ * Matthias Clasen <mclasen@redhat.com>
+ */
+
+#include "config.h"
+#include "cc-language-chooser.h"
+
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include <gtk/gtk.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#include "cc-common-language.h"
+#include "cc-util.h"
+
+#include <glib-object.h>
+
+struct _CcLanguageChooserPrivate
+{
+ GtkWidget *filter_entry;
+ GtkWidget *language_list;
+
+ GtkWidget *no_results;
+ GtkWidget *more_item;
+
+ gboolean showing_extra;
+ gchar *language;
+};
+typedef struct _CcLanguageChooserPrivate CcLanguageChooserPrivate;
+G_DEFINE_TYPE_WITH_PRIVATE (CcLanguageChooser, cc_language_chooser, GTK_TYPE_BOX);
+
+enum {
+ PROP_0,
+ PROP_LANGUAGE,
+ PROP_SHOWING_EXTRA,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+enum {
+ CONFIRM,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+typedef struct {
+ GtkWidget *box;
+ GtkWidget *checkmark;
+
+ gchar *locale_id;
+ gchar *locale_name;
+ gchar *locale_current_name;
+ gchar *locale_untranslated_name;
+ gchar *sort_key;
+ gboolean is_extra;
+} LanguageWidget;
+
+static LanguageWidget *
+get_language_widget (GtkWidget *widget)
+{
+ return g_object_get_data (G_OBJECT (widget), "language-widget");
+}
+
+static GtkWidget *
+padded_label_new (char *text)
+{
+ GtkWidget *widget;
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_halign (widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top (widget, 12);
+ gtk_widget_set_margin_bottom (widget, 12);
+ gtk_box_append (GTK_BOX (widget), gtk_label_new (text));
+ return widget;
+}
+
+static void
+language_widget_free (gpointer data)
+{
+ LanguageWidget *widget = data;
+
+ /* This is called when the box is destroyed,
+ * so don't bother destroying the widget and
+ * children again. */
+ g_free (widget->locale_id);
+ g_free (widget->locale_name);
+ g_free (widget->locale_current_name);
+ g_free (widget->locale_untranslated_name);
+ g_free (widget->sort_key);
+ g_free (widget);
+}
+
+static GtkWidget *
+language_widget_new (const char *locale_id,
+ gboolean is_extra)
+{
+ GtkWidget *label;
+ gchar *locale_name, *locale_current_name, *locale_untranslated_name;
+ gchar *language = NULL;
+ gchar *language_name;
+ gchar *country = NULL;
+ gchar *country_name = NULL;
+ LanguageWidget *widget = g_new0 (LanguageWidget, 1);
+
+ if (!gnome_parse_locale (locale_id, &language, &country, NULL, NULL))
+ return NULL;
+
+ language_name = gnome_get_language_from_code (language, locale_id);
+ if (language_name == NULL)
+ language_name = gnome_get_language_from_code (language, NULL);
+
+ if (country) {
+ country_name = gnome_get_country_from_code (country, locale_id);
+ if (country_name == NULL)
+ country_name = gnome_get_country_from_code (country, NULL);
+ }
+
+ locale_name = gnome_get_language_from_locale (locale_id, locale_id);
+ locale_current_name = gnome_get_language_from_locale (locale_id, NULL);
+ locale_untranslated_name = gnome_get_language_from_locale (locale_id, "C");
+
+ widget->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_margin_top (widget->box, 12);
+ gtk_widget_set_margin_bottom (widget->box, 12);
+ gtk_widget_set_margin_start (widget->box, 12);
+ gtk_widget_set_margin_end (widget->box, 12);
+ gtk_widget_set_halign (widget->box, GTK_ALIGN_FILL);
+
+ label = gtk_label_new (language_name);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 30);
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_box_append (GTK_BOX (widget->box), label);
+
+ widget->checkmark = gtk_image_new_from_icon_name ("object-select-symbolic");
+ gtk_box_append (GTK_BOX (widget->box), widget->checkmark);
+
+ if (country_name) {
+ label = gtk_label_new (country_name);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
+ gtk_label_set_max_width_chars (GTK_LABEL (label), 30);
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_widget_set_hexpand (label, TRUE);
+ gtk_widget_set_halign (label, GTK_ALIGN_END);
+ gtk_box_append (GTK_BOX (widget->box), label);
+ }
+
+ widget->locale_id = g_strdup (locale_id);
+ widget->locale_name = locale_name;
+ widget->locale_current_name = locale_current_name;
+ widget->locale_untranslated_name = locale_untranslated_name;
+ widget->is_extra = is_extra;
+ widget->sort_key = cc_util_normalize_casefold_and_unaccent (locale_name);
+
+ g_object_set_data_full (G_OBJECT (widget->box), "language-widget", widget,
+ language_widget_free);
+
+ g_free (language);
+ g_free (language_name);
+ g_free (country);
+ g_free (country_name);
+
+ return widget->box;
+}
+
+static void
+sync_all_checkmarks (CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ GtkWidget *row;
+
+ row = gtk_widget_get_first_child (priv->language_list);
+ while (row) {
+ LanguageWidget *widget;
+ GtkWidget *child;
+ gboolean should_be_visible;
+
+ child = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (row));
+ widget = get_language_widget (child);
+
+ if (widget == NULL)
+ return;
+
+ should_be_visible = g_str_equal (widget->locale_id, priv->language);
+ gtk_widget_set_opacity (widget->checkmark, should_be_visible ? 1.0 : 0.0);
+
+ row = gtk_widget_get_next_sibling (row);
+ }
+}
+
+static GtkWidget *
+more_widget_new (void)
+{
+ GtkWidget *widget;
+ GtkWidget *arrow;
+
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_tooltip_text (widget, _("More…"));
+
+ arrow = g_object_new (GTK_TYPE_IMAGE,
+ "icon-name", "view-more-symbolic",
+ "hexpand", TRUE,
+ "halign", GTK_ALIGN_CENTER,
+ NULL);
+ gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
+ gtk_widget_set_margin_top (widget, 12);
+ gtk_widget_set_margin_bottom (widget, 12);
+ gtk_box_append (GTK_BOX (widget), arrow);
+
+ return widget;
+}
+
+static GtkWidget *
+no_results_widget_new (void)
+{
+ GtkWidget *widget;
+
+ widget = padded_label_new (_("No languages found"));
+ gtk_widget_set_sensitive (widget, FALSE);
+
+ return widget;
+}
+
+static void
+add_one_language (CcLanguageChooser *chooser,
+ const char *locale_id,
+ gboolean is_initial)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ GtkWidget *widget;
+
+ if (!cc_common_language_has_font (locale_id)) {
+ return;
+ }
+
+ widget = language_widget_new (locale_id, !is_initial);
+ if (widget)
+ gtk_list_box_append (GTK_LIST_BOX (priv->language_list), widget);
+}
+
+static void
+add_languages (CcLanguageChooser *chooser,
+ char **locale_ids,
+ GHashTable *initial)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ GHashTableIter iter;
+ gchar *key;
+
+ g_hash_table_iter_init (&iter, initial);
+ while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL)) {
+ add_one_language (chooser, key, TRUE);
+ }
+
+ while (*locale_ids) {
+ const gchar *locale_id;
+
+ locale_id = *locale_ids;
+ locale_ids ++;
+
+ if (!g_hash_table_lookup (initial, locale_id))
+ add_one_language (chooser, locale_id, FALSE);
+ }
+
+ gtk_list_box_append (GTK_LIST_BOX (priv->language_list), priv->more_item);
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (priv->language_list), priv->no_results);
+}
+
+static void
+add_all_languages (CcLanguageChooser *chooser)
+{
+ g_auto(GStrv) locale_ids = NULL;
+ g_autoptr(GHashTable) initial = NULL;
+
+ locale_ids = gnome_get_all_locales ();
+ initial = cc_common_language_get_initial_languages ();
+ add_languages (chooser, locale_ids, initial);
+}
+
+static gboolean
+language_visible (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcLanguageChooser *chooser = user_data;
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ LanguageWidget *widget;
+ gboolean visible;
+ GtkWidget *child;
+ const char *search_term;
+
+ child = gtk_list_box_row_get_child (row);
+ if (child == priv->more_item)
+ return !priv->showing_extra;
+
+ widget = get_language_widget (child);
+
+ if (!priv->showing_extra && widget->is_extra)
+ return FALSE;
+
+ search_term = gtk_editable_get_text (GTK_EDITABLE (priv->filter_entry));
+ if (!search_term || !*search_term)
+ return TRUE;
+
+ visible = FALSE;
+
+ visible = g_str_match_string (search_term, widget->locale_name, TRUE);
+ if (visible)
+ goto out;
+
+ visible = g_str_match_string (search_term, widget->locale_current_name, TRUE);
+ if (visible)
+ goto out;
+
+ visible = g_str_match_string (search_term, widget->locale_untranslated_name, TRUE);
+ if (visible)
+ goto out;
+
+ out:
+ return visible;
+}
+
+static gint
+sort_languages (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ LanguageWidget *la, *lb;
+ int ret;
+
+ la = get_language_widget (gtk_list_box_row_get_child (a));
+ lb = get_language_widget (gtk_list_box_row_get_child (b));
+
+ if (la == NULL)
+ return 1;
+
+ if (lb == NULL)
+ return -1;
+
+ if (la->is_extra && !lb->is_extra)
+ return 1;
+
+ if (!la->is_extra && lb->is_extra)
+ return -1;
+
+ ret = g_strcmp0 (la->sort_key, lb->sort_key);
+ if (ret != 0)
+ return ret;
+
+ return g_strcmp0 (la->locale_id, lb->locale_id);
+}
+
+static void
+filter_changed (GtkEntry *entry,
+ CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->language_list));
+}
+
+static void
+show_more (CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+
+ gtk_widget_grab_focus (priv->filter_entry);
+
+ gtk_widget_set_valign (GTK_WIDGET (chooser), GTK_ALIGN_FILL);
+
+ priv->showing_extra = TRUE;
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (priv->language_list));
+ g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_SHOWING_EXTRA]);
+}
+
+static void
+set_locale_id (CcLanguageChooser *chooser,
+ const gchar *new_locale_id)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+
+ if (g_strcmp0 (priv->language, new_locale_id) == 0)
+ return;
+
+ g_free (priv->language);
+ priv->language = g_strdup (new_locale_id);
+
+ sync_all_checkmarks (chooser);
+
+ g_object_notify_by_pspec (G_OBJECT (chooser), obj_props[PROP_LANGUAGE]);
+}
+
+static gboolean
+confirm_choice (gpointer data)
+{
+ GtkWidget *widget = data;
+
+ g_signal_emit (widget, signals[CONFIRM], 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+row_activated (GtkListBox *box,
+ GtkListBoxRow *row,
+ CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ GtkWidget *child;
+ LanguageWidget *widget;
+
+ if (row == NULL)
+ return;
+
+ child = gtk_list_box_row_get_child (row);
+ if (child == priv->more_item) {
+ show_more (chooser);
+ } else {
+ widget = get_language_widget (child);
+ if (widget == NULL)
+ return;
+ if (g_strcmp0 (priv->language, widget->locale_id) == 0)
+ g_idle_add (confirm_choice, chooser);
+ else
+ set_locale_id (chooser, widget->locale_id);
+ }
+}
+
+static void
+cc_language_chooser_constructed (GObject *object)
+{
+ CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+
+ G_OBJECT_CLASS (cc_language_chooser_parent_class)->constructed (object);
+
+ priv->more_item = more_widget_new ();
+ priv->no_results = no_results_widget_new ();
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->language_list),
+ sort_languages, chooser, NULL);
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (priv->language_list),
+ language_visible, chooser, NULL);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->language_list),
+ GTK_SELECTION_NONE);
+ add_all_languages (chooser);
+
+ g_signal_connect (priv->filter_entry, "changed",
+ G_CALLBACK (filter_changed),
+ chooser);
+
+ g_signal_connect (priv->language_list, "row-activated",
+ G_CALLBACK (row_activated), chooser);
+
+ if (priv->language == NULL)
+ priv->language = cc_common_language_get_current_language ();
+
+ sync_all_checkmarks (chooser);
+}
+
+static void
+cc_language_chooser_finalize (GObject *object)
+{
+ CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+
+ g_free (priv->language);
+
+ G_OBJECT_CLASS (cc_language_chooser_parent_class)->finalize (object);
+}
+
+static void
+cc_language_chooser_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
+ switch (prop_id) {
+ case PROP_LANGUAGE:
+ g_value_set_string (value, cc_language_chooser_get_language (chooser));
+ break;
+ case PROP_SHOWING_EXTRA:
+ g_value_set_boolean (value, cc_language_chooser_get_showing_extra (chooser));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_language_chooser_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
+ switch (prop_id) {
+ case PROP_LANGUAGE:
+ cc_language_chooser_set_language (chooser, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_language_chooser_class_init (CcLanguageChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/control-center/language-chooser.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcLanguageChooser, filter_entry);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), CcLanguageChooser, language_list);
+
+ object_class->finalize = cc_language_chooser_finalize;
+ object_class->get_property = cc_language_chooser_get_property;
+ object_class->set_property = cc_language_chooser_set_property;
+ object_class->constructed = cc_language_chooser_constructed;
+
+ signals[CONFIRM] = g_signal_new ("confirm",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET (CcLanguageChooserClass, confirm),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ obj_props[PROP_LANGUAGE] =
+ g_param_spec_string ("language", "", "", "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ obj_props[PROP_SHOWING_EXTRA] =
+ g_param_spec_string ("showing-extra", "", "", "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+}
+
+static void
+cc_language_chooser_init (CcLanguageChooser *chooser)
+{
+ gtk_widget_init_template (GTK_WIDGET (chooser));
+}
+
+void
+cc_language_chooser_clear_filter (CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ gtk_editable_set_text (GTK_EDITABLE (priv->filter_entry), "");
+}
+
+const gchar *
+cc_language_chooser_get_language (CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ return priv->language;
+}
+
+void
+cc_language_chooser_set_language (CcLanguageChooser *chooser,
+ const gchar *language)
+{
+ set_locale_id (chooser, language);
+}
+
+gboolean
+cc_language_chooser_get_showing_extra (CcLanguageChooser *chooser)
+{
+ CcLanguageChooserPrivate *priv = cc_language_chooser_get_instance_private (chooser);
+ return priv->showing_extra;
+}
diff --git a/gnome-initial-setup/pages/language/cc-language-chooser.h b/gnome-initial-setup/pages/language/cc-language-chooser.h
new file mode 100644
index 0000000..749af78
--- /dev/null
+++ b/gnome-initial-setup/pages/language/cc-language-chooser.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ * Matthias Clasen <mclasen@redhat.com>
+ */
+
+#ifndef __CC_LANGUAGE_CHOOSER_H__
+#define __CC_LANGUAGE_CHOOSER_H__
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+#define CC_TYPE_LANGUAGE_CHOOSER (cc_language_chooser_get_type ())
+#define CC_LANGUAGE_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CC_TYPE_LANGUAGE_CHOOSER, CcLanguageChooser))
+#define CC_LANGUAGE_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CC_TYPE_LANGUAGE_CHOOSER, CcLanguageChooserClass))
+#define CC_IS_LANGUAGE_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CC_TYPE_LANGUAGE_CHOOSER))
+#define CC_IS_LANGUAGE_CHOOSER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CC_TYPE_LANGUAGE_CHOOSER))
+#define CC_LANGUAGE_CHOOSER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CC_TYPE_LANGUAGE_CHOOSER, CcLanguageChooserClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CcLanguageChooser CcLanguageChooser;
+typedef struct _CcLanguageChooserClass CcLanguageChooserClass;
+
+struct _CcLanguageChooser
+{
+ GtkBox parent;
+};
+
+struct _CcLanguageChooserClass
+{
+ GtkBoxClass parent_class;
+
+ void (*confirm) (CcLanguageChooser *chooser);
+};
+
+GType cc_language_chooser_get_type (void);
+
+void cc_language_chooser_clear_filter (CcLanguageChooser *chooser);
+const gchar * cc_language_chooser_get_language (CcLanguageChooser *chooser);
+void cc_language_chooser_set_language (CcLanguageChooser *chooser,
+ const gchar *language);
+gboolean cc_language_chooser_get_showing_extra (CcLanguageChooser *chooser);
+
+G_END_DECLS
+
+#endif /* __CC_LANGUAGE_CHOOSER_H__ */
diff --git a/gnome-initial-setup/pages/language/cc-util.c b/gnome-initial-setup/pages/language/cc-util.c
new file mode 100644
index 0000000..e51a9d2
--- /dev/null
+++ b/gnome-initial-setup/pages/language/cc-util.c
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
+ *
+ * The Control Center 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.
+ *
+ * The Control Center 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 the Control Center; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+
+#include "cc-util.h"
+
+/* Combining diacritical mark?
+ * Basic range: [0x0300,0x036F]
+ * Supplement: [0x1DC0,0x1DFF]
+ * For Symbols: [0x20D0,0x20FF]
+ * Half marks: [0xFE20,0xFE2F]
+ */
+#define IS_CDM_UCS4(c) (((c) >= 0x0300 && (c) <= 0x036F) || \
+ ((c) >= 0x1DC0 && (c) <= 0x1DFF) || \
+ ((c) >= 0x20D0 && (c) <= 0x20FF) || \
+ ((c) >= 0xFE20 && (c) <= 0xFE2F))
+
+/* Copied from tracker/src/libtracker-fts/tracker-parser-glib.c under the GPL
+ * And then from gnome-shell/src/shell-util.c
+ *
+ * Originally written by Aleksander Morgado <aleksander@gnu.org>
+ */
+char *
+cc_util_normalize_casefold_and_unaccent (const char *str)
+{
+ char *normalized, *tmp;
+ int i = 0, j = 0, ilen;
+
+ if (str == NULL)
+ return NULL;
+
+ normalized = g_utf8_normalize (str, -1, G_NORMALIZE_NFKD);
+ tmp = g_utf8_casefold (normalized, -1);
+ g_free (normalized);
+
+ ilen = strlen (tmp);
+
+ while (i < ilen)
+ {
+ gunichar unichar;
+ gchar *next_utf8;
+ gint utf8_len;
+
+ /* Get next character of the word as UCS4 */
+ unichar = g_utf8_get_char_validated (&tmp[i], -1);
+
+ /* Invalid UTF-8 character or end of original string. */
+ if (unichar == (gunichar) -1 ||
+ unichar == (gunichar) -2)
+ {
+ break;
+ }
+
+ /* Find next UTF-8 character */
+ next_utf8 = g_utf8_next_char (&tmp[i]);
+ utf8_len = next_utf8 - &tmp[i];
+
+ if (IS_CDM_UCS4 ((guint32) unichar))
+ {
+ /* If the given unichar is a combining diacritical mark,
+ * just update the original index, not the output one */
+ i += utf8_len;
+ continue;
+ }
+
+ /* If already found a previous combining
+ * diacritical mark, indexes are different so
+ * need to copy characters. As output and input
+ * buffers may overlap, need to use memmove
+ * instead of memcpy */
+ if (i != j)
+ {
+ memmove (&tmp[j], &tmp[i], utf8_len);
+ }
+
+ /* Update both indexes */
+ i += utf8_len;
+ j += utf8_len;
+ }
+
+ /* Force proper string end */
+ tmp[j] = '\0';
+
+ return tmp;
+}
diff --git a/gnome-initial-setup/pages/language/cc-util.h b/gnome-initial-setup/pages/language/cc-util.h
new file mode 100644
index 0000000..42b09ff
--- /dev/null
+++ b/gnome-initial-setup/pages/language/cc-util.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
+ *
+ * The Control Center 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.
+ *
+ * The Control Center 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 the Control Center; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef _CC_UTIL_H
+#define _CC_UTIL_H
+
+#include <glib.h>
+
+char *cc_util_normalize_casefold_and_unaccent (const char *str);
+
+#endif
diff --git a/gnome-initial-setup/pages/language/gis-language-page.c b/gnome-initial-setup/pages/language/gis-language-page.c
new file mode 100644
index 0000000..f62b2ad
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-language-page.c
@@ -0,0 +1,312 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ * Michael Wood <michael.g.wood@intel.com>
+ *
+ * Based on gnome-control-center cc-region-panel.c
+ */
+
+/* Language page {{{1 */
+
+#define PAGE_ID "language"
+
+#define GNOME_SYSTEM_LOCALE_DIR "org.gnome.system.locale"
+#define REGION_KEY "region"
+
+#include "config.h"
+#include "language-resources.h"
+#include "gis-welcome-widget.h"
+#include "cc-language-chooser.h"
+#include "gis-language-page.h"
+
+#include <act/act-user-manager.h>
+#include <polkit/polkit.h>
+#include <locale.h>
+#include <gtk/gtk.h>
+
+struct _GisLanguagePagePrivate
+{
+ GtkWidget *logo;
+ GtkWidget *welcome_widget;
+ GtkWidget *language_chooser;
+
+ GDBusProxy *localed;
+ GPermission *permission;
+ const gchar *new_locale_id;
+
+ GCancellable *cancellable;
+};
+typedef struct _GisLanguagePagePrivate GisLanguagePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisLanguagePage, gis_language_page, GIS_TYPE_PAGE);
+
+static void
+set_localed_locale (GisLanguagePage *self)
+{
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (self);
+ GVariantBuilder *b;
+ gchar *s;
+
+ b = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+ s = g_strconcat ("LANG=", priv->new_locale_id, NULL);
+ g_variant_builder_add (b, "s", s);
+ g_free (s);
+
+ g_dbus_proxy_call (priv->localed,
+ "SetLocale",
+ g_variant_new ("(asb)", b, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+ g_variant_builder_unref (b);
+}
+
+static void
+change_locale_permission_acquired (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GisLanguagePage *page = GIS_LANGUAGE_PAGE (data);
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (page);
+ GError *error = NULL;
+ gboolean allowed;
+
+ allowed = g_permission_acquire_finish (priv->permission, res, &error);
+ if (error) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to acquire permission: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ if (allowed)
+ set_localed_locale (page);
+}
+
+static void
+user_loaded (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ gchar *new_locale_id = user_data;
+
+ act_user_set_language (ACT_USER (object), new_locale_id);
+
+ g_free (new_locale_id);
+}
+
+static void
+language_changed (CcLanguageChooser *chooser,
+ GParamSpec *pspec,
+ GisLanguagePage *page)
+{
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (page);
+ GisDriver *driver;
+ GSettings *region_settings;
+ ActUser *user;
+
+ priv->new_locale_id = cc_language_chooser_get_language (chooser);
+ driver = GIS_PAGE (page)->driver;
+
+ gis_driver_set_user_language (driver, priv->new_locale_id, TRUE);
+ gtk_widget_set_default_direction (gtk_get_locale_direction ());
+
+ if (gis_driver_get_mode (driver) == GIS_DRIVER_MODE_NEW_USER) {
+ if (g_permission_get_allowed (priv->permission)) {
+ set_localed_locale (page);
+ }
+ else if (g_permission_get_can_acquire (priv->permission)) {
+ g_permission_acquire_async (priv->permission,
+ NULL,
+ change_locale_permission_acquired,
+ page);
+ }
+ }
+
+ /* Ensure we won't override the selected language for format strings */
+ region_settings = g_settings_new (GNOME_SYSTEM_LOCALE_DIR);
+ g_settings_reset (region_settings, REGION_KEY);
+ g_object_unref (region_settings);
+
+ user = act_user_manager_get_user (act_user_manager_get_default (),
+ g_get_user_name ());
+ if (act_user_is_loaded (user))
+ act_user_set_language (user, priv->new_locale_id);
+ else
+ g_signal_connect (user,
+ "notify::is-loaded",
+ G_CALLBACK (user_loaded),
+ g_strdup (priv->new_locale_id));
+
+ gis_welcome_widget_show_locale (GIS_WELCOME_WIDGET (priv->welcome_widget),
+ priv->new_locale_id);
+}
+
+static void
+localed_proxy_ready (GObject *source,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GisLanguagePage *self = data;
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (self);
+ GDBusProxy *proxy;
+ GError *error = NULL;
+
+ proxy = g_dbus_proxy_new_finish (res, &error);
+
+ if (!proxy) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to contact localed: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ priv->localed = proxy;
+}
+
+static void
+update_distro_logo (GisLanguagePage *page)
+{
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (page);
+ g_autofree char *id = g_get_os_info (G_OS_INFO_KEY_ID);
+ gsize i;
+
+ static const struct {
+ const char *id;
+ const char *logo;
+ } id_to_logo[] = {
+ { "debian", "emblem-debian" },
+ { "fedora", "fedora-logo-icon" },
+ { "ubuntu", "ubuntu-logo-icon" },
+ { "openSUSE Tumbleweed", "opensuse-logo-icon" },
+ { "openSUSE Leap", "opensuse-logo-icon" },
+ { "SLED", "suse-logo-icon" },
+ { "SLES", "suse-logo-icon" },
+ };
+
+ for (i = 0; i < G_N_ELEMENTS (id_to_logo); i++)
+ {
+ if (g_strcmp0 (id, id_to_logo[i].id) == 0)
+ {
+ g_object_set (priv->logo, "icon-name", id_to_logo[i].logo, NULL);
+ break;
+ }
+ }
+}
+
+static void
+language_confirmed (CcLanguageChooser *chooser,
+ GisLanguagePage *page)
+{
+ gis_assistant_next_page (gis_driver_get_assistant (GIS_PAGE (page)->driver));
+}
+
+static void
+gis_language_page_constructed (GObject *object)
+{
+ GisLanguagePage *page = GIS_LANGUAGE_PAGE (object);
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (page);
+ GDBusConnection *bus;
+
+ g_type_ensure (CC_TYPE_LANGUAGE_CHOOSER);
+
+ G_OBJECT_CLASS (gis_language_page_parent_class)->constructed (object);
+
+ update_distro_logo (page);
+
+ g_signal_connect (priv->language_chooser, "notify::language",
+ G_CALLBACK (language_changed), page);
+ g_signal_connect (priv->language_chooser, "confirm",
+ G_CALLBACK (language_confirmed), page);
+
+ /* If we're in new user mode then we're manipulating system settings */
+ if (gis_driver_get_mode (GIS_PAGE (page)->driver) == GIS_DRIVER_MODE_NEW_USER)
+ {
+ priv->permission = polkit_permission_new_sync ("org.freedesktop.locale1.set-locale", NULL, NULL, NULL);
+
+ bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL);
+ g_dbus_proxy_new (bus,
+ G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+ NULL,
+ "org.freedesktop.locale1",
+ "/org/freedesktop/locale1",
+ "org.freedesktop.locale1",
+ priv->cancellable,
+ (GAsyncReadyCallback) localed_proxy_ready,
+ object);
+ g_object_unref (bus);
+ }
+
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_language_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Welcome"));
+}
+
+static void
+gis_language_page_dispose (GObject *object)
+{
+ GisLanguagePage *page = GIS_LANGUAGE_PAGE (object);
+ GisLanguagePagePrivate *priv = gis_language_page_get_instance_private (page);
+
+ g_clear_object (&priv->permission);
+ g_clear_object (&priv->localed);
+ g_clear_object (&priv->cancellable);
+
+ G_OBJECT_CLASS (gis_language_page_parent_class)->dispose (object);
+}
+
+static void
+gis_language_page_class_init (GisLanguagePageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-language-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisLanguagePage, welcome_widget);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisLanguagePage, language_chooser);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisLanguagePage, logo);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_language_page_locale_changed;
+ object_class->constructed = gis_language_page_constructed;
+ object_class->dispose = gis_language_page_dispose;
+}
+
+static void
+gis_language_page_init (GisLanguagePage *page)
+{
+ g_resources_register (language_get_resource ());
+ g_type_ensure (GIS_TYPE_WELCOME_WIDGET);
+ g_type_ensure (CC_TYPE_LANGUAGE_CHOOSER);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_language_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_LANGUAGE_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/language/gis-language-page.h b/gnome-initial-setup/pages/language/gis-language-page.h
new file mode 100644
index 0000000..7636021
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-language-page.h
@@ -0,0 +1,57 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_LANGUAGE_PAGE_H__
+#define __GIS_LANGUAGE_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_LANGUAGE_PAGE (gis_language_page_get_type ())
+#define GIS_LANGUAGE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_LANGUAGE_PAGE, GisLanguagePage))
+#define GIS_LANGUAGE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_LANGUAGE_PAGE, GisLanguagePageClass))
+#define GIS_IS_LANGUAGE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_LANGUAGE_PAGE))
+#define GIS_IS_LANGUAGE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_LANGUAGE_PAGE))
+#define GIS_LANGUAGE_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_LANGUAGE_PAGE, GisLanguagePageClass))
+
+typedef struct _GisLanguagePage GisLanguagePage;
+typedef struct _GisLanguagePageClass GisLanguagePageClass;
+
+struct _GisLanguagePage
+{
+ GisPage parent;
+};
+
+struct _GisLanguagePageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_language_page_get_type (void);
+
+GisPage *gis_prepare_language_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_LANGUAGE_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/language/gis-language-page.ui b/gnome-initial-setup/pages/language/gis-language-page.ui
new file mode 100644
index 0000000..307360c
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-language-page.ui
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="GisLanguagePage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="hexpand" bind-source="language_chooser" bind-property="visible" bind-flags="invert-boolean|sync-create"/>
+ <property name="vexpand" bind-source="language_chooser" bind-property="visible" bind-flags="invert-boolean|sync-create"/>
+ <child>
+ <object class="GtkImage" id="logo">
+ <property name="visible" bind-source="GisLanguagePage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ <property name="margin_top">24</property>
+ <property name="pixel_size">96</property>
+ <property name="icon_name">start-here-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GisWelcomeWidget" id="welcome_widget">
+ <property name="margin_top">18</property>
+ <property name="margin_bottom">40</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="CcLanguageChooser" id="language_chooser">
+ <property name="margin_bottom">18</property>
+ <property name="width_request">400</property>
+ <property name="valign">start</property>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/language/gis-welcome-widget.c b/gnome-initial-setup/pages/language/gis-welcome-widget.c
new file mode 100644
index 0000000..4d58a37
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-welcome-widget.c
@@ -0,0 +1,242 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+#include "gis-welcome-widget.h"
+
+#include <errno.h>
+#include <locale.h>
+#include <glib/gi18n.h>
+
+#include "cc-common-language.h"
+
+struct _GisWelcomeWidgetPrivate
+{
+ AdwCarousel *carousel;
+ GHashTable *translation_widgets; /* (element-type owned utf8 unowned GtkWidget) (owned) */
+
+ guint timeout_id;
+};
+typedef struct _GisWelcomeWidgetPrivate GisWelcomeWidgetPrivate;
+
+#define TIMEOUT 5
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisWelcomeWidget, gis_welcome_widget, ADW_TYPE_BIN);
+
+static gboolean
+advance_stack (gpointer user_data)
+{
+ GisWelcomeWidget *widget = user_data;
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+ GtkWidget *child;
+ unsigned int next_page;
+ unsigned int n_pages;
+ double current_page;
+
+ n_pages = adw_carousel_get_n_pages (priv->carousel);
+ if (n_pages == 0)
+ goto out;
+
+ current_page = ceil (adw_carousel_get_position (priv->carousel));
+ next_page = ((int) current_page + 1) % n_pages;
+
+ child = gtk_widget_get_first_child (GTK_WIDGET (priv->carousel));
+ while (next_page-- > 0)
+ child = gtk_widget_get_next_sibling (child);
+
+ adw_carousel_scroll_to (priv->carousel, child, TRUE);
+
+ out:
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+gis_welcome_widget_start (GisWelcomeWidget *widget)
+{
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+
+ if (priv->timeout_id > 0)
+ return;
+
+ priv->timeout_id = g_timeout_add_seconds (5, advance_stack, widget);
+}
+
+static void
+gis_welcome_widget_stop (GisWelcomeWidget *widget)
+{
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+
+ if (priv->timeout_id == 0)
+ return;
+
+ g_source_remove (priv->timeout_id);
+ priv->timeout_id = 0;
+}
+
+static void
+gis_welcome_widget_map (GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS (gis_welcome_widget_parent_class)->map (widget);
+ gis_welcome_widget_start (GIS_WELCOME_WIDGET (widget));
+}
+
+static void
+gis_welcome_widget_unmap (GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS (gis_welcome_widget_parent_class)->unmap (widget);
+ gis_welcome_widget_stop (GIS_WELCOME_WIDGET (widget));
+}
+
+static const char *
+welcome (const char *locale_id)
+{
+ locale_t locale;
+ locale_t old_locale;
+ const char *welcome;
+
+ locale = newlocale (LC_MESSAGES_MASK, locale_id, (locale_t) 0);
+ if (locale == (locale_t) 0)
+ {
+ if (errno == ENOENT)
+ g_debug ("Failed to create locale %s: %s", locale_id, g_strerror (errno));
+ else
+ g_warning ("Failed to create locale %s: %s", locale_id, g_strerror (errno));
+
+ return "Welcome!";
+ }
+
+ old_locale = uselocale (locale);
+
+ /* Translators: This is meant to be a warm, engaging welcome message,
+ * like greeting somebody at the door. If the exclamation mark is not
+ * suitable for this in your language you may replace it.
+ */
+ welcome = _("Welcome!");
+
+ uselocale (old_locale);
+ freelocale (locale);
+
+ return welcome;
+}
+
+static GtkWidget *
+big_label (const char *text)
+{
+ GtkWidget *label = gtk_label_new (text);
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "title-1");
+
+ return label;
+}
+
+static void
+fill_carousel (GisWelcomeWidget *widget)
+{
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+ g_autoptr(GHashTable) initial = cc_common_language_get_initial_languages ();
+ GHashTableIter iter;
+ gpointer key, value;
+ g_autoptr(GHashTable) added_translations = NULL;
+
+ added_translations = g_hash_table_new (g_str_hash, g_str_equal);
+
+ g_hash_table_iter_init (&iter, initial);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ const char *locale_id = key;
+ const char *text;
+ GtkWidget *label;
+
+ if (!cc_common_language_has_font (locale_id))
+ continue;
+
+ text = welcome (locale_id);
+ label = g_hash_table_lookup (added_translations, text);
+ if (label == NULL) {
+ label = big_label (text);
+ adw_carousel_append (priv->carousel, label);
+ g_hash_table_insert (added_translations, (gpointer) text, label);
+ }
+
+ g_hash_table_insert (priv->translation_widgets, g_strdup (locale_id), label);
+ }
+}
+
+static void
+gis_welcome_widget_constructed (GObject *object)
+{
+ G_OBJECT_CLASS (gis_welcome_widget_parent_class)->constructed (object);
+
+ fill_carousel (GIS_WELCOME_WIDGET (object));
+}
+
+static void
+gis_welcome_widget_dispose (GObject *object)
+{
+ GisWelcomeWidget *widget = GIS_WELCOME_WIDGET (object);
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+
+ g_clear_pointer (&priv->translation_widgets, g_hash_table_unref);
+
+ G_OBJECT_CLASS (gis_welcome_widget_parent_class)->dispose (object);
+}
+
+static void
+gis_welcome_widget_class_init (GisWelcomeWidgetClass *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/initial-setup/gis-welcome-widget.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GisWelcomeWidget, carousel);
+
+ object_class->constructed = gis_welcome_widget_constructed;
+ object_class->dispose = gis_welcome_widget_dispose;
+ widget_class->map = gis_welcome_widget_map;
+ widget_class->unmap = gis_welcome_widget_unmap;
+}
+
+static void
+gis_welcome_widget_init (GisWelcomeWidget *widget)
+{
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+
+ priv->translation_widgets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ gtk_widget_init_template (GTK_WIDGET (widget));
+}
+
+void
+gis_welcome_widget_show_locale (GisWelcomeWidget *widget,
+ const char *locale_id)
+{
+ GisWelcomeWidgetPrivate *priv = gis_welcome_widget_get_instance_private (widget);
+ GtkWidget *label;
+
+ /* Restart the widget to reset the timer. */
+ gis_welcome_widget_stop (widget);
+ gis_welcome_widget_start (widget);
+
+ label = g_hash_table_lookup (priv->translation_widgets, locale_id);
+ if (label)
+ adw_carousel_scroll_to (priv->carousel, label, FALSE);
+}
diff --git a/gnome-initial-setup/pages/language/gis-welcome-widget.h b/gnome-initial-setup/pages/language/gis-welcome-widget.h
new file mode 100644
index 0000000..212b45f
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-welcome-widget.h
@@ -0,0 +1,56 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_WELCOME_WIDGET_H__
+#define __GIS_WELCOME_WIDGET_H__
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_WELCOME_WIDGET (gis_welcome_widget_get_type ())
+#define GIS_WELCOME_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_WELCOME_WIDGET, GisWelcomeWidget))
+#define GIS_WELCOME_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_WELCOME_WIDGET, GisWelcomeWidgetClass))
+#define GIS_IS_WELCOME_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_WELCOME_WIDGET))
+#define GIS_IS_WELCOME_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_WELCOME_WIDGET))
+#define GIS_WELCOME_WIDGET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_WELCOME_WIDGET, GisWelcomeWidgetClass))
+
+typedef struct _GisWelcomeWidget GisWelcomeWidget;
+typedef struct _GisWelcomeWidgetClass GisWelcomeWidgetClass;
+
+struct _GisWelcomeWidget
+{
+ AdwBin parent;
+};
+
+struct _GisWelcomeWidgetClass
+{
+ AdwBinClass parent_class;
+};
+
+GType gis_welcome_widget_get_type (void);
+
+void gis_welcome_widget_show_locale (GisWelcomeWidget *widget,
+ const char *locale_id);
+
+G_END_DECLS
+
+#endif /* __GIS_WELCOME_WIDGET_H__ */
diff --git a/gnome-initial-setup/pages/language/gis-welcome-widget.ui b/gnome-initial-setup/pages/language/gis-welcome-widget.ui
new file mode 100644
index 0000000..f6a26df
--- /dev/null
+++ b/gnome-initial-setup/pages/language/gis-welcome-widget.ui
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="GisWelcomeWidget" parent="AdwBin">
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <child>
+ <object class="AdwCarousel" id="carousel">
+ <property name="halign">center</property>
+ <property name="interactive">False</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/language/language-chooser.ui b/gnome-initial-setup/pages/language/language-chooser.ui
new file mode 100644
index 0000000..90ee096
--- /dev/null
+++ b/gnome-initial-setup/pages/language/language-chooser.ui
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="CcLanguageChooser" parent="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <child>
+ <object class="GtkSearchEntry" id="filter_entry">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="language_list">
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">start</property>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/language/language.gresource.xml b/gnome-initial-setup/pages/language/language.gresource.xml
new file mode 100644
index 0000000..de0688d
--- /dev/null
+++ b/gnome-initial-setup/pages/language/language.gresource.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-language-page.ui">gis-language-page.ui</file>
+ <file preprocess="xml-stripblanks" alias="gis-welcome-widget.ui">gis-welcome-widget.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/control-center">
+ <file preprocess="xml-stripblanks" alias="language-chooser.ui">language-chooser.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/language/meson.build b/gnome-initial-setup/pages/language/meson.build
new file mode 100644
index 0000000..ef6ba3f
--- /dev/null
+++ b/gnome-initial-setup/pages/language/meson.build
@@ -0,0 +1,16 @@
+sources += gnome.compile_resources(
+ 'language-resources',
+ files('language.gresource.xml'),
+ c_name: 'language'
+)
+
+sources += files(
+ 'cc-language-chooser.c',
+ 'cc-language-chooser.h',
+ 'cc-util.c',
+ 'cc-util.h',
+ 'gis-welcome-widget.c',
+ 'gis-welcome-widget.h',
+ 'gis-language-page.c',
+ 'gis-language-page.h',
+)
diff --git a/gnome-initial-setup/pages/meson.build b/gnome-initial-setup/pages/meson.build
new file mode 100644
index 0000000..3230501
--- /dev/null
+++ b/gnome-initial-setup/pages/meson.build
@@ -0,0 +1,21 @@
+pages = [
+ 'account',
+ 'language',
+ 'keyboard',
+ 'network',
+ 'timezone',
+ 'privacy',
+ 'goa',
+ 'password',
+ 'software',
+ 'summary',
+ 'welcome',
+]
+
+if libmalcontent_dep.found() and libmalcontent_ui_dep.found()
+ pages += 'parental-controls'
+endif
+
+foreach page: pages
+ subdir (page)
+endforeach
diff --git a/gnome-initial-setup/pages/network/gis-network-page.c b/gnome-initial-setup/pages/network/gis-network-page.c
new file mode 100644
index 0000000..859288f
--- /dev/null
+++ b/gnome-initial-setup/pages/network/gis-network-page.c
@@ -0,0 +1,828 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Network page {{{1 */
+
+#define PAGE_ID "network"
+
+#include "config.h"
+#include "network-resources.h"
+#include "gis-network-page.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include "network-dialogs.h"
+
+#include "gis-page-header.h"
+
+typedef enum {
+ NM_AP_SEC_UNKNOWN,
+ NM_AP_SEC_NONE,
+ NM_AP_SEC_WEP,
+ NM_AP_SEC_WPA,
+ NM_AP_SEC_WPA2
+} NMAccessPointSecurity;
+
+struct _GisNetworkPagePrivate {
+ GtkWidget *network_list;
+ GtkWidget *no_network_label;
+ GtkWidget *no_network_spinner;
+ GtkWidget *turn_on_label;
+ GtkWidget *turn_on_switch;
+
+ NMClient *nm_client;
+ NMDevice *nm_device;
+ gboolean refreshing;
+ GtkSizeGroup *icons;
+
+ guint refresh_timeout_id;
+
+ /* TRUE if the page has ever been shown to the user. */
+ gboolean ever_shown;
+};
+typedef struct _GisNetworkPagePrivate GisNetworkPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisNetworkPage, gis_network_page, GIS_TYPE_PAGE);
+
+static GPtrArray *
+get_strongest_unique_aps (const GPtrArray *aps)
+{
+ GBytes *ssid;
+ GBytes *ssid_tmp;
+ GPtrArray *unique = NULL;
+ gboolean add_ap;
+ guint i;
+ guint j;
+ NMAccessPoint *ap;
+ NMAccessPoint *ap_tmp;
+
+ /* we will have multiple entries for typical hotspots,
+ * just keep the one with the strongest signal
+ */
+ unique = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ if (aps == NULL)
+ goto out;
+
+ for (i = 0; i < aps->len; i++) {
+ ap = NM_ACCESS_POINT (g_ptr_array_index (aps, i));
+ ssid = nm_access_point_get_ssid (ap);
+ add_ap = TRUE;
+
+ if (!ssid)
+ continue;
+
+ /* get already added list */
+ for (j = 0; j < unique->len; j++) {
+ ap_tmp = NM_ACCESS_POINT (g_ptr_array_index (unique, j));
+ ssid_tmp = nm_access_point_get_ssid (ap_tmp);
+
+ /* is this the same type and data? */
+ if (ssid_tmp &&
+ nm_utils_same_ssid (g_bytes_get_data (ssid, NULL), g_bytes_get_size (ssid),
+ g_bytes_get_data (ssid_tmp, NULL), g_bytes_get_size (ssid_tmp), TRUE)) {
+ /* the new access point is stronger */
+ if (nm_access_point_get_strength (ap) >
+ nm_access_point_get_strength (ap_tmp)) {
+ g_ptr_array_remove (unique, ap_tmp);
+ add_ap = TRUE;
+ } else {
+ add_ap = FALSE;
+ }
+ break;
+ }
+ }
+ if (add_ap) {
+ g_ptr_array_add (unique, g_object_ref (ap));
+ }
+ }
+
+ out:
+ return unique;
+}
+
+static guint
+get_access_point_security (NMAccessPoint *ap)
+{
+ NM80211ApFlags flags;
+ NM80211ApSecurityFlags wpa_flags;
+ NM80211ApSecurityFlags rsn_flags;
+ guint type;
+
+ flags = nm_access_point_get_flags (ap);
+ wpa_flags = nm_access_point_get_wpa_flags (ap);
+ rsn_flags = nm_access_point_get_rsn_flags (ap);
+
+ if (!(flags & NM_802_11_AP_FLAGS_PRIVACY) &&
+ wpa_flags == NM_802_11_AP_SEC_NONE &&
+ rsn_flags == NM_802_11_AP_SEC_NONE)
+ type = NM_AP_SEC_NONE;
+ else if ((flags & NM_802_11_AP_FLAGS_PRIVACY) &&
+ wpa_flags == NM_802_11_AP_SEC_NONE &&
+ rsn_flags == NM_802_11_AP_SEC_NONE)
+ type = NM_AP_SEC_WEP;
+ else if (!(flags & NM_802_11_AP_FLAGS_PRIVACY) &&
+ wpa_flags != NM_802_11_AP_SEC_NONE &&
+ rsn_flags != NM_802_11_AP_SEC_NONE)
+ type = NM_AP_SEC_WPA;
+ else
+ type = NM_AP_SEC_WPA2;
+
+ return type;
+}
+
+static gint
+ap_sort (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ GtkWidget *wa, *wb;
+ guint sa, sb;
+
+ wa = gtk_list_box_row_get_child (a);
+ wb = gtk_list_box_row_get_child (b);
+
+ sa = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (wa), "strength"));
+ sb = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (wb), "strength"));
+ if (sa > sb) return -1;
+ if (sb > sa) return 1;
+
+ return 0;
+}
+
+static void
+add_access_point (GisNetworkPage *page, NMAccessPoint *ap, NMAccessPoint *active)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ GBytes *ssid;
+ GBytes *ssid_active = NULL;
+ gchar *ssid_text;
+ const gchar *object_path;
+ gboolean activated, activating;
+ guint security;
+ guint strength;
+ const gchar *icon_name;
+ GtkWidget *row;
+ GtkWidget *widget;
+ GtkWidget *grid;
+ GtkWidget *state_widget = NULL;
+
+ ssid = nm_access_point_get_ssid (ap);
+ object_path = nm_object_get_path (NM_OBJECT (ap));
+
+ if (ssid == NULL)
+ return;
+ ssid_text = nm_utils_ssid_to_utf8 (g_bytes_get_data (ssid, NULL), g_bytes_get_size (ssid));
+
+ if (active)
+ ssid_active = nm_access_point_get_ssid (active);
+ if (ssid_active && nm_utils_same_ssid (g_bytes_get_data (ssid, NULL), g_bytes_get_size (ssid),
+ g_bytes_get_data (ssid_active, NULL), g_bytes_get_size (ssid_active), TRUE)) {
+ switch (nm_device_get_state (priv->nm_device))
+ {
+ case NM_DEVICE_STATE_PREPARE:
+ case NM_DEVICE_STATE_CONFIG:
+ case NM_DEVICE_STATE_NEED_AUTH:
+ case NM_DEVICE_STATE_IP_CONFIG:
+ case NM_DEVICE_STATE_SECONDARIES:
+ activated = FALSE;
+ activating = TRUE;
+ break;
+ case NM_DEVICE_STATE_ACTIVATED:
+ activated = TRUE;
+ activating = FALSE;
+ break;
+ default:
+ activated = FALSE;
+ activating = FALSE;
+ break;
+ }
+ } else {
+ activated = FALSE;
+ activating = FALSE;
+ }
+
+ security = get_access_point_security (ap);
+ strength = nm_access_point_get_strength (ap);
+
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_margin_start (row, 12);
+ gtk_widget_set_margin_end (row, 12);
+ widget = gtk_label_new (ssid_text);
+ gtk_widget_set_margin_top (widget, 12);
+ gtk_widget_set_margin_bottom (widget, 12);
+ gtk_box_append (GTK_BOX (row), widget);
+
+ if (activated) {
+ state_widget = gtk_image_new_from_icon_name ("object-select-symbolic");
+ } else if (activating) {
+ state_widget = gtk_spinner_new ();
+ gtk_spinner_start (GTK_SPINNER (state_widget));
+ }
+
+ if (state_widget) {
+ gtk_widget_set_halign (state_widget, GTK_ALIGN_START);
+ gtk_widget_set_valign (state_widget, GTK_ALIGN_CENTER);
+ gtk_widget_set_hexpand (state_widget, TRUE);
+ gtk_box_append (GTK_BOX (row), state_widget);
+ }
+
+ grid = gtk_grid_new ();
+ gtk_grid_set_column_spacing (GTK_GRID (grid), 6);
+ gtk_grid_set_column_homogeneous (GTK_GRID (grid), TRUE);
+ gtk_widget_set_valign (grid, GTK_ALIGN_CENTER);
+ gtk_size_group_add_widget (priv->icons, grid);
+ gtk_box_append (GTK_BOX (row), grid);
+
+ if (security != NM_AP_SEC_UNKNOWN &&
+ security != NM_AP_SEC_NONE) {
+ widget = gtk_image_new_from_icon_name ("network-wireless-encrypted-symbolic");
+ gtk_grid_attach (GTK_GRID (grid), widget, 0, 0, 1, 1);
+ }
+
+ if (strength < 20)
+ icon_name = "network-wireless-signal-none-symbolic";
+ else if (strength < 40)
+ icon_name = "network-wireless-signal-weak-symbolic";
+ else if (strength < 50)
+ icon_name = "network-wireless-signal-ok-symbolic";
+ else if (strength < 80)
+ icon_name = "network-wireless-signal-good-symbolic";
+ else
+ icon_name = "network-wireless-signal-excellent-symbolic";
+ widget = gtk_image_new_from_icon_name (icon_name);
+ gtk_widget_set_halign (widget, GTK_ALIGN_END);
+ gtk_grid_attach (GTK_GRID (grid), widget, 1, 0, 1, 1);
+
+ /* if this connection is the active one or is being activated, then make sure
+ * it's sorted before all others */
+ if (activating || activated)
+ strength = G_MAXUINT;
+
+ g_object_set_data (G_OBJECT (row), "object-path", (gpointer) object_path);
+ g_object_set_data (G_OBJECT (row), "ssid", (gpointer) ssid);
+ g_object_set_data (G_OBJECT (row), "strength", GUINT_TO_POINTER (strength));
+
+ gtk_list_box_append (GTK_LIST_BOX (priv->network_list), row);
+}
+
+static void
+add_access_point_other (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ GtkWidget *row;
+ GtkWidget *widget;
+
+ row = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_margin_start (row, 12);
+ gtk_widget_set_margin_end (row, 12);
+ widget = gtk_label_new (C_("Wireless access point", "Other…"));
+ gtk_widget_set_margin_top (widget, 12);
+ gtk_widget_set_margin_bottom (widget, 12);
+ gtk_box_append (GTK_BOX (row), widget);
+
+ g_object_set_data (G_OBJECT (row), "object-path", "ap-other...");
+ g_object_set_data (G_OBJECT (row), "strength", GUINT_TO_POINTER (0));
+
+ gtk_list_box_append (GTK_LIST_BOX (priv->network_list), row);
+}
+
+static gboolean refresh_wireless_list (GisNetworkPage *page);
+
+static void
+cancel_periodic_refresh (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+
+ if (priv->refresh_timeout_id == 0)
+ return;
+
+ g_debug ("Stopping periodic/scheduled Wi-Fi list refresh");
+
+ g_clear_handle_id (&priv->refresh_timeout_id, g_source_remove);
+}
+
+static gboolean
+refresh_again (gpointer user_data)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (user_data);
+ refresh_wireless_list (page);
+ return G_SOURCE_REMOVE;
+}
+
+static void
+start_periodic_refresh (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ static const guint periodic_wifi_refresh_timeout_sec = 10;
+
+ cancel_periodic_refresh (page);
+
+ g_debug ("Starting periodic Wi-Fi list refresh (every %u secs)",
+ periodic_wifi_refresh_timeout_sec);
+ priv->refresh_timeout_id = g_timeout_add_seconds (periodic_wifi_refresh_timeout_sec,
+ refresh_again, page);
+}
+
+static gboolean
+refresh_wireless_list (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ NMAccessPoint *active_ap = NULL;
+ NMAccessPoint *ap;
+ const GPtrArray *aps;
+ GPtrArray *unique_aps;
+ GtkWidget *child;
+ guint i;
+ gboolean enabled;
+
+ g_debug ("Refreshing Wi-Fi networks list");
+
+ priv->refreshing = TRUE;
+
+ g_assert (NM_IS_DEVICE_WIFI (priv->nm_device));
+
+ cancel_periodic_refresh (page);
+
+ active_ap = nm_device_wifi_get_active_access_point (NM_DEVICE_WIFI (priv->nm_device));
+
+ while ((child = gtk_widget_get_first_child (priv->network_list)) != NULL)
+ gtk_list_box_remove (GTK_LIST_BOX (priv->network_list), child);
+
+ aps = nm_device_wifi_get_access_points (NM_DEVICE_WIFI (priv->nm_device));
+ enabled = nm_client_wireless_get_enabled (priv->nm_client);
+
+ if (aps == NULL || aps->len == 0) {
+ gboolean hw_enabled;
+
+ hw_enabled = nm_client_wireless_hardware_get_enabled (priv->nm_client);
+
+ if (!enabled || !hw_enabled) {
+ gtk_label_set_text (GTK_LABEL (priv->no_network_label), _("Wireless networking is disabled"));
+ gtk_widget_show (priv->no_network_label);
+ gtk_widget_hide (priv->no_network_spinner);
+
+ gtk_widget_set_visible (priv->turn_on_label, hw_enabled);
+ gtk_widget_set_visible (priv->turn_on_switch, hw_enabled);
+ } else {
+ gtk_label_set_text (GTK_LABEL (priv->no_network_label), _("Checking for available wireless networks"));
+ gtk_widget_show (priv->no_network_spinner);
+ gtk_widget_show (priv->no_network_label);
+ gtk_widget_hide (priv->turn_on_label);
+ gtk_widget_hide (priv->turn_on_switch);
+ }
+
+ gtk_widget_hide (priv->network_list);
+ goto out;
+
+ } else {
+ gtk_widget_hide (priv->no_network_spinner);
+ gtk_widget_hide (priv->no_network_label);
+ gtk_widget_hide (priv->turn_on_label);
+ gtk_widget_hide (priv->turn_on_switch);
+ gtk_widget_show (priv->network_list);
+ }
+
+ unique_aps = get_strongest_unique_aps (aps);
+ for (i = 0; i < unique_aps->len; i++) {
+ ap = NM_ACCESS_POINT (g_ptr_array_index (unique_aps, i));
+ add_access_point (page, ap, active_ap);
+ }
+ g_ptr_array_unref (unique_aps);
+ add_access_point_other (page);
+
+ out:
+
+ if (enabled)
+ start_periodic_refresh (page);
+
+ priv->refreshing = FALSE;
+
+ return G_SOURCE_REMOVE;
+}
+
+/* Avoid repeated calls to refreshing the wireless list by making it refresh at
+ * most once per second */
+static void
+schedule_refresh_wireless_list (GisNetworkPage *page)
+{
+ static const guint refresh_wireless_list_timeout_sec = 1;
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+
+ cancel_periodic_refresh (page);
+
+ g_debug ("Delaying Wi-Fi list refresh (for %u sec)",
+ refresh_wireless_list_timeout_sec);
+
+ priv->refresh_timeout_id = g_timeout_add_seconds (refresh_wireless_list_timeout_sec,
+ (GSourceFunc) refresh_wireless_list,
+ page);
+}
+
+static void
+connection_activate_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ NMClient *client = NM_CLIENT (object);
+ NMActiveConnection *connection = NULL;
+ g_autoptr(GError) error = NULL;
+
+ connection = nm_client_activate_connection_finish (client, result, &error);
+ if (connection != NULL) {
+ g_clear_object (&connection);
+ } else {
+ /* failed to activate */
+ g_warning ("Failed to activate a connection: %s", error->message);
+ }
+}
+
+static void
+connection_add_activate_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ NMClient *client = NM_CLIENT (object);
+ NMActiveConnection *connection = NULL;
+ g_autoptr(GError) error = NULL;
+
+ connection = nm_client_add_and_activate_connection_finish (client, result, &error);
+ if (connection != NULL) {
+ g_clear_object (&connection);
+ } else {
+ /* failed to activate */
+ g_warning ("Failed to add and activate a connection: %s", error->message);
+ }
+}
+
+static void
+connect_to_hidden_network (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (page));
+
+ cc_network_panel_connect_to_hidden_network (GTK_WIDGET (root), priv->nm_client);
+}
+
+static void
+row_activated (GtkListBox *box,
+ GtkListBoxRow *row,
+ GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ gchar *object_path;
+ const GPtrArray *list;
+ GPtrArray *filtered;
+ NMConnection *connection;
+ NMConnection *connection_to_activate;
+ NMSettingWireless *setting;
+ GBytes *ssid;
+ GBytes *ssid_target;
+ GtkWidget *child;
+ int i;
+
+ if (priv->refreshing)
+ return;
+
+ child = gtk_list_box_row_get_child (row);
+ object_path = g_object_get_data (G_OBJECT (child), "object-path");
+ ssid_target = g_object_get_data (G_OBJECT (child), "ssid");
+
+ if (g_strcmp0 (object_path, "ap-other...") == 0) {
+ connect_to_hidden_network (page);
+ goto out;
+ }
+
+ list = nm_client_get_connections (priv->nm_client);
+ filtered = nm_device_filter_connections (priv->nm_device, list);
+
+ connection_to_activate = NULL;
+
+ for (i = 0; i < filtered->len; i++) {
+ connection = NM_CONNECTION (filtered->pdata[i]);
+ setting = nm_connection_get_setting_wireless (connection);
+ if (!NM_IS_SETTING_WIRELESS (setting))
+ continue;
+
+ ssid = nm_setting_wireless_get_ssid (setting);
+ if (ssid == NULL)
+ continue;
+
+ if (nm_utils_same_ssid (g_bytes_get_data (ssid, NULL), g_bytes_get_size (ssid),
+ g_bytes_get_data (ssid_target, NULL), g_bytes_get_size (ssid_target), TRUE)) {
+ connection_to_activate = connection;
+ break;
+ }
+ }
+ g_ptr_array_unref (filtered);
+
+ if (connection_to_activate != NULL) {
+ nm_client_activate_connection_async (priv->nm_client,
+ connection_to_activate,
+ priv->nm_device, NULL,
+ NULL,
+ connection_activate_cb, page);
+ return;
+ }
+
+ nm_client_add_and_activate_connection_async (priv->nm_client,
+ NULL,
+ priv->nm_device, object_path,
+ NULL,
+ connection_add_activate_cb, page);
+
+ out:
+ schedule_refresh_wireless_list (page);
+}
+
+static void
+connection_state_changed (NMActiveConnection *c, GParamSpec *pspec, GisNetworkPage *page)
+{
+ schedule_refresh_wireless_list (page);
+}
+
+static void
+active_connections_changed (NMClient *client, GParamSpec *pspec, GisNetworkPage *page)
+{
+ const GPtrArray *connections;
+ guint i;
+
+ connections = nm_client_get_active_connections (client);
+ for (i = 0; connections && (i < connections->len); i++) {
+ NMActiveConnection *connection;
+
+ connection = g_ptr_array_index (connections, i);
+ if (!g_object_get_data (G_OBJECT (connection), "has-state-changed-handler")) {
+ g_signal_connect (connection, "notify::state",
+ G_CALLBACK (connection_state_changed), page);
+ g_object_set_data (G_OBJECT (connection), "has-state-changed-handler", GINT_TO_POINTER (1));
+ }
+ }
+}
+
+static void
+sync_complete (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ gboolean has_device;
+ gboolean activated;
+ gboolean visible;
+
+ has_device = priv->nm_device != NULL;
+ activated = priv->nm_device != NULL
+ && nm_device_get_state (priv->nm_device) == NM_DEVICE_STATE_ACTIVATED;
+
+ if (priv->ever_shown) {
+ visible = TRUE;
+ } else if (!has_device) {
+ g_debug ("No network device found, hiding network page");
+ visible = FALSE;
+ } else if (activated) {
+ g_debug ("Activated network device found, hiding network page");
+ visible = FALSE;
+ } else {
+ visible = TRUE;
+ }
+
+ gis_page_set_complete (GIS_PAGE (page), activated);
+ gtk_widget_set_visible (GTK_WIDGET (page), visible);
+
+ if (has_device)
+ schedule_refresh_wireless_list (page);
+}
+
+static void
+device_state_changed (GObject *object, GParamSpec *param, GisNetworkPage *page)
+{
+ sync_complete (page);
+}
+
+static void
+find_best_device (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ const GPtrArray *devices;
+ guint i;
+
+ /* FIXME: deal with multiple devices and devices being removed */
+ if (priv->nm_device != NULL) {
+ g_debug ("Already showing network device %s",
+ nm_device_get_description (priv->nm_device));
+ return;
+ }
+
+ devices = nm_client_get_devices (priv->nm_client);
+ g_return_if_fail (devices != NULL);
+ for (i = 0; i < devices->len; i++) {
+ NMDevice *device = g_ptr_array_index (devices, i);
+
+ if (!nm_device_get_managed (device))
+ continue;
+
+ if (nm_device_get_device_type (device) == NM_DEVICE_TYPE_WIFI) {
+ /* FIXME deal with multiple, dynamic devices */
+ priv->nm_device = g_object_ref (device);
+ g_debug ("Showing network device %s",
+ nm_device_get_description (priv->nm_device));
+
+ g_signal_connect (priv->nm_device, "notify::state",
+ G_CALLBACK (device_state_changed), page);
+ g_signal_connect (priv->nm_client, "notify::active-connections",
+ G_CALLBACK (active_connections_changed), page);
+
+ break;
+ }
+ }
+
+ sync_complete (page);
+}
+static void
+device_notify_managed (NMDevice *device,
+ GParamSpec *param,
+ void *user_data)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (user_data);
+
+ find_best_device (page);
+}
+
+static void
+client_device_added (NMClient *client,
+ NMDevice *device,
+ void *user_data)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (user_data);
+
+ g_signal_connect_object (device,
+ "notify::managed",
+ G_CALLBACK (device_notify_managed),
+ G_OBJECT (page),
+ 0);
+
+ find_best_device (page);
+}
+
+static void
+client_device_removed (NMClient *client,
+ NMDevice *device,
+ void *user_data)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (user_data);
+
+ /* TODO: reset page if priv->nm_device == device */
+ g_signal_handlers_disconnect_by_func (device, device_notify_managed, page);
+
+ find_best_device (page);
+}
+
+static void
+monitor_network_devices (GisNetworkPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ const GPtrArray *devices;
+ guint i;
+
+ g_assert (priv->nm_client != NULL);
+ devices = nm_client_get_devices (priv->nm_client);
+ g_return_if_fail (devices != NULL);
+ for (i = 0; devices != NULL && i < devices->len; i++) {
+ NMDevice *device = g_ptr_array_index (devices, i);
+
+ g_signal_connect_object (device,
+ "notify::managed",
+ G_CALLBACK (device_notify_managed),
+ G_OBJECT (page),
+ 0);
+ }
+
+ g_signal_connect_object (priv->nm_client,
+ "device-added",
+ G_CALLBACK (client_device_added),
+ G_OBJECT (page),
+ 0);
+ g_signal_connect_object (priv->nm_client,
+ "device-removed",
+ G_CALLBACK (client_device_removed),
+ G_OBJECT (page),
+ 0);
+
+ find_best_device (page);
+}
+
+static void
+gis_network_page_constructed (GObject *object)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (object);
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+ g_autoptr(GError) error = NULL;
+
+ G_OBJECT_CLASS (gis_network_page_parent_class)->constructed (object);
+
+ gis_page_set_skippable (GIS_PAGE (page), TRUE);
+
+ priv->ever_shown = g_getenv ("GIS_ALWAYS_SHOW_NETWORK_PAGE") != NULL;
+ priv->icons = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (priv->network_list), GTK_SELECTION_NONE);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->network_list), ap_sort, NULL, NULL);
+ g_signal_connect (priv->network_list, "row-activated",
+ G_CALLBACK (row_activated), page);
+
+ priv->nm_client = nm_client_new (NULL, &error);
+ if (!priv->nm_client) {
+ g_warning ("Can't create NetworkManager client, hiding network page: %s",
+ error->message);
+ sync_complete (page);
+ return;
+ }
+
+ g_object_bind_property (priv->nm_client, "wireless-enabled",
+ priv->turn_on_switch, "active",
+ G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+ monitor_network_devices (page);
+}
+
+static void
+gis_network_page_dispose (GObject *object)
+{
+ GisNetworkPage *page = GIS_NETWORK_PAGE (object);
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (page);
+
+ g_clear_object (&priv->nm_client);
+ g_clear_object (&priv->nm_device);
+ g_clear_object (&priv->icons);
+
+ cancel_periodic_refresh (page);
+
+ G_OBJECT_CLASS (gis_network_page_parent_class)->dispose (object);
+}
+
+static void
+gis_network_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Network"));
+}
+
+static void
+gis_network_page_shown (GisPage *page)
+{
+ GisNetworkPagePrivate *priv = gis_network_page_get_instance_private (GIS_NETWORK_PAGE (page));
+
+ priv->ever_shown = TRUE;
+}
+
+static void
+gis_network_page_class_init (GisNetworkPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-network-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisNetworkPage, network_list);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisNetworkPage, no_network_label);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisNetworkPage, no_network_spinner);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisNetworkPage, turn_on_label);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisNetworkPage, turn_on_switch);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_network_page_locale_changed;
+ page_class->shown = gis_network_page_shown;
+ object_class->constructed = gis_network_page_constructed;
+ object_class->dispose = gis_network_page_dispose;
+}
+
+static void
+gis_network_page_init (GisNetworkPage *page)
+{
+ g_resources_register (network_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_network_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_NETWORK_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/network/gis-network-page.h b/gnome-initial-setup/pages/network/gis-network-page.h
new file mode 100644
index 0000000..172b7d1
--- /dev/null
+++ b/gnome-initial-setup/pages/network/gis-network-page.h
@@ -0,0 +1,55 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_NETWORK_PAGE_H__
+#define __GIS_NETWORK_PAGE_H__
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_NETWORK_PAGE (gis_network_page_get_type ())
+#define GIS_NETWORK_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_NETWORK_PAGE, GisNetworkPage))
+#define GIS_NETWORK_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_NETWORK_PAGE, GisNetworkPageClass))
+#define GIS_IS_NETWORK_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_NETWORK_PAGE))
+#define GIS_IS_NETWORK_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_NETWORK_PAGE))
+#define GIS_NETWORK_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_NETWORK_PAGE, GisNetworkPageClass))
+
+typedef struct _GisNetworkPage GisNetworkPage;
+typedef struct _GisNetworkPageClass GisNetworkPageClass;
+
+struct _GisNetworkPage
+{
+ GisPage parent;
+};
+
+struct _GisNetworkPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_network_page_get_type (void);
+
+GisPage *gis_prepare_network_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_NETWORK_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/network/gis-network-page.ui b/gnome-initial-setup/pages/network/gis-network-page.ui
new file mode 100644
index 0000000..2bda59c
--- /dev/null
+++ b/gnome-initial-setup/pages/network/gis-network-page.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisNetworkPage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="margin_bottom">32</property>
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Wi-Fi</property>
+ <property name="subtitle" translatable="yes">Connecting to the internet helps you get new apps, information, and other upgrades. It also helps set the time and your location automatically.</property>
+ <property name="icon_name">network-wireless-symbolic</property>
+ <property name="show_icon" bind-source="GisNetworkPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="network_list">
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <property name="margin-top">18</property>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid" id="no_network_grid">
+ <property name="margin_top">18</property>
+ <property name="halign">center</property>
+ <property name="valign">start</property>
+ <property name="row-spacing">10</property>
+ <child>
+ <object class="GtkSpinner" id="no_network_spinner">
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="spinning">True</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ <property name="column-span">1</property>
+ <property name="row-span">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="no_network_label">
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">No wireless available</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ <property name="column-span">2</property>
+ <property name="row-span">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="turn_on_label">
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="label" translatable="yes">Turn On</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ <property name="column-span">1</property>
+ <property name="row-span">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="turn_on_switch">
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">1</property>
+ <property name="column-span">1</property>
+ <property name="row-span">1</property>
+ </layout>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/network/meson.build b/gnome-initial-setup/pages/network/meson.build
new file mode 100644
index 0000000..2c0f2a7
--- /dev/null
+++ b/gnome-initial-setup/pages/network/meson.build
@@ -0,0 +1,12 @@
+sources += gnome.compile_resources(
+ 'network-resources',
+ files('network.gresource.xml'),
+ c_name: 'network'
+)
+
+sources += files(
+ 'network-dialogs.c',
+ 'network-dialogs.h',
+ 'gis-network-page.c',
+ 'gis-network-page.h'
+)
diff --git a/gnome-initial-setup/pages/network/network-dialogs.c b/gnome-initial-setup/pages/network/network-dialogs.c
new file mode 100644
index 0000000..9018eac
--- /dev/null
+++ b/gnome-initial-setup/pages/network/network-dialogs.c
@@ -0,0 +1,515 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2011 Giovanni Campagna <scampa.giovanni@gmail.com>
+ *
+ * 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.
+ *
+ * Portions of this code were taken from network-manager-applet.
+ * Copyright 2008 - 2011 Red Hat, Inc.
+ */
+
+#include <gtk/gtk.h>
+#include <NetworkManager.h>
+#include <nma-wifi-dialog.h>
+#include <nma-mobile-wizard.h>
+
+typedef struct {
+ NMClient *client;
+} WirelessDialogClosure;
+
+typedef struct {
+ NMClient *client;
+ NMDevice *device;
+} MobileDialogClosure;
+
+static void
+wireless_dialog_closure_closure_notify (gpointer data,
+ GClosure *gclosure)
+{
+ WirelessDialogClosure *closure = data;
+ g_object_unref (closure->client);
+
+ g_slice_free (WirelessDialogClosure, data);
+}
+
+static void
+mobile_dialog_closure_free (gpointer data)
+{
+ MobileDialogClosure *closure = data;
+ g_object_unref (closure->client);
+ g_object_unref (closure->device);
+
+ g_slice_free (MobileDialogClosure, data);
+}
+
+static gboolean
+wifi_can_create_wifi_network (NMClient *client)
+{
+ NMClientPermissionResult perm;
+
+ /* FIXME: check WIFI_SHARE_PROTECTED too, and make the wireless dialog
+ * handle the permissions as well so that admins can restrict open network
+ * creation separately from protected network creation.
+ */
+ perm = nm_client_get_permission_result (client, NM_CLIENT_PERMISSION_WIFI_SHARE_OPEN);
+ if (perm == NM_CLIENT_PERMISSION_RESULT_YES || perm == NM_CLIENT_PERMISSION_RESULT_AUTH)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+activate_existing_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ NMClient *client = NM_CLIENT (object);
+ NMActiveConnection *connection;
+ GError *error = NULL;
+
+ connection = nm_client_activate_connection_finish (client, result, &error);
+ if (connection) {
+ g_object_unref (connection);
+ } else {
+ g_warning ("Failed to activate connection: (%d) %s", error->code, error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+activate_new_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ NMClient *client = NM_CLIENT (object);
+ NMActiveConnection *connection;
+ GError *error = NULL;
+
+ connection = nm_client_add_and_activate_connection_finish (client, result, &error);
+ if (connection) {
+ g_object_unref (connection);
+ } else {
+ g_warning ("Failed to add new connection: (%d) %s", error->code, error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+wireless_dialog_response_cb (GtkDialog *foo,
+ gint response,
+ gpointer user_data)
+{
+ NMAWifiDialog *dialog = NMA_WIFI_DIALOG (foo);
+ WirelessDialogClosure *closure = user_data;
+ NMConnection *connection, *fuzzy_match = NULL;
+ NMDevice *device;
+ NMAccessPoint *ap;
+ const GPtrArray *all;
+ int i;
+
+ if (response != GTK_RESPONSE_OK)
+ goto done;
+
+ /* nma_wifi_dialog_get_connection() returns a connection with the
+ * refcount incremented, so the caller must remember to unref it.
+ */
+ connection = nma_wifi_dialog_get_connection (dialog, &device, &ap);
+ g_assert (connection);
+ g_assert (device);
+
+ /* Find a similar connection and use that instead */
+ all = nm_client_get_connections (closure->client);
+ for (i = 0; i < all->len; i++) {
+ if (nm_connection_compare (connection,
+ NM_CONNECTION (all->pdata[i]),
+ (NM_SETTING_COMPARE_FLAG_FUZZY | NM_SETTING_COMPARE_FLAG_IGNORE_ID))) {
+ fuzzy_match = NM_CONNECTION (all->pdata[i]);
+ break;
+ }
+ }
+
+ if (fuzzy_match) {
+ nm_client_activate_connection_async (closure->client,
+ fuzzy_match,
+ device,
+ ap ? nm_object_get_path (NM_OBJECT (ap)) : NULL,
+ NULL,
+ activate_existing_cb,
+ NULL);
+ } else {
+ NMSetting *s_con;
+ NMSettingWireless *s_wifi;
+ const char *mode = NULL;
+
+ /* Entirely new connection */
+
+ /* Don't autoconnect adhoc networks by default for now */
+ s_wifi = (NMSettingWireless *) nm_connection_get_setting (connection, NM_TYPE_SETTING_WIRELESS);
+ if (s_wifi)
+ mode = nm_setting_wireless_get_mode (s_wifi);
+ if (g_strcmp0 (mode, "adhoc") == 0) {
+ s_con = nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION);
+ if (!s_con) {
+ s_con = nm_setting_connection_new ();
+ nm_connection_add_setting (connection, s_con);
+ }
+ g_object_set (G_OBJECT (s_con), NM_SETTING_CONNECTION_AUTOCONNECT, FALSE, NULL);
+ }
+
+ nm_client_add_and_activate_connection_async (closure->client,
+ connection,
+ device,
+ ap ? nm_object_get_path (NM_OBJECT (ap)) : NULL,
+ NULL,
+ activate_new_cb,
+ NULL);
+ }
+
+ /* Balance nma_wifi_dialog_get_connection() */
+ g_object_unref (connection);
+
+done:
+ gtk_window_destroy (GTK_WINDOW (dialog));
+}
+
+static void
+show_wireless_dialog (GtkWidget *toplevel,
+ NMClient *client,
+ GtkWidget *dialog)
+{
+ WirelessDialogClosure *closure;
+
+ g_debug ("About to parent and show a network dialog");
+
+ g_object_set (G_OBJECT (dialog),
+ "modal", TRUE,
+ "transient-for", toplevel,
+ NULL);
+
+ closure = g_slice_new (WirelessDialogClosure);
+ closure->client = g_object_ref (client);
+ g_signal_connect_data (dialog, "response",
+ G_CALLBACK (wireless_dialog_response_cb),
+ closure, wireless_dialog_closure_closure_notify, 0);
+
+ g_object_bind_property (G_OBJECT (toplevel), "visible",
+ G_OBJECT (dialog), "visible",
+ G_BINDING_SYNC_CREATE);
+}
+
+void
+cc_network_panel_create_wifi_network (GtkWidget *toplevel,
+ NMClient *client)
+{
+ if (wifi_can_create_wifi_network (client)) {
+ show_wireless_dialog (toplevel, client,
+ nma_wifi_dialog_new_for_create (client));
+ }
+}
+
+void
+cc_network_panel_connect_to_hidden_network (GtkWidget *toplevel,
+ NMClient *client)
+{
+ g_debug ("connect to hidden wifi");
+ show_wireless_dialog (toplevel, client,
+ nma_wifi_dialog_new_for_hidden (client));
+}
+
+void
+cc_network_panel_connect_to_8021x_network (GtkWidget *toplevel,
+ NMClient *client,
+ NMDevice *device,
+ const gchar *arg_access_point)
+{
+ NMConnection *connection;
+ NMSettingConnection *s_con;
+ NMSettingWireless *s_wifi;
+ NMSettingWirelessSecurity *s_wsec;
+ NMSetting8021x *s_8021x;
+ NM80211ApSecurityFlags wpa_flags, rsn_flags;
+ GtkWidget *dialog;
+ char *uuid;
+ NMAccessPoint *ap;
+
+ g_debug ("connect to 8021x wifi");
+ ap = nm_device_wifi_get_access_point_by_path (NM_DEVICE_WIFI (device), arg_access_point);
+ if (ap == NULL) {
+ g_warning ("didn't find access point with path %s", arg_access_point);
+ return;
+ }
+
+ /* If the AP is WPA[2]-Enterprise then we need to set up a minimal 802.1x
+ * setting and ask the user for more information.
+ */
+ rsn_flags = nm_access_point_get_rsn_flags (ap);
+ wpa_flags = nm_access_point_get_wpa_flags (ap);
+ if (!(rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)
+ && !(wpa_flags & NM_802_11_AP_SEC_KEY_MGMT_802_1X)) {
+ g_warning ("Network panel loaded with connect-8021x-wifi but the "
+ "access point does not support 802.1x");
+ return;
+ }
+
+ connection = nm_simple_connection_new ();
+
+ /* Need a UUID for the "always ask" stuff in the Dialog of Doom */
+ s_con = (NMSettingConnection *) nm_setting_connection_new ();
+ uuid = nm_utils_uuid_generate ();
+ g_object_set (s_con, NM_SETTING_CONNECTION_UUID, uuid, NULL);
+ g_free (uuid);
+ nm_connection_add_setting (connection, NM_SETTING (s_con));
+
+ s_wifi = (NMSettingWireless *) nm_setting_wireless_new ();
+ nm_connection_add_setting (connection, NM_SETTING (s_wifi));
+ g_object_set (s_wifi,
+ NM_SETTING_WIRELESS_SSID, nm_access_point_get_ssid (ap),
+ NULL);
+
+ s_wsec = (NMSettingWirelessSecurity *) nm_setting_wireless_security_new ();
+ g_object_set (s_wsec, NM_SETTING_WIRELESS_SECURITY_KEY_MGMT, "wpa-eap", NULL);
+ nm_connection_add_setting (connection, NM_SETTING (s_wsec));
+
+ s_8021x = (NMSetting8021x *) nm_setting_802_1x_new ();
+ nm_setting_802_1x_add_eap_method (s_8021x, "ttls");
+ g_object_set (s_8021x, NM_SETTING_802_1X_PHASE2_AUTH, "mschapv2", NULL);
+ nm_connection_add_setting (connection, NM_SETTING (s_8021x));
+
+ dialog = nma_wifi_dialog_new (client, connection, device, ap, FALSE);
+ show_wireless_dialog (toplevel, client, dialog);
+}
+
+static void
+connect_3g (NMConnection *connection,
+ gboolean canceled,
+ gpointer user_data)
+{
+ MobileDialogClosure *closure = user_data;
+
+ if (canceled == FALSE) {
+ g_return_if_fail (connection != NULL);
+
+ /* Ask NM to add the new connection and activate it; NM will fill in the
+ * missing details based on the specific object and the device.
+ */
+ nm_client_add_and_activate_connection_async (closure->client,
+ connection,
+ closure->device,
+ "/",
+ NULL,
+ activate_new_cb,
+ NULL);
+ }
+
+ mobile_dialog_closure_free (closure);
+}
+
+static void
+cdma_mobile_wizard_done (NMAMobileWizard *wizard,
+ gboolean canceled,
+ NMAMobileWizardAccessMethod *method,
+ gpointer user_data)
+{
+ NMConnection *connection = NULL;
+
+ if (!canceled && method) {
+ NMSetting *setting;
+ char *uuid, *id;
+
+ if (method->devtype != NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO) {
+ g_warning ("Unexpected device type (not CDMA).");
+ canceled = TRUE;
+ goto done;
+ }
+
+ connection = nm_simple_connection_new ();
+
+ setting = nm_setting_cdma_new ();
+ g_object_set (setting,
+ NM_SETTING_CDMA_NUMBER, "#777",
+ NM_SETTING_CDMA_USERNAME, method->username,
+ NM_SETTING_CDMA_PASSWORD, method->password,
+ NULL);
+ nm_connection_add_setting (connection, setting);
+
+ /* Serial setting */
+ setting = nm_setting_serial_new ();
+ g_object_set (setting,
+ NM_SETTING_SERIAL_BAUD, 115200,
+ NM_SETTING_SERIAL_BITS, 8,
+ NM_SETTING_SERIAL_PARITY, 'n',
+ NM_SETTING_SERIAL_STOPBITS, 1,
+ NULL);
+ nm_connection_add_setting (connection, setting);
+
+ nm_connection_add_setting (connection, nm_setting_ppp_new ());
+
+ setting = nm_setting_connection_new ();
+ if (method->plan_name)
+ id = g_strdup_printf ("%s %s", method->provider_name, method->plan_name);
+ else
+ id = g_strdup_printf ("%s connection", method->provider_name);
+ uuid = nm_utils_uuid_generate ();
+ g_object_set (setting,
+ NM_SETTING_CONNECTION_ID, id,
+ NM_SETTING_CONNECTION_TYPE, NM_SETTING_CDMA_SETTING_NAME,
+ NM_SETTING_CONNECTION_AUTOCONNECT, FALSE,
+ NM_SETTING_CONNECTION_UUID, uuid,
+ NULL);
+ g_free (uuid);
+ g_free (id);
+ nm_connection_add_setting (connection, setting);
+ }
+
+ done:
+ connect_3g (connection, canceled, user_data);
+ nma_mobile_wizard_destroy (wizard);
+}
+
+static void
+gsm_mobile_wizard_done (NMAMobileWizard *wizard,
+ gboolean canceled,
+ NMAMobileWizardAccessMethod *method,
+ gpointer user_data)
+{
+ NMConnection *connection = NULL;
+
+ if (!canceled && method) {
+ NMSetting *setting;
+ char *uuid, *id;
+
+ if (method->devtype != NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS) {
+ g_warning ("Unexpected device type (not GSM).");
+ canceled = TRUE;
+ goto done;
+ }
+
+ connection = nm_simple_connection_new ();
+
+ setting = nm_setting_gsm_new ();
+ g_object_set (setting,
+ NM_SETTING_GSM_NUMBER, "*99#",
+ NM_SETTING_GSM_USERNAME, method->username,
+ NM_SETTING_GSM_PASSWORD, method->password,
+ NM_SETTING_GSM_APN, method->gsm_apn,
+ NULL);
+ nm_connection_add_setting (connection, setting);
+
+ /* Serial setting */
+ setting = nm_setting_serial_new ();
+ g_object_set (setting,
+ NM_SETTING_SERIAL_BAUD, 115200,
+ NM_SETTING_SERIAL_BITS, 8,
+ NM_SETTING_SERIAL_PARITY, 'n',
+ NM_SETTING_SERIAL_STOPBITS, 1,
+ NULL);
+ nm_connection_add_setting (connection, setting);
+
+ nm_connection_add_setting (connection, nm_setting_ppp_new ());
+
+ setting = nm_setting_connection_new ();
+ if (method->plan_name)
+ id = g_strdup_printf ("%s %s", method->provider_name, method->plan_name);
+ else
+ id = g_strdup_printf ("%s connection", method->provider_name);
+ uuid = nm_utils_uuid_generate ();
+ g_object_set (setting,
+ NM_SETTING_CONNECTION_ID, id,
+ NM_SETTING_CONNECTION_TYPE, NM_SETTING_GSM_SETTING_NAME,
+ NM_SETTING_CONNECTION_AUTOCONNECT, FALSE,
+ NM_SETTING_CONNECTION_UUID, uuid,
+ NULL);
+ g_free (uuid);
+ g_free (id);
+ nm_connection_add_setting (connection, setting);
+ }
+
+done:
+ connect_3g (connection, canceled, user_data);
+ nma_mobile_wizard_destroy (wizard);
+}
+
+static void
+toplevel_shown (GtkWindow *toplevel,
+ GParamSpec *pspec,
+ NMAMobileWizard *wizard)
+{
+ gboolean visible = FALSE;
+
+ g_object_get (G_OBJECT (toplevel), "visible", &visible, NULL);
+ if (visible)
+ nma_mobile_wizard_present (wizard);
+}
+
+static gboolean
+show_wizard_idle_cb (NMAMobileWizard *wizard)
+{
+ nma_mobile_wizard_present (wizard);
+ return FALSE;
+}
+
+void
+cc_network_panel_connect_to_3g_network (GtkWidget *toplevel,
+ NMClient *client,
+ NMDevice *device)
+{
+ MobileDialogClosure *closure;
+ NMAMobileWizard *wizard;
+ NMDeviceModemCapabilities caps;
+ gboolean visible = FALSE;
+
+ g_debug ("connect to 3g");
+ if (!NM_IS_DEVICE_MODEM (device)) {
+ g_warning ("Network panel loaded with connect-3g but the selected device"
+ " is not a modem");
+ return;
+ }
+
+ closure = g_slice_new (MobileDialogClosure);
+ closure->client = g_object_ref (client);
+ closure->device = g_object_ref (device);
+
+ caps = nm_device_modem_get_current_capabilities (NM_DEVICE_MODEM (device));
+ if (caps & NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS) {
+ wizard = nma_mobile_wizard_new (GTK_WINDOW (toplevel), NULL, NM_DEVICE_MODEM_CAPABILITY_GSM_UMTS, FALSE,
+ gsm_mobile_wizard_done, closure);
+ if (wizard == NULL) {
+ g_warning ("failed to construct GSM wizard");
+ return;
+ }
+ } else if (caps & NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO) {
+ wizard = nma_mobile_wizard_new (GTK_WINDOW (toplevel), NULL, NM_DEVICE_MODEM_CAPABILITY_CDMA_EVDO, FALSE,
+ cdma_mobile_wizard_done, closure);
+ if (wizard == NULL) {
+ g_warning ("failed to construct CDMA wizard");
+ return;
+ }
+ } else {
+ g_warning ("Network panel loaded with connect-3g but the selected device"
+ " does not support GSM or CDMA");
+ mobile_dialog_closure_free (closure);
+ return;
+ }
+
+ g_object_get (G_OBJECT (toplevel), "visible", &visible, NULL);
+ if (visible) {
+ g_debug ("Scheduling showing the Mobile wizard");
+ g_idle_add ((GSourceFunc) show_wizard_idle_cb, wizard);
+ } else {
+ g_debug ("Will show wizard a bit later, toplevel is not visible");
+ g_signal_connect (G_OBJECT (toplevel), "notify::visible",
+ G_CALLBACK (toplevel_shown), wizard);
+ }
+}
diff --git a/gnome-initial-setup/pages/network/network-dialogs.h b/gnome-initial-setup/pages/network/network-dialogs.h
new file mode 100644
index 0000000..0f02a52
--- /dev/null
+++ b/gnome-initial-setup/pages/network/network-dialogs.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2011 Giovanni Campagna <scampa.giovanni@gmail.com>
+ *
+ * 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.
+ */
+
+#ifndef _NETWORK_DIALOGS_H
+#define _NETWORK_DIALOGS_H
+
+#include <NetworkManager.h>
+#include <gtk/gtk.h>
+
+void cc_network_panel_create_wifi_network (GtkWidget *toplevel,
+ NMClient *client);
+
+void cc_network_panel_connect_to_hidden_network (GtkWidget *toplevel,
+ NMClient *client);
+
+void cc_network_panel_connect_to_8021x_network (GtkWidget *toplevel,
+ NMClient *client,
+ NMDevice *device,
+ const gchar *arg_access_point);
+
+void cc_network_panel_connect_to_3g_network (GtkWidget *toplevel,
+ NMClient *client,
+ NMDevice *device);
+
+#endif /* _NETWORK_DIALOGS_H */
diff --git a/gnome-initial-setup/pages/network/network.gresource.xml b/gnome-initial-setup/pages/network/network.gresource.xml
new file mode 100644
index 0000000..bf5394f
--- /dev/null
+++ b/gnome-initial-setup/pages/network/network.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-network-page.ui">gis-network-page.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.c b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.c
new file mode 100644
index 0000000..736194d
--- /dev/null
+++ b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.c
@@ -0,0 +1,246 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright © 2020 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Philip Withnall <withnall@endlessm.com>
+ */
+
+/* Parental controls page {{{1 */
+
+#define PAGE_ID "parental-controls"
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+#include <libmalcontent-ui/malcontent-ui.h>
+
+#include "parental-controls-resources.h"
+#include "gis-page-header.h"
+#include "gis-parental-controls-page.h"
+
+struct _GisParentalControlsPage
+{
+ GisPage parent_instance;
+
+ GtkWidget *header;
+ GtkWidget *user_controls;
+};
+
+G_DEFINE_TYPE (GisParentalControlsPage, gis_parental_controls_page, GIS_TYPE_PAGE)
+
+static gboolean
+gis_parental_controls_page_save_data (GisPage *gis_page,
+ GError **error)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (gis_page);
+ g_autoptr(GDBusConnection) system_bus = NULL;
+ g_autoptr(MctManager) manager = NULL;
+ g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT ();
+ g_autoptr(MctAppFilter) app_filter = NULL;
+ ActUser *main_user;
+
+ /* The parent and child users are created by the #GisAccountPage earlier in
+ * the save_data() process. We now need to set the parental controls on the
+ * child user. The earlier step in the process must have succeeded. */
+ gis_driver_get_user_permissions (gis_page->driver, &main_user, NULL);
+ g_return_val_if_fail (main_user != NULL, FALSE);
+
+ mct_user_controls_build_app_filter (MCT_USER_CONTROLS (page->user_controls), &builder);
+ app_filter = mct_app_filter_builder_end (&builder);
+
+ /* FIXME: should become asynchronous */
+ system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, error);
+ if (system_bus == NULL)
+ return FALSE;
+
+ manager = mct_manager_new (system_bus);
+ if (!mct_manager_set_app_filter (manager,
+ act_user_get_uid (main_user),
+ app_filter,
+ MCT_MANAGER_SET_VALUE_FLAGS_NONE,
+ NULL,
+ error))
+ return FALSE;
+
+ return TRUE;
+}
+
+static void
+gis_parental_controls_page_shown (GisPage *gis_page)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (gis_page);
+
+ gtk_widget_grab_focus (page->user_controls);
+}
+
+static void
+update_header (GisParentalControlsPage *page)
+{
+ g_autofree gchar *title = NULL;
+ const gchar *subtitle, *icon_name;
+ GdkPaintable *paintable;
+
+ /* Translators: The placeholder is the user’s full name. */
+ title = g_strdup_printf (_("Parental Controls for %s"),
+ gis_driver_get_full_name (GIS_PAGE (page)->driver));
+ subtitle = _("Set restrictions on what this user can run or install.");
+ paintable = gis_driver_get_avatar (GIS_PAGE (page)->driver);
+ icon_name = (paintable != NULL) ? NULL : "dialog-password-symbolic";
+
+ g_object_set (G_OBJECT (page->header),
+ "title", title,
+ "subtitle", subtitle,
+ NULL);
+ if (paintable != NULL)
+ g_object_set (G_OBJECT (page->header), "paintable", paintable, NULL);
+ else if (icon_name != NULL)
+ g_object_set (G_OBJECT (page->header), "icon-name", icon_name, NULL);
+}
+
+static void
+update_user_controls (GisParentalControlsPage *page)
+{
+ mct_user_controls_set_user_locale (MCT_USER_CONTROLS (page->user_controls),
+ gis_driver_get_user_language (GIS_PAGE (page)->driver));
+ mct_user_controls_set_user_display_name (MCT_USER_CONTROLS (page->user_controls),
+ gis_driver_get_full_name (GIS_PAGE (page)->driver));
+}
+
+static void
+user_details_changed_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (user_data);
+
+ update_user_controls (page);
+ update_header (page);
+}
+
+static void
+gis_parental_controls_page_constructed (GObject *object)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (object);
+ g_autoptr(GPermission) permission = NULL;
+ g_auto(MctAppFilterBuilder) builder = MCT_APP_FILTER_BUILDER_INIT ();
+ g_autoptr(MctAppFilter) app_filter = NULL;
+
+ G_OBJECT_CLASS (gis_parental_controls_page_parent_class)->constructed (object);
+
+ /* No validation needed. */
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+
+ /* Set up the user controls. We can’t set #MctUserControls:user because
+ * there’s no way to represent a not-yet-created user using an #ActUser. */
+ mct_user_controls_set_user_account_type (MCT_USER_CONTROLS (page->user_controls),
+ ACT_USER_ACCOUNT_TYPE_STANDARD);
+ update_user_controls (page);
+
+ app_filter = mct_app_filter_builder_end (&builder);
+ mct_user_controls_set_app_filter (MCT_USER_CONTROLS (page->user_controls), app_filter);
+
+ /* The gnome-initial-setup user should always be allowed to set parental
+ * controls. */
+ permission = g_simple_permission_new (TRUE);
+ mct_user_controls_set_permission (MCT_USER_CONTROLS (page->user_controls), permission);
+
+ /* Connect to signals. */
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::full-name",
+ G_CALLBACK (user_details_changed_cb), page);
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::avatar",
+ G_CALLBACK (user_details_changed_cb), page);
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::user-locale",
+ G_CALLBACK (user_details_changed_cb), page);
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::user-display-name",
+ G_CALLBACK (user_details_changed_cb), page);
+
+ update_header (page);
+
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_parental_controls_page_dispose (GObject *object)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (object);
+
+ if (GIS_PAGE (object)->driver != NULL)
+ {
+ g_signal_handlers_disconnect_by_func (GIS_PAGE (page)->driver,
+ user_details_changed_cb, page);
+ }
+
+ G_OBJECT_CLASS (gis_parental_controls_page_parent_class)->dispose (object);
+}
+
+static void
+gis_parental_controls_page_locale_changed (GisPage *gis_page)
+{
+ GisParentalControlsPage *page = GIS_PARENTAL_CONTROLS_PAGE (gis_page);
+
+ gis_page_set_title (gis_page, _("Parental Controls"));
+ update_header (page);
+}
+
+static void
+gis_parental_controls_page_class_init (GisParentalControlsPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+
+ object_class->constructed = gis_parental_controls_page_constructed;
+ object_class->dispose = gis_parental_controls_page_dispose;
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_parental_controls_page_locale_changed;
+ page_class->save_data = gis_parental_controls_page_save_data;
+ page_class->shown = gis_parental_controls_page_shown;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/initial-setup/gis-parental-controls-page.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GisParentalControlsPage, header);
+ gtk_widget_class_bind_template_child (widget_class, GisParentalControlsPage, user_controls);
+}
+
+static void
+gis_parental_controls_page_init (GisParentalControlsPage *page)
+{
+ g_resources_register (parental_controls_get_resource ());
+
+ /* Ensure types exist for widgets in the UI file. */
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ g_type_ensure (MCT_TYPE_USER_CONTROLS);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_parental_controls_page (GisDriver *driver)
+{
+ /* Skip parental controls if they’re not enabled. */
+ if (!gis_driver_get_parental_controls_enabled (driver))
+ return NULL;
+
+ return g_object_new (GIS_TYPE_PARENTAL_CONTROLS_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.h b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.h
new file mode 100644
index 0000000..3b32341
--- /dev/null
+++ b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.h
@@ -0,0 +1,38 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright © 2020 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Philip Withnall <withnall@endlessm.com>
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_PARENTAL_CONTROLS_PAGE (gis_parental_controls_page_get_type ())
+G_DECLARE_FINAL_TYPE (GisParentalControlsPage, gis_parental_controls_page, GIS, PARENTAL_CONTROLS_PAGE, GisPage)
+
+GType gis_parental_controls_page_get_type (void);
+
+GisPage *gis_prepare_parental_controls_page (GisDriver *driver);
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.ui b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.ui
new file mode 100644
index 0000000..5efdbd1
--- /dev/null
+++ b/gnome-initial-setup/pages/parental-controls/gis-parental-controls-page.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="GisParentalControlsPage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin-top">24</property>
+ <!-- title and subtitle are set in code, so are not set here -->
+ <property name="icon-name">dialog-password-symbolic</property>
+ <property name="show-icon" bind-source="GisParentalControlsPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="MctUserControls" id="user_controls" />
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/parental-controls/meson.build b/gnome-initial-setup/pages/parental-controls/meson.build
new file mode 100644
index 0000000..539460c
--- /dev/null
+++ b/gnome-initial-setup/pages/parental-controls/meson.build
@@ -0,0 +1,10 @@
+sources += gnome.compile_resources(
+ 'parental-controls-resources',
+ files('parental-controls.gresource.xml'),
+ c_name: 'parental_controls',
+)
+
+sources += files(
+ 'gis-parental-controls-page.c',
+ 'gis-parental-controls-page.h',
+)
diff --git a/gnome-initial-setup/pages/parental-controls/parental-controls.gresource.xml b/gnome-initial-setup/pages/parental-controls/parental-controls.gresource.xml
new file mode 100644
index 0000000..59460b5
--- /dev/null
+++ b/gnome-initial-setup/pages/parental-controls/parental-controls.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-parental-controls-page.ui">gis-parental-controls-page.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/password/account-resources.h b/gnome-initial-setup/pages/password/account-resources.h
new file mode 100644
index 0000000..d2abafe
--- /dev/null
+++ b/gnome-initial-setup/pages/password/account-resources.h
@@ -0,0 +1,7 @@
+#ifndef __RESOURCE_account_H__
+#define __RESOURCE_account_H__
+
+#include <gio/gio.h>
+
+extern GResource *account_get_resource (void);
+#endif
diff --git a/gnome-initial-setup/pages/password/gis-password-page.c b/gnome-initial-setup/pages/password/gis-password-page.c
new file mode 100644
index 0000000..700d80a
--- /dev/null
+++ b/gnome-initial-setup/pages/password/gis-password-page.c
@@ -0,0 +1,521 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Password page {{{1 */
+
+#define PAGE_ID "password"
+
+#include "config.h"
+#include "password-resources.h"
+#include "gis-password-page.h"
+
+#include "gis-keyring.h"
+
+#include "pw-utils.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include "gis-page-header.h"
+
+#define VALIDATION_TIMEOUT 600
+
+struct _GisPasswordPagePrivate
+{
+ GtkWidget *password_entry;
+ GtkWidget *confirm_entry;
+ GtkWidget *password_strength;
+ GtkWidget *password_explanation;
+ GtkWidget *confirm_explanation;
+ GtkWidget *header;
+
+ gboolean valid_confirm;
+ gboolean valid_password;
+ guint timeout_id;
+ const gchar *username;
+ gboolean parent_mode;
+};
+typedef struct _GisPasswordPagePrivate GisPasswordPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisPasswordPage, gis_password_page, GIS_TYPE_PAGE);
+
+typedef enum
+{
+ PROP_PARENT_MODE = 1,
+} GisPasswordPageProperty;
+
+static GParamSpec *obj_props[PROP_PARENT_MODE + 1];
+
+static void
+clear_password_validation_error (GtkWidget *entry)
+{
+ gtk_widget_remove_css_class (entry, "error");
+}
+
+static void
+set_password_validation_error (GtkWidget *entry)
+{
+ gtk_widget_add_css_class (entry, "error");
+}
+
+static void
+update_header (GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *subtitle = NULL;
+ const gchar *icon_name;
+ GdkPaintable *paintable;
+
+#ifndef HAVE_PARENTAL_CONTROLS
+ /* Don’t break UI compatibility if parental controls are disabled. */
+ title = g_strdup (_("Set a Password"));
+ subtitle = g_strdup (_("Be careful not to lose your password."));
+ paintable = NULL;
+ icon_name = "dialog-password-symbolic";
+#else
+ if (!priv->parent_mode)
+ {
+ /* Translators: The placeholder is for the user’s full name. */
+ title = g_strdup_printf (_("Set a Password for %s"),
+ gis_driver_get_full_name (GIS_PAGE (page)->driver));
+ subtitle = g_strdup (_("Be careful not to lose your password."));
+ paintable = gis_driver_get_avatar (GIS_PAGE (page)->driver);
+ icon_name = (paintable != NULL) ? NULL : "dialog-password-symbolic";
+ }
+ else
+ {
+ title = g_strdup (_("Set a Parent Password"));
+ /* Translators: The placeholder is the full name of the child user on the system. */
+ subtitle = g_strdup_printf (_("This password will control access to the parental controls for %s."),
+ gis_driver_get_full_name (GIS_PAGE (page)->driver));
+ icon_name = "org.freedesktop.MalcontentControl-symbolic";
+ paintable = NULL;
+ }
+#endif
+
+ /* Doesn’t make sense to set both. */
+ g_assert (icon_name == NULL || paintable == NULL);
+
+ g_object_set (G_OBJECT (priv->header),
+ "title", title,
+ "subtitle", subtitle,
+ NULL);
+ if (paintable != NULL)
+ g_object_set (G_OBJECT (priv->header), "paintable", paintable, NULL);
+ else if (icon_name != NULL)
+ g_object_set (G_OBJECT (priv->header), "icon-name", icon_name, NULL);
+}
+
+static void
+set_parent_mode (GisPasswordPage *page,
+ gboolean parent_mode)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ g_return_if_fail (GIS_IS_PASSWORD_PAGE (page));
+
+ if (priv->parent_mode == parent_mode)
+ return;
+
+ priv->parent_mode = parent_mode;
+ g_object_notify_by_pspec (G_OBJECT (page), obj_props[PROP_PARENT_MODE]);
+
+ update_header (page);
+}
+
+static gboolean
+page_validate (GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ return priv->valid_confirm;
+}
+
+static void
+update_page_validation (GisPasswordPage *page)
+{
+ gis_page_set_complete (GIS_PAGE (page), page_validate (page));
+}
+
+static gboolean
+gis_password_page_save_data (GisPage *gis_page,
+ GError **error)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (gis_page);
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+ ActUser *act_user;
+ UmAccountMode account_mode;
+ const gchar *password = NULL;
+
+ g_assert (gis_page->driver != NULL);
+
+ account_mode = gis_driver_get_account_mode (gis_page->driver);
+
+ if (!priv->parent_mode)
+ gis_driver_get_user_permissions (gis_page->driver, &act_user, &password);
+ else
+ gis_driver_get_parent_permissions (gis_page->driver, &act_user, &password);
+
+ if (account_mode == UM_ENTERPRISE) {
+ g_assert (!priv->parent_mode);
+
+ if (password != NULL)
+ gis_update_login_keyring_password (password);
+ return TRUE;
+ }
+
+ password = gtk_editable_get_text (GTK_EDITABLE (priv->password_entry));
+
+ if (strlen (password) == 0)
+ act_user_set_password_mode (act_user, ACT_USER_PASSWORD_MODE_NONE);
+ else
+ act_user_set_password (act_user, password, "");
+
+ if (!priv->parent_mode)
+ gis_driver_set_user_permissions (gis_page->driver, act_user, password);
+ else
+ gis_driver_set_parent_permissions (gis_page->driver, act_user, password);
+
+ if (!priv->parent_mode)
+ gis_update_login_keyring_password (password);
+
+ return TRUE;
+}
+
+static void
+gis_password_page_shown (GisPage *gis_page)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (gis_page);
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ gtk_widget_grab_focus (priv->password_entry);
+}
+
+static gboolean
+validate (GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+ const gchar *password;
+ const gchar *verify;
+ gint strength_level;
+ const gchar *hint;
+
+ g_clear_handle_id (&priv->timeout_id, g_source_remove);
+
+ password = gtk_editable_get_text (GTK_EDITABLE (priv->password_entry));
+ verify = gtk_editable_get_text (GTK_EDITABLE (priv->confirm_entry));
+
+ pw_strength (password, NULL, priv->username, &hint, &strength_level);
+ gtk_level_bar_set_value (GTK_LEVEL_BAR (priv->password_strength), strength_level);
+ gtk_label_set_label (GTK_LABEL (priv->password_explanation), hint);
+
+ gtk_label_set_label (GTK_LABEL (priv->confirm_explanation), "");
+ priv->valid_confirm = FALSE;
+
+ priv->valid_password = (strlen (password) && strength_level > 1);
+ if (priv->valid_password)
+ clear_password_validation_error (priv->password_entry);
+ else
+ set_password_validation_error (priv->password_entry);
+
+ if (strlen (password) > 0 && strlen (verify) > 0) {
+ priv->valid_confirm = (strcmp (password, verify) == 0);
+ if (!priv->valid_confirm)
+ gtk_label_set_label (GTK_LABEL (priv->confirm_explanation), _("The passwords do not match."));
+ else
+ clear_password_validation_error (priv->password_entry);
+ }
+
+ /*
+ * We deliberately don’t validate that the parent password and main user
+ * password are different. It’s more feasible that someone would usefully
+ * want to set their system up that way, than it is that the parent and child
+ * would accidentally choose the same password.
+ */
+
+ update_page_validation (page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+on_focusout (GisPasswordPage *page)
+{
+ validate (page);
+
+ return FALSE;
+}
+
+static void
+password_changed (GtkWidget *w,
+ GParamSpec *pspec,
+ GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ clear_password_validation_error (w);
+ clear_password_validation_error (priv->confirm_entry);
+
+ priv->valid_password = FALSE;
+ update_page_validation (page);
+
+ if (priv->timeout_id != 0)
+ g_source_remove (priv->timeout_id);
+ priv->timeout_id = g_timeout_add (VALIDATION_TIMEOUT, (GSourceFunc)validate, page);
+}
+
+static void
+confirm_changed (GtkWidget *w,
+ GParamSpec *pspec,
+ GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ clear_password_validation_error (w);
+
+ priv->valid_confirm = FALSE;
+ update_page_validation (page);
+
+ if (priv->timeout_id != 0)
+ g_source_remove (priv->timeout_id);
+ priv->timeout_id = g_timeout_add (VALIDATION_TIMEOUT, (GSourceFunc)validate, page);
+}
+
+static void
+username_changed (GObject *obj, GParamSpec *pspec, GisPasswordPage *page)
+{
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+ priv->username = gis_driver_get_username (GIS_DRIVER (obj));
+
+ if (priv->username)
+ gtk_widget_show (GTK_WIDGET (page));
+ else
+ gtk_widget_hide (GTK_WIDGET (page));
+
+ clear_password_validation_error (priv->password_entry);
+ clear_password_validation_error (priv->confirm_entry);
+
+ validate (page);
+}
+
+static void
+full_name_or_avatar_changed (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (user_data);
+
+ update_header (page);
+}
+
+static void
+confirm (GisPasswordPage *page)
+{
+ if (page_validate (page))
+ gis_assistant_next_page (gis_driver_get_assistant (GIS_PAGE (page)->driver));
+}
+
+static void
+track_focus_out (GisPasswordPage *page,
+ GtkWidget *widget)
+{
+ GtkEventController *focus_controller;
+
+ focus_controller = gtk_event_controller_focus_new ();
+ gtk_widget_add_controller (widget, focus_controller);
+
+ g_signal_connect_swapped (focus_controller, "leave", G_CALLBACK (on_focusout), page);
+}
+
+static void
+gis_password_page_constructed (GObject *object)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (object);
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ G_OBJECT_CLASS (gis_password_page_parent_class)->constructed (object);
+
+ g_signal_connect (priv->password_entry, "notify::text",
+ G_CALLBACK (password_changed), page);
+ g_signal_connect_swapped (priv->password_entry, "activate",
+ G_CALLBACK (confirm), page);
+ track_focus_out (page, priv->password_entry);
+
+ g_signal_connect (priv->confirm_entry, "notify::text",
+ G_CALLBACK (confirm_changed), page);
+ g_signal_connect_swapped (priv->confirm_entry, "activate",
+ G_CALLBACK (confirm), page);
+ track_focus_out (page, priv->confirm_entry);
+
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::username",
+ G_CALLBACK (username_changed), page);
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::full-name",
+ G_CALLBACK (full_name_or_avatar_changed), page);
+ g_signal_connect (GIS_PAGE (page)->driver, "notify::avatar",
+ G_CALLBACK (full_name_or_avatar_changed), page);
+
+ validate (page);
+ update_header (page);
+
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_password_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (object);
+ GisPasswordPagePrivate *priv = gis_password_page_get_instance_private (page);
+
+ switch ((GisPasswordPageProperty) prop_id)
+ {
+ case PROP_PARENT_MODE:
+ g_value_set_boolean (value, priv->parent_mode);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_password_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GisPasswordPage *page = GIS_PASSWORD_PAGE (object);
+
+ switch ((GisPasswordPageProperty) prop_id)
+ {
+ case PROP_PARENT_MODE:
+ set_parent_mode (page, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_password_page_dispose (GObject *object)
+{
+ if (GIS_PAGE (object)->driver) {
+ g_signal_handlers_disconnect_by_func (GIS_PAGE (object)->driver,
+ username_changed, object);
+ g_signal_handlers_disconnect_by_func (GIS_PAGE (object)->driver,
+ full_name_or_avatar_changed, object);
+ }
+
+ G_OBJECT_CLASS (gis_password_page_parent_class)->dispose (object);
+}
+
+static void
+gis_password_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Password"));
+}
+
+static void
+gis_password_page_class_init (GisPasswordPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-password-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, password_entry);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, confirm_entry);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, password_strength);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, password_explanation);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, confirm_explanation);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPasswordPage, header);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_password_page_locale_changed;
+ page_class->save_data = gis_password_page_save_data;
+ page_class->shown = gis_password_page_shown;
+
+ object_class->constructed = gis_password_page_constructed;
+ object_class->get_property = gis_password_page_get_property;
+ object_class->set_property = gis_password_page_set_property;
+ object_class->dispose = gis_password_page_dispose;
+
+ /**
+ * GisPasswordPage:parent-mode:
+ *
+ * If %FALSE (the default), this page will collect a password for the main
+ * user account. If %TRUE, it will collect a password for controlling access
+ * to parental controls — this will affect where the password is stored, and
+ * the appearance of the page.
+ *
+ * Since: 3.36
+ */
+ obj_props[PROP_PARENT_MODE] =
+ g_param_spec_boolean ("parent-mode", "Parent Mode",
+ "Whether to collect a password for the main user account or a parent account.",
+ FALSE,
+ G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+}
+
+static void
+gis_password_page_init (GisPasswordPage *page)
+{
+ GtkCssProvider *provider;
+
+ g_resources_register (password_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider, "/org/gnome/initial-setup/gis-password-page.css");
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ g_object_unref (provider);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_password_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_PASSWORD_PAGE,
+ "driver", driver,
+ NULL);
+}
+
+GisPage *
+gis_prepare_parent_password_page (GisDriver *driver)
+{
+ /* Skip prompting for the parent password if parental controls aren’t enabled. */
+ if (!gis_driver_get_parental_controls_enabled (driver))
+ return NULL;
+
+ return g_object_new (GIS_TYPE_PASSWORD_PAGE,
+ "driver", driver,
+ "parent-mode", TRUE,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/password/gis-password-page.css b/gnome-initial-setup/pages/password/gis-password-page.css
new file mode 100644
index 0000000..e9b5f54
--- /dev/null
+++ b/gnome-initial-setup/pages/password/gis-password-page.css
@@ -0,0 +1,16 @@
+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;
+}
diff --git a/gnome-initial-setup/pages/password/gis-password-page.h b/gnome-initial-setup/pages/password/gis-password-page.h
new file mode 100644
index 0000000..2a4d1c6
--- /dev/null
+++ b/gnome-initial-setup/pages/password/gis-password-page.h
@@ -0,0 +1,58 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_PASSWORD_PAGE_H__
+#define __GIS_PASSWORD_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_PASSWORD_PAGE (gis_password_page_get_type ())
+#define GIS_PASSWORD_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_PASSWORD_PAGE, GisPasswordPage))
+#define GIS_PASSWORD_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_PASSWORD_PAGE, GisPasswordPageClass))
+#define GIS_IS_PASSWORD_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_PASSWORD_PAGE))
+#define GIS_IS_PASSWORD_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_PASSWORD_PAGE))
+#define GIS_PASSWORD_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_PASSWORD_PAGE, GisPasswordPageClass))
+
+typedef struct _GisPasswordPage GisPasswordPage;
+typedef struct _GisPasswordPageClass GisPasswordPageClass;
+
+struct _GisPasswordPage
+{
+ GisPage parent;
+};
+
+struct _GisPasswordPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_password_page_get_type (void);
+
+GisPage *gis_prepare_password_page (GisDriver *driver);
+GisPage *gis_prepare_parent_password_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_PASSWORD_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/password/gis-password-page.ui b/gnome-initial-setup/pages/password/gis-password-page.ui
new file mode 100644
index 0000000..7345e00
--- /dev/null
+++ b/gnome-initial-setup/pages/password/gis-password-page.ui
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisPasswordPage" parent="GisPage">
+
+ <child>
+ <object class="AdwPreferencesPage">
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <!-- title and subtitle are set in code, so are not set here -->
+ <property name="icon_name">dialog-password-symbolic</property>
+ <property name="show_icon" bind-source="GisPasswordPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkGrid" id="secrets">
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <property name="margin_top">40</property>
+ <child>
+ <object class="GtkLabel" id="password_label">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Password</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">password_entry</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPasswordEntry" id="password_entry">
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="confirm_label">
+ <property name="halign">end</property>
+ <property name="xalign">1</property>
+ <property name="label" translatable="yes">_Confirm</property>
+ <property name="use_underline">True</property>
+ <property name="mnemonic_widget">confirm_entry</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkPasswordEntry" id="confirm_entry">
+ <layout>
+ <property name="column">1</property>
+ <property name="row">3</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLevelBar" id="password_strength">
+ <property name="halign">fill</property>
+ <property name="valign">center</property>
+ <property name="max-value">5</property>
+ <property name="mode">discrete</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ </layout>
+ <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>
+ </child>
+ <child>
+ <object class="GtkLabel" id="password_explanation">
+ <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>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="confirm_explanation">
+ <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>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">4</property>
+ </layout>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.8"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/password/meson.build b/gnome-initial-setup/pages/password/meson.build
new file mode 100644
index 0000000..bba1ab9
--- /dev/null
+++ b/gnome-initial-setup/pages/password/meson.build
@@ -0,0 +1,13 @@
+sources += gnome.compile_resources(
+ 'password-resources',
+ files('password.gresource.xml'),
+ c_name: 'password'
+)
+
+sources += files(
+ 'gis-password-page.c',
+ 'gis-password-page.h',
+ join_paths(account_sources_dir, 'um-utils.h'),
+ 'pw-utils.c',
+ 'pw-utils.h',
+)
diff --git a/gnome-initial-setup/pages/password/password.gresource.xml b/gnome-initial-setup/pages/password/password.gresource.xml
new file mode 100644
index 0000000..22cad98
--- /dev/null
+++ b/gnome-initial-setup/pages/password/password.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-password-page.ui">gis-password-page.ui</file>
+ <file alias="gis-password-page.css">gis-password-page.css</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/password/pw-utils.c b/gnome-initial-setup/pages/password/pw-utils.c
new file mode 100644
index 0000000..0913b07
--- /dev/null
+++ b/gnome-initial-setup/pages/password/pw-utils.c
@@ -0,0 +1,164 @@
+/* -*- 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;
+ settings = pwquality_default_settings ();
+ if (pwquality_read_config (settings, NULL, (gpointer)&err) < 0) {
+ g_error ("failed to read pwquality configuration: %s", err);
+ }
+ }
+
+ return settings;
+}
+
+gint
+pw_min_length (void)
+{
+ gint value = 0;
+
+ if (pwquality_get_int_value (get_pwq (), PWQ_SETTING_MIN_LENGTH, &value) < 0) {
+ g_error ("Failed to read pwquality setting\n" );
+ }
+
+ return value;
+}
+
+gchar *
+pw_generate (void)
+{
+ gchar *res;
+ gint rv;
+
+ rv = pwquality_generate (get_pwq (), 0, &res);
+
+ if (rv < 0) {
+ g_error ("Password generation failed: %s",
+ 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", "This password is very similar to your last one. Try changing some letters and numbers.");
+ case PWQ_ERROR_TOO_SIMILAR:
+ return C_("Password hint", "This password is very similar to your last one. Try changing the password a bit more.");
+ case PWQ_ERROR_USER_CHECK:
+ return C_("Password hint", "This is a weak password. A password without your user name would be stronger.");
+ case PWQ_ERROR_GECOS_CHECK:
+ return C_("Password hint", "This is a weak password. Try to avoid using your name in the password.");
+ case PWQ_ERROR_BAD_WORDS:
+ return C_("Password hint", "This is a weak password. Try to avoid some of the words included in the password.");
+ case PWQ_ERROR_ROTATED:
+ return C_("Password hint", "This password is very similar to your last one. Try changing the password a bit more.");
+ case PWQ_ERROR_CRACKLIB_CHECK:
+ return C_("Password hint", "This is a weak password. Try to avoid common words.");
+ case PWQ_ERROR_PALINDROME:
+ return C_("Password hint", "This is a weak password. Try to avoid reordering existing words.");
+ case PWQ_ERROR_MIN_DIGITS:
+ return C_("Password hint", "This is a weak password. Try to use more numbers.");
+ case PWQ_ERROR_MIN_UPPERS:
+ return C_("Password hint", "This is a weak password. Try to use more uppercase letters.");
+ case PWQ_ERROR_MIN_LOWERS:
+ return C_("Password hint", "This is a weak password. Try to use more lowercase letters.");
+ case PWQ_ERROR_MIN_OTHERS:
+ return C_("Password hint", "This is a weak password. Try to use more special characters, like punctuation.");
+ case PWQ_ERROR_MIN_CLASSES:
+ return C_("Password hint", "This is a weak password. Try to use a mixture of letters, numbers and punctuation.");
+ case PWQ_ERROR_MAX_CONSECUTIVE:
+ return C_("Password hint", "This is a weak password. Try to avoid repeating the same character.");
+ case PWQ_ERROR_MAX_CLASS_REPEAT:
+ return C_("Password hint", "This is a weak password. 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", "This is a weak password. Try to avoid sequences like 1234 or abcd.");
+ case PWQ_ERROR_MIN_LENGTH:
+ return C_("Password hint", "This is a weak password. 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/gnome-initial-setup/pages/password/pw-utils.h b/gnome-initial-setup/pages/password/pw-utils.h
new file mode 100644
index 0000000..2a3cc42
--- /dev/null
+++ b/gnome-initial-setup/pages/password/pw-utils.h
@@ -0,0 +1,29 @@
+/* -*- 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 <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/gnome-initial-setup/pages/privacy/gis-privacy-page.c b/gnome-initial-setup/pages/privacy/gis-privacy-page.c
new file mode 100644
index 0000000..3632142
--- /dev/null
+++ b/gnome-initial-setup/pages/privacy/gis-privacy-page.c
@@ -0,0 +1,272 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015 Red Hat
+ *
+ * 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>
+ */
+
+/* Privacy page {{{1 */
+
+#define PAGE_ID "privacy"
+
+#include "config.h"
+#include "privacy-resources.h"
+#include "gis-privacy-page.h"
+
+#ifdef HAVE_WEBKITGTK_6_0
+#include <webkit/webkit.h>
+#else
+#include <webkit2/webkit2.h>
+#endif
+
+#include <locale.h>
+#include <gtk/gtk.h>
+
+#include "gis-page-header.h"
+
+struct _GisPrivacyPagePrivate
+{
+ GtkWidget *location_switch;
+ GtkWidget *reporting_group;
+ GtkWidget *reporting_row;
+ GtkWidget *reporting_switch;
+ GSettings *location_settings;
+ GSettings *privacy_settings;
+ guint abrt_watch_id;
+};
+typedef struct _GisPrivacyPagePrivate GisPrivacyPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisPrivacyPage, gis_privacy_page, GIS_TYPE_PAGE);
+
+static void
+update_os_data (GisPrivacyPage *page)
+{
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+ g_autofree char *name = g_get_os_info (G_OS_INFO_KEY_NAME);
+ g_autofree char *privacy_policy = g_get_os_info (G_OS_INFO_KEY_PRIVACY_POLICY_URL);
+ g_autofree char *subtitle = NULL;
+
+ if (!name)
+ name = g_strdup ("GNOME");
+
+ if (privacy_policy)
+ {
+ /* Translators: the first parameter here is the name of a distribution,
+ * like "Fedora" or "Ubuntu". It falls back to "GNOME" if we can't
+ * detect any distribution.
+ */
+ subtitle = g_strdup_printf (_("Sends technical reports that have personal information automatically "
+ "removed. Data is collected by %1$s (<a href='%2$s'>privacy policy</a>)."),
+ name, privacy_policy);
+ }
+ else
+ {
+ /* Translators: the parameter here is the name of a distribution,
+ * like "Fedora" or "Ubuntu". It falls back to "GNOME" if we can't
+ * detect any distribution.
+ */
+ subtitle = g_strdup_printf (_("Sends technical reports that have personal information automatically "
+ "removed. Data is collected by %s."), name);
+ }
+ adw_action_row_set_subtitle (ADW_ACTION_ROW (priv->reporting_row), subtitle);
+}
+
+static void
+abrt_appeared_cb (GDBusConnection *connection,
+ const gchar *name,
+ const gchar *name_owner,
+ gpointer user_data)
+{
+ GisPrivacyPage *page = user_data;
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+
+ gtk_widget_show (priv->reporting_group);
+}
+
+static void
+abrt_vanished_cb (GDBusConnection *connection,
+ const gchar *name,
+ gpointer user_data)
+{
+ GisPrivacyPage *page = user_data;
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+
+ gtk_widget_hide (priv->reporting_group);
+}
+
+static void
+gis_privacy_page_constructed (GObject *object)
+{
+ GisPrivacyPage *page = GIS_PRIVACY_PAGE (object);
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+
+ G_OBJECT_CLASS (gis_privacy_page_parent_class)->constructed (object);
+
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+
+ priv->location_settings = g_settings_new ("org.gnome.system.location");
+ priv->privacy_settings = g_settings_new ("org.gnome.desktop.privacy");
+
+ gtk_switch_set_active (GTK_SWITCH (priv->location_switch), TRUE);
+ gtk_switch_set_active (GTK_SWITCH (priv->reporting_switch), TRUE);
+
+ update_os_data (page);
+
+ priv->abrt_watch_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM,
+ "org.freedesktop.problems.daemon",
+ G_BUS_NAME_WATCHER_FLAGS_NONE,
+ abrt_appeared_cb,
+ abrt_vanished_cb,
+ page,
+ NULL);
+}
+
+static void
+gis_privacy_page_dispose (GObject *object)
+{
+ GisPrivacyPage *page = GIS_PRIVACY_PAGE (object);
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+
+ g_clear_object (&priv->location_settings);
+ g_clear_object (&priv->privacy_settings);
+
+ if (priv->abrt_watch_id > 0)
+ {
+ g_bus_unwatch_name (priv->abrt_watch_id);
+ priv->abrt_watch_id = 0;
+ }
+
+ G_OBJECT_CLASS (gis_privacy_page_parent_class)->dispose (object);
+}
+
+static gboolean
+gis_privacy_page_apply (GisPage *gis_page,
+ GCancellable *cancellable)
+{
+ GisPrivacyPage *page = GIS_PRIVACY_PAGE (gis_page);
+ GisPrivacyPagePrivate *priv = gis_privacy_page_get_instance_private (page);
+ gboolean active;
+
+ active = gtk_switch_get_active (GTK_SWITCH (priv->location_switch));
+ g_settings_set_boolean (priv->location_settings, "enabled", active);
+
+ active = gtk_switch_get_active (GTK_SWITCH (priv->reporting_switch));
+ g_settings_set_boolean (priv->privacy_settings, "report-technical-problems", active);
+
+ return FALSE;
+}
+
+static void
+notify_progress_cb (GObject *object, GParamSpec *pspec, gpointer user_data)
+{
+ GtkWidget *progress_bar = user_data;
+ WebKitWebView *web_view = WEBKIT_WEB_VIEW (object);
+ gdouble progress;
+
+ progress = webkit_web_view_get_estimated_load_progress (web_view);
+
+ if (progress == 1.0)
+ gtk_widget_hide (progress_bar);
+ else
+ gtk_widget_show (progress_bar);
+
+ gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (progress_bar), progress);
+}
+
+static gboolean
+activate_link (GtkLabel *label,
+ const gchar *uri,
+ GisPrivacyPage *page)
+{
+ GtkWidget *dialog;
+ GtkWidget *overlay;
+ GtkWidget *view;
+ GtkWidget *progress_bar;
+
+ dialog = gtk_dialog_new_with_buttons (_("Privacy Policy"),
+ GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (page))),
+ GTK_DIALOG_MODAL
+ | GTK_DIALOG_DESTROY_WITH_PARENT
+ | GTK_DIALOG_USE_HEADER_BAR,
+ NULL, NULL);
+
+ overlay = gtk_overlay_new ();
+ gtk_box_append (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), overlay);
+
+ progress_bar = gtk_progress_bar_new ();
+ gtk_widget_add_css_class (progress_bar, "osd");
+ gtk_widget_set_halign (progress_bar, GTK_ALIGN_FILL);
+ gtk_widget_set_valign (progress_bar, GTK_ALIGN_START);
+ gtk_overlay_add_overlay (GTK_OVERLAY (overlay), progress_bar);
+
+ view = webkit_web_view_new ();
+ gtk_widget_set_size_request (view, 600, 500);
+ gtk_widget_set_hexpand (view, TRUE);
+ gtk_widget_set_vexpand (view, TRUE);
+ g_signal_connect (view, "notify::estimated-load-progress",
+ G_CALLBACK (notify_progress_cb), progress_bar);
+ gtk_overlay_set_child (GTK_OVERLAY (overlay), view);
+
+
+ gtk_window_present (GTK_WINDOW (dialog));
+
+ webkit_web_view_load_uri (WEBKIT_WEB_VIEW (view), uri);
+
+ return TRUE;
+}
+
+static void
+gis_privacy_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Privacy"));
+}
+
+static void
+gis_privacy_page_class_init (GisPrivacyPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-privacy-page.ui");
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPrivacyPage, location_switch);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPrivacyPage, reporting_group);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPrivacyPage, reporting_row);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisPrivacyPage, reporting_switch);
+ gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), activate_link);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_privacy_page_locale_changed;
+ page_class->apply = gis_privacy_page_apply;
+ object_class->constructed = gis_privacy_page_constructed;
+ object_class->dispose = gis_privacy_page_dispose;
+}
+
+static void
+gis_privacy_page_init (GisPrivacyPage *page)
+{
+ g_resources_register (privacy_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_privacy_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_PRIVACY_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/privacy/gis-privacy-page.h b/gnome-initial-setup/pages/privacy/gis-privacy-page.h
new file mode 100644
index 0000000..9596b36
--- /dev/null
+++ b/gnome-initial-setup/pages/privacy/gis-privacy-page.h
@@ -0,0 +1,57 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2015 Red Hat
+ *
+ * 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>
+ */
+
+#ifndef __GIS_PRIVACY_PAGE_H__
+#define __GIS_PRIVACY_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_PRIVACY_PAGE (gis_privacy_page_get_type ())
+#define GIS_PRIVACY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_PRIVACY_PAGE, GisPrivacyPage))
+#define GIS_PRIVACY_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_PRIVACY_PAGE, GisPrivacyPageClass))
+#define GIS_IS_PRIVACY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_PRIVACY_PAGE))
+#define GIS_IS_PRIVACY_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_PRIVACY_PAGE))
+#define GIS_PRIVACY_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_PRIVACY_PAGE, GisPrivacyPageClass))
+
+typedef struct _GisPrivacyPage GisPrivacyPage;
+typedef struct _GisPrivacyPageClass GisPrivacyPageClass;
+
+struct _GisPrivacyPage
+{
+ GisPage parent;
+};
+
+struct _GisPrivacyPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_privacy_page_get_type (void);
+
+GisPage *gis_prepare_privacy_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_PRIVACY_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/privacy/gis-privacy-page.ui b/gnome-initial-setup/pages/privacy/gis-privacy-page.ui
new file mode 100644
index 0000000..84ea8e3
--- /dev/null
+++ b/gnome-initial-setup/pages/privacy/gis-privacy-page.ui
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="GisPrivacyPage" parent="GisPage">
+
+ <child>
+ <object class="AdwPreferencesPage">
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Privacy</property>
+ <property name="icon_name">preferences-system-privacy-symbolic</property>
+ <property name="show_icon" bind-source="GisPrivacyPage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="AdwActionRow">
+ <property name="use-markup">True</property>
+ <property name="title" translatable="yes">Location Services</property>
+ <property name="subtitle" translatable="yes">Allows applications to determine your geographical location. Uses the Mozilla Location Service (&lt;a href='https://location.services.mozilla.com/privacy'&gt;privacy policy&lt;/a&gt;).</property>
+ <property name="activatable-widget">location_switch</property>
+ <child>
+ <object class="GtkSwitch" id="location_switch">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup" id="reporting_group">
+ <child>
+ <object class="AdwActionRow" id="reporting_row">
+ <property name="title" translatable="yes">Automatic Problem Reporting</property>
+ <property name="activatable-widget">reporting_switch</property>
+ <child>
+ <object class="GtkSwitch" id="reporting_switch">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel" id="footer_label">
+ <property name="valign">end</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">Privacy controls can be changed at any time from the Settings application.</property>
+ <property name="xalign">0.5</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="margin_bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/privacy/meson.build b/gnome-initial-setup/pages/privacy/meson.build
new file mode 100644
index 0000000..560831c
--- /dev/null
+++ b/gnome-initial-setup/pages/privacy/meson.build
@@ -0,0 +1,10 @@
+sources += gnome.compile_resources(
+ 'privacy-resources',
+ files('privacy.gresource.xml'),
+ c_name: 'privacy'
+)
+
+sources += files(
+ 'gis-privacy-page.c',
+ 'gis-privacy-page.h'
+)
diff --git a/gnome-initial-setup/pages/privacy/privacy.gresource.xml b/gnome-initial-setup/pages/privacy/privacy.gresource.xml
new file mode 100644
index 0000000..be75b34
--- /dev/null
+++ b/gnome-initial-setup/pages/privacy/privacy.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-privacy-page.ui">gis-privacy-page.ui</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/software/gis-software-page.c b/gnome-initial-setup/pages/software/gis-software-page.c
new file mode 100644
index 0000000..b68d143
--- /dev/null
+++ b/gnome-initial-setup/pages/software/gis-software-page.c
@@ -0,0 +1,179 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2016, 2021 Red Hat
+ *
+ * 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>
+ * Kalev Lember <klember@redhat.com>
+ * Michael Catanzaro <mcatanzaro@redhat.com>
+ */
+
+/* SOFTWARE pages {{{1 */
+
+#define PAGE_ID "software"
+
+#include "config.h"
+#include "software-resources.h"
+#include "gis-software-page.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "gis-page-header.h"
+
+struct _GisSoftwarePagePrivate
+{
+ GtkWidget *header;
+ GtkWidget *enable_disable_button;
+ gboolean enabled;
+};
+
+typedef struct _GisSoftwarePagePrivate GisSoftwarePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSoftwarePage, gis_software_page, GIS_TYPE_PAGE);
+
+static void
+gis_software_page_constructed (GObject *object)
+{
+ GisSoftwarePage *page = GIS_SOFTWARE_PAGE (object);
+
+ G_OBJECT_CLASS (gis_software_page_parent_class)->constructed (object);
+
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+}
+
+/* Distro-specific stuff is isolated here so that the rest of this page can be
+ * used by other distros. Feel free to add your distro here.
+ */
+static char *
+find_fedora_third_party (void)
+{
+ return g_find_program_in_path ("fedora-third-party");
+}
+
+static gboolean
+should_show_software_page (void)
+{
+ g_autofree char *has_fedora_third_party = find_fedora_third_party ();
+ return has_fedora_third_party != NULL;
+}
+
+static gboolean
+gis_software_page_apply (GisPage *gis_page,
+ GCancellable *cancellable)
+{
+ GisSoftwarePage *page = GIS_SOFTWARE_PAGE (gis_page);
+ GisSoftwarePagePrivate *priv = gis_software_page_get_instance_private (page);
+ g_autofree char *program = NULL;
+ g_autoptr (GError) error = NULL;
+
+ program = find_fedora_third_party ();
+
+ if (program)
+ {
+ const char *arg1;
+
+ if (priv->enabled)
+ arg1 = "enable";
+ else
+ arg1 = "disable";
+
+ gis_pkexec (program, arg1, "root", &error);
+ if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("%s failed: %s", program, error->message);
+ }
+
+ return FALSE;
+}
+
+static void
+gis_software_page_locale_changed (GisPage *gis_page)
+{
+ GisSoftwarePage *page = GIS_SOFTWARE_PAGE (gis_page);
+ GisSoftwarePagePrivate *priv = gis_software_page_get_instance_private (page);
+
+ gis_page_set_title (GIS_PAGE (page), _("Third-Party Repositories"));
+ g_object_set (priv->header, "subtitle", _("Third-party repositories provide access to additional software from selected external sources, including popular apps and drivers that are important for some devices. Some proprietary software is included."), NULL);
+}
+
+static void
+enabled_state_changed (GisSoftwarePage *page)
+{
+ GisSoftwarePagePrivate *priv = gis_software_page_get_instance_private (page);
+
+ if (priv->enabled)
+ {
+ gtk_button_set_label (GTK_BUTTON (priv->enable_disable_button), _("_Disable Third-Party Repositories"));
+ gtk_widget_remove_css_class (priv->enable_disable_button, "suggested-action");
+ }
+ else
+ {
+ gtk_button_set_label (GTK_BUTTON (priv->enable_disable_button), _("_Enable Third-Party Repositories"));
+ gtk_widget_add_css_class (priv->enable_disable_button, "suggested-action");
+ }
+}
+
+static gboolean
+enable_disable_button_clicked_cb (GtkButton *button,
+ GisSoftwarePage *page)
+{
+ GisSoftwarePagePrivate *priv = gis_software_page_get_instance_private (page);
+ priv->enabled = !priv->enabled;
+ enabled_state_changed (page);
+ return GDK_EVENT_STOP;
+}
+
+static void
+gis_software_page_class_init (GisSoftwarePageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-software-page.ui");
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisSoftwarePage, header);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisSoftwarePage, enable_disable_button);
+ gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), enable_disable_button_clicked_cb);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_software_page_locale_changed;
+ page_class->apply = gis_software_page_apply;
+ object_class->constructed = gis_software_page_constructed;
+}
+
+static void
+gis_software_page_init (GisSoftwarePage *page)
+{
+ g_resources_register (software_get_resource ());
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+ enabled_state_changed (page);
+}
+
+GisPage *
+gis_prepare_software_page (GisDriver *driver)
+{
+ GisPage *page = NULL;
+ if (should_show_software_page ())
+ {
+ page = g_object_new (GIS_TYPE_SOFTWARE_PAGE,
+ "driver", driver,
+ NULL);
+ }
+
+ return page;
+}
diff --git a/gnome-initial-setup/pages/software/gis-software-page.h b/gnome-initial-setup/pages/software/gis-software-page.h
new file mode 100644
index 0000000..8d15245
--- /dev/null
+++ b/gnome-initial-setup/pages/software/gis-software-page.h
@@ -0,0 +1,58 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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>
+ */
+
+#ifndef __GIS_SOFTWARE_PAGE_H__
+#define __GIS_SOFTWARE_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_SOFTWARE_PAGE (gis_software_page_get_type ())
+#define GIS_SOFTWARE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_SOFTWARE_PAGE, GisSoftwarePage))
+#define GIS_SOFTWARE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_SOFTWARE_PAGE, GisSoftwarePageClass))
+#define GIS_IS_SOFTWARE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_SOFTWARE_PAGE))
+#define GIS_IS_SOFTWARE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_SOFTWARE_PAGE))
+#define GIS_SOFTWARE_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_SOFTWARE_PAGE, GisSoftwarePageClass))
+
+typedef struct _GisSoftwarePage GisSoftwarePage;
+typedef struct _GisSoftwarePageClass GisSoftwarePageClass;
+
+struct _GisSoftwarePage
+{
+ GisPage parent;
+};
+
+struct _GisSoftwarePageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_software_page_get_type (void);
+
+GisPage *gis_prepare_software_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_SOFTWARE_PAGE_H__ */
+
diff --git a/gnome-initial-setup/pages/software/gis-software-page.ui b/gnome-initial-setup/pages/software/gis-software-page.ui
new file mode 100644
index 0000000..838c24d
--- /dev/null
+++ b/gnome-initial-setup/pages/software/gis-software-page.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisSoftwarePage" parent="GisPage">
+ <child>
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Third-Party Repositories</property>
+ <property name="icon_name">gis-software-symbolic</property>
+ <property name="show_icon" bind-source="GisSoftwarePage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkButton" id="enable_disable_button">
+ <property name="halign">center</property>
+ <property name="margin-top">36</property>
+ <property name="margin-bottom">36</property>
+ <property name="margin-start">36</property>
+ <property name="margin-end">36</property>
+ <property name="use-underline">True</property>
+ <property name="visible">True</property>
+ <signal name="clicked" handler="enable_disable_button_clicked_cb"/>
+ <style>
+ <class name="suggested-action" />
+ <class name="pill" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/software/gis-software-symbolic.svg b/gnome-initial-setup/pages/software/gis-software-symbolic.svg
new file mode 100644
index 0000000..f344c64
--- /dev/null
+++ b/gnome-initial-setup/pages/software/gis-software-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#474747"><path d="M.002 12c.004-.224.112-.53.304-.704l2.161-2.117c.233-.243.787-.292 1.011-.157.081.002 1.136.632 1.289.866.616.946-.344 1.827-1.174 1.547l-.35-.212-1.602 1.572C1.004 13.32-.051 12.887 0 12zM11.62.118c.389-.101.79-.11 1.17-.08l-1.36 2.305a.975.975 0 00.354 1.348l.874.508a.99.99 0 001.357-.353l1.36-2.306c.212.314.4.666.502 1.053.486 1.853-.632 3.748-2.497 4.234-.39.101-.79.11-1.17.08l-.803 1.116L7.997 8s1.52-2.577 1.628-2.595a3.483 3.483 0 01-.502-1.053C8.636 2.5 9.754.604 11.619.118z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" color="#000" font-weight="400" font-family="Sans" overflow="visible"/><path d="M3.599.719a3.5 3.5 0 00-.655.219c-.017.624.086 1.441-.157 1.656-.238.21-1.036.034-1.653-.031a3.63 3.63 0 00-.312.75c.477.395 1.138.839 1.154 1.156.016.32-.625.805-1.06 1.25.102.258.249.493.405.719.607-.127 1.364-.374 1.623-.188.262.19.264 1.005.343 1.625.249.075.512.105.78.125.28-.555.566-1.32.874-1.406.315-.089.962.458 1.497.781.219-.152.413-.337.593-.531-.253-.573-.72-1.292-.593-1.594.128-.302.974-.47 1.56-.687.005-.074.032-.145.032-.22 0-.19-.035-.378-.063-.562-.605-.16-1.465-.241-1.622-.531-.157-.288.241-1.061.437-1.656a3.682 3.682 0 00-.656-.469c-.5.375-1.082.994-1.404.938-.316-.056-.662-.82-.998-1.344-.04.007-.085-.008-.125 0zm.624 1.875a1.686 1.686 0 11-1.685 1.688c0-.933.754-1.688 1.685-1.688z" style="marker:none" color="#000" overflow="visible"/><path d="M2.997 9h9.985l.008 6.063c0 .492-.472.937-.994.937H4.004a.996.996 0 01-.999-1z" style="marker:none" color="#bebebe" overflow="visible"/><path d="M15.96 11.977c-.003-.224-.112-.53-.303-.704l-2.162-2.117c-.232-.243-.786-.292-1.01-.156-.081.002-1.136.63-1.289.865-.616.946.344 1.827 1.174 1.547l.35-.212 1.602 1.572c.637.526 1.691.092 1.639-.795z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" color="#000" font-weight="400" font-family="Sans" overflow="visible"/></g></svg> \ No newline at end of file
diff --git a/gnome-initial-setup/pages/software/meson.build b/gnome-initial-setup/pages/software/meson.build
new file mode 100644
index 0000000..c464ec7
--- /dev/null
+++ b/gnome-initial-setup/pages/software/meson.build
@@ -0,0 +1,10 @@
+sources += gnome.compile_resources(
+ 'software-resources',
+ files('software.gresource.xml'),
+ c_name: 'software'
+)
+
+sources += files(
+ 'gis-software-page.c',
+ 'gis-software-page.h'
+)
diff --git a/gnome-initial-setup/pages/software/software.gresource.xml b/gnome-initial-setup/pages/software/software.gresource.xml
new file mode 100644
index 0000000..caee9bd
--- /dev/null
+++ b/gnome-initial-setup/pages/software/software.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-software-page.ui">gis-software-page.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/InitialSetup/icons">
+ <file compressed="true" alias="scalable/actions/gis-software-symbolic.svg" preprocess="xml-stripblanks">gis-software-symbolic.svg</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/summary/gis-summary-page.c b/gnome-initial-setup/pages/summary/gis-summary-page.c
new file mode 100644
index 0000000..b947455
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/gis-summary-page.c
@@ -0,0 +1,298 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Summary page {{{1 */
+
+#define PAGE_ID "summary"
+
+#include "config.h"
+#include "summary-resources.h"
+#include "gis-summary-page.h"
+
+#include <glib/gstdio.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <act/act-user-manager.h>
+
+#define SERVICE_NAME "gdm-password"
+
+struct _GisSummaryPagePrivate {
+ GtkWidget *start_button;
+ AdwStatusPage *status_page;
+
+ ActUser *user_account;
+ const gchar *user_password;
+};
+typedef struct _GisSummaryPagePrivate GisSummaryPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSummaryPage, gis_summary_page, GIS_TYPE_PAGE);
+
+static void
+request_info_query (GisSummaryPage *page,
+ GdmUserVerifier *user_verifier,
+ const char *question,
+ gboolean is_secret)
+{
+ /* TODO: pop up modal dialog */
+ g_debug ("user verifier asks%s question: %s",
+ is_secret ? " secret" : "",
+ question);
+}
+
+static void
+on_info (GdmUserVerifier *user_verifier,
+ const char *service_name,
+ const char *info,
+ GisSummaryPage *page)
+{
+ g_debug ("PAM module info: %s", info);
+}
+
+static void
+on_problem (GdmUserVerifier *user_verifier,
+ const char *service_name,
+ const char *problem,
+ GisSummaryPage *page)
+{
+ g_warning ("PAM module error: %s", problem);
+}
+
+static void
+on_info_query (GdmUserVerifier *user_verifier,
+ const char *service_name,
+ const char *question,
+ GisSummaryPage *page)
+{
+ request_info_query (page, user_verifier, question, FALSE);
+}
+
+static void
+on_secret_info_query (GdmUserVerifier *user_verifier,
+ const char *service_name,
+ const char *question,
+ GisSummaryPage *page)
+{
+ GisSummaryPagePrivate *priv = gis_summary_page_get_instance_private (page);
+ gboolean should_send_password = priv->user_password != NULL;
+
+ g_debug ("PAM module secret info query: %s", question);
+ if (should_send_password) {
+ g_debug ("sending password\n");
+ gdm_user_verifier_call_answer_query (user_verifier,
+ service_name,
+ priv->user_password,
+ NULL, NULL, NULL);
+ priv->user_password = NULL;
+ } else {
+ request_info_query (page, user_verifier, question, TRUE);
+ }
+}
+
+static void
+on_session_opened (GdmGreeter *greeter,
+ const char *service_name,
+ GisSummaryPage *page)
+{
+ gdm_greeter_call_start_session_when_ready_sync (greeter, service_name,
+ TRUE, NULL, NULL);
+}
+
+static void
+add_uid_file (uid_t uid)
+{
+ gchar *gis_uid_path;
+ gchar *uid_str;
+ g_autoptr(GError) error = NULL;
+
+ gis_uid_path = g_build_filename (g_get_home_dir (),
+ "gnome-initial-setup-uid",
+ NULL);
+ uid_str = g_strdup_printf ("%u", uid);
+
+ if (!g_file_set_contents (gis_uid_path, uid_str, -1, &error))
+ g_warning ("Unable to create %s: %s", gis_uid_path, error->message);
+
+ g_free (uid_str);
+ g_free (gis_uid_path);
+}
+
+static void
+log_user_in (GisSummaryPage *page)
+{
+ GisSummaryPagePrivate *priv = gis_summary_page_get_instance_private (page);
+ g_autoptr(GError) error = NULL;
+ GdmGreeter *greeter = NULL;
+ GdmUserVerifier *user_verifier = NULL;
+
+ if (!gis_driver_get_gdm_objects (GIS_PAGE (page)->driver,
+ &greeter, &user_verifier)) {
+ g_warning ("No GDM connection; not initiating login");
+ return;
+ }
+
+ g_signal_connect (user_verifier, "info",
+ G_CALLBACK (on_info), page);
+ g_signal_connect (user_verifier, "problem",
+ G_CALLBACK (on_problem), page);
+ g_signal_connect (user_verifier, "info-query",
+ G_CALLBACK (on_info_query), page);
+ g_signal_connect (user_verifier, "secret-info-query",
+ G_CALLBACK (on_secret_info_query), page);
+
+ g_signal_connect (greeter, "session-opened",
+ G_CALLBACK (on_session_opened), page);
+
+ /* We are in NEW_USER mode and we want to make it possible for third
+ * parties to find out which user ID we created.
+ */
+ add_uid_file (act_user_get_uid (priv->user_account));
+
+ gdm_user_verifier_call_begin_verification_for_user_sync (user_verifier,
+ SERVICE_NAME,
+ act_user_get_user_name (priv->user_account),
+ NULL, &error);
+
+ if (error != NULL)
+ g_warning ("Could not begin verification: %s", error->message);
+}
+
+static void
+done_cb (GtkButton *button, GisSummaryPage *page)
+{
+ gis_ensure_stamp_files (GIS_PAGE (page)->driver);
+
+ switch (gis_driver_get_mode (GIS_PAGE (page)->driver))
+ {
+ case GIS_DRIVER_MODE_NEW_USER:
+ gis_driver_hide_window (GIS_PAGE (page)->driver);
+ log_user_in (page);
+ break;
+ case GIS_DRIVER_MODE_EXISTING_USER:
+ g_application_quit (G_APPLICATION (GIS_PAGE (page)->driver));
+ default:
+ break;
+ }
+}
+
+static void
+gis_summary_page_shown (GisPage *page)
+{
+ GisSummaryPage *summary = GIS_SUMMARY_PAGE (page);
+ GisSummaryPagePrivate *priv = gis_summary_page_get_instance_private (summary);
+ g_autoptr(GError) local_error = NULL;
+
+ if (!gis_driver_save_data (GIS_PAGE (page)->driver, &local_error))
+ {
+ /* FIXME: This should probably be shown to the user and some options
+ * provided to them. */
+ g_warning ("Error saving data: %s", local_error->message);
+ }
+
+ gis_driver_get_user_permissions (GIS_PAGE (page)->driver,
+ &priv->user_account,
+ &priv->user_password);
+
+ gtk_widget_grab_focus (priv->start_button);
+}
+
+static void
+update_distro_name (GisSummaryPage *page)
+{
+ GisSummaryPagePrivate *priv = gis_summary_page_get_instance_private (page);
+ g_autofree char *name = g_get_os_info (G_OS_INFO_KEY_NAME);
+ char *text;
+
+ if (!name)
+ name = g_strdup ("GNOME");
+
+ /* Translators: the parameter here is the name of a distribution,
+ * like "Fedora" or "Ubuntu". It falls back to "GNOME" if we can't
+ * detect any distribution. */
+ text = g_strdup_printf (_("_Start Using %s"), name);
+ gtk_button_set_label (GTK_BUTTON (priv->start_button), text);
+ g_free (text);
+
+ /* Translators: the parameter here is the name of a distribution,
+ * like "Fedora" or "Ubuntu". It falls back to "GNOME" if we can't
+ * detect any distribution. */
+ text = g_strdup_printf (_("%s is ready to be used. We hope that you love it!"), name);
+ adw_status_page_set_description (priv->status_page, text);
+ g_free (text);
+}
+
+static void
+gis_summary_page_constructed (GObject *object)
+{
+ GisSummaryPage *page = GIS_SUMMARY_PAGE (object);
+ GisSummaryPagePrivate *priv = gis_summary_page_get_instance_private (page);
+
+ G_OBJECT_CLASS (gis_summary_page_parent_class)->constructed (object);
+
+ update_distro_name (page);
+ g_signal_connect (priv->start_button, "clicked", G_CALLBACK (done_cb), page);
+
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_summary_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (page, _("Setup Complete"));
+ update_distro_name (GIS_SUMMARY_PAGE (page));
+}
+
+static void
+gis_summary_page_class_init (GisSummaryPageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-summary-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisSummaryPage, start_button);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisSummaryPage, status_page);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_summary_page_locale_changed;
+ page_class->shown = gis_summary_page_shown;
+ object_class->constructed = gis_summary_page_constructed;
+}
+
+static void
+gis_summary_page_init (GisSummaryPage *page)
+{
+ g_resources_register (summary_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_summary_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_SUMMARY_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/summary/gis-summary-page.h b/gnome-initial-setup/pages/summary/gis-summary-page.h
new file mode 100644
index 0000000..20190f1
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/gis-summary-page.h
@@ -0,0 +1,55 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_SUMMARY_PAGE_H__
+#define __GIS_SUMMARY_PAGE_H__
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_SUMMARY_PAGE (gis_summary_page_get_type ())
+#define GIS_SUMMARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_SUMMARY_PAGE, GisSummaryPage))
+#define GIS_SUMMARY_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_SUMMARY_PAGE, GisSummaryPageClass))
+#define GIS_IS_SUMMARY_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_SUMMARY_PAGE))
+#define GIS_IS_SUMMARY_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_SUMMARY_PAGE))
+#define GIS_SUMMARY_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_SUMMARY_PAGE, GisSummaryPageClass))
+
+typedef struct _GisSummaryPage GisSummaryPage;
+typedef struct _GisSummaryPageClass GisSummaryPageClass;
+
+struct _GisSummaryPage
+{
+ GisPage parent;
+};
+
+struct _GisSummaryPageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_summary_page_get_type (void);
+
+GisPage *gis_prepare_summary_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_SUMMARY_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/summary/gis-summary-page.ui b/gnome-initial-setup/pages/summary/gis-summary-page.ui
new file mode 100644
index 0000000..b6dd300
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/gis-summary-page.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisSummaryPage" parent="GisPage">
+ <child>
+ <object class="AdwStatusPage" id="status_page">
+ <property name="paintable">resource:///org/gnome/initial-setup/ready-to-go.svg</property>
+ <property name="title" translatable="yes">All done!</property>
+
+ <child>
+ <object class="GtkButton" id="start_button">
+ <property name="use_underline">True</property>
+ <property name="halign">center</property>
+ <style>
+ <class name="suggested-action"/>
+ <class name="pill"/>
+ </style>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/summary/meson.build b/gnome-initial-setup/pages/summary/meson.build
new file mode 100644
index 0000000..4b203dc
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/meson.build
@@ -0,0 +1,10 @@
+sources += gnome.compile_resources(
+ 'summary-resources',
+ files('summary.gresource.xml'),
+ c_name: 'summary'
+)
+
+sources += files(
+ 'gis-summary-page.c',
+ 'gis-summary-page.h'
+)
diff --git a/gnome-initial-setup/pages/summary/ready-to-go.svg b/gnome-initial-setup/pages/summary/ready-to-go.svg
new file mode 100644
index 0000000..9ad6af4
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/ready-to-go.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128" version="1.0" enable-background="new"><defs><linearGradient id="d"><stop offset="0" stop-color="#26a269"/><stop offset=".143" stop-color="#75dfae"/><stop offset=".332" stop-color="#26a269"/><stop offset=".869" stop-color="#2cbb79"/><stop offset="1" stop-color="#1c774d"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#d5d3cf"/><stop offset="1" stop-color="#f6f5f4"/></linearGradient><linearGradient id="b"><stop offset="0" stop-color="#d5d3cf"/><stop offset="1" stop-color="#949390"/></linearGradient><linearGradient id="c"><stop offset="0" stop-color="#9a9996"/><stop offset="1" stop-color="#77767b"/></linearGradient><linearGradient xlink:href="#d" id="e" x1="13.25" y1="272" x2="118" y2="272" gradientUnits="userSpaceOnUse"/><filter id="g" color-interpolation-filters="sRGB"><feBlend mode="multiply" in2="BackgroundImage"/></filter><filter id="f" color-interpolation-filters="sRGB"><feBlend mode="multiply" in2="BackgroundImage"/></filter></defs><g transform="translate(0 -172)"><ellipse cy="237.761" cx="64" style="marker:none" rx="57.933" ry="56.383" fill="url(#e)"/><ellipse style="marker:none" cx="64" cy="233.14" rx="58.125" ry="57.269" fill="#2ec27e"/><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;marker:none" d="M111.592 192.047a8 8 0 00-5.498 2.422L60.563 240 45.28 224.719A8 8 0 1033.97 236.03l21.515 21.517c2.28 2.387 7.51 2.648 10.095.062l51.827-51.829c5.234-5.087 1.481-13.951-5.814-13.734z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#fff" enable-background="accumulate"/><path d="M-125.625 230.375l20.938 20.938 51.187-51.188" style="marker:none" fill="none" stroke="#a347ba" stroke-width="16" stroke-linecap="round"/><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;marker:none" d="M105.5 193.063L60.562 238l-15.28-15.281a8 8 0 00-13.704 6.572 8 8 0 0113.703-4.572L60.563 240l45.53-45.531a8 8 0 01.382-.354 58.125 57.269 0 00-.975-1.053z" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#26a269" enable-background="accumulate"/><path style="marker:none" d="M-125.625 310.375l20.938 20.938 51.187-51.188" fill="none" stroke="#a347ba" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/></g></svg> \ No newline at end of file
diff --git a/gnome-initial-setup/pages/summary/summary.gresource.xml b/gnome-initial-setup/pages/summary/summary.gresource.xml
new file mode 100644
index 0000000..75ed09e
--- /dev/null
+++ b/gnome-initial-setup/pages/summary/summary.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-summary-page.ui">gis-summary-page.ui</file>
+ <file alias="ready-to-go.svg">ready-to-go.svg</file>
+ </gresource>
+</gresources>
+
diff --git a/gnome-initial-setup/pages/timezone/backward b/gnome-initial-setup/pages/timezone/backward
new file mode 100644
index 0000000..8594be6
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/backward
@@ -0,0 +1,128 @@
+# This file is in the public domain, so clarified as of
+# 2009-05-17 by Arthur David Olson.
+
+# This file provides links between current names for time zones
+# and their old names. Many names changed in late 1993.
+
+# Link TARGET LINK-NAME
+Link Africa/Nairobi Africa/Asmera
+Link Africa/Abidjan Africa/Timbuktu
+Link America/Argentina/Catamarca America/Argentina/ComodRivadavia
+Link America/Adak America/Atka
+Link America/Argentina/Buenos_Aires America/Buenos_Aires
+Link America/Argentina/Catamarca America/Catamarca
+Link America/Atikokan America/Coral_Harbour
+Link America/Argentina/Cordoba America/Cordoba
+Link America/Tijuana America/Ensenada
+Link America/Indiana/Indianapolis America/Fort_Wayne
+Link America/Indiana/Indianapolis America/Indianapolis
+Link America/Argentina/Jujuy America/Jujuy
+Link America/Indiana/Knox America/Knox_IN
+Link America/Kentucky/Louisville America/Louisville
+Link America/Argentina/Mendoza America/Mendoza
+Link America/Toronto America/Montreal
+Link America/Rio_Branco America/Porto_Acre
+Link America/Argentina/Cordoba America/Rosario
+Link America/Tijuana America/Santa_Isabel
+Link America/Denver America/Shiprock
+Link America/Port_of_Spain America/Virgin
+Link Pacific/Auckland Antarctica/South_Pole
+Link Asia/Ashgabat Asia/Ashkhabad
+Link Asia/Kolkata Asia/Calcutta
+Link Asia/Shanghai Asia/Chongqing
+Link Asia/Shanghai Asia/Chungking
+Link Asia/Dhaka Asia/Dacca
+Link Asia/Shanghai Asia/Harbin
+Link Asia/Urumqi Asia/Kashgar
+Link Asia/Kathmandu Asia/Katmandu
+Link Asia/Macau Asia/Macao
+Link Asia/Yangon Asia/Rangoon
+Link Asia/Ho_Chi_Minh Asia/Saigon
+Link Asia/Jerusalem Asia/Tel_Aviv
+Link Asia/Thimphu Asia/Thimbu
+Link Asia/Makassar Asia/Ujung_Pandang
+Link Asia/Ulaanbaatar Asia/Ulan_Bator
+Link Atlantic/Faroe Atlantic/Faeroe
+Link Europe/Oslo Atlantic/Jan_Mayen
+Link Australia/Sydney Australia/ACT
+Link Australia/Sydney Australia/Canberra
+Link Australia/Lord_Howe Australia/LHI
+Link Australia/Sydney Australia/NSW
+Link Australia/Darwin Australia/North
+Link Australia/Brisbane Australia/Queensland
+Link Australia/Adelaide Australia/South
+Link Australia/Hobart Australia/Tasmania
+Link Australia/Melbourne Australia/Victoria
+Link Australia/Perth Australia/West
+Link Australia/Broken_Hill Australia/Yancowinna
+Link America/Rio_Branco Brazil/Acre
+Link America/Noronha Brazil/DeNoronha
+Link America/Sao_Paulo Brazil/East
+Link America/Manaus Brazil/West
+Link America/Halifax Canada/Atlantic
+Link America/Winnipeg Canada/Central
+# This line is commented out, as the name exceeded the 14-character limit
+# and was an unused misnomer.
+#Link America/Regina Canada/East-Saskatchewan
+Link America/Toronto Canada/Eastern
+Link America/Edmonton Canada/Mountain
+Link America/St_Johns Canada/Newfoundland
+Link America/Vancouver Canada/Pacific
+Link America/Regina Canada/Saskatchewan
+Link America/Whitehorse Canada/Yukon
+Link America/Santiago Chile/Continental
+Link Pacific/Easter Chile/EasterIsland
+Link America/Havana Cuba
+Link Africa/Cairo Egypt
+Link Europe/Dublin Eire
+Link Europe/London Europe/Belfast
+Link Europe/Chisinau Europe/Tiraspol
+Link Europe/London GB
+Link Europe/London GB-Eire
+Link Etc/GMT GMT+0
+Link Etc/GMT GMT-0
+Link Etc/GMT GMT0
+Link Etc/GMT Greenwich
+Link Asia/Hong_Kong Hongkong
+Link Atlantic/Reykjavik Iceland
+Link Asia/Tehran Iran
+Link Asia/Jerusalem Israel
+Link America/Jamaica Jamaica
+Link Asia/Tokyo Japan
+Link Pacific/Kwajalein Kwajalein
+Link Africa/Tripoli Libya
+Link America/Tijuana Mexico/BajaNorte
+Link America/Mazatlan Mexico/BajaSur
+Link America/Mexico_City Mexico/General
+Link Pacific/Auckland NZ
+Link Pacific/Chatham NZ-CHAT
+Link America/Denver Navajo
+Link Asia/Shanghai PRC
+Link Pacific/Honolulu Pacific/Johnston
+Link Pacific/Pohnpei Pacific/Ponape
+Link Pacific/Pago_Pago Pacific/Samoa
+Link Pacific/Chuuk Pacific/Truk
+Link Pacific/Chuuk Pacific/Yap
+Link Europe/Warsaw Poland
+Link Europe/Lisbon Portugal
+Link Asia/Taipei ROC
+Link Asia/Seoul ROK
+Link Asia/Singapore Singapore
+Link Europe/Istanbul Turkey
+Link Etc/UCT UCT
+Link America/Anchorage US/Alaska
+Link America/Adak US/Aleutian
+Link America/Phoenix US/Arizona
+Link America/Chicago US/Central
+Link America/Indiana/Indianapolis US/East-Indiana
+Link America/New_York US/Eastern
+Link Pacific/Honolulu US/Hawaii
+Link America/Indiana/Knox US/Indiana-Starke
+Link America/Detroit US/Michigan
+Link America/Denver US/Mountain
+Link America/Los_Angeles US/Pacific
+Link Pacific/Pago_Pago US/Samoa
+Link Etc/UTC UTC
+Link Etc/UTC Universal
+Link Europe/Moscow W-SU
+Link Etc/UTC Zulu \ No newline at end of file
diff --git a/gnome-initial-setup/pages/timezone/cc-timezone-map.c b/gnome-initial-setup/pages/timezone/cc-timezone-map.c
new file mode 100644
index 0000000..863b4c9
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/cc-timezone-map.c
@@ -0,0 +1,530 @@
+/*
+ * Copyright (C) 2010 Intel, Inc
+ *
+ * Portions from Ubiquity, Copyright (C) 2009 Canonical Ltd.
+ * Written by Evan Dandrea <evand@ubuntu.com>
+ *
+ * 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: Thomas Wood <thomas.wood@intel.com>
+ *
+ */
+
+#include "cc-timezone-map.h"
+#include <math.h>
+#include <string.h>
+#include "tz.h"
+
+#define PIN_HOT_POINT_X 8
+#define PIN_HOT_POINT_Y 15
+
+#define DATETIME_RESOURCE_PATH "/org/gnome/control-center/datetime"
+
+typedef struct
+{
+ gdouble offset;
+ guchar red;
+ guchar green;
+ guchar blue;
+ guchar alpha;
+} CcTimezoneMapOffset;
+
+struct _CcTimezoneMap
+{
+ GtkWidget parent_instance;
+
+ GdkTexture *orig_background;
+ GdkTexture *orig_background_dim;
+
+ GdkTexture *background;
+ GdkTexture *pin;
+
+ TzDB *tzdb;
+ TzLocation *location;
+
+ gchar *bubble_text;
+};
+
+G_DEFINE_TYPE (CcTimezoneMap, cc_timezone_map, GTK_TYPE_WIDGET)
+
+enum
+{
+ LOCATION_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static GdkTexture *
+texture_from_resource (const gchar *resource_path,
+ GError **error)
+{
+ g_autofree gchar *full_path = g_strdup_printf ("resource://%s", resource_path);
+ g_autoptr(GFile) file = g_file_new_for_uri (full_path);
+ g_autoptr(GdkTexture) texture = gdk_texture_new_from_file (file, error);
+
+ return g_steal_pointer (&texture);
+}
+
+static void
+cc_timezone_map_dispose (GObject *object)
+{
+ CcTimezoneMap *self = CC_TIMEZONE_MAP (object);
+
+ g_clear_object (&self->orig_background);
+ g_clear_object (&self->orig_background_dim);
+ g_clear_object (&self->background);
+ g_clear_object (&self->pin);
+ g_clear_pointer (&self->bubble_text, g_free);
+
+ G_OBJECT_CLASS (cc_timezone_map_parent_class)->dispose (object);
+}
+
+static void
+cc_timezone_map_finalize (GObject *object)
+{
+ CcTimezoneMap *self = CC_TIMEZONE_MAP (object);
+
+ g_clear_pointer (&self->tzdb, tz_db_free);
+
+ G_OBJECT_CLASS (cc_timezone_map_parent_class)->finalize (object);
+}
+
+/* GtkWidget functions */
+static void
+cc_timezone_map_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ CcTimezoneMap *map = CC_TIMEZONE_MAP (widget);
+ gint size;
+
+ switch (orientation)
+ {
+ case GTK_ORIENTATION_HORIZONTAL:
+ size = gdk_texture_get_width (map->orig_background);
+ break;
+
+ case GTK_ORIENTATION_VERTICAL:
+ size = gdk_texture_get_height (map->orig_background);
+ break;
+ }
+
+ if (minimum != NULL)
+ *minimum = size;
+ if (natural != NULL)
+ *natural = size;
+}
+
+static void
+cc_timezone_map_size_allocate (GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ CcTimezoneMap *map = CC_TIMEZONE_MAP (widget);
+ GdkTexture *texture;
+
+ if (!gtk_widget_is_sensitive (widget))
+ texture = map->orig_background_dim;
+ else
+ texture = map->orig_background;
+
+ g_clear_object (&map->background);
+ map->background = g_object_ref (texture);
+
+ GTK_WIDGET_CLASS (cc_timezone_map_parent_class)->size_allocate (widget,
+ width,
+ height,
+ baseline);
+}
+
+static gdouble
+convert_longitude_to_x (gdouble longitude, gint map_width)
+{
+ const gdouble xdeg_offset = -6;
+ gdouble x;
+
+ x = (map_width * (180.0 + longitude) / 360.0)
+ + (map_width * xdeg_offset / 180.0);
+
+ return x;
+}
+
+static gdouble
+radians (gdouble degrees)
+{
+ return (degrees / 360.0) * G_PI * 2;
+}
+
+static gdouble
+convert_latitude_to_y (gdouble latitude, gdouble map_height)
+{
+ gdouble bottom_lat = -59;
+ gdouble top_lat = 81;
+ gdouble top_per, y, full_range, top_offset, map_range;
+
+ top_per = top_lat / 180.0;
+ y = 1.25 * log (tan (G_PI_4 + 0.4 * radians (latitude)));
+ full_range = 4.6068250867599998;
+ top_offset = full_range * top_per;
+ map_range = fabs (1.25 * log (tan (G_PI_4 + 0.4 * radians (bottom_lat))) - top_offset);
+ y = fabs (y - top_offset);
+ y = y / map_range;
+ y = y * map_height;
+ return y;
+}
+
+static void
+draw_text_bubble (CcTimezoneMap *map,
+ GtkSnapshot *snapshot,
+ gint width,
+ gint height,
+ gdouble pointx,
+ gdouble pointy)
+{
+ static const double corner_radius = 9.0;
+ static const double margin_top = 12.0;
+ static const double margin_bottom = 12.0;
+ static const double margin_left = 24.0;
+ static const double margin_right = 24.0;
+
+ GskRoundedRect rounded_rect;
+ PangoRectangle text_rect;
+ PangoLayout *layout;
+ GdkRGBA rgba;
+ double x;
+ double y;
+ double bubble_width;
+ double bubble_height;
+
+ if (!map->bubble_text)
+ return;
+
+ layout = gtk_widget_create_pango_layout (GTK_WIDGET (map), NULL);
+
+ /* Layout the text */
+ pango_layout_set_alignment (layout, PANGO_ALIGN_CENTER);
+ pango_layout_set_spacing (layout, 3);
+ pango_layout_set_markup (layout, map->bubble_text, -1);
+
+ pango_layout_get_pixel_extents (layout, NULL, &text_rect);
+
+ /* Calculate the bubble size based on the text layout size */
+ bubble_width = text_rect.width + margin_left + margin_right;
+ bubble_height = text_rect.height + margin_top + margin_bottom;
+
+ if (pointx < width / 2)
+ x = pointx + 25;
+ else
+ x = pointx - bubble_width - 25;
+
+ y = pointy - bubble_height / 2;
+
+ /* Make sure it fits in the visible area */
+ x = CLAMP (x, 0, width - bubble_width);
+ y = CLAMP (y, 0, height - bubble_height);
+
+ gtk_snapshot_save (snapshot);
+
+ gsk_rounded_rect_init (&rounded_rect,
+ &GRAPHENE_RECT_INIT (x, y, bubble_width, bubble_height),
+ &GRAPHENE_SIZE_INIT (corner_radius, corner_radius),
+ &GRAPHENE_SIZE_INIT (corner_radius, corner_radius),
+ &GRAPHENE_SIZE_INIT (corner_radius, corner_radius),
+ &GRAPHENE_SIZE_INIT (corner_radius, corner_radius));
+
+ gtk_snapshot_push_rounded_clip (snapshot, &rounded_rect);
+
+ rgba = (GdkRGBA) {
+ .red = 0.2,
+ .green = 0.2,
+ .blue = 0.2,
+ .alpha = 0.7,
+ };
+ gtk_snapshot_append_color (snapshot,
+ &rgba,
+ &GRAPHENE_RECT_INIT (x, y, bubble_width, bubble_height));
+
+
+ rgba = (GdkRGBA) {
+ .red = 1.0,
+ .green = 1.0,
+ .blue = 1.0,
+ .alpha = 1.0,
+ };
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x + margin_left, y + margin_top));
+ gtk_snapshot_append_layout (snapshot, layout, &rgba);
+
+ gtk_snapshot_pop (snapshot);
+ gtk_snapshot_restore (snapshot);
+
+ g_object_unref (layout);
+}
+
+static void
+cc_timezone_map_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ CcTimezoneMap *map = CC_TIMEZONE_MAP (widget);
+ gdouble pointx, pointy;
+ gint width, height;
+
+ width = gtk_widget_get_width (widget);
+ height = gtk_widget_get_height (widget);
+
+ /* paint background */
+ gtk_snapshot_append_texture (snapshot,
+ map->background,
+ &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+ if (map->location)
+ {
+ pointx = convert_longitude_to_x (map->location->longitude, width);
+ pointy = convert_latitude_to_y (map->location->latitude, height);
+
+ pointx = CLAMP (floor (pointx), 0, width);
+ pointy = CLAMP (floor (pointy), 0, height);
+
+ draw_text_bubble (map, snapshot, width, height, pointx, pointy);
+
+ if (map->pin)
+ {
+ gtk_snapshot_append_texture (snapshot,
+ map->pin,
+ &GRAPHENE_RECT_INIT (pointx - PIN_HOT_POINT_X,
+ pointy - PIN_HOT_POINT_Y,
+ gdk_texture_get_width (map->pin),
+ gdk_texture_get_height (map->pin)));
+ }
+ }
+}
+
+static void
+update_cursor (GtkWidget *widget)
+{
+ const gchar *cursor_name = NULL;
+
+ if (!gtk_widget_get_realized (widget))
+ return;
+
+ if (gtk_widget_is_sensitive (widget))
+ cursor_name = "pointer";
+
+ gtk_widget_set_cursor_from_name (widget, cursor_name);
+}
+
+static void
+cc_timezone_map_state_flags_changed (GtkWidget *widget,
+ GtkStateFlags prev_state)
+{
+ update_cursor (widget);
+
+ if (GTK_WIDGET_CLASS (cc_timezone_map_parent_class)->state_flags_changed)
+ GTK_WIDGET_CLASS (cc_timezone_map_parent_class)->state_flags_changed (widget, prev_state);
+}
+
+
+static void
+cc_timezone_map_class_init (CcTimezoneMapClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_timezone_map_dispose;
+ object_class->finalize = cc_timezone_map_finalize;
+
+ widget_class->measure = cc_timezone_map_measure;
+ widget_class->size_allocate = cc_timezone_map_size_allocate;
+ widget_class->snapshot = cc_timezone_map_snapshot;
+ widget_class->state_flags_changed = cc_timezone_map_state_flags_changed;
+
+ signals[LOCATION_CHANGED] = g_signal_new ("location-changed",
+ CC_TYPE_TIMEZONE_MAP,
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL,
+ NULL,
+ g_cclosure_marshal_VOID__POINTER,
+ G_TYPE_NONE, 1,
+ G_TYPE_POINTER);
+}
+
+
+static gint
+sort_locations (TzLocation *a,
+ TzLocation *b)
+{
+ if (a->dist > b->dist)
+ return 1;
+
+ if (a->dist < b->dist)
+ return -1;
+
+ return 0;
+}
+
+static void
+set_location (CcTimezoneMap *map,
+ TzLocation *location)
+{
+ g_autoptr(TzInfo) info = NULL;
+
+ map->location = location;
+
+ info = tz_info_from_location (map->location);
+
+ gtk_widget_queue_draw (GTK_WIDGET (map));
+
+ g_signal_emit (map, signals[LOCATION_CHANGED], 0, map->location);
+}
+
+static gboolean
+map_clicked_cb (GtkGestureClick *self,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ CcTimezoneMap *map)
+{
+ const GPtrArray *array;
+ gint width, height;
+ GList *distances = NULL;
+ gint i;
+
+ /* work out the coordinates */
+
+ array = tz_get_locations (map->tzdb);
+
+ width = gtk_widget_get_width (GTK_WIDGET (map));
+ height = gtk_widget_get_height (GTK_WIDGET (map));
+
+ for (i = 0; i < array->len; i++)
+ {
+ gdouble pointx, pointy, dx, dy;
+ TzLocation *loc = array->pdata[i];
+
+ pointx = convert_longitude_to_x (loc->longitude, width);
+ pointy = convert_latitude_to_y (loc->latitude, height);
+
+ dx = pointx - x;
+ dy = pointy - y;
+
+ loc->dist = dx * dx + dy * dy;
+ distances = g_list_prepend (distances, loc);
+
+ }
+ distances = g_list_sort (distances, (GCompareFunc) sort_locations);
+
+
+ set_location (map, (TzLocation*) distances->data);
+
+ g_list_free (distances);
+
+ return TRUE;
+}
+
+static void
+cc_timezone_map_init (CcTimezoneMap *map)
+{
+ GtkGesture *click_gesture;
+ GError *err = NULL;
+
+ map->orig_background = texture_from_resource (DATETIME_RESOURCE_PATH "/bg.png", &err);
+ if (!map->orig_background)
+ {
+ g_warning ("Could not load background image: %s",
+ (err) ? err->message : "Unknown error");
+ g_clear_error (&err);
+ }
+
+ map->orig_background_dim = texture_from_resource (DATETIME_RESOURCE_PATH "/bg_dim.png", &err);
+ if (!map->orig_background_dim)
+ {
+ g_warning ("Could not load background image: %s",
+ (err) ? err->message : "Unknown error");
+ g_clear_error (&err);
+ }
+
+ map->pin = texture_from_resource (DATETIME_RESOURCE_PATH "/pin.png", &err);
+ if (!map->pin)
+ {
+ g_warning ("Could not load pin icon: %s",
+ (err) ? err->message : "Unknown error");
+ g_clear_error (&err);
+ }
+
+ map->tzdb = tz_load_db ();
+
+ click_gesture = gtk_gesture_click_new ();
+ g_signal_connect (click_gesture, "pressed", G_CALLBACK (map_clicked_cb), map);
+ gtk_widget_add_controller (GTK_WIDGET (map), GTK_EVENT_CONTROLLER (click_gesture));
+}
+
+CcTimezoneMap *
+cc_timezone_map_new (void)
+{
+ return g_object_new (CC_TYPE_TIMEZONE_MAP, NULL);
+}
+
+gboolean
+cc_timezone_map_set_timezone (CcTimezoneMap *map,
+ const gchar *timezone)
+{
+ GPtrArray *locations;
+ guint i;
+ g_autofree gchar *real_tz = NULL;
+ gboolean ret;
+
+ real_tz = tz_info_get_clean_name (map->tzdb, timezone);
+
+ locations = tz_get_locations (map->tzdb);
+ ret = FALSE;
+
+ for (i = 0; i < locations->len; i++)
+ {
+ TzLocation *loc = locations->pdata[i];
+
+ if (!g_strcmp0 (loc->zone, real_tz ? real_tz : timezone))
+ {
+ set_location (map, loc);
+ ret = TRUE;
+ break;
+ }
+ }
+
+ if (ret)
+ gtk_widget_queue_draw (GTK_WIDGET (map));
+
+ return ret;
+}
+
+void
+cc_timezone_map_set_bubble_text (CcTimezoneMap *map,
+ const gchar *text)
+{
+ g_free (map->bubble_text);
+ map->bubble_text = g_strdup (text);
+
+ gtk_widget_queue_draw (GTK_WIDGET (map));
+}
+
+TzLocation *
+cc_timezone_map_get_location (CcTimezoneMap *map)
+{
+ return map->location;
+}
diff --git a/gnome-initial-setup/pages/timezone/cc-timezone-map.h b/gnome-initial-setup/pages/timezone/cc-timezone-map.h
new file mode 100644
index 0000000..f4c99e5
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/cc-timezone-map.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2010 Intel, 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: Thomas Wood <thomas.wood@intel.com>
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "tz.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_TIMEZONE_MAP (cc_timezone_map_get_type ())
+G_DECLARE_FINAL_TYPE (CcTimezoneMap, cc_timezone_map, CC, TIMEZONE_MAP, GtkWidget)
+
+CcTimezoneMap *cc_timezone_map_new (void);
+
+gboolean cc_timezone_map_set_timezone (CcTimezoneMap *map,
+ const gchar *timezone);
+void cc_timezone_map_set_bubble_text (CcTimezoneMap *map,
+ const gchar *text);
+TzLocation * cc_timezone_map_get_location (CcTimezoneMap *map);
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/timezone/data/bg.png b/gnome-initial-setup/pages/timezone/data/bg.png
new file mode 100644
index 0000000..43e4016
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/data/bg.png
Binary files differ
diff --git a/gnome-initial-setup/pages/timezone/data/bg_dim.png b/gnome-initial-setup/pages/timezone/data/bg_dim.png
new file mode 100644
index 0000000..8db7a19
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/data/bg_dim.png
Binary files differ
diff --git a/gnome-initial-setup/pages/timezone/data/pin.png b/gnome-initial-setup/pages/timezone/data/pin.png
new file mode 100644
index 0000000..c347a9e
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/data/pin.png
Binary files differ
diff --git a/gnome-initial-setup/pages/timezone/datetime.gresource.xml b/gnome-initial-setup/pages/timezone/datetime.gresource.xml
new file mode 100644
index 0000000..5a3dbb3
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/datetime.gresource.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/datetime">
+ <file>backward</file>
+ <file alias="bg.png">data/bg.png</file>
+ <file alias="bg_dim.png">data/bg_dim.png</file>
+ <file alias="pin.png">data/pin.png</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/timezone/gis-bubble-widget.c b/gnome-initial-setup/pages/timezone/gis-bubble-widget.c
new file mode 100644
index 0000000..a6e8ab7
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-bubble-widget.c
@@ -0,0 +1,141 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#include "config.h"
+
+#include "gis-bubble-widget.h"
+
+struct _GisBubbleWidgetPrivate
+{
+ GtkWidget *icon;
+ GtkWidget *label;
+};
+typedef struct _GisBubbleWidgetPrivate GisBubbleWidgetPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisBubbleWidget, gis_bubble_widget, ADW_TYPE_BIN);
+
+enum {
+ PROP_0,
+ PROP_LABEL,
+ PROP_ICON_NAME,
+ PROP_LAST,
+};
+
+static GParamSpec *obj_props[PROP_LAST];
+
+static void
+gis_bubble_widget_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GisBubbleWidget *widget = GIS_BUBBLE_WIDGET (object);
+ GisBubbleWidgetPrivate *priv = gis_bubble_widget_get_instance_private (widget);
+
+ switch (prop_id)
+ {
+ case PROP_LABEL:
+ g_value_set_string (value, gtk_label_get_label (GTK_LABEL (priv->label)));
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, gtk_image_get_icon_name (GTK_IMAGE (priv->icon)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gis_bubble_widget_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GisBubbleWidget *widget = GIS_BUBBLE_WIDGET (object);
+ GisBubbleWidgetPrivate *priv = gis_bubble_widget_get_instance_private (widget);
+
+ switch (prop_id)
+ {
+ case PROP_LABEL:
+ gtk_label_set_label (GTK_LABEL (priv->label), g_value_get_string (value));
+ break;
+ case PROP_ICON_NAME:
+ g_object_set (GTK_IMAGE (priv->icon),
+ "icon-name", g_value_get_string (value),
+ NULL);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+add_style_from_resource (const char *resource)
+{
+ GtkCssProvider *provider;
+ GFile *file;
+ char *uri;
+
+ provider = gtk_css_provider_new ();
+
+ uri = g_strconcat ("resource://", resource, NULL);
+ file = g_file_new_for_uri (uri);
+
+ gtk_css_provider_load_from_file (provider, file);
+
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ g_object_unref (provider);
+ g_object_unref (file);
+ g_free (uri);
+}
+
+static void
+gis_bubble_widget_class_init (GisBubbleWidgetClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-bubble-widget.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisBubbleWidget, icon);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisBubbleWidget, label);
+
+ gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "bubble");
+
+ object_class->set_property = gis_bubble_widget_set_property;
+ object_class->get_property = gis_bubble_widget_get_property;
+
+ obj_props[PROP_LABEL] = g_param_spec_string ("label", "", "", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ obj_props[PROP_ICON_NAME] = g_param_spec_string ("icon-name", "", "", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+
+ add_style_from_resource ("/org/gnome/initial-setup/gis-bubble-widget.css");
+}
+
+static void
+gis_bubble_widget_init (GisBubbleWidget *widget)
+{
+ gtk_widget_init_template (GTK_WIDGET (widget));
+}
diff --git a/gnome-initial-setup/pages/timezone/gis-bubble-widget.css b/gnome-initial-setup/pages/timezone/gis-bubble-widget.css
new file mode 100644
index 0000000..768e69c
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-bubble-widget.css
@@ -0,0 +1,8 @@
+
+bubble {
+ border-radius: 20px;
+ background-color: rgba(0, 0, 0, 0.6);
+ color: white;
+ font-weight: bold;
+ padding: 2em;
+}
diff --git a/gnome-initial-setup/pages/timezone/gis-bubble-widget.h b/gnome-initial-setup/pages/timezone/gis-bubble-widget.h
new file mode 100644
index 0000000..6029166
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-bubble-widget.h
@@ -0,0 +1,53 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2013 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_BUBBLE_WIDGET_H__
+#define __GIS_BUBBLE_WIDGET_H__
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_BUBBLE_WIDGET (gis_bubble_widget_get_type ())
+#define GIS_BUBBLE_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_BUBBLE_WIDGET, GisBubbleWidget))
+#define GIS_BUBBLE_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_BUBBLE_WIDGET, GisBubbleWidgetClass))
+#define GIS_IS_BUBBLE_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_BUBBLE_WIDGET))
+#define GIS_IS_BUBBLE_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_BUBBLE_WIDGET))
+#define GIS_BUBBLE_WIDGET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_BUBBLE_WIDGET, GisBubbleWidgetClass))
+
+typedef struct _GisBubbleWidget GisBubbleWidget;
+typedef struct _GisBubbleWidgetClass GisBubbleWidgetClass;
+
+struct _GisBubbleWidget
+{
+ AdwBin parent;
+};
+
+struct _GisBubbleWidgetClass
+{
+ AdwBinClass parent_class;
+};
+
+GType gis_bubble_widget_get_type (void);
+
+G_END_DECLS
+
+#endif /* __GIS_BUBBLE_WIDGET_H__ */
diff --git a/gnome-initial-setup/pages/timezone/gis-bubble-widget.ui b/gnome-initial-setup/pages/timezone/gis-bubble-widget.ui
new file mode 100644
index 0000000..89dd121
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-bubble-widget.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="GisBubbleWidget" parent="AdwBin">
+ <child>
+ <object class="GtkAspectFrame" id="aspect_frame">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="width-request">128</property>
+ <property name="height-request">128</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="vexpand">True</property>
+ <property name="pixel-size">64</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="vexpand">True</property>
+ <property name="wrap">True</property>
+ <property name="justify">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/timezone/gis-location-entry.c b/gnome-initial-setup/pages/timezone/gis-location-entry.c
new file mode 100644
index 0000000..de8fdb8
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-location-entry.c
@@ -0,0 +1,871 @@
+/* gweather-location-entry.c - Location-selecting text entry
+ *
+ * SPDX-FileCopyrightText: 2008, Red Hat, Inc.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "gis-location-entry.h"
+
+#include <string.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <geocode-glib/geocode-glib.h>
+
+/**
+ * GisLocationEntry:
+ *
+ * A subclass of [class@Gtk.SearchEntry] that provides autocompletion on
+ * [struct@GWeather.Location]s.
+ *
+ */
+
+struct _GisLocationEntryPrivate {
+ GtkWidget *entry;
+ GWeatherLocation *location;
+ GWeatherLocation *top;
+ gboolean show_named_timezones;
+ GCancellable *cancellable;
+ GtkTreeModel *model;
+};
+
+static void editable_iface_init (GtkEditableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GisLocationEntry, gis_location_entry, GTK_TYPE_WIDGET,
+ G_ADD_PRIVATE (GisLocationEntry)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init));
+
+enum {
+ PROP_0,
+
+ PROP_TOP,
+ PROP_SHOW_NAMED_TIMEZONES,
+ PROP_LOCATION,
+
+ LAST_PROP
+};
+
+static void set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec);
+static void get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec);
+
+static void set_location_internal (GisLocationEntry *entry,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GWeatherLocation *loc);
+static void
+fill_location_entry_model (GtkListStore *store, GWeatherLocation *loc,
+ const char *parent_display_name,
+ const char *parent_sort_local_name,
+ const char *parent_compare_local_name,
+ const char *parent_compare_english_name,
+ gboolean show_named_timezones);
+
+enum LOC
+{
+ LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME = 0,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME,
+ LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME,
+ LOC_GIS_LOCATION_ENTRY_NUM_COLUMNS
+};
+
+enum PLACE
+{
+ PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME = 0,
+ PLACE_GIS_LOCATION_ENTRY_COL_PLACE,
+ PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME,
+ PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME
+};
+
+static gboolean matcher (GtkEntryCompletion *completion, const char *key,
+ GtkTreeIter *iter, gpointer user_data);
+static gboolean match_selected (GtkEntryCompletion *completion,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer entry);
+static void entry_changed (GisLocationEntry *entry);
+static void _no_matches (GtkEntryCompletion *completion, GisLocationEntry *entry);
+
+static GtkEditable*
+gis_location_entry_get_delegate (GtkEditable *editable)
+{
+ GisLocationEntry *entry = GIS_LOCATION_ENTRY (editable);
+ GisLocationEntryPrivate *priv = gis_location_entry_get_instance_private (entry);
+
+ return GTK_EDITABLE (priv->entry);
+}
+
+static void
+editable_iface_init (GtkEditableInterface *iface)
+{
+ iface->get_delegate = gis_location_entry_get_delegate;
+}
+
+static void
+gis_location_entry_init (GisLocationEntry *entry)
+{
+ GtkEntryCompletion *completion;
+ GisLocationEntryPrivate *priv;
+
+ priv = entry->priv = gis_location_entry_get_instance_private (entry);
+
+ priv->entry = gtk_entry_new ();
+ gtk_widget_set_parent (priv->entry, GTK_WIDGET (entry));
+ gtk_editable_init_delegate (GTK_EDITABLE (entry));
+
+ completion = gtk_entry_completion_new ();
+
+ gtk_entry_completion_set_popup_set_width (completion, TRUE);
+ gtk_entry_completion_set_text_column (completion, LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME);
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_inline_completion (completion, TRUE);
+
+ g_signal_connect (completion, "match-selected",
+ G_CALLBACK (match_selected), entry);
+
+ g_signal_connect (completion, "no-matches",
+ G_CALLBACK (_no_matches), entry);
+
+ gtk_entry_set_completion (GTK_ENTRY (entry->priv->entry), completion);
+ g_object_unref (completion);
+
+ g_signal_connect (entry, "changed",
+ G_CALLBACK (entry_changed), NULL);
+}
+
+static void
+finalize (GObject *object)
+{
+ GisLocationEntry *entry;
+ GisLocationEntryPrivate *priv;
+
+ entry = GIS_LOCATION_ENTRY (object);
+ priv = entry->priv;
+
+ g_clear_object (&priv->location);
+ g_clear_object (&priv->top);
+ g_clear_object (&priv->model);
+
+ G_OBJECT_CLASS (gis_location_entry_parent_class)->finalize (object);
+}
+
+static void
+dispose (GObject *object)
+{
+ GisLocationEntry *entry;
+ GisLocationEntryPrivate *priv;
+
+ entry = GIS_LOCATION_ENTRY (object);
+ priv = entry->priv;
+
+ if (priv->cancellable) {
+ g_cancellable_cancel (priv->cancellable);
+ g_object_unref (priv->cancellable);
+ priv->cancellable = NULL;
+ }
+
+ gtk_editable_finish_delegate (GTK_EDITABLE (entry));
+ g_clear_pointer (&priv->entry, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (gis_location_entry_parent_class)->dispose (object);
+}
+
+static int
+tree_compare_local_name (GtkTreeModel *model,
+ GtkTreeIter *a,
+ GtkTreeIter *b,
+ gpointer user_data)
+{
+ g_autofree gchar *name_a = NULL, *name_b = NULL;
+
+ gtk_tree_model_get (model, a,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_a,
+ -1);
+ gtk_tree_model_get (model, b,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, &name_b,
+ -1);
+
+ return g_utf8_collate (name_a, name_b);
+}
+
+
+static void
+constructed (GObject *object)
+{
+ GisLocationEntry *entry;
+ GtkListStore *store = NULL;
+ GtkEntryCompletion *completion;
+
+ entry = GIS_LOCATION_ENTRY (object);
+
+ if (!entry->priv->top)
+ entry->priv->top = gweather_location_get_world ();
+
+ store = gtk_list_store_new (5, G_TYPE_STRING, GWEATHER_TYPE_LOCATION, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+ gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (store),
+ tree_compare_local_name, NULL, NULL);
+ fill_location_entry_model (store, entry->priv->top, NULL, NULL, NULL, NULL, entry->priv->show_named_timezones);
+
+ entry->priv->model = GTK_TREE_MODEL (store);
+ completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry));
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
+
+ G_OBJECT_CLASS (gis_location_entry_parent_class)->constructed (object);
+}
+
+static void
+gis_location_entry_class_init (GisLocationEntryClass *location_entry_class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (location_entry_class);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (location_entry_class);
+
+ object_class->constructed = constructed;
+ object_class->finalize = finalize;
+ object_class->set_property = set_property;
+ object_class->get_property = get_property;
+ object_class->dispose = dispose;
+
+ /* properties */
+ g_object_class_install_property (
+ object_class, PROP_TOP,
+ g_param_spec_object ("top",
+ "Top Location",
+ "The GWeatherLocation whose children will be used to fill in the entry",
+ GWEATHER_TYPE_LOCATION,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (
+ object_class, PROP_SHOW_NAMED_TIMEZONES,
+ g_param_spec_boolean ("show-named-timezones",
+ "Show named timezones",
+ "Whether UTC and other named timezones are shown in the list of locations",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (
+ object_class, PROP_LOCATION,
+ g_param_spec_object ("location",
+ "Location",
+ "The selected GWeatherLocation",
+ GWEATHER_TYPE_LOCATION,
+ G_PARAM_READWRITE));
+
+ gtk_editable_install_properties (object_class, LAST_PROP);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+}
+
+static void
+set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ GisLocationEntry *entry = GIS_LOCATION_ENTRY (object);
+
+ if (gtk_editable_delegate_set_property (object, prop_id, value, pspec))
+ return;
+
+ switch (prop_id) {
+ case PROP_TOP:
+ entry->priv->top = g_value_dup_object (value);
+ break;
+ case PROP_SHOW_NAMED_TIMEZONES:
+ entry->priv->show_named_timezones = g_value_get_boolean (value);
+ break;
+ case PROP_LOCATION:
+ gis_location_entry_set_location (GIS_LOCATION_ENTRY (object),
+ g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ GisLocationEntry *entry = GIS_LOCATION_ENTRY (object);
+
+ if (gtk_editable_delegate_get_property (object, prop_id, value, pspec))
+ return;
+
+ switch (prop_id) {
+ case PROP_SHOW_NAMED_TIMEZONES:
+ g_value_set_boolean (value, entry->priv->show_named_timezones);
+ break;
+ case PROP_LOCATION:
+ g_value_set_object (value, entry->priv->location);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+entry_changed (GisLocationEntry *entry)
+{
+ GtkEntryCompletion *completion;
+ const gchar *text;
+
+ completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry));
+
+ if (entry->priv->cancellable) {
+ g_cancellable_cancel (entry->priv->cancellable);
+ g_object_unref (entry->priv->cancellable);
+ entry->priv->cancellable = NULL;
+ }
+
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_model (completion, entry->priv->model);
+
+ text = gtk_editable_get_text (GTK_EDITABLE (entry));
+
+ if (!text || *text == '\0')
+ set_location_internal (entry, NULL, NULL, NULL);
+}
+
+static void
+set_entry_text (GisLocationEntry *entry,
+ const char *text)
+{
+ GisLocationEntryPrivate *priv = entry->priv;
+
+ if (g_strcmp0 (gtk_editable_get_text (GTK_EDITABLE (priv->entry)), text) != 0)
+ gtk_editable_set_text (GTK_EDITABLE (priv->entry), text);
+}
+
+static void
+set_location_internal (GisLocationEntry *entry,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GWeatherLocation *loc)
+{
+ GisLocationEntryPrivate *priv;
+ char *name;
+
+ priv = entry->priv;
+
+ g_clear_object (&priv->location);
+
+ g_assert (iter == NULL || loc == NULL);
+
+ g_signal_handlers_block_by_func (entry, entry_changed, NULL);
+
+ if (iter) {
+ gtk_tree_model_get (model, iter,
+ LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, &name,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &priv->location,
+ -1);
+ set_entry_text (entry, name);
+ g_free (name);
+ } else if (loc) {
+ priv->location = g_object_ref (loc);
+ set_entry_text (entry, gweather_location_get_name (loc));
+ } else {
+ priv->location = NULL;
+ set_entry_text (entry, "");
+ }
+
+ g_signal_handlers_unblock_by_func (entry, entry_changed, NULL);
+
+ gtk_editable_set_position (GTK_EDITABLE (entry), -1);
+ g_object_notify (G_OBJECT (entry), "location");
+}
+
+/**
+ * gis_location_entry_set_location:
+ * @entry: a #GisLocationEntry
+ * @loc: (allow-none): a #GWeatherLocation in @entry, or %NULL to
+ * clear @entry
+ *
+ * Sets @entry's location to @loc, and updates the text of the
+ * entry accordingly.
+ * Note that if the database contains a location that compares
+ * equal to @loc, that will be chosen in place of @loc.
+ **/
+void
+gis_location_entry_set_location (GisLocationEntry *entry,
+ GWeatherLocation *loc)
+{
+ GtkEntryCompletion *completion;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ g_autoptr(GWeatherLocation) cmploc = NULL;
+
+ g_return_if_fail (GIS_IS_LOCATION_ENTRY (entry));
+
+ completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry));
+ model = gtk_entry_completion_get_model (completion);
+
+ if (loc == NULL) {
+ set_location_internal (entry, model, NULL, NULL);
+ return;
+ }
+
+ gtk_tree_model_get_iter_first (model, &iter);
+ do {
+ gtk_tree_model_get (model, &iter,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &cmploc,
+ -1);
+ if (gweather_location_equal (loc, cmploc)) {
+ set_location_internal (entry, model, &iter, NULL);
+ return;
+ }
+
+ g_clear_object (&cmploc);
+ } while (gtk_tree_model_iter_next (model, &iter));
+
+ set_location_internal (entry, model, NULL, loc);
+}
+
+/**
+ * gis_location_entry_get_location:
+ * @entry: a #GisLocationEntry
+ *
+ * Gets the location that was set by a previous call to
+ * gis_location_entry_set_location() or was selected by the user.
+ *
+ * Return value: (transfer full) (allow-none): the selected location
+ * (which you must unref when you are done with it), or %NULL if no
+ * location is selected.
+ **/
+GWeatherLocation *
+gis_location_entry_get_location (GisLocationEntry *entry)
+{
+ g_return_val_if_fail (GIS_IS_LOCATION_ENTRY (entry), NULL);
+
+ if (entry->priv->location)
+ return g_object_ref (entry->priv->location);
+ else
+ return NULL;
+}
+
+/**
+ * gis_location_entry_set_city:
+ * @entry: a #GisLocationEntry
+ * @city_name: (allow-none): the city name, or %NULL
+ * @code: the METAR station code
+ *
+ * Sets @entry's location to a city with the given @code, and given
+ * @city_name, if non-%NULL. If there is no matching city, sets
+ * @entry's location to %NULL.
+ *
+ * Return value: %TRUE if @entry's location could be set to a matching city,
+ * %FALSE otherwise.
+ **/
+gboolean
+gis_location_entry_set_city (GisLocationEntry *entry,
+ const char *city_name,
+ const char *code)
+{
+ GtkEntryCompletion *completion;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ const char *cmpcode;
+
+ g_return_val_if_fail (GIS_IS_LOCATION_ENTRY (entry), FALSE);
+ g_return_val_if_fail (code != NULL, FALSE);
+
+ completion = gtk_entry_get_completion (GTK_ENTRY (entry->priv->entry));
+ model = gtk_entry_completion_get_model (completion);
+
+ gtk_tree_model_get_iter_first (model, &iter);
+ do {
+ g_autoptr(GWeatherLocation) cmploc = NULL;
+ gtk_tree_model_get (model, &iter,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION, &cmploc,
+ -1);
+
+ cmpcode = gweather_location_get_code (cmploc);
+ if (!cmpcode || strcmp (cmpcode, code) != 0) {
+ continue;
+ }
+
+ if (city_name) {
+ g_autofree gchar *cmpname = gweather_location_get_city_name (cmploc);
+ if (!cmpname || strcmp (cmpname, city_name) != 0) {
+ continue;
+ }
+ }
+
+ set_location_internal (entry, model, &iter, NULL);
+ return TRUE;
+ } while (gtk_tree_model_iter_next (model, &iter));
+
+ set_location_internal (entry, model, NULL, NULL);
+
+ return FALSE;
+}
+
+static void
+fill_location_entry_model (GtkListStore *store, GWeatherLocation *loc,
+ const char *parent_display_name,
+ const char *parent_sort_local_name,
+ const char *parent_compare_local_name,
+ const char *parent_compare_english_name,
+ gboolean show_named_timezones)
+{
+ g_autoptr(GWeatherLocation) child = NULL;
+ char *display_name, *local_sort_name, *local_compare_name, *english_compare_name;
+
+ switch (gweather_location_get_level (loc)) {
+ case GWEATHER_LOCATION_WORLD:
+ case GWEATHER_LOCATION_REGION:
+ /* Ignore these levels of hierarchy; just recurse, passing on
+ * the names from the parent node.
+ */
+ while ((child = gweather_location_next_child (loc, child)))
+ fill_location_entry_model (store, child,
+ parent_display_name,
+ parent_sort_local_name,
+ parent_compare_local_name,
+ parent_compare_english_name,
+ show_named_timezones);
+ break;
+
+ case GWEATHER_LOCATION_COUNTRY:
+ /* Recurse, initializing the names to the country name */
+ while ((child = gweather_location_next_child (loc, child)))
+ fill_location_entry_model (store, child,
+ gweather_location_get_name (loc),
+ gweather_location_get_sort_name (loc),
+ gweather_location_get_sort_name (loc),
+ gweather_location_get_english_sort_name (loc),
+ show_named_timezones);
+ break;
+
+ case GWEATHER_LOCATION_ADM1:
+ /* Recurse, adding the ADM1 name to the country name */
+ /* Translators: this is the name of a location followed by a region, for example:
+ * 'London, United Kingdom'
+ * You shouldn't need to translate this string unless the language has a different comma.
+ */
+ display_name = g_strdup_printf (_("%s, %s"), gweather_location_get_name (loc), parent_display_name);
+ local_sort_name = g_strdup_printf ("%s, %s", parent_sort_local_name, gweather_location_get_sort_name (loc));
+ local_compare_name = g_strdup_printf ("%s, %s", gweather_location_get_sort_name (loc), parent_compare_local_name);
+ english_compare_name = g_strdup_printf ("%s, %s", gweather_location_get_english_sort_name (loc), parent_compare_english_name);
+
+ while ((child = gweather_location_next_child (loc, child)))
+ fill_location_entry_model (store, child,
+ display_name, local_sort_name, local_compare_name, english_compare_name,
+ show_named_timezones);
+
+ g_free (display_name);
+ g_free (local_compare_name);
+ g_free (english_compare_name);
+ break;
+
+ case GWEATHER_LOCATION_CITY:
+ /* If there are multiple (<location>) children, we use the one
+ * closest to the city center.
+ *
+ * Locations are already sorted by increasing distance from
+ * the city.
+ */
+ case GWEATHER_LOCATION_WEATHER_STATION:
+ /* <location> with no parent <city> */
+ /* Translators: this is the name of a location followed by a region, for example:
+ * 'London, United Kingdom'
+ * You shouldn't need to translate this string unless the language has a different comma.
+ */
+ display_name = g_strdup_printf (_("%s, %s"),
+ gweather_location_get_name (loc), parent_display_name);
+ local_sort_name = g_strdup_printf ("%s, %s",
+ parent_sort_local_name, gweather_location_get_sort_name (loc));
+ local_compare_name = g_strdup_printf ("%s, %s",
+ gweather_location_get_sort_name (loc), parent_compare_local_name);
+ english_compare_name = g_strdup_printf ("%s, %s",
+ gweather_location_get_english_sort_name (loc), parent_compare_english_name);
+
+ gtk_list_store_insert_with_values (store, NULL, -1,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION, loc,
+ LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, local_sort_name,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, local_compare_name,
+ LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, english_compare_name,
+ -1);
+
+ g_free (display_name);
+ g_free (local_compare_name);
+ g_free (english_compare_name);
+ break;
+
+ case GWEATHER_LOCATION_NAMED_TIMEZONE:
+ if (show_named_timezones) {
+ gtk_list_store_insert_with_values (store, NULL, -1,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCATION, loc,
+ LOC_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, gweather_location_get_name (loc),
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, gweather_location_get_sort_name (loc),
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, gweather_location_get_sort_name (loc),
+ LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, gweather_location_get_english_sort_name (loc),
+ -1);
+ }
+ break;
+
+ case GWEATHER_LOCATION_DETACHED:
+ g_assert_not_reached ();
+ }
+}
+
+static char *
+find_word (const char *full_name, const char *word, int word_len,
+ gboolean whole_word, gboolean is_first_word)
+{
+ char *p;
+
+ if (word == NULL || *word == '\0')
+ return NULL;
+
+ p = (char *)full_name - 1;
+ while ((p = strchr (p + 1, *word))) {
+ if (strncmp (p, word, word_len) != 0)
+ continue;
+
+ if (p > (char *)full_name) {
+ char *prev = g_utf8_prev_char (p);
+
+ /* Make sure p points to the start of a word */
+ if (g_unichar_isalpha (g_utf8_get_char (prev)))
+ continue;
+
+ /* If we're matching the first word of the key, it has to
+ * match the first word of the location, city, state, or
+ * country, or the abbreviation (in parenthesis).
+ * Eg, it either matches the start of the string
+ * (which we already know it doesn't at this point) or
+ * it is preceded by the string ", " or "(" (which isn't actually
+ * a perfect test. FIXME)
+ */
+ if (is_first_word) {
+ if (prev == (char *)full_name ||
+ ((prev - 1 <= full_name && strncmp (prev - 1, ", ", 2) != 0)
+ && *prev != '('))
+ continue;
+ }
+ }
+
+ if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len)))
+ continue;
+
+ return p;
+ }
+ return NULL;
+}
+
+static gboolean
+match_compare_name (const char *key, const char *name)
+{
+ gboolean is_first_word = TRUE;
+ size_t len;
+
+ /* Ignore whitespace before the string */
+ key += strspn (key, " ");
+
+ /* All but the last word in KEY must match a full word from NAME,
+ * in order (but possibly skipping some words from NAME).
+ */
+ len = strcspn (key, " ");
+ while (key[len]) {
+ name = find_word (name, key, len, TRUE, is_first_word);
+ if (!name)
+ return FALSE;
+
+ key += len;
+ while (*key && !g_unichar_isalpha (g_utf8_get_char (key)))
+ key = g_utf8_next_char (key);
+ while (*name && !g_unichar_isalpha (g_utf8_get_char (name)))
+ name = g_utf8_next_char (name);
+
+ len = strcspn (key, " ");
+ is_first_word = FALSE;
+ }
+
+ /* The last word in KEY must match a prefix of a following word in NAME */
+ if (len == 0) {
+ return TRUE;
+ } else {
+ // if we get here, key[len] == 0, so...
+ g_assert (len == strlen(key));
+ return find_word (name, key, len, FALSE, is_first_word) != NULL;
+ }
+}
+
+static gboolean
+matcher (GtkEntryCompletion *completion, const char *key,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ char *local_compare_name, *english_compare_name;
+ gboolean match;
+
+ gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter,
+ LOC_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, &local_compare_name,
+ LOC_GIS_LOCATION_ENTRY_COL_ENGLISH_COMPARE_NAME, &english_compare_name,
+ -1);
+
+ match = match_compare_name (key, local_compare_name) ||
+ match_compare_name (key, english_compare_name) ||
+ g_ascii_strcasecmp (key, english_compare_name) == 0;
+
+ g_free (local_compare_name);
+ g_free (english_compare_name);
+ return match;
+}
+
+static gboolean
+match_selected (GtkEntryCompletion *completion,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer entry)
+{
+ GisLocationEntryPrivate *priv;
+
+ priv = ((GisLocationEntry *)entry)->priv;
+
+ if (model != priv->model) {
+ GeocodePlace *place;
+ char *display_name;
+ GeocodeLocation *loc;
+ GWeatherLocation *location;
+ GWeatherLocation *scope = NULL;
+ const char* country_code;
+
+ gtk_tree_model_get (model, iter,
+ PLACE_GIS_LOCATION_ENTRY_COL_PLACE, &place,
+ PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, &display_name,
+ -1);
+
+ country_code = geocode_place_get_country_code (place);
+ if (country_code != NULL && gweather_location_get_level (priv->top) == GWEATHER_LOCATION_WORLD)
+ scope = gweather_location_find_by_country_code (priv->top, country_code);
+ if (!scope)
+ scope = priv->top;
+
+ loc = geocode_place_get_location (place);
+ location = gweather_location_new_detached (display_name,
+ NULL,
+ geocode_location_get_latitude (loc),
+ geocode_location_get_longitude (loc));
+
+ set_location_internal (entry, model, NULL, location);
+
+ g_object_unref (place);
+ g_object_unref (location);
+ g_free (display_name);
+ } else {
+ set_location_internal (entry, model, iter, NULL);
+ }
+ return TRUE;
+}
+
+static gboolean
+new_matcher (GtkEntryCompletion *completion, const char *key,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ return TRUE;
+}
+
+static void
+fill_store (gpointer data, gpointer user_data)
+{
+ GeocodePlace *place = GEOCODE_PLACE (data);
+ GeocodeLocation *loc = geocode_place_get_location (place);
+ const char *display_name;
+ char *normalized;
+ char *compare_name;
+
+ display_name = geocode_location_get_description (loc);
+ normalized = g_utf8_normalize (display_name, -1, G_NORMALIZE_ALL);
+ compare_name = g_utf8_casefold (normalized, -1);
+
+ g_debug ("Adding geocode match %s", display_name);
+
+ gtk_list_store_insert_with_values (user_data, NULL, -1,
+ PLACE_GIS_LOCATION_ENTRY_COL_PLACE, place,
+ PLACE_GIS_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
+ PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_SORT_NAME, compare_name,
+ PLACE_GIS_LOCATION_ENTRY_COL_LOCAL_COMPARE_NAME, compare_name,
+ -1);
+
+ g_free (normalized);
+ g_free (compare_name);
+}
+
+static void
+_got_places (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autolist(GeocodePlace) places = NULL;
+ GisLocationEntry *self = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GtkListStore) store = NULL;
+ GtkEntryCompletion *completion;
+
+ places = geocode_forward_search_finish (GEOCODE_FORWARD (source_object), result, &error);
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* return without touching anything if cancelled (the entry might have been disposed) */
+ g_debug ("Geocode query cancelled");
+ return;
+ }
+
+ self = GIS_LOCATION_ENTRY (user_data);
+ completion = gtk_entry_get_completion (GTK_ENTRY (self->priv->entry));
+
+ if (places == NULL) {
+ g_debug ("No geocode results, restoring default model");
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_model (completion, self->priv->model);
+ } else {
+ store = gtk_list_store_new (5, G_TYPE_STRING, GEOCODE_TYPE_PLACE, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+ gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (store),
+ tree_compare_local_name, NULL, NULL);
+ g_list_foreach (places, fill_store, store);
+ gtk_entry_completion_set_match_func (completion, new_matcher, NULL, NULL);
+ gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
+ }
+
+ g_clear_object (&self->priv->cancellable);
+}
+
+static void
+_no_matches (GtkEntryCompletion *completion, GisLocationEntry *entry) {
+ const gchar *key = gtk_editable_get_text (GTK_EDITABLE (entry->priv->entry));
+ GeocodeForward *forward;
+
+ if (entry->priv->cancellable) {
+ g_cancellable_cancel (entry->priv->cancellable);
+ g_object_unref (entry->priv->cancellable);
+ entry->priv->cancellable = NULL;
+ }
+
+ entry->priv->cancellable = g_cancellable_new ();
+
+ g_debug ("Starting geocode query for %s", key);
+ forward = geocode_forward_new_for_string(key);
+ geocode_forward_search_async (forward, entry->priv->cancellable, _got_places, entry);
+}
+
+/**
+ * gis_location_entry_new:
+ * @top: the top-level location for the entry.
+ *
+ * Creates a new #GisLocationEntry.
+ *
+ * @top will normally be the location returned from
+ * gweather_location_get_world(), but you can create an entry that
+ * only accepts a smaller set of locations if you want.
+ *
+ * Return value: the new #GisLocationEntry
+ **/
+GtkWidget *
+gis_location_entry_new (GWeatherLocation *top)
+{
+ return g_object_new (GIS_TYPE_LOCATION_ENTRY,
+ "top", top,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/timezone/gis-location-entry.h b/gnome-initial-setup/pages/timezone/gis-location-entry.h
new file mode 100644
index 0000000..ebd114f
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-location-entry.h
@@ -0,0 +1,46 @@
+/* gweather-location-entry.h - Location-selecting text entry
+ *
+ * SPDX-FileCopyrightText: 2008, Red Hat, Inc.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <adwaita.h>
+#include <libgweather/gweather.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GisLocationEntry GisLocationEntry;
+typedef struct _GisLocationEntryClass GisLocationEntryClass;
+typedef struct _GisLocationEntryPrivate GisLocationEntryPrivate;
+
+#define GIS_TYPE_LOCATION_ENTRY (gis_location_entry_get_type ())
+#define GIS_LOCATION_ENTRY(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), GIS_TYPE_LOCATION_ENTRY, GisLocationEntry))
+#define GIS_LOCATION_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_LOCATION_ENTRY, GisLocationEntryClass))
+#define GIS_IS_LOCATION_ENTRY(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), GIS_TYPE_LOCATION_ENTRY))
+#define GIS_IS_LOCATION_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_LOCATION_ENTRY))
+#define GIS_LOCATION_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_LOCATION_ENTRY, GisLocationEntryClass))
+
+struct _GisLocationEntry {
+ AdwBin parent;
+
+ /*< private >*/
+ GisLocationEntryPrivate *priv;
+};
+
+struct _GisLocationEntryClass {
+ AdwBinClass parent_class;
+};
+
+GType gis_location_entry_get_type (void);
+
+GtkWidget * gis_location_entry_new (GWeatherLocation *top);
+void gis_location_entry_set_location (GisLocationEntry *entry,
+ GWeatherLocation *loc);
+GWeatherLocation * gis_location_entry_get_location (GisLocationEntry *entry);
+gboolean gis_location_entry_set_city (GisLocationEntry *entry,
+ const char *city_name,
+ const char *code);
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/timezone/gis-timezone-page.c b/gnome-initial-setup/pages/timezone/gis-timezone-page.c
new file mode 100644
index 0000000..2ca34d0
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-timezone-page.c
@@ -0,0 +1,539 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+/* Timezone page {{{1 */
+
+#define PAGE_ID "timezone"
+
+#include "config.h"
+#include "gis-timezone-page.h"
+
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+#include <libgnome-desktop/gnome-wall-clock.h>
+#include <gdesktop-enums.h>
+#include <geoclue.h>
+#include <geocode-glib/geocode-glib.h>
+
+#include <libgweather/gweather.h>
+
+#include "timedated.h"
+#include "cc-datetime-resources.h"
+#include "timezone-resources.h"
+
+#include "cc-timezone-map.h"
+#include "gis-bubble-widget.h"
+
+#include "gis-page-header.h"
+#include "gis-location-entry.h"
+
+#define DEFAULT_TZ "Europe/London"
+#define DESKTOP_ID "gnome-datetime-panel"
+
+#define CLOCK_SCHEMA "org.gnome.desktop.interface"
+#define CLOCK_FORMAT_KEY "clock-format"
+
+/* FIXME: Drop this when we depend on a version of GeoClue which has
+ * https://gitlab.freedesktop.org/geoclue/geoclue/-/merge_requests/73 */
+typedef GClueSimple MyGClueSimple;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MyGClueSimple, g_object_unref)
+
+static void stop_geolocation (GisTimezonePage *page);
+
+struct _GisTimezonePagePrivate
+{
+ GtkWidget *map;
+ GtkWidget *search_entry;
+ GtkWidget *search_overlay;
+
+ GCancellable *geoclue_cancellable;
+ GClueClient *geoclue_client;
+ GClueSimple *geoclue_simple;
+ gboolean in_geoclue_callback;
+ GWeatherLocation *current_location;
+ Timedate1 *dtm;
+ GCancellable *dtm_cancellable;
+
+ GnomeWallClock *clock;
+ GDesktopClockFormat clock_format;
+ gboolean in_search;
+
+ gulong search_entry_text_changed_id;
+};
+typedef struct _GisTimezonePagePrivate GisTimezonePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisTimezonePage, gis_timezone_page, GIS_TYPE_PAGE);
+
+static void
+set_timezone_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ Timedate1 *dtm = TIMEDATE1 (source);
+ g_autoptr(GError) error = NULL;
+
+ if (!timedate1_call_set_timezone_finish (dtm,
+ res,
+ &error)) {
+ /* TODO: display any error in a user friendly way */
+ g_warning ("Could not set system timezone: %s", error->message);
+ }
+}
+
+
+static void
+queue_set_timezone (GisTimezonePage *page,
+ const char *tzid)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ /* for now just do it */
+ timedate1_call_set_timezone (priv->dtm,
+ tzid,
+ TRUE,
+ priv->dtm_cancellable,
+ set_timezone_cb,
+ page);
+}
+
+static void
+set_location (GisTimezonePage *page,
+ GWeatherLocation *location)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ g_clear_object (&priv->current_location);
+
+ gtk_widget_set_visible (priv->search_overlay, (location == NULL));
+ gis_page_set_complete (GIS_PAGE (page), (location != NULL));
+
+ if (location)
+ {
+ GTimeZone *zone;
+ const char *tzid;
+
+ priv->current_location = g_object_ref (location);
+
+ zone = gweather_location_get_timezone (location);
+ tzid = g_time_zone_get_identifier (zone);
+
+ cc_timezone_map_set_timezone (CC_TIMEZONE_MAP (priv->map), tzid);
+
+ /* If this location is manually set, stop waiting for geolocation. */
+ if (!priv->in_geoclue_callback)
+ stop_geolocation (page);
+ }
+}
+
+static void
+on_location_notify (GClueSimple *simple,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GisTimezonePage *page = user_data;
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ GClueLocation *location;
+ gdouble latitude, longitude;
+ g_autoptr(GWeatherLocation) world = gweather_location_get_world ();
+ g_autoptr(GWeatherLocation) glocation = NULL;
+
+ location = gclue_simple_get_location (simple);
+
+ latitude = gclue_location_get_latitude (location);
+ longitude = gclue_location_get_longitude (location);
+
+ glocation = gweather_location_find_nearest_city (world, latitude, longitude);
+ priv->in_geoclue_callback = TRUE;
+ set_location (page, glocation);
+ priv->in_geoclue_callback = FALSE;
+}
+
+static void
+on_geoclue_simple_ready (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GisTimezonePage *page = user_data;
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(MyGClueSimple) geoclue_simple = NULL;
+
+ /* This function may be called in an idle callback once @page has been
+ * disposed, if going through cancellation. So don’t dereference @priv or
+ * @page until the error has been checked. */
+ geoclue_simple = gclue_simple_new_finish (res, &local_error);
+ if (local_error != NULL)
+ {
+ if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_info ("Failed to connect to GeoClue2 service: %s", local_error->message);
+ return;
+ }
+
+ priv->geoclue_simple = g_steal_pointer (&geoclue_simple);
+ priv->geoclue_client = gclue_simple_get_client (priv->geoclue_simple);
+ gclue_client_set_distance_threshold (priv->geoclue_client,
+ GEOCODE_LOCATION_ACCURACY_CITY);
+
+ g_signal_connect (priv->geoclue_simple, "notify::location",
+ G_CALLBACK (on_location_notify), page);
+
+ on_location_notify (priv->geoclue_simple, NULL, page);
+}
+
+static void
+get_location_from_geoclue_async (GisTimezonePage *page)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ gclue_simple_new (DESKTOP_ID,
+ GCLUE_ACCURACY_LEVEL_CITY,
+ priv->geoclue_cancellable,
+ on_geoclue_simple_ready,
+ page);
+}
+
+static void
+entry_text_changed (GtkEditable *editable,
+ gpointer user_data)
+{
+ GisTimezonePage *page = GIS_TIMEZONE_PAGE (user_data);
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ stop_geolocation (GIS_TIMEZONE_PAGE (user_data));
+ g_signal_handler_disconnect (priv->search_entry,
+ priv->search_entry_text_changed_id);
+ priv->search_entry_text_changed_id = 0;
+}
+
+static void
+entry_location_changed (GObject *object, GParamSpec *param, GisTimezonePage *page)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ GisLocationEntry *entry = GIS_LOCATION_ENTRY (object);
+ g_autoptr(GWeatherLocation) location = NULL;
+
+ location = gis_location_entry_get_location (entry);
+ if (!location)
+ return;
+
+ priv->in_search = TRUE;
+ set_location (page, location);
+ priv->in_search = FALSE;
+}
+
+#define GETTEXT_PACKAGE_TIMEZONES "gnome-control-center-2.0-timezones"
+
+static char *
+translated_city_name (TzLocation *loc)
+{
+ char *country;
+ char *name;
+ char *zone_translated;
+ char **split_translated;
+ gint length;
+
+ /* Load the translation for it */
+ zone_translated = g_strdup (dgettext (GETTEXT_PACKAGE_TIMEZONES, loc->zone));
+ g_strdelimit (zone_translated, "_", ' ');
+ split_translated = g_regex_split_simple ("[\\x{2044}\\x{2215}\\x{29f8}\\x{ff0f}/]",
+ zone_translated,
+ 0, 0);
+ g_free (zone_translated);
+
+ length = g_strv_length (split_translated);
+
+ country = gnome_get_country_from_code (loc->country, NULL);
+ /* Translators: "city, country" */
+ name = g_strdup_printf (C_("timezone loc", "%s, %s"),
+ split_translated[length-1],
+ country);
+ g_free (country);
+ g_strfreev (split_translated);
+
+ return name;
+}
+
+static void
+update_timezone (GisTimezonePage *page, TzLocation *location)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ char *tz_desc;
+ char *bubble_text;
+ char *city_country;
+ char *utc_label;
+ char *time_label;
+ GTimeZone *zone;
+ GDateTime *date;
+ gboolean use_ampm;
+
+ if (priv->clock_format == G_DESKTOP_CLOCK_FORMAT_12H)
+ use_ampm = TRUE;
+ else
+ use_ampm = FALSE;
+
+ zone = g_time_zone_new (location->zone);
+ date = g_date_time_new_now (zone);
+ g_time_zone_unref (zone);
+
+ /* Update the text bubble in the timezone map */
+ city_country = translated_city_name (location);
+
+ /* Translators: UTC here means the Coordinated Universal Time.
+ * %:::z will be replaced by the offset from UTC e.g. UTC+02
+ */
+ utc_label = g_date_time_format (date, _("UTC%:::z"));
+
+ if (use_ampm)
+ /* Translators: This is the time format used in 12-hour mode. */
+ time_label = g_date_time_format (date, _("%l:%M %p"));
+ else
+ /* Translators: This is the time format used in 24-hour mode. */
+ time_label = g_date_time_format (date, _("%R"));
+
+ /* Translators: "timezone (utc shift)" */
+ tz_desc = g_strdup_printf (C_("timezone map", "%s (%s)"),
+ g_date_time_get_timezone_abbreviation (date),
+ utc_label);
+ bubble_text = g_strdup_printf ("<b>%s</b>\n"
+ "<small>%s</small>\n"
+ "<b>%s</b>",
+ tz_desc,
+ city_country,
+ time_label);
+ cc_timezone_map_set_bubble_text (CC_TIMEZONE_MAP (priv->map), bubble_text);
+
+ g_free (tz_desc);
+ g_free (city_country);
+ g_free (utc_label);
+ g_free (time_label);
+ g_free (bubble_text);
+
+ g_date_time_unref (date);
+}
+
+static void
+map_location_changed (CcTimezoneMap *map,
+ TzLocation *location,
+ GisTimezonePage *page)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ gtk_widget_set_visible (priv->search_overlay, (location == NULL));
+ gis_page_set_complete (GIS_PAGE (page), (location != NULL));
+
+ if (!priv->in_search)
+ gtk_editable_set_text (GTK_EDITABLE (priv->search_entry), "");
+
+ update_timezone (page, location);
+ queue_set_timezone (page, location->zone);
+}
+
+static void
+on_clock_changed (GnomeWallClock *clock,
+ GParamSpec *pspec,
+ GisTimezonePage *page)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ TzLocation *location;
+
+ if (!gtk_widget_get_mapped (priv->map))
+ return;
+
+ if (gtk_widget_is_visible (priv->search_overlay))
+ return;
+
+ location = cc_timezone_map_get_location (CC_TIMEZONE_MAP (priv->map));
+ if (location)
+ update_timezone (page, location);
+}
+
+static void
+entry_mapped (GtkWidget *widget,
+ gpointer user_data)
+{
+ gtk_widget_grab_focus (widget);
+}
+
+static void
+stop_geolocation (GisTimezonePage *page)
+{
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ if (priv->geoclue_cancellable)
+ {
+ g_cancellable_cancel (priv->geoclue_cancellable);
+ g_clear_object (&priv->geoclue_cancellable);
+ }
+
+ if (priv->geoclue_client)
+ {
+ gclue_client_call_stop (priv->geoclue_client, NULL, NULL, NULL);
+ priv->geoclue_client = NULL;
+ }
+ g_clear_object (&priv->geoclue_simple);
+}
+
+static void
+gis_timezone_page_root (GtkWidget *widget)
+{
+ GisTimezonePage *page = GIS_TIMEZONE_PAGE (widget);
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ GTK_WIDGET_CLASS (gis_timezone_page_parent_class)->root (widget);
+
+ if (priv->geoclue_cancellable == NULL)
+ {
+ priv->geoclue_cancellable = g_cancellable_new ();
+ get_location_from_geoclue_async (page);
+ }
+}
+
+static void
+gis_timezone_page_constructed (GObject *object)
+{
+ GisTimezonePage *page = GIS_TIMEZONE_PAGE (object);
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+ GError *error;
+ GSettings *settings;
+
+ G_OBJECT_CLASS (gis_timezone_page_parent_class)->constructed (object);
+
+ priv->dtm_cancellable = g_cancellable_new ();
+
+ error = NULL;
+ priv->dtm = timedate1_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "org.freedesktop.timedate1",
+ "/org/freedesktop/timedate1",
+ priv->dtm_cancellable,
+ &error);
+ if (priv->dtm == NULL) {
+ g_error ("Failed to create proxy for timedated: %s", error->message);
+ exit (1);
+ }
+
+ priv->clock = g_object_new (GNOME_TYPE_WALL_CLOCK, NULL);
+ g_signal_connect (priv->clock, "notify::clock", G_CALLBACK (on_clock_changed), page);
+
+ settings = g_settings_new (CLOCK_SCHEMA);
+ priv->clock_format = g_settings_get_enum (settings, CLOCK_FORMAT_KEY);
+ g_object_unref (settings);
+
+ set_location (page, NULL);
+
+ priv->search_entry_text_changed_id =
+ g_signal_connect (priv->search_entry, "changed",
+ G_CALLBACK (entry_text_changed), page);
+ g_signal_connect (priv->search_entry, "notify::location",
+ G_CALLBACK (entry_location_changed), page);
+ g_signal_connect (priv->search_entry, "map",
+ G_CALLBACK (entry_mapped), page);
+ g_signal_connect (priv->map, "location-changed",
+ G_CALLBACK (map_location_changed), page);
+
+ gtk_widget_show (GTK_WIDGET (page));
+}
+
+static void
+gis_timezone_page_dispose (GObject *object)
+{
+ GisTimezonePage *page = GIS_TIMEZONE_PAGE (object);
+ GisTimezonePagePrivate *priv = gis_timezone_page_get_instance_private (page);
+
+ stop_geolocation (page);
+
+ if (priv->dtm_cancellable != NULL)
+ {
+ g_cancellable_cancel (priv->dtm_cancellable);
+ g_clear_object (&priv->dtm_cancellable);
+ }
+
+ g_clear_object (&priv->dtm);
+ g_clear_object (&priv->clock);
+
+ G_OBJECT_CLASS (gis_timezone_page_parent_class)->dispose (object);
+}
+
+static void
+gis_timezone_page_locale_changed (GisPage *page)
+{
+ gis_page_set_title (GIS_PAGE (page), _("Time Zone"));
+}
+
+static gboolean
+gis_timezone_page_apply (GisPage *page,
+ GCancellable *cancellable)
+{
+ /* Once the user accepts the location, it would be unkind to change it if
+ * GeoClue suddenly tells us we're somewhere else.
+ */
+ stop_geolocation (GIS_TIMEZONE_PAGE (page));
+
+ return FALSE;
+}
+
+static void
+gis_timezone_page_class_init (GisTimezonePageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (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/initial-setup/gis-timezone-page.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, map);
+ gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, search_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisTimezonePage, search_overlay);
+
+ page_class->page_id = PAGE_ID;
+ page_class->locale_changed = gis_timezone_page_locale_changed;
+ page_class->apply = gis_timezone_page_apply;
+ object_class->constructed = gis_timezone_page_constructed;
+ object_class->dispose = gis_timezone_page_dispose;
+ widget_class->root = gis_timezone_page_root;
+}
+
+static void
+gis_timezone_page_init (GisTimezonePage *page)
+{
+ g_resources_register (timezone_get_resource ());
+ g_resources_register (datetime_get_resource ());
+ g_type_ensure (CC_TYPE_TIMEZONE_MAP);
+ g_type_ensure (GIS_TYPE_BUBBLE_WIDGET);
+ g_type_ensure (GIS_TYPE_PAGE_HEADER);
+ g_type_ensure (GIS_TYPE_LOCATION_ENTRY);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_timezone_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_TIMEZONE_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/timezone/gis-timezone-page.h b/gnome-initial-setup/pages/timezone/gis-timezone-page.h
new file mode 100644
index 0000000..e9ba8ed
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-timezone-page.h
@@ -0,0 +1,57 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ *
+ * 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:
+ * Jasper St. Pierre <jstpierre@mecheye.net>
+ */
+
+#ifndef __GIS_TIMEZONE_PAGE_H__
+#define __GIS_TIMEZONE_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_TIMEZONE_PAGE (gis_timezone_page_get_type ())
+#define GIS_TIMEZONE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_TIMEZONE_PAGE, GisTimezonePage))
+#define GIS_TIMEZONE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_TIMEZONE_PAGE, GisTimezonePageClass))
+#define GIS_IS_TIMEZONE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_TIMEZONE_PAGE))
+#define GIS_IS_TIMEZONE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_TIMEZONE_PAGE))
+#define GIS_TIMEZONE_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_TIMEZONE_PAGE, GisTimezonePageClass))
+
+typedef struct _GisTimezonePage GisTimezonePage;
+typedef struct _GisTimezonePageClass GisTimezonePageClass;
+
+struct _GisTimezonePage
+{
+ GisPage parent;
+};
+
+struct _GisTimezonePageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_timezone_page_get_type (void);
+
+GisPage *gis_prepare_timezone_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_TIMEZONE_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/timezone/gis-timezone-page.ui b/gnome-initial-setup/pages/timezone/gis-timezone-page.ui
new file mode 100644
index 0000000..a48b53e
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/gis-timezone-page.ui
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GisTimezonePage" parent="GisPage">
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="halign">center</property>
+ <property name="valign">fill</property>
+ <child>
+ <object class="GisPageHeader" id="header">
+ <property name="margin_top">24</property>
+ <property name="title" translatable="yes">Time Zone</property>
+ <property name="subtitle" translatable="yes">The time zone will be set automatically if your location can be found. You can also search for a city to set it yourself.</property>
+ <property name="icon_name">find-location-symbolic</property>
+ <property name="show_icon" bind-source="GisTimezonePage" bind-property="small-screen" bind-flags="invert-boolean|sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="page_box">
+ <property name="margin_top">18</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">14</property>
+ <child>
+ <object class="GisLocationEntry" id="search_entry">
+ <property name="halign">center</property>
+ <property name="max-width-chars">55</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkFrame" id="map_frame">
+ <property name="margin_bottom">18</property>
+ <property name="hexpand">True</property>
+ <property name="label_xalign">0</property>
+ <child>
+ <object class="GtkOverlay" id="map_overlay">
+ <child>
+ <object class="CcTimezoneMap" id="map">
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GisBubbleWidget" id="search_overlay">
+ <property name="label" translatable="yes">Please search for a nearby city</property>
+ <property name="icon-name">edit-find-symbolic</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/timezone/meson.build b/gnome-initial-setup/pages/timezone/meson.build
new file mode 100644
index 0000000..54a974e
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/meson.build
@@ -0,0 +1,45 @@
+sources += gnome.gdbus_codegen(
+ 'timedated',
+ 'timedated1-interface.xml',
+ interface_prefix: 'org.freedesktop.',
+)
+
+sources += gnome.compile_resources(
+ 'cc-datetime-resources',
+ files('datetime.gresource.xml'),
+ c_name: 'datetime'
+)
+
+sources += gnome.compile_resources(
+ 'timezone-resources',
+ files('timezone.gresource.xml'),
+ c_name: 'timezone'
+)
+
+sources += files(
+ 'cc-timezone-map.c',
+ 'cc-timezone-map.h',
+ 'tz.c',
+ 'tz.h',
+ 'gis-bubble-widget.c',
+ 'gis-bubble-widget.h',
+ 'gis-location-entry.c',
+ 'gis-location-entry.h',
+ 'gis-timezone-page.c',
+ 'gis-timezone-page.h'
+)
+
+executable('test-gis-location-entry',
+ files(
+ 'gis-location-entry.c',
+ 'gis-location-entry.h',
+ 'test-gis-location-entry.c',
+ ),
+ dependencies: [
+ libadwaita_dep,
+ gweather_dep,
+ geocode_glib_2_dep,
+ ],
+ include_directories: config_h_dir,
+ install: false,
+) \ No newline at end of file
diff --git a/gnome-initial-setup/pages/timezone/test-gis-location-entry.c b/gnome-initial-setup/pages/timezone/test-gis-location-entry.c
new file mode 100644
index 0000000..c6413e8
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/test-gis-location-entry.c
@@ -0,0 +1,54 @@
+#include "config.h"
+
+#include "gis-location-entry.h"
+
+#include <adwaita.h>
+
+static void
+entry_location_changed (GObject *object,
+ GParamSpec *param,
+ gpointer data)
+{
+ GtkLabel *label = GTK_LABEL (data);
+ GisLocationEntry *entry = GIS_LOCATION_ENTRY (object);
+ g_autoptr(GWeatherLocation) location = NULL;
+ const gchar *message = NULL;
+
+ location = gis_location_entry_get_location (entry);
+ message = location != NULL ? gweather_location_get_name (location) : "No location selected";
+ g_message ("%s: %s", G_STRLOC, message);
+ gtk_label_set_text (label, message);
+}
+
+static void
+activate_cb (GtkApplication *app)
+{
+ GtkWidget *window = gtk_application_window_new (app);
+ GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *label = gtk_label_new ("Pick a location…");
+ GtkWidget *entry = gis_location_entry_new (NULL);
+
+ gtk_box_append (GTK_BOX (box), label);
+ gtk_box_append (GTK_BOX (box), entry);
+
+ g_signal_connect (entry, "notify::location",
+ G_CALLBACK (entry_location_changed), label);
+
+ gtk_window_set_title (GTK_WINDOW (window), "Hello");
+ gtk_window_set_default_size (GTK_WINDOW (window), 1024, 768);
+ gtk_window_set_child (GTK_WINDOW (window), box);
+ gtk_window_present (GTK_WINDOW (window));
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_autoptr (AdwApplication) app = NULL;
+
+ app = adw_application_new ("org.gnome.InitialSetup.TestLocationEntry", G_APPLICATION_FLAGS_NONE);
+
+ g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL);
+
+ return g_application_run (G_APPLICATION (app), argc, argv);
+}
diff --git a/gnome-initial-setup/pages/timezone/timedated1-interface.xml b/gnome-initial-setup/pages/timezone/timedated1-interface.xml
new file mode 100644
index 0000000..3370e0e
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/timedated1-interface.xml
@@ -0,0 +1,28 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.freedesktop.timedate1">
+ <property name="Timezone" type="s" access="read"/>
+ <property name="LocalRTC" type="b" access="read"/>
+ <property name="CanNTP" type="b" access="read"/>
+ <property name="NTP" type="b" access="read"/>
+ <method name="SetTime">
+ <arg name="usec_utc" type="x" direction="in"/>
+ <arg name="relative" type="b" direction="in"/>
+ <arg name="user_interaction" type="b" direction="in"/>
+ </method>
+ <method name="SetTimezone">
+ <arg name="timezone" type="s" direction="in"/>
+ <arg name="user_interaction" type="b" direction="in"/>
+ </method>
+ <method name="SetLocalRTC">
+ <arg name="local_rtc" type="b" direction="in"/>
+ <arg name="fix_system" type="b" direction="in"/>
+ <arg name="user_interaction" type="b" direction="in"/>
+ </method>
+ <method name="SetNTP">
+ <arg name="use_ntp" type="b" direction="in"/>
+ <arg name="user_interaction" type="b" direction="in"/>
+ </method>
+ </interface>
+</node>
diff --git a/gnome-initial-setup/pages/timezone/timezone.gresource.xml b/gnome-initial-setup/pages/timezone/timezone.gresource.xml
new file mode 100644
index 0000000..cf36818
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/timezone.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-timezone-page.ui">gis-timezone-page.ui</file>
+ <file preprocess="xml-stripblanks" alias="gis-bubble-widget.ui">gis-bubble-widget.ui</file>
+ <file alias="gis-bubble-widget.css">gis-bubble-widget.css</file>
+ </gresource>
+</gresources>
diff --git a/gnome-initial-setup/pages/timezone/tz.c b/gnome-initial-setup/pages/timezone/tz.c
new file mode 100644
index 0000000..4fe2305
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/tz.c
@@ -0,0 +1,492 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* Generic timezone utilities.
+ *
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Authors: Hans Petter Jansson <hpj@ximian.com>
+ *
+ * Largely based on Michael Fulbright's work on Anaconda.
+ *
+ * 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/>.
+ */
+
+
+#include <glib.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <math.h>
+#include <string.h>
+#include <ctype.h>
+#include "tz.h"
+#include "cc-datetime-resources.h"
+
+
+/* Forward declarations for private functions */
+
+static float convert_pos (gchar *pos, int digits);
+static int compare_country_names (const void *a, const void *b);
+static void sort_locations_by_country (GPtrArray *locations);
+static gchar * tz_data_file_get (void);
+static void load_backward_tz (TzDB *tz_db);
+
+/* ---------------- *
+ * Public interface *
+ * ---------------- */
+TzDB *
+tz_load_db (void)
+{
+ g_autofree gchar *tz_data_file = NULL;
+ TzDB *tz_db;
+ FILE *tzfile;
+ char buf[4096];
+
+ tz_data_file = tz_data_file_get ();
+ if (!tz_data_file) {
+ g_warning ("Could not get the TimeZone data file name");
+ return NULL;
+ }
+ tzfile = fopen (tz_data_file, "r");
+ if (!tzfile) {
+ g_warning ("Could not open *%s*\n", tz_data_file);
+ return NULL;
+ }
+
+ tz_db = g_new0 (TzDB, 1);
+ tz_db->locations = g_ptr_array_new ();
+
+ while (fgets (buf, sizeof(buf), tzfile))
+ {
+ g_auto(GStrv) tmpstrarr = NULL;
+ g_autofree gchar *latstr = NULL;
+ g_autofree gchar *lngstr = NULL;
+ gchar *p;
+ TzLocation *loc;
+
+ if (*buf == '#') continue;
+
+ g_strchomp(buf);
+ tmpstrarr = g_strsplit(buf,"\t", 6);
+
+ latstr = g_strdup (tmpstrarr[1]);
+ p = latstr + 1;
+ while (*p != '-' && *p != '+') p++;
+ lngstr = g_strdup (p);
+ *p = '\0';
+
+ loc = g_new0 (TzLocation, 1);
+ loc->country = g_strdup (tmpstrarr[0]);
+ loc->zone = g_strdup (tmpstrarr[2]);
+ loc->latitude = convert_pos (latstr, 2);
+ loc->longitude = convert_pos (lngstr, 3);
+
+#ifdef __sun
+ if (tmpstrarr[3] && *tmpstrarr[3] == '-' && tmpstrarr[4])
+ loc->comment = g_strdup (tmpstrarr[4]);
+
+ if (tmpstrarr[3] && *tmpstrarr[3] != '-' && !islower(loc->zone)) {
+ TzLocation *locgrp;
+
+ /* duplicate entry */
+ locgrp = g_new0 (TzLocation, 1);
+ locgrp->country = g_strdup (tmpstrarr[0]);
+ locgrp->zone = g_strdup (tmpstrarr[3]);
+ locgrp->latitude = convert_pos (latstr, 2);
+ locgrp->longitude = convert_pos (lngstr, 3);
+ locgrp->comment = (tmpstrarr[4]) ? g_strdup (tmpstrarr[4]) : NULL;
+
+ g_ptr_array_add (tz_db->locations, (gpointer) locgrp);
+ }
+#else
+ loc->comment = (tmpstrarr[3]) ? g_strdup(tmpstrarr[3]) : NULL;
+#endif
+
+ g_ptr_array_add (tz_db->locations, (gpointer) loc);
+ }
+
+ fclose (tzfile);
+
+ /* now sort by country */
+ sort_locations_by_country (tz_db->locations);
+
+ /* Load up the hashtable of backward links */
+ load_backward_tz (tz_db);
+
+ return tz_db;
+}
+
+static void
+tz_location_free (TzLocation *loc)
+{
+ g_free (loc->country);
+ g_free (loc->zone);
+ g_free (loc->comment);
+
+ g_free (loc);
+}
+
+void
+tz_db_free (TzDB *db)
+{
+ g_ptr_array_foreach (db->locations, (GFunc) tz_location_free, NULL);
+ g_ptr_array_free (db->locations, TRUE);
+ g_hash_table_destroy (db->backward);
+ g_free (db);
+}
+
+GPtrArray *
+tz_get_locations (TzDB *db)
+{
+ return db->locations;
+}
+
+
+gchar *
+tz_location_get_country (TzLocation *loc)
+{
+ return loc->country;
+}
+
+
+gchar *
+tz_location_get_zone (TzLocation *loc)
+{
+ return loc->zone;
+}
+
+
+gchar *
+tz_location_get_comment (TzLocation *loc)
+{
+ return loc->comment;
+}
+
+
+void
+tz_location_get_position (TzLocation *loc, double *longitude, double *latitude)
+{
+ *longitude = loc->longitude;
+ *latitude = loc->latitude;
+}
+
+/* For timezone map display purposes, we try to highlight regions of the
+ * world that keep the same time. There is no reasonable API to discover
+ * this; at the moment we just group timezones by their non-daylight-savings
+ * UTC offset and hope that's good enough. However, in some cases that
+ * produces confusing results. For example, Irish Standard Time is legally
+ * defined as the country's summer time, with a negative DST offset in
+ * winter; but this results in the same observed clock times as countries
+ * that observe Western European (Summer) Time, not those that observe
+ * Central European (Summer) Time, so we should group Ireland with the
+ * former, matching the grouping implied by data/timezone_*.png.
+ *
+ * This is something of a hack, and there remain other problems with
+ * timezone grouping: for example, grouping timezones north and south of the
+ * equator together where DST is observed at different times of the year is
+ * dubious.
+ */
+struct {
+ const char *zone;
+ gint offset;
+} base_offset_overrides[] = {
+ { "Europe/Dublin", 0 },
+};
+
+glong
+tz_location_get_base_utc_offset (TzLocation *loc)
+{
+ g_autoptr(TzInfo) tz_info = NULL;
+ glong offset;
+ guint i;
+
+ tz_info = tz_info_from_location (loc);
+ offset = tz_info->utc_offset + (tz_info->daylight ? -3600 : 0);
+
+ for (i = 0; i < G_N_ELEMENTS (base_offset_overrides); i++) {
+ if (g_str_equal (loc->zone, base_offset_overrides[i].zone)) {
+ offset = base_offset_overrides[i].offset;
+ break;
+ }
+ }
+
+ return offset;
+}
+
+TzInfo *
+tz_info_from_location (TzLocation *loc)
+{
+ TzInfo *tzinfo;
+ time_t curtime;
+ struct tm *curzone;
+ g_autofree gchar *tz_env_value = NULL;
+
+ g_return_val_if_fail (loc != NULL, NULL);
+ g_return_val_if_fail (loc->zone != NULL, NULL);
+
+ tz_env_value = g_strdup (getenv ("TZ"));
+ setenv ("TZ", loc->zone, 1);
+
+#if 0
+ tzset ();
+#endif
+ tzinfo = g_new0 (TzInfo, 1);
+
+ curtime = time (NULL);
+ curzone = localtime (&curtime);
+
+#ifndef __sun
+ tzinfo->tzname = g_strdup (curzone->tm_zone);
+ tzinfo->utc_offset = curzone->tm_gmtoff;
+#else
+ tzinfo->tzname = NULL;
+ tzinfo->utc_offset = 0;
+#endif
+
+ tzinfo->daylight = curzone->tm_isdst;
+
+ if (tz_env_value)
+ setenv ("TZ", tz_env_value, 1);
+ else
+ unsetenv ("TZ");
+
+ return tzinfo;
+}
+
+
+void
+tz_info_free (TzInfo *tzinfo)
+{
+ g_return_if_fail (tzinfo != NULL);
+
+ if (tzinfo->tzname) g_free (tzinfo->tzname);
+ g_free (tzinfo);
+}
+
+struct {
+ const char *orig;
+ const char *dest;
+} aliases[] = {
+ { "Asia/Istanbul", "Europe/Istanbul" }, /* Istanbul is in both Europe and Asia */
+ { "Europe/Nicosia", "Asia/Nicosia" }, /* Ditto */
+ { "EET", "Europe/Istanbul" }, /* Same tz as the 2 above */
+ { "HST", "Pacific/Honolulu" },
+ { "WET", "Europe/Brussels" }, /* Other name for the mainland Europe tz */
+ { "CET", "Europe/Brussels" }, /* ditto */
+ { "MET", "Europe/Brussels" },
+ { "Etc/Zulu", "Etc/GMT" },
+ { "Etc/UTC", "Etc/GMT" },
+ { "GMT", "Etc/GMT" },
+ { "Greenwich", "Etc/GMT" },
+ { "Etc/UCT", "Etc/GMT" },
+ { "Etc/GMT0", "Etc/GMT" },
+ { "Etc/GMT+0", "Etc/GMT" },
+ { "Etc/GMT-0", "Etc/GMT" },
+ { "Etc/Universal", "Etc/GMT" },
+ { "PST8PDT", "America/Los_Angeles" }, /* Other name for the Atlantic tz */
+ { "EST", "America/New_York" }, /* Other name for the Eastern tz */
+ { "EST5EDT", "America/New_York" }, /* ditto */
+ { "CST6CDT", "America/Chicago" }, /* Other name for the Central tz */
+ { "MST", "America/Denver" }, /* Other name for the mountain tz */
+ { "MST7MDT", "America/Denver" }, /* ditto */
+};
+
+static gboolean
+compare_timezones (const char *a,
+ const char *b)
+{
+ if (g_str_equal (a, b))
+ return TRUE;
+ if (strchr (b, '/') == NULL) {
+ g_autofree gchar *prefixed = NULL;
+
+ prefixed = g_strdup_printf ("/%s", b);
+ if (g_str_has_suffix (a, prefixed))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+char *
+tz_info_get_clean_name (TzDB *tz_db,
+ const char *tz)
+{
+ char *ret;
+ const char *timezone;
+ guint i;
+ gboolean replaced;
+
+ /* Remove useless prefixes */
+ if (g_str_has_prefix (tz, "right/"))
+ tz = tz + strlen ("right/");
+ else if (g_str_has_prefix (tz, "posix/"))
+ tz = tz + strlen ("posix/");
+
+ /* Here start the crazies */
+ replaced = FALSE;
+
+ for (i = 0; i < G_N_ELEMENTS (aliases); i++) {
+ if (compare_timezones (tz, aliases[i].orig)) {
+ replaced = TRUE;
+ timezone = aliases[i].dest;
+ break;
+ }
+ }
+
+ /* Try again! */
+ if (!replaced) {
+ /* Ignore crazy solar times from the '80s */
+ if (g_str_has_prefix (tz, "Asia/Riyadh") ||
+ g_str_has_prefix (tz, "Mideast/Riyadh")) {
+ timezone = "Asia/Riyadh";
+ replaced = TRUE;
+ }
+ }
+
+ if (!replaced)
+ timezone = tz;
+
+ ret = g_hash_table_lookup (tz_db->backward, timezone);
+ if (ret == NULL)
+ return g_strdup (timezone);
+ return g_strdup (ret);
+}
+
+/* ----------------- *
+ * Private functions *
+ * ----------------- */
+
+static gchar *
+tz_data_file_get (void)
+{
+ gchar *file;
+
+ file = g_strdup (TZ_DATA_FILE);
+
+ return file;
+}
+
+static float
+convert_pos (gchar *pos, int digits)
+{
+ gchar whole[10];
+ gchar *fraction;
+ gint i;
+ float t1, t2;
+
+ if (!pos || strlen(pos) < 4 || digits > 9) return 0.0;
+
+ for (i = 0; i < digits + 1; i++) whole[i] = pos[i];
+ whole[i] = '\0';
+ fraction = pos + digits + 1;
+
+ t1 = g_strtod (whole, NULL);
+ t2 = g_strtod (fraction, NULL);
+
+ if (t1 >= 0.0) return t1 + t2/pow (10.0, strlen(fraction));
+ else return t1 - t2/pow (10.0, strlen(fraction));
+}
+
+
+#if 0
+
+/* Currently not working */
+static void
+free_tzdata (TzLocation *tz)
+{
+
+ if (tz->country)
+ g_free(tz->country);
+ if (tz->zone)
+ g_free(tz->zone);
+ if (tz->comment)
+ g_free(tz->comment);
+
+ g_free(tz);
+}
+#endif
+
+
+static int
+compare_country_names (const void *a, const void *b)
+{
+ const TzLocation *tza = * (TzLocation **) a;
+ const TzLocation *tzb = * (TzLocation **) b;
+
+ return strcmp (tza->zone, tzb->zone);
+}
+
+
+static void
+sort_locations_by_country (GPtrArray *locations)
+{
+ qsort (locations->pdata, locations->len, sizeof (gpointer),
+ compare_country_names);
+}
+
+static void
+load_backward_tz (TzDB *tz_db)
+{
+ g_auto(GStrv) lines = NULL;
+ g_autoptr(GBytes) bytes = NULL;
+ const char *contents;
+ guint i;
+
+ tz_db->backward = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ bytes = g_resources_lookup_data ("/org/gnome/control-center/datetime/backward",
+ G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
+ contents = (const char *) g_bytes_get_data (bytes, NULL);
+
+ lines = g_strsplit (contents, "\n", -1);
+
+ for (i = 0; lines[i] != NULL; i++)
+ {
+ g_auto(GStrv) items = NULL;
+ guint j;
+ char *real, *alias;
+
+ if (g_ascii_strncasecmp (lines[i], "Link\t", 5) != 0)
+ continue;
+
+ items = g_strsplit (lines[i], "\t", -1);
+ real = NULL;
+ alias = NULL;
+ /* Skip the "Link<tab>" part */
+ for (j = 1; items[j] != NULL; j++)
+ {
+ if (items[j][0] == '\0')
+ continue;
+ if (real == NULL)
+ {
+ real = items[j];
+ continue;
+ }
+ alias = items[j];
+ break;
+ }
+
+ if (real == NULL || alias == NULL)
+ g_warning ("Could not parse line: %s", lines[i]);
+
+ /* We don't need more than one name for it */
+ if (g_str_equal (real, "Etc/UTC") ||
+ g_str_equal (real, "Etc/UCT"))
+ real = "Etc/GMT";
+
+ g_hash_table_insert (tz_db->backward, g_strdup (alias), g_strdup (real));
+ }
+}
+
diff --git a/gnome-initial-setup/pages/timezone/tz.h b/gnome-initial-setup/pages/timezone/tz.h
new file mode 100644
index 0000000..feef165
--- /dev/null
+++ b/gnome-initial-setup/pages/timezone/tz.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/* Generic timezone utilities.
+ *
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Authors: Hans Petter Jansson <hpj@ximian.com>
+ *
+ * Largely based on Michael Fulbright's work on Anaconda.
+ *
+ * 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.h>
+
+G_BEGIN_DECLS
+
+#ifndef __sun
+# define TZ_DATA_FILE "/usr/share/zoneinfo/zone.tab"
+#else
+# define TZ_DATA_FILE "/usr/share/lib/zoneinfo/tab/zone_sun.tab"
+#endif
+
+typedef struct _TzDB TzDB;
+typedef struct _TzLocation TzLocation;
+typedef struct _TzInfo TzInfo;
+
+
+struct _TzDB
+{
+ GPtrArray *locations;
+ GHashTable *backward;
+};
+
+struct _TzLocation
+{
+ gchar *country;
+ gdouble latitude;
+ gdouble longitude;
+ gchar *zone;
+ gchar *comment;
+
+ gdouble dist; /* distance to clicked point for comparison */
+};
+
+/* see the glibc info page information on time zone information */
+/* tzname is the default name for the timezone */
+/* utc_offset is offset in seconds from utc */
+/* daylight if non-zero then location obeys daylight savings */
+
+struct _TzInfo
+{
+ gchar *tzname;
+ glong utc_offset;
+ gint daylight;
+};
+
+
+TzDB *tz_load_db (void);
+void tz_db_free (TzDB *db);
+char * tz_info_get_clean_name (TzDB *tz_db,
+ const char *tz);
+GPtrArray *tz_get_locations (TzDB *db);
+void tz_location_get_position (TzLocation *loc,
+ double *longitude, double *latitude);
+char *tz_location_get_country (TzLocation *loc);
+gchar *tz_location_get_zone (TzLocation *loc);
+gchar *tz_location_get_comment (TzLocation *loc);
+glong tz_location_get_base_utc_offset (TzLocation *loc);
+gint tz_location_set_locally (TzLocation *loc);
+TzInfo *tz_info_from_location (TzLocation *loc);
+void tz_info_free (TzInfo *tz_info);
+
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (TzDB, tz_db_free)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (TzInfo, tz_info_free)
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/welcome/gis-welcome-page.c b/gnome-initial-setup/pages/welcome/gis-welcome-page.c
new file mode 100644
index 0000000..41b1170
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/gis-welcome-page.c
@@ -0,0 +1,132 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2020 Red Hat
+ *
+ * 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>
+ */
+
+/* Welcome page {{{1 */
+
+#define PAGE_ID "welcome"
+
+#include "config.h"
+#include "welcome-resources.h"
+#include "gis-welcome-page.h"
+#include "gis-assistant.h"
+
+
+struct _GisWelcomePage
+{
+ GisPage parent;
+};
+
+typedef struct
+{
+ GtkWidget *header;
+ GtkWidget *title;
+} GisWelcomePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisWelcomePage, gis_welcome_page, GIS_TYPE_PAGE)
+
+static void
+update_welcome_title (GisWelcomePage *page)
+{
+ GisWelcomePagePrivate *priv = gis_welcome_page_get_instance_private (page);
+ g_autofree char *name = g_get_os_info (G_OS_INFO_KEY_NAME);
+ g_autofree char *entity = NULL;
+ g_autofree char *text = NULL;
+
+ if (name != NULL)
+ {
+ g_autofree char *version = g_get_os_info (G_OS_INFO_KEY_VERSION_ID);
+
+ if (version)
+ entity = g_strdup_printf ("%s %s", name, version);
+ else
+ entity = g_strdup (name);
+ }
+ else
+ {
+ entity = g_strdup ("GNOME");
+ }
+
+ /* Translators: This is meant to be a warm, engaging welcome message,
+ * like greeting somebody at the door. If the exclamation mark is not
+ * suitable for this in your language you may replace it. The space
+ * before the exclamation mark in this string is a typographical thin
+ * space (U200a) to improve the spacing in the title, which you can
+ * keep or remove. The %s is getting replaced with the name and version
+ * of the OS, e.g. "GNOME 3.38"
+ */
+ text = g_strdup_printf (_("Welcome to %s !"), entity);
+
+ gtk_label_set_label (GTK_LABEL (priv->title), text);
+}
+
+static void
+gis_welcome_page_constructed (GObject *object)
+{
+ GisWelcomePage *page = GIS_WELCOME_PAGE (object);
+
+ G_OBJECT_CLASS (gis_welcome_page_parent_class)->constructed (object);
+
+ update_welcome_title (page);
+
+ gis_page_set_complete (GIS_PAGE (page), TRUE);
+}
+
+static void
+start_setup (GtkButton *button, GisWelcomePage *page)
+{
+ GisAssistant *assistant;
+
+ assistant = GIS_ASSISTANT (gtk_widget_get_ancestor (GTK_WIDGET (page), GIS_TYPE_ASSISTANT));
+
+ gis_assistant_next_page (assistant);
+}
+
+static void
+gis_welcome_page_class_init (GisWelcomePageClass *klass)
+{
+ GisPageClass *page_class = GIS_PAGE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-welcome-page.ui");
+
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisWelcomePage, header);
+ gtk_widget_class_bind_template_child_private (GTK_WIDGET_CLASS (klass), GisWelcomePage, title);
+ gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), start_setup);
+
+ page_class->page_id = PAGE_ID;
+ object_class->constructed = gis_welcome_page_constructed;
+}
+
+static void
+gis_welcome_page_init (GisWelcomePage *page)
+{
+ g_resources_register (welcome_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+}
+
+GisPage *
+gis_prepare_welcome_page (GisDriver *driver)
+{
+ return g_object_new (GIS_TYPE_WELCOME_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/welcome/gis-welcome-page.h b/gnome-initial-setup/pages/welcome/gis-welcome-page.h
new file mode 100644
index 0000000..73cb6e4
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/gis-welcome-page.h
@@ -0,0 +1,37 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2020 Red Hat
+ *
+ * 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-object.h>
+
+#include "gnome-initial-setup.h"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_WELCOME_PAGE (gis_welcome_page_get_type ())
+G_DECLARE_FINAL_TYPE (GisWelcomePage, gis_welcome_page, GIS, WELCOME_PAGE, GisPage)
+
+GType gis_welcome_page_get_type (void);
+
+GisPage *gis_prepare_welcome_page (GisDriver *driver);
+
+G_END_DECLS
diff --git a/gnome-initial-setup/pages/welcome/gis-welcome-page.ui b/gnome-initial-setup/pages/welcome/gis-welcome-page.ui
new file mode 100644
index 0000000..555dbb4
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/gis-welcome-page.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<interface>
+ <template class="GisWelcomePage" parent="GisPage">
+ <property name="title" translatable="yes">Setup</property>
+ <property name="margin-top">0</property>
+ <property name="margin-bottom">0</property>
+ <property name="margin-start">0</property>
+ <property name="margin-end">0</property>
+ <property name="has-forward">1</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <child>
+ <object class="GtkPicture" id="header">
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <property name="file">resource:///org/gnome/initial-setup/initial-setup-welcome.svg</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="vexpand">1</property>
+ <property name="halign">center</property>
+ <property name="spacing">20</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <!-- This is set to a translated string at runtime -->
+ <property name="label">Welcome to the latest GNOME!</property>
+ <style>
+ <class name="title-1"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Setup will guide you through making an account and enabling some features. We’ll have you up and running in no time.</property>
+ <property name="wrap">1</property>
+ <property name="width-chars">35</property>
+ <property name="max-width-chars">35</property>
+ <property name="justify">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="halign">center</property>
+ <property name="label" translatable="yes">_Start Setup</property>
+ <property name="use-underline">1</property>
+ <signal name="clicked" handler="start_setup"/>
+ <style>
+ <class name="suggested-action"/>
+ <class name="pill"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/welcome/initial-setup-welcome.svg b/gnome-initial-setup/pages/welcome/initial-setup-welcome.svg
new file mode 100644
index 0000000..74a7eec
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/initial-setup-welcome.svg
@@ -0,0 +1,1816 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ id="svg313"
+ version="1.1"
+ height="754"
+ width="1920"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata
+ id="metadata317">
+ <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="defs28">
+ <linearGradient
+ id="linearGradient15705">
+ <stop
+ style="stop-color:#deddda;stop-opacity:0"
+ offset="0"
+ id="stop15699" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0.20000002"
+ id="stop15701" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop15703" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient18440">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop18436" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0;"
+ offset="1"
+ id="stop18438" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient8615">
+ <stop
+ style="stop-color:#241f31;stop-opacity:1;"
+ offset="0"
+ id="stop8603" />
+ <stop
+ style="stop-color:#3d3846;stop-opacity:1"
+ offset="0.03409091"
+ id="stop8605" />
+ <stop
+ style="stop-color:#241f31;stop-opacity:0.98823529;"
+ offset="0.07272727"
+ id="stop8607" />
+ <stop
+ style="stop-color:#241f31;stop-opacity:0.94117647;"
+ offset="0.94318181"
+ id="stop8609" />
+ <stop
+ style="stop-color:#3d3846;stop-opacity:1"
+ offset="0.9727273"
+ id="stop8611" />
+ <stop
+ style="stop-color:#241f31;stop-opacity:0.93941134"
+ offset="1"
+ id="stop8613" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient187064">
+ <stop
+ style="stop-color:#5e5b64;stop-opacity:1"
+ offset="0"
+ id="stop187060" />
+ <stop
+ style="stop-color:#605c67;stop-opacity:1"
+ offset="1"
+ id="stop187062" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient182526">
+ <stop
+ style="stop-color:#a5744a;stop-opacity:1"
+ offset="0"
+ id="stop182514" />
+ <stop
+ id="stop182516"
+ offset="0.03846154"
+ style="stop-color:#bc8f6c;stop-opacity:1" />
+ <stop
+ style="stop-color:#b5835a;stop-opacity:1"
+ offset="0.07685554"
+ id="stop182518" />
+ <stop
+ id="stop182520"
+ offset="0.92307693"
+ style="stop-color:#b5835a;stop-opacity:1" />
+ <stop
+ style="stop-color:#ba8b65;stop-opacity:1"
+ offset="0.96153849"
+ id="stop182522" />
+ <stop
+ style="stop-color:#a4734a;stop-opacity:1"
+ offset="1"
+ id="stop182524" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient182410">
+ <stop
+ style="stop-color:#a4724b;stop-opacity:1"
+ offset="0"
+ id="stop182398" />
+ <stop
+ id="stop182400"
+ offset="0.03846154"
+ style="stop-color:#b98b64;stop-opacity:1" />
+ <stop
+ style="stop-color:#b5835a;stop-opacity:1"
+ offset="0.07685554"
+ id="stop182402" />
+ <stop
+ id="stop182404"
+ offset="0.92307693"
+ style="stop-color:#b5835a;stop-opacity:1" />
+ <stop
+ style="stop-color:#b98a64;stop-opacity:1"
+ offset="0.96153849"
+ id="stop182406" />
+ <stop
+ style="stop-color:#865e3c;stop-opacity:1"
+ offset="1"
+ id="stop182408" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient161325">
+ <stop
+ style="stop-color:#fc9a91;stop-opacity:1"
+ offset="0"
+ id="stop161321" />
+ <stop
+ style="stop-color:#d94a4f;stop-opacity:1"
+ offset="1"
+ id="stop161323" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient99848">
+ <stop
+ style="stop-color:#c4cdd8;stop-opacity:1"
+ offset="0"
+ id="stop99844" />
+ <stop
+ style="stop-color:#e3ecf8;stop-opacity:1"
+ offset="1"
+ id="stop99846" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient58184">
+ <stop
+ style="stop-color:#22180f;stop-opacity:1"
+ offset="0"
+ id="stop58180" />
+ <stop
+ style="stop-color:#63452c;stop-opacity:1"
+ offset="1"
+ id="stop58182" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient41234">
+ <stop
+ style="stop-color:#2f2b36;stop-opacity:1"
+ offset="0"
+ id="stop41230" />
+ <stop
+ style="stop-color:#3d3846;stop-opacity:1"
+ offset="1"
+ id="stop41232" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient2796">
+ <stop
+ style="stop-color:#8ff0a4;stop-opacity:1"
+ offset="0"
+ id="stop2792" />
+ <stop
+ style="stop-color:#3584e4;stop-opacity:1"
+ offset="1"
+ id="stop2794" />
+ </linearGradient>
+ <linearGradient
+ id="a">
+ <stop
+ id="stop4"
+ stop-color="#d0bb8e"
+ offset="0" />
+ <stop
+ id="stop6"
+ stop-color="#fff"
+ offset="1" />
+ </linearGradient>
+ <clipPath
+ id="clipPath11679"
+ clipPathUnits="userSpaceOnUse">
+ <circle
+ style="opacity:1;vector-effect:none;fill:#9141ac;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="circle11681"
+ cx="239.05104"
+ cy="77.263527"
+ r="19.711458" />
+ </clipPath>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath984">
+ <circle
+ style="font-variation-settings:normal;display:none;opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:5.04302;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;stop-color:#000000;stop-opacity:1"
+ id="circle986"
+ cx="541.88373"
+ cy="647.72638"
+ r="73.975525" />
+ <path
+ id="lpe_path-effect988"
+ style="font-variation-settings:normal;display:block;opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:5.04302;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;stop-color:#000000;stop-opacity:1"
+ class="powerclip"
+ d="m 399.30566,389.6405 h 285.1561 v 408.04123 h -285.1561 z m 216.55359,258.08588 a 73.975525,73.975525 0 0 0 -73.97552,-73.97553 73.975525,73.975525 0 0 0 -73.97553,73.97553 73.975525,73.975525 0 0 0 73.97553,73.97552 73.975525,73.975525 0 0 0 73.97552,-73.97552 z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient2796"
+ id="linearGradient2798"
+ x1="2675.9324"
+ y1="-1342.3387"
+ x2="2930.8142"
+ y2="-1197.1143"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.8673154,0,0,1.8673154,-4275.3195,2533.0291)" />
+ <linearGradient
+ xlink:href="#linearGradient187064"
+ id="linearGradient38318"
+ x1="1285"
+ y1="584"
+ x2="1285"
+ y2="557"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.4,0,0,1,-514,0)" />
+ <linearGradient
+ xlink:href="#linearGradient41234"
+ id="linearGradient41236"
+ x1="1280"
+ y1="670"
+ x2="1280"
+ y2="555"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.1111111,0,0,1.0714286,-142.77778,-38.571429)" />
+ <linearGradient
+ y2="-56.517502"
+ x2="-230.06013"
+ y1="-56.517502"
+ x1="-253.63036"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient1958"
+ xlink:href="#linearGradient161325"
+ gradientTransform="translate(0,55.999776)" />
+ <linearGradient
+ y2="-101.0481"
+ x2="143.24614"
+ y1="-139.47939"
+ x1="181.67741"
+ gradientTransform="matrix(0.39784874,-0.70450288,0.7045029,-0.39784875,96.217456,330.43994)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient1547"
+ xlink:href="#a" />
+ <linearGradient
+ gradientTransform="translate(1.3378639e-8,75.960541)"
+ y2="-64.517509"
+ x2="-253.63036"
+ y1="-64.517509"
+ x1="-230.06013"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient1549"
+ xlink:href="#linearGradient1708" />
+ <linearGradient
+ id="linearGradient1708">
+ <stop
+ style="stop-color:#c0bfbc;stop-opacity:1"
+ offset="0"
+ id="stop1698" />
+ <stop
+ id="stop1700"
+ offset="0.22315237"
+ style="stop-color:#9a9996;stop-opacity:1" />
+ <stop
+ style="stop-color:#d6d5d2;stop-opacity:1;"
+ offset="0.74384123"
+ id="stop1702" />
+ <stop
+ style="stop-color:#f6f5f4;stop-opacity:1"
+ offset="1"
+ id="stop1706" />
+ </linearGradient>
+ <linearGradient
+ xlink:href="#linearGradient161325"
+ id="linearGradient49722"
+ gradientUnits="userSpaceOnUse"
+ x1="-253.63036"
+ y1="-56.517502"
+ x2="-230.06013"
+ y2="-56.517502"
+ gradientTransform="matrix(1,0,0,1.4063674,0,79.779379)" />
+ <linearGradient
+ xlink:href="#linearGradient58184"
+ id="linearGradient58186"
+ x1="219.33958"
+ y1="85.201027"
+ x2="258.7625"
+ y2="85.201027"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ xlink:href="#linearGradient182410"
+ id="linearGradient71993"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.69642846,0,0,0.44407966,-98.7007,899.56686)"
+ x1="88.595886"
+ y1="-449.39401"
+ x2="536.59589"
+ y2="-449.39401" />
+ <linearGradient
+ xlink:href="#linearGradient182526"
+ id="linearGradient74024"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.87053556,0,0,0.80592118,-27.125869,872.17614)"
+ x1="88.595879"
+ y1="-455.59808"
+ x2="536.59595"
+ y2="-455.59808" />
+ <linearGradient
+ xlink:href="#linearGradient99848"
+ id="linearGradient99850"
+ x1="0"
+ y1="377"
+ x2="1920"
+ y2="377"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-1,0,0,1,1920,0)" />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath15655">
+ <path
+ id="path15657"
+ fill="#1c71d8"
+ overflow="visible"
+ color="#000000"
+ style="fill:url(#linearGradient15659);fill-opacity:1;marker:none"
+ d="M -1732,754 H 187.99999 V 0 H -1732 Z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient99848"
+ id="linearGradient15659"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-1732)"
+ x1="0"
+ y1="377"
+ x2="1920"
+ y2="377" />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath15711">
+ <path
+ id="path15713"
+ fill="#1c71d8"
+ overflow="visible"
+ color="#000000"
+ style="fill:url(#linearGradient15715);fill-opacity:1;marker:none"
+ d="M 1920,754 H 0 V 0 h 1920 z" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient99848"
+ id="linearGradient15715"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-1,0,0,1,1920,0)"
+ x1="0"
+ y1="377"
+ x2="1920"
+ y2="377" />
+ <linearGradient
+ xlink:href="#linearGradient8615"
+ id="linearGradient8579"
+ x1="730"
+ y1="725"
+ x2="1170"
+ y2="725"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.0135741,0,0,1,-12.908976,5)" />
+ <linearGradient
+ xlink:href="#linearGradient1038"
+ id="linearGradient1159"
+ x1="27.99999"
+ y1="-276"
+ x2="115.99999"
+ y2="-276"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.52088879,0,0,0.52088879,-41.335838,-59.37247)" />
+ <linearGradient
+ id="linearGradient1038">
+ <stop
+ id="stop1026"
+ offset="0"
+ style="stop-color:#c0bfbc;stop-opacity:1" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1"
+ offset="0.04545454"
+ id="stop1028" />
+ <stop
+ style="stop-color:#deddda;stop-opacity:1"
+ offset="0.09090909"
+ id="stop1030" />
+ <stop
+ style="stop-color:#deddda;stop-opacity:1"
+ offset="0.90909088"
+ id="stop1032-7" />
+ <stop
+ id="stop1034-5"
+ offset="0.95454544"
+ style="stop-color:#e4e3e0;stop-opacity:1" />
+ <stop
+ id="stop1036"
+ offset="1"
+ style="stop-color:#c0bfbc;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ xlink:href="#linearGradient15705"
+ id="linearGradient31483"
+ x1="1447.7793"
+ y1="465.09149"
+ x2="1447.7793"
+ y2="174.34183"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-21,-7)" />
+ <linearGradient
+ xlink:href="#linearGradient18440"
+ id="linearGradient18442"
+ x1="524.99994"
+ y1="-55.49999"
+ x2="540"
+ y2="-55.49999"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(4.9999801,0,0,0.33333322,-2742.9892,-534)" />
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath20866">
+ <rect
+ y="60.79998"
+ x="73.62529"
+ height="81.666687"
+ width="140"
+ id="rect20868"
+ style="display:inline;fill:url(#linearGradient20870);fill-opacity:1;stroke:none;stroke-width:0.786377;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18"
+ transform="rotate(30)" />
+ </clipPath>
+ <linearGradient
+ xlink:href="#linearGradient182526"
+ id="linearGradient20870"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.31249998,0,0,0.26864043,45.939076,263.19201)"
+ x1="88.595879"
+ y1="-455.59808"
+ x2="536.59595"
+ y2="-455.59808" />
+ </defs>
+ <path
+ id="path30"
+ fill="#1c71d8"
+ overflow="visible"
+ color="#000000"
+ style="fill:url(#linearGradient99850);fill-opacity:1;marker:none"
+ d="M 1920,754 H 0 V 0 h 1920 z" />
+ <g
+ transform="matrix(-1.32708,1.32708,1.32708,1.32708,237.31005,217.3346)"
+ id="g1179"
+ style="display:inline;enable-background:new">
+ <path
+ id="path15618"
+ d="m 69.955937,198.84526 c -2.25097,2.1168 -4.369863,6.10177 -4.369831,13.60191 l -0.0149,34.49994 -10.178493,10.17849 c -0.400227,-0.0954 -0.858822,-0.0856 -1.343179,0.1576 l -12.124649,6.0886 c -4.633632,-2.88859 -10.848475,-2.32767 -14.864321,1.68818 -4.671489,4.67149 -4.672268,12.32091 -7.83e-4,16.99239 4.671483,4.67149 12.321684,4.67149 16.993171,0 0.01446,-0.0145 0.02797,-0.0294 0.04234,-0.0439 l 0.0055,0.0447 15.555904,-15.5559 0.01725,0.0173 5.891781,-5.89649 -0.0039,7.82931 0.02431,-7.8e-4 7.83e-4,21.9997 0.0345,-0.0282 c -1e-4,0.0205 -8.31e-4,0.0399 -7.83e-4,0.0604 0,6.60649 5.409965,12.01645 12.016442,12.01644 6.606478,10e-6 12.014086,-5.40917 12.01409,-12.01565 -4e-6,-5.67926 -3.997747,-10.47123 -9.316761,-11.70515 l -4.268686,-12.87818 c -0.170506,-0.51448 -0.487762,-0.84533 -0.838215,-1.0609 l 4e-6,-13.88811 31.562739,-31.58312 c 11.31372,-11.31372 4.49608,-18.13169 4.49608,-18.13169 l -36.058819,36.0588 7.83e-4,-40.84492 c 0,0 -2.72124,3.3e-4 -5.272336,2.39936 z m -39.359831,69.75026 c 2.760744,-2.76075 7.160569,-2.76152 9.921308,-7.8e-4 2.760744,2.76073 2.760744,7.16134 0,9.92209 -2.760739,2.76074 -7.161351,2.76074 -9.922091,0 -2.760743,-2.76074 -2.759957,-7.16057 7.83e-4,-9.92131 z m 42.068136,16.91084 c 1.265961,-1.26596 3.019884,-2.04417 4.972022,-2.04417 3.904278,10e-6 7.015419,3.1127 7.015411,7.01698 0,3.90426 -3.111125,7.01542 -7.015407,7.01541 -3.904279,1e-5 -7.016191,-3.11193 -7.016195,-7.0162 4e-6,-1.95214 0.778212,-3.70606 2.044169,-4.97202 z"
+ style="opacity:1;fill:#000000;fill-opacity:0.102362;fill-rule:evenodd;stroke:none;stroke-width:4.15049;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <g
+ style="display:inline;enable-background:new"
+ transform="translate(7.9947119,-21.03751)"
+ id="g1200">
+ <path
+ style="fill:#9a9996;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 58,290 h 6 v -72 l -1.414214,-1.41421 C 62.585786,216.58579 56,221.64656 56,234 l 0.0053,27.18969 0.09677,2.72237 L 58,266 Z"
+ id="path1186" />
+ <g
+ id="g1194"
+ transform="translate(2)" />
+ </g>
+ <use
+ style="display:inline;enable-background:new"
+ x="0"
+ y="0"
+ xlink:href="#g1059"
+ id="use1074"
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,292.42443,120.74209)"
+ width="100%"
+ height="100%" />
+ <g
+ style="display:inline;enable-background:new"
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,302.73447,101.58204)"
+ id="g1180">
+ <g
+ id="g1174"
+ transform="translate(2)" />
+ </g>
+ <g
+ style="display:inline;enable-background:new"
+ id="g1059"
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,302.75946,99.477469)">
+ <path
+ id="path1039-3"
+ d="m 55.975259,290 h 9.666423 v -82.65658 c 0,0 -9.641682,-1e-5 -9.641682,16 z"
+ style="fill:#ffffff;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <g
+ transform="translate(2)"
+ id="g1052">
+ <path
+ style="fill:#ed333b;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 54,312 v -24 c 0,-2 2,-2 2,-2 h 6.037213 c 0,0 1.917797,-0.13576 2.443399,1.45023 l 4.397602,13.26955 z"
+ id="path1047" />
+ <circle
+ r="9.515707"
+ cy="312.03348"
+ cx="-66.049789"
+ id="circle1045"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ed333b;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ transform="scale(-1,1)" />
+ <circle
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#e01b24;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ id="path1043-6"
+ cx="-66"
+ cy="312"
+ r="7"
+ transform="scale(-1,1)" />
+ </g>
+ </g>
+ <circle
+ transform="matrix(-0.70710678,-0.70710678,-0.70710678,0.70710678,0,0)"
+ style="display:inline;opacity:1;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
+ id="circle1204"
+ cx="-223.44576"
+ cy="125.86499"
+ r="2" />
+ </g>
+ <g
+ id="g15691"
+ clip-path="url(#clipPath15711)">
+ <g
+ transform="matrix(3.839591,0,0,3.839591,1838.2872,-275.30655)"
+ id="g11745"
+ style="display:inline">
+ <g
+ id="g173944"
+ transform="translate(5.2088882,-5.2088882)">
+ <rect
+ ry="4.6879992"
+ rx="4.6879992"
+ y="158.84671"
+ x="-19.087234"
+ height="54.79755"
+ width="46.367096"
+ id="rect15542"
+ style="display:inline;fill:#000000;fill-opacity:0.102362;stroke:none;stroke-width:1.9342;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
+ <rect
+ style="display:inline;fill:#c01c28;fill-opacity:1;stroke:none;stroke-width:0.131725;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect1551"
+ width="46.367096"
+ height="54.79755"
+ x="-19.087234"
+ y="157.78838"
+ rx="4.6879992"
+ ry="4.6879992" />
+ <rect
+ clip-path="none"
+ ry="4.6879992"
+ rx="4.6879992"
+ y="-211.47202"
+ x="-26.750967"
+ height="20.835554"
+ width="45.838219"
+ id="rect1151"
+ style="display:inline;fill:url(#linearGradient1159);fill-opacity:1;stroke:none;stroke-width:7.14597;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ transform="scale(-1)" />
+ <rect
+ ry="4.6879992"
+ rx="4.6879992"
+ y="153.66185"
+ x="-19.08725"
+ height="52.689953"
+ width="46.367157"
+ id="rect1555"
+ style="display:inline;fill:#c01c28;fill-opacity:1;stroke:none;stroke-width:0.131725;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
+ <rect
+ style="display:inline;fill:#ed333b;fill-opacity:1;stroke:none;stroke-width:0.142761;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect1557"
+ width="46.341946"
+ height="60.683544"
+ x="-19.08725"
+ y="144.62648"
+ rx="4.6854501"
+ ry="4.6854501" />
+ <g
+ style="display:inline;fill:#3d3846;stroke:#241f31;stroke-width:2;stroke-opacity:1;enable-background:new"
+ transform="matrix(0.52689953,0,0,0.52689953,-29.625289,51.0788)"
+ id="g679">
+ <circle
+ style="opacity:1;fill:#3d3846;fill-opacity:1;stroke:#241f31;stroke-width:2;stroke-opacity:1"
+ id="path632"
+ cx="32"
+ cy="187"
+ r="3" />
+ <circle
+ r="3"
+ cy="187"
+ cx="95.999992"
+ id="circle634"
+ style="opacity:1;fill:#3d3846;fill-opacity:1;stroke:#241f31;stroke-width:2;stroke-opacity:1" />
+ <circle
+ style="opacity:1;fill:#3d3846;fill-opacity:1;stroke:#241f31;stroke-width:2;stroke-opacity:1"
+ id="circle636"
+ cx="48"
+ cy="187"
+ r="3" />
+ <circle
+ style="opacity:1;fill:#3d3846;fill-opacity:1;stroke:#241f31;stroke-width:2;stroke-opacity:1"
+ id="circle642"
+ cx="64"
+ cy="187"
+ r="3" />
+ <circle
+ r="3"
+ cy="187"
+ cx="80"
+ id="circle646"
+ style="opacity:1;fill:#3d3846;fill-opacity:1;stroke:#241f31;stroke-width:2;stroke-opacity:1" />
+ </g>
+ <path
+ id="use1471"
+ d="m -12.781,140.11033 a 1.0539044,1.0539044 0 0 0 -1.037109,1.06836 v 8.42969 a 1.0539044,1.0539044 0 1 0 2.107421,0 v -8.42969 A 1.0539044,1.0539044 0 0 0 -12.781,140.11033 Z m 8.4316403,0 a 1.0539044,1.0539044 0 0 0 -1.0390625,1.06836 v 8.42969 a 1.0539044,1.0539044 0 1 0 2.1074219,0 v -8.42969 a 1.0539044,1.0539044 0 0 0 -1.0683594,-1.06836 z m 8.4296875,0 a 1.0539044,1.0539044 0 0 0 -1.0371094,1.06836 v 8.42969 a 1.0539044,1.0539044 0 1 0 2.1074219,0 v -8.42969 a 1.0539044,1.0539044 0 0 0 -1.0703125,-1.06836 z m 8.4296882,0 a 1.0539044,1.0539044 0 0 0 -1.03711,1.06836 v 8.42969 a 1.0539044,1.0539044 0 1 0 2.107422,0 v -8.42969 a 1.0539044,1.0539044 0 0 0 -1.070312,-1.06836 z m 8.43164,0 a 1.0539044,1.0539044 0 0 0 -1.039062,1.06836 v 8.42969 a 1.0539044,1.0539044 0 1 0 2.107422,0 v -8.42969 a 1.0539044,1.0539044 0 0 0 -1.06836,-1.06836 z"
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;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:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#deddda;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.1076;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <circle
+ r="1.0583333"
+ cy="142.26593"
+ cx="-12.73725"
+ id="path17421"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="circle17423"
+ cx="-4.2948747"
+ cy="142.26593"
+ r="1.0583333" />
+ <circle
+ r="1.0583333"
+ cy="142.26593"
+ cx="4.1007295"
+ id="circle17425"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="circle17427"
+ cx="12.543107"
+ cy="142.26593"
+ r="1.0583333" />
+ <circle
+ r="1.0583333"
+ cy="142.26593"
+ cx="21.008869"
+ id="circle17429"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.5875;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ </g>
+ </g>
+ <g
+ transform="matrix(0,7.4063284,7.4063284,0,510.05351,-623.5321)"
+ id="g11597"
+ style="display:inline">
+ <path
+ id="rect217-1"
+ style="font-variation-settings:normal;opacity:1;fill:#000000;fill-opacity:0.102;stroke:none;stroke-width:0.848038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;stop-color:#000000;stop-opacity:1"
+ d="m 174.8876,199.15395 v -33.81219 c 0,-0.78051 1.40879,-7.94608 1.40879,-7.94608 h 0.45887 c 0,0 1.40879,7.16557 1.40879,7.94608 v 33.81219 c 0,0.78046 -0.62833,1.40879 -1.40879,1.40879 h -0.45887 c -0.78046,0 -1.40879,-0.62833 -1.40879,-1.40879 z" />
+ <rect
+ style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="rect11580"
+ width="3.4395833"
+ height="2.1166666"
+ x="173.56667"
+ y="198.70209"
+ rx="0.46772167"
+ ry="0.46772167" />
+ <path
+ style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:0.982083;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ d="m 174.8049,157.42708 h 0.96312 c 0.0997,0 0.16352,0.0833 0.1799,0.1836 l 1.05833,6.4828 c 0.0426,0.26086 -0.2086,0.47734 -0.46772,0.47734 h -2.50414 c -0.25912,0 -0.5103,-0.21648 -0.46772,-0.47734 l 1.05833,-6.4828 c 0.0164,-0.10032 0.0802,-0.1836 0.1799,-0.1836 z"
+ id="rect11582" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="rect11578"
+ width="3.96875"
+ height="35.71875"
+ x="173.30208"
+ y="164.04167" />
+ <rect
+ style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="rect11585"
+ width="0.79374695"
+ height="2.3812485"
+ x="174.88959"
+ y="155.83958"
+ rx="0.39687347"
+ ry="0.46772248" />
+ <rect
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="rect11587"
+ width="1.3229166"
+ height="1.0583333"
+ x="174.625"
+ y="156.63333"
+ rx="0"
+ ry="0" />
+ <rect
+ y="164.04167"
+ x="175.94792"
+ height="35.71875"
+ width="1.3229121"
+ id="rect11589"
+ style="opacity:1;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:0.305514;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ </g>
+ </g>
+ <g
+ transform="matrix(1.2317356,0,0,1.2317356,-414.6169,117.26786)"
+ style="display:inline;stroke-width:0.959464;enable-background:new"
+ id="g65" />
+ <g
+ id="g46159"
+ transform="translate(0,10)">
+ <g
+ id="g973"
+ clip-path="url(#clipPath984)"
+ transform="matrix(0.67660422,0,0,0.67660422,593.42923,-33.157845)"
+ style="stroke-width:0.986377">
+ <path
+ style="font-variation-settings:normal;opacity:1;fill:#000000;fill-opacity:0.102362;stroke:none;stroke-width:4.96026;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;stop-color:#000000;stop-opacity:1"
+ id="rect926"
+ width="275.1561"
+ height="310.64557"
+ x="404.30566"
+ y="482.03616"
+ rx="20.673309"
+ ry="20.673309"
+ d="m 424.97897,482.03616 h 233.80948 c 11.45302,0 20.67331,9.2203 20.67331,20.67331 v 269.29895 c 0,11.45302 -9.22029,20.67331 -20.67331,20.67331 H 424.97897 c -11.45301,0 -20.67331,-9.22029 -20.67331,-20.67331 V 502.70947 c 0,-11.45301 9.2203,-20.67331 20.67331,-20.67331 z" />
+ <path
+ style="font-variation-settings:normal;opacity:1;fill:#504d56;fill-opacity:1;stroke:none;stroke-width:4.97431;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;stop-color:#000000;stop-opacity:1"
+ id="rect257"
+ width="275.1561"
+ height="310.64557"
+ x="404.30566"
+ y="475.97519"
+ rx="20.673309"
+ ry="20.673309"
+ d="m 424.97897,475.97519 h 233.80948 c 11.45302,0 20.67331,9.2203 20.67331,20.67331 v 269.29895 c 0,11.45301 -9.22029,20.67331 -20.67331,20.67331 H 424.97897 c -11.45301,0 -20.67331,-9.2203 -20.67331,-20.67331 V 496.6485 c 0,-11.45301 9.2203,-20.67331 20.67331,-20.67331 z" />
+ <path
+ style="fill:#3d3846;fill-opacity:1;stroke-width:0.986377;stroke-linecap:round;stop-color:#000000"
+ id="rect101"
+ width="275.1561"
+ height="310.64557"
+ x="404.30566"
+ y="394.6405"
+ rx="0"
+ ry="0"
+ d="m 404.30566,394.6405 h 275.1561 v 310.64557 h -275.1561 z" />
+ </g>
+ <rect
+ style="opacity:1;fill:#000000;fill-opacity:0.952659;stroke-width:3.56662;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect44566"
+ width="620.14215"
+ height="386.29166"
+ x="649.92889"
+ y="13.970249"
+ rx="20.071045"
+ ry="20.071045" />
+ <rect
+ style="fill:url(#linearGradient2798);fill-opacity:1;stroke:none;stroke-width:1.23922;stroke-linecap:round;stop-color:#000000"
+ id="rect1457"
+ width="594.82703"
+ height="359.37448"
+ x="663.13983"
+ y="26.45929"
+ rx="7.9667969"
+ ry="7.9667969" />
+ <g
+ id="g197-3-7"
+ fill="#ffffff"
+ stroke-width="1.12774"
+ transform="matrix(4.5425248,0,0,4.5425248,592.82104,-283.42834)"
+ style="display:inline;enable-background:new">
+ <path
+ id="path191-5-3"
+ opacity="0.2"
+ d="m 112.497,102.02 12.984,-12.984 v 12.985 z m 12.983,0.001 12.053,12.053 H 125.48 Z"
+ style="stroke-width:1.42577" />
+ <path
+ id="path193-6-6"
+ opacity="0.1"
+ d="m 101.339,138.053 12.053,-12.062 h -12.053 z"
+ style="stroke-width:1.42577" />
+ <path
+ id="path195-2-1"
+ opacity="0.2"
+ d="m 125.445,113.937 -12.053,12.053 h 12.053 z"
+ style="stroke-width:1.42577" />
+ <path
+ id="path62149"
+ opacity="0.2"
+ d="m 125.445,125.99 -12.053,12.053 h 12.053 z"
+ style="stroke-width:1.42577" />
+ <path
+ id="path62151"
+ opacity="0.2"
+ d="m 101.339,101.884 24.106,24.106 h -24.106 z"
+ style="stroke-width:1.42577" />
+ <path
+ id="path63328"
+ opacity="0.1"
+ d="m 89.286,138.053 12.053,-12.062 H 89.286 Z"
+ style="stroke-width:1.42577" />
+ </g>
+ </g>
+ <g
+ id="g23857"
+ transform="translate(10,-10)">
+ <rect
+ y="269.99991"
+ x="50"
+ height="245.00003"
+ width="389.99994"
+ id="rect144013"
+ style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.786377;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18" />
+ <rect
+ y="265.00012"
+ x="50"
+ height="245.00003"
+ width="389.99994"
+ id="rect73984"
+ style="display:inline;fill:url(#linearGradient74024);fill-opacity:1;stroke:none;stroke-width:0.786377;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18" />
+ <rect
+ style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.824674;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect73988"
+ width="389.99994"
+ height="115"
+ x="50"
+ y="270"
+ rx="18"
+ ry="18" />
+ <g
+ id="g72332-0"
+ transform="matrix(2.5980759,-1.4999998,1.4999998,2.5980759,-200.87583,82.6002)"
+ style="display:inline;opacity:0.5;enable-background:new"
+ clip-path="url(#clipPath20866)">
+ <rect
+ y="195.73203"
+ x="74.999992"
+ height="3"
+ width="28"
+ id="rect72324-5"
+ style="fill:#c01c28;stroke:none;stroke-width:2;stroke-linecap:square;stroke-linejoin:round" />
+ <rect
+ style="fill:#c01c28;stroke:none;stroke-width:2;stroke-linecap:square;stroke-linejoin:round"
+ id="rect72326-1"
+ width="24"
+ height="2"
+ x="74.999992"
+ y="200.73203" />
+ <rect
+ y="204.73203"
+ x="74.999992"
+ height="2"
+ width="27"
+ id="rect72328-1"
+ style="fill:#c01c28;stroke:none;stroke-width:2;stroke-linecap:square;stroke-linejoin:round" />
+ <rect
+ y="192.73203"
+ x="70.999992"
+ height="17"
+ width="37"
+ id="rect72330-0"
+ style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke:#c01c28;stroke-width:2;stroke-linecap:square;stroke-linejoin:round;stroke-opacity:1;enable-background:new" />
+ </g>
+ <rect
+ style="display:inline;fill:#cdab8f;fill-opacity:1;stroke:none;stroke-width:0.824676;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect21432"
+ width="389.99994"
+ height="160.00003"
+ x="50"
+ y="220"
+ rx="18"
+ ry="18.000002" />
+ <rect
+ style="display:inline;fill:#000000;fill-opacity:0.189975;stroke:none;stroke-width:0.141536;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect114989"
+ width="380.00006"
+ height="8.0001221"
+ x="54.999939"
+ y="295.99994" />
+ <g
+ id="g119181"
+ transform="translate(-53,-14.67767)">
+ <g
+ id="g119114-3"
+ transform="matrix(-1,0,0,1,1003,27)">
+ <g
+ id="g119109-3">
+ <path
+ id="path103457-8"
+ style="opacity:1;fill:#a47854;fill-opacity:1;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ d="M 510,213.67767 595.95247,127.7252 A 8.228854,8.228854 22.5 0 1 610,133.54388 v 106.13379 a 48.284271,48.284271 112.5 0 1 -14.14214,34.14214 L 510,359.67767 Z" />
+ <path
+ id="path103463-0"
+ style="opacity:1;fill:#bf9370;fill-opacity:1;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ d="m 414,134 v 106 a 48.284271,48.284271 67.5 0 0 14.14214,34.14214 L 510,356 V 216 a 14.485281,14.485281 67.5 0 0 -4.24264,-10.24264 L 428.14214,128.14214 A 8.2842712,8.2842712 157.5 0 0 414,134 Z"
+ transform="matrix(-1,0,0,1,1020,-2.32233)" />
+ </g>
+ </g>
+ </g>
+ <g
+ id="g136672"
+ transform="matrix(-1,0,0,1,543,-14.67767)">
+ <g
+ id="g136670"
+ transform="matrix(-1,0,0,1,1003,27)">
+ <g
+ id="g136668">
+ <path
+ id="path136664"
+ style="opacity:1;fill:#ac805d;fill-opacity:1;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ d="M 510,213.67767 623.66616,134.98572 A 10.40896,10.40896 27.652423 0 1 640,143.54388 v 106.13379 a 38.171367,38.171367 117.65242 0 1 -16.44384,31.3842 L 510,359.67767 Z" />
+ <path
+ id="path136666"
+ style="opacity:1;fill:#c59e7e;fill-opacity:1;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ d="m 384,143 v 106 a 38.113905,38.113905 62.312078 0 0 16.45794,31.36381 L 510,356 V 216 a 11.434172,11.434172 62.312078 0 0 -4.93738,-9.40914 L 400.45794,134.36381 A 10.494857,10.494857 152.31208 0 0 384,143 Z"
+ transform="matrix(-1,0,0,1,1020,-2.32233)" />
+ </g>
+ </g>
+ </g>
+ </g>
+ <g
+ id="g73964"
+ transform="matrix(-1,0,0,1,187.99999,0)"
+ clip-path="url(#clipPath15655)">
+ <rect
+ y="610"
+ x="-37.000008"
+ height="95"
+ width="311.99997"
+ id="rect144015"
+ style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.677771;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18" />
+ <rect
+ y="565"
+ x="-37.000008"
+ height="135.00021"
+ width="311.99997"
+ id="rect1286-0"
+ style="display:inline;fill:url(#linearGradient71993);fill-opacity:1;stroke:none;stroke-width:0.677772;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18" />
+ <rect
+ y="490.00018"
+ x="-37.000008"
+ height="140.00005"
+ width="311.99997"
+ id="rect1812-5"
+ style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.621312;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ rx="18"
+ ry="18" />
+ <rect
+ style="display:inline;fill:#cdab8f;fill-opacity:1;stroke:none;stroke-width:0.633279;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect1288-9"
+ width="311.99997"
+ height="154.00015"
+ x="-37.000008"
+ y="469.99994"
+ rx="18"
+ ry="18" />
+ <rect
+ y="469.99994"
+ x="127.99999"
+ height="154.0002"
+ width="14.999992"
+ id="rect1652-4"
+ style="display:inline;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:0.708024;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
+ <rect
+ style="display:inline;opacity:0.2;fill:url(#linearGradient18442);fill-opacity:1;stroke:none;stroke-width:0.852387;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect18434"
+ width="75.000008"
+ height="14.999996"
+ x="-118"
+ y="-560"
+ transform="scale(-1)" />
+ <path
+ id="path15515"
+ style="color:#000000;fill:#77767b;-inkscape-stroke:none"
+ d="M 48.10546,510.86523 119.64257,545 h 165.23242 v 15 H 116.24804 L 48.10546,527.48438 Z" />
+ <rect
+ style="display:inline;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:0.838524;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect1654-7"
+ width="14.999992"
+ height="85.000046"
+ x="128"
+ y="615" />
+ <circle
+ r="5.999999"
+ cy="682"
+ cx="51.000031"
+ id="path1887-9-5"
+ style="display:inline;opacity:0.7;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:6;stroke-linecap:square;stroke-linejoin:round;enable-background:new" />
+ <circle
+ style="display:inline;opacity:0.7;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:6;stroke-linecap:square;stroke-linejoin:round;enable-background:new"
+ id="circle1889-3-4"
+ cx="69.000031"
+ cy="682"
+ r="5.999999" />
+ <path
+ id="path1505-1"
+ d="m 3.000019,665.00008 19.999996,-21 19.999997,21 H 32.000014 v 23 H 14.000017 v -23 z"
+ style="display:inline;opacity:0.7;fill:#3d3846;stroke:none;stroke-width:3px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
+ <rect
+ style="display:inline;opacity:0.2;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.852387;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect19380"
+ width="15.000061"
+ height="5.0000024"
+ x="544.99994"
+ y="-127.99999"
+ transform="rotate(90)" />
+ <rect
+ style="display:inline;opacity:0.1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.852387;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ id="rect19436"
+ width="15.000061"
+ height="5.0000024"
+ x="544.99994"
+ y="-148"
+ transform="rotate(90)" />
+ <rect
+ y="-37.000008"
+ x="-560"
+ height="70"
+ width="14.999992"
+ id="rect18432"
+ style="display:inline;fill:#77767b;fill-opacity:1;stroke:none;stroke-width:0.708021;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ transform="rotate(-90)" />
+ </g>
+ <g
+ id="g41210"
+ transform="translate(10,-20)">
+ <rect
+ style="opacity:0.1;fill:#000000;fill-opacity:1;stroke-width:2.83466;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect54633"
+ width="100"
+ height="150"
+ x="1235"
+ y="545"
+ rx="50"
+ ry="50" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient41236);fill-opacity:1;stroke-width:2.83466;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect26273"
+ width="100"
+ height="150"
+ x="1235"
+ y="540"
+ rx="50"
+ ry="50" />
+ <rect
+ style="opacity:1;fill:#241f31;fill-opacity:1;stroke-width:2.73854;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect38424"
+ width="14"
+ height="54"
+ x="1278"
+ y="540"
+ rx="4.6666665"
+ ry="5" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient38318);fill-opacity:1;stroke-width:2.73854;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect36571"
+ width="14"
+ height="30"
+ x="1278"
+ y="555"
+ rx="4.6666665"
+ ry="5" />
+ <g
+ id="g38274"
+ style="fill:#241f31"
+ transform="matrix(0.93333333,0,0,1,85.666667,0)">
+ <rect
+ style="opacity:1;fill:#37323f;fill-opacity:1;stroke-width:2.83465;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect38160"
+ width="15"
+ height="2"
+ x="1277.5"
+ y="560" />
+ <rect
+ style="opacity:1;fill:#37323f;fill-opacity:1;stroke-width:2.83465;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect38264"
+ width="15"
+ height="2"
+ x="1277.5"
+ y="566" />
+ <rect
+ style="opacity:1;fill:#37323f;fill-opacity:1;stroke-width:2.83465;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect38266"
+ width="15"
+ height="2"
+ x="1277.5"
+ y="572" />
+ <rect
+ style="opacity:1;fill:#37323f;fill-opacity:1;stroke-width:2.83465;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect38268"
+ width="15"
+ height="2"
+ x="1277.5"
+ y="578" />
+ </g>
+ </g>
+ <g
+ id="g1508"
+ transform="matrix(-1.0606644,-0.90000374,1.0606644,-0.90000374,1638.1651,917.82003)"
+ style="display:inline;stroke-width:1.08559;enable-background:new">
+ <path
+ id="path1500"
+ d="m 92.489078,262.86508 -13.333331,-13.33333 96.057593,-96.05759 13.33333,13.33333 z"
+ style="display:inline;fill:#f5c211;fill-opacity:1;stroke:none;stroke-width:0.69798px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
+ <path
+ id="rect176096"
+ style="display:inline;fill:#000000;fill-opacity:0.1;stroke:none;stroke-width:3.55773;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
+ d="M 1674.9355,580 1630,595 l 44.8281,14.96484 V 610 H 1872 h 6.5977 10.2793 a 15.00006,15.00006 90 0 0 15,-15 15.00006,15.00006 90 0 0 -15,-15 H 1872 v 21 h -197.0645 z"
+ transform="matrix(0.47140264,-0.47140264,-0.55555325,-0.55555325,-376.00061,1373.5743)" />
+ <path
+ id="path1502"
+ d="m 82.408094,252.94605 -5.000007,-4.99997 96.057593,-96.05759 4.99999,4.99999 z"
+ style="display:inline;opacity:1;fill:#e5a50a;fill-opacity:1;stroke:none;stroke-width:0.493546px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
+ <path
+ id="path1504"
+ d="m 89.074777,259.61273 4.99998,5.00002 96.057593,-96.05759 -5,-5.00001 z"
+ style="display:inline;opacity:1;fill:#f8e45c;fill-opacity:1;stroke:none;stroke-width:0.493546px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;enable-background:new" />
+ <g
+ transform="translate(16,-16)"
+ id="g1494"
+ style="fill:#5e5c64">
+ <g
+ transform="translate(1.4142136,-1.4142136)"
+ style="fill:#5e5c64;fill-opacity:1"
+ id="g1490">
+ <rect
+ transform="rotate(-135)"
+ y="-13.768661"
+ x="-253.63036"
+ height="11.250942"
+ width="23.570223"
+ id="rect1488"
+ style="opacity:1;vector-effect:none;fill:url(#linearGradient49722);fill-opacity:1;stroke:none;stroke-width:2.57482;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ </g>
+ <ellipse
+ ry="10"
+ rx="11.785111"
+ transform="rotate(-135)"
+ cy="-0.51772368"
+ cx="-241.84525"
+ id="ellipse1492"
+ style="opacity:1;fill:url(#linearGradient1958);fill-opacity:1;stroke:none;stroke-width:2.17119;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ style="display:inline;opacity:1;vector-effect:none;fill:url(#linearGradient1547);fill-opacity:1;stroke:none;stroke-width:19.7339;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new"
+ d="m 64.609279,277.41155 12.849055,-29.51572 16.666668,16.66666 z"
+ id="path1496" />
+ <path
+ id="path1498"
+ d="m 64.609275,277.41155 5.580544,-12.81912 7.238569,7.23859 z"
+ style="display:inline;opacity:1;vector-effect:none;fill:#3d3846;fill-opacity:1;stroke:none;stroke-width:19.7339;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
+ <rect
+ style="opacity:1;fill:url(#linearGradient1549);fill-opacity:1;stroke:none;stroke-width:2.17119;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="rect1506"
+ width="23.570223"
+ height="8.0000019"
+ x="-253.63036"
+ y="7.4430614"
+ transform="rotate(-135)" />
+ </g>
+ <g
+ id="g12266"
+ transform="translate(8.9998779)">
+ <g
+ id="g11342">
+ <rect
+ style="fill:#000000;fill-opacity:0.1;stroke-width:2.6674;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect11223"
+ width="447.99979"
+ height="175"
+ x="727.00018"
+ y="555"
+ rx="32"
+ ry="32" />
+ <rect
+ style="fill:url(#linearGradient8579);fill-opacity:1;stroke-width:2.6674;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect8571"
+ width="447.99979"
+ height="175"
+ x="727.00018"
+ y="550"
+ rx="32"
+ ry="32" />
+ <rect
+ style="fill:#3d3846;fill-opacity:1;stroke-width:2.6674;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect16425"
+ width="447.99979"
+ height="175"
+ x="-1175"
+ y="-715"
+ rx="32"
+ ry="32"
+ transform="scale(-1)" />
+ <g
+ id="g5172"
+ transform="translate(0,4)"
+ style="fill:#241f31;fill-opacity:1">
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5098"
+ width="47"
+ height="28.000004"
+ x="747"
+ y="593"
+ rx="14.000001"
+ ry="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5100"
+ cx="761"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5102"
+ cx="799"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5104"
+ cx="837"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5106"
+ cx="875"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5108"
+ cx="913"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5110"
+ cx="951"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5112"
+ cx="989"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5114"
+ cx="1027"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5116"
+ cx="1065"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5118"
+ cx="1103"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5120"
+ cx="1141"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5122"
+ cx="818"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5124"
+ cx="856"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5126"
+ cx="894"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5128"
+ cx="932"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5130"
+ cx="970"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5132"
+ cx="1008"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5134"
+ cx="1046"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5136"
+ cx="1084"
+ cy="607"
+ r="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5138"
+ width="47"
+ height="28.000004"
+ x="1108"
+ y="593"
+ rx="14.000001"
+ ry="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5140"
+ width="66"
+ height="28.000004"
+ x="747"
+ y="631"
+ rx="14.000002"
+ ry="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5142"
+ cx="837"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5144"
+ cx="875"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5146"
+ cx="913"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5148"
+ cx="951"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5150"
+ cx="989"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5152"
+ cx="1027"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5154"
+ cx="1065"
+ cy="645"
+ r="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5156"
+ width="66"
+ height="28.000004"
+ x="1089"
+ y="631"
+ rx="14.000002"
+ ry="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5158"
+ cx="861"
+ cy="683"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5160"
+ cx="1103"
+ cy="683"
+ r="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5162"
+ cx="1141"
+ cy="683"
+ r="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5164"
+ width="132.00012"
+ height="28.000004"
+ x="885"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ <circle
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle5166"
+ cx="823.00745"
+ cy="683"
+ r="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5168"
+ width="52"
+ height="28.000004"
+ x="1027"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ <rect
+ style="fill:#241f31;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect5170"
+ width="52"
+ height="28.000004"
+ x="747"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ </g>
+ <g
+ id="g5096">
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect16507"
+ width="47"
+ height="28.000004"
+ x="747"
+ y="593"
+ rx="14.000001"
+ ry="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle16491"
+ cx="761"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle16493"
+ cx="799"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2091"
+ cx="837"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2093"
+ cx="875"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2095"
+ cx="913"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2097"
+ cx="951"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2099"
+ cx="989"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2101"
+ cx="1027"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2103"
+ cx="1065"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2105"
+ cx="1103"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2107"
+ cx="1141"
+ cy="569"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2189"
+ cx="818"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2191"
+ cx="856"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2193"
+ cx="894"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2195"
+ cx="932"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2197"
+ cx="970"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2199"
+ cx="1008"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2201"
+ cx="1046"
+ cy="607"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle2203"
+ cx="1084"
+ cy="607"
+ r="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect2927"
+ width="47"
+ height="28.000004"
+ x="1108"
+ y="593"
+ rx="14.000001"
+ ry="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect3027"
+ width="66"
+ height="28.000004"
+ x="747"
+ y="631"
+ rx="14.000002"
+ ry="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3029"
+ cx="837"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3031"
+ cx="875"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3033"
+ cx="913"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3035"
+ cx="951"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3037"
+ cx="989"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3039"
+ cx="1027"
+ cy="645"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3041"
+ cx="1065"
+ cy="645"
+ r="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect3147"
+ width="66"
+ height="28.000004"
+ x="1089"
+ y="631"
+ rx="14.000002"
+ ry="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3313"
+ cx="861"
+ cy="683"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3323"
+ cx="1103"
+ cy="683"
+ r="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3325"
+ cx="1141"
+ cy="683"
+ r="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect3434"
+ width="132.00012"
+ height="28.000004"
+ x="885"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ <circle
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="circle3870"
+ cx="823.00745"
+ cy="683"
+ r="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect4034"
+ width="52"
+ height="28.000004"
+ x="1027"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ <rect
+ style="fill:#605c67;fill-opacity:1;stroke-width:2.64567;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect4116"
+ width="52"
+ height="28.000004"
+ x="747"
+ y="669"
+ rx="14.000001"
+ ry="14.000002" />
+ </g>
+ </g>
+ </g>
+ <g
+ id="g40266"
+ transform="translate(40,-40)">
+ <g
+ transform="matrix(3.0334033,0,0,3.0334033,691.74852,21.822848)"
+ id="g11671"
+ style="display:inline">
+ <rect
+ ry="6.321938"
+ rx="6.321938"
+ y="153.5164"
+ x="255.73634"
+ height="16.483147"
+ width="26.373026"
+ id="rect11609"
+ style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4.94494;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <circle
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.571784;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="circle11603"
+ cx="251.48645"
+ cy="152.00313"
+ r="21.298958" />
+ <rect
+ y="152.00313"
+ x="230.18748"
+ height="22.246307"
+ width="42.597923"
+ id="rect11607"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.452027;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <circle
+ r="21.298958"
+ cy="175.89775"
+ cx="251.48645"
+ id="circle50661"
+ style="opacity:1;fill:#000000;fill-opacity:0.1;stroke:none;stroke-width:0.571784;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <circle
+ r="21.298958"
+ cy="174.24944"
+ cx="251.48645"
+ id="circle11605"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.571784;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ <g
+ clip-path="url(#clipPath11679)"
+ id="g11615"
+ transform="matrix(0.90939595,0,0,0.90939595,34.094406,81.739982)">
+ <circle
+ style="opacity:1;fill:#deddda;fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
+ id="path11601"
+ cx="239.05104"
+ cy="77.263527"
+ r="19.711458" />
+ <circle
+ r="19.711458"
+ cy="85.201027"
+ cx="239.05104"
+ id="circle11611"
+ style="opacity:1;fill:url(#linearGradient58186);fill-opacity:1;stroke:none;stroke-width:0.529167;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
+ </g>
+ </g>
+ <path
+ style="opacity:0.7;fill:url(#linearGradient31483);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 1444,468 v -40 a 20,20 135 0 1 20,-20 8.2856462,8.2856462 133.0573 0 0 8,-8.56177 A 8.5002244,8.5002244 44.583652 0 0 1463.4382,391 H 1438 a 29,29 45 0 1 -29,-29 28.504386,28.504386 136.00509 0 1 29,-28 h 24 a 10,10 135 0 0 10,-10 10,10 45 0 0 -10,-10 h -23 a 60,60 45 0 1 -60,-60 V 134 h 170 v 72.41169 A 52.588305,52.588305 135 0 1 1496.4117,259 h -35.4152 A 9.9965044,9.9965044 135 0 0 1451,268.9965 v 0.007 a 9.9965044,9.9965044 45 0 0 9.9965,9.9965 h 21.7924 A 40.211147,40.211147 45 0 1 1523,319.21115 40.000557,40.000557 135.30244 0 1 1482.7889,359 H 1461 a 7,7 135 0 0 -7,7 v 0.10303 A 7.8969727,7.8969727 45 0 0 1461.897,374 l 18.6012,0 A 23.501846,23.501846 45 0 1 1504,397.50185 23.5,23.5 135.0045 0 1 1480.4982,421 H 1469 a 15,15 135 0 0 -15,15 v 32 a 5,5 135 0 1 -5,5 5,5 45 0 1 -5,-5 z"
+ id="path23859" />
+ </g>
+ <rect
+ style="opacity:1;fill:#f66151;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect49790"
+ width="135"
+ height="15"
+ x="1785"
+ y="310" />
+ <rect
+ style="opacity:1;fill:#f66151;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:square;stroke-linejoin:round;stroke-dashoffset:26.4"
+ id="rect50816"
+ width="135"
+ height="15"
+ x="1785"
+ y="435" />
+</svg>
diff --git a/gnome-initial-setup/pages/welcome/meson.build b/gnome-initial-setup/pages/welcome/meson.build
new file mode 100644
index 0000000..be977dc
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/meson.build
@@ -0,0 +1,10 @@
+sources += gnome.compile_resources(
+ 'welcome-resources',
+ files('welcome.gresource.xml'),
+ c_name: 'welcome',
+)
+
+sources += files(
+ 'gis-welcome-page.c',
+ 'gis-welcome-page.h',
+)
diff --git a/gnome-initial-setup/pages/welcome/welcome.gresource.xml b/gnome-initial-setup/pages/welcome/welcome.gresource.xml
new file mode 100644
index 0000000..9a2f9c6
--- /dev/null
+++ b/gnome-initial-setup/pages/welcome/welcome.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks">gis-welcome-page.ui</file>
+ <file preprocess="xml-stripblanks">initial-setup-welcome.svg</file>
+ </gresource>
+</gresources>