summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.gitlab-ci.yml112
-rw-r--r--CONTRIBUTING.md88
-rw-r--r--COPYING339
-rw-r--r--DEBUGGING.md36
-rw-r--r--NEWS1956
-rw-r--r--README.md72
-rwxr-xr-xbuild-aux/maintainer-upload-release37
-rw-r--r--build-aux/meson.build4
-rw-r--r--data/20-gnome-initial-setup.rules.in29
-rw-r--r--data/gnome-initial-setup-copy-worker.desktop.in.in10
-rw-r--r--data/gnome-initial-setup-copy-worker.service.in15
-rw-r--r--data/gnome-initial-setup-first-login.desktop.in.in13
-rw-r--r--data/gnome-initial-setup-first-login.service.in16
-rw-r--r--data/gnome-initial-setup.conf3
-rw-r--r--data/gnome-initial-setup.desktop.in.in12
-rw-r--r--data/gnome-initial-setup.service.in13
-rw-r--r--data/gnome-initial-setup.session.conf.in8
-rw-r--r--data/gnome-initial-setup.session.in4
-rw-r--r--data/initial-setup.json8
-rwxr-xr-xdata/meson-add-wants.sh30
-rw-r--r--data/meson.build130
-rw-r--r--gnome-initial-setup.doap31
-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
-rw-r--r--meson.build108
-rw-r--r--meson_options.txt22
-rw-r--r--po/LINGUAS75
-rw-r--r--po/POTFILES.in46
-rw-r--r--po/POTFILES.skip2
-rw-r--r--po/ab.po1538
-rw-r--r--po/af.po796
-rw-r--r--po/an.po721
-rw-r--r--po/ar.po839
-rw-r--r--po/as.po789
-rw-r--r--po/be.po712
-rw-r--r--po/bg.po734
-rw-r--r--po/bn_IN.po404
-rw-r--r--po/bs.po574
-rw-r--r--po/ca.po773
-rw-r--r--po/ca@valencia.po715
-rw-r--r--po/ckb.po833
-rw-r--r--po/cs.po729
-rw-r--r--po/da.po878
-rw-r--r--po/de.po881
-rw-r--r--po/el.po1011
-rw-r--r--po/en_GB.po998
-rw-r--r--po/eo.po928
-rw-r--r--po/es.po1143
-rw-r--r--po/et.po408
-rw-r--r--po/eu.po1026
-rw-r--r--po/fa.po1059
-rw-r--r--po/fi.po1099
-rw-r--r--po/fr.po800
-rw-r--r--po/fur.po1004
-rw-r--r--po/ga.po573
-rw-r--r--po/gd.po802
-rw-r--r--po/gl.po844
-rw-r--r--po/gu.po684
-rw-r--r--po/he.po1051
-rw-r--r--po/hi.po749
-rw-r--r--po/hr.po886
-rw-r--r--po/hu.po756
-rw-r--r--po/ia.po358
-rw-r--r--po/id.po841
-rw-r--r--po/is.po917
-rw-r--r--po/it.po759
-rw-r--r--po/ja.po864
-rw-r--r--po/ka.po749
-rw-r--r--po/kk.po955
-rw-r--r--po/kn.po698
-rw-r--r--po/ko.po857
-rw-r--r--po/lt.po912
-rw-r--r--po/lv.po832
-rw-r--r--po/meson.build4
-rw-r--r--po/mjw.po638
-rw-r--r--po/ml.po727
-rw-r--r--po/mr.po743
-rw-r--r--po/ms.po606
-rw-r--r--po/nb.po731
-rw-r--r--po/ne.po808
-rw-r--r--po/nl.po991
-rw-r--r--po/oc.po1028
-rw-r--r--po/or.po689
-rw-r--r--po/pa.po1092
-rw-r--r--po/pl.po736
-rw-r--r--po/pt.po1096
-rw-r--r--po/pt_BR.po1134
-rw-r--r--po/ro.po840
-rw-r--r--po/ru.po763
-rw-r--r--po/sk.po1108
-rw-r--r--po/sl.po1187
-rw-r--r--po/sr.po1021
-rw-r--r--po/sr@latin.po833
-rw-r--r--po/sv.po768
-rw-r--r--po/ta.po728
-rw-r--r--po/te.po657
-rw-r--r--po/tg.po956
-rw-r--r--po/th.po324
-rw-r--r--po/tr.po737
-rw-r--r--po/ug.po385
-rw-r--r--po/uk.po761
-rw-r--r--po/vi.po1017
-rw-r--r--po/zh_CN.po1034
-rw-r--r--po/zh_HK.po587
-rw-r--r--po/zh_TW.po988
-rw-r--r--subprojects/geocode-glib.wrap3
-rw-r--r--subprojects/libadwaita.wrap3
-rw-r--r--subprojects/malcontent-ui.wrap3
247 files changed, 88260 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a5af89b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+# Editor temporary files
+*~
+*.swp
+
+# GNOME Builder litter
+/.buildconfig
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..c2d5aba
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,112 @@
+include:
+ - remote: 'https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/c5626190ec14b475271288dda7a7dae8dbe0cd76/templates/arch.yml'
+
+variables:
+ FDO_UPSTREAM_REPO: gnome/gnome-initial-setup
+
+stages:
+ - prepare
+ - build
+
+.arch.container.common:
+ variables:
+ # When branching a stable release, change 'main'
+ # to the release number/branch to ensure that
+ # a new image will be created, tailored for the
+ # stable branch.
+ FDO_DISTRIBUTION_TAG: '2022-12-02.0-43'
+
+# See also https://gitlab.freedesktop.org/freedesktop/ci-templates
+build.container.arch@x86_64:
+ extends:
+ - '.fdo.container-build@arch'
+ - '.arch.container.common'
+ stage: 'prepare'
+ variables:
+ # no need to pull the whole tree for rebuilding the image
+ GIT_STRATEGY: none
+ # Expiry sets fdo.expires on the image
+ FDO_EXPIRES_AFTER: 8w
+ FDO_DISTRIBUTION_PACKAGES: >-
+ accountsservice
+ appstream
+ base-devel
+ flatpak
+ fontconfig
+ gdm
+ geoclue
+ geocode-glib
+ git
+ gnome-desktop-4
+ gnome-online-accounts
+ gsettings-desktop-schemas
+ gobject-introspection
+ gtk4
+ gtk-doc
+ itstool
+ json-glib
+ krb5
+ libadwaita
+ libglib-testing
+ libgweather-4
+ libibus
+ libmalcontent
+ libnm
+ libnma-gtk4
+ libpwquality
+ librest
+ libsecret
+ malcontent
+ meson
+ polkit
+ sassc
+ webkit2gtk-5.0
+ yelp-tools
+
+.job_template: &job_definition
+ extends:
+ - '.fdo.distribution-image@arch'
+ - '.arch.container.common'
+
+ stage: build
+
+ script:
+ # In general, we would like warnings to be fatal. However, code copied from
+ # gnome-control-center uses many deprecated functions. Until we have a good
+ # answer to sharing that code (#68), make those warnings non-fatal.
+ - meson setup
+ --wrap-mode=nofallback
+ --fatal-meson-warnings
+ -Dsystemd=${EXPLICIT_FEATURES}
+ --auto-features ${AUTO_FEATURES}
+ ${OPTIONS}
+ -Dwerror=true -Dc_args=-Wno-error=deprecated-declarations
+ ${EXTRA_PARAMETERS}
+ _build
+ .
+ - cd _build
+ - ninja -v
+ # Check that strings can be extracted
+ - ninja -v gnome-initial-setup-pot
+ # Check the package can be installed
+ - DESTDIR=$(mktemp -d) ninja -v install
+ - meson test
+ artifacts:
+ when: on_failure
+ name: "gnome-initial-setup-${CI_COMMIT_REF_NAME}-${CI_JOB_NAME}"
+ paths:
+ - "${CI_PROJECT_DIR}/_build/meson-logs"
+
+build-minimal:
+ <<: *job_definition
+ variables:
+ EXPLICIT_FEATURES: 'false'
+ AUTO_FEATURES: 'disabled'
+ OPTIONS: ''
+
+build-maximal:
+ <<: *job_definition
+ variables:
+ EXPLICIT_FEATURES: 'true'
+ AUTO_FEATURES: 'enabled'
+ OPTIONS: '-Dvendor-conf-file=/var/lib/weird-vendor-specific-path.ini'
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..9c40d4c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,88 @@
+Initial Setup
+=============
+
+The first time a new system is booted is a special situation. There is
+no user account yet, and a few basic setup steps need to be performed
+before it can be considered fully usable. The initial setup mode is an
+attempt to solve these problems.
+
+When in initial setup mode, GDM does not bring up the regular greeter
+for the login screen, but instead starts the `gnome-initial-setup`
+application in a special session. `gnome-initial-setup` offers a series
+of steps to:
+
+1. Select a language
+2. Select a keyboard layout
+3. Connect to the network
+4. Adjust some privacy settings
+5. Set the right location/timezone
+6. Configure software sources
+7. Set up online accounts
+8. Create a new user account
+9. Configure parental controls
+
+In terms of the user experience, we want the initial setup to seamlessly
+switch to the regular user session. In particular, we don't want to
+make the user enter their credentials again on the login screen.
+
+We can't run the `gnome-initial-setup` application with the correct user,
+since the user account does not exist yet at that time. Therefore, GDM
+runs `gnome-initial-setup` as a `gnome-initial-setup` use. When
+`gnome-inital-setup` is done, it then initiates an autologin for the newly
+created user account to switch to the ā€˜realā€™ session.
+
+Due to this arrangement, we need to copy all the settings that have been
+changed during the initial setup session from the `gnome-initial-setup`
+user to the real user.
+
+Mechanics
+=========
+
+By default, this functionality is enabled in GDM. To disable it, add the
+following to GDM's configuration file:
+
+```ini
+[daemon]
+InitialSetupEnable=False
+```
+
+(By default, this file lives at `/etc/gdm/custom.conf`, but this can be
+customized at GDM build time. For Debian-like systems, use
+`/etc/gdm3/daemon.conf`.)
+
+If enabled, GDM will trigger Initial Setup only if there are no users configured
+yet and some other conditions also hold. You can force GDM to run Initial Setup
+even if users exist by appending `gnome.initial-setup=1` to the kernel command line.
+
+The session that gdm starts for the initial-setup session is
+defined by the file
+`/usr/share/gnome-session/sessions/gnome-initial-setup.session`.
+Like the regular greeter session, it uses the desktop files in
+`/usr/share/gdm/greeter/applications/`.
+
+`gnome-initial-setup` also has an "existing user" mode which activates
+`gnome-initial-setup` when a user first logs in. The
+`gnome-initial-setup-first-login.desktop` in the
+[xdg autostart][] directory uses `gnome-session` to check if the user has a
+`gnome-initial-setup-done` file in their `XDG_CONFIG_DIR`; if they don't,
+`gnome-initial-setup` will launch with pages that are suitable for a
+non-privileged user and on exiting will write the done file. However, since
+GNOME 40, this mode would interfere with the first-login tour
+prompt, so `gnome-initial-setup` silently writes the stamp file and exits.
+
+[xdg autostart]: https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
+ "The Desktop Application Autostart Specification"
+
+Tips for development
+====================
+
+Enterprise Login
+----------------
+
+Initial Setup can configure the system to be part of an enterprise domain.
+This functionality is available if `realmd` is installed (or, more precisely,
+the name `org.freedesktop.realmd` is owned on the system bus) and hidden if not.
+
+The FreeIPA project runs a [demo server](https://www.freeipa.org/page/Demo),
+which may be useful to test this functionality if you do not have an
+enterprise domain of your own to test against.
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/DEBUGGING.md b/DEBUGGING.md
new file mode 100644
index 0000000..d8063d9
--- /dev/null
+++ b/DEBUGGING.md
@@ -0,0 +1,36 @@
+Debugging Initial Setup can be tricky if what you need to work on relies on the
+system booting for the very first time, and can't be reproduced easily (or at
+all) from a regular session once the ā€œrealā€ initial setup has happened and the
+first user has been created.
+
+Examples of things you might want to do that can be especially tricky if you
+need to do them before creating the very first user:
+
+- Changing the default configuration of the system
+- Installing/updating packages to try something out
+- Getting logs from the journal
+- Getting a backtrace to debug a crash happening
+
+Here are some strategies that may be useful.
+
+## Force GDM to launch Initial Setup
+
+GDM is responsible for launching Initial Setup in a cut-down desktop session
+when no users exist on the system. Since GDM 3.26.1, you can force it to launch
+it even if users already exist by adding ` gnome.initial-setup=1` to the kernel
+command line. The exact method depends on your distribution, but in general terms:
+
+- Restart the computer
+- Force GRUB to display a boot menu (distro-dependent; try hitting `Escape` or
+ holding `Shift` during boot)
+- Hit `e` to edit the default menu entry
+- Use the arrow keys to select the line beginning with `linux` or `linuxefi`
+- Hit `End`
+- Type ` gnome.initial-setup=1`
+- Hit `F10` to boot
+
+## Get a root shell before Initial Setup is complete
+
+Follow the steps above, but add ` systemd.debug_shell` to the end of the kernel
+command line. Hit `Ctrl + Alt + F9` to switch to VT9, where you should find a
+root shell awaiting you.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..3b0943b
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,1956 @@
+43.2
+----
+
+* The webkitgtk-6.0 API version is now supported, in addition to the previous
+ webkit2gtk-5.0 API version.
+
+* Bugs fixed:
+ - The Shell quick settings menu will now be shown in the Initial Setup session.
+ This regressed when the previous menu was renamed in gnome-shell during the
+ 43 cycle. (!166)
+
+ - Error messages shown during enterprise account setup can now be closed by
+ clicking the Close button. (!169)
+
+* Translation updates:
+ - Abkhazian
+ - Dutch
+ - Greek, Modern (1453-)
+ - Icelandic
+
+43.1
+----
+
+* Several bugs in the enterprise login flow have been fixed (#154, !160, !161).
+
+* Translation updates:
+ - Abkhazian
+ - Chinese (Taiwan)
+ - Czech
+ - Friulian
+ - Kazakh
+ - Nepali
+ - Occitan (post 1500)
+ - Spanish
+ - Turkish
+
+43.0
+----
+
+* Translation updates:
+ - Bulgarian
+ - Croatian
+ - French
+ - Hebrew
+ - Lithuanian
+ - Slovenian
+
+43.rc.1
+-------
+
+* The authentication dialog in the enterprise login page regressed in the port
+ to GTK 4; in this release, its buttons are accessible once again, and its
+ appearance improved. (#152)
+
+* Translation updates:
+ - Galician
+ - Georgian
+ - German
+ - Korean
+ - Latvian
+ - Portuguese (Brazil)
+ - Serbian
+
+43.rc
+-----
+
+* Initial Setup no longer crashes if you attempt to use capital letters or
+ periods in your username on systems that reject such usernames. Instead,
+ it always forbids capital letters and periods in usernames. (#117, !30,
+ thanks to Xiang Fan)
+
+* Several regressions in the timezone page were fixed, including selecting a
+ timezone by typing the name of a city and picking a location from
+ the dropdown (#145, !157, Georges Stavracas) and searching for locations not
+ found in the gweather database (!158).
+
+* Page-skipping works again (!156, Adam Williamson)
+
+* Parental Controls support now correctly requires malcontent 0.11.0's new API
+ name. Beware: if libostree or libflatpak are built against libsoup2.4, then
+ linking Initial Setup to libmalcontent for parental controls support will
+ cause it to abort on startup, since Initial Setup also links to webkit2gtk5
+ which requires libsoup3.
+
+43.beta
+-------
+
+* Initial Setup has been ported to GTK 4, libadwaita, and friends, thanks to
+ Georges Stavracas. As a result:
+
+ - libnma-gtk4 is now required, rather than libnma.
+
+ - GTK 4 is required in addition to GTK 3. This is because goa-backend does
+ not have a GTK 4 port. Like in gnome-control-center, the use of goa-backend
+ is split out into a separate process.
+
+ - webkit2gtk5 is now required. (However, goa-backend brings a transitive
+ dependency on webkit2gtk4.)
+
+ - Since webkit2gtk5 requires libsoup 3, the soup2=true configuration option
+ has been removed. Initial Setup depends on geocode-glib-2.0, and versions
+ of librest and gweather4 built against libsoup 3.
+
+ - Support for taking a selfie as your account avatar has been removed, since
+ libcheese-gtk lacks a GTK 4 port, and the optional libcheese[-gtk]
+ dependency is no more.
+
+ - Parental controls support requires malcontent 0.11 which is not released
+ at the time of writing.
+
+ There is a known regression from the port: selecting a timezone by typing the
+ name of a city and picking a location from the dropdown does not work. To
+ work around this, click a location on the map to select a timezone. This is
+ tracked as https://gitlab.gnome.org/GNOME/gnome-initial-setup/-/issues/145
+ and will be addressed in a future release.
+
+* When systemd support is enabled, Initial Setup now installs a sysusers.d
+ snippet for its user, thanks to David King.
+
+* Translation updates:
+ - Abkhazian
+ - Chinese (China)
+ - Galician
+ - Georgian
+ - Hebrew
+ - Indonesian
+ - Persian
+ - Portuguese
+ - Russian
+ - Spanish
+ - Swedish
+ - Turkish
+ - Ukrainian
+
+43.alpha.1
+----------
+
+* When built with -Dsoup2=false (the default), Initial Setup now depends on
+ geocode-glib-2.0, introduced in geocode-glib 3.26.3 as a parallel-installable,
+ soup3-flavoured version of the library. When built with -Dsoup2=true, Initial
+ Setup continues to depend on geocode-glib-1.0.
+
+* Initial Setup's build system tries to detect when its dependencies are built
+ against an incompatible libsoup, but there is one known case that is not
+ detected at build time. If Initial Setup is built with parental controls
+ enabled, the malcontent parental controls library brings with it a dependency
+ on libflatpak and libostree. Neither of these libraries can currently be built
+ against libsoup3. Either they must be built against libcurl rather than
+ libsoup; or Initial Setup must be built with -Dsoup2=true; or Initial Setup
+ must be built with -Dparental_controls=disabled; or Initial Setup will abort
+ on startup.
+
+43.alpha
+--------
+
+* Initial Setup now builds against rest-1.0 if available, falling back to
+ rest-0.7 if not. There is no meson option to control this.
+
+* Initial Setup now builds against libsoup 3 by default. As a transitional
+ measure, you can opt to build against libsoup 2 by passing -Dsoup2=true
+ to meson setup. geocode-glib-1.0, gweather4, and rest (1.0 or 0.7) must
+ have been built against the same version of libsoup.
+
+* A number of Meson .wrap files for dependencies such as gweather4 have been
+ removed. These dependencies are now widely available in stable distros and
+ container images thereof.
+
+* Initial Setup no longer reads gnome-version.xml to determine the GNOME
+ version in the fallback case when no OS information is available from
+ /etc/os-release. gnome-version.xml has been removed from GNOME 43.
+
+* Translation updates:
+ - French
+ - Georgian
+
+42.1.1
+------
+
+* Translation updates:
+ - Friulian
+ - Icelandic
+
+42.1
+----
+
+* Translation updates:
+ - Czech
+ - Dutch
+ - Greek, Modern (1453-)
+ - Latvian
+ - Vietnamese
+
+42.0.1
+------
+
+* The dark theme preference support introduced in 42.rc caused a regression in
+ Initial Setup's handling of the login keyring. The effect was that the new
+ user's login keyring password does not match their system password, and so
+ cannot be unlocked. This regression is fixed in this release. Thanks to Adam
+ Williamson for diagnosing the problem & Matthias Clasen for suggesting the
+ fix. (#141)
+
+42.0
+----
+
+* Translation updates:
+ - BokmƄl, Norwegian
+ - Danish
+ - French
+ - Hungarian
+ - Italian
+ - Japanese
+ - Kazakh
+ - Occitan (post 1500)
+ - Panjabi
+ - Polish
+ - Serbian
+ - Slovak
+
+42.rc
+-----
+
+* Initial Setup now supports the GNOME 42 dark theme preference, and picks up a
+ dependency on libhandy to enable this. This will only have a visible impact
+ if a distribution overrides the default value of this preference, since there
+ is no way for a user to adjust this preference until Initial Setup is
+ complete. (Jeremy Bicha, !140)
+
+* Translation updates:
+ - Bulgarian
+ - Catalan
+ - Croatian
+ - Finnish
+ - German
+ - Korean
+ - Lithuanian
+ - Persian
+ - Swedish
+ - Turkish
+
+42.beta
+-------
+
+* Bugs fixed:
+ - !137 welcome: tweak illustration
+ - !138 timezone: Load GisLocationEntry type before page template
+
+* Translation updates:
+ - Basque
+ - Chinese (China)
+ - Galician
+ - Hebrew
+ - Indonesian
+ - Slovenian
+ - Spanish
+
+42.alpha.1
+----------
+
+* Fix gweather4 pkg-config name.
+
+* Fix project license identifier in Meson build rules.
+
+* Translation updates:
+ - Portuguese (Brazil)
+
+42.alpha
+----
+
+* Initial Setup now depends on libgweather-4 and gnome-desktop-4, rather than
+ their predecessors. Meson wrap files are provided in case these dependencies
+ are not available.
+
+* The third-party software sources page has been tweaked to more accurately
+ describe what the switch does, and to replace the switch with a
+ suggested-action toggle button. (This page is currently Fedora-specific;
+ patches welcome!)
+
+* The COPYING file has been clarified to contain only a copy of GPL version 2.
+ Previously it also contained a copy of LGPL version 2 (not 2.1). There is no
+ change to the actual licensing of GNOME Initial Setup: as before, it is
+ distributed under the terms of the GNU General Public License, version 2 or
+ later.
+
+* The summary page widget tree has been simplified, with no visual change.
+
+* Some Meson deprecation warnings have been addressed. Meson 0.53.0 or newer is
+ now required to build Initial Setup.
+
+* Translation updates:
+ - Basque
+ - Bulgarian
+ - Chinese (China)
+ - Croatian
+ - Friulian
+ - Hebrew
+ - Icelandic
+ - Indonesian
+ - Japanese
+ - Latvian
+ - Occitan (post 1500)
+ - Portuguese
+ - Portuguese (Brazil)
+ - Russian
+ - Serbian
+ - Slovak
+ - Spanish
+ - Swedish
+ - Ukrainian
+ - Vietnamese
+
+41.0
+----
+
+* Bugs fixed:
+ - !124 software: call fedora-third-party properly
+
+* Translation updates:
+ - Croatian
+ - Danish
+ - Hungarian
+ - Polish
+ - Turkish
+
+41.rc
+-----
+
+* Bugs fixed:
+ - !123 Fix the polkit rule for Fedora third party setup
+
+* Translation updates:
+ - Basque
+ - Chinese (China)
+ - Czech
+ - Dutch
+ - English (United Kingdom)
+ - Finnish
+ - French
+ - Galician
+ - German
+ - Greek, Modern (1453-)
+ - Hebrew
+ - Indonesian
+ - Kazakh
+ - Korean
+ - Lithuanian
+ - Occitan (post 1500)
+ - Persian
+ - Portuguese (Brazil)
+ - Romanian
+ - Russian
+ - Spanish
+ - Swedish
+
+41.beta
+-------
+
+* The Software page, which allows the user to enable or disable third-party
+ repositories, has been resurrected. It is currently Fedora-specific. Patches
+ welcome for other distro families. (!121)
+
+* Bugs fixed:
+ - #80 Cannot login as administrator if you set up enterprise login in gnome-initial-setup
+
+* Translation updates:
+ - Bulgarian
+ - Catalan
+ - Portuguese
+ - Slovenian
+ - Ukrainian
+
+41.alpha
+--------
+
+* Several core classes have been updated to use modern GLib niceties.
+
+* Bugs fixed:
+ - !119 timezone: fix critical on startup when geoclue is disabled
+
+* Translation updates:
+ - Chinese (China)
+ - Russian
+
+40.3
+----
+
+* Translation updates:
+ - Occitan
+
+40.2
+----
+
+* Translation updates:
+ - Occitan
+
+40.1
+----
+
+* The welcome page, displayed on systems where the language page is skipped,
+ features an illustration of a computer on a desk. This illustration has been
+ adjusted to make the hardware more generic. (#122)
+
+* Fix a small memory leak in language page. (!118)
+
+Translation updates:
+ - Catalan
+
+40.0
+----
+
+* No changes since 40.rc.
+
+40.rc
+-----
+
+* Translation updates:
+ - Galician
+ - Portuguese (Brazil)
+
+40.beta
+-------
+
+* Completing GNOME Initial Setup no longer triggers GNOME Tour. GNOME Shell 40
+ now prompts new and upgrading users to run the tour. (#120)
+
+* Previous versions of Initial Setup would run the first time a user logs in
+ ("existing user" mode) as well as when the system is booted with no users
+ ("new user" mode). Initial Setup no longer displays any UI in "existing user"
+ mode, to avoid colliding with the tour. The mode has not been removed in code
+ ā€“ a simple check has been added to the main() function. If your distribution
+ needs the old behaviour, this check can be patched out. The hope is that a
+ future version of GNOME will restore "existing user" mode, but run Initial
+ Setup from the same cut-down Shell that is used in "new user" mode. (#120)
+
+* Initial Setup's .desktop file is now installed to @datadir@/applications
+ (i.e. /usr/share/applications) rather than a GDM-specific directory. This
+ means its icon is displayed correctly when run within a normal shell session.
+ Some distros have previously worked around this issue by installing a
+ symbolic link. (The astute reader may note that this is a no-op given the
+ previous bullet point, but it is still a nice clean-up.) (#52)
+
+* Translation updates:
+ - Panjabi
+ - Portuguese
+
+40.alpha
+--------
+
+* Some enhancements to the avatar picker:
+ - The "Take a photoā€¦" button now has the suggested-action style. (!97)
+ - Generated avatars now have a darker border. (!99)
+ - Fix runtime warning when built without libcheese. (#114)
+
+* The "Welcome" page, displayed when the language picker is disabled, has a
+ refreshed splash image.
+
+* Translation updates:
+ - BokmƄl, Norwegian
+ - Catalan
+ - Esperanto
+ - Friulian
+ - Portuguese
+ - Romanian
+
+3.38.1
+------
+
+* #110 / !104: Fix the window sometimes opens at an unusably small size, rather
+ than being sized appropriately for the display. The fix causes a regression
+ where the window doesn't react to the display size changing in all cases
+ (#112) but the new behaviour is better than the old.
+
+* Other bugs fixed:
+ - !102 docs: Remove typo from DEBUGGING.md
+ - !103 Misc use-after-free and critical warning fixes
+ - !105 data: Leave to meson the duty of keeping the lists in sync
+
+* Translation updates:
+ - Chinese (Taiwan)
+ - Danish
+ - Hebrew
+ - Italian
+ - Latvian
+ - Portuguese
+ - Slovak
+
+3.38.0
+------
+
+* Translation updates:
+ - Greek
+ - Hungarian
+ - Portuguese
+
+3.37.92
+-------
+
+* Bugs fixed:
+ - !101 Exit gracefully if we are disabled systemwide by Anacontda
+
+* Translation updates:
+ - Basque
+ - Czech
+ - Dutch
+ - English (United Kingdom)
+ - Friulian
+ - German
+ - Korean
+ - Lithuanian
+ - Serbian
+ - Slovak
+ - Spanish
+ - Swedish
+
+3.37.91.1
+---------
+
+* Bugs fixed:
+ - !92 gnome-initial-setup: Allow welcome tour to be skipped via vendor config
+ - !95 network: Re-word the description for Wi-Fi
+ - !100: Avoid a warning about connecting to a NULL object
+
+* Translation updates:
+ - Basque
+ - Catalan
+ - Chinese (China)
+ - Croatian
+ - Finnish
+ - French
+ - Galician
+ - Indonesian
+ - Japanese
+ - Kazakh
+ - Persian
+ - Polish
+ - Portuguese
+ - Portuguese (Brazil)
+ - Romanian
+ - Slovenian
+ - Spanish
+ - Turkish
+ - Ukrainian
+
+3.37.91
+-------
+
+* Revamp the ā€˜welcomeā€™ screen. See #7 for more details and screenshots.
+
+* Bugs fixed:
+ - #7 Nicer welcome page
+
+3.37.90
+-------
+
+* Use `gnome-tour` instead of `yelp` to start the welcome tour. (MR !90.)
+
+* Switch to a new user session management approach of defining the session
+ services in `gnome-session@gnome-initial-setup.target.d/session.conf` in the
+ systemd user unit directory. This bumps the optional systemd dependency
+ to ā‰„ 242. See !66 for more details.
+
+* Bugs fixed:
+ - !66 Systemd unit update for 3.38
+ - !90 welcome tour: replace yelp with gnome-tour
+
+* Translation updates:
+ - Chinese (China)
+ - Friulian
+ - Indonesian
+ - Lithuanian
+ - Portuguese (Brazil)
+ - Swedish
+ - Tajik
+ - Turkish
+
+3.37.3
+------
+
+* The avatars offered in the 'account' page can now be customized by setting
+ the 'org.gnome.desktop.interface avatar-directories' GSetting, which also
+ controls the avatars offered in Settings. (This introduces a dependency on
+ gsettings-desktop-schemas ā‰„ 3.37.1.)
+
+* Since version 3.30, it has been possible to customize certain aspects of
+ Initial Setup by specifing the 'vendor-conf-file' option at build time, then
+ installing a configuration file to that path. This version adds two paths
+ that will be checked by default: one for distribution configuration, and
+ another to override that configuration on specific deployments.
+
+ If you specify the 'vendor-conf-file' at build time, the old behaviour is
+ preserved and the new paths will not be searched. If possible, please migrate
+ any existing configuration to the new path, and stop specifying the
+ build-time option.
+
+* Bugs fixed:
+ - #106 timezone: Don't set timezone if tz page is skipped
+ - #75 Polkit rules seem to list now unneeded privileges
+ - #95 Add error argument to save_data() vfunc
+ - #104 Japanese (Kana Kanji) no longer the default input method for Japanese in 3.36.2
+ - #105 Reconsider installation location of vendor.conf
+ - #106 gnome-initial-setup tries to set timezone even though timezone page is disabled
+ - !60 account: Use facesdirs setting to override faces
+ - !84 Fix pkgdata_dir location
+ - !87 Enable web process sandbox
+ - !88 Fix build with -Dvendor-conf-path
+
+* Translation updates:
+ - Chinese (China)
+ - Kazakh
+ - Persian
+ - Romanian
+ - Spanish
+ - Ukrainian
+
+3.37.1
+------
+
+* Bugs fixed:
+ - !82 keyboard: Nullify pointers if getting layout fails
+
+* Translation updates:
+ - Hebrew
+ - Kurdish, Central
+ - Slovak
+
+3.36.1
+------
+
+* Previous releases of gnome-initial-setup had an unversioned dependency on
+ libsecret; this release requires libsecret ā‰„ 0.18.8. (libsecret 0.18.8 was
+ released as part of GNOME 3.32; GNOME 3.36 includes libsecret 0.20.1.)
+
+* Bugs fixed:
+ - !78 account: Make parent account usable in case of errors
+ - !79 Improve readability when removing event sources
+ - !80 Fix two crashes when saving parental controls settings
+ - !81 Use g_auto and g_autoptr more
+
+* Translation updates:
+ - BokmƄl, Norwegian
+ - Dutch
+ - German
+ - Japanese
+ - Romanian
+ - Ukrainian
+
+3.36.0
+------
+
+* Translation updates:
+ - Croatian
+ - Italian
+ - Panjabi
+ - Slovenian
+ - Swedish
+
+3.35.92
+-------
+
+* Two tweaks to the parental controls functionality introduced in the last
+ release:
+ - #94 Add metadata to identify parent account
+ - !77 password: Use malcontent symbolic icon for parent password
+
+* Translation updates:
+ - Catalan
+ - Chinese (Taiwan)
+ - Czech
+ - Danish
+ - English (United Kingdom)
+ - Finnish
+ - French
+ - Greek
+ - Hungarian
+ - Indonesian
+ - Karbi
+ - Korean
+ - Lithuanian
+ - Persian
+ - Polish
+ - Portuguese (Brazil)
+ - Spanish
+
+3.35.91
+-------
+
+* New pages have been added to set up parental controls for the new user
+ account. They are shown (or not, by default) based on a checkbox on the
+ (local) user creation page. This feature requires malcontent ā‰„ 0.5.0
+ <https://gitlab.freedesktop.org/pwithnall/malcontent/> and complements
+ parental controls support already present in Flatpak and GNOME Software. If
+ disabled or if that dependency is not available, no new UI is introduced
+ compared to 3.35.90.
+
+* When running under JHBuild, gnome-keyring is left untouched, and state is not
+ saved when setup is complete. This heuristic is intended to make testing the
+ application during development easier.
+
+* GLib ā‰„ 2.63.1 is now required.
+
+* Fixed various memory leaks, warnings, and a memory safety bug.
+
+* Translation updates:
+ - Basque
+ - Croatian
+ - Czech
+ - French
+ - Galician
+ - Hungarian
+ - Indonesian
+ - Japanese
+ - Lithuanian
+ - Persian
+ - Serbian
+ - Turkish
+
+3.35.90
+-------
+
+* The final page has been revamped, with minor typography tweaks to other pages
+ (#8).
+
+* The timezone map was updated.
+
+* Three pages have been removed since they were unused in all known
+ configurations:
+
+ - the region page, which has not displayed even if enabled for many years
+ (#90)
+ - the software page, which has not displayed for several releases due to
+ changes in GNOME Software (#59)
+ - the eulas page (!71, #72)
+
+* The providers shown on the goa page can now be customized in the vendor conf
+ file (!63)
+
+* Bugs fixed:
+ - #61 CheeseCameraDeviceMonitor may hang account page during initialization
+ - #82 Support Wi-Fi devices appearing and disappearing dynamically
+ - !53 Change generated avatar for empty name
+
+* Translation updates:
+ - Finnish
+ - Kurdish, Central
+ - Malay
+ - Portuguese (Brazil)
+ - Slovak
+ - Spanish
+ - Swedish
+
+3.35.3
+
+* Bugs fixed:
+ - #85 GNOME intial setup segfaults, possibly due to locale related problems
+ - !59 Disable g-i-s service in GDM
+ - !61 assistant: mark "Accept" button as suggested action
+ - !64 Language: Make language list use full height when search is active
+
+* Translation updates:
+ - Catalan
+ - Galician
+ - Japanese
+
+3.34.0
+
+* Bugs fixed:
+ - !58 fixes for systemd user instance support
+
+* Translation updates:
+ - Danish
+ - Japanese
+
+3.33.92
+
+* Translation updates:
+ - English (United Kingdom)
+ - Japanese
+ - Panjabi
+ - Persian
+
+3.33.91
+
+* !54 build: Add a log domain
+
+* Translation updates:
+ - Japanese
+
+3.33.90
+
+* !38 systemd user instance support. This is inert without corresponding
+ changes in other GNOME modules, and can be disabled entirely with
+ `-Dsystemd=false` at build time.
+
+* Other improvements and bug fixes:
+ - !37 summary: don't free borrowed password string
+ - !41 data: use a11y menu in initial-setup session
+ - !42 driver: Account for multi monitor when checking for small screen
+ - !44 language: Refactor the logo selection
+ - !45 language: Fix the visibility of the placeholder
+ - !46 summary: Make the page fit narrow screens
+ - !49 Drop the headers for the first rows
+ - !50 account: Drop the avatar button padding
+ - !51 Refactor page headers
+ - !52 data: Update required gnome-settings-daemon plugins
+
+* Translation updates:
+ - Basque
+ - Karbi
+
+3.33.4
+
+* Bugs fixed:
+ - !39 Check for errors from newlocale()
+
+* Translation updates
+ - Indonesian
+
+3.33.2
+
+* Bugs fixed:
+ - #53 setlocale() usage is not threadsafe
+ - !33 Include bug report URL in .pot file
+
+* Translation updates:
+ - Chinese (China)
+ - Esperanto
+
+3.32.1
+
+* Translation updates:
+ - Croatian
+ - Esperanto
+ - Slovak
+
+3.32.0
+
+* Translation updates:
+ - Korean
+
+3.31.92
+
+* Connect to GDM as soon as gnome-initial-setup starts and fix memory leaks
+ (!22)
+
+* Improve account page behaviour when offline (!23)
+
+* Translation updates:
+ - Catalan
+ - Chinese (Taiwan)
+ - Czech
+ - Danish
+ - Dutch
+ - Finnish
+ - French
+ - Friulian
+ - Galician
+ - German
+ - Greek
+ - Hungarian
+ - Icelandic
+ - Indonesian
+ - Italian
+ - Kazakh
+ - Latvian
+ - Lithuanian
+ - Polish
+ - Portuguese (Brazil)
+ - Romanian
+ - Russian
+ - Serbian
+ - Slovenian
+ - Spanish
+ - Swedish
+ - Turkish
+
+3.31.90
+
+ * Generate default user account pictures
+ * Fix Meson build with (buggy) Meson 0.48.0 and 0.48.1
+ * Remove Autotools build
+ * Fix various compiler warnings
+ * Add welcome distro logo for openSUSE
+ * Translation updates:
+ - Afrikaans
+ - Arabic
+ - Aragonese
+ - Assamese
+ - Basque
+ - Belarusian
+ - Bengali (India)
+ - BokmƄl, Norwegian
+ - Bosnian
+ - Bulgarian
+ - Catalan
+ - Chinese (China)
+ - Chinese (Hong Kong)
+ - Czech
+ - English (United Kingdom)
+ - Esperanto
+ - Estonian
+ - Gaelic
+ - German
+ - Gujarati
+ - Hebrew
+ - Hindi
+ - Hungarian
+ - Indonesian
+ - Interlingua (International Auxiliary Language Association)
+ - Irish
+ - Japanese
+ - Kannada
+ - Malayalam
+ - Marathi
+ - Nepali
+ - Occitan (post 1500)
+ - Oriya
+ - Persian
+ - Portuguese
+ - Serbian
+ - Slovak
+ - Tajik
+ - Tamil
+ - Telugu
+ - Thai
+ - Uighur
+ - Vietnamese
+
+3.30.0
+
+ * Translation updates
+
+3.29.92
+
+ * Translation updates
+ * Improved network page
+ * Fix timezone selection in new-user mode
+ * Properly use libsecret unstable api
+
+3.29.1
+
+
+ * Translation updates
+ * Support more general page skipping
+ * Show the welcome page if the language page is skipped
+ * Look for vendor.conf in /usr/share instead of /var
+ * Go back to allowing weak passwords
+ * Set keyring password for enterprise accounts
+ * Use Debian, Ubuntu logos when appropriate
+ * Fix a crash
+ * Add meson support
+ * Uses a new avatar chooser
+
+3.28.0
+
+ * Translation updates
+ * Sync string changes with gnome-software
+
+3.27.90
+
+ * Adjust session components to recent changes
+ * Improve sorting in the language list
+ * Memory leak fixes
+ * Update titlebar when title changes
+ * Translation updates
+
+3.26.0
+
+ * Translation updates
+
+3.25.92
+
+ * Translation updates
+
+3.25.4
+
+ * Use Unicode in translatable strings
+ * Adjust to g-s-d's plugin removals
+ * Translation updates
+
+3.25.3
+
+ * Support a vendor configuration file
+ * Update text direction when the locale changes
+ * Allow configuring network connections
+ * Copy monitor configuration to user session
+ * Sync user account page with control-center
+ * Show strength indicator for passwords
+ * Translation updates
+
+3.24.0
+
+ * Fix g-i-s session startup deadlock
+ * Translation updates
+
+3.23.92
+
+ * Use correct capitalization and unicode characters
+ * Exit gracefully when disabled systemwide
+ * Adjust to gnome-settings-daemon changes
+ * Port network page to use libnm instead of libnm-glib
+ * Translation updates
+
+3.23.1
+
+ * Clarify presentation of privacy policies
+ * Translation updates
+
+3.22.0
+
+ * Translation updates
+
+3.21.92
+
+ * Port timezone page to use new Geoclue convencience library
+ * Minor UI fixes on privacy page
+ * Correct license of um-realm-manager
+ * Translation updates
+
+3.21.91
+
+ * Better handling of small screens
+ * Add a page to configure software sources
+ * Translation updates
+ British English
+ French
+ Icelandic
+ Indonesian
+ Norwegian bokmƄl
+ Occitan
+ Polish
+ Portuguese
+ Slovenian
+ Spanish
+
+3.20.1
+
+ * Translation updates
+ Bulgarian
+ Friulian
+ Persian
+ Vietnamese
+
+3.20.0
+
+ * Translation updates
+
+3.19.92
+
+ * Add a translator comment for 'Welcome!'
+ * Translation updates
+ Finnish
+ Friulian
+ Hebrew
+ Hungarian
+ Korean
+ Latvian
+ Lithuanian
+ Occitan
+ Russian
+ Slovenian
+ Ukrainian
+
+
+3.19.91
+
+ * Visual refinements on online accounts page
+ * Minor tweaks on the privacy and accounts pages
+ * Ensure keyring gets created for new users
+ * Translation updates
+ Catalan
+ Chinese (Taiwan)
+ Czech
+ Galician
+ German
+ Greek
+ Kazakh
+ Polish
+ Punjabi
+ Serbian
+ Slovak
+ Spanish
+ Swedish
+ Vietnamese
+
+3.19.90
+
+ * Visual refresh for the 'ready to go' page
+
+3.19.2
+
+ * Fix timezone lookup after locale change
+ * Handle screen resolution changes
+ * Design updates to the timezone page
+ * Don't create a keyring for existing users
+ * Translation updates
+ Bulgarian
+ Chinese (Taiwan)
+ German
+ Hungarian
+ Latvian
+ Lithuanian
+ Polish
+ Slovak
+ Vietnamese
+
+3.19.1
+
+ * Ensure locale region matches the selected language
+ * Sort languages case-insensitively
+ * Translation updates
+ Basque
+ Irish
+ Serbian
+ Serbian Latin
+ Vietnamese
+
+3.18.0
+
+ * Translation updates
+ Danish
+ Dutch
+ French
+ Indonesian
+ Italian
+ Japanese
+ Kazakh
+ Korean
+ Lithuanian
+ Persian
+
+3.17.91
+
+ * Go back to enforcing password policy (#754213)
+ * Translation updates
+ Chinese (Taiwan)
+ Galician
+ Polish
+ Swedish
+
+3.17.90
+
+ * Detect enterprise accounts without heuristic (#752980)
+ * Don't change keyring password explicitly (#752980)
+ * Fix problem with restarting geoclue client
+ * Translation updates
+ Norwegian bokmƄl
+ Brazilian Portuguese
+
+3.17.4
+
+ * Create keyring initially unencrypted
+ * Password page refinements (#735578)
+ * Make policy dialog resize properly (#749426)
+ * Use correct key for privacy policy (#749545)
+ * Handle disabled network better
+ * Avoid a crash on language change (#752134)
+ * Make gnome-shell launch first
+ * AVoid a race in launching welcome tour
+ * Translation updates
+ Catalan
+ Czech
+ Friulian
+ German
+ Greek
+ Hebrew
+ Hungarian
+ Occitan
+ Portuguese
+ Romanian
+ Russian
+ Slovak
+ Slovenian
+ Spanish
+ Tajik
+ Turkish
+ Vietnamese
+
+3.16.2
+
+ * Translation updates
+ Icelandic
+ Occitan
+ Simplified Chinese
+
+
+3.16.1
+
+ * Translation updates
+ Dutch
+ Latvian
+ Punjabi
+
+
+3.16.0
+
+ * Translation updates
+ Basque
+ Indonesian
+ Italian
+ Japanese
+
+
+3.15.92
+
+ * Stop using deprecated API
+ * Improved keyboard navigation
+ * Adapt to gnome-desktop locale codeset changes
+ * Translation updates
+ Aragonese
+ Bosnian
+ Bulgarian
+ Catalan
+ Danish
+ Esperanto
+ Finnish
+ German
+ Korean
+ Swedish
+ Tajik
+
+
+3.15.91.1
+
+ * Port to Webkit2
+ * Translation updates
+ Czech
+ Polish
+ Serbian
+ Slovenian
+
+
+3.15.91
+
+ * Fix a possible crash in the network page
+ * Translation updates
+ Brazilian Portuguese
+ Chinese (Taiwan)
+ French
+ Galician
+ Greek
+ Hebrew
+ Hungarian
+ Kazakh
+ Norwegian bokmal
+ Spanish
+ Turkish
+ Ukrainian
+
+
+3.15.90.1
+
+ * Append ibus input sources
+ * Allow resizing on small screens
+ * Improve account creation dialogs
+ * Move privacy page before timezone
+ * Use the permanent bus name for abrtd
+ * Translation updates
+ Lithuanian
+ Russian
+ Slovak
+ Vietnamese
+
+
+3.15.90
+
+ * Add a privacy page
+ * Don't wait for GDM to kill us
+ * Translation updates
+ Aragonese
+ Portuguese
+ Vietnamese
+
+
+3.14.2.1
+
+ * Disable SSLv3 because of POODLE
+
+3.14.2
+
+ * Fix changing the login keyring password
+ * Ensure that we show the selected input source
+ * Preserve system keyboard layouts
+ * Don't choose irrelevant layouts as first level choices
+ * Add a function to detect small screens
+ * Hide images when we have no space for them
+ * Drop the minimum size when we are on a small screen
+ * Translation updates
+ Dutch
+ Friulian
+ Kazakh
+ Persian
+
+
+3.14.1
+
+ * Don't allow to move window
+ * Avoid a crash in input chooser
+ * Require selecting an input source
+ * Translation updates
+ Bengali
+ Brazilian Portuguese
+ Bulgarian
+ Friulian
+ Gujarati
+ Italian
+ Latvian
+
+
+3.14.0
+
+ * Fix skip button showing up on the last page
+ * Translation updates
+ Hindi
+ Japanese
+ Nepali
+
+
+3.13.7
+
+ * Refine spacing and alignment on all pages
+ * keyboard: Show suggestions for the current locale
+ * keyboard: Add all IBus engines
+ * keyboard: Ellipsize long labels
+ * Translation updates
+ Bengali
+ Catalan
+ Catalan (Valencian)
+ Danish
+ Kannada
+ Marathi
+ Turkish
+
+
+3.13.6
+
+ * language: Drop British English to make page smaller
+ * region: Drop, instead show all locales in the language page
+ * keyboard: Show all layouts (in the expanded list)
+ * account: Whitelist realmd actions to enable enterprise setup
+ * timezone: Remove icon to make page smaller
+ * Improve keyboard navigation
+ * Bring back existing-user mode
+ * Translation updates
+ Indonesian
+ Korean
+ Oriya
+ Punjabi
+ Serbian
+ Slovak
+ Swedish
+ Tamil
+ Telugu
+
+3.13.5
+
+ * language: Don't dim distro logo
+ * timezone: Make page smaller
+ * input: Fix build without ibus
+ * password: Don't hard-enforce strong passwords
+
+ * Translation updates
+ Assamese
+ Basque
+ Chinese (simplified)
+ Czech
+ Finnish
+ French
+ Galician
+ German
+ Gujarati
+ Ukrainian
+
+
+3.13.4
+
+ * account: Improve wording
+ * account: Update to match control-center
+ * timezone: Add geoclue integration
+ * timezone: Add an icon
+ * password: Update to match control-center
+ * password: Properly clean up signal handler
+ * keyring: Open session
+ * copy-worker: Run in early initialization phase
+
+ * Translation updates
+ Basque
+ Belarusian
+ Brazilian Portuguese
+ Catalan
+ Catalan (Valencian)
+ Chinese (Taiwan)
+ Czech
+ Danish
+ Dutch
+ French
+ German
+ Greek
+ Hebrew
+ Hungarian
+ Indonesian
+ Italian
+ Japanese
+ Lithuanian
+ Norwegian bokmƄl
+ Polish
+ Punjabi
+ Russian
+ Slovak
+ Slovenian
+ Spanish
+
+3.11.92
+
+ * build: Initialize automake earlier
+ * build: Enable maintainer mode
+ * Update titlebar on locale change
+ * Use distro logo instead of GNOME foot
+ * timezone: Ensure map is properly allocated
+ * timezone: Draw location marker pixel-aligned
+
+ * Translation updates
+ Brazilian Portuguese
+ Chinese (China)
+ Czech
+ Finnish
+ French
+ Friulian
+ Galicican
+ Hebrew
+ Hungarian
+ Indonesian
+ Kazakh
+ Korean
+ Latvian
+ Lithuanian
+ Polish
+ Portuguese
+ Punjabi
+ Russian
+ Tajik
+ Traditional Chinese
+ Ukrainian
+
+3.11.91
+
+ * Add an avatar chooser back to the accounts page
+ * Fix prepopulating the accounts page from gmail accounts
+ * Fix keynav issues on various pages
+ * Fix layout issues on various pages
+ * Robustnes fixes
+
+ * Translation updates:
+ Arabic
+ Czech
+ Finnish
+ Galician
+ Hindi
+ Kannada
+ Kazakh
+ Lithuanian
+ Malayalam
+ Polish
+ Tajik
+
+
+3.11.90
+
+ * Updates to align with design
+ * Translation updates
+
+3.10.1.1:
+
+ * Fix build
+
+3.10.1:
+
+ * Fix GOA dialog not closing
+ * Use newer GTK+ API for code cleanups
+
+Translations:
+ Arash Mousavi
+ Carles Ferrando
+ Emin Tufan Ƈetin
+ Friedel Wolff
+ Reinout van Schouwen
+ ē”˜éœ²(Lu Gan)
+
+3.10.0.1:
+
+ * Fix an issue with the account page preventing advancing
+ through the wizard
+
+Code:
+ Jasper St. Pierre
+
+3.10.0:
+
+ * Now on the GNOME version scheme
+ * Prevent a few crashes
+ * Use a headerbar to match GNOME3 designs
+ * Use the distro name on the "Start" button
+ * Drop the intro animation and video player
+ * Fix accessibility issues
+
+Code:
+ Mattias Clasen, Michael Wood, Jasper St. Pierre, Colin Walters
+
+Translations:
+ A S Alam
+ Alexandre Franke
+ Andika Triwidada
+ AntĆ³nio Lima
+ Aurimas Černius
+ BalĆ”zs ƚr
+ Baurzhan Muftakhidinov
+ Benjamin Steinwender
+ Chao-Hsiung Liao
+ Cheng-Chia Tseng
+ Christian Kirbach
+ Daniel Korostil
+ Daniel Mustieles
+ Dimitris Spingos
+ DuŔan Kazik
+ Fabio Tomat
+ Fran DiƩguez
+ Gabor Kelemen
+ Georges Basile Stavracas Neto
+ Gianvito Cavasoli
+ Gil Forcada
+ Ihar Hrachyshka
+ Inaki Larranaga Murgoitio
+ Ivaylo Valkov
+ Jiro Matsuzawa
+ Josep SĆ nchez
+ Kenneth Nielsen
+ Khaled Hosny
+ Kjartan Maraas
+ Krishnababu Krothapalli
+ Marek ČernockĆ½
+ Matej Urbančič
+ Mattias PƵldaru
+ Nilamdyuti Goswami
+ Peter MrƔz
+ Piotr Drąg
+ Rafael Ferreira
+ RÅ«dolfs Mazurs
+ Sandeep Sheshrao Shedmake
+ Seong-ho Cho
+ SeĆ”n de BĆŗrca
+ Shankar Prasad
+ Shantha kumar
+ Sweta Kothari
+ Timo Jyrinki
+ Victor Ibragimov
+ Yaron Shahrabani
+ Yosef Or Boczko
+ Yuri Myasoedov
+ tuhaihe
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+0.11:
+
+ * Make the initial-setup shell mode valid JSON
+ * Fix passwordless users
+ * Fix visibility of the GOA page under strange network conditions
+ * Lots of realmd / Enterprise login fixes
+ * Increase the amount of initial languages
+ * Make sure usernames are less than 255 characters
+ * Remove duplicate username suggestions
+
+Code:
+ Michael Wood, Rui Matos, Mattias Clasen
+
+Translations:
+ A S Alam
+ Aurimas Černius
+ Daniel Mustieles
+ Fran DiƩguez
+ Gil Forcada
+ Khaled Hosny
+ Kjartan Maraas
+ ManojKumar Giri
+ Marek ČernockĆ½
+ Matej Urbančič
+ Nilamdyuti Goswami
+ Piotr Drąg
+ Rafael Ferreira
+ Shankar Prasad
+ Shantha kumar
+ Sweta Kothari
+ Yaron Shahrabani
+
+0.10:
+
+ * goa: Only set page visible when we have a network connection
+ * Bundle our own initial-setup mode file
+ * keyboard: Add a default input source for the locale
+ * Various bug fixes and code cleanups
+
+Code:
+ Michael Wood, Rui Matos, Jasper St. Pierre, Colin Walters
+
+Translations:
+ Alexandre Franke
+ Andika Triwidada
+ Anish A
+ A S Alam
+ Ask H. Larsen
+ Aurimas Černius
+ Benjamin Steinwender
+ Chetan Khona
+ Christian Kirbach
+ Daniel Mustieles
+ Dmitriy S. Seregin
+ Fran DiƩguez
+ Gabor Kelemen
+ Gianvito Cavasoli
+ Gil Forcada
+ Jiro Matsuzawa
+ Kjartan Maraas
+ Krishnababu Krothapalli
+ ManojKumar Giri
+ Matej Urbančič
+ Muhammet Kara
+ Nik Kalach
+ Nilamdyuti Goswami
+ Piotr Drąg
+ Rafael Ferreira
+ Rajesh Ranjan
+ Shankar Prasad
+ Shantha kumar
+ Sweta Kothari
+ tuhaihe
+ Yuri Myasoedov
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+
+0.9:
+
+ * Update the keyboard panel
+ * Hide the back/forward buttons on summary
+ * Fix launching the welcome-tour in unsupported locales
+ * Add a new infrastructure for launching without a trigger file
+ * Update the summary page with a checkmark
+ * Fix compiler warnings
+ * Launch gnome-initial-setup with the existing user mode on first
+ logins
+
+Code:
+ Jasper St. Pierre, Mattias Clasen, Michael Wood, Colin Walters
+
+Translations:
+
+ Andika Triwidada
+ Arash Mousavi
+ A S Alam
+ Aurimas Černius
+ BalĆ”zs ƚr
+ Carles Ferrando
+ Chao-Hsiung Liao
+ Daniel Korostil
+ Daniel Mustieles
+ Dimitris Spingos
+ Dr.T.Vasudevan
+ DuŔan Kazik
+ Enrico Nicoletto
+ Fran DiƩguez
+ Gabor Kelemen
+ Gianvito Cavasoli
+ Gil Forcada
+ Jasper St. Pierre
+ Jiro Matsuzawa
+ Lucas Lommer
+ Marek ČernockĆ½
+ Matej Urbančič
+ Michael Wood
+ Piotr Drąg
+ Praveen Illa
+ Rafael Ferreira
+ Rajesh Ranjan
+ Seong-ho Cho
+ Sweta Kothari
+ tuhaihe
+ Victor Ibragimov
+ Yuri Myasoedov
+
+0.8:
+
+ * Improve the language page
+ * Don't activate gnome-initial-setup on live media
+ * Add a progress indicator
+
+0.7:
+
+ * Port to libgd, and remove dependency on Clutter
+ * Add a new welcome page, and fix bugs related to page ordering
+ * Add a new page skip mechanism, used for current sessions in
+ gnome-initial-setup
+ * Performance improvements
+ * Remove geoclue
+ * Design updates for account page
+
+Code:
+ Jasper St. Pierre, Mattias Clasen, Michael Wood, Colin Walters
+
+Translations:
+
+ Adam MatouŔek
+ Alexandre Franke
+ Anish A
+ A S Alam
+ Aurimas Černius
+ Baurzhan Muftakhidinov
+ Benjamin Steinwender
+ Bruce Cowan
+ Chao-Hsiung Liao
+ Cheng-Chia Tseng
+ Daniel Mustieles
+ Dimitris Spingos
+ Duarte Loreto
+ DuŔan Kazik
+ Enrico Nicoletto
+ Fran DiƩguez
+ Gheyret Kenji
+ Ihar Hrachyshka
+ Inaki Larranaga Murgoitio
+ Ivaylo Valkov
+ Kjartan Maraas
+ Matej Urbančič
+ Nguyį»…n ThĆ”i Ngį»c Duy
+ Nilamdyuti Goswami
+ Piotr Drąg
+ Reinout van Schouwen
+ Tobias Endrigkeit
+ Tom Tryfonidis
+ Ville-Pekka Vainio
+ Wouter Bolsterlee
+ Yaron Shahrabani
+ Yuri Myasoedov
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+0.6:
+
+ * Port GOA and Network pages to egg-list-box
+ * Make account and language pages match new designs
+ * Remove symbolic icons for assistant "next" / "previous" buttons
+ * Make EULA pages not require checkboxes by default
+ * Add a new Keyboard Layout page, based on the code in the
+ control center.
+
+Code:
+ Jasper St. Pierre, Mattias Clasen, Michael Wood
+
+Translations:
+
+ Aron Xu
+ A S Alam
+ Daniel Mustieles
+ DuŔan Kazik
+ Fran DiƩguez
+ Gianvito Cavasoli
+ Khaled Hosny
+ Kristjan SCHMIDT
+ Matej Urbančič
+ Petr Kovar
+ Piotr Drąg
+ TmTFx
+ Yaron Shahrabani
+
+0.5:
+
+ * Be more forgiving if gnome-initial-setup user doesn't exist
+ * Improve password handling
+ * Eula page improvements
+ * Code cleanups, misc. fixes, crashes
+
+Code:
+ Jasper St. Pierre, Mattias Clasen, Michael Wood, Colin Walters
+
+Translations:
+
+Daniel Mustieles
+Jorge PĆ©rez PĆ©rez
+Matej Urbančič
+Mattias PƵldaru
+Runa Bhattacharjee
+Sayak Sarkar (1):
+
+0.4:
+
+ * Convert over to a new GisPage base class to help prevent
+ memory leaks when switching locales.
+ * Remove old summary override system with a system that simply
+ swaps out the GtkBuilder filename at runtime.
+ * Add a simple movie player for the welcome-tour, based on clutter-gst.
+ * Fix GTK+-only assistant.
+ * Revamp the account page by making it modal dialog-less, and removing
+ the avatar picker. (RIP)
+ * Use symbolic icons for assistant "next" / "previous" buttons instead
+ of using Unicode glyphs.
+ * Style and design updates.
+ * Code cleanups, misc. fixes, crashes
+
+Code:
+ Jasper St. Pierre, Mattias Clasen
+
+Translations:
+
+ Andika Triwidada
+ Fran DiƩguez
+ Gianvito Cavasoli
+ Kjartan Maraas
+ Matej Urbančič
+ Mattias PƵldaru
+ Piotr Drąg
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+0.3:
+
+ * Port to GStreamer 1.0.
+ * Add a minimal systemd service.
+ * Fix issues with summary and autologin.
+ * Fix some crashes and issues with garbage in private structures.
+ * Install desktop files in the right place.
+ * Rework skeleton and copy-worker for GDM integration.
+ * Redesign the account page to be more like the control center.
+ * Add enterprise login support to the account page.
+
+Code:
+ Jasper St. Pierre, Mattias Clasen, Ray Strode, Stef Walters
+
+Translations:
+
+ Abhay Kadam
+ Alexandre Franke
+ Alexsey Nadtochey
+ Andika Triwidada
+ Ani Peter
+ Arash Mousavi
+ Arvis Lacis
+ A S Alam
+ Aurimas Černius
+ Bruce Cowan
+ Carles Ferrando
+ chandankumar
+ Changwoo Ryu
+ Chao-Hsiung Liao
+ Cheng Lu
+ Daniel Korostil
+ Daniel Mustieles
+ Daniel Nylander
+ Dr.T.Vasudevan
+ Duarte Loreto
+ DuŔan Kazik
+ Felipe Borges
+ Fran DiƩguez
+ Gabor Kelemen
+ Gil Forcada
+ Ivaylo Valkov
+ Kenneth Nielsen
+ Khaled Hosny
+ Kjartan Maraas
+ Kris Thomsen
+ Matej Urbančič
+ Mattias PƵldaru
+ Muhammet Kara
+ Nguyį»…n ThĆ”i Ngį»c Duy
+ Nilamdyuti Goswami
+ Pavol KlačanskĆ½
+ Petr Kovar
+ Piotr Drąg
+ Priit Laes
+ RÅ«dolfs Mazurs
+ Sandeep Sheshrao Shedmake
+ Timo Jyrinki
+ Tobias Endrigkeit
+ Tom Tryfonidis
+ Unticha Pramgoed
+ Urmas D
+ Yaron Shahrabani
+ Yuri Matsuk
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+0.2
+===
+
+ * Add a language selector to replace the Welcome splash screen
+ before.
+ * GDM integration has been rewritten to use libgdm instead of
+ libgdmuser.
+ * A lot of code has been properly split out into static
+ libraries; each page now has one static library. In the
+ future, we will have an unsupported module API that can be
+ used to extend the system.
+ * Use a cc-notebook, stolen from gnome-control-center, that
+ allows smooth animations between pages. A plain GTK+
+ implementation is being written for systems without the
+ capabilities to run animations.
+ * Complete integration by including a copy worker run on session
+ init. This strategy should work for the use case of enterprise
+ users.
+ * Fix libcheese integration, and disallow the file picker when
+ choosing a user icon.
+
+Code:
+ Jasper St. Pierre, Cosimo Cecchi
+
+Translations:
+
+ Alexandre Franke
+ Alexsey Nadtochey
+ Andika Triwidada
+ Arvis Lacis
+ A S Alam
+ Aurimas Černius
+ chandankumar
+ Chao-Hsiung Liao
+ Cheng Lu
+ Daniel Korostil
+ Daniel Mustieles
+ Fran DiƩguez
+ Khaled Hosny
+ Kjartan Maraas
+ Matej Urbančič
+ Matthias Clasen
+ Muhammet Kara
+ Nguyį»…n ThĆ”i Ngį»c Duy
+ Nilamdyuti Goswami
+ Piotr Drąg
+ Tobias Endrigkeit
+ Tom Tryfonidis
+ Urmas D
+ Yaron Shahrabani
+ ŠœŠøрŠ¾ŃŠ»Š°Š² ŠŠøŠŗŠ¾Š»Šøћ
+
+0.1
+===
+
+ * First release
+ * EULA integration
+
+Known bugs:
+
+ * geoclue isn't working quite yet in all places.
+
+Code:
+
+ Mattias Clasen, Jasper St. Pierre, Ray Strode
+
+Design:
+
+ Allan Day, William Jon McCann, Jakub Steiner
+
+Translations:
+
+ Piotr Drag
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ecf3168
--- /dev/null
+++ b/README.md
@@ -0,0 +1,72 @@
+GNOME Initial Setup
+===================
+
+After acquiring or installing a new system there are a few essential things
+to set up before use. gnome-initial-setup aims to provide a simple, easy,
+and safe way to prepare a new system.
+
+This should only include a few essential steps for which we can't provide
+good defaults:
+
+ * Select your language
+ * Get onto the network
+ * Create a user account
+ * Set the correct timezone / location
+ * Set up online accounts
+ * Learn some basics about GNOME
+
+The desired experience is that the system boots straight into the
+initial-setup tool, and when the setup tasks are completed, we smoothly
+transition into the user session for the newly created user account.
+
+To realize this experience, we rely on gdm to launch gnome-initial-setup
+in a 'first boot' situation. We are using gnome-shell in an 'initial-setup'
+mode that shows a somewhat reduced UI, similar to the way it is used on
+the login screen.
+
+The design for the initial-setup application can be found here:
+https://live.gnome.org/GnomeOS/Design/Whiteboards/InitialSetup
+
+Vendor Configuration
+--------------------
+
+Some aspects of Initial Setup's behaviour can be overridden through a
+_vendor configuration file_.
+
+By default, Initial Setup will try to read configuration from
+`$(sysconfdir)/gnome-initial-setup/vendor.conf` (i.e.
+`/etc/gnome-initial-setup/vendor.conf` in a typical installation). If this file
+does not exist or cannot be read, Initial Setup will read
+`$(datadir)/gnome-initial-setup/vendor.conf` (i.e.
+`/usr/share/gnome-initial-setup/vendor.conf`). The intention is that
+distributions will provide their configuration (if any) in the latter file,
+with the former used by administrators or hardware vendors to override the
+distribution's configuration.
+
+For backwards-compatibility, a `vendor-conf-file` option can be passed to
+`meson configure`. If specified, Initial Setup will *only* try to read
+configuration from that path; neither of the default paths will be checked.
+
+Here's a (contrived) example of what can be controlled using this file:
+
+```ini
+[pages]
+# Never show the timezone page
+skip=timezone
+# Don't show the language and keyboard pages in the 'first boot' situation,
+# only when running for an existing user
+existing_user_only=language;keyboard
+# Only show the privacy page in the 'first boot' situation
+new_user_only=privacy
+
+[goa]
+# Offer a different set of GNOME Online Accounts providers on the Online
+# Accounts page
+providers=owncloud;imap_smtp
+```
+
+License
+-------
+
+GNOME Initial Setup is distributed under the terms of the GNU General Public License,
+version 2 or later. See the [COPYING](COPYING) file for details.
diff --git a/build-aux/maintainer-upload-release b/build-aux/maintainer-upload-release
new file mode 100755
index 0000000..a5ca951
--- /dev/null
+++ b/build-aux/maintainer-upload-release
@@ -0,0 +1,37 @@
+#!/bin/bash
+set -ex
+: "${MESON_BUILD_ROOT:?}"
+: "${MESON_SOURCE_ROOT:?}"
+project_name="${1:?project name is required}"
+project_version="${2:?project version is required}"
+tarball_basename="${project_name}-${project_version}.tar.xz"
+tarball_path="${MESON_BUILD_ROOT}/meson-dist/${tarball_basename}"
+[[ -e "$tarball_path" ]] # ninja dist must have been successful
+
+gnome_series=${project_version%.*}
+expected_branch=gnome-${gnome_series/./-}
+
+pushd "$MESON_SOURCE_ROOT"
+ branch=$(git rev-parse --abbrev-ref HEAD)
+ if [[ "$branch" != "master" ]] && [[ "$branch" != "$expected_branch" ]]; then
+ echo "Project version $project_version does not match branch $branch" >&2
+ exit 1
+ fi
+ if git show-ref --tags "$project_version" --quiet; then
+ # Tag already exists; verify that it points to HEAD
+ [ "$(git rev-parse "$project_version"^{})" = "$(git rev-parse HEAD)" ]
+ else
+ if type git-evtag &>/dev/null; then
+ # Can't specify tag message on command line
+ # https://github.com/cgwalters/git-evtag/issues/9
+ EDITOR=true git evtag sign "$project_version"
+ else
+ git tag -s "$project_version" -m "Version $project_version"
+ fi
+ fi
+ git push --atomic origin "$branch" "$project_version"
+popd
+
+scp "$tarball_path" master.gnome.org:
+# shellcheck disable=SC2029
+ssh -t master.gnome.org ftpadmin install "$tarball_basename"
diff --git a/build-aux/meson.build b/build-aux/meson.build
new file mode 100644
index 0000000..ab4cbea
--- /dev/null
+++ b/build-aux/meson.build
@@ -0,0 +1,4 @@
+run_target('maintainer-upload-release',
+ command: ['maintainer-upload-release',
+ meson.project_name(),
+ meson.project_version()])
diff --git a/data/20-gnome-initial-setup.rules.in b/data/20-gnome-initial-setup.rules.in
new file mode 100644
index 0000000..02fd21d
--- /dev/null
+++ b/data/20-gnome-initial-setup.rules.in
@@ -0,0 +1,29 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+//
+// DO NOT EDIT THIS FILE, it will be overwritten on update.
+//
+// Allow the gnome-initial-setup user to do certain actions without
+// being interrupted by password dialogs
+
+polkit.addRule(function(action, subject) {
+ if (subject.user !== 'gnome-initial-setup')
+ return undefined;
+
+ var actionMatches = (action.id.indexOf('org.freedesktop.hostname1.') === 0 ||
+ action.id.indexOf('org.freedesktop.NetworkManager.') === 0 ||
+ action.id.indexOf('org.freedesktop.locale1.') === 0 ||
+ action.id.indexOf('org.freedesktop.accounts.') === 0 ||
+ action.id.indexOf('org.freedesktop.timedate1.') === 0 ||
+ action.id.indexOf('org.freedesktop.realmd.') === 0 ||
+ action.id.indexOf('com.endlessm.ParentalControls.') === 0 ||
+ action.id.indexOf('org.fedoraproject.thirdparty.') === 0);
+
+ if (actionMatches) {
+ if (subject.local)
+ return 'yes';
+ else
+ return 'auth_admin';
+ }
+
+ return undefined;
+});
diff --git a/data/gnome-initial-setup-copy-worker.desktop.in.in b/data/gnome-initial-setup-copy-worker.desktop.in.in
new file mode 100644
index 0000000..7e4b2a4
--- /dev/null
+++ b/data/gnome-initial-setup-copy-worker.desktop.in.in
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Name=GNOME Initial Setup Copy Worker
+Type=Application
+Exec=@LIBEXECDIR@/gnome-initial-setup-copy-worker
+OnlyShowIn=GNOME;
+NoDisplay=true
+X-GNOME-AutoRestart=false
+X-GNOME-Autostart-Phase=EarlyInitialization
+AutostartCondition=unless-exists gnome-initial-setup-done
+X-GNOME-HiddenUnderSystemd=@systemd_hidden@
diff --git a/data/gnome-initial-setup-copy-worker.service.in b/data/gnome-initial-setup-copy-worker.service.in
new file mode 100644
index 0000000..ee3c0d0
--- /dev/null
+++ b/data/gnome-initial-setup-copy-worker.service.in
@@ -0,0 +1,15 @@
+[Unit]
+Description=GNOME Initial Setup Copy Worker
+
+# Make sure we run really early
+Before=gnome-session-pre.target graphical-session-pre.target
+
+# Never run in GDM
+ConditionUser=!@system
+
+ConditionPathExists=!%E/gnome-initial-setup-done
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/gnome-initial-setup-copy-worker
+Restart=no
diff --git a/data/gnome-initial-setup-first-login.desktop.in.in b/data/gnome-initial-setup-first-login.desktop.in.in
new file mode 100644
index 0000000..f3d0df7
--- /dev/null
+++ b/data/gnome-initial-setup-first-login.desktop.in.in
@@ -0,0 +1,13 @@
+[Desktop Entry]
+Name=Initial Setup
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=preferences-system
+Exec=@LIBEXECDIR@/gnome-initial-setup --existing-user
+Terminal=false
+Type=Application
+StartupNotify=true
+Categories=GNOME;GTK;System;
+OnlyShowIn=GNOME;
+NoDisplay=true
+AutostartCondition=unless-exists gnome-initial-setup-done
+X-GNOME-HiddenUnderSystemd=@systemd_hidden@
diff --git a/data/gnome-initial-setup-first-login.service.in b/data/gnome-initial-setup-first-login.service.in
new file mode 100644
index 0000000..45e6c80
--- /dev/null
+++ b/data/gnome-initial-setup-first-login.service.in
@@ -0,0 +1,16 @@
+[Unit]
+Description=GNOME Initial Setup
+
+BindsTo=gnome-session.target
+After=gnome-session.target
+
+# Never run in GDM
+Conflicts=gnome-session@gnome-login.target
+
+Conflicts=gnome-session@gnome-initial-setup.target
+ConditionPathExists=!%E/gnome-initial-setup-done
+
+[Service]
+Type=oneshot
+ExecStart=@libexecdir@/gnome-initial-setup --existing-user
+Restart=no
diff --git a/data/gnome-initial-setup.conf b/data/gnome-initial-setup.conf
new file mode 100644
index 0000000..452b2ad
--- /dev/null
+++ b/data/gnome-initial-setup.conf
@@ -0,0 +1,3 @@
+# sysusers.d snippet for creating the gnome-inital-setup system user. See
+# sysusers.d(5) for details.
+u gnome-initial-setup - "GNOME Initial Setup" /run/gnome-initial-setup
diff --git a/data/gnome-initial-setup.desktop.in.in b/data/gnome-initial-setup.desktop.in.in
new file mode 100644
index 0000000..e7aa666
--- /dev/null
+++ b/data/gnome-initial-setup.desktop.in.in
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Name=Initial Setup
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=preferences-system
+Exec=@LIBEXECDIR@/gnome-initial-setup
+Terminal=false
+Type=Application
+StartupNotify=true
+Categories=GNOME;GTK;System;
+OnlyShowIn=GNOME;
+NoDisplay=true
+X-GNOME-HiddenUnderSystemd=@systemd_hidden@
diff --git a/data/gnome-initial-setup.service.in b/data/gnome-initial-setup.service.in
new file mode 100644
index 0000000..bb0af2e
--- /dev/null
+++ b/data/gnome-initial-setup.service.in
@@ -0,0 +1,13 @@
+[Unit]
+Description=GNOME Initial Setup
+RefuseManualStart=true
+RefuseManualStop=true
+
+BindsTo=gnome-session.target
+After=gnome-session.target
+
+[Service]
+Type=simple
+ExecStart=@libexecdir@/gnome-initial-setup
+ExecStopPost=-@libexecdir@/gnome-session-ctl --shutdown
+Restart=no
diff --git a/data/gnome-initial-setup.session.conf.in b/data/gnome-initial-setup.session.conf.in
new file mode 100644
index 0000000..5c28f74
--- /dev/null
+++ b/data/gnome-initial-setup.session.conf.in
@@ -0,0 +1,8 @@
+[Unit]
+# Must be in sync with @this_component@.session
+@wants_required_components@
+
+Requires=@requires_component@.target
+
+# Only difference to a standard GNOME session is the @this_component@ service
+Requires=@this_component@.service
diff --git a/data/gnome-initial-setup.session.in b/data/gnome-initial-setup.session.in
new file mode 100644
index 0000000..e8d1890
--- /dev/null
+++ b/data/gnome-initial-setup.session.in
@@ -0,0 +1,4 @@
+[GNOME Session]
+Name=GNOME Initial Setup
+# Must be in sync with gnome-session@@this_component@.target.d/session.conf drop-in
+RequiredComponents=@gnome_session_required_components@;
diff --git a/data/initial-setup.json b/data/initial-setup.json
new file mode 100644
index 0000000..35fae95
--- /dev/null
+++ b/data/initial-setup.json
@@ -0,0 +1,8 @@
+{
+ "hasWindows": true,
+ "components": ["networkAgent"],
+ "panel": { "left": [],
+ "center": [],
+ "right": ["a11y", "keyboard", "quickSettings"]
+ }
+}
diff --git a/data/meson-add-wants.sh b/data/meson-add-wants.sh
new file mode 100755
index 0000000..c33d1b4
--- /dev/null
+++ b/data/meson-add-wants.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -eu
+
+# Script copied from systemd
+
+unitdir="$1"
+target="$2"
+unit="$3"
+
+case "$target" in
+ */?*) # a path, but not just a slash at the end
+ dir="${DESTDIR:-}${target}"
+ ;;
+ *)
+ dir="${DESTDIR:-}${unitdir}/${target}"
+ ;;
+esac
+
+unitpath="${DESTDIR:-}${unitdir}/${unit}"
+
+case "$target" in
+ */)
+ mkdir -vp -m 0755 "$dir"
+ ;;
+ *)
+ mkdir -vp -m 0755 "$(dirname "$dir")"
+ ;;
+esac
+
+ln -vfs --relative "$unitpath" "$dir"
diff --git a/data/meson.build b/data/meson.build
new file mode 100644
index 0000000..ba8d78e
--- /dev/null
+++ b/data/meson.build
@@ -0,0 +1,130 @@
+autostart_files = [
+ 'gnome-initial-setup-copy-worker.desktop',
+ 'gnome-initial-setup-first-login.desktop',
+]
+
+gis_shell_component = 'org.gnome.Shell'
+gis_gnome_session_required_components = [
+ 'org.gnome.SettingsDaemon.A11ySettings',
+ 'org.gnome.SettingsDaemon.Color',
+ 'org.gnome.SettingsDaemon.Datetime',
+ 'org.gnome.SettingsDaemon.Housekeeping',
+ 'org.gnome.SettingsDaemon.Keyboard',
+ 'org.gnome.SettingsDaemon.MediaKeys',
+ 'org.gnome.SettingsDaemon.Power',
+ 'org.gnome.SettingsDaemon.PrintNotifications',
+ 'org.gnome.SettingsDaemon.Rfkill',
+ 'org.gnome.SettingsDaemon.ScreensaverProxy',
+ 'org.gnome.SettingsDaemon.Sharing',
+ 'org.gnome.SettingsDaemon.Smartcard',
+ 'org.gnome.SettingsDaemon.Sound',
+ 'org.gnome.SettingsDaemon.UsbProtection',
+ 'org.gnome.SettingsDaemon.Wacom',
+ 'org.gnome.SettingsDaemon.XSettings',
+]
+
+gis_user_session_wanted_components = gis_gnome_session_required_components
+
+desktop_conf = configuration_data()
+desktop_conf.set('LIBEXECDIR', libexec_dir)
+desktop_conf.set('systemd_hidden', enable_systemd ? 'true' : 'false')
+
+foreach desktop_file: autostart_files
+ i18n.merge_file(
+ input: configure_file(
+ input: files(desktop_file + '.in.in'),
+ output: desktop_file + '.in',
+ configuration: desktop_conf
+ ),
+ output: desktop_file,
+ install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'autostart'),
+ po_dir: po_dir,
+ install: true,
+ type: 'desktop'
+ )
+endforeach
+
+i18n.merge_file(
+ input: configure_file(
+ input: files('gnome-initial-setup.desktop.in.in'),
+ output: 'gnome-initial-setup.desktop.in',
+ configuration: desktop_conf
+ ),
+ output: 'gnome-initial-setup.desktop',
+ install_dir: join_paths(data_dir, 'applications'),
+ po_dir: po_dir,
+ install: true,
+ type: 'desktop'
+)
+
+data_conf = configuration_data()
+data_conf.set('bindir', bin_dir)
+data_conf.set('libexecdir', libexec_dir)
+
+if enable_systemd
+ unit_files = {
+ 'gnome-initial-setup-first-login.service' : [ 'gnome-session.target.wants/' ],
+ 'gnome-initial-setup-copy-worker.service' : [ 'gnome-session.target.wants/' ],
+ }
+
+ foreach unit, wants: unit_files
+ configure_file(
+ input: unit + '.in',
+ output: unit,
+ configuration: data_conf,
+ install_dir: systemd_userunitdir
+ )
+
+ foreach target: wants
+ meson.add_install_script('meson-add-wants.sh', systemd_userunitdir, target, unit)
+ endforeach
+ endforeach
+
+ gis_user_session_wanted_targets = []
+ foreach component: gis_user_session_wanted_components
+ gis_user_session_wanted_targets += 'Wants=@0@.target'.format(component)
+ endforeach
+
+ configure_file(
+ input: '@0@.session.conf.in'.format(meson.project_name()),
+ output: 'session.conf',
+ configuration: {
+ 'this_component': meson.project_name(),
+ 'requires_component': gis_shell_component,
+ 'wants_required_components': '\n'.join(
+ gis_user_session_wanted_targets),
+ },
+ install_dir: systemd_userunitdir / 'gnome-session@@0@.target.d'.format(
+ meson.project_name()),
+ )
+
+ install_data('gnome-initial-setup.conf', install_dir: systemd_sysusersdir)
+endif
+
+
+rules_dir = join_paths(data_dir, 'polkit-1', 'rules.d')
+configure_file(
+ input: '20-gnome-initial-setup.rules.in',
+ output: '20-gnome-initial-setup.rules',
+ install: true,
+ install_dir: rules_dir,
+ configuration: data_conf,
+)
+
+session_dir = join_paths(data_dir, 'gnome-session', 'sessions')
+configure_file(
+ input: '@0@.session.in'.format(meson.project_name()),
+ output: '@BASENAME@',
+ configuration: {
+ 'this_component': meson.project_name(),
+ 'gnome_session_required_components': ';'.join([
+ gis_shell_component,
+ meson.project_name(),
+ ] +
+ gis_gnome_session_required_components),
+ },
+ install_dir: session_dir,
+)
+
+mode_dir = join_paths(data_dir, 'gnome-shell', 'modes')
+install_data('initial-setup.json', install_dir: mode_dir)
diff --git a/gnome-initial-setup.doap b/gnome-initial-setup.doap
new file mode 100644
index 0000000..c194c82
--- /dev/null
+++ b/gnome-initial-setup.doap
@@ -0,0 +1,31 @@
+<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
+ xmlns:gnome="http://api.gnome.org/doap-extensions#"
+ xmlns="http://usefulinc.com/ns/doap#">
+
+ <name xml:lang="en">GNOME Initial Setup</name>
+ <shortdesc xml:lang="en">Bootstrapping your OS</shortdesc>
+ <description xml:lang="en">gnome-initial-setup helps you set up your OS when you boot or log in for the first time.</description>
+
+ <download-page rdf:resource="http://download.gnome.org/sources/gnome-initial-setup/" />
+ <bug-database rdf:resource="https://gitlab.gnome.org/GNOME/gnome-initial-setup/issues/" />
+ <category rdf:resource="http://api.gnome.org/doap-extensions#apps" />
+ <programming-language>C</programming-language>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Matthias Clasen</foaf:name>
+ <foaf:mbox rdf:resource="mailto:mclasen@redhat.com" />
+ <gnome:userid>matthiasc</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Will Thompson</foaf:name>
+ <foaf:mbox rdf:resource="mailto:wjt@gnome.org" />
+ <gnome:userid>wjt</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+</Project>
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