diff options
Diffstat (limited to '')
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') +) + |