summaryrefslogtreecommitdiffstats
path: root/panels/common
diff options
context:
space:
mode:
Diffstat (limited to 'panels/common')
-rw-r--r--panels/common/cc-common-language.c304
-rw-r--r--panels/common/cc-common-language.h58
-rw-r--r--panels/common/cc-hostname-entry.c265
-rw-r--r--panels/common/cc-hostname-entry.h33
-rw-r--r--panels/common/cc-language-chooser.c343
-rw-r--r--panels/common/cc-language-chooser.h37
-rw-r--r--panels/common/cc-language-chooser.ui99
-rw-r--r--panels/common/cc-language-row.c184
-rw-r--r--panels/common/cc-language-row.h46
-rw-r--r--panels/common/cc-language-row.ui41
-rw-r--r--panels/common/cc-list-row.c283
-rw-r--r--panels/common/cc-list-row.h47
-rw-r--r--panels/common/cc-list-row.ui37
-rw-r--r--panels/common/cc-permission-infobar.c112
-rw-r--r--panels/common/cc-permission-infobar.h37
-rw-r--r--panels/common/cc-permission-infobar.ui71
-rw-r--r--panels/common/cc-time-editor.c372
-rw-r--r--panels/common/cc-time-editor.h45
-rw-r--r--panels/common/cc-time-editor.ui168
-rw-r--r--panels/common/cc-time-entry.c660
-rw-r--r--panels/common/cc-time-entry.h47
-rw-r--r--panels/common/cc-util.c210
-rw-r--r--panels/common/cc-util.h27
-rw-r--r--panels/common/common.gresource.xml10
-rw-r--r--panels/common/gnome-control-center.rules.in13
-rw-r--r--panels/common/gnome-settings-bus.h14
-rw-r--r--panels/common/gsd-device-manager.c688
-rw-r--r--panels/common/gsd-device-manager.h86
-rw-r--r--panels/common/gsd-input-helper.c108
-rw-r--r--panels/common/gsd-input-helper.h31
-rw-r--r--panels/common/hostname-helper.c204
-rw-r--r--panels/common/hostname-helper.h23
-rw-r--r--panels/common/meson.build126
33 files changed, 4829 insertions, 0 deletions
diff --git a/panels/common/cc-common-language.c b/panels/common/cc-common-language.c
new file mode 100644
index 0000000..9357c3e
--- /dev/null
+++ b/panels/common/cc-common-language.c
@@ -0,0 +1,304 @@
+/* -*- 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"
+#include "shell/cc-object-storage.h"
+
+static char *get_lang_for_user_object_path (const char *path);
+
+static gboolean
+iter_for_language (GtkTreeModel *model,
+ const gchar *lang,
+ GtkTreeIter *iter,
+ gboolean region)
+{
+ g_autofree gchar *name = NULL;
+
+ g_assert (gtk_tree_model_get_iter_first (model, iter));
+ do {
+ g_autofree gchar *l = NULL;
+ gtk_tree_model_get (model, iter, LOCALE_COL, &l, -1);
+ if (g_strcmp0 (l, lang) == 0)
+ return TRUE;
+ } while (gtk_tree_model_iter_next (model, iter));
+
+ name = gnome_normalize_locale (lang);
+ if (name != NULL) {
+ g_autofree gchar *language = NULL;
+
+ if (region) {
+ language = gnome_get_country_from_locale (name, NULL);
+ }
+ else {
+ language = gnome_get_language_from_locale (name, NULL);
+ }
+
+ gtk_list_store_insert_with_values (GTK_LIST_STORE (model),
+ iter,
+ -1,
+ LOCALE_COL, name,
+ DISPLAY_LOCALE_COL, language,
+ -1);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+cc_common_language_get_iter_for_language (GtkTreeModel *model,
+ const gchar *lang,
+ GtkTreeIter *iter)
+{
+ return iter_for_language (model, lang, iter, FALSE);
+}
+
+gboolean
+cc_common_language_has_font (const gchar *locale)
+{
+ const FcCharSet *charset;
+ FcPattern *pattern;
+ FcObjectSet *object_set;
+ FcFontSet *font_set;
+ g_autofree gchar *language_code = NULL;
+ 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);
+
+ return is_displayable;
+}
+
+gchar *
+cc_common_language_get_current_language (void)
+{
+ gchar *language;
+ g_autofree gchar *path = NULL;
+ const gchar *locale;
+
+ path = g_strdup_printf ("/org/freedesktop/Accounts/User%d", getuid ());
+ language = get_lang_for_user_object_path (path);
+ if (language != NULL && *language != '\0')
+ return language;
+
+ locale = (const gchar *) setlocale (LC_MESSAGES, NULL);
+ if (locale)
+ language = gnome_normalize_locale (locale);
+ else
+ language = NULL;
+
+ return language;
+}
+
+static char *
+get_lang_for_user_object_path (const char *path)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GDBusProxy) user = NULL;
+ g_autoptr(GVariant) props = NULL;
+ char *lang;
+
+ user = cc_object_storage_create_dbus_proxy_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "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);
+ return NULL;
+ }
+
+ props = g_dbus_proxy_get_cached_property (user, "Language");
+ if (props == NULL)
+ return NULL;
+ lang = g_variant_dup_string (props, NULL);
+
+ return lang;
+}
+
+/*
+ * 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)
+{
+ g_autofree gchar *label_own_lang = NULL;
+ g_autofree gchar *label_current_lang = NULL;
+ g_autofree gchar *label_untranslated = NULL;
+
+ label_own_lang = gnome_get_language_from_locale (lang, lang);
+ label_current_lang = gnome_get_language_from_locale (lang, NULL);
+ label_untranslated = gnome_get_language_from_locale (lang, "C");
+
+ /* We don't have a translation for the label in
+ * its own language? */
+ if (g_strcmp0 (label_own_lang, label_untranslated) == 0) {
+ if (g_strcmp0 (label_current_lang, label_untranslated) == 0)
+ g_hash_table_insert (ht, g_strdup (lang), g_strdup (label_untranslated));
+ else
+ g_hash_table_insert (ht, g_strdup (lang), g_strdup (label_current_lang));
+ } else {
+ g_hash_table_insert (ht, g_strdup (lang), g_strdup (label_own_lang));
+ }
+}
+
+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");
+ insert_language (ht, "en_GB.UTF-8");
+ 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");
+
+ return ht;
+}
+
+static void
+foreach_user_lang_cb (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ GtkListStore *store = (GtkListStore *) user_data;
+ const char *locale = (const char *) key;
+ const char *display_locale = (const char *) value;
+ GtkTreeIter iter;
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ LOCALE_COL, locale,
+ DISPLAY_LOCALE_COL, display_locale,
+ -1);
+}
+
+void
+cc_common_language_add_user_languages (GtkTreeModel *model)
+{
+ g_autofree gchar *name = NULL;
+ GtkTreeIter iter;
+ GtkListStore *store = GTK_LIST_STORE (model);
+ GHashTable *user_langs;
+ const char *display;
+
+ gtk_list_store_clear (store);
+
+ user_langs = cc_common_language_get_initial_languages ();
+
+ /* Add the current locale first */
+ name = cc_common_language_get_current_language ();
+ display = g_hash_table_lookup (user_langs, name);
+ if (!display) {
+ g_autofree gchar *language = NULL;
+ g_autofree gchar *country = NULL;
+ g_autofree gchar *codeset = NULL;
+
+ gnome_parse_locale (name, &language, &country, &codeset, NULL);
+
+ if (!codeset || !g_str_equal (codeset, "UTF-8"))
+ g_warning ("Current user locale codeset isn't UTF-8");
+
+ g_free (name);
+ name = g_strdup_printf ("%s_%s.UTF-8", language, country);
+
+ insert_language (user_langs, name);
+ display = g_hash_table_lookup (user_langs, name);
+ }
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, LOCALE_COL, name, DISPLAY_LOCALE_COL, display, -1);
+ g_hash_table_remove (user_langs, name);
+
+ /* The rest of the languages */
+ g_hash_table_foreach (user_langs, (GHFunc) foreach_user_lang_cb, store);
+
+ /* And now the "Other…" selection */
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter, LOCALE_COL, NULL, DISPLAY_LOCALE_COL, _("Other…"), -1);
+
+ g_hash_table_destroy (user_langs);
+}
+
diff --git a/panels/common/cc-common-language.h b/panels/common/cc-common-language.h
new file mode 100644
index 0000000..1f578b7
--- /dev/null
+++ b/panels/common/cc-common-language.h
@@ -0,0 +1,58 @@
+/* -*- 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>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+enum {
+ LOCALE_COL,
+ DISPLAY_LOCALE_COL,
+ SEPARATOR_COL,
+ USER_LANGUAGE,
+ NUM_COLS
+};
+
+gboolean cc_common_language_get_iter_for_language (GtkTreeModel *model,
+ const gchar *lang,
+ GtkTreeIter *iter);
+gboolean cc_common_language_get_iter_for_region (GtkTreeModel *model,
+ const gchar *lang,
+ GtkTreeIter *iter);
+guint cc_common_language_add_available_languages (GtkListStore *store,
+ gboolean regions,
+ GHashTable *user_langs);
+gboolean cc_common_language_has_font (const gchar *locale);
+gchar *cc_common_language_get_current_language (void);
+
+GHashTable *cc_common_language_get_initial_languages (void);
+GHashTable *cc_common_language_get_user_languages (void);
+GHashTable *cc_common_language_get_initial_regions (const gchar *lang);
+
+void cc_common_language_setup_list (GtkWidget *treeview,
+ GHashTable *users,
+ GHashTable *initial);
+void cc_common_language_select_current_language (GtkTreeView *treeview);
+
+void cc_common_language_add_user_languages (GtkTreeModel *model);
+
+G_END_DECLS
diff --git a/panels/common/cc-hostname-entry.c b/panels/common/cc-hostname-entry.c
new file mode 100644
index 0000000..8a79b03
--- /dev/null
+++ b/panels/common/cc-hostname-entry.c
@@ -0,0 +1,265 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 Intel, Inc
+ * Copyright (C) 2011,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/>.
+ *
+ */
+
+
+#include "cc-common-resources.h"
+#include "cc-hostname-entry.h"
+#include "hostname-helper.h"
+
+#include <polkit/polkit.h>
+
+struct _CcHostnameEntry
+{
+ GtkEntry parent;
+
+ GDBusProxy *hostnamed_proxy;
+ guint set_hostname_timeout_source_id;
+};
+
+G_DEFINE_TYPE (CcHostnameEntry, cc_hostname_entry, GTK_TYPE_ENTRY)
+
+#define SET_HOSTNAME_TIMEOUT 1
+
+static void
+cc_hostname_entry_set_hostname (CcHostnameEntry *self)
+{
+ g_autofree gchar *hostname = NULL;
+ g_autoptr(GVariant) pretty_result = NULL;
+ g_autoptr(GVariant) static_result = NULL;
+ g_autoptr(GError) pretty_error = NULL;
+ g_autoptr(GError) static_error = NULL;
+ const gchar *text;
+
+ text = gtk_editable_get_text (GTK_EDITABLE (self));
+
+ g_debug ("Setting PrettyHostname to '%s'", text);
+ pretty_result = g_dbus_proxy_call_sync (self->hostnamed_proxy,
+ "SetPrettyHostname",
+ g_variant_new ("(sb)", text, FALSE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, &pretty_error);
+ if (pretty_result == NULL)
+ g_warning ("Could not set PrettyHostname: %s", pretty_error->message);
+
+ /* Set the static hostname */
+ hostname = pretty_hostname_to_static (text, FALSE);
+ g_assert (hostname);
+
+ g_debug ("Setting StaticHostname to '%s'", hostname);
+ static_result = g_dbus_proxy_call_sync (self->hostnamed_proxy,
+ "SetStaticHostname",
+ g_variant_new ("(sb)", hostname, FALSE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, &static_error);
+ if (static_result == NULL)
+ g_warning ("Could not set StaticHostname: %s", static_error->message);
+}
+
+static char *
+get_hostname_property (CcHostnameEntry *self,
+ const char *property)
+{
+ g_autoptr(GVariant) variant = NULL;
+
+ if (!self->hostnamed_proxy)
+ return g_strdup ("");
+
+ variant = g_dbus_proxy_get_cached_property (self->hostnamed_proxy,
+ property);
+ if (!variant)
+ {
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) inner = NULL;
+
+ /* Work around systemd-hostname not sending us back
+ * the property value when changing values */
+ variant = g_dbus_proxy_call_sync (self->hostnamed_proxy,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new ("(ss)", "org.freedesktop.hostname1", property),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ if (variant == NULL)
+ {
+ g_warning ("Failed to get property '%s': %s", property, error->message);
+ return NULL;
+ }
+
+ g_variant_get (variant, "(v)", &inner);
+ return g_variant_dup_string (inner, NULL);
+ }
+ else
+ {
+ return g_variant_dup_string (variant, NULL);
+ }
+}
+
+static char *
+cc_hostname_entry_get_display_hostname (CcHostnameEntry *self)
+{
+ g_autofree gchar *str = NULL;
+
+ str = get_hostname_property (self, "PrettyHostname");
+
+ /* Empty strings means that we need to fallback */
+ if (str != NULL &&
+ *str == '\0')
+ return get_hostname_property (self, "Hostname");
+
+ return g_steal_pointer (&str);
+}
+
+static gboolean
+set_hostname_timeout (CcHostnameEntry *self)
+{
+ self->set_hostname_timeout_source_id = 0;
+
+ cc_hostname_entry_set_hostname (self);
+
+ return FALSE;
+}
+
+static void
+remove_hostname_timeout (CcHostnameEntry *self)
+{
+ if (self->set_hostname_timeout_source_id)
+ g_source_remove (self->set_hostname_timeout_source_id);
+
+ self->set_hostname_timeout_source_id = 0;
+}
+
+static void
+reset_hostname_timeout (CcHostnameEntry *self)
+{
+ remove_hostname_timeout (self);
+
+ self->set_hostname_timeout_source_id = g_timeout_add_seconds (SET_HOSTNAME_TIMEOUT,
+ (GSourceFunc) set_hostname_timeout,
+ self);
+}
+
+static void
+text_changed_cb (CcHostnameEntry *entry)
+{
+ reset_hostname_timeout (entry);
+}
+
+static void
+cc_hostname_entry_dispose (GObject *object)
+{
+ CcHostnameEntry *self = CC_HOSTNAME_ENTRY (object);
+
+ if (self->set_hostname_timeout_source_id)
+ {
+ remove_hostname_timeout (self);
+ set_hostname_timeout (self);
+ }
+
+ g_clear_object (&self->hostnamed_proxy);
+
+ G_OBJECT_CLASS (cc_hostname_entry_parent_class)->dispose (object);
+}
+
+static void
+cc_hostname_entry_constructed (GObject *object)
+{
+ CcHostnameEntry *self = CC_HOSTNAME_ENTRY (object);
+ GPermission *permission;
+ g_autoptr(GError) error = NULL;
+ g_autofree gchar *str = NULL;
+
+ permission = polkit_permission_new_sync ("org.freedesktop.hostname1.set-static-hostname",
+ NULL, NULL, NULL);
+
+ /* Is hostnamed installed? */
+ if (permission == NULL)
+ {
+ g_debug ("Will not show hostname, hostnamed not installed");
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+
+ return;
+ }
+
+ if (g_permission_get_allowed (permission))
+ gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE);
+ else
+ {
+ g_debug ("Not allowed to change the hostname");
+ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self),
+ g_permission_get_allowed (permission));
+
+ self->hostnamed_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.hostname1",
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ NULL,
+ &error);
+
+ /* This could only happen if the policy file was installed
+ * but not hostnamed, which points to a system bug */
+ if (self->hostnamed_proxy == NULL)
+ {
+ g_debug ("Couldn't get hostnamed to start, bailing: %s", error->message);
+ return;
+ }
+
+ str = cc_hostname_entry_get_display_hostname (CC_HOSTNAME_ENTRY (self));
+
+ if (str != NULL)
+ gtk_editable_set_text (GTK_EDITABLE (self), str);
+ else
+ gtk_editable_set_text (GTK_EDITABLE (self), "");
+
+ g_signal_connect (self, "changed", G_CALLBACK (text_changed_cb), NULL);
+}
+
+static void
+cc_hostname_entry_class_init (CcHostnameEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = cc_hostname_entry_constructed;
+ object_class->dispose = cc_hostname_entry_dispose;
+}
+
+static void
+cc_hostname_entry_init (CcHostnameEntry *self)
+{
+ g_resources_register (cc_common_get_resource ());
+}
+
+CcHostnameEntry *
+cc_hostname_entry_new (void)
+{
+ return g_object_new (CC_TYPE_HOSTNAME_ENTRY, NULL);
+}
+
+gchar*
+cc_hostname_entry_get_hostname (CcHostnameEntry *entry)
+{
+ return get_hostname_property (entry, "Hostname");
+}
diff --git a/panels/common/cc-hostname-entry.h b/panels/common/cc-hostname-entry.h
new file mode 100644
index 0000000..a481779
--- /dev/null
+++ b/panels/common/cc-hostname-entry.h
@@ -0,0 +1,33 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2013 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/>.
+ *
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_HOSTNAME_ENTRY (cc_hostname_entry_get_type())
+
+G_DECLARE_FINAL_TYPE (CcHostnameEntry, cc_hostname_entry, CC, HOSTNAME_ENTRY, GtkEntry)
+
+CcHostnameEntry *cc_hostname_entry_new (void);
+gchar* cc_hostname_entry_get_hostname (CcHostnameEntry *entry);
+
+G_END_DECLS
diff --git a/panels/common/cc-language-chooser.c b/panels/common/cc-language-chooser.c
new file mode 100644
index 0000000..1aa2ec4
--- /dev/null
+++ b/panels/common/cc-language-chooser.c
@@ -0,0 +1,343 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * 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: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#define _GNU_SOURCE
+#include <config.h>
+#include "cc-language-chooser.h"
+#include "cc-language-row.h"
+#include "cc-common-resources.h"
+
+#include <locale.h>
+#include <string.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "cc-common-language.h"
+#include "cc-util.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+struct _CcLanguageChooser {
+ GtkDialog parent_instance;
+
+ GtkSearchEntry *language_filter_entry;
+ GtkListBox *language_listbox;
+ GtkListBoxRow *more_row;
+ GtkSearchBar *search_bar;
+ GtkButton *select_button;
+
+ gboolean showing_extra;
+ gchar *language;
+ gchar **filter_words;
+};
+
+G_DEFINE_TYPE (CcLanguageChooser, cc_language_chooser, GTK_TYPE_DIALOG)
+
+static void
+add_all_languages (CcLanguageChooser *self)
+{
+ g_auto(GStrv) locale_ids = NULL;
+ g_autoptr(GHashTable) initial = NULL;
+
+ locale_ids = gnome_get_all_locales ();
+ initial = cc_common_language_get_initial_languages ();
+ for (int i = 0; locale_ids[i] != NULL; i++) {
+ CcLanguageRow *row;
+ gboolean is_initial;
+
+ if (!cc_common_language_has_font (locale_ids[i]))
+ continue;
+
+ row = cc_language_row_new (locale_ids[i]);
+ gtk_widget_show (GTK_WIDGET (row));
+ is_initial = (g_hash_table_lookup (initial, locale_ids[i]) != NULL);
+ cc_language_row_set_is_extra (row, !is_initial);
+ gtk_list_box_prepend (self->language_listbox, GTK_WIDGET (row));
+ }
+}
+
+static gboolean
+match_all (gchar **words,
+ const gchar *str)
+{
+ gchar **w;
+
+ if (str == NULL)
+ return FALSE;
+
+ for (w = words; *w; ++w)
+ if (!strstr (str, *w))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+language_visible (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcLanguageChooser *self = user_data;
+ g_autofree gchar *language = NULL;
+ g_autofree gchar *country = NULL;
+ g_autofree gchar *language_local = NULL;
+ g_autofree gchar *country_local = NULL;
+ gboolean visible;
+
+ if (row == self->more_row)
+ return !self->showing_extra;
+
+ if (!CC_IS_LANGUAGE_ROW (row))
+ return TRUE;
+
+ if (!self->showing_extra && cc_language_row_get_is_extra (CC_LANGUAGE_ROW (row)))
+ return FALSE;
+
+ if (!self->filter_words)
+ return TRUE;
+
+ language =
+ cc_util_normalize_casefold_and_unaccent (cc_language_row_get_language (CC_LANGUAGE_ROW (row)));
+ visible = match_all (self->filter_words, language);
+ if (visible)
+ return TRUE;
+
+ country =
+ cc_util_normalize_casefold_and_unaccent (cc_language_row_get_country (CC_LANGUAGE_ROW (row)));
+ visible = match_all (self->filter_words, country);
+ if (visible)
+ return TRUE;
+
+ language_local =
+ cc_util_normalize_casefold_and_unaccent (cc_language_row_get_language_local (CC_LANGUAGE_ROW (row)));
+ visible = match_all (self->filter_words, language_local);
+ if (visible)
+ return TRUE;
+
+ country_local =
+ cc_util_normalize_casefold_and_unaccent (cc_language_row_get_country_local (CC_LANGUAGE_ROW (row)));
+ return match_all (self->filter_words, country_local);
+}
+
+static gint
+sort_languages (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ int d;
+
+ if (!CC_IS_LANGUAGE_ROW (a))
+ return 1;
+ if (!CC_IS_LANGUAGE_ROW (b))
+ return -1;
+
+ d = g_strcmp0 (cc_language_row_get_language (CC_LANGUAGE_ROW (a)), cc_language_row_get_language (CC_LANGUAGE_ROW (b)));
+ if (d != 0)
+ return d;
+
+ return g_strcmp0 (cc_language_row_get_country (CC_LANGUAGE_ROW (a)), cc_language_row_get_country (CC_LANGUAGE_ROW (b)));
+}
+
+static void
+language_filter_entry_search_changed_cb (CcLanguageChooser *self)
+{
+ g_autofree gchar *filter_contents = NULL;
+
+ g_clear_pointer (&self->filter_words, g_strfreev);
+
+ filter_contents =
+ cc_util_normalize_casefold_and_unaccent (gtk_editable_get_text (GTK_EDITABLE (self->language_filter_entry)));
+ if (!filter_contents) {
+ gtk_list_box_invalidate_filter (self->language_listbox);
+ return;
+ }
+ self->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
+ gtk_list_box_invalidate_filter (self->language_listbox);
+}
+
+static void
+show_more (CcLanguageChooser *self, gboolean visible)
+{
+ gint width, height;
+
+ gtk_window_get_default_size (GTK_WINDOW (self), &width, &height);
+ gtk_widget_set_size_request (GTK_WIDGET (self), width, height);
+
+ gtk_search_bar_set_search_mode (self->search_bar, visible);
+ gtk_widget_grab_focus (visible ? GTK_WIDGET (self->language_filter_entry) : GTK_WIDGET (self->language_listbox));
+
+ self->showing_extra = visible;
+
+ gtk_list_box_invalidate_filter (self->language_listbox);
+}
+
+static void
+set_locale_id (CcLanguageChooser *self,
+ const gchar *locale_id)
+{
+ GtkWidget *child;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->select_button), FALSE);
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->language_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcLanguageRow *row;
+
+ if (!CC_IS_LANGUAGE_ROW (child))
+ continue;
+
+ row = CC_LANGUAGE_ROW (child);
+ if (g_strcmp0 (locale_id, cc_language_row_get_locale_id (row)) == 0) {
+ cc_language_row_set_checked (row, TRUE);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->select_button), TRUE);
+
+ /* make sure the selected language is shown */
+ if (!self->showing_extra && cc_language_row_get_is_extra (row)) {
+ cc_language_row_set_is_extra (row, FALSE);
+ gtk_list_box_invalidate_filter (self->language_listbox);
+ }
+ } else {
+ cc_language_row_set_checked (row, FALSE);
+ }
+ }
+
+ g_free (self->language);
+ self->language = g_strdup (locale_id);
+}
+
+static void
+language_listbox_row_activated_cb (CcLanguageChooser *self, GtkListBoxRow *row)
+{
+ const gchar *new_locale_id;
+
+ if (row == self->more_row) {
+ show_more (self, TRUE);
+ return;
+ }
+
+ if (!CC_IS_LANGUAGE_ROW (row))
+ return;
+
+ new_locale_id = cc_language_row_get_locale_id (CC_LANGUAGE_ROW (row));
+ if (g_strcmp0 (new_locale_id, self->language) == 0) {
+ gtk_dialog_response (GTK_DIALOG (self),
+ gtk_dialog_get_response_for_widget (GTK_DIALOG (self),
+ GTK_WIDGET (self->select_button)));
+ } else {
+ set_locale_id (self, new_locale_id);
+ }
+}
+
+static void
+activate_default_cb (CcLanguageChooser *self)
+{
+ GtkWidget *focus;
+
+ focus = gtk_window_get_focus (GTK_WINDOW (self));
+ if (!focus || !CC_IS_LANGUAGE_ROW (focus))
+ return;
+
+ if (g_strcmp0 (cc_language_row_get_locale_id (CC_LANGUAGE_ROW (focus)), self->language) == 0)
+ return;
+
+ g_signal_stop_emission_by_name (GTK_WINDOW (self), "activate-default");
+ gtk_widget_activate (focus);
+}
+
+void
+cc_language_chooser_init (CcLanguageChooser *self)
+{
+ g_resources_register (cc_common_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_sort_func (self->language_listbox,
+ sort_languages, self, NULL);
+ gtk_list_box_set_filter_func (self->language_listbox,
+ language_visible, self, NULL);
+ add_all_languages (self);
+
+ gtk_list_box_invalidate_filter (self->language_listbox);
+}
+
+static void
+cc_language_chooser_dispose (GObject *object)
+{
+ CcLanguageChooser *self = CC_LANGUAGE_CHOOSER (object);
+
+ g_clear_pointer (&self->filter_words, g_strfreev);
+ g_clear_pointer (&self->language, g_free);
+
+ G_OBJECT_CLASS (cc_language_chooser_parent_class)->dispose (object);
+}
+
+void
+cc_language_chooser_class_init (CcLanguageChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_language_chooser_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/common/cc-language-chooser.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, language_filter_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, language_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, more_row);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, search_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, select_button);
+
+ gtk_widget_class_bind_template_callback (widget_class, activate_default_cb);
+ gtk_widget_class_bind_template_callback (widget_class, language_filter_entry_search_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, language_listbox_row_activated_cb);
+}
+
+CcLanguageChooser *
+cc_language_chooser_new (void)
+{
+ return CC_LANGUAGE_CHOOSER (g_object_new (CC_TYPE_LANGUAGE_CHOOSER,
+ "use-header-bar", 1,
+ NULL));
+}
+
+void
+cc_language_chooser_clear_filter (CcLanguageChooser *self)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_CHOOSER (self));
+ gtk_editable_set_text (GTK_EDITABLE (self->language_filter_entry), "");
+ show_more (self, FALSE);
+}
+
+const gchar *
+cc_language_chooser_get_language (CcLanguageChooser *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_CHOOSER (self), NULL);
+ return self->language;
+}
+
+void
+cc_language_chooser_set_language (CcLanguageChooser *self,
+ const gchar *language)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_CHOOSER (self));
+ set_locale_id (self, language);
+}
diff --git a/panels/common/cc-language-chooser.h b/panels/common/cc-language-chooser.h
new file mode 100644
index 0000000..8e050ee
--- /dev/null
+++ b/panels/common/cc-language-chooser.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * 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: Matthias Clasen <mclasen@redhat.com>
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_LANGUAGE_CHOOSER (cc_language_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (CcLanguageChooser, cc_language_chooser, CC, LANGUAGE_CHOOSER, GtkDialog)
+
+CcLanguageChooser *cc_language_chooser_new (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);
+
+G_END_DECLS
diff --git a/panels/common/cc-language-chooser.ui b/panels/common/cc-language-chooser.ui
new file mode 100644
index 0000000..4efc9a8
--- /dev/null
+++ b/panels/common/cc-language-chooser.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcLanguageChooser" parent="GtkDialog">
+ <property name="title" translatable="yes">Select Language</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="default_width">400</property>
+ <property name="default_height">475</property>
+ <signal name="activate-default" handler="activate_default_cb"/>
+ <child type="action">
+ <object class="GtkButton" id="select_button">
+ <property name="label" translatable="yes">_Select</property>
+ <property name="sensitive">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">0</property>
+ <child>
+ <object class="GtkSearchBar" id="search_bar">
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="language_filter_entry">
+ <property name="width_chars">30</property>
+ <signal name="search-changed" handler="language_filter_entry_search_changed_cb" object="CcLanguageChooser" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hscrollbar-policy">never</property>
+ <property name="vscrollbar-policy">automatic</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="min-content-height">200</property>
+ <child>
+ <object class="GtkListBox" id="language_listbox">
+ <property name="can-focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="selection-mode">none</property>
+ <property name="show-separators">True</property>
+ <signal name="row-activated" handler="language_listbox_row_activated_cb" object="CcLanguageChooser" swapped="yes"/>
+ <child type="placeholder">
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">No languages found</property>
+ <property name="sensitive">False</property>
+ </object>
+ </child>
+
+ <!-- "More" row -->
+ <child>
+ <object class="GtkListBoxRow" id="more_row">
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">10</property>
+ <property name="tooltip_markup" translatable="yes">More…</property>
+ <child>
+ <object class="GtkImage">
+ <property name="hexpand">True</property>
+ <property name="halign">center</property>
+ <property name="icon-name">view-more-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5" default="true">select_button</action-widget>
+ <action-widget response="-6">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/panels/common/cc-language-row.c b/panels/common/cc-language-row.c
new file mode 100644
index 0000000..a2832df
--- /dev/null
+++ b/panels/common/cc-language-row.c
@@ -0,0 +1,184 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "cc-language-row.h"
+#include "cc-common-resources.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+struct _CcLanguageRow {
+ GtkListBoxRow parent_instance;
+
+ GtkImage *check_image;
+ GtkLabel *country_label;
+ GtkLabel *language_label;
+
+ gchar *locale_id;
+ gchar *language;
+ gchar *language_local;
+ gchar *country;
+ gchar *country_local;
+
+ gboolean is_extra;
+};
+
+G_DEFINE_TYPE (CcLanguageRow, cc_language_row, GTK_TYPE_LIST_BOX_ROW)
+
+static gchar *
+get_language_label (const gchar *language_code,
+ const gchar *modifier,
+ const gchar *locale_id)
+{
+ g_autofree gchar *language = NULL;
+
+ language = gnome_get_language_from_code (language_code, locale_id);
+
+ if (modifier == NULL)
+ return g_steal_pointer (&language);
+ else
+ {
+ g_autofree gchar *t_mod = gnome_get_translated_modifier (modifier, locale_id);
+ return g_strdup_printf ("%s — %s", language, t_mod);
+ }
+}
+
+static void
+cc_language_row_dispose (GObject *object)
+{
+ CcLanguageRow *self = CC_LANGUAGE_ROW (object);
+
+ g_clear_pointer (&self->locale_id, g_free);
+ g_clear_pointer (&self->country, g_free);
+ g_clear_pointer (&self->country_local, g_free);
+ g_clear_pointer (&self->language, g_free);
+ g_clear_pointer (&self->language_local, g_free);
+
+ G_OBJECT_CLASS (cc_language_row_parent_class)->dispose (object);
+}
+
+void
+cc_language_row_class_init (CcLanguageRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_language_row_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/common/cc-language-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageRow, check_image);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageRow, country_label);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageRow, language_label);
+}
+
+void
+cc_language_row_init (CcLanguageRow *self)
+{
+ g_resources_register (cc_common_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+CcLanguageRow *
+cc_language_row_new (const gchar *locale_id)
+{
+ CcLanguageRow *self;
+ g_autofree gchar *language_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ g_autofree gchar *modifier = NULL;
+
+ self = CC_LANGUAGE_ROW (g_object_new (CC_TYPE_LANGUAGE_ROW, NULL));
+ self->locale_id = g_strdup (locale_id);
+
+ gnome_parse_locale (locale_id, &language_code, &country_code, NULL, &modifier);
+
+ self->language = get_language_label (language_code, modifier, locale_id);
+ self->language_local = get_language_label (language_code, modifier, NULL);
+ gtk_label_set_label (self->language_label, self->language);
+
+ if (country_code == NULL)
+ {
+ self->country = NULL;
+ self->country_local = NULL;
+ }
+ else
+ {
+ self->country = gnome_get_country_from_code (country_code, locale_id);
+ self->country_local = gnome_get_country_from_code (country_code, NULL);
+ gtk_label_set_label (self->country_label, self->country);
+ }
+
+ return self;
+}
+
+const gchar *
+cc_language_row_get_locale_id (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), NULL);
+ return self->locale_id;
+}
+
+const gchar *
+cc_language_row_get_language (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), NULL);
+ return self->language;
+}
+
+const gchar *
+cc_language_row_get_language_local (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), NULL);
+ return self->language_local;
+}
+
+const gchar *
+cc_language_row_get_country (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), NULL);
+ return self->country;
+}
+
+const gchar *
+cc_language_row_get_country_local (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), NULL);
+ return self->country_local;
+}
+
+void
+cc_language_row_set_checked (CcLanguageRow *self, gboolean checked)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_ROW (self));
+ gtk_widget_set_visible (GTK_WIDGET (self->check_image), checked);
+}
+
+void
+cc_language_row_set_is_extra (CcLanguageRow *self, gboolean is_extra)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_ROW (self));
+ self->is_extra = is_extra;
+}
+
+gboolean
+cc_language_row_get_is_extra (CcLanguageRow *self)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_ROW (self), FALSE);
+ return self->is_extra;
+}
diff --git a/panels/common/cc-language-row.h b/panels/common/cc-language-row.h
new file mode 100644
index 0000000..2e3e8cd
--- /dev/null
+++ b/panels/common/cc-language-row.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2020 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_LANGUAGE_ROW (cc_language_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcLanguageRow, cc_language_row, CC, LANGUAGE_ROW, GtkListBoxRow)
+
+CcLanguageRow *cc_language_row_new (const gchar *locale_id);
+
+const gchar *cc_language_row_get_locale_id (CcLanguageRow *row);
+
+const gchar *cc_language_row_get_language (CcLanguageRow *row);
+
+const gchar *cc_language_row_get_language_local (CcLanguageRow *row);
+
+const gchar *cc_language_row_get_country (CcLanguageRow *row);
+
+const gchar *cc_language_row_get_country_local (CcLanguageRow *row);
+
+void cc_language_row_set_checked (CcLanguageRow *row, gboolean checked);
+
+void cc_language_row_set_is_extra (CcLanguageRow *row, gboolean is_extra);
+
+gboolean cc_language_row_get_is_extra (CcLanguageRow *row);
+
+G_END_DECLS
diff --git a/panels/common/cc-language-row.ui b/panels/common/cc-language-row.ui
new file mode 100644
index 0000000..957df54
--- /dev/null
+++ b/panels/common/cc-language-row.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcLanguageRow" parent="GtkListBoxRow">
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="spacing">12</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <child>
+ <object class="GtkLabel" id="language_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="ellipsize">end</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="check_image">
+ <property name="visible">False</property>
+ <property name="icon-name">object-select-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="country_label">
+ <property name="visible">True</property>
+ <property name="xalign">1</property>
+ <property name="ellipsize">end</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/common/cc-list-row.c b/panels/common/cc-list-row.c
new file mode 100644
index 0000000..6dc4388
--- /dev/null
+++ b/panels/common/cc-list-row.c
@@ -0,0 +1,283 @@
+/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* cc-list-row.c
+ *
+ * Copyright 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 3 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(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "cc-list-row"
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include "cc-common-resources.h"
+#include "cc-list-row.h"
+
+struct _CcListRow
+{
+ AdwActionRow parent_instance;
+
+ GtkLabel *secondary_label;
+
+ GtkImage *arrow;
+ gboolean show_arrow;
+
+ GtkSwitch *enable_switch;
+ gboolean show_switch;
+
+ gboolean switch_active;
+};
+
+G_DEFINE_TYPE (CcListRow, cc_list_row, ADW_TYPE_ACTION_ROW)
+
+
+enum {
+ PROP_0,
+ PROP_SECONDARY_LABEL,
+ PROP_SHOW_ARROW,
+ PROP_SHOW_SWITCH,
+ PROP_ACTIVE,
+ N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+static void
+cc_list_row_switch_active_cb (CcListRow *self)
+{
+ gboolean switch_active;
+
+ g_assert (CC_IS_LIST_ROW (self));
+ g_assert (self->show_switch);
+
+ switch_active = gtk_switch_get_active (self->enable_switch);
+
+ if (switch_active == self->switch_active)
+ return;
+
+ self->switch_active = switch_active;
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]);
+}
+
+static void
+cc_list_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcListRow *self = (CcListRow *)object;
+
+ switch (prop_id)
+ {
+ case PROP_SECONDARY_LABEL:
+ g_value_set_string (value, gtk_label_get_label (self->secondary_label));
+ break;
+
+ case PROP_SHOW_ARROW:
+ g_value_set_boolean (value, self->show_arrow);
+ break;
+
+ case PROP_ACTIVE:
+ g_value_set_boolean (value, self->switch_active);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_list_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcListRow *self = (CcListRow *)object;
+
+ switch (prop_id)
+ {
+ case PROP_SECONDARY_LABEL:
+ gtk_label_set_label (self->secondary_label, g_value_get_string (value));
+ break;
+
+ case PROP_SHOW_ARROW:
+ cc_list_row_set_show_arrow (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SHOW_SWITCH:
+ cc_list_row_set_show_switch (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ACTIVE:
+ g_signal_handlers_block_by_func (self->enable_switch,
+ cc_list_row_switch_active_cb, self);
+ gtk_switch_set_active (self->enable_switch,
+ g_value_get_boolean (value));
+ self->switch_active = g_value_get_boolean (value);
+ g_signal_handlers_unblock_by_func (self->enable_switch,
+ cc_list_row_switch_active_cb, self);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+cc_list_row_class_init (CcListRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = cc_list_row_get_property;
+ object_class->set_property = cc_list_row_set_property;
+
+ properties[PROP_SECONDARY_LABEL] =
+ g_param_spec_string ("secondary-label",
+ "Secondary Label",
+ "Set Secondary Label",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_SHOW_ARROW] =
+ g_param_spec_boolean ("show-arrow",
+ "Show Arrow",
+ "Whether to show an arrow at the end of the row",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_SHOW_SWITCH] =
+ g_param_spec_boolean ("show-switch",
+ "Show Switch",
+ "Whether to show a switch at the end of row",
+ FALSE,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_ACTIVE] =
+ g_param_spec_boolean ("active",
+ "Active",
+ "The active state of the switch",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/control-center/"
+ "common/cc-list-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, secondary_label);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, arrow);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, enable_switch);
+
+ gtk_widget_class_bind_template_callback (widget_class, cc_list_row_switch_active_cb);
+}
+
+static void
+cc_list_row_init (CcListRow *self)
+{
+ g_resources_register (cc_common_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+cc_list_row_set_show_arrow (CcListRow *self,
+ gboolean show_arrow)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+ g_return_if_fail (!self->show_switch);
+
+ if (self->show_arrow == show_arrow)
+ return;
+
+ self->show_arrow = show_arrow;
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_ARROW]);
+}
+
+void
+cc_list_row_set_show_switch (CcListRow *self,
+ gboolean show_switch)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+
+ self->show_switch = !!show_switch;
+
+ gtk_widget_set_visible (GTK_WIDGET (self->enable_switch), self->show_switch);
+ gtk_widget_set_visible (GTK_WIDGET (self->arrow), !self->show_switch);
+ gtk_widget_set_visible (GTK_WIDGET (self->secondary_label), !self->show_switch);
+
+ adw_action_row_set_activatable_widget (ADW_ACTION_ROW (self),
+ self->show_switch ? GTK_WIDGET (self->enable_switch) : NULL);
+}
+
+gboolean
+cc_list_row_get_active (CcListRow *self)
+{
+ g_return_val_if_fail (CC_IS_LIST_ROW (self), FALSE);
+ g_return_val_if_fail (self->show_switch, FALSE);
+
+ return self->switch_active;
+}
+
+void
+cc_list_row_set_secondary_label (CcListRow *self,
+ const gchar *label)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+ g_return_if_fail (!self->show_switch);
+
+ if (!label)
+ label = "";
+
+ if (g_str_equal (label, gtk_label_get_label (self->secondary_label)))
+ return;
+
+ gtk_label_set_text (self->secondary_label, label);
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SECONDARY_LABEL]);
+}
+
+void
+cc_list_row_set_secondary_markup (CcListRow *self,
+ const gchar *markup)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+ g_return_if_fail (!self->show_switch);
+
+ if (!markup)
+ markup = "";
+
+ if (g_str_equal (markup, gtk_label_get_label (self->secondary_label)))
+ return;
+
+ gtk_label_set_markup (self->secondary_label, markup);
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SECONDARY_LABEL]);
+}
+
+void
+cc_list_row_set_switch_sensitive (CcListRow *self,
+ gboolean sensitive)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->enable_switch), sensitive);
+}
diff --git a/panels/common/cc-list-row.h b/panels/common/cc-list-row.h
new file mode 100644
index 0000000..6e4c62e
--- /dev/null
+++ b/panels/common/cc-list-row.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* cc-list-row.h
+ *
+ * Copyright 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 3 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(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <adwaita.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_LIST_ROW (cc_list_row_get_type())
+G_DECLARE_FINAL_TYPE (CcListRow, cc_list_row, CC, LIST_ROW, AdwActionRow)
+
+void cc_list_row_set_show_arrow (CcListRow *self,
+ gboolean show_arrow);
+void cc_list_row_set_show_switch (CcListRow *self,
+ gboolean show_switch);
+gboolean cc_list_row_get_active (CcListRow *self);
+void cc_list_row_set_secondary_label (CcListRow *self,
+ const gchar *label);
+void cc_list_row_set_secondary_markup (CcListRow *self,
+ const gchar *markup);
+void cc_list_row_set_switch_sensitive (CcListRow *self,
+ gboolean sensitive);
+
+G_END_DECLS
diff --git a/panels/common/cc-list-row.ui b/panels/common/cc-list-row.ui
new file mode 100644
index 0000000..f668a10
--- /dev/null
+++ b/panels/common/cc-list-row.ui
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcListRow" parent="AdwActionRow">
+ <property name="activatable">True</property>
+
+ <!-- Secondary Label -->
+ <child type="suffix">
+ <object class="GtkLabel" id="secondary_label">
+ <property name="valign">center</property>
+ <property name="ellipsize">end</property>
+ <property name="selectable" bind-source="CcListRow" bind-property="activatable" bind-flags="sync-create|invert-boolean" />
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+
+ <!-- Switch -->
+ <child type="suffix">
+ <object class="GtkSwitch" id="enable_switch">
+ <property name="visible">False</property>
+ <property name="valign">center</property>
+ <signal name="notify::active" handler="cc_list_row_switch_active_cb" swapped="yes"/>
+ </object>
+ </child>
+
+ <!-- Arrow -->
+ <child type="suffix">
+ <object class="GtkImage" id="arrow">
+ <property name="visible" bind-source="CcListRow" bind-property="show-arrow" bind-flags="sync-create"/>
+ <property name="valign">center</property>
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/panels/common/cc-permission-infobar.c b/panels/common/cc-permission-infobar.c
new file mode 100644
index 0000000..78e2b07
--- /dev/null
+++ b/panels/common/cc-permission-infobar.c
@@ -0,0 +1,112 @@
+/* cc-permission-infobar.c
+ *
+ * Copyright (C) 2020 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 3 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(s):
+ * Felipe Borges <felipeborges@gnome.org>
+ *
+ */
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "cc-permission-infobar"
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <glib/gi18n.h>
+
+#include "cc-permission-infobar.h"
+
+struct _CcPermissionInfobar
+{
+ AdwBin parent_instance;
+
+ GtkRevealer *revealer;
+ GtkLabel *title;
+ GtkLockButton *lock_button;
+};
+
+G_DEFINE_TYPE (CcPermissionInfobar, cc_permission_infobar, ADW_TYPE_BIN)
+
+static void
+on_permission_changed (CcPermissionInfobar *self)
+{
+ GPermission *permission;
+ gboolean is_authorized;
+
+ permission = gtk_lock_button_get_permission (self->lock_button);
+ is_authorized = g_permission_get_allowed (permission);
+
+ gtk_revealer_set_reveal_child (self->revealer, !is_authorized);
+}
+
+static void
+cc_permission_infobar_class_init (CcPermissionInfobarClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/control-center/"
+ "common/cc-permission-infobar.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcPermissionInfobar, revealer);
+ gtk_widget_class_bind_template_child (widget_class, CcPermissionInfobar, title);
+ gtk_widget_class_bind_template_child (widget_class, CcPermissionInfobar, lock_button);
+}
+
+static void
+cc_permission_infobar_init (CcPermissionInfobar *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* Set the default title. */
+ cc_permission_infobar_set_title (self, NULL);
+}
+
+void
+cc_permission_infobar_set_permission (CcPermissionInfobar *self,
+ GPermission *permission)
+{
+ g_return_if_fail (CC_IS_PERMISSION_INFOBAR (self));
+
+ gtk_lock_button_set_permission (self->lock_button, permission);
+
+ g_signal_connect_object (permission, "notify",
+ G_CALLBACK (on_permission_changed),
+ self,
+ G_CONNECT_SWAPPED);
+ on_permission_changed (self);
+}
+
+/**
+ * cc_permission_infobar_set_title:
+ * @self: a #CcPermissionInfobar
+ * @title: (nullable): title to display in the infobar, or %NULL for the default
+ *
+ * Set the title text to display in the infobar.
+ */
+void
+cc_permission_infobar_set_title (CcPermissionInfobar *self,
+ const gchar *title)
+{
+ g_return_if_fail (CC_IS_PERMISSION_INFOBAR (self));
+
+ if (title == NULL)
+ title = _("Unlock to Change Settings");
+
+ gtk_label_set_text (self->title, title);
+}
diff --git a/panels/common/cc-permission-infobar.h b/panels/common/cc-permission-infobar.h
new file mode 100644
index 0000000..4d7064d
--- /dev/null
+++ b/panels/common/cc-permission-infobar.h
@@ -0,0 +1,37 @@
+/* cc-permission-infobar.h
+ *
+ * Copyright (C) 2020 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 3 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(s):
+ * Felipe Borges <felipeborges@gnome.org>
+ *
+ */
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_PERMISSION_INFOBAR (cc_permission_infobar_get_type())
+G_DECLARE_FINAL_TYPE (CcPermissionInfobar, cc_permission_infobar, CC, PERMISSION_INFOBAR, AdwBin)
+
+void cc_permission_infobar_set_permission (CcPermissionInfobar *self,
+ GPermission *permission);
+
+void cc_permission_infobar_set_title (CcPermissionInfobar *self,
+ const gchar *title);
+
+G_END_DECLS
diff --git a/panels/common/cc-permission-infobar.ui b/panels/common/cc-permission-infobar.ui
new file mode 100644
index 0000000..a2830b9
--- /dev/null
+++ b/panels/common/cc-permission-infobar.ui
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcPermissionInfobar" parent="AdwBin">
+ <property name="valign">start</property>
+ <child>
+ <object class="GtkRevealer" id="revealer">
+ <property name="hexpand">True</property>
+ <property name="reveal-child">True</property>
+ <child>
+ <object class="GtkInfoBar">
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <property name="margin-start">10</property>
+ <property name="margin-end">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">system-lock-screen-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <!-- Actual string set in code -->
+ <property name="label"></property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <property name="label" translatable="yes">Some settings must be unlocked before they can be changed.</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkCenterBox">
+ <property name="margin-top">10</property>
+ <property name="margin-bottom">10</property>
+ <property name="margin-start">10</property>
+ <property name="margin-end">10</property>
+ <child type="end">
+ <object class="GtkLockButton" id="lock_button">
+ <property name="receives-default">True</property>
+ <property name="label" translatable="yes">Unlock…</property>
+ <property name="valign">GTK_ALIGN_CENTER</property>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/common/cc-time-editor.c b/panels/common/cc-time-editor.c
new file mode 100644
index 0000000..572fc85
--- /dev/null
+++ b/panels/common/cc-time-editor.c
@@ -0,0 +1,372 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cc-time-editor.c
+ *
+ * Copyright 2020 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/>.
+ *
+ * Author(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "cc-time-editor"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-wall-clock.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+
+#include "cc-time-entry.h"
+#include "cc-time-editor.h"
+
+
+#define TIMEOUT_INITIAL 500
+#define TIMEOUT_REPEAT 50
+
+#define FILECHOOSER_SCHEMA "org.gtk.Settings.FileChooser"
+#define CLOCK_SCHEMA "org.gnome.desktop.interface"
+#define CLOCK_FORMAT_KEY "clock-format"
+#define SECONDS_PER_MINUTE (60)
+#define SECONDS_PER_HOUR (60 * 60)
+#define SECONDS_PER_DAY (60 * 60 * 24)
+
+
+struct _CcTimeEditor
+{
+ AdwBin parent_instance;
+
+ GtkButton *am_pm_button;
+ GtkStack *am_pm_stack;
+ GtkLabel *am_label;
+ GtkLabel *pm_label;
+ GtkButton *hour_up_button;
+ GtkButton *hour_down_button;
+ GtkButton *minute_up_button;
+ GtkButton *minute_down_button;
+ CcTimeEntry *time_entry;
+
+ GtkButton *clicked_button; /* The button currently being clicked */
+ GSettings *clock_settings;
+ GSettings *filechooser_settings;
+
+ guint timer_id;
+};
+
+G_DEFINE_TYPE (CcTimeEditor, cc_time_editor, ADW_TYPE_BIN)
+
+
+enum {
+ TIME_CHANGED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void
+time_editor_clock_changed_cb (CcTimeEditor *self)
+{
+ GDesktopClockFormat value;
+ gboolean is_am_pm;
+
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ value = g_settings_get_enum (self->clock_settings, CLOCK_FORMAT_KEY);
+
+ is_am_pm = value == G_DESKTOP_CLOCK_FORMAT_12H;
+ cc_time_entry_set_am_pm (self->time_entry, is_am_pm);
+ gtk_widget_set_visible (GTK_WIDGET (self->am_pm_button), is_am_pm);
+
+ if (is_am_pm)
+ {
+ if (cc_time_entry_get_is_am (self->time_entry))
+ gtk_stack_set_visible_child (self->am_pm_stack, GTK_WIDGET (self->am_label));
+ else
+ gtk_stack_set_visible_child (self->am_pm_stack, GTK_WIDGET (self->pm_label));
+ }
+}
+
+static void
+time_editor_time_changed_cb (CcTimeEditor *self)
+{
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ time_editor_clock_changed_cb (self);
+ g_signal_emit (self, signals[TIME_CHANGED], 0);
+}
+
+static void
+editor_change_time_clicked_cb (CcTimeEditor *self,
+ GtkButton *button)
+{
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ if (button == NULL)
+ return;
+
+ if (button == self->hour_up_button)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self->time_entry), 0);
+ g_signal_emit_by_name (self->time_entry, "change-value", GTK_SCROLL_STEP_UP);
+ }
+ else if (button == self->hour_down_button)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self->time_entry), 0);
+ g_signal_emit_by_name (self->time_entry, "change-value", GTK_SCROLL_STEP_DOWN);
+ }
+ else if (button == self->minute_up_button)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self->time_entry), 3);
+ g_signal_emit_by_name (self->time_entry, "change-value", GTK_SCROLL_STEP_UP);
+ }
+ else if (button == self->minute_down_button)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self->time_entry), 3);
+ g_signal_emit_by_name (self->time_entry, "change-value", GTK_SCROLL_STEP_DOWN);
+ }
+}
+
+static gboolean
+editor_change_time_repeat (CcTimeEditor *self)
+{
+ if (self->clicked_button == NULL)
+ {
+ self->timer_id = 0;
+
+ return G_SOURCE_REMOVE;
+ }
+
+ editor_change_time_clicked_cb (self, self->clicked_button);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+editor_change_time_cb (CcTimeEditor *self)
+{
+ g_assert (CC_IS_TIME_EDITOR (self));
+ g_clear_handle_id (&self->timer_id, g_source_remove);
+
+ editor_change_time_clicked_cb (self, self->clicked_button);
+ self->timer_id = g_timeout_add (TIMEOUT_REPEAT,
+ (GSourceFunc)editor_change_time_repeat,
+ self);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+editor_change_time_pressed_cb (CcTimeEditor *self,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ GtkGestureClick *click_gesture)
+{
+ GtkWidget *button;
+
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ button = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (click_gesture));
+
+ self->clicked_button = GTK_BUTTON (button);
+ /* Keep changing time until the press is released */
+ self->timer_id = g_timeout_add (TIMEOUT_INITIAL,
+ (GSourceFunc)editor_change_time_cb,
+ self);
+ editor_change_time_clicked_cb (self, GTK_BUTTON (button));
+ return FALSE;
+}
+
+static gboolean
+editor_change_time_released_cb (CcTimeEditor *self)
+{
+ self->clicked_button = NULL;
+ g_clear_handle_id (&self->timer_id, g_source_remove);
+
+ return FALSE;
+}
+
+static void
+editor_am_pm_button_clicked_cb (CcTimeEditor *self)
+{
+ gboolean is_am;
+
+ g_assert (CC_IS_TIME_EDITOR (self));
+ g_assert (cc_time_entry_get_am_pm (self->time_entry));
+
+ is_am = cc_time_entry_get_is_am (self->time_entry);
+ /* Toggle AM PM */
+ cc_time_entry_set_is_am (self->time_entry, !is_am);
+ time_editor_clock_changed_cb (self);
+}
+
+static void
+editor_am_pm_stack_changed_cb (CcTimeEditor *self)
+{
+ GtkWidget *label;
+ const gchar *text;
+
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ label = gtk_stack_get_visible_child (self->am_pm_stack);
+ text = gtk_label_get_text (GTK_LABEL (label));
+ gtk_accessible_update_property (GTK_ACCESSIBLE (self->am_pm_button),
+ GTK_ACCESSIBLE_PROPERTY_LABEL, text,
+ -1);
+}
+
+static void
+cc_time_editor_constructed (GObject *object)
+{
+ CcTimeEditor *self = (CcTimeEditor *)object;
+ GDateTime *date;
+ char *label;
+
+ G_OBJECT_CLASS (cc_time_editor_parent_class)->constructed (object);
+
+ /* Set localized identifier for AM */
+ date = g_date_time_new_utc (1, 1, 1, 0, 0, 0);
+ label = g_date_time_format (date, "%p");
+ gtk_label_set_label (self->am_label, label);
+ g_date_time_unref (date);
+ g_free (label);
+
+ /* Set localized identifier for PM */
+ date = g_date_time_new_utc (1, 1, 1, 12, 0, 0);
+ label = g_date_time_format (date, "%p");
+ gtk_label_set_label (self->pm_label, label);
+ g_date_time_unref (date);
+ g_free (label);
+}
+
+static void
+cc_time_editor_finalize (GObject *object)
+{
+ CcTimeEditor *self = (CcTimeEditor *)object;
+
+ g_clear_handle_id (&self->timer_id, g_source_remove);
+ g_clear_object (&self->clock_settings);
+ g_clear_object (&self->filechooser_settings);
+
+ G_OBJECT_CLASS (cc_time_editor_parent_class)->finalize (object);
+}
+
+static void
+cc_time_editor_class_init (CcTimeEditorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = cc_time_editor_constructed;
+ object_class->finalize = cc_time_editor_finalize;
+
+ signals[TIME_CHANGED] =
+ g_signal_new ("time-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/control-center/"
+ "common/cc-time-editor.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, am_pm_button);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, am_pm_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, am_label);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, pm_label);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, hour_up_button);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, hour_down_button);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, minute_up_button);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, minute_down_button);
+ gtk_widget_class_bind_template_child (widget_class, CcTimeEditor, time_entry);
+
+ gtk_widget_class_bind_template_callback (widget_class, editor_change_time_pressed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, editor_change_time_released_cb);
+ gtk_widget_class_bind_template_callback (widget_class, editor_am_pm_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, editor_am_pm_stack_changed_cb);
+
+ g_type_ensure (CC_TYPE_TIME_ENTRY);
+}
+
+static void
+cc_time_editor_init (CcTimeEditor *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->clock_settings = g_settings_new (CLOCK_SCHEMA);
+ self->filechooser_settings = g_settings_new (FILECHOOSER_SCHEMA);
+
+ g_signal_connect_object (self->clock_settings, "changed::" CLOCK_FORMAT_KEY,
+ G_CALLBACK (time_editor_clock_changed_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_swapped (self->time_entry, "time-changed",
+ G_CALLBACK (time_editor_time_changed_cb), self);
+ time_editor_clock_changed_cb (self);
+}
+
+CcTimeEditor *
+cc_time_editor_new (void)
+{
+ return g_object_new (CC_TYPE_TIME_EDITOR, NULL);
+}
+
+void
+cc_time_editor_set_time (CcTimeEditor *self,
+ guint hour,
+ guint minute)
+{
+ g_return_if_fail (CC_IS_TIME_EDITOR (self));
+
+ cc_time_entry_set_time (self->time_entry, hour, minute);
+}
+
+guint
+cc_time_editor_get_hour (CcTimeEditor *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_EDITOR (self), 0);
+
+ return cc_time_entry_get_hour (self->time_entry);
+}
+
+guint
+cc_time_editor_get_minute (CcTimeEditor *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_EDITOR (self), 0);
+
+ return cc_time_entry_get_minute (self->time_entry);
+}
+
+gboolean
+cc_time_editor_get_am_pm (CcTimeEditor *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_EDITOR (self), TRUE);
+
+ return TRUE;
+}
+
+void
+cc_time_editor_set_am_pm (CcTimeEditor *self,
+ gboolean is_am_pm)
+{
+ g_return_if_fail (CC_IS_TIME_EDITOR (self));
+
+ cc_time_entry_set_am_pm (self->time_entry, is_am_pm);
+}
diff --git a/panels/common/cc-time-editor.h b/panels/common/cc-time-editor.h
new file mode 100644
index 0000000..f33b10a
--- /dev/null
+++ b/panels/common/cc-time-editor.h
@@ -0,0 +1,45 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cc-time-editor.h
+ *
+ * Copyright 2020 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/>.
+ *
+ * Author(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_TIME_EDITOR (cc_time_editor_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcTimeEditor, cc_time_editor, CC, TIME_EDITOR, AdwBin)
+
+CcTimeEditor *cc_time_editor_new (void);
+void cc_time_editor_set_time (CcTimeEditor *self,
+ guint hour,
+ guint minute);
+guint cc_time_editor_get_hour (CcTimeEditor *self);
+guint cc_time_editor_get_minute (CcTimeEditor *self);
+gboolean cc_time_editor_get_am_pm (CcTimeEditor *self);
+void cc_time_editor_set_am_pm (CcTimeEditor *self,
+ gboolean is_am_pm);
+
+G_END_DECLS
diff --git a/panels/common/cc-time-editor.ui b/panels/common/cc-time-editor.ui
new file mode 100644
index 0000000..155d830
--- /dev/null
+++ b/panels/common/cc-time-editor.ui
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcTimeEditor" parent="AdwBin">
+ <child>
+ <object class="GtkGrid">
+ <property name="row-spacing">6</property>
+ <property name="column-spacing">6</property>
+
+ <!-- Increment Hour Button -->
+ <child>
+ <object class="GtkButton" id="hour_up_button">
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="icon-name">go-up-symbolic</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <accessibility>
+ <property name="label" translatable="yes">Increment Hour</property>
+ </accessibility>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="released" handler="editor_change_time_released_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <!-- Increment Minute Button -->
+ <child>
+ <object class="GtkButton" id="minute_up_button">
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="icon-name">go-up-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ </layout>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <accessibility>
+ <property name="label" translatable="yes">Increment Minute</property>
+ </accessibility>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="released" handler="editor_change_time_released_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="CcTimeEntry" id="time_entry">
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ <property name="column-span">2</property>
+ </layout>
+ <accessibility>
+ <property name="label" translatable="yes">Time</property>
+ </accessibility>
+ </object>
+ </child>
+
+ <!-- Decrement Hour Button -->
+ <child>
+ <object class="GtkButton" id="hour_down_button">
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="icon-name">go-down-symbolic</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <accessibility>
+ <property name="label" translatable="yes">Decrement Hour</property>
+ </accessibility>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="released" handler="editor_change_time_released_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <!-- Decrement Minute Button -->
+ <child>
+ <object class="GtkButton" id="minute_down_button">
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="icon-name">go-down-symbolic</property>
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <accessibility>
+ <property name="label" translatable="yes">Decrement Minute</property>
+ </accessibility>
+ <child>
+ <object class="GtkGestureClick">
+ <property name="propagation-phase">capture</property>
+ <signal name="pressed" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="released" handler="editor_change_time_released_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <!-- AM/PM Button -->
+ <child>
+ <object class="GtkButton" id="am_pm_button">
+ <property name="valign">center</property>
+ <signal name="clicked" handler="editor_am_pm_button_clicked_cb" swapped="yes"/>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">1</property>
+ </layout>
+ <child>
+ <object class="GtkStack" id="am_pm_stack">
+ <signal name="notify::visible-child" handler="editor_am_pm_stack_changed_cb" swapped="yes"/>
+ <child>
+ <object class="GtkLabel" id="am_label">
+ <attributes>
+ <attribute name="scale" value="1.4"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="pm_label">
+ <attributes>
+ <attribute name="scale" value="1.4"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/common/cc-time-entry.c b/panels/common/cc-time-entry.c
new file mode 100644
index 0000000..421a98b
--- /dev/null
+++ b/panels/common/cc-time-entry.c
@@ -0,0 +1,660 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cc-time-entry.c
+ *
+ * Copyright 2020 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/>.
+ *
+ * Author(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "cc-time-entry"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+
+#include "cc-time-entry.h"
+
+#define SEPARATOR_INDEX 2
+#define END_INDEX 4
+#define EMIT_CHANGED_TIMEOUT 100
+
+
+struct _CcTimeEntry
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *text;
+
+ guint insert_text_id;
+ guint time_changed_id;
+ int hour; /* Range: 0-23 in 24H and 1-12 in 12H with is_am set/unset */
+ int minute;
+ gboolean is_am_pm;
+ gboolean is_am; /* AM if TRUE. PM if FALSE. valid iff is_am_pm set */
+};
+
+
+static void editable_insert_text_cb (GtkText *text,
+ char *new_text,
+ gint new_text_length,
+ gint *position,
+ CcTimeEntry *self);
+
+static void gtk_editable_interface_init (GtkEditableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CcTimeEntry, cc_time_entry, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, gtk_editable_interface_init));
+
+enum {
+ CHANGE_VALUE,
+ TIME_CHANGED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static gboolean
+emit_time_changed (CcTimeEntry *self)
+{
+ self->time_changed_id = 0;
+
+ g_signal_emit (self, signals[TIME_CHANGED], 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+time_entry_fill_time (CcTimeEntry *self)
+{
+ g_autofree gchar *str = NULL;
+
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ str = g_strdup_printf ("%02d∶%02d", self->hour, self->minute);
+
+ g_signal_handlers_block_by_func (self->text, editable_insert_text_cb, self);
+ gtk_editable_set_text (GTK_EDITABLE (self->text), str);
+ g_signal_handlers_unblock_by_func (self->text, editable_insert_text_cb, self);
+}
+
+static void
+cursor_position_changed_cb (CcTimeEntry *self)
+{
+ int current_pos;
+
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ current_pos = gtk_editable_get_position (GTK_EDITABLE (self));
+
+ g_signal_handlers_block_by_func (self->text, cursor_position_changed_cb, self);
+
+ /* If cursor is on ‘:’ move to the next field */
+ if (current_pos == SEPARATOR_INDEX)
+ gtk_editable_set_position (GTK_EDITABLE (self->text), current_pos + 1);
+
+ /* If cursor is after the last digit and without selection, move to last digit */
+ if (current_pos > END_INDEX &&
+ !gtk_editable_get_selection_bounds (GTK_EDITABLE (self->text), NULL, NULL))
+ gtk_editable_set_position (GTK_EDITABLE (self->text), END_INDEX);
+
+ g_signal_handlers_unblock_by_func (self->text, cursor_position_changed_cb, self);
+}
+
+static void
+entry_selection_changed_cb (CcTimeEntry *self)
+{
+ GtkEditable *editable;
+
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ editable = GTK_EDITABLE (self->text);
+
+ g_signal_handlers_block_by_func (self->text, cursor_position_changed_cb, self);
+
+ /* If cursor is after the last digit and without selection, move to last digit */
+ if (gtk_editable_get_position (editable) > END_INDEX &&
+ !gtk_editable_get_selection_bounds (editable, NULL, NULL))
+ gtk_editable_set_position (editable, END_INDEX);
+
+ g_signal_handlers_unblock_by_func (self->text, cursor_position_changed_cb, self);
+}
+
+static void
+editable_insert_text_cb (GtkText *text,
+ char *new_text,
+ gint new_text_length,
+ gint *position,
+ CcTimeEntry *self)
+{
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ if (new_text_length == -1)
+ new_text_length = strlen (new_text);
+
+ if (new_text_length == 5)
+ {
+ const gchar *text = gtk_editable_get_text (GTK_EDITABLE (self));
+ guint16 text_length;
+
+ text_length = g_utf8_strlen (text, -1);
+
+ /* Return if the text matches XX:XX template (where X is a number) */
+ if (text_length == 0 &&
+ strstr (new_text, "0123456789:") == new_text + new_text_length &&
+ strchr (new_text, ':') == strrchr (new_text, ':'))
+ return;
+ }
+
+ /* Insert text if single digit number */
+ if (new_text_length == 1 &&
+ strspn (new_text, "0123456789"))
+ {
+ int pos, number;
+
+ pos = *position;
+ number = *new_text - '0';
+
+ if (pos == 0)
+ self->hour = self->hour % 10 + number * 10;
+ else if (pos == 1)
+ self->hour = self->hour / 10 * 10 + number;
+ else if (pos == 3)
+ self->minute = self->minute % 10 + number * 10;
+ else if (pos == 4)
+ self->minute = self->minute / 10 * 10 + number;
+
+ if (self->is_am_pm)
+ self->hour = CLAMP (self->hour, 1, 12);
+ else
+ self->hour = CLAMP (self->hour, 0, 23);
+
+ self->minute = CLAMP (self->minute, 0, 59);
+
+ g_signal_stop_emission_by_name (text, "insert-text");
+ time_entry_fill_time (self);
+ *position = pos + 1;
+
+ g_clear_handle_id (&self->time_changed_id, g_source_remove);
+ self->time_changed_id = g_timeout_add (EMIT_CHANGED_TIMEOUT,
+ (GSourceFunc)emit_time_changed, self);
+ return;
+ }
+
+ /* Warn otherwise */
+ g_signal_stop_emission_by_name (text, "insert-text");
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+
+static gboolean
+change_value_cb (GtkWidget *widget,
+ GVariant *arguments,
+ gpointer user_data)
+{
+ CcTimeEntry *self = CC_TIME_ENTRY (widget);
+ GtkScrollType type;
+ int position;
+
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ type = g_variant_get_int32 (arguments);
+ position = gtk_editable_get_position (GTK_EDITABLE (self));
+
+ if (position > SEPARATOR_INDEX)
+ {
+ if (type == GTK_SCROLL_STEP_UP)
+ self->minute++;
+ else
+ self->minute--;
+
+ if (self->minute >= 60)
+ self->minute = 0;
+ else if (self->minute <= -1)
+ self->minute = 59;
+ }
+ else
+ {
+ if (type == GTK_SCROLL_STEP_UP)
+ self->hour++;
+ else
+ self->hour--;
+
+ if (self->is_am_pm)
+ {
+ if (self->hour > 12)
+ self->hour = 1;
+ else if (self->hour < 1)
+ self->hour = 12;
+ }
+ else
+ {
+ if (self->hour >= 24)
+ self->hour = 0;
+ else if (self->hour <= -1)
+ self->hour = 23;
+ }
+ }
+
+ time_entry_fill_time (self);
+ gtk_editable_set_position (GTK_EDITABLE (self), position);
+
+ g_clear_handle_id (&self->time_changed_id, g_source_remove);
+ self->time_changed_id = g_timeout_add (EMIT_CHANGED_TIMEOUT,
+ (GSourceFunc)emit_time_changed, self);
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+value_changed_cb (CcTimeEntry *self,
+ GtkScrollType type)
+{
+ g_autoptr(GVariant) value;
+
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ value = g_variant_new_int32 (type);
+
+ change_value_cb (GTK_WIDGET (self), value, NULL);
+}
+
+static void
+on_text_cut_clipboard_cb (GtkText *text,
+ CcTimeEntry *self)
+{
+ gtk_widget_error_bell (GTK_WIDGET (self));
+ g_signal_stop_emission_by_name (text, "cut-clipboard");
+}
+
+static void
+on_text_delete_from_cursor_cb (GtkText *text,
+ GtkDeleteType *type,
+ gint count,
+ CcTimeEntry *self)
+{
+ gtk_widget_error_bell (GTK_WIDGET (self));
+ g_signal_stop_emission_by_name (text, "delete-from-cursor");
+}
+
+static void
+on_text_move_cursor_cb (GtkText *text,
+ GtkMovementStep step,
+ gint count,
+ gboolean extend,
+ CcTimeEntry *self)
+{
+ int current_pos;
+
+ current_pos = gtk_editable_get_position (GTK_EDITABLE (self));
+
+ /* If cursor is on ‘:’ move backward/forward depending on the current movement */
+ if ((step == GTK_MOVEMENT_LOGICAL_POSITIONS ||
+ step == GTK_MOVEMENT_VISUAL_POSITIONS) &&
+ current_pos + count == SEPARATOR_INDEX)
+ count > 0 ? count++ : count--;
+
+ g_signal_handlers_block_by_func (text, on_text_move_cursor_cb, self);
+ gtk_editable_set_position (GTK_EDITABLE (text), current_pos + count);
+ g_signal_handlers_unblock_by_func (text, on_text_move_cursor_cb, self);
+
+ g_signal_stop_emission_by_name (text, "move-cursor");
+}
+
+static void
+on_text_paste_clipboard_cb (GtkText *text,
+ CcTimeEntry *self)
+{
+ gtk_widget_error_bell (GTK_WIDGET (self));
+ g_signal_stop_emission_by_name (text, "paste-clipboard");
+}
+
+static void
+on_text_toggle_overwrite_cb (GtkText *text,
+ CcTimeEntry *self)
+{
+ gtk_widget_error_bell (GTK_WIDGET (self));
+ g_signal_stop_emission_by_name (text, "toggle-overwrite");
+}
+
+static gboolean
+on_key_pressed_cb (GtkEventControllerKey *key_controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ CcTimeEntry *self)
+{
+ /* Allow entering numbers */
+ if (!(state & GDK_SHIFT_MASK) &&
+ ((keyval >= GDK_KEY_KP_0 && keyval <= GDK_KEY_KP_9) ||
+ (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)))
+ return GDK_EVENT_PROPAGATE;
+
+ /* Allow navigation keys */
+ if ((keyval >= GDK_KEY_Left && keyval <= GDK_KEY_Down) ||
+ (keyval >= GDK_KEY_KP_Left && keyval <= GDK_KEY_KP_Down) ||
+ keyval == GDK_KEY_Home ||
+ keyval == GDK_KEY_End ||
+ keyval == GDK_KEY_Menu)
+ return GDK_EVENT_PROPAGATE;
+
+ if (state & (GDK_CONTROL_MASK | GDK_ALT_MASK))
+ return GDK_EVENT_PROPAGATE;
+
+ if (keyval == GDK_KEY_Tab)
+ {
+ /* If focus is on Hour field skip to minute field */
+ if (gtk_editable_get_position (GTK_EDITABLE (self)) <= 1)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self), SEPARATOR_INDEX + 1);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ /* Shift-Tab */
+ if (keyval == GDK_KEY_ISO_Left_Tab)
+ {
+ /* If focus is on Minute field skip back to Hour field */
+ if (gtk_editable_get_position (GTK_EDITABLE (self)) >= 2)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self), 0);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ return GDK_EVENT_STOP;
+}
+
+static GtkEditable *
+cc_time_entry_get_delegate (GtkEditable *editable)
+{
+ CcTimeEntry *self = CC_TIME_ENTRY (editable);
+ return GTK_EDITABLE (self->text);
+}
+
+static void
+gtk_editable_interface_init (GtkEditableInterface *iface)
+{
+ iface->get_delegate = cc_time_entry_get_delegate;
+}
+
+static void
+cc_time_entry_constructed (GObject *object)
+{
+ CcTimeEntry *self = CC_TIME_ENTRY (object);
+ PangoAttrList *list;
+ PangoAttribute *attribute;
+
+ G_OBJECT_CLASS (cc_time_entry_parent_class)->constructed (object);
+
+ time_entry_fill_time (CC_TIME_ENTRY (object));
+
+ list = pango_attr_list_new ();
+
+ attribute = pango_attr_size_new (PANGO_SCALE * 32);
+ pango_attr_list_insert (list, attribute);
+
+ attribute = pango_attr_weight_new (PANGO_WEIGHT_LIGHT);
+ pango_attr_list_insert (list, attribute);
+
+ /* Use tabular(monospace) letters */
+ attribute = pango_attr_font_features_new ("tnum");
+ pango_attr_list_insert (list, attribute);
+
+ gtk_text_set_attributes (GTK_TEXT (self->text), list);
+
+ pango_attr_list_unref (list);
+}
+
+static void
+cc_time_entry_dispose (GObject *object)
+{
+ CcTimeEntry *self = CC_TIME_ENTRY (object);
+
+ gtk_editable_finish_delegate (GTK_EDITABLE (self));
+ g_clear_pointer (&self->text, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (cc_time_entry_parent_class)->dispose (object);
+}
+
+static void
+cc_time_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ if (gtk_editable_delegate_get_property (object, property_id, value, pspec))
+ return;
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+cc_time_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ if (gtk_editable_delegate_set_property (object, property_id, value, pspec))
+ return;
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+cc_time_entry_class_init (CcTimeEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = cc_time_entry_constructed;
+ object_class->dispose = cc_time_entry_dispose;
+ object_class->get_property = cc_time_entry_get_property;
+ object_class->set_property = cc_time_entry_set_property;
+
+ signals[CHANGE_VALUE] =
+ g_signal_new ("change-value",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_ACTION,
+ 0, NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 1,
+ GTK_TYPE_SCROLL_TYPE);
+
+ signals[TIME_CHANGED] =
+ g_signal_new ("time-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ NULL,
+ G_TYPE_NONE, 0);
+
+ gtk_editable_install_properties (object_class, 1);
+
+ gtk_widget_class_set_css_name (widget_class, "entry");
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+
+ gtk_widget_class_add_binding (widget_class, GDK_KEY_Up, 0,
+ change_value_cb, "i", GTK_SCROLL_STEP_UP);
+ gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Up, 0,
+ change_value_cb, "i", GTK_SCROLL_STEP_UP);
+ gtk_widget_class_add_binding (widget_class, GDK_KEY_Down, 0,
+ change_value_cb, "i", GTK_SCROLL_STEP_DOWN);
+ gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Down, 0,
+ change_value_cb, "i", GTK_SCROLL_STEP_DOWN);
+}
+
+static void
+cc_time_entry_init (CcTimeEntry *self)
+{
+ GtkEventController *key_controller;
+
+ key_controller = gtk_event_controller_key_new ();
+ gtk_event_controller_set_propagation_phase (key_controller, GTK_PHASE_CAPTURE);
+ g_signal_connect (key_controller, "key-pressed", G_CALLBACK (on_key_pressed_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), key_controller);
+
+ self->text = g_object_new (GTK_TYPE_TEXT,
+ "input-purpose", GTK_INPUT_PURPOSE_DIGITS,
+ "input-hints", GTK_INPUT_HINT_NO_EMOJI,
+ "overwrite-mode", TRUE,
+ "xalign", 0.5,
+ "max-length", 5,
+ NULL);
+ gtk_widget_set_parent (self->text, GTK_WIDGET (self));
+ gtk_editable_init_delegate (GTK_EDITABLE (self));
+ g_object_connect (self->text,
+ "signal::cut-clipboard", on_text_cut_clipboard_cb, self,
+ "signal::delete-from-cursor", on_text_delete_from_cursor_cb, self,
+ "signal::insert-text", editable_insert_text_cb, self,
+ "signal::move-cursor", on_text_move_cursor_cb, self,
+ "swapped-signal::notify::cursor-position", cursor_position_changed_cb, self,
+ "swapped-signal::notify::selection-bound", entry_selection_changed_cb, self,
+ "signal::paste-clipboard", on_text_paste_clipboard_cb, self,
+ "signal::toggle-overwrite", on_text_toggle_overwrite_cb, self,
+ NULL);
+ g_signal_connect (self, "change-value",
+ G_CALLBACK (value_changed_cb), self);
+}
+
+GtkWidget *
+cc_time_entry_new (void)
+{
+ return g_object_new (CC_TYPE_TIME_ENTRY, NULL);
+}
+
+void
+cc_time_entry_set_time (CcTimeEntry *self,
+ guint hour,
+ guint minute)
+{
+ gboolean is_am_pm;
+
+ g_return_if_fail (CC_IS_TIME_ENTRY (self));
+
+ if (cc_time_entry_get_hour (self) == hour &&
+ cc_time_entry_get_minute (self) == minute)
+ return;
+
+ is_am_pm = cc_time_entry_get_am_pm (self);
+ cc_time_entry_set_am_pm (self, FALSE);
+
+ self->hour = CLAMP (hour, 0, 23);
+ self->minute = CLAMP (minute, 0, 59);
+
+ cc_time_entry_set_am_pm (self, is_am_pm);
+
+ g_signal_emit (self, signals[TIME_CHANGED], 0);
+ time_entry_fill_time (self);
+}
+
+guint
+cc_time_entry_get_hour (CcTimeEntry *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_ENTRY (self), 0);
+
+ if (!self->is_am_pm)
+ return self->hour;
+
+ if (self->is_am && self->hour == 12)
+ return 0;
+ else if (self->is_am || self->hour == 12)
+ return self->hour;
+ else
+ return self->hour + 12;
+}
+
+guint
+cc_time_entry_get_minute (CcTimeEntry *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_ENTRY (self), 0);
+
+ return self->minute;
+}
+
+gboolean
+cc_time_entry_get_is_am (CcTimeEntry *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_ENTRY (self), FALSE);
+
+ if (self->is_am_pm)
+ return self->is_am;
+
+ return self->hour < 12;
+}
+
+void
+cc_time_entry_set_is_am (CcTimeEntry *self,
+ gboolean is_am)
+{
+ g_return_if_fail (CC_IS_TIME_ENTRY (self));
+
+ self->is_am = !!is_am;
+ g_signal_emit (self, signals[TIME_CHANGED], 0);
+}
+
+gboolean
+cc_time_entry_get_am_pm (CcTimeEntry *self)
+{
+ g_return_val_if_fail (CC_IS_TIME_ENTRY (self), FALSE);
+
+ return self->is_am_pm;
+}
+
+void
+cc_time_entry_set_am_pm (CcTimeEntry *self,
+ gboolean is_am_pm)
+{
+ g_return_if_fail (CC_IS_TIME_ENTRY (self));
+
+ if (self->is_am_pm == !!is_am_pm)
+ return;
+
+ if (self->hour < 12)
+ self->is_am = TRUE;
+ else
+ self->is_am = FALSE;
+
+ if (is_am_pm)
+ {
+ if (self->hour == 0)
+ self->hour = 12;
+ else if (self->hour > 12)
+ self->hour = self->hour - 12;
+ }
+ else
+ {
+ if (self->hour == 12 && self->is_am)
+ self->hour = 0;
+ else if (!self->is_am)
+ self->hour = self->hour + 12;
+ }
+
+ self->is_am_pm = !!is_am_pm;
+ time_entry_fill_time (self);
+}
diff --git a/panels/common/cc-time-entry.h b/panels/common/cc-time-entry.h
new file mode 100644
index 0000000..f416143
--- /dev/null
+++ b/panels/common/cc-time-entry.h
@@ -0,0 +1,47 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cc-time-entry.h
+ *
+ * Copyright 2020 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/>.
+ *
+ * Author(s):
+ * Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_TIME_ENTRY (cc_time_entry_get_type ())
+G_DECLARE_FINAL_TYPE (CcTimeEntry, cc_time_entry, CC, TIME_ENTRY, GtkWidget)
+
+GtkWidget *cc_time_entry_new (void);
+void cc_time_entry_set_time (CcTimeEntry *self,
+ guint hour,
+ guint minute);
+guint cc_time_entry_get_minute (CcTimeEntry *self);
+guint cc_time_entry_get_hour (CcTimeEntry *self);
+gboolean cc_time_entry_get_is_am (CcTimeEntry *self);
+void cc_time_entry_set_is_am (CcTimeEntry *self,
+ gboolean is_am);
+gboolean cc_time_entry_get_am_pm (CcTimeEntry *self);
+void cc_time_entry_set_am_pm (CcTimeEntry *self,
+ gboolean is_am_pm);
+
+G_END_DECLS
diff --git a/panels/common/cc-util.c b/panels/common/cc-util.c
new file mode 100644
index 0000000..9418d56
--- /dev/null
+++ b/panels/common/cc-util.c
@@ -0,0 +1,210 @@
+/*
+ * 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 <glib/gi18n.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))
+
+#define IS_SOFT_HYPHEN(c) ((c) == 0x00AD)
+
+/* 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)
+{
+ g_autofree gchar *normalized = NULL;
+ gchar *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);
+
+ 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 (unichar) || IS_SOFT_HYPHEN (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;
+}
+
+char *
+cc_util_get_smart_date (GDateTime *date)
+{
+ g_autoptr(GDateTime) today = NULL;
+ g_autoptr(GDateTime) local = NULL;
+ GTimeSpan span;
+
+ /* Set today date */
+ local = g_date_time_new_now_local ();
+ today = g_date_time_new_local (g_date_time_get_year (local),
+ g_date_time_get_month (local),
+ g_date_time_get_day_of_month (local),
+ 0, 0, 0);
+
+ span = g_date_time_difference (today, date);
+ if (span <= 0)
+ {
+ return g_strdup (_("Today"));
+ }
+ else if (span <= G_TIME_SPAN_DAY)
+ {
+ return g_strdup (_("Yesterday"));
+ }
+ else
+ {
+ if (g_date_time_get_year (date) == g_date_time_get_year (today))
+ {
+ /* Translators: This is a date format string in the style of "Feb 24". */
+ return g_date_time_format (date, _("%b %e"));
+ }
+ else
+ {
+ /* Translators: This is a date format string in the style of "Feb 24, 2013". */
+ return g_date_time_format (date, _("%b %e, %Y"));
+ }
+ }
+}
+
+/* Copied from src/plugins/properties/bacon-video-widget-properties.c
+ * in totem */
+char *
+cc_util_time_to_string_text (gint64 msecs)
+{
+ g_autofree gchar *hours = NULL;
+ g_autofree gchar *mins = NULL;
+ g_autofree gchar *secs = NULL;
+ gint sec, min, hour, _time;
+
+ _time = (int) (msecs / 1000);
+ sec = _time % 60;
+ _time = _time - sec;
+ min = (_time % (60*60)) / 60;
+ _time = _time - (min * 60);
+ hour = _time / (60*60);
+
+ hours = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hour), hour);
+ mins = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", min), min);
+ secs = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d second", "%d seconds", sec), sec);
+
+ if (hour > 0)
+ {
+ if (min > 0 && sec > 0)
+ {
+ /* 5 hours 2 minutes 12 seconds */
+ return g_strdup_printf (C_("hours minutes seconds", "%s %s %s"), hours, mins, secs);
+ }
+ else if (min > 0)
+ {
+ /* 5 hours 2 minutes */
+ return g_strdup_printf (C_("hours minutes", "%s %s"), hours, mins);
+ }
+ else
+ {
+ /* 5 hours */
+ return g_strdup_printf (C_("hours", "%s"), hours);
+ }
+ }
+ else if (min > 0)
+ {
+ if (sec > 0)
+ {
+ /* 2 minutes 12 seconds */
+ return g_strdup_printf (C_("minutes seconds", "%s %s"), mins, secs);
+ }
+ else
+ {
+ /* 2 minutes */
+ return g_strdup_printf (C_("minutes", "%s"), mins);
+ }
+ }
+ else if (sec > 0)
+ {
+ /* 10 seconds */
+ return g_strdup (secs);
+ }
+ else
+ {
+ /* 0 seconds */
+ return g_strdup (_("0 seconds"));
+ }
+}
diff --git a/panels/common/cc-util.h b/panels/common/cc-util.h
new file mode 100644
index 0000000..131c2af
--- /dev/null
+++ b/panels/common/cc-util.h
@@ -0,0 +1,27 @@
+/*
+ * 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
+ *
+ */
+
+
+#pragma once
+
+#include <glib.h>
+
+char * cc_util_normalize_casefold_and_unaccent (const char *str);
+char * cc_util_get_smart_date (GDateTime *date);
+char * cc_util_time_to_string_text (gint64 msecs);
diff --git a/panels/common/common.gresource.xml b/panels/common/common.gresource.xml
new file mode 100644
index 0000000..b8c2946
--- /dev/null
+++ b/panels/common/common.gresource.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/control-center/common">
+ <file preprocess="xml-stripblanks">cc-language-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-language-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-list-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-time-editor.ui</file>
+ <file preprocess="xml-stripblanks">cc-permission-infobar.ui</file>
+ </gresource>
+</gresources>
diff --git a/panels/common/gnome-control-center.rules.in b/panels/common/gnome-control-center.rules.in
new file mode 100644
index 0000000..22cf785
--- /dev/null
+++ b/panels/common/gnome-control-center.rules.in
@@ -0,0 +1,13 @@
+polkit.addRule(function(action, subject) {
+ if ((action.id == "org.freedesktop.locale1.set-locale" ||
+ action.id == "org.freedesktop.locale1.set-keyboard" ||
+ action.id == "org.freedesktop.ModemManager1.Device.Control" ||
+ action.id == "org.freedesktop.hostname1.set-static-hostname" ||
+ action.id == "org.freedesktop.hostname1.set-hostname" ||
+ action.id == "org.gnome.controlcenter.datetime.configure") &&
+ subject.local &&
+ subject.active &&
+ subject.isInGroup ("@PRIVILEGED_GROUP@")) {
+ return polkit.Result.YES;
+ }
+});
diff --git a/panels/common/gnome-settings-bus.h b/panels/common/gnome-settings-bus.h
new file mode 100644
index 0000000..122289f
--- /dev/null
+++ b/panels/common/gnome-settings-bus.h
@@ -0,0 +1,14 @@
+/* Stub to replace gnome-settings-daemon's
+ * gnome-settings-bus.h helpers */
+
+#include <gdk/x11/gdkx.h>
+
+#ifdef GDK_WINDOWING_WAYLAND
+
+static inline gboolean
+gnome_settings_is_wayland (void)
+{
+ return !GDK_IS_X11_DISPLAY (gdk_display_get_default ());
+}
+
+#endif /* GDK_WINDOWING_WAYLAND */
diff --git a/panels/common/gsd-device-manager.c b/panels/common/gsd-device-manager.c
new file mode 100644
index 0000000..6283ccb
--- /dev/null
+++ b/panels/common/gsd-device-manager.c
@@ -0,0 +1,688 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <gudev/gudev.h>
+
+#include "gsd-device-manager.h"
+#include "gsd-common-enums.h"
+#include "gnome-settings-bus.h"
+#include "gsd-input-helper.h"
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/x11/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/wayland/gdkwayland.h>
+#endif
+
+typedef struct
+{
+ gchar *name;
+ gchar *device_file;
+ gchar *vendor_id;
+ gchar *product_id;
+ gchar *group;
+ GsdDeviceType type;
+ guint width;
+ guint height;
+} GsdDevicePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsdDevice, gsd_device, G_TYPE_OBJECT)
+
+typedef struct
+{
+ GObject parent_instance;
+ GHashTable *devices;
+ GUdevClient *udev_client;
+} GsdDeviceManagerPrivate;
+
+enum {
+ PROP_NAME = 1,
+ PROP_DEVICE_FILE,
+ PROP_VENDOR_ID,
+ PROP_PRODUCT_ID,
+ PROP_TYPE,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_GROUP
+};
+
+enum {
+ DEVICE_ADDED,
+ DEVICE_REMOVED,
+ DEVICE_CHANGED,
+ N_SIGNALS
+};
+
+/* Index matches GsdDeviceType */
+const gchar *udev_ids[] = {
+ "ID_INPUT_MOUSE",
+ "ID_INPUT_KEYBOARD",
+ "ID_INPUT_TOUCHPAD",
+ "ID_INPUT_TABLET",
+ "ID_INPUT_TOUCHSCREEN",
+ "ID_INPUT_TABLET_PAD",
+};
+
+static guint signals[N_SIGNALS] = { 0 };
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsdDeviceManager, gsd_device_manager, G_TYPE_OBJECT)
+
+static void
+gsd_device_init (GsdDevice *device)
+{
+}
+
+static void
+gsd_device_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsdDevicePrivate *priv;
+
+ priv = gsd_device_get_instance_private (GSD_DEVICE (object));
+
+ switch (prop_id) {
+ case PROP_NAME:
+ priv->name = g_value_dup_string (value);
+ break;
+ case PROP_DEVICE_FILE:
+ priv->device_file = g_value_dup_string (value);
+ break;
+ case PROP_VENDOR_ID:
+ priv->vendor_id = g_value_dup_string (value);
+ break;
+ case PROP_PRODUCT_ID:
+ priv->product_id = g_value_dup_string (value);
+ break;
+ case PROP_TYPE:
+ priv->type = g_value_get_flags (value);
+ break;
+ case PROP_WIDTH:
+ priv->width = g_value_get_uint (value);
+ break;
+ case PROP_HEIGHT:
+ priv->height = g_value_get_uint (value);
+ break;
+ case PROP_GROUP:
+ priv->group = g_value_dup_string (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gsd_device_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsdDevicePrivate *priv;
+
+ priv = gsd_device_get_instance_private (GSD_DEVICE (object));
+
+ switch (prop_id) {
+ case PROP_NAME:
+ g_value_set_string (value, priv->name);
+ break;
+ case PROP_DEVICE_FILE:
+ g_value_set_string (value, priv->device_file);
+ break;
+ case PROP_VENDOR_ID:
+ g_value_set_string (value, priv->vendor_id);
+ break;
+ case PROP_PRODUCT_ID:
+ g_value_set_string (value, priv->product_id);
+ break;
+ case PROP_TYPE:
+ g_value_set_flags (value, priv->type);
+ break;
+ case PROP_WIDTH:
+ g_value_set_uint (value, priv->width);
+ break;
+ case PROP_HEIGHT:
+ g_value_set_uint (value, priv->height);
+ break;
+ case PROP_GROUP:
+ g_value_set_string (value, priv->group);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gsd_device_finalize (GObject *object)
+{
+ GsdDevicePrivate *priv;
+
+ priv = gsd_device_get_instance_private (GSD_DEVICE (object));
+
+ g_free (priv->name);
+ g_free (priv->vendor_id);
+ g_free (priv->product_id);
+ g_free (priv->device_file);
+ g_free (priv->group);
+
+ G_OBJECT_CLASS (gsd_device_parent_class)->finalize (object);
+}
+
+static void
+gsd_device_class_init (GsdDeviceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = gsd_device_set_property;
+ object_class->get_property = gsd_device_get_property;
+ object_class->finalize = gsd_device_finalize;
+
+ g_object_class_install_property (object_class,
+ PROP_NAME,
+ g_param_spec_string ("name",
+ "Name",
+ "Name",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_DEVICE_FILE,
+ g_param_spec_string ("device-file",
+ "Device file",
+ "Device file",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_VENDOR_ID,
+ g_param_spec_string ("vendor-id",
+ "Vendor ID",
+ "Vendor ID",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_PRODUCT_ID,
+ g_param_spec_string ("product-id",
+ "Product ID",
+ "Product ID",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_TYPE,
+ g_param_spec_flags ("type",
+ "Device type",
+ "Device type",
+ GSD_TYPE_DEVICE_TYPE, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_WIDTH,
+ g_param_spec_uint ("width",
+ "Width",
+ "Width",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_HEIGHT,
+ g_param_spec_uint ("height",
+ "Height",
+ "Height",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (object_class,
+ PROP_GROUP,
+ g_param_spec_string ("group",
+ "Group",
+ "Group",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+gsd_device_manager_finalize (GObject *object)
+{
+ GsdDeviceManager *manager = GSD_DEVICE_MANAGER (object);
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+
+ g_hash_table_destroy (priv->devices);
+ g_object_unref (priv->udev_client);
+
+ G_OBJECT_CLASS (gsd_device_manager_parent_class)->finalize (object);
+}
+
+static GList *
+gsd_device_manager_real_list_devices (GsdDeviceManager *manager,
+ GsdDeviceType type)
+{
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+ GsdDeviceType device_type;
+ GList *devices = NULL;
+ GHashTableIter iter;
+ GsdDevice *device;
+
+ g_hash_table_iter_init (&iter, priv->devices);
+
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &device)) {
+ device_type = gsd_device_get_device_type (device);
+
+ if ((device_type & type) == type)
+ devices = g_list_prepend (devices, device);
+ }
+
+ return devices;
+}
+
+static GsdDevice *
+gsd_device_manager_real_lookup_device (GsdDeviceManager *manager,
+ GdkDevice *gdk_device)
+{
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+ GdkDisplay *display = gdk_device_get_display (gdk_device);
+ const gchar *node_path = NULL;
+ GHashTableIter iter;
+ GsdDevice *device;
+
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_DISPLAY (display))
+ node_path = xdevice_get_device_node (gdk_x11_device_get_id (gdk_device));
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_DISPLAY (display))
+ node_path = g_strdup (gdk_wayland_device_get_node_path (gdk_device));
+#endif
+ if (!node_path)
+ return NULL;
+
+ g_hash_table_iter_init (&iter, priv->devices);
+
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &device)) {
+ if (g_strcmp0 (node_path,
+ gsd_device_get_device_file (device)) == 0) {
+ return device;
+ }
+ }
+
+ return NULL;
+}
+
+static void
+gsd_device_manager_class_init (GsdDeviceManagerClass *klass)
+{
+ GsdDeviceManagerClass *manager_class = GSD_DEVICE_MANAGER_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_device_manager_finalize;
+ manager_class->list_devices = gsd_device_manager_real_list_devices;
+ manager_class->lookup_device = gsd_device_manager_real_lookup_device;
+
+ signals[DEVICE_ADDED] =
+ g_signal_new ("device-added",
+ GSD_TYPE_DEVICE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsdDeviceManagerClass, device_added),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ GSD_TYPE_DEVICE | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+ signals[DEVICE_REMOVED] =
+ g_signal_new ("device-removed",
+ GSD_TYPE_DEVICE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsdDeviceManagerClass, device_removed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ GSD_TYPE_DEVICE | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+ signals[DEVICE_CHANGED] =
+ g_signal_new ("device-changed",
+ GSD_TYPE_DEVICE_MANAGER,
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsdDeviceManagerClass, device_changed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ GSD_TYPE_DEVICE | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static GsdDeviceType
+udev_device_get_device_type (GUdevDevice *device)
+{
+ GsdDeviceType type = 0;
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (udev_ids); i++) {
+ if (g_udev_device_get_property_as_boolean (device, udev_ids[i]))
+ type |= (1 << i);
+ }
+
+ return type;
+}
+
+static gboolean
+device_is_evdev (GUdevDevice *device)
+{
+ const gchar *device_file;
+
+ device_file = g_udev_device_get_device_file (device);
+
+ if (!device_file || strstr (device_file, "/event") == NULL)
+ return FALSE;
+
+ return g_udev_device_get_property_as_boolean (device, "ID_INPUT");
+}
+
+static GsdDevice *
+create_device (GUdevDevice *udev_device, GUdevDevice *parent)
+{
+ const gchar *vendor, *product, *name, *group;
+ guint width, height;
+
+ name = g_udev_device_get_sysfs_attr (parent, "name");
+ vendor = g_udev_device_get_property (udev_device, "ID_VENDOR_ID");
+ product = g_udev_device_get_property (udev_device, "ID_MODEL_ID");
+
+ if (!vendor || !product) {
+ vendor = g_udev_device_get_sysfs_attr (udev_device, "device/id/vendor");
+ product = g_udev_device_get_sysfs_attr (udev_device, "device/id/product");
+ }
+
+ width = g_udev_device_get_property_as_int (udev_device, "ID_INPUT_WIDTH_MM");
+ height = g_udev_device_get_property_as_int (udev_device, "ID_INPUT_HEIGHT_MM");
+
+ group = g_udev_device_get_property (udev_device, "LIBINPUT_DEVICE_GROUP");
+
+ return g_object_new (GSD_TYPE_DEVICE,
+ "name", name,
+ "device-file", g_udev_device_get_device_file (udev_device),
+ "type", udev_device_get_device_type (udev_device),
+ "vendor-id", vendor,
+ "product-id", product,
+ "width", width,
+ "height", height,
+ "group", group,
+ NULL);
+}
+
+static void
+add_device (GsdDeviceManager *manager,
+ GUdevDevice *udev_device)
+{
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+ g_autoptr(GUdevDevice) parent = NULL;
+ GsdDevice *device;
+ const gchar *syspath;
+
+ parent = g_udev_device_get_parent (udev_device);
+
+ if (!parent)
+ return;
+
+ device = create_device (udev_device, parent);
+ syspath = g_udev_device_get_sysfs_path (udev_device);
+ g_hash_table_insert (priv->devices, g_strdup (syspath), device);
+ g_signal_emit_by_name (manager, "device-added", device);
+}
+
+static void
+remove_device (GsdDeviceManager *manager,
+ GUdevDevice *udev_device)
+{
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+ GsdDevice *device;
+ const gchar *syspath;
+
+ syspath = g_udev_device_get_sysfs_path (udev_device);
+ device = g_hash_table_lookup (priv->devices, syspath);
+
+ if (!device)
+ return;
+
+ g_hash_table_steal (priv->devices, syspath);
+ g_signal_emit_by_name (manager, "device-removed", device);
+
+ g_object_unref (device);
+}
+
+static void
+udev_event_cb (GUdevClient *client,
+ gchar *action,
+ GUdevDevice *device,
+ GsdDeviceManager *manager)
+{
+ if (!device_is_evdev (device))
+ return;
+
+ if (g_strcmp0 (action, "add") == 0) {
+ add_device (manager, device);
+ } else if (g_strcmp0 (action, "remove") == 0) {
+ remove_device (manager, device);
+ }
+}
+
+static void
+gsd_device_manager_init (GsdDeviceManager *manager)
+{
+ GsdDeviceManagerPrivate *priv = gsd_device_manager_get_instance_private (manager);
+ const gchar *subsystems[] = { "input", NULL };
+ g_autoptr(GList) devices = NULL;
+ GList *l;
+
+ priv->devices = g_hash_table_new_full (g_str_hash, g_str_equal,
+ (GDestroyNotify) g_free,
+ (GDestroyNotify) g_object_unref);
+
+ priv->udev_client = g_udev_client_new (subsystems);
+ g_signal_connect (priv->udev_client, "uevent",
+ G_CALLBACK (udev_event_cb), manager);
+
+ devices = g_udev_client_query_by_subsystem (priv->udev_client,
+ subsystems[0]);
+
+ for (l = devices; l; l = l->next) {
+ g_autoptr(GUdevDevice) device = l->data;
+
+ if (device_is_evdev (device))
+ add_device (manager, device);
+ }
+}
+
+GsdDeviceManager *
+gsd_device_manager_get (void)
+{
+ GsdDeviceManager *manager;
+ GdkDisplay *display;
+
+ display = gdk_display_get_default ();
+ g_return_val_if_fail (display != NULL, NULL);
+
+ manager = g_object_get_data (G_OBJECT (display), "gsd-device-manager-data");
+
+ if (!manager) {
+ manager = g_object_new (GSD_TYPE_DEVICE_MANAGER,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (display), "gsd-device-manager-data",
+ manager, (GDestroyNotify) g_object_unref);
+ }
+
+ return manager;
+}
+
+GList *
+gsd_device_manager_list_devices (GsdDeviceManager *manager,
+ GsdDeviceType type)
+{
+ g_return_val_if_fail (GSD_IS_DEVICE_MANAGER (manager), NULL);
+
+ return GSD_DEVICE_MANAGER_GET_CLASS (manager)->list_devices (manager, type);
+}
+
+GsdDeviceType
+gsd_device_get_device_type (GsdDevice *device)
+{
+ GsdDevicePrivate *priv;
+
+ g_return_val_if_fail (GSD_IS_DEVICE (device), 0);
+
+ priv = gsd_device_get_instance_private (device);
+
+ return priv->type;
+}
+
+void
+gsd_device_get_device_ids (GsdDevice *device,
+ const gchar **vendor,
+ const gchar **product)
+{
+ GsdDevicePrivate *priv;
+
+ g_return_if_fail (GSD_IS_DEVICE (device));
+
+ priv = gsd_device_get_instance_private (device);
+
+ if (vendor)
+ *vendor = priv->vendor_id;
+ if (product)
+ *product = priv->product_id;
+}
+
+GSettings *
+gsd_device_get_settings (GsdDevice *device)
+{
+ const gchar *schema = NULL, *vendor, *product;
+ GsdDeviceType type;
+ g_autofree gchar *path = NULL;
+
+ g_return_val_if_fail (GSD_IS_DEVICE (device), NULL);
+
+ type = gsd_device_get_device_type (device);
+
+ if (type & (GSD_DEVICE_TYPE_TOUCHSCREEN | GSD_DEVICE_TYPE_TABLET)) {
+ gsd_device_get_device_ids (device, &vendor, &product);
+
+ if (type & GSD_DEVICE_TYPE_TOUCHSCREEN) {
+ schema = "org.gnome.desktop.peripherals.touchscreen";
+ path = g_strdup_printf ("/org/gnome/desktop/peripherals/touchscreens/%s:%s/",
+ vendor, product);
+ } else if (type & GSD_DEVICE_TYPE_TABLET) {
+ schema = "org.gnome.desktop.peripherals.tablet";
+ path = g_strdup_printf ("/org/gnome/desktop/peripherals/tablets/%s:%s/",
+ vendor, product);
+ }
+ } else if (type & (GSD_DEVICE_TYPE_MOUSE | GSD_DEVICE_TYPE_TOUCHPAD)) {
+ schema = "org.gnome.desktop.peripherals.mouse";
+ } else if (type & GSD_DEVICE_TYPE_KEYBOARD) {
+ schema = "org.gnome.desktop.peripherals.keyboard";
+ } else {
+ return NULL;
+ }
+
+ if (path) {
+ return g_settings_new_with_path (schema, path);
+ } else {
+ return g_settings_new (schema);
+ }
+}
+
+const gchar *
+gsd_device_get_name (GsdDevice *device)
+{
+ GsdDevicePrivate *priv;
+
+ g_return_val_if_fail (GSD_IS_DEVICE (device), NULL);
+
+ priv = gsd_device_get_instance_private (device);
+
+ return priv->name;
+}
+
+const gchar *
+gsd_device_get_device_file (GsdDevice *device)
+{
+ GsdDevicePrivate *priv;
+
+ g_return_val_if_fail (GSD_IS_DEVICE (device), NULL);
+
+ priv = gsd_device_get_instance_private (device);
+
+ return priv->device_file;
+}
+
+gboolean
+gsd_device_get_dimensions (GsdDevice *device,
+ guint *width,
+ guint *height)
+{
+ GsdDevicePrivate *priv;
+
+ g_return_val_if_fail (GSD_IS_DEVICE (device), FALSE);
+
+ priv = gsd_device_get_instance_private (device);
+
+ if (width)
+ *width = priv->width;
+ if (height)
+ *height = priv->height;
+
+ return priv->width > 0 && priv->height > 0;
+}
+
+GsdDevice *
+gsd_device_manager_lookup_gdk_device (GsdDeviceManager *manager,
+ GdkDevice *gdk_device)
+{
+ GsdDeviceManagerClass *klass;
+
+ g_return_val_if_fail (GSD_IS_DEVICE_MANAGER (manager), NULL);
+ g_return_val_if_fail (GDK_IS_DEVICE (gdk_device), NULL);
+
+ klass = GSD_DEVICE_MANAGER_GET_CLASS (manager);
+ if (!klass->lookup_device)
+ return NULL;
+
+ return klass->lookup_device (manager, gdk_device);
+}
+
+gboolean
+gsd_device_shares_group (GsdDevice *device1,
+ GsdDevice *device2)
+{
+ GsdDevicePrivate *priv1, *priv2;
+
+ priv1 = gsd_device_get_instance_private (GSD_DEVICE (device1));
+ priv2 = gsd_device_get_instance_private (GSD_DEVICE (device2));
+
+ /* Don't group NULLs together */
+ if (!priv1->group && !priv2->group)
+ return FALSE;
+
+ return g_strcmp0 (priv1->group, priv2->group) == 0;
+}
diff --git a/panels/common/gsd-device-manager.h b/panels/common/gsd-device-manager.h
new file mode 100644
index 0000000..f48b5ad
--- /dev/null
+++ b/panels/common/gsd-device-manager.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2014 Carlos Garnacho <carlosg@gnome.org>
+ *
+ * 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.
+ */
+
+#pragma once
+
+#include <gdk/gdk.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_DEVICE (gsd_device_get_type ())
+G_DECLARE_DERIVABLE_TYPE (GsdDevice, gsd_device, GSD, DEVICE, GObject)
+
+#define GSD_TYPE_DEVICE_MANAGER (gsd_device_manager_get_type ())
+G_DECLARE_DERIVABLE_TYPE (GsdDeviceManager, gsd_device_manager, GSD, DEVICE_MANAGER, GObject)
+
+typedef enum {
+ GSD_DEVICE_TYPE_MOUSE = 1 << 0,
+ GSD_DEVICE_TYPE_KEYBOARD = 1 << 1,
+ GSD_DEVICE_TYPE_TOUCHPAD = 1 << 2,
+ GSD_DEVICE_TYPE_TABLET = 1 << 3,
+ GSD_DEVICE_TYPE_TOUCHSCREEN = 1 << 4,
+ GSD_DEVICE_TYPE_PAD = 1 << 5
+} GsdDeviceType;
+
+struct _GsdDeviceClass {
+ GObjectClass parent_class;
+};
+
+struct _GsdDeviceManagerClass
+{
+ GObjectClass parent_class;
+
+ GList * (* list_devices) (GsdDeviceManager *manager,
+ GsdDeviceType type);
+
+ void (* device_added) (GsdDeviceManager *manager,
+ GsdDevice *device);
+ void (* device_removed) (GsdDeviceManager *manager,
+ GsdDevice *device);
+ void (* device_changed) (GsdDeviceManager *manager,
+ GsdDevice *device);
+
+ GsdDevice * (* lookup_device) (GsdDeviceManager *manager,
+ GdkDevice *gdk_device);
+};
+
+GsdDeviceManager * gsd_device_manager_get (void);
+GList * gsd_device_manager_list_devices (GsdDeviceManager *manager,
+ GsdDeviceType type);
+
+const gchar * gsd_device_get_name (GsdDevice *device);
+GsdDeviceType gsd_device_get_device_type (GsdDevice *device);
+void gsd_device_get_device_ids (GsdDevice *device,
+ const gchar **vendor,
+ const gchar **product);
+GSettings * gsd_device_get_settings (GsdDevice *device);
+
+const gchar * gsd_device_get_device_file (GsdDevice *device);
+gboolean gsd_device_get_dimensions (GsdDevice *device,
+ guint *width,
+ guint *height);
+
+GsdDevice * gsd_device_manager_lookup_gdk_device (GsdDeviceManager *manager,
+ GdkDevice *gdk_device);
+gboolean gsd_device_shares_group (GsdDevice *device1,
+ GsdDevice *device2);
+
+G_END_DECLS
diff --git a/panels/common/gsd-input-helper.c b/panels/common/gsd-input-helper.c
new file mode 100644
index 0000000..025070e
--- /dev/null
+++ b/panels/common/gsd-input-helper.c
@@ -0,0 +1,108 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Bastien Nocera <hadess@hadess.net>
+ *
+ * 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"
+
+#include <string.h>
+
+#include <gdk/gdk.h>
+#include <gdk/x11/gdkx.h>
+
+#include <sys/types.h>
+#include <X11/Xatom.h>
+#include <X11/extensions/XInput2.h>
+
+#include "gsd-input-helper.h"
+#include "gsd-device-manager.h"
+
+static gboolean
+device_type_is_present (GsdDeviceType type)
+{
+ g_autoptr(GList) l = gsd_device_manager_list_devices (gsd_device_manager_get (),
+ type);
+ return l != NULL;
+}
+
+gboolean
+touchscreen_is_present (void)
+{
+ return device_type_is_present (GSD_DEVICE_TYPE_TOUCHSCREEN);
+}
+
+gboolean
+touchpad_is_present (void)
+{
+ return device_type_is_present (GSD_DEVICE_TYPE_TOUCHPAD);
+}
+
+gboolean
+mouse_is_present (void)
+{
+ return device_type_is_present (GSD_DEVICE_TYPE_MOUSE);
+}
+
+char *
+xdevice_get_device_node (int deviceid)
+{
+ GdkDisplay *display;
+ Atom prop;
+ Atom act_type;
+ int act_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *data;
+ char *ret;
+
+ display = gdk_display_get_default ();
+ gdk_display_sync (display);
+
+ prop = XInternAtom (GDK_DISPLAY_XDISPLAY (display), "Device Node", False);
+ if (!prop)
+ return NULL;
+
+ gdk_x11_display_error_trap_push (display);
+
+ if (!XIGetProperty (GDK_DISPLAY_XDISPLAY (display),
+ deviceid, prop, 0, 1000, False,
+ AnyPropertyType, &act_type, &act_format,
+ &nitems, &bytes_after, &data) == Success) {
+ gdk_x11_display_error_trap_pop_ignored (display);
+ return NULL;
+ }
+ if (gdk_x11_display_error_trap_pop (display))
+ goto out;
+
+ if (nitems == 0)
+ goto out;
+
+ if (act_type != XA_STRING)
+ goto out;
+
+ /* Unknown string format */
+ if (act_format != 8)
+ goto out;
+
+ ret = g_strdup ((char *) data);
+
+ XFree (data);
+ return ret;
+
+out:
+ XFree (data);
+ return NULL;
+}
diff --git a/panels/common/gsd-input-helper.h b/panels/common/gsd-input-helper.h
new file mode 100644
index 0000000..8fda1e6
--- /dev/null
+++ b/panels/common/gsd-input-helper.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2010 Bastien Nocera <hadess@hadess.net>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#include <glib.h>
+
+gboolean touchpad_is_present (void);
+gboolean touchscreen_is_present (void);
+gboolean mouse_is_present (void);
+
+char * xdevice_get_device_node (int deviceid);
+
+G_END_DECLS
diff --git a/panels/common/hostname-helper.c b/panels/common/hostname-helper.c
new file mode 100644
index 0000000..a8f7c41
--- /dev/null
+++ b/panels/common/hostname-helper.c
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2011 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 <glib.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "hostname-helper.h"
+
+static char *
+allowed_chars (void)
+{
+ GString *s;
+ char i;
+
+ s = g_string_new (NULL);
+ for (i = 'a'; i <= 'z'; i++)
+ g_string_append_c (s, i);
+ for (i = 'A'; i <= 'Z'; i++)
+ g_string_append_c (s, i);
+ for (i = '0'; i <= '9'; i++)
+ g_string_append_c (s, i);
+ g_string_append_c (s, '-');
+
+ return g_string_free (s, FALSE);
+}
+
+static char *
+remove_leading_dashes (char *input)
+{
+ char *start;
+
+ for (start = input; *start && (*start == '-'); start++)
+ ;
+
+ memmove (input, start, strlen (start) + 1);
+
+ return input;
+}
+
+static gboolean
+is_empty (const char *input)
+{
+ if (input == NULL ||
+ *input == '\0')
+ return TRUE;
+ return FALSE;
+}
+
+static char *
+remove_trailing_dashes (char *input)
+{
+ int len;
+
+ len = strlen (input);
+ while (len--) {
+ if (input[len] == '-')
+ input[len] = '\0';
+ else
+ break;
+ }
+ return input;
+}
+
+static char *
+remove_apostrophes (char *input)
+{
+ char *apo;
+
+ while ((apo = strchr (input, '\'')) != NULL)
+ memmove (apo, apo + 1, strlen (apo));
+ return input;
+}
+
+static char *
+remove_duplicate_dashes (char *input)
+{
+ char *dashes;
+
+ while ((dashes = strstr (input, "--")) != NULL)
+ memmove (dashes, dashes + 1, strlen (dashes));
+ return input;
+}
+
+#define CHECK if (is_empty (result)) return g_strdup ("localhost")
+
+char *
+pretty_hostname_to_static (const char *pretty,
+ gboolean for_display)
+{
+ g_autofree gchar *result = NULL;
+ g_autofree gchar *valid_chars = NULL;
+ g_autofree gchar *composed = NULL;
+
+ g_return_val_if_fail (pretty != NULL, NULL);
+ g_return_val_if_fail (g_utf8_validate (pretty, -1, NULL), NULL);
+
+ g_debug ("Input: '%s'", pretty);
+
+ composed = g_utf8_normalize (pretty, -1, G_NORMALIZE_ALL_COMPOSE);
+ g_debug ("\tcomposed: '%s'", composed);
+ /* Transform the pretty hostname to ASCII */
+ result = g_str_to_ascii (composed, NULL);
+ g_debug ("\ttranslit: '%s'", result);
+
+ CHECK;
+
+ /* Remove apostrophes */
+ remove_apostrophes (result);
+ g_debug ("\tapostrophes: '%s'", result);
+
+ CHECK;
+
+ /* Remove all the not-allowed chars */
+ valid_chars = allowed_chars ();
+ g_strcanon (result, valid_chars, '-');
+ g_debug ("\tcanon: '%s'", result);
+
+ CHECK;
+
+ /* Remove the leading dashes */
+ remove_leading_dashes (result);
+ g_debug ("\tleading: '%s'", result);
+
+ CHECK;
+
+ /* Remove trailing dashes */
+ remove_trailing_dashes (result);
+ g_debug ("\ttrailing: '%s'", result);
+
+ CHECK;
+
+ /* Remove duplicate dashes */
+ remove_duplicate_dashes (result);
+ g_debug ("\tduplicate: '%s'", result);
+
+ CHECK;
+
+ /* Lower case */
+ if (!for_display)
+ return g_ascii_strdown (result, -1);
+
+ return g_steal_pointer (&result);
+}
+#undef CHECK
+
+/* Max length of an SSID in bytes */
+#define SSID_MAX_LEN 32
+char *
+pretty_hostname_to_ssid (const char *pretty)
+{
+ const char *p, *prev;
+
+ if (pretty == NULL || *pretty == '\0') {
+ pretty = g_get_host_name ();
+ if (g_strcmp0 (pretty, "localhost") == 0)
+ pretty = NULL;
+ }
+
+ if (pretty == NULL) {
+ /* translators: This is the default hotspot name, need to be less than 32-bytes */
+ gchar *ret = g_strdup (C_("hotspot", "Hotspot"));
+ g_assert (strlen (ret) <= SSID_MAX_LEN);
+ return ret;
+ }
+
+ g_return_val_if_fail (g_utf8_validate (pretty, -1, NULL), NULL);
+
+ p = pretty;
+ prev = NULL;
+ while ((p = g_utf8_find_next_char (p, NULL)) != NULL) {
+ if (p == prev)
+ break;
+
+ if (p - pretty > SSID_MAX_LEN) {
+ return g_strndup (pretty, prev - pretty);
+ }
+ if (p - pretty == SSID_MAX_LEN) {
+ return g_strndup (pretty, p - pretty);
+ }
+
+ if (*p == '\0')
+ break;
+
+ prev = p;
+ }
+
+ return g_strdup (pretty);
+}
diff --git a/panels/common/hostname-helper.h b/panels/common/hostname-helper.h
new file mode 100644
index 0000000..3d2a143
--- /dev/null
+++ b/panels/common/hostname-helper.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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/>.
+ *
+ */
+
+#pragma once
+
+char *pretty_hostname_to_static (const char *pretty,
+ gboolean for_display);
+char *pretty_hostname_to_ssid (const char *pretty);
diff --git a/panels/common/meson.build b/panels/common/meson.build
new file mode 100644
index 0000000..13557c1
--- /dev/null
+++ b/panels/common/meson.build
@@ -0,0 +1,126 @@
+common_inc = include_directories('.')
+
+common_sources = []
+
+enums = 'gsd-common-enums'
+enums_header = files('gsd-device-manager.h')
+
+common_sources += gnome.mkenums(
+ enums + '.h',
+ sources: enums_header,
+ fhead: '#pragma once\n\n#include <glib-object.h>\n\nG_BEGIN_DECLS\n',
+ fprod: '/* enumerations from "@filename@" */\n',
+ vhead: 'GType @enum_name@_get_type (void) G_GNUC_CONST;\n#define GSD_TYPE_@ENUMSHORT@ (@enum_name@_get_type())\n',
+ ftail: 'G_END_DECLS\n'
+)
+
+common_sources += gnome.mkenums(
+ enums + '.c',
+ sources: enums_header,
+ fhead: '#include "gsd-device-manager.h"\n#include "gsd-common-enums.h"\n',
+ fprod: '\n/* enumerations from "@filename@" */',
+ vhead: 'GType\n@enum_name@_get_type (void)\n{\n static GType etype = 0;\n if (etype == 0) {\n static const G@Type@Value values[] = {',
+ vprod: ' { @VALUENAME@, "@VALUENAME@", "@valuenick@" },',
+ vtail: ' { 0, NULL, NULL }\n };\n etype = g_@type@_register_static ("@EnumName@", values);\n }\n return etype;\n}\n'
+)
+
+resource_data = files(
+ 'cc-language-chooser.ui',
+ 'cc-language-row.ui',
+ 'cc-list-row.ui',
+ 'cc-time-editor.ui',
+ 'cc-permission-infobar.ui',
+)
+
+common_sources += gnome.compile_resources(
+ 'cc-common-resources',
+ 'common.gresource.xml',
+ c_name: 'cc_common',
+ dependencies: resource_data,
+ export: true
+)
+
+generates_sources_dep = declare_dependency(
+ sources: common_sources,
+)
+
+sources = files(
+ 'cc-hostname-entry.c',
+ 'cc-time-entry.c',
+ 'hostname-helper.c',
+)
+
+libwidgets = static_library(
+ 'widgets',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: common_deps + [ generates_sources_dep, polkit_gobject_dep ]
+)
+libwidgets_dep = declare_dependency(
+ include_directories: common_inc,
+ link_with: libwidgets
+)
+
+sources = common_sources + files(
+ 'cc-common-language.c',
+ 'cc-language-chooser.c',
+ 'cc-language-row.c',
+ 'cc-list-row.c',
+ 'cc-time-editor.c',
+ 'cc-permission-infobar.c',
+ 'cc-util.c'
+)
+
+deps = common_deps + [
+ generates_sources_dep,
+ gnome_desktop_dep,
+ dependency('fontconfig')
+]
+
+liblanguage = static_library(
+ 'language',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: deps
+)
+
+liblanguage_dep = declare_dependency(
+ include_directories: common_inc,
+ link_with: liblanguage
+)
+
+gsd_headers = [
+ 'gsd-device-manager.h',
+ 'gsd-input-helper.h'
+]
+
+gsd_sources = [
+ 'gsd-device-manager.c',
+ 'gsd-input-helper.c'
+]
+
+sources = common_sources + files(gsd_sources)
+
+deps = common_deps + [ gudev_dep ]
+
+libdevice = static_library(
+ 'device',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: deps
+)
+
+libdevice_dep = declare_dependency(
+ include_directories: common_inc,
+ link_with: libdevice
+)
+
+polkit_conf = configuration_data()
+polkit_conf.set('PRIVILEGED_GROUP', get_option('privileged_group'))
+configure_file(
+ input: 'gnome-control-center.rules.in',
+ output: 'gnome-control-center.rules',
+ configuration: polkit_conf,
+ install_dir: join_paths(control_center_datadir, 'polkit-1', 'rules.d')
+)
+