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.c263
-rw-r--r--panels/common/cc-hostname-entry.h33
-rw-r--r--panels/common/cc-language-chooser.c479
-rw-r--r--panels/common/cc-language-chooser.h37
-rw-r--r--panels/common/cc-language-chooser.ui74
-rw-r--r--panels/common/cc-list-row.c362
-rw-r--r--panels/common/cc-list-row.h45
-rw-r--r--panels/common/cc-list-row.ui84
-rw-r--r--panels/common/cc-os-release.c84
-rw-r--r--panels/common/cc-os-release.h30
-rw-r--r--panels/common/cc-permission-infobar.c84
-rw-r--r--panels/common/cc-permission-infobar.h34
-rw-r--r--panels/common/cc-permission-infobar.ui65
-rw-r--r--panels/common/cc-time-editor.c368
-rw-r--r--panels/common/cc-time-editor.h45
-rw-r--r--panels/common/cc-time-editor.ui193
-rw-r--r--panels/common/cc-time-entry.c600
-rw-r--r--panels/common/cc-time-entry.h48
-rw-r--r--panels/common/cc-util.c210
-rw-r--r--panels/common/cc-util.h27
-rw-r--r--panels/common/common.gresource.xml9
-rw-r--r--panels/common/gnome-control-center.rules.in12
-rw-r--r--panels/common/gnome-settings-bus.h14
-rw-r--r--panels/common/gsd-device-manager.c692
-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/list-box-helper.c151
-rw-r--r--panels/common/list-box-helper.h39
-rw-r--r--panels/common/meson.build121
34 files changed, 5017 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..aff139c
--- /dev/null
+++ b/panels/common/cc-hostname-entry.c
@@ -0,0 +1,263 @@
+/* -*- 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-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_entry_get_text (GTK_ENTRY (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_entry_set_text (GTK_ENTRY (self), str);
+ else
+ gtk_entry_set_text (GTK_ENTRY (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)
+{
+}
+
+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..919cb1c
--- /dev/null
+++ b/panels/common/cc-language-chooser.c
@@ -0,0 +1,479 @@
+/* -*- 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-common-resources.h"
+
+#include <locale.h>
+#include <string.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "list-box-helper.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;
+
+ GtkWidget *select_button;
+ GtkWidget *no_results;
+ GtkListBoxRow *more_item;
+ GtkWidget *search_bar;
+ GtkWidget *language_filter_entry;
+ GtkWidget *language_listbox;
+ gboolean showing_extra;
+ gchar *language;
+ gchar **filter_words;
+};
+
+G_DEFINE_TYPE (CcLanguageChooser, cc_language_chooser, GTK_TYPE_DIALOG)
+
+static GtkWidget *
+language_widget_new (const gchar *locale_id,
+ gboolean is_extra)
+{
+ g_autofree gchar *language_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ g_autofree gchar *language = NULL;
+ g_autofree gchar *country = NULL;
+ g_autofree gchar *language_local = NULL;
+ g_autofree gchar *country_local = NULL;
+ GtkWidget *row;
+ GtkWidget *box;
+ GtkWidget *language_label;
+ GtkWidget *check;
+ GtkWidget *country_label;
+
+ gnome_parse_locale (locale_id, &language_code, &country_code, NULL, NULL);
+ language = gnome_get_language_from_code (language_code, locale_id);
+ country = gnome_get_country_from_code (country_code, locale_id);
+ language_local = gnome_get_language_from_code (language_code, NULL);
+ country_local = gnome_get_country_from_code (country_code, NULL);
+
+ row = gtk_list_box_row_new ();
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_show (box);
+ gtk_widget_set_margin_top (box, 12);
+ gtk_widget_set_margin_bottom (box, 12);
+ gtk_widget_set_margin_start (box, 18);
+ gtk_widget_set_margin_end (box, 18);
+ gtk_container_add (GTK_CONTAINER (row), box);
+
+ language_label = gtk_label_new (language);
+ gtk_widget_show (language_label);
+ gtk_label_set_xalign (GTK_LABEL (language_label), 0.0);
+ gtk_label_set_ellipsize (GTK_LABEL (language_label), PANGO_ELLIPSIZE_END);
+ gtk_box_pack_start (GTK_BOX (box), language_label, FALSE, TRUE, 0);
+
+ check = gtk_image_new ();
+ gtk_widget_hide (check);
+ gtk_image_set_from_icon_name (GTK_IMAGE (check), "object-select-symbolic", GTK_ICON_SIZE_MENU);
+ g_object_set (check, "icon-size", GTK_ICON_SIZE_MENU, NULL);
+ gtk_box_pack_start (GTK_BOX (box), check, FALSE, FALSE, 0);
+
+ country_label = gtk_label_new (country);
+ gtk_widget_show (country_label);
+ gtk_label_set_xalign (GTK_LABEL (country_label), 1.0);
+ gtk_label_set_ellipsize (GTK_LABEL (country_label), PANGO_ELLIPSIZE_END);
+ gtk_style_context_add_class (gtk_widget_get_style_context (country_label), "dim-label");
+ gtk_box_pack_start (GTK_BOX (box), country_label, TRUE, TRUE, 0);
+
+ g_object_set_data (G_OBJECT (row), "check", check);
+ g_object_set_data_full (G_OBJECT (row), "locale-id", g_strdup (locale_id), g_free);
+ g_object_set_data_full (G_OBJECT (row), "language", g_steal_pointer (&language), g_free);
+ g_object_set_data_full (G_OBJECT (row), "country", g_steal_pointer (&country), g_free);
+ g_object_set_data_full (G_OBJECT (row), "language-local", g_steal_pointer (&language_local), g_free);
+ g_object_set_data_full (G_OBJECT (row), "country-local", g_steal_pointer (&country_local), g_free);
+ g_object_set_data (G_OBJECT (row), "is-extra", GUINT_TO_POINTER (is_extra));
+
+ return row;
+}
+
+static GtkListBoxRow *
+more_widget_new (void)
+{
+ GtkWidget *box, *row;
+ GtkWidget *arrow;
+
+ row = gtk_list_box_row_new ();
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_widget_show (box);
+ gtk_container_add (GTK_CONTAINER (row), box);
+ gtk_widget_set_tooltip_text (box, _("More…"));
+
+ arrow = gtk_image_new_from_icon_name ("view-more-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_widget_show (arrow);
+ gtk_style_context_add_class (gtk_widget_get_style_context (arrow), "dim-label");
+ gtk_widget_set_margin_top (box, 10);
+ gtk_widget_set_margin_bottom (box, 10);
+ gtk_box_pack_start (GTK_BOX (box), arrow, TRUE, TRUE, 0);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkWidget *
+no_results_widget_new (void)
+{
+ GtkWidget *widget;
+
+ widget = gtk_label_new (_("No languages found"));
+ gtk_widget_set_sensitive (widget, FALSE);
+ return widget;
+}
+
+static void
+add_languages (CcLanguageChooser *chooser,
+ gchar **locale_ids,
+ GHashTable *initial)
+{
+ while (*locale_ids) {
+ gchar *locale_id;
+ gboolean is_initial;
+ GtkWidget *widget;
+
+ locale_id = *locale_ids;
+ locale_ids ++;
+
+ if (!cc_common_language_has_font (locale_id))
+ continue;
+
+ is_initial = (g_hash_table_lookup (initial, locale_id) != NULL);
+ widget = language_widget_new (locale_id, !is_initial);
+ gtk_widget_show (widget);
+ gtk_container_add (GTK_CONTAINER (chooser->language_listbox), widget);
+ }
+
+ gtk_container_add (GTK_CONTAINER (chooser->language_listbox), GTK_WIDGET (chooser->more_item));
+}
+
+static void
+add_all_languages (CcLanguageChooser *chooser)
+{
+ gchar **locale_ids;
+ GHashTable *initial;
+
+ locale_ids = gnome_get_all_locales ();
+ initial = cc_common_language_get_initial_languages ();
+ add_languages (chooser, locale_ids, initial);
+ g_hash_table_destroy (initial);
+ g_strfreev (locale_ids);
+}
+
+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 *chooser = 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 is_extra;
+ gboolean visible;
+
+ if (row == chooser->more_item)
+ return !chooser->showing_extra;
+
+ is_extra = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "is-extra"));
+
+ if (!chooser->showing_extra && is_extra)
+ return FALSE;
+
+ if (!chooser->filter_words)
+ return TRUE;
+
+ language =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "language"));
+ visible = match_all (chooser->filter_words, language);
+ if (visible)
+ return TRUE;
+
+ country =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "country"));
+ visible = match_all (chooser->filter_words, country);
+ if (visible)
+ return TRUE;
+
+ language_local =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "language-local"));
+ visible = match_all (chooser->filter_words, language_local);
+ if (visible)
+ return TRUE;
+
+ country_local =
+ cc_util_normalize_casefold_and_unaccent (g_object_get_data (G_OBJECT (row), "country-local"));
+ return match_all (chooser->filter_words, country_local);
+}
+
+static gint
+sort_languages (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ const gchar *la, *lb, *ca, *cb;
+ int d;
+
+ if (g_object_get_data (G_OBJECT (a), "locale-id") == NULL)
+ return 1;
+ if (g_object_get_data (G_OBJECT (b), "locale-id") == NULL)
+ return -1;
+
+ la = g_object_get_data (G_OBJECT (a), "language");
+ lb = g_object_get_data (G_OBJECT (b), "language");
+ d = g_strcmp0 (la, lb);
+ if (d != 0)
+ return d;
+
+ ca = g_object_get_data (G_OBJECT (a), "country");
+ cb = g_object_get_data (G_OBJECT (b), "country");
+ return g_strcmp0 (ca, cb);
+}
+
+static void
+filter_changed (CcLanguageChooser *chooser)
+{
+ g_autofree gchar *filter_contents = NULL;
+
+ g_clear_pointer (&chooser->filter_words, g_strfreev);
+
+ filter_contents =
+ cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (chooser->language_filter_entry)));
+ if (!filter_contents) {
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->language_listbox));
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (chooser->language_listbox), NULL);
+ return;
+ }
+ chooser->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
+ gtk_list_box_set_placeholder (GTK_LIST_BOX (chooser->language_listbox), chooser->no_results);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->language_listbox));
+}
+
+static void
+show_more (CcLanguageChooser *chooser, gboolean visible)
+{
+ gint width, height;
+
+ gtk_window_get_size (GTK_WINDOW (chooser), &width, &height);
+ gtk_widget_set_size_request (GTK_WIDGET (chooser), width, height);
+
+ gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (chooser->search_bar), visible);
+ gtk_widget_grab_focus (visible ? chooser->language_filter_entry : chooser->language_listbox);
+
+ chooser->showing_extra = visible;
+
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->language_listbox));
+}
+
+static void
+set_locale_id (CcLanguageChooser *chooser,
+ const gchar *locale_id)
+{
+ g_autoptr(GList) children = NULL;
+ GList *l;
+
+ gtk_widget_set_sensitive (chooser->select_button, FALSE);
+
+ children = gtk_container_get_children (GTK_CONTAINER (chooser->language_listbox));
+ for (l = children; l; l = l->next) {
+ GtkWidget *row = l->data;
+ GtkWidget *check = g_object_get_data (G_OBJECT (row), "check");
+ const gchar *language = g_object_get_data (G_OBJECT (row), "locale-id");
+ if (check == NULL || language == NULL)
+ continue;
+
+ if (g_strcmp0 (locale_id, language) == 0) {
+ gboolean is_extra;
+
+ gtk_widget_show (check);
+ gtk_widget_set_sensitive (chooser->select_button, TRUE);
+
+ /* make sure the selected language is shown */
+ is_extra = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "is-extra"));
+ if (!chooser->showing_extra && is_extra) {
+ g_object_set_data (G_OBJECT (row), "is-extra", GINT_TO_POINTER (FALSE));
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->language_listbox));
+ }
+ } else {
+ gtk_widget_hide (check);
+ }
+ }
+
+ g_free (chooser->language);
+ chooser->language = g_strdup (locale_id);
+}
+
+static void
+row_activated (CcLanguageChooser *chooser,
+ GtkListBoxRow *row)
+{
+ gchar *new_locale_id;
+
+ if (row == NULL)
+ return;
+
+ if (row == chooser->more_item) {
+ show_more (chooser, TRUE);
+ return;
+ }
+ new_locale_id = g_object_get_data (G_OBJECT (row), "locale-id");
+ if (g_strcmp0 (new_locale_id, chooser->language) == 0) {
+ gtk_dialog_response (GTK_DIALOG (chooser),
+ gtk_dialog_get_response_for_widget (GTK_DIALOG (chooser),
+ chooser->select_button));
+ } else {
+ set_locale_id (chooser, new_locale_id);
+ }
+}
+
+static void
+activate_default (CcLanguageChooser *chooser)
+{
+ GtkWidget *focus;
+ gchar *locale_id;
+
+ focus = gtk_window_get_focus (GTK_WINDOW (chooser));
+ if (!focus)
+ return;
+
+ locale_id = g_object_get_data (G_OBJECT (focus), "locale-id");
+ if (g_strcmp0 (locale_id, chooser->language) == 0)
+ return;
+
+ g_signal_stop_emission_by_name (GTK_WINDOW (chooser), "activate-default");
+ gtk_widget_activate (focus);
+}
+
+void
+cc_language_chooser_init (CcLanguageChooser *chooser)
+{
+ g_resources_register (cc_common_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (chooser));
+
+ chooser->more_item = more_widget_new ();
+ gtk_widget_show (GTK_WIDGET (chooser->more_item));
+ /* We ref-sink here so we can reuse this widget multiple times */
+ chooser->no_results = g_object_ref_sink (no_results_widget_new ());
+ gtk_widget_show (chooser->no_results);
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (chooser->language_listbox),
+ sort_languages, chooser, NULL);
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (chooser->language_listbox),
+ language_visible, chooser, NULL);
+ gtk_list_box_set_selection_mode (GTK_LIST_BOX (chooser->language_listbox),
+ GTK_SELECTION_NONE);
+ gtk_list_box_set_header_func (GTK_LIST_BOX (chooser->language_listbox),
+ cc_list_box_update_header_func, NULL, NULL);
+ add_all_languages (chooser);
+
+ g_signal_connect_object (chooser->language_filter_entry, "search-changed",
+ G_CALLBACK (filter_changed), chooser, G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (chooser->language_listbox, "row-activated",
+ G_CALLBACK (row_activated), chooser, G_CONNECT_SWAPPED);
+
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (chooser->language_listbox));
+
+ g_signal_connect (chooser, "activate-default",
+ G_CALLBACK (activate_default), NULL);
+}
+
+static void
+cc_language_chooser_dispose (GObject *object)
+{
+ CcLanguageChooser *chooser = CC_LANGUAGE_CHOOSER (object);
+
+ g_clear_object (&chooser->no_results);
+ g_clear_pointer (&chooser->filter_words, g_strfreev);
+ g_clear_pointer (&chooser->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, select_button);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, search_bar);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, language_filter_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcLanguageChooser, language_listbox);
+}
+
+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 *chooser)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_CHOOSER (chooser));
+ gtk_entry_set_text (GTK_ENTRY (chooser->language_filter_entry), "");
+ show_more (chooser, FALSE);
+}
+
+const gchar *
+cc_language_chooser_get_language (CcLanguageChooser *chooser)
+{
+ g_return_val_if_fail (CC_IS_LANGUAGE_CHOOSER (chooser), NULL);
+ return chooser->language;
+}
+
+void
+cc_language_chooser_set_language (CcLanguageChooser *chooser,
+ const gchar *language)
+{
+ g_return_if_fail (CC_IS_LANGUAGE_CHOOSER (chooser));
+ set_locale_id (chooser, 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..6b61d55
--- /dev/null
+++ b/panels/common/cc-language-chooser.ui
@@ -0,0 +1,74 @@
+<?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">340</property>
+ <property name="default_height">475</property>
+ <child type="action">
+ <object class="GtkButton" id="select_button">
+ <property name="label" translatable="yes">_Select</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</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="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">0</property>
+ <property name="border-width">0</property>
+ <child>
+ <object class="GtkSearchBar" id="search_bar">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="language_filter_entry">
+ <property name="visible">True</property>
+ <property name="width_chars">30</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <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="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="vexpand">True</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ </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-list-row.c b/panels/common/cc-list-row.c
new file mode 100644
index 0000000..20ee002
--- /dev/null
+++ b/panels/common/cc-list-row.c
@@ -0,0 +1,362 @@
+/* -*- 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-list-row.h"
+
+struct _CcListRow
+{
+ GtkListBoxRow parent_instance;
+
+ GtkBox *box;
+ GtkLabel *title;
+ GtkLabel *subtitle;
+ GtkLabel *secondary_label;
+ GtkImage *icon;
+
+ GtkSwitch *enable_switch;
+ gboolean show_switch;
+
+ gboolean switch_active;
+};
+
+G_DEFINE_TYPE (CcListRow, cc_list_row, GTK_TYPE_LIST_BOX_ROW)
+
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_SECONDARY_LABEL,
+ PROP_ICON_NAME,
+ PROP_SHOW_SWITCH,
+ PROP_ACTIVE,
+ PROP_USE_UNDERLINE,
+ N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+static void
+cc_list_row_activated_cb (CcListRow *self,
+ GtkListBoxRow *row)
+{
+ g_assert (CC_IS_LIST_ROW (self));
+
+ if (!self->show_switch || row != GTK_LIST_BOX_ROW (self))
+ return;
+
+ cc_list_row_activate (self);
+}
+
+static void
+cc_list_row_parent_changed_cb (CcListRow *self)
+{
+ GtkWidget *parent;
+
+ g_assert (CC_IS_LIST_ROW (self));
+
+ parent = gtk_widget_get_parent (GTK_WIDGET (self));
+
+ if (!parent)
+ return;
+
+ g_return_if_fail (GTK_IS_LIST_BOX (parent));
+ g_signal_connect_object (parent, "row-activated",
+ G_CALLBACK (cc_list_row_activated_cb),
+ self, G_CONNECT_SWAPPED);
+}
+
+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_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;
+ gint margin;
+
+ switch (prop_id)
+ {
+ case PROP_TITLE:
+ gtk_label_set_label (self->title, g_value_get_string (value));
+ break;
+
+ case PROP_SUBTITLE:
+ gtk_widget_set_visible (GTK_WIDGET (self->subtitle),
+ g_value_get_string (value) != NULL);
+ gtk_label_set_label (self->subtitle, g_value_get_string (value));
+ if (g_value_get_string (value) != NULL)
+ margin = 6;
+ else
+ margin = 12;
+ g_object_set (self->box,
+ "margin-top", margin,
+ "margin-bottom", margin,
+ NULL);
+ break;
+
+ case PROP_SECONDARY_LABEL:
+ gtk_label_set_label (self->secondary_label, g_value_get_string (value));
+ break;
+
+ case PROP_ICON_NAME:
+ cc_list_row_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_SHOW_SWITCH:
+ cc_list_row_set_show_switch (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_USE_UNDERLINE:
+ gtk_label_set_use_underline (self->title, g_value_get_boolean (value));
+ gtk_label_set_use_underline (self->subtitle, g_value_get_boolean (value));
+ gtk_label_set_mnemonic_widget (self->title, GTK_WIDGET (self));
+ gtk_label_set_mnemonic_widget (self->subtitle, GTK_WIDGET (self));
+ 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_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "List row primary title",
+ NULL,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ "Subtitle",
+ "List row primary subtitle",
+ NULL,
+ G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+ 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_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "Secondary Icon name",
+ NULL,
+ G_PARAM_WRITABLE | 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);
+
+ properties[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ "Use underline",
+ "If set, text prefixed with underline shall be used as mnemonic",
+ FALSE,
+ G_PARAM_WRITABLE | 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, box);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, title);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, subtitle);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, secondary_label);
+ gtk_widget_class_bind_template_child (widget_class, CcListRow, icon);
+ 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)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+ g_signal_connect_object (self, "notify::parent",
+ G_CALLBACK (cc_list_row_parent_changed_cb),
+ self, G_CONNECT_SWAPPED);
+}
+
+void
+cc_list_row_set_icon_name (CcListRow *self,
+ const gchar *icon_name)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+ g_return_if_fail (!self->show_switch);
+
+ if (icon_name)
+ g_object_set (self->icon, "icon-name", icon_name, NULL);
+
+ gtk_widget_set_visible (GTK_WIDGET (self->icon), icon_name != NULL);
+}
+
+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->icon), !self->show_switch);
+ gtk_widget_set_visible (GTK_WIDGET (self->secondary_label), !self->show_switch);
+}
+
+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_activate (CcListRow *self)
+{
+ g_return_if_fail (CC_IS_LIST_ROW (self));
+ g_return_if_fail (self->show_switch);
+
+ self->switch_active = !self->switch_active;
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACTIVE]);
+
+ gtk_widget_activate (GTK_WIDGET (self->enable_switch));
+}
+
+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]);
+}
diff --git a/panels/common/cc-list-row.h b/panels/common/cc-list-row.h
new file mode 100644
index 0000000..43997fa
--- /dev/null
+++ b/panels/common/cc-list-row.h
@@ -0,0 +1,45 @@
+/* -*- 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 <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, GtkListBoxRow)
+
+void cc_list_row_set_icon_name (CcListRow *self,
+ const gchar *icon_name);
+void cc_list_row_set_show_switch (CcListRow *self,
+ gboolean show_switch);
+gboolean cc_list_row_get_active (CcListRow *self);
+void cc_list_row_activate (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);
+
+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..730b8d9
--- /dev/null
+++ b/panels/common/cc-list-row.ui
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcListRow" parent="GtkListBoxRow">
+ <property name="visible">1</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">1</property>
+ <property name="margin">12</property>
+ <property name="spacing">12</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="visible">1</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+
+ <!-- Title -->
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="visible">1</property>
+ <property name="hexpand">1</property>
+ <property name="xalign">0.0</property>
+ <property name="ellipsize">end</property>
+ </object>
+ </child>
+
+ <!-- Subtitle -->
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="visible">0</property>
+ <property name="hexpand">1</property>
+ <property name="xalign">0.0</property>
+ <property name="wrap-mode">word</property>
+ <property name="max-width-chars">42</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ <attributes>
+ <attribute name="scale" value="0.83"/>
+ </attributes>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ <!-- Secondary Label -->
+ <child>
+ <object class="GtkLabel" id="secondary_label">
+ <property name="visible">1</property>
+ <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>
+
+ <!-- Icon -->
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="visible">0</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+
+ <!-- Switch -->
+ <child>
+ <object class="GtkSwitch" id="enable_switch">
+ <property name="visible">0</property>
+ <property name="can-focus">0</property>
+ <property name="valign">center</property>
+ <signal name="notify::active" handler="cc_list_row_switch_active_cb" swapped="yes"/>
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/common/cc-os-release.c b/panels/common/cc-os-release.c
new file mode 100644
index 0000000..4f04da9
--- /dev/null
+++ b/panels/common/cc-os-release.c
@@ -0,0 +1,84 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2019 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-os-release.h"
+
+gchar *
+cc_os_release_get_value (const gchar *key)
+{
+ g_autoptr(GHashTable) values = NULL;
+
+ values = cc_os_release_get_values ();
+ if (values == NULL)
+ return NULL;
+
+ return g_strdup (g_hash_table_lookup (values, key));
+}
+
+GHashTable *
+cc_os_release_get_values (void)
+{
+ g_autoptr(GHashTable) values = NULL;
+ g_autofree gchar *buffer = NULL;
+ g_auto(GStrv) lines = NULL;
+ int i;
+ g_autoptr(GError) error = NULL;
+
+ values = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ if (!g_file_get_contents ("/etc/os-release", &buffer, NULL, &error))
+ {
+ if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ return NULL;
+
+ if (!g_file_get_contents ("/usr/lib/os-release", &buffer, NULL, NULL))
+ return NULL;
+ }
+
+ /* Default values in spec */
+ g_hash_table_insert (values, g_strdup ("NAME"), g_strdup ("Linux"));
+ g_hash_table_insert (values, g_strdup ("ID"), g_strdup ("Linux"));
+ g_hash_table_insert (values, g_strdup ("PRETTY_NAME"), g_strdup ("Linux"));
+
+ lines = g_strsplit (buffer, "\n", -1);
+ for (i = 0; lines[i] != NULL; i++)
+ {
+ gchar *line = lines[i];
+ g_auto(GStrv) tokens = NULL;
+ const gchar *key, *value;
+ g_autofree gchar *unquoted_value = NULL;
+
+ /* Skip comments */
+ if (g_str_has_prefix (line, "#"))
+ continue;
+
+ tokens = g_strsplit (line, "=", 2);
+ if (g_strv_length (tokens) < 2)
+ continue;
+ key = tokens[0];
+ value = tokens[1];
+ unquoted_value = g_shell_unquote (value, NULL);
+ if (unquoted_value != NULL)
+ value = unquoted_value;
+
+ g_hash_table_insert (values, g_strdup (key), g_strdup (value));
+ }
+
+ return g_steal_pointer (&values);
+}
diff --git a/panels/common/cc-os-release.h b/panels/common/cc-os-release.h
new file mode 100644
index 0000000..3213d85
--- /dev/null
+++ b/panels/common/cc-os-release.h
@@ -0,0 +1,30 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2019 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 <glib.h>
+
+G_BEGIN_DECLS
+
+gchar *cc_os_release_get_value (const gchar *key);
+
+GHashTable *cc_os_release_get_values (void);
+
+G_END_DECLS
diff --git a/panels/common/cc-permission-infobar.c b/panels/common/cc-permission-infobar.c
new file mode 100644
index 0000000..31329ad
--- /dev/null
+++ b/panels/common/cc-permission-infobar.c
@@ -0,0 +1,84 @@
+/* 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 "cc-permission-infobar.h"
+
+struct _CcPermissionInfobar
+{
+ GtkRevealer parent_instance;
+
+ GtkLockButton *lock_button;
+};
+
+G_DEFINE_TYPE (CcPermissionInfobar, cc_permission_infobar, GTK_TYPE_REVEALER)
+
+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 (GTK_REVEALER (self), !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, lock_button);
+}
+
+static void
+cc_permission_infobar_init (CcPermissionInfobar *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+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);
+}
diff --git a/panels/common/cc-permission-infobar.h b/panels/common/cc-permission-infobar.h
new file mode 100644
index 0000000..4595a8b
--- /dev/null
+++ b/panels/common/cc-permission-infobar.h
@@ -0,0 +1,34 @@
+/* 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 <gtk/gtk.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, GtkRevealer)
+
+void cc_permission_infobar_set_permission (CcPermissionInfobar *self,
+ GPermission *permission);
+
+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..12eea75
--- /dev/null
+++ b/panels/common/cc-permission-infobar.ui
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcPermissionInfobar" parent="GtkRevealer">
+ <property name="reveal-child">True</property>
+ <property name="valign">start</property>
+ <child>
+ <object class="GtkInfoBar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox">
+ <property name="can-focus">False</property>
+ <property name="layout-style">end</property>
+ <property name="border-width">10</property>
+ <child>
+ <object class="GtkLockButton" id="lock_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="label" translatable="yes">Unlock…</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child internal-child="content_area">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="border-width">10</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-lock-screen-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Unlock to Change Settings</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Some settings must be unlocked before they can be changed.</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..684a9cd
--- /dev/null
+++ b/panels/common/cc-time-editor.c
@@ -0,0 +1,368 @@
+/* -*- 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
+{
+ GtkBin 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, GTK_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));
+
+ 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,
+ GdkEvent *event,
+ GtkButton *button)
+{
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ self->clicked_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, 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)
+{
+ AtkObject *accessible;
+ GtkWidget *label;
+ const gchar *text;
+
+ g_assert (CC_IS_TIME_EDITOR (self));
+
+ accessible = gtk_widget_get_accessible (GTK_WIDGET (self->am_pm_button));
+ if (accessible == NULL)
+ return;
+
+ label = gtk_stack_get_visible_child (self->am_pm_stack);
+ text = gtk_label_get_text (GTK_LABEL (label));
+ atk_object_set_name (accessible, text);
+}
+
+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..48c4534
--- /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 <gtk/gtk.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, GtkBin)
+
+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..1732fcd
--- /dev/null
+++ b/panels/common/cc-time-editor.ui
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcTimeEditor" parent="GtkBin">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <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="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <signal name="button-press-event" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="button-release-event" handler="editor_change_time_released_cb" swapped="yes"/>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-up-symbolic</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name" translatable="yes">Increment Hour</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+
+ <!-- Increment Minute Button -->
+ <child>
+ <object class="GtkButton" id="minute_up_button">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <signal name="button-press-event" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="button-release-event" handler="editor_change_time_released_cb" swapped="yes"/>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-up-symbolic</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name" translatable="yes">Increment Minute</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="CcTimeEntry" id="time_entry">
+ <property name="visible">True</property>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-description" translatable="yes">Time</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">1</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+
+ <!-- Decrement Hour Button -->
+ <child>
+ <object class="GtkButton" id="hour_down_button">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <signal name="button-press-event" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="button-release-event" handler="editor_change_time_released_cb" swapped="yes"/>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-down-symbolic</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name" translatable="yes">Decrement Hour</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+
+ <!-- Decrement Minute Button -->
+ <child>
+ <object class="GtkButton" id="minute_down_button">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <signal name="button-press-event" handler="editor_change_time_pressed_cb" swapped="yes"/>
+ <signal name="button-release-event" handler="editor_change_time_released_cb" swapped="yes"/>
+ <style>
+ <class name="titlebutton"/>
+ <class name="circular"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">go-down-symbolic</property>
+ </object>
+ </child>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="AtkObject::accessible-name" translatable="yes">Decrement Minute</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+
+ <!-- AM/PM Button -->
+ <child>
+ <object class="GtkButton" id="am_pm_button">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" handler="editor_am_pm_button_clicked_cb" swapped="yes"/>
+ <child>
+ <object class="GtkStack" id="am_pm_stack">
+ <property name="visible">True</property>
+ <signal name="notify::visible-child" handler="editor_am_pm_stack_changed_cb" swapped="yes"/>
+ <child>
+ <object class="GtkLabel" id="am_label">
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="scale" value="1.4"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="pm_label">
+ <property name="visible">True</property>
+ <attributes>
+ <attribute name="scale" value="1.4"/>
+ </attributes>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </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..d989935
--- /dev/null
+++ b/panels/common/cc-time-entry.c
@@ -0,0 +1,600 @@
+/* -*- 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
+{
+ GtkEntry parent_instance;
+
+ 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 */
+};
+
+G_DEFINE_TYPE (CcTimeEntry, cc_time_entry, GTK_TYPE_ENTRY)
+
+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_handler_block (self, self->insert_text_id);
+ gtk_entry_set_text (GTK_ENTRY (self), str);
+ g_signal_handler_unblock (self, self->insert_text_id);
+}
+
+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, 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), 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), NULL, NULL))
+ gtk_editable_set_position (GTK_EDITABLE (self), END_INDEX);
+
+ g_signal_handlers_unblock_by_func (self, 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);
+
+ g_signal_handlers_block_by_func (self, 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, cursor_position_changed_cb, self);
+}
+
+static void
+editable_insert_text_cb (CcTimeEntry *self,
+ char *new_text,
+ gint new_text_length,
+ gint *position)
+{
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ if (new_text_length == -1)
+ new_text_length = strlen (new_text);
+
+ if (new_text_length == 5)
+ {
+ guint16 text_length;
+
+ text_length = gtk_entry_get_text_length (GTK_ENTRY (self));
+
+ /* 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 (self, "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 (self, "insert-text");
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+
+static void
+entry_select_all (CcTimeEntry *self)
+{
+ gtk_editable_select_region (GTK_EDITABLE (self), 0, -1);
+}
+
+static void
+entry_populate_popup_cb (CcTimeEntry *self,
+ GtkWidget *widget)
+{
+ GList *children;
+
+ if (!GTK_IS_CONTAINER (widget))
+ return;
+
+ children = gtk_container_get_children (GTK_CONTAINER (widget));
+
+ if (GTK_IS_MENU (widget))
+ {
+ GtkWidget *menu_item;
+
+ for (GList *child = children; child; child = child->next)
+ gtk_container_remove (GTK_CONTAINER (widget), child->data);
+
+ menu_item = gtk_menu_item_new_with_mnemonic (_("_Copy"));
+ gtk_widget_set_sensitive (menu_item, gtk_editable_get_selection_bounds (GTK_EDITABLE (self), NULL, NULL));
+ g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (gtk_editable_copy_clipboard), self);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_append (GTK_MENU_SHELL (widget), menu_item);
+
+ menu_item = gtk_menu_item_new_with_mnemonic (_("Select _All"));
+ gtk_widget_set_sensitive (menu_item, gtk_entry_get_text_length (GTK_ENTRY (self)) > 0);
+ g_signal_connect_swapped (menu_item, "activate", G_CALLBACK (entry_select_all), self);
+ gtk_widget_show (menu_item);
+ gtk_menu_shell_append (GTK_MENU_SHELL (widget), menu_item);
+ }
+}
+
+static void
+time_entry_change_value_cb (CcTimeEntry *self,
+ GtkScrollType type)
+{
+ int position;
+ g_assert (CC_IS_TIME_ENTRY (self));
+
+ 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);
+}
+
+static void
+cc_entry_move_cursor (GtkEntry *entry,
+ GtkMovementStep step,
+ gint count,
+ gboolean extend_selection)
+{
+ int current_pos;
+
+ current_pos = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ /* 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--;
+
+ GTK_ENTRY_CLASS (cc_time_entry_parent_class)->move_cursor (entry, step, count, extend_selection);
+}
+
+static void
+cc_time_entry_error_bell (GtkEntry *entry)
+{
+ gtk_widget_error_bell (GTK_WIDGET (entry));
+}
+
+static void
+cc_time_entry_delete_from_cursor (GtkEntry *entry,
+ GtkDeleteType type,
+ gint count)
+{
+ gtk_widget_error_bell (GTK_WIDGET (entry));
+}
+
+static gboolean
+cc_time_entry_drag_motion (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ return TRUE;
+}
+
+static gboolean
+cc_time_entry_key_press (GtkWidget *widget,
+ GdkEventKey *event)
+{
+ CcTimeEntry *self = (CcTimeEntry *)widget;
+
+ /* Allow entering numbers */
+ if (!(event->state & GDK_SHIFT_MASK) &&
+ ((event->keyval >= GDK_KEY_KP_0 &&
+ event->keyval <= GDK_KEY_KP_9) ||
+ (event->keyval >= GDK_KEY_0 &&
+ event->keyval <= GDK_KEY_9)))
+ return GTK_WIDGET_CLASS (cc_time_entry_parent_class)->key_press_event (widget, event);
+
+ /* Allow navigation keys */
+ if ((event->keyval >= GDK_KEY_Left &&
+ event->keyval <= GDK_KEY_Down) ||
+ (event->keyval >= GDK_KEY_KP_Left &&
+ event->keyval <= GDK_KEY_KP_Down) ||
+ event->keyval == GDK_KEY_Home ||
+ event->keyval == GDK_KEY_End ||
+ event->keyval == GDK_KEY_Menu)
+ return GTK_WIDGET_CLASS (cc_time_entry_parent_class)->key_press_event (widget, event);
+
+ if (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK))
+ return GTK_WIDGET_CLASS (cc_time_entry_parent_class)->key_press_event (widget, event);
+
+ if (event->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 GTK_WIDGET_CLASS (cc_time_entry_parent_class)->key_press_event (widget, event);
+ }
+
+ /* Shift-Tab */
+ if (event->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 GTK_WIDGET_CLASS (cc_time_entry_parent_class)->key_press_event (widget, event);
+ }
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+cc_time_entry_constructed (GObject *object)
+{
+ PangoAttrList *list;
+ PangoAttribute *attribute;
+
+ G_OBJECT_CLASS (cc_time_entry_parent_class)->constructed (object);
+
+ g_object_set (object,
+ "input-purpose", GTK_INPUT_PURPOSE_DIGITS,
+ "input-hints", GTK_INPUT_HINT_NO_EMOJI,
+ "overwrite-mode", TRUE,
+ "xalign", 0.5,
+ "max-length", 5,
+ NULL);
+
+ 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_entry_set_attributes (GTK_ENTRY (object), list);
+}
+
+static void
+cc_time_entry_class_init (CcTimeEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkEntryClass *entry_class = GTK_ENTRY_CLASS (klass);
+ GtkBindingSet *binding_set;
+
+ object_class->constructed = cc_time_entry_constructed;
+
+ widget_class->drag_motion = cc_time_entry_drag_motion;
+ widget_class->key_press_event = cc_time_entry_key_press;
+
+ entry_class->delete_from_cursor = cc_time_entry_delete_from_cursor;
+ entry_class->move_cursor = cc_entry_move_cursor;
+ entry_class->toggle_overwrite = cc_time_entry_error_bell;
+ entry_class->backspace = cc_time_entry_error_bell;
+ entry_class->cut_clipboard = cc_time_entry_error_bell;
+ entry_class->paste_clipboard = cc_time_entry_error_bell;
+
+ 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);
+
+ binding_set = gtk_binding_set_by_class (klass);
+
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_Up, 0,
+ "change-value", 1,
+ GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_UP);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Up, 0,
+ "change-value", 1,
+ GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_UP);
+
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_Down, 0,
+ "change-value", 1,
+ GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_DOWN);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Down, 0,
+ "change-value", 1,
+ GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_DOWN);
+}
+
+static void
+cc_time_entry_init (CcTimeEntry *self)
+{
+ g_signal_connect_after (self, "notify::cursor-position",
+ G_CALLBACK (cursor_position_changed_cb), NULL);
+ g_signal_connect_after (self, "notify::selection-bound",
+ G_CALLBACK (entry_selection_changed_cb), NULL);
+ self->insert_text_id = g_signal_connect (self, "insert-text",
+ G_CALLBACK (editable_insert_text_cb), NULL);
+ g_signal_connect_after (self, "populate-popup",
+ G_CALLBACK (entry_populate_popup_cb), NULL);
+ g_signal_connect (self, "change-value",
+ G_CALLBACK (time_entry_change_value_cb), NULL);
+}
+
+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);
+ 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..f3ddb88
--- /dev/null
+++ b/panels/common/cc-time-entry.h
@@ -0,0 +1,48 @@
+/* -*- 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, GtkEntry)
+
+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..9620dc1
--- /dev/null
+++ b/panels/common/common.gresource.xml
@@ -0,0 +1,9 @@
+<?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-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..971ffac
--- /dev/null
+++ b/panels/common/gnome-control-center.rules.in
@@ -0,0 +1,12 @@
+polkit.addRule(function(action, subject) {
+ if ((action.id == "org.freedesktop.locale1.set-locale" ||
+ action.id == "org.freedesktop.locale1.set-keyboard" ||
+ 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..763a9cc
--- /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/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..9509b3f
--- /dev/null
+++ b/panels/common/gsd-device-manager.c
@@ -0,0 +1,692 @@
+/* -*- 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/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/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)
+{
+ const gchar *vendor, *product, *name, *group;
+ guint width, height;
+ g_autoptr(GUdevDevice) parent = NULL;
+
+ parent = g_udev_device_get_parent (udev_device);
+ g_assert (parent != NULL);
+
+ 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);
+ GUdevDevice *parent;
+ GsdDevice *device;
+ const gchar *syspath;
+
+ parent = g_udev_device_get_parent (udev_device);
+
+ if (!parent)
+ return;
+
+ device = create_device (udev_device);
+ 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;
+ GdkScreen *screen;
+
+ screen = gdk_screen_get_default ();
+ g_return_val_if_fail (screen != NULL, NULL);
+
+ manager = g_object_get_data (G_OBJECT (screen), "gsd-device-manager-data");
+
+ if (!manager) {
+ manager = g_object_new (GSD_TYPE_DEVICE_MANAGER,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (screen), "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..6ffc18b
--- /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/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/list-box-helper.c b/panels/common/list-box-helper.c
new file mode 100644
index 0000000..77b1f65
--- /dev/null
+++ b/panels/common/list-box-helper.c
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2014 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 "list-box-helper.h"
+
+#define MAX_ROWS_VISIBLE 5
+
+struct _CcListBoxRow
+{
+ GtkListBoxRow parent_instance;
+};
+G_DEFINE_TYPE (CcListBoxRow, cc_list_box_row, GTK_TYPE_LIST_BOX_ROW)
+enum
+{
+ BOX_ROW_ACTIVATED,
+ LAST_BOX_ROW_SIGNAL
+};
+static guint cc_list_box_row_signals[LAST_BOX_ROW_SIGNAL] = { 0 };
+static void
+cc_list_box_row_class_init (CcListBoxRowClass *klass)
+{
+ cc_list_box_row_signals[BOX_ROW_ACTIVATED] =
+ g_signal_new ("activated",
+ CC_TYPE_LIST_BOX_ROW,
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+static void cc_list_box_row_init (CcListBoxRow *self) {}
+
+struct _CcListBox
+{
+ GtkListBox parent_instance;
+};
+G_DEFINE_TYPE (CcListBox, cc_list_box, GTK_TYPE_LIST_BOX)
+static void
+cc_list_box_row_activated (GtkListBox *box, GtkListBoxRow *row)
+{
+ if (CC_IS_LIST_BOX_ROW (row))
+ g_signal_emit (row, cc_list_box_row_signals[BOX_ROW_ACTIVATED], 0);
+}
+static void
+cc_list_box_class_init (CcListBoxClass *klass)
+{
+ GtkListBoxClass *parent_class = GTK_LIST_BOX_CLASS (klass);
+ parent_class->row_activated = cc_list_box_row_activated;
+}
+static void cc_list_box_init (CcListBox *self) {}
+
+
+void
+cc_list_box_update_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ GtkWidget *current;
+
+ if (before == NULL)
+ {
+ gtk_list_box_row_set_header (row, NULL);
+ return;
+ }
+
+ current = gtk_list_box_row_get_header (row);
+ if (current == NULL)
+ {
+ current = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_widget_show (current);
+ gtk_list_box_row_set_header (row, current);
+ }
+}
+
+void
+cc_list_box_adjust_scrolling (GtkListBox *listbox)
+{
+ GtkWidget *scrolled_window;
+ g_autoptr(GList) children = NULL;
+ guint n_rows, num_max_rows;
+
+ scrolled_window = g_object_get_data (G_OBJECT (listbox), "cc-scrolling-scrolled-window");
+ if (!scrolled_window)
+ return;
+
+ children = gtk_container_get_children (GTK_CONTAINER (listbox));
+ n_rows = g_list_length (children);
+
+ num_max_rows = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (listbox), "cc-max-rows-visible"));
+
+ if (n_rows >= num_max_rows)
+ {
+ gint total_row_height = 0;
+ GList *l;
+ guint i;
+
+ for (l = children, i = 0; l != NULL && i < num_max_rows; l = l->next, i++) {
+ gint row_height;
+ gtk_widget_get_preferred_height (GTK_WIDGET (l->data), &row_height, NULL);
+ total_row_height += row_height;
+ }
+
+ gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled_window), total_row_height);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ }
+ else
+ {
+ gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled_window), -1);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
+ GTK_POLICY_NEVER, GTK_POLICY_NEVER);
+ }
+}
+
+void
+cc_list_box_setup_scrolling (GtkListBox *listbox,
+ guint num_max_rows)
+{
+ GtkWidget *parent;
+ GtkWidget *scrolled_window;
+
+ parent = gtk_widget_get_parent (GTK_WIDGET (listbox));
+ scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_show (scrolled_window);
+
+ g_object_ref (listbox);
+ gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (listbox));
+ gtk_container_add (GTK_CONTAINER (scrolled_window), GTK_WIDGET (listbox));
+ g_object_unref (listbox);
+
+ gtk_container_add (GTK_CONTAINER (parent), scrolled_window);
+
+ if (num_max_rows == 0)
+ num_max_rows = MAX_ROWS_VISIBLE;
+
+ g_object_set_data (G_OBJECT (listbox), "cc-scrolling-scrolled-window", scrolled_window);
+ g_object_set_data (G_OBJECT (listbox), "cc-max-rows-visible", GUINT_TO_POINTER (num_max_rows));
+}
diff --git a/panels/common/list-box-helper.h b/panels/common/list-box-helper.h
new file mode 100644
index 0000000..f3c2553
--- /dev/null
+++ b/panels/common/list-box-helper.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2014 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
+
+#include <gtk/gtk.h>
+
+#define CC_TYPE_LIST_BOX_ROW (cc_list_box_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcListBoxRow, cc_list_box_row, CC, LIST_BOX_ROW, GtkListBoxRow)
+
+#define CC_TYPE_LIST_BOX (cc_list_box_get_type ())
+G_DECLARE_FINAL_TYPE (CcListBox, cc_list_box, CC, LIST_BOX, GtkListBox)
+
+void
+cc_list_box_update_header_func (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data);
+
+void
+cc_list_box_adjust_scrolling (GtkListBox *listbox);
+
+void
+cc_list_box_setup_scrolling (GtkListBox *listbox,
+ guint num_rows);
diff --git a/panels/common/meson.build b/panels/common/meson.build
new file mode 100644
index 0000000..85fabf2
--- /dev/null
+++ b/panels/common/meson.build
@@ -0,0 +1,121 @@
+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'
+)
+
+sources = files(
+ 'cc-hostname-entry.c',
+ 'cc-time-entry.c',
+ 'cc-os-release.c',
+ 'hostname-helper.c',
+ 'list-box-helper.c',
+)
+
+libwidgets = static_library(
+ 'widgets',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: common_deps + [ 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-list-row.c',
+ 'cc-time-editor.c',
+ 'cc-permission-infobar.c',
+ 'cc-util.c'
+)
+
+resource_data = files(
+ 'cc-language-chooser.ui',
+ 'cc-list-row.ui',
+ 'cc-time-editor.ui',
+ 'cc-permission-infobar.ui',
+)
+
+sources += gnome.compile_resources(
+ 'cc-common-resources',
+ 'common.gresource.xml',
+ c_name: 'cc_common',
+ dependencies: resource_data,
+ export: true
+)
+
+deps = common_deps + [
+ 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')
+)
+