diff options
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. @@ -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. @@ -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 (¤t_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 |