[gnome-initial-setup/wip/pwithnall/misc-fixes: 47/70] site: add page to track site-specific details for deployments
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-initial-setup/wip/pwithnall/misc-fixes: 47/70] site: add page to track site-specific details for deployments
- Date: Fri, 11 Sep 2020 13:29:14 +0000 (UTC)
commit 32ec9d456c0eb1f6d9048ba86624dff17dbb0b95
Author: Travis Reitter <travis reitter endlessm com>
Date: Tue Apr 17 13:05:44 2018 -0700
site: add page to track site-specific details for deployments
These details will be included in metrics messages.
The ID field is intentionally editable; this is a requirement for
Conafe.
See README.md file for more details.
(Rebase 3.38: Drop mention of demo support in the prepare() vfunc.)
https://phabricator.endlessm.com/T18842
gnome-initial-setup/gnome-initial-setup.c | 2 +
gnome-initial-setup/pages/meson.build | 1 +
gnome-initial-setup/pages/site/README.md | 14 +
.../pages/site/deployment-sites.json.example | 34 ++
gnome-initial-setup/pages/site/eos-write-location | 16 +
gnome-initial-setup/pages/site/gis-site-page.c | 469 +++++++++++++++++++++
gnome-initial-setup/pages/site/gis-site-page.h | 62 +++
gnome-initial-setup/pages/site/gis-site-page.ui | 279 ++++++++++++
.../pages/site/gis-site-search-entry.c | 452 ++++++++++++++++++++
.../pages/site/gis-site-search-entry.h | 44 ++
gnome-initial-setup/pages/site/gis-site.c | 289 +++++++++++++
gnome-initial-setup/pages/site/gis-site.h | 47 +++
gnome-initial-setup/pages/site/meson.build | 20 +
gnome-initial-setup/pages/site/site.gresource.xml | 6 +
po/POTFILES.in | 2 +
15 files changed, 1737 insertions(+)
---
diff --git a/gnome-initial-setup/gnome-initial-setup.c b/gnome-initial-setup/gnome-initial-setup.c
index b4f4008f..bfe721ea 100644
--- a/gnome-initial-setup/gnome-initial-setup.c
+++ b/gnome-initial-setup/gnome-initial-setup.c
@@ -46,6 +46,7 @@
#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/site/gis-site-page.h"
#include "pages/summary/gis-summary-page.h"
#define VENDOR_PAGES_GROUP "pages"
@@ -85,6 +86,7 @@ static PageData page_table[] = {
PAGE (parental_controls, TRUE),
PAGE (parent_password, TRUE),
#endif
+ PAGE (site, TRUE),
PAGE (summary, FALSE),
{ NULL },
};
diff --git a/gnome-initial-setup/pages/meson.build b/gnome-initial-setup/pages/meson.build
index 0898d2ac..0f916488 100644
--- a/gnome-initial-setup/pages/meson.build
+++ b/gnome-initial-setup/pages/meson.build
@@ -10,6 +10,7 @@ pages = [
'privacy',
'goa',
'password',
+ 'site',
'summary',
'welcome',
]
diff --git a/gnome-initial-setup/pages/site/README.md b/gnome-initial-setup/pages/site/README.md
new file mode 100644
index 00000000..acf3e6e1
--- /dev/null
+++ b/gnome-initial-setup/pages/site/README.md
@@ -0,0 +1,14 @@
+Summary
+=======
+This page allows the user to search for a site among the ones in the global JSON
+file or enter their own.
+
+The resulting details will be stored globally for the metrics daemon to include
+in reports for analyzing specific deployments of computers.
+
+See `eos-write-location` and `gis-site-page.c` for details on the locations of
+these files on the system.
+
+See the `deployment-sites.json.example` file for the syntax. Note that this page
+will not be displayed in the first boot experience if a file matching the
+expected path does not exist or fails to be parsed correctly.
diff --git a/gnome-initial-setup/pages/site/deployment-sites.json.example
b/gnome-initial-setup/pages/site/deployment-sites.json.example
new file mode 100644
index 00000000..4c7d0a9e
--- /dev/null
+++ b/gnome-initial-setup/pages/site/deployment-sites.json.example
@@ -0,0 +1,34 @@
+[
+ {
+ "id": "00112233",
+ "facility": "Example facility A",
+ "locality": "Bogotá",
+ "region": "DC",
+ "country": "Colombia"
+ },
+ {
+ "id": "other_facility",
+ "facility": "Other Facility",
+ "street": "123 Easy Street",
+ "locality": "San Jose",
+ "region": "CA",
+ "country": "USA"
+ },
+ {
+ "id": "18a9e72bc",
+ "facility": "Yet another Facility",
+ "locality": "San José",
+ "region": "San José",
+ "country": "Costa Rica"
+ },
+ {
+ "facility": "Facility with no ID or country",
+ "locality": "Manaus",
+ "region": "Amazonas"
+ },
+ {
+ "facility": "Only facility, street address, locality",
+ "street": "456 Main Street",
+ "locality": "Boston"
+ }
+]
diff --git a/gnome-initial-setup/pages/site/eos-write-location
b/gnome-initial-setup/pages/site/eos-write-location
new file mode 100755
index 00000000..70975a57
--- /dev/null
+++ b/gnome-initial-setup/pages/site/eos-write-location
@@ -0,0 +1,16 @@
+#!/bin/bash -e
+
+LOCATION_FILENAME=/etc/metrics/location.conf
+LOCATION_DIRNAME=$(dirname $LOCATION_FILENAME)
+
+src_location_filename=$1
+
+if [ "$#" -ne 1 ]; then
+ echo "usage: $0 SRC_FILENAME" >&2
+ exit 1
+ fi
+
+mkdir -p $LOCATION_DIRNAME
+mv $src_location_filename $LOCATION_FILENAME
+chmod 644 $LOCATION_FILENAME
+chown metrics:metrics $LOCATION_FILENAME
diff --git a/gnome-initial-setup/pages/site/gis-site-page.c b/gnome-initial-setup/pages/site/gis-site-page.c
new file mode 100644
index 00000000..2d0ec228
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.c
@@ -0,0 +1,469 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Jasper St. Pierre <jstpierre mecheye net>
+ * Travis Reitter <travis reitter endlessm com>
+ */
+
+/* Site page {{{1 */
+
+#define PAGE_ID GIS_SITE_PAGE_ID
+
+#include "config.h"
+#include "gis-site.h"
+#include "gis-site-page.h"
+#include "gis-site-search-entry.h"
+#include "site-resources.h"
+
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include <json-glib/json-glib.h>
+
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define LOCATION_FILENAME_TEMPL "gnome-initial-setup-location-XXXXXX.conf"
+
+struct _GisSitePagePrivate
+{
+ GtkWidget *search_entry;
+ GtkWidget *manual_check;
+ GtkWidget *id_entry;
+ GtkWidget *facility_entry;
+ GtkWidget *street_entry;
+ GtkWidget *locality_entry;
+ GtkWidget *region_entry;
+ GtkWidget *country_entry;
+};
+typedef struct _GisSitePagePrivate GisSitePagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSitePage, gis_site_page, GIS_TYPE_PAGE)
+
+static void
+set_site (GisSitePage *page,
+ GisSite *site)
+{
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+ const gchar *id = NULL;
+ const gchar *facility = NULL;
+ const gchar *street = NULL;
+ const gchar *locality = NULL;
+ const gchar *region = NULL;
+ const gchar *country = NULL;
+
+ if (site)
+ {
+ id = gis_site_get_id (site);
+ facility = gis_site_get_facility (site);
+ street = gis_site_get_street (site);
+ locality = gis_site_get_locality (site);
+ region = gis_site_get_region (site);
+ country = gis_site_get_country (site);
+ }
+
+ gtk_entry_set_text (GTK_ENTRY (priv->id_entry), id ? id : "");
+ gtk_entry_set_text (GTK_ENTRY (priv->facility_entry),
+ facility ? facility : "");
+ gtk_entry_set_text (GTK_ENTRY (priv->street_entry), street ? street : "");
+ gtk_entry_set_text (GTK_ENTRY (priv->locality_entry),
+ locality ? locality : "");
+ gtk_entry_set_text (GTK_ENTRY (priv->region_entry), region ? region : "");
+ gtk_entry_set_text (GTK_ENTRY (priv->country_entry), country ? country : "");
+}
+
+static gboolean
+page_validate (GisSitePage *page)
+{
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+
+ if (gtk_entry_get_text_length (GTK_ENTRY (priv->id_entry)) ||
+ gtk_entry_get_text_length (GTK_ENTRY (priv->facility_entry)) ||
+ gtk_entry_get_text_length (GTK_ENTRY (priv->street_entry)) ||
+ gtk_entry_get_text_length (GTK_ENTRY (priv->locality_entry)) ||
+ gtk_entry_get_text_length (GTK_ENTRY (priv->region_entry)) ||
+ gtk_entry_get_text_length (GTK_ENTRY (priv->country_entry)))
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+update_page_validation (GObject *object, GParamSpec *param, GisSitePage *page)
+{
+ gis_page_set_complete (GIS_PAGE (page), page_validate (page));
+}
+
+static GisSite*
+get_site_from_widgets (GisSitePage *page)
+{
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+ const gchar *id = gtk_entry_get_text (GTK_ENTRY (priv->id_entry));
+ const gchar *facility = gtk_entry_get_text (GTK_ENTRY (priv->facility_entry));
+ const gchar *street = gtk_entry_get_text (GTK_ENTRY (priv->street_entry));
+ const gchar *locality = gtk_entry_get_text (GTK_ENTRY (priv->locality_entry));
+ const gchar *region = gtk_entry_get_text (GTK_ENTRY (priv->region_entry));
+ const gchar *country = gtk_entry_get_text (GTK_ENTRY (priv->country_entry));
+
+ return gis_site_new (id, facility, street, locality, region, country);
+}
+
+static void
+ini_string_append_safe (GString *string,
+ const gchar *property,
+ const gchar *value)
+{
+ g_string_append_printf (string, "%s = %s\n", property, value ? value : "");
+}
+
+static gchar*
+ini_file_content_from_site (GisSite *site)
+{
+ GString *builder = g_string_new ("[Label]\n");
+
+ /* Note the field names aren't 1:1 between GisSite and the file format */
+ ini_string_append_safe (builder, "id", gis_site_get_id (site));
+ ini_string_append_safe (builder, "facility", gis_site_get_facility (site));
+ ini_string_append_safe (builder, "street", gis_site_get_street (site));
+ ini_string_append_safe (builder, "city", gis_site_get_locality (site));
+ ini_string_append_safe (builder, "state", gis_site_get_region (site));
+ ini_string_append_safe (builder, "country", gis_site_get_country (site));
+
+ return g_string_free (builder, FALSE);
+}
+
+static gboolean
+gis_site_page_save_data (GisPage *gis_page,
+ GError **error)
+{
+ GisSitePage *page = GIS_SITE_PAGE (gis_page);
+ g_autofree gchar *location_file_content = NULL;
+ g_autofree gchar *filename_created = NULL;
+ g_autoptr(GisSite) site = NULL;
+ int fd;
+
+ if (gis_page->driver == NULL)
+ return TRUE;
+
+ site = get_site_from_widgets (page);
+ location_file_content = ini_file_content_from_site (site);
+
+ fd = g_file_open_tmp (LOCATION_FILENAME_TEMPL, &filename_created, error);
+ if (fd < 0)
+ return FALSE;
+
+ /* immediately close the new file so we don't have multiple FDs open for
+ * the file at the same time
+ */
+ if (!g_close (fd, error))
+ return FALSE;
+
+ if (!g_file_set_contents (filename_created, location_file_content, -1,
+ error))
+ return FALSE;
+
+ if (!gis_pkexec (LIBEXECDIR "/eos-write-location", filename_created, NULL,
+ error))
+ return FALSE;
+
+ return TRUE;
+}
+
+static void
+entry_site_changed (GObject *object, GParamSpec *param, GisSitePage *page)
+{
+ GisSiteSearchEntry *entry = GIS_SITE_SEARCH_ENTRY (object);
+ GisSite *site;
+
+ site = gis_site_search_entry_get_site (entry);
+ set_site (page, site);
+}
+
+static void
+manual_check_toggled (GtkToggleButton *manual_check, GisSitePage *page)
+{
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+ gboolean active = gtk_toggle_button_get_active (manual_check);
+
+ /* clear the search entry and field GtkEntrys */
+ gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+
+ /* make the search active and all the "field" GtkEntrys inactive or vice versa
+ */
+ gtk_widget_set_can_focus (priv->search_entry, !active);
+ gtk_widget_set_can_focus (priv->id_entry, active);
+ gtk_widget_set_can_focus (priv->facility_entry, active);
+ gtk_widget_set_can_focus (priv->street_entry, active);
+ gtk_widget_set_can_focus (priv->locality_entry, active);
+ gtk_widget_set_can_focus (priv->region_entry, active);
+ gtk_widget_set_can_focus (priv->country_entry, active);
+
+ gtk_widget_set_sensitive (priv->search_entry, !active);
+ gtk_widget_set_sensitive (priv->id_entry, active);
+ gtk_widget_set_sensitive (priv->facility_entry, active);
+ gtk_widget_set_sensitive (priv->street_entry, active);
+ gtk_widget_set_sensitive (priv->locality_entry, active);
+ gtk_widget_set_sensitive (priv->region_entry, active);
+ gtk_widget_set_sensitive (priv->country_entry, active);
+
+ /* focus the first sensible widget for entry */
+ if (active)
+ gtk_widget_grab_focus (priv->id_entry);
+ else
+ gtk_widget_grab_focus (priv->search_entry);
+}
+
+static void
+gis_site_page_shown (GisPage *gis_page)
+{
+ GisSitePage *page = GIS_SITE_PAGE (gis_page);
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+
+ gtk_widget_grab_focus (priv->search_entry);
+}
+
+/**
+ * Parses the sites file which is a JSON file in the format:
+ *
+ * [SITE_1, SITE_2, SITE_3, ...]
+ *
+ * where each SITE is a JSON object containing one or more of the following
+ * members:
+ * * id: (string) - a pre-existing ID (if any) the site admins use for it
+ * * facility: (string) - a name for the site/building
+ * * street: (string) - the street address for the site (eg, "123 Main St.")
+ * * locality: (string) - city or equivalent
+ * * region: (string) - state (US), department (Colombia), or equivalent
+ * * country: (string) - country
+ */
+static GPtrArray*
+parse_sites_file (const gchar *filename)
+{
+ gboolean sites_usable = TRUE;
+ GPtrArray *sites = NULL;
+ JsonParser *parser;
+ JsonNode *root;
+ JsonReader *reader = NULL;
+ GError *error;
+
+ parser = json_parser_new ();
+
+ error = NULL;
+ json_parser_load_from_file (parser, filename, &error);
+ if (error)
+ {
+ if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_warning ("Unable to parse ‘%s’: %s", filename, error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ root = json_parser_get_root (parser);
+ if (!root)
+ goto out;
+
+ reader = json_reader_new (root);
+ if (!json_reader_is_array (reader))
+ goto out;
+
+ sites = g_ptr_array_new_full (0, g_object_unref);
+ int count = json_reader_count_elements (reader);
+ for (int i = 0; count > 0 && i < count; i++)
+ {
+ json_reader_read_element (reader, i);
+ if (json_reader_is_object (reader))
+ {
+ GisSite *site;
+ const gchar *id = NULL;
+ const gchar *facility = NULL;
+ const gchar *street = NULL;
+ const gchar *locality = NULL;
+ const gchar *region = NULL;
+ const gchar *country = NULL;
+
+ json_reader_read_member (reader, "id");
+ id = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "facility");
+ facility = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "street");
+ street = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "locality");
+ locality = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "region");
+ region = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ json_reader_read_member (reader, "country");
+ country = json_reader_get_string_value (reader);
+ json_reader_end_member (reader);
+
+ site = gis_site_new (id, facility, street, locality, region, country);
+ g_ptr_array_add (sites, site);
+ }
+ else
+ {
+ sites_usable = FALSE;
+ goto out;
+ }
+ json_reader_end_element (reader);
+ }
+
+out:
+ g_clear_object (&reader);
+ g_clear_object (&parser);
+ if (!sites_usable)
+ g_clear_pointer (&sites, g_ptr_array_unref);
+
+ return sites;
+}
+
+static void
+gis_site_page_constructed (GObject *object)
+{
+ GisSitePage *page = GIS_SITE_PAGE (object);
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+ gboolean visible = TRUE;
+
+ G_OBJECT_CLASS (gis_site_page_parent_class)->constructed (object);
+
+ if (!g_file_test (GIS_SITE_PAGE_SITES_FILE, G_FILE_TEST_EXISTS))
+ {
+ visible = FALSE;
+ goto out;
+ }
+
+ /* make all the fields insensitive to start */
+ manual_check_toggled (GTK_TOGGLE_BUTTON (priv->manual_check), page);
+
+ /* let the ID field show its current value (if populated based on a matched
+ * pre-defined site) but don't let the user set it when editing manually
+ */
+ gtk_widget_set_can_focus (priv->id_entry, FALSE);
+ gtk_widget_set_sensitive (priv->id_entry, FALSE);
+
+ g_signal_connect (priv->search_entry,
+ "notify::" GIS_SITE_SEARCH_ENTRY_PROP_SITE,
+ G_CALLBACK (entry_site_changed), page);
+
+ g_signal_connect (priv->manual_check, "toggled",
+ G_CALLBACK (manual_check_toggled), page);
+
+ g_signal_connect (priv->id_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+ g_signal_connect (priv->facility_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+ g_signal_connect (priv->street_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+ g_signal_connect (priv->locality_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+ g_signal_connect (priv->region_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+ g_signal_connect (priv->country_entry,
+ "notify::text-length",
+ G_CALLBACK (update_page_validation), page);
+
+out:
+ gtk_widget_set_visible (GTK_WIDGET (page), visible);
+}
+
+static void
+gis_site_page_class_init (GisSitePageClass *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-site-page.ui");
+
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ search_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ manual_check);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ id_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ facility_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ street_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ locality_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ region_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GisSitePage,
+ country_entry);
+
+ page_class->page_id = PAGE_ID;
+ page_class->save_data = gis_site_page_save_data;
+ page_class->shown = gis_site_page_shown;
+ object_class->constructed = gis_site_page_constructed;
+}
+
+static void
+gis_site_page_init (GisSitePage *page)
+{
+ GisSitePagePrivate *priv = gis_site_page_get_instance_private (page);
+ GPtrArray *sites_array;
+
+ g_resources_register (site_get_resource ());
+
+ g_type_ensure (GIS_TYPE_SITE_SEARCH_ENTRY);
+
+ gtk_widget_init_template (GTK_WIDGET (page));
+
+ sites_array = parse_sites_file (GIS_SITE_PAGE_SITES_FILE);
+
+ g_object_set (priv->search_entry, GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE,
+ sites_array, NULL);
+
+ g_clear_pointer (&sites_array, g_ptr_array_unref);
+}
+
+GisPage *
+gis_prepare_site_page (GisDriver *driver)
+{
+ if (gis_driver_is_live_session (driver))
+ return NULL;
+
+ return g_object_new (GIS_TYPE_SITE_PAGE,
+ "driver", driver,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/site/gis-site-page.h b/gnome-initial-setup/pages/site/gis-site-page.h
new file mode 100644
index 00000000..df4c2232
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.h
@@ -0,0 +1,62 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2012 Red Hat
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Jasper St. Pierre <jstpierre mecheye net>
+ * Travis Reitter <travis reitter endlessm com>
+ */
+
+#ifndef __GIS_SITE_PAGE_H__
+#define __GIS_SITE_PAGE_H__
+
+#include <glib-object.h>
+
+#include "gnome-initial-setup.h"
+
+#define GIS_SITE_PAGE_ID "site"
+#define GIS_SITE_PAGE_SITES_FILE "/var/lib/eos-image-defaults/deployment-sites.json"
+
+G_BEGIN_DECLS
+
+#define GIS_TYPE_SITE_PAGE (gis_site_page_get_type ())
+#define GIS_SITE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GIS_TYPE_SITE_PAGE,
GisSitePage))
+#define GIS_SITE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GIS_TYPE_SITE_PAGE,
GisSitePageClass))
+#define GIS_IS_SITE_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GIS_TYPE_SITE_PAGE))
+#define GIS_IS_SITE_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GIS_TYPE_SITE_PAGE))
+#define GIS_SITE_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GIS_TYPE_SITE_PAGE,
GisSitePageClass))
+
+typedef struct _GisSitePage GisSitePage;
+typedef struct _GisSitePageClass GisSitePageClass;
+
+struct _GisSitePage
+{
+ GisPage parent;
+};
+
+struct _GisSitePageClass
+{
+ GisPageClass parent_class;
+};
+
+GType gis_site_page_get_type (void);
+
+GisPage *gis_prepare_site_page (GisDriver *driver);
+
+G_END_DECLS
+
+#endif /* __GIS_SITE_PAGE_H__ */
diff --git a/gnome-initial-setup/pages/site/gis-site-page.ui b/gnome-initial-setup/pages/site/gis-site-page.ui
new file mode 100644
index 00000000..7675b158
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-page.ui
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="GisSitePage" parent="GisPage">
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="halign">center</property>
+ <property name="valign">fill</property>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="visible" bind-source="GisSitePage" bind-property="small-screen"
bind-flags="invert-boolean|sync-create"/>
+ <property name="can_focus">False</property>
+ <property name="margin_top">24</property>
+ <property name="pixel_size">96</property>
+ <property name="icon_name">poi-building</property>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="visible" bind-source="GisSitePage" bind-property="small-screen"
bind-flags="invert-boolean|sync-create"/>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">12</property>
+ <property name="halign">center</property>
+ <property name="valign">start</property>
+ <property name="vexpand">True</property>
+ <property name="label" translatable="yes">Site</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="1.8"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="page_box">
+ <property name="visible">True</property>
+ <property name="margin_top">18</property>
+ <property name="margin_bottom">18</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">14</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="halign">center</property>
+ <property name="wrap">True</property>
+ <property name="label" translatable="yes">At which site is this computer located? Search for
your site here. You may enter manually if there is no match.</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="margin_top">10</property>
+ <property name="halign">center</property>
+ <property name="orientation">horizontal</property>
+ <property name="spacing">14</property>
+
+ <child>
+ <object class="GisSiteSearchEntry" id="search_entry">
+ <property name="visible">True</property>
+ <property name="halign">center</property>
+ <property name="max-width-chars">55</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkCheckButton" id="manual_check">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">enter manually</property>
+ <property name="halign">end</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkGrid" id="fields_grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="column_spacing">12</property>
+ <property name="row_spacing">6</property>
+ <property name="margin_top">20</property>
+ <property name="halign">center</property>
+
+ <child>
+ <object class="GtkLabel" id="id_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">ID</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="id_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ <property name="margin_end">30</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="facility_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Facility</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="facility_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">0</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkLabel" id="street_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Street address</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="street_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ <property name="margin_end">30</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="locality_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">City</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="locality_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">1</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkLabel" id="region_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">State</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="region_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ <property name="margin_end">30</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="country_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Country</property>
+ </object>
+ <packing>
+ <property name="left_attach">3</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="country_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="visibility">True</property>
+ <property name="width_chars">24</property>
+ </object>
+ <packing>
+ <property name="left_attach">4</property>
+ <property name="top_attach">2</property>
+ <property name="width">1</property>
+ <property name="height">1</property>
+ </packing>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gnome-initial-setup/pages/site/gis-site-search-entry.c
b/gnome-initial-setup/pages/site/gis-site-search-entry.c
new file mode 100644
index 00000000..f16f2686
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-search-entry.c
@@ -0,0 +1,452 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Copyright (C) 2008 Red Hat, Inc.
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Travis Reitter <travis reitter endlessm com>
+ */
+
+#include <string.h>
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gis-site-search-entry.h"
+
+/**
+ * SECTION:GisSiteSearchEntry
+ * @Title: GisSiteSearchEntry
+ *
+ * A subclass of #GtkSearchEntry that provides autocompletion on
+ * #GisSite<!-- -->s
+ */
+
+typedef struct {
+ GisSite *site;
+ GPtrArray *sites;
+} GisSiteSearchEntryPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSiteSearchEntry, gis_site_search_entry,
+ GTK_TYPE_SEARCH_ENTRY)
+
+enum {
+ PROP_0,
+
+ PROP_SITES_AVAILABLE,
+ PROP_SITE,
+
+ LAST_PROP
+};
+
+static void set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec);
+static void get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec);
+
+static void set_site_internal (GisSiteSearchEntry *entry,
+ GtkTreeIter *iter,
+ GisSite *site);
+
+static void fill_store (GisSite *site, GtkListStore *store);
+
+enum STORE_COL
+{
+ GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME = 0,
+ GIS_SITE_SEARCH_ENTRY_COL_SITE,
+ GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME,
+};
+
+static gboolean matcher (GtkEntryCompletion *completion, const char *key,
+ GtkTreeIter *iter, gpointer user_data);
+static gboolean match_selected (GtkEntryCompletion *completion,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer entry);
+static void entry_changed (GisSiteSearchEntry *entry);
+
+static void
+gis_site_search_entry_init (GisSiteSearchEntry *entry)
+{
+ GtkEntryCompletion *completion;
+
+ completion = gtk_entry_completion_new ();
+
+ gtk_entry_completion_set_popup_set_width (completion, FALSE);
+ gtk_entry_completion_set_text_column (completion,
+ GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME);
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_inline_completion (completion, TRUE);
+
+ g_signal_connect (completion, "match-selected", G_CALLBACK (match_selected),
+ entry);
+
+ gtk_entry_set_completion (GTK_ENTRY (entry), completion);
+ g_object_unref (completion);
+
+ g_signal_connect (entry, "changed",
+ G_CALLBACK (entry_changed), NULL);
+}
+
+static void
+finalize (GObject *object)
+{
+ GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+ GIS_SITE_SEARCH_ENTRY (object));
+
+ g_clear_object (&priv->site);
+ g_clear_pointer (&priv->sites, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (gis_site_search_entry_parent_class)->finalize (object);
+}
+
+static void
+gis_site_search_entry_class_init (
+ GisSiteSearchEntryClass *site_search_entry_class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (site_search_entry_class);
+
+ object_class->finalize = finalize;
+ object_class->set_property = set_property;
+ object_class->get_property = get_property;
+
+ /* properties */
+ g_object_class_install_property (
+ object_class, PROP_SITES_AVAILABLE,
+ g_param_spec_boxed (GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE,
+ "Full site list",
+ "The array of sites available",
+ G_TYPE_PTR_ARRAY,
+ G_PARAM_WRITABLE));
+
+ g_object_class_install_property (
+ object_class, PROP_SITE,
+ g_param_spec_object (GIS_SITE_SEARCH_ENTRY_PROP_SITE,
+ "Site",
+ "The selected site",
+ GIS_TYPE_SITE,
+ G_PARAM_READWRITE));
+}
+
+static void
+set_property_sites_available (GisSiteSearchEntry *self,
+ const GValue *value)
+{
+ GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+ self);
+ GtkListStore *store = NULL;
+ GtkEntryCompletion *completion;
+
+ store = gtk_list_store_new (3, G_TYPE_STRING, GIS_TYPE_SITE, G_TYPE_STRING);
+
+ g_clear_pointer (&priv->sites, g_ptr_array_unref);
+ priv->sites = g_value_dup_boxed (value);
+ for (int i = 0; priv->sites && i < priv->sites->len; i++)
+ {
+ GisSite *site = g_ptr_array_index (priv->sites, i);
+ fill_store (site, store);
+ }
+
+ completion = gtk_entry_get_completion (GTK_ENTRY (self));
+ gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
+ gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (store));
+}
+
+static void
+set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ switch (prop_id)
+ {
+ case PROP_SITES_AVAILABLE:
+ set_property_sites_available (GIS_SITE_SEARCH_ENTRY (object), value);
+ break;
+ case PROP_SITE:
+ gis_site_search_entry_set_site (GIS_SITE_SEARCH_ENTRY (object),
+ g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ GisSiteSearchEntryPrivate *priv = gis_site_search_entry_get_instance_private (
+ GIS_SITE_SEARCH_ENTRY (object));
+
+ switch (prop_id)
+ {
+ case PROP_SITE:
+ g_value_set_boxed (value, priv->site);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+entry_changed (GisSiteSearchEntry *entry)
+{
+ const gchar *text = gtk_entry_get_text (GTK_ENTRY (entry));
+
+ if (!text || text[0] == '\0')
+ set_site_internal (entry, NULL, NULL);
+}
+
+static void
+set_site_internal (GisSiteSearchEntry *entry,
+ GtkTreeIter *iter,
+ GisSite *site)
+{
+ GisSiteSearchEntryPrivate *priv =
+ gis_site_search_entry_get_instance_private (entry);
+
+ g_clear_object (&priv->site);
+
+ g_assert (iter == NULL || site == NULL);
+
+ if (iter)
+ {
+ char *name;
+ GtkEntryCompletion *completion = gtk_entry_get_completion (
+ GTK_ENTRY (entry));
+ GtkTreeModel *model = gtk_entry_completion_get_model (completion);
+
+ gtk_tree_model_get (model, iter,
+ GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME, &name,
+ GIS_SITE_SEARCH_ENTRY_COL_SITE, &priv->site,
+ -1);
+ gtk_entry_set_text (GTK_ENTRY (entry), name);
+ g_free (name);
+ }
+ else if (site)
+ {
+ gchar *display_name = gis_site_get_display_name (site);
+ priv->site = g_object_ref (site);
+ gtk_entry_set_text (GTK_ENTRY (entry), display_name);
+
+ g_free (display_name);
+ }
+ else
+ {
+ gtk_entry_set_text (GTK_ENTRY (entry), "");
+ }
+
+ gtk_editable_set_position (GTK_EDITABLE (entry), -1);
+ g_object_notify (G_OBJECT (entry), GIS_SITE_SEARCH_ENTRY_PROP_SITE);
+}
+
+/**
+ * gis_site_search_entry_set_site:
+ * @entry: a #GisSiteSearchEntry
+ * @site: (allow-none): a #GisSite in @entry, or %NULL to clear @entry
+ *
+ * Sets @entry's site to @site, and update the text of the entry accordingly.
+ **/
+void
+gis_site_search_entry_set_site (GisSiteSearchEntry *entry,
+ GisSite *site)
+{
+ g_return_if_fail (GIS_IS_SITE_SEARCH_ENTRY (entry));
+
+ set_site_internal (entry, NULL, site);
+}
+
+/**
+ * gis_site_search_entry_get_site:
+ * @entry: a #GisSiteSearchEntry
+ *
+ * Gets the site that was set by a previous call to
+ * gis_site_search_entry_set_site() or was selected by the user.
+ *
+ * Return value: (transfer none) (allow-none): the selected site or %NULL if no
+ * site is selected.
+ **/
+GisSite *
+gis_site_search_entry_get_site (GisSiteSearchEntry *entry)
+{
+ GisSiteSearchEntryPrivate *priv;
+
+ g_return_val_if_fail (GIS_IS_SITE_SEARCH_ENTRY (entry), NULL);
+
+ priv = gis_site_search_entry_get_instance_private (entry);
+
+ return priv->site;
+}
+
+static char *
+find_word (const char *full_name, const char *word, int word_len,
+ gboolean whole_word, gboolean is_first_word)
+{
+ char *p;
+
+ if (word == NULL || *word == '\0')
+ return NULL;
+
+ p = (char *)full_name - 1;
+ while ((p = strchr (p + 1, *word)))
+ {
+ if (strncmp (p, word, word_len) != 0)
+ continue;
+
+ if (p > (char *)full_name)
+ {
+ char *prev = g_utf8_prev_char (p);
+
+ /* Make sure p points to the start of a word */
+ if (g_unichar_isalpha (g_utf8_get_char (prev)))
+ continue;
+
+ /* If we're matching the first word of the key, it has to
+ * match the first word of the field.
+ * Eg, it either matches the start of the string
+ * (which we already know it doesn't at this point) or
+ * it is preceded by the string ", " or "(" (which isn't actually
+ * a perfect test.)
+ */
+ if (is_first_word)
+ {
+ if (prev == (char *)full_name ||
+ ((prev - 1 <= full_name && strncmp (prev - 1, ", ", 2) != 0)
+ && *prev != '('))
+ continue;
+ }
+ }
+
+ if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len)))
+ continue;
+
+ return p;
+ }
+
+ return NULL;
+}
+
+static gboolean
+match_compare_name (const char *key, const char *name)
+{
+ gboolean is_first_word = TRUE;
+ size_t len;
+
+ /* Ignore whitespace before the string */
+ key += strspn (key, " ");
+
+ /* All but the last word in KEY must match a full word from NAME,
+ * in order (but possibly skipping some words from NAME).
+ */
+ len = strcspn (key, " ");
+ while (key[len])
+ {
+ name = find_word (name, key, len, TRUE, is_first_word);
+ if (!name)
+ return FALSE;
+
+ key += len;
+ while (*key && !g_unichar_isalnum (g_utf8_get_char (key)))
+ key = g_utf8_next_char (key);
+ while (*name && !g_unichar_isalnum (g_utf8_get_char (name)))
+ name = g_utf8_next_char (name);
+
+ len = strcspn (key, " ");
+ is_first_word = FALSE;
+ }
+
+ /* The last word in KEY must match a prefix of a following word in NAME */
+ if (len == 0)
+ return TRUE;
+
+ /* if we get here, key[len] == 0 */
+ g_assert (len == strlen (key));
+
+ return find_word (name, key, len, FALSE, is_first_word) != NULL;
+}
+
+static gboolean
+matcher (GtkEntryCompletion *completion, const char *key,
+ GtkTreeIter *iter, gpointer user_data)
+{
+ char *compare_name;
+ gboolean match;
+
+ gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter,
+ GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME, &compare_name,
+ -1);
+
+ match = match_compare_name (key, compare_name);
+
+ g_free (compare_name);
+
+ return match;
+}
+
+static gboolean
+match_selected (GtkEntryCompletion *completion,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer entry)
+{
+ set_site_internal (entry, iter, NULL);
+
+ return TRUE;
+}
+
+static void
+fill_store (GisSite *site, GtkListStore *store)
+{
+ char *display_name;
+ char *normalized;
+ char *compare_name;
+
+ display_name = gis_site_get_display_name (site);
+ normalized = g_utf8_normalize (display_name, -1, G_NORMALIZE_ALL);
+ compare_name = g_utf8_casefold (normalized, -1);
+
+ gtk_list_store_insert_with_values (store, NULL, -1,
+ GIS_SITE_SEARCH_ENTRY_COL_DISPLAY_NAME, display_name,
+ GIS_SITE_SEARCH_ENTRY_COL_SITE, site,
+ GIS_SITE_SEARCH_ENTRY_COL_LOCAL_COMPARE_NAME, compare_name,
+ -1);
+
+ g_free (normalized);
+ g_free (compare_name);
+ g_free (display_name);
+}
+
+/**
+ * gis_site_search_entry_new:
+ * @sites: a #GPtrArray of #GisSite objects to serve as the data source for this
+ * search widget. This array and its content must not be modified after this
+ * function is called.
+ *
+ * Creates a new #GisSiteSearchEntry.
+ *
+ * To be nofified when the user makes a selection, add a signal handler for
+ * "notify::site".
+ *
+ * Return value: the new #GisSiteSearchEntry
+ **/
+GtkWidget *
+gis_site_search_entry_new (GPtrArray *sites)
+{
+ return g_object_new (GIS_TYPE_SITE_SEARCH_ENTRY,
+ GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE, sites,
+ NULL);
+}
diff --git a/gnome-initial-setup/pages/site/gis-site-search-entry.h
b/gnome-initial-setup/pages/site/gis-site-search-entry.h
new file mode 100644
index 00000000..02e84912
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site-search-entry.h
@@ -0,0 +1,44 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/* location-entry.h - Location-selecting text entry
+ *
+ * Copyright 2008, Red Hat, Inc.
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "gis-site.h"
+
+#define GIS_SITE_SEARCH_ENTRY_PROP_SITES_AVAILABLE "sites-available"
+#define GIS_SITE_SEARCH_ENTRY_PROP_SITE "site"
+
+#define GIS_TYPE_SITE_SEARCH_ENTRY (gis_site_search_entry_get_type())
+G_DECLARE_DERIVABLE_TYPE (GisSiteSearchEntry, gis_site_search_entry, GIS, SITE_SEARCH_ENTRY, GtkSearchEntry)
+
+struct _GisSiteSearchEntryClass {
+ GtkSearchEntryClass parent_class;
+};
+
+GtkWidget* gis_site_search_entry_new (GPtrArray *sites);
+
+void gis_site_search_entry_set_site (GisSiteSearchEntry *entry,
+ GisSite *site);
+
+GisSite* gis_site_search_entry_get_site (GisSiteSearchEntry *entry);
diff --git a/gnome-initial-setup/pages/site/gis-site.c b/gnome-initial-setup/pages/site/gis-site.c
new file mode 100644
index 00000000..556bb0fc
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site.c
@@ -0,0 +1,289 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Travis Reitter <travis reitter endlessm com>
+ */
+
+#include "gis-site.h"
+
+/**
+ * GisSite:
+ *
+ * All the details about a given deployment site needed for the purposes of
+ * grouping computers at a given site for the purposes of metrics.
+ *
+ * #GisSite is an opaque data structure and can only be accessed
+ * using the following functions.
+ */
+
+struct _GisSite
+{
+ GObject parent;
+};
+
+struct _GisSitePrivate
+{
+ gchar *id;
+
+ /* eg, name of a school */
+ gchar *facility;
+
+ /* eg, "123 Main Street" */
+ gchar *street;
+
+ /* city, township, or equivalent */
+ gchar *locality;
+
+ /* municipal division between a county and country such as a state in the
+ * United States or department in Colombia
+ */
+ gchar *region;
+
+ gchar *country;
+};
+typedef struct _GisSitePrivate GisSitePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GisSite, gis_site, G_TYPE_OBJECT)
+
+static void
+gis_site_finalize (GObject *object)
+{
+ GisSite *site = GIS_SITE (object);
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+ g_free (priv->id);
+ g_free (priv->facility);
+ g_free (priv->street);
+ g_free (priv->locality);
+ g_free (priv->region);
+ g_free (priv->country);
+
+ G_OBJECT_CLASS (gis_site_parent_class)->finalize (object);
+}
+
+static void
+gis_site_init (GisSite *self)
+{
+}
+
+static void
+gis_site_class_init (GisSiteClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gis_site_finalize;
+}
+
+/**
+ * gis_site_new:
+ * @id: (nullable): the ID, if any, the site's admins use for this site (such as
+ * a school ID).
+ * @facility: (nullable): name of the site (eg, the name of a school)
+ * @street: (nullable): street address (eg, "123 Main Street")
+ * @locality: (nullable): city, township, or equivalent
+ * @region: (nullable): municipal division between a county and country such as
+ * a state in the United States or department in Colombia
+ * @country: (nullable): country
+ *
+ * Creates a new #GisSite instance.
+ *
+ * All fields are nullable but at least the @id, @facility, or @locality should
+ * be set for this #GisSite to be useful.
+ *
+ * Returns: a new #GisSite instance
+ **/
+GisSite *
+gis_site_new (const gchar *id,
+ const gchar *facility,
+ const gchar *street,
+ const gchar *locality,
+ const gchar *region,
+ const gchar *country)
+{
+ GisSite *self;
+ GisSitePrivate *priv;
+
+ self = g_object_new (GIS_TYPE_SITE, NULL);
+
+ priv = gis_site_get_instance_private (self);
+ priv->id = g_strdup (id);
+ priv->facility = g_strdup (facility);
+ priv->street = g_strdup (street);
+ priv->locality = g_strdup (locality);
+ priv->region = g_strdup (region);
+ priv->country = g_strdup (country);
+
+ return self;
+}
+
+/**
+ * gis_site_get_id:
+ * @site: a #GisSite
+ *
+ * Gets the unique identifier for @site.
+ *
+ * Returns: (transfer none): the identifier or %NULL
+ **/
+const gchar *
+gis_site_get_id (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->id;
+}
+
+/**
+ * gis_site_get_facility:
+ * @site: a #GisSite
+ *
+ * Gets the name of the facility (eg, a school) for @site.
+ *
+ * Returns: (transfer none): the facility or %NULL
+ **/
+const gchar *
+gis_site_get_facility (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->facility;
+}
+
+/**
+ * gis_site_get_street:
+ * @site: a #GisSite
+ *
+ * Gets the street address (eg, 123 Main Street) for @site.
+ *
+ * Returns: (transfer none): the street address or %NULL
+ **/
+const gchar *
+gis_site_get_street (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->street;
+}
+
+/**
+ * gis_site_get_locality:
+ * @site: a #GisSite
+ *
+ * Gets the locality (city or equivalent) for @site.
+ *
+ * Returns: (transfer none): the locality or %NULL
+ **/
+const gchar *
+gis_site_get_locality (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->locality;
+}
+
+/**
+ * gis_site_get_region:
+ * @site: a #GisSite
+ *
+ * Gets the region (equivalent to a state in the US or department in Colombia)
+ * for @site.
+ *
+ * Returns: (transfer none): the region or %NULL
+ **/
+const gchar *
+gis_site_get_region (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->region;
+}
+
+/**
+ * gis_site_get_country:
+ * @site: a #GisSite
+ *
+ * Gets the country for @site.
+ *
+ * Returns: (transfer none): the country or %NULL
+ **/
+const gchar *
+gis_site_get_country (GisSite *site)
+{
+ GisSitePrivate *priv = gis_site_get_instance_private (site);
+
+ g_return_val_if_fail (GIS_IS_SITE (site), NULL);
+
+ return priv->country;
+}
+
+static void
+string_append_safe (GString *string,
+ const gchar *field)
+{
+ if (field && field[0] != '\0')
+ g_string_append_printf (string, "%s%s", string->len > 0 ? ", " : "", field);
+}
+
+/**
+ * Get a string for this #GisSite that's suitable to display to a user.
+ *
+ * NOTE: this assumes the values provided as arguments to this #GisSite are
+ * already translated into the relevant local language (and that it's a single
+ * language).
+ *
+ * @site: a #GisSite
+ *
+ * Returns: (transfer full): a display name suitable for display (which must be
+ * freed) or %NULL if none could be created.
+ */
+gchar*
+gis_site_get_display_name (GisSite *site)
+{
+ gchar *retval;
+ const gchar *id = gis_site_get_id (site);
+ const gchar *facility = gis_site_get_facility (site);
+ const gchar *street = gis_site_get_street (site);
+ const gchar *locality = gis_site_get_locality (site);
+ const gchar *region = gis_site_get_region (site);
+ const gchar *country = gis_site_get_country (site);
+
+ GString *string = g_string_new ("");
+
+ string_append_safe (string, id);
+ string_append_safe (string, facility);
+ string_append_safe (string, street);
+ string_append_safe (string, locality);
+ string_append_safe (string, region);
+ string_append_safe (string, country);
+
+ retval = g_string_free (string, FALSE);
+
+ if (g_str_equal (retval, ""))
+ g_clear_pointer (&retval, g_free);
+
+ return retval;
+}
+
diff --git a/gnome-initial-setup/pages/site/gis-site.h b/gnome-initial-setup/pages/site/gis-site.h
new file mode 100644
index 00000000..721d4ab4
--- /dev/null
+++ b/gnome-initial-setup/pages/site/gis-site.h
@@ -0,0 +1,47 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2018 Endless Mobile, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Written by:
+ * Travis Reitter <travis reitter endlessm com>
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+#define GIS_TYPE_SITE (gis_site_get_type())
+G_DECLARE_FINAL_TYPE (GisSite, gis_site, GIS, SITE, GObject)
+
+struct _GisSiteClass
+{
+ GObjectClass parent_class;
+};
+
+GisSite * gis_site_new (const gchar *id,
+ const gchar *facility,
+ const gchar *street,
+ const gchar *locality,
+ const gchar *region,
+ const gchar *country);
+const gchar * gis_site_get_id (GisSite *site);
+const gchar * gis_site_get_facility (GisSite *site);
+const gchar * gis_site_get_street (GisSite *site);
+const gchar * gis_site_get_locality (GisSite *site);
+const gchar * gis_site_get_region (GisSite *site);
+const gchar * gis_site_get_country (GisSite *site);
+gchar * gis_site_get_display_name (GisSite *site);
diff --git a/gnome-initial-setup/pages/site/meson.build b/gnome-initial-setup/pages/site/meson.build
new file mode 100644
index 00000000..395604b2
--- /dev/null
+++ b/gnome-initial-setup/pages/site/meson.build
@@ -0,0 +1,20 @@
+sources += gnome.compile_resources(
+ 'site-resources',
+ files('site.gresource.xml'),
+ c_name: 'site'
+)
+
+sources += files(
+ 'gis-site-page.c',
+ 'gis-site-page.h',
+ 'gis-site.c',
+ 'gis-site.h',
+ 'gis-site-search-entry.c',
+ 'gis-site-search-entry.h',
+)
+
+install_data(
+ 'eos-write-location',
+ install_dir: libexec_dir,
+ install_mode: 'rwxr-xr-x'
+)
diff --git a/gnome-initial-setup/pages/site/site.gresource.xml
b/gnome-initial-setup/pages/site/site.gresource.xml
new file mode 100644
index 00000000..960600a8
--- /dev/null
+++ b/gnome-initial-setup/pages/site/site.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/initial-setup">
+ <file preprocess="xml-stripblanks" alias="gis-site-page.ui">gis-site-page.ui</file>
+ </gresource>
+</gresources>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e96f9b40..c9caf787 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -43,6 +43,8 @@ gnome-initial-setup/pages/password/gis-password-page.ui
gnome-initial-setup/pages/password/pw-utils.c
gnome-initial-setup/pages/privacy/gis-privacy-page.c
gnome-initial-setup/pages/privacy/gis-privacy-page.ui
+gnome-initial-setup/pages/site/gis-site-page.c
+gnome-initial-setup/pages/site/gis-site-page.ui
gnome-initial-setup/pages/summary/gis-summary-page.c
gnome-initial-setup/pages/summary/gis-summary-page.ui
gnome-initial-setup/pages/timezone/gis-timezone-page.c
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]