diff options
Diffstat (limited to 'src')
215 files changed, 80586 insertions, 0 deletions
diff --git a/src/calendar-server/README b/src/calendar-server/README new file mode 100644 index 0000000..ad9b5e3 --- /dev/null +++ b/src/calendar-server/README @@ -0,0 +1 @@ +Please keep in sync with gnome-panel. diff --git a/src/calendar-server/calendar-debug.h b/src/calendar-server/calendar-debug.h new file mode 100644 index 0000000..39befd7 --- /dev/null +++ b/src/calendar-server/calendar-debug.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2004 Free Software Foundation, 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/>. + * + * Authors: + * Mark McLoughlin <mark@skynet.ie> + */ + +#ifndef __CALENDAR_DEBUG_H__ +#define __CALENDAR_DEBUG_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +#ifdef CALENDAR_ENABLE_DEBUG + +#include <stdio.h> + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) fprintf (stderr, __VA_ARGS__); +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) fprintf (stderr, args); +#endif + +#else /* if !defined (CALENDAR_DEBUG) */ + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) +#endif + +#endif /* CALENDAR_ENABLE_DEBUG */ + +G_END_DECLS + +#endif /* __CALENDAR_DEBUG_H__ */ diff --git a/src/calendar-server/calendar-sources.c b/src/calendar-server/calendar-sources.c new file mode 100644 index 0000000..9c25f4e --- /dev/null +++ b/src/calendar-server/calendar-sources.c @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2004 Free Software Foundation, 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/>. + * + * Authors: + * Mark McLoughlin <mark@skynet.ie> + * William Jon McCann <mccann@jhu.edu> + * Martin Grimme <martin@pycage.de> + * Christian Kellner <gicmo@xatom.net> + */ + +#include <config.h> + +#include "calendar-sources.h" + +#include <libintl.h> +#include <string.h> +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include <libecal/libecal.h> +G_GNUC_END_IGNORE_DEPRECATIONS + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +typedef struct _ClientData ClientData; +typedef struct _CalendarSourceData CalendarSourceData; + +struct _ClientData +{ + ECalClient *client; + gulong backend_died_id; +}; + +typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate; + +struct _CalendarSources +{ + GObject parent; + + ESourceRegistryWatcher *registry_watcher; + gulong filter_id; + gulong appeared_id; + gulong disappeared_id; + + GMutex clients_lock; + GHashTable *clients; /* ESource -> ClientData */ +}; + +G_DEFINE_TYPE (CalendarSources, calendar_sources, G_TYPE_OBJECT) + +enum +{ + CLIENT_APPEARED, + CLIENT_DISAPPEARED, + LAST_SIGNAL +}; +static guint signals [LAST_SIGNAL] = { 0, }; + +static void +calendar_sources_client_connected_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + CalendarSources *sources = CALENDAR_SOURCES (source_object); + ESource *source = user_data; + EClient *client; + g_autoptr (GError) error = NULL; + + /* The calendar_sources_connect_client_sync() already stored the 'client' + * into the sources->clients */ + client = calendar_sources_connect_client_finish (sources, result, &error); + if (error) + { + g_warning ("Could not load source '%s': %s", + e_source_get_uid (source), + error->message); + } + else + { + g_signal_emit (sources, signals[CLIENT_APPEARED], 0, client, NULL); + } + + g_clear_object (&client); + g_clear_object (&source); +} + +static gboolean +registry_watcher_filter_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + return e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR) && + e_source_selectable_get_selected (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR)); +} + +static void +registry_watcher_source_appeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + ECalClientSourceType source_type; + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + else + g_return_if_reached (); + + calendar_sources_connect_client (sources, source, source_type, 30, NULL, calendar_sources_client_connected_cb, g_object_ref (source)); +} + +static void +registry_watcher_source_disappeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + gboolean emit; + + g_mutex_lock (&sources->clients_lock); + + emit = g_hash_table_remove (sources->clients, source); + + g_mutex_unlock (&sources->clients_lock); + + if (emit) + g_signal_emit (sources, signals[CLIENT_DISAPPEARED], 0, e_source_get_uid (source), NULL); +} + +static void +client_data_free (ClientData *data) +{ + g_signal_handler_disconnect (data->client, data->backend_died_id); + g_object_unref (data->client); + g_free (data); +} + +static void +calendar_sources_constructed (GObject *object) +{ + CalendarSources *sources = CALENDAR_SOURCES (object); + ESourceRegistry *registry = NULL; + GError *error = NULL; + + G_OBJECT_CLASS (calendar_sources_parent_class)->constructed (object); + + registry = e_source_registry_new_sync (NULL, &error); + if (error != NULL) + { + /* Any error is fatal, but we don't want to crash gnome-shell-calendar-server + because of e-d-s problems. So just exit here. + */ + g_warning ("Failed to start evolution-source-registry: %s", error->message); + exit (EXIT_FAILURE); + } + + g_return_if_fail (registry != NULL); + + sources->registry_watcher = e_source_registry_watcher_new (registry, NULL); + + g_clear_object (®istry); + + sources->clients = g_hash_table_new_full ((GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) client_data_free); + sources->filter_id = g_signal_connect (sources->registry_watcher, + "filter", + G_CALLBACK (registry_watcher_filter_cb), + sources); + sources->appeared_id = g_signal_connect (sources->registry_watcher, + "appeared", + G_CALLBACK (registry_watcher_source_appeared_cb), + sources); + sources->disappeared_id = g_signal_connect (sources->registry_watcher, + "disappeared", + G_CALLBACK (registry_watcher_source_disappeared_cb), + sources); + + e_source_registry_watcher_reclaim (sources->registry_watcher); +} + +static void +calendar_sources_finalize (GObject *object) +{ + CalendarSources *sources = CALENDAR_SOURCES (object); + + g_clear_pointer (&sources->clients, g_hash_table_destroy); + + if (sources->registry_watcher) + { + g_signal_handler_disconnect (sources->registry_watcher, + sources->filter_id); + g_signal_handler_disconnect (sources->registry_watcher, + sources->appeared_id); + g_signal_handler_disconnect (sources->registry_watcher, + sources->disappeared_id); + g_clear_object (&sources->registry_watcher); + } + + g_mutex_clear (&sources->clients_lock); + + G_OBJECT_CLASS (calendar_sources_parent_class)->finalize (object); +} + +static void +calendar_sources_class_init (CalendarSourcesClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->constructed = calendar_sources_constructed; + gobject_class->finalize = calendar_sources_finalize; + + signals [CLIENT_APPEARED] = + g_signal_new ("client-appeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + E_TYPE_CAL_CLIENT); + + signals [CLIENT_DISAPPEARED] = + g_signal_new ("client-disappeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); /* ESource::uid of the disappeared client */ +} + +static void +calendar_sources_init (CalendarSources *sources) +{ + g_mutex_init (&sources->clients_lock); +} + +CalendarSources * +calendar_sources_get (void) +{ + static CalendarSources *calendar_sources_singleton = NULL; + gpointer singleton_location = &calendar_sources_singleton; + + if (calendar_sources_singleton) + return g_object_ref (calendar_sources_singleton); + + calendar_sources_singleton = g_object_new (CALENDAR_TYPE_SOURCES, NULL); + g_object_add_weak_pointer (G_OBJECT (calendar_sources_singleton), + singleton_location); + + return calendar_sources_singleton; +} + +ESourceRegistry * +calendar_sources_get_registry (CalendarSources *sources) +{ + return e_source_registry_watcher_get_registry (sources->registry_watcher); +} + +static void +gather_event_clients_cb (gpointer key, + gpointer value, + gpointer user_data) +{ + GSList **plist = user_data; + ClientData *cd = value; + + if (cd) + *plist = g_slist_prepend (*plist, g_object_ref (cd->client)); +} + +GSList * +calendar_sources_ref_clients (CalendarSources *sources) +{ + GSList *list = NULL; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); + + g_mutex_lock (&sources->clients_lock); + g_hash_table_foreach (sources->clients, gather_event_clients_cb, &list); + g_mutex_unlock (&sources->clients_lock); + + return list; +} + +gboolean +calendar_sources_has_clients (CalendarSources *sources) +{ + GHashTableIter iter; + gpointer value; + gboolean has = FALSE; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE); + + g_mutex_lock (&sources->clients_lock); + + g_hash_table_iter_init (&iter, sources->clients); + while (!has && g_hash_table_iter_next (&iter, NULL, &value)) + { + ClientData *cd = value; + + has = cd != NULL; + } + + g_mutex_unlock (&sources->clients_lock); + + return has; +} + +static void +backend_died_cb (EClient *client, + CalendarSources *sources) +{ + ESource *source; + const char *display_name; + + source = e_client_get_source (client); + display_name = e_source_get_display_name (source); + g_warning ("The calendar backend for '%s' has crashed.", display_name); + g_mutex_lock (&sources->clients_lock); + g_hash_table_remove (sources->clients, source); + g_mutex_unlock (&sources->clients_lock); +} + +static EClient * +calendar_sources_connect_client_sync (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error) +{ + EClient *client = NULL; + ClientData *client_data; + + g_mutex_lock (&sources->clients_lock); + client_data = g_hash_table_lookup (sources->clients, source); + if (client_data) + client = E_CLIENT (g_object_ref (client_data->client)); + g_mutex_unlock (&sources->clients_lock); + + if (client) + return client; + + client = e_cal_client_connect_sync (source, source_type, wait_for_connected_seconds, cancellable, error); + if (!client) + return NULL; + + g_mutex_lock (&sources->clients_lock); + client_data = g_hash_table_lookup (sources->clients, source); + if (client_data) + { + g_clear_object (&client); + client = E_CLIENT (g_object_ref (client_data->client)); + } + else + { + client_data = g_new0 (ClientData, 1); + client_data->client = E_CAL_CLIENT (g_object_ref (client)); + client_data->backend_died_id = g_signal_connect (client, + "backend-died", + G_CALLBACK (backend_died_cb), + sources); + + g_hash_table_insert (sources->clients, g_object_ref (source), client_data); + } + g_mutex_unlock (&sources->clients_lock); + + return client; +} + +typedef struct _AsyncContext { + ESource *source; + ECalClientSourceType source_type; + guint32 wait_for_connected_seconds; +} AsyncContext; + +static void +async_context_free (gpointer ptr) +{ + AsyncContext *ctx = ptr; + + if (ctx) + { + g_clear_object (&ctx->source); + g_free (ctx); + } +} + +static void +calendar_sources_connect_client_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CalendarSources *sources = source_object; + AsyncContext *ctx = task_data; + EClient *client; + GError *local_error = NULL; + + client = calendar_sources_connect_client_sync (sources, ctx->source, ctx->source_type, + ctx->wait_for_connected_seconds, cancellable, &local_error); + if (!client) + { + if (local_error) + g_task_return_error (task, local_error); + else + g_task_return_pointer (task, NULL, NULL); + } else { + g_task_return_pointer (task, client, g_object_unref); + } +} + +void +calendar_sources_connect_client (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + AsyncContext *ctx; + g_autoptr (GTask) task = NULL; + + ctx = g_new0 (AsyncContext, 1); + ctx->source = g_object_ref (source); + ctx->source_type = source_type; + ctx->wait_for_connected_seconds = wait_for_connected_seconds; + + task = g_task_new (sources, cancellable, callback, user_data); + g_task_set_source_tag (task, calendar_sources_connect_client); + g_task_set_task_data (task, ctx, async_context_free); + + g_task_run_in_thread (task, calendar_sources_connect_client_thread); +} + +EClient * +calendar_sources_connect_client_finish (CalendarSources *sources, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, sources), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, calendar_sources_connect_client), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + +void +print_debug (const gchar *format, + ...) +{ + g_autofree char *s = NULL; + g_autofree char *timestamp = NULL; + va_list ap; + g_autoptr (GDateTime) now = NULL; + static size_t once_init_value = 0; + static gboolean show_debug = FALSE; + static guint pid = 0; + + if (g_once_init_enter (&once_init_value)) + { + show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL); + pid = getpid (); + g_once_init_leave (&once_init_value, 1); + } + + if (!show_debug) + goto out; + + now = g_date_time_new_now_local (); + timestamp = g_date_time_format (now, "%H:%M:%S"); + + va_start (ap, format); + s = g_strdup_vprintf (format, ap); + va_end (ap); + + g_print ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; +} diff --git a/src/calendar-server/calendar-sources.h b/src/calendar-server/calendar-sources.h new file mode 100644 index 0000000..1ffc8ad --- /dev/null +++ b/src/calendar-server/calendar-sources.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2004 Free Software Foundation, 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/>. + * + * Authors: + * Mark McLoughlin <mark@skynet.ie> + * William Jon McCann <mccann@jhu.edu> + * Martin Grimme <martin@pycage.de> + * Christian Kellner <gicmo@xatom.net> + */ + +#ifndef __CALENDAR_SOURCES_H__ +#define __CALENDAR_SOURCES_H__ + +#include <glib-object.h> + +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include <libedataserver/libedataserver.h> +#include <libecal/libecal.h> +G_GNUC_END_IGNORE_DEPRECATIONS + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ()) +G_DECLARE_FINAL_TYPE (CalendarSources, calendar_sources, + CALENDAR, SOURCES, GObject) + +CalendarSources *calendar_sources_get (void); +ESourceRegistry *calendar_sources_get_registry (CalendarSources *sources); +GSList *calendar_sources_ref_clients (CalendarSources *sources); +gboolean calendar_sources_has_clients (CalendarSources *sources); + +void calendar_sources_connect_client (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EClient *calendar_sources_connect_client_finish + (CalendarSources *sources, + GAsyncResult *result, + GError **error); + +/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */ +void print_debug (const gchar *str, + ...) G_GNUC_PRINTF (1, 2); + +G_END_DECLS + +#endif /* __CALENDAR_SOURCES_H__ */ diff --git a/src/calendar-server/evolution-calendar.desktop.in b/src/calendar-server/evolution-calendar.desktop.in new file mode 100644 index 0000000..1e34997 --- /dev/null +++ b/src/calendar-server/evolution-calendar.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Evolution Calendar +Exec=evolution -c calendar +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=evolution +NoDisplay=true +Type=Application +StartupNotify=true diff --git a/src/calendar-server/gnome-shell-calendar-server.c b/src/calendar-server/gnome-shell-calendar-server.c new file mode 100644 index 0000000..4cd28d1 --- /dev/null +++ b/src/calendar-server/gnome-shell-calendar-server.c @@ -0,0 +1,1131 @@ +/* + * 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/>. + * + * Author: David Zeuthen <davidz@redhat.com> + * + * Based on code from gnome-panel's clock-applet, file calendar-client.c, with Authors: + * + * Mark McLoughlin <mark@skynet.ie> + * William Jon McCann <mccann@jhu.edu> + * Martin Grimme <martin@pycage.de> + * Christian Kellner <gicmo@xatom.net> + * + */ + +#include "config.h" + +#include <string.h> +#include <sys/types.h> +#include <unistd.h> + +#include <gio/gio.h> + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include <libecal/libecal.h> +G_GNUC_END_IGNORE_DEPRECATIONS + +#include "calendar-sources.h" + +#define BUS_NAME "org.gnome.Shell.CalendarServer" + +static const gchar introspection_xml[] = + "<node>" + " <interface name='org.gnome.Shell.CalendarServer'>" + " <method name='SetTimeRange'>" + " <arg type='x' name='since' direction='in'/>" + " <arg type='x' name='until' direction='in'/>" + " <arg type='b' name='force_reload' direction='in'/>" + " </method>" + " <signal name='EventsAddedOrUpdated'>" + " <arg type='a(ssxxa{sv})' name='events' direction='out'/>" + " </signal>" + " <signal name='EventsRemoved'>" + " <arg type='as' name='ids' direction='out'/>" + " </signal>" + " <signal name='ClientDisappeared'>" + " <arg type='s' name='source_uid' direction='out'/>" + " </signal>" + " <property name='Since' type='x' access='read'/>" + " <property name='Until' type='x' access='read'/>" + " <property name='HasCalendars' type='b' access='read'/>" + " </interface>" + "</node>"; +static GDBusNodeInfo *introspection_data = NULL; + +struct _App; +typedef struct _App App; + +static gboolean opt_replace = FALSE; +static GOptionEntry opt_entries[] = { + {"replace", 0, 0, G_OPTION_ARG_NONE, &opt_replace, "Replace existing daemon", NULL}, + {NULL } +}; +static App *_global_app = NULL; + +/* ---------------------------------------------------------------------------------------------------- */ + +/* While the UID is usually enough to identify an event, + * only the triple of (source,UID,RID) is fully unambiguous; + * neither may contain '\n', so we can safely use it to + * create a unique ID from the triple + */ +static gchar * +create_event_id (const gchar *source_uid, + const gchar *comp_uid, + const gchar *comp_rid) +{ + return g_strconcat ( + source_uid ? source_uid : "", + "\n", + comp_uid ? comp_uid : "", + "\n", + comp_rid ? comp_rid : "", + NULL); +} + +typedef struct +{ + ECalClient *client; + GSList **pappointments; /* CalendarAppointment * */ +} CollectAppointmentsData; + +typedef struct +{ + gchar *id; + gchar *summary; + time_t start_time; + time_t end_time; +} CalendarAppointment; + +static gboolean +get_time_from_property (ECalClient *cal, + ICalComponent *icomp, + ICalPropertyKind prop_kind, + ICalTime * (* get_prop_func) (ICalProperty *prop), + ICalTimezone *default_zone, + ICalTime **out_itt, + ICalTimezone **out_timezone) +{ + ICalProperty *prop; + ICalTime *itt; + ICalTimezone *timezone = NULL; + + prop = i_cal_component_get_first_property (icomp, prop_kind); + if (!prop) + return FALSE; + + itt = get_prop_func (prop); + + if (i_cal_time_is_utc (itt)) + timezone = i_cal_timezone_get_utc_timezone (); + else + { + ICalParameter *param; + + param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER); + if (param && !e_cal_client_get_timezone_sync (cal, i_cal_parameter_get_tzid (param), &timezone, NULL, NULL)) + print_debug ("Failed to get timezone '%s'\n", i_cal_parameter_get_tzid (param)); + + g_clear_object (¶m); + } + + if (timezone == NULL) + timezone = default_zone; + + i_cal_time_set_timezone (itt, timezone); + + g_clear_object (&prop); + + *out_itt = itt; + *out_timezone = timezone; + + return TRUE; +} + +static inline time_t +get_ical_start_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *timezone; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &timezone)) + { + return 0; + } + + retval = i_cal_time_as_timet_with_zone (itt, timezone); + + g_clear_object (&itt); + + return retval; +} + +static inline time_t +get_ical_end_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *timezone; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, + default_zone, + &itt, + &timezone)) + { + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &timezone)) + { + return 0; + } + + if (i_cal_time_is_date (itt)) + i_cal_time_adjust (itt, 1, 0, 0, 0); + } + + retval = i_cal_time_as_timet_with_zone (itt, timezone); + + g_clear_object (&itt); + + return retval; +} + +static CalendarAppointment * +calendar_appointment_new (ECalClient *cal, + ECalComponent *comp) +{ + CalendarAppointment *appt; + ICalTimezone *default_zone; + ICalComponent *ical; + ECalComponentId *id; + + default_zone = e_cal_client_get_default_timezone (cal); + ical = e_cal_component_get_icalcomponent (comp); + id = e_cal_component_get_id (comp); + + appt = g_new0 (CalendarAppointment, 1); + + appt->id = create_event_id (e_source_get_uid (e_client_get_source (E_CLIENT (cal))), + id ? e_cal_component_id_get_uid (id) : NULL, + id ? e_cal_component_id_get_rid (id) : NULL); + appt->summary = g_strdup (i_cal_component_get_summary (ical)); + appt->start_time = get_ical_start_time (cal, ical, default_zone); + appt->end_time = get_ical_end_time (cal, ical, default_zone); + + e_cal_component_id_free (id); + + return appt; +} + +static void +calendar_appointment_free (gpointer ptr) +{ + CalendarAppointment *appt = ptr; + + if (appt) + { + g_free (appt->id); + g_free (appt->summary); + g_free (appt); + } +} + +static time_t +timet_from_ical_time (ICalTime *time, + ICalTimezone *default_zone) +{ + ICalTimezone *timezone = NULL; + + timezone = i_cal_time_get_timezone (time); + if (timezone == NULL) + timezone = default_zone; + return i_cal_time_as_timet_with_zone (time, timezone); +} + +static gboolean +generate_instances_cb (ICalComponent *icomp, + ICalTime *instance_start, + ICalTime *instance_end, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + CollectAppointmentsData *data = user_data; + CalendarAppointment *appointment; + ECalComponent *comp; + ICalTimezone *default_zone; + + default_zone = e_cal_client_get_default_timezone (data->client); + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); + + appointment = calendar_appointment_new (data->client, comp); + appointment->start_time = timet_from_ical_time (instance_start, default_zone); + appointment->end_time = timet_from_ical_time (instance_end, default_zone); + + *(data->pappointments) = g_slist_prepend (*(data->pappointments), appointment); + + g_clear_object (&comp); + + return TRUE; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +struct _App +{ + GDBusConnection *connection; + + time_t since; + time_t until; + + ICalTimezone *zone; + + CalendarSources *sources; + gulong client_appeared_signal_id; + gulong client_disappeared_signal_id; + + gchar *timezone_location; + + GSList *notify_appointments; /* CalendarAppointment *, for EventsAdded */ + GSList *notify_ids; /* gchar *, for EventsRemoved */ + + GSList *live_views; +}; + +static void +app_update_timezone (App *app) +{ + g_autofree char *location = NULL; + + location = e_cal_system_timezone_get_location (); + if (g_strcmp0 (location, app->timezone_location) != 0) + { + if (location == NULL) + app->zone = i_cal_timezone_get_utc_timezone (); + else + app->zone = i_cal_timezone_get_builtin_timezone (location); + g_free (app->timezone_location); + app->timezone_location = g_steal_pointer (&location); + print_debug ("Using timezone %s", app->timezone_location); + } +} + +static void +app_notify_events_added (App *app) +{ + GVariantBuilder builder, extras_builder; + GSList *events, *link; + + events = g_slist_reverse (app->notify_appointments); + app->notify_appointments = NULL; + + print_debug ("Emitting EventsAddedOrUpdated with %d events", g_slist_length (events)); + + if (!events) + return; + + /* The a{sv} is used as an escape hatch in case we want to provide more + * information in the future without breaking ABI + */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssxxa{sv})")); + for (link = events; link; link = g_slist_next (link)) + { + CalendarAppointment *appt = link->data; + time_t start_time = appt->start_time; + time_t end_time = appt->end_time; + + if ((start_time >= app->since && + start_time < app->until) || + (start_time <= app->since && + (end_time - 1) > app->since)) + { + g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&builder, + "(ssxxa{sv})", + appt->id, + appt->summary != NULL ? appt->summary : "", + (gint64) start_time, + (gint64) end_time, + &extras_builder); + } + } + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.CalendarServer", + "EventsAddedOrUpdated", + g_variant_new ("(a(ssxxa{sv}))", &builder), + NULL); + + g_variant_builder_clear (&builder); + + g_slist_free_full (events, calendar_appointment_free); +} + +static void +app_notify_events_removed (App *app) +{ + GVariantBuilder builder; + GSList *ids, *link; + + ids = app->notify_ids; + app->notify_ids = NULL; + + print_debug ("Emitting EventsRemoved with %d ids", g_slist_length (ids)); + + if (!ids) + return; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + for (link = ids; link; link = g_slist_next (link)) + { + const gchar *id = link->data; + + g_variant_builder_add (&builder, "s", id); + } + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.CalendarServer", + "EventsRemoved", + g_variant_new ("(as)", &builder), + NULL); + g_variant_builder_clear (&builder); + + g_slist_free_full (ids, g_free); + + return; +} + +static void +app_process_added_modified_objects (App *app, + ECalClientView *view, + GSList *objects) /* ICalComponent * */ +{ + ECalClient *cal_client; + g_autoptr(GHashTable) covered_uids = NULL; + GSList *link; + gboolean expand_recurrences; + + cal_client = e_cal_client_view_ref_client (view); + covered_uids = g_hash_table_new (g_str_hash, g_str_equal); + expand_recurrences = e_cal_client_get_source_type (cal_client) == E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + + for (link = objects; link; link = g_slist_next (link)) + { + ECalComponent *comp; + ICalComponent *icomp = link->data; + const gchar *uid; + gboolean fallback = FALSE; + + if (!icomp) + continue; + + uid = i_cal_component_get_uid (icomp); + if (!uid || g_hash_table_contains (covered_uids, uid)) + continue; + + g_hash_table_add (covered_uids, (gpointer) uid); + + if (expand_recurrences && + !e_cal_util_component_is_instance (icomp) && + e_cal_util_component_has_recurrences (icomp)) + { + CollectAppointmentsData data; + + data.client = cal_client; + data.pappointments = &app->notify_appointments; + + e_cal_client_generate_instances_for_object_sync (cal_client, icomp, app->since, app->until, NULL, + generate_instances_cb, &data); + } + else if (expand_recurrences && + e_cal_util_component_is_instance (icomp)) + { + ICalComponent *main_comp = NULL; + + /* Always pass whole series of the recurring events, because + * the calendar removes events with the same UID first. */ + if (e_cal_client_get_object_sync (cal_client, uid, NULL, &main_comp, NULL, NULL)) + { + CollectAppointmentsData data; + + data.client = cal_client; + data.pappointments = &app->notify_appointments; + + e_cal_client_generate_instances_for_object_sync (cal_client, main_comp, app->since, app->until, NULL, + generate_instances_cb, &data); + + g_clear_object (&main_comp); + } + else + { + fallback = TRUE; + } + } + else + { + fallback = TRUE; + } + + if (fallback) + { + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); + if (!comp) + continue; + + app->notify_appointments = g_slist_prepend (app->notify_appointments, + calendar_appointment_new (cal_client, comp)); + g_object_unref (comp); + } + } + + g_clear_object (&cal_client); + + if (app->notify_appointments) + app_notify_events_added (app); +} + +static void +on_objects_added (ECalClientView *view, + GSList *objects, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); + + app_process_added_modified_objects (app, view, objects); +} + +static void +on_objects_modified (ECalClientView *view, + GSList *objects, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); + + app_process_added_modified_objects (app, view, objects); +} + +static void +on_objects_removed (ECalClientView *view, + GSList *uids, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + GSList *link; + const gchar *source_uid; + + client = e_cal_client_view_ref_client (view); + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); + + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (uids), source_uid); + + for (link = uids; link; link = g_slist_next (link)) + { + ECalComponentId *id = link->data; + + if (!id) + continue; + + app->notify_ids = g_slist_prepend (app->notify_ids, + create_event_id (source_uid, + e_cal_component_id_get_uid (id), + e_cal_component_id_get_rid (id))); + } + + g_clear_object (&client); + + if (app->notify_ids) + app_notify_events_removed (app); +} + +static gboolean +app_has_calendars (App *app) +{ + return app->live_views != NULL; +} + +static ECalClientView * +app_start_view (App *app, + ECalClient *cal_client) +{ + g_autofree char *since_iso8601 = NULL; + g_autofree char *until_iso8601 = NULL; + g_autofree char *query = NULL; + const gchar *tz_location; + ECalClientView *view = NULL; + g_autoptr (GError) error = NULL; + + if (app->since <= 0 || app->since >= app->until) + return NULL; + + if (!app->since || !app->until) + { + print_debug ("Skipping load of events, no time interval set yet"); + return NULL; + } + + /* timezone could have changed */ + app_update_timezone (app); + + since_iso8601 = isodate_from_time_t (app->since); + until_iso8601 = isodate_from_time_t (app->until); + tz_location = i_cal_timezone_get_location (app->zone); + + print_debug ("Loading events since %s until %s for calendar '%s'", + since_iso8601, + until_iso8601, + e_source_get_uid (e_client_get_source (E_CLIENT (cal_client)))); + + query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " + "(make-time \"%s\") \"%s\"", + since_iso8601, + until_iso8601, + tz_location); + + e_cal_client_set_default_timezone (cal_client, app->zone); + + if (!e_cal_client_get_view_sync (cal_client, query, &view, NULL /* cancellable */, &error)) + { + g_warning ("Error setting up live-query '%s' on calendar: %s\n", query, error ? error->message : "Unknown error"); + view = NULL; + } + else + { + g_signal_connect (view, + "objects-added", + G_CALLBACK (on_objects_added), + app); + g_signal_connect (view, + "objects-modified", + G_CALLBACK (on_objects_modified), + app); + g_signal_connect (view, + "objects-removed", + G_CALLBACK (on_objects_removed), + app); + e_cal_client_view_start (view, NULL); + } + + return view; +} + +static void +app_stop_view (App *app, + ECalClientView *view) +{ + e_cal_client_view_stop (view, NULL); + + g_signal_handlers_disconnect_by_func (view, on_objects_added, app); + g_signal_handlers_disconnect_by_func (view, on_objects_modified, app); + g_signal_handlers_disconnect_by_func (view, on_objects_removed, app); +} + +static void +app_notify_has_calendars (App *app) +{ + GVariantBuilder dict_builder; + + g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars", + g_variant_new_boolean (app_has_calendars (app))); + + g_dbus_connection_emit_signal (app->connection, + NULL, + "/org/gnome/Shell/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + "org.gnome.Shell.CalendarServer", + &dict_builder, + NULL), + NULL); + g_variant_builder_clear (&dict_builder); +} + +static void +app_update_views (App *app) +{ + GSList *link, *clients; + gboolean had_views, has_views; + + had_views = app->live_views != NULL; + + for (link = app->live_views; link; link = g_slist_next (link)) + { + app_stop_view (app, link->data); + } + + g_slist_free_full (app->live_views, g_object_unref); + app->live_views = NULL; + + clients = calendar_sources_ref_clients (app->sources); + + for (link = clients; link; link = g_slist_next (link)) + { + ECalClient *cal_client = link->data; + ECalClientView *view; + + if (!cal_client) + continue; + + view = app_start_view (app, cal_client); + if (view) + app->live_views = g_slist_prepend (app->live_views, view); + } + + has_views = app->live_views != NULL; + + if (has_views != had_views) + app_notify_has_calendars (app); + + g_slist_free_full (clients, g_object_unref); +} + +static void +on_client_appeared_cb (CalendarSources *sources, + ECalClient *client, + gpointer user_data) +{ + App *app = user_data; + ECalClientView *view; + GSList *link; + const gchar *source_uid; + + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); + + print_debug ("Client appeared '%s'", source_uid); + + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *view = link->data; + ECalClient *cal_client; + ESource *source; + + cal_client = e_cal_client_view_ref_client (view); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + return; + } + + g_clear_object (&cal_client); + } + + view = app_start_view (app, client); + + if (view) + { + app->live_views = g_slist_prepend (app->live_views, view); + + /* It's the first view, notify that it has calendars now */ + if (!g_slist_next (app->live_views)) + app_notify_has_calendars (app); + } +} + +static void +on_client_disappeared_cb (CalendarSources *sources, + const gchar *source_uid, + gpointer user_data) +{ + App *app = user_data; + GSList *link; + + print_debug ("Client disappeared '%s'", source_uid); + + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *view = link->data; + ECalClient *cal_client; + ESource *source; + + cal_client = e_cal_client_view_ref_client (view); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + app_stop_view (app, view); + app->live_views = g_slist_remove (app->live_views, view); + g_object_unref (view); + + print_debug ("Emitting ClientDisappeared for '%s'", source_uid); + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.CalendarServer", + "ClientDisappeared", + g_variant_new ("(s)", source_uid), + NULL); + + /* It was the last view, notify that it doesn't have calendars now */ + if (!app->live_views) + app_notify_has_calendars (app); + + break; + } + + g_clear_object (&cal_client); + } +} + +static App * +app_new (GDBusConnection *connection) +{ + App *app; + + app = g_new0 (App, 1); + app->connection = g_object_ref (connection); + app->sources = calendar_sources_get (); + app->client_appeared_signal_id = g_signal_connect (app->sources, + "client-appeared", + G_CALLBACK (on_client_appeared_cb), + app); + app->client_disappeared_signal_id = g_signal_connect (app->sources, + "client-disappeared", + G_CALLBACK (on_client_disappeared_cb), + app); + + app_update_timezone (app); + + return app; +} + +static void +app_free (App *app) +{ + GSList *ll; + + for (ll = app->live_views; ll != NULL; ll = g_slist_next (ll)) + { + ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data); + + app_stop_view (app, view); + } + + g_signal_handler_disconnect (app->sources, + app->client_appeared_signal_id); + g_signal_handler_disconnect (app->sources, + app->client_disappeared_signal_id); + + g_free (app->timezone_location); + + g_slist_free_full (app->live_views, g_object_unref); + g_slist_free_full (app->notify_appointments, calendar_appointment_free); + g_slist_free_full (app->notify_ids, g_free); + + g_object_unref (app->connection); + g_object_unref (app->sources); + + g_free (app); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + App *app = user_data; + + if (g_strcmp0 (method_name, "SetTimeRange") == 0) + { + gint64 since; + gint64 until; + gboolean force_reload = FALSE; + gboolean window_changed = FALSE; + + g_variant_get (parameters, + "(xxb)", + &since, + &until, + &force_reload); + + if (until < since) + { + g_dbus_method_invocation_return_dbus_error (invocation, + "org.gnome.Shell.CalendarServer.Error.Failed", + "until cannot be before since"); + goto out; + } + + print_debug ("Handling SetTimeRange (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)", + since, + until, + force_reload ? "true" : "false"); + + if (app->until != until || app->since != since) + { + GVariantBuilder *builder; + GVariantBuilder *invalidated_builder; + + app->until = until; + app->since = since; + window_changed = TRUE; + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + invalidated_builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (builder, "{sv}", + "Until", g_variant_new_int64 (app->until)); + g_variant_builder_add (builder, "{sv}", + "Since", g_variant_new_int64 (app->since)); + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + "/org/gnome/Shell/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + "org.gnome.Shell.CalendarServer", + builder, + invalidated_builder), + NULL); /* GError** */ + + g_variant_builder_unref (builder); + g_variant_builder_unref (invalidated_builder); + } + + g_dbus_method_invocation_return_value (invocation, NULL); + + if (window_changed || force_reload) + app_update_views (app); + } + else + { + g_assert_not_reached (); + } + + out: + ; +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + App *app = user_data; + GVariant *ret; + + ret = NULL; + if (g_strcmp0 (property_name, "Since") == 0) + { + ret = g_variant_new_int64 (app->since); + } + else if (g_strcmp0 (property_name, "Until") == 0) + { + ret = g_variant_new_int64 (app->until); + } + else if (g_strcmp0 (property_name, "HasCalendars") == 0) + { + ret = g_variant_new_boolean (app_has_calendars (app)); + } + else + { + g_assert_not_reached (); + } + return ret; +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + handle_get_property, + NULL /* handle_set_property */ +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GMainLoop *main_loop = user_data; + guint registration_id; + g_autoptr (GError) error = NULL; + + _global_app = app_new (connection); + + registration_id = g_dbus_connection_register_object (connection, + "/org/gnome/Shell/CalendarServer", + introspection_data->interfaces[0], + &interface_vtable, + _global_app, + NULL, /* user_data_free_func */ + &error); + if (registration_id == 0) + { + g_printerr ("Error exporting object: %s (%s %d)\n", + error->message, + g_quark_to_string (error->domain), + error->code); + g_main_loop_quit (main_loop); + return; + } + + print_debug ("Connected to the session bus"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GMainLoop *main_loop = user_data; + + g_print ("gnome-shell-calendar-server[%d]: Lost (or failed to acquire) the name " BUS_NAME " - exiting\n", + (gint) getpid ()); + g_main_loop_quit (main_loop); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Acquired the name " BUS_NAME); +} + +static gboolean +stdin_channel_io_func (GIOChannel *source, + GIOCondition condition, + gpointer data) +{ + GMainLoop *main_loop = data; + + if (condition & G_IO_HUP) + { + g_debug ("gnome-shell-calendar-server[%d]: Got HUP on stdin - exiting\n", + (gint) getpid ()); + g_main_loop_quit (main_loop); + } + else + { + g_warning ("Unhandled condition %d on GIOChannel for stdin", condition); + } + return FALSE; /* remove source */ +} + +int +main (int argc, + char **argv) +{ + g_autoptr (GError) error = NULL; + GOptionContext *opt_context; + GMainLoop *main_loop; + gint ret; + guint name_owner_id; + GIOChannel *stdin_channel; + + ret = 1; + opt_context = NULL; + name_owner_id = 0; + stdin_channel = NULL; + + introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (introspection_data != NULL); + + opt_context = g_option_context_new ("gnome-shell calendar server"); + g_option_context_add_main_entries (opt_context, opt_entries, NULL); + if (!g_option_context_parse (opt_context, &argc, &argv, &error)) + { + g_printerr ("Error parsing options: %s\n", error->message); + goto out; + } + + main_loop = g_main_loop_new (NULL, FALSE); + + stdin_channel = g_io_channel_unix_new (STDIN_FILENO); + g_io_add_watch_full (stdin_channel, + G_PRIORITY_DEFAULT, + G_IO_HUP, + stdin_channel_io_func, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + on_bus_acquired, + on_name_acquired, + on_name_lost, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + + g_main_loop_run (main_loop); + + g_main_loop_unref (main_loop); + + ret = 0; + + out: + if (stdin_channel != NULL) + g_io_channel_unref (stdin_channel); + if (_global_app != NULL) + app_free (_global_app); + if (name_owner_id != 0) + g_bus_unown_name (name_owner_id); + if (opt_context != NULL) + g_option_context_free (opt_context); + + return ret; +} diff --git a/src/calendar-server/meson.build b/src/calendar-server/meson.build new file mode 100644 index 0000000..8b4ef41 --- /dev/null +++ b/src/calendar-server/meson.build @@ -0,0 +1,37 @@ +calendar_sources = [ + 'gnome-shell-calendar-server.c', + 'calendar-debug.h', + 'calendar-sources.c', + 'calendar-sources.h' +] + +calendar_server = executable('gnome-shell-calendar-server', calendar_sources, + dependencies: [ecal_dep, eds_dep, gio_dep], + include_directories: include_directories('..', '../..'), + c_args: [ + '-DPREFIX="@0@"'.format(prefix), + '-DLIBDIR="@0@"'.format(libdir), + '-DDATADIR="@0@"'.format(datadir), + '-DG_LOG_DOMAIN="ShellCalendarServer"' + ], + install_dir: libexecdir, + install: true +) + +service_file = 'org.gnome.Shell.CalendarServer.service' + +configure_file( + input: service_file + '.in', + output: service_file, + configuration: service_data, + install_dir: servicedir +) + +i18n.merge_file( + input: 'evolution-calendar.desktop.in', + output: 'evolution-calendar.desktop', + po_dir: po_dir, + install: true, + install_dir: desktopdir, + type: 'desktop' +) diff --git a/src/calendar-server/org.gnome.Shell.CalendarServer.service.in b/src/calendar-server/org.gnome.Shell.CalendarServer.service.in new file mode 100644 index 0000000..5addce6 --- /dev/null +++ b/src/calendar-server/org.gnome.Shell.CalendarServer.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Shell.CalendarServer +Exec=@libexecdir@/gnome-shell-calendar-server diff --git a/src/data-to-c.pl b/src/data-to-c.pl new file mode 100755 index 0000000..69f7436 --- /dev/null +++ b/src/data-to-c.pl @@ -0,0 +1,37 @@ +#!/usr/bin/env perl + +# Copyright © 2011 Red Hat, Inc +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the licence, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, see <http://www.gnu.org/licenses/>. +# +# Author: Kalev Lember <kalevlember@gmail.com> + + +if (@ARGV != 2) { + die "Usage: data-to-c.pl <filename> <variable>\n"; +} + +$file = $ARGV[0]; + +open (FILE, $file) || die "Cannot open $file: $!\n"; + +printf ("const char %s[] = \"", $ARGV[1]); +while (my $line = <FILE>) { + foreach my $c (split //, $line) { + printf ("\\x%02x", ord ($c)); + } +} +print "\";\n"; + +close (FILE); diff --git a/src/gnome-shell-extension-prefs b/src/gnome-shell-extension-prefs new file mode 100755 index 0000000..303b196 --- /dev/null +++ b/src/gnome-shell-extension-prefs @@ -0,0 +1,31 @@ +#!/bin/sh + +openPrefs() { + if [ "$(which gnome-extensions)" ] + then + gnome-extensions prefs $1 + else + gdbus call --session \ + --dest=org.gnome.Shell.Extensions \ + --object-path=/org/gnome/Shell/Extensions \ + --method=org.gnome.Shell.Extensions.OpenExtensionPrefs $1 '' '{}' + fi +} + +cat >&2 <<EOT +gnome-shell-extension-prefs is deprecated + +Install https://flathub.org/apps/details/org.gnome.Extensions for extension +management, or use the gnome-extensions command line tool. + +Extensions can use the ExtensionUtils.openPrefs() method. +EOT + +UUID=$1 + +if [ "$UUID" ] +then + openPrefs $UUID +else + gapplication launch org.gnome.Extensions +fi diff --git a/src/gnome-shell-extension-tool.in b/src/gnome-shell-extension-tool.in new file mode 100755 index 0000000..fb3d0d8 --- /dev/null +++ b/src/gnome-shell-extension-tool.in @@ -0,0 +1,59 @@ +#!@PYTHON@ +# -*- mode: Python; indent-tabs-mode: nil; -*- + +import subprocess +import sys +import optparse + +def extension_command(args): + print("gnome-shell-extension-tool is deprecated, use gnome-extensions instead", + file=sys.stderr) + subprocess.run(["@bindir@/gnome-extensions"] + args) + +def create_extension(): + extension_command(["create", "--interactive"]) + +def enable_extension(uuid): + extension_command(["enable", uuid]) + +def disable_extension(uuid): + extension_command(["disable", uuid]) + +def reload_extension(uuid): + print("Reloading extensions does not work correctly and is no longer supported", + file=sys.stderr) + +def main(): + parser = optparse.OptionParser() + parser.add_option("-d", "--disable-extension", dest="disable", + help="Disable a GNOME Shell extension") + parser.add_option("-e", "--enable-extension", dest="enable", + help="Enable a GNOME Shell extension") + parser.add_option("-c", "--create-extension", dest="create", action="store_true", + help="Create a new GNOME Shell extension") + parser.add_option("-r", "--reload-extension", dest="reload", + help="Reload a GNOME Shell extension") + options, args = parser.parse_args() + + if args: + parser.print_usage() + sys.exit(1) + + if options.disable: + disable_extension(options.disable) + + elif options.enable: + enable_extension(options.enable) + + elif options.create: + create_extension() + + elif options.reload: + reload_extension(options.reload) + + else: + parser.print_usage() + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/src/gnome-shell-perf-tool.in b/src/gnome-shell-perf-tool.in new file mode 100755 index 0000000..a1b5d59 --- /dev/null +++ b/src/gnome-shell-perf-tool.in @@ -0,0 +1,326 @@ +#!@PYTHON@ +# -*- mode: Python; indent-tabs-mode: nil; -*- + +import datetime +from gi.repository import GLib, GObject, Gio +try: + import json +except ImportError: + import simplejson as json +import optparse +import os +import re +import subprocess +import sys +import tempfile +import base64 +from configparser import RawConfigParser +import hashlib +import hmac +from http import client +from urllib import parse + +def show_version(option, opt_str, value, parser): + print("GNOME Shell Performance Test @VERSION@") + sys.exit() + +def start_shell(perf_output=None): + # Set up environment + env = dict(os.environ) + env['SHELL_PERF_MODULE'] = options.perf + + filters = ['Gnome-shell-perf-helper'] + options.extra_filter + env['MUTTER_WM_CLASS_FILTER'] = ','.join(filters) + + if perf_output is not None: + env['SHELL_PERF_OUTPUT'] = perf_output + + # A fixed background image + env['SHELL_BACKGROUND_IMAGE'] = '@pkgdatadir@/perf-background.xml' + + self_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + args = [] + args.append(os.path.join(self_dir, 'gnome-shell')) + + if options.replace: + args.append('--replace') + + if options.wayland or options.nested: + args.append('--wayland') + if options.nested: + args.append('--nested') + else: + args.append('--display-server') + elif options.x11: + args.append('--x11') + + return subprocess.Popen(args, env=env) + +def run_shell(perf_output=None): + # we do no additional supervision of gnome-shell, + # beyond that of wait + # in particular, we don't kill the shell upon + # receiving a KeyboardInterrupt, as we expect to be + # in the same process group + shell = start_shell(perf_output=perf_output) + shell.wait() + return shell.returncode == 0 + +def restore_shell(): + pid = os.fork() + if (pid == 0): + os.execlp("gnome-shell", "gnome-shell", "--replace") + else: + sys.exit(0) + +def upload_performance_report(report_text): + try: + config_home = os.environ['XDG_CONFIG_HOME'] + except KeyError: + config_home = None + + if not config_home: + config_home = os.path.expanduser("~/.config") + + config_file = os.path.join(config_home, "gnome-shell/perf.ini") + + try: + config = RawConfigParser() + f = open(config_file) + config.readfp(f) + f.close() + + base_url = config.get('upload', 'url') + system_name = config.get('upload', 'name') + secret_key = config.get('upload', 'key') + except Exception as e: + print("Can't read upload configuration from %s: %s" % (config_file, str(e))) + sys.exit(1) + + # Determine host, port and upload URL from provided data, we're + # a bit extra-careful about normalization since the URL is part + # of the signature. + + split = parse.urlsplit(base_url) + scheme = split[0].lower() + netloc = split[1] + base_path = split[2] + + m = re.match(r'^(.*?)(?::(\d+))?$', netloc) + if m.group(2): + host, port = m.group(1), int(m.group(2)) + else: + host, port = m.group(1), None + + if scheme != "http": + print("'%s' is not a HTTP URL" % base_url) + sys.exit(1) + + if port is None: + port = 80 + + if base_path.endswith('/'): + base_path = base_path[:-1] + + if port == 80: + normalized_base = "%s://%s%s" % (scheme, host, base_path) + else: + normalized_base = "%s://%s:%d%s" % (scheme, host, port, base_path) + + upload_url = normalized_base + '/system/%s/upload' % system_name + upload_path = parse.urlsplit(upload_url)[2] # path portion + + # Create signature based on upload URL and the report data + + signature_data = 'POST&' + upload_url + "&&" + h = hmac.new(secret_key, digestmod=hashlib.sha1) + h.update(signature_data) + h.update(report_text) + signature = parse.quote(base64.b64encode(h.digest()), "~") + + headers = { + 'User-Agent': 'gnome-shell-performance-tool/@VERSION@', + 'Content-Type': 'application/json', + 'X-Shell-Signature': 'HMAC-SHA1 ' + signature + }; + + connection = client.HTTPConnection(host, port) + connection.request('POST', upload_path, report_text, headers) + response = connection.getresponse() + + if response.status == 200: + print("Performance report upload succeeded") + else: + print("Performance report upload failed with status %d" % response.status) + print(response.read()) + +def gnome_hwtest_log(*args): + command = ['gnome-hwtest-log', '-t', 'gnome-shell-perf-tool'] + command.extend(args) + subprocess.check_call(command) + +def run_performance_test(): + iters = options.perf_iters + if options.perf_warmup: + iters += 1 + + logs = [] + metric_summaries = {} + + for i in range(0, iters): + # We create an empty temporary file that the shell will overwrite + # with the contents. + handle, output_file = tempfile.mkstemp(".json", "gnome-shell-perf.") + os.close(handle) + + # Run the performance test and collect the output as JSON + normal_exit = False + try: + normal_exit = run_shell(perf_output=output_file) + except: + raise + finally: + if not normal_exit: + os.remove(output_file) + + if not normal_exit: + return False + + try: + f = open(output_file) + output = json.load(f) + f.close() + except: + raise + finally: + os.remove(output_file) + + # Grab the event definitions and monitor layout the first time around + if i == 0: + events = output['events'] + monitors = output['monitors'] + + if options.perf_warmup and i == 0: + continue + + for metric in output['metrics']: + name = metric['name'] + if not name in metric_summaries: + summary = {} + summary['description'] = metric['description'] + summary['units'] = metric['units'] + summary['values'] = [] + metric_summaries[name] = summary + else: + summary = metric_summaries[name] + + summary['values'].append(metric['value']) + + logs.append(output['log']) + + if options.perf_output or options.perf_upload: + # Write a complete report, formatted as JSON. The Javascript/C code that + # generates the individual reports we are summarizing here is very careful + # to format them nicely, but we just dump out a compressed no-whitespace + # version here for simplicity. Using json.dump(indent=0) doesn't real + # improve the readability of the output much. + report = { + 'date': datetime.datetime.utcnow().isoformat() + 'Z', + 'events': events, + 'monitors': monitors, + 'metrics': metric_summaries, + 'logs': logs + } + + # Add the Git revision if available + self_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + if os.path.exists(os.path.join(self_dir, 'gnome-shell-jhbuild.in')): + top_dir = os.path.dirname(self_dir) + git_dir = os.path.join(top_dir, '.git') + if os.path.exists(git_dir): + env = dict(os.environ) + env['GIT_DIR'] = git_dir + revision = subprocess.Popen(['git', 'rev-parse', 'HEAD'], + env=env, + stdout=subprocess.PIPE).communicate()[0].strip() + report['revision'] = revision + + if options.perf_output: + f = open(options.perf_output, 'w') + json.dump(report, f) + f.close() + + if options.perf_upload: + upload_performance_report(json.dumps(report)) + elif options.hwtest: + # Log to systemd journal + for metric in sorted(metric_summaries.keys()): + summary = metric_summaries[metric] + gnome_hwtest_log('--metric=' + metric + '=' + str(summary['values'][0]) + summary['units'], + '--metric-description=' + summary['description']) + gnome_hwtest_log('--finished') + else: + # Write a human readable summary + print('------------------------------------------------------------') + for metric in sorted(metric_summaries.keys()): + summary = metric_summaries[metric] + print("#", summary['description']) + print(metric, ", ".join((str(x) for x in summary['values']))) + print('------------------------------------------------------------') + + return True + +# Main program + +parser = optparse.OptionParser() +parser.add_option("", "--perf", metavar="PERF_MODULE", + help="Specify the name of a performance module to run") +parser.add_option("", "--perf-iters", type="int", metavar="ITERS", + help="Numbers of iterations of performance module to run", + default=1) +parser.add_option("", "--perf-warmup", action="store_true", + help="Run a dry run before performance tests") +parser.add_option("", "--perf-output", metavar="OUTPUT_FILE", + help="Output file to write performance report") +parser.add_option("", "--perf-upload", action="store_true", + help="Upload performance report to server") +parser.add_option("", "--extra-filter", action="append", + help="add an extra window class that should be allowed") +parser.add_option("", "--hwtest", action="store_true", + help="Log results appropriately for GNOME Hardware Testing") +parser.add_option("", "--version", action="callback", callback=show_version, + help="Display version and exit") + +parser.add_option("-r", "--replace", action="store_true", + help="Replace the running window manager") +parser.add_option("-w", "--wayland", action="store_true", + help="Run as a Wayland compositor") +parser.add_option("-n", "--nested", action="store_true", + help="Run as a Wayland nested compositor") +parser.add_option("-x", "--x11", action="store_true", + help="Run as an X11 compositor") + +options, args = parser.parse_args() + +if options.perf == None: + if options.hwtest: + options.perf = 'hwtest' + else: + options.perf = 'core' + +if options.extra_filter is None: + options.extra_filter = [] + +if options.perf == 'hwtest': + options.extra_filter.append('Gedit') + +if args: + parser.print_usage() + sys.exit(1) + +normal_exit = run_performance_test() +if normal_exit: + if not options.hwtest: + restore_shell() +else: + sys.exit(1) diff --git a/src/gnome-shell-plugin.c b/src/gnome-shell-plugin.c new file mode 100644 index 0000000..5364f04 --- /dev/null +++ b/src/gnome-shell-plugin.c @@ -0,0 +1,394 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* + * Copyright (c) 2008 Red Hat, Inc. + * Copyright (c) 2008 Intel Corp. + * + * Based on plugin skeleton by: + * Author: Tomas Frydrych <tf@linux.intel.com> + * + * 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/>. + */ + +/* + * GnomeShellPlugin is the entry point for for GNOME Shell into and out of + * Mutter. By registering itself into Mutter using + * meta_plugin_manager_set_plugin_type(), Mutter will call the vfuncs of the + * plugin at the appropriate time. + * + * The functions in in GnomeShellPlugin are all just stubs, which just call the + * similar methods in GnomeShellWm. + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> + +#include <clutter/clutter.h> +#include <gjs/gjs.h> +#include <meta/display.h> +#include <meta/meta-plugin.h> +#include <meta/meta-x11-display.h> +#include <meta/util.h> + +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "shell-wm-private.h" + +#define GNOME_TYPE_SHELL_PLUGIN (gnome_shell_plugin_get_type ()) +G_DECLARE_FINAL_TYPE (GnomeShellPlugin, gnome_shell_plugin, + GNOME, SHELL_PLUGIN, + MetaPlugin) + +struct _GnomeShellPlugin +{ + MetaPlugin parent; + + int glx_error_base; + int glx_event_base; + guint have_swap_event : 1; + CoglContext *cogl_context; + + ShellGlobal *global; +}; + +G_DEFINE_TYPE (GnomeShellPlugin, gnome_shell_plugin, META_TYPE_PLUGIN) + +static gboolean +gnome_shell_plugin_has_swap_event (GnomeShellPlugin *shell_plugin) +{ + CoglDisplay *cogl_display = + cogl_context_get_display (shell_plugin->cogl_context); + CoglRenderer *renderer = cogl_display_get_renderer (cogl_display); + const char * (* query_extensions_string) (Display *dpy, int screen); + Bool (* query_extension) (Display *dpy, int *error, int *event); + MetaDisplay *display = meta_plugin_get_display (META_PLUGIN (shell_plugin)); + MetaX11Display *x11_display = meta_display_get_x11_display (display); + Display *xdisplay; + int screen_number; + const char *glx_extensions; + + /* We will only get swap events if Cogl is using GLX */ + if (cogl_renderer_get_winsys_id (renderer) != COGL_WINSYS_ID_GLX) + return FALSE; + + xdisplay = meta_x11_display_get_xdisplay (x11_display); + + query_extensions_string = + (void *) cogl_get_proc_address ("glXQueryExtensionsString"); + query_extension = + (void *) cogl_get_proc_address ("glXQueryExtension"); + + query_extension (xdisplay, + &shell_plugin->glx_error_base, + &shell_plugin->glx_event_base); + + screen_number = XDefaultScreen (xdisplay); + glx_extensions = query_extensions_string (xdisplay, screen_number); + + return strstr (glx_extensions, "GLX_INTEL_swap_event") != NULL; +} + +static void +gnome_shell_plugin_start (MetaPlugin *plugin) +{ + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + GError *error = NULL; + uint8_t status; + GjsContext *gjs_context; + ClutterBackend *backend; + + backend = clutter_get_default_backend (); + shell_plugin->cogl_context = clutter_backend_get_cogl_context (backend); + + shell_plugin->have_swap_event = + gnome_shell_plugin_has_swap_event (shell_plugin); + + shell_perf_log_define_event (shell_perf_log_get_default (), + "glx.swapComplete", + "GL buffer swap complete event received (with timestamp of completion)", + "x"); + + shell_plugin->global = shell_global_get (); + _shell_global_set_plugin (shell_plugin->global, META_PLUGIN (shell_plugin)); + + gjs_context = _shell_global_get_gjs_context (shell_plugin->global); + + if (!gjs_context_eval_module_file (gjs_context, + "resource:///org/gnome/shell/ui/init.js", + &status, + &error)) + { + g_message ("Execution of main.js threw exception: %s", error->message); + g_error_free (error); + /* We just exit() here, since in a development environment you'll get the + * error in your shell output, and it's way better than a busted WM, + * which typically manifests as a white screen. + * + * In production, we shouldn't crash =) But if we do, we should get + * restarted by the session infrastructure, which is likely going + * to be better than some undefined state. + * + * If there was a generic "hook into bug-buddy for non-C crashes" + * infrastructure, here would be the place to put it. + */ + g_object_unref (gjs_context); + exit (1); + } +} + +static ShellWM * +get_shell_wm (void) +{ + ShellWM *wm; + + g_object_get (shell_global_get (), + "window-manager", &wm, + NULL); + /* drop extra ref added by g_object_get */ + g_object_unref (wm); + + return wm; +} + +static void +gnome_shell_plugin_minimize (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_minimize (get_shell_wm (), + actor); + +} + +static void +gnome_shell_plugin_unminimize (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_unminimize (get_shell_wm (), + actor); + +} + +static void +gnome_shell_plugin_size_changed (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_size_changed (get_shell_wm (), actor); +} + +static void +gnome_shell_plugin_size_change (MetaPlugin *plugin, + MetaWindowActor *actor, + MetaSizeChange which_change, + MetaRectangle *old_frame_rect, + MetaRectangle *old_buffer_rect) +{ + _shell_wm_size_change (get_shell_wm (), actor, which_change, old_frame_rect, old_buffer_rect); +} + +static void +gnome_shell_plugin_map (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_map (get_shell_wm (), + actor); +} + +static void +gnome_shell_plugin_destroy (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_destroy (get_shell_wm (), + actor); +} + +static void +gnome_shell_plugin_switch_workspace (MetaPlugin *plugin, + gint from, + gint to, + MetaMotionDirection direction) +{ + _shell_wm_switch_workspace (get_shell_wm(), from, to, direction); +} + +static void +gnome_shell_plugin_kill_window_effects (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_kill_window_effects (get_shell_wm(), actor); +} + +static void +gnome_shell_plugin_kill_switch_workspace (MetaPlugin *plugin) +{ + _shell_wm_kill_switch_workspace (get_shell_wm()); +} + +static void +gnome_shell_plugin_show_tile_preview (MetaPlugin *plugin, + MetaWindow *window, + MetaRectangle *tile_rect, + int tile_monitor) +{ + _shell_wm_show_tile_preview (get_shell_wm (), window, tile_rect, tile_monitor); +} + +static void +gnome_shell_plugin_hide_tile_preview (MetaPlugin *plugin) +{ + _shell_wm_hide_tile_preview (get_shell_wm ()); +} + +static void +gnome_shell_plugin_show_window_menu (MetaPlugin *plugin, + MetaWindow *window, + MetaWindowMenuType menu, + int x, + int y) +{ + _shell_wm_show_window_menu (get_shell_wm (), window, menu, x, y); +} + +static void +gnome_shell_plugin_show_window_menu_for_rect (MetaPlugin *plugin, + MetaWindow *window, + MetaWindowMenuType menu, + MetaRectangle *rect) +{ + _shell_wm_show_window_menu_for_rect (get_shell_wm (), window, menu, rect); +} + +static gboolean +gnome_shell_plugin_xevent_filter (MetaPlugin *plugin, + XEvent *xev) +{ +#ifdef GLX_INTEL_swap_event + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + + if (shell_plugin->have_swap_event && + xev->type == (shell_plugin->glx_event_base + GLX_BufferSwapComplete)) + { + GLXBufferSwapComplete *swap_complete_event; + swap_complete_event = (GLXBufferSwapComplete *)xev; + + /* Buggy early versions of the INTEL_swap_event implementation in Mesa + * can send this with a ust of 0. Simplify life for consumers + * by ignoring such events */ + if (swap_complete_event->ust != 0) + { + gboolean frame_timestamps; + g_object_get (shell_plugin->global, + "frame-timestamps", &frame_timestamps, + NULL); + + if (frame_timestamps) + shell_perf_log_event_x (shell_perf_log_get_default (), + "glx.swapComplete", + swap_complete_event->ust); + } + } +#endif + + return FALSE; +} + +static gboolean +gnome_shell_plugin_keybinding_filter (MetaPlugin *plugin, + MetaKeyBinding *binding) +{ + return _shell_wm_filter_keybinding (get_shell_wm (), binding); +} + +static void +gnome_shell_plugin_confirm_display_change (MetaPlugin *plugin) +{ + _shell_wm_confirm_display_change (get_shell_wm ()); +} + +static const MetaPluginInfo * +gnome_shell_plugin_plugin_info (MetaPlugin *plugin) +{ + static const MetaPluginInfo info = { + .name = "GNOME Shell", + .version = "0.1", + .author = "Various", + .license = "GPLv2+", + .description = "Provides GNOME Shell core functionality" + }; + + return &info; +} + +static MetaCloseDialog * +gnome_shell_plugin_create_close_dialog (MetaPlugin *plugin, + MetaWindow *window) +{ + return _shell_wm_create_close_dialog (get_shell_wm (), window); +} + +static MetaInhibitShortcutsDialog * +gnome_shell_plugin_create_inhibit_shortcuts_dialog (MetaPlugin *plugin, + MetaWindow *window) +{ + return _shell_wm_create_inhibit_shortcuts_dialog (get_shell_wm (), window); +} + +static void +gnome_shell_plugin_locate_pointer (MetaPlugin *plugin) +{ + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + _shell_global_locate_pointer (shell_plugin->global); +} + +static void +gnome_shell_plugin_class_init (GnomeShellPluginClass *klass) +{ + MetaPluginClass *plugin_class = META_PLUGIN_CLASS (klass); + + plugin_class->start = gnome_shell_plugin_start; + plugin_class->map = gnome_shell_plugin_map; + plugin_class->minimize = gnome_shell_plugin_minimize; + plugin_class->unminimize = gnome_shell_plugin_unminimize; + plugin_class->size_changed = gnome_shell_plugin_size_changed; + plugin_class->size_change = gnome_shell_plugin_size_change; + plugin_class->destroy = gnome_shell_plugin_destroy; + + plugin_class->switch_workspace = gnome_shell_plugin_switch_workspace; + + plugin_class->kill_window_effects = gnome_shell_plugin_kill_window_effects; + plugin_class->kill_switch_workspace = gnome_shell_plugin_kill_switch_workspace; + + plugin_class->show_tile_preview = gnome_shell_plugin_show_tile_preview; + plugin_class->hide_tile_preview = gnome_shell_plugin_hide_tile_preview; + plugin_class->show_window_menu = gnome_shell_plugin_show_window_menu; + plugin_class->show_window_menu_for_rect = gnome_shell_plugin_show_window_menu_for_rect; + + plugin_class->xevent_filter = gnome_shell_plugin_xevent_filter; + plugin_class->keybinding_filter = gnome_shell_plugin_keybinding_filter; + + plugin_class->confirm_display_change = gnome_shell_plugin_confirm_display_change; + + plugin_class->plugin_info = gnome_shell_plugin_plugin_info; + + plugin_class->create_close_dialog = gnome_shell_plugin_create_close_dialog; + plugin_class->create_inhibit_shortcuts_dialog = gnome_shell_plugin_create_inhibit_shortcuts_dialog; + + plugin_class->locate_pointer = gnome_shell_plugin_locate_pointer; +} + +static void +gnome_shell_plugin_init (GnomeShellPlugin *shell_plugin) +{ +} diff --git a/src/gnome-shell-portal-helper.c b/src/gnome-shell-portal-helper.c new file mode 100644 index 0000000..a0bebb2 --- /dev/null +++ b/src/gnome-shell-portal-helper.c @@ -0,0 +1,52 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <gjs/gjs.h> +#include <glib/gi18n.h> + +int +main (int argc, char *argv[]) +{ + const char *search_path[] = { "resource:///org/gnome/shell", NULL }; + GError *error = NULL; + GjsContext *context; + int status; + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + context = g_object_new (GJS_TYPE_CONTEXT, + "search-path", search_path, + NULL); + + if (!gjs_context_define_string_array(context, "ARGV", + argc, (const char**)argv, + &error)) + { + g_message("Failed to define ARGV: %s", error->message); + g_error_free (error); + g_object_unref (context); + + return 1; + } + + + if (!gjs_context_eval (context, + "const Main = imports.portalHelper.main; Main.main(ARGV);", + -1, + "<main>", + &status, + &error)) + { + g_message ("Execution of main.js threw exception: %s", error->message); + g_error_free (error); + g_object_unref (context); + + return status; + } + + g_object_unref (context); + return 0; +} diff --git a/src/gtkactionmuxer.c b/src/gtkactionmuxer.c new file mode 100644 index 0000000..7e3e86e --- /dev/null +++ b/src/gtkactionmuxer.c @@ -0,0 +1,945 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Ryan Lortie <desrt@desrt.ca> + */ + +#include "config.h" + +#include "gtkactionmuxer.h" + +#include "gtkactionobservable.h" +#include "gtkactionobserver.h" + +#include <clutter/clutter.h> + +#include <string.h> + +/** + * SECTION:gtkactionmuxer + * @short_description: Aggregate and monitor several action groups + * + * #GtkActionMuxer is a #GActionGroup and #GtkActionObservable that is + * capable of containing other #GActionGroup instances. + * + * The typical use is aggregating all of the actions applicable to a + * particular context into a single action group, with namespacing. + * + * Consider the case of two action groups -- one containing actions + * applicable to an entire application (such as 'quit') and one + * containing actions applicable to a particular window in the + * application (such as 'fullscreen'). + * + * In this case, each of these action groups could be added to a + * #GtkActionMuxer with the prefixes "app" and "win", respectively. This + * would expose the actions as "app.quit" and "win.fullscreen" on the + * #GActionGroup interface presented by the #GtkActionMuxer. + * + * Activations and state change requests on the #GtkActionMuxer are wired + * through to the underlying action group in the expected way. + * + * This class is typically only used at the site of "consumption" of + * actions (eg: when displaying a menu that contains many actions on + * different objects). + */ + +static void gtk_action_muxer_group_iface_init (GActionGroupInterface *iface); +static void gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface); + +typedef GObjectClass GtkActionMuxerClass; + +struct _GtkActionMuxer +{ + GObject parent_instance; + + GHashTable *observed_actions; + GHashTable *groups; + GHashTable *primary_accels; + GtkActionMuxer *parent; +}; + +G_DEFINE_TYPE_WITH_CODE (GtkActionMuxer, gtk_action_muxer, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, gtk_action_muxer_group_iface_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVABLE, gtk_action_muxer_observable_iface_init)) + +enum +{ + PROP_0, + PROP_PARENT, + NUM_PROPERTIES +}; + +static GParamSpec *properties[NUM_PROPERTIES]; + +guint accel_signal; + +typedef struct +{ + GtkActionMuxer *muxer; + GSList *watchers; + gchar *fullname; +} Action; + +typedef struct +{ + GtkActionMuxer *muxer; + GActionGroup *group; + gchar *prefix; + gulong handler_ids[4]; +} Group; + +static void +gtk_action_muxer_append_group_actions (gpointer key, + gpointer value, + gpointer user_data) +{ + const gchar *prefix = key; + Group *group = value; + GArray *actions = user_data; + gchar **group_actions; + gchar **action; + + group_actions = g_action_group_list_actions (group->group); + for (action = group_actions; *action; action++) + { + gchar *fullname; + + fullname = g_strconcat (prefix, ".", *action, NULL); + g_array_append_val (actions, fullname); + } + + g_strfreev (group_actions); +} + +static gchar ** +gtk_action_muxer_list_actions (GActionGroup *action_group) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + GArray *actions; + + actions = g_array_new (TRUE, FALSE, sizeof (gchar *)); + + for ( ; muxer != NULL; muxer = muxer->parent) + { + g_hash_table_foreach (muxer->groups, + gtk_action_muxer_append_group_actions, + actions); + } + + return (gchar **)(void *) g_array_free (actions, FALSE); +} + +static Group * +gtk_action_muxer_find_group (GtkActionMuxer *muxer, + const gchar *full_name, + const gchar **action_name) +{ + const gchar *dot; + gchar *prefix; + Group *group; + + dot = strchr (full_name, '.'); + + if (!dot) + return NULL; + + prefix = g_strndup (full_name, dot - full_name); + group = g_hash_table_lookup (muxer->groups, prefix); + g_free (prefix); + + if (action_name) + *action_name = dot + 1; + + return group; +} + +static void +gtk_action_muxer_action_enabled_changed (GtkActionMuxer *muxer, + const gchar *action_name, + gboolean enabled) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_enabled_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, enabled); + g_action_group_action_enabled_changed (G_ACTION_GROUP (muxer), action_name, enabled); +} + +static void +gtk_action_muxer_group_action_enabled_changed (GActionGroup *action_group, + const gchar *action_name, + gboolean enabled, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_enabled_changed (group->muxer, fullname, enabled); + + g_free (fullname); +} + +static void +gtk_action_muxer_parent_action_enabled_changed (GActionGroup *action_group, + const gchar *action_name, + gboolean enabled, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_enabled_changed (muxer, action_name, enabled); +} + +static void +gtk_action_muxer_action_state_changed (GtkActionMuxer *muxer, + const gchar *action_name, + GVariant *state) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_state_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, state); + g_action_group_action_state_changed (G_ACTION_GROUP (muxer), action_name, state); +} + +static void +gtk_action_muxer_group_action_state_changed (GActionGroup *action_group, + const gchar *action_name, + GVariant *state, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_state_changed (group->muxer, fullname, state); + + g_free (fullname); +} + +static void +gtk_action_muxer_parent_action_state_changed (GActionGroup *action_group, + const gchar *action_name, + GVariant *state, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_state_changed (muxer, action_name, state); +} + +static void +gtk_action_muxer_action_added (GtkActionMuxer *muxer, + const gchar *action_name, + GActionGroup *original_group, + const gchar *orignal_action_name) +{ + const GVariantType *parameter_type; + gboolean enabled; + GVariant *state; + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + + if (action && action->watchers && + g_action_group_query_action (original_group, orignal_action_name, + &enabled, ¶meter_type, NULL, NULL, &state)) + { + GSList *node; + + for (node = action->watchers; node; node = node->next) + gtk_action_observer_action_added (node->data, + GTK_ACTION_OBSERVABLE (muxer), + action_name, parameter_type, enabled, state); + + if (state) + g_variant_unref (state); + } + + g_action_group_action_added (G_ACTION_GROUP (muxer), action_name); +} + +static void +gtk_action_muxer_action_added_to_group (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_added (group->muxer, fullname, action_group, action_name); + + g_free (fullname); +} + +static void +gtk_action_muxer_action_added_to_parent (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_added (muxer, action_name, action_group, action_name); +} + +static void +gtk_action_muxer_action_removed (GtkActionMuxer *muxer, + const gchar *action_name) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_removed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name); + g_action_group_action_removed (G_ACTION_GROUP (muxer), action_name); +} + +static void +gtk_action_muxer_action_removed_from_group (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_removed (group->muxer, fullname); + + g_free (fullname); +} + +static void +gtk_action_muxer_action_removed_from_parent (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_removed (muxer, action_name); +} + +static void +gtk_action_muxer_primary_accel_changed (GtkActionMuxer *muxer, + const gchar *action_name, + const gchar *action_and_target) +{ + Action *action; + GSList *node; + + if (!action_name) + action_name = strrchr (action_and_target, '|') + 1; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_primary_accel_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), + action_name, action_and_target); + g_signal_emit (muxer, accel_signal, 0, action_name, action_and_target); +} + +static void +gtk_action_muxer_parent_primary_accel_changed (GtkActionMuxer *parent, + const gchar *action_name, + const gchar *action_and_target, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + /* If it's in our table then don't let the parent one filter through */ + if (muxer->primary_accels && g_hash_table_lookup (muxer->primary_accels, action_and_target)) + return; + + gtk_action_muxer_primary_accel_changed (muxer, action_name, action_and_target); +} + +static gboolean +gtk_action_muxer_query_action (GActionGroup *action_group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + return g_action_group_query_action (group->group, unprefixed_name, enabled, + parameter_type, state_type, state_hint, state); + + if (muxer->parent) + return g_action_group_query_action (G_ACTION_GROUP (muxer->parent), action_name, + enabled, parameter_type, + state_type, state_hint, state); + + return FALSE; +} + +static GVariant * +get_platform_data (void) +{ + gchar time[32]; + GVariantBuilder *builder; + GVariant *result; + + g_snprintf (time, 32, "_TIME%d", clutter_get_current_event_time ()); + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (builder, "{sv}", "desktop-startup-id", + g_variant_new_string (time)); + + result = g_variant_builder_end (builder); + g_variant_builder_unref (builder); + + return result; +} + +static void +gtk_action_muxer_activate_action (GActionGroup *action_group, + const gchar *action_name, + GVariant *parameter) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + { + if (G_IS_REMOTE_ACTION_GROUP (group->group)) + g_remote_action_group_activate_action_full (G_REMOTE_ACTION_GROUP (group->group), + unprefixed_name, parameter, + get_platform_data ()); + else + g_action_group_activate_action (group->group, unprefixed_name, parameter); + } + else if (muxer->parent) + g_action_group_activate_action (G_ACTION_GROUP (muxer->parent), action_name, parameter); +} + +static void +gtk_action_muxer_change_action_state (GActionGroup *action_group, + const gchar *action_name, + GVariant *state) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + { + if (G_IS_REMOTE_ACTION_GROUP (group->group)) + g_remote_action_group_change_action_state_full (G_REMOTE_ACTION_GROUP (group->group), + unprefixed_name, + state, + get_platform_data ()); + else + g_action_group_change_action_state (group->group, unprefixed_name, state); + } + else if (muxer->parent) + g_action_group_change_action_state (G_ACTION_GROUP (muxer->parent), action_name, state); +} + +static void +gtk_action_muxer_unregister_internal (Action *action, + gpointer observer) +{ + GtkActionMuxer *muxer = action->muxer; + GSList **ptr; + + for (ptr = &action->watchers; *ptr; ptr = &(*ptr)->next) + if ((*ptr)->data == observer) + { + *ptr = g_slist_remove (*ptr, observer); + + if (action->watchers == NULL) + g_hash_table_remove (muxer->observed_actions, action->fullname); + + break; + } +} + +static void +gtk_action_muxer_weak_notify (gpointer data, + GObject *where_the_object_was) +{ + Action *action = data; + + gtk_action_muxer_unregister_internal (action, where_the_object_was); +} + +static void +gtk_action_muxer_register_observer (GtkActionObservable *observable, + const gchar *name, + GtkActionObserver *observer) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable); + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, name); + + if (action == NULL) + { + action = g_new (Action, 1); + action->muxer = muxer; + action->fullname = g_strdup (name); + action->watchers = NULL; + + g_hash_table_insert (muxer->observed_actions, action->fullname, action); + } + + action->watchers = g_slist_prepend (action->watchers, observer); + g_object_weak_ref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action); +} + +static void +gtk_action_muxer_unregister_observer (GtkActionObservable *observable, + const gchar *name, + GtkActionObserver *observer) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable); + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, name); + g_object_weak_unref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action); + gtk_action_muxer_unregister_internal (action, observer); +} + +static void +gtk_action_muxer_free_group (gpointer data) +{ + Group *group = data; + gint i; + + /* 'for loop' or 'four loop'? */ + for (i = 0; i < 4; i++) + g_clear_signal_handler (&group->handler_ids[i], group->group); + + g_object_unref (group->group); + g_free (group->prefix); + + g_free (group); +} + +static void +gtk_action_muxer_free_action (gpointer data) +{ + Action *action = data; + GSList *it; + + for (it = action->watchers; it; it = it->next) + g_object_weak_unref (G_OBJECT (it->data), gtk_action_muxer_weak_notify, action); + + g_slist_free (action->watchers); + g_free (action->fullname); + + g_free (action); +} + +static void +gtk_action_muxer_finalize (GObject *object) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + g_assert_cmpint (g_hash_table_size (muxer->observed_actions), ==, 0); + g_hash_table_unref (muxer->observed_actions); + g_hash_table_unref (muxer->groups); + + G_OBJECT_CLASS (gtk_action_muxer_parent_class) + ->finalize (object); +} + +static void +gtk_action_muxer_dispose (GObject *object) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + if (muxer->parent) + { + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_primary_accel_changed, muxer); + + g_clear_object (&muxer->parent); + } + + g_hash_table_remove_all (muxer->observed_actions); + + G_OBJECT_CLASS (gtk_action_muxer_parent_class) + ->dispose (object); +} + +static void +gtk_action_muxer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + switch (property_id) + { + case PROP_PARENT: + g_value_set_object (value, gtk_action_muxer_get_parent (muxer)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtk_action_muxer_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + switch (property_id) + { + case PROP_PARENT: + gtk_action_muxer_set_parent (muxer, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtk_action_muxer_init (GtkActionMuxer *muxer) +{ + muxer->observed_actions = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_action); + muxer->groups = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_group); +} + +static void +gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface) +{ + iface->register_observer = gtk_action_muxer_register_observer; + iface->unregister_observer = gtk_action_muxer_unregister_observer; +} + +static void +gtk_action_muxer_group_iface_init (GActionGroupInterface *iface) +{ + iface->list_actions = gtk_action_muxer_list_actions; + iface->query_action = gtk_action_muxer_query_action; + iface->activate_action = gtk_action_muxer_activate_action; + iface->change_action_state = gtk_action_muxer_change_action_state; +} + +static void +gtk_action_muxer_class_init (GObjectClass *class) +{ + class->get_property = gtk_action_muxer_get_property; + class->set_property = gtk_action_muxer_set_property; + class->finalize = gtk_action_muxer_finalize; + class->dispose = gtk_action_muxer_dispose; + + accel_signal = g_signal_new ("primary-accel-changed", GTK_TYPE_ACTION_MUXER, G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); + + properties[PROP_PARENT] = g_param_spec_object ("parent", "Parent", + "The parent muxer", + GTK_TYPE_ACTION_MUXER, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (class, NUM_PROPERTIES, properties); +} + +/** + * gtk_action_muxer_insert: + * @muxer: a #GtkActionMuxer + * @prefix: the prefix string for the action group + * @action_group: a #GActionGroup + * + * Adds the actions in @action_group to the list of actions provided by + * @muxer. @prefix is prefixed to each action name, such that for each + * action <varname>x</varname> in @action_group, there is an equivalent + * action @prefix<literal>.</literal><varname>x</varname> in @muxer. + * + * For example, if @prefix is "<literal>app</literal>" and @action_group + * contains an action called "<literal>quit</literal>", then @muxer will + * now contain an action called "<literal>app.quit</literal>". + * + * If any #GtkActionObservers are registered for actions in the group, + * "action_added" notifications will be emitted, as appropriate. + * + * @prefix must not contain a dot ('.'). + */ +void +gtk_action_muxer_insert (GtkActionMuxer *muxer, + const gchar *prefix, + GActionGroup *action_group) +{ + gchar **actions; + Group *group; + gint i; + + /* TODO: diff instead of ripout and replace */ + gtk_action_muxer_remove (muxer, prefix); + + group = g_new (Group, 1); + group->muxer = muxer; + group->group = g_object_ref (action_group); + group->prefix = g_strdup (prefix); + + g_hash_table_insert (muxer->groups, group->prefix, group); + + actions = g_action_group_list_actions (group->group); + for (i = 0; actions[i]; i++) + gtk_action_muxer_action_added_to_group (group->group, actions[i], group); + g_strfreev (actions); + + group->handler_ids[0] = g_signal_connect (group->group, "action-added", + G_CALLBACK (gtk_action_muxer_action_added_to_group), group); + group->handler_ids[1] = g_signal_connect (group->group, "action-removed", + G_CALLBACK (gtk_action_muxer_action_removed_from_group), group); + group->handler_ids[2] = g_signal_connect (group->group, "action-enabled-changed", + G_CALLBACK (gtk_action_muxer_group_action_enabled_changed), group); + group->handler_ids[3] = g_signal_connect (group->group, "action-state-changed", + G_CALLBACK (gtk_action_muxer_group_action_state_changed), group); +} + +/** + * gtk_action_muxer_remove: + * @muxer: a #GtkActionMuxer + * @prefix: the prefix of the action group to remove + * + * Removes a #GActionGroup from the #GtkActionMuxer. + * + * If any #GtkActionObservers are registered for actions in the group, + * "action_removed" notifications will be emitted, as appropriate. + */ +void +gtk_action_muxer_remove (GtkActionMuxer *muxer, + const gchar *prefix) +{ + Group *group; + + group = g_hash_table_lookup (muxer->groups, prefix); + + if (group != NULL) + { + gchar **actions; + gint i; + + g_hash_table_steal (muxer->groups, prefix); + + actions = g_action_group_list_actions (group->group); + for (i = 0; actions[i]; i++) + gtk_action_muxer_action_removed_from_group (group->group, actions[i], group); + g_strfreev (actions); + + gtk_action_muxer_free_group (group); + } +} + +/** + * gtk_action_muxer_new: + * + * Creates a new #GtkActionMuxer. + */ +GtkActionMuxer * +gtk_action_muxer_new (void) +{ + return g_object_new (GTK_TYPE_ACTION_MUXER, NULL); +} + +/** + * gtk_action_muxer_get_parent: + * @muxer: a #GtkActionMuxer + * + * Returns: (transfer none): the parent of @muxer, or NULL. + */ +GtkActionMuxer * +gtk_action_muxer_get_parent (GtkActionMuxer *muxer) +{ + g_return_val_if_fail (GTK_IS_ACTION_MUXER (muxer), NULL); + + return muxer->parent; +} + +static void +emit_changed_accels (GtkActionMuxer *muxer, + GtkActionMuxer *parent) +{ + while (parent) + { + if (parent->primary_accels) + { + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, parent->primary_accels); + while (g_hash_table_iter_next (&iter, &key, NULL)) + gtk_action_muxer_primary_accel_changed (muxer, NULL, key); + } + + parent = parent->parent; + } +} + +/** + * gtk_action_muxer_set_parent: + * @muxer: a #GtkActionMuxer + * @parent: (nullable): the new parent #GtkActionMuxer + * + * Sets the parent of @muxer to @parent. + */ +void +gtk_action_muxer_set_parent (GtkActionMuxer *muxer, + GtkActionMuxer *parent) +{ + g_return_if_fail (GTK_IS_ACTION_MUXER (muxer)); + g_return_if_fail (parent == NULL || GTK_IS_ACTION_MUXER (parent)); + + if (muxer->parent == parent) + return; + + if (muxer->parent != NULL) + { + gchar **actions; + gchar **it; + + actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent)); + for (it = actions; *it; it++) + gtk_action_muxer_action_removed (muxer, *it); + g_strfreev (actions); + + emit_changed_accels (muxer, muxer->parent); + + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_primary_accel_changed, muxer); + + g_object_unref (muxer->parent); + } + + muxer->parent = parent; + + if (muxer->parent != NULL) + { + gchar **actions; + gchar **it; + + g_object_ref (muxer->parent); + + actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent)); + for (it = actions; *it; it++) + gtk_action_muxer_action_added (muxer, *it, G_ACTION_GROUP (muxer->parent), *it); + g_strfreev (actions); + + emit_changed_accels (muxer, muxer->parent); + + g_signal_connect (muxer->parent, "action-added", + G_CALLBACK (gtk_action_muxer_action_added_to_parent), muxer); + g_signal_connect (muxer->parent, "action-removed", + G_CALLBACK (gtk_action_muxer_action_removed_from_parent), muxer); + g_signal_connect (muxer->parent, "action-enabled-changed", + G_CALLBACK (gtk_action_muxer_parent_action_enabled_changed), muxer); + g_signal_connect (muxer->parent, "action-state-changed", + G_CALLBACK (gtk_action_muxer_parent_action_state_changed), muxer); + g_signal_connect (muxer->parent, "primary-accel-changed", + G_CALLBACK (gtk_action_muxer_parent_primary_accel_changed), muxer); + } + + g_object_notify_by_pspec (G_OBJECT (muxer), properties[PROP_PARENT]); +} + +void +gtk_action_muxer_set_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target, + const gchar *primary_accel) +{ + if (!muxer->primary_accels) + muxer->primary_accels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + if (primary_accel) + g_hash_table_insert (muxer->primary_accels, g_strdup (action_and_target), g_strdup (primary_accel)); + else + g_hash_table_remove (muxer->primary_accels, action_and_target); + + gtk_action_muxer_primary_accel_changed (muxer, NULL, action_and_target); +} + +const gchar * +gtk_action_muxer_get_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target) +{ + if (muxer->primary_accels) + { + const gchar *primary_accel; + + primary_accel = g_hash_table_lookup (muxer->primary_accels, action_and_target); + + if (primary_accel) + return primary_accel; + } + + if (!muxer->parent) + return NULL; + + return gtk_action_muxer_get_primary_accel (muxer->parent, action_and_target); +} + +gchar * +gtk_print_action_and_target (const gchar *action_namespace, + const gchar *action_name, + GVariant *target) +{ + GString *result; + + g_return_val_if_fail (strchr (action_name, '|') == NULL, NULL); + g_return_val_if_fail (action_namespace == NULL || strchr (action_namespace, '|') == NULL, NULL); + + result = g_string_new (NULL); + + if (target) + g_variant_print_string (target, result, TRUE); + g_string_append_c (result, '|'); + + if (action_namespace) + { + g_string_append (result, action_namespace); + g_string_append_c (result, '.'); + } + + g_string_append (result, action_name); + + return g_string_free (result, FALSE); +} diff --git a/src/gtkactionmuxer.h b/src/gtkactionmuxer.h new file mode 100644 index 0000000..d71abf4 --- /dev/null +++ b/src/gtkactionmuxer.h @@ -0,0 +1,64 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: Ryan Lortie <desrt@desrt.ca> + */ + +#ifndef __GTK_ACTION_MUXER_H__ +#define __GTK_ACTION_MUXER_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_MUXER (gtk_action_muxer_get_type ()) +#define GTK_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_MUXER, GtkActionMuxer)) +#define GTK_IS_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_MUXER)) + +typedef struct _GtkActionMuxer GtkActionMuxer; + +GType gtk_action_muxer_get_type (void); +GtkActionMuxer * gtk_action_muxer_new (void); + +void gtk_action_muxer_insert (GtkActionMuxer *muxer, + const gchar *prefix, + GActionGroup *action_group); + +void gtk_action_muxer_remove (GtkActionMuxer *muxer, + const gchar *prefix); + +GtkActionMuxer * gtk_action_muxer_get_parent (GtkActionMuxer *muxer); + +void gtk_action_muxer_set_parent (GtkActionMuxer *muxer, + GtkActionMuxer *parent); + +void gtk_action_muxer_set_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target, + const gchar *primary_accel); + +const gchar * gtk_action_muxer_get_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target); + +/* No better place for this... */ +gchar * gtk_print_action_and_target (const gchar *action_namespace, + const gchar *action_name, + GVariant *target); + +G_END_DECLS + +#endif /* __GTK_ACTION_MUXER_H__ */ diff --git a/src/gtkactionobservable.c b/src/gtkactionobservable.c new file mode 100644 index 0000000..ab90df2 --- /dev/null +++ b/src/gtkactionobservable.c @@ -0,0 +1,78 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * licence or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + */ + +#include "config.h" + +#include "gtkactionobservable.h" + +G_DEFINE_INTERFACE (GtkActionObservable, gtk_action_observable, G_TYPE_OBJECT) + +/* + * SECTION:gtkactionobserable + * @short_description: an interface implemented by objects that report + * changes to actions + */ + +void +gtk_action_observable_default_init (GtkActionObservableInterface *iface) +{ +} + +/** + * gtk_action_observable_register_observer: + * @observable: a #GtkActionObservable + * @action_name: the name of the action + * @observer: the #GtkActionObserver to which the events will be reported + * + * Registers @observer as being interested in changes to @action_name on + * @observable. + */ +void +gtk_action_observable_register_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable)); + + GTK_ACTION_OBSERVABLE_GET_IFACE (observable) + ->register_observer (observable, action_name, observer); +} + +/** + * gtk_action_observable_unregister_observer: + * @observable: a #GtkActionObservable + * @action_name: the name of the action + * @observer: the #GtkActionObserver to which the events will be reported + * + * Removes the registration of @observer as being interested in changes + * to @action_name on @observable. + * + * If the observer was registered multiple times, it must be + * unregistered an equal number of times. + */ +void +gtk_action_observable_unregister_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable)); + + GTK_ACTION_OBSERVABLE_GET_IFACE (observable) + ->unregister_observer (observable, action_name, observer); +} diff --git a/src/gtkactionobservable.h b/src/gtkactionobservable.h new file mode 100644 index 0000000..aa1514b --- /dev/null +++ b/src/gtkactionobservable.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * licence or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + */ + +#ifndef __GTK_ACTION_OBSERVABLE_H__ +#define __GTK_ACTION_OBSERVABLE_H__ + +#include "gtkactionobserver.h" + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_OBSERVABLE (gtk_action_observable_get_type ()) +#define GTK_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE, GtkActionObservable)) +#define GTK_IS_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE)) +#define GTK_ACTION_OBSERVABLE_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE, \ + GtkActionObservableInterface)) + +typedef struct _GtkActionObservableInterface GtkActionObservableInterface; + +struct _GtkActionObservableInterface +{ + GTypeInterface g_iface; + + void (* register_observer) (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); + void (* unregister_observer) (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); +}; + +GType gtk_action_observable_get_type (void); +void gtk_action_observable_register_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); +void gtk_action_observable_unregister_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); + +G_END_DECLS + +#endif /* __GTK_ACTION_OBSERVABLE_H__ */ diff --git a/src/gtkactionobserver.c b/src/gtkactionobserver.c new file mode 100644 index 0000000..3287106 --- /dev/null +++ b/src/gtkactionobserver.c @@ -0,0 +1,189 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * licence or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + */ + +#include "config.h" + +#include "gtkactionobserver.h" + +G_DEFINE_INTERFACE (GtkActionObserver, gtk_action_observer, G_TYPE_OBJECT) + +/** + * SECTION:gtkactionobserver + * @short_description: an interface implemented by objects that are + * interested in monitoring actions for changes + * + * GtkActionObserver is a simple interface allowing objects that wish to + * be notified of changes to actions to be notified of those changes. + * + * It is also possible to monitor changes to action groups using + * #GObject signals, but there are a number of reasons that this + * approach could become problematic: + * + * - there are four separate signals that must be manually connected + * and disconnected + * + * - when a large number of different observers wish to monitor a + * (usually disjoint) set of actions within the same action group, + * there is only one way to avoid having all notifications delivered + * to all observers: signal detail. In order to use signal detail, + * each action name must be quarked, which is not always practical. + * + * - even if quarking is acceptable, #GObject signal details are + * implemented by scanning a linked list, so there is no real + * decrease in complexity + */ + +void +gtk_action_observer_default_init (GtkActionObserverInterface *class) +{ +} + +/** + * gtk_action_observer_action_added: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @enabled: %TRUE if the action is now enabled + * @parameter_type: the parameter type for action invocations, or %NULL + * if no parameter is required + * @state: the current state of the action, or %NULL if the action is + * stateless + * + * This function is called when an action that the observer is + * registered to receive events for is added. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_added (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_added (observer, observable, action_name, parameter_type, enabled, state); +} + +/** + * gtk_action_observer_action_enabled_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @enabled: %TRUE if the action is now enabled + * + * This function is called when an action that the observer is + * registered to receive events for becomes enabled or disabled. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_enabled_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_enabled_changed (observer, observable, action_name, enabled); +} + +/** + * gtk_action_observer_action_state_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @state: the new state of the action + * + * This function is called when an action that the observer is + * registered to receive events for changes to its state. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_state_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_state_changed (observer, observable, action_name, state); +} + +/** + * gtk_action_observer_action_removed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * + * This function is called when an action that the observer is + * registered to receive events for is removed. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_removed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_removed (observer, observable, action_name); +} + +/** + * gtk_action_observer_primary_accel_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @action_and_target: detailed action of the changed accel, in "action and target" format + * + * This function is called when an action that the observer is + * registered to receive events for has one of its accelerators changed. + * + * Accelerator changes are reported for all targets associated with the + * action. The @action_and_target string should be used to check if the + * reported target is the one that the observer is interested in. + */ +void +gtk_action_observer_primary_accel_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target) +{ + GtkActionObserverInterface *iface; + + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + iface = GTK_ACTION_OBSERVER_GET_IFACE (observer); + + if (iface->primary_accel_changed) + iface->primary_accel_changed (observer, observable, action_name, action_and_target); +} diff --git a/src/gtkactionobserver.h b/src/gtkactionobserver.h new file mode 100644 index 0000000..a4e9659 --- /dev/null +++ b/src/gtkactionobserver.h @@ -0,0 +1,91 @@ +/* + * Copyright © 2011 Canonical Limited + * + * This library is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2 of the + * licence or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: Ryan Lortie <desrt@desrt.ca> + */ + +#ifndef __GTK_ACTION_OBSERVER_H__ +#define __GTK_ACTION_OBSERVER_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_OBSERVER (gtk_action_observer_get_type ()) +#define GTK_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_OBSERVER, GtkActionObserver)) +#define GTK_IS_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_OBSERVER)) +#define GTK_ACTION_OBSERVER_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \ + GTK_TYPE_ACTION_OBSERVER, GtkActionObserverInterface)) + +typedef struct _GtkActionObserverInterface GtkActionObserverInterface; +typedef struct _GtkActionObservable GtkActionObservable; +typedef struct _GtkActionObserver GtkActionObserver; + +struct _GtkActionObserverInterface +{ + GTypeInterface g_iface; + + void (* action_added) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state); + void (* action_enabled_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled); + void (* action_state_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state); + void (* action_removed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name); + void (* primary_accel_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target); +}; + +GType gtk_action_observer_get_type (void); +void gtk_action_observer_action_added (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state); +void gtk_action_observer_action_enabled_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled); +void gtk_action_observer_action_state_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state); +void gtk_action_observer_action_removed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name); +void gtk_action_observer_primary_accel_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target); + +G_END_DECLS + +#endif /* __GTK_ACTION_OBSERVER_H__ */ diff --git a/src/hotplug-sniffer/hotplug-mimetypes.h b/src/hotplug-sniffer/hotplug-mimetypes.h new file mode 100644 index 0000000..b034020 --- /dev/null +++ b/src/hotplug-sniffer/hotplug-mimetypes.h @@ -0,0 +1,141 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#ifndef __HOTPLUG_MIMETYPES_H__ +#define __HOTPLUG_MIMETYPES_H__ + +#include <glib.h> + +G_GNUC_UNUSED static const gchar *docs_mimetypes[] = { + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/msword", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/rtf", + "application/pdf", + "application/x-bzpdf", + "application/x-gzpdf", + "application/x-xzpdf", + "application/postscript", + "application/x-bzpostscript", + "application/x-gzpostscript", + "image/x-eps", + "image/x-bzeps", + "image/x-gzeps", + "application/x-dvi", + "application/x-bzdvi", + "application/x-gzdvi", + "image/vnd.djvu", + "application/x-cbr", + "application/x-cbz", + "application/x-cb7", + "application/x-cbt", + NULL +}; + +G_GNUC_UNUSED static const gchar *video_mimetypes[] = { + "application/mxf", + "application/ogg", + "application/ram", + "application/sdp", + "application/vnd.ms-wpl", + "application/vnd.rn-realmedia", + "application/x-extension-m4a", + "application/x-extension-mp4", + "application/x-flash-video", + "application/x-matroska", + "application/x-netshow-channel", + "application/x-ogg", + "application/x-quicktimeplayer", + "application/x-shorten", + "image/vnd.rn-realpix", + "image/x-pict", + "misc/ultravox", + "text/x-google-video-pointer", + "video/3gpp", + "video/dv", + "video/fli", + "video/flv", + "video/mp2t", + "video/mp4", + "video/mp4v-es", + "video/mpeg", + "video/msvideo", + "video/ogg", + "video/quicktime", + "video/vivo", + "video/vnd.divx", + "video/vnd.rn-realvideo", + "video/vnd.vivo", + "video/webm", + "video/x-anim", + "video/x-avi", + "video/x-flc", + "video/x-fli", + "video/x-flic", + "video/x-flv", + "video/x-m4v", + "video/x-matroska", + "video/x-mpeg", + "video/x-ms-asf", + "video/x-ms-asx", + "video/x-msvideo", + "video/x-ms-wm", + "video/x-ms-wmv", + "video/x-ms-wmx", + "video/x-ms-wvx", + "video/x-nsv", + "video/x-ogm+ogg", + "video/x-theora+ogg", + "video/x-totem-stream", + NULL +}; + +G_GNUC_UNUSED static const gchar *audio_mimetypes[] = { + "audio/3gpp", + "audio/ac3", + "audio/AMR", + "audio/AMR-WB", + "audio/basic", + "audio/flac", + "audio/midi", + "audio/mp2", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/prs.sid", + "audio/vnd.rn-realaudio", + "audio/x-aiff", + "audio/x-ape", + "audio/x-flac", + "audio/x-gsm", + "audio/x-it", + "audio/x-m4a", + "audio/x-matroska", + "audio/x-mod", + "audio/x-mp3", + "audio/x-mpeg", + "audio/x-ms-asf", + "audio/x-ms-asx", + "audio/x-ms-wax", + "audio/x-ms-wma", + "audio/x-musepack", + "audio/x-pn-aiff", + "audio/x-pn-au", + "audio/x-pn-wav", + "audio/x-pn-windows-acm", + "audio/x-realaudio", + "audio/x-real-audio", + "audio/x-sbc", + "audio/x-speex", + "audio/x-tta", + "audio/x-wav", + "audio/x-wavpack", + "audio/x-vorbis", + "audio/x-vorbis+ogg", + "audio/x-xm", + NULL +}; + +#endif /* __HOTPLUG_MIMETYPES_H__ */ diff --git a/src/hotplug-sniffer/hotplug-sniffer.c b/src/hotplug-sniffer/hotplug-sniffer.c new file mode 100644 index 0000000..4b70ec7 --- /dev/null +++ b/src/hotplug-sniffer/hotplug-sniffer.c @@ -0,0 +1,298 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * 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/>. + * + * Authors: David Zeuthen <davidz@redhat.com> + * Cosimo Cecchi <cosimoc@redhat.com> + * + */ + +#include "shell-mime-sniffer.h" +#include "hotplug-mimetypes.h" + +/* Set the environment variable HOTPLUG_SNIFFER_DEBUG to show debug */ +static void print_debug (const gchar *str, ...); + +#define BUS_NAME "org.gnome.Shell.HotplugSniffer" +#define AUTOQUIT_TIMEOUT 5 + +static const gchar introspection_xml[] = + "<node>" + " <interface name='org.gnome.Shell.HotplugSniffer'>" + " <method name='SniffURI'>" + " <arg type='s' name='uri' direction='in'/>" + " <arg type='as' name='content_types' direction='out'/>" + " </method>" + " </interface>" + "</node>"; + +static GDBusNodeInfo *introspection_data = NULL; +static GMainLoop *loop = NULL; +static guint autoquit_id = 0; + +static gboolean +autoquit_timeout_cb (gpointer _unused) +{ + print_debug ("Timeout reached, quitting..."); + + autoquit_id = 0; + g_main_loop_quit (loop); + + return FALSE; +} + +static void +ensure_autoquit_off (void) +{ + if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL) + return; + + g_clear_handle_id (&autoquit_id, g_source_remove); +} + +static void +ensure_autoquit_on (void) +{ + if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL) + return; + + autoquit_id = + g_timeout_add_seconds (AUTOQUIT_TIMEOUT, + autoquit_timeout_cb, NULL); + g_source_set_name_by_id (autoquit_id, "[gnome-shell] autoquit_timeout_cb"); +} + +typedef struct { + GVariant *parameters; + GDBusMethodInvocation *invocation; +} InvocationData; + +static InvocationData * +invocation_data_new (GVariant *params, + GDBusMethodInvocation *invocation) +{ + InvocationData *ret; + + ret = g_new0 (InvocationData, 1); + ret->parameters = g_variant_ref (params); + ret->invocation = g_object_ref (invocation); + + return ret; +} + +static void +invocation_data_free (InvocationData *data) +{ + g_variant_unref (data->parameters); + g_clear_object (&data->invocation); + + g_free (data); +} + +static void +sniff_async_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + InvocationData *data = user_data; + gchar **types; + GError *error = NULL; + + types = shell_mime_sniffer_sniff_finish (SHELL_MIME_SNIFFER (source), + res, &error); + + if (error != NULL) + { + g_dbus_method_invocation_return_gerror (data->invocation, error); + g_error_free (error); + goto out; + } + + g_dbus_method_invocation_return_value (data->invocation, + g_variant_new ("(^as)", types)); + g_strfreev (types); + + out: + invocation_data_free (data); + ensure_autoquit_on (); +} + +static void +handle_sniff_uri (InvocationData *data) +{ + ShellMimeSniffer *sniffer; + const gchar *uri; + GFile *file; + + ensure_autoquit_off (); + + g_variant_get (data->parameters, + "(&s)", &uri, + NULL); + file = g_file_new_for_uri (uri); + + print_debug ("Initiating sniff for uri %s", uri); + + sniffer = shell_mime_sniffer_new (file); + shell_mime_sniffer_sniff_async (sniffer, + sniff_async_ready_cb, + data); + + g_object_unref (sniffer); + g_object_unref (file); +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + InvocationData *data; + + data = invocation_data_new (parameters, invocation); + + if (g_strcmp0 (method_name, "SniffURI") == 0) + handle_sniff_uri (data); + else + g_assert_not_reached (); +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* get_property */ + NULL, /* set_property */ +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + + print_debug ("Connected to the session bus: %s", name); + + g_dbus_connection_register_object (connection, + "/org/gnome/Shell/HotplugSniffer", + introspection_data->interfaces[0], + &interface_vtable, + NULL, + NULL, + &error); + + if (error != NULL) + { + g_printerr ("Error exporting object on the session bus: %s", + error->message); + g_error_free (error); + + _exit(1); + } + + print_debug ("Object exported on the session bus"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Lost bus name: %s, exiting", name); + + g_main_loop_quit (loop); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Acquired bus name: %s", name); +} + +int +main (int argc, + char **argv) +{ + guint name_owner_id; + + introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (introspection_data != NULL); + + ensure_autoquit_on (); + loop = g_main_loop_new (NULL, FALSE); + + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, 0, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + if (name_owner_id != 0) + g_bus_unown_name (name_owner_id); + + if (loop != NULL) + g_main_loop_unref (loop); + + return 0; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void __attribute__((format(printf, 1, 0))) +print_debug (const gchar *format, ...) +{ + g_autofree char *s = NULL; + g_autofree char *timestamp = NULL; + va_list ap; + g_autoptr (GDateTime) now = NULL; + static size_t once_init_value = 0; + static gboolean show_debug = FALSE; + static guint pid = 0; + + if (g_once_init_enter (&once_init_value)) + { + show_debug = (g_getenv ("HOTPLUG_SNIFFER_DEBUG") != NULL); + pid = getpid (); + g_once_init_leave (&once_init_value, 1); + } + + if (!show_debug) + goto out; + + now = g_date_time_new_now_local (); + timestamp = g_date_time_format (now, "%H:%M:%S"); + + va_start (ap, format); + s = g_strdup_vprintf (format, ap); + va_end (ap); + + g_print ("gnome-shell-hotplug-sniffer[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; +} + diff --git a/src/hotplug-sniffer/meson.build b/src/hotplug-sniffer/meson.build new file mode 100644 index 0000000..4a777e5 --- /dev/null +++ b/src/hotplug-sniffer/meson.build @@ -0,0 +1,22 @@ +hotplug_sources = [ + 'hotplug-mimetypes.h', + 'shell-mime-sniffer.h', + 'shell-mime-sniffer.c', + 'hotplug-sniffer.c' +] + +executable('gnome-shell-hotplug-sniffer', hotplug_sources, + dependencies: [gio_dep, gdk_pixbuf_dep], + include_directories: include_directories('../..'), + install_dir: libexecdir, + install: true +) + +service_file = 'org.gnome.Shell.HotplugSniffer.service' + +configure_file( + input: service_file + '.in', + output: service_file, + configuration: service_data, + install_dir: servicedir +) diff --git a/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in b/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in new file mode 100644 index 0000000..b14cea9 --- /dev/null +++ b/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Shell.HotplugSniffer +Exec=@libexecdir@/gnome-shell-hotplug-sniffer diff --git a/src/hotplug-sniffer/shell-mime-sniffer.c b/src/hotplug-sniffer/shell-mime-sniffer.c new file mode 100644 index 0000000..7a1c1fe --- /dev/null +++ b/src/hotplug-sniffer/shell-mime-sniffer.c @@ -0,0 +1,590 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 1999, 2000, 2001 Eazel, Inc. + * 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/>. + * + * Author: Cosimo Cecchi <cosimoc@redhat.com> + * + * The code for crawling the directory hierarchy is based on + * nautilus/libnautilus-private/nautilus-directory-async.c, with + * the following copyright and author: + * + * Copyright (C) 1999, 2000, 2001 Eazel, Inc. + * Author: Darin Adler <darin@bentspoon.com> + * + */ + +#include "shell-mime-sniffer.h" +#include "hotplug-mimetypes.h" + +#include <glib/gi18n.h> + +#include <gdk-pixbuf/gdk-pixbuf.h> + +#define LOADER_ATTRS \ + G_FILE_ATTRIBUTE_STANDARD_TYPE "," \ + G_FILE_ATTRIBUTE_STANDARD_NAME "," \ + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + +#define WATCHDOG_TIMEOUT 1500 +#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100 +#define HIGH_SCORE_RATIO 0.10 + +enum { + PROP_FILE = 1, + NUM_PROPERTIES +}; + +static GHashTable *image_type_table = NULL; +static GHashTable *audio_type_table = NULL; +static GHashTable *video_type_table = NULL; +static GHashTable *docs_type_table = NULL; + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +typedef struct { + ShellMimeSniffer *self; + + GFile *file; + GFileEnumerator *enumerator; + GList *deep_count_subdirectories; + + gint audio_count; + gint image_count; + gint document_count; + gint video_count; + + gint total_items; +} DeepCountState; + +typedef struct _ShellMimeSnifferPrivate ShellMimeSnifferPrivate; + +struct _ShellMimeSniffer +{ + GObject parent_instance; + + ShellMimeSnifferPrivate *priv; +}; + +struct _ShellMimeSnifferPrivate { + GFile *file; + + GCancellable *cancellable; + guint watchdog_id; + + GTask *task; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellMimeSniffer, shell_mime_sniffer, G_TYPE_OBJECT); + +static void deep_count_load (DeepCountState *state, + GFile *file); + +static void +init_mimetypes (void) +{ + static gsize once_init = 0; + + if (g_once_init_enter (&once_init)) + { + GSList *formats, *l; + GdkPixbufFormat *format; + gchar **types; + gint idx; + + image_type_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + video_type_table = g_hash_table_new (g_str_hash, g_str_equal); + audio_type_table = g_hash_table_new (g_str_hash, g_str_equal); + docs_type_table = g_hash_table_new (g_str_hash, g_str_equal); + + formats = gdk_pixbuf_get_formats (); + + for (l = formats; l != NULL; l = l->next) + { + format = l->data; + types = gdk_pixbuf_format_get_mime_types (format); + + for (idx = 0; types[idx] != NULL; idx++) + g_hash_table_insert (image_type_table, g_strdup (types[idx]), GINT_TO_POINTER (1)); + + g_strfreev (types); + } + + g_slist_free (formats); + + for (idx = 0; audio_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (audio_type_table, (gpointer) audio_mimetypes[idx], GINT_TO_POINTER (1)); + + for (idx = 0; video_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (video_type_table, (gpointer) video_mimetypes[idx], GINT_TO_POINTER (1)); + + for (idx = 0; docs_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (docs_type_table, (gpointer) docs_mimetypes[idx], GINT_TO_POINTER (1)); + + g_once_init_leave (&once_init, 1); + } +} + +static void +add_content_type_to_cache (DeepCountState *state, + const gchar *content_type) +{ + gboolean matched = TRUE; + + if (g_hash_table_lookup (image_type_table, content_type)) + state->image_count++; + else if (g_hash_table_lookup (video_type_table, content_type)) + state->video_count++; + else if (g_hash_table_lookup (docs_type_table, content_type)) + state->document_count++; + else if (g_hash_table_lookup (audio_type_table, content_type)) + state->audio_count++; + else + matched = FALSE; + + if (matched) + state->total_items++; +} + +typedef struct { + const gchar *type; + gdouble ratio; +} SniffedResult; + +static gint +results_cmp_func (gconstpointer a, + gconstpointer b) +{ + const SniffedResult *sniffed_a = a; + const SniffedResult *sniffed_b = b; + + if (sniffed_a->ratio < sniffed_b->ratio) + return 1; + + if (sniffed_a->ratio > sniffed_b->ratio) + return -1; + + return 0; +} + +static void +prepare_async_result (DeepCountState *state) +{ + ShellMimeSniffer *self = state->self; + GArray *results; + GPtrArray *sniffed_mime; + SniffedResult result; + char **mimes; + + sniffed_mime = g_ptr_array_new (); + results = g_array_new (TRUE, TRUE, sizeof (SniffedResult)); + + if (state->total_items == 0) + goto out; + + result.type = "x-content/video"; + result.ratio = (gdouble) state->video_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/audio"; + result.ratio = (gdouble) state->audio_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/pictures"; + result.ratio = (gdouble) state->image_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/documents"; + result.ratio = (gdouble) state->document_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + g_array_sort (results, results_cmp_func); + + result = g_array_index (results, SniffedResult, 0); + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + /* if other types score high in ratio, add them, up to three */ + result = g_array_index (results, SniffedResult, 1); + if (result.ratio < HIGH_SCORE_RATIO) + goto out; + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + result = g_array_index (results, SniffedResult, 2); + if (result.ratio < HIGH_SCORE_RATIO) + goto out; + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + out: + g_ptr_array_add (sniffed_mime, NULL); + mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE); + + g_array_free (results, TRUE); + g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev); +} + +/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */ +static void +deep_count_one (DeepCountState *state, + GFileInfo *info) +{ + GFile *subdir; + const char *content_type; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + { + /* record the fact that we have to descend into this directory */ + subdir = g_file_get_child (state->file, g_file_info_get_name (info)); + state->deep_count_subdirectories = + g_list_append (state->deep_count_subdirectories, subdir); + } + else + { + content_type = g_file_info_get_content_type (info); + + if (content_type) + add_content_type_to_cache (state, content_type); + } +} + +static void +deep_count_finish (DeepCountState *state) +{ + prepare_async_result (state); + + if (state->enumerator) + { + if (!g_file_enumerator_is_closed (state->enumerator)) + g_file_enumerator_close_async (state->enumerator, + 0, NULL, NULL, NULL); + + g_object_unref (state->enumerator); + } + + g_cancellable_reset (state->self->priv->cancellable); + g_clear_object (&state->file); + + g_list_free_full (state->deep_count_subdirectories, g_object_unref); + + g_free (state); +} + +static void +deep_count_next_dir (DeepCountState *state) +{ + GFile *new_file; + + g_clear_object (&state->file); + + if (state->deep_count_subdirectories != NULL) + { + /* Work on a new directory. */ + new_file = state->deep_count_subdirectories->data; + state->deep_count_subdirectories = + g_list_remove (state->deep_count_subdirectories, new_file); + + deep_count_load (state, new_file); + g_object_unref (new_file); + } + else + { + deep_count_finish (state); + } +} + +static void +deep_count_more_files_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + DeepCountState *state; + GList *files, *l; + GFileInfo *info; + + state = user_data; + + if (g_cancellable_is_cancelled (state->self->priv->cancellable)) + { + deep_count_finish (state); + return; + } + + files = g_file_enumerator_next_files_finish (state->enumerator, + res, NULL); + + for (l = files; l != NULL; l = l->next) + { + info = l->data; + deep_count_one (state, info); + g_object_unref (info); + } + + if (files == NULL) + { + g_file_enumerator_close_async (state->enumerator, 0, NULL, NULL, NULL); + g_object_unref (state->enumerator); + state->enumerator = NULL; + + deep_count_next_dir (state); + } + else + { + g_file_enumerator_next_files_async (state->enumerator, + DIRECTORY_LOAD_ITEMS_PER_CALLBACK, + G_PRIORITY_LOW, + state->self->priv->cancellable, + deep_count_more_files_callback, + state); + } + + g_list_free (files); +} + +static void +deep_count_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + DeepCountState *state; + GFileEnumerator *enumerator; + + state = user_data; + + if (g_cancellable_is_cancelled (state->self->priv->cancellable)) + { + deep_count_finish (state); + return; + } + + enumerator = g_file_enumerate_children_finish (G_FILE (source_object), + res, NULL); + + if (enumerator == NULL) + { + deep_count_next_dir (state); + } + else + { + state->enumerator = enumerator; + g_file_enumerator_next_files_async (state->enumerator, + DIRECTORY_LOAD_ITEMS_PER_CALLBACK, + G_PRIORITY_LOW, + state->self->priv->cancellable, + deep_count_more_files_callback, + state); + } +} + +static void +deep_count_load (DeepCountState *state, + GFile *file) +{ + state->file = g_object_ref (file); + + g_file_enumerate_children_async (state->file, + LOADER_ATTRS, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, /* flags */ + G_PRIORITY_LOW, /* prio */ + state->self->priv->cancellable, + deep_count_callback, + state); +} + +static void +deep_count_start (ShellMimeSniffer *self) +{ + DeepCountState *state; + + state = g_new0 (DeepCountState, 1); + state->self = self; + + deep_count_load (state, self->priv->file); +} + +static void +query_info_async_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GFileInfo *info; + GError *error = NULL; + ShellMimeSniffer *self = user_data; + + info = g_file_query_info_finish (G_FILE (source), + res, &error); + + if (error != NULL) + { + g_task_return_error (self->priv->task, error); + + return; + } + + if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY) + { + g_task_return_new_error (self->priv->task, + G_IO_ERROR, + G_IO_ERROR_NOT_DIRECTORY, + "Not a directory"); + + return; + } + + deep_count_start (self); +} + +static gboolean +watchdog_timeout_reached_cb (gpointer user_data) +{ + ShellMimeSniffer *self = user_data; + + self->priv->watchdog_id = 0; + g_cancellable_cancel (self->priv->cancellable); + + return FALSE; +} + +static void +start_loading_file (ShellMimeSniffer *self) +{ + g_file_query_info_async (self->priv->file, + LOADER_ATTRS, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + self->priv->cancellable, + query_info_async_ready_cb, + self); +} + +static void +shell_mime_sniffer_set_file (ShellMimeSniffer *self, + GFile *file) +{ + g_clear_object (&self->priv->file); + self->priv->file = g_object_ref (file); +} + +static void +shell_mime_sniffer_dispose (GObject *object) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + g_clear_object (&self->priv->file); + g_clear_object (&self->priv->cancellable); + g_clear_object (&self->priv->task); + + g_clear_handle_id (&self->priv->watchdog_id, g_source_remove); + + G_OBJECT_CLASS (shell_mime_sniffer_parent_class)->dispose (object); +} + +static void +shell_mime_sniffer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + switch (prop_id) { + case PROP_FILE: + g_value_set_object (value, self->priv->file); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_mime_sniffer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + switch (prop_id) { + case PROP_FILE: + shell_mime_sniffer_set_file (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_mime_sniffer_class_init (ShellMimeSnifferClass *klass) +{ + GObjectClass *oclass; + + oclass = G_OBJECT_CLASS (klass); + oclass->dispose = shell_mime_sniffer_dispose; + oclass->get_property = shell_mime_sniffer_get_property; + oclass->set_property = shell_mime_sniffer_set_property; + + properties[PROP_FILE] = + g_param_spec_object ("file", + "File", + "The loaded file", + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (oclass, NUM_PROPERTIES, properties); +} + +static void +shell_mime_sniffer_init (ShellMimeSniffer *self) +{ + self->priv = shell_mime_sniffer_get_instance_private (self); + init_mimetypes (); +} + +ShellMimeSniffer * +shell_mime_sniffer_new (GFile *file) +{ + return g_object_new (SHELL_TYPE_MIME_SNIFFER, + "file", file, + NULL); +} + +void +shell_mime_sniffer_sniff_async (ShellMimeSniffer *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_assert (self->priv->watchdog_id == 0); + g_assert (self->priv->task == NULL); + + self->priv->cancellable = g_cancellable_new (); + self->priv->task = g_task_new (self, self->priv->cancellable, + callback, user_data); + + self->priv->watchdog_id = + g_timeout_add (WATCHDOG_TIMEOUT, + watchdog_timeout_reached_cb, self); + g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb"); + + start_loading_file (self); +} + +gchar ** +shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_pointer (self->priv->task, error); +} diff --git a/src/hotplug-sniffer/shell-mime-sniffer.h b/src/hotplug-sniffer/shell-mime-sniffer.h new file mode 100644 index 0000000..3936eef --- /dev/null +++ b/src/hotplug-sniffer/shell-mime-sniffer.h @@ -0,0 +1,46 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * 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/>. + * + * Author: Cosimo Cecchi <cosimoc@redhat.com> + * + */ + +#ifndef __SHELL_MIME_SNIFFER_H__ +#define __SHELL_MIME_SNIFFER_H__ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_MIME_SNIFFER (shell_mime_sniffer_get_type ()) +G_DECLARE_FINAL_TYPE (ShellMimeSniffer, shell_mime_sniffer, + SHELL, MIME_SNIFFER, GObject) + +ShellMimeSniffer *shell_mime_sniffer_new (GFile *file); + +void shell_mime_sniffer_sniff_async (ShellMimeSniffer *self, + GAsyncReadyCallback callback, + gpointer user_data); + +gchar ** shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self, + GAsyncResult *res, + GError **error); + +G_END_DECLS + +#endif /* __SHELL_MIME_SNIFFER_H__ */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..2311a74 --- /dev/null +++ b/src/main.c @@ -0,0 +1,599 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#if defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) +#include <malloc.h> +#endif +#include <stdlib.h> +#include <string.h> + +#include <cogl-pango/cogl-pango.h> +#include <clutter/clutter.h> +#include <gtk/gtk.h> +#include <glib-unix.h> +#include <glib/gi18n-lib.h> +#include <girepository.h> +#include <meta/meta-context.h> +#include <meta/meta-plugin.h> +#include <meta/prefs.h> +#include <atk-bridge.h> + +#include "shell-global.h" +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "st.h" + +extern GType gnome_shell_plugin_get_type (void); + +#define SHELL_DBUS_SERVICE "org.gnome.Shell" + +#define WM_NAME "GNOME Shell" +#define GNOME_WM_KEYBINDINGS "Mutter,GNOME Shell" + +static gboolean is_gdm_mode = FALSE; +static char *session_mode = NULL; +static int caught_signal = 0; + +#define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1 +#define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4 + +enum { + SHELL_DEBUG_BACKTRACE_WARNINGS = 1, + SHELL_DEBUG_BACKTRACE_SEGFAULTS = 2, +}; +static int _shell_debug; +static gboolean _tracked_signals[NSIG] = { 0 }; + +static void +shell_dbus_acquire_name (GDBusProxy *bus, + guint32 request_name_flags, + guint32 *request_name_result, + const gchar *name, + gboolean fatal) +{ + GError *error = NULL; + GVariant *request_name_variant; + + if (!(request_name_variant = g_dbus_proxy_call_sync (bus, + "RequestName", + g_variant_new ("(su)", name, request_name_flags), + 0, /* call flags */ + -1, /* timeout */ + NULL, /* cancellable */ + &error))) + { + g_printerr ("failed to acquire %s: %s\n", name, error->message); + g_clear_error (&error); + if (!fatal) + return; + exit (1); + } + g_variant_get (request_name_variant, "(u)", request_name_result); + g_variant_unref (request_name_variant); +} + +static void +shell_dbus_init (gboolean replace) +{ + GDBusConnection *session; + GDBusProxy *bus; + GError *error = NULL; + guint32 request_name_flags; + guint32 request_name_result; + + session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + if (error) { + g_printerr ("Failed to connect to session bus: %s", error->message); + exit (1); + } + + bus = g_dbus_proxy_new_sync (session, + G_DBUS_PROXY_FLAGS_NONE, + NULL, /* interface info */ + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + NULL, /* cancellable */ + &error); + + if (!bus) + { + g_printerr ("Failed to get a session bus proxy: %s", error->message); + exit (1); + } + + request_name_flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + if (replace) + request_name_flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; + + shell_dbus_acquire_name (bus, + request_name_flags, + &request_name_result, + SHELL_DBUS_SERVICE, TRUE); + if (!(request_name_result == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER + || request_name_result == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)) + { + g_printerr (SHELL_DBUS_SERVICE " already exists on bus and --replace not specified\n"); + exit (1); + } + + g_object_unref (bus); + g_object_unref (session); +} + +static void +shell_introspection_init (void) +{ + + g_irepository_prepend_search_path (MUTTER_TYPELIB_DIR); + g_irepository_prepend_search_path (GNOME_SHELL_PKGLIBDIR); + + /* We need to explicitly add the directories where the private libraries are + * installed to the GIR's library path, so that they can be found at runtime + * when linking using DT_RUNPATH (instead of DT_RPATH), which is the default + * for some linkers (e.g. gold) and in some distros (e.g. Debian). + */ + g_irepository_prepend_library_path (MUTTER_TYPELIB_DIR); + g_irepository_prepend_library_path (GNOME_SHELL_PKGLIBDIR); +} + +static void +shell_fonts_init (void) +{ + CoglPangoFontMap *fontmap; + + /* Disable text mipmapping; it causes problems on pre-GEM Intel + * drivers and we should just be rendering text at the right + * size rather than scaling it. If we do effects where we dynamically + * zoom labels, then we might want to reconsider. + */ + fontmap = COGL_PANGO_FONT_MAP (clutter_get_font_map ()); + cogl_pango_font_map_set_use_mipmapping (fontmap, FALSE); +} + +static void +shell_profiler_init (void) +{ + ShellGlobal *global; + GjsProfiler *profiler; + GjsContext *context; + const char *enabled; + const char *fd_str; + int fd = -1; + + /* Sysprof uses the "GJS_TRACE_FD=N" environment variable to connect GJS + * profiler data to the combined Sysprof capture. Since we are in control of + * the GjsContext, we need to proxy this FD across to the GJS profiler. + */ + + fd_str = g_getenv ("GJS_TRACE_FD"); + enabled = g_getenv ("GJS_ENABLE_PROFILER"); + if (fd_str == NULL || enabled == NULL) + return; + + global = shell_global_get (); + g_return_if_fail (global); + + context = _shell_global_get_gjs_context (global); + g_return_if_fail (context); + + profiler = gjs_context_get_profiler (context); + g_return_if_fail (profiler); + + if (fd_str) + { + fd = atoi (fd_str); + + if (fd > 2) + { + gjs_profiler_set_fd (profiler, fd); + gjs_profiler_start (profiler); + } + } +} + +static void +shell_profiler_shutdown (void) +{ + ShellGlobal *global; + GjsProfiler *profiler; + GjsContext *context; + + global = shell_global_get (); + context = _shell_global_get_gjs_context (global); + profiler = gjs_context_get_profiler (context); + + if (profiler) + gjs_profiler_stop (profiler); +} + +static void +malloc_statistics_callback (ShellPerfLog *perf_log, + gpointer data) +{ +#if defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) +#ifdef HAVE_MALLINFO2 + struct mallinfo2 info = mallinfo2 (); +#else + struct mallinfo info = mallinfo (); +#endif + + shell_perf_log_update_statistic_i (perf_log, + "malloc.arenaSize", + info.arena); + shell_perf_log_update_statistic_i (perf_log, + "malloc.mmapSize", + info.hblkhd); + shell_perf_log_update_statistic_i (perf_log, + "malloc.usedSize", + info.uordblks); +#endif /* defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) */ +} + +static void +shell_perf_log_init (void) +{ + ShellPerfLog *perf_log = shell_perf_log_get_default (); + + /* For probably historical reasons, mallinfo() defines the returned values, + * even those in bytes as int, not size_t. We're determined not to use + * more than 2G of malloc'ed memory, so are OK with that. + */ + shell_perf_log_define_statistic (perf_log, + "malloc.arenaSize", + "Amount of memory allocated by malloc() with brk(), in bytes", + "i"); + shell_perf_log_define_statistic (perf_log, + "malloc.mmapSize", + "Amount of memory allocated by malloc() with mmap(), in bytes", + "i"); + shell_perf_log_define_statistic (perf_log, + "malloc.usedSize", + "Amount of malloc'ed memory currently in use", + "i"); + + shell_perf_log_add_statistics_callback (perf_log, + malloc_statistics_callback, + NULL, NULL); +} + +static void +shell_a11y_init (void) +{ + cally_accessibility_init (); + + if (clutter_get_accessibility_enabled () == FALSE) + { + g_warning ("Accessibility: clutter has no accessibility enabled" + " skipping the atk-bridge load"); + } + else + { + atk_bridge_adaptor_init (NULL, NULL); + } +} + +static void +shell_init_debug (const char *debug_env) +{ + static const GDebugKey keys[] = { + { "backtrace-warnings", SHELL_DEBUG_BACKTRACE_WARNINGS }, + { "backtrace-segfaults", SHELL_DEBUG_BACKTRACE_SEGFAULTS }, + }; + + _shell_debug = g_parse_debug_string (debug_env, keys, + G_N_ELEMENTS (keys)); +} + +static GLogWriterOutput +default_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + GLogWriterOutput output; + int i; + + output = g_log_writer_default (log_level, fields, n_fields, user_data); + + if ((_shell_debug & SHELL_DEBUG_BACKTRACE_WARNINGS) && + ((log_level & G_LOG_LEVEL_CRITICAL) || + (log_level & G_LOG_LEVEL_WARNING))) + { + const char *log_domain = NULL; + + for (i = 0; i < n_fields; i++) + { + if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) + { + log_domain = fields[i].value; + break; + } + } + + /* Filter out Gjs logs, those already have the stack */ + if (g_strcmp0 (log_domain, "Gjs") != 0) + gjs_dumpstack (); + } + + return output; +} + +static GLogWriterOutput +shut_up (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + return (GLogWriterOutput) {0}; +} + +static void +dump_gjs_stack_alarm_sigaction (int signo) +{ + g_log_set_writer_func (g_log_writer_default, NULL, NULL); + g_warning ("Failed to dump Javascript stack, got stuck"); + g_log_set_writer_func (default_log_writer, NULL, NULL); + + raise (caught_signal); +} + +static void +dump_gjs_stack_on_signal_handler (int signo) +{ + struct sigaction sa = { .sa_handler = dump_gjs_stack_alarm_sigaction }; + gsize i; + + /* Ignore all the signals starting this point, a part the one we'll raise + * (which is implicitly ignored here through SA_RESETHAND), this is needed + * not to get this handler being called by other signals that we were + * tracking and that might be emitted by code called starting from now. + */ + for (i = 0; i < G_N_ELEMENTS (_tracked_signals); ++i) + { + if (_tracked_signals[i] && i != signo) + signal (i, SIG_IGN); + } + + /* Waiting at least 5 seconds for the dumpstack, if it fails, we raise the error */ + caught_signal = signo; + sigemptyset (&sa.sa_mask); + sigaction (SIGALRM, &sa, NULL); + + alarm (5); + gjs_dumpstack (); + alarm (0); + + raise (signo); +} + +static void +dump_gjs_stack_on_signal (int signo) +{ + struct sigaction sa = { + .sa_flags = SA_RESETHAND | SA_NODEFER, + .sa_handler = dump_gjs_stack_on_signal_handler, + }; + + sigemptyset (&sa.sa_mask); + + sigaction (signo, &sa, NULL); + _tracked_signals[signo] = TRUE; +} + +static gboolean +list_modes (const char *option_name, + const char *value, + gpointer data, + GError **error) +{ + ShellGlobal *global; + GjsContext *context; + const char *script; + int status; + + /* Many of our imports require global to be set, so rather than + * tayloring our imports carefully here to avoid that dependency, + * we just set it. + * ShellGlobal has some GTK+ dependencies, so initialize GTK+; we + * don't really care if it fails though (e.g. when running from a tty), + * so we mute all warnings */ + g_log_set_writer_func (shut_up, NULL, NULL); + gtk_init_check (NULL, NULL); + + _shell_global_init (NULL); + global = shell_global_get (); + context = _shell_global_get_gjs_context (global); + + shell_introspection_init (); + + script = "imports.ui.environment.init();" + "imports.ui.sessionMode.listModes();"; + if (!gjs_context_eval (context, script, -1, "<main>", &status, NULL)) + g_message ("Retrieving list of available modes failed."); + + g_object_unref (context); + exit (status); +} + +static gboolean +print_version (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + g_print ("GNOME Shell %s\n", VERSION); + exit (0); +} + +GOptionEntry gnome_shell_options[] = { + { + "version", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, + print_version, + N_("Print version"), + NULL + }, + { + "gdm-mode", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, + &is_gdm_mode, + N_("Mode used by GDM for login screen"), + NULL + }, + { + "mode", 0, 0, G_OPTION_ARG_STRING, + &session_mode, + N_("Use a specific mode, e.g. “gdm” for login screen"), + "MODE" + }, + { + "list-modes", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, + list_modes, + N_("List possible modes"), + NULL + }, + { NULL } +}; + +static gboolean +on_sigterm (gpointer user_data) +{ + MetaContext *context = META_CONTEXT (user_data); + + meta_context_terminate (context); + + return G_SOURCE_REMOVE; +} + +static void +init_signal_handlers (MetaContext *context) +{ + struct sigaction act = { 0 }; + sigset_t empty_mask; + + sigemptyset (&empty_mask); + act.sa_handler = SIG_IGN; + act.sa_mask = empty_mask; + act.sa_flags = 0; + if (sigaction (SIGPIPE, &act, NULL) < 0) + g_warning ("Failed to register SIGPIPE handler: %s", g_strerror (errno)); +#ifdef SIGXFSZ + if (sigaction (SIGXFSZ, &act, NULL) < 0) + g_warning ("Failed to register SIGXFSZ handler: %s", g_strerror (errno)); +#endif + + g_unix_signal_add (SIGTERM, on_sigterm, context); +} + +static void +change_to_home_directory (void) +{ + const char *home_dir; + + home_dir = g_get_home_dir (); + if (!home_dir) + return; + + if (chdir (home_dir) < 0) + g_warning ("Could not change to home directory %s", home_dir); +} + +int +main (int argc, char **argv) +{ + g_autoptr (MetaContext) context = NULL; + GError *error = NULL; + int ecode = EXIT_SUCCESS; + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + context = meta_create_context (WM_NAME); + meta_context_add_option_entries (context, gnome_shell_options, + GETTEXT_PACKAGE); + meta_context_add_option_group (context, g_irepository_get_option_group ()); + + session_mode = (char *) g_getenv ("GNOME_SHELL_SESSION_MODE"); + + if (!meta_context_configure (context, &argc, &argv, &error)) + { + g_printerr ("Failed to configure: %s", error->message); + return EXIT_FAILURE; + } + + meta_context_set_plugin_gtype (context, gnome_shell_plugin_get_type ()); + meta_context_set_gnome_wm_keybindings (context, GNOME_WM_KEYBINDINGS); + + init_signal_handlers (context); + change_to_home_directory (); + + if (!meta_context_setup (context, &error)) + { + g_printerr ("Failed to setup: %s", error->message); + return EXIT_FAILURE; + } + + /* FIXME: Add gjs API to set this stuff and don't depend on the + * environment. These propagate to child processes. + */ + g_setenv ("GJS_DEBUG_OUTPUT", "stderr", TRUE); + g_setenv ("GJS_DEBUG_TOPICS", "JS ERROR;JS LOG", TRUE); + + shell_init_debug (g_getenv ("SHELL_DEBUG")); + + shell_dbus_init (meta_context_is_replacing (context)); + shell_a11y_init (); + shell_perf_log_init (); + shell_introspection_init (); + shell_fonts_init (); + + g_log_set_writer_func (default_log_writer, NULL, NULL); + + /* Initialize the global object */ + if (session_mode == NULL) + session_mode = is_gdm_mode ? (char *)"gdm" : (char *)"user"; + + _shell_global_init ("session-mode", session_mode, NULL); + + dump_gjs_stack_on_signal (SIGABRT); + dump_gjs_stack_on_signal (SIGFPE); + dump_gjs_stack_on_signal (SIGIOT); + dump_gjs_stack_on_signal (SIGTRAP); + + if ((_shell_debug & SHELL_DEBUG_BACKTRACE_SEGFAULTS)) + { + dump_gjs_stack_on_signal (SIGBUS); + dump_gjs_stack_on_signal (SIGSEGV); + } + + shell_profiler_init (); + + if (meta_context_get_compositor_type (context) == META_COMPOSITOR_TYPE_WAYLAND) + meta_context_raise_rlimit_nofile (context, NULL); + + if (!meta_context_start (context, &error)) + { + g_printerr ("GNOME Shell failed to start: %s", error->message); + return EXIT_FAILURE; + } + + if (!meta_context_run_main_loop (context, &error)) + { + g_printerr ("GNOME Shell terminated with an error: %s", error->message); + ecode = EXIT_FAILURE; + } + + meta_context_destroy (g_steal_pointer (&context)); + + shell_profiler_shutdown (); + +#if 0 + g_debug ("Doing final cleanup"); + _shell_global_destroy_gjs_context (shell_global_get ()); + g_object_unref (shell_global_get ()); +#endif + + return ecode; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..fc7f8bf --- /dev/null +++ b/src/meson.build @@ -0,0 +1,276 @@ +service_data = configuration_data() +service_data.set('libexecdir', libexecdir) + +subdir('calendar-server') +subdir('hotplug-sniffer') +subdir('st') +subdir('tray') + +script_data = configuration_data() +script_data.set('bindir', bindir) +script_data.set('datadir', datadir) +script_data.set('libdir', libdir) +script_data.set('libexecdir', libexecdir) +script_data.set('pkgdatadir', pkgdatadir) +script_data.set('pkglibdir', pkglibdir) +script_data.set('PYTHON', python.full_path()) +script_data.set('VERSION', meson.project_version()) + +script_tools = ['gnome-shell-perf-tool'] + +if get_option('extensions_tool') + script_tools += 'gnome-shell-extension-tool' +endif + +foreach tool : script_tools + configure_file( + input: tool + '.in', + output: tool, + configuration: script_data, + install_dir: bindir + ) +endforeach + +install_data('gnome-shell-extension-prefs', + install_dir: bindir +) + +gnome_shell_cflags = [ + '-DCLUTTER_ENABLE_EXPERIMENTAL_API', + '-DCOGL_ENABLE_EXPERIMENTAL_API', + '-DVERSION="@0@"'.format(meson.project_version()), + '-DLOCALEDIR="@0@"'.format(localedir), + '-DDATADIR="@0@"'.format(datadir), + '-DGNOME_SHELL_LIBEXECDIR="@0@"'.format(libexecdir), + '-DGNOME_SHELL_DATADIR="@0@"'.format(pkgdatadir), + '-DGNOME_SHELL_PKGLIBDIR="@0@"'.format(pkglibdir) +] + +install_rpath = ':'.join([mutter_typelibdir, pkglibdir]) + +gnome_shell_deps = [ + gio_unix_dep, + libxml_dep, + gtk_dep, + atk_bridge_dep, + gjs_dep, + gdk_x11_dep, + clutter_dep, + cogl_pango_dep, + startup_dep, + gi_dep, + polkit_dep, + gcr_dep, + libsystemd_dep +] + +gnome_shell_deps += nm_deps + +tools_cflags = '-DLOCALEDIR="@0@"'.format(localedir) +tools_deps = [gio_dep, gjs_dep] + +libshell_menu_sources = [ + 'gtkactionmuxer.h', + 'gtkactionmuxer.c', + 'gtkactionobservable.h', + 'gtkactionobservable.c', + 'gtkactionobserver.h', + 'gtkactionobserver.c' +] + +libshell_menu = library('gnome-shell-menu', + sources: libshell_menu_sources, + dependencies: [gio_dep, clutter_dep], + include_directories: conf_inc, + build_rpath: mutter_typelibdir, + install_rpath: mutter_typelibdir, + install_dir: pkglibdir, + install: true +) + +libshell_menu_dep = declare_dependency(link_with: libshell_menu) + +libshell_public_headers = [ + 'shell-app.h', + 'shell-app-system.h', + 'shell-app-usage.h', + 'shell-blur-effect.h', + 'shell-embedded-window.h', + 'shell-glsl-effect.h', + 'shell-gtk-embed.h', + 'shell-global.h', + 'shell-invert-lightness-effect.h', + 'shell-action-modes.h', + 'shell-mount-operation.h', + 'shell-perf-log.h', + 'shell-screenshot.h', + 'shell-square-bin.h', + 'shell-stack.h', + 'shell-tray-icon.h', + 'shell-tray-manager.h', + 'shell-util.h', + 'shell-window-preview.h', + 'shell-window-preview-layout.h', + 'shell-window-tracker.h', + 'shell-wm.h', + 'shell-workspace-background.h' +] + +if have_networkmanager + libshell_public_headers += 'shell-network-agent.h' +endif + +libshell_private_headers = [ + 'shell-app-private.h', + 'shell-app-cache-private.h', + 'shell-app-system-private.h', + 'shell-global-private.h', + 'shell-window-tracker-private.h', + 'shell-wm-private.h' +] + +libshell_sources = [ + 'gnome-shell-plugin.c', + 'shell-app.c', + 'shell-app-system.c', + 'shell-app-usage.c', + 'shell-blur-effect.c', + 'shell-embedded-window.c', + 'shell-embedded-window-private.h', + 'shell-global.c', + 'shell-glsl-effect.c', + 'shell-gtk-embed.c', + 'shell-invert-lightness-effect.c', + 'shell-keyring-prompt.c', + 'shell-keyring-prompt.h', + 'shell-mount-operation.c', + 'shell-perf-log.c', + 'shell-polkit-authentication-agent.c', + 'shell-polkit-authentication-agent.h', + 'shell-screenshot.c', + 'shell-secure-text-buffer.c', + 'shell-secure-text-buffer.h', + 'shell-square-bin.c', + 'shell-stack.c', + 'shell-tray-icon.c', + 'shell-tray-manager.c', + 'shell-util.c', + 'shell-window-preview.c', + 'shell-window-preview-layout.c', + 'shell-window-tracker.c', + 'shell-wm.c', + 'shell-workspace-background.c' +] + +if have_networkmanager + libshell_sources += 'shell-network-agent.c' +endif + +libshell_private_sources = [ + 'shell-app-cache.c', +] + +libshell_enums = gnome.mkenums_simple('shell-enum-types', + sources: libshell_public_headers +) + +libshell_gir_sources = [ + libshell_enums, + libshell_public_headers, + libshell_sources +] + +libshell_no_gir_sources = [ + js_resources, + libshell_private_headers, + libshell_private_sources +] + +dbus_generated = gnome.gdbus_codegen('org-gtk-application', + 'org.gtk.Application.xml', + namespace: 'Shell' +) + +dbus_generated += gnome.gdbus_codegen('switcheroo-control', + '../data/dbus-interfaces/net.hadess.SwitcherooControl.xml', + namespace: 'Shell' +) + +libshell_no_gir_sources += dbus_generated + +libshell = library('gnome-shell', + sources: libshell_gir_sources + libshell_no_gir_sources, + dependencies: gnome_shell_deps + [libshell_menu_dep, libst_dep, mutter_dep, gnome_desktop_dep, m_dep], + include_directories: [conf_inc, st_inc, include_directories('tray')], + c_args: gnome_shell_cflags, + link_with: [libtray], + build_rpath: mutter_typelibdir, + install_rpath: install_rpath, + install_dir: pkglibdir, + install: true +) + +libshell_dep = declare_dependency(link_with: libshell) + +libshell_gir_includes = [ + 'Clutter-@0@'.format(mutter_api_version), + 'Meta-@0@'.format(mutter_api_version), + 'Gcr-4', + 'PolkitAgent-1.0' +] + +if have_networkmanager + libshell_gir_includes += ['NM-1.0'] +endif + +libshell_gir_includes += [ + libgvc_gir[0], + libst_gir[0] +] + +gnome.generate_gir(libshell, + sources: libshell_gir_sources, + nsversion: '0.1', + namespace: 'Shell', + includes: libshell_gir_includes, + extra_args: ['--quiet'], + install_dir_gir: pkgdatadir, + install_dir_typelib: pkglibdir, + install: true +) + +executable('gnome-shell', 'main.c', + c_args: gnome_shell_cflags + [ + '-DMUTTER_TYPELIB_DIR="@0@"'.format(mutter_typelibdir) + ], + dependencies: gnome_shell_deps + [libshell_dep, libst_dep, mutter_dep], + include_directories: [conf_inc, st_inc, include_directories('tray')], + build_rpath: mutter_typelibdir, + install_rpath: install_rpath, + install: true +) + +if have_networkmanager + executable('gnome-shell-portal-helper', + 'gnome-shell-portal-helper.c', portal_resources, + c_args: tools_cflags, + dependencies: tools_deps, + include_directories: [conf_inc], + install_dir: libexecdir, + install: true + ) +endif + +executable('gnome-shell-perf-helper', 'shell-perf-helper.c', + dependencies: [gtk_dep, gio_dep, m_dep], + include_directories: [conf_inc], + install_dir: libexecdir, + install: true +) + +executable('run-js-test', 'run-js-test.c', + dependencies: [mutter_dep, gio_dep, gi_dep, gjs_dep], + include_directories: [conf_inc], + link_with: libshell, + build_rpath: mutter_typelibdir, +) diff --git a/src/org.gtk.Application.xml b/src/org.gtk.Application.xml new file mode 100644 index 0000000..161aa1d --- /dev/null +++ b/src/org.gtk.Application.xml @@ -0,0 +1,19 @@ +<node> + <interface name='org.gtk.Application'> + <method name='Activate'> + <arg type='a{sv}' name='platform_data' direction='in'/> + </method> + <method name='Open'> + <arg type='as' name='uris' direction='in'/> + <arg type='s' name='hint' direction='in'/> + <arg type='a{sv}' name='platform_data' direction='in'/> + </method> + <method name='CommandLine'> + <arg type='o' name='path' direction='in'/> + <arg type='aay' name='arguments' direction='in'/> + <arg type='a{sv}' name='platform_data' direction='in'/> + <arg type='i' name='exit_status' direction='out'/> + </method> + <property name='Busy' type='b' access='read'/> + </interface> +</node> diff --git a/src/run-js-test.c b/src/run-js-test.c new file mode 100644 index 0000000..ba5e875 --- /dev/null +++ b/src/run-js-test.c @@ -0,0 +1,118 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Based on gjs/console.c from GJS + * + * Copyright (c) 2008 litl, LLC + * Copyright (c) 2010 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "config.h" + +#include <locale.h> +#include <stdlib.h> +#include <string.h> + +#include <girepository.h> +#include <gjs/gjs.h> + +#include "shell-global.h" +#include "shell-global-private.h" + +static char *command = NULL; + +static GOptionEntry entries[] = { + { "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" }, + { NULL } +}; + +int +main(int argc, char **argv) +{ + GOptionContext *context; + GError *error = NULL; + ShellGlobal *global; + GjsContext *js_context; + char *script; + const char *filename; + char *title; + gsize len; + int code; + + context = g_option_context_new (NULL); + + /* pass unknown through to the JS script */ + g_option_context_set_ignore_unknown_options (context, TRUE); + + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + g_error ("option parsing failed: %s", error->message); + + setlocale (LC_ALL, ""); + + _shell_global_init (NULL); + global = shell_global_get (); + js_context = _shell_global_get_gjs_context (global); + + /* prepare command line arguments */ + if (!gjs_context_define_string_array (js_context, "ARGV", + argc - 2, (const char**)argv + 2, + &error)) { + g_printerr ("Failed to defined ARGV: %s", error->message); + exit (1); + } + + if (command != NULL) { + script = command; + len = strlen (script); + filename = "<command line>"; + } else if (argc <= 1) { + script = g_strdup ("const Console = imports.console; Console.interact();"); + len = strlen (script); + filename = "<stdin>"; + } else /*if (argc >= 2)*/ { + error = NULL; + if (!g_file_get_contents (argv[1], &script, &len, &error)) { + g_printerr ("%s\n", error->message); + exit (1); + } + filename = argv[1]; + } + + title = g_filename_display_basename (filename); + g_set_prgname (title); + g_free (title); + + /* evaluate the script */ + error = NULL; + if (!gjs_context_eval (js_context, script, len, + filename, &code, &error)) { + g_free (script); + g_printerr ("%s\n", error->message); + exit (1); + } + + gjs_context_gc (js_context); + gjs_context_gc (js_context); + + g_object_unref (js_context); + g_free (script); + exit (code); +} diff --git a/src/shell-action-modes.h b/src/shell-action-modes.h new file mode 100644 index 0000000..edbdd16 --- /dev/null +++ b/src/shell-action-modes.h @@ -0,0 +1,35 @@ +/** + * ShellActionMode: + * @SHELL_ACTION_MODE_NONE: block action + * @SHELL_ACTION_MODE_NORMAL: allow action when in window mode, + * e.g. when the focus is in an application window + * @SHELL_ACTION_MODE_OVERVIEW: allow action while the overview + * is active + * @SHELL_ACTION_MODE_LOCK_SCREEN: allow action when the screen + * is locked, e.g. when the screen shield is shown + * @SHELL_ACTION_MODE_UNLOCK_SCREEN: allow action in the unlock + * dialog + * @SHELL_ACTION_MODE_LOGIN_SCREEN: allow action in the login screen + * @SHELL_ACTION_MODE_SYSTEM_MODAL: allow action when a system modal + * dialog (e.g. authentication or session dialogs) is open + * @SHELL_ACTION_MODE_LOOKING_GLASS: allow action in looking glass + * @SHELL_ACTION_MODE_POPUP: allow action while a shell menu is open + * @SHELL_ACTION_MODE_ALL: always allow action + * + * Controls in which GNOME Shell states an action (like keybindings and gestures) + * should be handled. +*/ +typedef enum { + SHELL_ACTION_MODE_NONE = 0, + SHELL_ACTION_MODE_NORMAL = 1 << 0, + SHELL_ACTION_MODE_OVERVIEW = 1 << 1, + SHELL_ACTION_MODE_LOCK_SCREEN = 1 << 2, + SHELL_ACTION_MODE_UNLOCK_SCREEN = 1 << 3, + SHELL_ACTION_MODE_LOGIN_SCREEN = 1 << 4, + SHELL_ACTION_MODE_SYSTEM_MODAL = 1 << 5, + SHELL_ACTION_MODE_LOOKING_GLASS = 1 << 6, + SHELL_ACTION_MODE_POPUP = 1 << 7, + + SHELL_ACTION_MODE_ALL = ~0, +} ShellActionMode; + diff --git a/src/shell-app-cache-private.h b/src/shell-app-cache-private.h new file mode 100644 index 0000000..b73094a --- /dev/null +++ b/src/shell-app-cache-private.h @@ -0,0 +1,19 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_CACHE_PRIVATE_H__ +#define __SHELL_APP_CACHE_PRIVATE_H__ + +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> + +#define SHELL_TYPE_APP_CACHE (shell_app_cache_get_type()) + +G_DECLARE_FINAL_TYPE (ShellAppCache, shell_app_cache, SHELL, APP_CACHE, GObject) + +ShellAppCache *shell_app_cache_get_default (void); +GList *shell_app_cache_get_all (ShellAppCache *cache); +GDesktopAppInfo *shell_app_cache_get_info (ShellAppCache *cache, + const char *id); +char *shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name); + +#endif /* __SHELL_APP_CACHE_PRIVATE_H__ */ diff --git a/src/shell-app-cache.c b/src/shell-app-cache.c new file mode 100644 index 0000000..44fc8b0 --- /dev/null +++ b/src/shell-app-cache.c @@ -0,0 +1,404 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-app-cache-private.h" + +/** + * SECTION:shell-app-cache + * @title: ShellAppCache + * @short_description: application information cache + * + * The #ShellAppCache is responsible for caching information about #GAppInfo + * to ensure that the compositor thread never needs to perform disk reads to + * access them. All of the work is done off-thread. When the new data has + * been loaded, a #ShellAppCache::changed signal is emitted. + * + * Additionally, the #ShellAppCache caches information about translations for + * directories. This allows translation provided in [Desktop Entry] GKeyFiles + * to be available when building StLabel and other elements without performing + * costly disk reads. + * + * Various monitors are used to keep this information up to date while the + * Shell is running. + */ + +#define DEFAULT_TIMEOUT_SECONDS 5 + +struct _ShellAppCache +{ + GObject parent_instance; + + GAppInfoMonitor *monitor; + GPtrArray *dir_monitors; + GHashTable *folders; + GCancellable *cancellable; + GList *app_infos; + + guint queued_update; +}; + +typedef struct +{ + GList *app_infos; + GHashTable *folders; +} CacheState; + +G_DEFINE_TYPE (ShellAppCache, shell_app_cache, G_TYPE_OBJECT) + +enum { + CHANGED, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +cache_state_free (CacheState *state) +{ + g_clear_pointer (&state->folders, g_hash_table_unref); + g_list_free_full (state->app_infos, g_object_unref); + g_free (state); +} + +static CacheState * +cache_state_new (void) +{ + CacheState *state; + + state = g_new0 (CacheState, 1); + state->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + return g_steal_pointer (&state); +} + +/** + * shell_app_cache_get_default: + * + * Gets the default #ShellAppCache. + * + * Returns: (transfer none): a #ShellAppCache + */ +ShellAppCache * +shell_app_cache_get_default (void) +{ + static ShellAppCache *instance; + + if (instance == NULL) + { + instance = g_object_new (SHELL_TYPE_APP_CACHE, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + + return instance; +} + +static void +load_folder (GHashTable *folders, + const char *path) +{ + g_autoptr(GDir) dir = NULL; + const char *name; + + g_assert (folders != NULL); + g_assert (path != NULL); + + dir = g_dir_open (path, 0, NULL); + if (dir == NULL) + return; + + while ((name = g_dir_read_name (dir))) + { + g_autofree gchar *filename = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + + /* First added wins */ + if (g_hash_table_contains (folders, name)) + continue; + + filename = g_build_filename (path, name, NULL); + keyfile = g_key_file_new (); + + if (g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL)) + { + gchar *translated; + + translated = g_key_file_get_locale_string (keyfile, + "Desktop Entry", "Name", + NULL, NULL); + + if (translated != NULL) + g_hash_table_insert (folders, g_strdup (name), translated); + } + } +} + +static void +load_folders (GHashTable *folders) +{ + const char * const *dirs; + g_autofree gchar *userdir = NULL; + guint i; + + g_assert (folders != NULL); + + userdir = g_build_filename (g_get_user_data_dir (), "desktop-directories", NULL); + load_folder (folders, userdir); + + dirs = g_get_system_data_dirs (); + for (i = 0; dirs[i] != NULL; i++) + { + g_autofree gchar *sysdir = g_build_filename (dirs[i], "desktop-directories", NULL); + load_folder (folders, sysdir); + } +} + +static void +shell_app_cache_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CacheState *state; + + g_assert (G_IS_TASK (task)); + g_assert (SHELL_IS_APP_CACHE (source_object)); + + state = cache_state_new (); + state->app_infos = g_app_info_get_all (); + load_folders (state->folders); + + g_task_return_pointer (task, state, (GDestroyNotify) cache_state_free); +} + +static void +apply_update_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellAppCache *cache = (ShellAppCache *)object; + g_autoptr(GError) error = NULL; + CacheState *state; + + g_assert (SHELL_IS_APP_CACHE (cache)); + g_assert (G_IS_TASK (result)); + g_assert (user_data == NULL); + + state = g_task_propagate_pointer (G_TASK (result), &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_list_free_full (cache->app_infos, g_object_unref); + cache->app_infos = g_steal_pointer (&state->app_infos); + + g_clear_pointer (&cache->folders, g_hash_table_unref); + cache->folders = g_steal_pointer (&state->folders); + + g_signal_emit (cache, signals[CHANGED], 0); + + cache_state_free (state); +} + +static gboolean +shell_app_cache_do_update (gpointer user_data) +{ + ShellAppCache *cache = user_data; + g_autoptr(GTask) task = NULL; + + cache->queued_update = 0; + + /* Reset the cancellable state so we don't race with + * two updates coming back overlapped and applying the + * information in the wrong order. + */ + g_cancellable_cancel (cache->cancellable); + g_clear_object (&cache->cancellable); + cache->cancellable = g_cancellable_new (); + + task = g_task_new (cache, cache->cancellable, apply_update_cb, NULL); + g_task_set_source_tag (task, shell_app_cache_do_update); + g_task_run_in_thread (task, shell_app_cache_worker); + + return G_SOURCE_REMOVE; +} + +static void +shell_app_cache_queue_update (ShellAppCache *self) +{ + g_assert (SHELL_IS_APP_CACHE (self)); + + if (self->queued_update != 0) + g_source_remove (self->queued_update); + + self->queued_update = g_timeout_add_seconds (DEFAULT_TIMEOUT_SECONDS, + shell_app_cache_do_update, + self); +} + +static void +monitor_desktop_directories_for_data_dir (ShellAppCache *self, + const gchar *directory) +{ + g_autofree gchar *subdir = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileMonitor) monitor = NULL; + + g_assert (SHELL_IS_APP_CACHE (self)); + + if (directory == NULL) + return; + + subdir = g_build_filename (directory, "desktop-directories", NULL); + file = g_file_new_for_path (subdir); + monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL); + + if (monitor != NULL) + { + g_file_monitor_set_rate_limit (monitor, DEFAULT_TIMEOUT_SECONDS * 1000); + g_signal_connect_object (monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + g_ptr_array_add (self->dir_monitors, g_steal_pointer (&monitor)); + } +} + +static void +shell_app_cache_finalize (GObject *object) +{ + ShellAppCache *self = (ShellAppCache *)object; + + g_clear_object (&self->monitor); + + if (self->queued_update) + { + g_source_remove (self->queued_update); + self->queued_update = 0; + } + + g_clear_pointer (&self->dir_monitors, g_ptr_array_unref); + g_clear_pointer (&self->folders, g_hash_table_unref); + g_list_free_full (self->app_infos, g_object_unref); + + G_OBJECT_CLASS (shell_app_cache_parent_class)->finalize (object); +} + +static void +shell_app_cache_class_init (ShellAppCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_app_cache_finalize; + + /** + * ShellAppCache::changed: + * + * The "changed" signal is emitted when the cache has updated + * information about installed applications. + */ + signals [CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +shell_app_cache_init (ShellAppCache *self) +{ + const gchar * const *sysdirs; + guint i; + + /* Monitor directories for translation changes */ + self->dir_monitors = g_ptr_array_new_with_free_func (g_object_unref); + monitor_desktop_directories_for_data_dir (self, g_get_user_data_dir ()); + sysdirs = g_get_system_data_dirs (); + for (i = 0; sysdirs[i] != NULL; i++) + monitor_desktop_directories_for_data_dir (self, sysdirs[i]); + + /* Load translated directory names immediately */ + self->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + load_folders (self->folders); + + /* Setup AppMonitor to track changes */ + self->monitor = g_app_info_monitor_get (); + g_signal_connect_object (self->monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + self->app_infos = g_app_info_get_all (); +} + +/** + * shell_app_cache_get_all: + * @cache: (nullable): a #ShellAppCache or %NULL + * + * Like g_app_info_get_all() but always returns a + * cached set of application info so the caller can be + * sure that I/O will not happen on the current thread. + * + * Returns: (transfer none) (element-type GAppInfo): + * a #GList of references to #GAppInfo. + */ +GList * +shell_app_cache_get_all (ShellAppCache *cache) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + return cache->app_infos; +} + +/** + * shell_app_cache_get_info: + * @cache: (nullable): a #ShellAppCache or %NULL + * @id: the application id + * + * A replacement for g_desktop_app_info_new() that will lookup the + * information from the cache instead of (re)loading from disk. + * + * Returns: (nullable) (transfer none): a #GDesktopAppInfo or %NULL + */ +GDesktopAppInfo * +shell_app_cache_get_info (ShellAppCache *cache, + const char *id) +{ + const GList *iter; + + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + for (iter = cache->app_infos; iter != NULL; iter = iter->next) + { + GAppInfo *info = iter->data; + + if (g_strcmp0 (id, g_app_info_get_id (info)) == 0) + return G_DESKTOP_APP_INFO (info); + } + + return NULL; +} + +/** + * shell_app_cache_translate_folder: + * @cache: (nullable): a #ShellAppCache or %NULL + * @name: the folder name + * + * Gets the translated folder name for @name if any exists. + * + * Returns: (nullable): the translated string or %NULL if there is no + * translation. + */ +char * +shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + if (name == NULL) + return NULL; + + return g_strdup (g_hash_table_lookup (cache->folders, name)); +} diff --git a/src/shell-app-private.h b/src/shell-app-private.h new file mode 100644 index 0000000..b1786b3 --- /dev/null +++ b/src/shell-app-private.h @@ -0,0 +1,24 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_PRIVATE_H__ +#define __SHELL_APP_PRIVATE_H__ + +#include "shell-app.h" +#include "shell-app-system.h" + +G_BEGIN_DECLS + +ShellApp* _shell_app_new_for_window (MetaWindow *window); + +ShellApp* _shell_app_new (GDesktopAppInfo *info); + +void _shell_app_set_app_info (ShellApp *app, GDesktopAppInfo *info); + +void _shell_app_handle_startup_sequence (ShellApp *app, MetaStartupSequence *sequence); + +void _shell_app_add_window (ShellApp *app, MetaWindow *window); + +void _shell_app_remove_window (ShellApp *app, MetaWindow *window); + +G_END_DECLS + +#endif /* __SHELL_APP_PRIVATE_H__ */ diff --git a/src/shell-app-system-private.h b/src/shell-app-system-private.h new file mode 100644 index 0000000..975d563 --- /dev/null +++ b/src/shell-app-system-private.h @@ -0,0 +1,9 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_SYSTEM_PRIVATE_H__ +#define __SHELL_APP_SYSTEM_PRIVATE_H__ + +#include "shell-app-system.h" + +void _shell_app_system_notify_app_state_changed (ShellAppSystem *self, ShellApp *app); + +#endif diff --git a/src/shell-app-system.c b/src/shell-app-system.c new file mode 100644 index 0000000..2899544 --- /dev/null +++ b/src/shell-app-system.c @@ -0,0 +1,586 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-app-system.h" +#include "shell-app-usage.h" +#include <string.h> + +#include <gio/gio.h> +#include <glib/gi18n.h> + +#include "shell-app-cache-private.h" +#include "shell-app-private.h" +#include "shell-window-tracker-private.h" +#include "shell-app-system-private.h" +#include "shell-global.h" +#include "shell-util.h" +#include "st.h" + +/* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That + * should be plenty of time for even a slow spinning drive to update + * the icon cache. + */ +#define RESCAN_TIMEOUT_MS 2500 +#define MAX_RESCAN_RETRIES 6 + +/* Vendor prefixes are something that can be preprended to a .desktop + * file name. Undo this. + */ +static const char*const vendor_prefixes[] = { "gnome-", + "fedora-", + "mozilla-", + "debian-", + NULL }; + +enum { + PROP_0, + +}; + +enum { + APP_STATE_CHANGED, + INSTALLED_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +typedef struct _ShellAppSystemPrivate ShellAppSystemPrivate; + +struct _ShellAppSystem +{ + GObject parent; + + ShellAppSystemPrivate *priv; +}; + +struct _ShellAppSystemPrivate { + GHashTable *running_apps; + GHashTable *id_to_app; + GHashTable *startup_wm_class_to_id; + GList *installed_apps; + + guint rescan_icons_timeout_id; + guint n_rescan_retries; +}; + +static void shell_app_system_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (ShellAppSystem, shell_app_system, G_TYPE_OBJECT); + +static void shell_app_system_class_init(ShellAppSystemClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *)klass; + + gobject_class->finalize = shell_app_system_finalize; + + signals[APP_STATE_CHANGED] = g_signal_new ("app-state-changed", + SHELL_TYPE_APP_SYSTEM, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + SHELL_TYPE_APP); + signals[INSTALLED_CHANGED] = + g_signal_new ("installed-changed", + SHELL_TYPE_APP_SYSTEM, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +/* + * Check whether @wm_class matches @id exactly when ignoring the .desktop suffix + */ +static gboolean +startup_wm_class_is_exact_match (const char *id, + const char *wm_class) +{ + size_t wm_class_len; + + if (!g_str_has_prefix (id, wm_class)) + return FALSE; + + wm_class_len = strlen (wm_class); + if (id[wm_class_len] == '\0') + return TRUE; + + return g_str_equal (id + wm_class_len, ".desktop"); +} + +static void +scan_startup_wm_class_to_id (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv = self->priv; + g_autoptr(GPtrArray) no_show_ids = NULL; + const GList *l; + GList *all; + + g_hash_table_remove_all (priv->startup_wm_class_to_id); + + all = shell_app_cache_get_all (shell_app_cache_get_default ()); + no_show_ids = g_ptr_array_new (); + + for (l = all; l != NULL; l = l->next) + { + GAppInfo *info = l->data; + const char *startup_wm_class, *id, *old_id; + gboolean should_show; + + id = g_app_info_get_id (info); + startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (info)); + + if (startup_wm_class == NULL) + continue; + + should_show = g_app_info_should_show (info); + if (!should_show) + g_ptr_array_add (no_show_ids, (char *) id); + + /* In case multiple .desktop files set the same StartupWMClass, prefer + * the one where ID and StartupWMClass match */ + old_id = g_hash_table_lookup (priv->startup_wm_class_to_id, startup_wm_class); + + if (old_id && startup_wm_class_is_exact_match (id, startup_wm_class)) + old_id = NULL; + + /* Give priority to the desktop files that should be shown */ + if (old_id && should_show && + g_ptr_array_find_with_equal_func (no_show_ids, old_id, g_str_equal, NULL)) + old_id = NULL; + + if (!old_id) + g_hash_table_insert (priv->startup_wm_class_to_id, + g_strdup (startup_wm_class), g_strdup (id)); + } +} + +static gboolean +app_is_stale (ShellApp *app) +{ + GDesktopAppInfo *info, *old; + GAppInfo *old_info, *new_info; + gboolean is_unchanged; + + if (shell_app_is_window_backed (app)) + return FALSE; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), + shell_app_get_id (app)); + if (!info) + return TRUE; + + old = shell_app_get_app_info (app); + old_info = G_APP_INFO (old); + new_info = G_APP_INFO (info); + + is_unchanged = + g_app_info_should_show (old_info) == g_app_info_should_show (new_info) && + strcmp (g_desktop_app_info_get_filename (old), + g_desktop_app_info_get_filename (info)) == 0 && + g_strcmp0 (g_app_info_get_executable (old_info), + g_app_info_get_executable (new_info)) == 0 && + g_strcmp0 (g_app_info_get_commandline (old_info), + g_app_info_get_commandline (new_info)) == 0 && + strcmp (g_app_info_get_name (old_info), + g_app_info_get_name (new_info)) == 0 && + g_strcmp0 (g_app_info_get_description (old_info), + g_app_info_get_description (new_info)) == 0 && + strcmp (g_app_info_get_display_name (old_info), + g_app_info_get_display_name (new_info)) == 0 && + g_icon_equal (g_app_info_get_icon (old_info), + g_app_info_get_icon (new_info)); + + return !is_unchanged; +} + +static gboolean +stale_app_remove_func (gpointer key, + gpointer value, + gpointer user_data) +{ + return app_is_stale (value); +} + +static void +collect_stale_windows (gpointer key, + gpointer value, + gpointer user_data) +{ + ShellApp *app = key; + GDesktopAppInfo *info; + GPtrArray *windows = user_data; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), + shell_app_get_id (app)); + + /* No info either means that the app became stale, or that it is + * window-backed. Re-tracking the app's windows allows us to reflect + * changes in either direction, i.e. from stale app to window-backed, + * or from window-backed to app-backed (if the app was launched right + * between installing the app and updating the app cache). + */ + if (info == NULL) + { + GSList *l; + + for (l = shell_app_get_windows (app); l; l = l->next) + g_ptr_array_add (windows, l->data); + } +} + +static void +retrack_window (gpointer data, + gpointer user_data) +{ + GObject *window = data; + + /* Make ShellWindowTracker retrack the window */ + g_object_notify (window, "wm-class"); +} + +static gboolean +rescan_icon_theme_cb (gpointer user_data) +{ + ShellAppSystemPrivate *priv; + ShellAppSystem *self; + StTextureCache *texture_cache; + gboolean rescanned; + + self = (ShellAppSystem *) user_data; + priv = self->priv; + + texture_cache = st_texture_cache_get_default (); + rescanned = st_texture_cache_rescan_icon_theme (texture_cache); + + priv->n_rescan_retries++; + + if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES) + { + priv->n_rescan_retries = 0; + priv->rescan_icons_timeout_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +rescan_icon_theme (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv = self->priv; + + priv->n_rescan_retries = 0; + + if (priv->rescan_icons_timeout_id > 0) + return; + + priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS, + rescan_icon_theme_cb, + self); +} + +static void +installed_changed (ShellAppCache *cache, + ShellAppSystem *self) +{ + GPtrArray *windows = g_ptr_array_new (); + + rescan_icon_theme (self); + scan_startup_wm_class_to_id (self); + + g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL); + g_hash_table_foreach (self->priv->running_apps, collect_stale_windows, windows); + + g_ptr_array_foreach (windows, retrack_window, NULL); + g_ptr_array_free (windows, TRUE); + + g_signal_emit (self, signals[INSTALLED_CHANGED], 0, NULL); +} + +static void +shell_app_system_init (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv; + ShellAppCache *cache; + + self->priv = priv = shell_app_system_get_instance_private (self); + + priv->running_apps = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL); + priv->id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify)g_object_unref); + + priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + cache = shell_app_cache_get_default (); + g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self); + installed_changed (cache, self); +} + +static void +shell_app_system_finalize (GObject *object) +{ + ShellAppSystem *self = SHELL_APP_SYSTEM (object); + ShellAppSystemPrivate *priv = self->priv; + + g_hash_table_destroy (priv->running_apps); + g_hash_table_destroy (priv->id_to_app); + g_hash_table_destroy (priv->startup_wm_class_to_id); + g_list_free_full (priv->installed_apps, g_object_unref); + g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove); + + G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object); +} + +/** + * shell_app_system_get_default: + * + * Return Value: (transfer none): The global #ShellAppSystem singleton + */ +ShellAppSystem * +shell_app_system_get_default (void) +{ + static ShellAppSystem *instance = NULL; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL); + + return instance; +} + +/** + * shell_app_system_lookup_app: + * + * Find a #ShellApp corresponding to an id. + * + * Return value: (transfer none): The #ShellApp for id, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_app (ShellAppSystem *self, + const char *id) +{ + ShellAppSystemPrivate *priv = self->priv; + ShellApp *app; + GDesktopAppInfo *info; + + app = g_hash_table_lookup (priv->id_to_app, id); + if (app) + return app; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), id); + if (!info) + return NULL; + + app = _shell_app_new (info); + g_hash_table_insert (priv->id_to_app, (char *) shell_app_get_id (app), app); + return app; +} + +/** + * shell_app_system_lookup_heuristic_basename: + * @system: a #ShellAppSystem + * @id: Probable application identifier + * + * Find a valid application corresponding to a given + * heuristically determined application identifier + * string, or %NULL if none. + * + * Returns: (transfer none): A #ShellApp for @name + */ +ShellApp * +shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *name) +{ + ShellApp *result; + const char *const *prefix; + + result = shell_app_system_lookup_app (system, name); + if (result != NULL) + return result; + + for (prefix = vendor_prefixes; *prefix != NULL; prefix++) + { + char *tmpid = g_strconcat (*prefix, name, NULL); + result = shell_app_system_lookup_app (system, tmpid); + g_free (tmpid); + if (result != NULL) + return result; + } + + return NULL; +} + +/** + * shell_app_system_lookup_desktop_wmclass: + * @system: a #ShellAppSystem + * @wmclass: (nullable): A WM_CLASS value + * + * Find a valid application whose .desktop file, without the extension + * and properly canonicalized, matches @wmclass. + * + * Returns: (transfer none): A #ShellApp for @wmclass + */ +ShellApp * +shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system, + const char *wmclass) +{ + char *canonicalized; + char *desktop_file; + ShellApp *app; + + if (wmclass == NULL) + return NULL; + + /* First try without changing the case (this handles + org.example.Foo.Bar.desktop applications) + + Note that is slightly wrong in that Gtk+ would set + the WM_CLASS to Org.example.Foo.Bar, but it also + sets the instance part to org.example.Foo.Bar, so we're ok + */ + desktop_file = g_strconcat (wmclass, ".desktop", NULL); + app = shell_app_system_lookup_heuristic_basename (system, desktop_file); + g_free (desktop_file); + + if (app) + return app; + + canonicalized = g_ascii_strdown (wmclass, -1); + + /* This handles "Fedora Eclipse", probably others. + * Note g_strdelimit is modify-in-place. */ + g_strdelimit (canonicalized, " ", '-'); + + desktop_file = g_strconcat (canonicalized, ".desktop", NULL); + + app = shell_app_system_lookup_heuristic_basename (system, desktop_file); + + g_free (canonicalized); + g_free (desktop_file); + + return app; +} + +/** + * shell_app_system_lookup_startup_wmclass: + * @system: a #ShellAppSystem + * @wmclass: (nullable): A WM_CLASS value + * + * Find a valid application whose .desktop file contains a + * StartupWMClass entry matching @wmclass. + * + * Returns: (transfer none): A #ShellApp for @wmclass + */ +ShellApp * +shell_app_system_lookup_startup_wmclass (ShellAppSystem *system, + const char *wmclass) +{ + const char *id; + + if (wmclass == NULL) + return NULL; + + id = g_hash_table_lookup (system->priv->startup_wm_class_to_id, wmclass); + if (id == NULL) + return NULL; + + return shell_app_system_lookup_app (system, id); +} + +void +_shell_app_system_notify_app_state_changed (ShellAppSystem *self, + ShellApp *app) +{ + ShellAppState state = shell_app_get_state (app); + + switch (state) + { + case SHELL_APP_STATE_RUNNING: + g_hash_table_insert (self->priv->running_apps, g_object_ref (app), NULL); + break; + case SHELL_APP_STATE_STARTING: + break; + case SHELL_APP_STATE_STOPPED: + g_hash_table_remove (self->priv->running_apps, app); + break; + default: + g_warn_if_reached(); + break; + } + g_signal_emit (self, signals[APP_STATE_CHANGED], 0, app); +} + +/** + * shell_app_system_get_running: + * @self: A #ShellAppSystem + * + * Returns the set of applications which currently have at least one + * open window. The returned list will be sorted by shell_app_compare(). + * + * Returns: (element-type ShellApp) (transfer container): Active applications + */ +GSList * +shell_app_system_get_running (ShellAppSystem *self) +{ + gpointer key, value; + GSList *ret; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, self->priv->running_apps); + + ret = NULL; + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellApp *app = key; + + ret = g_slist_prepend (ret, app); + } + + ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare); + + return ret; +} + +/** + * shell_app_system_search: + * @search_string: the search string to use + * + * Wrapper around g_desktop_app_info_search() that replaces results that + * don't validate as UTF-8 with the empty string. + * + * Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a + * list of strvs. Free each item with g_strfreev() and free the outer + * list with g_free(). + */ +char *** +shell_app_system_search (const char *search_string) +{ + char ***results = g_desktop_app_info_search (search_string); + char ***groups, **ids; + + for (groups = results; *groups; groups++) + for (ids = *groups; *ids; ids++) + if (!g_utf8_validate (*ids, -1, NULL)) + **ids = '\0'; + + return results; +} + +/** + * shell_app_system_get_installed: + * @self: the #ShellAppSystem + * + * Returns all installed apps, as a list of #GAppInfo + * + * Returns: (transfer none) (element-type GAppInfo): a list of #GAppInfo + * describing all known applications. This memory is owned by the + * #ShellAppSystem and should not be freed. + **/ +GList * +shell_app_system_get_installed (ShellAppSystem *self) +{ + return shell_app_cache_get_all (shell_app_cache_get_default ()); +} diff --git a/src/shell-app-system.h b/src/shell-app-system.h new file mode 100644 index 0000000..8719dbc --- /dev/null +++ b/src/shell-app-system.h @@ -0,0 +1,32 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_SYSTEM_H__ +#define __SHELL_APP_SYSTEM_H__ + +#include <gio/gio.h> +#include <clutter/clutter.h> +#include <meta/window.h> + +#include "shell-app.h" + +#define SHELL_TYPE_APP_SYSTEM (shell_app_system_get_type ()) +G_DECLARE_FINAL_TYPE (ShellAppSystem, shell_app_system, + SHELL, APP_SYSTEM, GObject) + +ShellAppSystem *shell_app_system_get_default (void); + +ShellApp *shell_app_system_lookup_app (ShellAppSystem *system, + const char *id); +ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *id); + +ShellApp *shell_app_system_lookup_startup_wmclass (ShellAppSystem *system, + const char *wmclass); +ShellApp *shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system, + const char *wmclass); + +GSList *shell_app_system_get_running (ShellAppSystem *self); +char ***shell_app_system_search (const char *search_string); + +GList *shell_app_system_get_installed (ShellAppSystem *self); + +#endif /* __SHELL_APP_SYSTEM_H__ */ diff --git a/src/shell-app-usage.c b/src/shell-app-usage.c new file mode 100644 index 0000000..0dfb209 --- /dev/null +++ b/src/shell-app-usage.c @@ -0,0 +1,774 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <string.h> +#include <stdlib.h> + +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <glib.h> +#include <gio/gio.h> +#include <meta/display.h> +#include <meta/group.h> +#include <meta/window.h> + +#include "shell-app-usage.h" +#include "shell-window-tracker.h" +#include "shell-global.h" + +/* This file includes modified code from + * desktop-data-engine/engine-dbus/hippo-application-monitor.c + * in the functions collecting application usage data. + * Written by Owen Taylor, originally licensed under LGPL 2.1. + * Copyright Red Hat, Inc. 2006-2008 + */ + +/** + * SECTION:shell-app-usage + * @short_description: Track application usage/state data + * + * This class maintains some usage and state statistics for + * applications by keeping track of the approximate time an application's + * windows are focused, as well as the last workspace it was seen on. + * This time tracking is implemented by watching for focus notifications, + * and computing a time delta between them. Also we watch the + * GNOME Session "StatusChanged" signal which by default is emitted after 5 + * minutes to signify idle. + */ + +#define PRIVACY_SCHEMA "org.gnome.desktop.privacy" +#define ENABLE_MONITORING_KEY "remember-app-usage" + +#define FOCUS_TIME_MIN_SECONDS 7 /* Need 7 continuous seconds of focus */ + +#define USAGE_CLEAN_DAYS 7 /* If after 7 days we haven't seen an app, purge it */ + +/* Data is saved to file SHELL_CONFIG_DIR/DATA_FILENAME */ +#define DATA_FILENAME "application_state" + +#define IDLE_TIME_TRANSITION_SECONDS 30 /* If we transition to idle, only count + * this many seconds of usage */ + +/* The ranking algorithm we use is: every time an app score reaches SCORE_MAX, + * divide all scores by 2. Scores are raised by 1 unit every SAVE_APPS_TIMEOUT + * seconds. This mechanism allows the list to update relatively fast when + * a new app is used intensively. + * To keep the list clean, and avoid being Big Brother, apps that have not been + * seen for a week and whose score is below SCORE_MIN are removed. + */ + +/* How often we save internally app data, in seconds */ +#define SAVE_APPS_TIMEOUT_SECONDS (5 * 60) + +/* With this value, an app goes from bottom to top of the + * usage list in 50 hours of use */ +#define SCORE_MAX (3600 * 50 / FOCUS_TIME_MIN_SECONDS) + +/* If an app's score in lower than this and the app has not been used in a week, + * remove it */ +#define SCORE_MIN (SCORE_MAX >> 3) + +/* http://www.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Presence */ +#define GNOME_SESSION_STATUS_IDLE 3 + +typedef struct UsageData UsageData; + +struct _ShellAppUsage +{ + GObject parent; + + GFile *configfile; + GDBusProxy *session_proxy; + GSettings *privacy_settings; + guint idle_focus_change_id; + guint save_id; + gboolean currently_idle; + gboolean enable_monitoring; + + long watch_start_time; + ShellApp *watched_app; + + /* <char *appid, UsageData *usage> */ + GHashTable *app_usages; +}; + +G_DEFINE_TYPE (ShellAppUsage, shell_app_usage, G_TYPE_OBJECT); + +/* Represents an application record for a given context */ +struct UsageData +{ + gdouble score; /* Based on the number of times we'e seen the app and normalized */ + long last_seen; /* Used to clear old apps we've only seen a few times */ +}; + +static void shell_app_usage_finalize (GObject *object); + +static void on_session_status_changed (GDBusProxy *proxy, guint status, ShellAppUsage *self); +static void on_focus_app_changed (ShellWindowTracker *tracker, GParamSpec *spec, ShellAppUsage *self); +static void ensure_queued_save (ShellAppUsage *self); + +static gboolean idle_save_application_usage (gpointer data); + +static void restore_from_file (ShellAppUsage *self); + +static void update_enable_monitoring (ShellAppUsage *self); + +static void on_enable_monitoring_key_changed (GSettings *settings, + const gchar *key, + ShellAppUsage *self); + +static long +get_time (void) +{ + return g_get_real_time () / G_TIME_SPAN_SECOND; +} + +static void +shell_app_usage_class_init (ShellAppUsageClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_app_usage_finalize; +} + +static UsageData * +get_usage_for_app (ShellAppUsage *self, + ShellApp *app) +{ + UsageData *usage; + const char *appid = shell_app_get_id (app); + + usage = g_hash_table_lookup (self->app_usages, appid); + if (usage) + return usage; + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (self->app_usages, g_strdup (appid), usage); + + return usage; +} + +/* Limit the score to a certain level so that most used apps can change */ +static void +normalize_usage (ShellAppUsage *self) +{ + GHashTableIter iter; + UsageData *usage; + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &usage)) + usage->score /= 2; +} + +static void +increment_usage_for_app_at_time (ShellAppUsage *self, + ShellApp *app, + long time) +{ + UsageData *usage; + guint elapsed; + guint usage_count; + + usage = get_usage_for_app (self, app); + + usage->last_seen = time; + + elapsed = time - self->watch_start_time; + usage_count = elapsed / FOCUS_TIME_MIN_SECONDS; + if (usage_count > 0) + { + usage->score += usage_count; + if (usage->score > SCORE_MAX) + normalize_usage (self); + ensure_queued_save (self); + } +} + +static void +increment_usage_for_app (ShellAppUsage *self, + ShellApp *app) +{ + long curtime = get_time (); + increment_usage_for_app_at_time (self, app, curtime); +} + +static void +on_app_state_changed (ShellAppSystem *app_system, + ShellApp *app, + gpointer user_data) +{ + ShellAppUsage *self = SHELL_APP_USAGE (user_data); + UsageData *usage; + gboolean running; + + if (shell_app_is_window_backed (app)) + return; + + usage = get_usage_for_app (self, app); + + running = shell_app_get_state (app) == SHELL_APP_STATE_RUNNING; + + if (running) + usage->last_seen = get_time (); +} + +static void +on_focus_app_changed (ShellWindowTracker *tracker, + GParamSpec *spec, + ShellAppUsage *self) +{ + if (self->watched_app != NULL) + increment_usage_for_app (self, self->watched_app); + + if (self->watched_app) + g_object_unref (self->watched_app); + + g_object_get (tracker, "focus-app", &(self->watched_app), NULL); + self->watch_start_time = get_time (); +} + +static void +on_session_status_changed (GDBusProxy *proxy, + guint status, + ShellAppUsage *self) +{ + gboolean idle; + + idle = (status >= GNOME_SESSION_STATUS_IDLE); + if (self->currently_idle == idle) + return; + + self->currently_idle = idle; + if (idle) + { + long end_time; + + /* The GNOME Session signal we watch is 5 minutes, but that's a long + * time for this purpose. Instead, just add a base 30 seconds. + */ + if (self->watched_app) + { + end_time = self->watch_start_time + IDLE_TIME_TRANSITION_SECONDS; + increment_usage_for_app_at_time (self, self->watched_app, end_time); + } + } + else + { + /* Transitioning to !idle, reset the start time */ + self->watch_start_time = get_time (); + } +} + +static void +session_proxy_signal (GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data) +{ + if (g_str_equal (signal_name, "StatusChanged")) + { + guint status; + g_variant_get (parameters, "(u)", &status); + on_session_status_changed (proxy, status, SHELL_APP_USAGE (user_data)); + } +} + +static void +shell_app_usage_init (ShellAppUsage *self) +{ + ShellGlobal *global; + char *shell_userdata_dir, *path; + GDBusConnection *session_bus; + ShellWindowTracker *tracker; + ShellAppSystem *app_system; + + global = shell_global_get (); + + self->app_usages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + tracker = shell_window_tracker_get_default (); + g_signal_connect (tracker, "notify::focus-app", G_CALLBACK (on_focus_app_changed), self); + + app_system = shell_app_system_get_default (); + g_signal_connect (app_system, "app-state-changed", G_CALLBACK (on_app_state_changed), self); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + self->session_proxy = g_dbus_proxy_new_sync (session_bus, + G_DBUS_PROXY_FLAGS_NONE, + NULL, /* interface info */ + "org.gnome.SessionManager", + "/org/gnome/SessionManager/Presence", + "org.gnome.SessionManager", + NULL, /* cancellable */ + NULL /* error */); + g_signal_connect (self->session_proxy, "g-signal", G_CALLBACK (session_proxy_signal), self); + g_object_unref (session_bus); + + self->currently_idle = FALSE; + self->enable_monitoring = FALSE; + + g_object_get (global, "userdatadir", &shell_userdata_dir, NULL), + path = g_build_filename (shell_userdata_dir, DATA_FILENAME, NULL); + g_free (shell_userdata_dir); + self->configfile = g_file_new_for_path (path); + g_free (path); + restore_from_file (self); + + self->privacy_settings = g_settings_new(PRIVACY_SCHEMA); + g_signal_connect (self->privacy_settings, + "changed::" ENABLE_MONITORING_KEY, + G_CALLBACK (on_enable_monitoring_key_changed), + self); + update_enable_monitoring (self); +} + +static void +shell_app_usage_finalize (GObject *object) +{ + ShellAppUsage *self = SHELL_APP_USAGE (object); + + g_clear_handle_id (&self->save_id, g_source_remove); + + g_object_unref (self->privacy_settings); + + g_object_unref (self->configfile); + + g_object_unref (self->session_proxy); + + G_OBJECT_CLASS (shell_app_usage_parent_class)->finalize(object); +} + +static int +sort_apps_by_usage (gconstpointer a, + gconstpointer b, + gpointer datap) +{ + ShellAppUsage *self = datap; + ShellApp *app_a, *app_b; + UsageData *usage_a, *usage_b; + + app_a = (ShellApp*)a; + app_b = (ShellApp*)b; + + usage_a = g_hash_table_lookup (self->app_usages, shell_app_get_id (app_a)); + usage_b = g_hash_table_lookup (self->app_usages, shell_app_get_id (app_b)); + + return usage_b->score - usage_a->score; +} + +/** + * shell_app_usage_get_most_used: + * @usage: the usage instance to request + * + * Returns: (element-type ShellApp) (transfer full): List of applications + */ +GSList * +shell_app_usage_get_most_used (ShellAppUsage *self) +{ + GSList *apps; + char *appid; + ShellAppSystem *appsys; + GHashTableIter iter; + + appsys = shell_app_system_get_default (); + + g_hash_table_iter_init (&iter, self->app_usages); + apps = NULL; + while (g_hash_table_iter_next (&iter, (gpointer *) &appid, NULL)) + { + ShellApp *app; + + app = shell_app_system_lookup_app (appsys, appid); + if (!app) + continue; + + apps = g_slist_prepend (apps, g_object_ref (app)); + } + + apps = g_slist_sort_with_data (apps, sort_apps_by_usage, self); + + return apps; +} + + +/** + * shell_app_usage_compare: + * @self: the usage instance to request + * @id_a: ID of first app + * @id_b: ID of second app + * + * Compare @id_a and @id_b based on frequency of use. + * + * Returns: -1 if @id_a ranks higher than @id_b, 1 if @id_b ranks higher + * than @id_a, and 0 if both rank equally. + */ +int +shell_app_usage_compare (ShellAppUsage *self, + const char *id_a, + const char *id_b) +{ + UsageData *usage_a, *usage_b; + + usage_a = g_hash_table_lookup (self->app_usages, id_a); + usage_b = g_hash_table_lookup (self->app_usages, id_b); + + if (usage_a == NULL && usage_b == NULL) + return 0; + else if (usage_a == NULL) + return 1; + else if (usage_b == NULL) + return -1; + + return usage_b->score - usage_a->score; +} + +static void +ensure_queued_save (ShellAppUsage *self) +{ + if (self->save_id != 0) + return; + self->save_id = g_timeout_add_seconds (SAVE_APPS_TIMEOUT_SECONDS, idle_save_application_usage, self); + g_source_set_name_by_id (self->save_id, "[gnome-shell] idle_save_application_usage"); +} + +/* Clean up apps we see rarely. + * The logic behind this is that if an app was seen less than SCORE_MIN times + * and not seen for a week, it can probably be forgotten about. + * This should much reduce the size of the list and avoid 'pollution'. */ +static gboolean +idle_clean_usage (ShellAppUsage *self) +{ + GHashTableIter iter; + UsageData *usage; + long current_time; + long week_ago; + + current_time = get_time (); + week_ago = current_time - (7 * 24 * 60 * 60); + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &usage)) + { + if ((usage->score < SCORE_MIN) && + (usage->last_seen < week_ago)) + g_hash_table_iter_remove (&iter); + } + + return FALSE; +} + +static gboolean +write_escaped (GDataOutputStream *stream, + const char *str, + GError **error) +{ + gboolean ret; + char *quoted = g_markup_escape_text (str, -1); + ret = g_data_output_stream_put_string (stream, quoted, NULL, error); + g_free (quoted); + return ret; +} + +static gboolean +write_attribute_string (GDataOutputStream *stream, + const char *elt_name, + const char *str, + GError **error) +{ + gboolean ret = FALSE; + char *elt; + + elt = g_strdup_printf (" %s=\"", elt_name); + ret = g_data_output_stream_put_string (stream, elt, NULL, error); + g_free (elt); + if (!ret) + goto out; + + ret = write_escaped (stream, str, error); + if (!ret) + goto out; + + ret = g_data_output_stream_put_string (stream, "\"", NULL, error); + +out: + return ret; +} + +static gboolean +write_attribute_uint (GDataOutputStream *stream, + const char *elt_name, + guint value, + GError **error) +{ + gboolean ret; + char *buf; + + buf = g_strdup_printf ("%u", value); + ret = write_attribute_string (stream, elt_name, buf, error); + g_free (buf); + + return ret; +} + +static gboolean +write_attribute_double (GDataOutputStream *stream, + const char *elt_name, + double value, + GError **error) +{ + gchar buf[G_ASCII_DTOSTR_BUF_SIZE]; + gboolean ret; + + g_ascii_dtostr (buf, sizeof (buf), value); + ret = write_attribute_string (stream, elt_name, buf, error); + + return ret; +} + +/* Save app data lists to file */ +static gboolean +idle_save_application_usage (gpointer data) +{ + ShellAppUsage *self = SHELL_APP_USAGE (data); + char *id; + GHashTableIter iter; + UsageData *usage; + GFileOutputStream *output; + GOutputStream *buffered_output; + GDataOutputStream *data_output; + GError *error = NULL; + + self->save_id = 0; + + /* Parent directory is already created by shell-global */ + output = g_file_replace (self->configfile, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error); + if (!output) + { + g_debug ("Could not save applications usage data: %s", error->message); + g_error_free (error); + return FALSE; + } + buffered_output = g_buffered_output_stream_new (G_OUTPUT_STREAM (output)); + g_object_unref (output); + data_output = g_data_output_stream_new (G_OUTPUT_STREAM (buffered_output)); + g_object_unref (buffered_output); + + if (!g_data_output_stream_put_string (data_output, "<?xml version=\"1.0\"?>\n<application-state>\n", NULL, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, " <context id=\"\">\n", NULL, &error)) + goto out; + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &usage)) + { + ShellApp *app; + + app = shell_app_system_lookup_app (shell_app_system_get_default(), id); + + if (!app) + continue; + + if (!g_data_output_stream_put_string (data_output, " <application", NULL, &error)) + goto out; + if (!write_attribute_string (data_output, "id", id, &error)) + goto out; + if (!write_attribute_double (data_output, "score", usage->score, &error)) + goto out; + if (!write_attribute_uint (data_output, "last-seen", usage->last_seen, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, "/>\n", NULL, &error)) + goto out; + } + if (!g_data_output_stream_put_string (data_output, " </context>\n", NULL, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, "</application-state>\n", NULL, &error)) + goto out; + +out: + if (!error) + g_output_stream_close_async (G_OUTPUT_STREAM (data_output), 0, NULL, NULL, NULL); + g_object_unref (data_output); + if (error) + { + g_debug ("Could not save applications usage data: %s", error->message); + g_error_free (error); + } + return FALSE; +} + +static void +shell_app_usage_start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ShellAppUsage *self = user_data; + + if (strcmp (element_name, "application-state") == 0) + { + } + else if (strcmp (element_name, "context") == 0) + { + } + else if (strcmp (element_name, "application") == 0) + { + const char **attribute; + const char **value; + UsageData *usage; + char *appid = NULL; + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "id") == 0) + { + appid = g_strdup (*value); + break; + } + } + + if (!appid) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Missing attribute id on <%s> element", + element_name); + return; + } + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (self->app_usages, appid, usage); + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "score") == 0) + { + usage->score = g_ascii_strtod (*value, NULL); + } + else if (strcmp (*attribute, "last-seen") == 0) + { + usage->last_seen = (guint) g_ascii_strtoull (*value, NULL, 10); + } + } + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Unknown element <%s>", + element_name); + } +} + +static GMarkupParser app_state_parse_funcs = +{ + shell_app_usage_start_element_handler, + NULL, + NULL, + NULL, + NULL +}; + +/* Load data about apps usage from file */ +static void +restore_from_file (ShellAppUsage *self) +{ + GFileInputStream *input; + GMarkupParseContext *parse_context; + GError *error = NULL; + char buf[1024]; + + input = g_file_read (self->configfile, NULL, &error); + if (error) + { + if (error->code != G_IO_ERROR_NOT_FOUND) + g_warning ("Could not load applications usage data: %s", error->message); + + g_error_free (error); + return; + } + + parse_context = g_markup_parse_context_new (&app_state_parse_funcs, 0, self, NULL); + + while (TRUE) + { + gssize count = g_input_stream_read ((GInputStream*) input, buf, sizeof(buf), NULL, &error); + if (count <= 0) + goto out; + if (!g_markup_parse_context_parse (parse_context, buf, count, &error)) + goto out; + } + +out: + g_markup_parse_context_free (parse_context); + g_input_stream_close ((GInputStream*)input, NULL, NULL); + g_object_unref (input); + + idle_clean_usage (self); + + if (error) + { + g_warning ("Could not load applications usage data: %s", error->message); + g_error_free (error); + } +} + +/* Enable or disable the timers, depending on the value of ENABLE_MONITORING_KEY + * and taking care of the previous state. If selfing is disabled, we still + * report apps usage based on (possibly) saved data, but don't collect data. + */ +static void +update_enable_monitoring (ShellAppUsage *self) +{ + gboolean enable; + + enable = g_settings_get_boolean (self->privacy_settings, + ENABLE_MONITORING_KEY); + + /* Be sure not to start the timers if they were already set */ + if (enable && !self->enable_monitoring) + { + on_focus_app_changed (shell_window_tracker_get_default (), NULL, self); + } + /* ...and don't try to stop them if they were not running */ + else if (!enable && self->enable_monitoring) + { + if (self->watched_app) + g_object_unref (self->watched_app); + self->watched_app = NULL; + g_clear_handle_id (&self->save_id, g_source_remove); + } + + self->enable_monitoring = enable; +} + +/* Called when the ENABLE_MONITORING_KEY boolean has changed */ +static void +on_enable_monitoring_key_changed (GSettings *settings, + const gchar *key, + ShellAppUsage *self) +{ + update_enable_monitoring (self); +} + +/** + * shell_app_usage_get_default: + * + * Return Value: (transfer none): The global #ShellAppUsage instance + */ +ShellAppUsage * +shell_app_usage_get_default (void) +{ + static ShellAppUsage *instance; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_APP_USAGE, NULL); + + return instance; +} diff --git a/src/shell-app-usage.h b/src/shell-app-usage.h new file mode 100644 index 0000000..4b0e169 --- /dev/null +++ b/src/shell-app-usage.h @@ -0,0 +1,23 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_USAGE_H__ +#define __SHELL_APP_USAGE_H__ + +#include "shell-app.h" +#include "shell-window-tracker.h" + +G_BEGIN_DECLS + +#define SHELL_TYPE_APP_USAGE (shell_app_usage_get_type ()) +G_DECLARE_FINAL_TYPE (ShellAppUsage, shell_app_usage, + SHELL, APP_USAGE, GObject) + +ShellAppUsage* shell_app_usage_get_default(void); + +GSList *shell_app_usage_get_most_used (ShellAppUsage *usage); +int shell_app_usage_compare (ShellAppUsage *self, + const char *id_a, + const char *id_b); + +G_END_DECLS + +#endif /* __SHELL_APP_USAGE_H__ */ diff --git a/src/shell-app.c b/src/shell-app.c new file mode 100644 index 0000000..5c38a9c --- /dev/null +++ b/src/shell-app.c @@ -0,0 +1,1756 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <string.h> + +#include <glib/gi18n-lib.h> + +#include <meta/display.h> +#include <meta/meta-context.h> +#include <meta/meta-workspace-manager.h> +#include <meta/meta-x11-display.h> + +#include "shell-app-private.h" +#include "shell-enum-types.h" +#include "shell-global.h" +#include "shell-util.h" +#include "shell-app-system-private.h" +#include "shell-window-tracker-private.h" +#include "st.h" +#include "gtkactionmuxer.h" +#include "org-gtk-application.h" +#include "switcheroo-control.h" + +#ifdef HAVE_SYSTEMD +#include <systemd/sd-journal.h> +#include <errno.h> +#include <unistd.h> +#endif + +/* This is mainly a memory usage optimization - the user is going to + * be running far fewer of the applications at one time than they have + * installed. But it also just helps keep the code more logically + * separated. + */ +typedef struct { + guint refcount; + + /* Signal connection to dirty window sort list on workspace changes */ + gulong workspace_switch_id; + + GSList *windows; + + guint interesting_windows; + + /* Whether or not we need to resort the windows; this is done on demand */ + guint window_sort_stale : 1; + + /* See GApplication documentation */ + GtkActionMuxer *muxer; + char *unique_bus_name; + GDBusConnection *session; + + /* GDBus Proxy for getting application busy state */ + ShellOrgGtkApplication *application_proxy; + GCancellable *cancellable; + +} ShellAppRunningState; + +/** + * SECTION:shell-app + * @short_description: Object representing an application + * + * This object wraps a #GDesktopAppInfo, providing methods and signals + * primarily useful for running applications. + */ +struct _ShellApp +{ + GObject parent; + + int started_on_workspace; + + ShellAppState state; + + GDesktopAppInfo *info; /* If NULL, this app is backed by one or more + * MetaWindow. For purposes of app title + * etc., we use the first window added, + * because it's most likely to be what we + * want (e.g. it will be of TYPE_NORMAL from + * the way shell-window-tracker.c works). + */ + GIcon *fallback_icon; + MetaWindow *fallback_icon_window; + + ShellAppRunningState *running_state; + + char *window_id_string; + char *name_collation_key; +}; + +enum { + PROP_0, + + PROP_STATE, + PROP_BUSY, + PROP_ID, + PROP_ACTION_GROUP, + PROP_ICON, + PROP_APP_INFO, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum { + WINDOWS_CHANGED, + LAST_SIGNAL +}; + +static guint shell_app_signals[LAST_SIGNAL] = { 0 }; + +static void create_running_state (ShellApp *app); +static void unref_running_state (ShellAppRunningState *state); + +G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT) + +static void +shell_app_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellApp *app = SHELL_APP (gobject); + + switch (prop_id) + { + case PROP_STATE: + g_value_set_enum (value, app->state); + break; + case PROP_BUSY: + g_value_set_boolean (value, shell_app_get_busy (app)); + break; + case PROP_ID: + g_value_set_string (value, shell_app_get_id (app)); + break; + case PROP_ICON: + g_value_set_object (value, shell_app_get_icon (app)); + break; + case PROP_ACTION_GROUP: + if (app->running_state) + g_value_set_object (value, app->running_state->muxer); + break; + case PROP_APP_INFO: + if (app->info) + g_value_set_object (value, app->info); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_app_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellApp *app = SHELL_APP (gobject); + + switch (prop_id) + { + case PROP_APP_INFO: + _shell_app_set_app_info (app, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +const char * +shell_app_get_id (ShellApp *app) +{ + if (app->info) + return g_app_info_get_id (G_APP_INFO (app->info)); + return app->window_id_string; +} + +static MetaWindow * +window_backed_app_get_window (ShellApp *app) +{ + g_assert (app->info == NULL); + if (app->running_state) + { + g_assert (app->running_state->windows); + return app->running_state->windows->data; + } + else + return NULL; +} + +static GIcon * +x11_window_create_fallback_gicon (MetaWindow *window) +{ + StTextureCache *texture_cache; + cairo_surface_t *surface; + + g_object_get (window, "icon", &surface, NULL); + + texture_cache = st_texture_cache_get_default (); + return st_texture_cache_load_cairo_surface_to_gicon (texture_cache, surface); +} + +static void +on_window_icon_changed (GObject *object, + const GParamSpec *pspec, + gpointer user_data) +{ + MetaWindow *window = META_WINDOW (object); + ShellApp *app = user_data; + + g_clear_object (&app->fallback_icon); + app->fallback_icon = x11_window_create_fallback_gicon (window); + + if (!app->fallback_icon) + app->fallback_icon = g_themed_icon_new ("application-x-executable"); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ICON]); +} + +/** + * shell_app_get_icon: + * + * Look up the icon for this application + * + * Return value: (transfer none): A #GIcon + */ +GIcon * +shell_app_get_icon (ShellApp *app) +{ + MetaWindow *window = NULL; + + g_return_val_if_fail (SHELL_IS_APP (app), NULL); + + if (app->info) + return g_app_info_get_icon (G_APP_INFO (app->info)); + + if (app->fallback_icon) + return app->fallback_icon; + + /* During a state transition from running to not-running for + * window-backend apps, it's possible we get a request for the icon. + * Avoid asserting here and just return a fallback icon + */ + if (app->running_state != NULL) + window = window_backed_app_get_window (app); + + if (window && + meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_X11) + { + app->fallback_icon_window = window; + app->fallback_icon = x11_window_create_fallback_gicon (window); + g_signal_connect (G_OBJECT (window), + "notify::icon", G_CALLBACK (on_window_icon_changed), app); + } + else + { + app->fallback_icon = g_themed_icon_new ("application-x-executable"); + } + + return app->fallback_icon; +} + +/** + * shell_app_create_icon_texture: + * + * Look up the icon for this application, and create a #ClutterActor + * for it at the given size. + * + * Return value: (transfer none): A floating #ClutterActor + */ +ClutterActor * +shell_app_create_icon_texture (ShellApp *app, + int size) +{ + ClutterActor *ret; + + ret = st_icon_new (); + st_icon_set_icon_size (ST_ICON (ret), size); + st_icon_set_fallback_icon_name (ST_ICON (ret), "application-x-executable"); + + g_object_bind_property (app, "icon", ret, "gicon", G_BINDING_SYNC_CREATE); + + if (shell_app_is_window_backed (app)) + st_widget_add_style_class_name (ST_WIDGET (ret), "fallback-app-icon"); + + return ret; +} + +const char * +shell_app_get_name (ShellApp *app) +{ + if (app->info) + return g_app_info_get_name (G_APP_INFO (app->info)); + else + { + MetaWindow *window = window_backed_app_get_window (app); + const char *name = NULL; + + if (window) + name = meta_window_get_wm_class (window); + if (!name) + name = C_("program", "Unknown"); + return name; + } +} + +const char * +shell_app_get_description (ShellApp *app) +{ + if (app->info) + return g_app_info_get_description (G_APP_INFO (app->info)); + else + return NULL; +} + +/** + * shell_app_is_window_backed: + * + * A window backed application is one which represents just an open + * window, i.e. there's no .desktop file association, so we don't know + * how to launch it again. + */ +gboolean +shell_app_is_window_backed (ShellApp *app) +{ + return app->info == NULL; +} + +typedef struct { + MetaWorkspace *workspace; + GSList **transients; +} CollectTransientsData; + +static gboolean +collect_transients_on_workspace (MetaWindow *window, + gpointer datap) +{ + CollectTransientsData *data = datap; + + if (data->workspace && meta_window_get_workspace (window) != data->workspace) + return TRUE; + + *data->transients = g_slist_prepend (*data->transients, window); + return TRUE; +} + +/* The basic idea here is that when we're targeting a window, + * if it has transients we want to pick the most recent one + * the user interacted with. + * This function makes raising GEdit with the file chooser + * open work correctly. + */ +static MetaWindow * +find_most_recent_transient_on_same_workspace (MetaDisplay *display, + MetaWindow *reference) +{ + GSList *transients, *transients_sorted, *iter; + MetaWindow *result; + CollectTransientsData data; + + transients = NULL; + data.workspace = meta_window_get_workspace (reference); + data.transients = &transients; + + meta_window_foreach_transient (reference, collect_transients_on_workspace, &data); + + transients_sorted = meta_display_sort_windows_by_stacking (display, transients); + /* Reverse this so we're top-to-bottom (yes, we should probably change the order + * returned from the sort_windows_by_stacking function) + */ + transients_sorted = g_slist_reverse (transients_sorted); + g_slist_free (transients); + transients = NULL; + + result = NULL; + for (iter = transients_sorted; iter; iter = iter->next) + { + MetaWindow *window = iter->data; + MetaWindowType wintype = meta_window_get_window_type (window); + + /* Don't want to focus UTILITY types, like the Gimp toolbars */ + if (wintype == META_WINDOW_NORMAL || + wintype == META_WINDOW_DIALOG) + { + result = window; + break; + } + } + g_slist_free (transients_sorted); + return result; +} + +static MetaWorkspace * +get_active_workspace (void) +{ + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + return meta_workspace_manager_get_active_workspace (workspace_manager); +} + +/** + * shell_app_activate_window: + * @app: a #ShellApp + * @window: (nullable): Window to be focused + * @timestamp: Event timestamp + * + * Bring all windows for the given app to the foreground, + * but ensure that @window is on top. If @window is %NULL, + * the window with the most recent user time for the app + * will be used. + * + * This function has no effect if @app is not currently running. + */ +void +shell_app_activate_window (ShellApp *app, + MetaWindow *window, + guint32 timestamp) +{ + g_autoptr (GSList) windows = NULL; + + if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING) + return; + + windows = shell_app_get_windows (app); + if (window == NULL && windows) + window = windows->data; + + if (!g_slist_find (windows, window)) + return; + else + { + GSList *windows_reversed, *iter; + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaWorkspace *active = get_active_workspace (); + MetaWorkspace *workspace = meta_window_get_workspace (window); + guint32 last_user_timestamp = meta_display_get_last_user_time (display); + MetaWindow *most_recent_transient; + + if (meta_display_xserver_time_is_before (display, timestamp, last_user_timestamp)) + { + meta_window_set_demands_attention (window); + return; + } + + /* Now raise all the other windows for the app that are on + * the same workspace, in reverse order to preserve the stacking. + */ + windows_reversed = g_slist_copy (windows); + windows_reversed = g_slist_reverse (windows_reversed); + for (iter = windows_reversed; iter; iter = iter->next) + { + MetaWindow *other_window = iter->data; + + if (other_window != window && meta_window_get_workspace (other_window) == workspace) + meta_window_raise (other_window); + } + g_slist_free (windows_reversed); + + /* If we have a transient that the user's interacted with more recently than + * the window, pick that. + */ + most_recent_transient = find_most_recent_transient_on_same_workspace (display, window); + if (most_recent_transient + && meta_display_xserver_time_is_before (display, + meta_window_get_user_time (window), + meta_window_get_user_time (most_recent_transient))) + window = most_recent_transient; + + + if (active != workspace) + meta_workspace_activate_with_focus (workspace, window, timestamp); + else + meta_window_activate (window, timestamp); + } +} + + +void +shell_app_update_window_actions (ShellApp *app, MetaWindow *window) +{ + const char *object_path; + + object_path = meta_window_get_gtk_window_object_path (window); + if (object_path != NULL) + { + GActionGroup *actions; + + actions = g_object_get_data (G_OBJECT (window), "actions"); + if (actions == NULL) + { + actions = G_ACTION_GROUP (g_dbus_action_group_get (app->running_state->session, + meta_window_get_gtk_unique_bus_name (window), + object_path)); + g_object_set_data_full (G_OBJECT (window), "actions", actions, g_object_unref); + } + + g_assert (app->running_state->muxer); + gtk_action_muxer_insert (app->running_state->muxer, "win", actions); + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ACTION_GROUP]); + } +} + +/** + * shell_app_activate: + * @app: a #ShellApp + * + * Like shell_app_activate_full(), but using the default workspace and + * event timestamp. + */ +void +shell_app_activate (ShellApp *app) +{ + return shell_app_activate_full (app, -1, 0); +} + +/** + * shell_app_activate_full: + * @app: a #ShellApp + * @workspace: launch on this workspace, or -1 for default. Ignored if + * activating an existing window + * @timestamp: Event timestamp + * + * Perform an appropriate default action for operating on this application, + * dependent on its current state. For example, if the application is not + * currently running, launch it. If it is running, activate the most + * recently used NORMAL window (or if that window has a transient, the most + * recently used transient for that window). + */ +void +shell_app_activate_full (ShellApp *app, + int workspace, + guint32 timestamp) +{ + ShellGlobal *global; + + global = shell_global_get (); + + if (timestamp == 0) + timestamp = shell_global_get_current_time (global); + + switch (app->state) + { + case SHELL_APP_STATE_STOPPED: + { + GError *error = NULL; + if (!shell_app_launch (app, timestamp, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, &error)) + { + char *msg; + msg = g_strdup_printf (_("Failed to launch “%s”"), shell_app_get_name (app)); + shell_global_notify_error (global, + msg, + error->message); + g_free (msg); + g_clear_error (&error); + } + } + break; + case SHELL_APP_STATE_STARTING: + break; + case SHELL_APP_STATE_RUNNING: + shell_app_activate_window (app, NULL, timestamp); + break; + default: + g_assert_not_reached(); + break; + } +} + +/** + * shell_app_open_new_window: + * @app: a #ShellApp + * @workspace: open on this workspace, or -1 for default + * + * Request that the application create a new window. + */ +void +shell_app_open_new_window (ShellApp *app, + int workspace) +{ + GActionGroup *group = NULL; + const char * const *actions; + + g_return_if_fail (app->info != NULL); + + /* First check whether the application provides a "new-window" desktop + * action - it is a safe bet that it will open a new window, and activating + * it will trigger startup notification if necessary + */ + actions = g_desktop_app_info_list_actions (G_DESKTOP_APP_INFO (app->info)); + + if (g_strv_contains (actions, "new-window")) + { + shell_app_launch_action (app, "new-window", 0, workspace); + return; + } + + /* Next, check whether the app exports an explicit "new-window" action + * that we can activate on the bus - the muxer will add startup notification + * information to the platform data, so this should work just as well as + * desktop actions. + */ + group = app->running_state ? G_ACTION_GROUP (app->running_state->muxer) + : NULL; + + if (group && + g_action_group_has_action (group, "app.new-window") && + g_action_group_get_action_parameter_type (group, "app.new-window") == NULL) + { + g_action_group_activate_action (group, "app.new-window", NULL); + + return; + } + + /* Lastly, just always launch the application again, even if we know + * it was already running. For most applications this + * should have the effect of creating a new window, whether that's + * a second process (in the case of Calculator) or IPC to existing + * instance (Firefox). There are a few less-sensical cases such + * as say Pidgin. + */ + shell_app_launch (app, 0, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, NULL); +} + +/** + * shell_app_can_open_new_window: + * @app: a #ShellApp + * + * Returns %TRUE if the app supports opening a new window through + * shell_app_open_new_window() (ie, if calling that function will + * result in actually opening a new window and not something else, + * like presenting the most recently active one) + */ +gboolean +shell_app_can_open_new_window (ShellApp *app) +{ + ShellAppRunningState *state; + MetaWindow *window; + GDesktopAppInfo *desktop_info; + const char * const *desktop_actions; + + /* Apps that are stopped can always open new windows, because + * activating them would open the first one; if they are starting, + * we cannot tell whether they can open additional windows until + * they are running */ + if (app->state != SHELL_APP_STATE_RUNNING) + return app->state == SHELL_APP_STATE_STOPPED; + + state = app->running_state; + + /* If the app has an explicit new-window action, then it can + (or it should be able to) ... + */ + if (g_action_group_has_action (G_ACTION_GROUP (state->muxer), "app.new-window")) + return TRUE; + + /* If the app doesn't have a desktop file, then nothing is possible */ + if (!app->info) + return FALSE; + + desktop_info = G_DESKTOP_APP_INFO (app->info); + + /* If the app is explicitly telling us via its desktop file, then we know + * for sure + */ + if (g_desktop_app_info_has_key (desktop_info, "SingleMainWindow")) + return !g_desktop_app_info_get_boolean (desktop_info, + "SingleMainWindow"); + + /* GNOME-specific key, for backwards compatibility with apps that haven't + * started using the XDG "SingleMainWindow" key yet + */ + if (g_desktop_app_info_has_key (desktop_info, "X-GNOME-SingleWindow")) + return !g_desktop_app_info_get_boolean (desktop_info, + "X-GNOME-SingleWindow"); + + /* If it has a new-window desktop action, it should be able to */ + desktop_actions = g_desktop_app_info_list_actions (desktop_info); + if (desktop_actions && g_strv_contains (desktop_actions, "new-window")) + return TRUE; + + /* If this is a unique GtkApplication, and we don't have a new-window, then + probably we can't + + We don't consider non-unique GtkApplications here to handle cases like + evince, which don't export a new-window action because each window is in + a different process. In any case, in a non-unique GtkApplication each + Activate() knows nothing about the other instances, so it will show a + new window. + */ + + window = state->windows->data; + + if (state->unique_bus_name != NULL && + meta_window_get_gtk_application_object_path (window) != NULL) + { + if (meta_window_get_gtk_application_id (window) != NULL) + return FALSE; + else + return TRUE; + } + + /* In all other cases, we don't have a reliable source of information + or a decent heuristic, so we err on the compatibility side and say + yes. + */ + return TRUE; +} + +/** + * shell_app_get_state: + * @app: a #ShellApp + * + * Returns: State of the application + */ +ShellAppState +shell_app_get_state (ShellApp *app) +{ + return app->state; +} + +typedef struct { + ShellApp *app; + MetaWorkspace *active_workspace; +} CompareWindowsData; + +static int +shell_app_compare_windows (gconstpointer a, + gconstpointer b, + gpointer datap) +{ + MetaWindow *win_a = (gpointer)a; + MetaWindow *win_b = (gpointer)b; + CompareWindowsData *data = datap; + gboolean ws_a, ws_b; + gboolean vis_a, vis_b; + + ws_a = meta_window_get_workspace (win_a) == data->active_workspace; + ws_b = meta_window_get_workspace (win_b) == data->active_workspace; + + if (ws_a && !ws_b) + return -1; + else if (!ws_a && ws_b) + return 1; + + vis_a = meta_window_showing_on_its_workspace (win_a); + vis_b = meta_window_showing_on_its_workspace (win_b); + + if (vis_a && !vis_b) + return -1; + else if (!vis_a && vis_b) + return 1; + + return meta_window_get_user_time (win_b) - meta_window_get_user_time (win_a); +} + +/** + * shell_app_get_windows: + * @app: + * + * Get the windows which are associated with this application. The + * returned list will be sorted first by whether they're on the + * active workspace, then by whether they're visible, and finally + * by the time the user last interacted with them. + * + * Returns: (transfer container) (element-type MetaWindow): List of windows + */ +GSList * +shell_app_get_windows (ShellApp *app) +{ + GSList *windows = NULL; + GSList *l; + + if (app->running_state == NULL) + return NULL; + + if (app->running_state->window_sort_stale) + { + CompareWindowsData data; + data.app = app; + data.active_workspace = get_active_workspace (); + app->running_state->windows = g_slist_sort_with_data (app->running_state->windows, shell_app_compare_windows, &data); + app->running_state->window_sort_stale = FALSE; + } + + for (l = app->running_state->windows; l; l = l->next) + if (!meta_window_is_override_redirect (META_WINDOW (l->data))) + windows = g_slist_prepend (windows, l->data); + + return g_slist_reverse (windows); +} + +guint +shell_app_get_n_windows (ShellApp *app) +{ + if (app->running_state == NULL) + return 0; + return g_slist_length (app->running_state->windows); +} + +gboolean +shell_app_is_on_workspace (ShellApp *app, + MetaWorkspace *workspace) +{ + GSList *iter; + + if (shell_app_get_state (app) == SHELL_APP_STATE_STARTING) + { + if (app->started_on_workspace == -1 || + meta_workspace_index (workspace) == app->started_on_workspace) + return TRUE; + else + return FALSE; + } + + if (app->running_state == NULL) + return FALSE; + + for (iter = app->running_state->windows; iter; iter = iter->next) + { + if (meta_window_get_workspace (iter->data) == workspace) + return TRUE; + } + + return FALSE; +} + +static int +shell_app_get_last_user_time (ShellApp *app) +{ + GSList *iter; + guint32 last_user_time; + + last_user_time = 0; + + if (app->running_state != NULL) + { + for (iter = app->running_state->windows; iter; iter = iter->next) + last_user_time = MAX (last_user_time, meta_window_get_user_time (iter->data)); + } + + return (int)last_user_time; +} + +static gboolean +shell_app_is_minimized (ShellApp *app) +{ + GSList *iter; + + if (app->running_state == NULL) + return FALSE; + + for (iter = app->running_state->windows; iter; iter = iter->next) + { + if (meta_window_showing_on_its_workspace (iter->data)) + return FALSE; + } + + return TRUE; +} + +/** + * shell_app_compare: + * @app: + * @other: A #ShellApp + * + * Compare one #ShellApp instance to another, in the following way: + * - Running applications sort before not-running applications. + * - If one of them has non-minimized windows and the other does not, + * the one with visible windows is first. + * - Finally, the application which the user interacted with most recently + * compares earlier. + */ +int +shell_app_compare (ShellApp *app, + ShellApp *other) +{ + gboolean min_app, min_other; + + if (app->state != other->state) + { + if (app->state == SHELL_APP_STATE_RUNNING) + return -1; + return 1; + } + + min_app = shell_app_is_minimized (app); + min_other = shell_app_is_minimized (other); + + if (min_app != min_other) + { + if (min_other) + return -1; + return 1; + } + + if (app->state == SHELL_APP_STATE_RUNNING) + { + if (app->running_state->windows && !other->running_state->windows) + return -1; + else if (!app->running_state->windows && other->running_state->windows) + return 1; + + return shell_app_get_last_user_time (other) - shell_app_get_last_user_time (app); + } + + return 0; +} + +ShellApp * +_shell_app_new_for_window (MetaWindow *window) +{ + ShellApp *app; + + app = g_object_new (SHELL_TYPE_APP, NULL); + + app->window_id_string = g_strdup_printf ("window:%d", meta_window_get_stable_sequence (window)); + + _shell_app_add_window (app, window); + + return app; +} + +ShellApp * +_shell_app_new (GDesktopAppInfo *info) +{ + ShellApp *app; + + app = g_object_new (SHELL_TYPE_APP, + "app-info", info, + NULL); + + return app; +} + +void +_shell_app_set_app_info (ShellApp *app, + GDesktopAppInfo *info) +{ + g_set_object (&app->info, info); + + g_clear_pointer (&app->name_collation_key, g_free); + if (app->info) + app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1); +} + +static void +shell_app_state_transition (ShellApp *app, + ShellAppState state) +{ + if (app->state == state) + return; + g_return_if_fail (!(app->state == SHELL_APP_STATE_RUNNING && + state == SHELL_APP_STATE_STARTING)); + app->state = state; + + _shell_app_system_notify_app_state_changed (shell_app_system_get_default (), app); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_STATE]); +} + +static void +shell_app_on_user_time_changed (MetaWindow *window, + GParamSpec *pspec, + ShellApp *app) +{ + g_assert (app->running_state != NULL); + + /* Ideally we don't want to emit windows-changed if the sort order + * isn't actually changing. This check catches most of those. + */ + if (window != app->running_state->windows->data) + { + app->running_state->window_sort_stale = TRUE; + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); + } +} + +static void +shell_app_sync_running_state (ShellApp *app) +{ + g_return_if_fail (app->running_state != NULL); + + if (app->state != SHELL_APP_STATE_STARTING) + { + if (app->running_state->interesting_windows == 0) + shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); + else + shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); + } +} + + +static void +shell_app_on_skip_taskbar_changed (MetaWindow *window, + GParamSpec *pspec, + ShellApp *app) +{ + g_assert (app->running_state != NULL); + + /* we rely on MetaWindow:skip-taskbar only being notified + * when it actually changes; when that assumption breaks, + * we'll have to track the "interesting" windows themselves + */ + if (meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows--; + else + app->running_state->interesting_windows++; + + shell_app_sync_running_state (app); +} + +static void +shell_app_on_ws_switch (MetaWorkspaceManager *workspace_manager, + int from, + int to, + MetaMotionDirection direction, + gpointer data) +{ + ShellApp *app = SHELL_APP (data); + + g_assert (app->running_state != NULL); + + app->running_state->window_sort_stale = TRUE; + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +gboolean +shell_app_get_busy (ShellApp *app) +{ + if (app->running_state != NULL && + app->running_state->application_proxy != NULL && + shell_org_gtk_application_get_busy (app->running_state->application_proxy)) + return TRUE; + + return FALSE; +} + +static void +busy_changed_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + ShellApp *app = user_data; + + g_assert (SHELL_IS_APP (app)); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_BUSY]); +} + +static void +get_application_proxy (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + ShellApp *app = user_data; + ShellOrgGtkApplication *proxy; + g_autoptr (GError) error = NULL; + + g_assert (SHELL_IS_APP (app)); + + proxy = shell_org_gtk_application_proxy_new_finish (result, &error); + if (proxy != NULL) + { + app->running_state->application_proxy = proxy; + g_signal_connect (proxy, + "notify::busy", + G_CALLBACK (busy_changed_cb), + app); + if (shell_org_gtk_application_get_busy (proxy)) + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_BUSY]); + } + + if (app->running_state != NULL && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_clear_object (&app->running_state->cancellable); + + g_object_unref (app); +} + +static void +shell_app_ensure_busy_watch (ShellApp *app) +{ + ShellAppRunningState *running_state = app->running_state; + MetaWindow *window; + const gchar *object_path; + + if (running_state->application_proxy != NULL || + running_state->cancellable != NULL) + return; + + if (running_state->unique_bus_name == NULL) + return; + + window = g_slist_nth_data (running_state->windows, 0); + object_path = meta_window_get_gtk_application_object_path (window); + + if (object_path == NULL) + return; + + running_state->cancellable = g_cancellable_new(); + /* Take a reference to app to make sure it isn't finalized before + get_application_proxy runs */ + shell_org_gtk_application_proxy_new (running_state->session, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + running_state->unique_bus_name, + object_path, + running_state->cancellable, + get_application_proxy, + g_object_ref (app)); +} + +void +_shell_app_add_window (ShellApp *app, + MetaWindow *window) +{ + if (app->running_state && g_slist_find (app->running_state->windows, window)) + return; + + g_object_freeze_notify (G_OBJECT (app)); + + if (!app->running_state) + create_running_state (app); + + app->running_state->window_sort_stale = TRUE; + app->running_state->windows = g_slist_prepend (app->running_state->windows, g_object_ref (window)); + g_signal_connect_object (window, "notify::user-time", G_CALLBACK(shell_app_on_user_time_changed), app, 0); + g_signal_connect_object (window, "notify::skip-taskbar", G_CALLBACK(shell_app_on_skip_taskbar_changed), app, 0); + + shell_app_update_app_actions (app, window); + shell_app_ensure_busy_watch (app); + + if (!meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows++; + shell_app_sync_running_state (app); + + if (app->started_on_workspace >= 0 && !meta_window_is_on_all_workspaces (window)) + meta_window_change_workspace_by_index (window, app->started_on_workspace, FALSE); + app->started_on_workspace = -1; + + g_object_thaw_notify (G_OBJECT (app)); + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +void +_shell_app_remove_window (ShellApp *app, + MetaWindow *window) +{ + g_assert (app->running_state != NULL); + + if (!g_slist_find (app->running_state->windows, window)) + return; + + app->running_state->windows = g_slist_remove (app->running_state->windows, window); + + if (!meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows--; + shell_app_sync_running_state (app); + + if (app->running_state->windows == NULL) + g_clear_pointer (&app->running_state, unref_running_state); + + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_skip_taskbar_changed), app); + if (window == app->fallback_icon_window) + { + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(on_window_icon_changed), app); + app->fallback_icon_window = NULL; + + /* Select a new icon from a different window. */ + g_clear_object (&app->fallback_icon); + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ICON]); + } + + g_object_unref (window); + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +/** + * shell_app_get_pids: + * @app: a #ShellApp + * + * Returns: (transfer container) (element-type int): An unordered list of process identifiers associated with this application. + */ +GSList * +shell_app_get_pids (ShellApp *app) +{ + GSList *result; + g_autoptr (GSList) windows = NULL; + GSList *iter; + + result = NULL; + windows = shell_app_get_windows (app); + for (iter = windows; iter; iter = iter->next) + { + MetaWindow *window = iter->data; + pid_t pid = meta_window_get_pid (window); + + if (pid < 1) + continue; + + /* Note in the (by far) common case, app will only have one pid, so + * we'll hit the first element, so don't worry about O(N^2) here. + */ + if (!g_slist_find (result, GINT_TO_POINTER (pid))) + result = g_slist_prepend (result, GINT_TO_POINTER (pid)); + } + return result; +} + +void +_shell_app_handle_startup_sequence (ShellApp *app, + MetaStartupSequence *sequence) +{ + gboolean starting = !meta_startup_sequence_get_completed (sequence); + + /* The Shell design calls for on application launch, the app title + * appears at top, and no X window is focused. So when we get + * a startup-notification for this app, transition it to STARTING + * if it's currently stopped, set it as our application focus, + * but focus the no_focus window. + */ + if (starting && shell_app_get_state (app) == SHELL_APP_STATE_STOPPED) + { + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + shell_app_state_transition (app, SHELL_APP_STATE_STARTING); + meta_display_unset_input_focus (display, + meta_startup_sequence_get_timestamp (sequence)); + } + + if (starting) + app->started_on_workspace = meta_startup_sequence_get_workspace (sequence); + else if (app->running_state && app->running_state->windows) + shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); + else /* application have > 1 .desktop file */ + shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); +} + +/** + * shell_app_request_quit: + * @app: A #ShellApp + * + * Initiate an asynchronous request to quit this application. + * The application may interact with the user, and the user + * might cancel the quit request from the application UI. + * + * This operation may not be supported for all applications. + * + * Returns: %TRUE if a quit request is supported for this application + */ +gboolean +shell_app_request_quit (ShellApp *app) +{ + GActionGroup *group = NULL; + GSList *iter; + + if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING) + return FALSE; + + /* First, check whether the app exports an explicit "quit" action + * that we can activate on the bus + */ + group = G_ACTION_GROUP (app->running_state->muxer); + + if (g_action_group_has_action (group, "app.quit") && + g_action_group_get_action_parameter_type (group, "app.quit") == NULL) + { + g_action_group_activate_action (group, "app.quit", NULL); + + return TRUE; + } + + /* Otherwise, fall back to closing all the app's windows */ + for (iter = app->running_state->windows; iter; iter = iter->next) + { + MetaWindow *win = iter->data; + + if (!meta_window_can_close (win)) + continue; + + meta_window_delete (win, shell_global_get_current_time (shell_global_get ())); + } + return TRUE; +} + +static void +child_context_setup (gpointer user_data) +{ + ShellGlobal *shell_global = user_data; + MetaContext *meta_context; + + g_object_get (shell_global, "context", &meta_context, NULL); + meta_context_restore_rlimit_nofile (meta_context, NULL); +} + +#if !defined(HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS) && defined(HAVE_SYSTEMD) +/* This sets up the launched application to log to the journal + * using its own identifier, instead of just "gnome-session". + */ +static void +app_child_setup (gpointer user_data) +{ + const char *appid = user_data; + int res; + int journalfd = sd_journal_stream_fd (appid, LOG_INFO, FALSE); + ShellGlobal *shell_global = shell_global_get (); + + if (journalfd >= 0) + { + do + res = dup2 (journalfd, 1); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + do + res = dup2 (journalfd, 2); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + (void) close (journalfd); + } + + child_context_setup (shell_global); +} +#endif + +static void +wait_pid (GDesktopAppInfo *appinfo, + GPid pid, + gpointer user_data) +{ + g_child_watch_add (pid, (GChildWatchFunc) g_spawn_close_pid, NULL); +} + +static void +apply_discrete_gpu_env (GAppLaunchContext *context, + ShellGlobal *global) +{ + GDBusProxy *proxy; + GVariant* variant; + guint num_children, i; + + proxy = shell_global_get_switcheroo_control (global); + if (!proxy) + { + g_warning ("Could not apply discrete GPU environment, switcheroo-control not available"); + return; + } + + variant = shell_net_hadess_switcheroo_control_get_gpus (SHELL_NET_HADESS_SWITCHEROO_CONTROL (proxy)); + if (!variant) + { + g_warning ("Could not apply discrete GPU environment, no GPUs in list"); + return; + } + + num_children = g_variant_n_children (variant); + for (i = 0; i < num_children; i++) + { + g_autoptr(GVariant) gpu = NULL; + g_autoptr(GVariant) env = NULL; + g_autoptr(GVariant) default_variant = NULL; + g_autofree const char **env_s = NULL; + guint j; + + gpu = g_variant_get_child_value (variant, i); + if (!gpu || + !g_variant_is_of_type (gpu, G_VARIANT_TYPE ("a{s*}"))) + continue; + + /* Skip over the default GPU */ + default_variant = g_variant_lookup_value (gpu, "Default", NULL); + if (!default_variant || g_variant_get_boolean (default_variant)) + continue; + + env = g_variant_lookup_value (gpu, "Environment", NULL); + if (!env) + continue; + + env_s = g_variant_get_strv (env, NULL); + for (j = 0; env_s[j] != NULL; j = j + 2) + g_app_launch_context_setenv (context, env_s[j], env_s[j+1]); + return; + } + + g_debug ("Could not find discrete GPU in switcheroo-control, not applying environment"); +} + +/** + * shell_app_launch: + * @timestamp: Event timestamp, or 0 for current event timestamp + * @workspace: Start on this workspace, or -1 for default + * @gpu_pref: the GPU to prefer launching on + * @error: A #GError + */ +gboolean +shell_app_launch (ShellApp *app, + guint timestamp, + int workspace, + ShellAppLaunchGpu gpu_pref, + GError **error) +{ + ShellGlobal *global; + GAppLaunchContext *context; + gboolean ret; + GSpawnFlags flags; + gboolean discrete_gpu = FALSE; + ShellGlobal *shell_global = shell_global_get (); + + if (app->info == NULL) + { + MetaWindow *window = window_backed_app_get_window (app); + /* We don't use an error return if there no longer any windows, because the + * user attempting to activate a stale window backed app isn't something + * we would expect the caller to meaningfully handle or display an error + * message to the user. + */ + if (window) + meta_window_activate (window, timestamp); + return TRUE; + } + + global = shell_global_get (); + context = shell_global_create_app_launch_context (global, timestamp, workspace); + if (gpu_pref == SHELL_APP_LAUNCH_GPU_APP_PREF) + discrete_gpu = g_desktop_app_info_get_boolean (app->info, "PrefersNonDefaultGPU"); + else + discrete_gpu = (gpu_pref == SHELL_APP_LAUNCH_GPU_DISCRETE); + + if (discrete_gpu) + apply_discrete_gpu_env (context, global); + + /* Set LEAVE_DESCRIPTORS_OPEN in order to use an optimized gspawn + * codepath. The shell's open file descriptors should be marked CLOEXEC + * so that they are automatically closed even with this flag set. + */ + flags = G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_LEAVE_DESCRIPTORS_OPEN; + +#ifdef HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS + /* Optimized spawn path, avoiding a child_setup function */ + { + int journalfd = -1; + +#ifdef HAVE_SYSTEMD + journalfd = sd_journal_stream_fd (shell_app_get_id (app), LOG_INFO, FALSE); +#endif /* HAVE_SYSTEMD */ + + ret = g_desktop_app_info_launch_uris_as_manager_with_fds (app->info, NULL, + context, + flags, + child_context_setup, shell_global, + wait_pid, NULL, + -1, + journalfd, + journalfd, + error); + + if (journalfd >= 0) + (void) close (journalfd); + } +#else /* !HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */ + ret = g_desktop_app_info_launch_uris_as_manager (app->info, NULL, + context, + flags, +#ifdef HAVE_SYSTEMD + app_child_setup, (gpointer)shell_app_get_id (app), +#else + child_context_setup, shell_global, +#endif + wait_pid, NULL, + error); +#endif /* HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */ + g_object_unref (context); + + return ret; +} + +/** + * shell_app_launch_action: + * @app: the #ShellApp + * @action_name: the name of the action to launch (as obtained by + * g_desktop_app_info_list_actions()) + * @timestamp: Event timestamp, or 0 for current event timestamp + * @workspace: Start on this workspace, or -1 for default + */ +void +shell_app_launch_action (ShellApp *app, + const char *action_name, + guint timestamp, + int workspace) +{ + ShellGlobal *global; + GAppLaunchContext *context; + + global = shell_global_get (); + context = shell_global_create_app_launch_context (global, timestamp, workspace); + + g_desktop_app_info_launch_action (G_DESKTOP_APP_INFO (app->info), + action_name, context); + + g_object_unref (context); +} + +/** + * shell_app_get_app_info: + * @app: a #ShellApp + * + * Returns: (transfer none): The #GDesktopAppInfo for this app, or %NULL if backed by a window + */ +GDesktopAppInfo * +shell_app_get_app_info (ShellApp *app) +{ + return app->info; +} + +static void +create_running_state (ShellApp *app) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + g_assert (app->running_state == NULL); + + app->running_state = g_new0 (ShellAppRunningState, 1); + app->running_state->refcount = 1; + app->running_state->workspace_switch_id = + g_signal_connect (workspace_manager, "workspace-switched", + G_CALLBACK (shell_app_on_ws_switch), app); + + app->running_state->session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_assert (app->running_state->session != NULL); + app->running_state->muxer = gtk_action_muxer_new (); +} + +void +shell_app_update_app_actions (ShellApp *app, + MetaWindow *window) +{ + const gchar *unique_bus_name; + + /* We assume that 'gtk-application-object-path' and + * 'gtk-app-menu-object-path' are the same for all windows which + * have it set. + * + * It could be possible, however, that the first window we see + * belonging to the app didn't have them set. For this reason, we + * take the values from the first window that has them set and ignore + * all the rest (until the app is stopped and restarted). + */ + + unique_bus_name = meta_window_get_gtk_unique_bus_name (window); + + if (g_strcmp0 (app->running_state->unique_bus_name, unique_bus_name) != 0) + { + const gchar *application_object_path; + GDBusActionGroup *actions; + + application_object_path = meta_window_get_gtk_application_object_path (window); + + if (application_object_path == NULL || unique_bus_name == NULL) + return; + + g_clear_pointer (&app->running_state->unique_bus_name, g_free); + app->running_state->unique_bus_name = g_strdup (unique_bus_name); + actions = g_dbus_action_group_get (app->running_state->session, unique_bus_name, application_object_path); + gtk_action_muxer_insert (app->running_state->muxer, "app", G_ACTION_GROUP (actions)); + g_object_unref (actions); + } +} + +static void +unref_running_state (ShellAppRunningState *state) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + g_assert (state->refcount > 0); + + state->refcount--; + if (state->refcount > 0) + return; + + g_clear_signal_handler (&state->workspace_switch_id, workspace_manager); + + g_clear_object (&state->application_proxy); + + if (state->cancellable != NULL) + { + g_cancellable_cancel (state->cancellable); + g_clear_object (&state->cancellable); + } + + g_clear_object (&state->muxer); + g_clear_object (&state->session); + g_clear_pointer (&state->unique_bus_name, g_free); + + g_free (state); +} + +/** + * shell_app_compare_by_name: + * @app: One app + * @other: The other app + * + * Order two applications by name. + * + * Returns: -1, 0, or 1; suitable for use as a comparison function + * for e.g. g_slist_sort() + */ +int +shell_app_compare_by_name (ShellApp *app, ShellApp *other) +{ + return strcmp (app->name_collation_key, other->name_collation_key); +} + +static void +shell_app_init (ShellApp *self) +{ + self->state = SHELL_APP_STATE_STOPPED; + self->started_on_workspace = -1; +} + +static void +shell_app_dispose (GObject *object) +{ + ShellApp *app = SHELL_APP (object); + + g_clear_object (&app->info); + g_clear_object (&app->fallback_icon); + + while (app->running_state) + _shell_app_remove_window (app, app->running_state->windows->data); + + /* We should have been transitioned when we removed all of our windows */ + g_assert (app->state == SHELL_APP_STATE_STOPPED); + g_assert (app->running_state == NULL); + + G_OBJECT_CLASS(shell_app_parent_class)->dispose (object); +} + +static void +shell_app_finalize (GObject *object) +{ + ShellApp *app = SHELL_APP (object); + + g_free (app->window_id_string); + + g_free (app->name_collation_key); + + G_OBJECT_CLASS(shell_app_parent_class)->finalize (object); +} + +static void +shell_app_class_init(ShellAppClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_app_get_property; + gobject_class->set_property = shell_app_set_property; + gobject_class->dispose = shell_app_dispose; + gobject_class->finalize = shell_app_finalize; + + shell_app_signals[WINDOWS_CHANGED] = g_signal_new ("windows-changed", + SHELL_TYPE_APP, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * ShellApp:state: + * + * The high-level state of the application, effectively whether it's + * running or not, or transitioning between those states. + */ + props[PROP_STATE] = + g_param_spec_enum ("state", + "State", + "Application state", + SHELL_TYPE_APP_STATE, + SHELL_APP_STATE_STOPPED, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:busy: + * + * Whether the application has marked itself as busy. + */ + props[PROP_BUSY] = + g_param_spec_boolean ("busy", + "Busy", + "Busy state", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:id: + * + * The id of this application (a desktop filename, or a special string + * like window:0xabcd1234) + */ + props[PROP_ID] = + g_param_spec_string ("id", + "Application id", + "The desktop file id of this ShellApp", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:icon: + * + * The #GIcon representing this ShellApp + */ + props[PROP_ICON] = + g_param_spec_object ("icon", + "GIcon", + "The GIcon representing this app", + G_TYPE_ICON, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:action-group: + * + * The #GDBusActionGroup associated with this ShellApp, if any. See the + * documentation of #GApplication and #GActionGroup for details. + */ + props[PROP_ACTION_GROUP] = + g_param_spec_object ("action-group", + "Application Action Group", + "The action group exported by the remote application", + G_TYPE_ACTION_GROUP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:app-info: + * + * The #GDesktopAppInfo associated with this ShellApp, if any. + */ + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", + "DesktopAppInfo", + "The DesktopAppInfo associated with this app", + G_TYPE_DESKTOP_APP_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} diff --git a/src/shell-app.h b/src/shell-app.h new file mode 100644 index 0000000..bf6f45e --- /dev/null +++ b/src/shell-app.h @@ -0,0 +1,83 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_H__ +#define __SHELL_APP_H__ + +#include <clutter/clutter.h> +#include <gio/gio.h> +#include <gio/gdesktopappinfo.h> +#include <meta/window.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_APP (shell_app_get_type ()) +G_DECLARE_FINAL_TYPE (ShellApp, shell_app, SHELL, APP, GObject) + +typedef enum { + SHELL_APP_STATE_STOPPED, + SHELL_APP_STATE_STARTING, + SHELL_APP_STATE_RUNNING +} ShellAppState; + +typedef enum { + SHELL_APP_LAUNCH_GPU_APP_PREF = 0, + SHELL_APP_LAUNCH_GPU_DISCRETE, + SHELL_APP_LAUNCH_GPU_DEFAULT +} ShellAppLaunchGpu; + +const char *shell_app_get_id (ShellApp *app); + +GDesktopAppInfo *shell_app_get_app_info (ShellApp *app); + +ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size); +GIcon *shell_app_get_icon (ShellApp *app); +const char *shell_app_get_name (ShellApp *app); +const char *shell_app_get_description (ShellApp *app); +gboolean shell_app_is_window_backed (ShellApp *app); + +void shell_app_activate_window (ShellApp *app, MetaWindow *window, guint32 timestamp); + +void shell_app_activate (ShellApp *app); + +void shell_app_activate_full (ShellApp *app, + int workspace, + guint32 timestamp); + +void shell_app_open_new_window (ShellApp *app, + int workspace); +gboolean shell_app_can_open_new_window (ShellApp *app); + +ShellAppState shell_app_get_state (ShellApp *app); + +gboolean shell_app_request_quit (ShellApp *app); + +guint shell_app_get_n_windows (ShellApp *app); + +GSList *shell_app_get_windows (ShellApp *app); + +GSList *shell_app_get_pids (ShellApp *app); + +gboolean shell_app_is_on_workspace (ShellApp *app, MetaWorkspace *workspace); + +gboolean shell_app_launch (ShellApp *app, + guint timestamp, + int workspace, + ShellAppLaunchGpu gpu_pref, + GError **error); + +void shell_app_launch_action (ShellApp *app, + const char *action_name, + guint timestamp, + int workspace); + +int shell_app_compare_by_name (ShellApp *app, ShellApp *other); + +int shell_app_compare (ShellApp *app, ShellApp *other); + +void shell_app_update_window_actions (ShellApp *app, MetaWindow *window); +void shell_app_update_app_actions (ShellApp *app, MetaWindow *window); + +gboolean shell_app_get_busy (ShellApp *app); + +G_END_DECLS + +#endif /* __SHELL_APP_H__ */ diff --git a/src/shell-blur-effect.c b/src/shell-blur-effect.c new file mode 100644 index 0000000..0d4bb45 --- /dev/null +++ b/src/shell-blur-effect.c @@ -0,0 +1,907 @@ +/* shell-blur-effect.c + * + * Copyright 2019 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "shell-blur-effect.h" + +#include "shell-enum-types.h" + +/** + * SECTION:shell-blur-effect + * @short_description: Blur effect for actors + * + * #ShellBlurEffect is a blur implementation based on Clutter. It also has + * an optional brightness property. + * + * # Modes + * + * #ShellBlurEffect can work in @SHELL_BLUR_MODE_BACKGROUND and @SHELL_BLUR_MODE_ACTOR + * modes. The actor mode blurs the actor itself, and all of its children. The + * background mode blurs the pixels beneath the actor, but not the actor itself. + * + * @SHELL_BLUR_MODE_BACKGROUND can be computationally expensive, since the contents + * beneath the actor cannot be cached, so beware of the performance implications + * of using this blur mode. + */ + +static const gchar *brightness_glsl_declarations = +"uniform float brightness; \n"; + +static const gchar *brightness_glsl = +" cogl_color_out.rgb *= brightness; \n"; + +#define MIN_DOWNSCALE_SIZE 256.f +#define MAX_SIGMA 6.f + +typedef enum +{ + ACTOR_PAINTED = 1 << 0, + BLUR_APPLIED = 1 << 1, +} CacheFlags; + +typedef struct +{ + CoglFramebuffer *framebuffer; + CoglPipeline *pipeline; + CoglTexture *texture; +} FramebufferData; + +struct _ShellBlurEffect +{ + ClutterEffect parent_instance; + + ClutterActor *actor; + + unsigned int tex_width; + unsigned int tex_height; + + /* The cached contents */ + FramebufferData actor_fb; + CacheFlags cache_flags; + + FramebufferData background_fb; + FramebufferData brightness_fb; + int brightness_uniform; + + ShellBlurMode mode; + float downscale_factor; + float brightness; + int sigma; +}; + +G_DEFINE_TYPE (ShellBlurEffect, shell_blur_effect, CLUTTER_TYPE_EFFECT) + +enum { + PROP_0, + PROP_SIGMA, + PROP_BRIGHTNESS, + PROP_MODE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + +static CoglPipeline* +create_base_pipeline (void) +{ + static CoglPipeline *base_pipeline = NULL; + + if (G_UNLIKELY (base_pipeline == NULL)) + { + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + base_pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_set_layer_null_texture (base_pipeline, 0); + cogl_pipeline_set_layer_filters (base_pipeline, + 0, + COGL_PIPELINE_FILTER_LINEAR, + COGL_PIPELINE_FILTER_LINEAR); + cogl_pipeline_set_layer_wrap_mode (base_pipeline, + 0, + COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE); + } + + return cogl_pipeline_copy (base_pipeline); +} + +static CoglPipeline* +create_brightness_pipeline (void) +{ + static CoglPipeline *brightness_pipeline = NULL; + + if (G_UNLIKELY (brightness_pipeline == NULL)) + { + CoglSnippet *snippet; + + brightness_pipeline = create_base_pipeline (); + + snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_FRAGMENT, + brightness_glsl_declarations, + brightness_glsl); + cogl_pipeline_add_snippet (brightness_pipeline, snippet); + cogl_object_unref (snippet); + } + + return cogl_pipeline_copy (brightness_pipeline); +} + + +static void +update_brightness (ShellBlurEffect *self, + uint8_t paint_opacity) +{ + cogl_pipeline_set_color4ub (self->brightness_fb.pipeline, + paint_opacity, + paint_opacity, + paint_opacity, + paint_opacity); + + if (self->brightness_uniform > -1) + { + cogl_pipeline_set_uniform_1f (self->brightness_fb.pipeline, + self->brightness_uniform, + self->brightness); + } +} + +static void +setup_projection_matrix (CoglFramebuffer *framebuffer, + float width, + float height) +{ + graphene_matrix_t projection; + + graphene_matrix_init_translate (&projection, + &GRAPHENE_POINT3D_INIT (-width / 2.0, + -height / 2.0, + 0.f)); + graphene_matrix_scale (&projection, 2.0 / width, -2.0 / height, 1.f); + + cogl_framebuffer_set_projection_matrix (framebuffer, &projection); +} + +static gboolean +update_fbo (FramebufferData *data, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + g_clear_pointer (&data->texture, cogl_object_unref); + g_clear_object (&data->framebuffer); + + float new_width = floorf (width / downscale_factor); + float new_height = floorf (height / downscale_factor); + + data->texture = cogl_texture_2d_new_with_size (ctx, new_width, new_height); + if (!data->texture) + return FALSE; + + cogl_pipeline_set_layer_texture (data->pipeline, 0, data->texture); + + data->framebuffer = + COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (data->texture)); + if (!data->framebuffer) + { + g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC); + return FALSE; + } + + setup_projection_matrix (data->framebuffer, new_width, new_height); + + return TRUE; +} + +static gboolean +update_actor_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + if (self->tex_width == width && + self->tex_height == height && + self->downscale_factor == downscale_factor && + self->actor_fb.framebuffer) + { + return TRUE; + } + + self->cache_flags &= ~ACTOR_PAINTED; + + return update_fbo (&self->actor_fb, width, height, downscale_factor); +} + +static gboolean +update_brightness_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + if (self->tex_width == width && + self->tex_height == height && + self->downscale_factor == downscale_factor && + self->brightness_fb.framebuffer) + { + return TRUE; + } + + return update_fbo (&self->brightness_fb, + width, height, + downscale_factor); +} + +static gboolean +update_background_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height) +{ + if (self->tex_width == width && + self->tex_height == height && + self->background_fb.framebuffer) + { + return TRUE; + } + + return update_fbo (&self->background_fb, width, height, 1.0); +} + +static void +clear_framebuffer_data (FramebufferData *fb_data) +{ + g_clear_pointer (&fb_data->texture, cogl_object_unref); + g_clear_object (&fb_data->framebuffer); +} + +static float +calculate_downscale_factor (float width, + float height, + float sigma) +{ + float downscale_factor = 1.0; + float scaled_width = width; + float scaled_height = height; + float scaled_sigma = sigma; + + /* This is the algorithm used by Firefox; keep downscaling until either the + * blur radius is lower than the threshold, or the downscaled texture is too + * small. + */ + while (scaled_sigma > MAX_SIGMA && + scaled_width > MIN_DOWNSCALE_SIZE && + scaled_height > MIN_DOWNSCALE_SIZE) + { + downscale_factor *= 2.f; + + scaled_width = width / downscale_factor; + scaled_height = height / downscale_factor; + scaled_sigma = sigma / downscale_factor; + } + + return downscale_factor; +} + +static void +shell_blur_effect_set_actor (ClutterActorMeta *meta, + ClutterActor *actor) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (meta); + ClutterActorMetaClass *meta_class; + + meta_class = CLUTTER_ACTOR_META_CLASS (shell_blur_effect_parent_class); + meta_class->set_actor (meta, actor); + + /* clear out the previous state */ + clear_framebuffer_data (&self->actor_fb); + clear_framebuffer_data (&self->background_fb); + clear_framebuffer_data (&self->brightness_fb); + + /* we keep a back pointer here, to avoid going through the ActorMeta */ + self->actor = clutter_actor_meta_get_actor (meta); +} + +static void +update_actor_box (ShellBlurEffect *self, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + ClutterStageView *stage_view; + float box_scale_factor = 1.0f; + float origin_x, origin_y; + float width, height; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + clutter_actor_get_allocation_box (self->actor, source_actor_box); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + stage_view = clutter_paint_context_get_stage_view (paint_context); + + clutter_actor_get_transformed_position (self->actor, &origin_x, &origin_y); + clutter_actor_get_transformed_size (self->actor, &width, &height); + + if (stage_view) + { + cairo_rectangle_int_t stage_view_layout; + + box_scale_factor = clutter_stage_view_get_scale (stage_view); + clutter_stage_view_get_layout (stage_view, &stage_view_layout); + + origin_x -= stage_view_layout.x; + origin_y -= stage_view_layout.y; + } + else + { + /* If we're drawing off stage, just assume scale = 1, this won't work + * with stage-view scaling though. + */ + } + + clutter_actor_box_set_origin (source_actor_box, origin_x, origin_y); + clutter_actor_box_set_size (source_actor_box, width, height); + + clutter_actor_box_scale (source_actor_box, box_scale_factor); + break; + } + + clutter_actor_box_clamp_to_pixel (source_actor_box); +} + +static void +add_blurred_pipeline (ShellBlurEffect *self, + ClutterPaintNode *node, + uint8_t paint_opacity) +{ + g_autoptr (ClutterPaintNode) pipeline_node = NULL; + float width, height; + + /* Use the untransformed actor size here, since the framebuffer itself already + * has the actor transform matrix applied. + */ + clutter_actor_get_size (self->actor, &width, &height); + + update_brightness (self, paint_opacity); + + pipeline_node = clutter_pipeline_node_new (self->brightness_fb.pipeline); + clutter_paint_node_set_static_name (pipeline_node, "ShellBlurEffect (final)"); + clutter_paint_node_add_child (node, pipeline_node); + + clutter_paint_node_add_rectangle (pipeline_node, + &(ClutterActorBox) { + 0.f, 0.f, + width, + height, + }); +} + +static ClutterPaintNode * +create_blur_nodes (ShellBlurEffect *self, + ClutterPaintNode *node, + uint8_t paint_opacity) +{ + g_autoptr (ClutterPaintNode) brightness_node = NULL; + g_autoptr (ClutterPaintNode) blur_node = NULL; + float width; + float height; + + clutter_actor_get_size (self->actor, &width, &height); + + update_brightness (self, paint_opacity); + brightness_node = clutter_layer_node_new_to_framebuffer (self->brightness_fb.framebuffer, + self->brightness_fb.pipeline); + clutter_paint_node_set_static_name (brightness_node, "ShellBlurEffect (brightness)"); + clutter_paint_node_add_child (node, brightness_node); + clutter_paint_node_add_rectangle (brightness_node, + &(ClutterActorBox) { + 0.f, 0.f, + width, height, + }); + + blur_node = clutter_blur_node_new (self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + self->sigma / self->downscale_factor); + clutter_paint_node_set_static_name (blur_node, "ShellBlurEffect (blur)"); + clutter_paint_node_add_child (brightness_node, blur_node); + clutter_paint_node_add_rectangle (blur_node, + &(ClutterActorBox) { + 0.f, 0.f, + cogl_texture_get_width (self->brightness_fb.texture), + cogl_texture_get_height (self->brightness_fb.texture), + }); + + self->cache_flags |= BLUR_APPLIED; + + return g_steal_pointer (&blur_node); +} + +static void +paint_background (ShellBlurEffect *self, + ClutterPaintNode *node, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + g_autoptr (ClutterPaintNode) background_node = NULL; + g_autoptr (ClutterPaintNode) blit_node = NULL; + CoglFramebuffer *src; + float transformed_x; + float transformed_y; + float transformed_width; + float transformed_height; + + clutter_actor_box_get_origin (source_actor_box, + &transformed_x, + &transformed_y); + clutter_actor_box_get_size (source_actor_box, + &transformed_width, + &transformed_height); + + /* Background layer node */ + background_node = + clutter_layer_node_new_to_framebuffer (self->background_fb.framebuffer, + self->background_fb.pipeline); + clutter_paint_node_set_static_name (background_node, "ShellBlurEffect (background)"); + clutter_paint_node_add_child (node, background_node); + clutter_paint_node_add_rectangle (background_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + + /* Blit node */ + src = clutter_paint_context_get_framebuffer (paint_context); + blit_node = clutter_blit_node_new (src); + clutter_paint_node_set_static_name (blit_node, "ShellBlurEffect (blit)"); + clutter_paint_node_add_child (background_node, blit_node); + clutter_blit_node_add_blit_rectangle (CLUTTER_BLIT_NODE (blit_node), + transformed_x, + transformed_y, + 0, 0, + transformed_width, + transformed_height); +} + +static gboolean +update_framebuffers (ShellBlurEffect *self, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + gboolean updated = FALSE; + float downscale_factor; + float height = -1; + float width = -1; + + clutter_actor_box_get_size (source_actor_box, &width, &height); + + downscale_factor = calculate_downscale_factor (width, height, self->sigma); + + updated = update_actor_fbo (self, width, height, downscale_factor) && + update_brightness_fbo (self, width, height, downscale_factor); + + if (self->mode == SHELL_BLUR_MODE_BACKGROUND) + updated = updated && update_background_fbo (self, width, height); + + self->tex_width = width; + self->tex_height = height; + self->downscale_factor = downscale_factor; + + return updated; +} + +static void +add_actor_node (ShellBlurEffect *self, + ClutterPaintNode *node, + int opacity) +{ + g_autoptr (ClutterPaintNode) actor_node = NULL; + + actor_node = clutter_actor_node_new (self->actor, opacity); + clutter_paint_node_add_child (node, actor_node); +} + +static void +paint_actor_offscreen (ShellBlurEffect *self, + ClutterPaintNode *node, + ClutterEffectPaintFlags flags) +{ + gboolean actor_dirty; + + actor_dirty = (flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY) != 0; + + /* The actor offscreen framebuffer is updated already */ + if (actor_dirty || !(self->cache_flags & ACTOR_PAINTED)) + { + g_autoptr (ClutterPaintNode) transform_node = NULL; + g_autoptr (ClutterPaintNode) layer_node = NULL; + graphene_matrix_t transform; + + /* Layer node */ + layer_node = clutter_layer_node_new_to_framebuffer (self->actor_fb.framebuffer, + self->actor_fb.pipeline); + clutter_paint_node_set_static_name (layer_node, "ShellBlurEffect (actor offscreen)"); + clutter_paint_node_add_child (node, layer_node); + clutter_paint_node_add_rectangle (layer_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + + /* Transform node */ + graphene_matrix_init_scale (&transform, + 1.f / self->downscale_factor, + 1.f / self->downscale_factor, + 1.f); + transform_node = clutter_transform_node_new (&transform); + clutter_paint_node_set_static_name (transform_node, "ShellBlurEffect (downscale)"); + clutter_paint_node_add_child (layer_node, transform_node); + + /* Actor node */ + add_actor_node (self, transform_node, 255); + + self->cache_flags |= ACTOR_PAINTED; + } + else + { + g_autoptr (ClutterPaintNode) pipeline_node = NULL; + + pipeline_node = clutter_pipeline_node_new (self->actor_fb.pipeline); + clutter_paint_node_set_static_name (pipeline_node, + "ShellBlurEffect (actor texture)"); + clutter_paint_node_add_child (node, pipeline_node); + clutter_paint_node_add_rectangle (pipeline_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + } +} + +static gboolean +needs_repaint (ShellBlurEffect *self, + ClutterEffectPaintFlags flags) +{ + gboolean actor_cached; + gboolean blur_cached; + gboolean actor_dirty; + + actor_dirty = (flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY) != 0; + blur_cached = (self->cache_flags & BLUR_APPLIED) != 0; + actor_cached = (self->cache_flags & ACTOR_PAINTED) != 0; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + return actor_dirty || !blur_cached || !actor_cached; + + case SHELL_BLUR_MODE_BACKGROUND: + return TRUE; + } + + return TRUE; +} + +static void +shell_blur_effect_paint_node (ClutterEffect *effect, + ClutterPaintNode *node, + ClutterPaintContext *paint_context, + ClutterEffectPaintFlags flags) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (effect); + uint8_t paint_opacity; + + g_assert (self->actor != NULL); + + if (self->sigma > 0) + { + g_autoptr (ClutterPaintNode) blur_node = NULL; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + paint_opacity = clutter_actor_get_paint_opacity (self->actor); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + paint_opacity = 255; + break; + + default: + g_assert_not_reached(); + break; + } + + if (needs_repaint (self, flags)) + { + ClutterActorBox source_actor_box; + + update_actor_box (self, paint_context, &source_actor_box); + + /* Failing to create or update the offscreen framebuffers prevents + * the entire effect to be applied. + */ + if (!update_framebuffers (self, paint_context, &source_actor_box)) + goto fail; + + blur_node = create_blur_nodes (self, node, paint_opacity); + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + paint_actor_offscreen (self, blur_node, flags); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + paint_background (self, blur_node, paint_context, &source_actor_box); + break; + } + } + else + { + /* Use the cached pipeline if no repaint is needed */ + add_blurred_pipeline (self, node, paint_opacity); + } + + /* Background blur needs to paint the actor after painting the blurred + * background. + */ + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + break; + + case SHELL_BLUR_MODE_BACKGROUND: + add_actor_node (self, node, -1); + break; + } + + return; + } + +fail: + /* When no blur is applied, or the offscreen framebuffers + * couldn't be created, fallback to simply painting the actor. + */ + add_actor_node (self, node, -1); +} + +static void +shell_blur_effect_finalize (GObject *object) +{ + ShellBlurEffect *self = (ShellBlurEffect *)object; + + clear_framebuffer_data (&self->actor_fb); + clear_framebuffer_data (&self->background_fb); + clear_framebuffer_data (&self->brightness_fb); + + g_clear_pointer (&self->actor_fb.pipeline, cogl_object_unref); + g_clear_pointer (&self->background_fb.pipeline, cogl_object_unref); + g_clear_pointer (&self->brightness_fb.pipeline, cogl_object_unref); + + G_OBJECT_CLASS (shell_blur_effect_parent_class)->finalize (object); +} + +static void +shell_blur_effect_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (object); + + switch (prop_id) + { + case PROP_SIGMA: + g_value_set_int (value, self->sigma); + break; + + case PROP_BRIGHTNESS: + g_value_set_float (value, self->brightness); + break; + + case PROP_MODE: + g_value_set_enum (value, self->mode); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shell_blur_effect_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (object); + + switch (prop_id) + { + case PROP_SIGMA: + shell_blur_effect_set_sigma (self, g_value_get_int (value)); + break; + + case PROP_BRIGHTNESS: + shell_blur_effect_set_brightness (self, g_value_get_float (value)); + break; + + case PROP_MODE: + shell_blur_effect_set_mode (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shell_blur_effect_class_init (ShellBlurEffectClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorMetaClass *meta_class = CLUTTER_ACTOR_META_CLASS (klass); + ClutterEffectClass *effect_class = CLUTTER_EFFECT_CLASS (klass); + + object_class->finalize = shell_blur_effect_finalize; + object_class->get_property = shell_blur_effect_get_property; + object_class->set_property = shell_blur_effect_set_property; + + meta_class->set_actor = shell_blur_effect_set_actor; + + effect_class->paint_node = shell_blur_effect_paint_node; + + properties[PROP_SIGMA] = + g_param_spec_int ("sigma", + "Sigma", + "Sigma", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_BRIGHTNESS] = + g_param_spec_float ("brightness", + "Brightness", + "Brightness", + 0.f, 1.f, 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_MODE] = + g_param_spec_enum ("mode", + "Blur mode", + "Blur mode", + SHELL_TYPE_BLUR_MODE, + SHELL_BLUR_MODE_ACTOR, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +shell_blur_effect_init (ShellBlurEffect *self) +{ + self->mode = SHELL_BLUR_MODE_ACTOR; + self->sigma = 0; + self->brightness = 1.f; + + self->actor_fb.pipeline = create_base_pipeline (); + self->background_fb.pipeline = create_base_pipeline (); + self->brightness_fb.pipeline = create_brightness_pipeline (); + self->brightness_uniform = + cogl_pipeline_get_uniform_location (self->brightness_fb.pipeline, "brightness"); +} + +ShellBlurEffect * +shell_blur_effect_new (void) +{ + return g_object_new (SHELL_TYPE_BLUR_EFFECT, NULL); +} + +int +shell_blur_effect_get_sigma (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), -1); + + return self->sigma; +} + +void +shell_blur_effect_set_sigma (ShellBlurEffect *self, + int sigma) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->sigma == sigma) + return; + + self->sigma = sigma; + self->cache_flags &= ~BLUR_APPLIED; + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIGMA]); +} + +float +shell_blur_effect_get_brightness (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), FALSE); + + return self->brightness; +} + +void +shell_blur_effect_set_brightness (ShellBlurEffect *self, + float brightness) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->brightness == brightness) + return; + + self->brightness = brightness; + self->cache_flags &= ~BLUR_APPLIED; + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BRIGHTNESS]); +} + +ShellBlurMode +shell_blur_effect_get_mode (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), -1); + + return self->mode; +} + +void +shell_blur_effect_set_mode (ShellBlurEffect *self, + ShellBlurMode mode) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->mode == mode) + return; + + self->mode = mode; + self->cache_flags &= ~BLUR_APPLIED; + + switch (mode) + { + case SHELL_BLUR_MODE_ACTOR: + clear_framebuffer_data (&self->background_fb); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + default: + /* Do nothing */ + break; + } + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODE]); +} diff --git a/src/shell-blur-effect.h b/src/shell-blur-effect.h new file mode 100644 index 0000000..a7486cc --- /dev/null +++ b/src/shell-blur-effect.h @@ -0,0 +1,57 @@ +/* shell-blur-effect.h + * + * Copyright 2019 Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + * 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/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +/** + * ShellBlurMode: + * @SHELL_BLUR_MODE_ACTOR: blur the actor contents, and its children + * @SHELL_BLUR_MODE_BACKGROUND: blur what's beneath the actor + * + * The mode of blurring of the effect. + */ +typedef enum +{ + SHELL_BLUR_MODE_ACTOR, + SHELL_BLUR_MODE_BACKGROUND, +} ShellBlurMode; + +#define SHELL_TYPE_BLUR_EFFECT (shell_blur_effect_get_type()) +G_DECLARE_FINAL_TYPE (ShellBlurEffect, shell_blur_effect, SHELL, BLUR_EFFECT, ClutterEffect) + +ShellBlurEffect *shell_blur_effect_new (void); + +int shell_blur_effect_get_sigma (ShellBlurEffect *self); +void shell_blur_effect_set_sigma (ShellBlurEffect *self, + int sigma); + +float shell_blur_effect_get_brightness (ShellBlurEffect *self); +void shell_blur_effect_set_brightness (ShellBlurEffect *self, + float brightness); + +ShellBlurMode shell_blur_effect_get_mode (ShellBlurEffect *self); +void shell_blur_effect_set_mode (ShellBlurEffect *self, + ShellBlurMode mode); + +G_END_DECLS diff --git a/src/shell-embedded-window-private.h b/src/shell-embedded-window-private.h new file mode 100644 index 0000000..5714af9 --- /dev/null +++ b/src/shell-embedded-window-private.h @@ -0,0 +1,20 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ +#define __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ + +#include "shell-embedded-window.h" +#include "shell-gtk-embed.h" + +void _shell_embedded_window_set_actor (ShellEmbeddedWindow *window, + ShellGtkEmbed *embed); + +void _shell_embedded_window_allocate (ShellEmbeddedWindow *window, + int x, + int y, + int width, + int height); + +void _shell_embedded_window_map (ShellEmbeddedWindow *window); +void _shell_embedded_window_unmap (ShellEmbeddedWindow *window); + +#endif /* __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ */ diff --git a/src/shell-embedded-window.c b/src/shell-embedded-window.c new file mode 100644 index 0000000..8fd6112 --- /dev/null +++ b/src/shell-embedded-window.c @@ -0,0 +1,247 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <gdk/gdkx.h> + +#include "shell-embedded-window-private.h" + +/* This type is a subclass of GtkWindow that ties the window to a + * ShellGtkEmbed; the resizing logic is bound to the clutter logic. + * + * The typical usage we might expect is + * + * - ShellEmbeddedWindow is created and filled with content + * - ShellEmbeddedWindow is shown with gtk_widget_show_all() + * - ShellGtkEmbed is created for the ShellEmbeddedWindow + * - actor is added to a stage + * + * The way it works is that the GtkWindow is mapped if and only if both: + * + * - gtk_widget_visible (window) [widget has been shown] + * - Actor is mapped [actor and all parents visible, actor in stage] + */ + +enum { + PROP_0 +}; + +typedef struct _ShellEmbeddedWindowPrivate ShellEmbeddedWindowPrivate; + +struct _ShellEmbeddedWindowPrivate { + ShellGtkEmbed *actor; + + GdkRectangle position; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellEmbeddedWindow, + shell_embedded_window, + GTK_TYPE_WINDOW); + +/* + * The normal gtk_window_show() starts all of the complicated asynchronous + * window resizing code running; we don't want or need any of that. + * Bypassing the normal code does mean that the extra geometry management + * available on GtkWindow: gridding, maximum sizes, etc, is ignored; we + * don't really want that anyways - we just want a way of embedding a + * GtkWidget into a Clutter stage. + */ +static void +shell_embedded_window_show (GtkWidget *widget) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (widget); + ShellEmbeddedWindowPrivate *priv; + GtkWidgetClass *widget_class; + + priv = shell_embedded_window_get_instance_private (window); + + /* Skip GtkWindow, but run the default GtkWidget handling which + * marks the widget visible */ + widget_class = g_type_class_peek (GTK_TYPE_WIDGET); + widget_class->show (widget); + + if (priv->actor) + { + /* Size is 0x0 if the GtkWindow is not shown */ + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); + + if (clutter_actor_is_realized (CLUTTER_ACTOR (priv->actor))) + gtk_widget_map (widget); + } +} + +static void +shell_embedded_window_hide (GtkWidget *widget) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (widget); + ShellEmbeddedWindowPrivate *priv; + + priv = shell_embedded_window_get_instance_private (window); + + if (priv->actor) + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); + + GTK_WIDGET_CLASS (shell_embedded_window_parent_class)->hide (widget); +} + +static gboolean +shell_embedded_window_configure_event (GtkWidget *widget, + GdkEventConfigure *event) +{ + /* Normally a configure event coming back from X triggers the + * resizing logic inside GtkWindow; we just ignore them + * since we are handling the resizing logic separately. + */ + return FALSE; +} + +static void +shell_embedded_window_check_resize (GtkContainer *container) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (container); + ShellEmbeddedWindowPrivate *priv; + + priv = shell_embedded_window_get_instance_private (window); + + /* Check resize is called when a resize is queued on something + * inside the GtkWindow; we need to make sure that in response + * to this gtk_widget_size_request() and then + * gtk_widget_size_allocate() are called; we defer to the Clutter + * logic and assume it will do the right thing. + */ + if (priv->actor) + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); +} + +static GObject * +shell_embedded_window_constructor (GType gtype, + guint n_properties, + GObjectConstructParam *properties) +{ + GObject *object; + GObjectClass *parent_class; + + parent_class = G_OBJECT_CLASS (shell_embedded_window_parent_class); + object = parent_class->constructor (gtype, n_properties, properties); + + /* Setting the resize mode to immediate means that calling queue_resize() + * on a widget within the window will immediately call check_resize() + * to be called, instead of having it queued to an idle. From our perspective, + * this is ideal since we just are going to queue a resize to Clutter's + * idle resize anyways. + */ + g_object_set (object, + "app-paintable", TRUE, + "resize-mode", GTK_RESIZE_IMMEDIATE, + "type", GTK_WINDOW_POPUP, + NULL); + + return object; +} + +static void +shell_embedded_window_class_init (ShellEmbeddedWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->constructor = shell_embedded_window_constructor; + + widget_class->show = shell_embedded_window_show; + widget_class->hide = shell_embedded_window_hide; + widget_class->configure_event = shell_embedded_window_configure_event; + + container_class->check_resize = shell_embedded_window_check_resize; +} + +static void +shell_embedded_window_init (ShellEmbeddedWindow *window) +{ +} + +/* + * Private routines called by ShellGtkEmbed + */ + +void +_shell_embedded_window_set_actor (ShellEmbeddedWindow *window, + ShellGtkEmbed *actor) + +{ + ShellEmbeddedWindowPrivate *priv; + + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + priv = shell_embedded_window_get_instance_private (window); + priv->actor = actor; + + if (actor && + clutter_actor_is_mapped (CLUTTER_ACTOR (actor)) && + gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_map (GTK_WIDGET (window)); +} + +void +_shell_embedded_window_allocate (ShellEmbeddedWindow *window, + int x, + int y, + int width, + int height) +{ + ShellEmbeddedWindowPrivate *priv; + GtkAllocation allocation; + + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + priv = shell_embedded_window_get_instance_private (window); + + if (priv->position.x == x && + priv->position.y == y && + priv->position.width == width && + priv->position.height == height) + return; + + priv->position.x = x; + priv->position.y = y; + priv->position.width = width; + priv->position.height = height; + + if (gtk_widget_get_realized (GTK_WIDGET (window))) + gdk_window_move_resize (gtk_widget_get_window (GTK_WIDGET (window)), + x, y, width, height); + + allocation.x = 0; + allocation.y = 0; + allocation.width = width; + allocation.height = height; + + gtk_widget_size_allocate (GTK_WIDGET (window), &allocation); +} + +void +_shell_embedded_window_map (ShellEmbeddedWindow *window) +{ + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_map (GTK_WIDGET (window)); +} + +void +_shell_embedded_window_unmap (ShellEmbeddedWindow *window) +{ + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + gtk_widget_unmap (GTK_WIDGET (window)); +} + +/* + * Public API + */ +GtkWidget * +shell_embedded_window_new (void) +{ + return g_object_new (SHELL_TYPE_EMBEDDED_WINDOW, + NULL); +} diff --git a/src/shell-embedded-window.h b/src/shell-embedded-window.h new file mode 100644 index 0000000..835165b --- /dev/null +++ b/src/shell-embedded-window.h @@ -0,0 +1,19 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_EMBEDDED_WINDOW_H__ +#define __SHELL_EMBEDDED_WINDOW_H__ + +#include <gtk/gtk.h> +#include <clutter/clutter.h> + +#define SHELL_TYPE_EMBEDDED_WINDOW (shell_embedded_window_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellEmbeddedWindow, shell_embedded_window, + SHELL, EMBEDDED_WINDOW, GtkWindow) + +struct _ShellEmbeddedWindowClass +{ + GtkWindowClass parent_class; +}; + +GtkWidget *shell_embedded_window_new (void); + +#endif /* __SHELL_EMBEDDED_WINDOW_H__ */ diff --git a/src/shell-global-private.h b/src/shell-global-private.h new file mode 100644 index 0000000..9969691 --- /dev/null +++ b/src/shell-global-private.h @@ -0,0 +1,23 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLOBAL_PRIVATE_H__ +#define __SHELL_GLOBAL_PRIVATE_H__ + +#include "shell-global.h" + +#include <gjs/gjs.h> + +void _shell_global_init (const char *first_property_name, + ...); +void _shell_global_set_plugin (ShellGlobal *global, + MetaPlugin *plugin); + +void _shell_global_destroy_gjs_context (ShellGlobal *global); + +GjsContext *_shell_global_get_gjs_context (ShellGlobal *global); + +gboolean _shell_global_check_xdnd_event (ShellGlobal *global, + XEvent *xev); + +void _shell_global_locate_pointer (ShellGlobal *global); + +#endif /* __SHELL_GLOBAL_PRIVATE_H__ */ diff --git a/src/shell-global.c b/src/shell-global.c new file mode 100644 index 0000000..0ccdb10 --- /dev/null +++ b/src/shell-global.c @@ -0,0 +1,1860 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <math.h> +#include <stdarg.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#ifdef HAVE_SYS_RESOURCE_H +#include <sys/resource.h> +#endif +#include <locale.h> + +#include <X11/extensions/Xfixes.h> +#include <gdk/gdkx.h> +#include <gio/gio.h> +#include <girepository.h> +#include <meta/meta-backend.h> +#include <meta/meta-context.h> +#include <meta/display.h> +#include <meta/util.h> +#include <meta/meta-shaped-texture.h> +#include <meta/meta-cursor-tracker.h> +#include <meta/meta-settings.h> +#include <meta/meta-workspace-manager.h> +#include <meta/meta-x11-display.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-systemd.h> + +#if defined __OpenBSD__ || defined __FreeBSD__ +#include <sys/sysctl.h> +#endif + +#include "shell-enum-types.h" +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "shell-window-tracker.h" +#include "shell-wm.h" +#include "shell-util.h" +#include "st.h" +#include "switcheroo-control.h" + +static ShellGlobal *the_object = NULL; + +struct _ShellGlobal { + GObject parent; + + ClutterStage *stage; + + MetaBackend *backend; + MetaContext *meta_context; + MetaDisplay *meta_display; + MetaWorkspaceManager *workspace_manager; + Display *xdisplay; + + char *session_mode; + + XserverRegion input_region; + + GjsContext *js_context; + MetaPlugin *plugin; + ShellWM *wm; + GSettings *settings; + const char *datadir; + char *imagedir; + char *userdatadir; + GFile *userdatadir_path; + GFile *runtime_state_path; + + StFocusManager *focus_manager; + + guint work_count; + GSList *leisure_closures; + guint leisure_function_id; + + GHashTable *save_ops; + + gboolean frame_timestamps; + gboolean frame_finish_timestamp; + + GDBusProxy *switcheroo_control; + GCancellable *switcheroo_cancellable; +}; + +enum { + PROP_0, + + PROP_SESSION_MODE, + PROP_BACKEND, + PROP_CONTEXT, + PROP_DISPLAY, + PROP_WORKSPACE_MANAGER, + PROP_SCREEN_WIDTH, + PROP_SCREEN_HEIGHT, + PROP_STAGE, + PROP_WINDOW_GROUP, + PROP_TOP_WINDOW_GROUP, + PROP_WINDOW_MANAGER, + PROP_SETTINGS, + PROP_DATADIR, + PROP_IMAGEDIR, + PROP_USERDATADIR, + PROP_FOCUS_MANAGER, + PROP_FRAME_TIMESTAMPS, + PROP_FRAME_FINISH_TIMESTAMP, + PROP_SWITCHEROO_CONTROL, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +/* Signals */ +enum +{ + NOTIFY_ERROR, + LOCATE_POINTER, + LAST_SIGNAL +}; + +G_DEFINE_TYPE(ShellGlobal, shell_global, G_TYPE_OBJECT); + +static guint shell_global_signals [LAST_SIGNAL] = { 0 }; + +static void +got_switcheroo_control_gpus_property_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + ShellGlobal *global; + GError *error = NULL; + GVariant *gpus; + + gpus = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, &error); + if (!gpus) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Could not get GPUs property from switcheroo-control: %s", error->message); + g_clear_error (&error); + return; + } + + global = user_data; + g_dbus_proxy_set_cached_property (global->switcheroo_control, "GPUs", gpus); + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); +} + +static void +switcheroo_control_ready_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + ShellGlobal *global; + GError *error = NULL; + ShellNetHadessSwitcherooControl *control; + g_auto(GStrv) cached_props = NULL; + + control = shell_net_hadess_switcheroo_control_proxy_new_for_bus_finish (res, &error); + if (!control) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Could not get switcheroo-control GDBusProxy: %s", error->message); + g_clear_error (&error); + return; + } + + global = user_data; + global->switcheroo_control = G_DBUS_PROXY (control); + g_debug ("Got switcheroo-control proxy successfully"); + + cached_props = g_dbus_proxy_get_cached_property_names (global->switcheroo_control); + if (cached_props != NULL && g_strv_contains ((const gchar * const *) cached_props, "GPUs")) + { + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); + return; + } + /* Delay property notification until we have all the properties gathered */ + + g_dbus_connection_call (g_dbus_proxy_get_connection (global->switcheroo_control), + g_dbus_proxy_get_name (global->switcheroo_control), + g_dbus_proxy_get_object_path (global->switcheroo_control), + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", + g_dbus_proxy_get_interface_name (global->switcheroo_control), + "GPUs"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + global->switcheroo_cancellable, + got_switcheroo_control_gpus_property_cb, + user_data); +} + +static void +shell_global_set_property(GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + switch (prop_id) + { + case PROP_SESSION_MODE: + g_clear_pointer (&global->session_mode, g_free); + global->session_mode = g_ascii_strdown (g_value_get_string (value), -1); + break; + case PROP_FRAME_TIMESTAMPS: + { + gboolean enable = g_value_get_boolean (value); + + if (global->frame_timestamps != enable) + { + global->frame_timestamps = enable; + g_object_notify_by_pspec (object, props[PROP_FRAME_TIMESTAMPS]); + } + } + break; + case PROP_FRAME_FINISH_TIMESTAMP: + { + gboolean enable = g_value_get_boolean (value); + + if (global->frame_finish_timestamp != enable) + { + global->frame_finish_timestamp = enable; + g_object_notify_by_pspec (object, props[PROP_FRAME_FINISH_TIMESTAMP]); + } + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_global_get_property(GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + switch (prop_id) + { + case PROP_SESSION_MODE: + g_value_set_string (value, shell_global_get_session_mode (global)); + break; + case PROP_BACKEND: + g_value_set_object (value, global->backend); + break; + case PROP_CONTEXT: + g_value_set_object (value, global->meta_context); + break; + case PROP_DISPLAY: + g_value_set_object (value, global->meta_display); + break; + case PROP_WORKSPACE_MANAGER: + g_value_set_object (value, global->workspace_manager); + break; + case PROP_SCREEN_WIDTH: + { + int width, height; + + meta_display_get_size (global->meta_display, &width, &height); + g_value_set_int (value, width); + } + break; + case PROP_SCREEN_HEIGHT: + { + int width, height; + + meta_display_get_size (global->meta_display, &width, &height); + g_value_set_int (value, height); + } + break; + case PROP_STAGE: + g_value_set_object (value, global->stage); + break; + case PROP_WINDOW_GROUP: + g_value_set_object (value, meta_get_window_group_for_display (global->meta_display)); + break; + case PROP_TOP_WINDOW_GROUP: + g_value_set_object (value, meta_get_top_window_group_for_display (global->meta_display)); + break; + case PROP_WINDOW_MANAGER: + g_value_set_object (value, global->wm); + break; + case PROP_SETTINGS: + g_value_set_object (value, global->settings); + break; + case PROP_DATADIR: + g_value_set_string (value, global->datadir); + break; + case PROP_IMAGEDIR: + g_value_set_string (value, global->imagedir); + break; + case PROP_USERDATADIR: + g_value_set_string (value, global->userdatadir); + break; + case PROP_FOCUS_MANAGER: + g_value_set_object (value, global->focus_manager); + break; + case PROP_FRAME_TIMESTAMPS: + g_value_set_boolean (value, global->frame_timestamps); + break; + case PROP_FRAME_FINISH_TIMESTAMP: + g_value_set_boolean (value, global->frame_finish_timestamp); + break; + case PROP_SWITCHEROO_CONTROL: + g_value_set_object (value, global->switcheroo_control); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +switcheroo_appeared_cb (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + g_debug ("switcheroo-control appeared"); + shell_net_hadess_switcheroo_control_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "net.hadess.SwitcherooControl", + "/net/hadess/SwitcherooControl", + global->switcheroo_cancellable, + switcheroo_control_ready_cb, + global); +} + +static void +switcheroo_vanished_cb (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + g_debug ("switcheroo-control vanished"); + g_clear_object (&global->switcheroo_control); + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); +} + +static void +shell_global_init (ShellGlobal *global) +{ + const char *datadir = g_getenv ("GNOME_SHELL_DATADIR"); + const char *shell_js = g_getenv("GNOME_SHELL_JS"); + char *imagedir, **search_path; + char *path; + const char *byteorder_string; + + if (!datadir) + datadir = GNOME_SHELL_DATADIR; + global->datadir = datadir; + + /* We make sure imagedir ends with a '/', since the JS won't have + * access to g_build_filename() and so will end up just + * concatenating global.imagedir to a filename. + */ + imagedir = g_build_filename (datadir, "images/", NULL); + if (g_file_test (imagedir, G_FILE_TEST_IS_DIR)) + global->imagedir = imagedir; + else + { + g_free (imagedir); + global->imagedir = g_strdup_printf ("%s/", datadir); + } + + /* Ensure config dir exists for later use */ + global->userdatadir = g_build_filename (g_get_user_data_dir (), "gnome-shell", NULL); + g_mkdir_with_parents (global->userdatadir, 0700); + global->userdatadir_path = g_file_new_for_path (global->userdatadir); + +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + byteorder_string = "LE"; +#else + byteorder_string = "BE"; +#endif + + /* And the runtime state */ + path = g_strdup_printf ("%s/gnome-shell/runtime-state-%s.%s", + g_get_user_runtime_dir (), + byteorder_string, + XDisplayName (NULL)); + (void) g_mkdir_with_parents (path, 0700); + global->runtime_state_path = g_file_new_for_path (path); + g_free (path); + + global->settings = g_settings_new ("org.gnome.shell"); + + if (shell_js) + { + int i, j; + search_path = g_strsplit (shell_js, ":", -1); + + /* The naive g_strsplit above will split 'resource:///foo/bar' into 'resource', + * '///foo/bar'. Combine these back together by looking for a literal 'resource' + * in the array. */ + for (i = 0, j = 0; search_path[i];) + { + char *out; + + if (strcmp (search_path[i], "resource") == 0 && search_path[i + 1] != NULL) + { + out = g_strconcat (search_path[i], ":", search_path[i + 1], NULL); + g_free (search_path[i]); + g_free (search_path[i + 1]); + i += 2; + } + else + { + out = search_path[i]; + i += 1; + } + + search_path[j++] = out; + } + + search_path[j] = NULL; /* NULL-terminate the now possibly shorter array */ + } + else + { + search_path = g_malloc0 (2 * sizeof (char *)); + search_path[0] = g_strdup ("resource:///org/gnome/shell"); + } + + global->js_context = g_object_new (GJS_TYPE_CONTEXT, + "search-path", search_path, + NULL); + + g_strfreev (search_path); + + global->save_ops = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, g_object_unref); + + global->switcheroo_cancellable = g_cancellable_new (); + g_bus_watch_name (G_BUS_TYPE_SYSTEM, + "net.hadess.SwitcherooControl", + G_BUS_NAME_WATCHER_FLAGS_NONE, + switcheroo_appeared_cb, + switcheroo_vanished_cb, + global, + NULL); +} + +static void +shell_global_finalize (GObject *object) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + g_clear_object (&global->js_context); + g_object_unref (global->settings); + + the_object = NULL; + + g_cancellable_cancel (global->switcheroo_cancellable); + g_clear_object (&global->switcheroo_cancellable); + + g_clear_object (&global->userdatadir_path); + g_clear_object (&global->runtime_state_path); + + g_free (global->session_mode); + g_free (global->imagedir); + g_free (global->userdatadir); + + g_hash_table_unref (global->save_ops); + + G_OBJECT_CLASS(shell_global_parent_class)->finalize (object); +} + +static void +shell_global_class_init (ShellGlobalClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_global_get_property; + gobject_class->set_property = shell_global_set_property; + gobject_class->finalize = shell_global_finalize; + + shell_global_signals[NOTIFY_ERROR] = + g_signal_new ("notify-error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_STRING); + shell_global_signals[LOCATE_POINTER] = + g_signal_new ("locate-pointer", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + props[PROP_SESSION_MODE] = + g_param_spec_string ("session-mode", + "Session Mode", + "The session mode to use", + "user", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_SCREEN_WIDTH] = + g_param_spec_int ("screen-width", + "Screen Width", + "Screen width, in pixels", + 0, G_MAXINT, 1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_SCREEN_HEIGHT] = + g_param_spec_int ("screen-height", + "Screen Height", + "Screen height, in pixels", + 0, G_MAXINT, 1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_BACKEND] = + g_param_spec_object ("backend", + "Backend", + "MetaBackend object", + META_TYPE_BACKEND, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_CONTEXT] = + g_param_spec_object ("context", + "Context", + "MetaContext object", + META_TYPE_CONTEXT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_DISPLAY] = + g_param_spec_object ("display", + "Display", + "Metacity display object for the shell", + META_TYPE_DISPLAY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WORKSPACE_MANAGER] = + g_param_spec_object ("workspace-manager", + "Workspace manager", + "Workspace manager", + META_TYPE_WORKSPACE_MANAGER, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_STAGE] = + g_param_spec_object ("stage", + "Stage", + "Stage holding the desktop scene graph", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WINDOW_GROUP] = + g_param_spec_object ("window-group", + "Window Group", + "Actor holding window actors", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_TOP_WINDOW_GROUP] = + g_param_spec_object ("top-window-group", + "Top Window Group", + "Actor holding override-redirect windows", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WINDOW_MANAGER] = + g_param_spec_object ("window-manager", + "Window Manager", + "Window management interface", + SHELL_TYPE_WM, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_SETTINGS] = + g_param_spec_object ("settings", + "Settings", + "GSettings instance for gnome-shell configuration", + G_TYPE_SETTINGS, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_DATADIR] = + g_param_spec_string ("datadir", + "Data directory", + "Directory containing gnome-shell data files", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_IMAGEDIR] = + g_param_spec_string ("imagedir", + "Image directory", + "Directory containing gnome-shell image files", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_USERDATADIR] = + g_param_spec_string ("userdatadir", + "User data directory", + "Directory containing gnome-shell user data", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_FOCUS_MANAGER] = + g_param_spec_object ("focus-manager", + "Focus manager", + "The shell's StFocusManager", + ST_TYPE_FOCUS_MANAGER, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_FRAME_TIMESTAMPS] = + g_param_spec_boolean ("frame-timestamps", + "Frame Timestamps", + "Whether to log frame timestamps in the performance log", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_FRAME_FINISH_TIMESTAMP] = + g_param_spec_boolean ("frame-finish-timestamp", + "Frame Finish Timestamps", + "Whether at the end of a frame to call glFinish and log paintCompletedTimestamp", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_SWITCHEROO_CONTROL] = + g_param_spec_object ("switcheroo-control", + "switcheroo-control", + "D-Bus Proxy for switcheroo-control daemon", + G_TYPE_DBUS_PROXY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +/* + * _shell_global_init: (skip) + * @first_property_name: the name of the first property + * @...: the value of the first property, followed optionally by more + * name/value pairs, followed by %NULL + * + * Initializes the shell global singleton with the construction-time + * properties. + * + * There are currently no such properties, so @first_property_name should + * always be %NULL. + * + * This call must be called before shell_global_get() and shouldn't be called + * more than once. + */ +void +_shell_global_init (const char *first_property_name, + ...) +{ + va_list argument_list; + + g_return_if_fail (the_object == NULL); + + va_start (argument_list, first_property_name); + the_object = SHELL_GLOBAL (g_object_new_valist (SHELL_TYPE_GLOBAL, + first_property_name, + argument_list)); + va_end (argument_list); + +} + +/** + * shell_global_get: + * + * Gets the singleton global object that represents the desktop. + * + * Return value: (transfer none): the singleton global object + */ +ShellGlobal * +shell_global_get (void) +{ + return the_object; +} + +/** + * _shell_global_destroy_gjs_context: (skip) + * @self: global object + * + * Destroys the GjsContext held by ShellGlobal, in order to break reference + * counting cycles. (The GjsContext holds a reference to ShellGlobal because + * it's available as window.global inside JS.) + */ +void +_shell_global_destroy_gjs_context (ShellGlobal *self) +{ + g_clear_object (&self->js_context); +} + +static guint32 +get_current_time_maybe_roundtrip (ShellGlobal *global) +{ + guint32 time; + + time = shell_global_get_current_time (global); + if (time != CurrentTime) + return time; + + return meta_display_get_current_time_roundtrip (global->meta_display); +} + +static void +focus_window_changed (MetaDisplay *display, + GParamSpec *param, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + /* If the stage window became unfocused, drop the key focus + * on Clutter's side. */ + if (!meta_stage_is_focused (global->meta_display)) + clutter_stage_set_key_focus (global->stage, NULL); +} + +static ClutterActor * +get_key_focused_actor (ShellGlobal *global) +{ + ClutterActor *actor; + + actor = clutter_stage_get_key_focus (global->stage); + + /* If there's no explicit key focus, clutter_stage_get_key_focus() + * returns the stage. This is a terrible API. */ + if (actor == CLUTTER_ACTOR (global->stage)) + actor = NULL; + + return actor; +} + +static void +sync_stage_window_focus (ShellGlobal *global) +{ + ClutterActor *actor; + + actor = get_key_focused_actor (global); + + /* An actor got key focus and the stage needs to be focused. */ + if (actor != NULL && !meta_stage_is_focused (global->meta_display)) + meta_focus_stage_window (global->meta_display, + get_current_time_maybe_roundtrip (global)); + + /* An actor dropped key focus. Focus the default window. */ + else if (actor == NULL && meta_stage_is_focused (global->meta_display)) + meta_display_focus_default_window (global->meta_display, + get_current_time_maybe_roundtrip (global)); +} + +static void +focus_actor_changed (ClutterStage *stage, + GParamSpec *param, + gpointer user_data) +{ + ShellGlobal *global = user_data; + sync_stage_window_focus (global); +} + +static void +sync_input_region (ShellGlobal *global) +{ + MetaDisplay *display = global->meta_display; + MetaX11Display *x11_display = meta_display_get_x11_display (display); + + meta_x11_display_set_stage_input_region (x11_display, global->input_region); +} + +/** + * shell_global_set_stage_input_region: + * @global: the #ShellGlobal + * @rectangles: (element-type Meta.Rectangle): a list of #MetaRectangle + * describing the input region. + * + * Sets the area of the stage that is responsive to mouse clicks when + * we don't have a modal or grab. + */ +void +shell_global_set_stage_input_region (ShellGlobal *global, + GSList *rectangles) +{ + MetaRectangle *rect; + XRectangle *rects; + int nrects, i; + GSList *r; + + g_return_if_fail (SHELL_IS_GLOBAL (global)); + + if (meta_is_wayland_compositor ()) + return; + + nrects = g_slist_length (rectangles); + rects = g_new (XRectangle, nrects); + for (r = rectangles, i = 0; r; r = r->next, i++) + { + rect = (MetaRectangle *)r->data; + rects[i].x = rect->x; + rects[i].y = rect->y; + rects[i].width = rect->width; + rects[i].height = rect->height; + } + + if (global->input_region) + XFixesDestroyRegion (global->xdisplay, global->input_region); + + global->input_region = XFixesCreateRegion (global->xdisplay, rects, nrects); + g_free (rects); + + sync_input_region (global); +} + +/** + * shell_global_get_stage: + * + * Return value: (transfer none): The default #ClutterStage + */ +ClutterStage * +shell_global_get_stage (ShellGlobal *global) +{ + return global->stage; +} + +/** + * shell_global_get_display: + * + * Return value: (transfer none): The default #MetaDisplay + */ +MetaDisplay * +shell_global_get_display (ShellGlobal *global) +{ + return global->meta_display; +} + +/** + * shell_global_get_workspace_manager: + * + * Return value: (transfer none): The default #MetaWorkspaceManager + */ +MetaWorkspaceManager * +shell_global_get_workspace_manager (ShellGlobal *global) +{ + return global->workspace_manager; +} + +/** + * shell_global_get_window_actors: + * + * Gets the list of #MetaWindowActor for the plugin's screen + * + * Return value: (element-type Meta.WindowActor) (transfer container): the list of windows + */ +GList * +shell_global_get_window_actors (ShellGlobal *global) +{ + GList *filtered = NULL; + GList *l; + + g_return_val_if_fail (SHELL_IS_GLOBAL (global), NULL); + + for (l = meta_get_window_actors (global->meta_display); l; l = l->next) + if (!meta_window_actor_is_destroyed (l->data)) + filtered = g_list_prepend (filtered, l->data); + + return g_list_reverse (filtered); +} + +static void +global_stage_notify_width (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_WIDTH]); +} + +static void +global_stage_notify_height (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_HEIGHT]); +} + +static gboolean +global_stage_before_paint (gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + if (global->frame_timestamps) + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.stagePaintStart"); + + return TRUE; +} + +static gboolean +load_gl_symbol (const char *name, + void **func) +{ + *func = cogl_get_proc_address (name); + if (!*func) + { + g_warning ("failed to resolve required GL symbol \"%s\"\n", name); + return FALSE; + } + return TRUE; +} + +static void +global_stage_after_paint (ClutterStage *stage, + ClutterStageView *stage_view, + ShellGlobal *global) +{ + /* At this point, we've finished all layout and painting, but haven't + * actually flushed or swapped */ + + if (global->frame_timestamps && global->frame_finish_timestamp) + { + /* It's interesting to find out when the paint actually finishes + * on the GPU. We could wait for this asynchronously with + * ARB_timer_query (see https://bugzilla.gnome.org/show_bug.cgi?id=732350 + * for an implementation of this), but what we actually would + * find out then is the latency for drawing a frame, not how much + * GPU work was needed, since frames can overlap. Calling glFinish() + * is a fairly reliable way to separate out adjacent frames + * and measure the amount of GPU work. This is turned on with a + * separate property from ::frame-timestamps, since it should not + * be turned on if we're trying to actual measure latency or frame + * rate. + */ + static void (*finish) (void); + + if (!finish) + load_gl_symbol ("glFinish", (void **)&finish); + + cogl_flush (); + finish (); + + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.paintCompletedTimestamp"); + } +} + +static gboolean +global_stage_after_swap (gpointer data) +{ + /* Everything is done, we're ready for a new frame */ + + ShellGlobal *global = SHELL_GLOBAL (data); + + if (global->frame_timestamps) + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.stagePaintDone"); + + return TRUE; +} + +static void +update_scaling_factor (ShellGlobal *global, + MetaSettings *settings) +{ + ClutterStage *stage = CLUTTER_STAGE (global->stage); + StThemeContext *context = st_theme_context_get_for_stage (stage); + int scaling_factor; + + scaling_factor = meta_settings_get_ui_scaling_factor (settings); + g_object_set (context, "scale-factor", scaling_factor, NULL); +} + +static void +ui_scaling_factor_changed (MetaSettings *settings, + ShellGlobal *global) +{ + update_scaling_factor (global, settings); +} + +static void +entry_cursor_func (StEntry *entry, + gboolean use_ibeam, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + meta_display_set_cursor (global->meta_display, + use_ibeam ? META_CURSOR_IBEAM : META_CURSOR_DEFAULT); +} + +static void +on_x11_display_closed (MetaDisplay *display, + ShellGlobal *global) +{ + g_signal_handlers_disconnect_by_data (global->stage, global); +} + +void +_shell_global_set_plugin (ShellGlobal *global, + MetaPlugin *plugin) +{ + MetaDisplay *display; + MetaBackend *backend; + MetaSettings *settings; + + g_return_if_fail (SHELL_IS_GLOBAL (global)); + g_return_if_fail (global->plugin == NULL); + + global->backend = meta_get_backend (); + global->plugin = plugin; + global->wm = shell_wm_new (plugin); + + display = meta_plugin_get_display (plugin); + global->meta_display = display; + global->meta_context = meta_display_get_context (display); + global->workspace_manager = meta_display_get_workspace_manager (display); + + global->stage = CLUTTER_STAGE (meta_get_stage_for_display (display)); + + if (!meta_is_wayland_compositor ()) + { + MetaX11Display *x11_display = meta_display_get_x11_display (display); + global->xdisplay = meta_x11_display_get_xdisplay (x11_display); + } + + st_entry_set_cursor_func (entry_cursor_func, global); + st_clipboard_set_selection (meta_display_get_selection (display)); + + g_signal_connect (global->stage, "notify::width", + G_CALLBACK (global_stage_notify_width), global); + g_signal_connect (global->stage, "notify::height", + G_CALLBACK (global_stage_notify_height), global); + + clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_PRE_PAINT, + global_stage_before_paint, + global, NULL); + + g_signal_connect (global->stage, "after-paint", + G_CALLBACK (global_stage_after_paint), global); + + clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_POST_PAINT, + global_stage_after_swap, + global, NULL); + + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.stagePaintStart", + "Start of stage page repaint", + ""); + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.paintCompletedTimestamp", + "Paint completion on GPU", + ""); + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.stagePaintDone", + "End of frame, possibly including swap time", + ""); + + g_signal_connect (global->stage, "notify::key-focus", + G_CALLBACK (focus_actor_changed), global); + g_signal_connect (global->meta_display, "notify::focus-window", + G_CALLBACK (focus_window_changed), global); + + if (global->xdisplay) + g_signal_connect_object (global->meta_display, "x11-display-closing", + G_CALLBACK (on_x11_display_closed), global, 0); + + backend = meta_get_backend (); + settings = meta_backend_get_settings (backend); + g_signal_connect (settings, "ui-scaling-factor-changed", + G_CALLBACK (ui_scaling_factor_changed), global); + + global->focus_manager = st_focus_manager_get_for_stage (global->stage); + + update_scaling_factor (global, settings); +} + +GjsContext * +_shell_global_get_gjs_context (ShellGlobal *global) +{ + return global->js_context; +} + +/* Code to close all file descriptors before we exec; copied from gspawn.c in GLib. + * + * Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering + * + * http://bugzilla.gnome.org/show_bug.cgi?id=469231 + * http://bugzilla.gnome.org/show_bug.cgi?id=357585 + */ + +static int +set_cloexec (void *data, gint fd) +{ + if (fd >= GPOINTER_TO_INT (data)) + fcntl (fd, F_SETFD, FD_CLOEXEC); + + return 0; +} + +#ifndef HAVE_FDWALK +static int +fdwalk (int (*cb)(void *data, int fd), void *data) +{ + gint open_max; + gint fd; + gint res = 0; + +#ifdef HAVE_SYS_RESOURCE_H + struct rlimit rl; +#endif + +#ifdef __linux__ + DIR *d; + + if ((d = opendir("/proc/self/fd"))) { + struct dirent *de; + + while ((de = readdir(d))) { + glong l; + gchar *e = NULL; + + if (de->d_name[0] == '.') + continue; + + errno = 0; + l = strtol(de->d_name, &e, 10); + if (errno != 0 || !e || *e) + continue; + + fd = (gint) l; + + if ((glong) fd != l) + continue; + + if (fd == dirfd(d)) + continue; + + if ((res = cb (data, fd)) != 0) + break; + } + + closedir(d); + return res; + } + + /* If /proc is not mounted or not accessible we fall back to the old + * rlimit trick */ + +#endif + +#ifdef HAVE_SYS_RESOURCE_H + if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) + open_max = rl.rlim_max; + else +#endif + open_max = sysconf (_SC_OPEN_MAX); + + for (fd = 0; fd < open_max; fd++) + if ((res = cb (data, fd)) != 0) + break; + + return res; +} +#endif + +static void +pre_exec_close_fds(void) +{ + fdwalk (set_cloexec, GINT_TO_POINTER(3)); +} + +/** + * shell_global_reexec_self: + * @global: A #ShellGlobal + * + * Restart the current process. Only intended for development purposes. + */ +void +shell_global_reexec_self (ShellGlobal *global) +{ + GPtrArray *arr; + gsize len; + MetaContext *meta_context; + +#if defined __linux__ || defined __sun + char *buf; + char *buf_p; + char *buf_end; + g_autoptr (GError) error = NULL; + + if (!g_file_get_contents ("/proc/self/cmdline", &buf, &len, &error)) + { + g_warning ("failed to get /proc/self/cmdline: %s", error->message); + return; + } + + buf_end = buf+len; + arr = g_ptr_array_new (); + /* The cmdline file is NUL-separated */ + for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) + g_ptr_array_add (arr, buf_p); + + g_ptr_array_add (arr, NULL); +#elif defined __OpenBSD__ + gchar **args, **args_p; + gint mib[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; + + if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) + return; + + args = g_malloc0 (len); + + if (sysctl (mib, G_N_ELEMENTS (mib), args, &len, NULL, 0) == -1) { + g_warning ("failed to get command line args: %d", errno); + g_free (args); + return; + } + + arr = g_ptr_array_new (); + for (args_p = args; *args_p != NULL; args_p++) { + g_ptr_array_add (arr, *args_p); + } + + g_ptr_array_add (arr, NULL); +#elif defined __FreeBSD__ + char *buf; + char *buf_p; + char *buf_end; + gint mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, getpid() }; + + if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) + return; + + buf = g_malloc0 (len); + + if (sysctl (mib, G_N_ELEMENTS (mib), buf, &len, NULL, 0) == -1) { + g_warning ("failed to get command line args: %d", errno); + g_free (buf); + return; + } + + buf_end = buf+len; + arr = g_ptr_array_new (); + /* The value returned by sysctl is NUL-separated */ + for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) + g_ptr_array_add (arr, buf_p); + + g_ptr_array_add (arr, NULL); +#else + return; +#endif + + /* Close all file descriptors other than stdin/stdout/stderr, otherwise + * they will leak and stay open after the exec. In particular, this is + * important for file descriptors that represent mapped graphics buffer + * objects. + */ + pre_exec_close_fds (); + + g_object_get (global, "context", &meta_context, NULL); + meta_context_restore_rlimit_nofile (meta_context, NULL); + + meta_display_close (shell_global_get_display (global), + shell_global_get_current_time (global)); + + execvp (arr->pdata[0], (char**)arr->pdata); + g_warning ("failed to reexec: %s", g_strerror (errno)); + g_ptr_array_free (arr, TRUE); +#if defined __linux__ || defined __FreeBSD__ + g_free (buf); +#elif defined __OpenBSD__ + g_free (args); +#endif +} + +/** + * shell_global_notify_error: + * @global: a #ShellGlobal + * @msg: Error message + * @details: Error details + * + * Show a system error notification. Use this function + * when a user-initiated action results in a non-fatal problem + * from causes that may not be under system control. For + * example, an application crash. + */ +void +shell_global_notify_error (ShellGlobal *global, + const char *msg, + const char *details) +{ + g_signal_emit_by_name (global, "notify-error", msg, details); +} + +/** + * shell_global_get_pointer: + * @global: the #ShellGlobal + * @x: (out): the X coordinate of the pointer, in global coordinates + * @y: (out): the Y coordinate of the pointer, in global coordinates + * @mods: (out): the current set of modifier keys that are pressed down + * + * Gets the pointer coordinates and current modifier key state. + */ +void +shell_global_get_pointer (ShellGlobal *global, + int *x, + int *y, + ClutterModifierType *mods) +{ + ClutterModifierType raw_mods; + MetaCursorTracker *tracker; + graphene_point_t point; + + tracker = meta_cursor_tracker_get_for_display (global->meta_display); + meta_cursor_tracker_get_pointer (tracker, &point, &raw_mods); + + if (x) + *x = point.x; + if (y) + *y = point.y; + + *mods = raw_mods & CLUTTER_MODIFIER_MASK; +} + +/** + * shell_global_get_switcheroo_control: + * @global: A #ShellGlobal + * + * Get the global #GDBusProxy instance for the switcheroo-control + * daemon. + * + * Return value: (transfer none): the #GDBusProxy for the daemon, + * or %NULL on error. + */ +GDBusProxy * +shell_global_get_switcheroo_control (ShellGlobal *global) +{ + return global->switcheroo_control; +} + +/** + * shell_global_get_settings: + * @global: A #ShellGlobal + * + * Get the global GSettings instance. + * + * Return value: (transfer none): The GSettings object + */ +GSettings * +shell_global_get_settings (ShellGlobal *global) +{ + return global->settings; +} + +/** + * shell_global_get_current_time: + * @global: A #ShellGlobal + * + * Returns: the current X server time from the current Clutter, Gdk, or X + * event. If called from outside an event handler, this may return + * %Clutter.CURRENT_TIME (aka 0), or it may return a slightly + * out-of-date timestamp. + */ +guint32 +shell_global_get_current_time (ShellGlobal *global) +{ + guint32 time; + + /* meta_display_get_current_time() will return the correct time + when handling an X or Gdk event, but will return CurrentTime + from some Clutter event callbacks. + + clutter_get_current_event_time() will return the correct time + from a Clutter event callback, but may return CLUTTER_CURRENT_TIME + timestamp if called at other times. + + So we try meta_display_get_current_time() first, since we + can recognize a "wrong" answer from that, and then fall back + to clutter_get_current_event_time(). + */ + + time = meta_display_get_current_time (global->meta_display); + if (time != CLUTTER_CURRENT_TIME) + return time; + + return clutter_get_current_event_time (); +} + +static void +shell_global_app_launched_cb (GAppLaunchContext *context, + GAppInfo *info, + GVariant *platform_data, + gpointer user_data) +{ + gint32 pid; + const gchar *app_name; + + if (!g_variant_lookup (platform_data, "pid", "i", &pid)) + return; + + /* If pid == 0 the application was launched through D-Bus + * activation, therefore it's already in its own unit */ + if (pid == 0) + return; + + app_name = g_app_info_get_id (info); + if (app_name == NULL) + app_name = g_app_info_get_executable (info); + + /* Start async request; we don't care about the result */ + gnome_start_systemd_scope (app_name, + pid, + NULL, + NULL, + NULL, NULL, NULL); +} + +/** + * shell_global_create_app_launch_context: + * @global: A #ShellGlobal + * @timestamp: the timestamp for the launch (or 0 for current time) + * @workspace: a workspace index, or -1 to indicate the current one + * + * Create a #GAppLaunchContext set up with the correct timestamp, and + * targeted to activate on the current workspace. + * + * Return value: (transfer full): A new #GAppLaunchContext + */ +GAppLaunchContext * +shell_global_create_app_launch_context (ShellGlobal *global, + guint32 timestamp, + int workspace) +{ + MetaWorkspaceManager *workspace_manager = global->workspace_manager; + MetaStartupNotification *sn; + MetaLaunchContext *context; + MetaWorkspace *ws = NULL; + + sn = meta_display_get_startup_notification (global->meta_display); + context = meta_startup_notification_create_launcher (sn); + + if (timestamp == 0) + timestamp = shell_global_get_current_time (global); + meta_launch_context_set_timestamp (context, timestamp); + + if (workspace < 0) + ws = meta_workspace_manager_get_active_workspace (workspace_manager); + else + ws = meta_workspace_manager_get_workspace_by_index (workspace_manager, workspace); + + meta_launch_context_set_workspace (context, ws); + + g_signal_connect (context, + "launched", + G_CALLBACK (shell_global_app_launched_cb), + NULL); + + return (GAppLaunchContext *) context; +} + +typedef struct +{ + ShellLeisureFunction func; + gpointer user_data; + GDestroyNotify notify; +} LeisureClosure; + +static gboolean +run_leisure_functions (gpointer data) +{ + ShellGlobal *global = data; + GSList *closures; + GSList *iter; + + global->leisure_function_id = 0; + + /* We started more work since we scheduled the idle */ + if (global->work_count > 0) + return FALSE; + + /* No leisure closures, so we are done */ + if (global->leisure_closures == NULL) + return FALSE; + + closures = global->leisure_closures; + global->leisure_closures = NULL; + + for (iter = closures; iter; iter = iter->next) + { + LeisureClosure *closure = closures->data; + closure->func (closure->user_data); + + if (closure->notify) + closure->notify (closure->user_data); + + g_free (closure); + } + + g_slist_free (closures); + + return FALSE; +} + +static void +schedule_leisure_functions (ShellGlobal *global) +{ + /* This is called when we think we are ready to run leisure functions + * by our own accounting. We try to handle other types of business + * (like ClutterAnimation) by adding a low priority idle function. + * + * This won't work properly if the mainloop goes idle waiting for + * the vertical blanking interval or waiting for work being done + * in another thread. + */ + if (!global->leisure_function_id) + { + global->leisure_function_id = g_idle_add_full (G_PRIORITY_LOW, + run_leisure_functions, + global, NULL); + g_source_set_name_by_id (global->leisure_function_id, "[gnome-shell] run_leisure_functions"); + } +} + +/** + * shell_global_begin_work: + * @global: the #ShellGlobal + * + * Marks that we are currently doing work. This is used to to track + * whether we are busy for the purposes of shell_global_run_at_leisure(). + * A count is kept and shell_global_end_work() must be called exactly + * as many times as shell_global_begin_work(). + */ +void +shell_global_begin_work (ShellGlobal *global) +{ + global->work_count++; +} + +/** + * shell_global_end_work: + * @global: the #ShellGlobal + * + * Marks the end of work that we started with shell_global_begin_work(). + * If no other work is ongoing and functions have been added with + * shell_global_run_at_leisure(), they will be run at the next + * opportunity. + */ +void +shell_global_end_work (ShellGlobal *global) +{ + g_return_if_fail (global->work_count > 0); + + global->work_count--; + if (global->work_count == 0) + schedule_leisure_functions (global); + +} + +/** + * shell_global_run_at_leisure: + * @global: the #ShellGlobal + * @func: function to call at leisure + * @user_data: data to pass to @func + * @notify: function to call to free @user_data + * + * Schedules a function to be called the next time the shell is idle. + * Idle means here no animations, no redrawing, and no ongoing background + * work. Since there is currently no way to hook into the Clutter master + * clock and know when is running, the implementation here is somewhat + * approximation. Animations may be detected as terminating early if they + * can be drawn fast enough so that the event loop goes idle between frames. + * + * The intent of this function is for performance measurement runs + * where a number of actions should be run serially and each action is + * timed individually. Using this function for other purposes will + * interfere with the ability to use it for performance measurement so + * should be avoided. + */ +void +shell_global_run_at_leisure (ShellGlobal *global, + ShellLeisureFunction func, + gpointer user_data, + GDestroyNotify notify) +{ + LeisureClosure *closure = g_new (LeisureClosure, 1); + closure->func = func; + closure->user_data = user_data; + closure->notify = notify; + + global->leisure_closures = g_slist_append (global->leisure_closures, + closure); + + if (global->work_count == 0) + schedule_leisure_functions (global); +} + +const char * +shell_global_get_session_mode (ShellGlobal *global) +{ + g_return_val_if_fail (SHELL_IS_GLOBAL (global), "user"); + + return global->session_mode; +} + +static void +delete_variant_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellGlobal *global = user_data; + GError *error = NULL; + + if (!g_file_delete_finish (G_FILE (object), result, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + g_warning ("Could not delete runtime/persistent state file: %s\n", + error->message); + } + + g_error_free (error); + } + + g_hash_table_remove (global->save_ops, object); +} + +static void +replace_contents_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GFile *file = source_object; + GBytes *bytes = task_data; + GError *error = NULL; + const gchar *data; + gsize len; + + data = g_bytes_get_data (bytes, &len); + + if (!g_file_replace_contents (file, data, len, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, + NULL, cancellable, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +static void +replace_contents_async (GFile *path, + GBytes *bytes, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_assert (G_IS_FILE (path)); + g_assert (bytes != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (path, cancellable, callback, user_data); + g_task_set_source_tag (task, replace_contents_async); + g_task_set_task_data (task, g_bytes_ref (bytes), (GDestroyNotify)g_bytes_unref); + g_task_run_in_thread (task, replace_contents_worker); +} + +static gboolean +replace_contents_finish (GFile *file, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +replace_variant_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellGlobal *global = user_data; + GError *error = NULL; + + if (!replace_contents_finish (G_FILE (object), result, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Could not replace runtime/persistent state file: %s\n", + error->message); + } + + g_error_free (error); + } + + g_hash_table_remove (global->save_ops, object); +} + +static void +save_variant (ShellGlobal *global, + GFile *dir, + const char *property_name, + GVariant *variant) +{ + GFile *path = g_file_get_child (dir, property_name); + GCancellable *cancellable; + + cancellable = g_hash_table_lookup (global->save_ops, path); + g_cancellable_cancel (cancellable); + + cancellable = g_cancellable_new (); + g_hash_table_insert (global->save_ops, g_object_ref (path), cancellable); + + if (variant == NULL || g_variant_get_data (variant) == NULL) + { + g_file_delete_async (path, G_PRIORITY_DEFAULT, cancellable, + delete_variant_cb, global); + } + else + { + g_autoptr(GBytes) bytes = NULL; + + bytes = g_bytes_new_with_free_func (g_variant_get_data (variant), + g_variant_get_size (variant), + (GDestroyNotify)g_variant_unref, + g_variant_ref (variant)); + /* g_file_replace_contents_async() can potentially fsync() from the + * calling thread when completing the asynchronous task. Instead, we + * want to force that fsync() to a thread to avoid blocking the + * compositor main loop. Using our own replace_contents_async() + * simply executes the operation synchronously from a thread. + */ + replace_contents_async (path, bytes, cancellable, replace_variant_cb, global); + } + + g_object_unref (path); +} + +static GVariant * +load_variant (GFile *dir, + const char *property_type, + const char *property_name) +{ + GVariant *res = NULL; + GMappedFile *mfile; + GFile *path = g_file_get_child (dir, property_name); + char *pathstr; + GError *local_error = NULL; + + pathstr = g_file_get_path (path); + mfile = g_mapped_file_new (pathstr, FALSE, &local_error); + if (!mfile) + { + if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + { + g_warning ("Failed to open runtime state: %s", local_error->message); + } + g_clear_error (&local_error); + } + else + { + GBytes *bytes = g_mapped_file_get_bytes (mfile); + res = g_variant_new_from_bytes (G_VARIANT_TYPE (property_type), bytes, FALSE); + g_bytes_unref (bytes); + g_mapped_file_unref (mfile); + } + + g_object_unref (path); + g_free (pathstr); + + return res; +} + +/** + * shell_global_set_runtime_state: + * @global: a #ShellGlobal + * @property_name: Name of the property + * @variant: (nullable): A #GVariant, or %NULL to unset + * + * Change the value of serialized runtime state. + */ +void +shell_global_set_runtime_state (ShellGlobal *global, + const char *property_name, + GVariant *variant) +{ + save_variant (global, global->runtime_state_path, property_name, variant); +} + +/** + * shell_global_get_runtime_state: + * @global: a #ShellGlobal + * @property_type: Expected data type + * @property_name: Name of the property + * + * The shell maintains "runtime" state which does not persist across + * logout or reboot. + * + * Returns: (transfer floating): The value of a serialized property, or %NULL if none stored + */ +GVariant * +shell_global_get_runtime_state (ShellGlobal *global, + const char *property_type, + const char *property_name) +{ + return load_variant (global->runtime_state_path, property_type, property_name); +} + +/** + * shell_global_set_persistent_state: + * @global: a #ShellGlobal + * @property_name: Name of the property + * @variant: (nullable): A #GVariant, or %NULL to unset + * + * Change the value of serialized persistent state. + */ +void +shell_global_set_persistent_state (ShellGlobal *global, + const char *property_name, + GVariant *variant) +{ + save_variant (global, global->userdatadir_path, property_name, variant); +} + +/** + * shell_global_get_persistent_state: + * @global: a #ShellGlobal + * @property_type: Expected data type + * @property_name: Name of the property + * + * The shell maintains "persistent" state which will persist after + * logout or reboot. + * + * Returns: (transfer none): The value of a serialized property, or %NULL if none stored + */ +GVariant * +shell_global_get_persistent_state (ShellGlobal *global, + const char *property_type, + const char *property_name) +{ + return load_variant (global->userdatadir_path, property_type, property_name); +} + +void +_shell_global_locate_pointer (ShellGlobal *global) +{ + g_signal_emit (global, shell_global_signals[LOCATE_POINTER], 0); +} diff --git a/src/shell-global.h b/src/shell-global.h new file mode 100644 index 0000000..8d8238c --- /dev/null +++ b/src/shell-global.h @@ -0,0 +1,94 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLOBAL_H__ +#define __SHELL_GLOBAL_H__ + +#include <clutter/clutter.h> +#include <glib-object.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <gtk/gtk.h> +#include <meta/meta-plugin.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_GLOBAL (shell_global_get_type ()) +G_DECLARE_FINAL_TYPE (ShellGlobal, shell_global, SHELL, GLOBAL, GObject) + +ShellGlobal *shell_global_get (void); + +ClutterStage *shell_global_get_stage (ShellGlobal *global); +MetaDisplay *shell_global_get_display (ShellGlobal *global); +GList *shell_global_get_window_actors (ShellGlobal *global); +GSettings *shell_global_get_settings (ShellGlobal *global); +guint32 shell_global_get_current_time (ShellGlobal *global); +MetaWorkspaceManager *shell_global_get_workspace_manager (ShellGlobal *global); + + +/* Input/event handling */ +void shell_global_set_stage_input_region (ShellGlobal *global, + GSList *rectangles); + +void shell_global_get_pointer (ShellGlobal *global, + int *x, + int *y, + ClutterModifierType *mods); + +typedef struct { + guint glibc_uordblks; + + guint js_bytes; + + guint gjs_boxed; + guint gjs_gobject; + guint gjs_function; + guint gjs_closure; + + /* 32 bit to avoid js conversion problems with 64 bit */ + guint last_gc_seconds_ago; +} ShellMemoryInfo; + +/* Run-at-leisure API */ +void shell_global_begin_work (ShellGlobal *global); +void shell_global_end_work (ShellGlobal *global); + +typedef void (*ShellLeisureFunction) (gpointer data); + +void shell_global_run_at_leisure (ShellGlobal *global, + ShellLeisureFunction func, + gpointer user_data, + GDestroyNotify notify); + + +/* Misc utilities / Shell API */ +GDBusProxy * + shell_global_get_switcheroo_control (ShellGlobal *global); + +GAppLaunchContext * + shell_global_create_app_launch_context (ShellGlobal *global, + guint32 timestamp, + int workspace); + +void shell_global_notify_error (ShellGlobal *global, + const char *msg, + const char *details); + +void shell_global_reexec_self (ShellGlobal *global); + +const char * shell_global_get_session_mode (ShellGlobal *global); + +void shell_global_set_runtime_state (ShellGlobal *global, + const char *property_name, + GVariant *variant); +GVariant * shell_global_get_runtime_state (ShellGlobal *global, + const char *property_type, + const char *property_name); + +void shell_global_set_persistent_state (ShellGlobal *global, + const char *property_name, + GVariant *variant); +GVariant * shell_global_get_persistent_state (ShellGlobal *global, + const char *property_type, + const char *property_name); + +G_END_DECLS + +#endif /* __SHELL_GLOBAL_H__ */ diff --git a/src/shell-glsl-effect.c b/src/shell-glsl-effect.c new file mode 100644 index 0000000..3051e0a --- /dev/null +++ b/src/shell-glsl-effect.c @@ -0,0 +1,205 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/** + * SECTION:shell-glsl-effect + * @short_description: An offscreen effect using GLSL + * + * A #ShellGLSLEffect is a #ClutterOffscreenEffect that allows + * running custom GLSL to the vertex and fragment stages of the + * graphic pipeline. + */ + +#include "config.h" + +#include <cogl/cogl.h> +#include "shell-glsl-effect.h" + +typedef struct _ShellGLSLEffectPrivate ShellGLSLEffectPrivate; +struct _ShellGLSLEffectPrivate +{ + CoglPipeline *pipeline; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellGLSLEffect, shell_glsl_effect, CLUTTER_TYPE_OFFSCREEN_EFFECT); + +static CoglPipeline * +shell_glsl_effect_create_pipeline (ClutterOffscreenEffect *effect, + CoglTexture *texture) +{ + ShellGLSLEffect *self = SHELL_GLSL_EFFECT (effect); + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (self); + + cogl_pipeline_set_layer_texture (priv->pipeline, 0, texture); + + return cogl_object_ref (priv->pipeline); +} + +/** + * shell_glsl_effect_add_glsl_snippet: + * @effect: a #ShellGLSLEffect + * @hook: where to insert the code + * @declarations: GLSL declarations + * @code: GLSL code + * @is_replace: whether Cogl code should be replaced by the custom shader + * + * Adds a GLSL snippet to the pipeline used for drawing the effect texture. + * See #CoglSnippet for details. + * + * This is only valid inside the a call to the build_pipeline() virtual + * function. + */ +void +shell_glsl_effect_add_glsl_snippet (ShellGLSLEffect *effect, + ShellSnippetHook hook, + const char *declarations, + const char *code, + gboolean is_replace) +{ + ShellGLSLEffectClass *klass = SHELL_GLSL_EFFECT_GET_CLASS (effect); + CoglSnippet *snippet; + + g_return_if_fail (klass->base_pipeline != NULL); + + if (is_replace) + { + snippet = cogl_snippet_new ((CoglSnippetHook)hook, declarations, NULL); + cogl_snippet_set_replace (snippet, code); + } + else + { + snippet = cogl_snippet_new ((CoglSnippetHook)hook, declarations, code); + } + + if (hook == SHELL_SNIPPET_HOOK_VERTEX || + hook == SHELL_SNIPPET_HOOK_FRAGMENT) + cogl_pipeline_add_snippet (klass->base_pipeline, snippet); + else + cogl_pipeline_add_layer_snippet (klass->base_pipeline, 0, snippet); + + cogl_object_unref (snippet); +} + +static void +shell_glsl_effect_dispose (GObject *gobject) +{ + ShellGLSLEffect *self = SHELL_GLSL_EFFECT (gobject); + ShellGLSLEffectPrivate *priv; + + priv = shell_glsl_effect_get_instance_private (self); + + g_clear_pointer (&priv->pipeline, cogl_object_unref); + + G_OBJECT_CLASS (shell_glsl_effect_parent_class)->dispose (gobject); +} + +static void +shell_glsl_effect_init (ShellGLSLEffect *effect) +{ +} + +static void +shell_glsl_effect_constructed (GObject *object) +{ + ShellGLSLEffect *self; + ShellGLSLEffectClass *klass; + ShellGLSLEffectPrivate *priv; + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + G_OBJECT_CLASS (shell_glsl_effect_parent_class)->constructed (object); + + /* Note that, differently from ClutterBlurEffect, we are calling + this inside constructed, not init, so klass points to the most-derived + GTypeClass, not ShellGLSLEffectClass. + */ + klass = SHELL_GLSL_EFFECT_GET_CLASS (object); + self = SHELL_GLSL_EFFECT (object); + priv = shell_glsl_effect_get_instance_private (self); + + if (G_UNLIKELY (klass->base_pipeline == NULL)) + { + klass->base_pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_set_blend (klass->base_pipeline, "RGBA = ADD (SRC_COLOR * (SRC_COLOR[A]), DST_COLOR * (1-SRC_COLOR[A]))", NULL); + + if (klass->build_pipeline != NULL) + klass->build_pipeline (self); + } + + priv->pipeline = cogl_pipeline_copy (klass->base_pipeline); + + cogl_pipeline_set_layer_null_texture (klass->base_pipeline, 0); +} + +static void +shell_glsl_effect_class_init (ShellGLSLEffectClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterOffscreenEffectClass *offscreen_class; + + offscreen_class = CLUTTER_OFFSCREEN_EFFECT_CLASS (klass); + offscreen_class->create_pipeline = shell_glsl_effect_create_pipeline; + + gobject_class->constructed = shell_glsl_effect_constructed; + gobject_class->dispose = shell_glsl_effect_dispose; +} + +/** + * shell_glsl_effect_get_uniform_location: + * @effect: a #ShellGLSLEffect + * @name: the uniform name + * + * Returns: the location of the uniform named @name, that can be + * passed to shell_glsl_effect_set_uniform_float(). + */ +int +shell_glsl_effect_get_uniform_location (ShellGLSLEffect *effect, + const char *name) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + return cogl_pipeline_get_uniform_location (priv->pipeline, name); +} + +/** + * shell_glsl_effect_set_uniform_float: + * @effect: a #ShellGLSLEffect + * @uniform: the uniform location (as returned by shell_glsl_effect_get_uniform_location()) + * @n_components: the number of components in the uniform (eg. 3 for a vec3) + * @total_count: the total number of floats in @value + * @value: (array length=total_count): the array of floats to set @uniform + */ +void +shell_glsl_effect_set_uniform_float (ShellGLSLEffect *effect, + int uniform, + int n_components, + int total_count, + const float *value) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + cogl_pipeline_set_uniform_float (priv->pipeline, uniform, + n_components, total_count / n_components, + value); +} + +/** + * shell_glsl_effect_set_uniform_matrix: + * @effect: a #ShellGLSLEffect + * @uniform: the uniform location (as returned by shell_glsl_effect_get_uniform_location()) + * @transpose: Whether to transpose the matrix + * @dimensions: the number of components in the uniform (eg. 3 for a vec3) + * @total_count: the total number of floats in @value + * @value: (array length=total_count): the array of floats to set @uniform + */ +void +shell_glsl_effect_set_uniform_matrix (ShellGLSLEffect *effect, + int uniform, + gboolean transpose, + int dimensions, + int total_count, + const float *value) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + cogl_pipeline_set_uniform_matrix (priv->pipeline, uniform, + dimensions, + total_count / (dimensions * dimensions), + transpose, value); +} diff --git a/src/shell-glsl-effect.h b/src/shell-glsl-effect.h new file mode 100644 index 0000000..3759e71 --- /dev/null +++ b/src/shell-glsl-effect.h @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLSL_EFFECT_H__ +#define __SHELL_GLSL_EFFECT_H__ + +#include "st.h" +#include <gtk/gtk.h> + +/** + * ShellSnippetHook: + * Temporary hack to work around Cogl not exporting CoglSnippetHook in + * the 1.0 API. Don't use. + */ +typedef enum { + /* Per pipeline vertex hooks */ + SHELL_SNIPPET_HOOK_VERTEX = 0, + SHELL_SNIPPET_HOOK_VERTEX_TRANSFORM, + + /* Per pipeline fragment hooks */ + SHELL_SNIPPET_HOOK_FRAGMENT = 2048, + + /* Per layer vertex hooks */ + SHELL_SNIPPET_HOOK_TEXTURE_COORD_TRANSFORM = 4096, + + /* Per layer fragment hooks */ + SHELL_SNIPPET_HOOK_LAYER_FRAGMENT = 6144, + SHELL_SNIPPET_HOOK_TEXTURE_LOOKUP +} ShellSnippetHook; + +#define SHELL_TYPE_GLSL_EFFECT (shell_glsl_effect_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellGLSLEffect, shell_glsl_effect, + SHELL, GLSL_EFFECT, ClutterOffscreenEffect) + +struct _ShellGLSLEffectClass +{ + ClutterOffscreenEffectClass parent_class; + + CoglPipeline *base_pipeline; + + void (*build_pipeline) (ShellGLSLEffect *effect); +}; + +void shell_glsl_effect_add_glsl_snippet (ShellGLSLEffect *effect, + ShellSnippetHook hook, + const char *declarations, + const char *code, + gboolean is_replace); + +int shell_glsl_effect_get_uniform_location (ShellGLSLEffect *effect, + const char *name); +void shell_glsl_effect_set_uniform_float (ShellGLSLEffect *effect, + int uniform, + int n_components, + int total_count, + const float *value); +void shell_glsl_effect_set_uniform_matrix (ShellGLSLEffect *effect, + int uniform, + gboolean transpose, + int dimensions, + int total_count, + const float *value); + +#endif /* __SHELL_GLSL_EFFECT_H__ */ diff --git a/src/shell-gtk-embed.c b/src/shell-gtk-embed.c new file mode 100644 index 0000000..2ad18a1 --- /dev/null +++ b/src/shell-gtk-embed.c @@ -0,0 +1,364 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-embedded-window-private.h" +#include "shell-global.h" +#include "shell-util.h" + +#include <gdk/gdkx.h> +#include <meta/display.h> +#include <meta/window.h> + +enum { + PROP_0, + + PROP_WINDOW +}; + +typedef struct _ShellGtkEmbedPrivate ShellGtkEmbedPrivate; + +struct _ShellGtkEmbedPrivate +{ + ShellEmbeddedWindow *window; + + ClutterActor *window_actor; + gulong window_actor_destroyed_handler; + + gulong window_created_handler; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellGtkEmbed, shell_gtk_embed, CLUTTER_TYPE_CLONE); + +static void shell_gtk_embed_set_window (ShellGtkEmbed *embed, + ShellEmbeddedWindow *window); + +static void +shell_gtk_embed_on_window_destroy (GtkWidget *object, + ShellGtkEmbed *embed) +{ + shell_gtk_embed_set_window (embed, NULL); +} + +static void +shell_gtk_embed_remove_window_actor (ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window_actor) + { + g_clear_signal_handler (&priv->window_actor_destroyed_handler, + priv->window_actor); + + g_object_unref (priv->window_actor); + priv->window_actor = NULL; + } + + clutter_clone_set_source (CLUTTER_CLONE (embed), NULL); +} + +static void +shell_gtk_embed_window_created_cb (MetaDisplay *display, + MetaWindow *window, + ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + Window xwindow = meta_window_get_xwindow (window); + GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (priv->window)); + + if (gdk_window && xwindow == gdk_x11_window_get_xid (gdk_window)) + { + ClutterActor *window_actor = + CLUTTER_ACTOR (meta_window_get_compositor_private (window)); + GCallback remove_cb = G_CALLBACK (shell_gtk_embed_remove_window_actor); + cairo_region_t *empty_region; + + clutter_clone_set_source (CLUTTER_CLONE (embed), window_actor); + + /* We want to explicitly clear the clone source when the window + actor is destroyed because otherwise we might end up keeping + it alive after it has been disposed. Otherwise this can cause + a crash if there is a paint after mutter notices that the top + level window has been destroyed, which causes it to dispose + the window, and before the tray manager notices that the + window is gone which would otherwise reset the window and + unref the clone */ + priv->window_actor = g_object_ref (window_actor); + priv->window_actor_destroyed_handler = + g_signal_connect_swapped (window_actor, + "destroy", + remove_cb, + embed); + + /* Hide the original actor otherwise it will appear in the scene + as a normal window */ + clutter_actor_set_opacity (window_actor, 0); + + /* Also make sure it (or any of its children) doesn't block + events on wayland */ + shell_util_set_hidden_from_pick (window_actor, TRUE); + + /* Set an empty input shape on the window so that it can't get + any input. This probably isn't the ideal way to achieve this. + It would probably be better to force the window to go behind + Mutter's guard window, but this is quite difficult to do as + Mutter doesn't manage the stacking for override redirect + windows and the guard window is repeatedly lowered to the + bottom of the stack. */ + empty_region = cairo_region_create (); + gdk_window_input_shape_combine_region (gdk_window, + empty_region, + 0, 0 /* offset x/y */); + cairo_region_destroy (empty_region); + + gdk_window_lower (gdk_window); + + /* Now that we've found the window we don't need to listen for + new windows anymore */ + g_clear_signal_handler (&priv->window_created_handler, + display); + } +} + +static void +shell_gtk_embed_on_window_mapped (GtkWidget *object, + ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + if (priv->window_created_handler == 0 && priv->window_actor == NULL) + /* Listen for new windows so we can detect when Mutter has + created a MutterWindow for this window */ + priv->window_created_handler = + g_signal_connect (display, + "window-created", + G_CALLBACK (shell_gtk_embed_window_created_cb), + embed); +} + +static void +shell_gtk_embed_set_window (ShellGtkEmbed *embed, + ShellEmbeddedWindow *window) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + if (priv->window) + { + g_clear_signal_handler (&priv->window_created_handler, display); + + shell_gtk_embed_remove_window_actor (embed); + + _shell_embedded_window_set_actor (priv->window, NULL); + + g_object_unref (priv->window); + + g_signal_handlers_disconnect_by_func (priv->window, + (gpointer)shell_gtk_embed_on_window_destroy, + embed); + + g_signal_handlers_disconnect_by_func (priv->window, + (gpointer)shell_gtk_embed_on_window_mapped, + embed); + } + + priv->window = window; + + if (priv->window) + { + g_object_ref (priv->window); + + _shell_embedded_window_set_actor (priv->window, embed); + + g_signal_connect (priv->window, "destroy", + G_CALLBACK (shell_gtk_embed_on_window_destroy), embed); + + g_signal_connect (priv->window, "map", + G_CALLBACK (shell_gtk_embed_on_window_mapped), embed); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (embed)); +} + +static void +shell_gtk_embed_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + + switch (prop_id) + { + case PROP_WINDOW: + shell_gtk_embed_set_window (embed, (ShellEmbeddedWindow *)g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_gtk_embed_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + switch (prop_id) + { + case PROP_WINDOW: + g_value_set_object (value, priv->window); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_gtk_embed_get_preferred_width (ClutterActor *actor, + float for_height, + float *min_width_p, + float *natural_width_p) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window + && gtk_widget_get_visible (GTK_WIDGET (priv->window))) + { + GtkRequisition min_req, natural_req; + gtk_widget_get_preferred_size (GTK_WIDGET (priv->window), &min_req, &natural_req); + + *min_width_p = min_req.width; + *natural_width_p = natural_req.width; + } + else + *min_width_p = *natural_width_p = 0; +} + +static void +shell_gtk_embed_get_preferred_height (ClutterActor *actor, + float for_width, + float *min_height_p, + float *natural_height_p) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window + && gtk_widget_get_visible (GTK_WIDGET (priv->window))) + { + GtkRequisition min_req, natural_req; + gtk_widget_get_preferred_size (GTK_WIDGET (priv->window), &min_req, &natural_req); + + *min_height_p = min_req.height; + *natural_height_p = natural_req.height; + } + else + *min_height_p = *natural_height_p = 0; +} + +static void +shell_gtk_embed_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + float wx, wy; + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)-> + allocate (actor, box); + + /* Find the actor's new coordinates in terms of the stage (which is + * priv->window's parent window. + */ + clutter_actor_get_transformed_position (actor, &wx, &wy); + + _shell_embedded_window_allocate (priv->window, + (int)(0.5 + wx), (int)(0.5 + wy), + box->x2 - box->x1, + box->y2 - box->y1); +} + +static void +shell_gtk_embed_map (ClutterActor *actor) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + _shell_embedded_window_map (priv->window); + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)->map (actor); +} + +static void +shell_gtk_embed_unmap (ClutterActor *actor) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + _shell_embedded_window_unmap (priv->window); + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)->unmap (actor); +} + +static void +shell_gtk_embed_dispose (GObject *object) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + + G_OBJECT_CLASS (shell_gtk_embed_parent_class)->dispose (object); + + shell_gtk_embed_set_window (embed, NULL); +} + +static void +shell_gtk_embed_class_init (ShellGtkEmbedClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + object_class->get_property = shell_gtk_embed_get_property; + object_class->set_property = shell_gtk_embed_set_property; + object_class->dispose = shell_gtk_embed_dispose; + + actor_class->get_preferred_width = shell_gtk_embed_get_preferred_width; + actor_class->get_preferred_height = shell_gtk_embed_get_preferred_height; + actor_class->allocate = shell_gtk_embed_allocate; + actor_class->map = shell_gtk_embed_map; + actor_class->unmap = shell_gtk_embed_unmap; + + g_object_class_install_property (object_class, + PROP_WINDOW, + g_param_spec_object ("window", + "Window", + "ShellEmbeddedWindow to embed", + SHELL_TYPE_EMBEDDED_WINDOW, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +shell_gtk_embed_init (ShellGtkEmbed *embed) +{ +} + +/* + * Public API + */ +ClutterActor * +shell_gtk_embed_new (ShellEmbeddedWindow *window) +{ + g_return_val_if_fail (SHELL_IS_EMBEDDED_WINDOW (window), NULL); + + return g_object_new (SHELL_TYPE_GTK_EMBED, + "window", window, + NULL); +} diff --git a/src/shell-gtk-embed.h b/src/shell-gtk-embed.h new file mode 100644 index 0000000..4cfc489 --- /dev/null +++ b/src/shell-gtk-embed.h @@ -0,0 +1,20 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GTK_EMBED_H__ +#define __SHELL_GTK_EMBED_H__ + +#include <clutter/clutter.h> + +#include "shell-embedded-window.h" + +#define SHELL_TYPE_GTK_EMBED (shell_gtk_embed_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellGtkEmbed, shell_gtk_embed, + SHELL, GTK_EMBED, ClutterClone) + +struct _ShellGtkEmbedClass +{ + ClutterCloneClass parent_class; +}; + +ClutterActor *shell_gtk_embed_new (ShellEmbeddedWindow *window); + +#endif /* __SHELL_GTK_EMBED_H__ */ diff --git a/src/shell-invert-lightness-effect.c b/src/shell-invert-lightness-effect.c new file mode 100644 index 0000000..f856ede --- /dev/null +++ b/src/shell-invert-lightness-effect.c @@ -0,0 +1,152 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2010-2012 Inclusive Design Research Centre, OCAD University. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: + * Joseph Scheuhammer <clown@alum.mit.edu> + */ + +/** + * SECTION:shell-invert-lightness-effect + * @short_description: A colorization effect where lightness is inverted but + * color is not. + * @see_also: #ClutterEffect, #ClutterOffscreenEffect + * + * #ShellInvertLightnessEffect is a sub-class of #ClutterEffect that enhances + * the appearance of a clutter actor. Specifically it inverts the lightness + * of a #ClutterActor (e.g., darker colors become lighter, white becomes black, + * and white, black). + */ + +#define SHELL_INVERT_LIGHTNESS_EFFECT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, ShellInvertLightnessEffectClass)) +#define SHELL_IS_INVERT_EFFECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT)) +#define SHELL_INVERT_LIGHTNESS_EFFECT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFEC, ShellInvertLightnessEffectClass)) + +#include "shell-invert-lightness-effect.h" + +#include <cogl/cogl.h> + +struct _ShellInvertLightnessEffect +{ + ClutterOffscreenEffect parent_instance; + + CoglPipeline *pipeline; +}; + +struct _ShellInvertLightnessEffectClass +{ + ClutterOffscreenEffectClass parent_class; + + CoglPipeline *base_pipeline; +}; + +/* Lightness inversion in GLSL. + */ +static const gchar *invert_lightness_source = + "cogl_texel = texture2D (cogl_sampler, cogl_tex_coord.st);\n" + "vec3 effect = vec3 (cogl_texel);\n" + "\n" + "float maxColor = max (cogl_texel.r, max (cogl_texel.g, cogl_texel.b));\n" + "float minColor = min (cogl_texel.r, min (cogl_texel.g, cogl_texel.b));\n" + "float lightness = (maxColor + minColor) / 2.0;\n" + "\n" + "float delta = (1.0 - lightness) - lightness;\n" + "effect.rgb = (effect.rgb + delta);\n" + "\n" + "cogl_texel = vec4 (effect, cogl_texel.a);\n"; + +G_DEFINE_TYPE (ShellInvertLightnessEffect, + shell_invert_lightness_effect, + CLUTTER_TYPE_OFFSCREEN_EFFECT); + +static CoglPipeline * +shell_glsl_effect_create_pipeline (ClutterOffscreenEffect *effect, + CoglTexture *texture) +{ + ShellInvertLightnessEffect *self = SHELL_INVERT_LIGHTNESS_EFFECT (effect); + + cogl_pipeline_set_layer_texture (self->pipeline, 0, texture); + + return cogl_object_ref (self->pipeline); +} + +static void +shell_invert_lightness_effect_dispose (GObject *gobject) +{ + ShellInvertLightnessEffect *self = SHELL_INVERT_LIGHTNESS_EFFECT (gobject); + + if (self->pipeline != NULL) + { + cogl_object_unref (self->pipeline); + self->pipeline = NULL; + } + + G_OBJECT_CLASS (shell_invert_lightness_effect_parent_class)->dispose (gobject); +} + +static void +shell_invert_lightness_effect_class_init (ShellInvertLightnessEffectClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterOffscreenEffectClass *offscreen_class; + + offscreen_class = CLUTTER_OFFSCREEN_EFFECT_CLASS (klass); + offscreen_class->create_pipeline = shell_glsl_effect_create_pipeline; + + gobject_class->dispose = shell_invert_lightness_effect_dispose; +} + +static void +shell_invert_lightness_effect_init (ShellInvertLightnessEffect *self) +{ + ShellInvertLightnessEffectClass *klass; + klass = SHELL_INVERT_LIGHTNESS_EFFECT_GET_CLASS (self); + + if (G_UNLIKELY (klass->base_pipeline == NULL)) + { + CoglSnippet *snippet; + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + klass->base_pipeline = cogl_pipeline_new (ctx); + + snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP, + NULL, + NULL); + cogl_snippet_set_replace (snippet, invert_lightness_source); + cogl_pipeline_add_layer_snippet (klass->base_pipeline, 0, snippet); + cogl_object_unref (snippet); + + cogl_pipeline_set_layer_null_texture (klass->base_pipeline, 0); + } + + self->pipeline = cogl_pipeline_copy (klass->base_pipeline); +} + +/** + * shell_invert_lightness_effect_new: + * + * Creates a new #ShellInvertLightnessEffect to be used with + * clutter_actor_add_effect() + * + * Return value: (transfer full): the newly created + * #ShellInvertLightnessEffect or %NULL. Use g_object_unref() when done. + */ +ClutterEffect * +shell_invert_lightness_effect_new (void) +{ + return g_object_new (SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, NULL); +} diff --git a/src/shell-invert-lightness-effect.h b/src/shell-invert-lightness-effect.h new file mode 100644 index 0000000..3d7cf3a --- /dev/null +++ b/src/shell-invert-lightness-effect.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright © 2010-2012 Inclusive Design Research Centre, OCAD University. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * + * Author: + * Joseph Scheuhammer <clown@alum.mit.edu> + */ +#ifndef __SHELL_INVERT_LIGHTNESS_EFFECT_H__ +#define __SHELL_INVERT_LIGHTNESS_EFFECT_H__ + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_INVERT_LIGHTNESS_EFFECT (shell_invert_lightness_effect_get_type ()) +#define SHELL_INVERT_LIGHTNESS_EFFECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, ShellInvertLightnessEffect)) +#define SHELL_IS_INVERT_LIGHTNESS_EFFECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT)) + +typedef struct _ShellInvertLightnessEffect ShellInvertLightnessEffect; +typedef struct _ShellInvertLightnessEffectClass ShellInvertLightnessEffectClass; + +GType shell_invert_lightness_effect_get_type (void) G_GNUC_CONST; + +ClutterEffect *shell_invert_lightness_effect_new (void); + +G_END_DECLS + +#endif /* __SHELL_INVERT_LIGHTNESS_EFFECT_H__ */ diff --git a/src/shell-keyring-prompt.c b/src/shell-keyring-prompt.c new file mode 100644 index 0000000..bb03279 --- /dev/null +++ b/src/shell-keyring-prompt.c @@ -0,0 +1,832 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2012 Red Hat, Inc. + * 2012 Stef Walter <stefw@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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Author: Stef Walter <stefw@gnome.org> + */ + +#include "config.h" + +#include "shell-keyring-prompt.h" +#include "shell-secure-text-buffer.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include <gcr/gcr.h> + +#include <glib/gi18n.h> + +#include <string.h> + +typedef struct _ShellPasswordPromptPrivate ShellPasswordPromptPrivate; + +typedef enum +{ + PROMPTING_NONE, + PROMPTING_FOR_CONFIRM, + PROMPTING_FOR_PASSWORD +} PromptingMode; + +struct _ShellKeyringPrompt +{ + GObject parent; + + gchar *title; + gchar *message; + gchar *description; + gchar *warning; + gchar *choice_label; + gboolean choice_chosen; + gboolean password_new; + guint password_strength; + gchar *continue_label; + gchar *cancel_label; + + GTask *task; + ClutterText *password_actor; + ClutterText *confirm_actor; + PromptingMode mode; + gboolean shown; +}; + +enum { + PROP_0, + + PROP_PASSWORD_VISIBLE, + PROP_CONFIRM_VISIBLE, + PROP_WARNING_VISIBLE, + PROP_CHOICE_VISIBLE, + PROP_PASSWORD_ACTOR, + PROP_CONFIRM_ACTOR, + + N_PROPS, + + /* GcrPrompt */ + PROP_TITLE, + PROP_MESSAGE, + PROP_DESCRIPTION, + PROP_WARNING, + PROP_CHOICE_LABEL, + PROP_CHOICE_CHOSEN, + PROP_PASSWORD_NEW, + PROP_PASSWORD_STRENGTH, + PROP_CALLER_WINDOW, + PROP_CONTINUE_LABEL, + PROP_CANCEL_LABEL +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +static void shell_keyring_prompt_iface (GcrPromptInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (ShellKeyringPrompt, shell_keyring_prompt, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GCR_TYPE_PROMPT, shell_keyring_prompt_iface); +); + +enum { + SIGNAL_SHOW_PASSWORD, + SIGNAL_SHOW_CONFIRM, + SIGNAL_LAST +}; + +static gint signals[SIGNAL_LAST]; + +static void +shell_keyring_prompt_init (ShellKeyringPrompt *self) +{ + +} + +static gchar * +remove_mnemonics (const GValue *value) +{ + const gchar mnemonic = '_'; + gchar *stripped_label, *temp; + const gchar *label; + + g_return_val_if_fail (value != NULL, NULL); + g_return_val_if_fail (G_VALUE_HOLDS_STRING (value), NULL); + + label = g_value_get_string (value); + if (!label) + return NULL; + + /* Stripped label will have the original label length at most */ + stripped_label = temp = g_new (gchar, strlen(label) + 1); + g_assert (stripped_label != NULL); + + while (*label != '\0') + { + if (*label == mnemonic) + label++; + *(temp++) = *(label++); + } + *temp = '\0'; + + return stripped_label; +} + +static void +shell_keyring_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + switch (prop_id) { + case PROP_TITLE: + g_free (self->title); + self->title = g_value_dup_string (value); + g_object_notify (obj, "title"); + break; + case PROP_MESSAGE: + g_free (self->message); + self->message = g_value_dup_string (value); + g_object_notify (obj, "message"); + break; + case PROP_DESCRIPTION: + g_free (self->description); + self->description = g_value_dup_string (value); + g_object_notify (obj, "description"); + break; + case PROP_WARNING: + g_free (self->warning); + self->warning = g_value_dup_string (value); + if (!self->warning) + self->warning = g_strdup (""); + g_object_notify (obj, "warning"); + g_object_notify_by_pspec (obj, props[PROP_WARNING_VISIBLE]); + break; + case PROP_CHOICE_LABEL: + g_free (self->choice_label); + self->choice_label = remove_mnemonics (value); + if (!self->choice_label) + self->choice_label = g_strdup (""); + g_object_notify (obj, "choice-label"); + g_object_notify_by_pspec (obj, props[PROP_CHOICE_VISIBLE]); + break; + case PROP_CHOICE_CHOSEN: + self->choice_chosen = g_value_get_boolean (value); + g_object_notify (obj, "choice-chosen"); + break; + case PROP_PASSWORD_NEW: + self->password_new = g_value_get_boolean (value); + g_object_notify (obj, "password-new"); + g_object_notify_by_pspec (obj, props[PROP_CONFIRM_VISIBLE]); + break; + case PROP_CALLER_WINDOW: + /* ignored */ + break; + case PROP_CONTINUE_LABEL: + g_free (self->continue_label); + self->continue_label = remove_mnemonics (value); + g_object_notify (obj, "continue-label"); + break; + case PROP_CANCEL_LABEL: + g_free (self->cancel_label); + self->cancel_label = remove_mnemonics (value); + g_object_notify (obj, "cancel-label"); + break; + case PROP_PASSWORD_ACTOR: + shell_keyring_prompt_set_password_actor (self, g_value_get_object (value)); + break; + case PROP_CONFIRM_ACTOR: + shell_keyring_prompt_set_confirm_actor (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +shell_keyring_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, self->title ? self->title : ""); + break; + case PROP_MESSAGE: + g_value_set_string (value, self->message ? self->message : ""); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->description ? self->description : ""); + break; + case PROP_WARNING: + g_value_set_string (value, self->warning ? self->warning : ""); + break; + case PROP_CHOICE_LABEL: + g_value_set_string (value, self->choice_label ? self->choice_label : ""); + break; + case PROP_CHOICE_CHOSEN: + g_value_set_boolean (value, self->choice_chosen); + break; + case PROP_PASSWORD_NEW: + g_value_set_boolean (value, self->password_new); + break; + case PROP_PASSWORD_STRENGTH: + g_value_set_int (value, self->password_strength); + break; + case PROP_CALLER_WINDOW: + g_value_set_string (value, ""); + break; + case PROP_CONTINUE_LABEL: + g_value_set_string (value, self->continue_label); + break; + case PROP_CANCEL_LABEL: + g_value_set_string (value, self->cancel_label); + break; + case PROP_PASSWORD_VISIBLE: + g_value_set_boolean (value, self->mode == PROMPTING_FOR_PASSWORD); + break; + case PROP_CONFIRM_VISIBLE: + g_value_set_boolean (value, self->password_new && + self->mode == PROMPTING_FOR_PASSWORD); + break; + case PROP_WARNING_VISIBLE: + g_value_set_boolean (value, self->warning && self->warning[0]); + break; + case PROP_CHOICE_VISIBLE: + g_value_set_boolean (value, self->choice_label && self->choice_label[0]); + break; + case PROP_PASSWORD_ACTOR: + g_value_set_object (value, shell_keyring_prompt_get_password_actor (self)); + break; + case PROP_CONFIRM_ACTOR: + g_value_set_object (value, shell_keyring_prompt_get_confirm_actor (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +shell_keyring_prompt_dispose (GObject *obj) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + if (self->shown) + gcr_prompt_close (GCR_PROMPT (self)); + + if (self->task) + shell_keyring_prompt_cancel (self); + g_assert (self->task == NULL); + + shell_keyring_prompt_set_password_actor (self, NULL); + shell_keyring_prompt_set_confirm_actor (self, NULL); + + G_OBJECT_CLASS (shell_keyring_prompt_parent_class)->dispose (obj); +} + +static void +shell_keyring_prompt_finalize (GObject *obj) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + g_free (self->title); + g_free (self->message); + g_free (self->description); + g_free (self->warning); + g_free (self->choice_label); + g_free (self->continue_label); + g_free (self->cancel_label); + + G_OBJECT_CLASS (shell_keyring_prompt_parent_class)->finalize (obj); +} + +static void +shell_keyring_prompt_class_init (ShellKeyringPromptClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_keyring_prompt_get_property; + gobject_class->set_property = shell_keyring_prompt_set_property; + gobject_class->dispose = shell_keyring_prompt_dispose; + gobject_class->finalize = shell_keyring_prompt_finalize; + + g_object_class_override_property (gobject_class, PROP_TITLE, "title"); + + g_object_class_override_property (gobject_class, PROP_MESSAGE, "message"); + + g_object_class_override_property (gobject_class, PROP_DESCRIPTION, "description"); + + g_object_class_override_property (gobject_class, PROP_WARNING, "warning"); + + g_object_class_override_property (gobject_class, PROP_PASSWORD_NEW, "password-new"); + + g_object_class_override_property (gobject_class, PROP_PASSWORD_STRENGTH, "password-strength"); + + g_object_class_override_property (gobject_class, PROP_CHOICE_LABEL, "choice-label"); + + g_object_class_override_property (gobject_class, PROP_CHOICE_CHOSEN, "choice-chosen"); + + g_object_class_override_property (gobject_class, PROP_CALLER_WINDOW, "caller-window"); + + g_object_class_override_property (gobject_class, PROP_CONTINUE_LABEL, "continue-label"); + + g_object_class_override_property (gobject_class, PROP_CANCEL_LABEL, "cancel-label"); + + /** + * ShellKeyringPrompt:password-visible: + * + * Whether the password entry is visible or not. + */ + props[PROP_PASSWORD_VISIBLE] = + g_param_spec_boolean ("password-visible", + "Password visible", + "Password field is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:confirm-visible: + * + * Whether the password confirm entry is visible or not. + */ + props[PROP_CONFIRM_VISIBLE] = + g_param_spec_boolean ("confirm-visible", + "Confirm visible", + "Confirm field is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:warning-visible: + * + * Whether the warning label is visible or not. + */ + props[PROP_WARNING_VISIBLE] = + g_param_spec_boolean ("warning-visible", + "Warning visible", + "Warning is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:choice-visible: + * + * Whether the choice check box is visible or not. + */ + props[PROP_CHOICE_VISIBLE] = + g_param_spec_boolean ("choice-visible", + "Choice visible", + "Choice is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:password-actor: + * + * Text field for password + */ + props[PROP_PASSWORD_ACTOR] = + g_param_spec_object ("password-actor", + "Password actor", + "Text field for password", + CLUTTER_TYPE_TEXT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * ShellKeyringPrompt:confirm-actor: + * + * Text field for confirmation password + */ + props[PROP_CONFIRM_ACTOR] = + g_param_spec_object ("confirm-actor", + "Confirm actor", + "Text field for confirming password", + CLUTTER_TYPE_TEXT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + signals[SIGNAL_SHOW_PASSWORD] = g_signal_new ("show-password", G_TYPE_FROM_CLASS (klass), + 0, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[SIGNAL_SHOW_CONFIRM] = g_signal_new ("show-confirm", G_TYPE_FROM_CLASS (klass), + 0, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +shell_keyring_prompt_password_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + GObject *obj; + + if (self->task != NULL) { + g_warning ("this prompt can only show one prompt at a time"); + return; + } + + self->mode = PROMPTING_FOR_PASSWORD; + self->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (self->task, shell_keyring_prompt_password_async); + + obj = G_OBJECT (self); + g_object_notify (obj, "password-visible"); + g_object_notify (obj, "confirm-visible"); + g_object_notify (obj, "warning-visible"); + g_object_notify (obj, "choice-visible"); + + self->shown = TRUE; + g_signal_emit (self, signals[SIGNAL_SHOW_PASSWORD], 0); +} + +static const gchar * +shell_keyring_prompt_password_finish (GcrPrompt *prompt, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_get_source_object (G_TASK (result)) == prompt, NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_keyring_prompt_password_async), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +shell_keyring_prompt_confirm_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + GObject *obj; + + if (self->task != NULL) { + g_warning ("this prompt is already prompting"); + return; + } + + self->mode = PROMPTING_FOR_CONFIRM; + self->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (self->task, shell_keyring_prompt_confirm_async); + + obj = G_OBJECT (self); + g_object_notify (obj, "password-visible"); + g_object_notify (obj, "confirm-visible"); + g_object_notify (obj, "warning-visible"); + g_object_notify (obj, "choice-visible"); + + self->shown = TRUE; + g_signal_emit (self, signals[SIGNAL_SHOW_CONFIRM], 0); +} + +static GcrPromptReply +shell_keyring_prompt_confirm_finish (GcrPrompt *prompt, + GAsyncResult *result, + GError **error) +{ + GTask *task = G_TASK (result); + gssize res; + + g_return_val_if_fail (g_task_get_source_object (task) == prompt, + GCR_PROMPT_REPLY_CANCEL); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_keyring_prompt_confirm_async), GCR_PROMPT_REPLY_CANCEL); + + res = g_task_propagate_int (task, error); + return res == -1 ? GCR_PROMPT_REPLY_CANCEL : (GcrPromptReply)res; +} + +static void +shell_keyring_prompt_close (GcrPrompt *prompt) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + + /* + * We expect keyring.js to connect to this signal and do the + * actual work of closing the prompt. + */ + + self->shown = FALSE; +} + +static void +shell_keyring_prompt_iface (GcrPromptInterface *iface) +{ + iface->prompt_password_async = shell_keyring_prompt_password_async; + iface->prompt_password_finish = shell_keyring_prompt_password_finish; + iface->prompt_confirm_async = shell_keyring_prompt_confirm_async; + iface->prompt_confirm_finish = shell_keyring_prompt_confirm_finish; + iface->prompt_close = shell_keyring_prompt_close; +} + +/** + * shell_keyring_prompt_new: + * + * Create new internal prompt base + * + * Returns: (transfer full): new internal prompt + */ +ShellKeyringPrompt * +shell_keyring_prompt_new (void) +{ + return g_object_new (SHELL_TYPE_KEYRING_PROMPT, NULL); +} + +/** + * shell_keyring_prompt_get_password_actor: + * @self: the internal prompt + * + * Get the prompt password text actor + * + * Returns: (transfer none) (nullable): the password actor + */ +ClutterText * +shell_keyring_prompt_get_password_actor (ShellKeyringPrompt *self) +{ + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), NULL); + return self->password_actor; +} + +/** + * shell_keyring_prompt_get_confirm_actor: + * @self: the internal prompt + * + * Get the prompt password text actor + * + * Returns: (transfer none) (nullable): the password actor + */ +ClutterText * +shell_keyring_prompt_get_confirm_actor (ShellKeyringPrompt *self) +{ + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), NULL); + return self->confirm_actor; +} + +static guint +calculate_password_strength (const gchar *password) +{ + int upper, lower, digit, misc; + gdouble pwstrength; + int length, i; + + /* + * This code is based on the Master Password dialog in Firefox + * (pref-masterpass.js) + * Original code triple-licensed under the MPL, GPL, and LGPL + * so is license-compatible with this file + */ + + length = strlen (password); + + /* Always return 0 for empty passwords */ + if (length == 0) + return 0; + + upper = 0; + lower = 0; + digit = 0; + misc = 0; + + for (i = 0; i < length ; i++) + { + if (g_ascii_isdigit (password[i])) + digit++; + else if (g_ascii_islower (password[i])) + lower++; + else if (g_ascii_isupper (password[i])) + upper++; + else + misc++; + } + + if (length > 5) + length = 5; + if (digit > 3) + digit = 3; + if (upper > 3) + upper = 3; + if (misc > 3) + misc = 3; + + pwstrength = ((length * 1) - 2) + + (digit * 1) + + (misc * 1.5) + + (upper * 1); + + /* Always return 1+ for non-empty passwords */ + if (pwstrength < 1.0) + pwstrength = 1.0; + if (pwstrength > 10.0) + pwstrength = 10.0; + + return (guint)pwstrength; +} + +static void +on_password_changed (ClutterText *text, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (user_data); + const gchar *password; + + password = clutter_text_get_text (self->password_actor); + + self->password_strength = calculate_password_strength (password); + g_object_notify (G_OBJECT (self), "password-strength"); +} + +/** + * shell_keyring_prompt_set_password_actor: + * @self: the internal prompt + * @password_actor: (nullable): the password actor + * + * Set the prompt password text actor + */ +void +shell_keyring_prompt_set_password_actor (ShellKeyringPrompt *self, + ClutterText *password_actor) +{ + ClutterTextBuffer *buffer; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + g_return_if_fail (password_actor == NULL || CLUTTER_IS_TEXT (password_actor)); + + if (self->password_actor == password_actor) + return; + + if (password_actor) + { + buffer = shell_secure_text_buffer_new (); + clutter_text_set_buffer (password_actor, buffer); + g_object_unref (buffer); + + g_signal_connect (password_actor, "text-changed", G_CALLBACK (on_password_changed), self); + g_object_ref (password_actor); + } + if (self->password_actor) + { + g_signal_handlers_disconnect_by_func (self->password_actor, on_password_changed, self); + g_object_unref (self->password_actor); + } + + self->password_actor = password_actor; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PASSWORD_ACTOR]); +} + +/** + * shell_keyring_prompt_set_confirm_actor: + * @self: the internal prompt + * @confirm_actor: (nullable): the confirm password actor + * + * Set the prompt password confirmation text actor + */ +void +shell_keyring_prompt_set_confirm_actor (ShellKeyringPrompt *self, + ClutterText *confirm_actor) +{ + ClutterTextBuffer *buffer; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + g_return_if_fail (confirm_actor == NULL || CLUTTER_IS_TEXT (confirm_actor)); + + if (self->confirm_actor == confirm_actor) + return; + + if (confirm_actor) + { + buffer = shell_secure_text_buffer_new (); + clutter_text_set_buffer (confirm_actor, buffer); + g_object_unref (buffer); + + g_object_ref (confirm_actor); + } + if (self->confirm_actor) + g_object_unref (self->confirm_actor); + self->confirm_actor = confirm_actor; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIRM_ACTOR]); +} + +/** + * shell_keyring_prompt_complete: + * @self: the internal prompt + * + * Called by the implementation when the prompt completes. There are various + * checks done. %TRUE is returned if the prompt actually should complete. + * + * Returns: whether the prompt completed + */ +gboolean +shell_keyring_prompt_complete (ShellKeyringPrompt *self) +{ + GTask *res; + PromptingMode mode; + const gchar *password; + const gchar *confirm; + const gchar *env; + + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), FALSE); + g_return_val_if_fail (self->mode != PROMPTING_NONE, FALSE); + g_return_val_if_fail (self->task != NULL, FALSE); + + password = clutter_text_get_text (self->password_actor); + + if (self->mode == PROMPTING_FOR_PASSWORD) + { + /* Is it a new password? */ + if (self->password_new) + { + confirm = clutter_text_get_text (self->confirm_actor); + + /* Do the passwords match? */ + if (!g_str_equal (password, confirm)) + { + gcr_prompt_set_warning (GCR_PROMPT (self), _("Passwords do not match.")); + return FALSE; + } + + /* Don't allow blank passwords if in paranoid mode */ + env = g_getenv ("GNOME_KEYRING_PARANOID"); + if (env && *env) + { + gcr_prompt_set_warning (GCR_PROMPT (self), _("Password cannot be blank")); + return FALSE; + } + } + + self->password_strength = calculate_password_strength (password); + g_object_notify (G_OBJECT (self), "password-strength"); + } + + res = self->task; + mode = self->mode; + self->task = NULL; + self->mode = PROMPTING_NONE; + + if (mode == PROMPTING_FOR_CONFIRM) + g_task_return_int (res, (gssize)GCR_PROMPT_REPLY_CONTINUE); + else + g_task_return_pointer (res, (gpointer)password, NULL); + g_object_unref (res); + + return TRUE; +} + +/** + * shell_keyring_prompt_cancel: + * @self: the internal prompt + * + * Called by implementation when the prompt is cancelled. + */ +void +shell_keyring_prompt_cancel (ShellKeyringPrompt *self) +{ + GTask *res; + PromptingMode mode; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + + /* + * If cancelled while not prompting, we should just close the prompt, + * the user wants it to go away. + */ + if (self->mode == PROMPTING_NONE) + { + if (self->shown) + gcr_prompt_close (GCR_PROMPT (self)); + return; + } + + g_return_if_fail (self->task != NULL); + + res = self->task; + mode = self->mode; + self->task = NULL; + self->mode = PROMPTING_NONE; + + if (mode == PROMPTING_FOR_CONFIRM) + g_task_return_int (res, (gssize) GCR_PROMPT_REPLY_CANCEL); + else + g_task_return_pointer (res, NULL, NULL); + g_object_unref (res); +} diff --git a/src/shell-keyring-prompt.h b/src/shell-keyring-prompt.h new file mode 100644 index 0000000..fcacf4c --- /dev/null +++ b/src/shell-keyring-prompt.h @@ -0,0 +1,57 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* shell-keyring-prompt.c - prompt handler for gnome-keyring-daemon + + Copyright (C) 2011 Stefan Walter + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + + Author: Stef Walter <stef@thewalter.net> +*/ + +#ifndef __SHELL_KEYRING_PROMPT_H__ +#define __SHELL_KEYRING_PROMPT_H__ + +#include <glib-object.h> +#include <glib.h> + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +typedef struct _ShellKeyringPrompt ShellKeyringPrompt; + +#define SHELL_TYPE_KEYRING_PROMPT (shell_keyring_prompt_get_type ()) +G_DECLARE_FINAL_TYPE (ShellKeyringPrompt, shell_keyring_prompt, + SHELL, KEYRING_PROMPT, GObject) + +ShellKeyringPrompt * shell_keyring_prompt_new (void); + +ClutterText * shell_keyring_prompt_get_password_actor (ShellKeyringPrompt *self); + +void shell_keyring_prompt_set_password_actor (ShellKeyringPrompt *self, + ClutterText *password_actor); + +ClutterText * shell_keyring_prompt_get_confirm_actor (ShellKeyringPrompt *self); + +void shell_keyring_prompt_set_confirm_actor (ShellKeyringPrompt *self, + ClutterText *confirm_actor); + +gboolean shell_keyring_prompt_complete (ShellKeyringPrompt *self); + +void shell_keyring_prompt_cancel (ShellKeyringPrompt *self); + +G_END_DECLS + +#endif /* __SHELL_KEYRING_PROMPT_H__ */ diff --git a/src/shell-mount-operation.c b/src/shell-mount-operation.c new file mode 100644 index 0000000..8d43368 --- /dev/null +++ b/src/shell-mount-operation.c @@ -0,0 +1,189 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * 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/>. + * + * Author: Cosimo Cecchi <cosimoc@redhat.com> + * + */ + +#include "shell-mount-operation.h" + +/* This is a dummy class; we would like to be able to subclass the + * object from JS but we can't yet; the default GMountOperation impl + * automatically calls g_mount_operation_reply(UNHANDLED) after an idle, + * in interactive methods. We want to handle the reply ourselves + * instead, so we just override the default methods with empty ones, + * except for ask-password, as we don't want to handle that. + * + * Also, we need to workaround the fact that gjs doesn't support type + * annotations for signals yet (so we can't effectively forward e.g. + * the GPid array to JS). + * See https://bugzilla.gnome.org/show_bug.cgi?id=645978 + */ + +enum { + SHOW_PROCESSES_2, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +typedef struct _ShellMountOperationPrivate ShellMountOperationPrivate; + +struct _ShellMountOperation +{ + GMountOperation parent_instance; + + ShellMountOperationPrivate *priv; +}; + +struct _ShellMountOperationPrivate { + GArray *pids; + gchar **choices; + gchar *message; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellMountOperation, shell_mount_operation, G_TYPE_MOUNT_OPERATION); + +static void +shell_mount_operation_init (ShellMountOperation *self) +{ + self->priv = shell_mount_operation_get_instance_private (self); +} + +static void +shell_mount_operation_ask_password (GMountOperation *op, + const char *message, + const char *default_user, + const char *default_domain, + GAskPasswordFlags flags) +{ + /* do nothing */ +} + +static void +shell_mount_operation_ask_question (GMountOperation *op, + const char *message, + const char *choices[]) +{ + /* do nothing */ +} + +static void +shell_mount_operation_show_processes (GMountOperation *operation, + const gchar *message, + GArray *processes, + const gchar *choices[]) +{ + ShellMountOperation *self = SHELL_MOUNT_OPERATION (operation); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + g_free (self->priv->message); + g_strfreev (self->priv->choices); + + /* save the parameters */ + self->priv->pids = g_array_ref (processes); + self->priv->choices = g_strdupv ((gchar **) choices); + self->priv->message = g_strdup (message); + + g_signal_emit (self, signals[SHOW_PROCESSES_2], 0); +} + +static void +shell_mount_operation_finalize (GObject *obj) +{ + ShellMountOperation *self = SHELL_MOUNT_OPERATION (obj); + + g_strfreev (self->priv->choices); + g_free (self->priv->message); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + G_OBJECT_CLASS (shell_mount_operation_parent_class)->finalize (obj); +} + +static void +shell_mount_operation_class_init (ShellMountOperationClass *klass) +{ + GMountOperationClass *mclass; + GObjectClass *oclass; + + mclass = G_MOUNT_OPERATION_CLASS (klass); + mclass->show_processes = shell_mount_operation_show_processes; + mclass->ask_question = shell_mount_operation_ask_question; + mclass->ask_password = shell_mount_operation_ask_password; + + oclass = G_OBJECT_CLASS (klass); + oclass->finalize = shell_mount_operation_finalize; + + signals[SHOW_PROCESSES_2] = + g_signal_new ("show-processes-2", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +GMountOperation * +shell_mount_operation_new (void) +{ + return g_object_new (SHELL_TYPE_MOUNT_OPERATION, NULL); +} + +/** + * shell_mount_operation_get_show_processes_pids: + * @self: a #ShellMountOperation + * + * Returns: (transfer full) (element-type GPid): a #GArray + */ +GArray * +shell_mount_operation_get_show_processes_pids (ShellMountOperation *self) +{ + return g_array_ref (self->priv->pids); +} + +/** + * shell_mount_operation_get_show_processes_choices: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar ** +shell_mount_operation_get_show_processes_choices (ShellMountOperation *self) +{ + return g_strdupv (self->priv->choices); +} + +/** + * shell_mount_operation_get_show_processes_message: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar * +shell_mount_operation_get_show_processes_message (ShellMountOperation *self) +{ + return g_strdup (self->priv->message); +} diff --git a/src/shell-mount-operation.h b/src/shell-mount-operation.h new file mode 100644 index 0000000..c4019a5 --- /dev/null +++ b/src/shell-mount-operation.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * 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/>. + * + * Author: Cosimo Cecchi <cosimoc@redhat.com> + * + */ + +#ifndef __SHELL_MOUNT_OPERATION_H__ +#define __SHELL_MOUNT_OPERATION_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_MOUNT_OPERATION (shell_mount_operation_get_type ()) +G_DECLARE_FINAL_TYPE (ShellMountOperation, shell_mount_operation, + SHELL, MOUNT_OPERATION, GMountOperation) + +GMountOperation *shell_mount_operation_new (void); + +GArray * shell_mount_operation_get_show_processes_pids (ShellMountOperation *self); +gchar ** shell_mount_operation_get_show_processes_choices (ShellMountOperation *self); +gchar * shell_mount_operation_get_show_processes_message (ShellMountOperation *self); + +G_END_DECLS + +#endif /* __SHELL_MOUNT_OPERATION_H__ */ diff --git a/src/shell-network-agent.c b/src/shell-network-agent.c new file mode 100644 index 0000000..474394f --- /dev/null +++ b/src/shell-network-agent.c @@ -0,0 +1,899 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2011 Red Hat, Inc. + * 2011 Giovanni Campagna <scampa.giovanni@gmail.com> + * 2017 Lubomir Rintel <lkundrak@v3.sk> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "config.h" +#include <string.h> + +#include <libsecret/secret.h> + +#include "shell-network-agent.h" + +enum { + SIGNAL_NEW_REQUEST, + SIGNAL_CANCEL_REQUEST, + SIGNAL_LAST +}; + +static gint signals[SIGNAL_LAST]; + +typedef struct { + GCancellable * cancellable; + ShellNetworkAgent *self; + + gchar *request_id; + NMConnection *connection; + gchar *setting_name; + gchar **hints; + NMSecretAgentGetSecretsFlags flags; + NMSecretAgentOldGetSecretsFunc callback; + gpointer callback_data; + + GVariantDict *entries; + GVariantBuilder builder_vpn; +} ShellAgentRequest; + +struct _ShellNetworkAgentPrivate { + /* <gchar *request_id, ShellAgentRequest *request> */ + GHashTable *requests; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellNetworkAgent, shell_network_agent, NM_TYPE_SECRET_AGENT_OLD) + +static const SecretSchema network_agent_schema = { + "org.freedesktop.NetworkManager.Connection", + SECRET_SCHEMA_DONT_MATCH_NAME, + { + { SHELL_KEYRING_UUID_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { SHELL_KEYRING_SN_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { SHELL_KEYRING_SK_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { NULL, 0 }, + } +}; + +static void +shell_agent_request_free (gpointer data) +{ + ShellAgentRequest *request = data; + + g_cancellable_cancel (request->cancellable); + g_object_unref (request->cancellable); + g_object_unref (request->self); + g_object_unref (request->connection); + g_free (request->setting_name); + g_strfreev (request->hints); + g_clear_pointer (&request->entries, g_variant_dict_unref); + g_variant_builder_clear (&request->builder_vpn); + + g_free (request); +} + +static void +shell_agent_request_cancel (ShellAgentRequest *request) +{ + GError *error; + ShellNetworkAgent *self; + + self = request->self; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "Canceled by NetworkManager"); + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + NULL, error, request->callback_data); + + g_signal_emit (self, signals[SIGNAL_CANCEL_REQUEST], 0, request->request_id); + + g_hash_table_remove (self->priv->requests, request->request_id); + g_error_free (error); +} + +static void +shell_network_agent_init (ShellNetworkAgent *agent) +{ + ShellNetworkAgentPrivate *priv; + + priv = agent->priv = shell_network_agent_get_instance_private (agent); + priv->requests = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, shell_agent_request_free); +} + +static void +shell_network_agent_finalize (GObject *object) +{ + ShellNetworkAgentPrivate *priv = SHELL_NETWORK_AGENT (object)->priv; + GError *error; + GHashTableIter iter; + gpointer key; + gpointer value; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "The secret agent is going away"); + + g_hash_table_iter_init (&iter, priv->requests); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellAgentRequest *request = value; + + request->callback (NM_SECRET_AGENT_OLD (object), + request->connection, + NULL, error, + request->callback_data); + } + + g_hash_table_destroy (priv->requests); + g_error_free (error); + + G_OBJECT_CLASS (shell_network_agent_parent_class)->finalize (object); +} + +static void +request_secrets_from_ui (ShellAgentRequest *request) +{ + g_signal_emit (request->self, signals[SIGNAL_NEW_REQUEST], 0, + request->request_id, + request->connection, + request->setting_name, + request->hints, + (int)request->flags); +} + +static void +check_always_ask_cb (NMSetting *setting, + const gchar *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + gboolean *always_ask = user_data; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + if (flags & NM_SETTING_PARAM_SECRET) + { + if (nm_setting_get_secret_flags (setting, key, &secret_flags, NULL)) + { + if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) + *always_ask = TRUE; + } + } +} + +static gboolean +has_always_ask (NMSetting *setting) +{ + gboolean always_ask = FALSE; + + nm_setting_enumerate_values (setting, check_always_ask_cb, &always_ask); + return always_ask; +} + +static gboolean +is_connection_always_ask (NMConnection *connection) +{ + NMSettingConnection *s_con; + const gchar *ctype; + NMSetting *setting; + + /* For the given connection type, check if the secrets for that connection + * are always-ask or not. + */ + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + ctype = nm_setting_connection_get_connection_type (s_con); + + setting = nm_connection_get_setting_by_name (connection, ctype); + g_return_val_if_fail (setting != NULL, FALSE); + + if (has_always_ask (setting)) + return TRUE; + + /* Try type-specific settings too; be a bit paranoid and only consider + * secrets from settings relevant to the connection type. + */ + if (NM_IS_SETTING_WIRELESS (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_WIRELESS_SECURITY); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + else if (NM_IS_SETTING_WIRED (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_PPPOE); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + + return FALSE; +} + +static void +get_secrets_keyring_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + ShellAgentRequest *closure; + ShellNetworkAgent *self; + ShellNetworkAgentPrivate *priv; + GError *secret_error = NULL; + GError *error = NULL; + GList *items; + GList *l; + gboolean secrets_found = FALSE; + GVariantBuilder builder_setting, builder_connection; + g_autoptr (GVariant) setting = NULL; + + items = secret_service_search_finish (NULL, result, &secret_error); + + if (g_error_matches (secret_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (secret_error); + return; + } + + closure = user_data; + self = closure->self; + priv = self->priv; + + if (secret_error != NULL) + { + g_set_error (&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Internal error while retrieving secrets from the keyring (%s)", secret_error->message); + g_error_free (secret_error); + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, NULL, error, closure->callback_data); + + goto out; + } + + g_variant_builder_init (&builder_setting, NM_VARIANT_TYPE_SETTING); + + for (l = items; l; l = g_list_next (l)) + { + SecretItem *item = l->data; + GHashTable *attributes; + GHashTableIter iter; + const gchar *name, *attribute; + SecretValue *secret = secret_item_get_secret (item); + + /* This can happen if the user denied a request to unlock */ + if (secret == NULL) + continue; + + attributes = secret_item_get_attributes (item); + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, (gpointer *)&name, (gpointer *)&attribute)) + { + if (g_strcmp0 (name, SHELL_KEYRING_SK_TAG) == 0) + { + g_variant_builder_add (&builder_setting, "{sv}", attribute, + g_variant_new_string (secret_value_get (secret, NULL))); + + secrets_found = TRUE; + + break; + } + } + + g_hash_table_unref (attributes); + secret_value_unref (secret); + } + + g_list_free_full (items, g_object_unref); + setting = g_variant_ref_sink (g_variant_builder_end (&builder_setting)); + + /* All VPN requests get sent to the VPN's auth dialog, since it knows better + * than the agent about what secrets are required. Otherwise, if no secrets + * were found and interaction is allowed the ask for some secrets, because + * NetworkManager will fail the connection if not secrets are returned + * instead of asking again with REQUEST_NEW. + */ + if (strcmp(closure->setting_name, NM_SETTING_VPN_SETTING_NAME) == 0 || + (!secrets_found && (closure->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION))) + { + nm_connection_update_secrets (closure->connection, closure->setting_name, + setting, NULL); + + closure->entries = g_variant_dict_new (setting); + request_secrets_from_ui (closure); + return; + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + closure->setting_name, setting); + + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, + g_variant_builder_end (&builder_connection), NULL, + closure->callback_data); + + out: + g_hash_table_remove (priv->requests, closure->request_id); + g_clear_error (&error); +} + +static void +shell_network_agent_get_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + const gchar *setting_name, + const gchar **hints, + NMSecretAgentGetSecretsFlags flags, + NMSecretAgentOldGetSecretsFunc callback, + gpointer callback_data) +{ + ShellNetworkAgent *self = SHELL_NETWORK_AGENT (agent); + ShellAgentRequest *request; + GHashTable *attributes; + char *request_id; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + if ((request = g_hash_table_lookup (self->priv->requests, request_id)) != NULL) + { + /* We already have a request pending for this (connection, setting) + * Cancel it before starting the new one. + * This will also free the request structure and associated resources. + */ + shell_agent_request_cancel (request); + } + + request = g_new0 (ShellAgentRequest, 1); + request->self = g_object_ref (self); + request->cancellable = g_cancellable_new (); + request->connection = g_object_ref (connection); + request->setting_name = g_strdup (setting_name); + request->hints = g_strdupv ((gchar **)hints); + request->flags = flags; + request->callback = callback; + request->callback_data = callback_data; + + request->request_id = request_id; + g_hash_table_replace (self->priv->requests, request->request_id, request); + + g_variant_builder_init (&request->builder_vpn, G_VARIANT_TYPE ("a{ss}")); + + if ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) || + ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) + && is_connection_always_ask (request->connection))) + { + request->entries = g_variant_dict_new (NULL); + request_secrets_from_ui (request); + return; + } + + attributes = secret_attributes_build (&network_agent_schema, + SHELL_KEYRING_UUID_TAG, nm_connection_get_uuid (connection), + SHELL_KEYRING_SN_TAG, setting_name, + NULL); + + secret_service_search (NULL, &network_agent_schema, attributes, + SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS, + request->cancellable, get_secrets_keyring_cb, request); + + g_hash_table_unref (attributes); +} + +void +shell_network_agent_add_vpn_secret (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_builder_add (&request->builder_vpn, "{ss}", setting_key, setting_value); +} + +void +shell_network_agent_set_password (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_dict_insert (request->entries, setting_key, "s", setting_value); +} + +void +shell_network_agent_respond (ShellNetworkAgent *self, + gchar *request_id, + ShellNetworkAgentResponse response) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + GVariantBuilder builder_connection; + GVariant *vpn_secrets, *setting; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + if (response == SHELL_NETWORK_AGENT_USER_CANCELED) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_USER_CANCELED, + "Network dialog was canceled by the user"); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + if (response == SHELL_NETWORK_AGENT_INTERNAL_ERROR) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "An internal error occurred while processing the request."); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + /* response == SHELL_NETWORK_AGENT_CONFIRMED */ + + /* VPN secrets are stored as a hash of secrets in a single setting */ + vpn_secrets = g_variant_builder_end (&request->builder_vpn); + if (g_variant_n_children (vpn_secrets)) + g_variant_dict_insert_value (request->entries, NM_SETTING_VPN_SECRETS, vpn_secrets); + else + g_variant_unref (vpn_secrets); + + setting = g_variant_dict_end (request->entries); + + /* Save any updated secrets */ + if ((request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) || + (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)) + { + NMConnection *dup = nm_simple_connection_new_clone (request->connection); + + nm_connection_update_secrets (dup, request->setting_name, setting, NULL); + nm_secret_agent_old_save_secrets (NM_SECRET_AGENT_OLD (self), dup, NULL, NULL); + g_object_unref (dup); + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + request->setting_name, setting); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + g_variant_builder_end (&builder_connection), NULL, + request->callback_data); + + g_hash_table_remove (priv->requests, request_id); +} + +static void +search_vpn_plugin (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + NMVpnPluginInfo *info = NULL; + char *service = task_data; + + info = nm_vpn_plugin_info_new_search_file (NULL, service); + + if (info) + { + g_task_return_pointer (task, info, g_object_unref); + } + else + { + g_task_return_new_error (task, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No plugin for %s", service); + } +} + +void +shell_network_agent_search_vpn_plugin (ShellNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + g_return_if_fail (service != NULL); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, shell_network_agent_search_vpn_plugin); + g_task_set_task_data (task, g_strdup (service), g_free); + + g_task_run_in_thread (task, search_vpn_plugin); +} + +/** + * shell_network_agent_search_vpn_plugin_finish: + * + * Returns: (nullable) (transfer full): The found plugin or %NULL + */ +NMVpnPluginInfo * +shell_network_agent_search_vpn_plugin_finish (ShellNetworkAgent *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_NETWORK_AGENT (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +shell_network_agent_cancel_get_secrets (NMSecretAgentOld *agent, + const gchar *connection_path, + const gchar *setting_name) +{ + ShellNetworkAgent *self = SHELL_NETWORK_AGENT (agent); + ShellNetworkAgentPrivate *priv = self->priv; + gchar *request_id; + ShellAgentRequest *request; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + request = g_hash_table_lookup (priv->requests, request_id); + g_free (request_id); + + if (!request) + { + /* We've already sent the result, but the caller cancelled the + * operation before receiving that result. + */ + return; + } + + shell_agent_request_cancel (request); +} + +/************************* saving of secrets ****************************************/ + +static GHashTable * +create_keyring_add_attr_list (NMConnection *connection, + const gchar *connection_uuid, + const gchar *connection_id, + const gchar *setting_name, + const gchar *setting_key, + gchar **out_display_name) +{ + NMSettingConnection *s_con; + + if (connection) + { + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_return_val_if_fail (s_con != NULL, NULL); + connection_uuid = nm_setting_connection_get_uuid (s_con); + connection_id = nm_setting_connection_get_id (s_con); + } + + g_return_val_if_fail (connection_uuid != NULL, NULL); + g_return_val_if_fail (connection_id != NULL, NULL); + g_return_val_if_fail (setting_name != NULL, NULL); + g_return_val_if_fail (setting_key != NULL, NULL); + + if (out_display_name) + { + *out_display_name = g_strdup_printf ("Network secret for %s/%s/%s", + connection_id, + setting_name, + setting_key); + } + + return secret_attributes_build (&network_agent_schema, + SHELL_KEYRING_UUID_TAG, connection_uuid, + SHELL_KEYRING_SN_TAG, setting_name, + SHELL_KEYRING_SK_TAG, setting_key, + NULL); +} + +typedef struct +{ + /* Sort of ref count, indicates the number of secrets we still need to save */ + gint n_secrets; + + NMSecretAgentOld *self; + NMConnection *connection; + gpointer callback; + gpointer callback_data; +} KeyringRequest; + +static void +keyring_request_free (KeyringRequest *r) +{ + g_object_unref (r->self); + g_object_unref (r->connection); + + g_free (r); +} + +static void +save_secret_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *call = user_data; + NMSecretAgentOldSaveSecretsFunc callback = call->callback; + + call->n_secrets--; + + if (call->n_secrets == 0) + { + if (callback) + callback (call->self, call->connection, NULL, call->callback_data); + keyring_request_free (call); + } +} + +static void +save_one_secret (KeyringRequest *r, + NMSetting *setting, + const gchar *key, + const gchar *secret, + const gchar *display_name) +{ + GHashTable *attrs; + gchar *alt_display_name = NULL; + const gchar *setting_name; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + /* Only save agent-owned secrets (not system-owned or always-ask) */ + nm_setting_get_secret_flags (setting, key, &secret_flags, NULL); + if (secret_flags != NM_SETTING_SECRET_FLAG_AGENT_OWNED) + return; + + setting_name = nm_setting_get_name (setting); + g_assert (setting_name); + + attrs = create_keyring_add_attr_list (r->connection, NULL, NULL, + setting_name, + key, + display_name ? NULL : &alt_display_name); + g_assert (attrs); + r->n_secrets++; + secret_password_storev (&network_agent_schema, attrs, SECRET_COLLECTION_DEFAULT, + display_name ? display_name : alt_display_name, + secret, NULL, save_secret_cb, r); + + g_hash_table_unref (attrs); + g_free (alt_display_name); +} + +static void +vpn_secret_iter_cb (const gchar *key, + const gchar *secret, + gpointer user_data) +{ + KeyringRequest *r = user_data; + NMSetting *setting; + const gchar *service_name, *id; + gchar *display_name; + + if (secret && strlen (secret)) + { + setting = nm_connection_get_setting (r->connection, NM_TYPE_SETTING_VPN); + g_assert (setting); + service_name = nm_setting_vpn_get_service_type (NM_SETTING_VPN (setting)); + g_assert (service_name); + id = nm_connection_get_id (r->connection); + g_assert (id); + + display_name = g_strdup_printf ("VPN %s secret for %s/%s/" NM_SETTING_VPN_SETTING_NAME, + key, + id, + service_name); + save_one_secret (r, setting, key, secret, display_name); + g_free (display_name); + } +} + +static void +write_one_secret_to_keyring (NMSetting *setting, + const gchar *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + KeyringRequest *r = user_data; + const gchar *secret; + + /* Non-secrets obviously don't get saved in the keyring */ + if (!(flags & NM_SETTING_PARAM_SECRET)) + return; + + if (NM_IS_SETTING_VPN (setting) && (g_strcmp0 (key, NM_SETTING_VPN_SECRETS) == 0)) + { + /* Process VPN secrets specially since it's a hash of secrets, not just one */ + nm_setting_vpn_foreach_secret (NM_SETTING_VPN (setting), + vpn_secret_iter_cb, + r); + } + else + { + if (!G_VALUE_HOLDS_STRING (value)) + return; + + secret = g_value_get_string (value); + if (secret && strlen (secret)) + save_one_secret (r, setting, key, secret, NULL); + } +} + +static void +save_delete_cb (NMSecretAgentOld *agent, + NMConnection *connection, + GError *error, + gpointer user_data) +{ + KeyringRequest *r = user_data; + + /* Ignore errors; now save all new secrets */ + nm_connection_for_each_setting_value (connection, write_one_secret_to_keyring, r); + + /* If no secrets actually got saved there may be nothing to do so + * try to complete the request here. If there were secrets to save the + * request will get completed when those keyring calls return (at the next + * mainloop iteration). + */ + if (r->n_secrets == 0) + { + if (r->callback) + ((NMSecretAgentOldSaveSecretsFunc)r->callback) (agent, connection, NULL, r->callback_data); + keyring_request_free (r); + } +} + +static void +shell_network_agent_save_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + NMSecretAgentOldSaveSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + + r = g_new (KeyringRequest, 1); + r->n_secrets = 0; + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + /* First delete any existing items in the keyring */ + nm_secret_agent_old_delete_secrets (agent, connection, save_delete_cb, r); +} + +static void +delete_items_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *r = user_data; + GError *secret_error = NULL; + GError *error = NULL; + NMSecretAgentOldDeleteSecretsFunc callback = r->callback; + + secret_password_clear_finish (result, &secret_error); + if (secret_error != NULL) + { + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "The request could not be completed. Keyring result: %s", + secret_error->message); + g_error_free (secret_error); + } + + callback (r->self, r->connection, error, r->callback_data); + g_clear_error (&error); + keyring_request_free (r); +} + +static void +shell_network_agent_delete_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const gchar *connection_path, + NMSecretAgentOldDeleteSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + NMSettingConnection *s_con; + const gchar *uuid; + + r = g_new (KeyringRequest, 1); + r->n_secrets = 0; /* ignored by delete secrets calls */ + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + uuid = nm_setting_connection_get_uuid (s_con); + g_assert (uuid); + + secret_password_clear (&network_agent_schema, NULL, delete_items_cb, r, + SHELL_KEYRING_UUID_TAG, uuid, + NULL); +} + +void +shell_network_agent_class_init (ShellNetworkAgentClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + NMSecretAgentOldClass *agent_class = NM_SECRET_AGENT_OLD_CLASS (klass); + + gobject_class->finalize = shell_network_agent_finalize; + + agent_class->get_secrets = shell_network_agent_get_secrets; + agent_class->cancel_get_secrets = shell_network_agent_cancel_get_secrets; + agent_class->save_secrets = shell_network_agent_save_secrets; + agent_class->delete_secrets = shell_network_agent_delete_secrets; + + signals[SIGNAL_NEW_REQUEST] = g_signal_new ("new-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, /* return */ + 5, /* n_params */ + G_TYPE_STRING, + NM_TYPE_CONNECTION, + G_TYPE_STRING, + G_TYPE_STRV, + G_TYPE_INT); + + signals[SIGNAL_CANCEL_REQUEST] = g_signal_new ("cancel-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, + 1, /* n_params */ + G_TYPE_STRING); +} diff --git a/src/shell-network-agent.h b/src/shell-network-agent.h new file mode 100644 index 0000000..0ffde02 --- /dev/null +++ b/src/shell-network-agent.h @@ -0,0 +1,73 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_NETWORK_AGENT_H__ +#define __SHELL_NETWORK_AGENT_H__ + +#include <glib-object.h> +#include <glib.h> +#include <NetworkManager.h> +#include <nm-secret-agent-old.h> + +G_BEGIN_DECLS + +typedef enum { + SHELL_NETWORK_AGENT_CONFIRMED, + SHELL_NETWORK_AGENT_USER_CANCELED, + SHELL_NETWORK_AGENT_INTERNAL_ERROR +} ShellNetworkAgentResponse; + +typedef struct _ShellNetworkAgent ShellNetworkAgent; +typedef struct _ShellNetworkAgentClass ShellNetworkAgentClass; +typedef struct _ShellNetworkAgentPrivate ShellNetworkAgentPrivate; + +#define SHELL_TYPE_NETWORK_AGENT (shell_network_agent_get_type ()) +#define SHELL_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgent)) +#define SHELL_IS_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_NETWORK_AGENT)) +#define SHELL_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgentClass)) +#define SHELL_IS_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_NETWORK_AGENT)) +#define SHELL_NETWORK_AGENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgentClass)) + +struct _ShellNetworkAgent +{ + /*< private >*/ + NMSecretAgentOld parent_instance; + + ShellNetworkAgentPrivate *priv; +}; + +struct _ShellNetworkAgentClass +{ + /*< private >*/ + NMSecretAgentOldClass parent_class; +}; + +/* used by SHELL_TYPE_NETWORK_AGENT */ +GType shell_network_agent_get_type (void); + +void shell_network_agent_add_vpn_secret (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void shell_network_agent_set_password (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void shell_network_agent_respond (ShellNetworkAgent *self, + gchar *request_id, + ShellNetworkAgentResponse response); + +void shell_network_agent_search_vpn_plugin (ShellNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data); +NMVpnPluginInfo *shell_network_agent_search_vpn_plugin_finish (ShellNetworkAgent *self, + GAsyncResult *result, + GError **error); + +/* If these are kept in sync with nm-applet, secrets will be shared */ +#define SHELL_KEYRING_UUID_TAG "connection-uuid" +#define SHELL_KEYRING_SN_TAG "setting-name" +#define SHELL_KEYRING_SK_TAG "setting-key" + +G_END_DECLS + +#endif /* __SHELL_NETWORK_AGENT_H__ */ diff --git a/src/shell-perf-helper.c b/src/shell-perf-helper.c new file mode 100644 index 0000000..a50376e --- /dev/null +++ b/src/shell-perf-helper.c @@ -0,0 +1,376 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* gnome-shell-perf-helper: a program to create windows for performance tests + * + * Running performance tests with whatever windows a user has open results + * in unreliable results, so instead we hide all other windows and talk + * to this program over D-Bus to create just the windows we want. + */ + +#include "config.h" + +#include <math.h> + +#include <gtk/gtk.h> + +#define BUS_NAME "org.gnome.Shell.PerfHelper" + +static void destroy_windows (void); +static void finish_wait_windows (void); +static void check_finish_wait_windows (void); + +static const gchar introspection_xml[] = + "<node>" + " <interface name='org.gnome.Shell.PerfHelper'>" + " <method name='Exit'/>" + " <method name='CreateWindow'>" + " <arg type='i' name='width' direction='in'/>" + " <arg type='i' name='height' direction='in'/>" + " <arg type='b' name='alpha' direction='in'/>" + " <arg type='b' name='maximized' direction='in'/>" + " <arg type='b' name='redraws' direction='in'/>" + " </method>" + " <method name='WaitWindows'/>" + " <method name='DestroyWindows'/>" + " </interface>" + "</node>"; + +typedef struct { + GtkWidget *window; + int width; + int height; + + guint alpha : 1; + guint maximized : 1; + guint redraws : 1; + guint mapped : 1; + guint exposed : 1; + guint pending : 1; + + gint64 start_time; + gint64 time; +} WindowInfo; + +static int opt_idle_timeout = 30; + +static GOptionEntry opt_entries[] = + { + { "idle-timeout", 'r', 0, G_OPTION_ARG_INT, &opt_idle_timeout, "Exit after N seconds", "N" }, + { NULL } + }; + +static guint timeout_id; +static GList *our_windows; +static GList *wait_windows_invocations; + +static gboolean +on_timeout (gpointer data) +{ + timeout_id = 0; + + destroy_windows (); + gtk_main_quit (); + + return FALSE; +} + +static void +establish_timeout (void) +{ + g_clear_handle_id (&timeout_id, g_source_remove); + + timeout_id = g_timeout_add (opt_idle_timeout * 1000, on_timeout, NULL); + g_source_set_name_by_id (timeout_id, "[gnome-shell] on_timeout"); +} + +static void +destroy_windows (void) +{ + GList *l; + + for (l = our_windows; l; l = l->next) + { + WindowInfo *info = l->data; + gtk_widget_destroy (info->window); + g_free (info); + } + + g_list_free (our_windows); + our_windows = NULL; + + check_finish_wait_windows (); +} + +static gboolean +on_window_map_event (GtkWidget *window, + GdkEventAny *event, + WindowInfo *info) +{ + info->mapped = TRUE; + + return FALSE; +} + +static gboolean +on_child_draw (GtkWidget *window, + cairo_t *cr, + WindowInfo *info) +{ + cairo_rectangle_int_t allocation; + double x_offset, y_offset; + + gtk_widget_get_allocation (window, &allocation); + + /* We draw an arbitrary pattern of red lines near the border of the + * window to make it more clear than empty windows if something + * is drastrically wrong. + */ + + cairo_save (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + + if (info->alpha) + cairo_set_source_rgba (cr, 1, 1, 1, 0.5); + else + cairo_set_source_rgb (cr, 1, 1, 1); + + cairo_paint (cr); + cairo_restore (cr); + + if (info->redraws) + { + double position = (info->time - info->start_time) / 1000000.; + x_offset = 20 * cos (2 * M_PI * position); + y_offset = 20 * sin (2 * M_PI * position); + } + else + { + x_offset = y_offset = 0; + } + + cairo_set_source_rgb (cr, 1, 0, 0); + cairo_set_line_width (cr, 10); + cairo_move_to (cr, 0, 40 + y_offset); + cairo_line_to (cr, allocation.width, 40 + y_offset); + cairo_move_to (cr, 0, allocation.height - 40 + y_offset); + cairo_line_to (cr, allocation.width, allocation.height - 40 + y_offset); + cairo_move_to (cr, 40 + x_offset, 0); + cairo_line_to (cr, 40 + x_offset, allocation.height); + cairo_move_to (cr, allocation.width - 40 + x_offset, 0); + cairo_line_to (cr, allocation.width - 40 + x_offset, allocation.height); + cairo_stroke (cr); + + info->exposed = TRUE; + + if (info->exposed && info->mapped && info->pending) + { + info->pending = FALSE; + check_finish_wait_windows (); + } + + return FALSE; +} + +static gboolean +tick_callback (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + WindowInfo *info = user_data; + + if (info->start_time < 0) + info->start_time = info->time = gdk_frame_clock_get_frame_time (frame_clock); + else + info->time = gdk_frame_clock_get_frame_time (frame_clock); + + gtk_widget_queue_draw (widget); + + return TRUE; +} + +static void +create_window (int width, + int height, + gboolean alpha, + gboolean maximized, + gboolean redraws) +{ + WindowInfo *info; + GtkWidget *child; + + info = g_new0 (WindowInfo, 1); + info->width = width; + info->height = height; + info->alpha = alpha; + info->maximized = maximized; + info->redraws = redraws; + info->window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + if (alpha) + gtk_widget_set_visual (info->window, gdk_screen_get_rgba_visual (gdk_screen_get_default ())); + if (maximized) + gtk_window_maximize (GTK_WINDOW (info->window)); + info->pending = TRUE; + info->start_time = -1; + + child = g_object_new (GTK_TYPE_BOX, "visible", TRUE, "app-paintable", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (info->window), child); + + gtk_widget_set_size_request (info->window, width, height); + gtk_widget_set_app_paintable (info->window, TRUE); + g_signal_connect (info->window, "map-event", G_CALLBACK (on_window_map_event), info); + g_signal_connect (child, "draw", G_CALLBACK (on_child_draw), info); + gtk_widget_show (info->window); + + if (info->redraws) + gtk_widget_add_tick_callback (info->window, tick_callback, + info, NULL); + + our_windows = g_list_prepend (our_windows, info); +} + +static void +finish_wait_windows (void) +{ + GList *l; + + for (l = wait_windows_invocations; l; l = l->next) + g_dbus_method_invocation_return_value (l->data, NULL); + + g_list_free (wait_windows_invocations); + wait_windows_invocations = NULL; +} + +static void +check_finish_wait_windows (void) +{ + GList *l; + gboolean have_pending = FALSE; + + for (l = our_windows; l; l = l->next) + { + WindowInfo *info = l->data; + if (info->pending) + have_pending = TRUE; + } + + if (!have_pending) + finish_wait_windows (); +} + +static void +handle_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + /* Push off the idle timeout */ + establish_timeout (); + + if (g_strcmp0 (method_name, "Exit") == 0) + { + destroy_windows (); + + g_dbus_method_invocation_return_value (invocation, NULL); + g_dbus_connection_flush_sync (connection, NULL, NULL); + + gtk_main_quit (); + } + else if (g_strcmp0 (method_name, "CreateWindow") == 0) + { + int width, height; + gboolean alpha, maximized, redraws; + + g_variant_get (parameters, "(iibbb)", &width, &height, &alpha, &maximized, &redraws); + + create_window (width, height, alpha, maximized, redraws); + g_dbus_method_invocation_return_value (invocation, NULL); + } + else if (g_strcmp0 (method_name, "WaitWindows") == 0) + { + wait_windows_invocations = g_list_prepend (wait_windows_invocations, invocation); + check_finish_wait_windows (); + } + else if (g_strcmp0 (method_name, "DestroyWindows") == 0) + { + destroy_windows (); + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, + NULL +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + + g_dbus_connection_register_object (connection, + "/org/gnome/Shell/PerfHelper", + introspection_data->interfaces[0], + &interface_vtable, + NULL, /* user_data */ + NULL, /* user_data_free_func */ + NULL); /* GError** */ +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + destroy_windows (); + gtk_main_quit (); +} + +int +main (int argc, char **argv) +{ + GOptionContext *context; + GError *error = NULL; + + /* Since we depend on this, avoid the possibility of lt-gnome-shell-perf-helper */ + g_set_prgname ("gnome-shell-perf-helper"); + + context = g_option_context_new (" - server to create windows for performance testing"); + g_option_context_add_main_entries (context, opt_entries, NULL); + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_print ("option parsing failed: %s\n", error->message); + return 1; + } + + g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + establish_timeout (); + + gtk_main (); + + return 0; +} diff --git a/src/shell-perf-log.c b/src/shell-perf-log.c new file mode 100644 index 0000000..3bd5228 --- /dev/null +++ b/src/shell-perf-log.c @@ -0,0 +1,959 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <string.h> + +#include "shell-perf-log.h" + +typedef struct _ShellPerfEvent ShellPerfEvent; +typedef struct _ShellPerfStatistic ShellPerfStatistic; +typedef struct _ShellPerfStatisticsClosure ShellPerfStatisticsClosure; +typedef union _ShellPerfStatisticValue ShellPerfStatisticValue; +typedef struct _ShellPerfBlock ShellPerfBlock; + +/** + * SECTION:shell-perf-log + * @short_description: Event recorder for performance measurement + * + * ShellPerfLog provides a way for different parts of the code to + * record information for subsequent analysis and interactive + * exploration. Events exist of a timestamp, an event ID, and + * arguments to the event. + * + * Emphasis is placed on storing recorded events in a compact + * fashion so log recording disturbs the execution of the program + * as little as possible, however events should not be recorded + * at too fine a granularity - an event that is recorded once + * per frame or once per user action is appropriate, an event that + * occurs many times per frame is not. + * + * Arguments are identified by a D-Bus style signature; at the moment + * only a limited number of event signatures are supported to + * simplify the code. + */ +struct _ShellPerfLog +{ + GObject parent; + + GPtrArray *events; + GHashTable *events_by_name; + GPtrArray *statistics; + GHashTable *statistics_by_name; + + GPtrArray *statistics_closures; + + GQueue *blocks; + + gint64 start_time; + gint64 last_time; + + guint statistics_timeout_id; + + guint enabled : 1; +}; + +struct _ShellPerfEvent +{ + guint16 id; + char *name; + char *description; + char *signature; +}; + +union _ShellPerfStatisticValue +{ + int i; + gint64 x; +}; + +struct _ShellPerfStatistic +{ + ShellPerfEvent *event; + + ShellPerfStatisticValue current_value; + ShellPerfStatisticValue last_value; + + guint initialized : 1; + guint recorded : 1; +}; + +struct _ShellPerfStatisticsClosure +{ + ShellPerfStatisticsCallback callback; + gpointer user_data; + GDestroyNotify notify; +}; + +/* The events in the log are stored in a linked list of fixed size + * blocks. + * + * Note that the power-of-two nature of BLOCK_SIZE here is superficial + * since the allocated block has the 'bytes' field and malloc + * overhead. The current value is well below the size that will + * typically be independently mmapped by the malloc implementation so + * it doesn't matter. If we switched to mmapping blocks manually + * (perhaps to avoid polluting malloc statistics), we'd want to use a + * different value of BLOCK_SIZE. + */ +#define BLOCK_SIZE 8192 + +struct _ShellPerfBlock +{ + guint32 bytes; + guchar buffer[BLOCK_SIZE]; +}; + +/* Number of milliseconds between periodic statistics collection when + * events are enabled. Statistics collection can also be explicitly + * triggered. + */ +#define STATISTIC_COLLECTION_INTERVAL_MS 5000 + +/* Builtin events */ +enum { + EVENT_SET_TIME, + EVENT_STATISTICS_COLLECTED +}; + +G_DEFINE_TYPE(ShellPerfLog, shell_perf_log, G_TYPE_OBJECT); + +static gint64 +get_time (void) +{ + return g_get_monotonic_time (); +} + +static void +shell_perf_log_init (ShellPerfLog *perf_log) +{ + perf_log->events = g_ptr_array_new (); + perf_log->events_by_name = g_hash_table_new (g_str_hash, g_str_equal); + perf_log->statistics = g_ptr_array_new (); + perf_log->statistics_by_name = g_hash_table_new (g_str_hash, g_str_equal); + perf_log->statistics_closures = g_ptr_array_new (); + perf_log->blocks = g_queue_new (); + + /* This event is used when timestamp deltas are greater than + * fits in a gint32. 0xffffffff microseconds is about 70 minutes, so this + * is not going to happen in normal usage. It might happen if performance + * logging is enabled some time after starting the shell */ + shell_perf_log_define_event (perf_log, "perf.setTime", "", "x"); + g_assert (perf_log->events->len == EVENT_SET_TIME + 1); + + /* The purpose of this event is to allow us to optimize out storing + * statistics that haven't changed. We want to mark every time we + * collect statistics even if we don't record any individual + * statistics so that we can distinguish sudden changes from gradual changes. + * + * The argument is the number of microseconds that statistics collection + * took; we record that since statistics collection could start taking + * significant time if we do things like grub around in /proc/ + */ + shell_perf_log_define_event (perf_log, "perf.statisticsCollected", + "Finished collecting statistics", + "x"); + g_assert (perf_log->events->len == EVENT_STATISTICS_COLLECTED + 1); + + perf_log->start_time = perf_log->last_time = get_time(); +} + +static void +shell_perf_log_class_init (ShellPerfLogClass *class) +{ +} + +/** + * shell_perf_log_get_default: + * + * Gets the global singleton performance log. This is initially disabled + * and must be explicitly enabled with shell_perf_log_set_enabled(). + * + * Return value: (transfer none): the global singleton performance log + */ +ShellPerfLog * +shell_perf_log_get_default (void) +{ + static ShellPerfLog *perf_log; + + if (perf_log == NULL) + perf_log = g_object_new (SHELL_TYPE_PERF_LOG, NULL); + + return perf_log; +} + +static gboolean +statistics_timeout (gpointer data) +{ + ShellPerfLog *perf_log = data; + + shell_perf_log_collect_statistics (perf_log); + + return TRUE; +} + +/** + * shell_perf_log_set_enabled: + * @perf_log: a #ShellPerfLog + * @enabled: whether to record events + * + * Sets whether events are currently being recorded. + */ +void +shell_perf_log_set_enabled (ShellPerfLog *perf_log, + gboolean enabled) +{ + enabled = enabled != FALSE; + + if (enabled != perf_log->enabled) + { + perf_log->enabled = enabled; + + if (enabled) + { + perf_log->statistics_timeout_id = g_timeout_add (STATISTIC_COLLECTION_INTERVAL_MS, + statistics_timeout, + perf_log); + g_source_set_name_by_id (perf_log->statistics_timeout_id, "[gnome-shell] statistics_timeout"); + } + else + { + g_clear_handle_id (&perf_log->statistics_timeout_id, g_source_remove); + } + } +} + +static ShellPerfEvent * +define_event (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + ShellPerfEvent *event; + + if (strcmp (signature, "") != 0 && + strcmp (signature, "s") != 0 && + strcmp (signature, "i") != 0 && + strcmp (signature, "x") != 0) + { + g_warning ("Only supported event signatures are '', 's', 'i', and 'x'\n"); + return NULL; + } + + if (perf_log->events->len == 65536) + { + g_warning ("Maximum number of events defined\n"); + return NULL; + } + + /* We could do stricter validation, but this will break our JSON dumps */ + if (strchr (name, '"') != NULL) + { + g_warning ("Event names can't include '\"'"); + return NULL; + } + + if (g_hash_table_lookup (perf_log->events_by_name, name) != NULL) + { + g_warning ("Duplicate event event for '%s'\n", name); + return NULL; + } + + event = g_new (ShellPerfEvent, 1); + + event->id = perf_log->events->len; + event->name = g_strdup (name); + event->signature = g_strdup (signature); + event->description = g_strdup (description); + + g_ptr_array_add (perf_log->events, event); + g_hash_table_insert (perf_log->events_by_name, event->name, event); + + return event; +} + +/** + * shell_perf_log_define_event: + * @perf_log: a #ShellPerfLog + * @name: name of the event. This should of the form + * '<namespace>.<specific eventf'>, for example + * 'clutter.stagePaintDone'. + * @description: human readable description of the event. + * @signature: signature defining the arguments that event takes. + * This is a string of type characters, using the same characters + * as D-Bus or GVariant. Only a very limited number of signatures + * are supported: , '', 's', 'i', and 'x'. This mean respectively: + * no arguments, one string, one 32-bit integer, and one 64-bit + * integer. + * + * Defines a performance event for later recording. + */ +void +shell_perf_log_define_event (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + define_event (perf_log, name, description, signature); +} + +static ShellPerfEvent * +lookup_event (ShellPerfLog *perf_log, + const char *name, + const char *signature) +{ + ShellPerfEvent *event = g_hash_table_lookup (perf_log->events_by_name, name); + + if (G_UNLIKELY (event == NULL)) + { + g_warning ("Discarding unknown event '%s'\n", name); + return NULL; + } + + if (G_UNLIKELY (strcmp (event->signature, signature) != 0)) + { + g_warning ("Event '%s'; defined with signature '%s', used with '%s'\n", + name, event->signature, signature); + return NULL; + } + + return event; +} + +static void +record_event (ShellPerfLog *perf_log, + gint64 event_time, + ShellPerfEvent *event, + const guchar *bytes, + size_t bytes_len) +{ + ShellPerfBlock *block; + size_t total_bytes; + guint32 time_delta; + guint32 pos; + + if (!perf_log->enabled) + return; + + total_bytes = sizeof (gint32) + sizeof (gint16) + bytes_len; + if (G_UNLIKELY (bytes_len > BLOCK_SIZE || total_bytes > BLOCK_SIZE)) + { + g_warning ("Discarding oversize event '%s'\n", event->name); + return; + } + + if (event_time > perf_log->last_time + G_GINT64_CONSTANT(0xffffffff)) + { + perf_log->last_time = event_time; + record_event (perf_log, event_time, + lookup_event (perf_log, "perf.setTime", "x"), + (const guchar *)&event_time, sizeof(gint64)); + time_delta = 0; + } + else if (event_time < perf_log->last_time) + time_delta = 0; + else + time_delta = (guint32)(event_time - perf_log->last_time); + + perf_log->last_time = event_time; + + if (perf_log->blocks->tail == NULL || + total_bytes + ((ShellPerfBlock *)perf_log->blocks->tail->data)->bytes > BLOCK_SIZE) + { + block = g_new (ShellPerfBlock, 1); + block->bytes = 0; + g_queue_push_tail (perf_log->blocks, block); + } + else + { + block = (ShellPerfBlock *)perf_log->blocks->tail->data; + } + + pos = block->bytes; + + memcpy (block->buffer + pos, &time_delta, sizeof (guint32)); + pos += sizeof (guint32); + memcpy (block->buffer + pos, &event->id, sizeof (guint16)); + pos += sizeof (guint16); + memcpy (block->buffer + pos, bytes, bytes_len); + pos += bytes_len; + + block->bytes = pos; +} + +/** + * shell_perf_log_event: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * + * Records a performance event with no arguments. + */ +void +shell_perf_log_event (ShellPerfLog *perf_log, + const char *name) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, ""); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, NULL, 0); +} + +/** + * shell_perf_log_event_i: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one 32-bit integer argument. + */ +void +shell_perf_log_event_i (ShellPerfLog *perf_log, + const char *name, + gint32 arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "i"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)&arg, sizeof (arg)); +} + +/** + * shell_perf_log_event_x: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one 64-bit integer argument. + */ +void +shell_perf_log_event_x (ShellPerfLog *perf_log, + const char *name, + gint64 arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "x"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)&arg, sizeof (arg)); +} + +/** + * shell_perf_log_event_s: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one string argument. + */ +void +shell_perf_log_event_s (ShellPerfLog *perf_log, + const char *name, + const char *arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "s"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)arg, strlen (arg) + 1); +} + +/** + * shell_perf_log_define_statistic: + * @name: name of the statistic and of the corresponding event. + * This should follow the same guidelines as for shell_perf_log_define_event() + * @description: human readable description of the statistic. + * @signature: The type of the data stored for statistic. Must + * currently be 'i' or 'x'. + * + * Defines a statistic. A statistic is a numeric value that is stored + * by the performance log and recorded periodically or when + * shell_perf_log_collect_statistics() is called explicitly. + * + * Code that defines a statistic should update it by calling + * the update function for the particular data type of the statistic, + * such as shell_perf_log_update_statistic_i(). This can be done + * at any time, but would normally done inside a function registered + * with shell_perf_log_add_statistics_callback(). These functions + * are called immediately before statistics are recorded. + */ +void +shell_perf_log_define_statistic (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + ShellPerfEvent *event; + ShellPerfStatistic *statistic; + + if (strcmp (signature, "i") != 0 && + strcmp (signature, "x") != 0) + { + g_warning ("Only supported statistic signatures are 'i' and 'x'\n"); + return; + } + + event = define_event (perf_log, name, description, signature); + if (event == NULL) + return; + + statistic = g_new (ShellPerfStatistic, 1); + statistic->event = event; + + statistic->initialized = FALSE; + statistic->recorded = FALSE; + + g_ptr_array_add (perf_log->statistics, statistic); + g_hash_table_insert (perf_log->statistics_by_name, event->name, statistic); +} + +static ShellPerfStatistic * +lookup_statistic (ShellPerfLog *perf_log, + const char *name, + const char *signature) +{ + ShellPerfStatistic *statistic = g_hash_table_lookup (perf_log->statistics_by_name, name); + + if (G_UNLIKELY (statistic == NULL)) + { + g_warning ("Unknown statistic '%s'\n", name); + return NULL; + } + + if (G_UNLIKELY (strcmp (statistic->event->signature, signature) != 0)) + { + g_warning ("Statistic '%s'; defined with signature '%s', used with '%s'\n", + name, statistic->event->signature, signature); + return NULL; + } + + return statistic; +} + +/** + * shell_perf_log_update_statistic_i: + * @perf_log: a #ShellPerfLog + * @name: name of the statistic + * @value: new value for the statistic + * + * Updates the current value of an 32-bit integer statistic. + */ +void +shell_perf_log_update_statistic_i (ShellPerfLog *perf_log, + const char *name, + gint32 value) +{ + ShellPerfStatistic *statistic; + + statistic = lookup_statistic (perf_log, name, "i"); + if (G_UNLIKELY (statistic == NULL)) + return; + + statistic->current_value.i = value; + statistic->initialized = TRUE; +} + +/** + * shell_perf_log_update_statistic_x: + * @perf_log: a #ShellPerfLog + * @name: name of the statistic + * @value: new value for the statistic + * + * Updates the current value of an 64-bit integer statistic. + */ +void +shell_perf_log_update_statistic_x (ShellPerfLog *perf_log, + const char *name, + gint64 value) +{ + ShellPerfStatistic *statistic; + + statistic = lookup_statistic (perf_log, name, "x"); + if (G_UNLIKELY (statistic == NULL)) + return; + + statistic->current_value.x = value; + statistic->initialized = TRUE; +} + +/** + * shell_perf_log_add_statistics_callback: + * @perf_log: a #ShellPerfLog + * @callback: function to call before recording statistics + * @user_data: data to pass to @callback + * @notify: function to call when @user_data is no longer needed + * + * Adds a function that will be called before statistics are recorded. + * The function would typically compute one or more statistics values + * and call a function such as shell_perf_log_update_statistic_i() + * to update the value that will be recorded. + */ +void +shell_perf_log_add_statistics_callback (ShellPerfLog *perf_log, + ShellPerfStatisticsCallback callback, + gpointer user_data, + GDestroyNotify notify) +{ + ShellPerfStatisticsClosure *closure = g_new (ShellPerfStatisticsClosure, 1); + + closure->callback = callback; + closure->user_data = user_data; + closure->notify = notify; + + g_ptr_array_add (perf_log->statistics_closures, closure); +} + +/** + * shell_perf_log_collect_statistics: + * @perf_log: a #ShellPerfLog + * + * Calls all the update functions added with + * shell_perf_log_add_statistics_callback() and then records events + * for all statistics, followed by a perf.statisticsCollected event. + */ +void +shell_perf_log_collect_statistics (ShellPerfLog *perf_log) +{ + gint64 event_time = get_time (); + gint64 collection_time; + guint i; + + if (!perf_log->enabled) + return; + + for (i = 0; i < perf_log->statistics_closures->len; i++) + { + ShellPerfStatisticsClosure *closure; + + closure = g_ptr_array_index (perf_log->statistics_closures, i); + closure->callback (perf_log, closure->user_data); + } + + collection_time = get_time() - event_time; + + for (i = 0; i < perf_log->statistics->len; i++) + { + ShellPerfStatistic *statistic = g_ptr_array_index (perf_log->statistics, i); + + if (!statistic->initialized) + continue; + + switch (statistic->event->signature[0]) + { + case 'i': + if (!statistic->recorded || + statistic->current_value.i != statistic->last_value.i) + { + record_event (perf_log, event_time, statistic->event, + (const guchar *)&statistic->current_value.i, + sizeof (gint32)); + statistic->last_value.i = statistic->current_value.i; + statistic->recorded = TRUE; + } + break; + case 'x': + if (!statistic->recorded || + statistic->current_value.x != statistic->last_value.x) + { + record_event (perf_log, event_time, statistic->event, + (const guchar *)&statistic->current_value.x, + sizeof (gint64)); + statistic->last_value.x = statistic->current_value.x; + statistic->recorded = TRUE; + } + break; + default: + g_warning ("Unsupported signature in event"); + break; + } + } + + record_event (perf_log, event_time, + g_ptr_array_index (perf_log->events, EVENT_STATISTICS_COLLECTED), + (const guchar *)&collection_time, sizeof (gint64)); +} + +/** + * shell_perf_log_replay: + * @perf_log: a #ShellPerfLog + * @replay_function: (scope call): function to call for each event in the log + * @user_data: data to pass to @replay_function + * + * Replays the log by calling the given function for each event + * in the log. + */ +void +shell_perf_log_replay (ShellPerfLog *perf_log, + ShellPerfReplayFunction replay_function, + gpointer user_data) +{ + gint64 event_time = perf_log->start_time; + GList *iter; + + for (iter = perf_log->blocks->head; iter; iter = iter->next) + { + ShellPerfBlock *block = iter->data; + guint32 pos = 0; + + while (pos < block->bytes) + { + ShellPerfEvent *event; + guint16 id; + guint32 time_delta; + GValue arg = { 0, }; + + memcpy (&time_delta, block->buffer + pos, sizeof (guint32)); + pos += sizeof (guint32); + memcpy (&id, block->buffer + pos, sizeof (guint16)); + pos += sizeof (guint16); + + if (id == EVENT_SET_TIME) + { + /* Internal, we don't include in the replay */ + memcpy (&event_time, block->buffer + pos, sizeof (gint64)); + pos += sizeof (gint64); + continue; + } + else + { + event_time += time_delta; + } + + event = g_ptr_array_index (perf_log->events, id); + + if (strcmp (event->signature, "") == 0) + { + /* We need to pass something, so pass an empty string */ + g_value_init (&arg, G_TYPE_STRING); + } + else if (strcmp (event->signature, "i") == 0) + { + gint32 l; + + memcpy (&l, block->buffer + pos, sizeof (gint32)); + pos += sizeof (gint32); + + g_value_init (&arg, G_TYPE_INT); + g_value_set_int (&arg, l); + } + else if (strcmp (event->signature, "x") == 0) + { + gint64 l; + + memcpy (&l, block->buffer + pos, sizeof (gint64)); + pos += sizeof (gint64); + + g_value_init (&arg, G_TYPE_INT64); + g_value_set_int64 (&arg, l); + } + else if (strcmp (event->signature, "s") == 0) + { + g_value_init (&arg, G_TYPE_STRING); + g_value_set_string (&arg, (char *)block->buffer + pos); + pos += strlen ((char *)(block->buffer + pos)) + 1; + } + + replay_function (event_time, event->name, event->signature, &arg, user_data); + g_value_unset (&arg); + } + } +} + +static char * +escape_quotes (const char *input) +{ + GString *result; + const char *p; + + if (strchr (input, '"') == NULL) + return (char *)input; + + result = g_string_new (NULL); + for (p = input; *p; p++) + { + if (*p == '"') + g_string_append (result, "\\\""); + else + g_string_append_c (result, *p); + } + + return g_string_free (result, FALSE); +} + +static gboolean +write_string (GOutputStream *out, + const char *str, + GError **error) +{ + return g_output_stream_write_all (out, str, strlen (str), + NULL, NULL, + error); +} + +/** + * shell_perf_log_dump_events: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event definitions + * @error: location to store #GError, or %NULL + * + * Dump the definition of currently defined events and statistics, formatted + * as JSON, to the specified output stream. The JSON output is an array, + * with each element being a dictionary of the form: + * + * { name: <name of event>, + * description: <description of string, + * statistic: true } (only for statistics) + * + * Return value: %TRUE if the dump succeeded. %FALSE if an IO error occurred + */ +gboolean +shell_perf_log_dump_events (ShellPerfLog *perf_log, + GOutputStream *out, + GError **error) +{ + GString *output; + guint i; + + output = g_string_new (NULL); + g_string_append (output, "[ "); + + for (i = 0; i < perf_log->events->len; i++) + { + ShellPerfEvent *event = g_ptr_array_index (perf_log->events, i); + char *escaped_description = escape_quotes (event->description); + gboolean is_statistic = g_hash_table_lookup (perf_log->statistics_by_name, event->name) != NULL; + + if (i != 0) + g_string_append (output, ",\n "); + + g_string_append_printf (output, + "{ \"name\": \"%s\",\n" + " \"description\": \"%s\"", + event->name, escaped_description); + if (is_statistic) + g_string_append (output, ",\n \"statistic\": true"); + + g_string_append (output, " }"); + + if (escaped_description != event->description) + g_free (escaped_description); + } + + g_string_append (output, " ]"); + + return write_string (out, g_string_free (output, FALSE), error); +} + +typedef struct { + GOutputStream *out; + GError *error; + gboolean first; +} ReplayToJsonClosure; + +static void +replay_to_json (gint64 time, + const char *name, + const char *signature, + GValue *arg, + gpointer user_data) +{ + ReplayToJsonClosure *closure = user_data; + g_autofree char *event_str = NULL; + + if (closure->error != NULL) + return; + + if (!closure->first) + { + if (!write_string (closure->out, ",\n ", &closure->error)) + return; + } + + closure->first = FALSE; + + if (strcmp (signature, "") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\"]", time, name); + } + else if (strcmp (signature, "i") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %i]", + time, + name, + g_value_get_int (arg)); + } + else if (strcmp (signature, "x") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %"G_GINT64_FORMAT "]", + time, + name, + g_value_get_int64 (arg)); + } + else if (strcmp (signature, "s") == 0) + { + const char *arg_str = g_value_get_string (arg); + char *escaped = escape_quotes (arg_str); + + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", \"%s\"]", + time, + name, + g_value_get_string (arg)); + + if (escaped != arg_str) + g_free (escaped); + } + else + { + g_assert_not_reached (); + } + + if (!write_string (closure->out, event_str, &closure->error)) + return; +} + +/** + * shell_perf_log_dump_log: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event log + * @error: location to store #GError, or %NULL + * + * Writes the performance event log, formatted as JSON, to the specified + * output stream. For performance reasons, the output stream passed + * in should generally be a buffered (or memory) output stream, since + * it will be written to in small pieces. The JSON output is an array + * with the elements of the array also being arrays, of the form + * '[' <time>, <event name> [, <event_arg>... ] ']'. + * + * Return value: %TRUE if the dump succeeded. %FALSE if an IO error occurred + */ +gboolean +shell_perf_log_dump_log (ShellPerfLog *perf_log, + GOutputStream *out, + GError **error) +{ + ReplayToJsonClosure closure; + + closure.out = out; + closure.error = NULL; + closure.first = TRUE; + + if (!write_string (out, "[ ", &closure.error)) + return FALSE; + + shell_perf_log_replay (perf_log, replay_to_json, &closure); + + if (closure.error != NULL) + { + g_propagate_error (error, closure.error); + return FALSE; + } + + if (!write_string (out, " ]", &closure.error)) + return FALSE; + + return TRUE; +} diff --git a/src/shell-perf-log.h b/src/shell-perf-log.h new file mode 100644 index 0000000..45d07a5 --- /dev/null +++ b/src/shell-perf-log.h @@ -0,0 +1,75 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_PERF_LOG_H__ +#define __SHELL_PERF_LOG_H__ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_PERF_LOG (shell_perf_log_get_type ()) +G_DECLARE_FINAL_TYPE (ShellPerfLog, shell_perf_log, SHELL, PERF_LOG, GObject) + +ShellPerfLog *shell_perf_log_get_default (void); + +void shell_perf_log_set_enabled (ShellPerfLog *perf_log, + gboolean enabled); + +void shell_perf_log_define_event (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature); +void shell_perf_log_event (ShellPerfLog *perf_log, + const char *name); +void shell_perf_log_event_i (ShellPerfLog *perf_log, + const char *name, + gint32 arg); +void shell_perf_log_event_x (ShellPerfLog *perf_log, + const char *name, + gint64 arg); +void shell_perf_log_event_s (ShellPerfLog *perf_log, + const char *name, + const char *arg); + +void shell_perf_log_define_statistic (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature); + +void shell_perf_log_update_statistic_i (ShellPerfLog *perf_log, + const char *name, + int value); +void shell_perf_log_update_statistic_x (ShellPerfLog *perf_log, + const char *name, + gint64 value); + +typedef void (*ShellPerfStatisticsCallback) (ShellPerfLog *perf_log, + gpointer data); + +void shell_perf_log_add_statistics_callback (ShellPerfLog *perf_log, + ShellPerfStatisticsCallback callback, + gpointer user_data, + GDestroyNotify notify); + +void shell_perf_log_collect_statistics (ShellPerfLog *perf_log); + +typedef void (*ShellPerfReplayFunction) (gint64 time, + const char *name, + const char *signature, + GValue *arg, + gpointer user_data); + +void shell_perf_log_replay (ShellPerfLog *perf_log, + ShellPerfReplayFunction replay_function, + gpointer user_data); + +gboolean shell_perf_log_dump_events (ShellPerfLog *perf_log, + GOutputStream *out, + GError **error); +gboolean shell_perf_log_dump_log (ShellPerfLog *perf_log, + GOutputStream *out, + GError **error); + +G_END_DECLS + +#endif /* __SHELL_PERF_LOG_H__ */ diff --git a/src/shell-polkit-authentication-agent.c b/src/shell-polkit-authentication-agent.c new file mode 100644 index 0000000..6815f01 --- /dev/null +++ b/src/shell-polkit-authentication-agent.c @@ -0,0 +1,429 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* + * Copyright (C) 20011 Red Hat, Inc. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" + +#include <pwd.h> + +#include "shell-polkit-authentication-agent.h" + +#include <glib/gi18n.h> + +/* uncomment for useful debug output */ +/* #define SHOW_DEBUG */ + +#ifdef SHOW_DEBUG +static void +print_debug (const gchar *format, ...) +{ + g_autofree char *s = NULL; + g_autofree char *timestamp = NULL; + g_autoptr (GDateTime) now = NULL; + va_list ap; + + now = g_date_time_new_now_local (); + timestamp = g_date_time_format (now, "%H:%M:%S"); + + va_start (ap, format); + s = g_strdup_vprintf (format, ap); + va_end (ap); + + g_print ("ShellPolkitAuthenticationAgent: %s.%03d: %s\n", + timestamp, g_date_time_get_microsecond (now), s); +} +#else +static void +print_debug (const gchar *str, ...) +{ +} +#endif + +struct _AuthRequest; +typedef struct _AuthRequest AuthRequest; + +struct _ShellPolkitAuthenticationAgent { + PolkitAgentListener parent_instance; + + GList *scheduled_requests; + AuthRequest *current_request; + + gpointer handle; +}; + +/* Signals */ +enum +{ + INITIATE_SIGNAL, + CANCEL_SIGNAL, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE (ShellPolkitAuthenticationAgent, shell_polkit_authentication_agent, POLKIT_AGENT_TYPE_LISTENER); + +static void initiate_authentication (PolkitAgentListener *listener, + const gchar *action_id, + const gchar *message, + const gchar *icon_name, + PolkitDetails *details, + const gchar *cookie, + GList *identities, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +static gboolean initiate_authentication_finish (PolkitAgentListener *listener, + GAsyncResult *res, + GError **error); + +void +shell_polkit_authentication_agent_init (ShellPolkitAuthenticationAgent *agent) +{ +} + +void +shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent, + GError **error_out) +{ + GError *error = NULL; + PolkitSubject *subject; + + subject = polkit_unix_session_new_for_process_sync (getpid (), + NULL, /* GCancellable* */ + &error); + if (subject == NULL) + { + if (error == NULL) /* polkit version 104 and older don't properly set error on failure */ + error = g_error_new (POLKIT_ERROR, POLKIT_ERROR_FAILED, + "PolKit failed to properly get our session"); + goto out; + } + + agent->handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (agent), + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + NULL, /* use default object path */ + NULL, /* GCancellable */ + &error); + + out: + if (error != NULL) + g_propagate_error (error_out, error); + + if (subject != NULL) + g_object_unref (subject); +} + +static void +shell_polkit_authentication_agent_finalize (GObject *object) +{ + ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (object); + shell_polkit_authentication_agent_unregister (agent); + G_OBJECT_CLASS (shell_polkit_authentication_agent_parent_class)->finalize (object); +} + +static void +shell_polkit_authentication_agent_class_init (ShellPolkitAuthenticationAgentClass *klass) +{ + GObjectClass *gobject_class; + PolkitAgentListenerClass *listener_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = shell_polkit_authentication_agent_finalize; + + listener_class = POLKIT_AGENT_LISTENER_CLASS (klass); + listener_class->initiate_authentication = initiate_authentication; + listener_class->initiate_authentication_finish = initiate_authentication_finish; + + signals[INITIATE_SIGNAL] = + g_signal_new ("initiate", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* class_offset */ + NULL, /* accumulator */ + NULL, /* accumulator data */ + NULL, /* marshaller */ + G_TYPE_NONE, + 5, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_STRV); + + signals[CANCEL_SIGNAL] = + g_signal_new ("cancel", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* class_offset */ + NULL, /* accumulator */ + NULL, /* accumulator data */ + NULL, /* marshaller */ + G_TYPE_NONE, + 0); +} + +ShellPolkitAuthenticationAgent * +shell_polkit_authentication_agent_new (void) +{ + return SHELL_POLKIT_AUTHENTICATION_AGENT (g_object_new (SHELL_TYPE_POLKIT_AUTHENTICATION_AGENT, NULL)); +} + +struct _AuthRequest { + /* not holding ref */ + ShellPolkitAuthenticationAgent *agent; + GCancellable *cancellable; + gulong handler_id; + + /* copies */ + gchar *action_id; + gchar *message; + gchar *icon_name; + PolkitDetails *details; + gchar *cookie; + GList *identities; + + GTask *simple; +}; + +static void +auth_request_free (AuthRequest *request) +{ + g_free (request->action_id); + g_free (request->message); + g_free (request->icon_name); + g_object_unref (request->details); + g_free (request->cookie); + g_list_foreach (request->identities, (GFunc) g_object_unref, NULL); + g_list_free (request->identities); + g_object_unref (request->simple); + g_free (request); +} + +static void +auth_request_initiate (AuthRequest *request) +{ + gchar **user_names; + GPtrArray *p; + GList *l; + + p = g_ptr_array_new (); + for (l = request->identities; l != NULL; l = l->next) + { + if (POLKIT_IS_UNIX_USER (l->data)) + { + PolkitUnixUser *user = POLKIT_UNIX_USER (l->data); + gint uid; + gchar buf[4096]; + struct passwd pwd; + struct passwd *ppwd; + + uid = polkit_unix_user_get_uid (user); + if (getpwuid_r (uid, &pwd, buf, sizeof (buf), &ppwd) == 0) + { + if (!g_utf8_validate (pwd.pw_name, -1, NULL)) + { + g_warning ("Invalid UTF-8 in username for uid %d. Skipping", uid); + } + else + { + g_ptr_array_add (p, g_strdup (pwd.pw_name)); + } + } + else + { + g_warning ("Error looking up user name for uid %d", uid); + } + } + else + { + g_warning ("Unsupporting identity of GType %s", g_type_name (G_TYPE_FROM_INSTANCE (l->data))); + } + } + g_ptr_array_add (p, NULL); + user_names = (gchar **) g_ptr_array_free (p, FALSE); + g_signal_emit (request->agent, + signals[INITIATE_SIGNAL], + 0, /* detail */ + request->action_id, + request->message, + request->icon_name, + request->cookie, + user_names); + g_strfreev (user_names); +} + +static void auth_request_complete (AuthRequest *request, + gboolean dismissed); + +static gboolean +handle_cancelled_in_idle (gpointer user_data) +{ + AuthRequest *request = user_data; + + print_debug ("CANCELLED %s cookie %s", request->action_id, request->cookie); + if (request == request->agent->current_request) + { + g_signal_emit (request->agent, + signals[CANCEL_SIGNAL], + 0); /* detail */ + } + else + { + auth_request_complete (request, FALSE); + } + + return FALSE; +} + +static void +on_request_cancelled (GCancellable *cancellable, + gpointer user_data) +{ + AuthRequest *request = user_data; + guint id; + + /* post-pone to idle to handle GCancellable deadlock in + * + * https://bugzilla.gnome.org/show_bug.cgi?id=642968 + */ + id = g_idle_add (handle_cancelled_in_idle, request); + g_source_set_name_by_id (id, "[gnome-shell] handle_cancelled_in_idle"); +} + +static void +auth_request_dismiss (AuthRequest *request) +{ + auth_request_complete (request, TRUE); +} + +void +shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent) +{ + if (agent->scheduled_requests != NULL) + { + g_list_foreach (agent->scheduled_requests, (GFunc)auth_request_dismiss, NULL); + agent->scheduled_requests = NULL; + } + if (agent->current_request != NULL) + auth_request_dismiss (agent->current_request); + + if (agent->handle) + { + polkit_agent_listener_unregister (agent->handle); + agent->handle = NULL; + } +} + +static void maybe_process_next_request (ShellPolkitAuthenticationAgent *agent); + +static void +auth_request_complete (AuthRequest *request, + gboolean dismissed) +{ + ShellPolkitAuthenticationAgent *agent = request->agent; + gboolean is_current = agent->current_request == request; + + print_debug ("COMPLETING %s %s cookie %s", is_current ? "CURRENT" : "SCHEDULED", + request->action_id, request->cookie); + + if (!is_current) + agent->scheduled_requests = g_list_remove (agent->scheduled_requests, request); + g_cancellable_disconnect (request->cancellable, request->handler_id); + + if (dismissed) + g_task_return_new_error (request->simple, + POLKIT_ERROR, + POLKIT_ERROR_CANCELLED, + _("Authentication dialog was dismissed by the user")); + else + g_task_return_boolean (request->simple, TRUE); + + auth_request_free (request); + + if (is_current) + { + agent->current_request = NULL; + maybe_process_next_request (agent); + } +} + +static void +maybe_process_next_request (ShellPolkitAuthenticationAgent *agent) +{ + print_debug ("MAYBE_PROCESS cur=%p len(scheduled)=%d", agent->current_request, g_list_length (agent->scheduled_requests)); + + if (agent->current_request == NULL && agent->scheduled_requests != NULL) + { + AuthRequest *request; + + request = agent->scheduled_requests->data; + + agent->current_request = request; + agent->scheduled_requests = g_list_remove (agent->scheduled_requests, request); + + print_debug ("INITIATING %s cookie %s", request->action_id, request->cookie); + auth_request_initiate (request); + } +} + +static void +initiate_authentication (PolkitAgentListener *listener, + const gchar *action_id, + const gchar *message, + const gchar *icon_name, + PolkitDetails *details, + const gchar *cookie, + GList *identities, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellPolkitAuthenticationAgent *agent = SHELL_POLKIT_AUTHENTICATION_AGENT (listener); + AuthRequest *request; + + request = g_new0 (AuthRequest, 1); + request->agent = agent; + request->action_id = g_strdup (action_id); + request->message = g_strdup (message); + request->icon_name = g_strdup (icon_name); + request->details = g_object_ref (details); + request->cookie = g_strdup (cookie); + request->identities = g_list_copy (identities); + g_list_foreach (request->identities, (GFunc) g_object_ref, NULL); + request->simple = g_task_new (listener, NULL, callback, user_data); + request->cancellable = cancellable; + request->handler_id = g_cancellable_connect (request->cancellable, + G_CALLBACK (on_request_cancelled), + request, + NULL); /* GDestroyNotify for request */ + + print_debug ("SCHEDULING %s cookie %s", request->action_id, request->cookie); + agent->scheduled_requests = g_list_append (agent->scheduled_requests, request); + + maybe_process_next_request (agent); +} + +static gboolean +initiate_authentication_finish (PolkitAgentListener *listener, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +void +shell_polkit_authentication_agent_complete (ShellPolkitAuthenticationAgent *agent, + gboolean dismissed) +{ + g_return_if_fail (SHELL_IS_POLKIT_AUTHENTICATION_AGENT (agent)); + g_return_if_fail (agent->current_request != NULL); + + auth_request_complete (agent->current_request, dismissed); +} diff --git a/src/shell-polkit-authentication-agent.h b/src/shell-polkit-authentication-agent.h new file mode 100644 index 0000000..4f14749 --- /dev/null +++ b/src/shell-polkit-authentication-agent.h @@ -0,0 +1,35 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* + * Copyright (C) 20011 Red Hat, Inc. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#pragma once + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +#include <polkitagent/polkitagent.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#ifndef HAVE_POLKIT_AUTOCLEANUP +/* Polkit doesn't have g_autoptr support, thus we have to manually set the autoptr function here */ +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PolkitAgentListener, g_object_unref) +#endif + +#define SHELL_TYPE_POLKIT_AUTHENTICATION_AGENT (shell_polkit_authentication_agent_get_type()) + +G_DECLARE_FINAL_TYPE (ShellPolkitAuthenticationAgent, shell_polkit_authentication_agent, SHELL, POLKIT_AUTHENTICATION_AGENT, PolkitAgentListener) + +ShellPolkitAuthenticationAgent *shell_polkit_authentication_agent_new (void); + +void shell_polkit_authentication_agent_complete (ShellPolkitAuthenticationAgent *agent, + gboolean dismissed); +void shell_polkit_authentication_agent_register (ShellPolkitAuthenticationAgent *agent, + GError **error_out); +void shell_polkit_authentication_agent_unregister (ShellPolkitAuthenticationAgent *agent); + +G_END_DECLS + diff --git a/src/shell-screenshot.c b/src/shell-screenshot.c new file mode 100644 index 0000000..f7132ee --- /dev/null +++ b/src/shell-screenshot.c @@ -0,0 +1,1217 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include <clutter/clutter.h> +#include <cogl/cogl.h> +#include <meta/display.h> +#include <meta/util.h> +#include <meta/meta-plugin.h> +#include <meta/meta-cursor-tracker.h> +#include <st/st.h> + +#include "shell-global.h" +#include "shell-screenshot.h" +#include "shell-util.h" + +typedef enum _ShellScreenshotFlag +{ + SHELL_SCREENSHOT_FLAG_NONE, + SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR, +} ShellScreenshotFlag; + +typedef enum _ShellScreenshotMode +{ + SHELL_SCREENSHOT_SCREEN, + SHELL_SCREENSHOT_WINDOW, + SHELL_SCREENSHOT_AREA, +} ShellScreenshotMode; + +enum +{ + SCREENSHOT_TAKEN, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +typedef struct _ShellScreenshotPrivate ShellScreenshotPrivate; + +struct _ShellScreenshot +{ + GObject parent_instance; + + ShellScreenshotPrivate *priv; +}; + +struct _ShellScreenshotPrivate +{ + ShellGlobal *global; + + GOutputStream *stream; + ShellScreenshotFlag flags; + ShellScreenshotMode mode; + + GDateTime *datetime; + + cairo_surface_t *image; + cairo_rectangle_int_t screenshot_area; + + gboolean include_frame; + + float scale; + ClutterContent *cursor_content; + graphene_point_t cursor_point; + float cursor_scale; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellScreenshot, shell_screenshot, G_TYPE_OBJECT); + +static void +shell_screenshot_class_init (ShellScreenshotClass *screenshot_class) +{ + signals[SCREENSHOT_TAKEN] = + g_signal_new ("screenshot-taken", + G_TYPE_FROM_CLASS(screenshot_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + META_TYPE_RECTANGLE); +} + +static void +shell_screenshot_init (ShellScreenshot *screenshot) +{ + screenshot->priv = shell_screenshot_get_instance_private (screenshot); + screenshot->priv->global = shell_global_get (); +} + +static void +on_screenshot_written (GObject *source, + GAsyncResult *task, + gpointer user_data) +{ + ShellScreenshot *screenshot = SHELL_SCREENSHOT (source); + ShellScreenshotPrivate *priv = screenshot->priv; + GTask *result = user_data; + + g_task_return_boolean (result, g_task_propagate_boolean (G_TASK (task), NULL)); + g_object_unref (result); + + g_clear_pointer (&priv->image, cairo_surface_destroy); + g_clear_object (&priv->stream); + g_clear_pointer (&priv->datetime, g_date_time_unref); +} + +static void +write_screenshot_thread (GTask *result, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + ShellScreenshot *screenshot = SHELL_SCREENSHOT (object); + ShellScreenshotPrivate *priv; + g_autoptr (GOutputStream) stream = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autofree char *creation_time = NULL; + GError *error = NULL; + + g_assert (screenshot != NULL); + + priv = screenshot->priv; + + stream = g_object_ref (priv->stream); + + pixbuf = gdk_pixbuf_get_from_surface (priv->image, + 0, 0, + cairo_image_surface_get_width (priv->image), + cairo_image_surface_get_height (priv->image)); + creation_time = g_date_time_format (priv->datetime, "%c"); + + if (!creation_time) + creation_time = g_date_time_format (priv->datetime, "%FT%T%z"); + + gdk_pixbuf_save_to_stream (pixbuf, stream, "png", NULL, &error, + "tEXt::Software", "gnome-screenshot", + "tEXt::Creation Time", creation_time, + NULL); + + if (error) + g_task_return_error (result, error); + else + g_task_return_boolean (result, TRUE); +} + +static void +do_grab_screenshot (ShellScreenshot *screenshot, + int x, + int y, + int width, + int height, + ShellScreenshotFlag flags) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + ClutterStage *stage = shell_global_get_stage (priv->global); + cairo_rectangle_int_t screenshot_rect = { x, y, width, height }; + int image_width; + int image_height; + float scale; + cairo_surface_t *image; + ClutterPaintFlag paint_flags = CLUTTER_PAINT_FLAG_NONE; + g_autoptr (GError) error = NULL; + + clutter_stage_get_capture_final_size (stage, &screenshot_rect, + &image_width, + &image_height, + &scale); + image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + image_width, image_height); + + if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR) + paint_flags |= CLUTTER_PAINT_FLAG_FORCE_CURSORS; + else + paint_flags |= CLUTTER_PAINT_FLAG_NO_CURSORS; + if (!clutter_stage_paint_to_buffer (stage, &screenshot_rect, scale, + cairo_image_surface_get_data (image), + cairo_image_surface_get_stride (image), + CLUTTER_CAIRO_FORMAT_ARGB32, + paint_flags, + &error)) + { + cairo_surface_destroy (image); + g_warning ("Failed to take screenshot: %s", error->message); + return; + } + + priv->image = image; + + priv->datetime = g_date_time_new_now_local (); +} + +static void +draw_cursor_image (cairo_surface_t *surface, + cairo_rectangle_int_t area) +{ + CoglTexture *texture; + int width, height; + int stride; + guint8 *data; + MetaDisplay *display; + MetaCursorTracker *tracker; + cairo_surface_t *cursor_surface; + cairo_region_t *screenshot_region; + cairo_t *cr; + int x, y; + int xhot, yhot; + double xscale, yscale; + graphene_point_t point; + + display = shell_global_get_display (shell_global_get ()); + tracker = meta_cursor_tracker_get_for_display (display); + texture = meta_cursor_tracker_get_sprite (tracker); + + if (!texture) + return; + + screenshot_region = cairo_region_create_rectangle (&area); + meta_cursor_tracker_get_pointer (tracker, &point, NULL); + x = point.x; + y = point.y; + + if (!cairo_region_contains_point (screenshot_region, point.x, point.y)) + { + cairo_region_destroy (screenshot_region); + return; + } + + meta_cursor_tracker_get_hot (tracker, &xhot, &yhot); + width = cogl_texture_get_width (texture); + height = cogl_texture_get_height (texture); + stride = 4 * width; + data = g_new (guint8, stride * height); + cogl_texture_get_data (texture, CLUTTER_CAIRO_FORMAT_ARGB32, stride, data); + + /* FIXME: cairo-gl? */ + cursor_surface = cairo_image_surface_create_for_data (data, + CAIRO_FORMAT_ARGB32, + width, height, + stride); + + cairo_surface_get_device_scale (surface, &xscale, &yscale); + + if (xscale != 1.0 || yscale != 1.0) + { + int monitor; + float monitor_scale; + MetaRectangle cursor_rect = { + .x = x, .y = y, .width = width, .height = height + }; + + monitor = meta_display_get_monitor_index_for_rect (display, &cursor_rect); + monitor_scale = meta_display_get_monitor_scale (display, monitor); + + cairo_surface_set_device_scale (cursor_surface, monitor_scale, monitor_scale); + } + + cr = cairo_create (surface); + cairo_set_source_surface (cr, + cursor_surface, + x - xhot - area.x, + y - yhot - area.y); + cairo_paint (cr); + + cairo_destroy (cr); + cairo_surface_destroy (cursor_surface); + cairo_region_destroy (screenshot_region); + g_free (data); +} + +static void +grab_screenshot (ShellScreenshot *screenshot, + ShellScreenshotFlag flags, + GTask *result) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display; + int width, height; + GTask *task; + + display = shell_global_get_display (priv->global); + meta_display_get_size (display, &width, &height); + + do_grab_screenshot (screenshot, + 0, 0, width, height, + flags); + + priv->screenshot_area.x = 0; + priv->screenshot_area.y = 0; + priv->screenshot_area.width = width; + priv->screenshot_area.height = height; + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + g_object_unref (task); +} + +static void +grab_screenshot_content (ShellScreenshot *screenshot, + GTask *result) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display; + int width, height; + cairo_rectangle_int_t screenshot_rect; + ClutterStage *stage; + int image_width; + int image_height; + float scale; + g_autoptr (GError) error = NULL; + g_autoptr (ClutterContent) content = NULL; + g_autoptr (GTask) task = result; + MetaCursorTracker *tracker; + CoglTexture *cursor_texture; + int cursor_hot_x, cursor_hot_y; + + display = shell_global_get_display (priv->global); + meta_display_get_size (display, &width, &height); + screenshot_rect = (cairo_rectangle_int_t) { + .x = 0, + .y = 0, + .width = width, + .height = height, + }; + + stage = shell_global_get_stage (priv->global); + + clutter_stage_get_capture_final_size (stage, &screenshot_rect, + &image_width, + &image_height, + &scale); + + priv->scale = scale; + + content = clutter_stage_paint_to_content (stage, &screenshot_rect, scale, + CLUTTER_PAINT_FLAG_NO_CURSORS, + &error); + if (!content) + { + g_task_return_error (result, g_steal_pointer (&error)); + return; + } + + tracker = meta_cursor_tracker_get_for_display (display); + cursor_texture = meta_cursor_tracker_get_sprite (tracker); + + // If the cursor is invisible, the texture is NULL. + if (cursor_texture) + { + unsigned int width, height; + CoglContext *ctx; + CoglPipeline *pipeline; + CoglTexture2D *texture; + CoglOffscreen *offscreen; + ClutterStageView *view; + + // Copy the texture to prevent it from changing shortly after. + width = cogl_texture_get_width (cursor_texture); + height = cogl_texture_get_height (cursor_texture); + + ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + texture = cogl_texture_2d_new_with_size (ctx, width, height); + offscreen = cogl_offscreen_new_with_texture (texture); + cogl_framebuffer_clear4f (COGL_FRAMEBUFFER (offscreen), + COGL_BUFFER_BIT_COLOR, + 0, 0, 0, 0); + + pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_set_layer_texture (pipeline, 0, cursor_texture); + + cogl_framebuffer_draw_textured_rectangle (COGL_FRAMEBUFFER (offscreen), + pipeline, + -1, 1, 1, -1, + 0, 0, 1, 1); + cogl_object_unref (pipeline); + g_object_unref (offscreen); + + priv->cursor_content = + clutter_texture_content_new_from_texture (texture, NULL); + cogl_object_unref (texture); + + priv->cursor_scale = meta_cursor_tracker_get_scale (tracker); + + meta_cursor_tracker_get_pointer (tracker, &priv->cursor_point, NULL); + + view = clutter_stage_get_view_at (stage, + priv->cursor_point.x, + priv->cursor_point.y); + + meta_cursor_tracker_get_hot (tracker, &cursor_hot_x, &cursor_hot_y); + priv->cursor_point.x -= cursor_hot_x * priv->cursor_scale; + priv->cursor_point.y -= cursor_hot_y * priv->cursor_scale; + + // Align the coordinates to the pixel grid the same way it's done in + // MetaCursorRenderer. + if (view) + { + cairo_rectangle_int_t view_layout; + float view_scale; + + clutter_stage_view_get_layout (view, &view_layout); + view_scale = clutter_stage_view_get_scale (view); + + priv->cursor_point.x -= view_layout.x; + priv->cursor_point.y -= view_layout.y; + + priv->cursor_point.x = + floorf (priv->cursor_point.x * view_scale) / view_scale; + priv->cursor_point.y = + floorf (priv->cursor_point.y * view_scale) / view_scale; + + priv->cursor_point.x += view_layout.x; + priv->cursor_point.y += view_layout.y; + } + } + + g_task_return_pointer (result, g_steal_pointer (&content), g_object_unref); +} + +static void +grab_window_screenshot (ShellScreenshot *screenshot, + ShellScreenshotFlag flags, + GTask *result) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + GTask *task; + MetaDisplay *display = shell_global_get_display (priv->global); + MetaWindow *window = meta_display_get_focus_window (display); + ClutterActor *window_actor; + gfloat actor_x, actor_y; + MetaRectangle rect; + + window_actor = CLUTTER_ACTOR (meta_window_get_compositor_private (window)); + clutter_actor_get_position (window_actor, &actor_x, &actor_y); + + meta_window_get_frame_rect (window, &rect); + + if (!priv->include_frame) + meta_window_frame_rect_to_client_rect (window, &rect, &rect); + + priv->screenshot_area = rect; + + priv->image = meta_window_actor_get_image (META_WINDOW_ACTOR (window_actor), + NULL); + + if (!priv->image) + { + g_task_report_new_error (screenshot, on_screenshot_written, result, NULL, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Capturing window failed"); + return; + } + + priv->datetime = g_date_time_new_now_local (); + + if (flags & SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR) + { + if (meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_WAYLAND) + { + float resource_scale; + resource_scale = clutter_actor_get_resource_scale (window_actor); + + cairo_surface_set_device_scale (priv->image, resource_scale, resource_scale); + } + + draw_cursor_image (priv->image, priv->screenshot_area); + } + + g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0, &rect); + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + g_object_unref (task); +} + +static gboolean +finish_screenshot (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return FALSE; + + if (area) + *area = &priv->screenshot_area; + + return TRUE; +} + +static void +on_after_paint (ClutterStage *stage, + ClutterStageView *view, + GTask *result) +{ + ShellScreenshot *screenshot = g_task_get_task_data (result); + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display = shell_global_get_display (priv->global); + GTask *task; + + g_signal_handlers_disconnect_by_func (stage, on_after_paint, result); + + if (priv->mode == SHELL_SCREENSHOT_AREA) + { + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + priv->screenshot_area.width, + priv->screenshot_area.height, + priv->flags); + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + } + else + { + grab_screenshot (screenshot, priv->flags, result); + } + + g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0, + (cairo_rectangle_int_t *) &priv->screenshot_area); + + meta_enable_unredirect_for_display (display); +} + +/** + * shell_screenshot_screenshot: + * @screenshot: the #ShellScreenshot + * @include_cursor: Whether to include the cursor or not + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the whole screen + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot (ShellScreenshot *screenshot, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + GTask *result; + ShellScreenshotFlag flags; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + + if (priv->stream != NULL) { + if (callback) + g_task_report_new_error (screenshot, + callback, + user_data, + shell_screenshot_screenshot, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot); + g_task_set_task_data (result, screenshot, NULL); + + priv->stream = g_object_ref (stream); + + flags = SHELL_SCREENSHOT_FLAG_NONE; + if (include_cursor) + flags |= SHELL_SCREENSHOT_FLAG_INCLUDE_CURSOR; + + if (meta_is_wayland_compositor ()) + { + grab_screenshot (screenshot, flags, result); + + g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0, + (cairo_rectangle_int_t *) &priv->screenshot_area); + } + else + { + MetaDisplay *display = shell_global_get_display (priv->global); + ClutterStage *stage = shell_global_get_stage (priv->global); + + meta_disable_unredirect_for_display (display); + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + priv->flags = flags; + priv->mode = SHELL_SCREENSHOT_SCREEN; + g_signal_connect (stage, "after-paint", + G_CALLBACK (on_after_paint), result); + } +} + +/** + * shell_screenshot_screenshot_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +static void +screenshot_stage_to_content_on_after_paint (ClutterStage *stage, + ClutterStageView *view, + GTask *result) +{ + ShellScreenshot *screenshot = g_task_get_task_data (result); + ShellScreenshotPrivate *priv = screenshot->priv; + MetaDisplay *display = shell_global_get_display (priv->global); + + g_signal_handlers_disconnect_by_func (stage, + screenshot_stage_to_content_on_after_paint, + result); + + meta_enable_unredirect_for_display (display); + + grab_screenshot_content (screenshot, result); +} + +/** + * shell_screenshot_screenshot_stage_to_content: + * @screenshot: the #ShellScreenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the whole screen as #ClutterContent. + * + */ +void +shell_screenshot_screenshot_stage_to_content (ShellScreenshot *screenshot, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + GTask *result; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot_stage_to_content); + g_task_set_task_data (result, screenshot, NULL); + + if (meta_is_wayland_compositor ()) + { + grab_screenshot_content (screenshot, result); + } + else + { + priv = screenshot->priv; + + MetaDisplay *display = shell_global_get_display (priv->global); + ClutterStage *stage = shell_global_get_stage (priv->global); + + meta_disable_unredirect_for_display (display); + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + g_signal_connect (stage, "after-paint", + G_CALLBACK (screenshot_stage_to_content_on_after_paint), + result); + } +} + +/** + * shell_screenshot_screenshot_stage_to_content_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @scale: (out) (optional): location to store the content scale + * @cursor_content: (out) (optional): location to store the cursor content + * @cursor_point: (out) (optional): location to store the point at which to + * draw the cursor content + * @cursor_scale: (out) (optional): location to store the cursor scale + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by + * shell_screenshot_screenshot_stage_to_content() and obtain its result. + * + * Returns: (transfer full): the #ClutterContent, or NULL + * + */ +ClutterContent * +shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + float *scale, + ClutterContent **cursor_content, + graphene_point_t *cursor_point, + float *cursor_scale, + GError **error) +{ + ShellScreenshotPrivate *priv = screenshot->priv; + ClutterContent *content; + + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot_stage_to_content), + FALSE); + + content = g_task_propagate_pointer (G_TASK (result), error); + if (!content) + return NULL; + + if (scale) + *scale = priv->scale; + + if (cursor_content) + *cursor_content = g_steal_pointer (&priv->cursor_content); + else + g_clear_pointer (&priv->cursor_content, g_object_unref); + + if (cursor_point) + *cursor_point = priv->cursor_point; + + if (cursor_scale) + *cursor_scale = priv->cursor_scale; + + return content; +} + +/** + * shell_screenshot_screenshot_area: + * @screenshot: the #ShellScreenshot + * @x: The X coordinate of the area + * @y: The Y coordinate of the area + * @width: The width of the area + * @height: The height of the area + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the passed in area and saves it + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot_area (ShellScreenshot *screenshot, + int x, + int y, + int width, + int height, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + GTask *result; + g_autoptr (GTask) task = NULL; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + + if (priv->stream != NULL) { + if (callback) + g_task_report_new_error (screenshot, + callback, + NULL, + shell_screenshot_screenshot_area, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot_area); + g_task_set_task_data (result, screenshot, NULL); + + priv->stream = g_object_ref (stream); + priv->screenshot_area.x = x; + priv->screenshot_area.y = y; + priv->screenshot_area.width = width; + priv->screenshot_area.height = height; + + + if (meta_is_wayland_compositor ()) + { + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + priv->screenshot_area.width, + priv->screenshot_area.height, + SHELL_SCREENSHOT_FLAG_NONE); + + g_signal_emit (screenshot, signals[SCREENSHOT_TAKEN], 0, + (cairo_rectangle_int_t *) &priv->screenshot_area); + + task = g_task_new (screenshot, NULL, on_screenshot_written, result); + g_task_run_in_thread (task, write_screenshot_thread); + } + else + { + MetaDisplay *display = shell_global_get_display (priv->global); + ClutterStage *stage = shell_global_get_stage (priv->global); + + meta_disable_unredirect_for_display (display); + clutter_actor_queue_redraw (CLUTTER_ACTOR (stage)); + priv->flags = SHELL_SCREENSHOT_FLAG_NONE; + priv->mode = SHELL_SCREENSHOT_AREA; + g_signal_connect (stage, "after-paint", + G_CALLBACK (on_after_paint), result); + } +} + +/** + * shell_screenshot_screenshot_area_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot_area() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_area_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot_area), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +/** + * shell_screenshot_screenshot_window: + * @screenshot: the #ShellScreenshot + * @include_frame: Whether to include the frame or not + * @include_cursor: Whether to include the cursor or not + * @stream: The stream for the screenshot + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * @user_data: the data to pass to callback function + * + * Takes a screenshot of the focused window (optionally omitting the frame) + * in @stream as png image. + * + */ +void +shell_screenshot_screenshot_window (ShellScreenshot *screenshot, + gboolean include_frame, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + MetaDisplay *display; + MetaWindow *window; + GTask *result; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + g_return_if_fail (G_IS_OUTPUT_STREAM (stream)); + + priv = screenshot->priv; + display = shell_global_get_display (priv->global); + window = meta_display_get_focus_window (display); + + if (priv->stream != NULL || !window) { + if (callback) + g_task_report_new_error (screenshot, + callback, + NULL, + shell_screenshot_screenshot_window, + G_IO_ERROR, + G_IO_ERROR_PENDING, + "Only one screenshot operation at a time " + "is permitted"); + return; + } + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_screenshot_window); + + priv->stream = g_object_ref (stream); + priv->include_frame = include_frame; + + grab_window_screenshot (screenshot, include_cursor, result); +} + +/** + * shell_screenshot_screenshot_window_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @area: (out) (transfer none): the area that was grabbed in screen coordinates + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_screenshot_window() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_screenshot_window_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_screenshot_window), + FALSE); + return finish_screenshot (screenshot, result, area, error); +} + +/** + * shell_screenshot_pick_color: + * @screenshot: the #ShellScreenshot + * @x: The X coordinate to pick + * @y: The Y coordinate to pick + * @callback: (scope async): function to call returning success or failure + * of the async grabbing + * + * Picks the pixel at @x, @y and returns its color as #ClutterColor. + * + */ +void +shell_screenshot_pick_color (ShellScreenshot *screenshot, + int x, + int y, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellScreenshotPrivate *priv; + g_autoptr (GTask) result = NULL; + + g_return_if_fail (SHELL_IS_SCREENSHOT (screenshot)); + + result = g_task_new (screenshot, NULL, callback, user_data); + g_task_set_source_tag (result, shell_screenshot_pick_color); + + priv = screenshot->priv; + + priv->screenshot_area.x = x; + priv->screenshot_area.y = y; + priv->screenshot_area.width = 1; + priv->screenshot_area.height = 1; + + do_grab_screenshot (screenshot, + priv->screenshot_area.x, + priv->screenshot_area.y, + 1, + 1, + SHELL_SCREENSHOT_FLAG_NONE); + + g_task_return_boolean (result, TRUE); +} + +#if G_BYTE_ORDER == G_LITTLE_ENDIAN +#define INDEX_A 3 +#define INDEX_R 2 +#define INDEX_G 1 +#define INDEX_B 0 +#else +#define INDEX_A 0 +#define INDEX_R 1 +#define INDEX_G 2 +#define INDEX_B 3 +#endif + +/** + * shell_screenshot_pick_color_finish: + * @screenshot: the #ShellScreenshot + * @result: the #GAsyncResult that was provided to the callback + * @color: (out caller-allocates): the picked color + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by shell_screenshot_pick_color() + * and obtain its result. + * + * Returns: whether the operation was successful + * + */ +gboolean +shell_screenshot_pick_color_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + ClutterColor *color, + GError **error) +{ + ShellScreenshotPrivate *priv; + + g_return_val_if_fail (SHELL_IS_SCREENSHOT (screenshot), FALSE); + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (color != NULL, FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_pick_color), + FALSE); + + if (!g_task_propagate_boolean (G_TASK (result), error)) + return FALSE; + + priv = screenshot->priv; + + /* protect against mutter changing the format used for stage captures */ + g_assert (cairo_image_surface_get_format (priv->image) == CAIRO_FORMAT_ARGB32); + + if (color) + { + uint8_t *data = cairo_image_surface_get_data (priv->image); + + color->alpha = data[INDEX_A]; + color->red = data[INDEX_R]; + color->green = data[INDEX_G]; + color->blue = data[INDEX_B]; + } + + return TRUE; +} + +#undef INDEX_A +#undef INDEX_R +#undef INDEX_G +#undef INDEX_B + +static void +composite_to_stream_on_png_saved (GObject *pixbuf, + GAsyncResult *result, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + GError *error = NULL; + + if (!gdk_pixbuf_save_to_stream_finish (result, &error)) + g_task_return_error (task, error); + else + g_task_return_pointer (task, g_object_ref (pixbuf), g_object_unref); + + g_object_unref (task); +} + +/** + * shell_screenshot_composite_to_stream: + * @texture: the source texture + * @x: x coordinate of the rectangle + * @y: y coordinate of the rectangle + * @width: width of the rectangle, or -1 to use the full texture + * @height: height of the rectangle, or -1 to use the full texture + * @scale: scale of the source texture + * @cursor: (nullable): the cursor texture + * @cursor_x: x coordinate to put the cursor texture at, relative to the full + * source texture + * @cursor_y: y coordinate to put the cursor texture at, relative to the full + * source texture + * @cursor_scale: scale of the cursor texture + * @stream: the stream to write the PNG image into + * @callback: (scope async): function to call returning success or failure + * @user_data: the data to pass to callback function + * + * Composite a rectangle defined by x, y, width, height from the texture to a + * pixbuf and write it as a PNG image into the stream. + * + */ +void +shell_screenshot_composite_to_stream (CoglTexture *texture, + int x, + int y, + int width, + int height, + float scale, + CoglTexture *cursor, + int cursor_x, + int cursor_y, + float cursor_scale, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CoglContext *ctx; + CoglTexture *sub_texture; + cairo_surface_t *surface; + cairo_surface_t *cursor_surface; + cairo_t *cr; + g_autoptr (GTask) task = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autofree char *creation_time = NULL; + g_autoptr (GDateTime) date_time = NULL; + + task = g_task_new (NULL, NULL, callback, user_data); + g_task_set_source_tag (task, shell_screenshot_composite_to_stream); + + if (width == -1 || height == -1) + { + x = 0; + y = 0; + width = cogl_texture_get_width (texture); + height = cogl_texture_get_height (texture); + } + + ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ()); + sub_texture = cogl_sub_texture_new (ctx, texture, x, y, width, height); + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + cogl_texture_get_width (sub_texture), + cogl_texture_get_height (sub_texture)); + + cogl_texture_get_data (sub_texture, CLUTTER_CAIRO_FORMAT_ARGB32, + cairo_image_surface_get_stride (surface), + cairo_image_surface_get_data (surface)); + cairo_surface_mark_dirty (surface); + + cogl_object_unref (sub_texture); + + cairo_surface_set_device_scale (surface, scale, scale); + + if (cursor != NULL) + { + // Paint the cursor on top. + cursor_surface = + cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + cogl_texture_get_width (cursor), + cogl_texture_get_height (cursor)); + cogl_texture_get_data (cursor, CLUTTER_CAIRO_FORMAT_ARGB32, + cairo_image_surface_get_stride (cursor_surface), + cairo_image_surface_get_data (cursor_surface)); + cairo_surface_mark_dirty (cursor_surface); + + cairo_surface_set_device_scale (cursor_surface, + 1 / cursor_scale, + 1 / cursor_scale); + + cr = cairo_create (surface); + cairo_set_source_surface (cr, cursor_surface, + (cursor_x - x) / scale, + (cursor_y - y) / scale); + cairo_paint (cr); + cairo_destroy (cr); + + cairo_surface_destroy (cursor_surface); + } + + // Save to an image. + pixbuf = gdk_pixbuf_get_from_surface (surface, + 0, 0, + cairo_image_surface_get_width (surface), + cairo_image_surface_get_height (surface)); + cairo_surface_destroy (surface); + + date_time = g_date_time_new_now_local (); + creation_time = g_date_time_format (date_time, "%c"); + + if (!creation_time) + creation_time = g_date_time_format (date_time, "%FT%T%z"); + + gdk_pixbuf_save_to_stream_async (pixbuf, stream, "png", NULL, + composite_to_stream_on_png_saved, + g_steal_pointer (&task), + "tEXt::Software", "gnome-screenshot", + "tEXt::Creation Time", creation_time, + NULL); +} + +/** + * shell_screenshot_composite_to_stream_finish: + * @result: the #GAsyncResult that was provided to the callback + * @error: #GError for error reporting + * + * Finish the asynchronous operation started by + * shell_screenshot_composite_to_stream () and obtain its result. + * + * Returns: (transfer full) (nullable): a GdkPixbuf with the final image if the + * operation was successful, or NULL on error. + * + */ +GdkPixbuf * +shell_screenshot_composite_to_stream_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (G_IS_TASK (result), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, + shell_screenshot_composite_to_stream), + FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +ShellScreenshot * +shell_screenshot_new (void) +{ + return g_object_new (SHELL_TYPE_SCREENSHOT, NULL); +} diff --git a/src/shell-screenshot.h b/src/shell-screenshot.h new file mode 100644 index 0000000..441410d --- /dev/null +++ b/src/shell-screenshot.h @@ -0,0 +1,90 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_SCREENSHOT_H__ +#define __SHELL_SCREENSHOT_H__ + +/** + * SECTION:shell-screenshot + * @short_description: Grabs screenshots of areas and/or windows + * + * The #ShellScreenshot object is used to take screenshots of screen + * areas or windows and write them out as png files. + * + */ +#define SHELL_TYPE_SCREENSHOT (shell_screenshot_get_type ()) +G_DECLARE_FINAL_TYPE (ShellScreenshot, shell_screenshot, + SHELL, SCREENSHOT, GObject) + +ShellScreenshot *shell_screenshot_new (void); + +void shell_screenshot_screenshot_area (ShellScreenshot *screenshot, + int x, + int y, + int width, + int height, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_screenshot_screenshot_area_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error); + +void shell_screenshot_screenshot_window (ShellScreenshot *screenshot, + gboolean include_frame, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_screenshot_screenshot_window_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error); + +void shell_screenshot_screenshot (ShellScreenshot *screenshot, + gboolean include_cursor, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_screenshot_screenshot_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + cairo_rectangle_int_t **area, + GError **error); + +void shell_screenshot_screenshot_stage_to_content (ShellScreenshot *screenshot, + GAsyncReadyCallback callback, + gpointer user_data); +ClutterContent *shell_screenshot_screenshot_stage_to_content_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + float *scale, + ClutterContent **cursor_content, + graphene_point_t *cursor_point, + float *cursor_scale, + GError **error); + +void shell_screenshot_pick_color (ShellScreenshot *screenshot, + int x, + int y, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_screenshot_pick_color_finish (ShellScreenshot *screenshot, + GAsyncResult *result, + ClutterColor *color, + GError **error); + +void shell_screenshot_composite_to_stream (CoglTexture *texture, + int x, + int y, + int width, + int height, + float scale, + CoglTexture *cursor, + int cursor_x, + int cursor_y, + float cursor_scale, + GOutputStream *stream, + GAsyncReadyCallback callback, + gpointer user_data); +GdkPixbuf *shell_screenshot_composite_to_stream_finish (GAsyncResult *result, + GError **error); + +#endif /* ___SHELL_SCREENSHOT_H__ */ diff --git a/src/shell-secure-text-buffer.c b/src/shell-secure-text-buffer.c new file mode 100644 index 0000000..8271410 --- /dev/null +++ b/src/shell-secure-text-buffer.c @@ -0,0 +1,191 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* shell-secure-text-buffer.c - secure memory clutter text buffer + + Copyright (C) 2009 Stefan Walter + Copyright (C) 2012 Red Hat Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with the Gnome Library; see the file COPYING.LIB. If not, + see <http://www.gnu.org/licenses/>. + + Author: Stef Walter <stefw@gnome.org> +*/ + +#include "config.h" + +#include "shell-secure-text-buffer.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include <gcr/gcr.h> + +#include <string.h> + +struct _ShellSecureTextBuffer { + ClutterTextBuffer parent; + gchar *text; + gsize text_size; + gsize text_bytes; + guint text_chars; +}; + +/* Initial size of buffer, in bytes */ +#define MIN_SIZE 16 + +G_DEFINE_TYPE (ShellSecureTextBuffer, shell_secure_text_buffer, CLUTTER_TYPE_TEXT_BUFFER); + +static const gchar * +shell_secure_text_buffer_real_get_text (ClutterTextBuffer *buffer, + gsize *n_bytes) +{ + ShellSecureTextBuffer *self = SHELL_SECURE_TEXT_BUFFER (buffer); + if (n_bytes) + *n_bytes = self->text_bytes; + if (!self->text) + return ""; + return self->text; +} + +static guint +shell_secure_text_buffer_real_get_length (ClutterTextBuffer *buffer) +{ + ShellSecureTextBuffer *self = SHELL_SECURE_TEXT_BUFFER (buffer); + return self->text_chars; +} + +static guint +shell_secure_text_buffer_real_insert_text (ClutterTextBuffer *buffer, + guint position, + const gchar *chars, + guint n_chars) +{ + ShellSecureTextBuffer *self = SHELL_SECURE_TEXT_BUFFER (buffer); + gsize n_bytes; + gsize at; + + n_bytes = g_utf8_offset_to_pointer (chars, n_chars) - chars; + + /* Need more memory */ + if (n_bytes + self->text_bytes + 1 > self->text_size) + { + /* Calculate our new buffer size */ + while (n_bytes + self->text_bytes + 1 > self->text_size) + { + if (self->text_size == 0) + { + self->text_size = MIN_SIZE; + } + else + { + if (2 * self->text_size < CLUTTER_TEXT_BUFFER_MAX_SIZE) + { + self->text_size *= 2; + } + else + { + self->text_size = CLUTTER_TEXT_BUFFER_MAX_SIZE; + if (n_bytes > self->text_size - self->text_bytes - 1) + { + n_bytes = self->text_size - self->text_bytes - 1; + n_bytes = g_utf8_find_prev_char (chars, chars + n_bytes + 1) - chars; + n_chars = g_utf8_strlen (chars, n_bytes); + } + break; + } + } + } + self->text = gcr_secure_memory_realloc (self->text, self->text_size); + } + + /* Actual text insertion */ + at = g_utf8_offset_to_pointer (self->text, position) - self->text; + memmove (self->text + at + n_bytes, self->text + at, self->text_bytes - at); + memcpy (self->text + at, chars, n_bytes); + + /* Book keeping */ + self->text_bytes += n_bytes; + self->text_chars += n_chars; + self->text[self->text_bytes] = '\0'; + + clutter_text_buffer_emit_inserted_text (buffer, position, chars, n_chars); + return n_chars; +} + +static guint +shell_secure_text_buffer_real_delete_text (ClutterTextBuffer *buffer, + guint position, + guint n_chars) +{ + ShellSecureTextBuffer *self = SHELL_SECURE_TEXT_BUFFER (buffer); + gsize start, end; + + if (position > self->text_chars) + position = self->text_chars; + if (position + n_chars > self->text_chars) + n_chars = self->text_chars - position; + + if (n_chars > 0) + { + start = g_utf8_offset_to_pointer (self->text, position) - self->text; + end = g_utf8_offset_to_pointer (self->text, position + n_chars) - self->text; + + memmove (self->text + start, self->text + end, self->text_bytes + 1 - end); + self->text_chars -= n_chars; + self->text_bytes -= (end - start); + + clutter_text_buffer_emit_deleted_text (buffer, position, n_chars); + } + + return n_chars; +} + +static void +shell_secure_text_buffer_init (ShellSecureTextBuffer *self) +{ + +} + +static void +shell_secure_text_buffer_finalize (GObject *obj) +{ + ShellSecureTextBuffer *self = SHELL_SECURE_TEXT_BUFFER (obj); + + if (self->text) + { + gcr_secure_memory_strfree (self->text); + self->text = NULL; + self->text_bytes = self->text_size = 0; + self->text_chars = 0; + } + + G_OBJECT_CLASS (shell_secure_text_buffer_parent_class)->finalize (obj); +} + +static void +shell_secure_text_buffer_class_init (ShellSecureTextBufferClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterTextBufferClass *buffer_class = CLUTTER_TEXT_BUFFER_CLASS (klass); + + gobject_class->finalize = shell_secure_text_buffer_finalize; + + buffer_class->get_text = shell_secure_text_buffer_real_get_text; + buffer_class->get_length = shell_secure_text_buffer_real_get_length; + buffer_class->insert_text = shell_secure_text_buffer_real_insert_text; + buffer_class->delete_text = shell_secure_text_buffer_real_delete_text; +} + +ClutterTextBuffer * +shell_secure_text_buffer_new (void) +{ + return g_object_new (SHELL_TYPE_SECURE_TEXT_BUFFER, NULL); +} diff --git a/src/shell-secure-text-buffer.h b/src/shell-secure-text-buffer.h new file mode 100644 index 0000000..6685b72 --- /dev/null +++ b/src/shell-secure-text-buffer.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ +/* shell-secure-text-buffer.h - secure memory clutter text buffer + + Copyright (C) 2009 Stefan Walter + Copyright (C) 2012 Red Hat Inc. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with the Gnome Library; see the file COPYING.LIB. If not, + see <http://www.gnu.org/licenses/>. + + Author: Stef Walter <stefw@gnome.org> +*/ + +#ifndef __SHELL_SECURE_TEXT_BUFFER_H__ +#define __SHELL_SECURE_TEXT_BUFFER_H__ + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_SECURE_TEXT_BUFFER (shell_secure_text_buffer_get_type ()) +G_DECLARE_FINAL_TYPE (ShellSecureTextBuffer, shell_secure_text_buffer, + SHELL, SECURE_TEXT_BUFFER, ClutterTextBuffer) + +ClutterTextBuffer * shell_secure_text_buffer_new (void); + +G_END_DECLS + +#endif /* __SHELL_SECURE_TEXT_BUFFER_H__ */ diff --git a/src/shell-square-bin.c b/src/shell-square-bin.c new file mode 100644 index 0000000..06587fb --- /dev/null +++ b/src/shell-square-bin.c @@ -0,0 +1,43 @@ +#include "config.h" + +#include "shell-square-bin.h" + +struct _ShellSquareBin +{ + /*< private >*/ + StBin parent_instance; +}; + +G_DEFINE_TYPE (ShellSquareBin, shell_square_bin, ST_TYPE_BIN); + +static void +shell_square_bin_get_preferred_width (ClutterActor *actor, + float for_height, + float *min_width_p, + float *natural_width_p) +{ + float min_width, nat_width; + + /* Return the actual height to keep the squared aspect */ + clutter_actor_get_preferred_height (actor, -1, + &min_width, &nat_width); + + if (min_width_p) + *min_width_p = min_width; + + if (natural_width_p) + *natural_width_p = nat_width; +} + +static void +shell_square_bin_init (ShellSquareBin *self) +{ +} + +static void +shell_square_bin_class_init (ShellSquareBinClass *klass) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + actor_class->get_preferred_width = shell_square_bin_get_preferred_width; +} diff --git a/src/shell-square-bin.h b/src/shell-square-bin.h new file mode 100644 index 0000000..2b7d4b2 --- /dev/null +++ b/src/shell-square-bin.h @@ -0,0 +1,13 @@ +#ifndef __SHELL_SQUARE_BIN_H__ +#define __SHELL_SQUARE_BIN_H__ + +#include <st/st.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_SQUARE_BIN (shell_square_bin_get_type ()) +G_DECLARE_FINAL_TYPE (ShellSquareBin, shell_square_bin, SHELL, SquareBin, StBin) + +G_END_DECLS + +#endif /* __SHELL_SQUARE_BIN_H__ */ diff --git a/src/shell-stack.c b/src/shell-stack.c new file mode 100644 index 0000000..79841e0 --- /dev/null +++ b/src/shell-stack.c @@ -0,0 +1,196 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/** + * SECTION:shell-stack + * @short_description: Pure "Z-axis" container class + * + * A #ShellStack draws its children on top of each other, + * aligned to the top left. It will be sized in width/height + * according to the largest such dimension of its children, and + * all children will be allocated that size. This differs + * from #ClutterGroup which allocates its children their natural + * size, even if that would overflow the size allocated to the stack. + */ + +#include "config.h" + +#include "shell-stack.h" + +struct _ShellStack +{ + StWidget parent; +}; + +G_DEFINE_TYPE (ShellStack, shell_stack, ST_TYPE_WIDGET); + +static void +shell_stack_allocate (ClutterActor *self, + const ClutterActorBox *box) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + ClutterActorBox content_box; + ClutterActor *child; + + clutter_actor_set_allocation (self, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + for (child = clutter_actor_get_first_child (self); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + { + ClutterActorBox child_box = content_box; + clutter_actor_allocate (child, &child_box); + } +} + +static void +shell_stack_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean first = TRUE; + float min = 0, natural = 0; + ClutterActor *child; + + st_theme_node_adjust_for_width (theme_node, &for_width); + + for (child = clutter_actor_get_first_child (actor); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + { + float child_min, child_natural; + + clutter_actor_get_preferred_height (child, + for_width, + &child_min, + &child_natural); + + if (first) + { + first = FALSE; + min = child_min; + natural = child_natural; + } + else + { + if (child_min > min) + min = child_min; + + if (child_natural > natural) + natural = child_natural; + } + } + + if (min_height_p) + *min_height_p = min; + + if (natural_height_p) + *natural_height_p = natural; + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +shell_stack_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean first = TRUE; + float min = 0, natural = 0; + ClutterActor *child; + + st_theme_node_adjust_for_height (theme_node, &for_height); + + for (child = clutter_actor_get_first_child (actor); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + { + float child_min, child_natural; + + clutter_actor_get_preferred_width (child, + for_height, + &child_min, + &child_natural); + + if (first) + { + first = FALSE; + min = child_min; + natural = child_natural; + } + else + { + if (child_min > min) + min = child_min; + + if (child_natural > natural) + natural = child_natural; + } + } + + if (min_width_p) + *min_width_p = min; + + if (natural_width_p) + *natural_width_p = natural; + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static gboolean +shell_stack_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction) +{ + ClutterActor *top_actor; + + /* If the stack is itself focusable, then focus into or out of + * it, as appropriate. + */ + if (st_widget_get_can_focus (widget)) + { + if (from && clutter_actor_contains (CLUTTER_ACTOR (widget), from)) + return FALSE; + + if (clutter_actor_is_mapped (CLUTTER_ACTOR (widget))) + { + clutter_actor_grab_key_focus (CLUTTER_ACTOR (widget)); + return TRUE; + } + else + { + return FALSE; + } + } + + top_actor = clutter_actor_get_last_child (CLUTTER_ACTOR (widget)); + while (top_actor && !clutter_actor_is_visible (top_actor)) + top_actor = clutter_actor_get_previous_sibling (top_actor); + if (ST_IS_WIDGET (top_actor)) + return st_widget_navigate_focus (ST_WIDGET (top_actor), from, direction, FALSE); + else + return FALSE; +} + +static void +shell_stack_class_init (ShellStackClass *klass) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + actor_class->get_preferred_width = shell_stack_get_preferred_width; + actor_class->get_preferred_height = shell_stack_get_preferred_height; + actor_class->allocate = shell_stack_allocate; + + widget_class->navigate_focus = shell_stack_navigate_focus; +} + +static void +shell_stack_init (ShellStack *actor) +{ +} diff --git a/src/shell-stack.h b/src/shell-stack.h new file mode 100644 index 0000000..e54160d --- /dev/null +++ b/src/shell-stack.h @@ -0,0 +1,11 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_STACK_H__ +#define __SHELL_STACK_H__ + +#include "st.h" +#include <gtk/gtk.h> + +#define SHELL_TYPE_STACK (shell_stack_get_type ()) +G_DECLARE_FINAL_TYPE (ShellStack, shell_stack, SHELL, STACK, StWidget) + +#endif /* __SHELL_STACK_H__ */ diff --git a/src/shell-tray-icon.c b/src/shell-tray-icon.c new file mode 100644 index 0000000..14b2191 --- /dev/null +++ b/src/shell-tray-icon.c @@ -0,0 +1,294 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-tray-icon.h" +#include "shell-gtk-embed.h" +#include "tray/na-tray-child.h" +#include <gdk/gdkx.h> +#include <X11/Xatom.h> +#include "st.h" + +enum { + PROP_0, + + PROP_PID, + PROP_TITLE, + PROP_WM_CLASS +}; + +typedef struct _ShellTrayIconPrivate ShellTrayIconPrivate; + +struct _ShellTrayIcon +{ + ShellGtkEmbed parent; + + ShellTrayIconPrivate *priv; +}; + +struct _ShellTrayIconPrivate +{ + NaTrayChild *socket; + + pid_t pid; + char *title, *wm_class; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellTrayIcon, shell_tray_icon, SHELL_TYPE_GTK_EMBED); + +static void +shell_tray_icon_finalize (GObject *object) +{ + ShellTrayIcon *icon = SHELL_TRAY_ICON (object); + + g_free (icon->priv->title); + g_free (icon->priv->wm_class); + + G_OBJECT_CLASS (shell_tray_icon_parent_class)->finalize (object); +} + +static void +shell_tray_icon_constructed (GObject *object) +{ + GdkWindow *icon_app_window; + ShellTrayIcon *icon = SHELL_TRAY_ICON (object); + ShellEmbeddedWindow *window; + GdkDisplay *display; + Window plug_xid; + Atom _NET_WM_PID, type; + int result, format; + gulong nitems, bytes_after, *val = NULL; + + /* We do all this now rather than computing it on the fly later, + * because the shell may want to see their values from a + * tray-icon-removed signal handler, at which point the plug has + * already been removed from the socket. + */ + + g_object_get (object, "window", &window, NULL); + g_return_if_fail (window != NULL); + icon->priv->socket = NA_TRAY_CHILD (gtk_bin_get_child (GTK_BIN (window))); + g_object_unref (window); + + icon->priv->title = na_tray_child_get_title (icon->priv->socket); + na_tray_child_get_wm_class (icon->priv->socket, NULL, &icon->priv->wm_class); + + icon_app_window = gtk_socket_get_plug_window (GTK_SOCKET (icon->priv->socket)); + plug_xid = GDK_WINDOW_XID (icon_app_window); + + display = gtk_widget_get_display (GTK_WIDGET (icon->priv->socket)); + gdk_x11_display_error_trap_push (display); + _NET_WM_PID = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_PID"); + result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), plug_xid, + _NET_WM_PID, 0, G_MAXLONG, False, XA_CARDINAL, + &type, &format, &nitems, + &bytes_after, (guchar **)&val); + if (!gdk_x11_display_error_trap_pop (display) && + result == Success && + type == XA_CARDINAL && + nitems == 1) + icon->priv->pid = *val; + + if (val) + XFree (val); +} + +static void +shell_tray_icon_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellTrayIcon *icon = SHELL_TRAY_ICON (object); + + switch (prop_id) + { + case PROP_PID: + g_value_set_uint (value, icon->priv->pid); + break; + + case PROP_TITLE: + g_value_set_string (value, icon->priv->title); + break; + + case PROP_WM_CLASS: + g_value_set_string (value, icon->priv->wm_class); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_tray_icon_class_init (ShellTrayIconClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = shell_tray_icon_get_property; + object_class->constructed = shell_tray_icon_constructed; + object_class->finalize = shell_tray_icon_finalize; + + g_object_class_install_property (object_class, + PROP_PID, + g_param_spec_uint ("pid", + "PID", + "The PID of the icon's application", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_TITLE, + g_param_spec_string ("title", + "Title", + "The icon's window title", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, + PROP_WM_CLASS, + g_param_spec_string ("wm-class", + "WM Class", + "The icon's window WM_CLASS", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); +} + +static void +shell_tray_icon_init (ShellTrayIcon *icon) +{ + icon->priv = shell_tray_icon_get_instance_private (icon); +} + +/* + * Public API + */ +ClutterActor * +shell_tray_icon_new (ShellEmbeddedWindow *window) +{ + g_return_val_if_fail (SHELL_IS_EMBEDDED_WINDOW (window), NULL); + + return g_object_new (SHELL_TYPE_TRAY_ICON, + "window", window, + NULL); +} + +/** + * shell_tray_icon_click: + * @icon: a #ShellTrayIcon + * @event: the #ClutterEvent triggering the fake click + * + * Fakes a press and release on @icon. @event must be a + * %CLUTTER_BUTTON_RELEASE, %CLUTTER_KEY_PRESS or %CLUTTER_KEY_RELEASE event. + * Its relevant details will be passed on to the icon, but its + * coordinates will be ignored; the click is + * always made on the center of @icon. + */ +void +shell_tray_icon_click (ShellTrayIcon *icon, + ClutterEvent *event) +{ + XKeyEvent xkevent; + XButtonEvent xbevent; + XCrossingEvent xcevent; + GdkDisplay *display; + GdkWindow *remote_window; + GdkScreen *screen; + int x_root, y_root; + Display *xdisplay; + Window xwindow, xrootwindow; + ClutterEventType event_type = clutter_event_type (event); + + g_return_if_fail (event_type == CLUTTER_BUTTON_RELEASE || + event_type == CLUTTER_KEY_PRESS || + event_type == CLUTTER_KEY_RELEASE); + + remote_window = gtk_socket_get_plug_window (GTK_SOCKET (icon->priv->socket)); + if (remote_window == NULL) + { + g_warning ("shell tray: plug window is gone"); + return; + } + xdisplay = GDK_WINDOW_XDISPLAY (remote_window); + + display = gdk_x11_lookup_xdisplay (xdisplay); + gdk_x11_display_error_trap_push (display); + + xwindow = GDK_WINDOW_XID (remote_window); + screen = gdk_window_get_screen (remote_window); + xrootwindow = GDK_WINDOW_XID (gdk_screen_get_root_window (screen)); + gdk_window_get_origin (remote_window, &x_root, &y_root); + + + /* First make the icon believe the pointer is inside it */ + xcevent.type = EnterNotify; + xcevent.window = xwindow; + xcevent.root = xrootwindow; + xcevent.subwindow = None; + xcevent.time = clutter_event_get_time (event); + xcevent.x = gdk_window_get_width (remote_window) / 2; + xcevent.y = gdk_window_get_height (remote_window) / 2; + xcevent.x_root = x_root + xcevent.x; + xcevent.y_root = y_root + xcevent.y; + xcevent.mode = NotifyNormal; + xcevent.detail = NotifyNonlinear; + xcevent.same_screen = True; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xcevent); + + /* Now do the click */ + if (event_type == CLUTTER_BUTTON_RELEASE) + { + xbevent.window = xwindow; + xbevent.root = xrootwindow; + xbevent.subwindow = None; + xbevent.time = xcevent.time; + xbevent.x = xcevent.x; + xbevent.y = xcevent.y; + xbevent.x_root = xcevent.x_root; + xbevent.y_root = xcevent.y_root; + xbevent.state = clutter_event_get_state (event); + xbevent.same_screen = True; + xbevent.type = ButtonPress; + xbevent.button = clutter_event_get_button (event); + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xbevent); + + xbevent.type = ButtonRelease; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xbevent); + } + else + { + xkevent.window = xwindow; + xkevent.root = xrootwindow; + xkevent.subwindow = None; + xkevent.time = xcevent.time; + xkevent.x = xcevent.x; + xkevent.y = xcevent.y; + xkevent.x_root = xcevent.x_root; + xkevent.y_root = xcevent.y_root; + xkevent.state = clutter_event_get_state (event); + xkevent.same_screen = True; + xkevent.keycode = clutter_event_get_key_code (event); + + xkevent.type = KeyPress; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xkevent); + + if (event_type == CLUTTER_KEY_RELEASE) + { + /* If the application takes a grab on KeyPress, we don't + * want to send it a KeyRelease. There's no good way of + * knowing whether a tray icon will take a grab, so just + * assume it does, and don't send the KeyRelease. That might + * make the tracking for key events messed up if it doesn't take + * a grab, but the tray icon won't get key focus in normal cases, + * so let's hope this isn't too damaging... + */ + xkevent.type = KeyRelease; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xkevent); + } + } + + /* And move the pointer back out */ + xcevent.type = LeaveNotify; + XSendEvent (xdisplay, xwindow, False, 0, (XEvent *)&xcevent); + + gdk_x11_display_error_trap_pop_ignored (display); +} diff --git a/src/shell-tray-icon.h b/src/shell-tray-icon.h new file mode 100644 index 0000000..f0e1191 --- /dev/null +++ b/src/shell-tray-icon.h @@ -0,0 +1,16 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_TRAY_ICON_H__ +#define __SHELL_TRAY_ICON_H__ + +#include "shell-gtk-embed.h" + +#define SHELL_TYPE_TRAY_ICON (shell_tray_icon_get_type ()) +G_DECLARE_FINAL_TYPE (ShellTrayIcon, shell_tray_icon, + SHELL, TRAY_ICON, ShellGtkEmbed) + +ClutterActor *shell_tray_icon_new (ShellEmbeddedWindow *window); + +void shell_tray_icon_click (ShellTrayIcon *icon, + ClutterEvent *event); + +#endif /* __SHELL_TRAY_ICON_H__ */ diff --git a/src/shell-tray-manager.c b/src/shell-tray-manager.c new file mode 100644 index 0000000..c8e3259 --- /dev/null +++ b/src/shell-tray-manager.c @@ -0,0 +1,374 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <clutter/clutter.h> +#include <girepository.h> +#include <gtk/gtk.h> +#include <meta/display.h> + +#include "shell-tray-manager.h" +#include "na-tray-manager.h" + +#include "shell-tray-icon.h" +#include "shell-embedded-window.h" +#include "shell-global.h" + +typedef struct _ShellTrayManagerPrivate ShellTrayManagerPrivate; + +struct _ShellTrayManager +{ + GObject parent_instance; + + ShellTrayManagerPrivate *priv; +}; + +struct _ShellTrayManagerPrivate { + NaTrayManager *na_manager; + ClutterColor bg_color; + + GHashTable *icons; + StWidget *theme_widget; +}; + +typedef struct { + ShellTrayManager *manager; + GtkWidget *socket; + GtkWidget *window; + ClutterActor *actor; +} ShellTrayManagerChild; + +enum { + PROP_0, + + PROP_BG_COLOR +}; + +/* Signals */ +enum +{ + TRAY_ICON_ADDED, + TRAY_ICON_REMOVED, + LAST_SIGNAL +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellTrayManager, shell_tray_manager, G_TYPE_OBJECT); + +static guint shell_tray_manager_signals [LAST_SIGNAL] = { 0 }; + +static const ClutterColor default_color = { 0x00, 0x00, 0x00, 0xff }; + +static void shell_tray_manager_release_resources (ShellTrayManager *manager); + +static void na_tray_icon_added (NaTrayManager *na_manager, GtkWidget *child, gpointer manager); +static void na_tray_icon_removed (NaTrayManager *na_manager, GtkWidget *child, gpointer manager); + +static void +free_tray_icon (gpointer data) +{ + ShellTrayManagerChild *child = data; + + gtk_widget_destroy (child->window); + if (child->actor) + { + g_signal_handlers_disconnect_matched (child->actor, G_SIGNAL_MATCH_DATA, + 0, 0, NULL, NULL, child); + g_object_unref (child->actor); + } + g_free (child); +} + +static void +shell_tray_manager_set_property(GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellTrayManager *manager = SHELL_TRAY_MANAGER (object); + + switch (prop_id) + { + case PROP_BG_COLOR: + { + ClutterColor *color = g_value_get_boxed (value); + if (color) + manager->priv->bg_color = *color; + else + manager->priv->bg_color = default_color; + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_tray_manager_get_property(GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellTrayManager *manager = SHELL_TRAY_MANAGER (object); + + switch (prop_id) + { + case PROP_BG_COLOR: + g_value_set_boxed (value, &manager->priv->bg_color); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_tray_manager_init (ShellTrayManager *manager) +{ + manager->priv = shell_tray_manager_get_instance_private (manager); + + manager->priv->bg_color = default_color; +} + +static void +shell_tray_manager_finalize (GObject *object) +{ + ShellTrayManager *manager = SHELL_TRAY_MANAGER (object); + + shell_tray_manager_release_resources (manager); + + G_OBJECT_CLASS (shell_tray_manager_parent_class)->finalize (object); +} + +static void +shell_tray_manager_class_init (ShellTrayManagerClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_tray_manager_finalize; + gobject_class->set_property = shell_tray_manager_set_property; + gobject_class->get_property = shell_tray_manager_get_property; + + shell_tray_manager_signals[TRAY_ICON_ADDED] = + g_signal_new ("tray-icon-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + CLUTTER_TYPE_ACTOR); + shell_tray_manager_signals[TRAY_ICON_REMOVED] = + g_signal_new ("tray-icon-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + CLUTTER_TYPE_ACTOR); + + /* Lifting the CONSTRUCT_ONLY here isn't hard; you just need to + * iterate through the icons, reset the background pixmap, and + * call na_tray_child_force_redraw() + */ + g_object_class_install_property (gobject_class, + PROP_BG_COLOR, + g_param_spec_boxed ("bg-color", + "BG Color", + "Background color (only if we don't have transparency)", + CLUTTER_TYPE_COLOR, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +ShellTrayManager * +shell_tray_manager_new (void) +{ + return g_object_new (SHELL_TYPE_TRAY_MANAGER, NULL); +} + +static void +shell_tray_manager_ensure_resources (ShellTrayManager *manager) +{ + if (manager->priv->na_manager != NULL) + return; + + manager->priv->icons = g_hash_table_new_full (NULL, NULL, + NULL, free_tray_icon); + + manager->priv->na_manager = na_tray_manager_new (); + + g_signal_connect (manager->priv->na_manager, "tray-icon-added", + G_CALLBACK (na_tray_icon_added), manager); + g_signal_connect (manager->priv->na_manager, "tray-icon-removed", + G_CALLBACK (na_tray_icon_removed), manager); +} + +static void +shell_tray_manager_release_resources (ShellTrayManager *manager) +{ + g_clear_object (&manager->priv->na_manager); + g_clear_pointer (&manager->priv->icons, g_hash_table_destroy); +} + +static void +shell_tray_manager_style_changed (StWidget *theme_widget, + gpointer user_data) +{ + ShellTrayManager *manager = user_data; + StThemeNode *theme_node; + StIconColors *icon_colors; + + if (manager->priv->na_manager == NULL) + return; + + theme_node = st_widget_get_theme_node (theme_widget); + icon_colors = st_theme_node_get_icon_colors (theme_node); + na_tray_manager_set_colors (manager->priv->na_manager, + &icon_colors->foreground, &icon_colors->warning, + &icon_colors->error, &icon_colors->success); +} + +static void +shell_tray_manager_manage_screen_internal (ShellTrayManager *manager) +{ + shell_tray_manager_ensure_resources (manager); + na_tray_manager_manage_screen (manager->priv->na_manager); +} + +void +shell_tray_manager_manage_screen (ShellTrayManager *manager, + StWidget *theme_widget) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + g_set_weak_pointer (&manager->priv->theme_widget, theme_widget); + + if (meta_display_get_x11_display (display) != NULL) + shell_tray_manager_manage_screen_internal (manager); + + g_signal_connect_object (display, "x11-display-setup", + G_CALLBACK (shell_tray_manager_manage_screen_internal), + manager, G_CONNECT_SWAPPED); + g_signal_connect_object (display, "x11-display-closing", + G_CALLBACK (shell_tray_manager_release_resources), + manager, G_CONNECT_SWAPPED); + + g_signal_connect_object (theme_widget, "style-changed", + G_CALLBACK (shell_tray_manager_style_changed), + manager, 0); + shell_tray_manager_style_changed (theme_widget, manager); +} + +void +shell_tray_manager_unmanage_screen (ShellTrayManager *manager) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + g_signal_handlers_disconnect_by_data (display, manager); + + if (manager->priv->theme_widget != NULL) + { + g_signal_handlers_disconnect_by_func (manager->priv->theme_widget, + G_CALLBACK (shell_tray_manager_style_changed), + manager); + } + g_set_weak_pointer (&manager->priv->theme_widget, NULL); + + shell_tray_manager_release_resources (manager); +} + +static void +shell_tray_manager_child_on_realize (GtkWidget *widget, + ShellTrayManagerChild *child) +{ + /* If the tray child is using an RGBA colormap (and so we have real + * transparency), we don't need to worry about the background. If + * not, we obey the bg-color property by creating a cairo pattern of + * that color and setting it as our background. Then "parent-relative" + * background on the socket and the plug within that will cause + * the icons contents to appear on top of our background color. + */ + if (!na_tray_child_has_alpha (NA_TRAY_CHILD (child->socket))) + { + ClutterColor color = child->manager->priv->bg_color; + cairo_pattern_t *bg_pattern; + + bg_pattern = cairo_pattern_create_rgb (color.red / 255., + color.green / 255., + color.blue / 255.); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gdk_window_set_background_pattern (gtk_widget_get_window (widget), + bg_pattern); +G_GNUC_END_IGNORE_DEPRECATIONS + + cairo_pattern_destroy (bg_pattern); + } +} + +static void +on_plug_added (GtkSocket *socket, + ShellTrayManager *manager) +{ + ShellTrayManagerChild *child; + + g_signal_handlers_disconnect_by_func (socket, on_plug_added, manager); + + child = g_hash_table_lookup (manager->priv->icons, socket); + + child->actor = shell_tray_icon_new (SHELL_EMBEDDED_WINDOW (child->window)); + g_object_ref_sink (child->actor); + + g_signal_emit (manager, shell_tray_manager_signals[TRAY_ICON_ADDED], 0, + child->actor); +} + +static void +na_tray_icon_added (NaTrayManager *na_manager, GtkWidget *socket, + gpointer user_data) +{ + ShellTrayManager *manager = user_data; + GtkWidget *win; + ShellTrayManagerChild *child; + + win = shell_embedded_window_new (); + gtk_container_add (GTK_CONTAINER (win), socket); + + /* The visual of the socket matches that of its contents; make + * the window we put it in match that as well */ + gtk_widget_set_visual (win, gtk_widget_get_visual (socket)); + + child = g_new0 (ShellTrayManagerChild, 1); + child->manager = manager; + child->window = win; + child->socket = socket; + + g_signal_connect (win, "realize", + G_CALLBACK (shell_tray_manager_child_on_realize), child); + + gtk_widget_show_all (win); + + g_hash_table_insert (manager->priv->icons, socket, child); + + g_signal_connect (socket, "plug-added", G_CALLBACK (on_plug_added), manager); +} + +static void +na_tray_icon_removed (NaTrayManager *na_manager, GtkWidget *socket, + gpointer user_data) +{ + ShellTrayManager *manager = user_data; + ShellTrayManagerChild *child; + + child = g_hash_table_lookup (manager->priv->icons, socket); + g_return_if_fail (child != NULL); + + if (child->actor != NULL) + { + /* Only emit signal if a corresponding tray-icon-added signal was emitted, + that is, if embedding did not fail and we got a plug-added + */ + g_signal_emit (manager, + shell_tray_manager_signals[TRAY_ICON_REMOVED], 0, + child->actor); + } + g_hash_table_remove (manager->priv->icons, socket); +} diff --git a/src/shell-tray-manager.h b/src/shell-tray-manager.h new file mode 100644 index 0000000..d6279d4 --- /dev/null +++ b/src/shell-tray-manager.h @@ -0,0 +1,22 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#ifndef __SHELL_TRAY_MANAGER_H__ +#define __SHELL_TRAY_MANAGER_H__ + +#include <clutter/clutter.h> +#include "st.h" + +G_BEGIN_DECLS + +#define SHELL_TYPE_TRAY_MANAGER (shell_tray_manager_get_type ()) +G_DECLARE_FINAL_TYPE (ShellTrayManager, shell_tray_manager, + SHELL, TRAY_MANAGER, GObject) + +ShellTrayManager *shell_tray_manager_new (void); +void shell_tray_manager_manage_screen (ShellTrayManager *manager, + StWidget *theme_widget); +void shell_tray_manager_unmanage_screen (ShellTrayManager *manager); + +G_END_DECLS + +#endif /* __SHELL_TRAY_MANAGER_H__ */ diff --git a/src/shell-util.c b/src/shell-util.c new file mode 100644 index 0000000..aeb81a1 --- /dev/null +++ b/src/shell-util.c @@ -0,0 +1,854 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <errno.h> +#include <math.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/wait.h> +#ifdef HAVE_SYS_RESOURCE_H +#include <sys/resource.h> +#endif + +#include <GL/gl.h> +#include <cogl/cogl.h> + +#include "shell-app-cache-private.h" +#include "shell-util.h" +#include <glib/gi18n-lib.h> +#include <gtk/gtk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <meta/display.h> +#include <meta/meta-x11-display.h> + +#include <locale.h> +#ifdef HAVE__NL_TIME_FIRST_WEEKDAY +#include <langinfo.h> +#endif + +#ifdef HAVE_SYSTEMD +#include <systemd/sd-daemon.h> +#include <systemd/sd-login.h> +#else +/* So we don't need to add ifdef's everywhere */ +#define sd_notify(u, m) do {} while (0) +#define sd_notifyf(u, m, ...) do {} while (0) +#endif + +static void +stop_pick (ClutterActor *actor) +{ + g_signal_stop_emission_by_name (actor, "pick"); +} + +/** + * shell_util_set_hidden_from_pick: + * @actor: A #ClutterActor + * @hidden: Whether @actor should be hidden from pick + * + * If @hidden is %TRUE, hide @actor from pick even with a mode of + * %CLUTTER_PICK_ALL; if @hidden is %FALSE, unhide @actor. + */ +void +shell_util_set_hidden_from_pick (ClutterActor *actor, + gboolean hidden) +{ + gpointer existing_handler_data; + + existing_handler_data = g_object_get_data (G_OBJECT (actor), + "shell-stop-pick"); + if (hidden) + { + if (existing_handler_data != NULL) + return; + g_signal_connect (actor, "pick", G_CALLBACK (stop_pick), NULL); + g_object_set_data (G_OBJECT (actor), + "shell-stop-pick", GUINT_TO_POINTER (1)); + } + else + { + if (existing_handler_data == NULL) + return; + g_signal_handlers_disconnect_by_func (actor, stop_pick, NULL); + g_object_set_data (G_OBJECT (actor), "shell-stop-pick", NULL); + } +} + +/** + * shell_util_get_week_start: + * + * Gets the first week day for the current locale, expressed as a + * number in the range 0..6, representing week days from Sunday to + * Saturday. + * + * Returns: A number representing the first week day for the current + * locale + */ +/* Copied from gtkcalendar.c */ +int +shell_util_get_week_start (void) +{ + int week_start; +#ifdef HAVE__NL_TIME_FIRST_WEEKDAY + union { unsigned int word; char *string; } langinfo; + int week_1stday = 0; + int first_weekday = 1; + guint week_origin; +#else + char *gtk_week_start; +#endif + +#ifdef HAVE__NL_TIME_FIRST_WEEKDAY + langinfo.string = nl_langinfo (_NL_TIME_FIRST_WEEKDAY); + first_weekday = langinfo.string[0]; + langinfo.string = nl_langinfo (_NL_TIME_WEEK_1STDAY); + week_origin = langinfo.word; + if (week_origin == 19971130) /* Sunday */ + week_1stday = 0; + else if (week_origin == 19971201) /* Monday */ + week_1stday = 1; + else + g_warning ("Unknown value of _NL_TIME_WEEK_1STDAY.\n"); + + week_start = (week_1stday + first_weekday - 1) % 7; +#else + /* Use a define to hide the string from xgettext */ +# define GTK_WEEK_START "calendar:week_start:0" + gtk_week_start = dgettext ("gtk30", GTK_WEEK_START); + + if (strncmp (gtk_week_start, "calendar:week_start:", 20) == 0) + week_start = *(gtk_week_start + 20) - '0'; + else + week_start = -1; + + if (week_start < 0 || week_start > 6) + { + g_warning ("Whoever translated calendar:week_start:0 for GTK+ " + "did so wrongly.\n"); + return 0; + } +#endif + + return week_start; +} + +/** + * shell_util_translate_time_string: + * @str: String to translate + * + * Translate @str according to the locale defined by LC_TIME; unlike + * dcgettext(), the translations is still taken from the LC_MESSAGES + * catalogue and not the LC_TIME one. + * + * Returns: the translated string + */ +const char * +shell_util_translate_time_string (const char *str) +{ + const char *locale = g_getenv ("LC_TIME"); + const char *res; + char *sep; + locale_t old_loc; + locale_t loc = (locale_t) 0; + + if (locale) + loc = newlocale (LC_MESSAGES_MASK, locale, (locale_t) 0); + + old_loc = uselocale (loc); + + sep = strchr (str, '\004'); + res = g_dpgettext (NULL, str, sep ? sep - str + 1 : 0); + + uselocale (old_loc); + + if (loc != (locale_t) 0) + freelocale (loc); + + return res; +} + +/** + * shell_util_regex_escape: + * @str: a UTF-8 string to escape + * + * A wrapper around g_regex_escape_string() that takes its argument as + * \0-terminated string rather than a byte-array that confuses gjs. + * + * Returns: @str with all regex-special characters escaped + */ +char * +shell_util_regex_escape (const char *str) +{ + return g_regex_escape_string (str, -1); +} + +/** + * shell_write_string_to_stream: + * @stream: a #GOutputStream + * @str: a UTF-8 string to write to @stream + * @error: location to store GError + * + * Write a string to a GOutputStream as UTF-8. This is a workaround + * for not having binary buffers in GJS. + * + * Return value: %TRUE if write succeeded + */ +gboolean +shell_write_string_to_stream (GOutputStream *stream, + const char *str, + GError **error) +{ + return g_output_stream_write_all (stream, str, strlen (str), + NULL, NULL, error); +} + +/** + * shell_get_file_contents_utf8_sync: + * @path: UTF-8 encoded filename path + * @error: a #GError + * + * Synchronously load the contents of a file as a NUL terminated + * string, validating it as UTF-8. Embedded NUL characters count as + * invalid content. + * + * Returns: (transfer full): File contents + */ +char * +shell_get_file_contents_utf8_sync (const char *path, + GError **error) +{ + char *contents; + gsize len; + if (!g_file_get_contents (path, &contents, &len, error)) + return NULL; + if (!g_utf8_validate (contents, len, NULL)) + { + g_free (contents); + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "File %s contains invalid UTF-8", + path); + return NULL; + } + return contents; +} + +static void +touch_file (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + GFile *file = object; + g_autoptr (GFile) parent = NULL; + g_autoptr (GFileOutputStream) stream = NULL; + GError *error = NULL; + + parent = g_file_get_parent (file); + g_file_make_directory_with_parents (parent, cancellable, &error); + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + { + g_task_return_error (task, error); + return; + } + g_clear_error (&error); + + stream = g_file_create (file, G_FILE_CREATE_NONE, cancellable, &error); + + if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS)) + { + g_task_return_error (task, error); + return; + } + g_clear_error (&error); + + if (stream) + g_output_stream_close (G_OUTPUT_STREAM (stream), NULL, NULL); + + g_task_return_boolean (task, stream != NULL); +} + +void +shell_util_touch_file_async (GFile *file, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + + g_return_if_fail (G_IS_FILE (file)); + + task = g_task_new (file, NULL, callback, user_data); + g_task_set_source_tag (task, shell_util_touch_file_async); + + g_task_run_in_thread (task, touch_file); +} + +gboolean +shell_util_touch_file_finish (GFile *file, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (G_IS_FILE (file), FALSE); + g_return_val_if_fail (G_IS_TASK (res), FALSE); + + return g_task_propagate_boolean (G_TASK (res), error); +} + +/** + * shell_util_wifexited: + * @status: the status returned by wait() or waitpid() + * @exit: (out): the actual exit status of the process + * + * Implements libc standard WIFEXITED, that cannot be used JS + * code. + * Returns: TRUE if the process exited normally, FALSE otherwise + */ +gboolean +shell_util_wifexited (int status, + int *exit) +{ + gboolean ret; + + ret = WIFEXITED(status); + + if (ret) + *exit = WEXITSTATUS(status); + + return ret; +} + +/** + * shell_util_create_pixbuf_from_data: + * @data: (array length=len) (element-type guint8) (transfer full): + * @len: + * @colorspace: + * @has_alpha: + * @bits_per_sample: + * @width: + * @height: + * @rowstride: + * + * Workaround for non-introspectability of gdk_pixbuf_from_data(). + * + * Returns: (transfer full): + */ +GdkPixbuf * +shell_util_create_pixbuf_from_data (const guchar *data, + gsize len, + GdkColorspace colorspace, + gboolean has_alpha, + int bits_per_sample, + int width, + int height, + int rowstride) +{ + return gdk_pixbuf_new_from_data (data, colorspace, has_alpha, + bits_per_sample, width, height, rowstride, + (GdkPixbufDestroyNotify) g_free, NULL); +} + +typedef const gchar *(*ShellGLGetString) (GLenum); + +cairo_surface_t * +shell_util_composite_capture_images (ClutterCapture *captures, + int n_captures, + int x, + int y, + int target_width, + int target_height, + float target_scale) +{ + int i; + cairo_format_t format; + cairo_surface_t *image; + cairo_t *cr; + + g_assert (n_captures > 0); + g_assert (target_scale > 0.0f); + + format = cairo_image_surface_get_format (captures[0].image); + image = cairo_image_surface_create (format, target_width, target_height); + cairo_surface_set_device_scale (image, target_scale, target_scale); + + cr = cairo_create (image); + + for (i = 0; i < n_captures; i++) + { + ClutterCapture *capture = &captures[i]; + + cairo_save (cr); + + cairo_translate (cr, + capture->rect.x - x, + capture->rect.y - y); + cairo_set_source_surface (cr, capture->image, 0, 0); + cairo_paint (cr); + + cairo_restore (cr); + } + cairo_destroy (cr); + + return image; +} + +#ifndef HAVE_FDWALK +static int +fdwalk (int (*cb)(void *data, int fd), + void *data) +{ + gint open_max; + gint fd; + gint res = 0; + +#ifdef HAVE_SYS_RESOURCE_H + struct rlimit rl; +#endif + +#ifdef __linux__ + DIR *d; + + if ((d = opendir("/proc/self/fd"))) + { + struct dirent *de; + + while ((de = readdir(d))) + { + glong l; + gchar *e = NULL; + + if (de->d_name[0] == '.') + continue; + + errno = 0; + l = strtol(de->d_name, &e, 10); + if (errno != 0 || !e || *e) + continue; + + fd = (gint) l; + + if ((glong) fd != l) + continue; + + if (fd == dirfd(d)) + continue; + + if ((res = cb (data, fd)) != 0) + break; + } + + closedir(d); + return res; + } + + /* If /proc is not mounted or not accessible we fall back to the old + * rlimit trick */ + +#endif + +#ifdef HAVE_SYS_RESOURCE_H + if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) + open_max = rl.rlim_max; + else +#endif + open_max = sysconf (_SC_OPEN_MAX); + + for (fd = 0; fd < open_max; fd++) + if ((res = cb (data, fd)) != 0) + break; + + return res; +} +#endif + +static int +check_cloexec (void *data, + gint fd) +{ + int r; + + if (fd < 3) + return 0; + + r = fcntl (fd, F_GETFD); + if (r < 0) + return 0; + + if (!(r & FD_CLOEXEC)) + g_warning ("fd %d is not CLOEXEC", fd); + + return 0; +} + +/** + * shell_util_check_cloexec_fds: + * + * Walk over all open file descriptors. Check them for the FD_CLOEXEC flag. + * If this flag is not set, log the offending file descriptor number. + * + * It is important that gnome-shell's file descriptors are all marked CLOEXEC, + * so that the shell's open file descriptors are not passed to child processes + * that we launch. + */ +void +shell_util_check_cloexec_fds (void) +{ + fdwalk (check_cloexec, NULL); + g_info ("Open fd CLOEXEC check complete"); +} + +/** + * shell_util_get_uid: + * + * A wrapper around getuid() so that it can be used from JavaScript. This + * function will always succeed. + * + * Returns: the real user ID of the calling process + */ +gint +shell_util_get_uid (void) +{ + return getuid (); +} + +typedef enum { + SYSTEMD_CALL_FLAGS_NONE = 0, + SYSTEMD_CALL_FLAGS_WATCH_JOB = 1 << 0, +} SystemdFlags; + +#ifdef HAVE_SYSTEMD +typedef struct { + GDBusConnection *connection; + gchar *command; + SystemdFlags flags; + + GCancellable *cancellable; + gulong cancel_id; + + guint job_watch; + gchar *job; +} SystemdCall; + +static void +shell_util_systemd_call_data_free (SystemdCall *data) +{ + if (data->job_watch) + { + g_dbus_connection_signal_unsubscribe (data->connection, data->job_watch); + data->job_watch = 0; + } + + if (data->cancellable) + { + g_cancellable_disconnect (data->cancellable, data->cancel_id); + g_clear_object (&data->cancellable); + data->cancel_id = 0; + } + + g_clear_object (&data->connection); + g_clear_pointer (&data->job, g_free); + g_clear_pointer (&data->command, g_free); + g_free (data); +} + +static void +shell_util_systemd_call_cancelled_cb (GCancellable *cancellable, + GTask *task) +{ + SystemdCall *data = g_task_get_task_data (task); + + /* Task has returned, but data is not yet free'ed, ignore signal. */ + if (g_task_get_completed (task)) + return; + + /* We are still in the DBus call; it will return the error. */ + if (data->job == NULL) + return; + + g_task_return_error_if_cancelled (task); + g_object_unref (task); +} + +static void +on_systemd_call_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GVariant) reply = NULL; + g_autoptr (GError) error = NULL; + GTask *task = G_TASK (user_data); + SystemdCall *data; + + reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), + res, &error); + + data = g_task_get_task_data (task); + + if (error) { + g_warning ("Could not issue '%s' systemd call", data->command); + g_task_return_error (task, g_steal_pointer (&error)); + g_object_unref (task); + + return; + } + + g_assert (data->job == NULL); + g_variant_get (reply, "(o)", &data->job); + + /* we should either wait for the JobRemoved notification, or + * notify here */ + if ((data->flags & SYSTEMD_CALL_FLAGS_WATCH_JOB) == 0) + g_task_return_boolean (task, TRUE); +} + +static void +on_systemd_job_removed_cb (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + GTask *task = G_TASK (user_data); + SystemdCall *data; + guint32 id; + const char *path, *unit, *result; + + /* Task has returned, but data is not yet free'ed, ignore signal. */ + if (g_task_get_completed (task)) + return; + + data = g_task_get_task_data (task); + + /* No job information yet, ignore. */ + if (data->job == NULL) + return; + + g_variant_get (parameters, "(u&o&s&s)", &id, &path, &unit, &result); + + /* Is it the job we are waiting for? */ + if (g_strcmp0 (path, data->job) != 0) + return; + + /* Task has completed; return the result of the job */ + if (g_strcmp0 (result, "done") == 0) + g_task_return_boolean (task, TRUE); + else + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Systemd job completed with status \"%s\"", + result); + + g_object_unref (task); +} +#endif /* HAVE_SYSTEMD */ + +static void +shell_util_systemd_call (const char *command, + GVariant *params, + SystemdFlags flags, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = g_task_new (NULL, cancellable, callback, user_data); + +#ifdef HAVE_SYSTEMD + g_autoptr (GDBusConnection) connection = NULL; + GError *error = NULL; + SystemdCall *data; + g_autofree char *self_unit = NULL; + int res; + + connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + if (connection == NULL) { + g_task_return_error (task, error); + return; + } + + /* We look up the systemd unit that our own process is running in here. + * This way we determine whether the session is managed using systemd. + */ + res = sd_pid_get_user_unit (getpid (), &self_unit); + + if (res == -ENODATA) + { + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "Not systemd managed"); + return; + } + else if (res < 0) + { + g_task_return_new_error (task, + G_IO_ERROR, + g_io_error_from_errno (-res), + "Error fetching own systemd unit: %s", + g_strerror (-res)); + return; + } + + data = g_new0 (SystemdCall, 1); + data->command = g_strdup (command); + data->connection = g_object_ref (connection); + data->flags = flags; + + if ((data->flags & SYSTEMD_CALL_FLAGS_WATCH_JOB) != 0) + { + data->job_watch = g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.systemd1", + "org.freedesktop.systemd1.Manager", + "JobRemoved", + "/org/freedesktop/systemd1", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_systemd_job_removed_cb, + task, + NULL); + } + + g_task_set_task_data (task, + data, + (GDestroyNotify) shell_util_systemd_call_data_free); + + if (cancellable) + { + data->cancellable = g_object_ref (cancellable); + data->cancel_id = g_cancellable_connect (cancellable, + G_CALLBACK (shell_util_systemd_call_cancelled_cb), + task, + NULL); + } + + g_dbus_connection_call (connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + command, + params, + G_VARIANT_TYPE ("(o)"), + G_DBUS_CALL_FLAGS_NONE, + -1, cancellable, + on_systemd_call_cb, + g_steal_pointer (&task)); +#else /* HAVE_SYSTEMD */ + g_task_return_new_error (task, + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + "systemd not supported by gnome-shell"); +#endif /* !HAVE_SYSTEMD */ +} + +void +shell_util_start_systemd_unit (const char *unit, + const char *mode, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + shell_util_systemd_call ("StartUnit", + g_variant_new ("(ss)", unit, mode), + SYSTEMD_CALL_FLAGS_WATCH_JOB, + cancellable, callback, user_data); +} + +gboolean +shell_util_start_systemd_unit_finish (GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +void +shell_util_stop_systemd_unit (const char *unit, + const char *mode, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + shell_util_systemd_call ("StopUnit", + g_variant_new ("(ss)", unit, mode), + SYSTEMD_CALL_FLAGS_WATCH_JOB, + cancellable, callback, user_data); +} + +gboolean +shell_util_stop_systemd_unit_finish (GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +void +shell_util_systemd_unit_exists (const gchar *unit, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + shell_util_systemd_call ("GetUnit", + g_variant_new ("(s)", unit), + SYSTEMD_CALL_FLAGS_NONE, + cancellable, callback, user_data); +} + +gboolean +shell_util_systemd_unit_exists_finish (GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +void +shell_util_sd_notify (void) +{ + /* We only use NOTIFY_SOCKET exactly once; unset it so it doesn't remain in + * our environment. */ + sd_notify (1, "READY=1"); +} + +/** + * shell_util_has_x11_display_extension: + * @display: A #MetaDisplay + * @extension: An X11 extension + * + * If the corresponding X11 display provides the passed extension, return %TRUE, + * otherwise %FALSE. If there is no X11 display, %FALSE is passed. + */ +gboolean +shell_util_has_x11_display_extension (MetaDisplay *display, + const char *extension) +{ + MetaX11Display *x11_display; + Display *xdisplay; + int op, event, error; + + x11_display = meta_display_get_x11_display (display); + if (!x11_display) + return FALSE; + + xdisplay = meta_x11_display_get_xdisplay (x11_display); + return XQueryExtension (xdisplay, extension, &op, &event, &error); +} + +/** + * shell_util_get_translated_folder_name: + * @name: the untranslated folder name + * + * Attempts to translate the folder @name using translations provided + * by .directory files. + * + * Returns: (nullable): a translated string or %NULL + */ +char * +shell_util_get_translated_folder_name (const char *name) +{ + return shell_app_cache_translate_folder (shell_app_cache_get_default (), name); +} diff --git a/src/shell-util.h b/src/shell-util.h new file mode 100644 index 0000000..7e1f7d8 --- /dev/null +++ b/src/shell-util.h @@ -0,0 +1,93 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#ifndef __SHELL_UTIL_H__ +#define __SHELL_UTIL_H__ + +#include <gio/gio.h> +#include <clutter/clutter.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <meta/meta-cursor-tracker.h> +#include <meta/meta-window-actor.h> + +G_BEGIN_DECLS + +void shell_util_set_hidden_from_pick (ClutterActor *actor, + gboolean hidden); + +int shell_util_get_week_start (void); + +const char *shell_util_translate_time_string (const char *str); + +char *shell_util_regex_escape (const char *str); + +gboolean shell_write_string_to_stream (GOutputStream *stream, + const char *str, + GError **error); + +char *shell_get_file_contents_utf8_sync (const char *path, + GError **error); + +void shell_util_touch_file_async (GFile *file, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_util_touch_file_finish (GFile *file, + GAsyncResult *res, + GError **error); + +gboolean shell_util_wifexited (int status, + int *exit); + +GdkPixbuf *shell_util_create_pixbuf_from_data (const guchar *data, + gsize len, + GdkColorspace colorspace, + gboolean has_alpha, + int bits_per_sample, + int width, + int height, + int rowstride); + +cairo_surface_t * shell_util_composite_capture_images (ClutterCapture *captures, + int n_captures, + int x, + int y, + int target_width, + int target_height, + float target_scale); + +void shell_util_check_cloexec_fds (void); + +void shell_util_start_systemd_unit (const char *unit, + const char *mode, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_util_start_systemd_unit_finish (GAsyncResult *res, + GError **error); + +void shell_util_stop_systemd_unit (const char *unit, + const char *mode, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_util_stop_systemd_unit_finish (GAsyncResult *res, + GError **error); + +void shell_util_systemd_unit_exists (const gchar *unit, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean shell_util_systemd_unit_exists_finish (GAsyncResult *res, + GError **error); + +void shell_util_sd_notify (void); + +gboolean shell_util_has_x11_display_extension (MetaDisplay *display, + const char *extension); + +char *shell_util_get_translated_folder_name (const char *name); + +gint shell_util_get_uid (void); + +G_END_DECLS + +#endif /* __SHELL_UTIL_H__ */ diff --git a/src/shell-window-preview-layout.c b/src/shell-window-preview-layout.c new file mode 100644 index 0000000..fa3cc1f --- /dev/null +++ b/src/shell-window-preview-layout.c @@ -0,0 +1,495 @@ +#include "config.h" + +#include <clutter/clutter.h> +#include <meta/window.h> +#include "shell-window-preview-layout.h" + +typedef struct _ShellWindowPreviewLayoutPrivate ShellWindowPreviewLayoutPrivate; +struct _ShellWindowPreviewLayoutPrivate +{ + ClutterActor *container; + GHashTable *windows; + + ClutterActorBox bounding_box; +}; + +enum +{ + PROP_0, + + PROP_BOUNDING_BOX, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellWindowPreviewLayout, shell_window_preview_layout, + CLUTTER_TYPE_LAYOUT_MANAGER); + +typedef struct _WindowInfo +{ + MetaWindow *window; + ClutterActor *window_actor; + + gulong size_changed_id; + gulong position_changed_id; + gulong window_actor_destroy_id; + gulong destroy_id; +} WindowInfo; + +static void +shell_window_preview_layout_get_property (GObject *object, + unsigned int property_id, + GValue *value, + GParamSpec *pspec) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (object); + ShellWindowPreviewLayoutPrivate *priv; + + priv = shell_window_preview_layout_get_instance_private (self); + + switch (property_id) + { + case PROP_BOUNDING_BOX: + g_value_set_boxed (value, &priv->bounding_box); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +shell_window_preview_layout_set_container (ClutterLayoutManager *layout, + ClutterContainer *container) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout); + ShellWindowPreviewLayoutPrivate *priv; + ClutterLayoutManagerClass *parent_class; + + priv = shell_window_preview_layout_get_instance_private (self); + + priv->container = CLUTTER_ACTOR (container); + + parent_class = CLUTTER_LAYOUT_MANAGER_CLASS (shell_window_preview_layout_parent_class); + parent_class->set_container (layout, container); +} + +static void +shell_window_preview_layout_get_preferred_width (ClutterLayoutManager *layout, + ClutterContainer *container, + float for_height, + float *min_width_p, + float *natural_width_p) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout); + ShellWindowPreviewLayoutPrivate *priv; + + priv = shell_window_preview_layout_get_instance_private (self); + + if (min_width_p) + *min_width_p = 0; + + if (natural_width_p) + *natural_width_p = clutter_actor_box_get_width (&priv->bounding_box); +} + +static void +shell_window_preview_layout_get_preferred_height (ClutterLayoutManager *layout, + ClutterContainer *container, + float for_width, + float *min_height_p, + float *natural_height_p) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout); + ShellWindowPreviewLayoutPrivate *priv; + + priv = shell_window_preview_layout_get_instance_private (self); + + if (min_height_p) + *min_height_p = 0; + + if (natural_height_p) + *natural_height_p = clutter_actor_box_get_height (&priv->bounding_box); +} + + +static void +shell_window_preview_layout_allocate (ClutterLayoutManager *layout, + ClutterContainer *container, + const ClutterActorBox *box) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (layout); + ShellWindowPreviewLayoutPrivate *priv; + float scale_x, scale_y; + float bounding_box_width, bounding_box_height; + ClutterActorIter iter; + ClutterActor *child; + + priv = shell_window_preview_layout_get_instance_private (self); + + bounding_box_width = clutter_actor_box_get_width (&priv->bounding_box); + bounding_box_height = clutter_actor_box_get_height (&priv->bounding_box); + + if (bounding_box_width == 0) + scale_x = 1.f; + else + scale_x = clutter_actor_box_get_width (box) / bounding_box_width; + + if (bounding_box_height == 0) + scale_y = 1.f; + else + scale_y = clutter_actor_box_get_height (box) / bounding_box_height; + + clutter_actor_iter_init (&iter, CLUTTER_ACTOR (container)); + while (clutter_actor_iter_next (&iter, &child)) + { + ClutterActorBox child_box = { 0, }; + WindowInfo *window_info; + + if (!clutter_actor_is_visible (child)) + continue; + + window_info = g_hash_table_lookup (priv->windows, child); + + if (window_info) + { + MetaRectangle buffer_rect; + float child_nat_width, child_nat_height; + + meta_window_get_buffer_rect (window_info->window, &buffer_rect); + + clutter_actor_box_set_origin (&child_box, + buffer_rect.x - priv->bounding_box.x1, + buffer_rect.y - priv->bounding_box.y1); + + clutter_actor_get_preferred_size (child, NULL, NULL, + &child_nat_width, &child_nat_height); + + clutter_actor_box_set_size (&child_box, child_nat_width, child_nat_height); + + child_box.x1 *= scale_x; + child_box.x2 *= scale_x; + child_box.y1 *= scale_y; + child_box.y2 *= scale_y; + + clutter_actor_allocate (child, &child_box); + } + else + { + float x, y; + + clutter_actor_get_fixed_position (child, &x, &y); + clutter_actor_allocate_preferred_size (child, x, y); + } + } +} + +static void +on_layout_changed (ShellWindowPreviewLayout *self) +{ + ShellWindowPreviewLayoutPrivate *priv; + GHashTableIter iter; + gpointer value; + gboolean first_rect = TRUE; + MetaRectangle bounding_rect = { 0, }; + ClutterActorBox old_bounding_box; + + priv = shell_window_preview_layout_get_instance_private (self); + + old_bounding_box = + (ClutterActorBox) CLUTTER_ACTOR_BOX_INIT (priv->bounding_box.x1, + priv->bounding_box.y1, + priv->bounding_box.x2, + priv->bounding_box.y2); + + g_hash_table_iter_init (&iter, priv->windows); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + WindowInfo *window_info = value; + MetaRectangle frame_rect; + + meta_window_get_frame_rect (window_info->window, &frame_rect); + + if (first_rect) + { + bounding_rect = frame_rect; + first_rect = FALSE; + continue; + } + + meta_rectangle_union (&frame_rect, &bounding_rect, &bounding_rect); + } + + clutter_actor_box_set_origin (&priv->bounding_box, + (float) bounding_rect.x, + (float) bounding_rect.y); + clutter_actor_box_set_size (&priv->bounding_box, + (float) bounding_rect.width, + (float) bounding_rect.height); + + if (!clutter_actor_box_equal (&priv->bounding_box, &old_bounding_box)) + g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_BOUNDING_BOX]); + + clutter_layout_manager_layout_changed (CLUTTER_LAYOUT_MANAGER (self)); +} + +static void +on_window_size_position_changed (MetaWindow *window, + ShellWindowPreviewLayout *self) +{ + on_layout_changed (self); +} + +static void +on_window_destroyed (ClutterActor *actor) +{ + clutter_actor_destroy (actor); +} + +static void +on_actor_destroyed (ClutterActor *actor, + ShellWindowPreviewLayout *self) +{ + ShellWindowPreviewLayoutPrivate *priv; + WindowInfo *window_info; + + priv = shell_window_preview_layout_get_instance_private (self); + + window_info = g_hash_table_lookup (priv->windows, actor); + g_assert (window_info != NULL); + + shell_window_preview_layout_remove_window (self, window_info->window); +} + +static void +shell_window_preview_layout_dispose (GObject *gobject) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (gobject); + ShellWindowPreviewLayoutPrivate *priv; + GHashTableIter iter; + gpointer key, value; + + priv = shell_window_preview_layout_get_instance_private (self); + + g_hash_table_iter_init (&iter, priv->windows); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ClutterActor *actor = key; + WindowInfo *info = value; + + g_clear_signal_handler (&info->size_changed_id, info->window); + g_clear_signal_handler (&info->position_changed_id, info->window); + g_clear_signal_handler (&info->window_actor_destroy_id, info->window_actor); + g_clear_signal_handler (&info->destroy_id, actor); + + clutter_actor_remove_child (priv->container, actor); + } + + g_hash_table_remove_all (priv->windows); + + G_OBJECT_CLASS (shell_window_preview_layout_parent_class)->dispose (gobject); +} + +static void +shell_window_preview_layout_finalize (GObject *gobject) +{ + ShellWindowPreviewLayout *self = SHELL_WINDOW_PREVIEW_LAYOUT (gobject); + ShellWindowPreviewLayoutPrivate *priv; + + priv = shell_window_preview_layout_get_instance_private (self); + + g_hash_table_destroy (priv->windows); + + G_OBJECT_CLASS (shell_window_preview_layout_parent_class)->finalize (gobject); +} + +static void +shell_window_preview_layout_init (ShellWindowPreviewLayout *self) +{ + ShellWindowPreviewLayoutPrivate *priv; + + priv = shell_window_preview_layout_get_instance_private (self); + + priv->windows = g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify) g_free); +} + +static void +shell_window_preview_layout_class_init (ShellWindowPreviewLayoutClass *klass) +{ + ClutterLayoutManagerClass *layout_class = CLUTTER_LAYOUT_MANAGER_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + layout_class->get_preferred_width = shell_window_preview_layout_get_preferred_width; + layout_class->get_preferred_height = shell_window_preview_layout_get_preferred_height; + layout_class->allocate = shell_window_preview_layout_allocate; + layout_class->set_container = shell_window_preview_layout_set_container; + + gobject_class->dispose = shell_window_preview_layout_dispose; + gobject_class->finalize = shell_window_preview_layout_finalize; + gobject_class->get_property = shell_window_preview_layout_get_property; + + /** + * ShellWindowPreviewLayout:bounding-box: + */ + obj_props[PROP_BOUNDING_BOX] = + g_param_spec_boxed ("bounding-box", + "Bounding Box", + "Bounding Box", + CLUTTER_TYPE_ACTOR_BOX, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} + +/** + * shell_window_preview_layout_add_window: + * @self: a #ShellWindowPreviewLayout + * @window: the #MetaWindow + * + * Creates a ClutterActor drawing the texture of @window and adds it + * to the container. If @window is already part of the preview, this + * function will do nothing. + * + * Returns: (nullable) (transfer none): The newly created actor drawing @window + */ +ClutterActor * +shell_window_preview_layout_add_window (ShellWindowPreviewLayout *self, + MetaWindow *window) +{ + ShellWindowPreviewLayoutPrivate *priv; + ClutterActor *window_actor, *actor; + WindowInfo *window_info; + GHashTableIter iter; + gpointer value; + + g_return_val_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self), NULL); + g_return_val_if_fail (META_IS_WINDOW (window), NULL); + + priv = shell_window_preview_layout_get_instance_private (self); + + g_hash_table_iter_init (&iter, priv->windows); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + WindowInfo *info = value; + + if (info->window == window) + return NULL; + } + + window_actor = CLUTTER_ACTOR (meta_window_get_compositor_private (window)); + actor = clutter_clone_new (window_actor); + + window_info = g_new0 (WindowInfo, 1); + + window_info->window = window; + window_info->window_actor = window_actor; + window_info->size_changed_id = + g_signal_connect (window, "size-changed", + G_CALLBACK (on_window_size_position_changed), self); + window_info->position_changed_id = + g_signal_connect (window, "position-changed", + G_CALLBACK (on_window_size_position_changed), self); + window_info->window_actor_destroy_id = + g_signal_connect_swapped (window_actor, "destroy", + G_CALLBACK (on_window_destroyed), actor); + window_info->destroy_id = + g_signal_connect (actor, "destroy", + G_CALLBACK (on_actor_destroyed), self); + + g_hash_table_insert (priv->windows, actor, window_info); + + clutter_actor_add_child (priv->container, actor); + + on_layout_changed (self); + + return actor; +} + +/** + * shell_window_preview_layout_remove_window: + * @self: a #ShellWindowPreviewLayout + * @window: the #MetaWindow + * + * Removes a MetaWindow @window from the preview which has been added + * previously using shell_window_preview_layout_add_window(). + * If @window is not part of preview, this function will do nothing. + */ +void +shell_window_preview_layout_remove_window (ShellWindowPreviewLayout *self, + MetaWindow *window) +{ + ShellWindowPreviewLayoutPrivate *priv; + ClutterActor *actor; + WindowInfo *window_info = NULL; + GHashTableIter iter; + gpointer key, value; + + g_return_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self)); + g_return_if_fail (META_IS_WINDOW (window)); + + priv = shell_window_preview_layout_get_instance_private (self); + + g_hash_table_iter_init (&iter, priv->windows); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + WindowInfo *info = value; + + if (info->window == window) + { + actor = CLUTTER_ACTOR (key); + window_info = info; + break; + } + } + + if (window_info == NULL) + return; + + g_clear_signal_handler (&window_info->size_changed_id, window); + g_clear_signal_handler (&window_info->position_changed_id, window); + g_clear_signal_handler (&window_info->window_actor_destroy_id, window_info->window_actor); + g_clear_signal_handler (&window_info->destroy_id, actor); + + g_hash_table_remove (priv->windows, actor); + + clutter_actor_remove_child (priv->container, actor); + + on_layout_changed (self); +} + +/** + * shell_window_preview_layout_get_windows: + * @self: a #ShellWindowPreviewLayout + * + * Gets an array of all MetaWindows that were added to the layout + * using shell_window_preview_layout_add_window(), ordered by the + * insertion order. + * + * Returns: (transfer container) (element-type Meta.Window): The list of windows + */ +GList * +shell_window_preview_layout_get_windows (ShellWindowPreviewLayout *self) +{ + ShellWindowPreviewLayoutPrivate *priv; + GList *windows = NULL; + GHashTableIter iter; + gpointer value; + + g_return_val_if_fail (SHELL_IS_WINDOW_PREVIEW_LAYOUT (self), NULL); + + priv = shell_window_preview_layout_get_instance_private (self); + + g_hash_table_iter_init (&iter, priv->windows); + while (g_hash_table_iter_next (&iter, NULL, &value)) + { + WindowInfo *window_info = value; + + windows = g_list_prepend (windows, window_info->window); + } + + return windows; +} diff --git a/src/shell-window-preview-layout.h b/src/shell-window-preview-layout.h new file mode 100644 index 0000000..9376b0d --- /dev/null +++ b/src/shell-window-preview-layout.h @@ -0,0 +1,33 @@ +#ifndef __SHELL_WINDOW_PREVIEW_LAYOUT_H__ +#define __SHELL_WINDOW_PREVIEW_LAYOUT_H__ + +G_BEGIN_DECLS + +#include <clutter/clutter.h> + +#define SHELL_TYPE_WINDOW_PREVIEW_LAYOUT (shell_window_preview_layout_get_type ()) +G_DECLARE_FINAL_TYPE (ShellWindowPreviewLayout, shell_window_preview_layout, + SHELL, WINDOW_PREVIEW_LAYOUT, ClutterLayoutManager) + +typedef struct _ShellWindowPreviewLayout ShellWindowPreviewLayout; +typedef struct _ShellWindowPreviewLayoutPrivate ShellWindowPreviewLayoutPrivate; + +struct _ShellWindowPreviewLayout +{ + /*< private >*/ + ClutterLayoutManager parent; + + ShellWindowPreviewLayoutPrivate *priv; +}; + +ClutterActor * shell_window_preview_layout_add_window (ShellWindowPreviewLayout *self, + MetaWindow *window); + +void shell_window_preview_layout_remove_window (ShellWindowPreviewLayout *self, + MetaWindow *window); + +GList * shell_window_preview_layout_get_windows (ShellWindowPreviewLayout *self); + +G_END_DECLS + +#endif /* __SHELL_WINDOW_PREVIEW_LAYOUT_H__ */ diff --git a/src/shell-window-preview.c b/src/shell-window-preview.c new file mode 100644 index 0000000..47a11c5 --- /dev/null +++ b/src/shell-window-preview.c @@ -0,0 +1,177 @@ +#include "config.h" + +#include "shell-window-preview.h" + +enum +{ + PROP_0, + + PROP_WINDOW_CONTAINER, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +struct _ShellWindowPreview +{ + /*< private >*/ + StWidget parent_instance; + + ClutterActor *window_container; +}; + +G_DEFINE_TYPE (ShellWindowPreview, shell_window_preview, ST_TYPE_WIDGET); + +static void +shell_window_preview_get_property (GObject *gobject, + unsigned int property_id, + GValue *value, + GParamSpec *pspec) +{ + ShellWindowPreview *self = SHELL_WINDOW_PREVIEW (gobject); + + switch (property_id) + { + case PROP_WINDOW_CONTAINER: + g_value_set_object (value, self->window_container); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, property_id, pspec); + } +} + +static void +shell_window_preview_set_property (GObject *gobject, + unsigned int property_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellWindowPreview *self = SHELL_WINDOW_PREVIEW (gobject); + + switch (property_id) + { + case PROP_WINDOW_CONTAINER: + if (g_set_object (&self->window_container, g_value_get_object (value))) + g_object_notify_by_pspec (gobject, obj_props[PROP_WINDOW_CONTAINER]); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, property_id, pspec); + } +} + +static void +shell_window_preview_get_preferred_width (ClutterActor *actor, + float for_height, + float *min_width_p, + float *natural_width_p) +{ + ShellWindowPreview *self = SHELL_WINDOW_PREVIEW (actor); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + float min_width, nat_width; + + st_theme_node_adjust_for_height (theme_node, &for_height); + + clutter_actor_get_preferred_width (self->window_container, for_height, + &min_width, &nat_width); + + st_theme_node_adjust_preferred_width (theme_node, &min_width, &nat_width); + + if (min_width_p) + *min_width_p = min_width; + + if (natural_width_p) + *natural_width_p = nat_width; +} + +static void +shell_window_preview_get_preferred_height (ClutterActor *actor, + float for_width, + float *min_height_p, + float *natural_height_p) +{ + ShellWindowPreview *self = SHELL_WINDOW_PREVIEW (actor); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + float min_height, nat_height; + + st_theme_node_adjust_for_width (theme_node, &for_width); + + clutter_actor_get_preferred_height (self->window_container, for_width, + &min_height, &nat_height); + + st_theme_node_adjust_preferred_height (theme_node, &min_height, &nat_height); + + if (min_height_p) + *min_height_p = min_height; + + if (natural_height_p) + *natural_height_p = nat_height; +} + +static void +shell_window_preview_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox content_box; + float x, y, max_width, max_height; + ClutterActorIter iter; + ClutterActor *child; + + clutter_actor_set_allocation (actor, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + clutter_actor_box_get_origin (&content_box, &x, &y); + clutter_actor_box_get_size (&content_box, &max_width, &max_height); + + clutter_actor_iter_init (&iter, actor); + while (clutter_actor_iter_next (&iter, &child)) + clutter_actor_allocate_available_size (child, x, y, max_width, max_height); +} + +static void +shell_window_preview_dispose (GObject *gobject) +{ + ShellWindowPreview *self = SHELL_WINDOW_PREVIEW (gobject); + + g_clear_object (&self->window_container); + + G_OBJECT_CLASS (shell_window_preview_parent_class)->dispose (gobject); +} + +static void +shell_window_preview_init (ShellWindowPreview *self) +{ +} + +static void +shell_window_preview_class_init (ShellWindowPreviewClass *klass) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + actor_class->get_preferred_width = shell_window_preview_get_preferred_width; + actor_class->get_preferred_height = shell_window_preview_get_preferred_height; + actor_class->allocate = shell_window_preview_allocate; + + gobject_class->dispose = shell_window_preview_dispose; + gobject_class->get_property = shell_window_preview_get_property; + gobject_class->set_property = shell_window_preview_set_property; + + /** + * ShellWindowPreview:window-container: + */ + obj_props[PROP_WINDOW_CONTAINER] = + g_param_spec_object ("window-container", + "window-container", + "window-container", + CLUTTER_TYPE_ACTOR, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} diff --git a/src/shell-window-preview.h b/src/shell-window-preview.h new file mode 100644 index 0000000..e503800 --- /dev/null +++ b/src/shell-window-preview.h @@ -0,0 +1,14 @@ +#ifndef __SHELL_WINDOW_PREVIEW_H__ +#define __SHELL_WINDOW_PREVIEW_H__ + +#include <st/st.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_WINDOW_PREVIEW (shell_window_preview_get_type ()) +G_DECLARE_FINAL_TYPE (ShellWindowPreview, shell_window_preview, + SHELL, WINDOW_PREVIEW, StWidget) + +G_END_DECLS + +#endif /* __SHELL_WINDOW_PREVIEW_H__ */ diff --git a/src/shell-window-tracker-private.h b/src/shell-window-tracker-private.h new file mode 100644 index 0000000..4307d15 --- /dev/null +++ b/src/shell-window-tracker-private.h @@ -0,0 +1,11 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_WINDOW_TRACKER_PRIVATE_H__ +#define __SHELL_WINDOW_TRACKER_PRIVATE_H__ + +#include "shell-window-tracker.h" + +void _shell_window_tracker_add_child_process_app (ShellWindowTracker *tracker, + GPid pid, + ShellApp *app); + +#endif diff --git a/src/shell-window-tracker.c b/src/shell-window-tracker.c new file mode 100644 index 0000000..dda9e2b --- /dev/null +++ b/src/shell-window-tracker.c @@ -0,0 +1,811 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <string.h> +#include <stdlib.h> + +#include <meta/display.h> +#include <meta/group.h> +#include <meta/util.h> +#include <meta/window.h> +#include <meta/meta-workspace-manager.h> +#include <meta/meta-startup-notification.h> + +#include "shell-window-tracker-private.h" +#include "shell-app-private.h" +#include "shell-global.h" +#include "st.h" + +/* This file includes modified code from + * desktop-data-engine/engine-dbus/hippo-application-monitor.c + * in the functions collecting application usage data. + * Written by Owen Taylor, originally licensed under LGPL 2.1. + * Copyright Red Hat, Inc. 2006-2008 + */ + +/** + * SECTION:shell-window-tracker + * @short_description: Associate windows with applications + * + * Maintains a mapping from windows to applications (.desktop file ids). + * It currently implements this with some heuristics on the WM_CLASS X11 + * property (and some static override regexps); in the future, we want to + * have it also track through startup-notification. + */ + +struct _ShellWindowTracker +{ + GObject parent; + + ShellApp *focus_app; + + /* <MetaWindow * window, ShellApp *app> */ + GHashTable *window_to_app; +}; + +G_DEFINE_TYPE (ShellWindowTracker, shell_window_tracker, G_TYPE_OBJECT); + +enum { + PROP_0, + + PROP_FOCUS_APP, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum { + STARTUP_SEQUENCE_CHANGED, + TRACKED_WINDOWS_CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static void shell_window_tracker_finalize (GObject *object); +static void set_focus_app (ShellWindowTracker *tracker, + ShellApp *new_focus_app); +static void on_focus_window_changed (MetaDisplay *display, GParamSpec *spec, ShellWindowTracker *tracker); + +static void track_window (ShellWindowTracker *tracker, MetaWindow *window); +static void disassociate_window (ShellWindowTracker *tracker, MetaWindow *window); + +static ShellApp * shell_startup_sequence_get_app (MetaStartupSequence *sequence); + +static void +shell_window_tracker_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellWindowTracker *tracker = SHELL_WINDOW_TRACKER (gobject); + + switch (prop_id) + { + case PROP_FOCUS_APP: + g_value_set_object (value, tracker->focus_app); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_window_tracker_class_init (ShellWindowTrackerClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_window_tracker_get_property; + gobject_class->finalize = shell_window_tracker_finalize; + + props[PROP_FOCUS_APP] = + g_param_spec_object ("focus-app", + "Focus App", + "Focused application", + SHELL_TYPE_APP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + signals[STARTUP_SEQUENCE_CHANGED] = g_signal_new ("startup-sequence-changed", + SHELL_TYPE_WINDOW_TRACKER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, META_TYPE_STARTUP_SEQUENCE); + signals[TRACKED_WINDOWS_CHANGED] = g_signal_new ("tracked-windows-changed", + SHELL_TYPE_WINDOW_TRACKER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static gboolean +check_app_id_prefix (ShellApp *app, + const char *prefix) +{ + if (prefix == NULL) + return TRUE; + + return g_str_has_prefix (shell_app_get_id (app), prefix); +} + +/* + * get_app_from_window_wmclass: + * + * Looks only at the given window, and attempts to determine + * an application based on WM_CLASS. If one can't be determined, + * return %NULL. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp * +get_app_from_window_wmclass (MetaWindow *window) +{ + ShellApp *app; + ShellAppSystem *appsys; + const char *wm_class; + const char *wm_instance; + const char *sandbox_id; + g_autofree char *app_prefix = NULL; + + appsys = shell_app_system_get_default (); + + sandbox_id = meta_window_get_sandboxed_app_id (window); + if (sandbox_id) + app_prefix = g_strdup_printf ("%s.", sandbox_id); + + /* Notes on the heuristics used here: + much of the complexity here comes from the desire to support + Chrome apps. + + From https://bugzilla.gnome.org/show_bug.cgi?id=673657#c13 + + Currently chrome sets WM_CLASS as follows (the first string is the 'instance', + the second one is the 'class': + + For the normal browser: + WM_CLASS(STRING) = "chromium", "Chromium" + + For a bookmarked page (through 'Tools -> Create application shortcuts') + WM_CLASS(STRING) = "wiki.gnome.org__GnomeShell_ApplicationBased", "Chromium" + + For an application from the chrome store (with a .desktop file created through + right click, "Create shortcuts" from Chrome's apps overview) + WM_CLASS(STRING) = "crx_blpcfgokakmgnkcojhhkbfbldkacnbeo", "Chromium" + + The .desktop file has a matching StartupWMClass, but the name differs, e.g. for + the store app (youtube) there is + + .local/share/applications/chrome-blpcfgokakmgnkcojhhkbfbldkacnbeo-Default.desktop + + with + + StartupWMClass=crx_blpcfgokakmgnkcojhhkbfbldkacnbeo + + Note that chromium (but not google-chrome!) includes a StartupWMClass=chromium + in their .desktop file, so we must match the instance first. + + Also note that in the good case (regular gtk+ app without hacks), instance and + class are the same except for case and there is no StartupWMClass at all. + */ + + /* first try a match from WM_CLASS (instance part) to StartupWMClass */ + wm_instance = meta_window_get_wm_class_instance (window); + app = shell_app_system_lookup_startup_wmclass (appsys, wm_instance); + if (app != NULL && check_app_id_prefix (app, app_prefix)) + return g_object_ref (app); + + /* then try a match from WM_CLASS to StartupWMClass */ + wm_class = meta_window_get_wm_class (window); + app = shell_app_system_lookup_startup_wmclass (appsys, wm_class); + if (app != NULL && check_app_id_prefix (app, app_prefix)) + return g_object_ref (app); + + /* then try a match from WM_CLASS (instance part) to .desktop */ + app = shell_app_system_lookup_desktop_wmclass (appsys, wm_instance); + if (app != NULL && check_app_id_prefix (app, app_prefix)) + return g_object_ref (app); + + /* finally, try a match from WM_CLASS to .desktop */ + app = shell_app_system_lookup_desktop_wmclass (appsys, wm_class); + if (app != NULL && check_app_id_prefix (app, app_prefix)) + return g_object_ref (app); + + return NULL; +} + +/* + * get_app_from_id: + * @window: a #MetaWindow + * + * Looks only at the given window, and attempts to determine + * an application based on %id. If one can't be determined, + * return %NULL. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp * +get_app_from_id (MetaWindow *window, + const char *id) +{ + ShellApp *app; + ShellAppSystem *appsys; + g_autofree char *desktop_file = NULL; + + g_return_val_if_fail (id != NULL, NULL); + + appsys = shell_app_system_get_default (); + + desktop_file = g_strconcat (id, ".desktop", NULL); + app = shell_app_system_lookup_app (appsys, desktop_file); + if (app) + return g_object_ref (app); + + return NULL; +} + +/* + * get_app_from_gapplication_id: + * @window: a #MetaWindow + * + * Looks only at the given window, and attempts to determine + * an application based on _GTK_APPLICATION_ID. If one can't be determined, + * return %NULL. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp * +get_app_from_gapplication_id (MetaWindow *window) +{ + const char *id; + + id = meta_window_get_gtk_application_id (window); + if (!id) + return NULL; + + return get_app_from_id (window, id); +} + +/* + * get_app_from_sandboxed_app_id: + * @window: a #MetaWindow + * + * Looks only at the given window, and attempts to determine + * an application based on its Flatpak or Snap ID. If one can't be determined, + * return %NULL. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp * +get_app_from_sandboxed_app_id (MetaWindow *window) +{ + const char *id; + + id = meta_window_get_sandboxed_app_id (window); + if (!id) + return NULL; + + return get_app_from_id (window, id); +} + +/* + * get_app_from_window_group: + * @monitor: a #ShellWindowTracker + * @window: a #MetaWindow + * + * Check other windows in the group for @window to see if we have + * an application for one of them. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp* +get_app_from_window_group (ShellWindowTracker *tracker, + MetaWindow *window) +{ + ShellApp *result; + GSList *group_windows; + MetaGroup *group; + GSList *iter; + + group = meta_window_get_group (window); + if (group == NULL) + return NULL; + + group_windows = meta_group_list_windows (group); + + result = NULL; + /* Try finding a window in the group of type NORMAL; if we + * succeed, use that as our source. */ + for (iter = group_windows; iter; iter = iter->next) + { + MetaWindow *group_window = iter->data; + + if (meta_window_get_window_type (group_window) != META_WINDOW_NORMAL) + continue; + + result = g_hash_table_lookup (tracker->window_to_app, group_window); + if (result) + break; + } + + g_slist_free (group_windows); + + if (result) + g_object_ref (result); + + return result; +} + +/* + * get_app_from_window_pid: + * @tracker: a #ShellWindowTracker + * @window: a #MetaWindow + * + * Check if the pid associated with @window corresponds to an + * application. + * + * Return value: (transfer full): A newly-referenced #ShellApp, or %NULL + */ +static ShellApp * +get_app_from_window_pid (ShellWindowTracker *tracker, + MetaWindow *window) +{ + ShellApp *result; + pid_t pid; + + if (meta_window_is_remote (window)) + return NULL; + + pid = meta_window_get_pid (window); + + if (pid < 1) + return NULL; + + result = shell_window_tracker_get_app_from_pid (tracker, pid); + if (result != NULL) + g_object_ref (result); + + return result; +} + +/** + * get_app_for_window: + * + * Determines the application associated with a window, using + * all available information such as the window's MetaGroup, + * and what we know about other windows. + * + * Returns: (transfer full): a #ShellApp, or NULL if none is found + */ +static ShellApp * +get_app_for_window (ShellWindowTracker *tracker, + MetaWindow *window) +{ + ShellApp *result = NULL; + MetaWindow *transient_for; + const char *startup_id; + + transient_for = meta_window_get_transient_for (window); + if (transient_for != NULL) + return get_app_for_window (tracker, transient_for); + + /* First, we check whether we already know about this window, + * if so, just return that. + */ + if (meta_window_get_window_type (window) == META_WINDOW_NORMAL + || meta_window_is_remote (window)) + { + result = g_hash_table_lookup (tracker->window_to_app, window); + if (result != NULL) + { + g_object_ref (result); + return result; + } + } + + if (meta_window_is_remote (window)) + return _shell_app_new_for_window (window); + + /* Check if the app's WM_CLASS specifies an app; this is + * canonical if it does. + */ + result = get_app_from_window_wmclass (window); + if (result != NULL) + return result; + + /* Check if the window was opened from within a sandbox; if this + * is the case, a corresponding .desktop file is guaranteed to match; + */ + result = get_app_from_sandboxed_app_id (window); + if (result != NULL) + return result; + + /* Check if the window has a GApplication ID attached; this is + * canonical if it does + */ + result = get_app_from_gapplication_id (window); + if (result != NULL) + return result; + + result = get_app_from_window_pid (tracker, window); + if (result != NULL) + return result; + + /* Now we check whether we have a match through startup-notification */ + startup_id = meta_window_get_startup_id (window); + if (startup_id) + { + GSList *iter, *sequences; + + sequences = shell_window_tracker_get_startup_sequences (tracker); + for (iter = sequences; iter; iter = iter->next) + { + MetaStartupSequence *sequence = iter->data; + const char *id = meta_startup_sequence_get_id (sequence); + if (strcmp (id, startup_id) != 0) + continue; + + result = shell_startup_sequence_get_app (sequence); + if (result) + { + result = g_object_ref (result); + break; + } + } + } + + /* If we didn't get a startup-notification match, see if we matched + * any other windows in the group. + */ + if (result == NULL) + result = get_app_from_window_group (tracker, window); + + /* Our last resort - we create a fake app from the window */ + if (result == NULL) + result = _shell_app_new_for_window (window); + + return result; +} + +static void +update_focus_app (ShellWindowTracker *self) +{ + MetaWindow *new_focus_win; + ShellApp *new_focus_app; + + new_focus_win = meta_display_get_focus_window (shell_global_get_display (shell_global_get ())); + + /* we only consider an app focused if the focus window can be clearly + * associated with a running app; this is the case if the focus window + * or one of its parents is visible in the taskbar, e.g. + * - 'nautilus' should appear focused when its about dialog has focus + * - 'nautilus' should not appear focused when the DESKTOP has focus + */ + while (new_focus_win && meta_window_is_skip_taskbar (new_focus_win)) + new_focus_win = meta_window_get_transient_for (new_focus_win); + + new_focus_app = new_focus_win ? shell_window_tracker_get_window_app (self, new_focus_win) : NULL; + + if (new_focus_app) + { + shell_app_update_window_actions (new_focus_app, new_focus_win); + shell_app_update_app_actions (new_focus_app, new_focus_win); + } + + set_focus_app (self, new_focus_app); + + g_clear_object (&new_focus_app); +} + +static void +tracked_window_changed (ShellWindowTracker *self, + MetaWindow *window) +{ + /* It's simplest to just treat this as a remove + add. */ + disassociate_window (self, window); + track_window (self, window); + /* also just recalculate the focused app, in case it was the focused + window that changed */ + update_focus_app (self); +} + +static void +on_wm_class_changed (MetaWindow *window, + GParamSpec *pspec, + gpointer user_data) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); + tracked_window_changed (self, window); +} + +static void +on_title_changed (MetaWindow *window, + GParamSpec *pspec, + gpointer user_data) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); + g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0); +} + +static void +on_gtk_application_id_changed (MetaWindow *window, + GParamSpec *pspec, + gpointer user_data) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); + tracked_window_changed (self, window); +} + +static void +on_window_unmanaged (MetaWindow *window, + gpointer user_data) +{ + disassociate_window (SHELL_WINDOW_TRACKER (user_data), window); +} + +static void +track_window (ShellWindowTracker *self, + MetaWindow *window) +{ + ShellApp *app; + + app = get_app_for_window (self, window); + if (!app) + return; + + /* At this point we've stored the association from window -> application */ + g_hash_table_insert (self->window_to_app, window, app); + + g_signal_connect (window, "notify::wm-class", G_CALLBACK (on_wm_class_changed), self); + g_signal_connect (window, "notify::title", G_CALLBACK (on_title_changed), self); + g_signal_connect (window, "notify::gtk-application-id", G_CALLBACK (on_gtk_application_id_changed), self); + g_signal_connect (window, "unmanaged", G_CALLBACK (on_window_unmanaged), self); + + _shell_app_add_window (app, window); + + g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0); +} + +static void +on_window_created (MetaDisplay *display, + MetaWindow *window, + gpointer user_data) +{ + track_window (SHELL_WINDOW_TRACKER (user_data), window); +} + +static void +disassociate_window (ShellWindowTracker *self, + MetaWindow *window) +{ + ShellApp *app; + + app = g_hash_table_lookup (self->window_to_app, window); + if (!app) + return; + + g_object_ref (app); + + g_hash_table_remove (self->window_to_app, window); + + _shell_app_remove_window (app, window); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK (on_wm_class_changed), self); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK (on_title_changed), self); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK (on_gtk_application_id_changed), self); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK (on_window_unmanaged), self); + + g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0); + + g_object_unref (app); +} + +static void +load_initial_windows (ShellWindowTracker *tracker) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + g_autoptr (GList) windows = NULL; + GList *l; + + windows = meta_display_list_all_windows (display); + for (l = windows; l; l = l->next) + track_window (tracker, l->data); +} + +static void +init_window_tracking (ShellWindowTracker *self) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + g_signal_connect (display, "notify::focus-window", + G_CALLBACK (on_focus_window_changed), self); + g_signal_connect(display, "window-created", + G_CALLBACK (on_window_created), self); +} + +static void +on_startup_sequence_changed (MetaStartupNotification *sn, + MetaStartupSequence *sequence, + ShellWindowTracker *self) +{ + ShellApp *app; + + app = shell_startup_sequence_get_app (sequence); + if (app) + _shell_app_handle_startup_sequence (app, sequence); + + g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence); +} + +static void +shell_window_tracker_init (ShellWindowTracker *self) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + MetaStartupNotification *sn = meta_display_get_startup_notification (display); + + self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, (GDestroyNotify) g_object_unref); + + + g_signal_connect (sn, "changed", + G_CALLBACK (on_startup_sequence_changed), self); + + load_initial_windows (self); + init_window_tracking (self); +} + +static void +shell_window_tracker_finalize (GObject *object) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (object); + + g_hash_table_destroy (self->window_to_app); + + G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object); +} + +/** + * shell_window_tracker_get_window_app: + * @tracker: An app monitor instance + * @metawin: A #MetaWindow + * + * Returns: (transfer full): Application associated with window + */ +ShellApp * +shell_window_tracker_get_window_app (ShellWindowTracker *tracker, + MetaWindow *metawin) +{ + ShellApp *app; + + app = g_hash_table_lookup (tracker->window_to_app, metawin); + if (app) + g_object_ref (app); + + return app; +} + + +/** + * shell_window_tracker_get_app_from_pid: + * @tracker: A #ShellAppSystem + * @pid: A Unix process identifier + * + * Look up the application corresponding to a process. + * + * Returns: (transfer none): A #ShellApp, or %NULL if none + */ +ShellApp * +shell_window_tracker_get_app_from_pid (ShellWindowTracker *tracker, + int pid) +{ + GSList *running = shell_app_system_get_running (shell_app_system_get_default()); + GSList *iter; + ShellApp *result = NULL; + + for (iter = running; iter; iter = iter->next) + { + ShellApp *app = iter->data; + GSList *pids = shell_app_get_pids (app); + GSList *pids_iter; + + for (pids_iter = pids; pids_iter; pids_iter = pids_iter->next) + { + int app_pid = GPOINTER_TO_INT (pids_iter->data); + if (app_pid == pid) + { + result = app; + break; + } + } + g_slist_free (pids); + + if (result != NULL) + break; + } + + g_slist_free (running); + + return result; +} + +static void +set_focus_app (ShellWindowTracker *tracker, + ShellApp *new_focus_app) +{ + if (new_focus_app == tracker->focus_app) + return; + + if (tracker->focus_app != NULL) + g_object_unref (tracker->focus_app); + + tracker->focus_app = new_focus_app; + + if (tracker->focus_app != NULL) + g_object_ref (tracker->focus_app); + + g_object_notify_by_pspec (G_OBJECT (tracker), props[PROP_FOCUS_APP]); +} + +static void +on_focus_window_changed (MetaDisplay *display, + GParamSpec *spec, + ShellWindowTracker *tracker) +{ + update_focus_app (tracker); +} + +/** + * shell_window_tracker_get_startup_sequences: + * @tracker: + * + * Returns: (transfer none) (element-type MetaStartupSequence): Currently active startup sequences + */ +GSList * +shell_window_tracker_get_startup_sequences (ShellWindowTracker *self) +{ + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaStartupNotification *sn = meta_display_get_startup_notification (display); + + return meta_startup_notification_get_sequences (sn); +} + +static ShellApp * +shell_startup_sequence_get_app (MetaStartupSequence *sequence) +{ + const char *appid; + char *basename; + ShellAppSystem *appsys; + ShellApp *app; + + appid = meta_startup_sequence_get_application_id (sequence); + if (!appid) + return NULL; + + basename = g_path_get_basename (appid); + appsys = shell_app_system_get_default (); + app = shell_app_system_lookup_app (appsys, basename); + g_free (basename); + return app; +} + +/** + * shell_window_tracker_get_default: + * + * Return Value: (transfer none): The global #ShellWindowTracker instance + */ +ShellWindowTracker * +shell_window_tracker_get_default (void) +{ + static ShellWindowTracker *instance; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_WINDOW_TRACKER, NULL); + + return instance; +} diff --git a/src/shell-window-tracker.h b/src/shell-window-tracker.h new file mode 100644 index 0000000..87c38f8 --- /dev/null +++ b/src/shell-window-tracker.h @@ -0,0 +1,29 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_WINDOW_TRACKER_H__ +#define __SHELL_WINDOW_TRACKER_H__ + +#include <glib-object.h> +#include <glib.h> +#include <meta/window.h> +#include <meta/meta-startup-notification.h> + +#include "shell-app.h" +#include "shell-app-system.h" + +G_BEGIN_DECLS + +#define SHELL_TYPE_WINDOW_TRACKER (shell_window_tracker_get_type ()) +G_DECLARE_FINAL_TYPE (ShellWindowTracker, shell_window_tracker, + SHELL, WINDOW_TRACKER, GObject) + +ShellWindowTracker* shell_window_tracker_get_default(void); + +ShellApp *shell_window_tracker_get_window_app (ShellWindowTracker *tracker, MetaWindow *metawin); + +ShellApp *shell_window_tracker_get_app_from_pid (ShellWindowTracker *tracker, int pid); + +GSList *shell_window_tracker_get_startup_sequences (ShellWindowTracker *tracker); + +G_END_DECLS + +#endif /* __SHELL_WINDOW_TRACKER_H__ */ diff --git a/src/shell-wm-private.h b/src/shell-wm-private.h new file mode 100644 index 0000000..1363087 --- /dev/null +++ b/src/shell-wm-private.h @@ -0,0 +1,63 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_WM_PRIVATE_H__ +#define __SHELL_WM_PRIVATE_H__ + +#include "shell-wm.h" + +G_BEGIN_DECLS + +/* These forward along the different effects from GnomeShellPlugin */ + +void _shell_wm_minimize (ShellWM *wm, + MetaWindowActor *actor); +void _shell_wm_unminimize (ShellWM *wm, + MetaWindowActor *actor); +void _shell_wm_size_changed(ShellWM *wm, + MetaWindowActor *actor); +void _shell_wm_size_change(ShellWM *wm, + MetaWindowActor *actor, + MetaSizeChange which_change, + MetaRectangle *old_frame_rect, + MetaRectangle *old_buffer_rect); +void _shell_wm_map (ShellWM *wm, + MetaWindowActor *actor); +void _shell_wm_destroy (ShellWM *wm, + MetaWindowActor *actor); + +void _shell_wm_switch_workspace (ShellWM *wm, + gint from, + gint to, + MetaMotionDirection direction); +void _shell_wm_kill_window_effects (ShellWM *wm, + MetaWindowActor *actor); +void _shell_wm_kill_switch_workspace (ShellWM *wm); + +void _shell_wm_show_tile_preview (ShellWM *wm, + MetaWindow *window, + MetaRectangle *tile_rect, + int tile_monitor); +void _shell_wm_hide_tile_preview (ShellWM *wm); +void _shell_wm_show_window_menu (ShellWM *wm, + MetaWindow *window, + MetaWindowMenuType menu, + int x, + int y); +void _shell_wm_show_window_menu_for_rect (ShellWM *wm, + MetaWindow *window, + MetaWindowMenuType menu, + MetaRectangle *rect); + +gboolean _shell_wm_filter_keybinding (ShellWM *wm, + MetaKeyBinding *binding); + +void _shell_wm_confirm_display_change (ShellWM *wm); + +MetaCloseDialog * _shell_wm_create_close_dialog (ShellWM *wm, + MetaWindow *window); + +MetaInhibitShortcutsDialog * _shell_wm_create_inhibit_shortcuts_dialog (ShellWM *wm, + MetaWindow *window); + +G_END_DECLS + +#endif /* __SHELL_WM_PRIVATE_H__ */ diff --git a/src/shell-wm.c b/src/shell-wm.c new file mode 100644 index 0000000..a11e419 --- /dev/null +++ b/src/shell-wm.c @@ -0,0 +1,462 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include <string.h> + +#include <meta/meta-enum-types.h> +#include <meta/keybindings.h> + +#include "shell-wm-private.h" +#include "shell-global.h" + +struct _ShellWM { + GObject parent; + + MetaPlugin *plugin; +}; + +/* Signals */ +enum +{ + MINIMIZE, + UNMINIMIZE, + SIZE_CHANGED, + SIZE_CHANGE, + MAP, + DESTROY, + SWITCH_WORKSPACE, + KILL_SWITCH_WORKSPACE, + KILL_WINDOW_EFFECTS, + SHOW_TILE_PREVIEW, + HIDE_TILE_PREVIEW, + SHOW_WINDOW_MENU, + FILTER_KEYBINDING, + CONFIRM_DISPLAY_CHANGE, + CREATE_CLOSE_DIALOG, + CREATE_INHIBIT_SHORTCUTS_DIALOG, + + LAST_SIGNAL +}; + +G_DEFINE_TYPE(ShellWM, shell_wm, G_TYPE_OBJECT); + +static guint shell_wm_signals [LAST_SIGNAL] = { 0 }; + +static void +shell_wm_init (ShellWM *wm) +{ +} + +static void +shell_wm_finalize (GObject *object) +{ + G_OBJECT_CLASS (shell_wm_parent_class)->finalize (object); +} + +static void +shell_wm_class_init (ShellWMClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_wm_finalize; + + shell_wm_signals[MINIMIZE] = + g_signal_new ("minimize", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[UNMINIMIZE] = + g_signal_new ("unminimize", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[SIZE_CHANGED] = + g_signal_new ("size-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[SIZE_CHANGE] = + g_signal_new ("size-change", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 4, + META_TYPE_WINDOW_ACTOR, META_TYPE_SIZE_CHANGE, META_TYPE_RECTANGLE, META_TYPE_RECTANGLE); + shell_wm_signals[MAP] = + g_signal_new ("map", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[DESTROY] = + g_signal_new ("destroy", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[SWITCH_WORKSPACE] = + g_signal_new ("switch-workspace", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 3, + G_TYPE_INT, G_TYPE_INT, G_TYPE_INT); + shell_wm_signals[KILL_SWITCH_WORKSPACE] = + g_signal_new ("kill-switch-workspace", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + shell_wm_signals[KILL_WINDOW_EFFECTS] = + g_signal_new ("kill-window-effects", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + META_TYPE_WINDOW_ACTOR); + shell_wm_signals[SHOW_TILE_PREVIEW] = + g_signal_new ("show-tile-preview", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, + META_TYPE_WINDOW, + META_TYPE_RECTANGLE, + G_TYPE_INT); + shell_wm_signals[HIDE_TILE_PREVIEW] = + g_signal_new ("hide-tile-preview", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + shell_wm_signals[SHOW_WINDOW_MENU] = + g_signal_new ("show-window-menu", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 3, + META_TYPE_WINDOW, G_TYPE_INT, META_TYPE_RECTANGLE); + shell_wm_signals[FILTER_KEYBINDING] = + g_signal_new ("filter-keybinding", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + g_signal_accumulator_true_handled, NULL, NULL, + G_TYPE_BOOLEAN, 1, + META_TYPE_KEY_BINDING); + shell_wm_signals[CONFIRM_DISPLAY_CHANGE] = + g_signal_new ("confirm-display-change", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + /** + * ShellWM::create-close-dialog: + * @wm: The WM + * @window: The window to create the dialog for + * + * Creates a close dialog for the given window. + * + * Returns: (transfer full): The close dialog instance. + */ + shell_wm_signals[CREATE_CLOSE_DIALOG] = + g_signal_new ("create-close-dialog", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + META_TYPE_CLOSE_DIALOG, 1, META_TYPE_WINDOW); + /** + * ShellWM::create-inhibit-shortcuts-dialog: + * @wm: The WM + * @window: The window to create the dialog for + * + * Creates an inhibit shortcuts dialog for the given window. + * + * Returns: (transfer full): The inhibit shortcuts dialog instance. + */ + shell_wm_signals[CREATE_INHIBIT_SHORTCUTS_DIALOG] = + g_signal_new ("create-inhibit-shortcuts-dialog", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + META_TYPE_INHIBIT_SHORTCUTS_DIALOG, 1, META_TYPE_WINDOW); +} + +void +_shell_wm_switch_workspace (ShellWM *wm, + gint from, + gint to, + MetaMotionDirection direction) +{ + g_signal_emit (wm, shell_wm_signals[SWITCH_WORKSPACE], 0, + from, to, direction); +} + +/** + * shell_wm_completed_switch_workspace: + * @wm: the ShellWM + * + * The plugin must call this when it has finished switching the + * workspace. + **/ +void +shell_wm_completed_switch_workspace (ShellWM *wm) +{ + meta_plugin_switch_workspace_completed (wm->plugin); +} + +/** + * shell_wm_completed_minimize: + * @wm: the ShellWM + * @actor: the MetaWindowActor actor + * + * The plugin must call this when it has completed a window minimize effect. + **/ +void +shell_wm_completed_minimize (ShellWM *wm, + MetaWindowActor *actor) +{ + meta_plugin_minimize_completed (wm->plugin, actor); +} + +/** + * shell_wm_completed_unminimize: + * @wm: the ShellWM + * @actor: the MetaWindowActor actor + * + * The plugin must call this when it has completed a window unminimize effect. + **/ +void +shell_wm_completed_unminimize (ShellWM *wm, + MetaWindowActor *actor) +{ + meta_plugin_unminimize_completed (wm->plugin, actor); +} + +void +shell_wm_completed_size_change (ShellWM *wm, + MetaWindowActor *actor) +{ + meta_plugin_size_change_completed (wm->plugin, actor); +} + +/** + * shell_wm_completed_map: + * @wm: the ShellWM + * @actor: the MetaWindowActor actor + * + * The plugin must call this when it has completed a window map effect. + **/ +void +shell_wm_completed_map (ShellWM *wm, + MetaWindowActor *actor) +{ + meta_plugin_map_completed (wm->plugin, actor); +} + +/** + * shell_wm_completed_destroy: + * @wm: the ShellWM + * @actor: the MetaWindowActor actor + * + * The plugin must call this when it has completed a window destroy effect. + **/ +void +shell_wm_completed_destroy (ShellWM *wm, + MetaWindowActor *actor) +{ + meta_plugin_destroy_completed (wm->plugin, actor); +} + +/** + * shell_wm_complete_display_change: + * @wm: the ShellWM + * @ok: if the new configuration was OK + * + * The plugin must call this after the user responded to the confirmation dialog. + */ +void +shell_wm_complete_display_change (ShellWM *wm, + gboolean ok) +{ + meta_plugin_complete_display_change (wm->plugin, ok); +} + +void +_shell_wm_kill_switch_workspace (ShellWM *wm) +{ + g_signal_emit (wm, shell_wm_signals[KILL_SWITCH_WORKSPACE], 0); +} + +void +_shell_wm_kill_window_effects (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[KILL_WINDOW_EFFECTS], 0, actor); +} + +void +_shell_wm_show_tile_preview (ShellWM *wm, + MetaWindow *window, + MetaRectangle *tile_rect, + int tile_monitor) +{ + g_signal_emit (wm, shell_wm_signals[SHOW_TILE_PREVIEW], 0, + window, tile_rect, tile_monitor); +} + +void +_shell_wm_hide_tile_preview (ShellWM *wm) +{ + g_signal_emit (wm, shell_wm_signals[HIDE_TILE_PREVIEW], 0); +} + +void +_shell_wm_show_window_menu (ShellWM *wm, + MetaWindow *window, + MetaWindowMenuType menu, + int x, + int y) +{ + MetaRectangle rect; + + rect.x = x; + rect.y = y; + rect.width = rect.height = 0; + + _shell_wm_show_window_menu_for_rect (wm, window, menu, &rect); +} + +void +_shell_wm_show_window_menu_for_rect (ShellWM *wm, + MetaWindow *window, + MetaWindowMenuType menu, + MetaRectangle *rect) +{ + g_signal_emit (wm, shell_wm_signals[SHOW_WINDOW_MENU], 0, window, menu, rect); +} + +void +_shell_wm_minimize (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[MINIMIZE], 0, actor); +} + +void +_shell_wm_unminimize (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[UNMINIMIZE], 0, actor); +} + +void +_shell_wm_size_changed (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[SIZE_CHANGED], 0, actor); +} + +void +_shell_wm_size_change (ShellWM *wm, + MetaWindowActor *actor, + MetaSizeChange which_change, + MetaRectangle *old_frame_rect, + MetaRectangle *old_buffer_rect) +{ + g_signal_emit (wm, shell_wm_signals[SIZE_CHANGE], 0, actor, which_change, old_frame_rect, old_buffer_rect); +} + +void +_shell_wm_map (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[MAP], 0, actor); +} + +void +_shell_wm_destroy (ShellWM *wm, + MetaWindowActor *actor) +{ + g_signal_emit (wm, shell_wm_signals[DESTROY], 0, actor); +} + +gboolean +_shell_wm_filter_keybinding (ShellWM *wm, + MetaKeyBinding *binding) +{ + gboolean rv; + + g_signal_emit (wm, shell_wm_signals[FILTER_KEYBINDING], 0, binding, &rv); + + return rv; +} + +void +_shell_wm_confirm_display_change (ShellWM *wm) +{ + g_signal_emit (wm, shell_wm_signals[CONFIRM_DISPLAY_CHANGE], 0); +} + +MetaCloseDialog * +_shell_wm_create_close_dialog (ShellWM *wm, + MetaWindow *window) +{ + MetaCloseDialog *dialog; + + g_signal_emit (wm, shell_wm_signals[CREATE_CLOSE_DIALOG], 0, window, &dialog); + + return dialog; +} + +MetaInhibitShortcutsDialog * +_shell_wm_create_inhibit_shortcuts_dialog (ShellWM *wm, + MetaWindow *window) +{ + MetaInhibitShortcutsDialog *dialog; + + g_signal_emit (wm, shell_wm_signals[CREATE_INHIBIT_SHORTCUTS_DIALOG], 0, window, &dialog); + + return dialog; +} + +/** + * shell_wm_new: + * @plugin: the #MetaPlugin + * + * Creates a new window management interface by hooking into @plugin. + * + * Return value: the new window-management interface + **/ +ShellWM * +shell_wm_new (MetaPlugin *plugin) +{ + ShellWM *wm; + + wm = g_object_new (SHELL_TYPE_WM, NULL); + wm->plugin = plugin; + + return wm; +} diff --git a/src/shell-wm.h b/src/shell-wm.h new file mode 100644 index 0000000..ddfe095 --- /dev/null +++ b/src/shell-wm.h @@ -0,0 +1,32 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_WM_H__ +#define __SHELL_WM_H__ + +#include <glib-object.h> +#include <meta/meta-plugin.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_WM (shell_wm_get_type ()) +G_DECLARE_FINAL_TYPE (ShellWM, shell_wm, SHELL, WM, GObject) + +ShellWM *shell_wm_new (MetaPlugin *plugin); + +void shell_wm_completed_minimize (ShellWM *wm, + MetaWindowActor *actor); +void shell_wm_completed_unminimize (ShellWM *wm, + MetaWindowActor *actor); +void shell_wm_completed_size_change (ShellWM *wm, + MetaWindowActor *actor); +void shell_wm_completed_map (ShellWM *wm, + MetaWindowActor *actor); +void shell_wm_completed_destroy (ShellWM *wm, + MetaWindowActor *actor); +void shell_wm_completed_switch_workspace (ShellWM *wm); + +void shell_wm_complete_display_change (ShellWM *wm, + gboolean ok); + +G_END_DECLS + +#endif /* __SHELL_WM_H__ */ diff --git a/src/shell-workspace-background.c b/src/shell-workspace-background.c new file mode 100644 index 0000000..1c0bbc3 --- /dev/null +++ b/src/shell-workspace-background.c @@ -0,0 +1,226 @@ +#include "config.h" + +#include "shell-workspace-background.h" + +#include "shell-global.h" +#include <meta/meta-workspace-manager.h> + +#define BACKGROUND_MARGIN 12 + +enum +{ + PROP_0, + + PROP_MONITOR_INDEX, + PROP_STATE_ADJUSTMENT_VALUE, + + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +struct _ShellWorkspaceBackground +{ + /*< private >*/ + StWidget parent_instance; + + int monitor_index; + double state_adjustment_value; + + MetaRectangle work_area; + MetaRectangle monitor_geometry; +}; + +G_DEFINE_TYPE (ShellWorkspaceBackground, shell_workspace_background, ST_TYPE_WIDGET); + +static void +on_workareas_changed (ShellWorkspaceBackground *self) +{ + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaWorkspaceManager *workspace_manager = shell_global_get_workspace_manager (global); + MetaWorkspace *workspace = + meta_workspace_manager_get_workspace_by_index (workspace_manager, 0); + + meta_workspace_get_work_area_for_monitor (workspace, + self->monitor_index, + &self->work_area); + + meta_display_get_monitor_geometry (display, + self->monitor_index, + &self->monitor_geometry); +} + +static void +shell_workspace_background_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + ShellWorkspaceBackground *self = SHELL_WORKSPACE_BACKGROUND (actor); + ShellGlobal *global = shell_global_get (); + ClutterStage *stage = shell_global_get_stage (global); + StThemeContext *context = st_theme_context_get_for_stage (stage); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox scaled_box, my_box, content_box; + ClutterActor *child; + float content_width, content_height; + float scaled_width, scaled_height; + float x_scale, y_scale; + float width, height; + int left_offset, right_offset; + int top_offset, bottom_offset; + int scale_factor; + + scale_factor = st_theme_context_get_scale_factor (context); + + clutter_actor_box_get_size (box, &width, &height); + scaled_height = height - BACKGROUND_MARGIN * 2 * scale_factor; + scaled_width = (scaled_height / height) * width; + + scaled_box.x1 = box->x1 + (width - scaled_width) / 2; + scaled_box.y1 = box->y1 + (height - scaled_height) / 2; + clutter_actor_box_set_size (&scaled_box, scaled_width, scaled_height); + + clutter_actor_box_interpolate(box, &scaled_box, + self->state_adjustment_value, &my_box); + + clutter_actor_set_allocation (actor, &my_box); + + st_theme_node_get_content_box (theme_node, &my_box, &content_box); + + child = clutter_actor_get_first_child (actor); + clutter_actor_allocate (child, &content_box); + + clutter_actor_box_get_size (&content_box, &content_width, &content_height); + x_scale = content_width / self->work_area.width; + y_scale = content_height / self->work_area.height; + + left_offset = self->work_area.x - self->monitor_geometry.x; + top_offset = self->work_area.y - self->monitor_geometry.y; + right_offset = self->monitor_geometry.width - self->work_area.width - left_offset; + bottom_offset = self->monitor_geometry.height - self->work_area.height - top_offset; + + clutter_actor_box_set_origin (&content_box, + -left_offset * x_scale, + -top_offset * y_scale); + clutter_actor_box_set_size (&content_box, + content_width + (left_offset + right_offset) * x_scale, + content_height + (top_offset + bottom_offset) * y_scale); + + child = clutter_actor_get_first_child (child); + clutter_actor_allocate (child, &content_box); +} + +static void +shell_workspace_background_constructed (GObject *object) +{ + G_OBJECT_CLASS (shell_workspace_background_parent_class)->constructed (object); + + on_workareas_changed (SHELL_WORKSPACE_BACKGROUND (object)); +} + +static void +shell_workspace_background_get_property (GObject *gobject, + unsigned int property_id, + GValue *value, + GParamSpec *pspec) +{ + ShellWorkspaceBackground *self = SHELL_WORKSPACE_BACKGROUND (gobject); + + switch (property_id) + { + case PROP_MONITOR_INDEX: + g_value_set_int (value, self->monitor_index); + break; + + case PROP_STATE_ADJUSTMENT_VALUE: + g_value_set_double (value, self->state_adjustment_value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, property_id, pspec); + } +} + +static void +shell_workspace_background_set_property (GObject *gobject, + unsigned int property_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellWorkspaceBackground *self = SHELL_WORKSPACE_BACKGROUND (gobject); + + switch (property_id) + { + case PROP_MONITOR_INDEX: + { + int new_value = g_value_get_int (value); + if (self->monitor_index != new_value) + { + self->monitor_index = new_value; + g_object_notify_by_pspec (gobject, obj_props[PROP_MONITOR_INDEX]); + } + } + break; + + case PROP_STATE_ADJUSTMENT_VALUE: + { + double new_value = g_value_get_double (value); + if (self->state_adjustment_value != new_value) + { + self->state_adjustment_value = new_value; + g_object_notify_by_pspec (gobject, obj_props[PROP_STATE_ADJUSTMENT_VALUE]); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, property_id, pspec); + } +} + +static void +shell_workspace_background_class_init (ShellWorkspaceBackgroundClass *klass) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + actor_class->allocate = shell_workspace_background_allocate; + + gobject_class->constructed = shell_workspace_background_constructed; + gobject_class->get_property = shell_workspace_background_get_property; + gobject_class->set_property = shell_workspace_background_set_property; + + /** + * ShellWorkspaceBackground:monitor-index: + */ + obj_props[PROP_MONITOR_INDEX] = + g_param_spec_int ("monitor-index", "", "", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * ShellWorkspaceBackground:state-adjustment-value: + */ + obj_props[PROP_STATE_ADJUSTMENT_VALUE] = + g_param_spec_double ("state-adjustment-value", "", "", + -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, PROP_LAST, obj_props); +} + +static void +shell_workspace_background_init (ShellWorkspaceBackground *self) +{ + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + + g_signal_connect_object (display, "workareas-changed", + G_CALLBACK (on_workareas_changed), + self, G_CONNECT_SWAPPED); +} diff --git a/src/shell-workspace-background.h b/src/shell-workspace-background.h new file mode 100644 index 0000000..48b2bd6 --- /dev/null +++ b/src/shell-workspace-background.h @@ -0,0 +1,14 @@ +#ifndef __SHELL_WORKSPACE_BACKGROUND_H__ +#define __SHELL_WORKSPACE_BACKGROUND_H__ + +#include <st/st.h> + +G_BEGIN_DECLS + +#define SHELL_TYPE_WORKSPACE_BACKGROUND (shell_workspace_background_get_type ()) +G_DECLARE_FINAL_TYPE (ShellWorkspaceBackground, shell_workspace_background, + SHELL, WORKSPACE_BACKGROUND, StWidget) + +G_END_DECLS + +#endif /* __SHELL_WORKSPACE_BACKGROUND_H__ */ diff --git a/src/st/croco/cr-additional-sel.c b/src/st/croco/cr-additional-sel.c new file mode 100644 index 0000000..9bd8d6a --- /dev/null +++ b/src/st/croco/cr-additional-sel.c @@ -0,0 +1,498 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + * + */ + +#include "cr-additional-sel.h" +#include "string.h" + +/** + * CRAdditionalSel: + * + * #CRAdditionalSel abstracts an additional selector. + * An additional selector is the selector part + * that comes after the combination of type selectors. + * It can be either "a class selector (the .class part), + * a pseudo class selector, an attribute selector + * or an id selector. + */ + +/** + * cr_additional_sel_new: + * + * Default constructor of #CRAdditionalSel. + * Returns the newly build instance of #CRAdditionalSel. + */ +CRAdditionalSel * +cr_additional_sel_new (void) +{ + CRAdditionalSel *result = NULL; + + result = g_try_malloc (sizeof (CRAdditionalSel)); + + if (result == NULL) { + cr_utils_trace_debug ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRAdditionalSel)); + + return result; +} + +/** + * cr_additional_sel_new_with_type: + * @a_sel_type: the type of the newly built instance + * of #CRAdditionalSel. + * + * Constructor of #CRAdditionalSel. + * Returns the newly built instance of #CRAdditionalSel. + */ +CRAdditionalSel * +cr_additional_sel_new_with_type (enum AddSelectorType a_sel_type) +{ + CRAdditionalSel *result = NULL; + + result = cr_additional_sel_new (); + + g_return_val_if_fail (result, NULL); + + result->type = a_sel_type; + + return result; +} + +/** + * cr_additional_sel_set_class_name: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_class_name: the new class name to set. + * + * Sets a new class name to a + * CLASS additional selector. + */ +void +cr_additional_sel_set_class_name (CRAdditionalSel * a_this, + CRString * a_class_name) +{ + g_return_if_fail (a_this && a_this->type == CLASS_ADD_SELECTOR); + + if (a_this->content.class_name) { + cr_string_destroy (a_this->content.class_name); + } + + a_this->content.class_name = a_class_name; +} + +/** + * cr_additional_sel_set_id_name: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_id: the new id to set. + * + * Sets a new id name to an + * ID additional selector. + */ +void +cr_additional_sel_set_id_name (CRAdditionalSel * a_this, CRString * a_id) +{ + g_return_if_fail (a_this && a_this->type == ID_ADD_SELECTOR); + + if (a_this->content.id_name) { + cr_string_destroy (a_this->content.id_name); + } + + a_this->content.id_name = a_id; +} + +/** + * cr_additional_sel_set_pseudo: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_pseudo: the new pseudo to set. + * + * Sets a new pseudo to a + * PSEUDO additional selector. + */ +void +cr_additional_sel_set_pseudo (CRAdditionalSel * a_this, CRPseudo * a_pseudo) +{ + g_return_if_fail (a_this + && a_this->type == PSEUDO_CLASS_ADD_SELECTOR); + + if (a_this->content.pseudo) { + cr_pseudo_destroy (a_this->content.pseudo); + } + + a_this->content.pseudo = a_pseudo; +} + +/** + * cr_additional_sel_set_attr_sel: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_sel: the new instance of #CRAttrSel to set. + * + * Sets a new instance of #CRAttrSel to + * a ATTRIBUTE additional selector. + */ +void +cr_additional_sel_set_attr_sel (CRAdditionalSel * a_this, CRAttrSel * a_sel) +{ + g_return_if_fail (a_this && a_this->type == ATTRIBUTE_ADD_SELECTOR); + + if (a_this->content.attr_sel) { + cr_attr_sel_destroy (a_this->content.attr_sel); + } + + a_this->content.attr_sel = a_sel; +} + +/** + * cr_additional_sel_append: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_sel: the new instance to #CRAdditional to append. + * + * Appends a new instance of #CRAdditional to the + * current list of #CRAdditional. + * + * Returns the new list of CRAdditionalSel or NULL if an error arises. + */ +CRAdditionalSel * +cr_additional_sel_append (CRAdditionalSel * a_this, CRAdditionalSel * a_sel) +{ + CRAdditionalSel *cur_sel = NULL; + + g_return_val_if_fail (a_sel, NULL); + + if (a_this == NULL) { + return a_sel; + } + + if (a_sel == NULL) + return NULL; + + for (cur_sel = a_this; + cur_sel && cur_sel->next; cur_sel = cur_sel->next) ; + + g_return_val_if_fail (cur_sel != NULL, NULL); + + cur_sel->next = a_sel; + a_sel->prev = cur_sel; + + return a_this; +} + +/** + * cr_additional_sel_prepend: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * @a_sel: the new instance to #CRAdditional to preappend. + * + * Preppends a new instance of #CRAdditional to the + * current list of #CRAdditional. + * + * Returns the new list of CRAdditionalSel or NULL if an error arises. + */ +CRAdditionalSel * +cr_additional_sel_prepend (CRAdditionalSel * a_this, CRAdditionalSel * a_sel) +{ + g_return_val_if_fail (a_sel, NULL); + + if (a_this == NULL) { + return a_sel; + } + + a_sel->next = a_this; + a_this->prev = a_sel; + + return a_sel; +} + +guchar * +cr_additional_sel_to_string (CRAdditionalSel const * a_this) +{ + guchar *result = NULL; + GString *str_buf = NULL; + CRAdditionalSel const *cur = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + + for (cur = a_this; cur; cur = cur->next) { + switch (cur->type) { + case CLASS_ADD_SELECTOR: + { + guchar *name = NULL; + + if (cur->content.class_name) { + name = (guchar *) g_strndup + (cur->content.class_name->stryng->str, + cur->content.class_name->stryng->len); + + if (name) { + g_string_append_printf + (str_buf, ".%s", + name); + g_free (name); + name = NULL; + } + } + } + break; + + case ID_ADD_SELECTOR: + { + guchar *name = NULL; + + if (cur->content.id_name) { + name = (guchar *) g_strndup + (cur->content.id_name->stryng->str, + cur->content.id_name->stryng->len); + + if (name) { + g_string_append_printf + (str_buf, "#%s", + name); + g_free (name); + name = NULL; + } + } + } + + break; + + case PSEUDO_CLASS_ADD_SELECTOR: + { + if (cur->content.pseudo) { + guchar *tmp_str = NULL; + + tmp_str = cr_pseudo_to_string + (cur->content.pseudo); + if (tmp_str) { + g_string_append_printf + (str_buf, ":%s", + tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + } + break; + + case ATTRIBUTE_ADD_SELECTOR: + if (cur->content.attr_sel) { + guchar *tmp_str = NULL; + + g_string_append_c (str_buf, '['); + tmp_str = cr_attr_sel_to_string + (cur->content.attr_sel); + if (tmp_str) { + g_string_append_printf + (str_buf, "%s]", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + break; + + default: + break; + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +guchar * +cr_additional_sel_one_to_string (CRAdditionalSel const *a_this) +{ + guchar *result = NULL; + GString *str_buf = NULL; + + g_return_val_if_fail (a_this, NULL) ; + + str_buf = g_string_new (NULL) ; + + switch (a_this->type) { + case CLASS_ADD_SELECTOR: + { + guchar *name = NULL; + + if (a_this->content.class_name) { + name = (guchar *) g_strndup + (a_this->content.class_name->stryng->str, + a_this->content.class_name->stryng->len); + + if (name) { + g_string_append_printf + (str_buf, ".%s", + name); + g_free (name); + name = NULL; + } + } + } + break; + + case ID_ADD_SELECTOR: + { + guchar *name = NULL; + + if (a_this->content.id_name) { + name = (guchar *) g_strndup + (a_this->content.id_name->stryng->str, + a_this->content.id_name->stryng->len); + + if (name) { + g_string_append_printf + (str_buf, "#%s", + name); + g_free (name); + name = NULL; + } + } + } + + break; + + case PSEUDO_CLASS_ADD_SELECTOR: + { + if (a_this->content.pseudo) { + guchar *tmp_str = NULL; + + tmp_str = cr_pseudo_to_string + (a_this->content.pseudo); + if (tmp_str) { + g_string_append_printf + (str_buf, ":%s", + tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + } + break; + + case ATTRIBUTE_ADD_SELECTOR: + if (a_this->content.attr_sel) { + guchar *tmp_str = NULL; + + g_string_append_printf (str_buf, "["); + tmp_str = cr_attr_sel_to_string + (a_this->content.attr_sel); + if (tmp_str) { + g_string_append_printf + (str_buf, "%s]", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + break; + + default: + break; + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +/** + * cr_additional_sel_dump: + * @a_this: the "this pointer" of the current instance of + * #CRAdditionalSel. + * @a_fp: the destination file. + * + * Dumps the current instance of #CRAdditionalSel to a file + */ +void +cr_additional_sel_dump (CRAdditionalSel const * a_this, FILE * a_fp) +{ + guchar *tmp_str = NULL; + + g_return_if_fail (a_fp); + + if (a_this) { + tmp_str = cr_additional_sel_to_string (a_this); + if (tmp_str) { + fprintf (a_fp, "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } +} + +/** + * cr_additional_sel_destroy: + * @a_this: the "this pointer" of the current instance + * of #CRAdditionalSel . + * + * Destroys an instance of #CRAdditional. + */ +void +cr_additional_sel_destroy (CRAdditionalSel * a_this) +{ + g_return_if_fail (a_this); + + switch (a_this->type) { + case CLASS_ADD_SELECTOR: + cr_string_destroy (a_this->content.class_name); + a_this->content.class_name = NULL; + break; + + case PSEUDO_CLASS_ADD_SELECTOR: + cr_pseudo_destroy (a_this->content.pseudo); + a_this->content.pseudo = NULL; + break; + + case ID_ADD_SELECTOR: + cr_string_destroy (a_this->content.id_name); + a_this->content.id_name = NULL; + break; + + case ATTRIBUTE_ADD_SELECTOR: + cr_attr_sel_destroy (a_this->content.attr_sel); + a_this->content.attr_sel = NULL; + break; + + default: + break; + } + + if (a_this->next) { + cr_additional_sel_destroy (a_this->next); + } + + g_free (a_this); +} diff --git a/src/st/croco/cr-additional-sel.h b/src/st/croco/cr-additional-sel.h new file mode 100644 index 0000000..7ca3e07 --- /dev/null +++ b/src/st/croco/cr-additional-sel.h @@ -0,0 +1,98 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See the COPYRIGHTS file for copyright information. + */ + + +#ifndef __CR_ADD_SEL_H__ +#define __CR_ADD_SEL_H__ + +#include <stdio.h> +#include <glib.h> +#include "cr-utils.h" +#include "cr-attr-sel.h" +#include "cr-pseudo.h" +#include "cr-additional-sel.h" + +G_BEGIN_DECLS + +enum AddSelectorType +{ + NO_ADD_SELECTOR = 0 , + CLASS_ADD_SELECTOR = 1 , + PSEUDO_CLASS_ADD_SELECTOR = 1 << 1, + ID_ADD_SELECTOR = 1 << 3, + ATTRIBUTE_ADD_SELECTOR = 1 << 4 +} ; + +union CRAdditionalSelectorContent +{ + CRString *class_name ; + CRString *id_name ; + CRPseudo *pseudo ; + CRAttrSel *attr_sel ; +} ; + +typedef struct _CRAdditionalSel CRAdditionalSel ; + +struct _CRAdditionalSel +{ + enum AddSelectorType type ; + union CRAdditionalSelectorContent content ; + + CRAdditionalSel * next ; + CRAdditionalSel * prev ; + CRParsingLocation location ; +} ; + +CRAdditionalSel * cr_additional_sel_new (void) ; + +CRAdditionalSel * cr_additional_sel_new_with_type (enum AddSelectorType a_sel_type) ; + +CRAdditionalSel * cr_additional_sel_append (CRAdditionalSel *a_this, + CRAdditionalSel *a_sel) ; + +void cr_additional_sel_set_class_name (CRAdditionalSel *a_this, + CRString *a_class_name) ; + +void cr_additional_sel_set_id_name (CRAdditionalSel *a_this, + CRString *a_id) ; + +void cr_additional_sel_set_pseudo (CRAdditionalSel *a_this, + CRPseudo *a_pseudo) ; + +void cr_additional_sel_set_attr_sel (CRAdditionalSel *a_this, + CRAttrSel *a_sel) ; + +CRAdditionalSel * cr_additional_sel_prepend (CRAdditionalSel *a_this, + CRAdditionalSel *a_sel) ; + +guchar * cr_additional_sel_to_string (CRAdditionalSel const *a_this) ; + +guchar * cr_additional_sel_one_to_string (CRAdditionalSel const *a_this) ; + +void cr_additional_sel_dump (CRAdditionalSel const *a_this, FILE *a_fp) ; + +void cr_additional_sel_destroy (CRAdditionalSel *a_this) ; + +G_END_DECLS + +#endif /*__CR_ADD_SEL_H*/ diff --git a/src/st/croco/cr-attr-sel.c b/src/st/croco/cr-attr-sel.c new file mode 100644 index 0000000..fc8e6ef --- /dev/null +++ b/src/st/croco/cr-attr-sel.c @@ -0,0 +1,234 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See COPYRIGHTS file for copyrights information. + */ + +#include <stdio.h> +#include "cr-attr-sel.h" + +/** + * CRAttrSel: + * + * #CRAdditionalSel abstracts an attribute selector. + * Attributes selectors are described in the css2 spec [5.8]. + * There are more generally used in the css2 selectors described in + * css2 spec [5] . + */ + +/** + * cr_attr_sel_new: + * The constructor of #CRAttrSel. + * Returns the newly allocated instance + * of #CRAttrSel. + */ +CRAttrSel * +cr_attr_sel_new (void) +{ + CRAttrSel *result = NULL; + + result = g_malloc0 (sizeof (CRAttrSel)); + + return result; +} + +/** + * cr_attr_sel_append_attr_sel: + * @a_this: the this pointer of the current instance of #CRAttrSel. + * @a_attr_sel: selector to append. + * + * Appends an attribute selector to the current list of + * attribute selectors represented by a_this. + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_attr_sel_append_attr_sel (CRAttrSel * a_this, CRAttrSel * a_attr_sel) +{ + CRAttrSel *cur_sel = NULL; + + g_return_val_if_fail (a_this && a_attr_sel, + CR_BAD_PARAM_ERROR); + + for (cur_sel = a_this; + cur_sel->next; + cur_sel = cur_sel->next) ; + + cur_sel->next = a_attr_sel; + a_attr_sel->prev = cur_sel; + + return CR_OK; +} + +/** + * cr_attr_sel_prepend_attr_sel: + *@a_this: the "this pointer" of the current instance *of #CRAttrSel. + *@a_attr_sel: the attribute selector to append. + * + *Prepends an attribute selector to the list of + *attributes selector represented by a_this. + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_attr_sel_prepend_attr_sel (CRAttrSel * a_this, + CRAttrSel * a_attr_sel) +{ + g_return_val_if_fail (a_this && a_attr_sel, + CR_BAD_PARAM_ERROR); + + a_attr_sel->next = a_this; + a_this->prev = a_attr_sel; + + return CR_OK; +} + +/** + * cr_attr_sel_to_string: + * @a_this: the current instance of #CRAttrSel. + * + * Serializes an attribute selector into a string + * Returns the serialized attribute selector. + */ +guchar * +cr_attr_sel_to_string (CRAttrSel const * a_this) +{ + CRAttrSel const *cur = NULL; + guchar *result = NULL; + GString *str_buf = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + + for (cur = a_this; cur; cur = cur->next) { + if (cur->prev) { + g_string_append_c (str_buf, ' '); + } + + if (cur->name) { + guchar *name = NULL; + + name = (guchar *) g_strndup (cur->name->stryng->str, + cur->name->stryng->len); + if (name) { + g_string_append (str_buf, (const gchar *) name); + g_free (name); + name = NULL; + } + } + + if (cur->value) { + guchar *value = NULL; + + value = (guchar *) g_strndup (cur->value->stryng->str, + cur->value->stryng->len); + if (value) { + switch (cur->match_way) { + case SET: + break; + + case EQUALS: + g_string_append_c (str_buf, '='); + break; + + case INCLUDES: + g_string_append (str_buf, "~="); + break; + + case DASHMATCH: + g_string_append (str_buf, "|="); + break; + + default: + break; + } + + g_string_append_printf + (str_buf, "\"%s\"", value); + + g_free (value); + value = NULL; + } + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + } + + return result; +} + +/** + * cr_attr_sel_dump: + * @a_this: the "this pointer" of the current instance of + * #CRAttrSel. + * @a_fp: the destination file. + * + * Dumps the current instance of #CRAttrSel to a file. + */ +void +cr_attr_sel_dump (CRAttrSel const * a_this, FILE * a_fp) +{ + guchar *tmp_str = NULL; + + g_return_if_fail (a_this); + + tmp_str = cr_attr_sel_to_string (a_this); + + if (tmp_str) { + fprintf (a_fp, "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } +} + +/** + *cr_attr_sel_destroy: + *@a_this: the "this pointer" of the current + *instance of #CRAttrSel. + * + *Destroys the current instance of #CRAttrSel. + *Frees all the fields if they are non null. + */ +void +cr_attr_sel_destroy (CRAttrSel * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->name) { + cr_string_destroy (a_this->name); + a_this->name = NULL; + } + + if (a_this->value) { + cr_string_destroy (a_this->value); + a_this->value = NULL; + } + + if (a_this->next) { + cr_attr_sel_destroy (a_this->next); + a_this->next = NULL; + } + + if (a_this) { + g_free (a_this); + a_this = NULL; + } +} + diff --git a/src/st/croco/cr-attr-sel.h b/src/st/croco/cr-attr-sel.h new file mode 100644 index 0000000..82d5a87 --- /dev/null +++ b/src/st/croco/cr-attr-sel.h @@ -0,0 +1,74 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_ATTR_SEL_H__ +#define __CR_ATTR_SEL_H__ + +#include <stdio.h> +#include <glib.h> +#include "cr-utils.h" +#include "cr-parsing-location.h" +#include "cr-string.h" + +G_BEGIN_DECLS + + +struct _CRAttrSel ; +typedef struct _CRAttrSel CRAttrSel ; + +enum AttrMatchWay +{ + NO_MATCH = 0, + SET, + EQUALS, + INCLUDES, + DASHMATCH +} ; + +struct _CRAttrSel +{ + CRString *name ; + CRString *value ; + enum AttrMatchWay match_way ; + CRAttrSel *next ; + CRAttrSel *prev ; + CRParsingLocation location ; +} ; + +CRAttrSel * cr_attr_sel_new (void) ; + +enum CRStatus cr_attr_sel_append_attr_sel (CRAttrSel * a_this, + CRAttrSel *a_attr_sel) ; + +enum CRStatus cr_attr_sel_prepend_attr_sel (CRAttrSel *a_this, + CRAttrSel *a_attr_sel) ; + +guchar * cr_attr_sel_to_string (CRAttrSel const *a_this) ; + +void cr_attr_sel_dump (CRAttrSel const *a_this, FILE *a_fp) ; + +void cr_attr_sel_destroy (CRAttrSel *a_this) ; + +G_END_DECLS + +#endif /*__CR_ATTR_SEL_H__*/ diff --git a/src/st/croco/cr-cascade.c b/src/st/croco/cr-cascade.c new file mode 100644 index 0000000..68f59bb --- /dev/null +++ b/src/st/croco/cr-cascade.c @@ -0,0 +1,215 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2003 Dodji Seketeli <dodji@seketeli.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the + * GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +/* + *$Id$ + */ + +#include <string.h> +#include "cr-cascade.h" + +#define PRIVATE(a_this) ((a_this)->priv) + +struct _CRCascadePriv { + /** + *the 3 style sheets of the cascade: + *author, user, and useragent sheet. + *Intended to be addressed by + *sheets[ORIGIN_AUTHOR] or sheets[ORIGIN_USER] + *of sheets[ORIGIN_UA] ; + */ + CRStyleSheet *sheets[3]; + guint ref_count; +}; + +/** + * cr_cascade_new: + *@a_author_sheet: the author origin style sheet. May be NULL. + *@a_user_sheet: the user origin style sheet. May be NULL. + *@a_ua_sheet: the user agent origin style sheet. May be NULL. + * + *Constructor of the #CRCascade class. + *Note that all three parameters of this + *method are ref counted and their refcount is increased. + *Their refcount will be decreased at the destruction of + *the instance of #CRCascade. + *So the caller should not call their destructor. The caller + *should call their ref/unref method instead if it wants + * + *Returns the newly built instance of CRCascade or NULL if + *an error arose during construction. + */ +CRCascade * +cr_cascade_new (CRStyleSheet * a_author_sheet, + CRStyleSheet * a_user_sheet, CRStyleSheet * a_ua_sheet) +{ + CRCascade *result = NULL; + + result = g_try_malloc (sizeof (CRCascade)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRCascade)); + + PRIVATE (result) = g_try_malloc (sizeof (CRCascadePriv)); + if (!PRIVATE (result)) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + memset (PRIVATE (result), 0, sizeof (CRCascadePriv)); + + if (a_author_sheet) { + cr_cascade_set_sheet (result, a_author_sheet, ORIGIN_AUTHOR); + } + if (a_user_sheet) { + cr_cascade_set_sheet (result, a_user_sheet, ORIGIN_USER); + } + if (a_ua_sheet) { + cr_cascade_set_sheet (result, a_ua_sheet, ORIGIN_UA); + } + + return result; +} + +/** + * cr_cascade_get_sheet: + *@a_this: the current instance of #CRCascade. + *@a_origin: the origin of the style sheet as + *defined in the css2 spec in chapter 6.4. + *Gets a given origin sheet. + * + *Gets a sheet, part of the cascade. + *Note that the returned stylesheet + *is refcounted so if the caller wants + *to manage it's lifecycle, it must use + *cr_stylesheet_ref()/cr_stylesheet_unref() instead + *of the cr_stylesheet_destroy() method. + *Returns the style sheet, or NULL if it does not + *exist. + */ +CRStyleSheet * +cr_cascade_get_sheet (CRCascade * a_this, enum CRStyleOrigin a_origin) +{ + g_return_val_if_fail (a_this + && a_origin >= ORIGIN_UA + && a_origin < NB_ORIGINS, NULL); + + return PRIVATE (a_this)->sheets[a_origin]; +} + +/** + * cr_cascade_set_sheet: + *@a_this: the current instance of #CRCascade. + *@a_sheet: the stylesheet to set. + *@a_origin: the origin of the stylesheet. + * + *Sets a stylesheet in the cascade + * + *Returns CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_cascade_set_sheet (CRCascade * a_this, + CRStyleSheet * a_sheet, enum CRStyleOrigin a_origin) +{ + g_return_val_if_fail (a_this + && a_sheet + && a_origin >= ORIGIN_UA + && a_origin < NB_ORIGINS, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->sheets[a_origin]) + cr_stylesheet_unref (PRIVATE (a_this)->sheets[a_origin]); + PRIVATE (a_this)->sheets[a_origin] = a_sheet; + cr_stylesheet_ref (a_sheet); + a_sheet->origin = a_origin; + return CR_OK; +} + +/** + *cr_cascade_ref: + *@a_this: the current instance of #CRCascade + * + *Increases the reference counter of the current instance + *of #CRCascade. + */ +void +cr_cascade_ref (CRCascade * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + PRIVATE (a_this)->ref_count++; +} + +/** + * cr_cascade_unref: + *@a_this: the current instance of + *#CRCascade. + * + *Decrements the reference counter associated + *to this instance of #CRCascade. If the reference + *counter reaches zero, the instance is destroyed + *using cr_cascade_destroy() + */ +void +cr_cascade_unref (CRCascade * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + if (PRIVATE (a_this)->ref_count) + PRIVATE (a_this)->ref_count--; + if (!PRIVATE (a_this)->ref_count) { + cr_cascade_destroy (a_this); + } +} + +/** + * cr_cascade_destroy: + * @a_this: the current instance of #CRCascade + * + * Destructor of #CRCascade. + */ +void +cr_cascade_destroy (CRCascade * a_this) +{ + g_return_if_fail (a_this); + + if (PRIVATE (a_this)) { + gulong i = 0; + + for (i = 0; i < NB_ORIGINS; i++) { + if (PRIVATE (a_this)->sheets[i]) { + if (cr_stylesheet_unref + (PRIVATE (a_this)->sheets[i]) + == TRUE) { + PRIVATE (a_this)->sheets[i] = NULL; + } + } + } + g_free (PRIVATE (a_this)); + PRIVATE (a_this) = NULL; + } + g_free (a_this); +} diff --git a/src/st/croco/cr-cascade.h b/src/st/croco/cr-cascade.h new file mode 100644 index 0000000..3119ae8 --- /dev/null +++ b/src/st/croco/cr-cascade.h @@ -0,0 +1,74 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the + * GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + */ + +/* + *$Id$ + */ + +#ifndef __CR_CASCADE_H__ +#define __CR_CASCADE_H__ + +#include "cr-stylesheet.h" + +/** + *@file + *the declaration of the #CRCascade class. + */ + +G_BEGIN_DECLS + + +typedef struct _CRCascadePriv CRCascadePriv ; + +/** + *An abstraction of the "Cascade" defined + *in the css2 spec, chapter 6.4. + */ +typedef struct _CRCascade CRCascade ; + +struct _CRCascade +{ + CRCascadePriv *priv ; +}; + + +CRCascade * cr_cascade_new (CRStyleSheet *a_author_sheet, + CRStyleSheet *a_user_sheet, + CRStyleSheet *a_ua_sheet) ; + +CRStyleSheet * cr_cascade_get_sheet (CRCascade *a_this, + enum CRStyleOrigin a_origin) ; + +enum CRStatus cr_cascade_set_sheet (CRCascade *a_this, + CRStyleSheet *a_sheet, + enum CRStyleOrigin a_origin) ; + +void cr_cascade_ref (CRCascade *a_this) ; + +void cr_cascade_unref (CRCascade *a_this) ; + +void cr_cascade_destroy (CRCascade *a_this) ; + +G_END_DECLS + +#endif /*__CR_CASCADE_H__*/ diff --git a/src/st/croco/cr-declaration.c b/src/st/croco/cr-declaration.c new file mode 100644 index 0000000..6c70128 --- /dev/null +++ b/src/st/croco/cr-declaration.c @@ -0,0 +1,798 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli. + * See COPYRIGHTS file for copyright information. + */ + + +#include <string.h> +#include "cr-declaration.h" +#include "cr-statement.h" +#include "cr-parser.h" + +/** + *@CRDeclaration: + * + *The definition of the #CRDeclaration class. + */ + +/** + * dump: + *@a_this: the current instance of #CRDeclaration. + *@a_fp: the destination file pointer. + *@a_indent: the number of indentation white char. + * + *Dumps (serializes) one css declaration to a file. + */ +static void +dump (CRDeclaration const * a_this, FILE * a_fp, glong a_indent) +{ + guchar *str = NULL; + + g_return_if_fail (a_this); + + str = (guchar *) cr_declaration_to_string (a_this, a_indent); + if (str) { + fprintf (a_fp, "%s", str); + g_free (str); + str = NULL; + } +} + +/** + * cr_declaration_new: + * @a_statement: the statement this declaration belongs to. can be NULL. + *@a_property: the property string of the declaration + *@a_value: the value expression of the declaration. + *Constructor of #CRDeclaration. + * + *Returns the newly built instance of #CRDeclaration, or NULL in + *case of error. + * + *The returned CRDeclaration takes ownership of @a_property and @a_value. + *(E.g. cr_declaration_destroy on this CRDeclaration will also free + *@a_property and @a_value.) + */ +CRDeclaration * +cr_declaration_new (CRStatement * a_statement, + CRString * a_property, CRTerm * a_value) +{ + CRDeclaration *result = NULL; + + g_return_val_if_fail (a_property, NULL); + + if (a_statement) + g_return_val_if_fail (a_statement + && ((a_statement->type == RULESET_STMT) + || (a_statement->type + == AT_FONT_FACE_RULE_STMT) + || (a_statement->type + == AT_PAGE_RULE_STMT)), NULL); + + result = g_try_malloc (sizeof (CRDeclaration)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRDeclaration)); + result->property = a_property; + result->value = a_value; + + if (a_value) { + cr_term_ref (a_value); + } + result->parent_statement = a_statement; + return result; +} + +/** + * cr_declaration_parse_from_buf: + *@a_statement: the parent css2 statement of this + *this declaration. Must be non NULL and of type + *RULESET_STMT (must be a ruleset). + *@a_str: the string that contains the statement. + *@a_enc: the encoding of a_str. + * + *Parses a text buffer that contains + *a css declaration. + *Returns the parsed declaration, or NULL in case of error. + */ +CRDeclaration * +cr_declaration_parse_from_buf (CRStatement * a_statement, + const guchar * a_str, enum CREncoding a_enc) +{ + enum CRStatus status = CR_OK; + CRTerm *value = NULL; + CRString *property = NULL; + CRDeclaration *result = NULL; + CRParser *parser = NULL; + gboolean important = FALSE; + + g_return_val_if_fail (a_str, NULL); + if (a_statement) + g_return_val_if_fail (a_statement->type == RULESET_STMT, + NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_str, strlen ((const char *) a_str), a_enc, FALSE); + g_return_val_if_fail (parser, NULL); + + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_declaration (parser, &property, + &value, &important); + if (status != CR_OK || !property) + goto cleanup; + + result = cr_declaration_new (a_statement, property, value); + if (result) { + property = NULL; + value = NULL; + result->important = important; + } + + cleanup: + + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + + if (property) { + cr_string_destroy (property); + property = NULL; + } + + if (value) { + cr_term_destroy (value); + value = NULL; + } + + return result; +} + +/** + * cr_declaration_parse_list_from_buf: + *@a_str: the input buffer that contains the list of declaration to + *parse. + *@a_enc: the encoding of a_str + * + *Parses a ';' separated list of properties declaration. + *Returns the parsed list of declaration, NULL if parsing failed. + */ +CRDeclaration * +cr_declaration_parse_list_from_buf (const guchar * a_str, + enum CREncoding a_enc) +{ + + enum CRStatus status = CR_OK; + CRTerm *value = NULL; + CRString *property = NULL; + CRDeclaration *result = NULL, + *cur_decl = NULL; + CRParser *parser = NULL; + CRTknzr *tokenizer = NULL; + gboolean important = FALSE; + + g_return_val_if_fail (a_str, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_str, strlen ((const char *) a_str), a_enc, FALSE); + g_return_val_if_fail (parser, NULL); + status = cr_parser_get_tknzr (parser, &tokenizer); + if (status != CR_OK || !tokenizer) { + if (status == CR_OK) + status = CR_ERROR; + goto cleanup; + } + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_declaration (parser, &property, + &value, &important); + if (status != CR_OK || !property) { + if (status != CR_OK) + status = CR_ERROR; + goto cleanup; + } + result = cr_declaration_new (NULL, property, value); + if (result) { + property = NULL; + value = NULL; + result->important = important; + } + /*now, go parse the other declarations */ + for (;;) { + guint32 c = 0; + + cr_parser_try_to_skip_spaces_and_comments (parser); + status = cr_tknzr_peek_char (tokenizer, &c); + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) + status = CR_OK; + goto cleanup; + } + if (c == ';') { + status = cr_tknzr_read_char (tokenizer, &c); + } else { + break; + } + important = FALSE; + cr_parser_try_to_skip_spaces_and_comments (parser); + status = cr_parser_parse_declaration (parser, &property, + &value, &important); + if (status != CR_OK || !property) { + if (status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + } + break; + } + cur_decl = cr_declaration_new (NULL, property, value); + if (cur_decl) { + cur_decl->important = important; + result = cr_declaration_append (result, cur_decl); + property = NULL; + value = NULL; + cur_decl = NULL; + } else { + break; + } + } + + cleanup: + + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + + if (property) { + cr_string_destroy (property); + property = NULL; + } + + if (value) { + cr_term_destroy (value); + value = NULL; + } + + if (status != CR_OK && result) { + cr_declaration_destroy (result); + result = NULL; + } + return result; +} + +/** + * cr_declaration_append: + *@a_this: the current declaration list. + *@a_new: the declaration to append. + * + *Appends a new declaration to the current declarations list. + *Returns the declaration list with a_new appended to it, or NULL + *in case of error. + */ +CRDeclaration * +cr_declaration_append (CRDeclaration * a_this, CRDeclaration * a_new) +{ + CRDeclaration *cur = NULL; + + g_return_val_if_fail (a_new, NULL); + + if (!a_this) + return a_new; + + for (cur = a_this; cur && cur->next; cur = cur->next) ; + + cur->next = a_new; + a_new->prev = cur; + + return a_this; +} + +/** + * cr_declaration_unlink: + *@a_decls: the declaration to unlink. + * + *Unlinks the declaration from the declaration list. + *case of a successful completion, NULL otherwise. + * + *Returns a pointer to the unlinked declaration in + */ +CRDeclaration * +cr_declaration_unlink (CRDeclaration * a_decl) +{ + CRDeclaration *result = a_decl; + + g_return_val_if_fail (result, NULL); + + /* + *some sanity checks first + */ + if (a_decl->prev) { + g_return_val_if_fail (a_decl->prev->next == a_decl, NULL); + + } + if (a_decl->next) { + g_return_val_if_fail (a_decl->next->prev == a_decl, NULL); + } + + /* + *now, the real unlinking job. + */ + if (a_decl->prev) { + a_decl->prev->next = a_decl->next; + } + if (a_decl->next) { + a_decl->next->prev = a_decl->prev; + } + if (a_decl->parent_statement) { + CRDeclaration **children_decl_ptr = NULL; + + switch (a_decl->parent_statement->type) { + case RULESET_STMT: + if (a_decl->parent_statement->kind.ruleset) { + children_decl_ptr = + &a_decl->parent_statement-> + kind.ruleset->decl_list; + } + + break; + + case AT_FONT_FACE_RULE_STMT: + if (a_decl->parent_statement->kind.font_face_rule) { + children_decl_ptr = + &a_decl->parent_statement-> + kind.font_face_rule->decl_list; + } + break; + case AT_PAGE_RULE_STMT: + if (a_decl->parent_statement->kind.page_rule) { + children_decl_ptr = + &a_decl->parent_statement-> + kind.page_rule->decl_list; + } + + default: + break; + } + if (children_decl_ptr + && *children_decl_ptr && *children_decl_ptr == a_decl) + *children_decl_ptr = (*children_decl_ptr)->next; + } + + a_decl->next = NULL; + a_decl->prev = NULL; + a_decl->parent_statement = NULL; + + return result; +} + +/** + * cr_declaration_prepend: + * @a_this: the current declaration list. + * @a_new: the declaration to prepend. + * + * prepends a declaration to the current declaration list. + * + * Returns the list with a_new prepended or NULL in case of error. + */ +CRDeclaration * +cr_declaration_prepend (CRDeclaration * a_this, CRDeclaration * a_new) +{ + CRDeclaration *cur = NULL; + + g_return_val_if_fail (a_new, NULL); + + if (!a_this) + return a_new; + + a_this->prev = a_new; + a_new->next = a_this; + + for (cur = a_new; cur && cur->prev; cur = cur->prev) ; + + return cur; +} + +/** + * cr_declaration_append2: + *@a_this: the current declaration list. + *@a_prop: the property string of the declaration to append. + *@a_value: the value of the declaration to append. + * + *Appends a declaration to the current declaration list. + *Returns the list with the new property appended to it, or NULL in + *case of an error. + */ +CRDeclaration * +cr_declaration_append2 (CRDeclaration * a_this, + CRString * a_prop, CRTerm * a_value) +{ + CRDeclaration *new_elem = NULL; + + if (a_this) { + new_elem = cr_declaration_new (a_this->parent_statement, + a_prop, a_value); + } else { + new_elem = cr_declaration_new (NULL, a_prop, a_value); + } + + g_return_val_if_fail (new_elem, NULL); + + return cr_declaration_append (a_this, new_elem); +} + +/** + * cr_declaration_dump: + *@a_this: the current instance of #CRDeclaration. + *@a_fp: the destination file. + *@a_indent: the number of indentation white char. + *@a_one_per_line: whether to put one declaration per line of not . + * + * + *Dumps a declaration list to a file. + */ +void +cr_declaration_dump (CRDeclaration const * a_this, FILE * a_fp, glong a_indent, + gboolean a_one_per_line) +{ + CRDeclaration const *cur = NULL; + + g_return_if_fail (a_this); + + for (cur = a_this; cur; cur = cur->next) { + if (cur->prev) { + if (a_one_per_line == TRUE) + fprintf (a_fp, ";\n"); + else + fprintf (a_fp, "; "); + } + dump (cur, a_fp, a_indent); + } +} + +/** + * cr_declaration_dump_one: + *@a_this: the current instance of #CRDeclaration. + *@a_fp: the destination file. + *@a_indent: the number of indentation white char. + * + *Dumps the first declaration of the declaration list to a file. + */ +void +cr_declaration_dump_one (CRDeclaration const * a_this, FILE * a_fp, glong a_indent) +{ + g_return_if_fail (a_this); + + dump (a_this, a_fp, a_indent); +} + +/** + * cr_declaration_to_string: + *@a_this: the current instance of #CRDeclaration. + *@a_indent: the number of indentation white char + *to put before the actual serialisation. + * + *Serializes the declaration into a string + *Returns the serialized form the declaration. The caller must + *free the string using g_free(). + */ +gchar * +cr_declaration_to_string (CRDeclaration const * a_this, gulong a_indent) +{ + GString *stringue = NULL; + + gchar *str = NULL, + *result = NULL; + + g_return_val_if_fail (a_this, NULL); + + stringue = g_string_new (NULL); + + if (a_this->property + && a_this->property->stryng + && a_this->property->stryng->str) { + str = g_strndup (a_this->property->stryng->str, + a_this->property->stryng->len); + if (str) { + cr_utils_dump_n_chars2 (' ', stringue, + a_indent); + g_string_append (stringue, str); + g_free (str); + str = NULL; + } else + goto error; + + if (a_this->value) { + guchar *value_str = NULL; + + value_str = cr_term_to_string (a_this->value); + if (value_str) { + g_string_append_printf (stringue, " : %s", + value_str); + g_free (value_str); + } else + goto error; + } + if (a_this->important == TRUE) { + g_string_append_printf (stringue, " %s", + "!important"); + } + } + if (stringue && stringue->str) { + result = g_string_free (stringue, FALSE); + } + return result; + + error: + if (stringue) { + g_string_free (stringue, TRUE); + stringue = NULL; + } + if (str) { + g_free (str); + str = NULL; + } + + return result; +} + +/** + * cr_declaration_list_to_string: + *@a_this: the current instance of #CRDeclaration. + *@a_indent: the number of indentation white char + *to put before the actual serialisation. + * + *Serializes the declaration list into a string + */ +guchar * +cr_declaration_list_to_string (CRDeclaration const * a_this, gulong a_indent) +{ + CRDeclaration const *cur = NULL; + GString *stringue = NULL; + guchar *str = NULL, + *result = NULL; + + g_return_val_if_fail (a_this, NULL); + + stringue = g_string_new (NULL); + + for (cur = a_this; cur; cur = cur->next) { + str = (guchar *) cr_declaration_to_string (cur, a_indent); + if (str) { + g_string_append_printf (stringue, "%s;", str); + g_free (str); + } else + break; + } + if (stringue && stringue->str) { + result = (guchar *) g_string_free (stringue, FALSE); + } + + return result; +} + +/** + * cr_declaration_list_to_string2: + *@a_this: the current instance of #CRDeclaration. + *@a_indent: the number of indentation white char + *@a_one_decl_per_line: whether to output one doc per line or not. + *to put before the actual serialisation. + * + *Serializes the declaration list into a string + *Returns the serialized form the declararation. + */ +guchar * +cr_declaration_list_to_string2 (CRDeclaration const * a_this, + gulong a_indent, gboolean a_one_decl_per_line) +{ + CRDeclaration const *cur = NULL; + GString *stringue = NULL; + guchar *str = NULL, + *result = NULL; + + g_return_val_if_fail (a_this, NULL); + + stringue = g_string_new (NULL); + + for (cur = a_this; cur; cur = cur->next) { + str = (guchar *) cr_declaration_to_string (cur, a_indent); + if (str) { + if (a_one_decl_per_line == TRUE) { + if (cur->next) + g_string_append_printf (stringue, + "%s;\n", str); + else + g_string_append (stringue, + (const gchar *) str); + } else { + if (cur->next) + g_string_append_printf (stringue, + "%s;", str); + else + g_string_append (stringue, + (const gchar *) str); + } + g_free (str); + } else + break; + } + if (stringue && stringue->str) { + result = (guchar *) g_string_free (stringue, FALSE); + } + + return result; +} + +/** + * cr_declaration_nr_props: + *@a_this: the current instance of #CRDeclaration. + *Return the number of properties in the declaration + */ +gint +cr_declaration_nr_props (CRDeclaration const * a_this) +{ + CRDeclaration const *cur = NULL; + int nr = 0; + + g_return_val_if_fail (a_this, -1); + + for (cur = a_this; cur; cur = cur->next) + nr++; + return nr; +} + +/** + * cr_declaration_get_from_list: + *@a_this: the current instance of #CRDeclaration. + *@itemnr: the index into the declaration list. + * + *Use an index to get a CRDeclaration from the declaration list. + * + *Returns #CRDeclaration at position itemnr, + *if itemnr > number of declarations - 1, + *it will return NULL. + */ +CRDeclaration * +cr_declaration_get_from_list (CRDeclaration * a_this, int itemnr) +{ + CRDeclaration *cur = NULL; + int nr = 0; + + g_return_val_if_fail (a_this, NULL); + + for (cur = a_this; cur; cur = cur->next) + if (nr++ == itemnr) + return cur; + return NULL; +} + +/** + * cr_declaration_get_by_prop_name: + *@a_this: the current instance of #CRDeclaration. + *@a_prop: the property name to search for. + * + *Use property name to get a CRDeclaration from the declaration list. + *Returns #CRDeclaration with property name a_prop, or NULL if not found. + */ +CRDeclaration * +cr_declaration_get_by_prop_name (CRDeclaration * a_this, + const guchar * a_prop) +{ + CRDeclaration *cur = NULL; + + g_return_val_if_fail (a_this, NULL); + g_return_val_if_fail (a_prop, NULL); + + for (cur = a_this; cur; cur = cur->next) { + if (cur->property + && cur->property->stryng + && cur->property->stryng->str) { + if (!strcmp (cur->property->stryng->str, + (const char *) a_prop)) { + return cur; + } + } + } + return NULL; +} + +/** + * cr_declaration_ref: + *@a_this: the current instance of #CRDeclaration. + * + *Increases the ref count of the current instance of #CRDeclaration. + */ +void +cr_declaration_ref (CRDeclaration * a_this) +{ + g_return_if_fail (a_this); + + a_this->ref_count++; +} + +/** + * cr_declaration_unref: + *@a_this: the current instance of #CRDeclaration. + * + *Decrements the ref count of the current instance of #CRDeclaration. + *If the ref count reaches zero, the current instance of #CRDeclaration + *if destroyed. + *Returns TRUE if @a_this was destroyed (ref count reached zero), + *FALSE otherwise. + */ +gboolean +cr_declaration_unref (CRDeclaration * a_this) +{ + g_return_val_if_fail (a_this, FALSE); + + if (a_this->ref_count) { + a_this->ref_count--; + } + + if (a_this->ref_count == 0) { + cr_declaration_destroy (a_this); + return TRUE; + } + return FALSE; +} + +/** + * cr_declaration_destroy: + *@a_this: the current instance of #CRDeclaration. + * + *Destructor of the declaration list. + */ +void +cr_declaration_destroy (CRDeclaration * a_this) +{ + CRDeclaration *cur = NULL; + + g_return_if_fail (a_this); + + /* + * Go to the last element of the list. + */ + for (cur = a_this; cur->next; cur = cur->next) + g_assert (cur->next->prev == cur); + + /* + * Walk backward the list and free each "next" element. + * Meanwhile, free each property/value pair contained in the list. + */ + for (; cur; cur = cur->prev) { + g_free (cur->next); + cur->next = NULL; + + if (cur->property) { + cr_string_destroy (cur->property); + cur->property = NULL; + } + + if (cur->value) { + cr_term_destroy (cur->value); + cur->value = NULL; + } + } + + g_free (a_this); +} diff --git a/src/st/croco/cr-declaration.h b/src/st/croco/cr-declaration.h new file mode 100644 index 0000000..eee8be3 --- /dev/null +++ b/src/st/croco/cr-declaration.h @@ -0,0 +1,136 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See the COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_DECLARATION_H__ +#define __CR_DECLARATION_H__ + +#include <stdio.h> +#include "cr-utils.h" +#include "cr-term.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +/** + *@file + *The declaration of the #CRDeclaration class. + */ + +/*forward declaration of what is defined in cr-statement.h*/ +typedef struct _CRStatement CRStatement ; + +/** + *The abstraction of a css declaration defined by the + *css2 spec in chapter 4. + *It is actually a chained list of property/value pairs. + */ +typedef struct _CRDeclaration CRDeclaration ; +struct _CRDeclaration +{ + /**The property.*/ + CRString *property ; + + /**The value of the property.*/ + CRTerm *value ; + + /*the ruleset that contains this declaration*/ + CRStatement *parent_statement ; + + /*the next declaration*/ + CRDeclaration *next ; + + /*the previous one declaration*/ + CRDeclaration *prev ; + + /*does the declaration have the important keyword ?*/ + gboolean important ; + + glong ref_count ; + + CRParsingLocation location ; + /*reserved for future usage*/ + gpointer rfu0 ; + gpointer rfu1 ; + gpointer rfu2 ; + gpointer rfu3 ; +} ; + + +CRDeclaration * cr_declaration_new (CRStatement *a_statement, + CRString *a_property, + CRTerm *a_value) ; + + +CRDeclaration * cr_declaration_parse_from_buf (CRStatement *a_statement, + const guchar *a_str, + enum CREncoding a_enc) ; + +CRDeclaration * cr_declaration_parse_list_from_buf (const guchar *a_str, + enum CREncoding a_enc) ; + +CRDeclaration * cr_declaration_append (CRDeclaration *a_this, + CRDeclaration *a_new) ; + +CRDeclaration * cr_declaration_append2 (CRDeclaration *a_this, + CRString *a_prop, + CRTerm *a_value) ; + +CRDeclaration * cr_declaration_prepend (CRDeclaration *a_this, + CRDeclaration *a_new) ; + +CRDeclaration * cr_declaration_unlink (CRDeclaration * a_decl) ; + +void +cr_declaration_dump (CRDeclaration const *a_this, + FILE *a_fp, glong a_indent, + gboolean a_one_per_line) ; + +void cr_declaration_dump_one (CRDeclaration const *a_this, + FILE *a_fp, glong a_indent) ; + +gint cr_declaration_nr_props (CRDeclaration const *a_this) ; + +CRDeclaration * cr_declaration_get_from_list (CRDeclaration *a_this, + int itemnr) ; + +CRDeclaration * cr_declaration_get_by_prop_name (CRDeclaration *a_this, + const guchar *a_str) ; + +gchar * cr_declaration_to_string (CRDeclaration const *a_this, + gulong a_indent) ; + +guchar * cr_declaration_list_to_string (CRDeclaration const *a_this, + gulong a_indent) ; + +guchar * cr_declaration_list_to_string2 (CRDeclaration const *a_this, + gulong a_indent, + gboolean a_one_decl_per_line) ; + +void cr_declaration_ref (CRDeclaration *a_this) ; + +gboolean cr_declaration_unref (CRDeclaration *a_this) ; + +void cr_declaration_destroy (CRDeclaration *a_this) ; + +G_END_DECLS + +#endif /*__CR_DECLARATION_H__*/ diff --git a/src/st/croco/cr-doc-handler.c b/src/st/croco/cr-doc-handler.c new file mode 100644 index 0000000..b0ef13c --- /dev/null +++ b/src/st/croco/cr-doc-handler.c @@ -0,0 +1,276 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See COPRYRIGHTS file for copyright information. + */ + +#include <string.h> +#include "cr-doc-handler.h" +#include "cr-parser.h" + +/** + *@CRDocHandler: + * + *The definition of the CRDocHandler class. + *Contains methods to instantiate, destroy, + *and initialize instances of #CRDocHandler + *to custom values. + */ + +#define PRIVATE(obj) (obj)->priv + +struct _CRDocHandlerPriv { + /** + *This pointer is to hold an application parsing context. + *For example, it used by the Object Model parser to + *store it parsing context. #CRParser does not touch it, but + *#CROMParser does. #CROMParser allocates this pointer at + *the beginning of the css document, and frees it at the end + *of the document. + */ + gpointer context; + + /** + *The place where #CROMParser puts the result of its parsing, if + *any. + */ + gpointer result; + /** + *a pointer to the parser used to parse + *the current document. + */ + CRParser *parser ; +}; + +/** + * cr_doc_handler_new: + *Constructor of #CRDocHandler. + * + *Returns the newly built instance of + *#CRDocHandler + * + */ +CRDocHandler * +cr_doc_handler_new (void) +{ + CRDocHandler *result = NULL; + + result = g_try_malloc (sizeof (CRDocHandler)); + + g_return_val_if_fail (result, NULL); + + memset (result, 0, sizeof (CRDocHandler)); + result->ref_count++; + + result->priv = g_try_malloc (sizeof (CRDocHandlerPriv)); + if (!result->priv) { + cr_utils_trace_info ("Out of memory exception"); + g_free (result); + return NULL; + } + + cr_doc_handler_set_default_sac_handler (result); + + return result; +} + +/** + * cr_doc_handler_get_ctxt: + *@a_this: the current instance of #CRDocHandler. + *@a_ctxt: out parameter. The new parsing context. + * + *Gets the private parsing context associated to the document handler + *The private parsing context is used by libcroco only. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_doc_handler_get_ctxt (CRDocHandler const * a_this, gpointer * a_ctxt) +{ + g_return_val_if_fail (a_this && a_this->priv, CR_BAD_PARAM_ERROR); + + *a_ctxt = a_this->priv->context; + + return CR_OK; +} + +/** + * cr_doc_handler_set_ctxt: + *@a_this: the current instance of #CRDocHandler + *@a_ctxt: a pointer to the parsing context. + * + *Sets the private parsing context. + *This is used by libcroco only. + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_doc_handler_set_ctxt (CRDocHandler * a_this, gpointer a_ctxt) +{ + g_return_val_if_fail (a_this && a_this->priv, CR_BAD_PARAM_ERROR); + a_this->priv->context = a_ctxt; + return CR_OK; +} + +/** + * cr_doc_handler_get_result: + *@a_this: the current instance of #CRDocHandler + *@a_result: out parameter. The returned result. + * + *Gets the private parsing result. + *The private parsing result is used by libcroco only. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_doc_handler_get_result (CRDocHandler const * a_this, gpointer * a_result) +{ + g_return_val_if_fail (a_this && a_this->priv, CR_BAD_PARAM_ERROR); + + *a_result = a_this->priv->result; + + return CR_OK; +} + +/** + * cr_doc_handler_set_result: + *@a_this: the current instance of #CRDocHandler + *@a_result: the new result. + * + *Sets the private parsing context. + *This is used by libcroco only. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_doc_handler_set_result (CRDocHandler * a_this, gpointer a_result) +{ + g_return_val_if_fail (a_this && a_this->priv, CR_BAD_PARAM_ERROR); + a_this->priv->result = a_result; + return CR_OK; +} + +/** + *cr_doc_handler_set_default_sac_handler: + *@a_this: a pointer to the current instance of #CRDocHandler. + * + *Sets the sac handlers contained in the current + *instance of DocHandler to the default handlers. + *For the time being the default handlers are + *test handlers. This is expected to change in a + *near future, when the libcroco gets a bit debugged. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_doc_handler_set_default_sac_handler (CRDocHandler * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + a_this->start_document = NULL; + a_this->end_document = NULL; + a_this->import_style = NULL; + a_this->namespace_declaration = NULL; + a_this->comment = NULL; + a_this->start_selector = NULL; + a_this->end_selector = NULL; + a_this->property = NULL; + a_this->start_font_face = NULL; + a_this->end_font_face = NULL; + a_this->start_media = NULL; + a_this->end_media = NULL; + a_this->start_page = NULL; + a_this->end_page = NULL; + a_this->ignorable_at_rule = NULL; + a_this->error = NULL; + a_this->unrecoverable_error = NULL; + return CR_OK; +} + +/** + * cr_doc_handler_ref: + *@a_this: the current instance of #CRDocHandler. + */ +void +cr_doc_handler_ref (CRDocHandler * a_this) +{ + g_return_if_fail (a_this); + + a_this->ref_count++; +} + +/** + * cr_doc_handler_unref: + *@a_this: the current instance of #CRDocHandler. + * + *Decreases the ref count of the current instance of #CRDocHandler. + *If the ref count reaches '0' then, destroys the instance. + * + *Returns TRUE if the instance as been destroyed, FALSE otherwise. + */ +gboolean +cr_doc_handler_unref (CRDocHandler * a_this) +{ + g_return_val_if_fail (a_this, FALSE); + + if (a_this->ref_count > 0) { + a_this->ref_count--; + } + + if (a_this->ref_count == 0) { + cr_doc_handler_destroy (a_this); + return TRUE; + } + return FALSE ; +} + +/** + * cr_doc_handler_destroy: + *@a_this: the instance of #CRDocHandler to + *destroy. + * + *The destructor of the #CRDocHandler class. + */ +void +cr_doc_handler_destroy (CRDocHandler * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->priv) { + g_free (a_this->priv); + a_this->priv = NULL; + } + g_free (a_this); +} + +/** + * cr_doc_handler_associate_a_parser: + *Associates a parser to the current document handler + * + *@a_this: the current instance of document handler. + *@a_parser: the parser to associate. + */ +void +cr_doc_handler_associate_a_parser (CRDocHandler *a_this, + gpointer a_parser) +{ + g_return_if_fail (a_this && PRIVATE (a_this) + && a_parser) ; + + PRIVATE (a_this)->parser = a_parser ; +} diff --git a/src/st/croco/cr-doc-handler.h b/src/st/croco/cr-doc-handler.h new file mode 100644 index 0000000..d12673f --- /dev/null +++ b/src/st/croco/cr-doc-handler.h @@ -0,0 +1,298 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See the COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_DOC_HANDLER_H__ +#define __CR_DOC_HANDLER_H__ + +/** + *@file + *The declaration of the #CRDocumentHandler class. + *This class is actually the parsing events handler. + */ + +#include <glib.h> +#include "cr-utils.h" +#include "cr-input.h" +#include "cr-stylesheet.h" + +G_BEGIN_DECLS + + +typedef struct _CRDocHandler CRDocHandler ; + +struct _CRDocHandlerPriv ; +typedef struct _CRDocHandlerPriv CRDocHandlerPriv ; + + +/** + *The SAC document handler. + *An instance of this class is to + *be passed to a parser. Then, during the parsing + *the parser calls the convenient function pointer + *whenever a particular event (a css construction) occurs. + */ +struct _CRDocHandler +{ + CRDocHandlerPriv *priv ; + + /** + *This pointer is to be used by the application for + *it custom needs. It is there to extend the doc handler. + */ + gpointer app_data ; + + /** + *Is called at the beginning of the parsing of the document. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + */ + void (*start_document) (CRDocHandler *a_this) ; + + /** + *Is called to notify the end of the parsing of the document. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + */ + void (*end_document) (CRDocHandler *a_this) ; + + /** + *Is called to notify an at charset rule. + *@param a_this the document handler. + *@param a_charset the declared charset. + */ + void (*charset) (CRDocHandler *a_this, + CRString *a_charset, + CRParsingLocation *a_charset_sym_location) ; + + /** + *Is called to notify an import statement in + *the stylesheet. + *@param a_this the current instance of #CRDocHandler. + *@param a_media_list a doubly linked list of GString objects. + *Each GString object contains a string which is the + *destination media for style information. + *@param a_uri the uri of the imported style sheet. + *@param a_uri_default_ns the default namespace of URI + *@param a_location the parsing location of the '\@import' + *keyword. + *of the imported style sheet. + */ + void (*import_style) (CRDocHandler *a_this, + GList *a_media_list, + CRString *a_uri, + CRString *a_uri_default_ns, + CRParsingLocation *a_location) ; + + void (*import_style_result) (CRDocHandler *a_this, + GList *a_media_list, + CRString *a_uri, + CRString *a_uri_default_ns, + CRStyleSheet *a_sheet) ; + + /** + *Is called to notify a namespace declaration. + *Not used yet. + *@param a_this the current instance of #CRDocHandler. + *@param a_prefix the prefix of the namespace. + *@param a_uri the uri of the namespace. + *@param a_location the location of the "@namespace" keyword. + */ + void (*namespace_declaration) (CRDocHandler *a_this, + CRString *a_prefix, + CRString *a_uri, + CRParsingLocation *a_location) ; + + /** + *Is called to notify a comment. + *@param a_this a pointer to the current instance + *of #CRDocHandler. + *@param a_comment the comment. + */ + void (*comment) (CRDocHandler *a_this, + CRString *a_comment) ; + + /** + *Is called to notify the beginning of a rule + *statement. + *@param a_this the current instance of #CRDocHandler. + *@param a_selector_list the list of selectors that precedes + *the rule declarations. + */ + void (*start_selector) (CRDocHandler * a_this, + CRSelector *a_selector_list) ; + + /** + *Is called to notify the end of a rule statement. + *@param a_this the current instance of #CRDocHandler. + *@param a_selector_list the list of selectors that precedes + *the rule declarations. This pointer is the same as + *the one passed to start_selector() ; + */ + void (*end_selector) (CRDocHandler *a_this, + CRSelector *a_selector_list) ; + + + /** + *Is called to notify a declaration. + *@param a_this a pointer to the current instance + *of #CRDocHandler. + *@param a_name the name of the parsed property. + *@param a_expression a css expression that represents + *the value of the property. A css expression is + *actually a linked list of 'terms'. Each term can + *be linked to other using operators. + * + */ + void (*property) (CRDocHandler *a_this, + CRString *a_name, + CRTerm *a_expression, + gboolean a_is_important) ; + /** + *Is called to notify the start of a font face statement. + *The parser invokes this method at the beginning of every + *font face statement in the style sheet. There will + *be a corresponding end_font_face () event for every + *start_font_face () event. + * + *@param a_this a pointer to the current instance of + *#CRDocHandler. + *@param a_location the parsing location of the "\@font-face" + *keyword. + */ + void (*start_font_face) (CRDocHandler *a_this, + CRParsingLocation *a_location) ; + + /** + *Is called to notify the end of a font face statement. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + */ + void (*end_font_face) (CRDocHandler *a_this) ; + + + /** + *Is called to notify the beginning of a media statement. + *The parser will invoke this method at the beginning of + *every media statement in the style sheet. There will be + *a corresponding end_media() event for every start_media() + *event. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + *@param a_media_list a double linked list of + #CRString * objects. + *Each CRString objects is actually a destination media for + *the style information. + */ + void (*start_media) (CRDocHandler *a_this, + GList *a_media_list, + CRParsingLocation *a_location) ; + + /** + *Is called to notify the end of a media statement. + *@param a_this a pointer to the current instance + *of #CRDocHandler. + *@param a_media_list a double linked list of GString * objects. + *Each GString objects is actually a destination media for + *the style information. + */ + void (*end_media) (CRDocHandler *a_this, + GList *a_media_list) ; + + /** + *Is called to notify the beginning of a page statement. + *The parser invokes this function at the beginning of + *every page statement in the style sheet. There will be + *a corresponding end_page() event for every single + *start_page() event. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + *@param a_name the name of the page (if any, null otherwise). + *@param a_pseudo_page the pseudo page (if any, null otherwise). + *@param a_location the parsing location of the "\@page" keyword. + */ + void (*start_page) (CRDocHandler *a_this, + CRString *a_name, + CRString *a_pseudo_page, + CRParsingLocation *a_location) ; + + /** + *Is called to notify the end of a page statement. + *@param a_this a pointer to the current instance of + *#CRDocHandler. + *@param a_name the name of the page (if any, null otherwise). + *@param a_pseudo_page the pseudo page (if any, null otherwise). + */ + void (*end_page) (CRDocHandler *a_this, + CRString *a_name, + CRString *pseudo_page) ; + + /** + *Is Called to notify an unknown at-rule not supported + *by this parser. + */ + void (*ignorable_at_rule) (CRDocHandler *a_this, + CRString *a_name) ; + + /** + *Is called to notify a parsing error. After this error + *the application must ignore the rule being parsed, if + *any. After completion of this callback, + *the parser will then try to resume the parsing, + *ignoring the current error. + */ + void (*error) (CRDocHandler *a_this) ; + + /** + *Is called to notify an unrecoverable parsing error. + *This is the place to put emergency routines that free allocated + *resources. + */ + void (*unrecoverable_error) (CRDocHandler *a_this) ; + + gboolean resolve_import ; + gulong ref_count ; +} ; + +CRDocHandler * cr_doc_handler_new (void) ; + +enum CRStatus cr_doc_handler_set_result (CRDocHandler *a_this, gpointer a_result) ; + +enum CRStatus cr_doc_handler_get_result (CRDocHandler const *a_this, gpointer * a_result) ; + +enum CRStatus cr_doc_handler_set_ctxt (CRDocHandler *a_this, gpointer a_ctxt) ; + +enum CRStatus cr_doc_handler_get_ctxt (CRDocHandler const *a_this, gpointer * a_ctxt) ; + +enum CRStatus cr_doc_handler_set_default_sac_handler (CRDocHandler *a_this) ; + +void cr_doc_handler_associate_a_parser (CRDocHandler *a_this, + gpointer a_parser) ; + +void cr_doc_handler_ref (CRDocHandler *a_this) ; + +gboolean cr_doc_handler_unref (CRDocHandler *a_this) ; + +void cr_doc_handler_destroy (CRDocHandler *a_this) ; + +G_END_DECLS + +#endif /*__CR_DOC_HANDLER_H__*/ diff --git a/src/st/croco/cr-enc-handler.c b/src/st/croco/cr-enc-handler.c new file mode 100644 index 0000000..65adc7a --- /dev/null +++ b/src/st/croco/cr-enc-handler.c @@ -0,0 +1,184 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2003 Dodji Seketeli <dodji@seketeli.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +/* + *$Id$ + */ + +/** + *@file + *The definition of the #CREncHandler class. + */ + +#include "cr-enc-handler.h" +#include "cr-utils.h" + +#include <string.h> + +struct CREncAlias { + const gchar *name; + enum CREncoding encoding; +}; + +static struct CREncAlias gv_default_aliases[] = { + {"UTF-8", CR_UTF_8}, + {"UTF_8", CR_UTF_8}, + {"UTF8", CR_UTF_8}, + {"UTF-16", CR_UTF_16}, + {"UTF_16", CR_UTF_16}, + {"UTF16", CR_UTF_16}, + {"UCS1", CR_UCS_1}, + {"UCS-1", CR_UCS_1}, + {"UCS_1", CR_UCS_1}, + {"ISO-8859-1", CR_UCS_1}, + {"ISO_8859-1", CR_UCS_1}, + {"UCS-1", CR_UCS_1}, + {"UCS_1", CR_UCS_1}, + {"UCS4", CR_UCS_4}, + {"UCS-4", CR_UCS_4}, + {"UCS_4", CR_UCS_4}, + {"ASCII", CR_ASCII}, + {0, 0} +}; + +static CREncHandler gv_default_enc_handlers[] = { + {CR_UCS_1, cr_utils_ucs1_to_utf8, cr_utils_utf8_to_ucs1, + cr_utils_ucs1_str_len_as_utf8, cr_utils_utf8_str_len_as_ucs1}, + + {CR_ISO_8859_1, cr_utils_ucs1_to_utf8, cr_utils_utf8_to_ucs1, + cr_utils_ucs1_str_len_as_utf8, cr_utils_utf8_str_len_as_ucs1}, + + {CR_ASCII, cr_utils_ucs1_to_utf8, cr_utils_utf8_to_ucs1, + cr_utils_ucs1_str_len_as_utf8, cr_utils_utf8_str_len_as_ucs1}, + + {0, NULL, NULL, NULL, NULL} +}; + +/** + * cr_enc_handler_get_instance: + *@a_enc: the encoding of the Handler. + * + *Gets the instance of encoding handler. + *This function implements a singleton pattern. + * + *Returns the instance of #CREncHandler. + */ +CREncHandler * +cr_enc_handler_get_instance (enum CREncoding a_enc) +{ + gulong i = 0; + + for (i = 0; gv_default_enc_handlers[i].encoding; i++) { + if (gv_default_enc_handlers[i].encoding == a_enc) { + return (CREncHandler *) & gv_default_enc_handlers[i]; + } + } + + return NULL; +} + +/** + * cr_enc_handler_resolve_enc_alias: + *@a_alias_name: the encoding name. + *@a_enc: output param. The returned encoding type + *or 0 if the alias is not supported. + * + *Given an encoding name (called an alias name) + *the function returns the matching encoding type. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_enc_handler_resolve_enc_alias (const guchar * a_alias_name, + enum CREncoding *a_enc) +{ + gulong i = 0; + guchar *alias_name_up = NULL; + enum CRStatus status = CR_ENCODING_NOT_FOUND_ERROR; + + g_return_val_if_fail (a_alias_name != NULL, CR_BAD_PARAM_ERROR); + + alias_name_up = (guchar *) g_ascii_strup ((const gchar *) a_alias_name, -1); + + for (i = 0; gv_default_aliases[i].name; i++) { + if (!strcmp (gv_default_aliases[i].name, (const gchar *) alias_name_up)) { + *a_enc = gv_default_aliases[i].encoding; + status = CR_OK; + break; + } + } + + return status; +} + +/** + * cr_enc_handler_convert_input: + *@a_this: the current instance of #CREncHandler. + *@a_in: the input buffer to convert. + *@a_in_len: in/out parameter. The len of the input + *buffer to convert. After return, contains the number of + *bytes actually consumed. + *@a_out: output parameter. The converted output buffer. + *Must be freed by the buffer. + *@a_out_len: output parameter. The length of the output buffer. + * + *Converts a raw input buffer into an utf8 buffer. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_enc_handler_convert_input (CREncHandler * a_this, + const guchar * a_in, + gulong * a_in_len, + guchar ** a_out, gulong * a_out_len) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && a_in && a_in_len && a_out, + CR_BAD_PARAM_ERROR); + + if (a_this->decode_input == NULL) + return CR_OK; + + if (a_this->enc_str_len_as_utf8) { + status = a_this->enc_str_len_as_utf8 (a_in, + &a_in[*a_in_len - 1], + a_out_len); + + g_return_val_if_fail (status == CR_OK, status); + } else { + *a_out_len = *a_in_len; + } + + *a_out = g_malloc0 (*a_out_len); + + status = a_this->decode_input (a_in, a_in_len, *a_out, a_out_len); + + if (status != CR_OK) { + g_free (*a_out); + *a_out = NULL; + } + + g_return_val_if_fail (status == CR_OK, status); + + return CR_OK; +} diff --git a/src/st/croco/cr-enc-handler.h b/src/st/croco/cr-enc-handler.h new file mode 100644 index 0000000..0727764 --- /dev/null +++ b/src/st/croco/cr-enc-handler.h @@ -0,0 +1,94 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2003 Dodji Seketeli <dodji@seketeli.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +/* + *$Id$ + */ + +/** + *@file: + *The declaration of the #CREncHandler class. + * + */ + +#ifndef __CR_ENC_HANDLER_H__ +#define __CR_ENC_HANDLER_H__ + +#include "cr-utils.h" + +G_BEGIN_DECLS + + +typedef struct _CREncHandler CREncHandler ; + +typedef enum CRStatus (*CREncInputFunc) (const guchar * a_in, + gulong *a_in_len, + guchar *a_out, + gulong *a_out_len) ; + +typedef enum CRStatus (*CREncOutputFunc) (const guchar * a_in, + gulong *a_in_len, + guchar *a_out, + gulong *a_out_len) ; + +typedef enum CRStatus (*CREncInputStrLenAsUtf8Func) +(const guchar *a_in_start, + const guchar *a_in_end, + gulong *a_in_size); + +typedef enum CRStatus (*CREncUtf8StrLenAsOutputFunc) +(const guchar *a_in_start, + const guchar *a_in_end, + gulong *a_in_size) ; + +/** + *This class is responsible of the + *the encoding conversions stuffs in + *libcroco. + */ + +struct _CREncHandler +{ + enum CREncoding encoding ; + CREncInputFunc decode_input ; + CREncInputFunc encode_output ; + CREncInputStrLenAsUtf8Func enc_str_len_as_utf8 ; + CREncUtf8StrLenAsOutputFunc utf8_str_len_as_enc ; +} ; + +CREncHandler * +cr_enc_handler_get_instance (enum CREncoding a_enc) ; + +enum CRStatus +cr_enc_handler_resolve_enc_alias (const guchar *a_alias_name, + enum CREncoding *a_enc) ; + +enum CRStatus +cr_enc_handler_convert_input (CREncHandler *a_this, + const guchar *a_in, + gulong *a_in_len, + guchar **a_out, + gulong *a_out_len) ; + +G_END_DECLS + +#endif /*__CR_ENC_HANDLER_H__*/ diff --git a/src/st/croco/cr-fonts.c b/src/st/croco/cr-fonts.c new file mode 100644 index 0000000..a64ffc0 --- /dev/null +++ b/src/st/croco/cr-fonts.c @@ -0,0 +1,948 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of + * the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + *See COPYRIGHTS file for copyright information + */ + +#include "cr-fonts.h" +#include <string.h> + +static enum CRStatus +cr_font_family_to_string_real (CRFontFamily const * a_this, + gboolean a_walk_list, GString ** a_string) +{ + guchar const *name = NULL; + enum CRStatus result = CR_OK; + + if (!*a_string) { + *a_string = g_string_new (NULL); + g_return_val_if_fail (*a_string, + CR_INSTANCIATION_FAILED_ERROR); + } + + if (!a_this) { + g_string_append (*a_string, "NULL"); + return CR_OK; + } + + switch (a_this->type) { + case FONT_FAMILY_SANS_SERIF: + name = (guchar const *) "sans-serif"; + break; + + case FONT_FAMILY_SERIF: + name = (guchar const *) "sans-serif"; + break; + + case FONT_FAMILY_CURSIVE: + name = (guchar const *) "cursive"; + break; + + case FONT_FAMILY_FANTASY: + name = (guchar const *) "fantasy"; + break; + + case FONT_FAMILY_MONOSPACE: + name = (guchar const *) "monospace"; + break; + + case FONT_FAMILY_NON_GENERIC: + name = (guchar const *) a_this->name; + break; + + default: + name = NULL; + break; + } + + if (name) { + if (a_this->prev) { + g_string_append_printf (*a_string, ", %s", name); + } else { + g_string_append (*a_string, (const gchar *) name); + } + } + if (a_walk_list == TRUE && a_this->next) { + result = cr_font_family_to_string_real (a_this->next, + TRUE, a_string); + } + return result; +} + +static const gchar * +cr_predefined_absolute_font_size_to_string (enum CRPredefinedAbsoluteFontSize + a_code) +{ + gchar const *str = NULL; + + switch (a_code) { + case FONT_SIZE_XX_SMALL: + str = "xx-small"; + break; + case FONT_SIZE_X_SMALL: + str = "x-small"; + break; + case FONT_SIZE_SMALL: + str = "small"; + break; + case FONT_SIZE_MEDIUM: + str = "medium"; + break; + case FONT_SIZE_LARGE: + str = "large"; + break; + case FONT_SIZE_X_LARGE: + str = "x-large"; + break; + case FONT_SIZE_XX_LARGE: + str = "xx-large"; + break; + default: + str = "unknown absolute font size value"; + } + return str; +} + +static const gchar * +cr_relative_font_size_to_string (enum CRRelativeFontSize a_code) +{ + gchar const *str = NULL; + + switch (a_code) { + case FONT_SIZE_LARGER: + str = "larger"; + break; + case FONT_SIZE_SMALLER: + str = "smaller"; + break; + default: + str = "unknown relative font size value"; + break; + } + return str; +} + +/** + * cr_font_family_new: + * @a_type: the type of font family to create. + * @a_name: the name of the font family. + * + * create a font family. + * + * Returns the newly built font family. + */ +CRFontFamily * +cr_font_family_new (enum CRFontFamilyType a_type, guchar * a_name) +{ + CRFontFamily *result = NULL; + + result = g_try_malloc (sizeof (CRFontFamily)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRFontFamily)); + result->type = a_type; + + cr_font_family_set_name (result, a_name); + + return result; +} + +/** + * cr_font_family_to_string: + * @a_this: the current instance of #CRFontFamily. + * @a_walk_font_family_list: whether the serialize the entire list. + * + * Returns the seriliazed font family. The caller has to free it using + * g_free(). + */ +guchar * +cr_font_family_to_string (CRFontFamily const * a_this, + gboolean a_walk_font_family_list) +{ + enum CRStatus status = CR_OK; + guchar *result = NULL; + GString *stringue = NULL; + + if (!a_this) { + result = (guchar *) g_strdup ("NULL"); + g_return_val_if_fail (result, NULL); + return result; + } + status = cr_font_family_to_string_real (a_this, + a_walk_font_family_list, + &stringue); + + if (status == CR_OK && stringue) { + result = (guchar *) g_string_free (stringue, FALSE); + stringue = NULL; + + } else { + if (stringue) { + g_string_free (stringue, TRUE); + stringue = NULL; + } + } + + return result; +} + +/** + * cr_font_family_set_name: + * @a_this: the current instance of #CRFontFamily. + * @a_name: the new name + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_family_set_name (CRFontFamily * a_this, guchar * a_name) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + /* + *only non generic font families can have a name + */ + + if (a_this->type != FONT_FAMILY_NON_GENERIC) { + return CR_BAD_PARAM_ERROR; + } + + if (a_this->name) { + g_free (a_this->name); + a_this->name = NULL; + } + + a_this->name = a_name; + return CR_OK; +} + +/** + * cr_font_family_append: + * @a_this: the current instance of #CRFontFamily. + * @a_family_to_append: the font family to append to the list + * + * Returns the new font family list. + */ +CRFontFamily * +cr_font_family_append (CRFontFamily * a_this, + CRFontFamily * a_family_to_append) +{ + CRFontFamily *cur_ff = NULL; + + g_return_val_if_fail (a_family_to_append, NULL); + + if (!a_this) + return a_family_to_append; + + for (cur_ff = a_this; cur_ff && cur_ff->next; cur_ff = cur_ff->next) ; + + cur_ff->next = a_family_to_append; + a_family_to_append->prev = cur_ff; + + return a_this; + +} + +/** + * cr_font_family_prepend: + * @a_this: the current instance #CRFontFamily. + * @a_family_to_prepend: the font family to prepend to the list. + * + * Returns the font family list. + */ +CRFontFamily * +cr_font_family_prepend (CRFontFamily * a_this, + CRFontFamily * a_family_to_prepend) +{ + g_return_val_if_fail (a_this && a_family_to_prepend, NULL); + + if (!a_this) + return a_family_to_prepend; + + a_family_to_prepend->next = a_this; + a_this->prev = a_family_to_prepend; + + return a_family_to_prepend; +} + +/** + * cr_font_family_destroy: + * @a_this: the current instance of #CRFontFamily. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_family_destroy (CRFontFamily * a_this) +{ + CRFontFamily *cur_ff = NULL; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + for (cur_ff = a_this; cur_ff && cur_ff->next; cur_ff = cur_ff->next) ; + + for (; cur_ff; cur_ff = cur_ff->prev) { + if (a_this->name) { + g_free (a_this->name); + a_this->name = NULL; + } + + if (cur_ff->next) { + g_free (cur_ff->next); + + } + + if (cur_ff->prev == NULL) { + g_free (a_this); + } + } + + return CR_OK; +} + +/*************************************************** + *'font-size' manipulation functions definitions + ***************************************************/ + +/** + * cr_font_size_new: + * + * Returns the newly created font size. + */ +CRFontSize * +cr_font_size_new (void) +{ + CRFontSize *result = NULL; + + result = g_try_malloc (sizeof (CRFontSize)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRFontSize)); + + return result; +} + +/** + * cr_font_size_clear: + * @a_this: the current instance of #CRFontSize + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_clear (CRFontSize * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + switch (a_this->type) { + case PREDEFINED_ABSOLUTE_FONT_SIZE: + case RELATIVE_FONT_SIZE: + case INHERITED_FONT_SIZE: + memset (a_this, 0, sizeof (CRFontSize)); + break; + + case ABSOLUTE_FONT_SIZE: + memset (a_this, 0, sizeof (CRFontSize)); + break; + + default: + return CR_UNKNOWN_TYPE_ERROR; + } + + return CR_OK; +} + +/** + * cr_font_size_copy: + * @a_dst: the destination #CRFontSize (where to copy to). + * @a_src: the source #CRFontSize (where to copy from). + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_copy (CRFontSize * a_dst, CRFontSize const * a_src) +{ + g_return_val_if_fail (a_dst && a_src, CR_BAD_PARAM_ERROR); + + switch (a_src->type) { + case PREDEFINED_ABSOLUTE_FONT_SIZE: + case RELATIVE_FONT_SIZE: + case INHERITED_FONT_SIZE: + cr_font_size_clear (a_dst); + memcpy (a_dst, a_src, sizeof (CRFontSize)); + break; + + case ABSOLUTE_FONT_SIZE: + cr_font_size_clear (a_dst); + cr_num_copy (&a_dst->value.absolute, + &a_src->value.absolute); + a_dst->type = a_src->type; + break; + + default: + return CR_UNKNOWN_TYPE_ERROR; + } + return CR_OK; +} + +/** + * cr_font_size_set_predefined_absolute_font_size: + * @a_this: the current instance of #CRFontSize. + * @a_predefined: what to set. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_set_predefined_absolute_font_size (CRFontSize *a_this, + enum CRPredefinedAbsoluteFontSize a_predefined) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR) ; + g_return_val_if_fail (a_predefined >= FONT_SIZE_XX_SMALL + && a_predefined < NB_PREDEFINED_ABSOLUTE_FONT_SIZES, + CR_BAD_PARAM_ERROR) ; + + a_this->type = PREDEFINED_ABSOLUTE_FONT_SIZE ; + a_this->value.predefined = a_predefined ; + + return CR_OK ; +} + +/** + * cr_font_size_set_relative_font_size: + * @a_this: the current instance of #CRFontSize + * @a_relative: the new relative font size + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_set_relative_font_size (CRFontSize *a_this, + enum CRRelativeFontSize a_relative) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR) ; + g_return_val_if_fail (a_relative >= FONT_SIZE_LARGER + && a_relative < NB_RELATIVE_FONT_SIZE, + CR_BAD_PARAM_ERROR) ; + + a_this->type = RELATIVE_FONT_SIZE ; + a_this->value.relative = a_relative ; + return CR_OK ; +} + +/** + * cr_font_size_set_absolute_font_size: + * @a_this: the current instance of #CRFontSize + * @a_num_type: the type of number to set. + * @a_value: the actual value to set. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_set_absolute_font_size (CRFontSize *a_this, + enum CRNumType a_num_type, + gdouble a_value) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR) ; + g_return_val_if_fail (a_num_type >= NUM_AUTO + && a_num_type < NB_NUM_TYPE, + CR_BAD_PARAM_ERROR) ; + + a_this->type = ABSOLUTE_FONT_SIZE ; + cr_num_set (&a_this->value.absolute, + a_value, a_num_type) ; + return CR_OK ; +} + +/** + * cr_font_size_set_to_inherit: + * @a_this: the current instance of #CRFontSize + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_font_size_set_to_inherit (CRFontSize *a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR) ; + + cr_font_size_clear (a_this) ; + a_this->type = INHERITED_FONT_SIZE ; + + return CR_OK ; +} + +/** + * cr_font_size_is_set_to_inherit: + * @a_this: the current instance of #CRFontSize. + * + * Returns TRUE if the current instance is set to 'inherit'. + */ +gboolean +cr_font_size_is_set_to_inherit (CRFontSize const *a_this) +{ + g_return_val_if_fail (a_this, FALSE) ; + + return a_this->type == INHERITED_FONT_SIZE ; +} + +/** + * cr_font_size_to_string: + * @a_this: the current instance of #CRFontSize + * + * Returns the serialized form of #CRFontSize. The returned string + * has to bee freed using g_free(). + */ +gchar * +cr_font_size_to_string (CRFontSize const * a_this) +{ + gchar *str = NULL; + + if (!a_this) { + str = g_strdup ("NULL"); + g_return_val_if_fail (str, NULL); + return str; + } + switch (a_this->type) { + case PREDEFINED_ABSOLUTE_FONT_SIZE: + str = g_strdup (cr_predefined_absolute_font_size_to_string + (a_this->value.predefined)); + break; + case ABSOLUTE_FONT_SIZE: + str = (gchar *) cr_num_to_string (&a_this->value.absolute); + break; + case RELATIVE_FONT_SIZE: + str = g_strdup (cr_relative_font_size_to_string + (a_this->value.relative)); + break; + case INHERITED_FONT_SIZE: + str = g_strdup ("inherit"); + break; + default: + break; + } + return str; +} + +/** + * cr_font_size_get_smaller_predefined: + * @a_font_size: the font size to consider. + * @a_smaller_size: out parameter. The a smaller value than @a_font_size. + */ +void +cr_font_size_get_smaller_predefined_font_size + (enum CRPredefinedAbsoluteFontSize a_font_size, + enum CRPredefinedAbsoluteFontSize *a_smaller_size) +{ + enum CRPredefinedAbsoluteFontSize result = FONT_SIZE_MEDIUM ; + + g_return_if_fail (a_smaller_size) ; + g_return_if_fail (a_font_size < NB_PREDEFINED_ABSOLUTE_FONT_SIZES + && a_font_size >= FONT_SIZE_XX_SMALL) ; + + switch (a_font_size) { + case FONT_SIZE_XX_SMALL: + result = FONT_SIZE_XX_SMALL ; + break ; + case FONT_SIZE_X_SMALL: + result = FONT_SIZE_XX_SMALL ; + break ; + case FONT_SIZE_SMALL: + result = FONT_SIZE_X_SMALL; + break ; + case FONT_SIZE_MEDIUM: + result = FONT_SIZE_SMALL; + break ; + case FONT_SIZE_LARGE: + result = FONT_SIZE_MEDIUM; + break ; + case FONT_SIZE_X_LARGE: + result = FONT_SIZE_LARGE; + break ; + case FONT_SIZE_XX_LARGE: + result = FONT_SIZE_XX_LARGE; + break ; + case FONT_SIZE_INHERIT: + cr_utils_trace_info ("can't return a smaller size for FONT_SIZE_INHERIT") ; + result = FONT_SIZE_MEDIUM ; + break ; + default: + cr_utils_trace_info ("Unknown FONT_SIZE") ; + result = FONT_SIZE_MEDIUM ; + break ; + } + *a_smaller_size = result ; +} + + +/** + * cr_font_size_get_larger_predefined_font_size: + * @a_font_size: the font size to consider. + * @a_larger_size: out parameter. the font size considered larger than + * @a_font_size. + * + */ +void +cr_font_size_get_larger_predefined_font_size + (enum CRPredefinedAbsoluteFontSize a_font_size, + enum CRPredefinedAbsoluteFontSize *a_larger_size) +{ + enum CRPredefinedAbsoluteFontSize result = FONT_SIZE_MEDIUM ; + + g_return_if_fail (a_larger_size) ; + g_return_if_fail (a_font_size >= FONT_SIZE_XX_SMALL + && a_font_size < NB_PREDEFINED_ABSOLUTE_FONT_SIZES) ; + + switch (a_font_size) { + case FONT_SIZE_XX_SMALL: + result = FONT_SIZE_X_SMALL ; + break ; + case FONT_SIZE_X_SMALL: + result = FONT_SIZE_SMALL ; + break ; + case FONT_SIZE_SMALL: + result = FONT_SIZE_MEDIUM; + break ; + case FONT_SIZE_MEDIUM: + result = FONT_SIZE_LARGE; + break ; + case FONT_SIZE_LARGE: + result = FONT_SIZE_X_LARGE; + break ; + case FONT_SIZE_X_LARGE: + result = FONT_SIZE_XX_LARGE ; + break ; + case FONT_SIZE_XX_LARGE: + result = FONT_SIZE_XX_LARGE; + break ; + case FONT_SIZE_INHERIT: + cr_utils_trace_info ("can't return a bigger size for FONT_SIZE_INHERIT") ; + result = FONT_SIZE_MEDIUM ; + break ; + default: + cr_utils_trace_info ("Unknown FONT_SIZE") ; + result = FONT_SIZE_MEDIUM ; + break ; + } + *a_larger_size = result ; +} + +/** + * cr_font_size_is_predefined_absolute_font_size: + * @a_font_size: the font size to consider. + * + * Returns TRUE if the instance is an predefined absolute font size, FALSE + * otherwise. + */ +gboolean +cr_font_size_is_predefined_absolute_font_size + (enum CRPredefinedAbsoluteFontSize a_font_size) +{ + if (a_font_size >= FONT_SIZE_XX_SMALL + && a_font_size < NB_PREDEFINED_ABSOLUTE_FONT_SIZES) { + return TRUE ; + } else { + return FALSE ; + } +} + +/** + * cr_font_size_adjust_to_string: + * @a_this: the instance of #CRFontSizeAdjust. + * + * Returns the serialized form of #CRFontSizeAdjust + */ +gchar * +cr_font_size_adjust_to_string (CRFontSizeAdjust const * a_this) +{ + gchar *str = NULL; + + if (!a_this) { + str = g_strdup ("NULL"); + g_return_val_if_fail (str, NULL); + return str; + } + + switch (a_this->type) { + case FONT_SIZE_ADJUST_NONE: + str = g_strdup ("none"); + break; + case FONT_SIZE_ADJUST_NUMBER: + if (a_this->num) + str = (gchar *) cr_num_to_string (a_this->num); + else + str = g_strdup ("unknown font-size-adjust property value"); /* Should raise an error no?*/ + break; + case FONT_SIZE_ADJUST_INHERIT: + str = g_strdup ("inherit"); + } + return str; +} + +/** + * cr_font_style_to_string: + * @a_code: the current instance of #CRFontStyle . + * + * Returns the serialized #CRFontStyle. The caller must free the returned + * string using g_free(). + */ +const gchar * +cr_font_style_to_string (enum CRFontStyle a_code) +{ + gchar *str = NULL; + + switch (a_code) { + case FONT_STYLE_NORMAL: + str = (gchar *) "normal"; + break; + case FONT_STYLE_ITALIC: + str = (gchar *) "italic"; + break; + case FONT_STYLE_OBLIQUE: + str = (gchar *) "oblique"; + break; + case FONT_STYLE_INHERIT: + str = (gchar *) "inherit"; + break; + default: + str = (gchar *) "unknown font style value"; + break; + } + return str; +} + +/** + * cr_font_variant_to_string: + * @a_code: the current instance of #CRFontVariant. + * + * Returns the serialized form of #CRFontVariant. The caller has + * to free the returned string using g_free(). + */ +const gchar * +cr_font_variant_to_string (enum CRFontVariant a_code) +{ + gchar *str = NULL; + + switch (a_code) { + case FONT_VARIANT_NORMAL: + str = (gchar *) "normal"; + break; + case FONT_VARIANT_SMALL_CAPS: + str = (gchar *) "small-caps"; + break; + case FONT_VARIANT_INHERIT: + str = (gchar *) "inherit"; + break; + } + return str; +} + +/** + * cr_font_weight_get_bolder: + * @a_weight: the #CRFontWeight to consider. + * + * Returns a font weight bolder than @a_weight + */ +enum CRFontWeight +cr_font_weight_get_bolder (enum CRFontWeight a_weight) +{ + if (a_weight == FONT_WEIGHT_INHERIT) { + cr_utils_trace_info ("can't return a bolder weight for FONT_WEIGHT_INHERIT") ; + return a_weight; + } else if (a_weight >= FONT_WEIGHT_900) { + return FONT_WEIGHT_900 ; + } else if (a_weight < FONT_WEIGHT_NORMAL) { + return FONT_WEIGHT_NORMAL ; + } else if (a_weight == FONT_WEIGHT_BOLDER + || a_weight == FONT_WEIGHT_LIGHTER) { + cr_utils_trace_info ("FONT_WEIGHT_BOLDER or FONT_WEIGHT_LIGHTER should not appear here") ; + return FONT_WEIGHT_NORMAL ; + } else { + return a_weight << 1 ; + } +} + +/** + * cr_font_weight_to_string: + * @a_code: the font weight to consider. + * + * Returns the serialized form of #CRFontWeight. + */ +const gchar * +cr_font_weight_to_string (enum CRFontWeight a_code) +{ + gchar *str = NULL; + + switch (a_code) { + case FONT_WEIGHT_NORMAL: + str = (gchar *) "normal"; + break; + case FONT_WEIGHT_BOLD: + str = (gchar *) "bold"; + break; + case FONT_WEIGHT_BOLDER: + str = (gchar *) "bolder"; + break; + case FONT_WEIGHT_LIGHTER: + str = (gchar *) "lighter"; + break; + case FONT_WEIGHT_100: + str = (gchar *) "100"; + break; + case FONT_WEIGHT_200: + str = (gchar *) "200"; + break; + case FONT_WEIGHT_300: + str = (gchar *) "300"; + break; + case FONT_WEIGHT_400: + str = (gchar *) "400"; + break; + case FONT_WEIGHT_500: + str = (gchar *) "500"; + break; + case FONT_WEIGHT_600: + str = (gchar *) "600"; + break; + case FONT_WEIGHT_700: + str = (gchar *) "700"; + break; + case FONT_WEIGHT_800: + str = (gchar *) "800"; + break; + case FONT_WEIGHT_900: + str = (gchar *) "900"; + break; + case FONT_WEIGHT_INHERIT: + str = (gchar *) "inherit"; + break; + default: + str = (gchar *) "unknown font-weight property value"; + break; + } + return str; +} + +/** + * cr_font_stretch_to_string: + * @a_code: the instance of #CRFontStretch to consider. + * + * Returns the serialized form of #CRFontStretch. + */ +const gchar * +cr_font_stretch_to_string (enum CRFontStretch a_code) +{ + gchar *str = NULL; + + switch (a_code) { + case FONT_STRETCH_NORMAL: + str = (gchar *) "normal"; + break; + case FONT_STRETCH_WIDER: + str = (gchar *) "wider"; + break; + case FONT_STRETCH_NARROWER: + str = (gchar *) "narrower"; + break; + case FONT_STRETCH_ULTRA_CONDENSED: + str = (gchar *) "ultra-condensed"; + break; + case FONT_STRETCH_EXTRA_CONDENSED: + str = (gchar *) "extra-condensed"; + break; + case FONT_STRETCH_CONDENSED: + str = (gchar *) "condensed"; + break; + case FONT_STRETCH_SEMI_CONDENSED: + str = (gchar *) "semi-condensed"; + break; + case FONT_STRETCH_SEMI_EXPANDED: + str = (gchar *) "semi-expanded"; + break; + case FONT_STRETCH_EXPANDED: + str = (gchar *) "expanded"; + break; + case FONT_STRETCH_EXTRA_EXPANDED: + str = (gchar *) "extra-expaned"; + break; + case FONT_STRETCH_ULTRA_EXPANDED: + str = (gchar *) "ultra-expanded"; + break; + case FONT_STRETCH_INHERIT: + str = (gchar *) "inherit"; + break; + } + return str; +} + +/** + * cr_font_size_destroy: + * @a_font_size: the font size to destroy + * + */ +void +cr_font_size_destroy (CRFontSize * a_font_size) +{ + g_return_if_fail (a_font_size); + + g_free (a_font_size) ; +} + +/******************************************************* + *'font-size-adjust' manipulation function definition + *******************************************************/ + +/** + * cr_font_size_adjust_new: + * + * Returns a newly built instance of #CRFontSizeAdjust + */ +CRFontSizeAdjust * +cr_font_size_adjust_new (void) +{ + CRFontSizeAdjust *result = NULL; + + result = g_try_malloc (sizeof (CRFontSizeAdjust)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRFontSizeAdjust)); + + return result; +} + +/** + * cr_font_size_adjust_destroy: + * @a_this: the current instance of #CRFontSizeAdjust. + * + */ +void +cr_font_size_adjust_destroy (CRFontSizeAdjust * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->type == FONT_SIZE_ADJUST_NUMBER && a_this->num) { + cr_num_destroy (a_this->num); + a_this->num = NULL; + } +} diff --git a/src/st/croco/cr-fonts.h b/src/st/croco/cr-fonts.h new file mode 100644 index 0000000..9eaeeeb --- /dev/null +++ b/src/st/croco/cr-fonts.h @@ -0,0 +1,315 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of + * the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_FONTS_H__ +#define __CR_FONTS_H__ + +#include "cr-utils.h" +#include "cr-num.h" + +/** + *@file + *Various type declarations about font selection related + *properties. + */ +G_BEGIN_DECLS + + +enum CRFontFamilyType +{ + FONT_FAMILY_SANS_SERIF, + FONT_FAMILY_SERIF, + FONT_FAMILY_CURSIVE, + FONT_FAMILY_FANTASY, + FONT_FAMILY_MONOSPACE, + FONT_FAMILY_NON_GENERIC, + FONT_FAMILY_INHERIT, + /**/ + NB_FONT_FAMILIE_TYPES +} ; + +typedef struct _CRFontFamily CRFontFamily ; + +struct _CRFontFamily +{ + enum CRFontFamilyType type ; + + /* + *The name of the font family, in case + *it is non generic. + *Is set only if the type is FONT_FAMILY_NON_GENERIC. + */ + guchar *name ; + + CRFontFamily *next ; + CRFontFamily *prev ; +} ; + + +/** + *The different types + *of absolute font size. + *This is used by the 'font-size' + *property defined in css2 spec + *in chapter 15.2.4 . + *These values a indexes of + *table of size so please, do not + *change their definition order unless + *you know what you are doing. + */ +enum CRPredefinedAbsoluteFontSize +{ + FONT_SIZE_XX_SMALL=0, + FONT_SIZE_X_SMALL, + FONT_SIZE_SMALL, + FONT_SIZE_MEDIUM, + FONT_SIZE_LARGE, + FONT_SIZE_X_LARGE, + FONT_SIZE_XX_LARGE, + FONT_SIZE_INHERIT, + NB_PREDEFINED_ABSOLUTE_FONT_SIZES +} ; + +/** + *The different types + *of relative font size. + *This is used by the 'font-size' + *property defined in css2 spec + *in chapter 15.2.4 . + *These values a indexes of + *table of size so please, do not + *change their definition order unless + *you know what you are doing. + */ +enum CRRelativeFontSize +{ + FONT_SIZE_LARGER, + FONT_SIZE_SMALLER, + NB_RELATIVE_FONT_SIZE +} ; + +/** + *The type of font-size property. + *Used to define the type of #CRFontSize . + *See css2 spec chapter 15.2.4 to understand. + */ +enum CRFontSizeType { + /** + *If the type of #CRFontSize is + *PREDEFINED_ABSOLUTE_FONT_SIZE, + *the CRFontSize::value.predefined_absolute + *field will be defined. + */ + PREDEFINED_ABSOLUTE_FONT_SIZE, + + /** + *If the type of #CRFontSize is + *ABSOLUTE_FONT_SIZE, + *the CRFontSize::value.absolute + *field will be defined. + */ + ABSOLUTE_FONT_SIZE, + + /** + *If the type of #CRFontSize is + *RELATIVE_FONT_SIZE, + *the CRFontSize::value.relative + *field will be defined. + */ + RELATIVE_FONT_SIZE, + + /** + *If the type of #CRFontSize is + *INHERITED_FONT_SIZE, + *the None of the field of the CRFontSize::value enum + *will be defined. + */ + INHERITED_FONT_SIZE, + + NB_FONT_SIZE_TYPE +} ; + +typedef struct _CRFontSize CRFontSize ; +struct _CRFontSize { + enum CRFontSizeType type ; + union { + enum CRPredefinedAbsoluteFontSize predefined ; + enum CRRelativeFontSize relative ; + CRNum absolute ; + } value; +} ; + +enum CRFontSizeAdjustType +{ + FONT_SIZE_ADJUST_NONE = 0, + FONT_SIZE_ADJUST_NUMBER, + FONT_SIZE_ADJUST_INHERIT +} ; +typedef struct _CRFontSizeAdjust CRFontSizeAdjust ; +struct _CRFontSizeAdjust +{ + enum CRFontSizeAdjustType type ; + CRNum *num ; +} ; + +enum CRFontStyle +{ + FONT_STYLE_NORMAL=0, + FONT_STYLE_ITALIC, + FONT_STYLE_OBLIQUE, + FONT_STYLE_INHERIT +} ; + +enum CRFontVariant +{ + FONT_VARIANT_NORMAL=0, + FONT_VARIANT_SMALL_CAPS, + FONT_VARIANT_INHERIT +} ; + +enum CRFontWeight +{ + FONT_WEIGHT_NORMAL = 1, + FONT_WEIGHT_BOLD = 1<<1, + FONT_WEIGHT_BOLDER = 1<<2, + FONT_WEIGHT_LIGHTER = 1<<3, + FONT_WEIGHT_100 = 1<<4, + FONT_WEIGHT_200 = 1<<5, + FONT_WEIGHT_300 = 1<<6, + FONT_WEIGHT_400 = 1<<7, + FONT_WEIGHT_500 = 1<<8, + FONT_WEIGHT_600 = 1<<9, + FONT_WEIGHT_700 = 1<<10, + FONT_WEIGHT_800 = 1<<11, + FONT_WEIGHT_900 = 1<<12, + FONT_WEIGHT_INHERIT = 1<<13, + NB_FONT_WEIGHTS +} ; + +enum CRFontStretch +{ + FONT_STRETCH_NORMAL=0, + FONT_STRETCH_WIDER, + FONT_STRETCH_NARROWER, + FONT_STRETCH_ULTRA_CONDENSED, + FONT_STRETCH_EXTRA_CONDENSED, + FONT_STRETCH_CONDENSED, + FONT_STRETCH_SEMI_CONDENSED, + FONT_STRETCH_SEMI_EXPANDED, + FONT_STRETCH_EXPANDED, + FONT_STRETCH_EXTRA_EXPANDED, + FONT_STRETCH_ULTRA_EXPANDED, + FONT_STRETCH_INHERIT +} ; + +/************************************** + *'font-family' manipulation functions + ***************************************/ +CRFontFamily * +cr_font_family_new (enum CRFontFamilyType a_type, guchar *a_name) ; + +CRFontFamily * +cr_font_family_append (CRFontFamily *a_this, + CRFontFamily *a_family_to_append) ; + +guchar * +cr_font_family_to_string (CRFontFamily const *a_this, + gboolean a_walk_font_family_list) ; + +CRFontFamily * +cr_font_family_prepend (CRFontFamily *a_this, + CRFontFamily *a_family_to_prepend); + +enum CRStatus +cr_font_family_destroy (CRFontFamily *a_this) ; + +enum CRStatus +cr_font_family_set_name (CRFontFamily *a_this, guchar *a_name) ; + + +/************************************ + *'font-size' manipulation functions + ***********************************/ + +CRFontSize * cr_font_size_new (void) ; + +enum CRStatus cr_font_size_clear (CRFontSize *a_this) ; + +enum CRStatus cr_font_size_copy (CRFontSize *a_dst, + CRFontSize const *a_src) ; +enum CRStatus cr_font_size_set_predefined_absolute_font_size (CRFontSize *a_this, + enum CRPredefinedAbsoluteFontSize a_predefined) ; +enum CRStatus cr_font_size_set_relative_font_size (CRFontSize *a_this, + enum CRRelativeFontSize a_relative) ; + +enum CRStatus cr_font_size_set_absolute_font_size (CRFontSize *a_this, + enum CRNumType a_num_type, + gdouble a_value) ; + +enum CRStatus cr_font_size_set_to_inherit (CRFontSize *a_this) ; + +gboolean cr_font_size_is_set_to_inherit (CRFontSize const *a_this) ; + +gchar* cr_font_size_to_string (CRFontSize const *a_this) ; + +void cr_font_size_destroy (CRFontSize *a_font_size) ; + +/******************************************************* + *'font-size-adjust' manipulation function declarations + *******************************************************/ + +CRFontSizeAdjust * cr_font_size_adjust_new (void) ; + +gchar * cr_font_size_adjust_to_string (CRFontSizeAdjust const *a_this) ; + +void cr_font_size_adjust_destroy (CRFontSizeAdjust *a_this) ; + +void +cr_font_size_get_smaller_predefined_font_size (enum CRPredefinedAbsoluteFontSize a_font_size, + enum CRPredefinedAbsoluteFontSize *a_smaller_size) ; +void +cr_font_size_get_larger_predefined_font_size (enum CRPredefinedAbsoluteFontSize a_font_size, + enum CRPredefinedAbsoluteFontSize *a_larger_size) ; + +gboolean +cr_font_size_is_predefined_absolute_font_size (enum CRPredefinedAbsoluteFontSize a_font_size) ; + +/*********************************** + *various other font related functions + ***********************************/ +const gchar * cr_font_style_to_string (enum CRFontStyle a_code) ; + +const gchar * cr_font_weight_to_string (enum CRFontWeight a_code) ; + +enum CRFontWeight +cr_font_weight_get_bolder (enum CRFontWeight a_weight) ; + +const gchar * cr_font_variant_to_string (enum CRFontVariant a_code) ; + +const gchar * cr_font_stretch_to_string (enum CRFontStretch a_code) ; + +G_END_DECLS + +#endif diff --git a/src/st/croco/cr-input.c b/src/st/croco/cr-input.c new file mode 100644 index 0000000..430e75e --- /dev/null +++ b/src/st/croco/cr-input.c @@ -0,0 +1,1191 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include "stdio.h" +#include <string.h> +#include "cr-input.h" +#include "cr-enc-handler.h" + +/** + *@CRInput: + * + *The definition of the #CRInput class. + */ + +/******************* + *Private type defs + *******************/ + +/** + *The private attributes of + *the #CRInputPriv class. + */ +struct _CRInputPriv { + /* + *The input buffer + */ + guchar *in_buf; + gulong in_buf_size; + + gulong nb_bytes; + + /* + *The index of the next byte + *to be read. + */ + gulong next_byte_index; + + /* + *The current line number + */ + gulong line; + + /* + *The current col number + */ + gulong col; + + gboolean end_of_line; + gboolean end_of_input; + + /* + *the reference count of this + *instance. + */ + guint ref_count; + gboolean free_in_buf; +}; + +#define PRIVATE(object) (object)->priv + +/*************************** + *private constants + **************************/ +#define CR_INPUT_MEM_CHUNK_SIZE 1024 * 4 + +static CRInput *cr_input_new_real (void); + +static CRInput * +cr_input_new_real (void) +{ + CRInput *result = NULL; + + result = g_try_malloc (sizeof (CRInput)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRInput)); + + PRIVATE (result) = g_try_malloc (sizeof (CRInputPriv)); + if (!PRIVATE (result)) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + memset (PRIVATE (result), 0, sizeof (CRInputPriv)); + PRIVATE (result)->free_in_buf = TRUE; + return result; +} + +/**************** + *Public methods + ***************/ + +/** + * cr_input_new_from_buf: + *@a_buf: the memory buffer to create the input stream from. + *The #CRInput keeps this pointer so user should not free it !. + *@a_len: the size of the input buffer. + *@a_enc: the buffer's encoding. + *@a_free_buf: if set to TRUE, this a_buf will be freed + *at the destruction of this instance. If set to false, it is up + *to the caller to free it. + * + *Creates a new input stream from a memory buffer. + *Returns the newly built instance of #CRInput. + */ +CRInput * +cr_input_new_from_buf (guchar * a_buf, + gulong a_len, + enum CREncoding a_enc, + gboolean a_free_buf) +{ + CRInput *result = NULL; + enum CRStatus status = CR_OK; + CREncHandler *enc_handler = NULL; + gulong len = a_len; + + g_return_val_if_fail (a_buf, NULL); + + result = cr_input_new_real (); + g_return_val_if_fail (result, NULL); + + /*transform the encoding in utf8 */ + if (a_enc != CR_UTF_8) { + enc_handler = cr_enc_handler_get_instance (a_enc); + if (!enc_handler) { + goto error; + } + + status = cr_enc_handler_convert_input + (enc_handler, a_buf, &len, + &PRIVATE (result)->in_buf, + &PRIVATE (result)->in_buf_size); + if (status != CR_OK) + goto error; + PRIVATE (result)->free_in_buf = TRUE; + if (a_free_buf == TRUE && a_buf) { + g_free (a_buf) ; + a_buf = NULL ; + } + PRIVATE (result)->nb_bytes = PRIVATE (result)->in_buf_size; + } else { + PRIVATE (result)->in_buf = (guchar *) a_buf; + PRIVATE (result)->in_buf_size = a_len; + PRIVATE (result)->nb_bytes = a_len; + PRIVATE (result)->free_in_buf = a_free_buf; + } + PRIVATE (result)->line = 1; + PRIVATE (result)->col = 0; + return result; + + error: + if (result) { + cr_input_destroy (result); + result = NULL; + } + + return NULL; +} + +/** + * cr_input_new_from_uri: + *@a_file_uri: the file to create *the input stream from. + *@a_enc: the encoding of the file *to create the input from. + * + *Creates a new input stream from + *a file. + * + *Returns the newly created input stream if + *this method could read the file and create it, + *NULL otherwise. + */ + +CRInput * +cr_input_new_from_uri (const gchar * a_file_uri, enum CREncoding a_enc) +{ + CRInput *result = NULL; + enum CRStatus status = CR_OK; + FILE *file_ptr = NULL; + guchar tmp_buf[CR_INPUT_MEM_CHUNK_SIZE] = { 0 }; + gulong nb_read = 0, + len = 0, + buf_size = 0; + gboolean loop = TRUE; + guchar *buf = NULL; + + g_return_val_if_fail (a_file_uri, NULL); + + file_ptr = fopen (a_file_uri, "r"); + + if (file_ptr == NULL) { + +#ifdef CR_DEBUG + cr_utils_trace_debug ("could not open file"); +#endif + g_warning ("Could not open file %s\n", a_file_uri); + + return NULL; + } + + /*load the file */ + while (loop) { + nb_read = fread (tmp_buf, 1 /*read bytes */ , + CR_INPUT_MEM_CHUNK_SIZE /*nb of bytes */ , + file_ptr); + + if (nb_read != CR_INPUT_MEM_CHUNK_SIZE) { + /*we read less chars than we wanted */ + if (feof (file_ptr)) { + /*we reached eof */ + loop = FALSE; + } else { + /*a pb occurred !! */ + cr_utils_trace_debug ("an io error occurred"); + status = CR_ERROR; + goto cleanup; + } + } + + if (status == CR_OK) { + /*read went well */ + buf = g_realloc (buf, len + CR_INPUT_MEM_CHUNK_SIZE); + memcpy (buf + len, tmp_buf, nb_read); + len += nb_read; + buf_size += CR_INPUT_MEM_CHUNK_SIZE; + } + } + + if (status == CR_OK) { + result = cr_input_new_from_buf (buf, len, a_enc, TRUE); + if (!result) { + goto cleanup; + } + /* + *we should free buf here because it's own by CRInput. + *(see the last parameter of cr_input_new_from_buf(). + */ + buf = NULL; + } + + cleanup: + if (file_ptr) { + fclose (file_ptr); + file_ptr = NULL; + } + + if (buf) { + g_free (buf); + buf = NULL; + } + + return result; +} + +/** + * cr_input_destroy: + *@a_this: the current instance of #CRInput. + * + *The destructor of the #CRInput class. + */ +void +cr_input_destroy (CRInput * a_this) +{ + if (a_this == NULL) + return; + + if (PRIVATE (a_this)) { + if (PRIVATE (a_this)->in_buf && PRIVATE (a_this)->free_in_buf) { + g_free (PRIVATE (a_this)->in_buf); + PRIVATE (a_this)->in_buf = NULL; + } + + g_free (PRIVATE (a_this)); + PRIVATE (a_this) = NULL; + } + + g_free (a_this); +} + +/** + * cr_input_ref: + *@a_this: the current instance of #CRInput. + * + *Increments the reference count of the current + *instance of #CRInput. + */ +void +cr_input_ref (CRInput * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + PRIVATE (a_this)->ref_count++; +} + +/** + * cr_input_unref: + *@a_this: the current instance of #CRInput. + * + *Decrements the reference count of this instance + *of #CRInput. If the reference count goes down to + *zero, this instance is destroyed. + * + * Returns TRUE if the instance of #CRInput got destroyed, false otherwise. + */ +gboolean +cr_input_unref (CRInput * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), FALSE); + + if (PRIVATE (a_this)->ref_count) { + PRIVATE (a_this)->ref_count--; + } + + if (PRIVATE (a_this)->ref_count == 0) { + cr_input_destroy (a_this); + return TRUE; + } + return FALSE; +} + +/** + * cr_input_end_of_input: + *@a_this: the current instance of #CRInput. + *@a_end_of_input: out parameter. Is set to TRUE if + *the current instance has reached the end of its input buffer, + *FALSE otherwise. + * + *Tests whether the current instance of + *#CRInput has reached its input buffer. + * + * Returns CR_OK upon successful completion, an error code otherwise. + * Note that all the out parameters of this method are valid if + * and only if this method returns CR_OK. + */ +enum CRStatus +cr_input_end_of_input (CRInput const * a_this, gboolean * a_end_of_input) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_end_of_input, CR_BAD_PARAM_ERROR); + + *a_end_of_input = (PRIVATE (a_this)->next_byte_index + >= PRIVATE (a_this)->in_buf_size) ? TRUE : FALSE; + + return CR_OK; +} + +/** + * cr_input_get_nb_bytes_left: + *@a_this: the current instance of #CRInput. + * + *Returns the number of bytes left in the input stream + *before the end, -1 in case of error. + */ +glong +cr_input_get_nb_bytes_left (CRInput const * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), -1); + g_return_val_if_fail (PRIVATE (a_this)->nb_bytes + <= PRIVATE (a_this)->in_buf_size, -1); + g_return_val_if_fail (PRIVATE (a_this)->next_byte_index + <= PRIVATE (a_this)->nb_bytes, -1); + + if (PRIVATE (a_this)->end_of_input) + return 0; + + return PRIVATE (a_this)->nb_bytes - PRIVATE (a_this)->next_byte_index; +} + +/** + * cr_input_read_byte: + *@a_this: the current instance of #CRInput. + *@a_byte: out parameter the returned byte. + * + *Gets the next byte of the input. + *Updates the state of the input so that + *the next invocation of this method returns + *the next coming byte. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. All the out parameters of this method are valid if + *and only if this method returns CR_OK. + */ +enum CRStatus +cr_input_read_byte (CRInput * a_this, guchar * a_byte) +{ + gulong nb_bytes_left = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_byte, CR_BAD_PARAM_ERROR); + + g_return_val_if_fail (PRIVATE (a_this)->next_byte_index <= + PRIVATE (a_this)->nb_bytes, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->end_of_input == TRUE) + return CR_END_OF_INPUT_ERROR; + + nb_bytes_left = cr_input_get_nb_bytes_left (a_this); + + if (nb_bytes_left < 1) { + return CR_END_OF_INPUT_ERROR; + } + + *a_byte = PRIVATE (a_this)->in_buf[PRIVATE (a_this)->next_byte_index]; + + if (PRIVATE (a_this)->nb_bytes - + PRIVATE (a_this)->next_byte_index < 2) { + PRIVATE (a_this)->end_of_input = TRUE; + } else { + PRIVATE (a_this)->next_byte_index++; + } + + return CR_OK; +} + +/** + * cr_input_read_char: + *@a_this: the current instance of CRInput. + *@a_char: out parameter. The read character. + * + *Reads an unicode character from the current instance of + *#CRInput. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_input_read_char (CRInput * a_this, guint32 * a_char) +{ + enum CRStatus status = CR_OK; + gulong consumed = 0, + nb_bytes_left = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_char, + CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->end_of_input == TRUE) + return CR_END_OF_INPUT_ERROR; + + nb_bytes_left = cr_input_get_nb_bytes_left (a_this); + + if (nb_bytes_left < 1) { + return CR_END_OF_INPUT_ERROR; + } + + status = cr_utils_read_char_from_utf8_buf + (PRIVATE (a_this)->in_buf + + + PRIVATE (a_this)->next_byte_index, + nb_bytes_left, a_char, &consumed); + + if (status == CR_OK) { + /*update next byte index */ + PRIVATE (a_this)->next_byte_index += consumed; + + /*update line and column number */ + if (PRIVATE (a_this)->end_of_line == TRUE) { + PRIVATE (a_this)->col = 1; + PRIVATE (a_this)->line++; + PRIVATE (a_this)->end_of_line = FALSE; + } else if (*a_char != '\n') { + PRIVATE (a_this)->col++; + } + + if (*a_char == '\n') { + PRIVATE (a_this)->end_of_line = TRUE; + } + } + + return status; +} + +/** + * cr_input_set_line_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_line_num: the new line number. + * + *Setter of the current line number. + * + *Return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_set_line_num (CRInput * a_this, glong a_line_num) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->line = a_line_num; + + return CR_OK; +} + +/** + * cr_input_get_line_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_line_num: the returned line number. + * + *Getter of the current line number. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_get_line_num (CRInput const * a_this, glong * a_line_num) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_line_num, CR_BAD_PARAM_ERROR); + + *a_line_num = PRIVATE (a_this)->line; + + return CR_OK; +} + +/** + * cr_input_set_column_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_col: the new column number. + * + *Setter of the current column number. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_set_column_num (CRInput * a_this, glong a_col) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->col = a_col; + + return CR_OK; +} + +/** + * cr_input_get_column_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_col: out parameter + * + *Getter of the current column number. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_get_column_num (CRInput const * a_this, glong * a_col) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_col, + CR_BAD_PARAM_ERROR); + + *a_col = PRIVATE (a_this)->col; + + return CR_OK; +} + +/** + * cr_input_increment_line_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_increment: the increment to add to the line number. + * + *Increments the current line number. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_increment_line_num (CRInput * a_this, glong a_increment) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->line += a_increment; + + return CR_OK; +} + +/** + * cr_input_increment_col_num: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_increment: the increment to add to the column number. + * + *Increments the current column number. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_increment_col_num (CRInput * a_this, glong a_increment) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->col += a_increment; + + return CR_OK; +} + +/** + * cr_input_consume_char: + *@a_this: the this pointer. + *@a_char: the character to consume. If set to zero, + *consumes any character. + * + *Consumes the next character of the input stream if + *and only if that character equals a_char. + * + *Returns CR_OK upon successful completion, CR_PARSING_ERROR if + *next char is different from a_char, an other error code otherwise + */ +enum CRStatus +cr_input_consume_char (CRInput * a_this, guint32 a_char) +{ + guint32 c; + enum CRStatus status; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + if ((status = cr_input_peek_char (a_this, &c)) != CR_OK) { + return status; + } + + if (c == a_char || a_char == 0) { + status = cr_input_read_char (a_this, &c); + } else { + return CR_PARSING_ERROR; + } + + return status; +} + +/** + * cr_input_consume_chars: + *@a_this: the this pointer of the current instance of #CRInput. + *@a_char: the character to consume. + *@a_nb_char: in/out parameter. The number of characters to consume. + *If set to a negative value, the function will consume all the occurrences + *of a_char found. + *After return, if the return value equals CR_OK, this variable contains + *the number of characters actually consumed. + * + *Consumes up to a_nb_char occurrences of the next contiguous characters + *which equal a_char. Note that the next character of the input stream + **MUST* equal a_char to trigger the consumption, or else, the error + *code CR_PARSING_ERROR is returned. + *If the number of contiguous characters that equals a_char is less than + *a_nb_char, then this function consumes all the characters it can consume. + * + *Returns CR_OK if at least one character has been consumed, an error code + *otherwise. + */ +enum CRStatus +cr_input_consume_chars (CRInput * a_this, guint32 a_char, gulong * a_nb_char) +{ + enum CRStatus status = CR_OK; + gulong nb_consumed = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_nb_char, + CR_BAD_PARAM_ERROR); + + g_return_val_if_fail (a_char != 0 || a_nb_char != NULL, + CR_BAD_PARAM_ERROR); + + for (nb_consumed = 0; ((status == CR_OK) + && (*a_nb_char > 0 + && nb_consumed < *a_nb_char)); + nb_consumed++) { + status = cr_input_consume_char (a_this, a_char); + } + + *a_nb_char = nb_consumed; + + if ((nb_consumed > 0) + && ((status == CR_PARSING_ERROR) + || (status == CR_END_OF_INPUT_ERROR))) { + status = CR_OK; + } + + return status; +} + +/** + * cr_input_consume_white_spaces: + *@a_this: the "this pointer" of the current instance of #CRInput. + *@a_nb_chars: in/out parameter. The number of white spaces to + *consume. After return, holds the number of white spaces actually consumed. + * + *Same as cr_input_consume_chars() but this one consumes white + *spaces. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_consume_white_spaces (CRInput * a_this, gulong * a_nb_chars) +{ + enum CRStatus status = CR_OK; + guint32 cur_char = 0, + nb_consumed = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_nb_chars, + CR_BAD_PARAM_ERROR); + + for (nb_consumed = 0; + ((*a_nb_chars > 0) && (nb_consumed < *a_nb_chars)); + nb_consumed++) { + status = cr_input_peek_char (a_this, &cur_char); + if (status != CR_OK) + break; + + /*if the next char is a white space, consume it ! */ + if (cr_utils_is_white_space (cur_char) == TRUE) { + status = cr_input_read_char (a_this, &cur_char); + if (status != CR_OK) + break; + continue; + } + + break; + + } + + *a_nb_chars = (gulong) nb_consumed; + + if (nb_consumed && status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + } + + return status; +} + +/** + * cr_input_peek_char: + *@a_this: the current instance of #CRInput. + *@a_char: out parameter. The returned character. + * + *Same as cr_input_read_char() but does not update the + *internal state of the input stream. The next call + *to cr_input_peek_char() or cr_input_read_char() will thus + *return the same character as the current one. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_input_peek_char (CRInput const * a_this, guint32 * a_char) +{ + enum CRStatus status = CR_OK; + gulong consumed = 0, + nb_bytes_left = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_char, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->next_byte_index >= + PRIVATE (a_this)->in_buf_size) { + return CR_END_OF_INPUT_ERROR; + } + + nb_bytes_left = cr_input_get_nb_bytes_left (a_this); + + if (nb_bytes_left < 1) { + return CR_END_OF_INPUT_ERROR; + } + + status = cr_utils_read_char_from_utf8_buf + (PRIVATE (a_this)->in_buf + + PRIVATE (a_this)->next_byte_index, + nb_bytes_left, a_char, &consumed); + + return status; +} + +/** + * cr_input_peek_byte: + *@a_this: the current instance of #CRInput. + *@a_origin: the origin to consider in the calculation + *of the position of the byte to peek. + *@a_offset: the offset of the byte to peek, starting from + *the origin specified by a_origin. + *@a_byte: out parameter the peeked byte. + * + *Gets a byte from the input stream, + *starting from the current position in the input stream. + *Unlike cr_input_peek_next_byte() this method + *does not update the state of the current input stream. + *Subsequent calls to cr_input_peek_byte with the same arguments + *will return the same byte. + * + *Returns CR_OK upon successful completion or, + *CR_BAD_PARAM_ERROR if at least one of the parameters is invalid; + *CR_OUT_OF_BOUNDS_ERROR if the indexed byte is out of bounds. + */ +enum CRStatus +cr_input_peek_byte (CRInput const * a_this, enum CRSeekPos a_origin, + gulong a_offset, guchar * a_byte) +{ + gulong abs_offset = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_byte, CR_BAD_PARAM_ERROR); + + switch (a_origin) { + + case CR_SEEK_CUR: + abs_offset = PRIVATE (a_this)->next_byte_index - 1 + a_offset; + break; + + case CR_SEEK_BEGIN: + abs_offset = a_offset; + break; + + case CR_SEEK_END: + abs_offset = PRIVATE (a_this)->in_buf_size - 1 - a_offset; + break; + + default: + return CR_BAD_PARAM_ERROR; + } + + if (abs_offset < PRIVATE (a_this)->in_buf_size) { + + *a_byte = PRIVATE (a_this)->in_buf[abs_offset]; + + return CR_OK; + + } else { + return CR_END_OF_INPUT_ERROR; + } +} + +/** + * cr_input_peek_byte2: + *@a_this: the current byte input stream. + *@a_offset: the offset of the byte to peek, starting + *from the current input position pointer. + *@a_eof: out parameter. Is set to true is we reach end of + *stream. If set to NULL by the caller, this parameter is not taken + *in account. + * + *Same as cr_input_peek_byte() but with a simplified + *interface. + * + *Returns the read byte or 0 if something bad happened. + */ +guchar +cr_input_peek_byte2 (CRInput const * a_this, gulong a_offset, gboolean * a_eof) +{ + guchar result = 0; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), 0); + + if (a_eof) + *a_eof = FALSE; + + status = cr_input_peek_byte (a_this, CR_SEEK_CUR, a_offset, &result); + + if ((status == CR_END_OF_INPUT_ERROR) + && a_eof) + *a_eof = TRUE; + + return result; +} + +/** + * cr_input_get_byte_addr: + *@a_this: the current instance of #CRInput. + *@a_offset: the offset of the byte in the input stream starting + *from the beginning of the stream. + * + *Gets the memory address of the byte located at a given offset + *in the input stream. + * + *Returns the address, otherwise NULL if an error occurred. + */ +guchar * +cr_input_get_byte_addr (CRInput * a_this, gulong a_offset) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), NULL); + + if (a_offset >= PRIVATE (a_this)->nb_bytes) { + return NULL; + } + + return &PRIVATE (a_this)->in_buf[a_offset]; +} + +/** + * cr_input_get_cur_byte_addr: + *@a_this: the current input stream + *@a_offset: out parameter. The returned address. + * + *Gets the address of the current character pointer. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_get_cur_byte_addr (CRInput * a_this, guchar ** a_offset) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_offset, + CR_BAD_PARAM_ERROR); + + if (!PRIVATE (a_this)->next_byte_index) { + return CR_START_OF_INPUT_ERROR; + } + + *a_offset = cr_input_get_byte_addr + (a_this, PRIVATE (a_this)->next_byte_index - 1); + + return CR_OK; +} + +/** + * cr_input_seek_index: + *@a_this: the current instance of #CRInput. + *@a_origin: the origin to consider during the calculation + *of the absolute position of the new "current byte index". + *@a_pos: the relative offset of the new "current byte index." + *This offset is relative to the origin a_origin. + * + *Sets the "current byte index" of the current instance + *of #CRInput. Next call to cr_input_get_byte() will return + *the byte next after the new "current byte index". + * + *Returns CR_OK upon successful completion otherwise returns + *CR_BAD_PARAM_ERROR if at least one of the parameters is not valid + *or CR_OUT_BOUNDS_ERROR in case of error. + */ +enum CRStatus +cr_input_seek_index (CRInput * a_this, enum CRSeekPos a_origin, gint a_pos) +{ + + glong abs_offset = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + switch (a_origin) { + + case CR_SEEK_CUR: + abs_offset = PRIVATE (a_this)->next_byte_index - 1 + a_pos; + break; + + case CR_SEEK_BEGIN: + abs_offset = a_pos; + break; + + case CR_SEEK_END: + abs_offset = PRIVATE (a_this)->in_buf_size - 1 - a_pos; + break; + + default: + return CR_BAD_PARAM_ERROR; + } + + if ((abs_offset > 0) + && (gulong) abs_offset < PRIVATE (a_this)->nb_bytes) { + + /*update the input stream's internal state */ + PRIVATE (a_this)->next_byte_index = abs_offset + 1; + + return CR_OK; + } + + return CR_OUT_OF_BOUNDS_ERROR; +} + +/** + * cr_input_get_cur_pos: + *@a_this: the current instance of #CRInput. + *@a_pos: out parameter. The returned position. + * + *Gets the position of the "current byte index" which + *is basically the position of the last returned byte in the + *input stream. + * + *Returns CR_OK upon successful completion. Otherwise, + *CR_BAD_PARAMETER_ERROR if at least one of the arguments is invalid. + *CR_START_OF_INPUT if no call to either cr_input_read_byte() + *or cr_input_seek_index() have been issued before calling + *cr_input_get_cur_pos() + *Note that the out parameters of this function are valid if and only if this + *function returns CR_OK. + */ +enum CRStatus +cr_input_get_cur_pos (CRInput const * a_this, CRInputPos * a_pos) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_pos, + CR_BAD_PARAM_ERROR); + + a_pos->next_byte_index = PRIVATE (a_this)->next_byte_index; + a_pos->line = PRIVATE (a_this)->line; + a_pos->col = PRIVATE (a_this)->col; + a_pos->end_of_line = PRIVATE (a_this)->end_of_line; + a_pos->end_of_file = PRIVATE (a_this)->end_of_input; + + return CR_OK; +} + +/** + * cr_input_get_parsing_location: + *@a_this: the current instance of #CRInput + *@a_loc: the set parsing location. + * + *Gets the current parsing location. + *The Parsing location is a public datastructure that + *represents the current line/column/byte offset/ in the input + *stream. + * + *Returns CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_input_get_parsing_location (CRInput const *a_this, + CRParsingLocation *a_loc) +{ + g_return_val_if_fail (a_this + && PRIVATE (a_this) + && a_loc, + CR_BAD_PARAM_ERROR) ; + + a_loc->line = PRIVATE (a_this)->line ; + a_loc->column = PRIVATE (a_this)->col ; + if (PRIVATE (a_this)->next_byte_index) { + a_loc->byte_offset = PRIVATE (a_this)->next_byte_index - 1 ; + } else { + a_loc->byte_offset = PRIVATE (a_this)->next_byte_index ; + } + return CR_OK ; +} + +/** + * cr_input_get_cur_index: + *@a_this: the "this pointer" of the current instance of + *#CRInput + *@a_index: out parameter. The returned index. + * + *Getter of the next byte index. + *It actually returns the index of the + *next byte to be read. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_input_get_cur_index (CRInput const * a_this, glong * a_index) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_index, CR_BAD_PARAM_ERROR); + + *a_index = PRIVATE (a_this)->next_byte_index; + + return CR_OK; +} + +/** + * cr_input_set_cur_index: + *@a_this: the "this pointer" of the current instance + *of #CRInput . + *@a_index: the new index to set. + * + *Setter of the next byte index. + *It sets the index of the next byte to be read. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_set_cur_index (CRInput * a_this, glong a_index) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->next_byte_index = a_index; + + return CR_OK; +} + +/** + * cr_input_set_end_of_file: + *@a_this: the current instance of #CRInput. + *@a_eof: the new end of file flag. + * + *Sets the end of file flag. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_set_end_of_file (CRInput * a_this, gboolean a_eof) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->end_of_input = a_eof; + + return CR_OK; +} + +/** + * cr_input_get_end_of_file: + *@a_this: the current instance of #CRInput. + *@a_eof: out parameter the place to put the end of + *file flag. + * + *Gets the end of file flag. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_get_end_of_file (CRInput const * a_this, gboolean * a_eof) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_eof, CR_BAD_PARAM_ERROR); + + *a_eof = PRIVATE (a_this)->end_of_input; + + return CR_OK; +} + +/** + * cr_input_set_end_of_line: + *@a_this: the current instance of #CRInput. + *@a_eol: the new end of line flag. + * + *Sets the end of line flag. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_input_set_end_of_line (CRInput * a_this, gboolean a_eol) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->end_of_line = a_eol; + + return CR_OK; +} + +/** + * cr_input_get_end_of_line: + *@a_this: the current instance of #CRInput + *@a_eol: out parameter. The place to put + *the returned flag + * + *Gets the end of line flag of the current input. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_input_get_end_of_line (CRInput const * a_this, gboolean * a_eol) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_eol, CR_BAD_PARAM_ERROR); + + *a_eol = PRIVATE (a_this)->end_of_line; + + return CR_OK; +} + +/** + * cr_input_set_cur_pos: + *@a_this: the "this pointer" of the current instance of + *#CRInput. + *@a_pos: the new position. + * + *Sets the current position in the input stream. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_input_set_cur_pos (CRInput * a_this, CRInputPos const * a_pos) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_pos, + CR_BAD_PARAM_ERROR); + + cr_input_set_column_num (a_this, a_pos->col); + cr_input_set_line_num (a_this, a_pos->line); + cr_input_set_cur_index (a_this, a_pos->next_byte_index); + cr_input_set_end_of_line (a_this, a_pos->end_of_line); + cr_input_set_end_of_file (a_this, a_pos->end_of_file); + + return CR_OK; +} diff --git a/src/st/croco/cr-input.h b/src/st/croco/cr-input.h new file mode 100644 index 0000000..9eb402a --- /dev/null +++ b/src/st/croco/cr-input.h @@ -0,0 +1,174 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset:8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See the COPYRIGHTS file for copyrights information. + */ + +#ifndef __CR_INPUT_SRC_H__ +#define __CR_INPUT_SRC_H__ + + +#include <glib.h> +#include "cr-utils.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +/** + *@file + *The libcroco basic input stream class + *declaration file. + */ + +typedef struct _CRInput CRInput ; +typedef struct _CRInputPriv CRInputPriv ; + +/** + *The #CRInput class provides the abstraction of + *an utf8-encoded character stream. + */ +struct _CRInput +{ + CRInputPriv *priv ; +} ; + +typedef struct _CRInputPos CRInputPos ; + +struct _CRInputPos +{ + glong line ; + glong col ; + gboolean end_of_file ; + gboolean end_of_line ; + glong next_byte_index ; +} ; + +CRInput * +cr_input_new_from_buf (guchar *a_buf, gulong a_len, + enum CREncoding a_enc, gboolean a_free_buf) ; +CRInput * +cr_input_new_from_uri (const gchar *a_file_uri, + enum CREncoding a_enc) ; + +void +cr_input_destroy (CRInput *a_this) ; + +void +cr_input_ref (CRInput *a_this) ; + +gboolean +cr_input_unref (CRInput *a_this) ; + +enum CRStatus +cr_input_read_byte (CRInput *a_this, guchar *a_byte) ; + +enum CRStatus +cr_input_read_char (CRInput *a_this, guint32 *a_char) ; + +enum CRStatus +cr_input_consume_chars (CRInput *a_this, guint32 a_char, + gulong *a_nb_char) ; + +enum CRStatus +cr_input_consume_char (CRInput *a_this, guint32 a_char) ; + +enum CRStatus +cr_input_consume_white_spaces (CRInput *a_this, gulong *a_nb_chars) ; + +enum CRStatus +cr_input_peek_byte (CRInput const *a_this, enum CRSeekPos a_origin, + gulong a_offset, guchar *a_byte) ; + +guchar +cr_input_peek_byte2 (CRInput const *a_this, gulong a_offset, + gboolean *a_eof) ; + +enum CRStatus +cr_input_peek_char (CRInput const *a_this, guint32 *a_char) ; + +guchar * +cr_input_get_byte_addr (CRInput *a_this, + gulong a_offset) ; + +enum CRStatus +cr_input_get_cur_byte_addr (CRInput *a_this, guchar ** a_offset) ; + +enum CRStatus +cr_input_seek_index (CRInput *a_this, + enum CRSeekPos a_origin, gint a_pos) ; + +enum CRStatus +cr_input_get_cur_index (CRInput const *a_this, glong *a_index) ; + +enum CRStatus +cr_input_set_cur_index (CRInput *a_this, glong a_index) ; + +enum CRStatus +cr_input_get_cur_pos (CRInput const *a_this, CRInputPos * a_pos) ; + +enum CRStatus +cr_input_set_cur_pos (CRInput *a_this, CRInputPos const *a_pos) ; + +enum CRStatus +cr_input_get_parsing_location (CRInput const *a_this, + CRParsingLocation *a_loc) ; + +enum CRStatus +cr_input_get_end_of_line (CRInput const *a_this, gboolean *a_eol) ; + +enum CRStatus +cr_input_set_end_of_line (CRInput *a_this, gboolean a_eol) ; + +enum CRStatus +cr_input_get_end_of_file (CRInput const *a_this, gboolean *a_eof) ; + +enum CRStatus +cr_input_set_end_of_file (CRInput *a_this, gboolean a_eof) ; + +enum CRStatus +cr_input_set_line_num (CRInput *a_this, glong a_line_num) ; + +enum CRStatus +cr_input_get_line_num (CRInput const *a_this, glong *a_line_num) ; + +enum CRStatus +cr_input_set_column_num (CRInput *a_this, glong a_col) ; + +enum CRStatus +cr_input_get_column_num (CRInput const *a_this, glong *a_col) ; + +enum CRStatus +cr_input_increment_line_num (CRInput *a_this, + glong a_increment) ; + +enum CRStatus +cr_input_increment_col_num (CRInput *a_this, + glong a_increment) ; + +glong +cr_input_get_nb_bytes_left (CRInput const *a_this) ; + +enum CRStatus +cr_input_end_of_input (CRInput const *a_this, gboolean *a_end_of_input) ; + +G_END_DECLS + +#endif /*__CR_INPUT_SRC_H__*/ + diff --git a/src/st/croco/cr-num.c b/src/st/croco/cr-num.c new file mode 100644 index 0000000..ba17285 --- /dev/null +++ b/src/st/croco/cr-num.c @@ -0,0 +1,313 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +/** + *@CRNum: + * + *The definition + *of the #CRNum class. + */ + +#include "cr-num.h" +#include "string.h" + +/** + * cr_num_new: + * + *#CRNum. + * + *Returns the newly built instance of + *#CRNum. + */ +CRNum * +cr_num_new (void) +{ + CRNum *result = NULL; + + result = g_try_malloc (sizeof (CRNum)); + + if (result == NULL) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRNum)); + + return result; +} + +/** + * cr_num_new_with_val: + * @a_val: the numerical value of the number. + * @a_type: the type of number. + * + * A constructor of #CRNum. + * + * Returns the newly built instance of #CRNum or + * NULL if an error arises. + */ +CRNum * +cr_num_new_with_val (gdouble a_val, enum CRNumType a_type) +{ + CRNum *result = NULL; + + result = cr_num_new (); + + g_return_val_if_fail (result, NULL); + + result->val = a_val; + result->type = a_type; + + return result; +} + +/** + * cr_num_to_string: + *@a_this: the current instance of #CRNum. + * + *Returns the newly built string representation + *of the current instance of #CRNum. The returned + *string is NULL terminated. The caller *must* + *free the returned string. + */ +guchar * +cr_num_to_string (CRNum const * a_this) +{ + gdouble test_val = 0.0; + + guchar *tmp_char1 = NULL, + *tmp_char2 = NULL, + *result = NULL; + + g_return_val_if_fail (a_this, NULL); + + test_val = a_this->val - (glong) a_this->val; + + if (!test_val) { + tmp_char1 = (guchar *) g_strdup_printf ("%ld", (glong) a_this->val); + } else { + tmp_char1 = (guchar *) g_new0 (char, G_ASCII_DTOSTR_BUF_SIZE + 1); + if (tmp_char1 != NULL) + g_ascii_dtostr ((gchar *) tmp_char1, G_ASCII_DTOSTR_BUF_SIZE, a_this->val); + } + + g_return_val_if_fail (tmp_char1, NULL); + + switch (a_this->type) { + case NUM_LENGTH_EM: + tmp_char2 = (guchar *) "em"; + break; + + case NUM_LENGTH_EX: + tmp_char2 = (guchar *) "ex"; + break; + + case NUM_LENGTH_PX: + tmp_char2 = (guchar *) "px"; + break; + + case NUM_LENGTH_IN: + tmp_char2 = (guchar *) "in"; + break; + + case NUM_LENGTH_CM: + tmp_char2 = (guchar *) "cm"; + break; + + case NUM_LENGTH_MM: + tmp_char2 = (guchar *) "mm"; + break; + + case NUM_LENGTH_PT: + tmp_char2 = (guchar *) "pt"; + break; + + case NUM_LENGTH_PC: + tmp_char2 = (guchar *) "pc"; + break; + + case NUM_ANGLE_DEG: + tmp_char2 = (guchar *) "deg"; + break; + + case NUM_ANGLE_RAD: + tmp_char2 = (guchar *) "rad"; + break; + + case NUM_ANGLE_GRAD: + tmp_char2 = (guchar *) "grad"; + break; + + case NUM_TIME_MS: + tmp_char2 = (guchar *) "ms"; + break; + + case NUM_TIME_S: + tmp_char2 = (guchar *) "s"; + break; + + case NUM_FREQ_HZ: + tmp_char2 = (guchar *) "Hz"; + break; + + case NUM_FREQ_KHZ: + tmp_char2 = (guchar *) "KHz"; + break; + + case NUM_PERCENTAGE: + tmp_char2 = (guchar *) "%"; + break; + case NUM_INHERIT: + tmp_char2 = (guchar *) "inherit"; + break ; + case NUM_AUTO: + tmp_char2 = (guchar *) "auto"; + break ; + case NUM_GENERIC: + tmp_char2 = NULL ; + break ; + default: + tmp_char2 = (guchar *) "unknown"; + break; + } + + if (tmp_char2) { + result = (guchar *) g_strconcat ((gchar *) tmp_char1, tmp_char2, NULL); + g_free (tmp_char1); + } else { + result = tmp_char1; + } + + return result; +} + +/** + * cr_num_copy: + *@a_src: the instance of #CRNum to copy. + *Must be non NULL. + *@a_dest: the destination of the copy. + *Must be non NULL + * + *Copies an instance of #CRNum. + * + *Returns CR_OK upon successful completion, an + *error code otherwise. + */ +enum CRStatus +cr_num_copy (CRNum * a_dest, CRNum const * a_src) +{ + g_return_val_if_fail (a_dest && a_src, CR_BAD_PARAM_ERROR); + + memcpy (a_dest, a_src, sizeof (CRNum)); + + return CR_OK; +} + +/** + * cr_num_dup: + *@a_this: the instance of #CRNum to duplicate. + * + *Duplicates an instance of #CRNum + * + *Returns the newly created (duplicated) instance of #CRNum. + *Must be freed by cr_num_destroy(). + */ +CRNum * +cr_num_dup (CRNum const * a_this) +{ + CRNum *result = NULL; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this, NULL); + + result = cr_num_new (); + g_return_val_if_fail (result, NULL); + + status = cr_num_copy (result, a_this); + g_return_val_if_fail (status == CR_OK, NULL); + + return result; +} + +/** + * cr_num_set: + *Sets an instance of #CRNum. + *@a_this: the current instance of #CRNum to be set. + *@a_val: the new numerical value to be hold by the current + *instance of #CRNum + *@a_type: the new type of #CRNum. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_num_set (CRNum * a_this, gdouble a_val, enum CRNumType a_type) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + a_this->val = a_val; + a_this->type = a_type; + + return CR_OK; +} + +/** + * cr_num_is_fixed_length: + * @a_this: the current instance of #CRNum . + * + *Tests if the current instance of #CRNum is a fixed + *length value or not. Typically a fixed length value + *is anything from NUM_LENGTH_EM to NUM_LENGTH_PC. + *See the definition of #CRNumType to see what we mean. + * + *Returns TRUE if the instance of #CRNum is a fixed length number, + *FALSE otherwise. + */ +gboolean +cr_num_is_fixed_length (CRNum const * a_this) +{ + gboolean result = FALSE; + + g_return_val_if_fail (a_this, FALSE); + + if (a_this->type >= NUM_LENGTH_EM + && a_this->type <= NUM_LENGTH_PC) { + result = TRUE ; + } + return result ; +} + +/** + * cr_num_destroy: + *@a_this: the this pointer of + *the current instance of #CRNum. + * + *The destructor of #CRNum. + */ +void +cr_num_destroy (CRNum * a_this) +{ + g_return_if_fail (a_this); + + g_free (a_this); +} diff --git a/src/st/croco/cr-num.h b/src/st/croco/cr-num.h new file mode 100644 index 0000000..2b73aaf --- /dev/null +++ b/src/st/croco/cr-num.h @@ -0,0 +1,127 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information + */ + + +/** + *@file + *The declaration + *of the #CRNum class. + */ + +#ifndef __CR_NUM_H__ +#define __CR_NUM_H__ + +#include <glib.h> +#include "cr-utils.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +/** + *@file + *The declaration of the #CRNum class. + * + */ + +/** + *The different types + *of numbers. + *Please, do not modify + *the declaration order of the enum + *members, unless you know + *what you are doing. + */ +enum CRNumType +{ + NUM_AUTO = 0, + NUM_GENERIC, + NUM_LENGTH_EM, + NUM_LENGTH_EX, + NUM_LENGTH_PX, + NUM_LENGTH_IN, + NUM_LENGTH_CM, + NUM_LENGTH_MM, + NUM_LENGTH_PT, + NUM_LENGTH_PC, + NUM_ANGLE_DEG, + NUM_ANGLE_RAD, + NUM_ANGLE_GRAD, + NUM_TIME_MS, + NUM_TIME_S, + NUM_FREQ_HZ, + NUM_FREQ_KHZ, + NUM_PERCENTAGE, + NUM_INHERIT, + NUM_UNKNOWN_TYPE, + NB_NUM_TYPE +} ; + + +/** + *An abstraction of a number (num) + *as defined in the css2 spec. + */ +typedef struct _CRNum CRNum ; + +/** + *An abstraction of a number (num) + *as defined in the css2 spec. + */ +struct _CRNum +{ + enum CRNumType type ; + gdouble val ; + CRParsingLocation location ; +} ; + +CRNum * +cr_num_new (void) ; + +CRNum * +cr_num_new_with_val (gdouble a_val, + enum CRNumType a_type) ; + +CRNum * +cr_num_dup (CRNum const *a_this) ; + +guchar * +cr_num_to_string (CRNum const *a_this) ; + +enum CRStatus +cr_num_copy (CRNum *a_dest, CRNum const *a_src) ; + +enum CRStatus +cr_num_set (CRNum *a_this, gdouble a_val, + enum CRNumType a_type) ; + +gboolean +cr_num_is_fixed_length (CRNum const *a_this) ; + +void +cr_num_destroy (CRNum *a_this) ; + + +G_END_DECLS + + +#endif /*__CR_NUM_H__*/ diff --git a/src/st/croco/cr-om-parser.c b/src/st/croco/cr-om-parser.c new file mode 100644 index 0000000..90f7106 --- /dev/null +++ b/src/st/croco/cr-om-parser.c @@ -0,0 +1,1142 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include <string.h> +#include "cr-utils.h" +#include "cr-om-parser.h" + +/** + *@CROMParser: + * + *The definition of the CSS Object Model Parser. + *This parser uses (and sits) the SAC api of libcroco defined + *in cr-parser.h and cr-doc-handler.h + */ + +struct _CROMParserPriv { + CRParser *parser; +}; + +#define PRIVATE(a_this) ((a_this)->priv) + +/* + *Forward declaration of a type defined later + *in this file. + */ +struct _ParsingContext; +typedef struct _ParsingContext ParsingContext; + +static ParsingContext *new_parsing_context (void); + +static void destroy_context (ParsingContext * a_ctxt); + +static void unrecoverable_error (CRDocHandler * a_this); + +static void error (CRDocHandler * a_this); + +static void property (CRDocHandler * a_this, + CRString * a_name, + CRTerm * a_expression, + gboolean a_important); + +static void end_selector (CRDocHandler * a_this, + CRSelector * a_selector_list); + +static void start_selector (CRDocHandler * a_this, + CRSelector * a_selector_list); + +static void start_font_face (CRDocHandler * a_this, + CRParsingLocation *a_location); + +static void end_font_face (CRDocHandler * a_this); + +static void end_document (CRDocHandler * a_this); + +static void start_document (CRDocHandler * a_this); + +static void charset (CRDocHandler * a_this, + CRString * a_charset, + CRParsingLocation *a_location); + +static void start_page (CRDocHandler * a_this, CRString * a_page, + CRString * a_pseudo_page, + CRParsingLocation *a_location); + +static void end_page (CRDocHandler * a_this, CRString * a_page, + CRString * a_pseudo_page); + +static void start_media (CRDocHandler * a_this, + GList * a_media_list, + CRParsingLocation *a_location); + +static void end_media (CRDocHandler * a_this, + GList * a_media_list); + +static void import_style (CRDocHandler * a_this, + GList * a_media_list, + CRString * a_uri, + CRString * a_uri_default_ns, + CRParsingLocation *a_location); + +struct _ParsingContext { + CRStyleSheet *stylesheet; + CRStatement *cur_stmt; + CRStatement *cur_media_stmt; +}; + +/******************************************** + *Private methods + ********************************************/ + +static ParsingContext * +new_parsing_context (void) +{ + ParsingContext *result = NULL; + + result = g_try_malloc (sizeof (ParsingContext)); + if (!result) { + cr_utils_trace_info ("Out of Memory"); + return NULL; + } + memset (result, 0, sizeof (ParsingContext)); + return result; +} + +static void +destroy_context (ParsingContext * a_ctxt) +{ + g_return_if_fail (a_ctxt); + + if (a_ctxt->stylesheet) { + cr_stylesheet_destroy (a_ctxt->stylesheet); + a_ctxt->stylesheet = NULL; + } + if (a_ctxt->cur_stmt) { + cr_statement_destroy (a_ctxt->cur_stmt); + a_ctxt->cur_stmt = NULL; + } + g_free (a_ctxt); +} + +static enum CRStatus +cr_om_parser_init_default_sac_handler (CROMParser * a_this) +{ + CRDocHandler *sac_handler = NULL; + gboolean created_handler = FALSE; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->parser, + CR_BAD_PARAM_ERROR); + + status = cr_parser_get_sac_handler (PRIVATE (a_this)->parser, + &sac_handler); + g_return_val_if_fail (status == CR_OK, status); + + if (!sac_handler) { + sac_handler = cr_doc_handler_new (); + created_handler = TRUE; + } + + /* + *initialize here the sac handler. + */ + sac_handler->start_document = start_document; + sac_handler->end_document = end_document; + sac_handler->start_selector = start_selector; + sac_handler->end_selector = end_selector; + sac_handler->property = property; + sac_handler->start_font_face = start_font_face; + sac_handler->end_font_face = end_font_face; + sac_handler->error = error; + sac_handler->unrecoverable_error = unrecoverable_error; + sac_handler->charset = charset; + sac_handler->start_page = start_page; + sac_handler->end_page = end_page; + sac_handler->start_media = start_media; + sac_handler->end_media = end_media; + sac_handler->import_style = import_style; + + if (created_handler) { + status = cr_parser_set_sac_handler (PRIVATE (a_this)->parser, + sac_handler); + cr_doc_handler_unref (sac_handler); + } + + return status; + +} + +static void +start_document (CRDocHandler * a_this) +{ + ParsingContext *ctxt = NULL; + CRStyleSheet *stylesheet = NULL; + + g_return_if_fail (a_this); + + ctxt = new_parsing_context (); + g_return_if_fail (ctxt); + + stylesheet = cr_stylesheet_new (NULL); + ctxt->stylesheet = stylesheet; + cr_doc_handler_set_ctxt (a_this, ctxt); +} + +static void +start_font_face (CRDocHandler * a_this, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + g_return_if_fail (ctxt->cur_stmt == NULL); + + ctxt->cur_stmt = + cr_statement_new_at_font_face_rule (ctxt->stylesheet, NULL); + + g_return_if_fail (ctxt->cur_stmt); +} + +static void +end_font_face (CRDocHandler * a_this) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + CRStatement *stmts = NULL; + + g_return_if_fail (a_this); + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + g_return_if_fail + (ctxt->cur_stmt + && ctxt->cur_stmt->type == AT_FONT_FACE_RULE_STMT + && ctxt->stylesheet); + + stmts = cr_statement_append (ctxt->stylesheet->statements, + ctxt->cur_stmt); + if (!stmts) + goto error; + + ctxt->stylesheet->statements = stmts; + stmts = NULL; + ctxt->cur_stmt = NULL; + + return; + + error: + + if (ctxt->cur_stmt) { + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + } + + if (!stmts) { + cr_statement_destroy (stmts); + stmts = NULL; + } +} + +static void +end_document (CRDocHandler * a_this) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + + if (!ctxt->stylesheet || ctxt->cur_stmt) + goto error; + + status = cr_doc_handler_set_result (a_this, ctxt->stylesheet); + g_return_if_fail (status == CR_OK); + + ctxt->stylesheet = NULL; + destroy_context (ctxt); + cr_doc_handler_set_ctxt (a_this, NULL); + + return; + + error: + if (ctxt) { + destroy_context (ctxt); + } +} + +static void +charset (CRDocHandler * a_this, CRString * a_charset, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + CRStatement *stmt = NULL, + *stmt2 = NULL; + CRString *charset = NULL; + + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + g_return_if_fail (ctxt->stylesheet); + + charset = cr_string_dup (a_charset) ; + stmt = cr_statement_new_at_charset_rule (ctxt->stylesheet, charset); + g_return_if_fail (stmt); + stmt2 = cr_statement_append (ctxt->stylesheet->statements, stmt); + if (!stmt2) { + if (stmt) { + cr_statement_destroy (stmt); + stmt = NULL; + } + if (charset) { + cr_string_destroy (charset); + } + return; + } + ctxt->stylesheet->statements = stmt2; + stmt2 = NULL; +} + +static void +start_page (CRDocHandler * a_this, + CRString * a_page, + CRString * a_pseudo, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + g_return_if_fail (ctxt->cur_stmt == NULL); + + ctxt->cur_stmt = cr_statement_new_at_page_rule + (ctxt->stylesheet, NULL, NULL, NULL); + if (a_page) { + ctxt->cur_stmt->kind.page_rule->name = + cr_string_dup (a_page) ; + + if (!ctxt->cur_stmt->kind.page_rule->name) { + goto error; + } + } + if (a_pseudo) { + ctxt->cur_stmt->kind.page_rule->pseudo = + cr_string_dup (a_pseudo) ; + if (!ctxt->cur_stmt->kind.page_rule->pseudo) { + goto error; + } + } + return; + + error: + if (ctxt->cur_stmt) { + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + } +} + +static void +end_page (CRDocHandler * a_this, + CRString * a_page, + CRString * a_pseudo_page) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + CRStatement *stmt = NULL; + + (void) a_page; + (void) a_pseudo_page; + + g_return_if_fail (a_this); + + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + + g_return_if_fail (status == CR_OK && ctxt); + + g_return_if_fail (ctxt->cur_stmt + && ctxt->cur_stmt->type == AT_PAGE_RULE_STMT + && ctxt->stylesheet); + + stmt = cr_statement_append (ctxt->stylesheet->statements, + ctxt->cur_stmt); + + if (stmt) { + ctxt->stylesheet->statements = stmt; + stmt = NULL; + ctxt->cur_stmt = NULL; + } + + if (ctxt->cur_stmt) { + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + } + a_page = NULL; /*keep compiler happy */ + a_pseudo_page = NULL; /*keep compiler happy */ +} + +static void +start_media (CRDocHandler * a_this, + GList * a_media_list, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + GList *media_list = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + + g_return_if_fail (ctxt + && ctxt->cur_stmt == NULL + && ctxt->cur_media_stmt == NULL + && ctxt->stylesheet); + if (a_media_list) { + /*duplicate the media_list */ + media_list = cr_utils_dup_glist_of_cr_string + (a_media_list); + } + ctxt->cur_media_stmt = + cr_statement_new_at_media_rule + (ctxt->stylesheet, NULL, media_list); + +} + +static void +end_media (CRDocHandler * a_this, GList * a_media_list) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + CRStatement *stmts = NULL; + + (void) a_media_list; + + g_return_if_fail (a_this); + + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + + g_return_if_fail (status == CR_OK && ctxt); + + g_return_if_fail (ctxt + && ctxt->cur_media_stmt + && ctxt->cur_media_stmt->type == AT_MEDIA_RULE_STMT + && ctxt->stylesheet); + + stmts = cr_statement_append (ctxt->stylesheet->statements, + ctxt->cur_media_stmt); + + if (!stmts) { + cr_statement_destroy (ctxt->cur_media_stmt); + ctxt->cur_media_stmt = NULL; + } + + ctxt->stylesheet->statements = stmts; + stmts = NULL; + + ctxt->cur_stmt = NULL ; + ctxt->cur_media_stmt = NULL ; + a_media_list = NULL; +} + +static void +import_style (CRDocHandler * a_this, + GList * a_media_list, + CRString * a_uri, + CRString * a_uri_default_ns, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + CRString *uri = NULL; + CRStatement *stmt = NULL, + *stmt2 = NULL; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + GList *media_list = NULL ; + + (void) a_uri_default_ns; + + g_return_if_fail (a_this); + + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + + g_return_if_fail (status == CR_OK && ctxt); + + g_return_if_fail (ctxt->stylesheet); + + uri = cr_string_dup (a_uri) ; + + if (a_media_list) + media_list = cr_utils_dup_glist_of_cr_string (a_media_list) ; + + stmt = cr_statement_new_at_import_rule + (ctxt->stylesheet, uri, media_list, NULL); + + if (!stmt) + goto error; + + if (ctxt->cur_stmt) { + stmt2 = cr_statement_append (ctxt->cur_stmt, stmt); + if (!stmt2) + goto error; + ctxt->cur_stmt = stmt2; + stmt2 = NULL; + stmt = NULL; + } else { + stmt2 = cr_statement_append (ctxt->stylesheet->statements, + stmt); + if (!stmt2) + goto error; + ctxt->stylesheet->statements = stmt2; + stmt2 = NULL; + stmt = NULL; + } + + return; + + error: + if (uri) { + cr_string_destroy (uri); + } + + if (stmt) { + cr_statement_destroy (stmt); + stmt = NULL; + } + a_uri_default_ns = NULL; /*keep compiler happy */ +} + +static void +start_selector (CRDocHandler * a_this, CRSelector * a_selector_list) +{ + enum CRStatus status = CR_OK ; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + if (ctxt->cur_stmt) { + /*hmm, this should be NULL so free it */ + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + } + + ctxt->cur_stmt = cr_statement_new_ruleset + (ctxt->stylesheet, a_selector_list, NULL, NULL); +} + +static void +end_selector (CRDocHandler * a_this, CRSelector * a_selector_list) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + (void) a_selector_list; + + g_return_if_fail (a_this); + + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + + g_return_if_fail (status == CR_OK && ctxt); + + g_return_if_fail (ctxt->cur_stmt && ctxt->stylesheet); + + if (ctxt->cur_stmt) { + CRStatement *stmts = NULL; + + if (ctxt->cur_media_stmt) { + CRAtMediaRule *media_rule = NULL; + + media_rule = ctxt->cur_media_stmt->kind.media_rule; + + stmts = cr_statement_append + (media_rule->rulesets, ctxt->cur_stmt); + + if (!stmts) { + cr_utils_trace_info + ("Could not append a new statement"); + cr_statement_destroy (media_rule->rulesets); + ctxt->cur_media_stmt-> + kind.media_rule->rulesets = NULL; + return; + } + media_rule->rulesets = stmts; + ctxt->cur_stmt = NULL; + } else { + stmts = cr_statement_append + (ctxt->stylesheet->statements, + ctxt->cur_stmt); + if (!stmts) { + cr_utils_trace_info + ("Could not append a new statement"); + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + return; + } + ctxt->stylesheet->statements = stmts; + ctxt->cur_stmt = NULL; + } + + } + + a_selector_list = NULL; /*keep compiler happy */ +} + +static void +property (CRDocHandler * a_this, + CRString * a_name, + CRTerm * a_expression, + gboolean a_important) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + CRDeclaration *decl = NULL, + *decl2 = NULL; + CRString *str = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + + /* + *make sure a current ruleset statement has been allocated + *already. + */ + g_return_if_fail + (ctxt->cur_stmt + && + (ctxt->cur_stmt->type == RULESET_STMT + || ctxt->cur_stmt->type == AT_FONT_FACE_RULE_STMT + || ctxt->cur_stmt->type == AT_PAGE_RULE_STMT)); + + if (a_name) { + str = cr_string_dup (a_name); + g_return_if_fail (str); + } + + /*instantiates a new declaration */ + decl = cr_declaration_new (ctxt->cur_stmt, str, a_expression); + g_return_if_fail (decl); + str = NULL; + decl->important = a_important; + /* + *add the new declaration to the current statement + *being build. + */ + switch (ctxt->cur_stmt->type) { + case RULESET_STMT: + decl2 = cr_declaration_append + (ctxt->cur_stmt->kind.ruleset->decl_list, decl); + if (!decl2) { + cr_declaration_destroy (decl); + cr_utils_trace_info + ("Could not append decl to ruleset"); + goto error; + } + ctxt->cur_stmt->kind.ruleset->decl_list = decl2; + decl = NULL; + decl2 = NULL; + break; + + case AT_FONT_FACE_RULE_STMT: + decl2 = cr_declaration_append + (ctxt->cur_stmt->kind.font_face_rule->decl_list, + decl); + if (!decl2) { + cr_declaration_destroy (decl); + cr_utils_trace_info + ("Could not append decl to ruleset"); + goto error; + } + ctxt->cur_stmt->kind.font_face_rule->decl_list = decl2; + decl = NULL; + decl2 = NULL; + break; + case AT_PAGE_RULE_STMT: + decl2 = cr_declaration_append + (ctxt->cur_stmt->kind.page_rule->decl_list, decl); + if (!decl2) { + cr_declaration_destroy (decl); + cr_utils_trace_info + ("Could not append decl to ruleset"); + goto error; + } + ctxt->cur_stmt->kind.page_rule->decl_list = decl2; + decl = NULL; + decl2 = NULL; + break; + + default: + goto error; + break; + } + + return; + + error: + if (str) { + g_free (str); + str = NULL; + } + + if (decl) { + cr_declaration_destroy (decl); + decl = NULL; + } +} + +static void +error (CRDocHandler * a_this) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + g_return_if_fail (a_this); + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK && ctxt); + + if (ctxt->cur_stmt) { + cr_statement_destroy (ctxt->cur_stmt); + ctxt->cur_stmt = NULL; + } +} + +static void +unrecoverable_error (CRDocHandler * a_this) +{ + enum CRStatus status = CR_OK; + ParsingContext *ctxt = NULL; + ParsingContext **ctxtptr = NULL; + + ctxtptr = &ctxt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) ctxtptr); + g_return_if_fail (status == CR_OK); + + if (ctxt) { + if (ctxt->stylesheet) { + status = cr_doc_handler_set_result + (a_this, ctxt->stylesheet); + g_return_if_fail (status == CR_OK); + } + g_free (ctxt); + cr_doc_handler_set_ctxt (a_this, NULL); + } +} + +/******************************************** + *Public methods + ********************************************/ + +/** + * cr_om_parser_new: + *@a_input: the input stream. + * + *Constructor of the CROMParser. + *Returns the newly built instance of #CROMParser. + */ +CROMParser * +cr_om_parser_new (CRInput * a_input) +{ + CROMParser *result = NULL; + enum CRStatus status = CR_OK; + + result = g_try_malloc (sizeof (CROMParser)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CROMParser)); + PRIVATE (result) = g_try_malloc (sizeof (CROMParserPriv)); + + if (!PRIVATE (result)) { + cr_utils_trace_info ("Out of memory"); + goto error; + } + + memset (PRIVATE (result), 0, sizeof (CROMParserPriv)); + + PRIVATE (result)->parser = cr_parser_new_from_input (a_input); + + if (!PRIVATE (result)->parser) { + cr_utils_trace_info ("parsing instantiation failed"); + goto error; + } + + status = cr_om_parser_init_default_sac_handler (result); + + if (status != CR_OK) { + goto error; + } + + return result; + + error: + + if (result) { + cr_om_parser_destroy (result); + } + + return NULL; +} + +/** + * cr_om_parser_parse_buf: + *@a_this: the current instance of #CROMParser. + *@a_buf: the in memory buffer to parse. + *@a_len: the length of the in memory buffer in number of bytes. + *@a_enc: the encoding of the in memory buffer. + *@a_result: out parameter the resulting style sheet + * + *Parses the content of an in memory buffer. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_om_parser_parse_buf (CROMParser * a_this, + const guchar * a_buf, + gulong a_len, + enum CREncoding a_enc, CRStyleSheet ** a_result) +{ + + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && a_result, CR_BAD_PARAM_ERROR); + + if (!PRIVATE (a_this)->parser) { + PRIVATE (a_this)->parser = cr_parser_new (NULL); + } + + status = cr_parser_parse_buf (PRIVATE (a_this)->parser, + a_buf, a_len, a_enc); + + if (status == CR_OK) { + CRStyleSheet *result = NULL; + CRStyleSheet **resultptr = NULL; + CRDocHandler *sac_handler = NULL; + + cr_parser_get_sac_handler (PRIVATE (a_this)->parser, + &sac_handler); + g_return_val_if_fail (sac_handler, CR_ERROR); + resultptr = &result; + status = cr_doc_handler_get_result (sac_handler, + (gpointer *) resultptr); + g_return_val_if_fail (status == CR_OK, status); + + if (result) + *a_result = result; + } + + return status; +} + +/** + * cr_om_parser_simply_parse_buf: + *@a_buf: the css2 in memory buffer. + *@a_len: the length of the in memory buffer. + *@a_enc: the encoding of the in memory buffer. + *@a_result: out parameter. The resulting css2 style sheet. + * + *The simpler way to parse an in memory css2 buffer. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_om_parser_simply_parse_buf (const guchar * a_buf, + gulong a_len, + enum CREncoding a_enc, + CRStyleSheet ** a_result) +{ + CROMParser *parser = NULL; + enum CRStatus status = CR_OK; + + parser = cr_om_parser_new (NULL); + if (!parser) { + cr_utils_trace_info ("Could not create om parser"); + cr_utils_trace_info ("System possibly out of memory"); + return CR_ERROR; + } + + status = cr_om_parser_parse_buf (parser, a_buf, a_len, + a_enc, a_result); + + if (parser) { + cr_om_parser_destroy (parser); + parser = NULL; + } + + return status; +} + +/** + * cr_om_parser_parse_file: + *@a_this: the current instance of the cssom parser. + *@a_file_uri: the uri of the file. + *(only local file paths are supported so far) + *@a_enc: the encoding of the file. + *@a_result: out parameter. A pointer + *the build css object model. + * + *Parses a css2 stylesheet contained + *in a file. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_om_parser_parse_file (CROMParser * a_this, + const guchar * a_file_uri, + enum CREncoding a_enc, CRStyleSheet ** a_result) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && a_file_uri && a_result, + CR_BAD_PARAM_ERROR); + + if (!PRIVATE (a_this)->parser) { + PRIVATE (a_this)->parser = cr_parser_new_from_file + (a_file_uri, a_enc); + } + + status = cr_parser_parse_file (PRIVATE (a_this)->parser, + a_file_uri, a_enc); + + if (status == CR_OK) { + CRStyleSheet *result = NULL; + CRStyleSheet **resultptr = NULL; + CRDocHandler *sac_handler = NULL; + + cr_parser_get_sac_handler (PRIVATE (a_this)->parser, + &sac_handler); + g_return_val_if_fail (sac_handler, CR_ERROR); + resultptr = &result; + status = cr_doc_handler_get_result + (sac_handler, (gpointer *) resultptr); + g_return_val_if_fail (status == CR_OK, status); + if (result) + *a_result = result; + } + + return status; +} + +/** + * cr_om_parser_simply_parse_file: + *@a_file_path: the css2 local file path. + *@a_enc: the file encoding. + *@a_result: out parameter. The returned css stylesheet. + *Must be freed by the caller using cr_stylesheet_destroy. + * + *The simpler method to parse a css2 file. + * + *Returns CR_OK upon successful completion, an error code otherwise. + *Note that this method uses cr_om_parser_parse_file() so both methods + *have the same return values. + */ +enum CRStatus +cr_om_parser_simply_parse_file (const guchar * a_file_path, + enum CREncoding a_enc, + CRStyleSheet ** a_result) +{ + CROMParser *parser = NULL; + enum CRStatus status = CR_OK; + + parser = cr_om_parser_new (NULL); + if (!parser) { + cr_utils_trace_info ("Could not allocate om parser"); + cr_utils_trace_info ("System may be out of memory"); + return CR_ERROR; + } + + status = cr_om_parser_parse_file (parser, a_file_path, + a_enc, a_result); + if (parser) { + cr_om_parser_destroy (parser); + parser = NULL; + } + + return status; +} + +/** + * cr_om_parser_parse_paths_to_cascade: + *@a_this: the current instance of #CROMParser + *@a_author_path: the path to the author stylesheet + *@a_user_path: the path to the user stylesheet + *@a_ua_path: the path to the User Agent stylesheet + *@a_encoding: the encoding of the sheets. + *@a_result: out parameter. The resulting cascade if the parsing + *was okay + * + *Parses three sheets located by their paths and build a cascade + * + *Returns CR_OK upon successful completion, an error code otherwise + */ +enum CRStatus +cr_om_parser_parse_paths_to_cascade (CROMParser * a_this, + const guchar * a_author_path, + const guchar * a_user_path, + const guchar * a_ua_path, + enum CREncoding a_encoding, + CRCascade ** a_result) +{ + enum CRStatus status = CR_OK; + + /*0->author sheet, 1->user sheet, 2->UA sheet */ + CRStyleSheet *sheets[3]; + guchar *paths[3]; + CRCascade *result = NULL; + gint i = 0; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + memset (sheets, 0, sizeof (CRStyleSheet*) * 3); + paths[0] = (guchar *) a_author_path; + paths[1] = (guchar *) a_user_path; + paths[2] = (guchar *) a_ua_path; + + for (i = 0; i < 3; i++) { + status = cr_om_parser_parse_file (a_this, paths[i], + a_encoding, &sheets[i]); + if (status != CR_OK) { + if (sheets[i]) { + cr_stylesheet_unref (sheets[i]); + sheets[i] = NULL; + } + continue; + } + } + result = cr_cascade_new (sheets[0], sheets[1], sheets[2]); + if (!result) { + for (i = 0; i < 3; i++) { + cr_stylesheet_unref (sheets[i]); + sheets[i] = 0; + } + return CR_ERROR; + } + *a_result = result; + return CR_OK; +} + +/** + * cr_om_parser_simply_parse_paths_to_cascade: + *@a_author_path: the path to the author stylesheet + *@a_user_path: the path to the user stylesheet + *@a_ua_path: the path to the User Agent stylesheet + *@a_encoding: the encoding of the sheets. + *@a_result: out parameter. The resulting cascade if the parsing + *was okay + * + *Parses three sheets located by their paths and build a cascade + * + *Returns CR_OK upon successful completion, an error code otherwise + */ +enum CRStatus +cr_om_parser_simply_parse_paths_to_cascade (const guchar * a_author_path, + const guchar * a_user_path, + const guchar * a_ua_path, + enum CREncoding a_encoding, + CRCascade ** a_result) +{ + enum CRStatus status = CR_OK; + CROMParser *parser = NULL; + + parser = cr_om_parser_new (NULL); + if (!parser) { + cr_utils_trace_info ("could not allocated om parser"); + cr_utils_trace_info ("System may be out of memory"); + return CR_ERROR; + } + status = cr_om_parser_parse_paths_to_cascade (parser, + a_author_path, + a_user_path, + a_ua_path, + a_encoding, a_result); + if (parser) { + cr_om_parser_destroy (parser); + parser = NULL; + } + return status; +} + +/** + * cr_om_parser_destroy: + *@a_this: the current instance of #CROMParser. + * + *Destructor of the #CROMParser. + */ +void +cr_om_parser_destroy (CROMParser * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + if (PRIVATE (a_this)->parser) { + cr_parser_destroy (PRIVATE (a_this)->parser); + PRIVATE (a_this)->parser = NULL; + } + + if (PRIVATE (a_this)) { + g_free (PRIVATE (a_this)); + PRIVATE (a_this) = NULL; + } + + if (a_this) { + g_free (a_this); + a_this = NULL; + } +} diff --git a/src/st/croco/cr-om-parser.h b/src/st/croco/cr-om-parser.h new file mode 100644 index 0000000..13d35b1 --- /dev/null +++ b/src/st/croco/cr-om-parser.h @@ -0,0 +1,98 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2003 Dodji Seketeli <dodji@seketeli.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +/* + *$Id$ + */ + +#ifndef __CR_OM_PARSER_H__ +#define __CR_OM_PARSER_H__ + +#include "cr-parser.h" +#include "cr-cascade.h" + + +/** + *@file + *The definition of the CSS Object Model Parser. + *This parser uses (and sits) the SAC api of libcroco defined + *in cr-parser.h and cr-doc-handler.h + */ + +G_BEGIN_DECLS + +typedef struct _CROMParser CROMParser ; +typedef struct _CROMParserPriv CROMParserPriv ; + +/** + *The Object model parser. + *Can parse a css file and build a css object model. + *This parser uses an instance of #CRParser and defines + *a set of SAC callbacks to build the Object Model. + */ +struct _CROMParser +{ + CROMParserPriv *priv ; +} ; + +CROMParser * cr_om_parser_new (CRInput *a_input) ; + + +enum CRStatus cr_om_parser_simply_parse_file (const guchar *a_file_path, + enum CREncoding a_enc, + CRStyleSheet **a_result) ; + +enum CRStatus cr_om_parser_parse_file (CROMParser *a_this, + const guchar *a_file_uri, + enum CREncoding a_enc, + CRStyleSheet **a_result) ; + +enum CRStatus cr_om_parser_simply_parse_buf (const guchar *a_buf, + gulong a_len, + enum CREncoding a_enc, + CRStyleSheet **a_result) ; + +enum CRStatus cr_om_parser_parse_buf (CROMParser *a_this, + const guchar *a_buf, + gulong a_len, + enum CREncoding a_enc, + CRStyleSheet **a_result) ; + +enum CRStatus cr_om_parser_parse_paths_to_cascade (CROMParser *a_this, + const guchar *a_author_path, + const guchar *a_user_path, + const guchar *a_ua_path, + enum CREncoding a_encoding, + CRCascade ** a_result) ; + +enum CRStatus cr_om_parser_simply_parse_paths_to_cascade (const guchar *a_author_path, + const guchar *a_user_path, + const guchar *a_ua_path, + enum CREncoding a_encoding, + CRCascade ** a_result) ; + +void cr_om_parser_destroy (CROMParser *a_this) ; + +G_END_DECLS + +#endif /*__CR_OM_PARSER_H__*/ diff --git a/src/st/croco/cr-parser.c b/src/st/croco/cr-parser.c new file mode 100644 index 0000000..d4f40cf --- /dev/null +++ b/src/st/croco/cr-parser.c @@ -0,0 +1,4539 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the + * GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +/** + *@CRParser: + * + *The definition of the #CRParser class. + */ + +#include "string.h" +#include "cr-parser.h" +#include "cr-num.h" +#include "cr-term.h" +#include "cr-simple-sel.h" +#include "cr-attr-sel.h" + +/* + *Random notes: + *CSS core syntax vs CSS level 2 syntax + *===================================== + * + *One must keep in mind + *that css UA must comply with two syntaxes. + * + *1/the specific syntax that defines the css language + *for a given level of specification (e.g css2 syntax + *defined in appendix D.1 of the css2 spec) + * + *2/the core (general) syntax that is there to allow + *UAs to parse style sheets written in levels of CSS that + *didn't exist at the time the UAs were created. + * + *the name of parsing functions (or methods) contained in this file + *follows the following scheme: cr_parser_parse_<production_name> (...) ; + *where <production_name> is the name + *of a production of the css2 language. + *When a given production is + *defined by the css2 level grammar *and* by the + *css core syntax, there will be two functions to parse that production: + *one will parse the production defined by the css2 level grammar and the + *other will parse the production defined by the css core grammar. + *The css2 level grammar related parsing function will be called: + *cr_parser_parse_<production_name> (...) ; + *Then css core grammar related parsing function will be called: + *cr_parser_parse_<production_name>_core (...) ; + * + *If a production is defined only by the css core grammar, then + *it will be named: + *cr_parser_parse_<production_name>_core (...) ; + */ + +typedef struct _CRParserError CRParserError; + +/** + *An abstraction of an error reported by by the + *parsing routines. + */ +struct _CRParserError { + guchar *msg; + enum CRStatus status; + glong line; + glong column; + glong byte_num; +}; + +enum CRParserState { + READY_STATE = 0, + TRY_PARSE_CHARSET_STATE, + CHARSET_PARSED_STATE, + TRY_PARSE_IMPORT_STATE, + IMPORT_PARSED_STATE, + TRY_PARSE_RULESET_STATE, + RULESET_PARSED_STATE, + TRY_PARSE_MEDIA_STATE, + MEDIA_PARSED_STATE, + TRY_PARSE_PAGE_STATE, + PAGE_PARSED_STATE, + TRY_PARSE_FONT_FACE_STATE, + FONT_FACE_PARSED_STATE +} ; + +/** + *The private attributes of + *#CRParser. + */ +struct _CRParserPriv { + /** + *The tokenizer + */ + CRTknzr *tknzr; + + /** + *The sac handlers to call + *to notify the parsing of + *the css2 constructions. + */ + CRDocHandler *sac_handler; + + /** + *A stack of errors reported + *by the parsing routines. + *Contains instance of #CRParserError. + *This pointer is the top of the stack. + */ + GList *err_stack; + + enum CRParserState state; + gboolean resolve_import; + gboolean is_case_sensitive; + gboolean use_core_grammar; +}; + +#define PRIVATE(obj) ((obj)->priv) + +#define CHARS_TAB_SIZE 12 + +#define RECURSIVE_CALLERS_LIMIT 100 + +/** + * IS_NUM: + *@a_char: the char to test. + *return TRUE if the character is a number ([0-9]), FALSE otherwise + */ +#define IS_NUM(a_char) (((a_char) >= '0' && (a_char) <= '9')?TRUE:FALSE) + +/** + *Checks if 'status' equals CR_OK. If not, goto the 'error' label. + * + *@param status the status (of type enum CRStatus) to test. + *@param is_exception if set to FALSE, the final status returned + *by the current function will be CR_PARSING_ERROR. If set to TRUE, the + *current status will be the current value of the 'status' variable. + * + */ +#define CHECK_PARSING_STATUS(status, is_exception) \ +if ((status) != CR_OK) \ +{ \ + if (is_exception == FALSE) \ + { \ + status = CR_PARSING_ERROR ; \ + } \ + goto error ; \ +} + +/** + * CHECK_PARSING_STATUS_ERR: + *@a_this: the current instance of #CRParser . + *@a_status: the status to check. Is of type enum #CRStatus. + *@a_is_exception: in case of error, if is TRUE, the status + *is set to CR_PARSING_ERROR before goto error. If is false, the + *real low level status is kept and will be returned by the + *upper level function that called this macro. Usually,this must + *be set to FALSE. + * + *same as CHECK_PARSING_STATUS() but this one pushes an error + *on the parser error stack when an error arises. + * + */ +#define CHECK_PARSING_STATUS_ERR(a_this, a_status, a_is_exception,\ + a_err_msg, a_err_status) \ +if ((a_status) != CR_OK) \ +{ \ + if (a_is_exception == FALSE) a_status = CR_PARSING_ERROR ; \ + cr_parser_push_error (a_this, a_err_msg, a_err_status) ; \ + goto error ; \ +} + +/** + *Peeks the next char from the input stream of the current parser + *by invoking cr_tknzr_input_peek_char(). + *invokes CHECK_PARSING_STATUS on the status returned by + *cr_tknzr_peek_char(). + * + *@param a_this the current instance of #CRParser. + *@param a_to_char a pointer to the char where to store the + *char peeked. + */ +#define PEEK_NEXT_CHAR(a_this, a_to_char) \ +{\ +status = cr_tknzr_peek_char (PRIVATE (a_this)->tknzr, a_to_char) ; \ +CHECK_PARSING_STATUS (status, TRUE) \ +} + +/** + *Reads the next char from the input stream of the current parser. + *In case of error, jumps to the "error:" label located in the + *function where this macro is called. + *@param a_this the current instance of #CRParser + *@param to_char a pointer to the guint32 char where to store + *the character read. + */ +#define READ_NEXT_CHAR(a_this, a_to_char) \ +status = cr_tknzr_read_char (PRIVATE (a_this)->tknzr, a_to_char) ; \ +CHECK_PARSING_STATUS (status, TRUE) + +/** + *Gets information about the current position in + *the input of the parser. + *In case of failure, this macro returns from the + *calling function and + *returns a status code of type enum #CRStatus. + *@param a_this the current instance of #CRParser. + *@param a_pos out parameter. A pointer to the position + *inside the current parser input. Must + */ +#define RECORD_INITIAL_POS(a_this, a_pos) \ +status = cr_tknzr_get_cur_pos (PRIVATE \ +(a_this)->tknzr, a_pos) ; \ +g_return_val_if_fail (status == CR_OK, status) + +/** + *Gets the address of the current byte inside the + *parser input. + *@param parser the current instance of #CRParser. + *@param addr out parameter a pointer (guchar*) + *to where the address must be put. + */ +#define RECORD_CUR_BYTE_ADDR(a_this, a_addr) \ +status = cr_tknzr_get_cur_byte_addr \ + (PRIVATE (a_this)->tknzr, a_addr) ; \ +CHECK_PARSING_STATUS (status, TRUE) + +/** + *Peeks a byte from the topmost parser input at + *a given offset from the current position. + *If it fails, goto the "error:" label. + * + *@param a_parser the current instance of #CRParser. + *@param a_offset the offset of the byte to peek, the + *current byte having the offset '0'. + *@param a_byte_ptr out parameter a pointer (guchar*) to + *where the peeked char is to be stored. + */ +#define PEEK_BYTE(a_parser, a_offset, a_byte_ptr) \ +status = cr_tknzr_peek_byte (PRIVATE (a_this)->tknzr, \ + a_offset, \ + a_byte_ptr) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; + +#define BYTE(a_parser, a_offset, a_eof) \ +cr_tknzr_peek_byte2 (PRIVATE (a_this)->tknzr, a_offset, a_eof) + +/** + *Reads a byte from the topmost parser input + *steam. + *If it fails, goto the "error" label. + *@param a_this the current instance of #CRParser. + *@param a_byte_ptr the guchar * where to put the read char. + */ +#define READ_NEXT_BYTE(a_this, a_byte_ptr) \ +status = cr_tknzr_read_byte (PRIVATE (a_this)->tknzr, a_byte_ptr) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; + +/** + *Skips a given number of byte in the topmost + *parser input. Don't update line and column number. + *In case of error, jumps to the "error:" label + *of the surrounding function. + *@param a_parser the current instance of #CRParser. + *@param a_nb_bytes the number of bytes to skip. + */ +#define SKIP_BYTES(a_this, a_nb_bytes) \ +status = cr_tknzr_seek_index (PRIVATE (a_this)->tknzr, \ + CR_SEEK_CUR, a_nb_bytes) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; + +/** + *Skip utf8 encoded characters. + *Updates line and column numbers. + *@param a_parser the current instance of #CRParser. + *@param a_nb_chars the number of chars to skip. Must be of + *type glong. + */ +#define SKIP_CHARS(a_parser, a_nb_chars) \ +{ \ +glong nb_chars = a_nb_chars ; \ +status = cr_tknzr_consume_chars \ + (PRIVATE (a_parser)->tknzr,0, &nb_chars) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; \ +} + +/** + *Tests the condition and if it is false, sets + *status to "CR_PARSING_ERROR" and goto the 'error' + *label. + *@param condition the condition to test. + */ +#define ENSURE_PARSING_COND(condition) \ +if (! (condition)) {status = CR_PARSING_ERROR; goto error ;} + +#define ENSURE_PARSING_COND_ERR(a_this, a_condition, \ + a_err_msg, a_err_status) \ +if (! (a_condition)) \ +{ \ + status = CR_PARSING_ERROR; \ + cr_parser_push_error (a_this, a_err_msg, a_err_status) ; \ + goto error ; \ +} + +#define GET_NEXT_TOKEN(a_this, a_token_ptr) \ +status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, \ + a_token_ptr) ; \ +ENSURE_PARSING_COND (status == CR_OK) ; + +#ifdef WITH_UNICODE_ESCAPE_AND_RANGE +static enum CRStatus cr_parser_parse_unicode_escape (CRParser * a_this, + guint32 * a_unicode); +static enum CRStatus cr_parser_parse_escape (CRParser * a_this, + guint32 * a_esc_code); + +static enum CRStatus cr_parser_parse_unicode_range (CRParser * a_this, + CRString ** a_inf, + CRString ** a_sup); +#endif + +static enum CRStatus cr_parser_parse_stylesheet_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_atrule_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_ruleset_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_selector_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_declaration_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_any_core (CRParser * a_this, + guint n_calls); + +static enum CRStatus cr_parser_parse_block_core (CRParser * a_this, + guint n_calls); + +static enum CRStatus cr_parser_parse_value_core (CRParser * a_this); + +static enum CRStatus cr_parser_parse_string (CRParser * a_this, + CRString ** a_str); + +static enum CRStatus cr_parser_parse_ident (CRParser * a_this, + CRString ** a_str); + +static enum CRStatus cr_parser_parse_uri (CRParser * a_this, + CRString ** a_str); + +static enum CRStatus cr_parser_parse_function (CRParser * a_this, + CRString ** a_func_name, + CRTerm ** a_expr); +static enum CRStatus cr_parser_parse_property (CRParser * a_this, + CRString ** a_property); + +static enum CRStatus cr_parser_parse_attribute_selector (CRParser * a_this, + CRAttrSel ** a_sel); + +static enum CRStatus cr_parser_parse_simple_selector (CRParser * a_this, + CRSimpleSel ** a_sel); + +static enum CRStatus cr_parser_parse_simple_sels (CRParser * a_this, + CRSimpleSel ** a_sel); + +static CRParserError *cr_parser_error_new (const guchar * a_msg, + enum CRStatus); + +static void cr_parser_error_set_msg (CRParserError * a_this, + const guchar * a_msg); + +static void cr_parser_error_dump (CRParserError * a_this); + +static void cr_parser_error_set_status (CRParserError * a_this, + enum CRStatus a_status); + +static void cr_parser_error_set_pos (CRParserError * a_this, + glong a_line, + glong a_column, glong a_byte_num); +static void + cr_parser_error_destroy (CRParserError * a_this); + +static enum CRStatus cr_parser_push_error (CRParser * a_this, + const guchar * a_msg, + enum CRStatus a_status); + +static enum CRStatus cr_parser_dump_err_stack (CRParser * a_this, + gboolean a_clear_errs); +static enum CRStatus + cr_parser_clear_errors (CRParser * a_this); + +/***************************** + *error managemet methods + *****************************/ + +/** + *Constructor of #CRParserError class. + *@param a_msg the brute error message. + *@param a_status the error status. + *@return the newly built instance of #CRParserError. + */ +static CRParserError * +cr_parser_error_new (const guchar * a_msg, enum CRStatus a_status) +{ + CRParserError *result = NULL; + + result = g_try_malloc (sizeof (CRParserError)); + + if (result == NULL) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRParserError)); + + cr_parser_error_set_msg (result, a_msg); + cr_parser_error_set_status (result, a_status); + + return result; +} + +/** + *Sets the message associated to this instance of #CRError. + *@param a_this the current instance of #CRParserError. + *@param a_msg the new message. + */ +static void +cr_parser_error_set_msg (CRParserError * a_this, const guchar * a_msg) +{ + g_return_if_fail (a_this); + + if (a_this->msg) { + g_free (a_this->msg); + } + + a_this->msg = (guchar *) g_strdup ((const gchar *) a_msg); +} + +/** + *Sets the error status. + *@param a_this the current instance of #CRParserError. + *@param a_status the new error status. + * + */ +static void +cr_parser_error_set_status (CRParserError * a_this, enum CRStatus a_status) +{ + g_return_if_fail (a_this); + + a_this->status = a_status; +} + +/** + *Sets the position of the parser error. + *@param a_this the current instance of #CRParserError. + *@param a_line the line number. + *@param a_column the column number. + *@param a_byte_num the byte number. + */ +static void +cr_parser_error_set_pos (CRParserError * a_this, + glong a_line, glong a_column, glong a_byte_num) +{ + g_return_if_fail (a_this); + + a_this->line = a_line; + a_this->column = a_column; + a_this->byte_num = a_byte_num; +} + +static void +cr_parser_error_dump (CRParserError * a_this) +{ + g_return_if_fail (a_this); + + g_printerr ("parsing error: %ld:%ld:", a_this->line, a_this->column); + + g_printerr ("%s\n", a_this->msg); +} + +/** + *The destructor of #CRParserError. + *@param a_this the current instance of #CRParserError. + */ +static void +cr_parser_error_destroy (CRParserError * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->msg) { + g_free (a_this->msg); + a_this->msg = NULL; + } + + g_free (a_this); +} + +/** + *Pushes an error on the parser error stack. + *@param a_this the current instance of #CRParser. + *@param a_msg the error message. + *@param a_status the error status. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_push_error (CRParser * a_this, + const guchar * a_msg, enum CRStatus a_status) +{ + enum CRStatus status = CR_OK; + + CRParserError *error = NULL; + CRInputPos pos; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_msg, CR_BAD_PARAM_ERROR); + + error = cr_parser_error_new (a_msg, a_status); + + g_return_val_if_fail (error, CR_ERROR); + + RECORD_INITIAL_POS (a_this, &pos); + + cr_parser_error_set_pos + (error, pos.line, pos.col, pos.next_byte_index - 1); + + PRIVATE (a_this)->err_stack = + g_list_prepend (PRIVATE (a_this)->err_stack, error); + + if (PRIVATE (a_this)->err_stack == NULL) + goto error; + + return CR_OK; + + error: + + if (error) { + cr_parser_error_destroy (error); + error = NULL; + } + + return status; +} + +/** + *Dumps the error stack on stdout. + *@param a_this the current instance of #CRParser. + *@param a_clear_errs whether to clear the error stack + *after the dump or not. + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +static enum CRStatus +cr_parser_dump_err_stack (CRParser * a_this, gboolean a_clear_errs) +{ + GList *cur = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->err_stack == NULL) + return CR_OK; + + for (cur = PRIVATE (a_this)->err_stack; cur; cur = cur->next) { + cr_parser_error_dump ((CRParserError *) cur->data); + } + + if (a_clear_errs == TRUE) { + cr_parser_clear_errors (a_this); + } + + return CR_OK; +} + +/** + *Clears all the errors contained in the parser error stack. + *Frees all the errors, and the stack that contains'em. + *@param a_this the current instance of #CRParser. + */ +static enum CRStatus +cr_parser_clear_errors (CRParser * a_this) +{ + GList *cur = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + for (cur = PRIVATE (a_this)->err_stack; cur; cur = cur->next) { + if (cur->data) { + cr_parser_error_destroy ((CRParserError *) + cur->data); + } + } + + if (PRIVATE (a_this)->err_stack) { + g_list_free (PRIVATE (a_this)->err_stack); + PRIVATE (a_this)->err_stack = NULL; + } + + return CR_OK; +} + +/** + * cr_parser_try_to_skip_spaces_and_comments: + *@a_this: the current instance of #CRParser. + * + *Same as cr_parser_try_to_skip_spaces() but this one skips + *spaces and comments. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_try_to_skip_spaces_and_comments (CRParser * a_this) +{ + enum CRStatus status = CR_ERROR; + CRToken *token = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr, CR_BAD_PARAM_ERROR); + do { + if (token) { + cr_token_destroy (token); + token = NULL; + } + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + if (status != CR_OK) + goto error; + } + while ((token != NULL) + && (token->type == COMMENT_TK || token->type == S_TK)); + + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + + return status; + + error: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + return status; +} + +/*************************************** + *End of Parser input handling routines + ***************************************/ + + +/************************************* + *Non trivial terminal productions + *parsing routines + *************************************/ + +/** + *Parses a css stylesheet following the core css grammar. + *This is mainly done for test purposes. + *During the parsing, no callback is called. This is just + *to validate that the stylesheet is well formed according to the + *css core syntax. + *stylesheet : [ CDO | CDC | S | statement ]*; + *@param a_this the current instance of #CRParser. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_stylesheet_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + continue_parsing: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + if (status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + goto done; + } else if (status != CR_OK) { + goto error; + } + + switch (token->type) { + + case CDO_TK: + case CDC_TK: + goto continue_parsing; + break; + default: + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + status = cr_parser_parse_statement_core (a_this); + cr_parser_clear_errors (a_this); + if (status == CR_OK) { + goto continue_parsing; + } else if (status == CR_END_OF_INPUT_ERROR) { + goto done; + } else { + goto error; + } + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + cr_parser_push_error + (a_this, (const guchar *) "could not recognize next production", CR_ERROR); + + cr_parser_dump_err_stack (a_this, TRUE); + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses an at-rule as defined by the css core grammar + *in chapter 4.1 in the css2 spec. + *at-rule : ATKEYWORD S* any* [ block | ';' S* ]; + *@param a_this the current instance of #CRParser. + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +static enum CRStatus +cr_parser_parse_atrule_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + ENSURE_PARSING_COND (status == CR_OK + && token + && + (token->type == ATKEYWORD_TK + || token->type == IMPORT_SYM_TK + || token->type == PAGE_SYM_TK + || token->type == MEDIA_SYM_TK + || token->type == FONT_FACE_SYM_TK + || token->type == CHARSET_SYM_TK)); + + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + do { + status = cr_parser_parse_any_core (a_this, 0); + } while (status == CR_OK); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == CBO_TK) { + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + status = cr_parser_parse_block_core (a_this, 0); + CHECK_PARSING_STATUS (status, + FALSE); + goto done; + } else if (token->type == SEMICOLON_TK) { + goto done; + } else { + status = CR_PARSING_ERROR ; + goto error; + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, + &init_pos); + return status; +} + +/** + *Parses a ruleset as defined by the css core grammar in chapter + *4.1 of the css2 spec. + *ruleset ::= selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*; + *@param a_this the current instance of #CRParser. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_ruleset_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_selector_core (a_this); + + ENSURE_PARSING_COND (status == CR_OK + || status == CR_PARSING_ERROR + || status == CR_END_OF_INPUT_ERROR); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == CBO_TK); + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_declaration_core (a_this); + + parse_declaration_list: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + if (token->type == CBC_TK) { + goto done; + } + + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == SEMICOLON_TK); + + cr_token_destroy (token); + token = NULL; + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_declaration_core (a_this); + cr_parser_clear_errors (a_this); + ENSURE_PARSING_COND (status == CR_OK || status == CR_PARSING_ERROR); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + if (token->type == CBC_TK) { + cr_token_destroy (token); + token = NULL; + cr_parser_try_to_skip_spaces_and_comments (a_this); + goto done; + } else { + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + goto parse_declaration_list; + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (status == CR_OK) { + return CR_OK; + } + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses a "selector" as specified by the css core + *grammar. + *selector : any+; + *@param a_this the current instance of #CRParser. + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +static enum CRStatus +cr_parser_parse_selector_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_any_core (a_this, 0); + CHECK_PARSING_STATUS (status, FALSE); + + do { + status = cr_parser_parse_any_core (a_this, 0); + + } while (status == CR_OK); + + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses a "block" as defined in the css core grammar + *in chapter 4.1 of the css2 spec. + *block ::= '{' S* [ any | block | ATKEYWORD S* | ';' ]* '}' S*; + *@param a_this the current instance of #CRParser. + *@param n_calls used to limit recursion depth + *FIXME: code this function. + */ +static enum CRStatus +cr_parser_parse_block_core (CRParser * a_this, + guint n_calls) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + if (n_calls > RECURSIVE_CALLERS_LIMIT) + return CR_ERROR; + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == CBO_TK); + + parse_block_content: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == CBC_TK) { + cr_parser_try_to_skip_spaces_and_comments (a_this); + goto done; + } else if (token->type == SEMICOLON_TK) { + goto parse_block_content; + } else if (token->type == ATKEYWORD_TK) { + cr_parser_try_to_skip_spaces_and_comments (a_this); + goto parse_block_content; + } else if (token->type == CBO_TK) { + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + status = cr_parser_parse_block_core (a_this, n_calls + 1); + CHECK_PARSING_STATUS (status, FALSE); + goto parse_block_content; + } else { + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + status = cr_parser_parse_any_core (a_this, n_calls + 1); + CHECK_PARSING_STATUS (status, FALSE); + goto parse_block_content; + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (status == CR_OK) + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +static enum CRStatus +cr_parser_parse_declaration_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + CRString *prop = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_property (a_this, &prop); + CHECK_PARSING_STATUS (status, FALSE); + cr_parser_clear_errors (a_this); + ENSURE_PARSING_COND (status == CR_OK && prop); + cr_string_destroy (prop); + prop = NULL; + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token + && token->type == DELIM_TK + && token->u.unichar == ':'); + cr_token_destroy (token); + token = NULL; + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_value_core (a_this); + CHECK_PARSING_STATUS (status, FALSE); + + return CR_OK; + + error: + + if (prop) { + cr_string_destroy (prop); + prop = NULL; + } + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses a "value" production as defined by the css core grammar + *in chapter 4.1. + *value ::= [ any | block | ATKEYWORD S* ]+; + *@param a_this the current instance of #CRParser. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_value_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + glong ref = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + RECORD_INITIAL_POS (a_this, &init_pos); + + continue_parsing: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + switch (token->type) { + case CBO_TK: + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + status = cr_parser_parse_block_core (a_this, 0); + CHECK_PARSING_STATUS (status, FALSE); + ref++; + goto continue_parsing; + + case ATKEYWORD_TK: + cr_parser_try_to_skip_spaces_and_comments (a_this); + ref++; + goto continue_parsing; + + default: + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + status = cr_parser_parse_any_core (a_this, 0); + if (status == CR_OK) { + ref++; + goto continue_parsing; + } else if (status == CR_PARSING_ERROR) { + status = CR_OK; + goto done; + } else { + goto error; + } + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (status == CR_OK && ref) + return CR_OK; + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses an "any" as defined by the css core grammar in the + *css2 spec in chapter 4.1. + *any ::= [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING + * | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES + * | FUNCTION | DASHMATCH | '(' any* ')' | '[' any* ']' ] S*; + * + *@param a_this the current instance of #CRParser. + *@param n_calls used to limit recursion depth + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_any_core (CRParser * a_this, + guint n_calls) +{ + CRToken *token1 = NULL, + *token2 = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + if (n_calls > RECURSIVE_CALLERS_LIMIT) + return CR_ERROR; + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token1); + + ENSURE_PARSING_COND (status == CR_OK && token1); + + switch (token1->type) { + case IDENT_TK: + case NUMBER_TK: + case RGB_TK: + case PERCENTAGE_TK: + case DIMEN_TK: + case EMS_TK: + case EXS_TK: + case LENGTH_TK: + case ANGLE_TK: + case FREQ_TK: + case TIME_TK: + case STRING_TK: + case DELIM_TK: + case URI_TK: + case HASH_TK: + case UNICODERANGE_TK: + case INCLUDES_TK: + case DASHMATCH_TK: + case S_TK: + case COMMENT_TK: + case IMPORTANT_SYM_TK: + status = CR_OK; + break; + case FUNCTION_TK: + /* + *this case isn't specified by the spec but it + *does happen. So we have to handle it. + *We must consider function with parameters. + *We consider parameter as being an "any*" production. + */ + do { + status = cr_parser_parse_any_core (a_this, n_calls + 1); + } while (status == CR_OK); + + ENSURE_PARSING_COND (status == CR_PARSING_ERROR); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token2); + ENSURE_PARSING_COND (status == CR_OK + && token2 && token2->type == PC_TK); + break; + case PO_TK: + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token2); + ENSURE_PARSING_COND (status == CR_OK && token2); + + if (token2->type == PC_TK) { + cr_token_destroy (token2); + token2 = NULL; + goto done; + } else { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token2); + token2 = NULL; + } + + do { + status = cr_parser_parse_any_core (a_this, n_calls + 1); + } while (status == CR_OK); + + ENSURE_PARSING_COND (status == CR_PARSING_ERROR); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token2); + ENSURE_PARSING_COND (status == CR_OK + && token2 && token2->type == PC_TK); + status = CR_OK; + break; + + case BO_TK: + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token2); + ENSURE_PARSING_COND (status == CR_OK && token2); + + if (token2->type == BC_TK) { + cr_token_destroy (token2); + token2 = NULL; + goto done; + } else { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token2); + token2 = NULL; + } + + do { + status = cr_parser_parse_any_core (a_this, n_calls + 1); + } while (status == CR_OK); + + ENSURE_PARSING_COND (status == CR_PARSING_ERROR); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token2); + ENSURE_PARSING_COND (status == CR_OK + && token2 && token2->type == BC_TK); + status = CR_OK; + break; + default: + status = CR_PARSING_ERROR; + goto error; + } + + done: + if (token1) { + cr_token_destroy (token1); + token1 = NULL; + } + + if (token2) { + cr_token_destroy (token2); + token2 = NULL; + } + + return CR_OK; + + error: + + if (token1) { + cr_token_destroy (token1); + token1 = NULL; + } + + if (token2) { + cr_token_destroy (token2); + token2 = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + return status; +} + +/** + *Parses an attribute selector as defined in the css2 spec in + *appendix D.1: + *attrib ::= '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S* + * [ IDENT | STRING ] S* ]? ']' + * + *@param a_this the "this pointer" of the current instance of + *#CRParser . + *@param a_sel out parameter. The successfully parsed attribute selector. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_attribute_selector (CRParser * a_this, + CRAttrSel ** a_sel) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRToken *token = NULL; + CRAttrSel *result = NULL; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this && a_sel, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == BO_TK); + cr_parsing_location_copy + (&location, &token->location) ; + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + result = cr_attr_sel_new (); + if (!result) { + cr_utils_trace_info ("result failed") ; + status = CR_OUT_OF_MEMORY_ERROR ; + goto error ; + } + cr_parsing_location_copy (&result->location, + &location) ; + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == IDENT_TK); + + result->name = token->u.str; + token->u.str = NULL; + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == INCLUDES_TK) { + result->match_way = INCLUDES; + goto parse_right_part; + } else if (token->type == DASHMATCH_TK) { + result->match_way = DASHMATCH; + goto parse_right_part; + } else if (token->type == DELIM_TK && token->u.unichar == '=') { + result->match_way = EQUALS; + goto parse_right_part; + } else if (token->type == BC_TK) { + result->match_way = SET; + goto done; + } + + parse_right_part: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == IDENT_TK) { + result->value = token->u.str; + token->u.str = NULL; + } else if (token->type == STRING_TK) { + result->value = token->u.str; + token->u.str = NULL; + } else { + status = CR_PARSING_ERROR; + goto error; + } + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == BC_TK); + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (*a_sel) { + status = cr_attr_sel_append_attr_sel (*a_sel, result); + CHECK_PARSING_STATUS (status, FALSE); + } else { + *a_sel = result; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + if (result) { + cr_attr_sel_destroy (result); + result = NULL; + } + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + *Parses a "property" as specified by the css2 spec at [4.1.1]: + *property : IDENT S*; + * + *@param a_this the "this pointer" of the current instance of #CRParser. + *@param GString a_property out parameter. The parsed property without the + *trailing spaces. If *a_property is NULL, this function allocates a + *new instance of GString and set it content to the parsed property. + *If not, the property is just appended to a_property's previous content. + *In both cases, it is up to the caller to free a_property. + *@return CR_OK upon successful completion, CR_PARSING_ERROR if the + *next construction was not a "property", or an error code. + */ +static enum CRStatus +cr_parser_parse_property (CRParser * a_this, + CRString ** a_property) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr + && a_property, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_ident (a_this, a_property); + CHECK_PARSING_STATUS (status, TRUE); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_term: + *@a_term: out parameter. The successfully parsed term. + * + *Parses a "term" as defined in the css2 spec, appendix D.1: + *term ::= unary_operator? [NUMBER S* | PERCENTAGE S* | LENGTH S* | + *EMS S* | EXS S* | ANGLE S* | TIME S* | FREQ S* | function ] | + *STRING S* | IDENT S* | URI S* | RGB S* | UNICODERANGE S* | hexcolor + * + *TODO: handle parsing of 'RGB' + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_term (CRParser * a_this, CRTerm ** a_term) +{ + enum CRStatus status = CR_PARSING_ERROR; + CRInputPos init_pos; + CRTerm *result = NULL; + CRTerm *param = NULL; + CRToken *token = NULL; + CRString *func_name = NULL; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this && a_term, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + result = cr_term_new (); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + if (status != CR_OK || !token) + goto error; + + cr_parsing_location_copy (&location, &token->location) ; + if (token->type == DELIM_TK && token->u.unichar == '+') { + result->unary_op = PLUS_UOP; + cr_token_destroy (token) ; + token = NULL ; + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + if (status != CR_OK || !token) + goto error; + } else if (token->type == DELIM_TK && token->u.unichar == '-') { + result->unary_op = MINUS_UOP; + cr_token_destroy (token) ; + token = NULL ; + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + if (status != CR_OK || !token) + goto error; + } + + if (token->type == EMS_TK + || token->type == EXS_TK + || token->type == LENGTH_TK + || token->type == ANGLE_TK + || token->type == TIME_TK + || token->type == FREQ_TK + || token->type == PERCENTAGE_TK + || token->type == NUMBER_TK) { + status = cr_term_set_number (result, token->u.num); + CHECK_PARSING_STATUS (status, TRUE); + token->u.num = NULL; + status = CR_OK; + } else if (token && token->type == FUNCTION_TK) { + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + status = cr_parser_parse_function (a_this, &func_name, + ¶m); + + if (status == CR_OK) { + status = cr_term_set_function (result, + func_name, + param); + CHECK_PARSING_STATUS (status, TRUE); + } + } else if (token && token->type == STRING_TK) { + status = cr_term_set_string (result, + token->u.str); + CHECK_PARSING_STATUS (status, TRUE); + token->u.str = NULL; + } else if (token && token->type == IDENT_TK) { + status = cr_term_set_ident (result, token->u.str); + CHECK_PARSING_STATUS (status, TRUE); + token->u.str = NULL; + } else if (token && token->type == URI_TK) { + status = cr_term_set_uri (result, token->u.str); + CHECK_PARSING_STATUS (status, TRUE); + token->u.str = NULL; + } else if (token && token->type == RGB_TK) { + status = cr_term_set_rgb (result, token->u.rgb); + CHECK_PARSING_STATUS (status, TRUE); + token->u.rgb = NULL; + } else if (token && token->type == UNICODERANGE_TK) { + result->type = TERM_UNICODERANGE; + status = CR_PARSING_ERROR; + } else if (token && token->type == HASH_TK) { + status = cr_term_set_hash (result, token->u.str); + CHECK_PARSING_STATUS (status, TRUE); + token->u.str = NULL; + } else { + status = CR_PARSING_ERROR; + } + + if (status != CR_OK) { + goto error; + } + cr_parsing_location_copy (&result->location, + &location) ; + *a_term = cr_term_append_term (*a_term, result); + + result = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + if (result) { + cr_term_destroy (result); + result = NULL; + } + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (param) { + cr_term_destroy (param); + param = NULL; + } + + if (func_name) { + cr_string_destroy (func_name); + func_name = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_simple_selector: + *@a_this: the "this pointer" of the current instance of #CRParser. + *@a_sel: out parameter. Is set to the successfully parsed simple + *selector. + * + *Parses a "simple_selector" as defined by the css2 spec in appendix D.1 : + *element_name? [ HASH | class | attrib | pseudo ]* S* + *and where pseudo is: + *pseudo ::= ':' [ IDENT | FUNCTION S* IDENT S* ')' ] + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_simple_selector (CRParser * a_this, CRSimpleSel ** a_sel) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + CRToken *token = NULL; + CRSimpleSel *sel = NULL; + CRAdditionalSel *add_sel_list = NULL; + gboolean found_sel = FALSE; + guint32 cur_char = 0; + + g_return_val_if_fail (a_this && a_sel, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + if (status != CR_OK) + goto error; + + sel = cr_simple_sel_new (); + ENSURE_PARSING_COND (sel); + + cr_parsing_location_copy + (&sel->location, + &token->location) ; + + if (token && token->type == DELIM_TK + && token->u.unichar == '*') { + sel->type_mask |= UNIVERSAL_SELECTOR; + sel->name = cr_string_new_from_string ("*"); + found_sel = TRUE; + } else if (token && token->type == IDENT_TK) { + sel->name = token->u.str; + sel->type_mask |= TYPE_SELECTOR; + token->u.str = NULL; + found_sel = TRUE; + } else { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, + token); + token = NULL; + } + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + for (;;) { + if (token) { + cr_token_destroy (token); + token = NULL; + } + + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, + &token); + if (status != CR_OK) + goto error; + + if (token && token->type == HASH_TK) { + /*we parsed an attribute id */ + CRAdditionalSel *add_sel = NULL; + + add_sel = cr_additional_sel_new_with_type + (ID_ADD_SELECTOR); + + add_sel->content.id_name = token->u.str; + token->u.str = NULL; + + cr_parsing_location_copy + (&add_sel->location, + &token->location) ; + add_sel_list = + cr_additional_sel_append + (add_sel_list, add_sel); + found_sel = TRUE; + } else if (token && (token->type == DELIM_TK) + && (token->u.unichar == '.')) { + cr_token_destroy (token); + token = NULL; + + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + if (status != CR_OK) + goto error; + + if (token && token->type == IDENT_TK) { + CRAdditionalSel *add_sel = NULL; + + add_sel = cr_additional_sel_new_with_type + (CLASS_ADD_SELECTOR); + + add_sel->content.class_name = token->u.str; + token->u.str = NULL; + + add_sel_list = + cr_additional_sel_append + (add_sel_list, add_sel); + found_sel = TRUE; + + cr_parsing_location_copy + (&add_sel->location, + & token->location) ; + } else { + status = CR_PARSING_ERROR; + goto error; + } + } else if (token && token->type == BO_TK) { + CRAttrSel *attr_sel = NULL; + CRAdditionalSel *add_sel = NULL; + + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + if (status != CR_OK) + goto error; + token = NULL; + + status = cr_parser_parse_attribute_selector + (a_this, &attr_sel); + CHECK_PARSING_STATUS (status, FALSE); + + add_sel = cr_additional_sel_new_with_type + (ATTRIBUTE_ADD_SELECTOR); + + ENSURE_PARSING_COND (add_sel != NULL); + + add_sel->content.attr_sel = attr_sel; + + add_sel_list = + cr_additional_sel_append + (add_sel_list, add_sel); + found_sel = TRUE; + cr_parsing_location_copy + (&add_sel->location, + &attr_sel->location) ; + } else if (token && (token->type == DELIM_TK) + && (token->u.unichar == ':')) { + CRPseudo *pseudo = NULL; + + /*try to parse a pseudo */ + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + pseudo = cr_pseudo_new (); + + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + cr_parsing_location_copy + (&pseudo->location, + &token->location) ; + + if (token->type == IDENT_TK) { + pseudo->type = IDENT_PSEUDO; + pseudo->name = token->u.str; + token->u.str = NULL; + found_sel = TRUE; + } else if (token->type == FUNCTION_TK) { + pseudo->name = token->u.str; + token->u.str = NULL; + cr_parser_try_to_skip_spaces_and_comments + (a_this); + status = cr_parser_parse_ident + (a_this, &pseudo->extra); + + ENSURE_PARSING_COND (status == CR_OK); + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == ')'); + pseudo->type = FUNCTION_PSEUDO; + found_sel = TRUE; + } else { + status = CR_PARSING_ERROR; + goto error; + } + + if (status == CR_OK) { + CRAdditionalSel *add_sel = NULL; + + add_sel = cr_additional_sel_new_with_type + (PSEUDO_CLASS_ADD_SELECTOR); + + add_sel->content.pseudo = pseudo; + cr_parsing_location_copy + (&add_sel->location, + &pseudo->location) ; + add_sel_list = + cr_additional_sel_append + (add_sel_list, add_sel); + status = CR_OK; + } + } else { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + break; + } + } + + if (status == CR_OK && found_sel == TRUE) { + cr_parser_try_to_skip_spaces_and_comments (a_this); + + sel->add_sel = add_sel_list; + add_sel_list = NULL; + + if (*a_sel == NULL) { + *a_sel = sel; + } else { + cr_simple_sel_append_simple_sel (*a_sel, sel); + } + + sel = NULL; + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + } else { + status = CR_PARSING_ERROR; + } + + error: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (add_sel_list) { + cr_additional_sel_destroy (add_sel_list); + add_sel_list = NULL; + } + + if (sel) { + cr_simple_sel_destroy (sel); + sel = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; + +} + +/** + * cr_parser_parse_simple_sels: + *@a_this: the this pointer of the current instance of #CRParser. + *@a_start: a pointer to the + *first character of the successfully parsed + *string. + *@a_end: a pointer to the last character of the successfully parsed + *string. + * + *Parses a "selector" as defined by the css2 spec in appendix D.1: + *selector ::= simple_selector [ combinator simple_selector ]* + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_simple_sels (CRParser * a_this, + CRSimpleSel ** a_sel) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + CRSimpleSel *sel = NULL; + guint32 cur_char = 0; + + g_return_val_if_fail (a_this + && PRIVATE (a_this) + && a_sel, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_simple_selector (a_this, &sel); + CHECK_PARSING_STATUS (status, FALSE); + + *a_sel = cr_simple_sel_append_simple_sel (*a_sel, sel); + + for (;;) { + guint32 next_char = 0; + enum Combinator comb = 0; + + sel = NULL; + + PEEK_NEXT_CHAR (a_this, &next_char); + + if (next_char == '+') { + READ_NEXT_CHAR (a_this, &cur_char); + comb = COMB_PLUS; + cr_parser_try_to_skip_spaces_and_comments (a_this); + } else if (next_char == '>') { + READ_NEXT_CHAR (a_this, &cur_char); + comb = COMB_GT; + cr_parser_try_to_skip_spaces_and_comments (a_this); + } else { + comb = COMB_WS; + } + + status = cr_parser_parse_simple_selector (a_this, &sel); + if (status != CR_OK) + break; + + if (comb && sel) { + sel->combinator = comb; + comb = 0; + } + if (sel) { + *a_sel = cr_simple_sel_append_simple_sel (*a_sel, + sel) ; + } + } + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_selector: + *@a_this: the current instance of #CRParser. + *@a_selector: the parsed list of comma separated + *selectors. + * + *Parses a comma separated list of selectors. + * + *Returns CR_OK upon successful completion, an error + *code otherwise. + */ +static enum CRStatus +cr_parser_parse_selector (CRParser * a_this, + CRSelector ** a_selector) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + guint32 cur_char = 0, + next_char = 0; + CRSimpleSel *simple_sels = NULL; + CRSelector *selector = NULL; + + g_return_val_if_fail (a_this && a_selector, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_simple_sels (a_this, &simple_sels); + CHECK_PARSING_STATUS (status, FALSE); + + if (simple_sels) { + selector = cr_selector_append_simple_sel + (selector, simple_sels); + if (selector) { + cr_parsing_location_copy + (&selector->location, + &simple_sels->location) ; + } + simple_sels = NULL; + } else { + status = CR_PARSING_ERROR ; + goto error ; + } + + status = cr_tknzr_peek_char (PRIVATE (a_this)->tknzr, + &next_char); + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + goto okay; + } else { + goto error; + } + } + + if (next_char == ',') { + for (;;) { + simple_sels = NULL; + + status = cr_tknzr_peek_char (PRIVATE (a_this)->tknzr, + &next_char); + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + break; + } else { + goto error; + } + } + + if (next_char != ',') + break; + + /*consume the ',' char */ + READ_NEXT_CHAR (a_this, &cur_char); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_simple_sels + (a_this, &simple_sels); + + CHECK_PARSING_STATUS (status, FALSE); + + if (simple_sels) { + selector = + cr_selector_append_simple_sel + (selector, simple_sels); + + simple_sels = NULL; + } + } + } + + okay: + cr_parser_try_to_skip_spaces_and_comments (a_this); + + if (!*a_selector) { + *a_selector = selector; + } else { + *a_selector = cr_selector_append (*a_selector, selector); + } + + selector = NULL; + return CR_OK; + + error: + + if (simple_sels) { + cr_simple_sel_destroy (simple_sels); + simple_sels = NULL; + } + + if (selector) { + cr_selector_unref (selector); + selector = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_function: + *@a_this: the "this pointer" of the current instance of #CRParser. + * + *@a_func_name: out parameter. The parsed function name + *@a_expr: out parameter. The successfully parsed term. + * + *Parses a "function" as defined in css spec at appendix D.1: + *function ::= FUNCTION S* expr ')' S* + *FUNCTION ::= ident'(' + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_function (CRParser * a_this, + CRString ** a_func_name, + CRTerm ** a_expr) +{ + CRInputPos init_pos; + enum CRStatus status = CR_OK; + CRToken *token = NULL; + CRTerm *expr = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_func_name, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + if (status != CR_OK) + goto error; + + if (token && token->type == FUNCTION_TK) { + *a_func_name = token->u.str; + token->u.str = NULL; + } else { + status = CR_PARSING_ERROR; + goto error; + } + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this) ; + + status = cr_parser_parse_expr (a_this, &expr); + + CHECK_PARSING_STATUS (status, FALSE); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + if (status != CR_OK) + goto error; + + ENSURE_PARSING_COND (token && token->type == PC_TK); + + cr_token_destroy (token); + token = NULL; + + if (expr) { + *a_expr = cr_term_append_term (*a_expr, expr); + expr = NULL; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + if (*a_func_name) { + cr_string_destroy (*a_func_name); + *a_func_name = NULL; + } + + if (expr) { + cr_term_destroy (expr); + expr = NULL; + } + + if (token) { + cr_token_destroy (token); + + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_uri: + *@a_this: the current instance of #CRParser. + *@a_str: the successfully parsed url. + * + *Parses an uri as defined by the css spec [4.1.1]: + * URI ::= url\({w}{string}{w}\) + * |url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}\) + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_uri (CRParser * a_this, CRString ** a_str) +{ + + enum CRStatus status = CR_PARSING_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr, CR_BAD_PARAM_ERROR); + + status = cr_tknzr_parse_token (PRIVATE (a_this)->tknzr, + URI_TK, NO_ET, a_str, NULL); + return status; +} + +/** + * cr_parser_parse_string: + *@a_this: the current instance of #CRParser. + *@a_start: out parameter. Upon successful completion, + *points to the beginning of the string, points to an undefined value + *otherwise. + *@a_end: out parameter. Upon successful completion, points to + *the beginning of the string, points to an undefined value otherwise. + * + *Parses a string type as defined in css spec [4.1.1]: + * + *string ::= {string1}|{string2} + *string1 ::= \"([\t !#$%&(-~]|\\{nl}|\'|{nonascii}|{escape})*\" + *string2 ::= \'([\t !#$%&(-~]|\\{nl}|\"|{nonascii}|{escape})*\' + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_string (CRParser * a_this, CRString ** a_str) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr + && a_str, CR_BAD_PARAM_ERROR); + + status = cr_tknzr_parse_token (PRIVATE (a_this)->tknzr, + STRING_TK, NO_ET, a_str, NULL); + return status; +} + +/** + *Parses an "ident" as defined in css spec [4.1.1]: + *ident ::= {nmstart}{nmchar}* + * + *@param a_this the currens instance of #CRParser. + * + *@param a_str a pointer to parsed ident. If *a_str is NULL, + *this function allocates a new instance of #CRString. If not, + *the function just appends the parsed string to the one passed. + *In both cases it is up to the caller to free *a_str. + * + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +static enum CRStatus +cr_parser_parse_ident (CRParser * a_this, CRString ** a_str) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr + && a_str, CR_BAD_PARAM_ERROR); + + status = cr_tknzr_parse_token (PRIVATE (a_this)->tknzr, + IDENT_TK, NO_ET, a_str, NULL); + return status; +} + +/** + *the next rule is ignored as well. This seems to be a bug + *Parses a stylesheet as defined in the css2 spec in appendix D.1: + *stylesheet ::= [ CHARSET_SYM S* STRING S* ';' ]? + * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* + * [ [ ruleset | media | page | font_face ] [S|CDO|CDC]* ]* + * + *TODO: Finish the code of this function. Think about splitting it into + *smaller functions. + * + *@param a_this the "this pointer" of the current instance of #CRParser. + *@param a_start out parameter. A pointer to the first character of + *the successfully parsed string. + *@param a_end out parameter. A pointer to the first character of + *the successfully parsed string. + * + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_parser_parse_stylesheet (CRParser * a_this) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRToken *token = NULL; + CRString *charset = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PRIVATE (a_this)->state = READY_STATE; + + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->start_document) { + PRIVATE (a_this)->sac_handler->start_document + (PRIVATE (a_this)->sac_handler); + } + + parse_charset: + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + if (status == CR_END_OF_INPUT_ERROR) + goto done; + CHECK_PARSING_STATUS (status, TRUE); + + if (token && token->type == CHARSET_SYM_TK) { + CRParsingLocation location = {0} ; + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + + status = cr_parser_parse_charset (a_this, + &charset, + &location); + + if (status == CR_OK && charset) { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->charset) { + PRIVATE (a_this)->sac_handler->charset + (PRIVATE (a_this)->sac_handler, + charset, &location); + } + } else if (status != CR_END_OF_INPUT_ERROR) { + status = cr_parser_parse_atrule_core (a_this); + CHECK_PARSING_STATUS (status, FALSE); + } + + if (charset) { + cr_string_destroy (charset); + charset = NULL; + } + } else if (token + && (token->type == S_TK + || token->type == COMMENT_TK)) { + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + CHECK_PARSING_STATUS (status, TRUE); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + goto parse_charset ; + } else if (token) { + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + CHECK_PARSING_STATUS (status, TRUE); + } + +/* parse_imports:*/ + do { + if (token) { + cr_token_destroy (token); + token = NULL; + } + cr_parser_try_to_skip_spaces_and_comments (a_this) ; + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + + if (status == CR_END_OF_INPUT_ERROR) + goto done; + CHECK_PARSING_STATUS (status, TRUE); + } while (token + && (token->type == S_TK + || token->type == CDO_TK || token->type == CDC_TK)); + + if (token) { + status = cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, + token); + token = NULL; + } + + for (;;) { + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + if (status == CR_END_OF_INPUT_ERROR) + goto done; + CHECK_PARSING_STATUS (status, TRUE); + + if (token && token->type == IMPORT_SYM_TK) { + GList *media_list = NULL; + CRString *import_string = NULL; + CRParsingLocation location = {0} ; + + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + CHECK_PARSING_STATUS (status, TRUE); + + status = cr_parser_parse_import (a_this, + &media_list, + &import_string, + &location); + if (status == CR_OK) { + if (import_string + && PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->import_style) { + PRIVATE (a_this)->sac_handler->import_style + (PRIVATE(a_this)->sac_handler, + media_list, + import_string, + NULL, &location) ; + + if ((PRIVATE (a_this)->sac_handler->resolve_import == TRUE)) { + /* + *TODO: resolve the + *import rule. + */ + } + + if ((PRIVATE (a_this)->sac_handler->import_style_result)) { + PRIVATE (a_this)->sac_handler->import_style_result + (PRIVATE (a_this)->sac_handler, + media_list, import_string, + NULL, NULL); + } + } + } else if (status != CR_END_OF_INPUT_ERROR) { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler->error + (PRIVATE (a_this)->sac_handler); + } + status = cr_parser_parse_atrule_core (a_this); + CHECK_PARSING_STATUS (status, TRUE) ; + } else { + goto error ; + } + + /* + *then, after calling the appropriate + *SAC handler, free + *the media_list and import_string. + */ + if (media_list) { + GList *cur = NULL; + + /*free the medium list */ + for (cur = media_list; cur; cur = cur->next) { + if (cur->data) { + cr_string_destroy (cur->data); + } + } + + g_list_free (media_list); + media_list = NULL; + } + + if (import_string) { + cr_string_destroy (import_string); + import_string = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + } else if (token + && (token->type == S_TK + || token->type == CDO_TK + || token->type == CDC_TK)) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + + do { + if (token) { + cr_token_destroy (token); + token = NULL; + } + + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + + if (status == CR_END_OF_INPUT_ERROR) + goto done; + CHECK_PARSING_STATUS (status, TRUE); + } while (token + && (token->type == S_TK + || token->type == CDO_TK + || token->type == CDC_TK)); + } else { + if (token) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + } + goto parse_ruleset_and_others; + } + } + + parse_ruleset_and_others: + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + for (;;) { + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + if (status == CR_END_OF_INPUT_ERROR) + goto done; + CHECK_PARSING_STATUS (status, TRUE); + + if (token + && (token->type == S_TK + || token->type == CDO_TK || token->type == CDC_TK)) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + + do { + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments + (a_this); + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + } while (token + && (token->type == S_TK + || token->type == COMMENT_TK + || token->type == CDO_TK + || token->type == CDC_TK)); + if (token) { + cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + token = NULL; + } + } else if (token + && (token->type == HASH_TK + || (token->type == DELIM_TK + && token->u.unichar == '.') + || (token->type == DELIM_TK + && token->u.unichar == ':') + || (token->type == DELIM_TK + && token->u.unichar == '*') + || (token->type == BO_TK) + || token->type == IDENT_TK)) { + /* + *Try to parse a CSS2 ruleset. + *if the parsing fails, try to parse + *a css core ruleset. + */ + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + + status = cr_parser_parse_ruleset (a_this); + + if (status == CR_OK) { + continue; + } else { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler-> + error + (PRIVATE (a_this)-> + sac_handler); + } + + status = cr_parser_parse_ruleset_core + (a_this); + + if (status == CR_OK) { + continue; + } else { + break; + } + } + } else if (token && token->type == MEDIA_SYM_TK) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + + status = cr_parser_parse_media (a_this); + if (status == CR_OK) { + continue; + } else { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler-> + error + (PRIVATE (a_this)-> + sac_handler); + } + + status = cr_parser_parse_atrule_core (a_this); + + if (status == CR_OK) { + continue; + } else { + break; + } + } + + } else if (token && token->type == PAGE_SYM_TK) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + status = cr_parser_parse_page (a_this); + + if (status == CR_OK) { + continue; + } else { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler-> + error + (PRIVATE (a_this)-> + sac_handler); + } + + status = cr_parser_parse_atrule_core (a_this); + + if (status == CR_OK) { + continue; + } else { + break; + } + } + } else if (token && token->type == FONT_FACE_SYM_TK) { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + status = cr_parser_parse_font_face (a_this); + + if (status == CR_OK) { + continue; + } else { + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler-> + error + (PRIVATE (a_this)-> + sac_handler); + } + + status = cr_parser_parse_atrule_core (a_this); + + if (status == CR_OK) { + continue; + } else { + break; + } + } + } else { + status = cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, token); + CHECK_PARSING_STATUS (status, TRUE); + token = NULL; + status = cr_parser_parse_statement_core (a_this); + + if (status == CR_OK) { + continue; + } else { + break; + } + } + } + + done: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (status == CR_END_OF_INPUT_ERROR || status == CR_OK) { + + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->end_document) { + PRIVATE (a_this)->sac_handler->end_document + (PRIVATE (a_this)->sac_handler); + } + + return CR_OK; + } + + cr_parser_push_error + (a_this, (const guchar *) "could not recognize next production", CR_ERROR); + + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->unrecoverable_error) { + PRIVATE (a_this)->sac_handler-> + unrecoverable_error (PRIVATE (a_this)->sac_handler); + } + + cr_parser_dump_err_stack (a_this, TRUE); + + return status; + + error: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->unrecoverable_error) { + PRIVATE (a_this)->sac_handler-> + unrecoverable_error (PRIVATE (a_this)->sac_handler); + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/**************************************** + *Public CRParser Methods + ****************************************/ + +/** + * cr_parser_new: + * @a_tknzr: the tokenizer to use for the parsing. + * + *Creates a new parser to parse data + *coming the input stream given in parameter. + * + *Returns the newly created instance of #CRParser, + *or NULL if an error occurred. + */ +CRParser * +cr_parser_new (CRTknzr * a_tknzr) +{ + CRParser *result = NULL; + enum CRStatus status = CR_OK; + + result = g_malloc0 (sizeof (CRParser)); + + PRIVATE (result) = g_malloc0 (sizeof (CRParserPriv)); + + if (a_tknzr) { + status = cr_parser_set_tknzr (result, a_tknzr); + } + + g_return_val_if_fail (status == CR_OK, NULL); + + return result; +} + +/** + * cr_parser_new_from_buf: + *@a_buf: the buffer to parse. + *@a_len: the length of the data in the buffer. + *@a_enc: the encoding of the input buffer a_buf. + *@a_free_buf: if set to TRUE, a_buf will be freed + *during the destruction of the newly built instance + *of #CRParser. If set to FALSE, it is up to the caller to + *eventually free it. + * + *Instantiates a new parser from a memory buffer. + * + *Returns the newly built parser, or NULL if an error arises. + */ +CRParser * +cr_parser_new_from_buf (guchar * a_buf, + gulong a_len, + enum CREncoding a_enc, + gboolean a_free_buf) +{ + CRParser *result = NULL; + CRInput *input = NULL; + + g_return_val_if_fail (a_buf && a_len, NULL); + + input = cr_input_new_from_buf (a_buf, a_len, a_enc, a_free_buf); + g_return_val_if_fail (input, NULL); + + result = cr_parser_new_from_input (input); + if (!result) { + cr_input_destroy (input); + input = NULL; + return NULL; + } + return result; +} + +/** + * cr_parser_new_from_input: + * @a_input: the parser input stream to use. + * + * Returns a newly built parser input. + */ +CRParser * +cr_parser_new_from_input (CRInput * a_input) +{ + CRParser *result = NULL; + CRTknzr *tokenizer = NULL; + + if (a_input) { + tokenizer = cr_tknzr_new (a_input); + g_return_val_if_fail (tokenizer, NULL); + } + + result = cr_parser_new (tokenizer); + g_return_val_if_fail (result, NULL); + + return result; +} + +/** + * cr_parser_new_from_file: + * @a_file_uri: the uri of the file to parse. + * @a_enc: the file encoding to use. + * + * Returns the newly built parser. + */ +CRParser * +cr_parser_new_from_file (const guchar * a_file_uri, enum CREncoding a_enc) +{ + CRParser *result = NULL; + CRTknzr *tokenizer = NULL; + + tokenizer = cr_tknzr_new_from_uri (a_file_uri, a_enc); + if (!tokenizer) { + cr_utils_trace_info ("Could not open input file"); + return NULL; + } + + result = cr_parser_new (tokenizer); + g_return_val_if_fail (result, NULL); + return result; +} + +/** + * cr_parser_set_sac_handler: + *@a_this: the "this pointer" of the current instance of #CRParser. + *@a_handler: the handler to set. + * + *Sets a SAC document handler to the parser. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_set_sac_handler (CRParser * a_this, CRDocHandler * a_handler) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->sac_handler) { + cr_doc_handler_unref (PRIVATE (a_this)->sac_handler); + } + + PRIVATE (a_this)->sac_handler = a_handler; + cr_doc_handler_ref (a_handler); + + return CR_OK; +} + +/** + * cr_parser_get_sac_handler: + *@a_this: the "this pointer" of the current instance of + *#CRParser. + *@a_handler: out parameter. The returned handler. + * + *Gets the SAC document handler. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_parser_get_sac_handler (CRParser * a_this, CRDocHandler ** a_handler) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + *a_handler = PRIVATE (a_this)->sac_handler; + + return CR_OK; +} + +/** + * cr_parser_set_default_sac_handler: + *@a_this: a pointer to the current instance of #CRParser. + * + *Sets the SAC handler associated to the current instance + *of #CRParser to the default SAC handler. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_set_default_sac_handler (CRParser * a_this) +{ + CRDocHandler *default_sac_handler = NULL; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + default_sac_handler = cr_doc_handler_new (); + + cr_doc_handler_set_default_sac_handler (default_sac_handler); + + status = cr_parser_set_sac_handler (a_this, default_sac_handler); + + if (status != CR_OK) { + cr_doc_handler_destroy (default_sac_handler); + default_sac_handler = NULL; + } + + return status; +} + +/** + * cr_parser_set_use_core_grammar: + * @a_this: the current instance of #CRParser. + * @a_use_core_grammar: where to parse against the css core grammar. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_set_use_core_grammar (CRParser * a_this, + gboolean a_use_core_grammar) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->use_core_grammar = a_use_core_grammar; + + return CR_OK; +} + +/** + * cr_parser_get_use_core_grammar: + * @a_this: the current instance of #CRParser. + * @a_use_core_grammar: whether to use the core grammar or not. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_get_use_core_grammar (CRParser const * a_this, + gboolean * a_use_core_grammar) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + *a_use_core_grammar = PRIVATE (a_this)->use_core_grammar; + + return CR_OK; +} + +/** + * cr_parser_parse_file: + *@a_this: a pointer to the current instance of #CRParser. + *@a_file_uri: the uri to the file to load. For the time being, + *@a_enc: the encoding of the file to parse. + *only local files are supported. + * + *Parses a the given in parameter. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_file (CRParser * a_this, + const guchar * a_file_uri, enum CREncoding a_enc) +{ + enum CRStatus status = CR_ERROR; + CRTknzr *tknzr = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_file_uri, CR_BAD_PARAM_ERROR); + + tknzr = cr_tknzr_new_from_uri (a_file_uri, a_enc); + + g_return_val_if_fail (tknzr != NULL, CR_ERROR); + + status = cr_parser_set_tknzr (a_this, tknzr); + g_return_val_if_fail (status == CR_OK, CR_ERROR); + + status = cr_parser_parse (a_this); + + return status; +} + +/** + * cr_parser_parse_expr: + * @a_this: the current instance of #CRParser. + * @a_expr: out parameter. the parsed expression. + * + *Parses an expression as defined by the css2 spec in appendix + *D.1: + *expr: term [ operator term ]* + * + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_expr (CRParser * a_this, CRTerm ** a_expr) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + CRTerm *expr = NULL, + *expr2 = NULL; + guchar next_byte = 0; + gulong nb_terms = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_expr, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_term (a_this, &expr); + + CHECK_PARSING_STATUS (status, FALSE); + + for (;;) { + guchar operator = 0; + + status = cr_tknzr_peek_byte (PRIVATE (a_this)->tknzr, + 1, &next_byte); + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) { + /* + if (!nb_terms) + { + goto error ; + } + */ + status = CR_OK; + break; + } else { + goto error; + } + } + + if (next_byte == '/' || next_byte == ',') { + READ_NEXT_BYTE (a_this, &operator); + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_term (a_this, &expr2); + + if (status != CR_OK || expr2 == NULL) { + status = CR_OK; + break; + } + + switch (operator) { + case '/': + expr2->the_operator = DIVIDE; + break; + case ',': + expr2->the_operator = COMMA; + + default: + break; + } + + expr = cr_term_append_term (expr, expr2); + expr2 = NULL; + operator = 0; + nb_terms++; + } + + if (status == CR_OK) { + *a_expr = cr_term_append_term (*a_expr, expr); + expr = NULL; + + cr_parser_clear_errors (a_this); + return CR_OK; + } + + error: + + if (expr) { + cr_term_destroy (expr); + expr = NULL; + } + + if (expr2) { + cr_term_destroy (expr2); + expr2 = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_prio: + *@a_this: the current instance of #CRParser. + *@a_prio: a string representing the priority. + *Today, only "!important" is returned as only this + *priority is defined by css2. + * + *Parses a declaration priority as defined by + *the css2 grammar in appendix C: + *prio: IMPORTANT_SYM S* + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_prio (CRParser * a_this, CRString ** a_prio) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + CRToken *token = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_prio + && *a_prio == NULL, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + if (status == CR_END_OF_INPUT_ERROR) { + goto error; + } + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == IMPORTANT_SYM_TK); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + *a_prio = cr_string_new_from_string ("!important"); + cr_token_destroy (token); + token = NULL; + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_declaration: + *@a_this: the "this pointer" of the current instance of #CRParser. + *@a_property: the successfully parsed property. The caller + * *must* free the returned pointer. + *@a_expr: the expression that represents the attribute value. + *The caller *must* free the returned pointer. + * + *TODO: return the parsed priority, so that + *upper layers can take benefit from it. + *Parses a "declaration" as defined by the css2 spec in appendix D.1: + *declaration ::= [property ':' S* expr prio?]? + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_declaration (CRParser * a_this, + CRString ** a_property, + CRTerm ** a_expr, gboolean * a_important) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + guint32 cur_char = 0; + CRTerm *expr = NULL; + CRString *prio = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_property && a_expr + && a_important, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_property (a_this, a_property); + + if (status == CR_END_OF_INPUT_ERROR) + goto error; + + CHECK_PARSING_STATUS_ERR + (a_this, status, FALSE, + (const guchar *) "while parsing declaration: next property is malformed", + CR_SYNTAX_ERROR); + + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char != ':') { + status = CR_PARSING_ERROR; + cr_parser_push_error + (a_this, + (const guchar *) "while parsing declaration: this char must be ':'", + CR_SYNTAX_ERROR); + goto error; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_expr (a_this, &expr); + + CHECK_PARSING_STATUS_ERR + (a_this, status, FALSE, + (const guchar *) "while parsing declaration: next expression is malformed", + CR_SYNTAX_ERROR); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_prio (a_this, &prio); + if (prio) { + cr_string_destroy (prio); + prio = NULL; + *a_important = TRUE; + } else { + *a_important = FALSE; + } + if (*a_expr) { + cr_term_append_term (*a_expr, expr); + expr = NULL; + } else { + *a_expr = expr; + expr = NULL; + } + + cr_parser_clear_errors (a_this); + return CR_OK; + + error: + + if (expr) { + cr_term_destroy (expr); + expr = NULL; + } + + if (*a_property) { + cr_string_destroy (*a_property); + *a_property = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_statement_core: + *@a_this: the current instance of #CRParser. + * + *Parses a statement as defined by the css core grammar in + *chapter 4.1 of the css2 spec. + *statement : ruleset | at-rule; + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_statement_core (CRParser * a_this) +{ + CRToken *token = NULL; + CRInputPos init_pos; + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK && token); + + switch (token->type) { + case ATKEYWORD_TK: + case IMPORT_SYM_TK: + case PAGE_SYM_TK: + case MEDIA_SYM_TK: + case FONT_FACE_SYM_TK: + case CHARSET_SYM_TK: + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + status = cr_parser_parse_atrule_core (a_this); + CHECK_PARSING_STATUS (status, TRUE); + break; + + default: + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + status = cr_parser_parse_ruleset_core (a_this); + cr_parser_clear_errors (a_this); + CHECK_PARSING_STATUS (status, TRUE); + } + + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_ruleset: + *@a_this: the "this pointer" of the current instance of #CRParser. + * + *Parses a "ruleset" as defined in the css2 spec at appendix D.1. + *ruleset ::= selector [ ',' S* selector ]* + *'{' S* declaration? [ ';' S* declaration? ]* '}' S*; + * + *This methods calls the the SAC handler on the relevant SAC handler + *callbacks whenever it encounters some specific constructions. + *See the documentation of #CRDocHandler (the SAC handler) to know + *when which SAC handler is called. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_ruleset (CRParser * a_this) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + guint32 cur_char = 0, + next_char = 0; + CRString *property = NULL; + CRTerm *expr = NULL; + CRSimpleSel *simple_sels = NULL; + CRSelector *selector = NULL; + gboolean start_selector = FALSE, + is_important = FALSE; + CRParsingLocation end_parsing_location; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_parser_parse_selector (a_this, &selector); + CHECK_PARSING_STATUS (status, FALSE); + + READ_NEXT_CHAR (a_this, &cur_char); + + ENSURE_PARSING_COND_ERR + (a_this, cur_char == '{', + (const guchar *) "while parsing rulset: current char should be '{'", + CR_SYNTAX_ERROR); + + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->start_selector) { + /* + *the selector is ref counted so that the parser's user + *can choose to keep it. + */ + if (selector) { + cr_selector_ref (selector); + } + + PRIVATE (a_this)->sac_handler->start_selector + (PRIVATE (a_this)->sac_handler, selector); + start_selector = TRUE; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + PRIVATE (a_this)->state = TRY_PARSE_RULESET_STATE; + + status = cr_parser_parse_declaration (a_this, &property, + &expr, + &is_important); + if (expr) { + cr_term_ref (expr); + } + if (status == CR_OK + && PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->property) { + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, property, expr, + is_important); + } + if (status == CR_OK) { + /* + *free the allocated + *'property' and 'term' before parsing + *next declarations. + */ + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (expr) { + cr_term_unref (expr); + expr = NULL; + } + } else {/*status != CR_OK*/ + guint32 c = 0 ; + /* + *test if we have reached '}', which + *would mean that we are parsing an empty ruleset (eg. x{ }) + *In that case, goto end_of_ruleset. + */ + status = cr_tknzr_peek_char (PRIVATE (a_this)->tknzr, &c) ; + if (status == CR_OK && c == '}') { + status = CR_OK ; + goto end_of_ruleset ; + } + } + CHECK_PARSING_STATUS_ERR + (a_this, status, FALSE, + (const guchar *) "while parsing ruleset: next construction should be a declaration", + CR_SYNTAX_ERROR); + + for (;;) { + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char != ';') + break; + + /*consume the ';' char */ + READ_NEXT_CHAR (a_this, &cur_char); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_declaration (a_this, &property, + &expr, &is_important); + + if (expr) { + cr_term_ref (expr); + } + if (status == CR_OK + && PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->property) { + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, + property, expr, is_important); + } + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (expr) { + cr_term_unref (expr); + expr = NULL; + } + } + + end_of_ruleset: + cr_parser_try_to_skip_spaces_and_comments (a_this); + cr_parser_get_parsing_location (a_this, &end_parsing_location); + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND_ERR + (a_this, cur_char == '}', + (const guchar *) "while parsing rulset: current char must be a '}'", + CR_SYNTAX_ERROR); + + selector->location = end_parsing_location; + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->end_selector) { + PRIVATE (a_this)->sac_handler->end_selector + (PRIVATE (a_this)->sac_handler, selector); + start_selector = FALSE; + } + + if (expr) { + cr_term_unref (expr); + expr = NULL; + } + + if (simple_sels) { + cr_simple_sel_destroy (simple_sels); + simple_sels = NULL; + } + + if (selector) { + cr_selector_unref (selector); + selector = NULL; + } + + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->state = RULESET_PARSED_STATE; + + return CR_OK; + + error: + if (start_selector == TRUE + && PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->error) { + PRIVATE (a_this)->sac_handler->error + (PRIVATE (a_this)->sac_handler); + } + if (expr) { + cr_term_unref (expr); + expr = NULL; + } + if (simple_sels) { + cr_simple_sel_destroy (simple_sels); + simple_sels = NULL; + } + if (property) { + cr_string_destroy (property); + } + if (selector) { + cr_selector_unref (selector); + selector = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_import: + *@a_this: the "this pointer" of the current instance + *of #CRParser. + *@a_media_list: out parameter. A linked list of + *#CRString + *Each CRString is a string that contains + *a 'medium' declaration part of the successfully + *parsed 'import' declaration. + *@a_import_string: out parameter. + *A string that contains the 'import + *string". The import string can be either an uri (if it starts with + *the substring "uri(") or a any other css2 string. Note that + * *a_import_string must be initially set to NULL or else, this function + *will return CR_BAD_PARAM_ERROR. + *@a_location: the location (line, column) where the import has been parsed + * + *Parses an 'import' declaration as defined in the css2 spec + *in appendix D.1: + * + *import ::= + *\@import [STRING|URI] S* [ medium [ ',' S* medium]* ]? ';' S* + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_import (CRParser * a_this, + GList ** a_media_list, + CRString ** a_import_string, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + guint32 cur_char = 0, + next_char = 0; + CRString *medium = NULL; + + g_return_val_if_fail (a_this + && a_import_string + && (*a_import_string == NULL), + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + if (BYTE (a_this, 1, NULL) == '@' + && BYTE (a_this, 2, NULL) == 'i' + && BYTE (a_this, 3, NULL) == 'm' + && BYTE (a_this, 4, NULL) == 'p' + && BYTE (a_this, 5, NULL) == 'o' + && BYTE (a_this, 6, NULL) == 'r' + && BYTE (a_this, 7, NULL) == 't') { + SKIP_CHARS (a_this, 1); + if (a_location) { + cr_parser_get_parsing_location + (a_this, a_location) ; + } + SKIP_CHARS (a_this, 6); + status = CR_OK; + } else { + status = CR_PARSING_ERROR; + goto error; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + PRIVATE (a_this)->state = TRY_PARSE_IMPORT_STATE; + + PEEK_NEXT_CHAR (a_this, &next_char); + + if (next_char == '"' || next_char == '\'') { + status = cr_parser_parse_string (a_this, a_import_string); + + CHECK_PARSING_STATUS (status, FALSE); + } else { + status = cr_parser_parse_uri (a_this, a_import_string); + + CHECK_PARSING_STATUS (status, FALSE); + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_ident (a_this, &medium); + + if (status == CR_OK && medium) { + *a_media_list = g_list_append (*a_media_list, medium); + medium = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + for (; status == CR_OK;) { + if ((status = cr_tknzr_peek_char (PRIVATE (a_this)->tknzr, + &next_char)) != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) { + status = CR_OK; + goto okay; + } + goto error; + } + + if (next_char == ',') { + READ_NEXT_CHAR (a_this, &cur_char); + } else { + break; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_ident (a_this, &medium); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + if ((status == CR_OK) && medium) { + *a_media_list = g_list_append (*a_media_list, medium); + + medium = NULL; + } + + CHECK_PARSING_STATUS (status, FALSE); + cr_parser_try_to_skip_spaces_and_comments (a_this); + } + cr_parser_try_to_skip_spaces_and_comments (a_this); + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == ';'); + cr_parser_try_to_skip_spaces_and_comments (a_this); + okay: + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->state = IMPORT_PARSED_STATE; + + return CR_OK; + + error: + + if (*a_media_list) { + GList *cur = NULL; + + /* + *free each element of *a_media_list. + *Note that each element of *a_medium list *must* + *be a GString* or else, the code that is coming next + *will corrupt the memory and lead to hard to debug + *random crashes. + *This is where C++ and its compile time + *type checking mechanism (through STL containers) would + *have prevented us to go through this hassle. + */ + for (cur = *a_media_list; cur; cur = cur->next) { + if (cur->data) { + cr_string_destroy (cur->data); + } + } + + g_list_free (*a_media_list); + *a_media_list = NULL; + } + + if (*a_import_string) { + cr_string_destroy (*a_import_string); + *a_import_string = NULL; + } + + if (medium) { + cr_string_destroy (medium); + medium = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_media: + *@a_this: the "this pointer" of the current instance of #CRParser. + * + *Parses a 'media' declaration as specified in the css2 spec at + *appendix D.1: + * + *media ::= \@media S* medium [ ',' S* medium ]* '{' S* ruleset* '}' S* + * + *Note that this function calls the required sac handlers during the parsing + *to notify media productions. See #CRDocHandler to know the callback called + *during \@media parsing. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_media (CRParser * a_this) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRToken *token = NULL; + guint32 next_char = 0, + cur_char = 0; + CRString *medium = NULL; + GList *media_list = NULL; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this + && PRIVATE (a_this), + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + ENSURE_PARSING_COND (status == CR_OK + && token + && token->type == MEDIA_SYM_TK); + cr_parsing_location_copy (&location, &token->location) ; + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == IDENT_TK); + + medium = token->u.str; + token->u.str = NULL; + cr_token_destroy (token); + token = NULL; + + if (medium) { + media_list = g_list_append (media_list, medium); + medium = NULL; + } + + for (; status == CR_OK;) { + cr_parser_try_to_skip_spaces_and_comments (a_this); + PEEK_NEXT_CHAR (a_this, &next_char); + + if (next_char == ',') { + READ_NEXT_CHAR (a_this, &cur_char); + } else { + break; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_ident (a_this, &medium); + + CHECK_PARSING_STATUS (status, FALSE); + + if (medium) { + media_list = g_list_append (media_list, medium); + medium = NULL; + } + } + + READ_NEXT_CHAR (a_this, &cur_char); + + ENSURE_PARSING_COND (cur_char == '{'); + + /* + *call the SAC handler api here. + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->start_media) { + PRIVATE (a_this)->sac_handler->start_media + (PRIVATE (a_this)->sac_handler, media_list, + &location); + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + PRIVATE (a_this)->state = TRY_PARSE_MEDIA_STATE; + + for (; status == CR_OK;) { + status = cr_parser_parse_ruleset (a_this); + cr_parser_try_to_skip_spaces_and_comments (a_this); + } + + READ_NEXT_CHAR (a_this, &cur_char); + + ENSURE_PARSING_COND (cur_char == '}'); + + /* + *call the right SAC handler api here. + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->end_media) { + PRIVATE (a_this)->sac_handler->end_media + (PRIVATE (a_this)->sac_handler, media_list); + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + /* + *Then, free the data structures passed to + *the last call to the SAC handler. + */ + if (medium) { + cr_string_destroy (medium); + medium = NULL; + } + + if (media_list) { + GList *cur = NULL; + + for (cur = media_list; cur; cur = cur->next) { + cr_string_destroy (cur->data); + } + + g_list_free (media_list); + media_list = NULL; + } + + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->state = MEDIA_PARSED_STATE; + + return CR_OK; + + error: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (medium) { + cr_string_destroy (medium); + medium = NULL; + } + + if (media_list) { + GList *cur = NULL; + + for (cur = media_list; cur; cur = cur->next) { + cr_string_destroy (cur->data); + } + + g_list_free (media_list); + media_list = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_page: + *@a_this: the "this pointer" of the current instance of #CRParser. + * + *Parses '\@page' rule as specified in the css2 spec in appendix D.1: + *page ::= PAGE_SYM S* IDENT? pseudo_page? S* + *'{' S* declaration [ ';' S* declaration ]* '}' S* + * + *This function also calls the relevant SAC handlers whenever it + *encounters a construction that must + *be reported to the calling application. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_page (CRParser * a_this) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRToken *token = NULL; + CRTerm *css_expression = NULL; + CRString *page_selector = NULL, + *page_pseudo_class = NULL, + *property = NULL; + gboolean important = TRUE; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token) ; + ENSURE_PARSING_COND (status == CR_OK + && token + && token->type == PAGE_SYM_TK); + + cr_parsing_location_copy (&location, &token->location) ; + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == IDENT_TK) { + page_selector = token->u.str; + token->u.str = NULL; + cr_token_destroy (token); + token = NULL; + } else { + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + } + + /* + *try to parse pseudo_page + */ + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type == DELIM_TK && token->u.unichar == ':') { + cr_token_destroy (token); + token = NULL; + status = cr_parser_parse_ident (a_this, &page_pseudo_class); + CHECK_PARSING_STATUS (status, FALSE); + } else { + cr_tknzr_unget_token (PRIVATE (a_this)->tknzr, token); + token = NULL; + } + + /* + *parse_block + * + */ + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == CBO_TK); + + cr_token_destroy (token); + token = NULL; + + /* + *Call the appropriate SAC handler here. + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->start_page) { + PRIVATE (a_this)->sac_handler->start_page + (PRIVATE (a_this)->sac_handler, + page_selector, page_pseudo_class, + &location); + } + cr_parser_try_to_skip_spaces_and_comments (a_this); + + PRIVATE (a_this)->state = TRY_PARSE_PAGE_STATE; + + status = cr_parser_parse_declaration (a_this, &property, + &css_expression, + &important); + ENSURE_PARSING_COND (status == CR_OK); + + /* + *call the relevant SAC handler here... + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->property) { + if (css_expression) + cr_term_ref (css_expression); + + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, + property, css_expression, important); + } + /* + *... and free the data structure passed to that last + *SAC handler. + */ + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_unref (css_expression); + css_expression = NULL; + } + + for (;;) { + /*parse the other ';' separated declarations */ + if (token) { + cr_token_destroy (token); + token = NULL; + } + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK && token); + + if (token->type != SEMICOLON_TK) { + cr_tknzr_unget_token + (PRIVATE (a_this)->tknzr, + token); + token = NULL ; + break; + } + + cr_token_destroy (token); + token = NULL; + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_parser_parse_declaration (a_this, &property, + &css_expression, + &important); + if (status != CR_OK) + break ; + + /* + *call the relevant SAC handler here... + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->property) { + cr_term_ref (css_expression); + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, + property, css_expression, important); + } + /* + *... and free the data structure passed to that last + *SAC handler. + */ + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_unref (css_expression); + css_expression = NULL; + } + } + cr_parser_try_to_skip_spaces_and_comments + (a_this) ; + if (token) { + cr_token_destroy (token) ; + token = NULL ; + } + + status = cr_tknzr_get_next_token + (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token + && token->type == CBC_TK) ; + cr_token_destroy (token) ; + token = NULL ; + /* + *call the relevant SAC handler here. + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->end_page) { + PRIVATE (a_this)->sac_handler->end_page + (PRIVATE (a_this)->sac_handler, + page_selector, page_pseudo_class); + } + + if (page_selector) { + cr_string_destroy (page_selector); + page_selector = NULL; + } + + if (page_pseudo_class) { + cr_string_destroy (page_pseudo_class); + page_pseudo_class = NULL; + } + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + /*here goes the former implem of this function ... */ + + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->state = PAGE_PARSED_STATE; + + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + if (page_selector) { + cr_string_destroy (page_selector); + page_selector = NULL; + } + if (page_pseudo_class) { + cr_string_destroy (page_pseudo_class); + page_pseudo_class = NULL; + } + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_destroy (css_expression); + css_expression = NULL; + } + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + return status; +} + +/** + * cr_parser_parse_charset: + *@a_this: the "this pointer" of the current instance of #CRParser. + *@a_value: out parameter. The actual parsed value of the charset + *declararation. Note that for safety check reasons, *a_value must be + *set to NULL. + *@a_charset_sym_location: the parsing location of the charset rule + * + *Parses a charset declaration as defined implicitly by the css2 spec in + *appendix D.1: + *charset ::= CHARSET_SYM S* STRING S* ';' + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_charset (CRParser * a_this, CRString ** a_value, + CRParsingLocation *a_charset_sym_location) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRToken *token = NULL; + CRString *charset_str = NULL; + + g_return_val_if_fail (a_this && a_value + && (*a_value == NULL), + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == CHARSET_SYM_TK); + if (a_charset_sym_location) { + cr_parsing_location_copy (a_charset_sym_location, + &token->location) ; + } + cr_token_destroy (token); + token = NULL; + + PRIVATE (a_this)->state = TRY_PARSE_CHARSET_STATE; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == STRING_TK); + charset_str = token->u.str; + token->u.str = NULL; + cr_token_destroy (token); + token = NULL; + + cr_parser_try_to_skip_spaces_and_comments (a_this); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + + ENSURE_PARSING_COND (status == CR_OK + && token && token->type == SEMICOLON_TK); + cr_token_destroy (token); + token = NULL; + + if (charset_str) { + *a_value = charset_str; + charset_str = NULL; + } + + PRIVATE (a_this)->state = CHARSET_PARSED_STATE; + return CR_OK; + + error: + + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (*a_value) { + cr_string_destroy (*a_value); + *a_value = NULL; + } + + if (charset_str) { + cr_string_destroy (charset_str); + charset_str = NULL; + } + + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + + return status; +} + +/** + * cr_parser_parse_font_face: + *@a_this: the current instance of #CRParser. + * + *Parses the "\@font-face" rule specified in the css1 spec in + *appendix D.1: + * + *font_face ::= FONT_FACE_SYM S* + *'{' S* declaration [ ';' S* declaration ]* '}' S* + * + *This function will call SAC handlers whenever it is necessary. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_font_face (CRParser * a_this) +{ + enum CRStatus status = CR_ERROR; + CRInputPos init_pos; + CRString *property = NULL; + CRTerm *css_expression = NULL; + CRToken *token = NULL; + gboolean important = FALSE; + guint32 next_char = 0, + cur_char = 0; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, &token); + ENSURE_PARSING_COND (status == CR_OK + && token + && token->type == FONT_FACE_SYM_TK); + + cr_parser_try_to_skip_spaces_and_comments (a_this); + if (token) { + cr_parsing_location_copy (&location, + &token->location) ; + cr_token_destroy (token); + token = NULL; + } + status = cr_tknzr_get_next_token (PRIVATE (a_this)->tknzr, + &token); + ENSURE_PARSING_COND (status == CR_OK && token + && token->type == CBO_TK); + if (token) { + cr_token_destroy (token); + token = NULL; + } + /* + *here, call the relevant SAC handler. + */ + if (PRIVATE (a_this)->sac_handler + && PRIVATE (a_this)->sac_handler->start_font_face) { + PRIVATE (a_this)->sac_handler->start_font_face + (PRIVATE (a_this)->sac_handler, &location); + } + PRIVATE (a_this)->state = TRY_PARSE_FONT_FACE_STATE; + /* + *and resume the parsing. + */ + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_declaration (a_this, &property, + &css_expression, &important); + if (status == CR_OK) { + /* + *here, call the relevant SAC handler. + */ + cr_term_ref (css_expression); + if (PRIVATE (a_this)->sac_handler && + PRIVATE (a_this)->sac_handler->property) { + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, + property, css_expression, important); + } + ENSURE_PARSING_COND (css_expression && property); + } + /*free the data structures allocated during last parsing. */ + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_unref (css_expression); + css_expression = NULL; + } + for (;;) { + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char == ';') { + READ_NEXT_CHAR (a_this, &cur_char); + } else { + break; + } + cr_parser_try_to_skip_spaces_and_comments (a_this); + status = cr_parser_parse_declaration (a_this, + &property, + &css_expression, + &important); + if (status != CR_OK) + break; + /* + *here, call the relevant SAC handler. + */ + cr_term_ref (css_expression); + if (PRIVATE (a_this)->sac_handler->property) { + PRIVATE (a_this)->sac_handler->property + (PRIVATE (a_this)->sac_handler, + property, css_expression, important); + } + /* + *Then, free the data structures allocated during + *last parsing. + */ + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_unref (css_expression); + css_expression = NULL; + } + } + cr_parser_try_to_skip_spaces_and_comments (a_this); + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == '}'); + /* + *here, call the relevant SAC handler. + */ + if (PRIVATE (a_this)->sac_handler->end_font_face) { + PRIVATE (a_this)->sac_handler->end_font_face + (PRIVATE (a_this)->sac_handler); + } + cr_parser_try_to_skip_spaces_and_comments (a_this); + + if (token) { + cr_token_destroy (token); + token = NULL; + } + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->state = FONT_FACE_PARSED_STATE; + return CR_OK; + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + if (property) { + cr_string_destroy (property); + property = NULL; + } + if (css_expression) { + cr_term_destroy (css_expression); + css_expression = NULL; + } + cr_tknzr_set_cur_pos (PRIVATE (a_this)->tknzr, &init_pos); + return status; +} + +/** + * cr_parser_parse: + *@a_this: the current instance of #CRParser. + * + *Parses the data that comes from the + *input previously associated to the current instance of + *#CRParser. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse (CRParser * a_this) +{ + enum CRStatus status = CR_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->tknzr, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->use_core_grammar == FALSE) { + status = cr_parser_parse_stylesheet (a_this); + } else { + status = cr_parser_parse_stylesheet_core (a_this); + } + + return status; +} + +/** + * cr_parser_set_tknzr: + * @a_this: the current instance of #CRParser; + * @a_tknzr: the new tokenizer. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_set_tknzr (CRParser * a_this, CRTknzr * a_tknzr) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->tknzr) { + cr_tknzr_unref (PRIVATE (a_this)->tknzr); + } + + PRIVATE (a_this)->tknzr = a_tknzr; + + if (a_tknzr) + cr_tknzr_ref (a_tknzr); + + return CR_OK; +} + +/** + * cr_parser_get_tknzr: + *@a_this: the current instance of #CRParser + *@a_tknzr: out parameter. The returned tokenizer + * + *Getter of the parser's underlying tokenizer + * + *Returns CR_OK upon successful completion, an error code + *otherwise + */ +enum CRStatus +cr_parser_get_tknzr (CRParser * a_this, CRTknzr ** a_tknzr) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_tknzr, CR_BAD_PARAM_ERROR); + + *a_tknzr = PRIVATE (a_this)->tknzr; + return CR_OK; +} + +/** + * cr_parser_get_parsing_location: + *@a_this: the current instance of #CRParser + *@a_loc: the parsing location to get. + * + *Gets the current parsing location. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_parser_get_parsing_location (CRParser const *a_this, + CRParsingLocation *a_loc) +{ + g_return_val_if_fail (a_this + && PRIVATE (a_this) + && a_loc, CR_BAD_PARAM_ERROR) ; + + return cr_tknzr_get_parsing_location + (PRIVATE (a_this)->tknzr, a_loc) ; +} + +/** + * cr_parser_parse_buf: + *@a_this: the current instance of #CRparser + *@a_buf: the input buffer + *@a_len: the length of the input buffer + *@a_enc: the encoding of the buffer + * + *Parses a stylesheet from a buffer + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parser_parse_buf (CRParser * a_this, + const guchar * a_buf, + gulong a_len, enum CREncoding a_enc) +{ + enum CRStatus status = CR_ERROR; + CRTknzr *tknzr = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_buf, CR_BAD_PARAM_ERROR); + + tknzr = cr_tknzr_new_from_buf ((guchar*)a_buf, a_len, a_enc, FALSE); + + g_return_val_if_fail (tknzr != NULL, CR_ERROR); + + status = cr_parser_set_tknzr (a_this, tknzr); + g_return_val_if_fail (status == CR_OK, CR_ERROR); + + status = cr_parser_parse (a_this); + + return status; +} + +/** + * cr_parser_destroy: + *@a_this: the current instance of #CRParser to + *destroy. + * + *Destroys the current instance + *of #CRParser. + */ +void +cr_parser_destroy (CRParser * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + if (PRIVATE (a_this)->tknzr) { + if (cr_tknzr_unref (PRIVATE (a_this)->tknzr) == TRUE) + PRIVATE (a_this)->tknzr = NULL; + } + + if (PRIVATE (a_this)->sac_handler) { + cr_doc_handler_unref (PRIVATE (a_this)->sac_handler); + PRIVATE (a_this)->sac_handler = NULL; + } + + if (PRIVATE (a_this)->err_stack) { + cr_parser_clear_errors (a_this); + PRIVATE (a_this)->err_stack = NULL; + } + + if (PRIVATE (a_this)) { + g_free (PRIVATE (a_this)); + PRIVATE (a_this) = NULL; + } + + if (a_this) { + g_free (a_this); + a_this = NULL; /*useless. Just for the sake of coherence */ + } +} diff --git a/src/st/croco/cr-parser.h b/src/st/croco/cr-parser.h new file mode 100644 index 0000000..6dce943 --- /dev/null +++ b/src/st/croco/cr-parser.h @@ -0,0 +1,128 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +#ifndef __CR_PARSER_H__ +#define __CR_PARSER_H__ + +#include <glib.h> +#include "cr-input.h" +#include "cr-tknzr.h" +#include "cr-utils.h" +#include "cr-doc-handler.h" + +G_BEGIN_DECLS + +/** + *@file + *The declaration file + *of the #CRParser class. + */ +typedef struct _CRParser CRParser ; +typedef struct _CRParserPriv CRParserPriv ; + + +/** + *The implementation of + *the SAC parser. + *The Class is opaque + *and must be manipulated through + *the provided methods. + */ +struct _CRParser { + CRParserPriv *priv ; +} ; + + +CRParser * cr_parser_new (CRTknzr *a_tknzr) ; + +CRParser * cr_parser_new_from_buf (guchar *a_buf, gulong a_len, + enum CREncoding a_enc, + gboolean a_free_buf) ; + +CRParser * cr_parser_new_from_file (const guchar *a_file_uri, + enum CREncoding a_enc) ; + +CRParser * cr_parser_new_from_input (CRInput *a_input) ; + +enum CRStatus cr_parser_set_tknzr (CRParser *a_this, CRTknzr *a_tknzr) ; + +enum CRStatus cr_parser_get_tknzr (CRParser *a_this, CRTknzr **a_tknzr) ; + +enum CRStatus cr_parser_get_parsing_location (CRParser const *a_this, CRParsingLocation *a_loc) ; + +enum CRStatus cr_parser_try_to_skip_spaces_and_comments (CRParser *a_this) ; + + +enum CRStatus cr_parser_set_sac_handler (CRParser *a_this, + CRDocHandler *a_handler) ; + +enum CRStatus cr_parser_get_sac_handler (CRParser *a_this, + CRDocHandler **a_handler) ; + +enum CRStatus cr_parser_set_use_core_grammar (CRParser *a_this, + gboolean a_use_core_grammar) ; +enum CRStatus cr_parser_get_use_core_grammar (CRParser const *a_this, + gboolean *a_use_core_grammar) ; + +enum CRStatus cr_parser_parse (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_file (CRParser *a_this, + const guchar *a_file_uri, + enum CREncoding a_enc) ; + +enum CRStatus cr_parser_parse_buf (CRParser *a_this, const guchar *a_buf, + gulong a_len, enum CREncoding a_enc) ; + +enum CRStatus cr_parser_set_default_sac_handler (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_term (CRParser *a_this, CRTerm **a_term) ; + +enum CRStatus cr_parser_parse_expr (CRParser *a_this, CRTerm **a_expr) ; + +enum CRStatus cr_parser_parse_prio (CRParser *a_this, CRString **a_prio) ; + +enum CRStatus cr_parser_parse_declaration (CRParser *a_this, CRString **a_property, + CRTerm **a_expr, gboolean *a_important) ; + +enum CRStatus cr_parser_parse_statement_core (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_ruleset (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_import (CRParser *a_this, GList ** a_media_list, + CRString **a_import_string, + CRParsingLocation *a_location) ; + +enum CRStatus cr_parser_parse_media (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_page (CRParser *a_this) ; + +enum CRStatus cr_parser_parse_charset (CRParser *a_this, CRString **a_value, + CRParsingLocation *a_charset_sym_location) ; + +enum CRStatus cr_parser_parse_font_face (CRParser *a_this) ; + +void cr_parser_destroy (CRParser *a_this) ; + +G_END_DECLS + +#endif /*__CR_PARSER_H__*/ diff --git a/src/st/croco/cr-parsing-location.c b/src/st/croco/cr-parsing-location.c new file mode 100644 index 0000000..2b40974 --- /dev/null +++ b/src/st/croco/cr-parsing-location.c @@ -0,0 +1,171 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli. + * See the COPYRIGHTS file for copyright information. + */ + +#include <string.h> +#include "cr-parsing-location.h" + +/** + *@CRParsingLocation: + * + *Definition of the #CRparsingLocation class. + */ + + +/** + * cr_parsing_location_new: + *Instantiates a new parsing location. + * + *Returns the newly instantiated #CRParsingLocation. + *Must be freed by cr_parsing_location_destroy() + */ +CRParsingLocation * +cr_parsing_location_new (void) +{ + CRParsingLocation * result = NULL ; + + result = g_try_malloc (sizeof (CRParsingLocation)) ; + if (!result) { + cr_utils_trace_info ("Out of memory error") ; + return NULL ; + } + cr_parsing_location_init (result) ; + return result ; +} + +/** + * cr_parsing_location_init: + *@a_this: the current instance of #CRParsingLocation. + * + *Initializes the an instance of #CRparsingLocation. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_parsing_location_init (CRParsingLocation *a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR) ; + + memset (a_this, 0, sizeof (CRParsingLocation)) ; + return CR_OK ; +} + +/** + * cr_parsing_location_copy: + *@a_to: the destination of the copy. + *Must be allocated by the caller. + *@a_from: the source of the copy. + * + *Copies an instance of CRParsingLocation into another one. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_parsing_location_copy (CRParsingLocation *a_to, + CRParsingLocation const *a_from) +{ + g_return_val_if_fail (a_to && a_from, CR_BAD_PARAM_ERROR) ; + + memcpy (a_to, a_from, sizeof (CRParsingLocation)) ; + return CR_OK ; +} + +/** + * cr_parsing_location_to_string: + *@a_this: the current instance of #CRParsingLocation. + *@a_mask: a bitmap that defines which parts of the + *parsing location are to be serialized (line, column or byte offset) + * + *Returns the serialized string or NULL in case of an error. + */ +gchar * +cr_parsing_location_to_string (CRParsingLocation const *a_this, + enum CRParsingLocationSerialisationMask a_mask) +{ + GString *result = NULL ; + gchar *str = NULL ; + + g_return_val_if_fail (a_this, NULL) ; + + if (!a_mask) { + a_mask = DUMP_LINE | DUMP_COLUMN | DUMP_BYTE_OFFSET ; + } + result =g_string_new (NULL) ; + if (!result) + return NULL ; + if (a_mask & DUMP_LINE) { + g_string_append_printf (result, "line:%d ", + a_this->line) ; + } + if (a_mask & DUMP_COLUMN) { + g_string_append_printf (result, "column:%d ", + a_this->column) ; + } + if (a_mask & DUMP_BYTE_OFFSET) { + g_string_append_printf (result, "byte offset:%d ", + a_this->byte_offset) ; + } + if (result->len) { + str = g_string_free (result, FALSE) ; + } else { + g_string_free (result, TRUE) ; + } + return str ; +} + +/** + * cr_parsing_location_dump: + * @a_this: current instance of #CRParsingLocation + * @a_mask: the serialization mask. + * @a_fp: the file pointer to dump the parsing location to. + */ +void +cr_parsing_location_dump (CRParsingLocation const *a_this, + enum CRParsingLocationSerialisationMask a_mask, + FILE *a_fp) +{ + gchar *str = NULL ; + + g_return_if_fail (a_this && a_fp) ; + str = cr_parsing_location_to_string (a_this, a_mask) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + * cr_parsing_location_destroy: + *@a_this: the current instance of #CRParsingLocation. Must + *have been allocated with cr_parsing_location_new(). + * + *Destroys the current instance of #CRParsingLocation + */ +void +cr_parsing_location_destroy (CRParsingLocation *a_this) +{ + g_return_if_fail (a_this) ; + g_free (a_this) ; +} + diff --git a/src/st/croco/cr-parsing-location.h b/src/st/croco/cr-parsing-location.h new file mode 100644 index 0000000..b8064a5 --- /dev/null +++ b/src/st/croco/cr-parsing-location.h @@ -0,0 +1,70 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli. + * See the COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_PARSING_LOCATION_H__ +#define __CR_PARSING_LOCATION_H__ + +#include "cr-utils.h" + +G_BEGIN_DECLS + +/** + *@file + *The declaration of the CRParsingLocation + *object. This object keeps track of line/column/byte offset/ + *at which the parsing of a given CSS construction appears. + */ + +typedef struct _CRParsingLocation CRParsingLocation; +struct _CRParsingLocation { + guint line ; + guint column ; + guint byte_offset ; +} ; + + +enum CRParsingLocationSerialisationMask { + DUMP_LINE = 1, + DUMP_COLUMN = 1 << 1, + DUMP_BYTE_OFFSET = 1 << 2 +} ; + +CRParsingLocation * cr_parsing_location_new (void) ; + +enum CRStatus cr_parsing_location_init (CRParsingLocation *a_this) ; + +enum CRStatus cr_parsing_location_copy (CRParsingLocation *a_to, + CRParsingLocation const *a_from) ; + +gchar * cr_parsing_location_to_string (CRParsingLocation const *a_this, + enum CRParsingLocationSerialisationMask a_mask) ; +void cr_parsing_location_dump (CRParsingLocation const *a_this, + enum CRParsingLocationSerialisationMask a_mask, + FILE *a_fp) ; + +void cr_parsing_location_destroy (CRParsingLocation *a_this) ; + + + +G_END_DECLS +#endif diff --git a/src/st/croco/cr-prop-list.c b/src/st/croco/cr-prop-list.c new file mode 100644 index 0000000..03c4478 --- /dev/null +++ b/src/st/croco/cr-prop-list.c @@ -0,0 +1,404 @@ +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +#include <string.h> +#include "cr-prop-list.h" + +#define PRIVATE(a_obj) (a_obj)->priv + +struct _CRPropListPriv { + CRString *prop; + CRDeclaration *decl; + CRPropList *next; + CRPropList *prev; +}; + +static CRPropList *cr_prop_list_allocate (void); + +/** + *Default allocator of CRPropList + *@return the newly allocated CRPropList or NULL + *if an error arises. + */ +static CRPropList * +cr_prop_list_allocate (void) +{ + CRPropList *result = NULL; + + result = g_try_malloc (sizeof (CRPropList)); + if (!result) { + cr_utils_trace_info ("could not allocate CRPropList"); + return NULL; + } + memset (result, 0, sizeof (CRPropList)); + PRIVATE (result) = g_try_malloc (sizeof (CRPropListPriv)); + if (!result) { + cr_utils_trace_info ("could not allocate CRPropListPriv"); + g_free (result); + return NULL; + } + memset (PRIVATE (result), 0, sizeof (CRPropListPriv)); + return result; +} + +/**************** + *public methods + ***************/ + +/** + * cr_prop_list_append: + *@a_this: the current instance of #CRPropList + *@a_to_append: the property list to append + * + *Appends a property list to the current one. + * + *Returns the resulting prop list, or NULL if an error + *occurred + */ +CRPropList * +cr_prop_list_append (CRPropList * a_this, CRPropList * a_to_append) +{ + CRPropList *cur = NULL; + + g_return_val_if_fail (a_to_append, NULL); + + if (!a_this) + return a_to_append; + + /*go fetch the last element of the list */ + for (cur = a_this; + cur && PRIVATE (cur) && PRIVATE (cur)->next; + cur = PRIVATE (cur)->next) ; + g_return_val_if_fail (cur, NULL); + PRIVATE (cur)->next = a_to_append; + PRIVATE (a_to_append)->prev = cur; + return a_this; +} + +/** + * cr_prop_list_append2: + *Appends a pair of prop/declaration to + *the current prop list. + *@a_this: the current instance of #CRPropList + *@a_prop: the property to consider + *@a_decl: the declaration to consider + * + *Returns the resulting property list, or NULL in case + *of an error. + */ +CRPropList * +cr_prop_list_append2 (CRPropList * a_this, + CRString * a_prop, + CRDeclaration * a_decl) +{ + CRPropList *list = NULL, + *result = NULL; + + g_return_val_if_fail (a_prop && a_decl, NULL); + + list = cr_prop_list_allocate (); + g_return_val_if_fail (list && PRIVATE (list), NULL); + + PRIVATE (list)->prop = a_prop; + PRIVATE (list)->decl = a_decl; + + result = cr_prop_list_append (a_this, list); + return result; +} + +/** + * cr_prop_list_prepend: + *@a_this: the current instance of #CRPropList + *@a_to_prepend: the new list to prepend. + * + *Prepends a list to the current list + *Returns the new properties list. + */ +CRPropList * +cr_prop_list_prepend (CRPropList * a_this, CRPropList * a_to_prepend) +{ + CRPropList *cur = NULL; + + g_return_val_if_fail (a_to_prepend, NULL); + + if (!a_this) + return a_to_prepend; + + for (cur = a_to_prepend; cur && PRIVATE (cur)->next; + cur = PRIVATE (cur)->next) ; + g_return_val_if_fail (cur, NULL); + PRIVATE (cur)->next = a_this; + PRIVATE (a_this)->prev = cur; + return a_to_prepend; +} + +/** + * cr_prop_list_prepend2: + *@a_this: the current instance of #CRPropList + *@a_prop_name: property name to append + *@a_decl: the property value to append. + * + *Prepends a property to a list of properties + * + *Returns the new property list. + */ +CRPropList * +cr_prop_list_prepend2 (CRPropList * a_this, + CRString * a_prop_name, CRDeclaration * a_decl) +{ + CRPropList *list = NULL, + *result = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_prop_name && a_decl, NULL); + + list = cr_prop_list_allocate (); + g_return_val_if_fail (list, NULL); + PRIVATE (list)->prop = a_prop_name; + PRIVATE (list)->decl = a_decl; + result = cr_prop_list_prepend (a_this, list); + return result; +} + +/** + * cr_prop_list_set_prop: + *@a_this: the current instance of #CRPropList + *@a_prop: the property to set + * + *Sets the property of a CRPropList + */ +enum CRStatus +cr_prop_list_set_prop (CRPropList * a_this, CRString * a_prop) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_prop, CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->prop = a_prop; + return CR_OK; +} + +/** + * cr_prop_list_get_prop: + *@a_this: the current instance of #CRPropList + *@a_prop: out parameter. The returned property + * + *Getter of the property associated to the current instance + *of #CRPropList + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_prop_list_get_prop (CRPropList const * a_this, CRString ** a_prop) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_prop, CR_BAD_PARAM_ERROR); + + *a_prop = PRIVATE (a_this)->prop; + return CR_OK; +} + +/** + * cr_prop_list_set_decl: + * @a_this: the current instance of #CRPropList + * @a_decl: the new property value. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_prop_list_set_decl (CRPropList * a_this, CRDeclaration * a_decl) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_decl, CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->decl = a_decl; + return CR_OK; +} + +/** + * cr_prop_list_get_decl: + * @a_this: the current instance of #CRPropList + * @a_decl: out parameter. The property value + * + * Returns CR_OK upon successful completion. + */ +enum CRStatus +cr_prop_list_get_decl (CRPropList const * a_this, CRDeclaration ** a_decl) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_decl, CR_BAD_PARAM_ERROR); + + *a_decl = PRIVATE (a_this)->decl; + return CR_OK; +} + +/** + * cr_prop_list_lookup_prop: + *@a_this: the current instance of #CRPropList + *@a_prop: the property to lookup + *@a_prop_list: out parameter. The property/declaration + *pair found (if and only if the function returned code if CR_OK) + * + *Lookup a given property/declaration pair + * + *Returns CR_OK if a prop/decl pair has been found, + *CR_VALUE_NOT_FOUND_ERROR if not, or an error code if something + *bad happens. + */ +enum CRStatus +cr_prop_list_lookup_prop (CRPropList * a_this, + CRString * a_prop, CRPropList ** a_pair) +{ + CRPropList *cur = NULL; + + g_return_val_if_fail (a_prop && a_pair, CR_BAD_PARAM_ERROR); + + if (!a_this) + return CR_VALUE_NOT_FOUND_ERROR; + + g_return_val_if_fail (PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + for (cur = a_this; cur; cur = PRIVATE (cur)->next) { + if (PRIVATE (cur)->prop + && PRIVATE (cur)->prop->stryng + && PRIVATE (cur)->prop->stryng->str + && a_prop->stryng + && a_prop->stryng->str + && !strcmp (PRIVATE (cur)->prop->stryng->str, + a_prop->stryng->str)) + break; + } + + if (cur) { + *a_pair = cur; + return CR_OK; + } + + return CR_VALUE_NOT_FOUND_ERROR; +} + +/** + * cr_prop_list_get_next: + *@a_this: the current instance of CRPropList + * + *Gets the next prop/decl pair in the list + * + *Returns the next prop/declaration pair of the list, + *or NULL if we reached end of list (or if an error occurs) + */ +CRPropList * +cr_prop_list_get_next (CRPropList * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), NULL); + + return PRIVATE (a_this)->next; +} + +/** + * cr_prop_list_get_prev: + *@a_this: the current instance of CRPropList + * + *Gets the previous prop/decl pair in the list + * + *Returns the previous prop/declaration pair of the list, + *or NULL if we reached end of list (or if an error occurs) + */ +CRPropList * +cr_prop_list_get_prev (CRPropList * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), NULL); + + return PRIVATE (a_this)->prev; +} + +/** + * cr_prop_list_unlink: + *@a_this: the current list of prop/decl pairs + *@a_pair: the prop/decl pair to unlink. + * + *Unlinks a prop/decl pair from the list + * + *Returns the new list or NULL in case of an error. + */ +CRPropList * +cr_prop_list_unlink (CRPropList * a_this, CRPropList * a_pair) +{ + CRPropList *prev = NULL, + *next = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_pair, NULL); + + /*some sanity checks */ + if (PRIVATE (a_pair)->next) { + next = PRIVATE (a_pair)->next; + g_return_val_if_fail (PRIVATE (next), NULL); + g_return_val_if_fail (PRIVATE (next)->prev == a_pair, NULL); + } + if (PRIVATE (a_pair)->prev) { + prev = PRIVATE (a_pair)->prev; + g_return_val_if_fail (PRIVATE (prev), NULL); + g_return_val_if_fail (PRIVATE (prev)->next == a_pair, NULL); + } + if (prev) { + PRIVATE (prev)->next = next; + } + if (next) { + PRIVATE (next)->prev = prev; + } + PRIVATE (a_pair)->prev = PRIVATE (a_pair)->next = NULL; + if (a_this == a_pair) { + if (next) + return next; + return NULL; + } + return a_this; +} + +/** + * cr_prop_list_destroy: + * @a_this: the current instance of #CRPropList + */ +void +cr_prop_list_destroy (CRPropList * a_this) +{ + CRPropList *tail = NULL, + *cur = NULL; + + g_return_if_fail (a_this && PRIVATE (a_this)); + + for (tail = a_this; + tail && PRIVATE (tail) && PRIVATE (tail)->next; + tail = cr_prop_list_get_next (tail)) ; + g_return_if_fail (tail); + + cur = tail; + + while (cur) { + tail = PRIVATE (cur)->prev; + if (tail && PRIVATE (tail)) + PRIVATE (tail)->next = NULL; + PRIVATE (cur)->prev = NULL; + g_free (PRIVATE (cur)); + PRIVATE (cur) = NULL; + g_free (cur); + cur = tail; + } +} diff --git a/src/st/croco/cr-prop-list.h b/src/st/croco/cr-prop-list.h new file mode 100644 index 0000000..797ba43 --- /dev/null +++ b/src/st/croco/cr-prop-list.h @@ -0,0 +1,80 @@ +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +#ifndef __CR_PROP_LIST_H__ +#define __CR_PROP_LIST_H__ + +#include "cr-utils.h" +#include "cr-declaration.h" +#include "cr-string.h" + +G_BEGIN_DECLS + +typedef struct _CRPropList CRPropList ; +typedef struct _CRPropListPriv CRPropListPriv ; + +struct _CRPropList +{ + CRPropListPriv * priv; +} ; + +CRPropList * cr_prop_list_append (CRPropList *a_this, + CRPropList *a_to_append) ; + +CRPropList * cr_prop_list_append2 (CRPropList *a_this, + CRString *a_prop, + CRDeclaration *a_decl) ; + +CRPropList * cr_prop_list_prepend (CRPropList *a_this, + CRPropList *a_to_append) ; + +CRPropList * cr_prop_list_prepend2 (CRPropList *a_this, + CRString *a_prop, + CRDeclaration *a_decl) ; + +enum CRStatus cr_prop_list_set_prop (CRPropList *a_this, + CRString *a_prop) ; + +enum CRStatus cr_prop_list_get_prop (CRPropList const *a_this, + CRString **a_prop) ; + +enum CRStatus cr_prop_list_lookup_prop (CRPropList *a_this, + CRString *a_prop, + CRPropList**a_pair) ; + +CRPropList * cr_prop_list_get_next (CRPropList *a_this) ; + +CRPropList * cr_prop_list_get_prev (CRPropList *a_this) ; + +enum CRStatus cr_prop_list_set_decl (CRPropList *a_this, + CRDeclaration *a_decl); + +enum CRStatus cr_prop_list_get_decl (CRPropList const *a_this, + CRDeclaration **a_decl) ; + +CRPropList * cr_prop_list_unlink (CRPropList *a_this, + CRPropList *a_pair) ; + +void cr_prop_list_destroy (CRPropList *a_this) ; + +G_END_DECLS + +#endif /*__CR_PROP_LIST_H__*/ diff --git a/src/st/croco/cr-pseudo.c b/src/st/croco/cr-pseudo.c new file mode 100644 index 0000000..f81f9a6 --- /dev/null +++ b/src/st/croco/cr-pseudo.c @@ -0,0 +1,166 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include "cr-pseudo.h" + +/** + *@CRPseudo: + *The definition of the #CRPseudo class. + */ + +/** + * cr_pseudo_new: + *Constructor of the #CRPseudo class. + * + *Returns the newly build instance. + */ +CRPseudo * +cr_pseudo_new (void) +{ + CRPseudo *result = NULL; + + result = g_malloc0 (sizeof (CRPseudo)); + + return result; +} + +/** + * cr_pseudo_to_string: + * @a_this: the current instance of #CRPseud. + * + * Returns the serialized pseudo. Caller must free the returned + * string using g_free(). + */ +guchar * +cr_pseudo_to_string (CRPseudo const * a_this) +{ + guchar *result = NULL; + GString *str_buf = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + + if (a_this->type == IDENT_PSEUDO) { + guchar *name = NULL; + + if (a_this->name == NULL) { + goto error; + } + + name = (guchar *) g_strndup (a_this->name->stryng->str, + a_this->name->stryng->len); + + if (name) { + g_string_append (str_buf, (const gchar *) name); + g_free (name); + name = NULL; + } + } else if (a_this->type == FUNCTION_PSEUDO) { + guchar *name = NULL, + *arg = NULL; + + if (a_this->name == NULL) + goto error; + + name = (guchar *) g_strndup (a_this->name->stryng->str, + a_this->name->stryng->len); + + if (a_this->extra) { + arg = (guchar *) g_strndup (a_this->extra->stryng->str, + a_this->extra->stryng->len); + } + + if (name) { + g_string_append_printf (str_buf, "%s(", name); + g_free (name); + name = NULL; + + if (arg) { + g_string_append (str_buf, (const gchar *) arg); + g_free (arg); + arg = NULL; + } + + g_string_append_c (str_buf, ')'); + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; + + error: + g_string_free (str_buf, TRUE); + return NULL; +} + +/** + * cr_pseudo_dump: + *@a_this: the current instance of pseudo + *@a_fp: the destination file pointer. + * + *Dumps the pseudo to a file. + * + */ +void +cr_pseudo_dump (CRPseudo const * a_this, FILE * a_fp) +{ + guchar *tmp_str = NULL; + + if (a_this) { + tmp_str = cr_pseudo_to_string (a_this); + if (tmp_str) { + fprintf (a_fp, "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } +} + +/** + * cr_pseudo_destroy: + *@a_this: the current instance to destroy. + * + *destructor of the #CRPseudo class. + */ +void +cr_pseudo_destroy (CRPseudo * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->name) { + cr_string_destroy (a_this->name); + a_this->name = NULL; + } + + if (a_this->extra) { + cr_string_destroy (a_this->extra); + a_this->extra = NULL; + } + + g_free (a_this); +} diff --git a/src/st/croco/cr-pseudo.h b/src/st/croco/cr-pseudo.h new file mode 100644 index 0000000..8917da4 --- /dev/null +++ b/src/st/croco/cr-pseudo.h @@ -0,0 +1,64 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See COPYRIGHTS file for copyright information + */ + +#ifndef __CR_PSEUDO_H__ +#define __CR_PSEUDO_H__ + +#include <stdio.h> +#include <glib.h> +#include "cr-attr-sel.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +enum CRPseudoType +{ + IDENT_PSEUDO = 0, + FUNCTION_PSEUDO +} ; + +typedef struct _CRPseudo CRPseudo ; + +/** + *The CRPseudo Class. + *Abstract a "pseudo" as defined by the css2 spec + *in appendix D.1 . + */ +struct _CRPseudo +{ + enum CRPseudoType type ; + CRString *name ; + CRString *extra ; + CRParsingLocation location ; +} ; + +CRPseudo * cr_pseudo_new (void) ; + +guchar * cr_pseudo_to_string (CRPseudo const *a_this) ; + +void cr_pseudo_dump (CRPseudo const *a_this, FILE *a_fp) ; + +void cr_pseudo_destroy (CRPseudo *a_this) ; + +G_END_DECLS + +#endif /*__CR_PSEUDO_H__*/ diff --git a/src/st/croco/cr-rgb.c b/src/st/croco/cr-rgb.c new file mode 100644 index 0000000..a2b478f --- /dev/null +++ b/src/st/croco/cr-rgb.c @@ -0,0 +1,604 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyrights information. + */ + +#include "config.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include "cr-rgb.h" +#include "cr-term.h" +#include "cr-parser.h" + +static const CRRgb gv_standard_colors[] = { + {(const guchar*)"aliceblue", 240, 248, 255, FALSE, {0,0,0}}, + {(const guchar*)"antiquewhite", 250, 235, 215, FALSE, {0,0,0}}, + {(const guchar*)"aqua", 0, 255, 255, FALSE, {0,0,0}}, + {(const guchar*)"aquamarine", 127, 255, 212, FALSE, {0,0,0}}, + {(const guchar*)"azure", 240, 255, 255, FALSE, {0,0,0}}, + {(const guchar*)"beige", 245, 245, 220, FALSE, {0,0,0}}, + {(const guchar*)"bisque", 255, 228, 196, FALSE, {0,0,0}}, + {(const guchar*)"black", 0, 0, 0, FALSE, {0,0,0}}, + {(const guchar*)"blanchedalmond", 255, 235, 205, FALSE, {0,0,0}}, + {(const guchar*)"blue", 0, 0, 255, FALSE, {0,0,0}}, + {(const guchar*)"blueviolet", 138, 43, 226, FALSE, {0,0,0}}, + {(const guchar*)"brown", 165, 42, 42, FALSE, {0,0,0}}, + {(const guchar*)"burlywood", 222, 184, 135, FALSE, {0,0,0}}, + {(const guchar*)"cadetblue", 95, 158, 160, FALSE, {0,0,0}}, + {(const guchar*)"chartreuse", 127, 255, 0, FALSE, {0,0,0}}, + {(const guchar*)"chocolate", 210, 105, 30, FALSE, {0,0,0}}, + {(const guchar*)"coral", 255, 127, 80, FALSE, {0,0,0}}, + {(const guchar*)"cornflowerblue", 100, 149, 237, FALSE, {0,0,0}}, + {(const guchar*)"cornsilk", 255, 248, 220, FALSE, {0,0,0}}, + {(const guchar*)"crimson", 220, 20, 60, FALSE, {0,0,0}}, + {(const guchar*)"cyan", 0, 255, 255, FALSE, {0,0,0}}, + {(const guchar*)"darkblue", 0, 0, 139, FALSE, {0,0,0}}, + {(const guchar*)"darkcyan", 0, 139, 139, FALSE, {0,0,0}}, + {(const guchar*)"darkgoldenrod", 184, 134, 11, FALSE, {0,0,0}}, + {(const guchar*)"darkgray", 169, 169, 169, FALSE, {0,0,0}}, + {(const guchar*)"darkgreen", 0, 100, 0, FALSE, {0,0,0}}, + {(const guchar*)"darkgrey", 169, 169, 169, FALSE, {0,0,0}}, + {(const guchar*)"darkkhaki", 189, 183, 107, FALSE, {0,0,0}}, + {(const guchar*)"darkmagenta", 139, 0, 139, FALSE, {0,0,0}}, + {(const guchar*)"darkolivegreen", 85, 107, 47, FALSE, {0,0,0}}, + {(const guchar*)"darkorange", 255, 140, 0, FALSE, {0,0,0}}, + {(const guchar*)"darkorchid", 153, 50, 204, FALSE, {0,0,0}}, + {(const guchar*)"darkred", 139, 0, 0, FALSE, {0,0,0}}, + {(const guchar*)"darksalmon", 233, 150, 122, FALSE, {0,0,0}}, + {(const guchar*)"darkseagreen", 143, 188, 143, FALSE, {0,0,0}}, + {(const guchar*)"darkslateblue", 72, 61, 139, FALSE, {0,0,0}}, + {(const guchar*)"darkslategray", 47, 79, 79, FALSE, {0,0,0}}, + {(const guchar*)"darkslategrey", 47, 79, 79, FALSE, {0,0,0}}, + {(const guchar*)"darkturquoise", 0, 206, 209, FALSE, {0,0,0}}, + {(const guchar*)"darkviolet", 148, 0, 211, FALSE, {0,0,0}}, + {(const guchar*)"deeppink", 255, 20, 147, FALSE, {0,0,0}}, + {(const guchar*)"deepskyblue", 0, 191, 255, FALSE, {0,0,0}}, + {(const guchar*)"dimgray", 105, 105, 105, FALSE, {0,0,0}}, + {(const guchar*)"dimgrey", 105, 105, 105, FALSE, {0,0,0}}, + {(const guchar*)"dodgerblue", 30, 144, 255, FALSE, {0,0,0}}, + {(const guchar*)"firebrick", 178, 34, 34, FALSE, {0,0,0}}, + {(const guchar*)"floralwhite", 255, 250, 240, FALSE, {0,0,0}}, + {(const guchar*)"forestgreen", 34, 139, 34, FALSE, {0,0,0}}, + {(const guchar*)"fuchsia", 255, 0, 255, FALSE, {0,0,0}}, + {(const guchar*)"gainsboro", 220, 220, 220, FALSE, {0,0,0}}, + {(const guchar*)"ghostwhite", 248, 248, 255, FALSE, {0,0,0}}, + {(const guchar*)"gold", 255, 215, 0, FALSE, {0,0,0}}, + {(const guchar*)"goldenrod", 218, 165, 32, FALSE, {0,0,0}}, + {(const guchar*)"gray", 128, 128, 128, FALSE, {0,0,0}}, + {(const guchar*)"green", 0, 128, 0, FALSE, {0,0,0}}, + {(const guchar*)"greenyellow", 173, 255, 47, FALSE, {0,0,0}}, + {(const guchar*)"grey", 128, 128, 128, FALSE, {0,0,0}}, + {(const guchar*)"honeydew", 240, 255, 240, FALSE, {0,0,0}}, + {(const guchar*)"hotpink", 255, 105, 180, FALSE, {0,0,0}}, + {(const guchar*)"indianred", 205, 92, 92, FALSE, {0,0,0}}, + {(const guchar*)"indigo", 75, 0, 130, FALSE, {0,0,0}}, + {(const guchar*)"ivory", 255, 255, 240, FALSE, {0,0,0}}, + {(const guchar*)"khaki", 240, 230, 140, FALSE, {0,0,0}}, + {(const guchar*)"lavender", 230, 230, 250, FALSE, {0,0,0}}, + {(const guchar*)"lavenderblush", 255, 240, 245, FALSE, {0,0,0}}, + {(const guchar*)"lawngreen", 124, 252, 0, FALSE, {0,0,0}}, + {(const guchar*)"lemonchiffon", 255, 250, 205, FALSE, {0,0,0}}, + {(const guchar*)"lightblue", 173, 216, 230, FALSE, {0,0,0}}, + {(const guchar*)"lightcoral", 240, 128, 128, FALSE, {0,0,0}}, + {(const guchar*)"lightcyan", 224, 255, 255, FALSE, {0,0,0}}, + {(const guchar*)"lightgoldenrodyellow", 250, 250, 210, FALSE, {0,0,0}}, + {(const guchar*)"lightgray", 211, 211, 211, FALSE, {0,0,0}}, + {(const guchar*)"lightgreen", 144, 238, 144, FALSE, {0,0,0}}, + {(const guchar*)"lightgrey", 211, 211, 211, FALSE, {0,0,0}}, + {(const guchar*)"lightpink", 255, 182, 193, FALSE, {0,0,0}}, + {(const guchar*)"lightsalmon", 255, 160, 122, FALSE, {0,0,0}}, + {(const guchar*)"lightseagreen", 32, 178, 170, FALSE, {0,0,0}}, + {(const guchar*)"lightskyblue", 135, 206, 250, FALSE, {0,0,0}}, + {(const guchar*)"lightslategray", 119, 136, 153, FALSE, {0,0,0}}, + {(const guchar*)"lightslategrey", 119, 136, 153, FALSE, {0,0,0}}, + {(const guchar*)"lightsteelblue", 176, 196, 222, FALSE, {0,0,0}}, + {(const guchar*)"lightyellow", 255, 255, 224, FALSE, {0,0,0}}, + {(const guchar*)"lime", 0, 255, 0, FALSE, {0,0,0}}, + {(const guchar*)"limegreen", 50, 205, 50, FALSE, {0,0,0}}, + {(const guchar*)"linen", 250, 240, 230, FALSE, {0,0,0}}, + {(const guchar*)"magenta", 255, 0, 255, FALSE, {0,0,0}}, + {(const guchar*)"maroon", 128, 0, 0, FALSE, {0,0,0}}, + {(const guchar*)"mediumaquamarine", 102, 205, 170, FALSE, {0,0,0}}, + {(const guchar*)"mediumblue", 0, 0, 205, FALSE, {0,0,0}}, + {(const guchar*)"mediumorchid", 186, 85, 211, FALSE, {0,0,0}}, + {(const guchar*)"mediumpurple", 147, 112, 219, FALSE, {0,0,0}}, + {(const guchar*)"mediumseagreen", 60, 179, 113, FALSE, {0,0,0}}, + {(const guchar*)"mediumslateblue", 123, 104, 238, FALSE, {0,0,0}}, + {(const guchar*)"mediumspringgreen", 0, 250, 154, FALSE, {0,0,0}}, + {(const guchar*)"mediumturquoise", 72, 209, 204, FALSE, {0,0,0}}, + {(const guchar*)"mediumvioletred", 199, 21, 133, FALSE, {0,0,0}}, + {(const guchar*)"midnightblue", 25, 25, 112, FALSE, {0,0,0}}, + {(const guchar*)"mintcream", 245, 255, 250, FALSE, {0,0,0}}, + {(const guchar*)"mistyrose", 255, 228, 225, FALSE, {0,0,0}}, + {(const guchar*)"moccasin", 255, 228, 181, FALSE, {0,0,0}}, + {(const guchar*)"navajowhite", 255, 222, 173, FALSE, {0,0,0}}, + {(const guchar*)"navy", 0, 0, 128, FALSE, {0,0,0}}, + {(const guchar*)"oldlace", 253, 245, 230, FALSE, {0,0,0}}, + {(const guchar*)"olive", 128, 128, 0, FALSE, {0,0,0}}, + {(const guchar*)"olivedrab", 107, 142, 35, FALSE, {0,0,0}}, + {(const guchar*)"orange", 255, 165, 0, FALSE, {0,0,0}}, + {(const guchar*)"orangered", 255, 69, 0, FALSE, {0,0,0}}, + {(const guchar*)"orchid", 218, 112, 214, FALSE, {0,0,0}}, + {(const guchar*)"palegoldenrod", 238, 232, 170, FALSE, {0,0,0}}, + {(const guchar*)"palegreen", 152, 251, 152, FALSE, {0,0,0}}, + {(const guchar*)"paleturquoise", 175, 238, 238, FALSE, {0,0,0}}, + {(const guchar*)"palevioletred", 219, 112, 147, FALSE, {0,0,0}}, + {(const guchar*)"papayawhip", 255, 239, 213, FALSE, {0,0,0}}, + {(const guchar*)"peachpuff", 255, 218, 185, FALSE, {0,0,0}}, + {(const guchar*)"peru", 205, 133, 63, FALSE, {0,0,0}}, + {(const guchar*)"pink", 255, 192, 203, FALSE, {0,0,0}}, + {(const guchar*)"plum", 221, 160, 221, FALSE, {0,0,0}}, + {(const guchar*)"powderblue", 176, 224, 230, FALSE, {0,0,0}}, + {(const guchar*)"purple", 128, 0, 128, FALSE, {0,0,0}}, + {(const guchar*)"red", 255, 0, 0, FALSE, {0,0,0}}, + {(const guchar*)"rosybrown", 188, 143, 143, FALSE, {0,0,0}}, + {(const guchar*)"royalblue", 65, 105, 225, FALSE, {0,0,0}}, + {(const guchar*)"saddlebrown", 139, 69, 19, FALSE, {0,0,0}}, + {(const guchar*)"salmon", 250, 128, 114, FALSE, {0,0,0}}, + {(const guchar*)"sandybrown", 244, 164, 96, FALSE, {0,0,0}}, + {(const guchar*)"seagreen", 46, 139, 87, FALSE, {0,0,0}}, + {(const guchar*)"seashell", 255, 245, 238, FALSE, {0,0,0}}, + {(const guchar*)"sienna", 160, 82, 45, FALSE, {0,0,0}}, + {(const guchar*)"silver", 192, 192, 192, FALSE, {0,0,0}}, + {(const guchar*)"skyblue", 135, 206, 235, FALSE, {0,0,0}}, + {(const guchar*)"slateblue", 106, 90, 205, FALSE, {0,0,0}}, + {(const guchar*)"slategray", 112, 128, 144, FALSE, {0,0,0}}, + {(const guchar*)"slategrey", 112, 128, 144, FALSE, {0,0,0}}, + {(const guchar*)"snow", 255, 250, 250, FALSE, {0,0,0}}, + {(const guchar*)"springgreen", 0, 255, 127, FALSE, {0,0,0}}, + {(const guchar*)"steelblue", 70, 130, 180, FALSE, {0,0,0}}, + {(const guchar*)"tan", 210, 180, 140, FALSE, {0,0,0}}, + {(const guchar*)"teal", 0, 128, 128, FALSE, {0,0,0}}, + {(const guchar*)"thistle", 216, 191, 216, FALSE, {0,0,0}}, + {(const guchar*)"tomato", 255, 99, 71, FALSE, {0,0,0}}, + {(const guchar*)"turquoise", 64, 224, 208, FALSE, {0,0,0}}, + {(const guchar*)"violet", 238, 130, 238, FALSE, {0,0,0}}, + {(const guchar*)"wheat", 245, 222, 179, FALSE, {0,0,0}}, + {(const guchar*)"white", 255, 255, 255, FALSE, {0,0,0}}, + {(const guchar*)"whitesmoke", 245, 245, 245, FALSE, {0,0,0}}, + {(const guchar*)"yellow", 255, 255, 0, FALSE, {0,0,0}}, + {(const guchar*)"yellowgreen", 154, 205, 50, FALSE, {0,0,0}} +}; + +/** + * cr_rgb_new: + * + *The default constructor of #CRRgb. + * + *Returns the newly built instance of #CRRgb + */ +CRRgb * +cr_rgb_new (void) +{ + CRRgb *result = NULL; + + result = g_try_malloc (sizeof (CRRgb)); + + if (result == NULL) { + cr_utils_trace_info ("No more memory"); + return NULL; + } + + memset (result, 0, sizeof (CRRgb)); + + return result; +} + +/** + * cr_rgb_new_with_vals: + *@a_red: the red component of the color. + *@a_green: the green component of the color. + *@a_blue: the blue component of the color. + *@a_unit: the unit of the rgb values. + *(either percentage or integer values) + * + *A constructor of #CRRgb. + * + *Returns the newly built instance of #CRRgb. + */ +CRRgb * +cr_rgb_new_with_vals (gulong a_red, gulong a_green, + gulong a_blue, gboolean a_is_percentage) +{ + CRRgb *result = NULL; + + result = cr_rgb_new (); + + g_return_val_if_fail (result, NULL); + + result->red = a_red; + result->green = a_green; + result->blue = a_blue; + result->is_percentage = a_is_percentage; + + return result; +} + +/** + * cr_rgb_to_string: + *@a_this: the instance of #CRRgb to serialize. + * + *Serializes the rgb into a zero terminated string. + * + *Returns the zero terminated string containing the serialized + *rgb. MUST BE FREED by the caller using g_free(). + */ +guchar * +cr_rgb_to_string (CRRgb const * a_this) +{ + guchar *result = NULL; + GString *str_buf = NULL; + + str_buf = g_string_new (NULL); + g_return_val_if_fail (str_buf, NULL); + + if (a_this->is_percentage == 1) { + g_string_append_printf (str_buf, "%ld", a_this->red); + + g_string_append (str_buf, "%, "); + + g_string_append_printf (str_buf, "%ld", a_this->green); + g_string_append (str_buf, "%, "); + + g_string_append_printf (str_buf, "%ld", a_this->blue); + g_string_append_c (str_buf, '%'); + } else { + g_string_append_printf (str_buf, "%ld", a_this->red); + g_string_append (str_buf, ", "); + + g_string_append_printf (str_buf, "%ld", a_this->green); + g_string_append (str_buf, ", "); + + g_string_append_printf (str_buf, "%ld", a_this->blue); + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + } + + return result; +} + +/** + * cr_rgb_dump: + *@a_this: the "this pointer" of + *the current instance of #CRRgb. + *@a_fp: the destination file pointer. + * + *Dumps the current instance of #CRRgb + *to a file. + */ +void +cr_rgb_dump (CRRgb const * a_this, FILE * a_fp) +{ + guchar *str = NULL; + + g_return_if_fail (a_this); + + str = cr_rgb_to_string (a_this); + + if (str) { + fprintf (a_fp, "%s", str); + g_free (str); + str = NULL; + } +} + +/** + * cr_rgb_compute_from_percentage: + *@a_this: the current instance of #CRRgb + * + *If the rgb values are expressed in percentage, + *compute their real value. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_rgb_compute_from_percentage (CRRgb * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + if (a_this->is_percentage == FALSE) + return CR_OK; + a_this->red = a_this->red * 255 / 100; + a_this->green = a_this->green * 255 / 100; + a_this->blue = a_this->blue * 255 / 100; + a_this->is_percentage = FALSE; + return CR_OK; +} + +/** + * cr_rgb_set: + *@a_this: the current instance of #CRRgb. + *@a_red: the red value. + *@a_green: the green value. + *@a_blue: the blue value. + * + *Sets rgb values to the RGB. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_rgb_set (CRRgb * a_this, gulong a_red, + gulong a_green, gulong a_blue, gboolean a_is_percentage) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + if (a_is_percentage != FALSE) { + g_return_val_if_fail (a_red <= 100 + && a_green <= 100 + && a_blue <= 100, CR_BAD_PARAM_ERROR); + } + + a_this->is_percentage = a_is_percentage; + + a_this->red = a_red; + a_this->green = a_green; + a_this->blue = a_blue; + return CR_OK; +} + +/** + * cr_rgb_set_from_rgb: + *@a_this: the current instance of #CRRgb. + *@a_rgb: the rgb to "copy" + * + *Sets the rgb from an other one. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_rgb_set_from_rgb (CRRgb * a_this, CRRgb const * a_rgb) +{ + g_return_val_if_fail (a_this && a_rgb, CR_BAD_PARAM_ERROR); + + cr_rgb_copy (a_this, a_rgb) ; + + return CR_OK; +} + +static int +cr_rgb_color_name_compare (const void *a, + const void *b) +{ + const char *a_color_name = a; + const CRRgb *rgb = b; + + return g_ascii_strcasecmp (a_color_name, (const char *) rgb->name); +} + +/** + * cr_rgb_set_from_name: + * @a_this: the current instance of #CRRgb + * @a_color_name: the color name + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_rgb_set_from_name (CRRgb * a_this, const guchar * a_color_name) +{ + enum CRStatus status = CR_OK; + CRRgb *result; + + g_return_val_if_fail (a_this && a_color_name, CR_BAD_PARAM_ERROR); + + result = bsearch (a_color_name, + gv_standard_colors, + G_N_ELEMENTS (gv_standard_colors), + sizeof (gv_standard_colors[0]), + cr_rgb_color_name_compare); + if (result != NULL) + cr_rgb_set_from_rgb (a_this, result); + else + status = CR_UNKNOWN_TYPE_ERROR; + + return status; +} + +/** + * cr_rgb_set_from_hex_str: + * @a_this: the current instance of #CRRgb + * @a_hex: the hexadecimal value to set. + * + * Returns CR_OK upon successful completion. + */ +enum CRStatus +cr_rgb_set_from_hex_str (CRRgb * a_this, const guchar * a_hex) +{ + enum CRStatus status = CR_OK; + gulong i = 0; + guchar colors[3] = { 0 }; + + g_return_val_if_fail (a_this && a_hex, CR_BAD_PARAM_ERROR); + + if (strlen ((const char *) a_hex) == 3) { + for (i = 0; i < 3; i++) { + if (a_hex[i] >= '0' && a_hex[i] <= '9') { + colors[i] = a_hex[i] - '0'; + colors[i] = (colors[i] << 4) | colors[i]; + } else if (a_hex[i] >= 'a' && a_hex[i] <= 'z') { + colors[i] = 10 + a_hex[i] - 'a'; + colors[i] = (colors[i] << 4) | colors[i]; + } else if (a_hex[i] >= 'A' && a_hex[i] <= 'Z') { + colors[i] = 10 + a_hex[i] - 'A'; + colors[i] = (colors[i] << 4) | colors[i]; + } else { + status = CR_UNKNOWN_TYPE_ERROR; + } + } + } else if (strlen ((const char *) a_hex) == 6) { + for (i = 0; i < 6; i++) { + if (a_hex[i] >= '0' && a_hex[i] <= '9') { + colors[i / 2] <<= 4; + colors[i / 2] |= a_hex[i] - '0'; + status = CR_OK; + } else if (a_hex[i] >= 'a' && a_hex[i] <= 'z') { + colors[i / 2] <<= 4; + colors[i / 2] |= 10 + a_hex[i] - 'a'; + status = CR_OK; + } else if (a_hex[i] >= 'A' && a_hex[i] <= 'Z') { + colors[i / 2] <<= 4; + colors[i / 2] |= 10 + a_hex[i] - 'A'; + status = CR_OK; + } else { + status = CR_UNKNOWN_TYPE_ERROR; + } + } + } else { + status = CR_UNKNOWN_TYPE_ERROR; + } + + if (status == CR_OK) { + status = cr_rgb_set (a_this, colors[0], + colors[1], colors[2], FALSE); + } + return status; +} + +/** + * cr_rgb_set_from_term: + *@a_this: the instance of #CRRgb to set + *@a_value: the terminal from which to set + * + *Set the rgb from a terminal symbol + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_rgb_set_from_term (CRRgb *a_this, const struct _CRTerm *a_value) +{ + enum CRStatus status = CR_OK ; + g_return_val_if_fail (a_this && a_value, + CR_BAD_PARAM_ERROR) ; + + switch(a_value->type) { + case TERM_RGB: + if (a_value->content.rgb) { + cr_rgb_set_from_rgb + (a_this, a_value->content.rgb) ; + } + break ; + case TERM_IDENT: + if (a_value->content.str + && a_value->content.str->stryng + && a_value->content.str->stryng->str) { + status = cr_rgb_set_from_name + (a_this, + (const guchar *) a_value->content.str->stryng->str) ; + } else { + cr_utils_trace_info + ("a_value has NULL string value") ; + } + break ; + case TERM_HASH: + if (a_value->content.str + && a_value->content.str->stryng + && a_value->content.str->stryng->str) { + status = cr_rgb_set_from_hex_str + (a_this, + (const guchar *) a_value->content.str->stryng->str) ; + } else { + cr_utils_trace_info + ("a_value has NULL string value") ; + } + break ; + default: + status = CR_UNKNOWN_TYPE_ERROR ; + } + return status ; +} + +enum CRStatus +cr_rgb_copy (CRRgb *a_dest, CRRgb const *a_src) +{ + g_return_val_if_fail (a_dest && a_src, + CR_BAD_PARAM_ERROR) ; + + memcpy (a_dest, a_src, sizeof (CRRgb)) ; + return CR_OK ; +} + +/** + * cr_rgb_destroy: + *@a_this: the "this pointer" of the + *current instance of #CRRgb. + * + *Destructor of #CRRgb. + */ +void +cr_rgb_destroy (CRRgb * a_this) +{ + g_return_if_fail (a_this); + g_free (a_this); +} + +/** + * cr_rgb_parse_from_buf: + *@a_str: a string that contains a color description + *@a_enc: the encoding of a_str + * + *Parses a text buffer that contains a rgb color + * + *Returns the parsed color, or NULL in case of error + */ +CRRgb * +cr_rgb_parse_from_buf (const guchar *a_str, + enum CREncoding a_enc) +{ + enum CRStatus status = CR_OK ; + CRTerm *value = NULL ; + CRParser * parser = NULL; + CRRgb *result = NULL; + + g_return_val_if_fail (a_str, NULL); + + parser = cr_parser_new_from_buf ((guchar *) a_str, strlen ((const char *) a_str), a_enc, FALSE); + + g_return_val_if_fail (parser, NULL); + + status = cr_parser_try_to_skip_spaces_and_comments (parser) ; + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_term (parser, &value); + if (status != CR_OK) + goto cleanup; + + result = cr_rgb_new (); + if (!result) + goto cleanup; + + status = cr_rgb_set_from_term (result, value); + +cleanup: + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + if (value) { + cr_term_destroy(value); + value = NULL; + } + return result ; +} + diff --git a/src/st/croco/cr-rgb.h b/src/st/croco/cr-rgb.h new file mode 100644 index 0000000..a77e309 --- /dev/null +++ b/src/st/croco/cr-rgb.h @@ -0,0 +1,84 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * see COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_RGB_H__ +#define __CR_RGB_H__ + +#include <stdio.h> +#include <glib.h> +#include "cr-utils.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + + +typedef struct _CRRgb CRRgb ; +struct _CRRgb +{ + /* + *the unit of the rgb. + *Either NO_UNIT (integer) or + *UNIT_PERCENTAGE (percentage). + */ + const guchar *name ; + glong red ; + glong green ; + glong blue ; + gboolean is_percentage ; + CRParsingLocation location ; +} ; + +CRRgb * cr_rgb_new (void) ; + +CRRgb * cr_rgb_new_with_vals (gulong a_red, gulong a_green, + gulong a_blue, gboolean a_is_percentage) ; + +CRRgb *cr_rgb_parse_from_buf(const guchar *a_str, + enum CREncoding a_enc); + +enum CRStatus cr_rgb_compute_from_percentage (CRRgb *a_this) ; + +enum CRStatus cr_rgb_set (CRRgb *a_this, gulong a_red, + gulong a_green, gulong a_blue, + gboolean a_is_percentage) ; + +enum CRStatus cr_rgb_copy (CRRgb *a_dest, CRRgb const *a_src) ; + +enum CRStatus cr_rgb_set_from_rgb (CRRgb *a_this, CRRgb const *a_rgb) ; + +enum CRStatus cr_rgb_set_from_name (CRRgb *a_this, const guchar *a_color_name) ; + +enum CRStatus cr_rgb_set_from_hex_str (CRRgb *a_this, const guchar * a_hex_value) ; + +struct _CRTerm; + +enum CRStatus cr_rgb_set_from_term (CRRgb *a_this, const struct _CRTerm *a_value); + +guchar * cr_rgb_to_string (CRRgb const *a_this) ; + +void cr_rgb_dump (CRRgb const *a_this, FILE *a_fp) ; + +void cr_rgb_destroy (CRRgb *a_this) ; + +G_END_DECLS + +#endif /*__CR_RGB_H__*/ diff --git a/src/st/croco/cr-selector.c b/src/st/croco/cr-selector.c new file mode 100644 index 0000000..c9aad43 --- /dev/null +++ b/src/st/croco/cr-selector.c @@ -0,0 +1,305 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See COPYRIGHTS file for copyright information. + */ + +#include <string.h> +#include "cr-selector.h" +#include "cr-parser.h" + +/** + * cr_selector_new: + * + *@a_simple_sel: the initial simple selector list + *of the current instance of #CRSelector. + * + *Creates a new instance of #CRSelector. + * + *Returns the newly built instance of #CRSelector, or + *NULL in case of failure. + */ +CRSelector * +cr_selector_new (CRSimpleSel * a_simple_sel) +{ + CRSelector *result = NULL; + + result = g_try_malloc (sizeof (CRSelector)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRSelector)); + result->simple_sel = a_simple_sel; + return result; +} + +CRSelector * +cr_selector_parse_from_buf (const guchar * a_char_buf, enum CREncoding a_enc) +{ + CRParser *parser = NULL; + + g_return_val_if_fail (a_char_buf, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_char_buf, strlen ((const char *) a_char_buf), + a_enc, FALSE); + g_return_val_if_fail (parser, NULL); + + return NULL; +} + +/** + * cr_selector_append: + * + *@a_this: the current instance of #CRSelector. + *@a_new: the instance of #CRSelector to be appended. + * + *Appends a new instance of #CRSelector to the current selector list. + * + *Returns the new list. + */ +CRSelector * +cr_selector_append (CRSelector * a_this, CRSelector * a_new) +{ + CRSelector *cur = NULL; + + if (!a_this) { + return a_new; + } + + /*walk forward the list headed by a_this to get the list tail */ + for (cur = a_this; cur && cur->next; cur = cur->next) ; + + cur->next = a_new; + a_new->prev = cur; + + return a_this; +} + +/** + * cr_selector_prepend: + * + *@a_this: the current instance of #CRSelector list. + *@a_new: the instance of #CRSelector. + * + *Prepends an element to the #CRSelector list. + * + *Returns the new list. + */ +CRSelector * +cr_selector_prepend (CRSelector * a_this, CRSelector * a_new) +{ + CRSelector *cur = NULL; + + a_new->next = a_this; + a_this->prev = a_new; + + for (cur = a_new; cur && cur->prev; cur = cur->prev) ; + + return cur; +} + +/** + * cr_selector_append_simple_sel: + * + *@a_this: the current instance of #CRSelector. + *@a_simple_sel: the simple selector to append. + * + *append a simple selector to the current #CRSelector list. + * + *Returns the new list or NULL in case of failure. + */ +CRSelector * +cr_selector_append_simple_sel (CRSelector * a_this, + CRSimpleSel * a_simple_sel) +{ + CRSelector *selector = NULL; + + selector = cr_selector_new (a_simple_sel); + g_return_val_if_fail (selector, NULL); + + return cr_selector_append (a_this, selector); +} + +guchar * +cr_selector_to_string (CRSelector const * a_this) +{ + guchar *result = NULL; + GString *str_buf = NULL; + + str_buf = g_string_new (NULL); + g_return_val_if_fail (str_buf, NULL); + + if (a_this) { + CRSelector const *cur = NULL; + + for (cur = a_this; cur; cur = cur->next) { + if (cur->simple_sel) { + guchar *tmp_str = NULL; + + tmp_str = cr_simple_sel_to_string + (cur->simple_sel); + + if (tmp_str) { + if (cur->prev) + g_string_append (str_buf, + ", "); + + g_string_append (str_buf, (const gchar *) tmp_str); + + g_free (tmp_str); + tmp_str = NULL; + } + } + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +/** + * cr_selector_dump: + * + *@a_this: the current instance of #CRSelector. + *@a_fp: the destination file. + * + *Serializes the current instance of #CRSelector to a file. + */ +void +cr_selector_dump (CRSelector const * a_this, FILE * a_fp) +{ + guchar *tmp_buf = NULL; + + if (a_this) { + tmp_buf = cr_selector_to_string (a_this); + if (tmp_buf) { + fprintf (a_fp, "%s", tmp_buf); + g_free (tmp_buf); + tmp_buf = NULL; + } + } +} + +/** + * cr_selector_ref: + * + *@a_this: the current instance of #CRSelector. + * + *Increments the ref count of the current instance + *of #CRSelector. + */ +void +cr_selector_ref (CRSelector * a_this) +{ + g_return_if_fail (a_this); + + a_this->ref_count++; +} + +/** + * cr_selector_unref: + * + *@a_this: the current instance of #CRSelector. + * + *Decrements the ref count of the current instance of + *#CRSelector. + *If the ref count reaches zero, the current instance of + *#CRSelector is destroyed. + * + *Returns TRUE if this function destroyed the current instance + *of #CRSelector, FALSE otherwise. + */ +gboolean +cr_selector_unref (CRSelector * a_this) +{ + g_return_val_if_fail (a_this, FALSE); + + if (a_this->ref_count) { + a_this->ref_count--; + } + + if (a_this->ref_count == 0) { + cr_selector_destroy (a_this); + return TRUE; + } + + return FALSE; +} + +/** + * cr_selector_destroy: + * + *@a_this: the current instance of #CRSelector. + * + *Destroys the selector list. + */ +void +cr_selector_destroy (CRSelector * a_this) +{ + CRSelector *cur = NULL; + + g_return_if_fail (a_this); + + /* + *go and get the list tail. In the same time, free + *all the simple selectors contained in the list. + */ + for (cur = a_this; cur && cur->next; cur = cur->next) { + if (cur->simple_sel) { + cr_simple_sel_destroy (cur->simple_sel); + cur->simple_sel = NULL; + } + } + + if (cur) { + if (cur->simple_sel) { + cr_simple_sel_destroy (cur->simple_sel); + cur->simple_sel = NULL; + } + } + + /*in case the list has only one element */ + if (cur && !cur->prev) { + g_free (cur); + return; + } + + /*walk backward the list and free each "next element" */ + for (cur = cur->prev; cur && cur->prev; cur = cur->prev) { + if (cur->next) { + g_free (cur->next); + cur->next = NULL; + } + } + + if (!cur) + return; + + if (cur->next) { + g_free (cur->next); + cur->next = NULL; + } + + g_free (cur); +} diff --git a/src/st/croco/cr-selector.h b/src/st/croco/cr-selector.h new file mode 100644 index 0000000..dd6a7f7 --- /dev/null +++ b/src/st/croco/cr-selector.h @@ -0,0 +1,95 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_SELECTOR_H__ +#define __CR_SELECTOR_H__ + +#include <stdio.h> +#include "cr-utils.h" +#include "cr-simple-sel.h" +#include "cr-parsing-location.h" + +/** + *@file + *The declaration file of the #CRSelector file. + */ + +G_BEGIN_DECLS + +typedef struct _CRSelector CRSelector ; + +/** + *Abstracts a CSS2 selector as defined in the right part + *of the 'ruleset" production in the appendix D.1 of the + *css2 spec. + *It is actually the abstraction of a comma separated list + *of simple selectors list. + *In a css2 file, a selector is a list of simple selectors + *separated by a comma. + *e.g: sel0, sel1, sel2 ... + *Each seln is a simple selector + */ +struct _CRSelector +{ + /** + *A Selection expression. + *It is a list of basic selectors. + *Each basic selector can be either an element + *selector, an id selector, a class selector, an + *attribute selector, an universal selector etc ... + */ + CRSimpleSel *simple_sel ; + + /**The next selector list element*/ + CRSelector *next ; + CRSelector *prev ; + CRParsingLocation location ; + glong ref_count ; +}; + +CRSelector* cr_selector_new (CRSimpleSel *a_sel_expr) ; + +CRSelector * cr_selector_parse_from_buf (const guchar * a_char_buf, + enum CREncoding a_enc) ; + +CRSelector* cr_selector_append (CRSelector *a_this, CRSelector *a_new) ; + +CRSelector* cr_selector_append_simple_sel (CRSelector *a_this, + CRSimpleSel *a_simple_sel) ; + +CRSelector* cr_selector_prepend (CRSelector *a_this, CRSelector *a_new) ; + +guchar * cr_selector_to_string (CRSelector const *a_this) ; + +void cr_selector_dump (CRSelector const *a_this, FILE *a_fp) ; + +void cr_selector_ref (CRSelector *a_this) ; + +gboolean cr_selector_unref (CRSelector *a_this) ; + +void cr_selector_destroy (CRSelector *a_this) ; + +G_END_DECLS + +#endif /*__CR_SELECTOR_H__*/ diff --git a/src/st/croco/cr-simple-sel.c b/src/st/croco/cr-simple-sel.c new file mode 100644 index 0000000..bac8621 --- /dev/null +++ b/src/st/croco/cr-simple-sel.c @@ -0,0 +1,323 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include <string.h> +#include <glib.h> +#include "cr-simple-sel.h" + +/** + * cr_simple_sel_new: + * + *The constructor of #CRSimpleSel. + * + *Returns the new instance of #CRSimpleSel. + */ +CRSimpleSel * +cr_simple_sel_new (void) +{ + CRSimpleSel *result = NULL; + + result = g_try_malloc (sizeof (CRSimpleSel)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRSimpleSel)); + + return result; +} + +/** + * cr_simple_sel_append_simple_sel: + * + *Appends a simpe selector to the current list of simple selector. + * + *@a_this: the this pointer of the current instance of #CRSimpleSel. + *@a_sel: the simple selector to append. + * + *Returns: the new list upon successful completion, an error code otherwise. + */ +CRSimpleSel * +cr_simple_sel_append_simple_sel (CRSimpleSel * a_this, CRSimpleSel * a_sel) +{ + CRSimpleSel *cur = NULL; + + g_return_val_if_fail (a_sel, NULL); + + if (a_this == NULL) + return a_sel; + + for (cur = a_this; cur->next; cur = cur->next) ; + + cur->next = a_sel; + a_sel->prev = cur; + + return a_this; +} + +/** + * cr_simple_sel_prepend_simple_sel: + * + *@a_this: the this pointer of the current instance of #CRSimpleSel. + *@a_sel: the simple selector to prepend. + * + *Prepends a simple selector to the current list of simple selectors. + * + *Returns the new list upon successful completion, an error code otherwise. + */ +CRSimpleSel * +cr_simple_sel_prepend_simple_sel (CRSimpleSel * a_this, CRSimpleSel * a_sel) +{ + g_return_val_if_fail (a_sel, NULL); + + if (a_this == NULL) + return a_sel; + + a_sel->next = a_this; + a_this->prev = a_sel; + + return a_sel; +} + +guchar * +cr_simple_sel_to_string (CRSimpleSel const * a_this) +{ + GString *str_buf = NULL; + guchar *result = NULL; + + CRSimpleSel const *cur = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + for (cur = a_this; cur; cur = cur->next) { + if (cur->name) { + guchar *str = (guchar *) g_strndup (cur->name->stryng->str, + cur->name->stryng->len); + + if (str) { + switch (cur->combinator) { + case COMB_WS: + g_string_append (str_buf, " "); + break; + + case COMB_PLUS: + g_string_append (str_buf, "+"); + break; + + case COMB_GT: + g_string_append (str_buf, ">"); + break; + + default: + break; + } + + g_string_append (str_buf, (const gchar *) str); + g_free (str); + str = NULL; + } + } + + if (cur->add_sel) { + guchar *tmp_str = NULL; + + tmp_str = cr_additional_sel_to_string (cur->add_sel); + if (tmp_str) { + g_string_append (str_buf, (const gchar *) tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + + +guchar * +cr_simple_sel_one_to_string (CRSimpleSel const * a_this) +{ + GString *str_buf = NULL; + guchar *result = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + if (a_this->name) { + guchar *str = (guchar *) g_strndup (a_this->name->stryng->str, + a_this->name->stryng->len); + + if (str) { + g_string_append_printf (str_buf, "%s", str); + g_free (str); + str = NULL; + } + } + + if (a_this->add_sel) { + guchar *tmp_str = NULL; + + tmp_str = cr_additional_sel_to_string (a_this->add_sel); + if (tmp_str) { + g_string_append_printf + (str_buf, "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +/** + * cr_simple_sel_dump: + *@a_this: the current instance of #CRSimpleSel. + *@a_fp: the destination file pointer. + * + *Dumps the selector to a file. + *TODO: add the support of unicode in the dump. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_simple_sel_dump (CRSimpleSel const * a_this, FILE * a_fp) +{ + guchar *tmp_str = NULL; + + g_return_val_if_fail (a_fp, CR_BAD_PARAM_ERROR); + + if (a_this) { + tmp_str = cr_simple_sel_to_string (a_this); + if (tmp_str) { + fprintf (a_fp, "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + + return CR_OK; +} + +/** + * cr_simple_sel_compute_specificity: + * + *@a_this: the current instance of #CRSimpleSel + * + *Computes the selector (combinator separated list of simple selectors) + *as defined in the css2 spec in chapter 6.4.3 + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_simple_sel_compute_specificity (CRSimpleSel * a_this) +{ + CRAdditionalSel const *cur_add_sel = NULL; + CRSimpleSel const *cur_sel = NULL; + gulong a = 0, + b = 0, + c = 0; + + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + for (cur_sel = a_this; cur_sel; cur_sel = cur_sel->next) { + if (cur_sel->type_mask & TYPE_SELECTOR) { + c++; /*hmmh, is this a new language ? */ + } else if (!cur_sel->name + || !cur_sel->name->stryng + || !cur_sel->name->stryng->str) { + if (cur_sel->add_sel->type == + PSEUDO_CLASS_ADD_SELECTOR) { + /* + *this is a pseudo element, and + *the spec says, "ignore pseudo elements". + */ + continue; + } + } + + for (cur_add_sel = cur_sel->add_sel; + cur_add_sel; cur_add_sel = cur_add_sel->next) { + switch (cur_add_sel->type) { + case ID_ADD_SELECTOR: + a++; + break; + + case NO_ADD_SELECTOR: + continue; + + default: + b++; + break; + } + } + } + + /*we suppose a, b and c have 1 to 3 digits */ + a_this->specificity = a * 1000000 + b * 1000 + c; + + return CR_OK; +} + +/** + * cr_simple_sel_destroy: + * + *@a_this: the this pointer of the current instance of #CRSimpleSel. + * + *The destructor of the current instance of + *#CRSimpleSel. + */ +void +cr_simple_sel_destroy (CRSimpleSel * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->name) { + cr_string_destroy (a_this->name); + a_this->name = NULL; + } + + if (a_this->add_sel) { + cr_additional_sel_destroy (a_this->add_sel); + a_this->add_sel = NULL; + } + + if (a_this->next) { + cr_simple_sel_destroy (a_this->next); + } + + if (a_this) { + g_free (a_this); + } +} diff --git a/src/st/croco/cr-simple-sel.h b/src/st/croco/cr-simple-sel.h new file mode 100644 index 0000000..72b15fd --- /dev/null +++ b/src/st/croco/cr-simple-sel.h @@ -0,0 +1,130 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + + +#ifndef __CR_SEL_H__ +#define __CR_SEL_H__ + +#include <stdio.h> +#include <glib.h> +#include "cr-additional-sel.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +/** + *@file + *the declaration of the #CRSimpleSel class. + * + */ +enum Combinator +{ + NO_COMBINATOR, + COMB_WS,/*whitespace: descendent*/ + COMB_PLUS,/*'+': preceded by*/ + COMB_GT/*greater than ('>'): child*/ +} ; + +enum SimpleSelectorType +{ + NO_SELECTOR_TYPE = 0, + UNIVERSAL_SELECTOR = 1, + TYPE_SELECTOR = 1 << 1 +} ; + +typedef struct _CRSimpleSel CRSimpleSel ; + +/** + *The abstraction of a css2 simple selection list + *as defined by the right part of the "selector" production in the + *appendix D.1 of the css2 spec. + *It is basically a list of simple selector, each + *simple selector being separated by a combinator. + * + *In the libcroco's implementation, each simple selector + *is made of at most two parts: + * + *1/An element name or 'type selector' (which can hold a '*' and + *then been called 'universal selector') + * + *2/An additional selector that "specializes" the preceding type or + *universal selector. The additional selector can be either + *an id selector, or a class selector, or an attribute selector. + */ +struct _CRSimpleSel +{ + enum SimpleSelectorType type_mask ; + gboolean is_case_sentive ; + CRString * name ; + /** + *The combinator that separates + *this simple selector from the previous + *one. + */ + enum Combinator combinator ; + + /** + *The additional selector list of the + *current simple selector. + *An additional selector may + *be a class selector, an id selector, + *or an attribute selector. + *Note that this field is a linked list. + */ + CRAdditionalSel *add_sel ; + + /* + *the specificity as specified by + *chapter 6.4.3 of the spec. + */ + gulong specificity ; + + CRSimpleSel *next ; + CRSimpleSel *prev ; + CRParsingLocation location ; +} ; + +CRSimpleSel * cr_simple_sel_new (void) ; + +CRSimpleSel * cr_simple_sel_append_simple_sel (CRSimpleSel *a_this, + CRSimpleSel *a_sel) ; + +CRSimpleSel * cr_simple_sel_prepend_simple_sel (CRSimpleSel *a_this, + CRSimpleSel *a_sel) ; + +guchar * cr_simple_sel_to_string (CRSimpleSel const *a_this) ; + +guchar * cr_simple_sel_one_to_string (CRSimpleSel const * a_this) ; + +enum CRStatus cr_simple_sel_dump (CRSimpleSel const *a_this, FILE *a_fp) ; + +enum CRStatus cr_simple_sel_dump_attr_sel_list (CRSimpleSel const *a_this) ; + +enum CRStatus cr_simple_sel_compute_specificity (CRSimpleSel *a_this) ; + +void cr_simple_sel_destroy (CRSimpleSel *a_this) ; + +G_END_DECLS + + +#endif /*__CR_SIMPLE_SEL_H__*/ diff --git a/src/st/croco/cr-statement.c b/src/st/croco/cr-statement.c new file mode 100644 index 0000000..eaeb49f --- /dev/null +++ b/src/st/croco/cr-statement.c @@ -0,0 +1,2784 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli. + * See COPYRIGHTS files for copyrights information. + */ + +#include <string.h> +#include "cr-statement.h" +#include "cr-parser.h" + +/** + *@file + *Definition of the #CRStatement class. + */ + +#define DECLARATION_INDENT_NB 2 + +static void cr_statement_clear (CRStatement * a_this); + +static void +parse_font_face_start_font_face_cb (CRDocHandler * a_this, + CRParsingLocation *a_location) +{ + CRStatement *stmt = NULL; + enum CRStatus status = CR_OK; + + stmt = cr_statement_new_at_font_face_rule (NULL, NULL); + g_return_if_fail (stmt); + + status = cr_doc_handler_set_ctxt (a_this, stmt); + g_return_if_fail (status == CR_OK); +} + +static void +parse_font_face_unrecoverable_error_cb (CRDocHandler * a_this) +{ + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + enum CRStatus status = CR_OK; + + g_return_if_fail (a_this); + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + if (status != CR_OK) { + cr_utils_trace_info ("Couldn't get parsing context. " + "This may lead to some memory leaks."); + return; + } + if (stmt) { + cr_statement_destroy (stmt); + cr_doc_handler_set_ctxt (a_this, NULL); + return; + } +} + +static void +parse_font_face_property_cb (CRDocHandler * a_this, + CRString * a_name, + CRTerm * a_value, gboolean a_important) +{ + enum CRStatus status = CR_OK; + CRString *name = NULL; + CRDeclaration *decl = NULL; + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + + g_return_if_fail (a_this && a_name); + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + g_return_if_fail (status == CR_OK && stmt); + g_return_if_fail (stmt->type == AT_FONT_FACE_RULE_STMT); + + name = cr_string_dup (a_name) ; + g_return_if_fail (name); + decl = cr_declaration_new (stmt, name, a_value); + if (!decl) { + cr_utils_trace_info ("cr_declaration_new () failed."); + goto error; + } + name = NULL; + + stmt->kind.font_face_rule->decl_list = + cr_declaration_append (stmt->kind.font_face_rule->decl_list, + decl); + if (!stmt->kind.font_face_rule->decl_list) + goto error; + decl = NULL; + + error: + if (decl) { + cr_declaration_unref (decl); + decl = NULL; + } + if (name) { + cr_string_destroy (name); + name = NULL; + } +} + +static void +parse_font_face_end_font_face_cb (CRDocHandler * a_this) +{ + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + enum CRStatus status = CR_OK; + + g_return_if_fail (a_this); + + resultptr = &result; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) resultptr); + g_return_if_fail (status == CR_OK && result); + g_return_if_fail (result->type == AT_FONT_FACE_RULE_STMT); + + status = cr_doc_handler_set_result (a_this, result); + g_return_if_fail (status == CR_OK); +} + +static void +parse_page_start_page_cb (CRDocHandler * a_this, + CRString * a_name, + CRString * a_pseudo_page, + CRParsingLocation *a_location) +{ + CRStatement *stmt = NULL; + enum CRStatus status = CR_OK; + CRString *page_name = NULL, *pseudo_name = NULL ; + + if (a_name) + page_name = cr_string_dup (a_name) ; + if (a_pseudo_page) + pseudo_name = cr_string_dup (a_pseudo_page) ; + + stmt = cr_statement_new_at_page_rule (NULL, NULL, + page_name, + pseudo_name); + page_name = NULL ; + pseudo_name = NULL ; + g_return_if_fail (stmt); + status = cr_doc_handler_set_ctxt (a_this, stmt); + g_return_if_fail (status == CR_OK); +} + +static void +parse_page_unrecoverable_error_cb (CRDocHandler * a_this) +{ + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + enum CRStatus status = CR_OK; + + g_return_if_fail (a_this); + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + if (status != CR_OK) { + cr_utils_trace_info ("Couldn't get parsing context. " + "This may lead to some memory leaks."); + return; + } + if (stmt) { + cr_statement_destroy (stmt); + stmt = NULL; + cr_doc_handler_set_ctxt (a_this, NULL); + } +} + +static void +parse_page_property_cb (CRDocHandler * a_this, + CRString * a_name, + CRTerm * a_expression, gboolean a_important) +{ + CRString *name = NULL; + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + CRDeclaration *decl = NULL; + enum CRStatus status = CR_OK; + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + g_return_if_fail (status == CR_OK && stmt->type == AT_PAGE_RULE_STMT); + + name = cr_string_dup (a_name); + g_return_if_fail (name); + + decl = cr_declaration_new (stmt, name, a_expression); + g_return_if_fail (decl); + decl->important = a_important; + stmt->kind.page_rule->decl_list = + cr_declaration_append (stmt->kind.page_rule->decl_list, decl); + g_return_if_fail (stmt->kind.page_rule->decl_list); +} + +static void +parse_page_end_page_cb (CRDocHandler * a_this, + CRString * a_name, + CRString * a_pseudo_page) +{ + enum CRStatus status = CR_OK; + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + g_return_if_fail (status == CR_OK && stmt); + g_return_if_fail (stmt->type == AT_PAGE_RULE_STMT); + + status = cr_doc_handler_set_result (a_this, stmt); + g_return_if_fail (status == CR_OK); +} + +static void +parse_at_media_start_media_cb (CRDocHandler * a_this, + GList * a_media_list, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + CRStatement *at_media = NULL; + GList *media_list = NULL; + + g_return_if_fail (a_this && a_this->priv); + + if (a_media_list) { + /*duplicate media list */ + media_list = cr_utils_dup_glist_of_cr_string + (a_media_list); + } + + g_return_if_fail (media_list); + + /*make sure cr_statement_new_at_media_rule works in this case. */ + at_media = cr_statement_new_at_media_rule (NULL, NULL, media_list); + + status = cr_doc_handler_set_ctxt (a_this, at_media); + g_return_if_fail (status == CR_OK); + status = cr_doc_handler_set_result (a_this, at_media); + g_return_if_fail (status == CR_OK); +} + +static void +parse_at_media_unrecoverable_error_cb (CRDocHandler * a_this) +{ + enum CRStatus status = CR_OK; + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + + g_return_if_fail (a_this); + + stmtptr = &stmt; + status = cr_doc_handler_get_result (a_this, (gpointer *) stmtptr); + if (status != CR_OK) { + cr_utils_trace_info ("Couldn't get parsing context. " + "This may lead to some memory leaks."); + return; + } + if (stmt) { + cr_statement_destroy (stmt); + stmt = NULL; + cr_doc_handler_set_ctxt (a_this, NULL); + cr_doc_handler_set_result (a_this, NULL); + } +} + +static void +parse_at_media_start_selector_cb (CRDocHandler * a_this, + CRSelector * a_sellist) +{ + enum CRStatus status = CR_OK; + CRStatement *at_media = NULL; + CRStatement **at_media_ptr = NULL; + CRStatement *ruleset = NULL; + + g_return_if_fail (a_this && a_this->priv && a_sellist); + + at_media_ptr = &at_media; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) at_media_ptr); + g_return_if_fail (status == CR_OK && at_media); + g_return_if_fail (at_media->type == AT_MEDIA_RULE_STMT); + ruleset = cr_statement_new_ruleset (NULL, a_sellist, NULL, at_media); + g_return_if_fail (ruleset); + status = cr_doc_handler_set_ctxt (a_this, ruleset); + g_return_if_fail (status == CR_OK); +} + +static void +parse_at_media_property_cb (CRDocHandler * a_this, + CRString * a_name, CRTerm * a_value, + gboolean a_important) +{ + enum CRStatus status = CR_OK; + + /* + *the current ruleset stmt, child of the + *current at-media being parsed. + */ + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + CRDeclaration *decl = NULL; + CRString *name = NULL; + + g_return_if_fail (a_this && a_name); + + name = cr_string_dup (a_name) ; + g_return_if_fail (name); + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, + (gpointer *) stmtptr); + g_return_if_fail (status == CR_OK && stmt); + g_return_if_fail (stmt->type == RULESET_STMT); + + decl = cr_declaration_new (stmt, name, a_value); + g_return_if_fail (decl); + decl->important = a_important; + status = cr_statement_ruleset_append_decl (stmt, decl); + g_return_if_fail (status == CR_OK); +} + +static void +parse_at_media_end_selector_cb (CRDocHandler * a_this, + CRSelector * a_sellist) +{ + enum CRStatus status = CR_OK; + + /* + *the current ruleset stmt, child of the + *current at-media being parsed. + */ + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + + g_return_if_fail (a_this && a_sellist); + + stmtptr = &stmt; + status = cr_doc_handler_get_ctxt (a_this, (gpointer *) stmtptr); + g_return_if_fail (status == CR_OK && stmt + && stmt->type == RULESET_STMT); + g_return_if_fail (stmt->kind.ruleset->parent_media_rule); + + status = cr_doc_handler_set_ctxt + (a_this, stmt->kind.ruleset->parent_media_rule); + g_return_if_fail (status == CR_OK); +} + +static void +parse_at_media_end_media_cb (CRDocHandler * a_this, + GList * a_media_list) +{ + enum CRStatus status = CR_OK; + CRStatement *at_media = NULL; + CRStatement **at_media_ptr = NULL; + + g_return_if_fail (a_this && a_this->priv); + + at_media_ptr = &at_media; + status = cr_doc_handler_get_ctxt (a_this, + (gpointer *) at_media_ptr); + g_return_if_fail (status == CR_OK && at_media); + status = cr_doc_handler_set_result (a_this, at_media); +} + +static void +parse_ruleset_start_selector_cb (CRDocHandler * a_this, + CRSelector * a_sellist) +{ + CRStatement *ruleset = NULL; + + g_return_if_fail (a_this && a_this->priv && a_sellist); + + ruleset = cr_statement_new_ruleset (NULL, a_sellist, NULL, NULL); + g_return_if_fail (ruleset); + + cr_doc_handler_set_result (a_this, ruleset); +} + +static void +parse_ruleset_unrecoverable_error_cb (CRDocHandler * a_this) +{ + CRStatement *stmt = NULL; + CRStatement **stmtptr = NULL; + enum CRStatus status = CR_OK; + + stmtptr = &stmt; + status = cr_doc_handler_get_result (a_this, (gpointer *) stmtptr); + if (status != CR_OK) { + cr_utils_trace_info ("Couldn't get parsing context. " + "This may lead to some memory leaks."); + return; + } + if (stmt) { + cr_statement_destroy (stmt); + stmt = NULL; + cr_doc_handler_set_result (a_this, NULL); + } +} + +static void +parse_ruleset_property_cb (CRDocHandler * a_this, + CRString * a_name, + CRTerm * a_value, gboolean a_important) +{ + enum CRStatus status = CR_OK; + CRStatement *ruleset = NULL; + CRStatement **rulesetptr = NULL; + CRDeclaration *decl = NULL; + CRString *stringue = NULL; + + g_return_if_fail (a_this && a_this->priv && a_name); + + stringue = cr_string_dup (a_name); + g_return_if_fail (stringue); + + rulesetptr = &ruleset; + status = cr_doc_handler_get_result (a_this, (gpointer *) rulesetptr); + g_return_if_fail (status == CR_OK + && ruleset + && ruleset->type == RULESET_STMT); + + decl = cr_declaration_new (ruleset, stringue, a_value); + g_return_if_fail (decl); + decl->important = a_important; + status = cr_statement_ruleset_append_decl (ruleset, decl); + g_return_if_fail (status == CR_OK); +} + +static void +parse_ruleset_end_selector_cb (CRDocHandler * a_this, + CRSelector * a_sellist) +{ + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + enum CRStatus status = CR_OK; + + g_return_if_fail (a_this && a_sellist); + + resultptr = &result; + status = cr_doc_handler_get_result (a_this, (gpointer *) resultptr); + + g_return_if_fail (status == CR_OK + && result + && result->type == RULESET_STMT); +} + +static void +cr_statement_clear (CRStatement * a_this) +{ + g_return_if_fail (a_this); + + switch (a_this->type) { + case AT_RULE_STMT: + break; + case RULESET_STMT: + if (!a_this->kind.ruleset) + return; + if (a_this->kind.ruleset->sel_list) { + cr_selector_unref (a_this->kind.ruleset->sel_list); + a_this->kind.ruleset->sel_list = NULL; + } + if (a_this->kind.ruleset->decl_list) { + cr_declaration_destroy + (a_this->kind.ruleset->decl_list); + a_this->kind.ruleset->decl_list = NULL; + } + g_free (a_this->kind.ruleset); + a_this->kind.ruleset = NULL; + break; + + case AT_IMPORT_RULE_STMT: + if (!a_this->kind.import_rule) + return; + if (a_this->kind.import_rule->url) { + cr_string_destroy + (a_this->kind.import_rule->url) ; + a_this->kind.import_rule->url = NULL; + } + g_free (a_this->kind.import_rule); + a_this->kind.import_rule = NULL; + break; + + case AT_MEDIA_RULE_STMT: + if (!a_this->kind.media_rule) + return; + if (a_this->kind.media_rule->rulesets) { + cr_statement_destroy + (a_this->kind.media_rule->rulesets); + a_this->kind.media_rule->rulesets = NULL; + } + if (a_this->kind.media_rule->media_list) { + GList *cur = NULL; + + for (cur = a_this->kind.media_rule->media_list; + cur; cur = cur->next) { + if (cur->data) { + cr_string_destroy ((CRString *) cur->data); + cur->data = NULL; + } + + } + g_list_free (a_this->kind.media_rule->media_list); + a_this->kind.media_rule->media_list = NULL; + } + g_free (a_this->kind.media_rule); + a_this->kind.media_rule = NULL; + break; + + case AT_PAGE_RULE_STMT: + if (!a_this->kind.page_rule) + return; + + if (a_this->kind.page_rule->decl_list) { + cr_declaration_destroy + (a_this->kind.page_rule->decl_list); + a_this->kind.page_rule->decl_list = NULL; + } + if (a_this->kind.page_rule->name) { + cr_string_destroy + (a_this->kind.page_rule->name); + a_this->kind.page_rule->name = NULL; + } + if (a_this->kind.page_rule->pseudo) { + cr_string_destroy + (a_this->kind.page_rule->pseudo); + a_this->kind.page_rule->pseudo = NULL; + } + g_free (a_this->kind.page_rule); + a_this->kind.page_rule = NULL; + break; + + case AT_CHARSET_RULE_STMT: + if (!a_this->kind.charset_rule) + return; + + if (a_this->kind.charset_rule->charset) { + cr_string_destroy + (a_this->kind.charset_rule->charset); + a_this->kind.charset_rule->charset = NULL; + } + g_free (a_this->kind.charset_rule); + a_this->kind.charset_rule = NULL; + break; + + case AT_FONT_FACE_RULE_STMT: + if (!a_this->kind.font_face_rule) + return; + + if (a_this->kind.font_face_rule->decl_list) { + cr_declaration_unref + (a_this->kind.font_face_rule->decl_list); + a_this->kind.font_face_rule->decl_list = NULL; + } + g_free (a_this->kind.font_face_rule); + a_this->kind.font_face_rule = NULL; + break; + + default: + break; + } +} + +/** + * cr_statement_ruleset_to_string: + * + *@a_this: the current instance of #CRStatement + *@a_indent: the number of whitespace to use for indentation + * + *Serializes the ruleset statement into a string + * + *Returns the newly allocated serialised string. Must be freed + *by the caller, using g_free(). + */ +static gchar * +cr_statement_ruleset_to_string (CRStatement const * a_this, glong a_indent) +{ + GString *stringue = NULL; + gchar *tmp_str = NULL, + *result = NULL; + + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT, NULL); + + stringue = g_string_new (NULL); + + if (a_this->kind.ruleset->sel_list) { + if (a_indent) + cr_utils_dump_n_chars2 (' ', stringue, a_indent); + + tmp_str = + (gchar *) cr_selector_to_string (a_this->kind.ruleset-> + sel_list); + if (tmp_str) { + g_string_append (stringue, tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + g_string_append (stringue, " {\n"); + if (a_this->kind.ruleset->decl_list) { + tmp_str = (gchar *) cr_declaration_list_to_string2 + (a_this->kind.ruleset->decl_list, + a_indent + DECLARATION_INDENT_NB, TRUE); + if (tmp_str) { + g_string_append (stringue, tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + g_string_append (stringue, "\n"); + cr_utils_dump_n_chars2 (' ', stringue, a_indent); + } + g_string_append (stringue, "}"); + result = g_string_free (stringue, FALSE); + + if (tmp_str) { + g_free (tmp_str); + tmp_str = NULL; + } + return result; +} + + +/** + * cr_statement_font_face_rule_to_string: + * + *@a_this: the current instance of #CRStatement to consider + *It must be a font face rule statement. + *@a_indent: the number of white spaces of indentation. + * + *Serializes a font face rule statement into a string. + * + *Returns the serialized string. Must be deallocated by the caller + *using g_free(). + */ +static gchar * +cr_statement_font_face_rule_to_string (CRStatement const * a_this, + glong a_indent) +{ + gchar *result = NULL, *tmp_str = NULL ; + GString *stringue = NULL ; + + g_return_val_if_fail (a_this + && a_this->type == AT_FONT_FACE_RULE_STMT, + NULL); + + if (a_this->kind.font_face_rule->decl_list) { + stringue = g_string_new (NULL) ; + g_return_val_if_fail (stringue, NULL) ; + if (a_indent) + cr_utils_dump_n_chars2 (' ', stringue, + a_indent); + g_string_append (stringue, "@font-face {\n"); + tmp_str = (gchar *) cr_declaration_list_to_string2 + (a_this->kind.font_face_rule->decl_list, + a_indent + DECLARATION_INDENT_NB, TRUE) ; + if (tmp_str) { + g_string_append (stringue, + tmp_str) ; + g_free (tmp_str) ; + tmp_str = NULL ; + } + g_string_append (stringue, "\n}"); + } + if (stringue) { + result = g_string_free (stringue, FALSE); + stringue = NULL ; + } + return result ; +} + + +/** + * cr_statement_charset_to_string: + * + *Serialises an \@charset statement into a string. + *@a_this: the statement to serialize. + *@a_indent: the number of indentation spaces + * + *Returns the serialized charset statement. Must be + *freed by the caller using g_free(). + */ +static gchar * +cr_statement_charset_to_string (CRStatement const *a_this, + gulong a_indent) +{ + gchar *str = NULL ; + GString *stringue = NULL ; + + g_return_val_if_fail (a_this + && a_this->type == AT_CHARSET_RULE_STMT, + NULL) ; + + if (a_this->kind.charset_rule + && a_this->kind.charset_rule->charset + && a_this->kind.charset_rule->charset->stryng + && a_this->kind.charset_rule->charset->stryng->str) { + str = g_strndup (a_this->kind.charset_rule->charset->stryng->str, + a_this->kind.charset_rule->charset->stryng->len); + g_return_val_if_fail (str, NULL); + stringue = g_string_new (NULL) ; + g_return_val_if_fail (stringue, NULL) ; + cr_utils_dump_n_chars2 (' ', stringue, a_indent); + g_string_append_printf (stringue, + "@charset \"%s\" ;", str); + if (str) { + g_free (str); + str = NULL; + } + } + if (stringue) { + str = g_string_free (stringue, FALSE); + } + return str ; +} + + +/** + * cr_statement_at_page_rule_to_string: + * + *Serialises the at page rule statement into a string + *@a_this: the current instance of #CRStatement. Must + *be an "\@page" rule statement. + * + *Returns the serialized string. Must be freed by the caller + */ +static gchar * +cr_statement_at_page_rule_to_string (CRStatement const *a_this, + gulong a_indent) +{ + GString *stringue = NULL; + gchar *result = NULL ; + + stringue = g_string_new (NULL) ; + + cr_utils_dump_n_chars2 (' ', stringue, a_indent) ; + g_string_append (stringue, "@page"); + if (a_this->kind.page_rule->name + && a_this->kind.page_rule->name->stryng) { + g_string_append_printf + (stringue, " %s", + a_this->kind.page_rule->name->stryng->str) ; + } else { + g_string_append (stringue, " "); + } + if (a_this->kind.page_rule->pseudo + && a_this->kind.page_rule->pseudo->stryng) { + g_string_append_printf + (stringue, " :%s", + a_this->kind.page_rule->pseudo->stryng->str) ; + } + if (a_this->kind.page_rule->decl_list) { + gchar *str = NULL ; + g_string_append (stringue, " {\n"); + str = (gchar *) cr_declaration_list_to_string2 + (a_this->kind.page_rule->decl_list, + a_indent + DECLARATION_INDENT_NB, TRUE) ; + if (str) { + g_string_append (stringue, str) ; + g_free (str) ; + str = NULL ; + } + g_string_append (stringue, "\n}\n"); + } + result = g_string_free (stringue, FALSE) ; + stringue = NULL ; + return result ; +} + + +/** + *Serializes an \@media statement. + *@param a_this the current instance of #CRStatement + *@param a_indent the number of spaces of indentation. + *@return the serialized \@media statement. Must be freed + *by the caller using g_free(). + */ +static gchar * +cr_statement_media_rule_to_string (CRStatement const *a_this, + gulong a_indent) +{ + gchar *str = NULL ; + GString *stringue = NULL ; + GList const *cur = NULL; + + g_return_val_if_fail (a_this->type == AT_MEDIA_RULE_STMT, + NULL); + + if (a_this->kind.media_rule) { + stringue = g_string_new (NULL) ; + cr_utils_dump_n_chars2 (' ', stringue, a_indent); + g_string_append (stringue, "@media"); + + for (cur = a_this->kind.media_rule->media_list; cur; + cur = cur->next) { + if (cur->data) { + gchar *str2 = cr_string_dup2 + ((CRString const *) cur->data); + + if (str2) { + if (cur->prev) { + g_string_append + (stringue, + ","); + } + g_string_append_printf + (stringue, + " %s", str2); + g_free (str2); + str2 = NULL; + } + } + } + g_string_append (stringue, " {\n"); + str = cr_statement_list_to_string + (a_this->kind.media_rule->rulesets, + a_indent + DECLARATION_INDENT_NB) ; + if (str) { + g_string_append (stringue, str) ; + g_free (str) ; + str = NULL ; + } + g_string_append (stringue, "\n}"); + } + if (stringue) { + str = g_string_free (stringue, FALSE) ; + } + return str ; +} + + +static gchar * +cr_statement_import_rule_to_string (CRStatement const *a_this, + gulong a_indent) +{ + GString *stringue = NULL ; + gchar *str = NULL; + + g_return_val_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_this->kind.import_rule, + NULL) ; + + if (a_this->kind.import_rule->url + && a_this->kind.import_rule->url->stryng) { + stringue = g_string_new (NULL) ; + g_return_val_if_fail (stringue, NULL) ; + str = g_strndup (a_this->kind.import_rule->url->stryng->str, + a_this->kind.import_rule->url->stryng->len); + cr_utils_dump_n_chars2 (' ', stringue, a_indent); + if (str) { + g_string_append_printf (stringue, + "@import url(\"%s\")", + str); + g_free (str); + str = NULL ; + } else /*there is no url, so no import rule, get out! */ + return NULL; + + if (a_this->kind.import_rule->media_list) { + GList const *cur = NULL; + + for (cur = a_this->kind.import_rule->media_list; + cur; cur = cur->next) { + if (cur->data) { + CRString const *crstr = cur->data; + + if (cur->prev) { + g_string_append + (stringue, ", "); + } + if (crstr + && crstr->stryng + && crstr->stryng->str) { + g_string_append_len + (stringue, + crstr->stryng->str, + crstr->stryng->len) ; + } + } + } + } + g_string_append (stringue, " ;"); + } + if (stringue) { + str = g_string_free (stringue, FALSE) ; + stringue = NULL ; + } + return str ; +} + + +/******************* + *public functions + ******************/ + +/** + * cr_statement_does_buf_parses_against_core: + * + *@a_buf: the buffer to parse. + *@a_encoding: the character encoding of a_buf. + * + *Tries to parse a buffer and says whether if the content of the buffer + *is a css statement as defined by the "Core CSS Grammar" (chapter 4 of the + *css spec) or not. + * + *Returns TRUE if the buffer parses against the core grammar, false otherwise. + */ +gboolean +cr_statement_does_buf_parses_against_core (const guchar * a_buf, + enum CREncoding a_encoding) +{ + CRParser *parser = NULL; + enum CRStatus status = CR_OK; + gboolean result = FALSE; + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + g_return_val_if_fail (parser, FALSE); + + status = cr_parser_set_use_core_grammar (parser, TRUE); + if (status != CR_OK) { + goto cleanup; + } + + status = cr_parser_parse_statement_core (parser); + if (status == CR_OK) { + result = TRUE; + } + + cleanup: + if (parser) { + cr_parser_destroy (parser); + } + + return result; +} + +/** + * cr_statement_parse_from_buf: + * + *@a_buf: the buffer to parse. + *@a_encoding: the character encoding of a_buf. + * + *Parses a buffer that contains a css statement and returns + *an instance of #CRStatement in case of successful parsing. + *TODO: at support of "\@import" rules. + * + *Returns the newly built instance of #CRStatement in case + *of successful parsing, NULL otherwise. + */ +CRStatement * +cr_statement_parse_from_buf (const guchar * a_buf, enum CREncoding a_encoding) +{ + CRStatement *result = NULL; + + /* + *The strategy of this function is "brute force". + *It tries to parse all the types of CRStatement it knows about. + *I could do this a smarter way but I don't have the time now. + *I think I will revisit this when time of performances and + *pull based incremental parsing comes. + */ + + result = cr_statement_ruleset_parse_from_buf (a_buf, a_encoding); + if (!result) { + result = cr_statement_at_charset_rule_parse_from_buf + (a_buf, a_encoding); + } else { + goto out; + } + + if (!result) { + result = cr_statement_at_media_rule_parse_from_buf + (a_buf, a_encoding); + } else { + goto out; + } + + if (!result) { + result = cr_statement_at_charset_rule_parse_from_buf + (a_buf, a_encoding); + } else { + goto out; + } + + if (!result) { + result = cr_statement_font_face_rule_parse_from_buf + (a_buf, a_encoding); + + } else { + goto out; + } + + if (!result) { + result = cr_statement_at_page_rule_parse_from_buf + (a_buf, a_encoding); + } else { + goto out; + } + + if (!result) { + result = cr_statement_at_import_rule_parse_from_buf + (a_buf, a_encoding); + } else { + goto out; + } + + out: + return result; +} + +/** + * cr_statement_ruleset_parse_from_buf: + * + *@a_buf: the buffer to parse. + *@a_enc: the character encoding of a_buf. + * + *Parses a buffer that contains a ruleset statement an instantiates + *a #CRStatement of type RULESET_STMT. + * + *Returns the newly built instance of #CRStatement in case of successful parsing, + *NULL otherwise. + */ +CRStatement * +cr_statement_ruleset_parse_from_buf (const guchar * a_buf, + enum CREncoding a_enc) +{ + enum CRStatus status = CR_OK; + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + CRParser *parser = NULL; + CRDocHandler *sac_handler = NULL; + + g_return_val_if_fail (a_buf, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_enc, FALSE); + + g_return_val_if_fail (parser, NULL); + + sac_handler = cr_doc_handler_new (); + g_return_val_if_fail (parser, NULL); + + sac_handler->start_selector = parse_ruleset_start_selector_cb; + sac_handler->end_selector = parse_ruleset_end_selector_cb; + sac_handler->property = parse_ruleset_property_cb; + sac_handler->unrecoverable_error = + parse_ruleset_unrecoverable_error_cb; + + cr_parser_set_sac_handler (parser, sac_handler); + cr_parser_try_to_skip_spaces_and_comments (parser); + status = cr_parser_parse_ruleset (parser); + if (status != CR_OK) { + goto cleanup; + } + + resultptr = &result; + status = cr_doc_handler_get_result (sac_handler, + (gpointer *) resultptr); + if (!((status == CR_OK) && result)) { + if (result) { + cr_statement_destroy (result); + result = NULL; + } + } + + cleanup: + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + sac_handler = NULL ; + } + if (sac_handler) { + cr_doc_handler_unref (sac_handler); + sac_handler = NULL; + } + return result; +} + +/** + * cr_statement_new_ruleset: + * + *@a_sel_list: the list of #CRSimpleSel (selectors) + *the rule applies to. + *@a_decl_list: the list of instances of #CRDeclaration + *that composes the ruleset. + *@a_media_types: a list of instances of GString that + *describe the media list this ruleset applies to. + * + *Creates a new instance of #CRStatement of type + *#CRRulSet. + * + *Returns the new instance of #CRStatement or NULL if something + *went wrong. + */ +CRStatement * +cr_statement_new_ruleset (CRStyleSheet * a_sheet, + CRSelector * a_sel_list, + CRDeclaration * a_decl_list, + CRStatement * a_parent_media_rule) +{ + CRStatement *result = NULL; + + g_return_val_if_fail (a_sel_list, NULL); + + if (a_parent_media_rule) { + g_return_val_if_fail + (a_parent_media_rule->type == AT_MEDIA_RULE_STMT, + NULL); + g_return_val_if_fail (a_parent_media_rule->kind.media_rule, + NULL); + } + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStatement)); + result->type = RULESET_STMT; + result->kind.ruleset = g_try_malloc (sizeof (CRRuleSet)); + + if (!result->kind.ruleset) { + cr_utils_trace_info ("Out of memory"); + if (result) + g_free (result); + return NULL; + } + + memset (result->kind.ruleset, 0, sizeof (CRRuleSet)); + result->kind.ruleset->sel_list = a_sel_list; + if (a_sel_list) + cr_selector_ref (a_sel_list); + result->kind.ruleset->decl_list = a_decl_list; + + if (a_parent_media_rule) { + result->kind.ruleset->parent_media_rule = a_parent_media_rule; + a_parent_media_rule->kind.media_rule->rulesets = + cr_statement_append + (a_parent_media_rule->kind.media_rule->rulesets, + result); + } + + cr_statement_set_parent_sheet (result, a_sheet); + + return result; +} + +/** + * cr_statement_at_media_rule_parse_from_buf: + * + *@a_buf: the input to parse. + *@a_enc: the encoding of the buffer. + * + *Parses a buffer that contains an "\@media" declaration + *and builds an \@media css statement. + * + *Returns the \@media statement, or NULL if the buffer could not + *be successfully parsed. + */ +CRStatement * +cr_statement_at_media_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_enc) +{ + CRParser *parser = NULL; + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + CRDocHandler *sac_handler = NULL; + enum CRStatus status = CR_OK; + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_enc, FALSE); + if (!parser) { + cr_utils_trace_info ("Instantiation of the parser failed"); + goto cleanup; + } + + sac_handler = cr_doc_handler_new (); + if (!sac_handler) { + cr_utils_trace_info + ("Instantiation of the sac handler failed"); + goto cleanup; + } + + sac_handler->start_media = parse_at_media_start_media_cb; + sac_handler->start_selector = parse_at_media_start_selector_cb; + sac_handler->property = parse_at_media_property_cb; + sac_handler->end_selector = parse_at_media_end_selector_cb; + sac_handler->end_media = parse_at_media_end_media_cb; + sac_handler->unrecoverable_error = + parse_at_media_unrecoverable_error_cb; + + status = cr_parser_set_sac_handler (parser, sac_handler); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_media (parser); + if (status != CR_OK) + goto cleanup; + + resultptr = &result; + status = cr_doc_handler_get_result (sac_handler, + (gpointer *) resultptr); + if (status != CR_OK) + goto cleanup; + + cleanup: + + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + sac_handler = NULL ; + } + if (sac_handler) { + cr_doc_handler_unref (sac_handler); + sac_handler = NULL; + } + + return result; +} + +/** + * cr_statement_new_at_media_rule: + * + *@a_ruleset: the ruleset statements contained + *in the \@media rule. + *@a_media: the media string list. A list of GString pointers. + * + *Instantiates an instance of #CRStatement of type + *AT_MEDIA_RULE_STMT (\@media ruleset). + * + */ +CRStatement * +cr_statement_new_at_media_rule (CRStyleSheet * a_sheet, + CRStatement * a_rulesets, GList * a_media) +{ + CRStatement *result = NULL, + *cur = NULL; + + if (a_rulesets) + g_return_val_if_fail (a_rulesets->type == RULESET_STMT, NULL); + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStatement)); + result->type = AT_MEDIA_RULE_STMT; + + result->kind.media_rule = g_try_malloc (sizeof (CRAtMediaRule)); + if (!result->kind.media_rule) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + memset (result->kind.media_rule, 0, sizeof (CRAtMediaRule)); + result->kind.media_rule->rulesets = a_rulesets; + for (cur = a_rulesets; cur; cur = cur->next) { + if (cur->type != RULESET_STMT || !cur->kind.ruleset) { + cr_utils_trace_info ("Bad parameter a_rulesets. " + "It should be a list of " + "correct ruleset statement only !"); + goto error; + } + cur->kind.ruleset->parent_media_rule = result; + } + + result->kind.media_rule->media_list = a_media; + if (a_sheet) { + cr_statement_set_parent_sheet (result, a_sheet); + } + + return result; + + error: + return NULL; +} + +/** + * cr_statement_new_at_import_rule: + * + *@a_url: the url to connect to the get the file + *to be imported. + *@a_sheet: the imported parsed stylesheet. + * + *Creates a new instance of #CRStatment of type + *#CRAtImportRule. + * + *Returns the newly built instance of #CRStatement. + */ +CRStatement * +cr_statement_new_at_import_rule (CRStyleSheet * a_container_sheet, + CRString * a_url, + GList * a_media_list, + CRStyleSheet * a_imported_sheet) +{ + CRStatement *result = NULL; + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStatement)); + result->type = AT_IMPORT_RULE_STMT; + + result->kind.import_rule = g_try_malloc (sizeof (CRAtImportRule)); + + if (!result->kind.import_rule) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + + memset (result->kind.import_rule, 0, sizeof (CRAtImportRule)); + result->kind.import_rule->url = a_url; + result->kind.import_rule->media_list = a_media_list; + result->kind.import_rule->sheet = a_imported_sheet; + if (a_container_sheet) + cr_statement_set_parent_sheet (result, a_container_sheet); + + return result; +} + +/** + * cr_statement_at_import_rule_parse_from_buf: + * + *@a_buf: the buffer to parse. + *@a_encoding: the encoding of a_buf. + * + *Parses a buffer that contains an "\@import" rule and + *instantiate a #CRStatement of type AT_IMPORT_RULE_STMT + * + *Returns the newly built instance of #CRStatement in case of + *a successful parsing, NULL otherwise. + */ +CRStatement * +cr_statement_at_import_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) +{ + enum CRStatus status = CR_OK; + CRParser *parser = NULL; + CRStatement *result = NULL; + GList *media_list = NULL; + CRString *import_string = NULL; + CRParsingLocation location = {0} ; + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + if (!parser) { + cr_utils_trace_info ("Instantiation of parser failed."); + goto cleanup; + } + + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_import (parser, + &media_list, + &import_string, + &location); + if (status != CR_OK || !import_string) + goto cleanup; + + result = cr_statement_new_at_import_rule (NULL, import_string, + media_list, NULL); + if (result) { + cr_parsing_location_copy (&result->location, + &location) ; + import_string = NULL; + media_list = NULL; + } + + cleanup: + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + if (media_list) { + for (; media_list; + media_list = g_list_next (media_list)) { + if (media_list->data) { + cr_string_destroy ((CRString*)media_list->data); + media_list->data = NULL; + } + } + g_list_free (media_list); + media_list = NULL; + } + if (import_string) { + cr_string_destroy (import_string); + import_string = NULL; + } + + return result; +} + +/** + * cr_statement_new_at_page_rule: + * + *@a_decl_list: a list of instances of #CRDeclarations + *which is actually the list of declarations that applies to + *this page rule. + *@a_selector: the page rule selector. + * + *Creates a new instance of #CRStatement of type + *#CRAtPageRule. + * + *Returns the newly built instance of #CRStatement or NULL + *in case of error. + */ +CRStatement * +cr_statement_new_at_page_rule (CRStyleSheet * a_sheet, + CRDeclaration * a_decl_list, + CRString * a_name, CRString * a_pseudo) +{ + CRStatement *result = NULL; + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStatement)); + result->type = AT_PAGE_RULE_STMT; + + result->kind.page_rule = g_try_malloc (sizeof (CRAtPageRule)); + + if (!result->kind.page_rule) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + + memset (result->kind.page_rule, 0, sizeof (CRAtPageRule)); + if (a_decl_list) { + result->kind.page_rule->decl_list = a_decl_list; + cr_declaration_ref (a_decl_list); + } + result->kind.page_rule->name = a_name; + result->kind.page_rule->pseudo = a_pseudo; + if (a_sheet) + cr_statement_set_parent_sheet (result, a_sheet); + + return result; +} + +/** + * cr_statement_at_page_rule_parse_from_buf: + * + *@a_buf: the character buffer to parse. + *@a_encoding: the character encoding of a_buf. + * + *Parses a buffer that contains an "\@page" production and, + *if the parsing succeeds, builds the page statement. + * + *Returns the newly built at page statement in case of successful parsing, + *NULL otherwise. + */ +CRStatement * +cr_statement_at_page_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) +{ + enum CRStatus status = CR_OK; + CRParser *parser = NULL; + CRDocHandler *sac_handler = NULL; + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + + g_return_val_if_fail (a_buf, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + if (!parser) { + cr_utils_trace_info ("Instantiation of the parser failed."); + goto cleanup; + } + + sac_handler = cr_doc_handler_new (); + if (!sac_handler) { + cr_utils_trace_info + ("Instantiation of the sac handler failed."); + goto cleanup; + } + + sac_handler->start_page = parse_page_start_page_cb; + sac_handler->property = parse_page_property_cb; + sac_handler->end_page = parse_page_end_page_cb; + sac_handler->unrecoverable_error = parse_page_unrecoverable_error_cb; + + status = cr_parser_set_sac_handler (parser, sac_handler); + if (status != CR_OK) + goto cleanup; + + /*Now, invoke the parser to parse the "@page production" */ + cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + status = cr_parser_parse_page (parser); + if (status != CR_OK) + goto cleanup; + + resultptr = &result; + status = cr_doc_handler_get_result (sac_handler, + (gpointer *) resultptr); + + cleanup: + + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + sac_handler = NULL ; + } + if (sac_handler) { + cr_doc_handler_unref (sac_handler); + sac_handler = NULL; + } + return result; +} + +/** + * cr_statement_new_at_charset_rule: + * + *@a_charset: the string representing the charset. + *Note that the newly built instance of #CRStatement becomes + *the owner of a_charset. The caller must not free a_charset !!!. + * + *Creates a new instance of #CRStatement of type + *#CRAtCharsetRule. + * + *Returns the newly built instance of #CRStatement or NULL + *if an error arises. + */ +CRStatement * +cr_statement_new_at_charset_rule (CRStyleSheet * a_sheet, + CRString * a_charset) +{ + CRStatement *result = NULL; + + g_return_val_if_fail (a_charset, NULL); + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStatement)); + result->type = AT_CHARSET_RULE_STMT; + + result->kind.charset_rule = g_try_malloc (sizeof (CRAtCharsetRule)); + + if (!result->kind.charset_rule) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + memset (result->kind.charset_rule, 0, sizeof (CRAtCharsetRule)); + result->kind.charset_rule->charset = a_charset; + cr_statement_set_parent_sheet (result, a_sheet); + + return result; +} + +/** + * cr_statement_at_charset_rule_parse_from_buf: + * + *@a_buf: the buffer to parse. + *@a_encoding: the character encoding of the buffer. + * + *Parses a buffer that contains an '\@charset' rule and + *creates an instance of #CRStatement of type AT_CHARSET_RULE_STMT. + * + *Returns the newly built instance of #CRStatement. + */ +CRStatement * +cr_statement_at_charset_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) +{ + enum CRStatus status = CR_OK; + CRParser *parser = NULL; + CRStatement *result = NULL; + CRString *charset = NULL; + + g_return_val_if_fail (a_buf, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + if (!parser) { + cr_utils_trace_info ("Instantiation of the parser failed."); + goto cleanup; + } + + /*Now, invoke the parser to parse the "@charset production" */ + cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + status = cr_parser_parse_charset (parser, &charset, NULL); + if (status != CR_OK || !charset) + goto cleanup; + + result = cr_statement_new_at_charset_rule (NULL, charset); + if (result) + charset = NULL; + + cleanup: + + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + if (charset) { + cr_string_destroy (charset); + } + + return result; +} + +/** + * cr_statement_new_at_font_face_rule: + * + *@a_font_decls: a list of instances of #CRDeclaration. Each declaration + *is actually a font declaration. + * + *Creates an instance of #CRStatement of type #CRAtFontFaceRule. + * + *Returns the newly built instance of #CRStatement. + */ +CRStatement * +cr_statement_new_at_font_face_rule (CRStyleSheet * a_sheet, + CRDeclaration * a_font_decls) +{ + CRStatement *result = NULL; + + result = g_try_malloc (sizeof (CRStatement)); + + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRStatement)); + result->type = AT_FONT_FACE_RULE_STMT; + + result->kind.font_face_rule = g_try_malloc + (sizeof (CRAtFontFaceRule)); + + if (!result->kind.font_face_rule) { + cr_utils_trace_info ("Out of memory"); + g_free (result); + return NULL; + } + memset (result->kind.font_face_rule, 0, sizeof (CRAtFontFaceRule)); + + result->kind.font_face_rule->decl_list = a_font_decls; + if (a_sheet) + cr_statement_set_parent_sheet (result, a_sheet); + + return result; +} + +/** + * cr_statement_font_face_rule_parse_from_buf: + * + * + *@a_buf: the buffer to parse. + *@a_encoding: the character encoding of a_buf. + * + *Parses a buffer that contains an "\@font-face" rule and builds + *an instance of #CRStatement of type AT_FONT_FACE_RULE_STMT out of it. + * + *Returns the newly built instance of #CRStatement in case of successufull + *parsing, NULL otherwise. + */ +CRStatement * +cr_statement_font_face_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) +{ + CRStatement *result = NULL; + CRStatement **resultptr = NULL; + CRParser *parser = NULL; + CRDocHandler *sac_handler = NULL; + enum CRStatus status = CR_OK; + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + if (!parser) + goto cleanup; + + sac_handler = cr_doc_handler_new (); + if (!sac_handler) + goto cleanup; + + /* + *set sac callbacks here + */ + sac_handler->start_font_face = parse_font_face_start_font_face_cb; + sac_handler->property = parse_font_face_property_cb; + sac_handler->end_font_face = parse_font_face_end_font_face_cb; + sac_handler->unrecoverable_error = + parse_font_face_unrecoverable_error_cb; + + status = cr_parser_set_sac_handler (parser, sac_handler); + if (status != CR_OK) + goto cleanup; + + /* + *cleanup spaces of comment that may be there before the real + *"@font-face" thing. + */ + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) + goto cleanup; + + status = cr_parser_parse_font_face (parser); + if (status != CR_OK) + goto cleanup; + + resultptr = &result; + status = cr_doc_handler_get_result (sac_handler, + (gpointer *) resultptr); + if (status != CR_OK || !result) + goto cleanup; + + cleanup: + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + sac_handler = NULL ; + } + if (sac_handler) { + cr_doc_handler_unref (sac_handler); + sac_handler = NULL; + } + return result; +} + +/** + * cr_statement_set_parent_sheet: + * + *@a_this: the current instance of #CRStatement. + *@a_sheet: the sheet that contains the current statement. + * + *Sets the container stylesheet. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_set_parent_sheet (CRStatement * a_this, CRStyleSheet * a_sheet) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + a_this->parent_sheet = a_sheet; + return CR_OK; +} + +/** + * cr_statement_get_parent_sheet: + * + *@a_this: the current #CRStatement. + *@a_sheet: out parameter. A pointer to the sheets that + * + *Gets the sheets that contains the current statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_get_parent_sheet (CRStatement * a_this, CRStyleSheet ** a_sheet) +{ + g_return_val_if_fail (a_this && a_sheet, CR_BAD_PARAM_ERROR); + *a_sheet = a_this->parent_sheet; + return CR_OK; +} + +/** + * cr_statement_append: + * + *@a_this: the current instance of the statement list. + *@a_new: a_new the new instance of #CRStatement to append. + * + *Appends a new statement to the statement list. + * + *Returns the new list statement list, or NULL in cas of failure. + */ +CRStatement * +cr_statement_append (CRStatement * a_this, CRStatement * a_new) +{ + CRStatement *cur = NULL; + + g_return_val_if_fail (a_new, NULL); + + if (!a_this) { + return a_new; + } + + /*walk forward in the current list to find the tail list element */ + for (cur = a_this; cur && cur->next; cur = cur->next) ; + + cur->next = a_new; + a_new->prev = cur; + + return a_this; +} + +/** + * cr_statement_prepend: + * + *@a_this: the current instance of #CRStatement. + *@a_new: the new statement to prepend. + * + *Prepends the an instance of #CRStatement to + *the current statement list. + * + *Returns the new list with the new statement prepended, + *or NULL in case of an error. + */ +CRStatement * +cr_statement_prepend (CRStatement * a_this, CRStatement * a_new) +{ + CRStatement *cur = NULL; + + g_return_val_if_fail (a_new, NULL); + + if (!a_this) + return a_new; + + a_new->next = a_this; + a_this->prev = a_new; + + /*walk backward in the prepended list to find the head list element */ + for (cur = a_new; cur && cur->prev; cur = cur->prev) ; + + return cur; +} + +/** + * cr_statement_unlink: + * + *@a_this: the current statements list. + *@a_to_unlink: the statement to unlink from the list. + * + *Unlinks a statement from the statements list. + * + *Returns the new list where a_to_unlink has been unlinked + *from, or NULL in case of error. + */ +CRStatement * +cr_statement_unlink (CRStatement * a_stmt) +{ + CRStatement *result = a_stmt; + + g_return_val_if_fail (result, NULL); + + /** + *Some sanity checks first + */ + if (a_stmt->next) { + g_return_val_if_fail (a_stmt->next->prev == a_stmt, NULL); + } + if (a_stmt->prev) { + g_return_val_if_fail (a_stmt->prev->next == a_stmt, NULL); + } + + /** + *Now, the real unlinking job. + */ + if (a_stmt->next) { + a_stmt->next->prev = a_stmt->prev; + } + if (a_stmt->prev) { + a_stmt->prev->next = a_stmt->next; + } + + if (a_stmt->parent_sheet + && a_stmt->parent_sheet->statements == a_stmt) { + a_stmt->parent_sheet->statements = + a_stmt->parent_sheet->statements->next; + } + + a_stmt->next = NULL; + a_stmt->prev = NULL; + a_stmt->parent_sheet = NULL; + + return result; +} + +/** + * cr_statement_nr_rules: + * + *@a_this: the current instance of #CRStatement. + * + *Gets the number of rules in the statement list; + * + *Returns number of rules in the statement list. + */ +gint +cr_statement_nr_rules (CRStatement const * a_this) +{ + CRStatement const *cur = NULL; + int nr = 0; + + g_return_val_if_fail (a_this, -1); + + for (cur = a_this; cur; cur = cur->next) + nr++; + return nr; +} + +/** + * cr_statement_get_from_list: + * + *@a_this: the current instance of #CRStatement. + *@itemnr: the index into the statement list. + * + *Use an index to get a CRStatement from the statement list. + * + *Returns CRStatement at position itemnr, if itemnr > number of statements - 1, + *it will return NULL. + */ +CRStatement * +cr_statement_get_from_list (CRStatement * a_this, int itemnr) +{ + CRStatement *cur = NULL; + int nr = 0; + + g_return_val_if_fail (a_this, NULL); + + for (cur = a_this; cur; cur = cur->next) + if (nr++ == itemnr) + return cur; + return NULL; +} + +/** + * cr_statement_ruleset_set_sel_list: + * + *@a_this: the current ruleset statement. + *@a_sel_list: the selector list to set. Note + *that this function increments the ref count of a_sel_list. + *The sel list will be destroyed at the destruction of the + *current instance of #CRStatement. + * + *Sets a selector list to a ruleset statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_ruleset_set_sel_list (CRStatement * a_this, + CRSelector * a_sel_list) +{ + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT, + CR_BAD_PARAM_ERROR); + + if (a_this->kind.ruleset->sel_list) + cr_selector_unref (a_this->kind.ruleset->sel_list); + + a_this->kind.ruleset->sel_list = a_sel_list; + + if (a_sel_list) + cr_selector_ref (a_sel_list); + + return CR_OK; +} + +/** + * cr_statement_ruleset_get_declarations: + * + *@a_this: the current instance of #CRStatement. + *@a_decl_list: out parameter. A pointer to the the returned + *list of declaration. Must not be NULL. + * + *Gets a pointer to the list of declaration contained + *in the ruleset statement. + * + *Returns CR_OK upon successful completion, an error code if something + *bad happened. + */ +enum CRStatus +cr_statement_ruleset_get_declarations (CRStatement * a_this, + CRDeclaration ** a_decl_list) +{ + g_return_val_if_fail (a_this + && a_this->type == RULESET_STMT + && a_this->kind.ruleset + && a_decl_list, CR_BAD_PARAM_ERROR); + + *a_decl_list = a_this->kind.ruleset->decl_list; + + return CR_OK; +} + +/** + * cr_statement_ruleset_get_sel_list: + * + *@a_this: the current ruleset statement. + *@a_list: out parameter. The returned selector list, + *if and only if the function returned CR_OK. + * + *Gets a pointer to the selector list contained in + *the current ruleset statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_ruleset_get_sel_list (CRStatement const * a_this, CRSelector ** a_list) +{ + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT + && a_this->kind.ruleset, CR_BAD_PARAM_ERROR); + + *a_list = a_this->kind.ruleset->sel_list; + + return CR_OK; +} + +/** + * cr_statement_ruleset_set_decl_list: + * + *@a_this: the current ruleset statement. + *@a_list: the declaration list to be added to the current + *ruleset statement. + * + *Sets a declaration list to the current ruleset statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_ruleset_set_decl_list (CRStatement * a_this, + CRDeclaration * a_list) +{ + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT + && a_this->kind.ruleset, CR_BAD_PARAM_ERROR); + + if (a_this->kind.ruleset->decl_list == a_list) + return CR_OK; + + if (a_this->kind.ruleset->sel_list) { + cr_declaration_destroy (a_this->kind.ruleset->decl_list); + } + + a_this->kind.ruleset->sel_list = NULL; + + return CR_OK; +} + +/** + * cr_statement_ruleset_append_decl2: + * + *@a_this: the current statement. + *@a_prop: the property of the declaration. + *@a_value: the value of the declaration. + * + *Appends a declaration to the current ruleset statement. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_statement_ruleset_append_decl2 (CRStatement * a_this, + CRString * a_prop, + CRTerm * a_value) +{ + CRDeclaration *new_decls = NULL; + + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT + && a_this->kind.ruleset, CR_BAD_PARAM_ERROR); + + new_decls = cr_declaration_append2 + (a_this->kind.ruleset->decl_list, + a_prop, a_value); + g_return_val_if_fail (new_decls, CR_ERROR); + a_this->kind.ruleset->decl_list = new_decls; + + return CR_OK; +} + +/** + * cr_statement_ruleset_append_decl: + * + *Appends a declaration to the current statement. + * + *@a_this: the current statement. + *@a_declaration: the declaration to append. + * + *Returns CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_statement_ruleset_append_decl (CRStatement * a_this, + CRDeclaration * a_decl) +{ + CRDeclaration *new_decls = NULL; + + g_return_val_if_fail (a_this && a_this->type == RULESET_STMT + && a_this->kind.ruleset, CR_BAD_PARAM_ERROR); + + new_decls = cr_declaration_append + (a_this->kind.ruleset->decl_list, a_decl); + g_return_val_if_fail (new_decls, CR_ERROR); + a_this->kind.ruleset->decl_list = new_decls; + + return CR_OK; +} + +/** + * cr_statement_at_import_rule_set_imported_sheet: + * + *Sets a stylesheet to the current \@import rule. + *@a_this: the current \@import rule. + *@a_sheet: the stylesheet. The stylesheet is owned + *by the current instance of #CRStatement, that is, the + *stylesheet will be destroyed when the current instance + *of #CRStatement is destroyed. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_import_rule_set_imported_sheet (CRStatement * a_this, + CRStyleSheet * a_sheet) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_this->kind.import_rule, + CR_BAD_PARAM_ERROR); + + a_this->kind.import_rule->sheet = a_sheet; + + return CR_OK; +} + +/** + * cr_statement_at_import_rule_get_imported_sheet: + * + *@a_this: the current \@import rule statement. + *@a_sheet: out parameter. The returned stylesheet if and + *only if the function returns CR_OK. + * + *Gets the stylesheet contained by the \@import rule statement. + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_import_rule_get_imported_sheet (CRStatement * a_this, + CRStyleSheet ** a_sheet) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_this->kind.import_rule, + CR_BAD_PARAM_ERROR); + *a_sheet = a_this->kind.import_rule->sheet; + return CR_OK; + +} + +/** + * cr_statement_at_import_rule_set_url: + * + *@a_this: the current \@import rule statement. + *@a_url: the url to set. + * + *Sets an url to the current \@import rule statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_import_rule_set_url (CRStatement * a_this, + CRString * a_url) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_this->kind.import_rule, + CR_BAD_PARAM_ERROR); + + if (a_this->kind.import_rule->url) { + cr_string_destroy (a_this->kind.import_rule->url); + } + + a_this->kind.import_rule->url = a_url; + + return CR_OK; +} + +/** + * cr_statement_at_import_rule_get_url: + * + *@a_this: the current \@import rule statement. + *@a_url: out parameter. The returned url if + *and only if the function returned CR_OK. + * + *Gets the url of the \@import rule statement. + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_import_rule_get_url (CRStatement const * a_this, + CRString ** a_url) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_this->kind.import_rule, + CR_BAD_PARAM_ERROR); + + *a_url = a_this->kind.import_rule->url; + + return CR_OK; +} + +/** + * cr_statement_at_media_nr_rules: + * + *@a_this: the current instance of #CRStatement. + * + *Returns the number of rules in the media rule; + */ +int +cr_statement_at_media_nr_rules (CRStatement const * a_this) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_MEDIA_RULE_STMT + && a_this->kind.media_rule, CR_BAD_PARAM_ERROR); + + return cr_statement_nr_rules (a_this->kind.media_rule->rulesets); +} + +/** + * cr_statement_at_media_get_from_list: + * + *@a_this: the current instance of #CRStatement. + *@itemnr: the index into the media rule list of rules. + * + *Use an index to get a CRStatement from the media rule list of rules. + * + *Returns CRStatement at position itemnr, if itemnr > number of rules - 1, + *it will return NULL. + */ +CRStatement * +cr_statement_at_media_get_from_list (CRStatement * a_this, int itemnr) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_MEDIA_RULE_STMT + && a_this->kind.media_rule, NULL); + + return cr_statement_get_from_list (a_this->kind.media_rule->rulesets, + itemnr); +} + +/** + * cr_statement_at_page_rule_set_declarations: + * + *@a_this: the current \@page rule statement. + *@a_decl_list: the declaration list to add. Will be freed + *by the current instance of #CRStatement when it is destroyed. + * + *Sets a declaration list to the current \@page rule statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_page_rule_set_declarations (CRStatement * a_this, + CRDeclaration * a_decl_list) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_PAGE_RULE_STMT + && a_this->kind.page_rule, CR_BAD_PARAM_ERROR); + + if (a_this->kind.page_rule->decl_list) { + cr_declaration_unref (a_this->kind.page_rule->decl_list); + } + + a_this->kind.page_rule->decl_list = a_decl_list; + + if (a_decl_list) { + cr_declaration_ref (a_decl_list); + } + + return CR_OK; +} + +/** + * cr_statement_at_page_rule_get_declarations: + * + *@a_this: the current \@page rule statement. + *@a_decl_list: out parameter. The returned declaration list. + * + *Gets the declaration list associated to the current \@page rule + *statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_page_rule_get_declarations (CRStatement * a_this, + CRDeclaration ** a_decl_list) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_PAGE_RULE_STMT + && a_this->kind.page_rule, CR_BAD_PARAM_ERROR); + + *a_decl_list = a_this->kind.page_rule->decl_list; + + return CR_OK; +} + +/** + * cr_statement_at_charset_rule_set_charset: + * + * + *@a_this: the current \@charset rule statement. + *@a_charset: the charset to set. + * + *Sets the charset of the current \@charset rule statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_charset_rule_set_charset (CRStatement * a_this, + CRString * a_charset) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_CHARSET_RULE_STMT + && a_this->kind.charset_rule, + CR_BAD_PARAM_ERROR); + + if (a_this->kind.charset_rule->charset) { + cr_string_destroy (a_this->kind.charset_rule->charset); + } + a_this->kind.charset_rule->charset = a_charset; + return CR_OK; +} + +/** + * cr_statement_at_charset_rule_get_charset: + *@a_this: the current \@charset rule statement. + *@a_charset: out parameter. The returned charset string if + *and only if the function returned CR_OK. + * + *Gets the charset string associated to the current + *\@charset rule statement. + * + * Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_charset_rule_get_charset (CRStatement const * a_this, + CRString ** a_charset) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_CHARSET_RULE_STMT + && a_this->kind.charset_rule, + CR_BAD_PARAM_ERROR); + + *a_charset = a_this->kind.charset_rule->charset; + + return CR_OK; +} + +/** + * cr_statement_at_font_face_rule_set_decls: + * + *@a_this: the current \@font-face rule statement. + *@a_decls: the declarations list to set. + * + *Sets a declaration list to the current \@font-face rule statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_font_face_rule_set_decls (CRStatement * a_this, + CRDeclaration * a_decls) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_FONT_FACE_RULE_STMT + && a_this->kind.font_face_rule, + CR_BAD_PARAM_ERROR); + + if (a_this->kind.font_face_rule->decl_list) { + cr_declaration_unref (a_this->kind.font_face_rule->decl_list); + } + + a_this->kind.font_face_rule->decl_list = a_decls; + cr_declaration_ref (a_decls); + + return CR_OK; +} + +/** + * cr_statement_at_font_face_rule_get_decls: + * + *@a_this: the current \@font-face rule statement. + *@a_decls: out parameter. The returned declaration list if + *and only if this function returns CR_OK. + * + *Gets the declaration list associated to the current instance + *of \@font-face rule statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_font_face_rule_get_decls (CRStatement * a_this, + CRDeclaration ** a_decls) +{ + g_return_val_if_fail (a_this + && a_this->type == AT_FONT_FACE_RULE_STMT + && a_this->kind.font_face_rule, + CR_BAD_PARAM_ERROR); + + *a_decls = a_this->kind.font_face_rule->decl_list; + + return CR_OK; +} + +/** + * cr_statement_at_font_face_rule_add_decl: + * + *@a_this: the current \@font-face rule statement. + *@a_prop: the property of the declaration. + *@a_value: the value of the declaration. + * + *Adds a declaration to the current \@font-face rule + *statement. + * + *Returns CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_statement_at_font_face_rule_add_decl (CRStatement * a_this, + CRString * a_prop, CRTerm * a_value) +{ + CRDeclaration *decls = NULL; + + g_return_val_if_fail (a_this + && a_this->type == AT_FONT_FACE_RULE_STMT + && a_this->kind.font_face_rule, + CR_BAD_PARAM_ERROR); + + decls = cr_declaration_append2 + (a_this->kind.font_face_rule->decl_list, + a_prop, a_value); + + g_return_val_if_fail (decls, CR_ERROR); + + if (a_this->kind.font_face_rule->decl_list == NULL) + cr_declaration_ref (decls); + + a_this->kind.font_face_rule->decl_list = decls; + + return CR_OK; +} + + +/** + * cr_statement_to_string: + * + *@a_this: the current statement to serialize + *@a_indent: the number of white space of indentation. + * + *Serializes a css statement into a string + * + *Returns the serialized statement. Must be freed by the caller + *using g_free(). + */ +gchar * +cr_statement_to_string (CRStatement const * a_this, gulong a_indent) +{ + gchar *str = NULL ; + + if (!a_this) + return NULL; + + switch (a_this->type) { + case RULESET_STMT: + str = cr_statement_ruleset_to_string + (a_this, a_indent); + break; + + case AT_FONT_FACE_RULE_STMT: + str = cr_statement_font_face_rule_to_string + (a_this, a_indent) ; + break; + + case AT_CHARSET_RULE_STMT: + str = cr_statement_charset_to_string + (a_this, a_indent); + break; + + case AT_PAGE_RULE_STMT: + str = cr_statement_at_page_rule_to_string + (a_this, a_indent); + break; + + case AT_MEDIA_RULE_STMT: + str = cr_statement_media_rule_to_string + (a_this, a_indent); + break; + + case AT_IMPORT_RULE_STMT: + str = cr_statement_import_rule_to_string + (a_this, a_indent); + break; + + default: + cr_utils_trace_info ("Statement unrecognized"); + break; + } + return str ; +} + +gchar* +cr_statement_list_to_string (CRStatement const *a_this, gulong a_indent) +{ + CRStatement const *cur_stmt = NULL ; + GString *stringue = NULL ; + gchar *str = NULL ; + + g_return_val_if_fail (a_this, NULL) ; + + stringue = g_string_new (NULL) ; + if (!stringue) { + cr_utils_trace_info ("Out of memory") ; + return NULL ; + } + for (cur_stmt = a_this ; cur_stmt; + cur_stmt = cur_stmt->next) { + str = cr_statement_to_string (cur_stmt, a_indent) ; + if (str) { + if (!cur_stmt->prev) { + g_string_append (stringue, str) ; + } else { + g_string_append_printf + (stringue, "\n%s", str) ; + } + g_free (str) ; + str = NULL ; + } + } + str = g_string_free (stringue, FALSE) ; + return str ; +} + +/** + * cr_statement_dump: + * + *@a_this: the current css2 statement. + *@a_fp: the destination file pointer. + *@a_indent: the number of white space indentation characters. + * + *Dumps the css2 statement to a file. + */ +void +cr_statement_dump (CRStatement const * a_this, FILE * a_fp, gulong a_indent) +{ + gchar *str = NULL ; + + if (!a_this) + return; + + str = cr_statement_to_string (a_this, a_indent) ; + if (str) { + fprintf (a_fp, "%s",str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + * cr_statement_dump_ruleset: + * + *@a_this: the current instance of #CRStatement. + *@a_fp: the destination file pointer. + *@a_indent: the number of indentation white spaces to add. + * + *Dumps a ruleset statement to a file. + */ +void +cr_statement_dump_ruleset (CRStatement const * a_this, FILE * a_fp, glong a_indent) +{ + gchar *str = NULL; + + g_return_if_fail (a_fp && a_this); + str = cr_statement_ruleset_to_string (a_this, a_indent); + if (str) { + fprintf (a_fp, "%s", str); + g_free (str); + str = NULL; + } +} + +/** + * cr_statement_dump_font_face_rule: + * + *@a_this: the current instance of font face rule statement. + *@a_fp: the destination file pointer. + *@a_indent: the number of white space indentation. + * + *Dumps a font face rule statement to a file. + */ +void +cr_statement_dump_font_face_rule (CRStatement const * a_this, FILE * a_fp, + glong a_indent) +{ + gchar *str = NULL ; + g_return_if_fail (a_this + && a_this->type == AT_FONT_FACE_RULE_STMT); + + str = cr_statement_font_face_rule_to_string (a_this, + a_indent) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + * cr_statement_dump_charset: + * + *@a_this: the current instance of the \@charset rule statement. + *@a_fp: the destination file pointer. + *@a_indent: the number of indentation white spaces. + * + *Dumps an \@charset rule statement to a file. + */ +void +cr_statement_dump_charset (CRStatement const * a_this, FILE * a_fp, gulong a_indent) +{ + gchar *str = NULL; + + g_return_if_fail (a_this && a_this->type == AT_CHARSET_RULE_STMT); + + str = cr_statement_charset_to_string (a_this, + a_indent) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + + +/** + * cr_statement_dump_page: + * + *@a_this: the statement to dump on stdout. + *@a_fp: the destination file pointer. + *@a_indent: the number of indentation white spaces. + * + *Dumps an \@page rule statement on stdout. + */ +void +cr_statement_dump_page (CRStatement const * a_this, FILE * a_fp, gulong a_indent) +{ + gchar *str = NULL; + + g_return_if_fail (a_this + && a_this->type == AT_PAGE_RULE_STMT + && a_this->kind.page_rule); + + str = cr_statement_at_page_rule_to_string (a_this, a_indent) ; + if (str) { + fprintf (a_fp, "%s", str); + g_free (str) ; + str = NULL ; + } +} + + +/** + * cr_statement_dump_media_rule: + * + *@a_this: the statement to dump. + *@a_fp: the destination file pointer + *@a_indent: the number of white spaces indentation. + * + *Dumps an \@media rule statement to a file. + */ +void +cr_statement_dump_media_rule (CRStatement const * a_this, + FILE * a_fp, + gulong a_indent) +{ + gchar *str = NULL ; + g_return_if_fail (a_this->type == AT_MEDIA_RULE_STMT); + + str = cr_statement_media_rule_to_string (a_this, a_indent) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + * cr_statement_dump_import_rule: + * + *@a_fp: the destination file pointer. + *@a_indent: the number of white space indentations. + * + *Dumps an \@import rule statement to a file. + */ +void +cr_statement_dump_import_rule (CRStatement const * a_this, FILE * a_fp, + gulong a_indent) +{ + gchar *str = NULL ; + g_return_if_fail (a_this + && a_this->type == AT_IMPORT_RULE_STMT + && a_fp + && a_this->kind.import_rule); + + str = cr_statement_import_rule_to_string (a_this, a_indent) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + * cr_statement_destroy: + * + * @a_this: the current instance of #CRStatement. + * + *Destructor of #CRStatement. + */ +void +cr_statement_destroy (CRStatement * a_this) +{ + CRStatement *cur = NULL; + + g_return_if_fail (a_this); + + /*go get the tail of the list */ + for (cur = a_this; cur && cur->next; cur = cur->next) { + cr_statement_clear (cur); + } + + if (cur) + cr_statement_clear (cur); + + if (cur->prev == NULL) { + g_free (a_this); + return; + } + + /*walk backward and free next element */ + for (cur = cur->prev; cur && cur->prev; cur = cur->prev) { + if (cur->next) { + g_free (cur->next); + cur->next = NULL; + } + } + + if (!cur) + return; + + /*free the one remaining list */ + if (cur->next) { + g_free (cur->next); + cur->next = NULL; + } + + g_free (cur); + cur = NULL; +} diff --git a/src/st/croco/cr-statement.h b/src/st/croco/cr-statement.h new file mode 100644 index 0000000..c5bec97 --- /dev/null +++ b/src/st/croco/cr-statement.h @@ -0,0 +1,440 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include <stdio.h> +#include "cr-utils.h" +#include "cr-term.h" +#include "cr-selector.h" +#include "cr-declaration.h" + +#ifndef __CR_STATEMENT_H__ +#define __CR_STATEMENT_H__ + +G_BEGIN_DECLS + +/** + *@file + *Declaration of the #CRStatement class. + */ + +/* + *forward declaration of CRStyleSheet which is defined in + *cr-stylesheet.h + */ + +struct _CRStatement ; + +/* + *typedef struct _CRStatement CRStatement ; + *this is forward declared in + *cr-declaration.h already. + */ + +struct _CRAtMediaRule ; +typedef struct _CRAtMediaRule CRAtMediaRule ; + +typedef struct _CRRuleSet CRRuleSet ; + +/** + *The abstraction of a css ruleset. + *A ruleset is made of a list of selectors, + *followed by a list of declarations. + */ +struct _CRRuleSet +{ + /**A list of instances of #CRSimpeSel*/ + CRSelector *sel_list ; + + /**A list of instances of #CRDeclaration*/ + CRDeclaration *decl_list ; + + /** + *The parent media rule, or NULL if + *no parent media rule exists. + */ + CRStatement *parent_media_rule ; +} ; + +/* + *a forward declaration of CRStylesheet. + *CRStylesheet is actually declared in + *cr-stylesheet.h + */ +struct _CRStyleSheet ; +typedef struct _CRStyleSheet CRStyleSheet; + + +/**The \@import rule abstraction.*/ +typedef struct _CRAtImportRule CRAtImportRule ; +struct _CRAtImportRule +{ + /**the url of the import rule*/ + CRString *url ; + + GList *media_list ; + + /** + *the stylesheet fetched from the url, if any. + *this is not "owned" by #CRAtImportRule which means + *it is not destroyed by the destructor of #CRAtImportRule. + */ + CRStyleSheet * sheet; +}; + + +/**abstraction of an \@media rule*/ +struct _CRAtMediaRule +{ + GList *media_list ; + CRStatement *rulesets ; +} ; + + +typedef struct _CRAtPageRule CRAtPageRule ; +/**The \@page rule abstraction*/ +struct _CRAtPageRule +{ + /**a list of instances of #CRDeclaration*/ + CRDeclaration *decl_list ; + + /**page selector. Is a pseudo selector*/ + CRString *name ; + CRString *pseudo ; +} ; + +/**The \@charset rule abstraction*/ +typedef struct _CRAtCharsetRule CRAtCharsetRule ; +struct _CRAtCharsetRule +{ + CRString * charset ; +}; + +/**The abstraction of the \@font-face rule.*/ +typedef struct _CRAtFontFaceRule CRAtFontFaceRule ; +struct _CRAtFontFaceRule +{ + /*a list of instanaces of #CRDeclaration*/ + CRDeclaration *decl_list ; +} ; + + +/** + *The possible types of css2 statements. + */ +enum CRStatementType +{ + /** + *A generic css at-rule + *each unknown at-rule will + *be of this type. + */ + + /**A css at-rule*/ + AT_RULE_STMT = 0, + + /*A css ruleset*/ + RULESET_STMT, + + /**A css2 import rule*/ + AT_IMPORT_RULE_STMT, + + /**A css2 media rule*/ + AT_MEDIA_RULE_STMT, + + /**A css2 page rule*/ + AT_PAGE_RULE_STMT, + + /**A css2 charset rule*/ + AT_CHARSET_RULE_STMT, + + /**A css2 font face rule*/ + AT_FONT_FACE_RULE_STMT +} ; + + +/** + *The abstraction of css statement as defined + *in the chapter 4 and appendix D.1 of the css2 spec. + *A statement is actually a double chained list of + *statements.A statement can be a ruleset, an \@import + *rule, an \@page rule etc ... + */ +struct _CRStatement +{ + /** + *The type of the statement. + */ + enum CRStatementType type ; + + union + { + CRRuleSet *ruleset ; + CRAtImportRule *import_rule ; + CRAtMediaRule *media_rule ; + CRAtPageRule *page_rule ; + CRAtCharsetRule *charset_rule ; + CRAtFontFaceRule *font_face_rule ; + } kind ; + + /* + *the specificity of the selector + *that matched this statement. + *This is only used by the cascading + *order determination algorithm. + */ + gulong specificity ; + + /* + *the style sheet that contains + *this css statement. + */ + CRStyleSheet *parent_sheet ; + CRStatement *next ; + CRStatement *prev ; + + CRParsingLocation location ; + + /** + *a custom pointer useable by + *applications that use libcroco. + *libcroco itself will never modify + *this pointer. + */ + gpointer app_data ; + + /** + *a custom pointer used + *by the upper layers of libcroco. + *application should never use this + *pointer. + */ + gpointer croco_data ; + +} ; + + +gboolean +cr_statement_does_buf_parses_against_core (const guchar *a_buf, + enum CREncoding a_encoding) ; +CRStatement * +cr_statement_parse_from_buf (const guchar *a_buf, + enum CREncoding a_encoding) ; +CRStatement* +cr_statement_new_ruleset (CRStyleSheet *a_sheet, + CRSelector *a_sel_list, + CRDeclaration *a_decl_list, + CRStatement *a_media_rule) ; +CRStatement * +cr_statement_ruleset_parse_from_buf (const guchar * a_buf, + enum CREncoding a_enc) ; + +CRStatement* +cr_statement_new_at_import_rule (CRStyleSheet *a_container_sheet, + CRString *a_url, + GList *a_media_list, + CRStyleSheet *a_imported_sheet) ; + +CRStatement * +cr_statement_at_import_rule_parse_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) ; + +CRStatement * +cr_statement_new_at_media_rule (CRStyleSheet *a_sheet, + CRStatement *a_ruleset, + GList *a_media) ; +CRStatement * +cr_statement_at_media_rule_parse_from_buf (const guchar *a_buf, + enum CREncoding a_enc) ; + +CRStatement * +cr_statement_new_at_charset_rule (CRStyleSheet *a_sheet, + CRString *a_charset) ; +CRStatement * +cr_statement_at_charset_rule_parse_from_buf (const guchar *a_buf, + enum CREncoding a_encoding); + + +CRStatement * +cr_statement_new_at_font_face_rule (CRStyleSheet *a_sheet, + CRDeclaration *a_font_decls) ; +CRStatement * +cr_statement_font_face_rule_parse_from_buf (const guchar *a_buf, + enum CREncoding a_encoding) ; + +CRStatement * +cr_statement_new_at_page_rule (CRStyleSheet *a_sheet, + CRDeclaration *a_decl_list, + CRString *a_name, + CRString *a_pseudo) ; +CRStatement * +cr_statement_at_page_rule_parse_from_buf (const guchar *a_buf, + enum CREncoding a_encoding) ; + +enum CRStatus +cr_statement_set_parent_sheet (CRStatement *a_this, + CRStyleSheet *a_sheet) ; + +enum CRStatus +cr_statement_get_parent_sheet (CRStatement *a_this, + CRStyleSheet **a_sheet) ; + +CRStatement * +cr_statement_append (CRStatement *a_this, + CRStatement *a_new) ; + +CRStatement* +cr_statement_prepend (CRStatement *a_this, + CRStatement *a_new) ; + +CRStatement * +cr_statement_unlink (CRStatement *a_stmt) ; + +enum CRStatus +cr_statement_ruleset_set_sel_list (CRStatement *a_this, + CRSelector *a_sel_list) ; + +enum CRStatus +cr_statement_ruleset_get_sel_list (CRStatement const *a_this, + CRSelector **a_list) ; + +enum CRStatus +cr_statement_ruleset_set_decl_list (CRStatement *a_this, + CRDeclaration *a_list) ; + +enum CRStatus +cr_statement_ruleset_get_declarations (CRStatement *a_this, + CRDeclaration **a_decl_list) ; + +enum CRStatus +cr_statement_ruleset_append_decl2 (CRStatement *a_this, + CRString *a_prop, CRTerm *a_value) ; + +enum CRStatus +cr_statement_ruleset_append_decl (CRStatement *a_this, + CRDeclaration *a_decl) ; + +enum CRStatus +cr_statement_at_import_rule_set_imported_sheet (CRStatement *a_this, + CRStyleSheet *a_sheet) ; + +enum CRStatus +cr_statement_at_import_rule_get_imported_sheet (CRStatement *a_this, + CRStyleSheet **a_sheet) ; + +enum CRStatus +cr_statement_at_import_rule_set_url (CRStatement *a_this, + CRString *a_url) ; + +enum CRStatus +cr_statement_at_import_rule_get_url (CRStatement const *a_this, + CRString **a_url) ; + +gint +cr_statement_at_media_nr_rules (CRStatement const *a_this) ; + +CRStatement * +cr_statement_at_media_get_from_list (CRStatement *a_this, int itemnr) ; + +enum CRStatus +cr_statement_at_page_rule_set_sel (CRStatement *a_this, + CRSelector *a_sel) ; + +enum CRStatus +cr_statement_at_page_rule_get_sel (CRStatement const *a_this, + CRSelector **a_sel) ; + +enum CRStatus +cr_statement_at_page_rule_set_declarations (CRStatement *a_this, + CRDeclaration *a_decl_list) ; + +enum CRStatus +cr_statement_at_page_rule_get_declarations (CRStatement *a_this, + CRDeclaration **a_decl_list) ; + +enum CRStatus +cr_statement_at_charset_rule_set_charset (CRStatement *a_this, + CRString *a_charset) ; + +enum CRStatus +cr_statement_at_charset_rule_get_charset (CRStatement const *a_this, + CRString **a_charset) ; + +enum CRStatus +cr_statement_at_font_face_rule_set_decls (CRStatement *a_this, + CRDeclaration *a_decls) ; + +enum CRStatus +cr_statement_at_font_face_rule_get_decls (CRStatement *a_this, + CRDeclaration **a_decls) ; + +enum CRStatus +cr_statement_at_font_face_rule_add_decl (CRStatement *a_this, + CRString *a_prop, + CRTerm *a_value) ; + +gchar * +cr_statement_to_string (CRStatement const * a_this, gulong a_indent) ; + +gchar* +cr_statement_list_to_string (CRStatement const *a_this, gulong a_indent) ; + +void +cr_statement_dump (CRStatement const *a_this, FILE *a_fp, gulong a_indent) ; + +void +cr_statement_dump_ruleset (CRStatement const * a_this, FILE * a_fp, + glong a_indent) ; + +void +cr_statement_dump_font_face_rule (CRStatement const * a_this, + FILE * a_fp, + glong a_indent) ; + +void +cr_statement_dump_page (CRStatement const * a_this, FILE * a_fp, + gulong a_indent) ; + + +void +cr_statement_dump_media_rule (CRStatement const * a_this, + FILE * a_fp, + gulong a_indent) ; + +void +cr_statement_dump_import_rule (CRStatement const * a_this, FILE * a_fp, + gulong a_indent) ; +void +cr_statement_dump_charset (CRStatement const * a_this, FILE * a_fp, + gulong a_indent) ; +gint +cr_statement_nr_rules (CRStatement const *a_this) ; + +CRStatement * +cr_statement_get_from_list (CRStatement *a_this, int itemnr) ; + +void +cr_statement_destroy (CRStatement *a_this) ; + +G_END_DECLS + +#endif /*__CR_STATEMENT_H__*/ diff --git a/src/st/croco/cr-string.c b/src/st/croco/cr-string.c new file mode 100644 index 0000000..6a16676 --- /dev/null +++ b/src/st/croco/cr-string.c @@ -0,0 +1,168 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli. + * See COPYRIGHTS file for copyright information. + */ + +#include <string.h> +#include "cr-string.h" + +/** + *Instantiates a #CRString + *@return the newly instantiated #CRString + *Must be freed with cr_string_destroy(). + */ +CRString * +cr_string_new (void) +{ + CRString *result = NULL ; + + result = g_try_malloc (sizeof (CRString)) ; + if (!result) { + cr_utils_trace_info ("Out of memory") ; + return NULL ; + } + memset (result, 0, sizeof (CRString)) ; + result->stryng = g_string_new (NULL) ; + return result ; +} + +/** + *Instantiate a string and initialise it to + *a_string. + *@param a_string the initial string + *@return the newly instantiated string. + */ +CRString * +cr_string_new_from_string (const gchar * a_string) +{ + CRString *result = NULL ; + + result = cr_string_new () ; + if (!result) { + cr_utils_trace_info ("Out of memory") ; + return NULL ; + } + if (a_string) + g_string_append (result->stryng, a_string) ; + return result ; +} + +/** + *Instantiates a #CRString from an instance of GString. + *@param a_string the input string that will be copied into + *the newly instantiated #CRString + *@return the newly instantiated #CRString. + */ +CRString * +cr_string_new_from_gstring (GString const *a_string) +{ + CRString *result = NULL ; + + result = cr_string_new () ; + if (!result) { + cr_utils_trace_info ("Out of memory") ; + return NULL ; + } + if (a_string) { + g_string_append_len (result->stryng, + a_string->str, + a_string->len); + + } + return result ; +} + +CRString * +cr_string_dup (CRString const *a_this) +{ + CRString *result = NULL ; + g_return_val_if_fail (a_this, NULL) ; + + result = cr_string_new_from_gstring (a_this->stryng) ; + if (!result) { + cr_utils_trace_info ("Out of memory") ; + return NULL ; + } + cr_parsing_location_copy (&result->location, + &a_this->location) ; + return result ; +} + +gchar * +cr_string_dup2 (CRString const *a_this) +{ + gchar *result = NULL ; + + g_return_val_if_fail (a_this, NULL) ; + + if (a_this + && a_this->stryng + && a_this->stryng->str) { + result = g_strndup (a_this->stryng->str, + a_this->stryng->len) ; + } + return result ; +} + +/** + *Returns a pointer to the internal raw NULL terminated string + *of the current instance of #CRString. + *@param a_this the current instance of #CRString + */ +const gchar * +cr_string_peek_raw_str (CRString const *a_this) +{ + g_return_val_if_fail (a_this, NULL) ; + + if (a_this->stryng && a_this->stryng->str) + return a_this->stryng->str ; + return NULL ; +} + +/** + *Returns the length of the internal raw NULL terminated + *string of the current instance of #CRString. + *@param a_this the current instance of #CRString. + *@return the len of the internal raw NULL termninated string, + *of -1 if no length can be returned. + */ +gint +cr_string_peek_raw_str_len (CRString const *a_this) +{ + g_return_val_if_fail (a_this && a_this->stryng, + -1) ; + return a_this->stryng->len ; +} + +/** + *@param a_this the #CRString to destroy. + */ +void +cr_string_destroy (CRString *a_this) +{ + g_return_if_fail (a_this) ; + + if (a_this->stryng) { + g_string_free (a_this->stryng, TRUE) ; + a_this->stryng = NULL ; + } + g_free (a_this) ; +} diff --git a/src/st/croco/cr-string.h b/src/st/croco/cr-string.h new file mode 100644 index 0000000..2700f0e --- /dev/null +++ b/src/st/croco/cr-string.h @@ -0,0 +1,76 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * See COPYRIGHTS file for copyright information. + */ + +/** + *@file + *Declaration file of the #CRString class. + */ + +#ifndef __CR_STRING_H__ +#define __CR_STRING_H__ + +#include <glib.h> +#include "cr-utils.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +typedef struct _CRString CRString ; + +/** + *This is a ship implementation of string based on GString. + *Actually, the aim of CRString is to store the parsing location + *(line,column,byte offset) at which a given string has been parsed + *in the input CSS. + *So this class has a gstring field of type GString that users can + *freely manipulate, and also a CRParginLocation type where the + *parsing location is store. If you don't want to deal with parsing + *location stuffs, then use GString instead. If we were in C++ for example, + *CRString would just inherit GString and just add accessors to + *the CRParsingLocation data ... but we are not and we still have + *to provide the parsing location information. + */ +struct _CRString { + /** + *The GString where all the string + *operation happen. + */ + GString *stryng ; + /** + *The parsing location storage area. + */ + CRParsingLocation location ; +} ; + +CRString * cr_string_new (void) ; + +CRString *cr_string_new_from_string (const gchar * a_string) ; +CRString * cr_string_new_from_gstring (GString const *a_string) ; +CRString *cr_string_dup (CRString const *a_this) ; +gchar *cr_string_dup2 (CRString const *a_this) ; +const gchar *cr_string_peek_raw_str (CRString const *a_this) ; +gint cr_string_peek_raw_str_len (CRString const *a_this) ; +void cr_string_destroy (CRString *a_this) ; + +G_END_DECLS + +#endif diff --git a/src/st/croco/cr-stylesheet.c b/src/st/croco/cr-stylesheet.c new file mode 100644 index 0000000..63e763f --- /dev/null +++ b/src/st/croco/cr-stylesheet.c @@ -0,0 +1,177 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2004 Dodji Seketeli + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +#include "string.h" +#include "cr-stylesheet.h" + +/** + *@file + *The definition of the #CRStyleSheet class + */ + +/** + *Constructor of the #CRStyleSheet class. + *@param the initial list of css statements. + *@return the newly built css2 stylesheet, or NULL in case of error. + */ +CRStyleSheet * +cr_stylesheet_new (CRStatement * a_stmts) +{ + CRStyleSheet *result; + + result = g_try_malloc (sizeof (CRStyleSheet)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRStyleSheet)); + + if (a_stmts) + result->statements = a_stmts; + + return result; +} + +/** + *@param a_this the current instance of #CRStyleSheet + *@return the serialized stylesheet. + */ +gchar * +cr_stylesheet_to_string (CRStyleSheet const *a_this) +{ + gchar *str = NULL; + GString *stringue = NULL; + CRStatement const *cur_stmt = NULL; + + g_return_val_if_fail (a_this, NULL); + + if (a_this->statements) { + stringue = g_string_new (NULL) ; + g_return_val_if_fail (stringue, NULL) ; + } + for (cur_stmt = a_this->statements; + cur_stmt; cur_stmt = cur_stmt->next) { + if (cur_stmt->prev) { + g_string_append (stringue, "\n\n") ; + } + str = cr_statement_to_string (cur_stmt, 0) ; + if (str) { + g_string_append (stringue, str) ; + g_free (str) ; + str = NULL ; + } + } + if (stringue) { + str = g_string_free (stringue, FALSE) ; + stringue = NULL ; + } + return str ; +} + +/** + *Dumps the current css2 stylesheet to a file. + *@param a_this the current instance of #CRStyleSheet. + *@param a_fp the destination file + */ +void +cr_stylesheet_dump (CRStyleSheet const * a_this, FILE * a_fp) +{ + gchar *str = NULL ; + + g_return_if_fail (a_this); + + str = cr_stylesheet_to_string (a_this) ; + if (str) { + fprintf (a_fp, "%s", str) ; + g_free (str) ; + str = NULL ; + } +} + +/** + *Return the number of rules in the stylesheet. + *@param a_this the current instance of #CRStyleSheet. + *@return number of rules in the stylesheet. + */ +gint +cr_stylesheet_nr_rules (CRStyleSheet const * a_this) +{ + g_return_val_if_fail (a_this, -1); + + return cr_statement_nr_rules (a_this->statements); +} + +/** + *Use an index to get a CRStatement from the rules in a given stylesheet. + *@param a_this the current instance of #CRStatement. + *@param itemnr the index into the rules. + *@return CRStatement at position itemnr, if itemnr > number of rules - 1, + *it will return NULL. + */ +CRStatement * +cr_stylesheet_statement_get_from_list (CRStyleSheet * a_this, int itemnr) +{ + g_return_val_if_fail (a_this, NULL); + + return cr_statement_get_from_list (a_this->statements, itemnr); +} + +void +cr_stylesheet_ref (CRStyleSheet * a_this) +{ + g_return_if_fail (a_this); + + a_this->ref_count++; +} + +gboolean +cr_stylesheet_unref (CRStyleSheet * a_this) +{ + g_return_val_if_fail (a_this, FALSE); + + if (a_this->ref_count) + a_this->ref_count--; + + if (!a_this->ref_count) { + cr_stylesheet_destroy (a_this); + return TRUE; + } + + return FALSE; +} + +/** + *Destructor of the #CRStyleSheet class. + *@param a_this the current instance of the #CRStyleSheet class. + */ +void +cr_stylesheet_destroy (CRStyleSheet * a_this) +{ + g_return_if_fail (a_this); + + if (a_this->statements) { + cr_statement_destroy (a_this->statements); + a_this->statements = NULL; + } + g_free (a_this); +} diff --git a/src/st/croco/cr-stylesheet.h b/src/st/croco/cr-stylesheet.h new file mode 100644 index 0000000..2d6b4fa --- /dev/null +++ b/src/st/croco/cr-stylesheet.h @@ -0,0 +1,102 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + * + * see COPYRIGHTS file for copyright information. + */ + + +#ifndef __CR_STYLESHEET_H__ +#define __CR_STYLESHEET_H__ + +#include "cr-utils.h" +#include "cr-statement.h" + +G_BEGIN_DECLS + +/** + *@file + *The declaration of the #CRStyleSheet class. + */ + + +enum CRStyleOrigin +{ + /*Please don't change the order of + *the values enumerated here ... + *New values should be added at the end, + *just before ORIGIN_END. + */ + ORIGIN_UA = 0, + ORIGIN_USER, + ORIGIN_AUTHOR, + + /*must always be the last one*/ + NB_ORIGINS +} ; + +/** + *An abstraction of a css stylesheet as defined + *by the css2 spec in chapter 4. + */ +struct _CRStyleSheet +{ + /**The css statements list*/ + CRStatement *statements ; + + enum CRStyleOrigin origin ; + + /*the parent import rule, if any.*/ + CRStatement *parent_import_rule ; + + /**custom data used by libcroco*/ + gpointer croco_data ; + + /** + *custom application data pointer + *Can be used by applications. + */ + gpointer app_data ; + + /** + *the reference count of this instance + *Please, don't never ever modify it + *directly. Use cr_stylesheet_ref() + *and cr_stylesheet_unref() instead. + */ + gulong ref_count ; +} ; + +CRStyleSheet * cr_stylesheet_new (CRStatement *a_stmts) ; + +gchar * cr_stylesheet_to_string (CRStyleSheet const *a_this) ; +void cr_stylesheet_dump (CRStyleSheet const *a_this, FILE *a_fp) ; + +gint cr_stylesheet_nr_rules (CRStyleSheet const *a_this) ; + +CRStatement * cr_stylesheet_statement_get_from_list (CRStyleSheet *a_this, int itemnr) ; + +void cr_stylesheet_ref (CRStyleSheet *a_this) ; + +gboolean cr_stylesheet_unref (CRStyleSheet *a_this) ; + +void cr_stylesheet_destroy (CRStyleSheet *a_this) ; + +G_END_DECLS + +#endif /*__CR_STYLESHEET_H__*/ diff --git a/src/st/croco/cr-term.c b/src/st/croco/cr-term.c new file mode 100644 index 0000000..b527d95 --- /dev/null +++ b/src/st/croco/cr-term.c @@ -0,0 +1,786 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include <stdio.h> +#include <string.h> +#include "cr-term.h" +#include "cr-num.h" +#include "cr-parser.h" + +/** + *@file + *Definition of the #CRTem class. + */ + +static void +cr_term_clear (CRTerm * a_this) +{ + g_return_if_fail (a_this); + + switch (a_this->type) { + case TERM_NUMBER: + if (a_this->content.num) { + cr_num_destroy (a_this->content.num); + a_this->content.num = NULL; + } + break; + + case TERM_FUNCTION: + if (a_this->ext_content.func_param) { + cr_term_destroy (a_this->ext_content.func_param); + a_this->ext_content.func_param = NULL; + } + case TERM_STRING: + case TERM_IDENT: + case TERM_URI: + case TERM_HASH: + if (a_this->content.str) { + cr_string_destroy (a_this->content.str); + a_this->content.str = NULL; + } + break; + + case TERM_RGB: + if (a_this->content.rgb) { + cr_rgb_destroy (a_this->content.rgb); + a_this->content.rgb = NULL; + } + break; + + case TERM_UNICODERANGE: + case TERM_NO_TYPE: + default: + break; + } + + a_this->type = TERM_NO_TYPE; +} + +/** + *Instantiate a #CRTerm. + *@return the newly build instance + *of #CRTerm. + */ +CRTerm * +cr_term_new (void) +{ + CRTerm *result = NULL; + + result = g_try_malloc (sizeof (CRTerm)); + if (!result) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + memset (result, 0, sizeof (CRTerm)); + return result; +} + +/** + *Parses an expression as defined by the css2 spec + *and builds the expression as a list of terms. + *@param a_buf the buffer to parse. + *@return a pointer to the first term of the expression or + *NULL if parsing failed. + */ +CRTerm * +cr_term_parse_expression_from_buf (const guchar * a_buf, + enum CREncoding a_encoding) +{ + CRParser *parser = NULL; + CRTerm *result = NULL; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_buf, NULL); + + parser = cr_parser_new_from_buf ((guchar*)a_buf, strlen ((const char *) a_buf), + a_encoding, FALSE); + g_return_val_if_fail (parser, NULL); + + status = cr_parser_try_to_skip_spaces_and_comments (parser); + if (status != CR_OK) { + goto cleanup; + } + status = cr_parser_parse_expr (parser, &result); + if (status != CR_OK) { + if (result) { + cr_term_destroy (result); + result = NULL; + } + } + + cleanup: + if (parser) { + cr_parser_destroy (parser); + parser = NULL; + } + + return result; +} + +enum CRStatus +cr_term_set_number (CRTerm * a_this, CRNum * a_num) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_NUMBER; + a_this->content.num = a_num; + return CR_OK; +} + +enum CRStatus +cr_term_set_function (CRTerm * a_this, CRString * a_func_name, + CRTerm * a_func_param) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_FUNCTION; + a_this->content.str = a_func_name; + a_this->ext_content.func_param = a_func_param; + return CR_OK; +} + +enum CRStatus +cr_term_set_string (CRTerm * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_STRING; + a_this->content.str = a_str; + return CR_OK; +} + +enum CRStatus +cr_term_set_ident (CRTerm * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_IDENT; + a_this->content.str = a_str; + return CR_OK; +} + +enum CRStatus +cr_term_set_uri (CRTerm * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_URI; + a_this->content.str = a_str; + return CR_OK; +} + +enum CRStatus +cr_term_set_rgb (CRTerm * a_this, CRRgb * a_rgb) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_RGB; + a_this->content.rgb = a_rgb; + return CR_OK; +} + +enum CRStatus +cr_term_set_hash (CRTerm * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_term_clear (a_this); + + a_this->type = TERM_HASH; + a_this->content.str = a_str; + return CR_OK; +} + +/** + *Appends a new term to the current list of #CRTerm. + * + *@param a_this the "this pointer" of the current instance + *of #CRTerm . + *@param a_new_term the term to append. + *@return the list of terms with the a_new_term appended to it. + */ +CRTerm * +cr_term_append_term (CRTerm * a_this, CRTerm * a_new_term) +{ + CRTerm *cur = NULL; + + g_return_val_if_fail (a_new_term, NULL); + + if (a_this == NULL) + return a_new_term; + + for (cur = a_this; cur->next; cur = cur->next) ; + + cur->next = a_new_term; + a_new_term->prev = cur; + + return a_this; +} + +/** + *Prepends a term to the list of terms represented by a_this. + * + *@param a_this the "this pointer" of the current instance of + *#CRTerm . + *@param a_new_term the term to prepend. + *@return the head of the new list. + */ +CRTerm * +cr_term_prepend_term (CRTerm * a_this, CRTerm * a_new_term) +{ + g_return_val_if_fail (a_this && a_new_term, NULL); + + a_new_term->next = a_this; + a_this->prev = a_new_term; + + return a_new_term; +} + +/** + *Serializes the expression represented by + *the chained instances of #CRterm. + *@param a_this the current instance of #CRTerm + *@return the zero terminated string containing the serialized + *form of #CRTerm. MUST BE FREED BY THE CALLER using g_free(). + */ +guchar * +cr_term_to_string (CRTerm const * a_this) +{ + GString *str_buf = NULL; + CRTerm const *cur = NULL; + guchar *result = NULL, + *content = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + g_return_val_if_fail (str_buf, NULL); + + for (cur = a_this; cur; cur = cur->next) { + if ((cur->content.str == NULL) + && (cur->content.num == NULL) + && (cur->content.rgb == NULL)) + continue; + + switch (cur->the_operator) { + case DIVIDE: + g_string_append (str_buf, " / "); + break; + + case COMMA: + g_string_append (str_buf, ", "); + break; + + case NO_OP: + if (cur->prev) { + g_string_append (str_buf, " "); + } + break; + default: + + break; + } + + switch (cur->unary_op) { + case PLUS_UOP: + g_string_append (str_buf, "+"); + break; + + case MINUS_UOP: + g_string_append (str_buf, "-"); + break; + + default: + break; + } + + switch (cur->type) { + case TERM_NUMBER: + if (cur->content.num) { + content = cr_num_to_string (cur->content.num); + } + + if (content) { + g_string_append (str_buf, (const gchar *) content); + g_free (content); + content = NULL; + } + + break; + + case TERM_FUNCTION: + if (cur->content.str) { + content = (guchar *) g_strndup + (cur->content.str->stryng->str, + cur->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, "%s(", + content); + + if (cur->ext_content.func_param) { + guchar *tmp_str = NULL; + + tmp_str = cr_term_to_string + (cur-> + ext_content.func_param); + + if (tmp_str) { + g_string_append (str_buf, + (const gchar *) tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + } + g_string_append (str_buf, ")"); + g_free (content); + content = NULL; + } + + break; + + case TERM_STRING: + if (cur->content.str) { + content = (guchar *) g_strndup + (cur->content.str->stryng->str, + cur->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, + "\"%s\"", content); + g_free (content); + content = NULL; + } + break; + + case TERM_IDENT: + if (cur->content.str) { + content = (guchar *) g_strndup + (cur->content.str->stryng->str, + cur->content.str->stryng->len); + } + + if (content) { + g_string_append (str_buf, (const gchar *) content); + g_free (content); + content = NULL; + } + break; + + case TERM_URI: + if (cur->content.str) { + content = (guchar *) g_strndup + (cur->content.str->stryng->str, + cur->content.str->stryng->len); + } + + if (content) { + g_string_append_printf + (str_buf, "url(%s)", content); + g_free (content); + content = NULL; + } + break; + + case TERM_RGB: + if (cur->content.rgb) { + guchar *tmp_str = NULL; + + g_string_append (str_buf, "rgb("); + tmp_str = cr_rgb_to_string (cur->content.rgb); + + if (tmp_str) { + g_string_append (str_buf, (const gchar *) tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + g_string_append (str_buf, ")"); + } + + break; + + case TERM_UNICODERANGE: + g_string_append + (str_buf, + "?found unicoderange: dump not supported yet?"); + break; + + case TERM_HASH: + if (cur->content.str) { + content = (guchar *) g_strndup + (cur->content.str->stryng->str, + cur->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, + "#%s", content); + g_free (content); + content = NULL; + } + break; + + default: + g_string_append (str_buf, + "Unrecognized Term type"); + break; + } + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +guchar * +cr_term_one_to_string (CRTerm const * a_this) +{ + GString *str_buf = NULL; + guchar *result = NULL, + *content = NULL; + + g_return_val_if_fail (a_this, NULL); + + str_buf = g_string_new (NULL); + g_return_val_if_fail (str_buf, NULL); + + if ((a_this->content.str == NULL) + && (a_this->content.num == NULL) + && (a_this->content.rgb == NULL)) + return NULL ; + + switch (a_this->the_operator) { + case DIVIDE: + g_string_append_printf (str_buf, " / "); + break; + + case COMMA: + g_string_append_printf (str_buf, ", "); + break; + + case NO_OP: + if (a_this->prev) { + g_string_append_printf (str_buf, " "); + } + break; + default: + + break; + } + + switch (a_this->unary_op) { + case PLUS_UOP: + g_string_append_printf (str_buf, "+"); + break; + + case MINUS_UOP: + g_string_append_printf (str_buf, "-"); + break; + + default: + break; + } + + switch (a_this->type) { + case TERM_NUMBER: + if (a_this->content.num) { + content = cr_num_to_string (a_this->content.num); + } + + if (content) { + g_string_append (str_buf, (const gchar *) content); + g_free (content); + content = NULL; + } + + break; + + case TERM_FUNCTION: + if (a_this->content.str) { + content = (guchar *) g_strndup + (a_this->content.str->stryng->str, + a_this->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, "%s(", + content); + + if (a_this->ext_content.func_param) { + guchar *tmp_str = NULL; + + tmp_str = cr_term_to_string + (a_this-> + ext_content.func_param); + + if (tmp_str) { + g_string_append_printf + (str_buf, + "%s", tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + + g_string_append_printf (str_buf, ")"); + g_free (content); + content = NULL; + } + } + + break; + + case TERM_STRING: + if (a_this->content.str) { + content = (guchar *) g_strndup + (a_this->content.str->stryng->str, + a_this->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, + "\"%s\"", content); + g_free (content); + content = NULL; + } + break; + + case TERM_IDENT: + if (a_this->content.str) { + content = (guchar *) g_strndup + (a_this->content.str->stryng->str, + a_this->content.str->stryng->len); + } + + if (content) { + g_string_append (str_buf, (const gchar *) content); + g_free (content); + content = NULL; + } + break; + + case TERM_URI: + if (a_this->content.str) { + content = (guchar *) g_strndup + (a_this->content.str->stryng->str, + a_this->content.str->stryng->len); + } + + if (content) { + g_string_append_printf + (str_buf, "url(%s)", content); + g_free (content); + content = NULL; + } + break; + + case TERM_RGB: + if (a_this->content.rgb) { + guchar *tmp_str = NULL; + + g_string_append_printf (str_buf, "rgb("); + tmp_str = cr_rgb_to_string (a_this->content.rgb); + + if (tmp_str) { + g_string_append (str_buf, (const gchar *) tmp_str); + g_free (tmp_str); + tmp_str = NULL; + } + g_string_append_printf (str_buf, ")"); + } + + break; + + case TERM_UNICODERANGE: + g_string_append_printf + (str_buf, + "?found unicoderange: dump not supported yet?"); + break; + + case TERM_HASH: + if (a_this->content.str) { + content = (guchar *) g_strndup + (a_this->content.str->stryng->str, + a_this->content.str->stryng->len); + } + + if (content) { + g_string_append_printf (str_buf, + "#%s", content); + g_free (content); + content = NULL; + } + break; + + default: + g_string_append_printf (str_buf, + "%s", + "Unrecognized Term type"); + break; + } + + if (str_buf) { + result = (guchar *) g_string_free (str_buf, FALSE); + str_buf = NULL; + } + + return result; +} + +/** + *Dumps the expression (a list of terms connected by operators) + *to a file. + *TODO: finish the dump. The dump of some type of terms have not yet been + *implemented. + *@param a_this the current instance of #CRTerm. + *@param a_fp the destination file pointer. + */ +void +cr_term_dump (CRTerm const * a_this, FILE * a_fp) +{ + guchar *content = NULL; + + g_return_if_fail (a_this); + + content = cr_term_to_string (a_this); + + if (content) { + fprintf (a_fp, "%s", content); + g_free (content); + } +} + +/** + *Return the number of terms in the expression. + *@param a_this the current instance of #CRTerm. + *@return number of terms in the expression. + */ +int +cr_term_nr_values (CRTerm const *a_this) +{ + CRTerm const *cur = NULL ; + int nr = 0; + + g_return_val_if_fail (a_this, -1) ; + + for (cur = a_this ; cur ; cur = cur->next) + nr ++; + return nr; +} + +/** + *Use an index to get a CRTerm from the expression. + *@param a_this the current instance of #CRTerm. + *@param itemnr the index into the expression. + *@return CRTerm at position itemnr, if itemnr > number of terms - 1, + *it will return NULL. + */ +CRTerm * +cr_term_get_from_list (CRTerm *a_this, int itemnr) +{ + CRTerm *cur = NULL ; + int nr = 0; + + g_return_val_if_fail (a_this, NULL) ; + + for (cur = a_this ; cur ; cur = cur->next) + if (nr++ == itemnr) + return cur; + return NULL; +} + +/** + *Increments the reference counter of the current instance + *of #CRTerm.* + *@param a_this the current instance of #CRTerm. + */ +void +cr_term_ref (CRTerm * a_this) +{ + g_return_if_fail (a_this); + + a_this->ref_count++; +} + +/** + *Decrements the ref count of the current instance of + *#CRTerm. If the ref count reaches zero, the instance is + *destroyed. + *@param a_this the current instance of #CRTerm. + *@return TRUE if the current instance has been destroyed, FALSE otherwise. + */ +gboolean +cr_term_unref (CRTerm * a_this) +{ + g_return_val_if_fail (a_this, FALSE); + + if (a_this->ref_count) { + a_this->ref_count--; + } + + if (a_this->ref_count == 0) { + cr_term_destroy (a_this); + return TRUE; + } + + return FALSE; +} + +/** + *The destructor of the the #CRTerm class. + *@param a_this the "this pointer" of the current instance + *of #CRTerm. + */ +void +cr_term_destroy (CRTerm * a_this) +{ + g_return_if_fail (a_this); + + cr_term_clear (a_this); + + if (a_this->next) { + cr_term_destroy (a_this->next); + a_this->next = NULL; + } + + if (a_this) { + g_free (a_this); + } + +} diff --git a/src/st/croco/cr-term.h b/src/st/croco/cr-term.h new file mode 100644 index 0000000..0f22dda --- /dev/null +++ b/src/st/croco/cr-term.h @@ -0,0 +1,190 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include <stdio.h> +#include <glib.h> +#include "cr-utils.h" +#include "cr-rgb.h" +#include "cr-num.h" +#include "cr-string.h" + +#ifndef __CR_TERM_H__ +#define __CR_TERM_H__ + +G_BEGIN_DECLS + +/** + *@file + *Declaration of the #CRTem class. + */ + +enum CRTermType +{ + TERM_NO_TYPE = 0, + TERM_NUMBER, + TERM_FUNCTION, + TERM_STRING, + TERM_IDENT, + TERM_URI, + TERM_RGB, + TERM_UNICODERANGE, + TERM_HASH +} ; + + +enum UnaryOperator +{ + NO_UNARY_UOP = 0, + PLUS_UOP, + MINUS_UOP, + EMPTY_UNARY_UOP +} ; + +enum Operator +{ + NO_OP = 0, + DIVIDE, + COMMA +} ; + +struct _CRTerm ; +typedef struct _CRTerm CRTerm ; + +/** + *An abstraction of a css2 term as + *defined in the CSS2 spec in appendix D.1: + *term ::= + *[ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* + *| ANGLE S* | TIME S* | FREQ S* | function ] + * | STRING S* | IDENT S* | URI S* | RGB S* + *| UNICODERANGE S* | hexcolor + */ +struct _CRTerm +{ + /** + *The type of the term. + */ + enum CRTermType type ; + + /** + *The unary operator associated to + *the current term. + */ + enum UnaryOperator unary_op ; + + /** + *The operator associated to the current term. + */ + enum Operator the_operator ; + + + /** + *The content of the term. + *Depending of the type of the term, + *this holds either a number, a percentage ... + */ + union + { + CRNum *num ; + CRString * str ; + CRRgb * rgb ; + } content ; + + /** + *If the term is of type UNICODERANGE, + *this field holds the upper bound of the range. + *if the term is of type FUNCTION, this holds + *an instance of CRTerm that represents + * the expression which is the argument of the function. + */ + union + { + CRTerm *func_param ; + } ext_content ; + + /** + *A spare pointer, just in case. + *Can be used by the application. + */ + gpointer app_data ; + + glong ref_count ; + + /** + *A pointer to the next term, + *just in case this term is part of + *an expression. + */ + CRTerm *next ; + + /** + *A pointer to the previous + *term. + */ + CRTerm *prev ; + CRParsingLocation location ; +} ; + +CRTerm * cr_term_parse_expression_from_buf (const guchar *a_buf, + enum CREncoding a_encoding) ; +CRTerm * cr_term_new (void) ; + +enum CRStatus cr_term_set_number (CRTerm *a_this, CRNum *a_num) ; + +enum CRStatus cr_term_set_function (CRTerm *a_this, + CRString *a_func_name, + CRTerm *a_func_param) ; + +enum CRStatus cr_term_set_string (CRTerm *a_this, CRString *a_str) ; + +enum CRStatus cr_term_set_ident (CRTerm *a_this, CRString *a_str) ; + +enum CRStatus cr_term_set_uri (CRTerm *a_this, CRString *a_str) ; + +enum CRStatus cr_term_set_rgb (CRTerm *a_this, CRRgb *a_rgb) ; + +enum CRStatus cr_term_set_hash (CRTerm *a_this, CRString *a_str) ; + +CRTerm * cr_term_append_term (CRTerm *a_this, CRTerm *a_new_term) ; + +CRTerm * cr_term_prepend_term (CRTerm *a_this, CRTerm *a_new_term) ; + +guchar * cr_term_to_string (CRTerm const *a_this) ; + +guchar * cr_term_one_to_string (CRTerm const * a_this) ; + +void cr_term_dump (CRTerm const *a_this, FILE *a_fp) ; + +int cr_term_nr_values (CRTerm const *a_this) ; + +CRTerm * cr_term_get_from_list (CRTerm *a_this, int itemnr) ; + +void cr_term_ref (CRTerm *a_this) ; + +gboolean cr_term_unref (CRTerm *a_this) ; + +void cr_term_destroy (CRTerm * a_term) ; + +G_END_DECLS + +#endif /*__CR_TERM_H__*/ diff --git a/src/st/croco/cr-tknzr.c b/src/st/croco/cr-tknzr.c new file mode 100644 index 0000000..54f18f2 --- /dev/null +++ b/src/st/croco/cr-tknzr.c @@ -0,0 +1,2762 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See the COPYRIGHTS file for copyrights information. + */ + +/** + *@file + *The definition of the #CRTknzr (tokenizer) + *class. + */ + +#include "string.h" +#include "cr-tknzr.h" +#include "cr-doc-handler.h" + +struct _CRTknzrPriv { + /**The parser input stream of bytes*/ + CRInput *input; + + /** + *A cache where tknzr_unget_token() + *puts back the token. tknzr_get_next_token() + *first look in this cache, and if and + *only if it's empty, fetches the next token + *from the input stream. + */ + CRToken *token_cache; + + /** + *The position of the end of the previous token + *or char fetched. + */ + CRInputPos prev_pos; + + CRDocHandler *sac_handler; + + /** + *The reference count of the current instance + *of #CRTknzr. Is manipulated by cr_tknzr_ref() + *and cr_tknzr_unref(). + */ + glong ref_count; +}; + +#define PRIVATE(obj) ((obj)->priv) + +/** + *return TRUE if the character is a number ([0-9]), FALSE otherwise + *@param a_char the char to test. + */ +#define IS_NUM(a_char) (((a_char) >= '0' && (a_char) <= '9')?TRUE:FALSE) + +/** + *Checks if 'status' equals CR_OK. If not, goto the 'error' label. + * + *@param status the status (of type enum CRStatus) to test. + *@param is_exception if set to FALSE, the final status returned the + *current function will be CR_PARSING_ERROR. If set to TRUE, the + *current status will be the current value of the 'status' variable. + * + */ +#define CHECK_PARSING_STATUS(status, is_exception) \ +if ((status) != CR_OK) \ +{ \ + if (is_exception == FALSE) \ + { \ + status = CR_PARSING_ERROR ; \ + } \ + goto error ; \ +} + +/** + *Peeks the next char from the input stream of the current tokenizer. + *invokes CHECK_PARSING_STATUS on the status returned by + *cr_tknzr_input_peek_char(). + * + *@param the current instance of #CRTkzr. + *@param to_char a pointer to the char where to store the + *char peeked. + */ +#define PEEK_NEXT_CHAR(a_tknzr, a_to_char) \ +{\ +status = cr_tknzr_peek_char (a_tknzr, a_to_char) ; \ +CHECK_PARSING_STATUS (status, TRUE) \ +} + +/** + *Reads the next char from the input stream of the current parser. + *In case of error, jumps to the "error:" label located in the + *function where this macro is called. + *@param parser the current instance of #CRTknzr + *@param to_char a pointer to the guint32 char where to store + *the character read. + */ +#define READ_NEXT_CHAR(a_tknzr, to_char) \ +status = cr_tknzr_read_char (a_tknzr, to_char) ;\ +CHECK_PARSING_STATUS (status, TRUE) + +/** + *Gets information about the current position in + *the input of the parser. + *In case of failure, this macro returns from the + *calling function and + *returns a status code of type enum #CRStatus. + *@param parser the current instance of #CRTknzr. + *@param pos out parameter. A pointer to the position + *inside the current parser input. Must + */ +#define RECORD_INITIAL_POS(a_tknzr, a_pos) \ +status = cr_input_get_cur_pos (PRIVATE \ +(a_tknzr)->input, a_pos) ; \ +g_return_val_if_fail (status == CR_OK, status) + +/** + *Gets the address of the current byte inside the + *parser input. + *@param parser the current instance of #CRTknzr. + *@param addr out parameter a pointer (guchar*) + *to where the address must be put. + */ +#define RECORD_CUR_BYTE_ADDR(a_tknzr, a_addr) \ +status = cr_input_get_cur_byte_addr \ + (PRIVATE (a_tknzr)->input, a_addr) ; \ +CHECK_PARSING_STATUS (status, TRUE) + +/** + *Peeks a byte from the topmost parser input at + *a given offset from the current position. + *If it fails, goto the "error:" label. + * + *@param a_parser the current instance of #CRTknzr. + *@param a_offset the offset of the byte to peek, the + *current byte having the offset '0'. + *@param a_byte_ptr out parameter a pointer (guchar*) to + *where the peeked char is to be stored. + */ +#define PEEK_BYTE(a_tknzr, a_offset, a_byte_ptr) \ +status = cr_tknzr_peek_byte (a_tknzr, \ + a_offset, \ + a_byte_ptr) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; + +#define BYTE(a_input, a_n, a_eof) \ +cr_input_peek_byte2 (a_input, a_n, a_eof) + +/** + *Reads a byte from the topmost parser input + *steam. + *If it fails, goto the "error" label. + *@param a_parser the current instance of #CRTknzr. + *@param a_byte_ptr the guchar * where to put the read char. + */ +#define READ_NEXT_BYTE(a_tknzr, a_byte_ptr) \ +status = \ +cr_input_read_byte (PRIVATE (a_tknzr)->input, a_byte_ptr) ;\ +CHECK_PARSING_STATUS (status, TRUE) ; + +/** + *Skips a given number of byte in the topmost + *parser input. Don't update line and column number. + *In case of error, jumps to the "error:" label + *of the surrounding function. + *@param a_parser the current instance of #CRTknzr. + *@param a_nb_bytes the number of bytes to skip. + */ +#define SKIP_BYTES(a_tknzr, a_nb_bytes) \ +status = cr_input_seek_index (PRIVATE (a_tknzr)->input, \ + CR_SEEK_CUR, a_nb_bytes) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; + +/** + *Skip utf8 encoded characters. + *Updates line and column numbers. + *@param a_parser the current instance of #CRTknzr. + *@param a_nb_chars the number of chars to skip. Must be of + *type glong. + */ +#define SKIP_CHARS(a_tknzr, a_nb_chars) \ +{ \ +gulong nb_chars = a_nb_chars ; \ +status = cr_input_consume_chars \ + (PRIVATE (a_tknzr)->input,0, &nb_chars) ; \ +CHECK_PARSING_STATUS (status, TRUE) ; \ +} + +/** + *Tests the condition and if it is false, sets + *status to "CR_PARSING_ERROR" and goto the 'error' + *label. + *@param condition the condition to test. + */ +#define ENSURE_PARSING_COND(condition) \ +if (! (condition)) {status = CR_PARSING_ERROR; goto error ;} + +static enum CRStatus cr_tknzr_parse_nl (CRTknzr * a_this, + guchar ** a_start, + guchar ** a_end, + CRParsingLocation *a_location); + +static enum CRStatus cr_tknzr_parse_w (CRTknzr * a_this, + guchar ** a_start, + guchar ** a_end, + CRParsingLocation *a_location) ; + +static enum CRStatus cr_tknzr_parse_unicode_escape (CRTknzr * a_this, + guint32 * a_unicode, + CRParsingLocation *a_location) ; + +static enum CRStatus cr_tknzr_parse_escape (CRTknzr * a_this, + guint32 * a_esc_code, + CRParsingLocation *a_location); + +static enum CRStatus cr_tknzr_parse_string (CRTknzr * a_this, + CRString ** a_str); + +static enum CRStatus cr_tknzr_parse_comment (CRTknzr * a_this, + CRString ** a_comment); + +static enum CRStatus cr_tknzr_parse_nmstart (CRTknzr * a_this, + guint32 * a_char, + CRParsingLocation *a_location); + +static enum CRStatus cr_tknzr_parse_num (CRTknzr * a_this, + CRNum ** a_num); + +/********************************** + *PRIVATE methods + **********************************/ + +/** + *Parses a "w" as defined by the css spec at [4.1.1]: + * w ::= [ \t\r\n\f]* + * + *@param a_this the current instance of #CRTknzr. + *@param a_start out param. Upon successful completion, points + *to the beginning of the parsed white space, points to NULL otherwise. + *Can also point to NULL is there is no white space actually. + *@param a_end out param. Upon successful completion, points + *to the end of the parsed white space, points to NULL otherwise. + *Can also point to NULL is there is no white space actually. + */ +static enum CRStatus +cr_tknzr_parse_w (CRTknzr * a_this, + guchar ** a_start, + guchar ** a_end, + CRParsingLocation *a_location) +{ + guint32 cur_char = 0; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_start && a_end, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + *a_start = NULL; + *a_end = NULL; + + READ_NEXT_CHAR (a_this, &cur_char); + + if (cr_utils_is_white_space (cur_char) == FALSE) { + status = CR_PARSING_ERROR; + goto error; + } + if (a_location) { + cr_tknzr_get_parsing_location (a_this, + a_location) ; + } + RECORD_CUR_BYTE_ADDR (a_this, a_start); + *a_end = *a_start; + + for (;;) { + gboolean is_eof = FALSE; + + cr_input_get_end_of_file (PRIVATE (a_this)->input, &is_eof); + if (is_eof) + break; + + status = cr_tknzr_peek_char (a_this, &cur_char); + if (status == CR_END_OF_INPUT_ERROR) { + break; + } else if (status != CR_OK) { + goto error; + } + + if (cr_utils_is_white_space (cur_char) == TRUE) { + READ_NEXT_CHAR (a_this, &cur_char); + RECORD_CUR_BYTE_ADDR (a_this, a_end); + } else { + break; + } + } + + return CR_OK; + + error: + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; +} + +/** + *Parses a newline as defined in the css2 spec: + * nl ::= \n|\r\n|\r|\f + * + *@param a_this the "this pointer" of the current instance of #CRTknzr. + *@param a_start a pointer to the first character of the successfully + *parsed string. + *@param a_end a pointer to the last character of the successfully parsed + *string. + *@result CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_nl (CRTknzr * a_this, + guchar ** a_start, + guchar ** a_end, + CRParsingLocation *a_location) +{ + CRInputPos init_pos; + guchar next_chars[2] = { 0 }; + enum CRStatus status = CR_PARSING_ERROR; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_start && a_end, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PEEK_BYTE (a_this, 1, &next_chars[0]); + PEEK_BYTE (a_this, 2, &next_chars[1]); + + if ((next_chars[0] == '\r' && next_chars[1] == '\n')) { + SKIP_BYTES (a_this, 1); + if (a_location) { + cr_tknzr_get_parsing_location + (a_this, a_location) ; + } + SKIP_CHARS (a_this, 1); + + RECORD_CUR_BYTE_ADDR (a_this, a_end); + + status = CR_OK; + } else if (next_chars[0] == '\n' + || next_chars[0] == '\r' || next_chars[0] == '\f') { + SKIP_CHARS (a_this, 1); + if (a_location) { + cr_tknzr_get_parsing_location + (a_this, a_location) ; + } + RECORD_CUR_BYTE_ADDR (a_this, a_start); + *a_end = *a_start; + status = CR_OK; + } else { + status = CR_PARSING_ERROR; + goto error; + } + return CR_OK ; + + error: + cr_tknzr_set_cur_pos (a_this, &init_pos) ; + return status; +} + +/** + *Go ahead in the parser input, skipping all the spaces. + *If the next char if not a white space, this function does nothing. + *In any cases, it stops when it encounters a non white space character. + * + *@param a_this the current instance of #CRTknzr. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_try_to_skip_spaces (CRTknzr * a_this) +{ + enum CRStatus status = CR_ERROR; + guint32 cur_char = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + + status = cr_input_peek_char (PRIVATE (a_this)->input, &cur_char); + + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) + return CR_OK; + return status; + } + + if (cr_utils_is_white_space (cur_char) == TRUE) { + gulong nb_chars = -1; /*consume all spaces */ + + status = cr_input_consume_white_spaces + (PRIVATE (a_this)->input, &nb_chars); + } + + return status; +} + +/** + *Parses a "comment" as defined in the css spec at [4.1.1]: + *COMMENT ::= \/\*[^*]*\*+([^/][^*]*\*+)*\/ . + *This complex regexp is just to say that comments start + *with the two chars '/''*' and ends with the two chars '*''/'. + *It also means that comments cannot be nested. + *So based on that, I've just tried to implement the parsing function + *simply and in a straight forward manner. + */ +static enum CRStatus +cr_tknzr_parse_comment (CRTknzr * a_this, + CRString ** a_comment) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + guint32 cur_char = 0, next_char= 0; + CRString *comment = NULL; + CRParsingLocation loc = {0} ; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + READ_NEXT_CHAR (a_this, &cur_char) ; + ENSURE_PARSING_COND (cur_char == '/'); + cr_tknzr_get_parsing_location (a_this, &loc) ; + + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == '*'); + comment = cr_string_new (); + for (;;) { /* [^*]* */ + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char == '*') + break; + READ_NEXT_CHAR (a_this, &cur_char); + g_string_append_unichar (comment->stryng, cur_char); + } + /* Stop condition: next_char == '*' */ + for (;;) { /* \*+ */ + READ_NEXT_CHAR(a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == '*'); + g_string_append_unichar (comment->stryng, cur_char); + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char != '*') + break; + } + /* Stop condition: next_char != '*' */ + for (;;) { /* ([^/][^*]*\*+)* */ + if (next_char == '/') + break; + READ_NEXT_CHAR(a_this, &cur_char); + g_string_append_unichar (comment->stryng, cur_char); + for (;;) { /* [^*]* */ + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char == '*') + break; + READ_NEXT_CHAR (a_this, &cur_char); + g_string_append_unichar (comment->stryng, cur_char); + } + /* Stop condition: next_char = '*', no need to verify, because peek and read exit to error anyway */ + for (;;) { /* \*+ */ + READ_NEXT_CHAR(a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == '*'); + g_string_append_unichar (comment->stryng, cur_char); + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char != '*') + break; + } + /* Continue condition: next_char != '*' */ + } + /* Stop condition: next_char == '\/' */ + READ_NEXT_CHAR(a_this, &cur_char); + g_string_append_unichar (comment->stryng, cur_char); + + if (status == CR_OK) { + cr_parsing_location_copy (&comment->location, + &loc) ; + *a_comment = comment; + return CR_OK; + } + error: + + if (comment) { + cr_string_destroy (comment); + comment = NULL; + } + + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; +} + +/** + *Parses an 'unicode' escape sequence defined + *in css spec at chap 4.1.1: + *unicode ::= \\[0-9a-f]{1,6}[ \n\r\t\f]? + *@param a_this the current instance of #CRTknzr. + *@param a_start out parameter. A pointer to the start + *of the unicode escape sequence. Must *NOT* be deleted by + *the caller. + *@param a_end out parameter. A pointer to the last character + *of the unicode escape sequence. Must *NOT* be deleted by the caller. + *@return CR_OK if parsing succeeded, an error code otherwise. + *Error code can be either CR_PARSING_ERROR if the string + *parsed just doesn't + *respect the production or another error if a + *lower level error occurred. + */ +static enum CRStatus +cr_tknzr_parse_unicode_escape (CRTknzr * a_this, + guint32 * a_unicode, + CRParsingLocation *a_location) +{ + guint32 cur_char; + CRInputPos init_pos; + glong occur = 0; + guint32 unicode = 0; + guchar *tmp_char_ptr1 = NULL, + *tmp_char_ptr2 = NULL; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_unicode, CR_BAD_PARAM_ERROR); + + /*first, let's backup the current position pointer */ + RECORD_INITIAL_POS (a_this, &init_pos); + + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char != '\\') { + status = CR_PARSING_ERROR; + goto error; + } + if (a_location) { + cr_tknzr_get_parsing_location + (a_this, a_location) ; + } + PEEK_NEXT_CHAR (a_this, &cur_char); + + for (occur = 0, unicode = 0; ((cur_char >= '0' && cur_char <= '9') + || (cur_char >= 'a' && cur_char <= 'f') + || (cur_char >= 'A' && cur_char <= 'F')) + && occur < 6; occur++) { + gint cur_char_val = 0; + + READ_NEXT_CHAR (a_this, &cur_char); + + if ((cur_char >= '0' && cur_char <= '9')) { + cur_char_val = (cur_char - '0'); + } else if ((cur_char >= 'a' && cur_char <= 'f')) { + cur_char_val = 10 + (cur_char - 'a'); + } else if ((cur_char >= 'A' && cur_char <= 'F')) { + cur_char_val = 10 + (cur_char - 'A'); + } + + unicode = unicode * 16 + cur_char_val; + + PEEK_NEXT_CHAR (a_this, &cur_char); + } + + /* Eat a whitespace if possible. */ + cr_tknzr_parse_w (a_this, &tmp_char_ptr1, + &tmp_char_ptr2, NULL); + *a_unicode = unicode; + return CR_OK; + + error: + /* + *restore the initial position pointer backuped at + *the beginning of this function. + */ + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; +} + +/** + *parses an escape sequence as defined by the css spec: + *escape ::= {unicode}|\\[ -~\200-\4177777] + *@param a_this the current instance of #CRTknzr . + */ +static enum CRStatus +cr_tknzr_parse_escape (CRTknzr * a_this, guint32 * a_esc_code, + CRParsingLocation *a_location) +{ + enum CRStatus status = CR_OK; + guint32 cur_char = 0; + CRInputPos init_pos; + guchar next_chars[2]; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_esc_code, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PEEK_BYTE (a_this, 1, &next_chars[0]); + PEEK_BYTE (a_this, 2, &next_chars[1]); + + if (next_chars[0] != '\\') { + status = CR_PARSING_ERROR; + goto error; + } + + if ((next_chars[1] >= '0' && next_chars[1] <= '9') + || (next_chars[1] >= 'a' && next_chars[1] <= 'f') + || (next_chars[1] >= 'A' && next_chars[1] <= 'F')) { + status = cr_tknzr_parse_unicode_escape (a_this, a_esc_code, + a_location); + } else { + /*consume the '\' char */ + READ_NEXT_CHAR (a_this, &cur_char); + if (a_location) { + cr_tknzr_get_parsing_location (a_this, + a_location) ; + } + /*then read the char after the '\' */ + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char != ' ' && (cur_char < 200 || cur_char > 4177777)) { + status = CR_PARSING_ERROR; + goto error; + } + *a_esc_code = cur_char; + + } + if (status == CR_OK) { + return CR_OK; + } + error: + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +/** + *Parses a string type as defined in css spec [4.1.1]: + * + *string ::= {string1}|{string2} + *string1 ::= \"([\t !#$%&(-~]|\\{nl}|\'|{nonascii}|{escape})*\" + *string2 ::= \'([\t !#$%&(-~]|\\{nl}|\"|{nonascii}|{escape})*\' + * + *@param a_this the current instance of #CRTknzr. + *@param a_start out parameter. Upon successful completion, + *points to the beginning of the string, points to an undefined value + *otherwise. + *@param a_end out parameter. Upon successful completion, points to + *the beginning of the string, points to an undefined value otherwise. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_string (CRTknzr * a_this, CRString ** a_str) +{ + guint32 cur_char = 0, + delim = 0; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + CRString *str = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_str, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char == '"') + delim = '"'; + else if (cur_char == '\'') + delim = '\''; + else { + status = CR_PARSING_ERROR; + goto error; + } + str = cr_string_new (); + if (str) { + cr_tknzr_get_parsing_location + (a_this, &str->location) ; + } + for (;;) { + guchar next_chars[2] = { 0 }; + + PEEK_BYTE (a_this, 1, &next_chars[0]); + PEEK_BYTE (a_this, 2, &next_chars[1]); + + if (next_chars[0] == '\\') { + guchar *tmp_char_ptr1 = NULL, + *tmp_char_ptr2 = NULL; + guint32 esc_code = 0; + + if (next_chars[1] == '\'' || next_chars[1] == '"') { + g_string_append_unichar (str->stryng, + next_chars[1]); + SKIP_BYTES (a_this, 2); + status = CR_OK; + } else { + status = cr_tknzr_parse_escape + (a_this, &esc_code, NULL); + + if (status == CR_OK) { + g_string_append_unichar + (str->stryng, + esc_code); + } + } + + if (status != CR_OK) { + /* + *consume the '\' char, and try to parse + *a newline. + */ + READ_NEXT_CHAR (a_this, &cur_char); + + status = cr_tknzr_parse_nl + (a_this, &tmp_char_ptr1, + &tmp_char_ptr2, NULL); + } + + CHECK_PARSING_STATUS (status, FALSE); + } else if (strchr ("\t !#$%&", next_chars[0]) + || (next_chars[0] >= '(' && next_chars[0] <= '~')) { + READ_NEXT_CHAR (a_this, &cur_char); + g_string_append_unichar (str->stryng, + cur_char); + status = CR_OK; + } + + else if (cr_utils_is_nonascii (next_chars[0])) { + READ_NEXT_CHAR (a_this, &cur_char); + g_string_append_unichar (str->stryng, cur_char); + } else if (next_chars[0] == delim) { + READ_NEXT_CHAR (a_this, &cur_char); + break; + } else { + status = CR_PARSING_ERROR; + goto error; + } + } + + if (status == CR_OK) { + if (*a_str == NULL) { + *a_str = str; + str = NULL; + } else { + (*a_str)->stryng = g_string_append_len + ((*a_str)->stryng, + str->stryng->str, + str->stryng->len); + cr_string_destroy (str); + } + return CR_OK; + } + + error: + + if (str) { + cr_string_destroy (str) ; + str = NULL; + } + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +/** + *Parses the an nmstart as defined by the css2 spec [4.1.1]: + * nmstart [a-zA-Z]|{nonascii}|{escape} + * + *@param a_this the current instance of #CRTknzr. + *@param a_start out param. A pointer to the starting point of + *the token. + *@param a_end out param. A pointer to the ending point of the + *token. + *@param a_char out param. The actual parsed nmchar. + *@return CR_OK upon successful completion, + *an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_nmstart (CRTknzr * a_this, + guint32 * a_char, + CRParsingLocation *a_location) +{ + CRInputPos init_pos; + enum CRStatus status = CR_OK; + guint32 cur_char = 0, + next_char = 0; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_char, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PEEK_NEXT_CHAR (a_this, &next_char); + + if (next_char == '\\') { + status = cr_tknzr_parse_escape (a_this, a_char, + a_location); + + if (status != CR_OK) + goto error; + + } else if (cr_utils_is_nonascii (next_char) == TRUE + || ((next_char >= 'a') && (next_char <= 'z')) + || ((next_char >= 'A') && (next_char <= 'Z')) + ) { + READ_NEXT_CHAR (a_this, &cur_char); + if (a_location) { + cr_tknzr_get_parsing_location (a_this, + a_location) ; + } + *a_char = cur_char; + status = CR_OK; + } else { + status = CR_PARSING_ERROR; + goto error; + } + + return CR_OK; + + error: + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; + +} + +/** + *Parses an nmchar as described in the css spec at + *chap 4.1.1: + *nmchar ::= [a-z0-9-]|{nonascii}|{escape} + * + *Humm, I have added the possibility for nmchar to + *contain upper case letters. + * + *@param a_this the current instance of #CRTknzr. + *@param a_start out param. A pointer to the starting point of + *the token. + *@param a_end out param. A pointer to the ending point of the + *token. + *@param a_char out param. The actual parsed nmchar. + *@return CR_OK upon successful completion, + *an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_nmchar (CRTknzr * a_this, guint32 * a_char, + CRParsingLocation *a_location) +{ + guint32 cur_char = 0, + next_char = 0; + enum CRStatus status = CR_OK; + CRInputPos init_pos; + + g_return_val_if_fail (a_this && PRIVATE (a_this) && a_char, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_input_peek_char (PRIVATE (a_this)->input, + &next_char) ; + if (status != CR_OK) + goto error; + + if (next_char == '\\') { + status = cr_tknzr_parse_escape (a_this, a_char, + a_location); + + if (status != CR_OK) + goto error; + + } else if (cr_utils_is_nonascii (next_char) == TRUE + || ((next_char >= 'a') && (next_char <= 'z')) + || ((next_char >= 'A') && (next_char <= 'Z')) + || ((next_char >= '0') && (next_char <= '9')) + || (next_char == '-') + || (next_char == '_') /*'_' not allowed by the spec. */ + ) { + READ_NEXT_CHAR (a_this, &cur_char); + *a_char = cur_char; + status = CR_OK; + if (a_location) { + cr_tknzr_get_parsing_location + (a_this, a_location) ; + } + } else { + status = CR_PARSING_ERROR; + goto error; + } + return CR_OK; + + error: + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +/** + *Parses an "ident" as defined in css spec [4.1.1]: + *ident ::= {nmstart}{nmchar}* + * + *Actually parses it using the css3 grammar: + *ident ::= -?{nmstart}{nmchar}* + *@param a_this the currens instance of #CRTknzr. + * + *@param a_str a pointer to parsed ident. If *a_str is NULL, + *this function allocates a new instance of CRString. If not, + *the function just appends the parsed string to the one passed. + *In both cases it is up to the caller to free *a_str. + * + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +static enum CRStatus +cr_tknzr_parse_ident (CRTknzr * a_this, CRString ** a_str) +{ + guint32 tmp_char = 0; + CRString *stringue = NULL ; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + gboolean location_is_set = FALSE ; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_str, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + PEEK_NEXT_CHAR (a_this, &tmp_char) ; + stringue = cr_string_new () ; + g_return_val_if_fail (stringue, + CR_OUT_OF_MEMORY_ERROR) ; + + if (tmp_char == '-') { + READ_NEXT_CHAR (a_this, &tmp_char) ; + cr_tknzr_get_parsing_location + (a_this, &stringue->location) ; + location_is_set = TRUE ; + g_string_append_unichar (stringue->stryng, + tmp_char) ; + } + status = cr_tknzr_parse_nmstart (a_this, &tmp_char, NULL); + if (status != CR_OK) { + status = CR_PARSING_ERROR; + goto end ; + } + if (location_is_set == FALSE) { + cr_tknzr_get_parsing_location + (a_this, &stringue->location) ; + location_is_set = TRUE ; + } + g_string_append_unichar (stringue->stryng, tmp_char); + for (;;) { + status = cr_tknzr_parse_nmchar (a_this, + &tmp_char, + NULL); + if (status != CR_OK) { + status = CR_OK ; + break; + } + g_string_append_unichar (stringue->stryng, tmp_char); + } + if (status == CR_OK) { + if (!*a_str) { + *a_str = stringue ; + + } else { + g_string_append_len ((*a_str)->stryng, + stringue->stryng->str, + stringue->stryng->len) ; + cr_string_destroy (stringue) ; + } + stringue = NULL ; + } + + error: + end: + if (stringue) { + cr_string_destroy (stringue) ; + stringue = NULL ; + } + if (status != CR_OK ) { + cr_tknzr_set_cur_pos (a_this, &init_pos) ; + } + return status ; +} + + +/** + *Parses a "name" as defined by css spec [4.1.1]: + *name ::= {nmchar}+ + * + *@param a_this the current instance of #CRTknzr. + * + *@param a_str out parameter. A pointer to the successfully parsed + *name. If *a_str is set to NULL, this function allocates a new instance + *of CRString. If not, it just appends the parsed name to the passed *a_str. + *In both cases, it is up to the caller to free *a_str. + * + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_name (CRTknzr * a_this, + CRString ** a_str) +{ + guint32 tmp_char = 0; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + gboolean str_needs_free = FALSE, + is_first_nmchar=TRUE ; + glong i = 0; + CRParsingLocation loc = {0} ; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_str, + CR_BAD_PARAM_ERROR) ; + + RECORD_INITIAL_POS (a_this, &init_pos); + + if (*a_str == NULL) { + *a_str = cr_string_new (); + str_needs_free = TRUE; + } + for (i = 0;; i++) { + if (is_first_nmchar == TRUE) { + status = cr_tknzr_parse_nmchar + (a_this, &tmp_char, + &loc) ; + is_first_nmchar = FALSE ; + } else { + status = cr_tknzr_parse_nmchar + (a_this, &tmp_char, NULL) ; + } + if (status != CR_OK) + break; + g_string_append_unichar ((*a_str)->stryng, + tmp_char); + } + if (i > 0) { + cr_parsing_location_copy + (&(*a_str)->location, &loc) ; + return CR_OK; + } + if (str_needs_free == TRUE && *a_str) { + cr_string_destroy (*a_str); + *a_str = NULL; + } + cr_tknzr_set_cur_pos (a_this, &init_pos); + return CR_PARSING_ERROR; +} + +/** + *Parses a "hash" as defined by the css spec in [4.1.1]: + *HASH ::= #{name} + */ +static enum CRStatus +cr_tknzr_parse_hash (CRTknzr * a_this, CRString ** a_str) +{ + guint32 cur_char = 0; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + gboolean str_needs_free = FALSE; + CRParsingLocation loc = {0} ; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + READ_NEXT_CHAR (a_this, &cur_char); + if (cur_char != '#') { + status = CR_PARSING_ERROR; + goto error; + } + if (*a_str == NULL) { + *a_str = cr_string_new (); + str_needs_free = TRUE; + } + cr_tknzr_get_parsing_location (a_this, + &loc) ; + status = cr_tknzr_parse_name (a_this, a_str); + cr_parsing_location_copy (&(*a_str)->location, &loc) ; + if (status != CR_OK) { + goto error; + } + return CR_OK; + + error: + if (str_needs_free == TRUE && *a_str) { + cr_string_destroy (*a_str); + *a_str = NULL; + } + + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +/** + *Parses an uri as defined by the css spec [4.1.1]: + * URI ::= url\({w}{string}{w}\) + * |url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}\) + * + *@param a_this the current instance of #CRTknzr. + *@param a_str the successfully parsed url. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_uri (CRTknzr * a_this, + CRString ** a_str) +{ + guint32 cur_char = 0; + CRInputPos init_pos; + enum CRStatus status = CR_PARSING_ERROR; + guchar tab[4] = { 0 }, *tmp_ptr1 = NULL, *tmp_ptr2 = NULL; + CRString *str = NULL; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this + && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_str, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PEEK_BYTE (a_this, 1, &tab[0]); + PEEK_BYTE (a_this, 2, &tab[1]); + PEEK_BYTE (a_this, 3, &tab[2]); + PEEK_BYTE (a_this, 4, &tab[3]); + + if (tab[0] != 'u' || tab[1] != 'r' || tab[2] != 'l' || tab[3] != '(') { + status = CR_PARSING_ERROR; + goto error; + } + /* + *Here, we want to skip 4 bytes ('u''r''l''('). + *But we also need to keep track of the parsing location + *of the 'u'. So, we skip 1 byte, we record the parsing + *location, then we skip the 3 remaining bytes. + */ + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, &location) ; + SKIP_CHARS (a_this, 3); + cr_tknzr_try_to_skip_spaces (a_this); + status = cr_tknzr_parse_string (a_this, a_str); + + if (status == CR_OK) { + guint32 next_char = 0; + status = cr_tknzr_parse_w (a_this, &tmp_ptr1, + &tmp_ptr2, NULL); + cr_tknzr_try_to_skip_spaces (a_this); + PEEK_NEXT_CHAR (a_this, &next_char); + if (next_char == ')') { + READ_NEXT_CHAR (a_this, &cur_char); + status = CR_OK; + } else { + status = CR_PARSING_ERROR; + } + } + if (status != CR_OK) { + str = cr_string_new (); + for (;;) { + guint32 next_char = 0; + PEEK_NEXT_CHAR (a_this, &next_char); + if (strchr ("!#$%&", next_char) + || (next_char >= '*' && next_char <= '~') + || (cr_utils_is_nonascii (next_char) == TRUE)) { + READ_NEXT_CHAR (a_this, &cur_char); + g_string_append_unichar + (str->stryng, cur_char); + status = CR_OK; + } else { + guint32 esc_code = 0; + status = cr_tknzr_parse_escape + (a_this, &esc_code, NULL); + if (status == CR_OK) { + g_string_append_unichar + (str->stryng, + esc_code); + } else { + status = CR_OK; + break; + } + } + } + cr_tknzr_try_to_skip_spaces (a_this); + READ_NEXT_CHAR (a_this, &cur_char); + if (cur_char == ')') { + status = CR_OK; + } else { + status = CR_PARSING_ERROR; + goto error; + } + if (str) { + if (*a_str == NULL) { + *a_str = str; + str = NULL; + } else { + g_string_append_len + ((*a_str)->stryng, + str->stryng->str, + str->stryng->len); + cr_string_destroy (str); + } + } + } + + cr_parsing_location_copy + (&(*a_str)->location, + &location) ; + return CR_OK ; + error: + if (str) { + cr_string_destroy (str); + str = NULL; + } + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +/** + *parses an RGB as defined in the css2 spec. + *rgb: rgb '('S*{num}%?S* ',' {num}#?S*,S*{num}#?S*')' + * + *@param a_this the "this pointer" of the current instance of + *@param a_rgb out parameter the parsed rgb. + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_rgb (CRTknzr * a_this, CRRgb ** a_rgb) +{ + enum CRStatus status = CR_OK; + CRInputPos init_pos; + CRNum *num = NULL; + guchar next_bytes[3] = { 0 }, cur_byte = 0; + glong red = 0, + green = 0, + blue = 0, + i = 0; + gboolean is_percentage = FALSE; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + PEEK_BYTE (a_this, 1, &next_bytes[0]); + PEEK_BYTE (a_this, 2, &next_bytes[1]); + PEEK_BYTE (a_this, 3, &next_bytes[2]); + + if (((next_bytes[0] == 'r') || (next_bytes[0] == 'R')) + && ((next_bytes[1] == 'g') || (next_bytes[1] == 'G')) + && ((next_bytes[2] == 'b') || (next_bytes[2] == 'B'))) { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, &location) ; + SKIP_CHARS (a_this, 2); + } else { + status = CR_PARSING_ERROR; + goto error; + } + READ_NEXT_BYTE (a_this, &cur_byte); + ENSURE_PARSING_COND (cur_byte == '('); + + cr_tknzr_try_to_skip_spaces (a_this); + status = cr_tknzr_parse_num (a_this, &num); + ENSURE_PARSING_COND ((status == CR_OK) && (num != NULL)); + + if (num->val > G_MAXLONG) { + status = CR_PARSING_ERROR; + goto error; + } + + red = num->val; + cr_num_destroy (num); + num = NULL; + + PEEK_BYTE (a_this, 1, &next_bytes[0]); + if (next_bytes[0] == '%') { + SKIP_CHARS (a_this, 1); + is_percentage = TRUE; + } + cr_tknzr_try_to_skip_spaces (a_this); + + for (i = 0; i < 2; i++) { + READ_NEXT_BYTE (a_this, &cur_byte); + ENSURE_PARSING_COND (cur_byte == ','); + + cr_tknzr_try_to_skip_spaces (a_this); + status = cr_tknzr_parse_num (a_this, &num); + ENSURE_PARSING_COND ((status == CR_OK) && (num != NULL)); + + if (num->val > G_MAXLONG) { + status = CR_PARSING_ERROR; + goto error; + } + + PEEK_BYTE (a_this, 1, &next_bytes[0]); + if (next_bytes[0] == '%') { + SKIP_CHARS (a_this, 1); + is_percentage = 1; + } + + if (i == 0) { + green = num->val; + } else if (i == 1) { + blue = num->val; + } + + if (num) { + cr_num_destroy (num); + num = NULL; + } + cr_tknzr_try_to_skip_spaces (a_this); + } + + READ_NEXT_BYTE (a_this, &cur_byte); + if (*a_rgb == NULL) { + *a_rgb = cr_rgb_new_with_vals (red, green, blue, + is_percentage); + + if (*a_rgb == NULL) { + status = CR_ERROR; + goto error; + } + status = CR_OK; + } else { + (*a_rgb)->red = red; + (*a_rgb)->green = green; + (*a_rgb)->blue = blue; + (*a_rgb)->is_percentage = is_percentage; + + status = CR_OK; + } + + if (status == CR_OK) { + if (a_rgb && *a_rgb) { + cr_parsing_location_copy + (&(*a_rgb)->location, + &location) ; + } + return CR_OK; + } + + error: + if (num) { + cr_num_destroy (num); + num = NULL; + } + + cr_tknzr_set_cur_pos (a_this, &init_pos); + return CR_OK; +} + +/** + *Parses a atkeyword as defined by the css spec in [4.1.1]: + *ATKEYWORD ::= @{ident} + * + *@param a_this the "this pointer" of the current instance of + *#CRTknzr. + * + *@param a_str out parameter. The parsed atkeyword. If *a_str is + *set to NULL this function allocates a new instance of CRString and + *sets it to the parsed atkeyword. If not, this function just appends + *the parsed atkeyword to the end of *a_str. In both cases it is up to + *the caller to free *a_str. + * + *@return CR_OK upon successful completion, an error code otherwise. + */ +static enum CRStatus +cr_tknzr_parse_atkeyword (CRTknzr * a_this, + CRString ** a_str) +{ + guint32 cur_char = 0; + CRInputPos init_pos; + gboolean str_needs_free = FALSE; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_str, CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char != '@') { + status = CR_PARSING_ERROR; + goto error; + } + + if (*a_str == NULL) { + *a_str = cr_string_new (); + str_needs_free = TRUE; + } + status = cr_tknzr_parse_ident (a_this, a_str); + if (status != CR_OK) { + goto error; + } + return CR_OK; + error: + + if (str_needs_free == TRUE && *a_str) { + cr_string_destroy (*a_str); + *a_str = NULL; + } + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; +} + +static enum CRStatus +cr_tknzr_parse_important (CRTknzr * a_this, + CRParsingLocation *a_location) +{ + guint32 cur_char = 0; + CRInputPos init_pos; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + READ_NEXT_CHAR (a_this, &cur_char); + ENSURE_PARSING_COND (cur_char == '!'); + if (a_location) { + cr_tknzr_get_parsing_location (a_this, + a_location) ; + } + cr_tknzr_try_to_skip_spaces (a_this); + + if (BYTE (PRIVATE (a_this)->input, 1, NULL) == 'i' + && BYTE (PRIVATE (a_this)->input, 2, NULL) == 'm' + && BYTE (PRIVATE (a_this)->input, 3, NULL) == 'p' + && BYTE (PRIVATE (a_this)->input, 4, NULL) == 'o' + && BYTE (PRIVATE (a_this)->input, 5, NULL) == 'r' + && BYTE (PRIVATE (a_this)->input, 6, NULL) == 't' + && BYTE (PRIVATE (a_this)->input, 7, NULL) == 'a' + && BYTE (PRIVATE (a_this)->input, 8, NULL) == 'n' + && BYTE (PRIVATE (a_this)->input, 9, NULL) == 't') { + SKIP_BYTES (a_this, 9); + if (a_location) { + cr_tknzr_get_parsing_location (a_this, + a_location) ; + } + return CR_OK; + } else { + status = CR_PARSING_ERROR; + } + + error: + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; +} + +/** + *Parses a num as defined in the css spec [4.1.1]: + *[0-9]+|[0-9]*\.[0-9]+ + *@param a_this the current instance of #CRTknzr. + *@param a_num out parameter. The parsed number. + *@return CR_OK upon successful completion, + *an error code otherwise. + * + *The CSS specification says that numbers may be + *preceded by '+' or '-' to indicate the sign. + *Technically, the "num" construction as defined + *by the tokenizer doesn't allow this, but we parse + *it here for simplicity. + */ +static enum CRStatus +cr_tknzr_parse_num (CRTknzr * a_this, + CRNum ** a_num) +{ + enum CRStatus status = CR_PARSING_ERROR; + enum CRNumType val_type = NUM_GENERIC; + gboolean parsing_dec, /* true iff seen decimal point. */ + parsed; /* true iff the substring seen so far is a valid CSS + number, i.e. `[0-9]+|[0-9]*\.[0-9]+'. */ + guint32 cur_char = 0, + next_char = 0; + gdouble numerator, denominator = 1; + CRInputPos init_pos; + CRParsingLocation location = {0} ; + int sign = 1; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, + CR_BAD_PARAM_ERROR); + + RECORD_INITIAL_POS (a_this, &init_pos); + READ_NEXT_CHAR (a_this, &cur_char); + + if (cur_char == '+' || cur_char == '-') { + if (cur_char == '-') { + sign = -1; + } + READ_NEXT_CHAR (a_this, &cur_char); + } + + if (IS_NUM (cur_char)) { + numerator = (cur_char - '0'); + parsing_dec = FALSE; + parsed = TRUE; + } else if (cur_char == '.') { + numerator = 0; + parsing_dec = TRUE; + parsed = FALSE; + } else { + status = CR_PARSING_ERROR; + goto error; + } + cr_tknzr_get_parsing_location (a_this, &location) ; + + for (;;) { + status = cr_tknzr_peek_char (a_this, &next_char); + if (status != CR_OK) { + if (status == CR_END_OF_INPUT_ERROR) + status = CR_OK; + break; + } + if (next_char == '.') { + if (parsing_dec) { + status = CR_PARSING_ERROR; + goto error; + } + + READ_NEXT_CHAR (a_this, &cur_char); + parsing_dec = TRUE; + parsed = FALSE; /* In CSS, there must be at least + one digit after `.'. */ + } else if (IS_NUM (next_char)) { + READ_NEXT_CHAR (a_this, &cur_char); + parsed = TRUE; + + numerator = numerator * 10 + (cur_char - '0'); + if (parsing_dec) { + denominator *= 10; + } + } else { + break; + } + } + + if (!parsed) { + status = CR_PARSING_ERROR; + } + + /* + *Now, set the output param values. + */ + if (status == CR_OK) { + gdouble val = (numerator / denominator) * sign; + if (*a_num == NULL) { + *a_num = cr_num_new_with_val (val, val_type); + + if (*a_num == NULL) { + status = CR_ERROR; + goto error; + } + } else { + (*a_num)->val = val; + (*a_num)->type = val_type; + } + cr_parsing_location_copy (&(*a_num)->location, + &location) ; + return CR_OK; + } + + error: + + cr_tknzr_set_cur_pos (a_this, &init_pos); + + return status; +} + +/********************************************* + *PUBLIC methods + ********************************************/ + +CRTknzr * +cr_tknzr_new (CRInput * a_input) +{ + CRTknzr *result = NULL; + + result = g_try_malloc (sizeof (CRTknzr)); + + if (result == NULL) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRTknzr)); + + result->priv = g_try_malloc (sizeof (CRTknzrPriv)); + + if (result->priv == NULL) { + cr_utils_trace_info ("Out of memory"); + + if (result) { + g_free (result); + result = NULL; + } + + return NULL; + } + memset (result->priv, 0, sizeof (CRTknzrPriv)); + if (a_input) + cr_tknzr_set_input (result, a_input); + return result; +} + +CRTknzr * +cr_tknzr_new_from_buf (guchar * a_buf, gulong a_len, + enum CREncoding a_enc, + gboolean a_free_at_destroy) +{ + CRTknzr *result = NULL; + CRInput *input = NULL; + + input = cr_input_new_from_buf (a_buf, a_len, a_enc, + a_free_at_destroy); + + g_return_val_if_fail (input != NULL, NULL); + + result = cr_tknzr_new (input); + + return result; +} + +CRTknzr * +cr_tknzr_new_from_uri (const guchar * a_file_uri, + enum CREncoding a_enc) +{ + CRTknzr *result = NULL; + CRInput *input = NULL; + + input = cr_input_new_from_uri ((const gchar *) a_file_uri, a_enc); + g_return_val_if_fail (input != NULL, NULL); + + result = cr_tknzr_new (input); + + return result; +} + +void +cr_tknzr_ref (CRTknzr * a_this) +{ + g_return_if_fail (a_this && PRIVATE (a_this)); + + PRIVATE (a_this)->ref_count++; +} + +gboolean +cr_tknzr_unref (CRTknzr * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), FALSE); + + if (PRIVATE (a_this)->ref_count > 0) { + PRIVATE (a_this)->ref_count--; + } + + if (PRIVATE (a_this)->ref_count == 0) { + cr_tknzr_destroy (a_this); + return TRUE; + } + + return FALSE; +} + +enum CRStatus +cr_tknzr_set_input (CRTknzr * a_this, CRInput * a_input) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->input) { + cr_input_unref (PRIVATE (a_this)->input); + } + + PRIVATE (a_this)->input = a_input; + + cr_input_ref (PRIVATE (a_this)->input); + + return CR_OK; +} + +enum CRStatus +cr_tknzr_get_input (CRTknzr * a_this, CRInput ** a_input) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + *a_input = PRIVATE (a_this)->input; + + return CR_OK; +} + +/********************************* + *Tokenizer input handling routines + *********************************/ + +/** + *Reads the next byte from the parser input stream. + *@param a_this the "this pointer" of the current instance of + *#CRParser. + *@param a_byte out parameter the place where to store the byte + *read. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_tknzr_read_byte (CRTknzr * a_this, guchar * a_byte) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this), CR_BAD_PARAM_ERROR); + + return cr_input_read_byte (PRIVATE (a_this)->input, a_byte); + +} + +/** + *Reads the next char from the parser input stream. + *@param a_this the current instance of #CRTknzr. + *@param a_char out parameter. The read char. + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_tknzr_read_char (CRTknzr * a_this, guint32 * a_char) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_char, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_read_char (PRIVATE (a_this)->input, a_char); +} + +/** + *Peeks a char from the parser input stream. + *To "peek a char" means reads the next char without consuming it. + *Subsequent calls to this function return the same char. + *@param a_this the current instance of #CRTknzr. + *@param a_char out parameter. The peeked char upon successful completion. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_tknzr_peek_char (CRTknzr * a_this, guint32 * a_char) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_char, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_peek_char (PRIVATE (a_this)->input, a_char); +} + +/** + *Peeks a byte ahead at a given position in the parser input stream. + *@param a_this the current instance of #CRTknzr. + *@param a_offset the offset of the peeked byte starting from the current + *byte in the parser input stream. + *@param a_byte out parameter. The peeked byte upon + *successful completion. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_tknzr_peek_byte (CRTknzr * a_this, gulong a_offset, guchar * a_byte) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input && a_byte, + CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_peek_byte (PRIVATE (a_this)->input, + CR_SEEK_CUR, a_offset, a_byte); +} + +/** + *Same as cr_tknzr_peek_byte() but this api returns the byte peeked. + *@param a_this the current instance of #CRTknzr. + *@param a_offset the offset of the peeked byte starting from the current + *byte in the parser input stream. + *@param a_eof out parameter. If not NULL, is set to TRUE if we reached end of + *file, FALE otherwise. If the caller sets it to NULL, this parameter + *is just ignored. + *@return the peeked byte. + */ +guchar +cr_tknzr_peek_byte2 (CRTknzr * a_this, gulong a_offset, gboolean * a_eof) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, 0); + + return cr_input_peek_byte2 (PRIVATE (a_this)->input, a_offset, a_eof); +} + +/** + *Gets the number of bytes left in the topmost input stream + *associated to this parser. + *@param a_this the current instance of #CRTknzr + *@return the number of bytes left or -1 in case of error. + */ +glong +cr_tknzr_get_nb_bytes_left (CRTknzr * a_this) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_get_nb_bytes_left (PRIVATE (a_this)->input); +} + +enum CRStatus +cr_tknzr_get_cur_pos (CRTknzr * a_this, CRInputPos * a_pos) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_pos, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_get_cur_pos (PRIVATE (a_this)->input, a_pos); +} + +enum CRStatus +cr_tknzr_get_parsing_location (CRTknzr *a_this, + CRParsingLocation *a_loc) +{ + g_return_val_if_fail (a_this + && PRIVATE (a_this) + && a_loc, + CR_BAD_PARAM_ERROR) ; + + return cr_input_get_parsing_location + (PRIVATE (a_this)->input, a_loc) ; +} + +enum CRStatus +cr_tknzr_get_cur_byte_addr (CRTknzr * a_this, guchar ** a_addr) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_get_cur_byte_addr (PRIVATE (a_this)->input, a_addr); +} + +enum CRStatus +cr_tknzr_seek_index (CRTknzr * a_this, enum CRSeekPos a_origin, gint a_pos) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_seek_index (PRIVATE (a_this)->input, a_origin, a_pos); +} + +enum CRStatus +cr_tknzr_consume_chars (CRTknzr * a_this, guint32 a_char, glong * a_nb_char) +{ + gulong consumed = *(gulong *) a_nb_char; + enum CRStatus status; + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_input_set_cur_pos (PRIVATE (a_this)->input, + &PRIVATE (a_this)->prev_pos); + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + status = cr_input_consume_chars (PRIVATE (a_this)->input, + a_char, &consumed); + *a_nb_char = (glong) consumed; + return status; +} + +enum CRStatus +cr_tknzr_set_cur_pos (CRTknzr * a_this, CRInputPos * a_pos) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input, CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + return cr_input_set_cur_pos (PRIVATE (a_this)->input, a_pos); +} + +enum CRStatus +cr_tknzr_unget_token (CRTknzr * a_this, CRToken * a_token) +{ + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->token_cache == NULL, + CR_BAD_PARAM_ERROR); + + PRIVATE (a_this)->token_cache = a_token; + + return CR_OK; +} + +/** + *Returns the next token of the input stream. + *This method is really central. Each parsing + *method calls it. + *@param a_this the current tokenizer. + *@param a_tk out parameter. The returned token. + *for the sake of mem leak avoidance, *a_tk must + *be NULL. + *@param CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_tknzr_get_next_token (CRTknzr * a_this, CRToken ** a_tk) +{ + enum CRStatus status = CR_OK; + CRToken *token = NULL; + CRInputPos init_pos; + guint32 next_char = 0; + guchar next_bytes[4] = { 0 }; + gboolean reached_eof = FALSE; + CRInput *input = NULL; + CRString *str = NULL; + CRRgb *rgb = NULL; + CRParsingLocation location = {0} ; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && a_tk && *a_tk == NULL + && PRIVATE (a_this)->input, + CR_BAD_PARAM_ERROR); + + if (PRIVATE (a_this)->token_cache) { + *a_tk = PRIVATE (a_this)->token_cache; + PRIVATE (a_this)->token_cache = NULL; + return CR_OK; + } + + RECORD_INITIAL_POS (a_this, &init_pos); + + status = cr_input_get_end_of_file + (PRIVATE (a_this)->input, &reached_eof); + ENSURE_PARSING_COND (status == CR_OK); + + if (reached_eof == TRUE) { + status = CR_END_OF_INPUT_ERROR; + goto error; + } + + input = PRIVATE (a_this)->input; + + PEEK_NEXT_CHAR (a_this, &next_char); + token = cr_token_new (); + ENSURE_PARSING_COND (token); + + switch (next_char) { + case '@': + { + if (BYTE (input, 2, NULL) == 'f' + && BYTE (input, 3, NULL) == 'o' + && BYTE (input, 4, NULL) == 'n' + && BYTE (input, 5, NULL) == 't' + && BYTE (input, 6, NULL) == '-' + && BYTE (input, 7, NULL) == 'f' + && BYTE (input, 8, NULL) == 'a' + && BYTE (input, 9, NULL) == 'c' + && BYTE (input, 10, NULL) == 'e') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location + (a_this, &location) ; + SKIP_CHARS (a_this, 9); + status = cr_token_set_font_face_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + + if (BYTE (input, 2, NULL) == 'c' + && BYTE (input, 3, NULL) == 'h' + && BYTE (input, 4, NULL) == 'a' + && BYTE (input, 5, NULL) == 'r' + && BYTE (input, 6, NULL) == 's' + && BYTE (input, 7, NULL) == 'e' + && BYTE (input, 8, NULL) == 't') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location + (a_this, &location) ; + SKIP_CHARS (a_this, 7); + status = cr_token_set_charset_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + + if (BYTE (input, 2, NULL) == 'i' + && BYTE (input, 3, NULL) == 'm' + && BYTE (input, 4, NULL) == 'p' + && BYTE (input, 5, NULL) == 'o' + && BYTE (input, 6, NULL) == 'r' + && BYTE (input, 7, NULL) == 't') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location + (a_this, &location) ; + SKIP_CHARS (a_this, 6); + status = cr_token_set_import_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + + if (BYTE (input, 2, NULL) == 'm' + && BYTE (input, 3, NULL) == 'e' + && BYTE (input, 4, NULL) == 'd' + && BYTE (input, 5, NULL) == 'i' + && BYTE (input, 6, NULL) == 'a') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 5); + status = cr_token_set_media_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + + if (BYTE (input, 2, NULL) == 'p' + && BYTE (input, 3, NULL) == 'a' + && BYTE (input, 4, NULL) == 'g' + && BYTE (input, 5, NULL) == 'e') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 4); + status = cr_token_set_page_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + status = cr_tknzr_parse_atkeyword (a_this, &str); + if (status == CR_OK) { + status = cr_token_set_atkeyword (token, str); + CHECK_PARSING_STATUS (status, TRUE); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + goto done; + } + } + break; + + case 'u': + + if (BYTE (input, 2, NULL) == 'r' + && BYTE (input, 3, NULL) == 'l' + && BYTE (input, 4, NULL) == '(') { + CRString *str2 = NULL; + + status = cr_tknzr_parse_uri (a_this, &str2); + if (status == CR_OK) { + status = cr_token_set_uri (token, str2); + CHECK_PARSING_STATUS (status, TRUE); + if (str2) { + cr_parsing_location_copy (&token->location, + &str2->location) ; + } + goto done; + } + } + goto fallback; + break; + + case 'r': + if (BYTE (input, 2, NULL) == 'g' + && BYTE (input, 3, NULL) == 'b' + && BYTE (input, 4, NULL) == '(') { + status = cr_tknzr_parse_rgb (a_this, &rgb); + if (status == CR_OK && rgb) { + status = cr_token_set_rgb (token, rgb); + CHECK_PARSING_STATUS (status, TRUE); + if (rgb) { + cr_parsing_location_copy (&token->location, + &rgb->location) ; + } + rgb = NULL; + goto done; + } + + } + goto fallback; + break; + + case '<': + if (BYTE (input, 2, NULL) == '!' + && BYTE (input, 3, NULL) == '-' + && BYTE (input, 4, NULL) == '-') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 3); + status = cr_token_set_cdo (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + break; + + case '-': + if (BYTE (input, 2, NULL) == '-' + && BYTE (input, 3, NULL) == '>') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 2); + status = cr_token_set_cdc (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } else { + status = cr_tknzr_parse_ident + (a_this, &str); + if (status == CR_OK) { + cr_token_set_ident + (token, str); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + goto done; + } else { + goto parse_number; + } + } + break; + + case '~': + if (BYTE (input, 2, NULL) == '=') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 1); + status = cr_token_set_includes (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + break; + + case '|': + if (BYTE (input, 2, NULL) == '=') { + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + SKIP_CHARS (a_this, 1); + status = cr_token_set_dashmatch (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + break; + + case '/': + if (BYTE (input, 2, NULL) == '*') { + status = cr_tknzr_parse_comment (a_this, &str); + + if (status == CR_OK) { + status = cr_token_set_comment (token, str); + str = NULL; + CHECK_PARSING_STATUS (status, TRUE); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + goto done; + } + } + break ; + + case ';': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_semicolon (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case '{': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_cbo (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_tknzr_get_parsing_location (a_this, + &location) ; + goto done; + + case '}': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_cbc (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case '(': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_po (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case ')': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_pc (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case '[': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_bo (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case ']': + SKIP_CHARS (a_this, 1); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_bc (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + + case ' ': + case '\t': + case '\n': + case '\f': + case '\r': + { + guchar *start = NULL, + *end = NULL; + + status = cr_tknzr_parse_w (a_this, &start, + &end, &location); + if (status == CR_OK) { + status = cr_token_set_s (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_tknzr_get_parsing_location (a_this, + &location) ; + goto done; + } + } + break; + + case '#': + { + status = cr_tknzr_parse_hash (a_this, &str); + if (status == CR_OK && str) { + status = cr_token_set_hash (token, str); + CHECK_PARSING_STATUS (status, TRUE); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + str = NULL; + goto done; + } + } + break; + + case '\'': + case '"': + status = cr_tknzr_parse_string (a_this, &str); + if (status == CR_OK && str) { + status = cr_token_set_string (token, str); + CHECK_PARSING_STATUS (status, TRUE); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + str = NULL; + goto done; + } + break; + + case '!': + status = cr_tknzr_parse_important (a_this, &location); + if (status == CR_OK) { + status = cr_token_set_important_sym (token); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + goto done; + } + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '.': + case '+': + /* '-' case is handled separately above for --> comments */ + parse_number: + { + CRNum *num = NULL; + + status = cr_tknzr_parse_num (a_this, &num); + if (status == CR_OK && num) { + next_bytes[0] = BYTE (input, 1, NULL); + next_bytes[1] = BYTE (input, 2, NULL); + next_bytes[2] = BYTE (input, 3, NULL); + next_bytes[3] = BYTE (input, 4, NULL); + + if (next_bytes[0] == 'e' + && next_bytes[1] == 'm') { + num->type = NUM_LENGTH_EM; + status = cr_token_set_ems (token, + num); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'e' + && next_bytes[1] == 'x') { + num->type = NUM_LENGTH_EX; + status = cr_token_set_exs (token, + num); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'p' + && next_bytes[1] == 'x') { + num->type = NUM_LENGTH_PX; + status = cr_token_set_length + (token, num, LENGTH_PX_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'c' + && next_bytes[1] == 'm') { + num->type = NUM_LENGTH_CM; + status = cr_token_set_length + (token, num, LENGTH_CM_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'm' + && next_bytes[1] == 'm') { + num->type = NUM_LENGTH_MM; + status = cr_token_set_length + (token, num, LENGTH_MM_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'i' + && next_bytes[1] == 'n') { + num->type = NUM_LENGTH_IN; + status = cr_token_set_length + (token, num, LENGTH_IN_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'p' + && next_bytes[1] == 't') { + num->type = NUM_LENGTH_PT; + status = cr_token_set_length + (token, num, LENGTH_PT_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'p' + && next_bytes[1] == 'c') { + num->type = NUM_LENGTH_PC; + status = cr_token_set_length + (token, num, LENGTH_PC_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'd' + && next_bytes[1] == 'e' + && next_bytes[2] == 'g') { + num->type = NUM_ANGLE_DEG; + status = cr_token_set_angle + (token, num, ANGLE_DEG_ET); + num = NULL; + SKIP_CHARS (a_this, 3); + } else if (next_bytes[0] == 'r' + && next_bytes[1] == 'a' + && next_bytes[2] == 'd') { + num->type = NUM_ANGLE_RAD; + status = cr_token_set_angle + (token, num, ANGLE_RAD_ET); + num = NULL; + SKIP_CHARS (a_this, 3); + } else if (next_bytes[0] == 'g' + && next_bytes[1] == 'r' + && next_bytes[2] == 'a' + && next_bytes[3] == 'd') { + num->type = NUM_ANGLE_GRAD; + status = cr_token_set_angle + (token, num, ANGLE_GRAD_ET); + num = NULL; + SKIP_CHARS (a_this, 4); + } else if (next_bytes[0] == 'm' + && next_bytes[1] == 's') { + num->type = NUM_TIME_MS; + status = cr_token_set_time + (token, num, TIME_MS_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 's') { + num->type = NUM_TIME_S; + status = cr_token_set_time + (token, num, TIME_S_ET); + num = NULL; + SKIP_CHARS (a_this, 1); + } else if (next_bytes[0] == 'H' + && next_bytes[1] == 'z') { + num->type = NUM_FREQ_HZ; + status = cr_token_set_freq + (token, num, FREQ_HZ_ET); + num = NULL; + SKIP_CHARS (a_this, 2); + } else if (next_bytes[0] == 'k' + && next_bytes[1] == 'H' + && next_bytes[2] == 'z') { + num->type = NUM_FREQ_KHZ; + status = cr_token_set_freq + (token, num, FREQ_KHZ_ET); + num = NULL; + SKIP_CHARS (a_this, 3); + } else if (next_bytes[0] == '%') { + num->type = NUM_PERCENTAGE; + status = cr_token_set_percentage + (token, num); + num = NULL; + SKIP_CHARS (a_this, 1); + } else { + status = cr_tknzr_parse_ident (a_this, + &str); + if (status == CR_OK && str) { + num->type = NUM_UNKNOWN_TYPE; + status = cr_token_set_dimen + (token, num, str); + num = NULL; + CHECK_PARSING_STATUS (status, + TRUE); + str = NULL; + } else { + status = cr_token_set_number + (token, num); + num = NULL; + CHECK_PARSING_STATUS (status, CR_OK); + str = NULL; + } + } + if (token && token->u.num) { + cr_parsing_location_copy (&token->location, + &token->u.num->location) ; + } else { + status = CR_ERROR ; + } + goto done ; + } + } + break; + + default: + fallback: + /*process the fallback cases here */ + + if (next_char == '\\' + || (cr_utils_is_nonascii (next_bytes[0]) == TRUE) + || ((next_char >= 'a') && (next_char <= 'z')) + || ((next_char >= 'A') && (next_char <= 'Z'))) { + status = cr_tknzr_parse_ident (a_this, &str); + if (status == CR_OK && str) { + guint32 next_c = 0; + + status = cr_input_peek_char + (PRIVATE (a_this)->input, &next_c); + + if (status == CR_OK && next_c == '(') { + + SKIP_CHARS (a_this, 1); + status = cr_token_set_function + (token, str); + CHECK_PARSING_STATUS (status, TRUE); + /*ownership is transferred + *to token by cr_token_set_function. + */ + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + str = NULL; + } else { + status = cr_token_set_ident (token, + str); + CHECK_PARSING_STATUS (status, TRUE); + if (str) { + cr_parsing_location_copy (&token->location, + &str->location) ; + } + str = NULL; + } + goto done; + } else { + if (str) { + cr_string_destroy (str); + str = NULL; + } + } + } + break; + } + + READ_NEXT_CHAR (a_this, &next_char); + cr_tknzr_get_parsing_location (a_this, + &location) ; + status = cr_token_set_delim (token, next_char); + CHECK_PARSING_STATUS (status, TRUE); + cr_parsing_location_copy (&token->location, + &location) ; + done: + + if (status == CR_OK && token) { + *a_tk = token; + /* + *store the previous position input stream pos. + */ + memmove (&PRIVATE (a_this)->prev_pos, + &init_pos, sizeof (CRInputPos)); + return CR_OK; + } + + error: + if (token) { + cr_token_destroy (token); + token = NULL; + } + + if (str) { + cr_string_destroy (str); + str = NULL; + } + cr_tknzr_set_cur_pos (a_this, &init_pos); + return status; + +} + +enum CRStatus +cr_tknzr_parse_token (CRTknzr * a_this, enum CRTokenType a_type, + enum CRTokenExtraType a_et, gpointer a_res, + gpointer a_extra_res) +{ + enum CRStatus status = CR_OK; + CRToken *token = NULL; + + g_return_val_if_fail (a_this && PRIVATE (a_this) + && PRIVATE (a_this)->input + && a_res, CR_BAD_PARAM_ERROR); + + status = cr_tknzr_get_next_token (a_this, &token); + if (status != CR_OK) + return status; + if (token == NULL) + return CR_PARSING_ERROR; + + if (token->type == a_type) { + switch (a_type) { + case NO_TK: + case S_TK: + case CDO_TK: + case CDC_TK: + case INCLUDES_TK: + case DASHMATCH_TK: + case IMPORT_SYM_TK: + case PAGE_SYM_TK: + case MEDIA_SYM_TK: + case FONT_FACE_SYM_TK: + case CHARSET_SYM_TK: + case IMPORTANT_SYM_TK: + status = CR_OK; + break; + + case STRING_TK: + case IDENT_TK: + case HASH_TK: + case ATKEYWORD_TK: + case FUNCTION_TK: + case COMMENT_TK: + case URI_TK: + *((CRString **) a_res) = token->u.str; + token->u.str = NULL; + status = CR_OK; + break; + + case EMS_TK: + case EXS_TK: + case PERCENTAGE_TK: + case NUMBER_TK: + *((CRNum **) a_res) = token->u.num; + token->u.num = NULL; + status = CR_OK; + break; + + case LENGTH_TK: + case ANGLE_TK: + case TIME_TK: + case FREQ_TK: + if (token->extra_type == a_et) { + *((CRNum **) a_res) = token->u.num; + token->u.num = NULL; + status = CR_OK; + } + break; + + case DIMEN_TK: + *((CRNum **) a_res) = token->u.num; + if (a_extra_res == NULL) { + status = CR_BAD_PARAM_ERROR; + goto error; + } + + *((CRString **) a_extra_res) = token->dimen; + token->u.num = NULL; + token->dimen = NULL; + status = CR_OK; + break; + + case DELIM_TK: + *((guint32 *) a_res) = token->u.unichar; + status = CR_OK; + break; + + case UNICODERANGE_TK: + default: + status = CR_PARSING_ERROR; + break; + } + + cr_token_destroy (token); + token = NULL; + } else { + cr_tknzr_unget_token (a_this, token); + token = NULL; + status = CR_PARSING_ERROR; + } + + return status; + + error: + + if (token) { + cr_tknzr_unget_token (a_this, token); + token = NULL; + } + + return status; +} + +void +cr_tknzr_destroy (CRTknzr * a_this) +{ + g_return_if_fail (a_this); + + if (PRIVATE (a_this) && PRIVATE (a_this)->input) { + if (cr_input_unref (PRIVATE (a_this)->input) + == TRUE) { + PRIVATE (a_this)->input = NULL; + } + } + + if (PRIVATE (a_this)->token_cache) { + cr_token_destroy (PRIVATE (a_this)->token_cache); + PRIVATE (a_this)->token_cache = NULL; + } + + if (PRIVATE (a_this)) { + g_free (PRIVATE (a_this)); + PRIVATE (a_this) = NULL; + } + + g_free (a_this); +} diff --git a/src/st/croco/cr-tknzr.h b/src/st/croco/cr-tknzr.h new file mode 100644 index 0000000..13985b3 --- /dev/null +++ b/src/st/croco/cr-tknzr.h @@ -0,0 +1,115 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for coypyright information. + */ + +/** + *@file + *The declaration of the #CRTknzr (tokenizer) + *class. + */ + +#ifndef __CR_TKNZR_H__ +#define __CR_TKNZR_H__ + +#include "cr-utils.h" +#include "cr-input.h" +#include "cr-token.h" + +G_BEGIN_DECLS + + +typedef struct _CRTknzr CRTknzr ; +typedef struct _CRTknzrPriv CRTknzrPriv ; + +/** + *The tokenizer is the class that knows + *about all the css token. Its main job is + *to return the next token found in the character + *input stream. + */ +struct _CRTknzr +{ + /*the private data of the tokenizer.*/ + CRTknzrPriv *priv ; +} ; + +CRTknzr * cr_tknzr_new (CRInput *a_input) ; + +CRTknzr * cr_tknzr_new_from_uri (const guchar *a_file_uri, + enum CREncoding a_enc) ; + +CRTknzr * cr_tknzr_new_from_buf (guchar *a_buf, gulong a_len, + enum CREncoding a_enc, + gboolean a_free_at_destroy) ; + +gboolean cr_tknzr_unref (CRTknzr *a_this) ; + +void cr_tknzr_ref (CRTknzr *a_this) ; + +enum CRStatus cr_tknzr_read_byte (CRTknzr *a_this, guchar *a_byte) ; + +enum CRStatus cr_tknzr_read_char (CRTknzr *a_this, guint32 *a_char); + +enum CRStatus cr_tknzr_peek_char (CRTknzr *a_this, guint32 *a_char) ; + +enum CRStatus cr_tknzr_peek_byte (CRTknzr *a_this, gulong a_offset, + guchar *a_byte) ; + +guchar cr_tknzr_peek_byte2 (CRTknzr *a_this, gulong a_offset, + gboolean *a_eof) ; + +enum CRStatus cr_tknzr_set_cur_pos (CRTknzr *a_this, CRInputPos *a_pos) ; + +glong cr_tknzr_get_nb_bytes_left (CRTknzr *a_this) ; + +enum CRStatus cr_tknzr_get_cur_pos (CRTknzr *a_this, CRInputPos *a_pos) ; + +enum CRStatus cr_tknzr_get_parsing_location (CRTknzr *a_this, + CRParsingLocation *a_loc) ; + +enum CRStatus cr_tknzr_seek_index (CRTknzr *a_this, + enum CRSeekPos a_origin, + gint a_pos) ; + +enum CRStatus cr_tknzr_get_cur_byte_addr (CRTknzr *a_this, guchar **a_addr) ; + + +enum CRStatus cr_tknzr_consume_chars (CRTknzr *a_this, guint32 a_char, + glong *a_nb_char) ; + +enum CRStatus cr_tknzr_get_next_token (CRTknzr *a_this, CRToken ** a_tk) ; + +enum CRStatus cr_tknzr_unget_token (CRTknzr *a_this, CRToken *a_token) ; + + +enum CRStatus cr_tknzr_parse_token (CRTknzr *a_this, enum CRTokenType a_type, + enum CRTokenExtraType a_et, gpointer a_res, + gpointer a_extra_res) ; +enum CRStatus cr_tknzr_set_input (CRTknzr *a_this, CRInput *a_input) ; + +enum CRStatus cr_tknzr_get_input (CRTknzr *a_this, CRInput **a_input) ; + +void cr_tknzr_destroy (CRTknzr *a_this) ; + +G_END_DECLS + +#endif /*__CR_TKZNR_H__*/ diff --git a/src/st/croco/cr-token.c b/src/st/croco/cr-token.c new file mode 100644 index 0000000..91dd632 --- /dev/null +++ b/src/st/croco/cr-token.c @@ -0,0 +1,636 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * see COPYRIGHTS file for copyright information. + */ + +/** + *@file + *The definition of the #CRToken class. + *Abstracts a css2 token. + */ +#include <string.h> +#include "cr-token.h" + +/* + *TODO: write a CRToken::to_string() method. + */ + +/** + *Frees the attributes of the current instance + *of #CRtoken. + *@param a_this the current instance of #CRToken. + */ +static void +cr_token_clear (CRToken * a_this) +{ + g_return_if_fail (a_this); + + switch (a_this->type) { + case S_TK: + case CDO_TK: + case CDC_TK: + case INCLUDES_TK: + case DASHMATCH_TK: + case PAGE_SYM_TK: + case MEDIA_SYM_TK: + case FONT_FACE_SYM_TK: + case CHARSET_SYM_TK: + case IMPORT_SYM_TK: + case IMPORTANT_SYM_TK: + case SEMICOLON_TK: + case NO_TK: + case DELIM_TK: + case CBO_TK: + case CBC_TK: + case BO_TK: + case BC_TK: + break; + + case STRING_TK: + case IDENT_TK: + case HASH_TK: + case URI_TK: + case FUNCTION_TK: + case COMMENT_TK: + case ATKEYWORD_TK: + if (a_this->u.str) { + cr_string_destroy (a_this->u.str); + a_this->u.str = NULL; + } + break; + + case EMS_TK: + case EXS_TK: + case LENGTH_TK: + case ANGLE_TK: + case TIME_TK: + case FREQ_TK: + case PERCENTAGE_TK: + case NUMBER_TK: + case PO_TK: + case PC_TK: + if (a_this->u.num) { + cr_num_destroy (a_this->u.num); + a_this->u.num = NULL; + } + break; + + case DIMEN_TK: + if (a_this->u.num) { + cr_num_destroy (a_this->u.num); + a_this->u.num = NULL; + } + + if (a_this->dimen) { + cr_string_destroy (a_this->dimen); + a_this->dimen = NULL; + } + + break; + + case RGB_TK: + if (a_this->u.rgb) { + cr_rgb_destroy (a_this->u.rgb) ; + a_this->u.rgb = NULL ; + } + break ; + + case UNICODERANGE_TK: + /*not supported yet. */ + break; + + default: + cr_utils_trace_info ("I don't know how to clear this token\n") ; + break; + } + + a_this->type = NO_TK; +} + +/** + *Default constructor of + *the #CRToken class. + *@return the newly built instance of #CRToken. + */ +CRToken * +cr_token_new (void) +{ + CRToken *result = NULL; + + result = g_try_malloc (sizeof (CRToken)); + + if (result == NULL) { + cr_utils_trace_info ("Out of memory"); + return NULL; + } + + memset (result, 0, sizeof (CRToken)); + + return result; +} + +/** + *Sets the type of curren instance of + *#CRToken to 'S_TK' (S in the css2 spec) + *@param a_this the current instance of #CRToken. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_token_set_s (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = S_TK; + + return CR_OK; +} + +/** + *Sets the type of the current instance of + *#CRToken to 'CDO_TK' (CDO as said by the css2 spec) + *@param a_this the current instance of #CRToken. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_token_set_cdo (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = CDO_TK; + + return CR_OK; +} + +/** + *Sets the type of the current token to + *CDC_TK (CDC as said by the css2 spec). + *@param a_this the current instance of #CRToken. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_token_set_cdc (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = CDC_TK; + + return CR_OK; +} + +/** + *Sets the type of the current instance of + *#CRToken to INCLUDES_TK (INCLUDES as said by the css2 spec). + *@param a_this the current instance of #CRToken. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_token_set_includes (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = INCLUDES_TK; + + return CR_OK; +} + +/** + *Sets the type of the current instance of + *#CRToken to DASHMATCH_TK (DASHMATCH as said by the css2 spec). + *@param a_this the current instance of #CRToken. + *@return CR_OK upon successful completion, an error + *code otherwise. + */ +enum CRStatus +cr_token_set_dashmatch (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = DASHMATCH_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_comment (CRToken * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = COMMENT_TK; + a_this->u.str = a_str ; + return CR_OK; +} + +enum CRStatus +cr_token_set_string (CRToken * a_this, CRString * a_str) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = STRING_TK; + + a_this->u.str = a_str ; + + return CR_OK; +} + +enum CRStatus +cr_token_set_ident (CRToken * a_this, CRString * a_ident) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = IDENT_TK; + a_this->u.str = a_ident; + return CR_OK; +} + + +enum CRStatus +cr_token_set_function (CRToken * a_this, CRString * a_fun_name) +{ + g_return_val_if_fail (a_this, + CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = FUNCTION_TK; + a_this->u.str = a_fun_name; + return CR_OK; +} + +enum CRStatus +cr_token_set_hash (CRToken * a_this, CRString * a_hash) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = HASH_TK; + a_this->u.str = a_hash; + + return CR_OK; +} + +enum CRStatus +cr_token_set_rgb (CRToken * a_this, CRRgb * a_rgb) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = RGB_TK; + a_this->u.rgb = a_rgb; + + return CR_OK; +} + +enum CRStatus +cr_token_set_import_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = IMPORT_SYM_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_page_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = PAGE_SYM_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_media_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = MEDIA_SYM_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_font_face_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = FONT_FACE_SYM_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_charset_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = CHARSET_SYM_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_atkeyword (CRToken * a_this, CRString * a_atname) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + a_this->type = ATKEYWORD_TK; + a_this->u.str = a_atname; + return CR_OK; +} + +enum CRStatus +cr_token_set_important_sym (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + cr_token_clear (a_this); + a_this->type = IMPORTANT_SYM_TK; + return CR_OK; +} + +enum CRStatus +cr_token_set_ems (CRToken * a_this, CRNum * a_num) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + cr_token_clear (a_this); + a_this->type = EMS_TK; + a_this->u.num = a_num; + return CR_OK; +} + +enum CRStatus +cr_token_set_exs (CRToken * a_this, CRNum * a_num) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + cr_token_clear (a_this); + a_this->type = EXS_TK; + a_this->u.num = a_num; + return CR_OK; +} + +enum CRStatus +cr_token_set_length (CRToken * a_this, CRNum * a_num, + enum CRTokenExtraType a_et) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = LENGTH_TK; + a_this->extra_type = a_et; + a_this->u.num = a_num; + + return CR_OK; +} + +enum CRStatus +cr_token_set_angle (CRToken * a_this, CRNum * a_num, + enum CRTokenExtraType a_et) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = ANGLE_TK; + a_this->extra_type = a_et; + a_this->u.num = a_num; + + return CR_OK; +} + +enum CRStatus +cr_token_set_time (CRToken * a_this, CRNum * a_num, + enum CRTokenExtraType a_et) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = TIME_TK; + a_this->extra_type = a_et; + a_this->u.num = a_num; + + return CR_OK; +} + +enum CRStatus +cr_token_set_freq (CRToken * a_this, CRNum * a_num, + enum CRTokenExtraType a_et) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = FREQ_TK; + a_this->extra_type = a_et; + a_this->u.num = a_num; + + return CR_OK; +} + +enum CRStatus +cr_token_set_dimen (CRToken * a_this, CRNum * a_num, + CRString * a_dim) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + cr_token_clear (a_this); + a_this->type = DIMEN_TK; + a_this->u.num = a_num; + a_this->dimen = a_dim; + return CR_OK; + +} + +enum CRStatus +cr_token_set_percentage (CRToken * a_this, CRNum * a_num) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = PERCENTAGE_TK; + a_this->u.num = a_num; + + return CR_OK; +} + +enum CRStatus +cr_token_set_number (CRToken * a_this, CRNum * a_num) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = NUMBER_TK; + a_this->u.num = a_num; + return CR_OK; +} + +enum CRStatus +cr_token_set_uri (CRToken * a_this, CRString * a_uri) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = URI_TK; + a_this->u.str = a_uri; + + return CR_OK; +} + +enum CRStatus +cr_token_set_delim (CRToken * a_this, guint32 a_char) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = DELIM_TK; + a_this->u.unichar = a_char; + + return CR_OK; +} + +enum CRStatus +cr_token_set_semicolon (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = SEMICOLON_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_cbo (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = CBO_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_cbc (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = CBC_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_po (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = PO_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_pc (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = PC_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_bo (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = BO_TK; + + return CR_OK; +} + +enum CRStatus +cr_token_set_bc (CRToken * a_this) +{ + g_return_val_if_fail (a_this, CR_BAD_PARAM_ERROR); + + cr_token_clear (a_this); + + a_this->type = BC_TK; + + return CR_OK; +} + +/** + *The destructor of the #CRToken class. + *@param a_this the current instance of #CRToken. + */ +void +cr_token_destroy (CRToken * a_this) +{ + g_return_if_fail (a_this); + + cr_token_clear (a_this); + + g_free (a_this); +} diff --git a/src/st/croco/cr-token.h b/src/st/croco/cr-token.h new file mode 100644 index 0000000..f1257b7 --- /dev/null +++ b/src/st/croco/cr-token.h @@ -0,0 +1,212 @@ +/* -*- Mode: C; indent-tabs-mode:nil; c-basic-offset: 8-*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#ifndef __CR_TOKEN_H__ +#define __CR_TOKEN_H__ + +#include "cr-utils.h" +#include "cr-input.h" +#include "cr-num.h" +#include "cr-rgb.h" +#include "cr-string.h" +#include "cr-parsing-location.h" + +G_BEGIN_DECLS + +enum CRTokenType +{ + NO_TK, + S_TK, + CDO_TK, + CDC_TK, + INCLUDES_TK, + DASHMATCH_TK, + COMMENT_TK, + STRING_TK, + IDENT_TK, + HASH_TK, + IMPORT_SYM_TK, + PAGE_SYM_TK, + MEDIA_SYM_TK, + FONT_FACE_SYM_TK, + CHARSET_SYM_TK, + ATKEYWORD_TK, + IMPORTANT_SYM_TK, + EMS_TK, + EXS_TK, + LENGTH_TK, + ANGLE_TK, + TIME_TK, + FREQ_TK, + DIMEN_TK, + PERCENTAGE_TK, + NUMBER_TK, + RGB_TK, + URI_TK, + FUNCTION_TK, + UNICODERANGE_TK, + SEMICOLON_TK, + CBO_TK, /*opening curly bracket*/ + CBC_TK, /*closing curly bracket*/ + PO_TK, /*opening parenthesis*/ + PC_TK, /*closing parenthesis*/ + BO_TK, /*opening bracket*/ + BC_TK, /*closing bracket*/ + DELIM_TK +} ; + +enum CRTokenExtraType +{ + NO_ET = 0, + LENGTH_PX_ET, + LENGTH_CM_ET, + LENGTH_MM_ET, + LENGTH_IN_ET, + LENGTH_PT_ET, + LENGTH_PC_ET, + ANGLE_DEG_ET, + ANGLE_RAD_ET, + ANGLE_GRAD_ET, + TIME_MS_ET, + TIME_S_ET, + FREQ_HZ_ET, + FREQ_KHZ_ET +} ; + +typedef struct _CRToken CRToken ; + +/** + *This class abstracts a css2 token. + */ +struct _CRToken +{ + enum CRTokenType type ; + enum CRTokenExtraType extra_type ; + CRInputPos pos ; + + union + { + CRString *str ; + CRRgb *rgb ; + CRNum *num ; + guint32 unichar ; + } u ; + + CRString * dimen ; + CRParsingLocation location ; +} ; + +CRToken* cr_token_new (void) ; + +enum CRStatus cr_token_set_s (CRToken *a_this) ; + +enum CRStatus cr_token_set_cdo (CRToken *a_this) ; + +enum CRStatus cr_token_set_cdc (CRToken *a_this) ; + +enum CRStatus cr_token_set_includes (CRToken *a_this) ; + +enum CRStatus cr_token_set_dashmatch (CRToken *a_this) ; + +enum CRStatus cr_token_set_comment (CRToken *a_this, CRString *a_str) ; + +enum CRStatus cr_token_set_string (CRToken *a_this, CRString *a_str) ; + +enum CRStatus cr_token_set_ident (CRToken *a_this, CRString * a_ident) ; + +enum CRStatus cr_token_set_hash (CRToken *a_this, CRString *a_hash) ; + +enum CRStatus cr_token_set_rgb (CRToken *a_this, CRRgb *a_rgb) ; + +enum CRStatus cr_token_set_import_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_page_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_media_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_font_face_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_charset_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_atkeyword (CRToken *a_this, CRString *a_atname) ; + +enum CRStatus cr_token_set_important_sym (CRToken *a_this) ; + +enum CRStatus cr_token_set_ems (CRToken *a_this, CRNum *a_num) ; + +enum CRStatus cr_token_set_exs (CRToken *a_this, CRNum *a_num) ; + +enum CRStatus cr_token_set_length (CRToken *a_this, CRNum *a_num, + enum CRTokenExtraType a_et) ; + +enum CRStatus cr_token_set_angle (CRToken *a_this, CRNum *a_num, + enum CRTokenExtraType a_et) ; + +enum CRStatus cr_token_set_time (CRToken *a_this, CRNum *a_num, + enum CRTokenExtraType a_et) ; + +enum CRStatus cr_token_set_freq (CRToken *a_this, CRNum *a_num, + enum CRTokenExtraType a_et) ; + +enum CRStatus cr_token_set_dimen (CRToken *a_this, CRNum *a_num, + CRString *a_dim) ; + +enum CRStatus cr_token_set_percentage (CRToken *a_this, CRNum *a_num) ; + +enum CRStatus cr_token_set_number (CRToken *a_this, CRNum *a_num) ; + +enum CRStatus cr_token_set_uri (CRToken *a_this, CRString *a_uri) ; + +enum CRStatus cr_token_set_function (CRToken *a_this, + CRString *a_fun_name) ; + +enum CRStatus cr_token_set_bc (CRToken *a_this) ; + +enum CRStatus cr_token_set_bo (CRToken *a_this) ; + +enum CRStatus cr_token_set_po (CRToken *a_this) ; + +enum CRStatus cr_token_set_pc (CRToken *a_this) ; + +enum CRStatus cr_token_set_cbc (CRToken *a_this) ; + +enum CRStatus cr_token_set_cbo (CRToken *a_this) ; + +enum CRStatus cr_token_set_semicolon (CRToken *a_this) ; + +enum CRStatus cr_token_set_delim (CRToken *a_this, guint32 a_char) ; + + +/* + enum CRStatus + cr_token_set_unicoderange (CRToken *a_this, + CRUnicodeRange *a_range) ; +*/ + +void +cr_token_destroy (CRToken *a_this) ; + + +G_END_DECLS + +#endif /*__CR_TOKEN_H__*/ diff --git a/src/st/croco/cr-utils.c b/src/st/croco/cr-utils.c new file mode 100644 index 0000000..5fafade --- /dev/null +++ b/src/st/croco/cr-utils.c @@ -0,0 +1,1330 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * See COPYRIGHTS file for copyright information. + */ + +#include "cr-utils.h" +#include "cr-string.h" + +/** + *@file: + *Some misc utility functions used + *in the libcroco. + *Note that troughout this file I will + *refer to the CSS SPECIFICATIONS DOCUMENTATION + *written by the w3c guys. You can find that document + *at http://www.w3.org/TR/REC-CSS2/ . + */ + +/**************************** + *Encoding transformations and + *encoding helpers + ****************************/ + +/* + *Here is the correspondence between the ucs-4 charactere codes + *and there matching utf-8 encoding pattern as described by RFC 2279: + * + *UCS-4 range (hex.) UTF-8 octet sequence (binary) + *------------------ ----------------------------- + *0000 0000-0000 007F 0xxxxxxx + *0000 0080-0000 07FF 110xxxxx 10xxxxxx + *0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx + *0001 0000-001F FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + *0020 0000-03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + *0400 0000-7FFF FFFF 1111110x 10xxxxxx ... 10xxxxxx + */ + +/** + *Given an utf8 string buffer, calculates + *the length of this string if it was encoded + *in ucs4. + *@param a_in_start a pointer to the beginning of + *the input utf8 string. + *@param a_in_end a pointre to the end of the input + *utf8 string (points to the last byte of the buffer) + *@param a_len out parameter the calculated length. + *@return CR_OK upon successful completion, an error code + *otherwise. + */ +enum CRStatus +cr_utils_utf8_str_len_as_ucs4 (const guchar * a_in_start, + const guchar * a_in_end, gulong * a_len) +{ + guchar *byte_ptr = NULL; + gint len = 0; + + /* + *to store the final decoded + *unicode char + */ + guint c = 0; + + g_return_val_if_fail (a_in_start && a_in_end && a_len, + CR_BAD_PARAM_ERROR); + *a_len = 0; + + for (byte_ptr = (guchar *) a_in_start; + byte_ptr <= a_in_end; byte_ptr++) { + gint nb_bytes_2_decode = 0; + + if (*byte_ptr <= 0x7F) { + /* + *7 bits long char + *encoded over 1 byte: + * 0xxx xxxx + */ + c = *byte_ptr; + nb_bytes_2_decode = 1; + + } else if ((*byte_ptr & 0xE0) == 0xC0) { + /* + *up to 11 bits long char. + *encoded over 2 bytes: + *110x xxxx 10xx xxxx + */ + c = *byte_ptr & 0x1F; + nb_bytes_2_decode = 2; + + } else if ((*byte_ptr & 0xF0) == 0xE0) { + /* + *up to 16 bit long char + *encoded over 3 bytes: + *1110 xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 0x0F; + nb_bytes_2_decode = 3; + + } else if ((*byte_ptr & 0xF8) == 0xF0) { + /* + *up to 21 bits long char + *encoded over 4 bytes: + *1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 0x7; + nb_bytes_2_decode = 4; + + } else if ((*byte_ptr & 0xFC) == 0xF8) { + /* + *up to 26 bits long char + *encoded over 5 bytes. + *1111 10xx 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 3; + nb_bytes_2_decode = 5; + + } else if ((*byte_ptr & 0xFE) == 0xFC) { + /* + *up to 31 bits long char + *encoded over 6 bytes: + *1111 110x 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 1; + nb_bytes_2_decode = 6; + + } else { + /* + *BAD ENCODING + */ + return CR_ENCODING_ERROR; + } + + /* + *Go and decode the remaining byte(s) + *(if any) to get the current character. + */ + for (; nb_bytes_2_decode > 1; nb_bytes_2_decode--) { + /*decode the next byte */ + byte_ptr++; + + /*byte pattern must be: 10xx xxxx */ + if ((*byte_ptr & 0xC0) != 0x80) { + return CR_ENCODING_ERROR; + } + + c = (c << 6) | (*byte_ptr & 0x3F); + } + + len++; + } + + *a_len = len; + + return CR_OK; +} + +/** + *Given an ucs4 string, this function + *returns the size (in bytes) this string + *would have occupied if it was encoded in utf-8. + *@param a_in_start a pointer to the beginning of the input + *buffer. + *@param a_in_end a pointer to the end of the input buffer. + *@param a_len out parameter. The computed length. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_ucs4_str_len_as_utf8 (const guint32 * a_in_start, + const guint32 * a_in_end, gulong * a_len) +{ + gint len = 0; + guint32 *char_ptr = NULL; + + g_return_val_if_fail (a_in_start && a_in_end && a_len, + CR_BAD_PARAM_ERROR); + + for (char_ptr = (guint32 *) a_in_start; + char_ptr <= a_in_end; char_ptr++) { + if (*char_ptr <= 0x7F) { + /*the utf-8 char would take 1 byte */ + len += 1; + } else if (*char_ptr <= 0x7FF) { + /*the utf-8 char would take 2 bytes */ + len += 2; + } else if (*char_ptr <= 0xFFFF) { + len += 3; + } else if (*char_ptr <= 0x1FFFFF) { + len += 4; + } else if (*char_ptr <= 0x3FFFFFF) { + len += 5; + } else if (*char_ptr <= 0x7FFFFFFF) { + len += 6; + } + } + + *a_len = len; + return CR_OK; +} + +/** + *Given an ucsA string, this function + *returns the size (in bytes) this string + *would have occupied if it was encoded in utf-8. + *@param a_in_start a pointer to the beginning of the input + *buffer. + *@param a_in_end a pointer to the end of the input buffer. + *@param a_len out parameter. The computed length. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_ucs1_str_len_as_utf8 (const guchar * a_in_start, + const guchar * a_in_end, gulong * a_len) +{ + gint len = 0; + guchar *char_ptr = NULL; + + g_return_val_if_fail (a_in_start && a_in_end && a_len, + CR_BAD_PARAM_ERROR); + + for (char_ptr = (guchar *) a_in_start; + char_ptr <= a_in_end; char_ptr++) { + if (*char_ptr <= 0x7F) { + /*the utf-8 char would take 1 byte */ + len += 1; + } else { + /*the utf-8 char would take 2 bytes */ + len += 2; + } + } + + *a_len = len; + return CR_OK; +} + +/** + *Converts an utf8 buffer into an ucs4 buffer. + * + *@param a_in the input utf8 buffer to convert. + *@param a_in_len in/out parameter. The size of the + *input buffer to convert. After return, this parameter contains + *the actual number of bytes consumed. + *@param a_out the output converted ucs4 buffer. Must be allocated by + *the caller. + *@param a_out_len in/out parameter. The size of the output buffer. + *If this size is actually smaller than the real needed size, the function + *just converts what it can and returns a success status. After return, + *this param points to the actual number of characters decoded. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_utf8_to_ucs4 (const guchar * a_in, + gulong * a_in_len, guint32 * a_out, gulong * a_out_len) +{ + gulong in_len = 0, + out_len = 0, + in_index = 0, + out_index = 0; + enum CRStatus status = CR_OK; + + /* + *to store the final decoded + *unicode char + */ + guint c = 0; + + g_return_val_if_fail (a_in && a_in_len + && a_out && a_out_len, CR_BAD_PARAM_ERROR); + + if (*a_in_len < 1) { + status = CR_OK; + goto end; + } + + in_len = *a_in_len; + out_len = *a_out_len; + + for (in_index = 0, out_index = 0; + (in_index < in_len) && (out_index < out_len); + in_index++, out_index++) { + gint nb_bytes_2_decode = 0; + + if (a_in[in_index] <= 0x7F) { + /* + *7 bits long char + *encoded over 1 byte: + * 0xxx xxxx + */ + c = a_in[in_index]; + nb_bytes_2_decode = 1; + + } else if ((a_in[in_index] & 0xE0) == 0xC0) { + /* + *up to 11 bits long char. + *encoded over 2 bytes: + *110x xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x1F; + nb_bytes_2_decode = 2; + + } else if ((a_in[in_index] & 0xF0) == 0xE0) { + /* + *up to 16 bit long char + *encoded over 3 bytes: + *1110 xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x0F; + nb_bytes_2_decode = 3; + + } else if ((a_in[in_index] & 0xF8) == 0xF0) { + /* + *up to 21 bits long char + *encoded over 4 bytes: + *1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x7; + nb_bytes_2_decode = 4; + + } else if ((a_in[in_index] & 0xFC) == 0xF8) { + /* + *up to 26 bits long char + *encoded over 5 bytes. + *1111 10xx 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 3; + nb_bytes_2_decode = 5; + + } else if ((a_in[in_index] & 0xFE) == 0xFC) { + /* + *up to 31 bits long char + *encoded over 6 bytes: + *1111 110x 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 1; + nb_bytes_2_decode = 6; + + } else { + /*BAD ENCODING */ + goto end; + } + + /* + *Go and decode the remaining byte(s) + *(if any) to get the current character. + */ + for (; nb_bytes_2_decode > 1; nb_bytes_2_decode--) { + /*decode the next byte */ + in_index++; + + /*byte pattern must be: 10xx xxxx */ + if ((a_in[in_index] & 0xC0) != 0x80) { + goto end; + } + + c = (c << 6) | (a_in[in_index] & 0x3F); + } + + /* + *The decoded ucs4 char is now + *in c. + */ + + /************************ + *Some security tests + ***********************/ + + /*be sure c is a char */ + if (c == 0xFFFF || c == 0xFFFE) + goto end; + + /*be sure c is inferior to the max ucs4 char value */ + if (c > 0x10FFFF) + goto end; + + /* + *c must be less than UTF16 "lower surrogate begin" + *or higher than UTF16 "High surrogate end" + */ + if (c >= 0xD800 && c <= 0xDFFF) + goto end; + + /*Avoid characters that equals zero */ + if (c == 0) + goto end; + + a_out[out_index] = c; + } + + end: + *a_out_len = out_index + 1; + *a_in_len = in_index + 1; + + return status; +} + +/** + *Reads a character from an utf8 buffer. + *Actually decode the next character code (unicode character code) + *and returns it. + *@param a_in the starting address of the utf8 buffer. + *@param a_in_len the length of the utf8 buffer. + *@param a_out output parameter. The resulting read char. + *@param a_consumed the number of the bytes consumed to + *decode the returned character code. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_read_char_from_utf8_buf (const guchar * a_in, + gulong a_in_len, + guint32 * a_out, gulong * a_consumed) +{ + gulong in_index = 0, + nb_bytes_2_decode = 0; + enum CRStatus status = CR_OK; + + /* + *to store the final decoded + *unicode char + */ + guint32 c = 0; + + g_return_val_if_fail (a_in && a_out && a_out + && a_consumed, CR_BAD_PARAM_ERROR); + + if (a_in_len < 1) { + status = CR_OK; + goto end; + } + + if (*a_in <= 0x7F) { + /* + *7 bits long char + *encoded over 1 byte: + * 0xxx xxxx + */ + c = *a_in; + nb_bytes_2_decode = 1; + + } else if ((*a_in & 0xE0) == 0xC0) { + /* + *up to 11 bits long char. + *encoded over 2 bytes: + *110x xxxx 10xx xxxx + */ + c = *a_in & 0x1F; + nb_bytes_2_decode = 2; + + } else if ((*a_in & 0xF0) == 0xE0) { + /* + *up to 16 bit long char + *encoded over 3 bytes: + *1110 xxxx 10xx xxxx 10xx xxxx + */ + c = *a_in & 0x0F; + nb_bytes_2_decode = 3; + + } else if ((*a_in & 0xF8) == 0xF0) { + /* + *up to 21 bits long char + *encoded over 4 bytes: + *1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *a_in & 0x7; + nb_bytes_2_decode = 4; + + } else if ((*a_in & 0xFC) == 0xF8) { + /* + *up to 26 bits long char + *encoded over 5 bytes. + *1111 10xx 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx + */ + c = *a_in & 3; + nb_bytes_2_decode = 5; + + } else if ((*a_in & 0xFE) == 0xFC) { + /* + *up to 31 bits long char + *encoded over 6 bytes: + *1111 110x 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *a_in & 1; + nb_bytes_2_decode = 6; + + } else { + /*BAD ENCODING */ + goto end; + } + + if (nb_bytes_2_decode > a_in_len) { + status = CR_END_OF_INPUT_ERROR; + goto end; + } + + /* + *Go and decode the remaining byte(s) + *(if any) to get the current character. + */ + for (in_index = 1; in_index < nb_bytes_2_decode; in_index++) { + /*byte pattern must be: 10xx xxxx */ + if ((a_in[in_index] & 0xC0) != 0x80) { + goto end; + } + + c = (c << 6) | (a_in[in_index] & 0x3F); + } + + /* + *The decoded ucs4 char is now + *in c. + */ + + /************************ + *Some security tests + ***********************/ + + /*be sure c is a char */ + if (c == 0xFFFF || c == 0xFFFE) + goto end; + + /*be sure c is inferior to the max ucs4 char value */ + if (c > 0x10FFFF) + goto end; + + /* + *c must be less than UTF16 "lower surrogate begin" + *or higher than UTF16 "High surrogate end" + */ + if (c >= 0xD800 && c <= 0xDFFF) + goto end; + + /*Avoid characters that equals zero */ + if (c == 0) + goto end; + + *a_out = c; + + end: + *a_consumed = nb_bytes_2_decode; + + return status; +} + +/** + * + */ +enum CRStatus +cr_utils_utf8_str_len_as_ucs1 (const guchar * a_in_start, + const guchar * a_in_end, gulong * a_len) +{ + /* + *Note: this function can be made shorter + *but it considers all the cases of the utf8 encoding + *to ease further extensions ... + */ + + guchar *byte_ptr = NULL; + gint len = 0; + + /* + *to store the final decoded + *unicode char + */ + guint c = 0; + + g_return_val_if_fail (a_in_start && a_in_end && a_len, + CR_BAD_PARAM_ERROR); + *a_len = 0; + + for (byte_ptr = (guchar *) a_in_start; + byte_ptr <= a_in_end; byte_ptr++) { + gint nb_bytes_2_decode = 0; + + if (*byte_ptr <= 0x7F) { + /* + *7 bits long char + *encoded over 1 byte: + * 0xxx xxxx + */ + c = *byte_ptr; + nb_bytes_2_decode = 1; + + } else if ((*byte_ptr & 0xE0) == 0xC0) { + /* + *up to 11 bits long char. + *encoded over 2 bytes: + *110x xxxx 10xx xxxx + */ + c = *byte_ptr & 0x1F; + nb_bytes_2_decode = 2; + + } else if ((*byte_ptr & 0xF0) == 0xE0) { + /* + *up to 16 bit long char + *encoded over 3 bytes: + *1110 xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 0x0F; + nb_bytes_2_decode = 3; + + } else if ((*byte_ptr & 0xF8) == 0xF0) { + /* + *up to 21 bits long char + *encoded over 4 bytes: + *1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 0x7; + nb_bytes_2_decode = 4; + + } else if ((*byte_ptr & 0xFC) == 0xF8) { + /* + *up to 26 bits long char + *encoded over 5 bytes. + *1111 10xx 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 3; + nb_bytes_2_decode = 5; + + } else if ((*byte_ptr & 0xFE) == 0xFC) { + /* + *up to 31 bits long char + *encoded over 6 bytes: + *1111 110x 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx 10xx xxxx + */ + c = *byte_ptr & 1; + nb_bytes_2_decode = 6; + + } else { + /* + *BAD ENCODING + */ + return CR_ENCODING_ERROR; + } + + /* + *Go and decode the remaining byte(s) + *(if any) to get the current character. + */ + for (; nb_bytes_2_decode > 1; nb_bytes_2_decode--) { + /*decode the next byte */ + byte_ptr++; + + /*byte pattern must be: 10xx xxxx */ + if ((*byte_ptr & 0xC0) != 0x80) { + return CR_ENCODING_ERROR; + } + + c = (c << 6) | (*byte_ptr & 0x3F); + } + + /* + *The decoded ucs4 char is now + *in c. + */ + + if (c <= 0xFF) { /*Add other conditions to support + *other char sets (ucs2, ucs3, ucs4). + */ + len++; + } else { + /*the char is too long to fit + *into the supposed charset len. + */ + return CR_ENCODING_ERROR; + } + } + + *a_len = len; + + return CR_OK; +} + +/** + *Converts an utf8 string into an ucs4 string. + *@param a_in the input string to convert. + *@param a_in_len in/out parameter. The length of the input + *string. After return, points to the actual number of bytes + *consumed. This can be useful to debug the input stream in case + *of encoding error. + *@param a_out out parameter. Points to the output string. It is allocated + *by this function and must be freed by the caller. + *@param a_out_len out parameter. The length of the output string. + *@return CR_OK upon successful completion, an error code otherwise. + * + */ +enum CRStatus +cr_utils_utf8_str_to_ucs4 (const guchar * a_in, + gulong * a_in_len, + guint32 ** a_out, gulong * a_out_len) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len + && a_out && a_out_len, CR_BAD_PARAM_ERROR); + + status = cr_utils_utf8_str_len_as_ucs4 (a_in, + &a_in[*a_in_len - 1], + a_out_len); + + g_return_val_if_fail (status == CR_OK, status); + + *a_out = g_malloc0 (*a_out_len * sizeof (guint32)); + + status = cr_utils_utf8_to_ucs4 (a_in, a_in_len, *a_out, a_out_len); + + return status; +} + +/** + *Converts an ucs4 buffer into an utf8 buffer. + * + *@param a_in the input ucs4 buffer to convert. + *@param a_in_len in/out parameter. The size of the + *input buffer to convert. After return, this parameter contains + *the actual number of characters consumed. + *@param a_out the output converted utf8 buffer. Must be allocated by + *the caller. + *@param a_out_len in/out parameter. The size of the output buffer. + *If this size is actually smaller than the real needed size, the function + *just converts what it can and returns a success status. After return, + *this param points to the actual number of bytes in the buffer. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_ucs4_to_utf8 (const guint32 * a_in, + gulong * a_in_len, guchar * a_out, gulong * a_out_len) +{ + gulong in_len = 0, + in_index = 0, + out_index = 0; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len && a_out && a_out_len, + CR_BAD_PARAM_ERROR); + + if (*a_in_len < 1) { + status = CR_OK; + goto end; + } + + in_len = *a_in_len; + + for (in_index = 0; in_index < in_len; in_index++) { + /* + *FIXME: return whenever we encounter forbidden char values. + */ + + if (a_in[in_index] <= 0x7F) { + a_out[out_index] = a_in[in_index]; + out_index++; + } else if (a_in[in_index] <= 0x7FF) { + a_out[out_index] = (0xC0 | (a_in[in_index] >> 6)); + a_out[out_index + 1] = + (0x80 | (a_in[in_index] & 0x3F)); + out_index += 2; + } else if (a_in[in_index] <= 0xFFFF) { + a_out[out_index] = (0xE0 | (a_in[in_index] >> 12)); + a_out[out_index + 1] = + (0x80 | ((a_in[in_index] >> 6) & 0x3F)); + a_out[out_index + 2] = + (0x80 | (a_in[in_index] & 0x3F)); + out_index += 3; + } else if (a_in[in_index] <= 0x1FFFFF) { + a_out[out_index] = (0xF0 | (a_in[in_index] >> 18)); + a_out[out_index + 1] + = (0x80 | ((a_in[in_index] >> 12) & 0x3F)); + a_out[out_index + 2] + = (0x80 | ((a_in[in_index] >> 6) & 0x3F)); + a_out[out_index + 3] + = (0x80 | (a_in[in_index] & 0x3F)); + out_index += 4; + } else if (a_in[in_index] <= 0x3FFFFFF) { + a_out[out_index] = (0xF8 | (a_in[in_index] >> 24)); + a_out[out_index + 1] = + (0x80 | (a_in[in_index] >> 18)); + a_out[out_index + 2] + = (0x80 | ((a_in[in_index] >> 12) & 0x3F)); + a_out[out_index + 3] + = (0x80 | ((a_in[in_index] >> 6) & 0x3F)); + a_out[out_index + 4] + = (0x80 | (a_in[in_index] & 0x3F)); + out_index += 5; + } else if (a_in[in_index] <= 0x7FFFFFFF) { + a_out[out_index] = (0xFC | (a_in[in_index] >> 30)); + a_out[out_index + 1] = + (0x80 | (a_in[in_index] >> 24)); + a_out[out_index + 2] + = (0x80 | ((a_in[in_index] >> 18) & 0x3F)); + a_out[out_index + 3] + = (0x80 | ((a_in[in_index] >> 12) & 0x3F)); + a_out[out_index + 4] + = (0x80 | ((a_in[in_index] >> 6) & 0x3F)); + a_out[out_index + 4] + = (0x80 | (a_in[in_index] & 0x3F)); + out_index += 6; + } else { + status = CR_ENCODING_ERROR; + goto end; + } + } /*end for */ + + end: + *a_in_len = in_index + 1; + *a_out_len = out_index + 1; + + return status; +} + +/** + *Converts an ucs4 string into an utf8 string. + *@param a_in the input string to convert. + *@param a_in_len in/out parameter. The length of the input + *string. After return, points to the actual number of characters + *consumed. This can be useful to debug the input string in case + *of encoding error. + *@param a_out out parameter. Points to the output string. It is allocated + *by this function and must be freed by the caller. + *@param a_out_len out parameter. The length (in bytes) of the output string. + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_ucs4_str_to_utf8 (const guint32 * a_in, + gulong * a_in_len, + guchar ** a_out, gulong * a_out_len) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len && a_out + && a_out_len, CR_BAD_PARAM_ERROR); + + status = cr_utils_ucs4_str_len_as_utf8 (a_in, + &a_in[*a_out_len - 1], + a_out_len); + + g_return_val_if_fail (status == CR_OK, status); + + status = cr_utils_ucs4_to_utf8 (a_in, a_in_len, *a_out, a_out_len); + + return status; +} + +/** + *Converts an ucs1 buffer into an utf8 buffer. + *The caller must know the size of the resulting buffer and + *allocate it prior to calling this function. + * + *@param a_in the input ucs1 buffer. + * + *@param a_in_len in/out parameter. The length of the input buffer. + *After return, points to the number of bytes actually consumed even + *in case of encoding error. + * + *@param a_out out parameter. The output utf8 converted buffer. + * + *@param a_out_len in/out parameter. The size of the output buffer. + *If the output buffer size is shorter than the actual needed size, + *this function just convert what it can. + * + *@return CR_OK upon successful completion, an error code otherwise. + * + */ +enum CRStatus +cr_utils_ucs1_to_utf8 (const guchar * a_in, + gulong * a_in_len, guchar * a_out, gulong * a_out_len) +{ + gulong out_index = 0, + in_index = 0, + in_len = 0, + out_len = 0; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len + && a_out_len, + CR_BAD_PARAM_ERROR); + + if (*a_in_len == 0) { + *a_out_len = 0 ; + return status; + } + g_return_val_if_fail (a_out, CR_BAD_PARAM_ERROR) ; + + in_len = *a_in_len; + out_len = *a_out_len; + + for (in_index = 0, out_index = 0; + (in_index < in_len) && (out_index < out_len); in_index++) { + /* + *FIXME: return whenever we encounter forbidden char values. + */ + + if (a_in[in_index] <= 0x7F) { + a_out[out_index] = a_in[in_index]; + out_index++; + } else { + a_out[out_index] = (0xC0 | (a_in[in_index] >> 6)); + a_out[out_index + 1] = + (0x80 | (a_in[in_index] & 0x3F)); + out_index += 2; + } + } /*end for */ + + *a_in_len = in_index; + *a_out_len = out_index; + + return status; +} + +/** + *Converts an ucs1 string into an utf8 string. + *@param a_in_start the beginning of the input string to convert. + *@param a_in_end the end of the input string to convert. + *@param a_out out parameter. The converted string. + *@param a_out out parameter. The length of the converted string. + *@return CR_OK upon successful completion, an error code otherwise. + * + */ +enum CRStatus +cr_utils_ucs1_str_to_utf8 (const guchar * a_in, + gulong * a_in_len, + guchar ** a_out, gulong * a_out_len) +{ + gulong out_len = 0; + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len && a_out + && a_out_len, CR_BAD_PARAM_ERROR); + + if (*a_in_len < 1) { + *a_out_len = 0; + *a_out = NULL; + return CR_OK; + } + + status = cr_utils_ucs1_str_len_as_utf8 (a_in, &a_in[*a_in_len - 1], + &out_len); + + g_return_val_if_fail (status == CR_OK, status); + + *a_out = g_malloc0 (out_len); + + status = cr_utils_ucs1_to_utf8 (a_in, a_in_len, *a_out, &out_len); + + *a_out_len = out_len; + + return status; +} + +/** + *Converts an utf8 buffer into an ucs1 buffer. + *The caller must know the size of the resulting + *converted buffer, and allocated it prior to calling this + *function. + * + *@param a_in the input utf8 buffer to convert. + * + *@param a_in_len in/out parameter. The size of the input utf8 buffer. + *After return, points to the number of bytes consumed + *by the function even in case of encoding error. + * + *@param a_out out parameter. Points to the resulting buffer. + *Must be allocated by the caller. If the size of a_out is shorter + *than its required size, this function converts what it can and return + *a successful status. + * + *@param a_out_len in/out parameter. The size of the output buffer. + *After return, points to the number of bytes consumed even in case of + *encoding error. + * + *@return CR_OK upon successful completion, an error code otherwise. + */ +enum CRStatus +cr_utils_utf8_to_ucs1 (const guchar * a_in, + gulong * a_in_len, guchar * a_out, gulong * a_out_len) +{ + gulong in_index = 0, + out_index = 0, + in_len = 0, + out_len = 0; + enum CRStatus status = CR_OK; + + /* + *to store the final decoded + *unicode char + */ + guint32 c = 0; + + g_return_val_if_fail (a_in && a_in_len + && a_out && a_out_len, CR_BAD_PARAM_ERROR); + + if (*a_in_len < 1) { + goto end; + } + + in_len = *a_in_len; + out_len = *a_out_len; + + for (in_index = 0, out_index = 0; + (in_index < in_len) && (out_index < out_len); + in_index++, out_index++) { + gint nb_bytes_2_decode = 0; + + if (a_in[in_index] <= 0x7F) { + /* + *7 bits long char + *encoded over 1 byte: + * 0xxx xxxx + */ + c = a_in[in_index]; + nb_bytes_2_decode = 1; + + } else if ((a_in[in_index] & 0xE0) == 0xC0) { + /* + *up to 11 bits long char. + *encoded over 2 bytes: + *110x xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x1F; + nb_bytes_2_decode = 2; + + } else if ((a_in[in_index] & 0xF0) == 0xE0) { + /* + *up to 16 bit long char + *encoded over 3 bytes: + *1110 xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x0F; + nb_bytes_2_decode = 3; + + } else if ((a_in[in_index] & 0xF8) == 0xF0) { + /* + *up to 21 bits long char + *encoded over 4 bytes: + *1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 0x7; + nb_bytes_2_decode = 4; + + } else if ((a_in[in_index] & 0xFC) == 0xF8) { + /* + *up to 26 bits long char + *encoded over 5 bytes. + *1111 10xx 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 3; + nb_bytes_2_decode = 5; + + } else if ((a_in[in_index] & 0xFE) == 0xFC) { + /* + *up to 31 bits long char + *encoded over 6 bytes: + *1111 110x 10xx xxxx 10xx xxxx + *10xx xxxx 10xx xxxx 10xx xxxx + */ + c = a_in[in_index] & 1; + nb_bytes_2_decode = 6; + + } else { + /*BAD ENCODING */ + status = CR_ENCODING_ERROR; + goto end; + } + + /* + *Go and decode the remaining byte(s) + *(if any) to get the current character. + */ + if (in_index + nb_bytes_2_decode - 1 >= in_len) { + goto end; + } + + for (; nb_bytes_2_decode > 1; nb_bytes_2_decode--) { + /*decode the next byte */ + in_index++; + + /*byte pattern must be: 10xx xxxx */ + if ((a_in[in_index] & 0xC0) != 0x80) { + status = CR_ENCODING_ERROR; + goto end; + } + + c = (c << 6) | (a_in[in_index] & 0x3F); + } + + /* + *The decoded ucs4 char is now + *in c. + */ + + if (c > 0xFF) { + status = CR_ENCODING_ERROR; + goto end; + } + + a_out[out_index] = c; + } + + end: + *a_out_len = out_index; + *a_in_len = in_index; + + return status; +} + +/** + *Converts an utf8 buffer into an + *ucs1 buffer. + *@param a_in_start the start of the input buffer. + *@param a_in_end the end of the input buffer. + *@param a_out out parameter. The resulting converted ucs4 buffer. + *Must be freed by the caller. + *@param a_out_len out parameter. The length of the converted buffer. + *@return CR_OK upon successful completion, an error code otherwise. + *Note that out parameters are valid if and only if this function + *returns CR_OK. + */ +enum CRStatus +cr_utils_utf8_str_to_ucs1 (const guchar * a_in, + gulong * a_in_len, + guchar ** a_out, gulong * a_out_len) +{ + enum CRStatus status = CR_OK; + + g_return_val_if_fail (a_in && a_in_len + && a_out && a_out_len, CR_BAD_PARAM_ERROR); + + if (*a_in_len < 1) { + *a_out_len = 0; + *a_out = NULL; + return CR_OK; + } + + status = cr_utils_utf8_str_len_as_ucs4 (a_in, &a_in[*a_in_len - 1], + a_out_len); + + g_return_val_if_fail (status == CR_OK, status); + + *a_out = g_malloc0 (*a_out_len * sizeof (guint32)); + + status = cr_utils_utf8_to_ucs1 (a_in, a_in_len, *a_out, a_out_len); + return status; +} + +/***************************************** + *CSS basic types identification utilities + *****************************************/ + +/** + *Returns TRUE if a_char is a white space as + *defined in the css spec in chap 4.1.1. + * + *white-space ::= ' '| \t|\r|\n|\f + * + *@param a_char the character to test. + *return TRUE if is a white space, false otherwise. + */ +gboolean +cr_utils_is_white_space (guint32 a_char) +{ + switch (a_char) { + case ' ': + case '\t': + case '\r': + case '\n': + case '\f': + return TRUE; + break; + default: + return FALSE; + } +} + +/** + *Returns true if the character is a newline + *as defined in the css spec in the chap 4.1.1. + * + *nl ::= \n|\r\n|\r|\f + * + *@param a_char the character to test. + *@return TRUE if the character is a newline, FALSE otherwise. + */ +gboolean +cr_utils_is_newline (guint32 a_char) +{ + switch (a_char) { + case '\n': + case '\r': + case '\f': + return TRUE; + break; + default: + return FALSE; + } +} + +/** + *returns TRUE if the char is part of an hexa num char: + *i.e hexa_char ::= [0-9A-F] + */ +gboolean +cr_utils_is_hexa_char (guint32 a_char) +{ + if ((a_char >= '0' && a_char <= '9') + || (a_char >= 'A' && a_char <= 'F')) { + return TRUE; + } + return FALSE; +} + +/** + *Returns true if the character is a nonascii + *character (as defined in the css spec chap 4.1.1): + * + *nonascii ::= [^\0-\177] + * + *@param a_char the character to test. + *@return TRUE if the character is a nonascii char, + *FALSE otherwise. + */ +gboolean +cr_utils_is_nonascii (guint32 a_char) +{ + if (a_char <= 177) { + return FALSE; + } + + return TRUE; +} + +/** + *Dumps a character a_nb times on a file. + *@param a_char the char to dump + *@param a_fp the destination file pointer + *@param a_nb the number of times a_char is to be dumped. + */ +void +cr_utils_dump_n_chars (guchar a_char, FILE * a_fp, glong a_nb) +{ + glong i = 0; + + for (i = 0; i < a_nb; i++) { + fprintf (a_fp, "%c", a_char); + } +} + +void +cr_utils_dump_n_chars2 (guchar a_char, GString * a_string, glong a_nb) +{ + glong i = 0; + + g_return_if_fail (a_string); + + for (i = 0; i < a_nb; i++) { + g_string_append_printf (a_string, "%c", a_char); + } +} + +/** + *Duplicates a list of GString instances. + *@return the duplicated list of GString instances or NULL if + *something bad happened. + *@param a_list_of_strings the list of strings to be duplicated. + */ +GList * +cr_utils_dup_glist_of_string (GList const * a_list_of_strings) +{ + GList const *cur = NULL; + GList *result = NULL; + + g_return_val_if_fail (a_list_of_strings, NULL); + + for (cur = a_list_of_strings; cur; cur = cur->next) { + GString *str = NULL; + + str = g_string_new_len (((GString *) cur->data)->str, + ((GString *) cur->data)->len); + if (str) + result = g_list_append (result, str); + } + + return result; +} + +/** + *Duplicate a GList where the GList::data is a CRString. + *@param a_list_of_strings the list to duplicate + *@return the duplicated list, or NULL if something bad + *happened. + */ +GList * +cr_utils_dup_glist_of_cr_string (GList const * a_list_of_strings) +{ + GList const *cur = NULL; + GList *result = NULL; + + g_return_val_if_fail (a_list_of_strings, NULL); + + for (cur = a_list_of_strings; cur; cur = cur->next) { + CRString *str = NULL; + + str = cr_string_dup ((CRString const *) cur->data) ; + if (str) + result = g_list_append (result, str); + } + + return result; +} diff --git a/src/st/croco/cr-utils.h b/src/st/croco/cr-utils.h new file mode 100644 index 0000000..54aa249 --- /dev/null +++ b/src/st/croco/cr-utils.h @@ -0,0 +1,246 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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: Dodji Seketeli + * Look at file COPYRIGHTS for copyright information + */ + +#ifndef __CR_DEFS_H__ +#define __CR_DEFS_H__ + +#include <stdio.h> +#include <glib.h> +#include "libcroco-config.h" + +G_BEGIN_DECLS + +/** + *@file + *The Croco library basic types definitions + *And global definitions. + */ + +/** + *The status type returned + *by the methods of the croco library. + */ +enum CRStatus { + CR_OK, + CR_BAD_PARAM_ERROR, + CR_INSTANCIATION_FAILED_ERROR, + CR_UNKNOWN_TYPE_ERROR, + CR_UNKNOWN_PROP_ERROR, + CR_UNKNOWN_PROP_VAL_ERROR, + CR_UNEXPECTED_POSITION_SCHEME, + CR_START_OF_INPUT_ERROR, + CR_END_OF_INPUT_ERROR, + CR_OUTPUT_TOO_SHORT_ERROR, + CR_INPUT_TOO_SHORT_ERROR, + CR_OUT_OF_BOUNDS_ERROR, + CR_EMPTY_PARSER_INPUT_ERROR, + CR_ENCODING_ERROR, + CR_ENCODING_NOT_FOUND_ERROR, + CR_PARSING_ERROR, + CR_SYNTAX_ERROR, + CR_NO_ROOT_NODE_ERROR, + CR_NO_TOKEN, + CR_OUT_OF_MEMORY_ERROR, + CR_PSEUDO_CLASS_SEL_HANDLER_NOT_FOUND_ERROR, + CR_BAD_PSEUDO_CLASS_SEL_HANDLER_ERROR, + CR_ERROR, + CR_FILE_NOT_FOUND_ERROR, + CR_VALUE_NOT_FOUND_ERROR +} ; + +/** + *Values used by + *cr_input_seek_position() ; + */ +enum CRSeekPos { + CR_SEEK_CUR, + CR_SEEK_BEGIN, + CR_SEEK_END +} ; + +/** + *Encoding values. + */ +enum CREncoding +{ + CR_UCS_4 = 1/*Must be not NULL*/, + CR_UCS_1, + CR_ISO_8859_1, + CR_ASCII, + CR_UTF_8, + CR_UTF_16, + CR_AUTO/*should be the last one*/ +} ; + + + + +#define CROCO_LOG_DOMAIN "LIBCROCO" + +#ifdef __GNUC__ +#define cr_utils_trace(a_log_level, a_msg) \ +g_log (CROCO_LOG_DOMAIN, \ + G_LOG_LEVEL_CRITICAL, \ + "file %s: line %d (%s): %s\n", \ + __FILE__, \ + __LINE__, \ + __PRETTY_FUNCTION__, \ + a_msg) +#else /*__GNUC__*/ + +#define cr_utils_trace(a_log_level, a_msg) \ +g_log (CROCO_LOG_DOMAIN, \ + G_LOG_LEVEL_CRITICAL, \ + "file %s: line %d: %s\n", \ + __FILE__, \ + __LINE__, \ + a_msg) +#endif + +/** + *Traces an info message. + *The file, line and enclosing function + *of the message will be automatically + *added to the message. + *@param a_msg the msg to trace. + */ +#define cr_utils_trace_info(a_msg) \ +cr_utils_trace (G_LOG_LEVEL_INFO, a_msg) + +/** + *Trace a debug message. + *The file, line and enclosing function + *of the message will be automatically + *added to the message. + *@param a_msg the msg to trace. + */ +#define cr_utils_trace_debug(a_msg) \ +cr_utils_trace (G_LOG_LEVEL_DEBUG, a_msg) ; + + +/**************************** + *Encoding transformations and + *encoding helpers + ****************************/ + +enum CRStatus +cr_utils_read_char_from_utf8_buf (const guchar * a_in, gulong a_in_len, + guint32 *a_out, gulong *a_consumed) ; + +enum CRStatus +cr_utils_ucs1_to_utf8 (const guchar *a_in, gulong *a_in_len, + guchar *a_out, gulong *a_out_len) ; + +enum CRStatus +cr_utils_utf8_to_ucs1 (const guchar * a_in, gulong * a_in_len, + guchar *a_out, gulong *a_out_len) ; + +enum CRStatus +cr_utils_ucs4_to_utf8 (const guint32 *a_in, gulong *a_in_len, + guchar *a_out, gulong *a_out_len) ; + +enum CRStatus +cr_utils_utf8_str_len_as_ucs4 (const guchar *a_in_start, + const guchar *a_in_end, + gulong *a_len) ; +enum CRStatus +cr_utils_ucs1_str_len_as_utf8 (const guchar *a_in_start, + const guchar *a_in_end, + gulong *a_len) ; +enum CRStatus +cr_utils_utf8_str_len_as_ucs1 (const guchar *a_in_start, + const guchar *a_in_end, + gulong *a_len) ; +enum CRStatus +cr_utils_ucs4_str_len_as_utf8 (const guint32 *a_in_start, + const guint32 *a_in_end, + gulong *a_len) ; + +enum CRStatus +cr_utils_ucs1_str_to_utf8 (const guchar *a_in_start, + gulong *a_in_len, + guchar **a_out, + gulong *a_len) ; + +enum CRStatus +cr_utils_utf8_str_to_ucs1 (const guchar * a_in_start, + gulong * a_in_len, + guchar **a_out, + gulong *a_out_len) ; + +enum CRStatus +cr_utils_utf8_to_ucs4 (const guchar * a_in, + gulong * a_in_len, + guint32 *a_out, gulong *a_out_len) ; + +enum CRStatus +cr_utils_ucs4_str_to_utf8 (const guint32 *a_in, + gulong *a_in_len, + guchar **a_out, gulong *a_out_len) ; + +enum CRStatus +cr_utils_utf8_str_to_ucs4 (const guchar * a_in, + gulong *a_in_len, + guint32 **a_out, + gulong *a_out_len) ; + + +/***************************************** + *CSS basic types identification utilities + *****************************************/ + +gboolean +cr_utils_is_newline (guint32 a_char) ; + +gboolean +cr_utils_is_white_space (guint32 a_char) ; + +gboolean +cr_utils_is_nonascii (guint32 a_char) ; + +gboolean +cr_utils_is_hexa_char (guint32 a_char) ; + + +/********************************** + *Miscellaneous utility functions + ***********************************/ + +void +cr_utils_dump_n_chars (guchar a_char, + FILE *a_fp, + glong a_nb) ; + +void +cr_utils_dump_n_chars2 (guchar a_char, + GString *a_string, + glong a_nb) ; +GList * +cr_utils_dup_glist_of_string (GList const *a_list) ; + +GList * +cr_utils_dup_glist_of_cr_string (GList const * a_list_of_strings) ; + +G_END_DECLS + +#endif /*__CR_DEFS_H__*/ diff --git a/src/st/croco/libcroco-config.h b/src/st/croco/libcroco-config.h new file mode 100644 index 0000000..1ffb758 --- /dev/null +++ b/src/st/croco/libcroco-config.h @@ -0,0 +1,13 @@ +#ifndef LIBCROCO_VERSION_NUMBER +#define LIBCROCO_VERSION_NUMBER 612 +#endif + +#ifndef LIBCROCO_VERSION +#define LIBCROCO_VERSION "0.6.12" +#endif + +#ifndef G_DISABLE_CHECKS +#if 0 +#define G_DISABLE_CHECKS 0 +#endif +#endif diff --git a/src/st/croco/libcroco.h b/src/st/croco/libcroco.h new file mode 100644 index 0000000..6187a7c --- /dev/null +++ b/src/st/croco/libcroco.h @@ -0,0 +1,42 @@ +/* + * This file is part of The Croco Library + * + * Copyright (C) 2002-2003 Dodji Seketeli <dodji@seketeli.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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 + */ + +#ifndef __LIBCROCO_H__ +#define __LIBCROCO_H__ + +#include "libcroco-config.h" + +#include "cr-utils.h" +#include "cr-pseudo.h" +#include "cr-term.h" +#include "cr-attr-sel.h" +#include "cr-simple-sel.h" +#include "cr-selector.h" +#include "cr-enc-handler.h" +#include "cr-doc-handler.h" +#include "cr-input.h" +#include "cr-parser.h" +#include "cr-statement.h" +#include "cr-stylesheet.h" +#include "cr-om-parser.h" +#include "cr-prop-list.h" +#include "cr-string.h" + +#endif /*__LIBCROCO_H__*/ diff --git a/src/st/meson.build b/src/st/meson.build new file mode 100644 index 0000000..717aa05 --- /dev/null +++ b/src/st/meson.build @@ -0,0 +1,220 @@ +# please, keep this sorted alphabetically +st_headers = [ + 'st-adjustment.h', + 'st-bin.h', + 'st-border-image.h', + 'st-box-layout.h', + 'st-button.h', + 'st-clipboard.h', + 'st-drawing-area.h', + 'st-entry.h', + 'st-focus-manager.h', + 'st-generic-accessible.h', + 'st-icon.h', + 'st-icon-colors.h', + 'st-image-content.h', + 'st-label.h', + 'st-password-entry.h', + 'st-scrollable.h', + 'st-scroll-bar.h', + 'st-scroll-view.h', + 'st-scroll-view-fade.h', + 'st-settings.h', + 'st-shadow.h', + 'st-texture-cache.h', + 'st-theme.h', + 'st-theme-context.h', + 'st-theme-node.h', + 'st-types.h', + 'st-viewport.h', + 'st-widget.h', + 'st-widget-accessible.h' +] + +st_includes = [] +foreach include : st_headers + st_includes += '#include <@0@>'.format(include) +endforeach + +st_h_data = configuration_data() +st_h_data.set('includes', '\n'.join(st_includes)) + +st_h = configure_file( + input: 'st.h.in', + output: 'st.h', + configuration: st_h_data +) + +st_inc = include_directories('.', '..') + +# please, keep this sorted alphabetically +st_private_headers = [ + 'croco/cr-additional-sel.h', + 'croco/cr-attr-sel.h', + 'croco/cr-cascade.h', + 'croco/cr-declaration.h', + 'croco/cr-doc-handler.h', + 'croco/cr-enc-handler.h', + 'croco/cr-fonts.h', + 'croco/cr-input.h', + 'croco/cr-num.h', + 'croco/cr-om-parser.h', + 'croco/cr-parser.h', + 'croco/cr-parsing-location.h', + 'croco/cr-prop-list.h', + 'croco/cr-pseudo.h', + 'croco/cr-rgb.h', + 'croco/cr-selector.h', + 'croco/cr-simple-sel.h', + 'croco/cr-statement.h', + 'croco/cr-string.h', + 'croco/cr-stylesheet.h', + 'croco/cr-term.h', + 'croco/cr-tknzr.h', + 'croco/cr-token.h', + 'croco/cr-utils.h', + 'croco/libcroco-config.h', + 'croco/libcroco.h', + 'st-private.h', + 'st-theme-private.h', + 'st-theme-node-private.h', + 'st-theme-node-transition.h' +] + +# please, keep this sorted alphabetically +croco_sources = [ + 'croco/cr-additional-sel.c', + 'croco/cr-attr-sel.c', + 'croco/cr-cascade.c', + 'croco/cr-declaration.c', + 'croco/cr-doc-handler.c', + 'croco/cr-enc-handler.c', + 'croco/cr-fonts.c', + 'croco/cr-input.c', + 'croco/cr-num.c', + 'croco/cr-om-parser.c', + 'croco/cr-parser.c', + 'croco/cr-parsing-location.c', + 'croco/cr-prop-list.c', + 'croco/cr-pseudo.c', + 'croco/cr-rgb.c', + 'croco/cr-selector.c', + 'croco/cr-simple-sel.c', + 'croco/cr-statement.c', + 'croco/cr-string.c', + 'croco/cr-stylesheet.c', + 'croco/cr-term.c', + 'croco/cr-tknzr.c', + 'croco/cr-token.c', + 'croco/cr-utils.c', +] + +# please, keep this sorted alphabetically +st_sources = [ + 'st-adjustment.c', + 'st-bin.c', + 'st-border-image.c', + 'st-box-layout.c', + 'st-button.c', + 'st-clipboard.c', + 'st-drawing-area.c', + 'st-entry.c', + 'st-focus-manager.c', + 'st-generic-accessible.c', + 'st-icon.c', + 'st-icon-colors.c', + 'st-image-content.c', + 'st-label.c', + 'st-password-entry.c', + 'st-private.c', + 'st-scrollable.c', + 'st-scroll-bar.c', + 'st-scroll-view.c', + 'st-scroll-view-fade.c', + 'st-settings.c', + 'st-shadow.c', + 'st-texture-cache.c', + 'st-theme.c', + 'st-theme-context.c', + 'st-theme-node.c', + 'st-theme-node-drawing.c', + 'st-theme-node-transition.c', + 'st-viewport.c', + 'st-widget.c' +] + +st_enums = gnome.mkenums_simple('st-enum-types', + sources: st_headers, + header_prefix: ''' +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif''' +) + +st_gir_sources = st_sources + st_headers + st_enums + +data_to_c = find_program(meson.project_source_root() + '/src/data-to-c.pl') + +glsl_sources = custom_target('scroll-view-fade-glsl', + input: ['st-scroll-view-fade.glsl'], + output: ['st-scroll-view-fade-generated.h'], + capture: true, + command: [data_to_c, '@INPUT@', 'st_scroll_view_fade_glsl'] +) + +st_nogir_sources = [glsl_sources] + +st_cflags = [ + '-I@0@/src'.format(meson.project_source_root()), + '-I@0@'.format(meson.project_build_root()), + '-DPREFIX="@0@"'.format(prefix), + '-DLIBDIR="@0@"'.format(libdir), + '-DG_LOG_DOMAIN="St"', + '-DST_COMPILATION', + '-DCLUTTER_ENABLE_EXPERIMENTAL_API', + '-DCOGL_ENABLE_EXPERIMENTAL_API', + '-DPACKAGE_DATA_DIR="@0@"'.format(pkgdatadir) +] + +# Currently meson requires a shared library for building girs +libst = shared_library('st-1.0', + sources: st_gir_sources + st_nogir_sources + croco_sources, + c_args: st_cflags, + dependencies: [clutter_dep, gtk_dep, mutter_dep, libxml_dep, m_dep], + build_rpath: mutter_typelibdir, + install_rpath: mutter_typelibdir, + install_dir: pkglibdir, + install: true +) + +libst_dep = declare_dependency(link_with: libst, + sources: st_enums[1] +) + +if get_option('tests') + mutter_test_dep = dependency(libmutter_test_pc, version: mutter_req) + test_theme = executable('test-theme', + sources: 'test-theme.c', + c_args: st_cflags, + dependencies: [mutter_test_dep, gtk_dep, libxml_dep], + build_rpath: mutter_typelibdir, + link_with: libst + ) + + test('CSS styling support', test_theme, + workdir: meson.current_source_dir() + ) +endif + +libst_gir = gnome.generate_gir(libst, + sources: st_gir_sources, + nsversion: '1.0', + namespace: 'St', + includes: ['Clutter-' + mutter_api_version, 'Cally-' + mutter_api_version, 'Meta-' + mutter_api_version, 'Gtk-3.0'], + dependencies: [mutter_dep], + include_directories: include_directories('..'), + extra_args: ['-DST_COMPILATION', '--quiet'], + install_dir_gir: pkgdatadir, + install_dir_typelib: pkglibdir, + install: true +) diff --git a/src/st/st-adjustment.c b/src/st/st-adjustment.c new file mode 100644 index 0000000..d2baa66 --- /dev/null +++ b/src/st/st-adjustment.c @@ -0,0 +1,1013 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-adjustment.c: Adjustment object + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-adjustment + * @short_description: A GObject representing an adjustable bounded value + * + * The #StAdjustment object represents a range of values bounded between a + * minimum and maximum, together with step and page increments and a page size. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <glib-object.h> +#include <clutter/clutter.h> +#include <math.h> + +#include "st-adjustment.h" +#include "st-private.h" + +typedef struct _StAdjustmentPrivate StAdjustmentPrivate; + +struct _StAdjustmentPrivate +{ + ClutterActor *actor; + + /* Do not sanity-check values while constructing, + * not all properties may be set yet. */ + guint is_constructing : 1; + + GHashTable *transitions; + + gdouble lower; + gdouble upper; + gdouble value; + gdouble step_increment; + gdouble page_increment; + gdouble page_size; +}; + +static void animatable_iface_init (ClutterAnimatableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (StAdjustment, st_adjustment, G_TYPE_OBJECT, + G_ADD_PRIVATE (StAdjustment) + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_ANIMATABLE, + animatable_iface_init)); + +enum +{ + PROP_0, + + PROP_ACTOR, + PROP_LOWER, + PROP_UPPER, + PROP_VALUE, + PROP_STEP_INC, + PROP_PAGE_INC, + PROP_PAGE_SIZE, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum +{ + CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +typedef struct _TransitionClosure +{ + StAdjustment *adjustment; + ClutterTransition *transition; + char *name; + gulong completed_id; +} TransitionClosure; + +static gboolean st_adjustment_set_lower (StAdjustment *adjustment, + gdouble lower); +static gboolean st_adjustment_set_upper (StAdjustment *adjustment, + gdouble upper); +static gboolean st_adjustment_set_step_increment (StAdjustment *adjustment, + gdouble step); +static gboolean st_adjustment_set_page_increment (StAdjustment *adjustment, + gdouble page); +static gboolean st_adjustment_set_page_size (StAdjustment *adjustment, + gdouble size); + +static ClutterActor * +st_adjustment_get_actor (ClutterAnimatable *animatable) +{ + StAdjustment *adjustment = ST_ADJUSTMENT (animatable); + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + g_warn_if_fail (priv->actor); + + return priv->actor; +} + +static void +animatable_iface_init (ClutterAnimatableInterface *iface) +{ + iface->get_actor = st_adjustment_get_actor; +} + +static void +st_adjustment_constructed (GObject *object) +{ + GObjectClass *g_class; + StAdjustment *self = ST_ADJUSTMENT (object); + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (self); + + g_class = G_OBJECT_CLASS (st_adjustment_parent_class); + /* The docs say we're suppose to chain up, but would crash without + * some extra care. */ + if (g_class && g_class->constructed && + g_class->constructed != st_adjustment_constructed) + { + g_class->constructed (object); + } + + priv->is_constructing = FALSE; + st_adjustment_clamp_page (self, priv->lower, priv->upper); +} + +static void +st_adjustment_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (ST_ADJUSTMENT (gobject)); + + switch (prop_id) + { + case PROP_ACTOR: + g_value_set_object (value, priv->actor); + break; + + case PROP_LOWER: + g_value_set_double (value, priv->lower); + break; + + case PROP_UPPER: + g_value_set_double (value, priv->upper); + break; + + case PROP_VALUE: + g_value_set_double (value, priv->value); + break; + + case PROP_STEP_INC: + g_value_set_double (value, priv->step_increment); + break; + + case PROP_PAGE_INC: + g_value_set_double (value, priv->page_increment); + break; + + case PROP_PAGE_SIZE: + g_value_set_double (value, priv->page_size); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +actor_destroyed (gpointer user_data, + GObject *where_the_object_was) +{ + StAdjustment *adj = ST_ADJUSTMENT (user_data); + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adj); + + priv->actor = NULL; + + g_object_notify_by_pspec (G_OBJECT (adj), props[PROP_ACTOR]); +} + +static void +st_adjustment_set_actor (StAdjustment *adj, + ClutterActor *actor) +{ + StAdjustmentPrivate *priv; + + priv = st_adjustment_get_instance_private (adj); + + if (priv->actor == actor) + return; + + if (priv->actor) + g_object_weak_unref (G_OBJECT (priv->actor), actor_destroyed, adj); + priv->actor = actor; + if (priv->actor) + g_object_weak_ref (G_OBJECT (priv->actor), actor_destroyed, adj); + + g_object_notify_by_pspec (G_OBJECT (adj), props[PROP_ACTOR]); +} + +static void +st_adjustment_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StAdjustment *adj = ST_ADJUSTMENT (gobject); + + switch (prop_id) + { + case PROP_ACTOR: + st_adjustment_set_actor (adj, g_value_get_object (value)); + break; + + case PROP_LOWER: + st_adjustment_set_lower (adj, g_value_get_double (value)); + break; + + case PROP_UPPER: + st_adjustment_set_upper (adj, g_value_get_double (value)); + break; + + case PROP_VALUE: + st_adjustment_set_value (adj, g_value_get_double (value)); + break; + + case PROP_STEP_INC: + st_adjustment_set_step_increment (adj, g_value_get_double (value)); + break; + + case PROP_PAGE_INC: + st_adjustment_set_page_increment (adj, g_value_get_double (value)); + break; + + case PROP_PAGE_SIZE: + st_adjustment_set_page_size (adj, g_value_get_double (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_adjustment_dispose (GObject *object) +{ + StAdjustmentPrivate *priv; + + priv = st_adjustment_get_instance_private (ST_ADJUSTMENT (object)); + if (priv->actor) + { + g_object_weak_unref (G_OBJECT (priv->actor), actor_destroyed, object); + priv->actor = NULL; + } + g_clear_pointer (&priv->transitions, g_hash_table_unref); + + G_OBJECT_CLASS (st_adjustment_parent_class)->dispose (object); +} + +static void +st_adjustment_class_init (StAdjustmentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = st_adjustment_constructed; + object_class->get_property = st_adjustment_get_property; + object_class->set_property = st_adjustment_set_property; + object_class->dispose = st_adjustment_dispose; + + /** + * StAdjustment:actor: + * + * If the adjustment is used as #ClutterAnimatable for a + * #ClutterPropertyTransition, this property is used to determine which + * monitor should drive the animation. + */ + props[PROP_ACTOR] = + g_param_spec_object ("actor", "Actor", "Actor", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:lower: + * + * The minimum value of the adjustment. + */ + props[PROP_LOWER] = + g_param_spec_double ("lower", "Lower", "Lower bound", + -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:upper: + * + * The maximum value of the adjustment. + * + * Note that values will be restricted by `upper - page-size` if + * #StAdjustment:page-size is non-zero. + */ + props[PROP_UPPER] = + g_param_spec_double ("upper", "Upper", "Upper bound", + -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:value: + * + * The value of the adjustment. + */ + props[PROP_VALUE] = + g_param_spec_double ("value", "Value", "Current value", + -G_MAXDOUBLE, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:step-increment: + * + * The step increment of the adjustment. + */ + props[PROP_STEP_INC] = + g_param_spec_double ("step-increment", "Step Increment", "Step increment", + 0.0, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:page-increment: + * + * The page increment of the adjustment. + */ + props[PROP_PAGE_INC] = + g_param_spec_double ("page-increment", "Page Increment", "Page increment", + 0.0, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * StAdjustment:page-size: + * + * The page size of the adjustment. + * + * Note that the page-size is irrelevant and should be set to zero if the + * adjustment is used for a simple scalar value. + */ + props[PROP_PAGE_SIZE] = + g_param_spec_double ("page-size", "Page Size", "Page size", + 0.0, G_MAXDOUBLE, 0.0, + ST_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); + + /** + * StAdjustment::changed: + * @self: the #StAdjustment + * + * Emitted when any of the adjustment properties have changed, except for + * #StAdjustment:value. + */ + signals[CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StAdjustmentClass, changed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +st_adjustment_init (StAdjustment *self) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (self); + priv->is_constructing = TRUE; +} + +/** + * st_adjustment_new: + * @actor: (nullable): a #ClutterActor + * @value: the initial value + * @lower: the minimum value + * @upper: the maximum value + * @step_increment: the step increment + * @page_increment: the page increment + * @page_size: the page size + * + * Creates a new #StAdjustment + * + * Returns: a new #StAdjustment + */ +StAdjustment * +st_adjustment_new (ClutterActor *actor, + gdouble value, + gdouble lower, + gdouble upper, + gdouble step_increment, + gdouble page_increment, + gdouble page_size) +{ + return g_object_new (ST_TYPE_ADJUSTMENT, + "actor", actor, + "value", value, + "lower", lower, + "upper", upper, + "step-increment", step_increment, + "page-increment", page_increment, + "page-size", page_size, + NULL); +} + +/** + * st_adjustment_get_value: + * @adjustment: a #StAdjustment + * + * Gets the current value of the adjustment. See st_adjustment_set_value(). + * + * Returns: The current value of the adjustment + */ +gdouble +st_adjustment_get_value (StAdjustment *adjustment) +{ + g_return_val_if_fail (ST_IS_ADJUSTMENT (adjustment), 0); + + return ((StAdjustmentPrivate *)st_adjustment_get_instance_private (adjustment))->value; +} + +/** + * st_adjustment_set_value: + * @adjustment: a #StAdjustment + * @value: the new value + * + * Sets the #StAdjustment value. The value is clamped to lie between + * #StAdjustment:lower and #StAdjustment:upper - #StAdjustment:page-size. + */ +void +st_adjustment_set_value (StAdjustment *adjustment, + gdouble value) +{ + StAdjustmentPrivate *priv; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + + priv = st_adjustment_get_instance_private (adjustment); + + /* Defer clamp until after construction. */ + if (!priv->is_constructing) + { + value = CLAMP (value, + priv->lower, + MAX (priv->lower, priv->upper - priv->page_size)); + } + + if (priv->value != value) + { + priv->value = value; + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_VALUE]); + } +} + +/** + * st_adjustment_clamp_page: + * @adjustment: a #StAdjustment + * @lower: the lower value + * @upper: the upper value + * + * Set #StAdjustment:value to a value clamped between @lower and @upper. The + * clamping described by st_adjustment_set_value() still applies. + */ +void +st_adjustment_clamp_page (StAdjustment *adjustment, + gdouble lower, + gdouble upper) +{ + StAdjustmentPrivate *priv; + gboolean changed; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + + priv = st_adjustment_get_instance_private (adjustment); + + lower = CLAMP (lower, priv->lower, priv->upper - priv->page_size); + upper = CLAMP (upper, priv->lower + priv->page_size, priv->upper); + + changed = FALSE; + + if (priv->value + priv->page_size > upper) + { + priv->value = upper - priv->page_size; + changed = TRUE; + } + + if (priv->value < lower) + { + priv->value = lower; + changed = TRUE; + } + + if (changed) + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_VALUE]); +} + +/** + * st_adjustment_set_lower: + * @adjustment: a #StAdjustment + * @lower: the new minimum value + * + * Sets the minimum value of the adjustment. + * + * When setting multiple adjustment properties via their individual + * setters, multiple #GObject::notify and #StAdjustment::changed + * signals will be emitted. However, it’s possible to compress the + * #GObject::notify signals into one by calling + * g_object_freeze_notify() and g_object_thaw_notify() around the + * calls to the individual setters. + * + * Alternatively, using st_adjustment_set_values() will compress both + * #GObject::notify and #StAdjustment::changed emissions. + */ +static gboolean +st_adjustment_set_lower (StAdjustment *adjustment, + gdouble lower) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + if (priv->lower != lower) + { + priv->lower = lower; + + g_signal_emit (adjustment, signals[CHANGED], 0); + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_LOWER]); + + /* Defer clamp until after construction. */ + if (!priv->is_constructing) + st_adjustment_clamp_page (adjustment, priv->lower, priv->upper); + + return TRUE; + } + + return FALSE; +} + +/** + * st_adjustment_set_upper: + * @adjustment: a #StAdjustment + * @upper: the new maximum value + * + * Sets the maximum value of the adjustment. + * + * Note that values will be restricted by `upper - page-size` + * if the page-size property is nonzero. + * + * See st_adjustment_set_lower() about how to compress multiple + * signal emissions when setting multiple adjustment properties. + * + * Returns: %TRUE if the value was changed + */ +static gboolean +st_adjustment_set_upper (StAdjustment *adjustment, + gdouble upper) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + if (priv->upper != upper) + { + priv->upper = upper; + + g_signal_emit (adjustment, signals[CHANGED], 0); + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_UPPER]); + + /* Defer clamp until after construction. */ + if (!priv->is_constructing) + st_adjustment_clamp_page (adjustment, priv->lower, priv->upper); + + return TRUE; + } + + return FALSE; +} + +/** + * st_adjustment_set_step_increment: + * @adjustment: a #StAdjustment + * @step: the new step increment + * + * Sets the step increment of the adjustment. + * + * See st_adjustment_set_lower() about how to compress multiple + * signal emissions when setting multiple adjustment properties. + * + * Returns: %TRUE if the value was changed + */ +static gboolean +st_adjustment_set_step_increment (StAdjustment *adjustment, + gdouble step) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + if (priv->step_increment != step) + { + priv->step_increment = step; + + g_signal_emit (adjustment, signals[CHANGED], 0); + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_STEP_INC]); + + return TRUE; + } + + return FALSE; +} + +/** + * st_adjustment_set_page_increment: + * @adjustment: a #StAdjustment + * @page: the new page increment + * + * Sets the page increment of the adjustment. + * + * See st_adjustment_set_lower() about how to compress multiple + * signal emissions when setting multiple adjustment properties. + * + * Returns: %TRUE if the value was changed + */ +static gboolean +st_adjustment_set_page_increment (StAdjustment *adjustment, + gdouble page) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + if (priv->page_increment != page) + { + priv->page_increment = page; + + g_signal_emit (adjustment, signals[CHANGED], 0); + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_PAGE_INC]); + + return TRUE; + } + + return FALSE; +} + +/** + * st_adjustment_set_page_size: + * @adjustment: a #StAdjustment + * @size: the new page size + * + * Sets the page size of the adjustment. + * + * See st_adjustment_set_lower() about how to compress multiple + * signal emissions when setting multiple adjustment properties. + * + * Returns: %TRUE if the value was changed + */ +static gboolean +st_adjustment_set_page_size (StAdjustment *adjustment, + gdouble size) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + if (priv->page_size != size) + { + priv->page_size = size; + + g_signal_emit (adjustment, signals[CHANGED], 0); + + g_object_notify_by_pspec (G_OBJECT (adjustment), props[PROP_PAGE_SIZE]); + + /* We'll explicitly clamp after construction. */ + if (!priv->is_constructing) + st_adjustment_clamp_page (adjustment, priv->lower, priv->upper); + + return TRUE; + } + + return FALSE; +} + +/** + * st_adjustment_set_values: + * @adjustment: a #StAdjustment + * @value: the new value + * @lower: the new minimum value + * @upper: the new maximum value + * @step_increment: the new step increment + * @page_increment: the new page increment + * @page_size: the new page size + * + * Sets all properties of the adjustment at once. + * + * Use this function to avoid multiple emissions of the #GObject::notify and + * #StAdjustment::changed signals. See st_adjustment_set_lower() for an + * alternative way of compressing multiple emissions of #GObject::notify into + * one. + */ +void +st_adjustment_set_values (StAdjustment *adjustment, + gdouble value, + gdouble lower, + gdouble upper, + gdouble step_increment, + gdouble page_increment, + gdouble page_size) +{ + StAdjustmentPrivate *priv; + gboolean emit_changed = FALSE; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + g_return_if_fail (page_size >= 0 && page_size <= G_MAXDOUBLE); + g_return_if_fail (step_increment >= 0 && step_increment <= G_MAXDOUBLE); + g_return_if_fail (page_increment >= 0 && page_increment <= G_MAXDOUBLE); + + priv = st_adjustment_get_instance_private (adjustment); + + emit_changed = FALSE; + + g_object_freeze_notify (G_OBJECT (adjustment)); + + emit_changed |= st_adjustment_set_lower (adjustment, lower); + emit_changed |= st_adjustment_set_upper (adjustment, upper); + emit_changed |= st_adjustment_set_step_increment (adjustment, step_increment); + emit_changed |= st_adjustment_set_page_increment (adjustment, page_increment); + emit_changed |= st_adjustment_set_page_size (adjustment, page_size); + + if (value != priv->value) + { + st_adjustment_set_value (adjustment, value); + emit_changed = TRUE; + } + + if (emit_changed) + g_signal_emit (G_OBJECT (adjustment), signals[CHANGED], 0); + + g_object_thaw_notify (G_OBJECT (adjustment)); +} + +/** + * st_adjustment_get_values: + * @adjustment: an #StAdjustment + * @value: (out) (optional): the current value + * @lower: (out) (optional): the lower bound + * @upper: (out) (optional): the upper bound + * @step_increment: (out) (optional): the step increment + * @page_increment: (out) (optional): the page increment + * @page_size: (out) (optional): the page size + * + * Gets all of @adjustment's values at once. + */ +void +st_adjustment_get_values (StAdjustment *adjustment, + gdouble *value, + gdouble *lower, + gdouble *upper, + gdouble *step_increment, + gdouble *page_increment, + gdouble *page_size) +{ + StAdjustmentPrivate *priv; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + + priv = st_adjustment_get_instance_private (adjustment); + + if (lower) + *lower = priv->lower; + + if (upper) + *upper = priv->upper; + + if (value) + *value = st_adjustment_get_value (adjustment); + + if (step_increment) + *step_increment = priv->step_increment; + + if (page_increment) + *page_increment = priv->page_increment; + + if (page_size) + *page_size = priv->page_size; +} + +/** + * st_adjustment_adjust_for_scroll_event: + * @adjustment: An #StAdjustment + * @delta: A delta, retrieved directly from clutter_event_get_scroll_delta() + * or similar. + * + * Adjusts the adjustment using delta values from a scroll event. + * You should use this instead of using st_adjustment_set_value() + * as this method will tweak the values directly using the same + * math as GTK+, to ensure that scrolling is consistent across + * the environment. + */ +void +st_adjustment_adjust_for_scroll_event (StAdjustment *adjustment, + gdouble delta) +{ + StAdjustmentPrivate *priv; + gdouble new_value, scroll_unit; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + + priv = st_adjustment_get_instance_private (adjustment); + + scroll_unit = pow (priv->page_size, 2.0 / 3.0); + + new_value = priv->value + delta * scroll_unit; + st_adjustment_set_value (adjustment, new_value); +} + +static void +transition_closure_free (gpointer data) +{ + TransitionClosure *clos; + ClutterTimeline *timeline; + + if (G_UNLIKELY (data == NULL)) + return; + + clos = data; + timeline = CLUTTER_TIMELINE (clos->transition); + + g_clear_signal_handler (&clos->completed_id, clos->transition); + + if (clutter_timeline_is_playing (timeline)) + clutter_timeline_stop (timeline); + + g_object_unref (clos->transition); + g_free (clos->name); + g_free (clos); +} + +static void +remove_transition (StAdjustment *adjustment, + const char *name) +{ + StAdjustmentPrivate *priv = st_adjustment_get_instance_private (adjustment); + + g_hash_table_remove (priv->transitions, name); + + if (g_hash_table_size (priv->transitions) == 0) + g_clear_pointer (&priv->transitions, g_hash_table_unref); +} + +static void +on_transition_stopped (ClutterTransition *transition, + gboolean is_finished, + TransitionClosure *clos) +{ + StAdjustment *adjustment = clos->adjustment; + + if (!clutter_transition_get_remove_on_complete (transition)) + return; + + /* Take a reference, because removing the closure will + * release the reference on the transition, and we want + * it to survive the signal emission; ClutterTransition's + * own ::stopped signal closure will release it after all + * other handlers have run. + */ + g_object_ref (transition); + + remove_transition (adjustment, clos->name); +} + +/** + * st_adjustment_get_transition: + * @adjustment: a #StAdjustment + * @name: a transition name + * + * Get the #ClutterTransition for @name previously added with + * st_adjustment_add_transition() or %NULL if not found. + * + * Returns: (transfer none) (nullable): a #ClutterTransition + */ +ClutterTransition * +st_adjustment_get_transition (StAdjustment *adjustment, + const char *name) +{ + StAdjustmentPrivate *priv; + TransitionClosure *clos; + + g_return_val_if_fail (ST_IS_ADJUSTMENT (adjustment), NULL); + + priv = st_adjustment_get_instance_private (adjustment); + + if (priv->transitions == NULL) + return NULL; + + clos = g_hash_table_lookup (priv->transitions, name); + if (clos == NULL) + return NULL; + + return clos->transition; +} + +/** + * st_adjustment_add_transition: + * @adjustment: a #StAdjustment + * @name: a unique name for the transition + * @transition: a #ClutterTransition + * + * Add a #ClutterTransition for the adjustment. If the transition stops, it will + * be automatically removed if #ClutterTransition:remove-on-complete is %TRUE. + */ +void +st_adjustment_add_transition (StAdjustment *adjustment, + const char *name, + ClutterTransition *transition) +{ + StAdjustmentPrivate *priv; + TransitionClosure *clos; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + g_return_if_fail (name != NULL); + g_return_if_fail (CLUTTER_IS_TRANSITION (transition)); + + priv = st_adjustment_get_instance_private (adjustment); + + if (priv->transitions == NULL) + priv->transitions = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + transition_closure_free); + + if (g_hash_table_lookup (priv->transitions, name) != NULL) + { + g_warning ("A transition with name '%s' already exists for " + "adjustment '%p'", name, adjustment); + return; + } + + clutter_transition_set_animatable (transition, CLUTTER_ANIMATABLE (adjustment)); + + clos = g_new (TransitionClosure, 1); + clos->adjustment = adjustment; + clos->transition = g_object_ref (transition); + clos->name = g_strdup (name); + clos->completed_id = g_signal_connect (transition, "stopped", + G_CALLBACK (on_transition_stopped), + clos); + + g_hash_table_insert (priv->transitions, clos->name, clos); + clutter_timeline_start (CLUTTER_TIMELINE (transition)); +} + +/** + * st_adjusmtent_remove_transition: + * @adjustment: a #StAdjustment + * @name: the name of the transition to remove + * + * Remove a #ClutterTransition previously added by st_adjustment_add_transtion() + * with @name. + */ +void +st_adjustment_remove_transition (StAdjustment *adjustment, + const char *name) +{ + StAdjustmentPrivate *priv; + TransitionClosure *clos; + + g_return_if_fail (ST_IS_ADJUSTMENT (adjustment)); + g_return_if_fail (name != NULL); + + priv = st_adjustment_get_instance_private (adjustment); + + if (priv->transitions == NULL) + return; + + clos = g_hash_table_lookup (priv->transitions, name); + if (clos == NULL) + return; + + remove_transition (adjustment, name); +} diff --git a/src/st/st-adjustment.h b/src/st/st-adjustment.h new file mode 100644 index 0000000..08a0fc3 --- /dev/null +++ b/src/st/st-adjustment.h @@ -0,0 +1,92 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-adjustment.h: Adjustment object + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_ADJUSTMENT_H__ +#define __ST_ADJUSTMENT_H__ + +#include <glib-object.h> +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define ST_TYPE_ADJUSTMENT (st_adjustment_get_type()) +G_DECLARE_DERIVABLE_TYPE (StAdjustment, st_adjustment, ST, ADJUSTMENT, GObject) + +/** + * StAdjustmentClass: + * @changed: Class handler for the ::changed signal. + * + * Base class for #StAdjustment. + */ +struct _StAdjustmentClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + void (* changed) (StAdjustment *adjustment); +}; + +StAdjustment *st_adjustment_new (ClutterActor *actor, + gdouble value, + gdouble lower, + gdouble upper, + gdouble step_increment, + gdouble page_increment, + gdouble page_size); +gdouble st_adjustment_get_value (StAdjustment *adjustment); +void st_adjustment_set_value (StAdjustment *adjustment, + gdouble value); +void st_adjustment_clamp_page (StAdjustment *adjustment, + gdouble lower, + gdouble upper); +void st_adjustment_set_values (StAdjustment *adjustment, + gdouble value, + gdouble lower, + gdouble upper, + gdouble step_increment, + gdouble page_increment, + gdouble page_size); +void st_adjustment_get_values (StAdjustment *adjustment, + gdouble *value, + gdouble *lower, + gdouble *upper, + gdouble *step_increment, + gdouble *page_increment, + gdouble *page_size); + +void st_adjustment_adjust_for_scroll_event (StAdjustment *adjustment, + gdouble delta); + +ClutterTransition * st_adjustment_get_transition (StAdjustment *adjustment, + const char *name); +void st_adjustment_add_transition (StAdjustment *adjustment, + const char *name, + ClutterTransition *transition); +void st_adjustment_remove_transition (StAdjustment *adjustment, + const char *name); + +G_END_DECLS + +#endif /* __ST_ADJUSTMENT_H__ */ diff --git a/src/st/st-bin.c b/src/st/st-bin.c new file mode 100644 index 0000000..9d86ea2 --- /dev/null +++ b/src/st/st-bin.c @@ -0,0 +1,404 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-bin.c: Basic container actor + * + * Copyright 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-bin + * @short_description: a simple container with one actor + * + * #StBin is a simple container capable of having only one + * #ClutterActor as a child. + * + * #StBin inherits from #StWidget, so it is fully themable. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <clutter/clutter.h> + +#include "st-bin.h" +#include "st-enum-types.h" +#include "st-private.h" + +typedef struct _StBinPrivate StBinPrivate; +struct _StBinPrivate +{ + ClutterActor *child; +}; + +enum +{ + PROP_0, + + PROP_CHILD, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +static void clutter_container_iface_init (ClutterContainerIface *iface); + +G_DEFINE_TYPE_WITH_CODE (StBin, st_bin, ST_TYPE_WIDGET, + G_ADD_PRIVATE (StBin) + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER, + clutter_container_iface_init)); + +static void +st_bin_add (ClutterContainer *container, + ClutterActor *actor) +{ + st_bin_set_child (ST_BIN (container), actor); +} + +static void +st_bin_remove (ClutterContainer *container, + ClutterActor *actor) +{ + StBin *bin = ST_BIN (container); + StBinPrivate *priv = st_bin_get_instance_private (bin); + + if (priv->child == actor) + st_bin_set_child (bin, NULL); +} + +static void +clutter_container_iface_init (ClutterContainerIface *iface) +{ + iface->add = st_bin_add; + iface->remove = st_bin_remove; +} + +static double +get_align_factor (ClutterActorAlign align) +{ + switch (align) + { + case CLUTTER_ACTOR_ALIGN_CENTER: + return 0.5; + + case CLUTTER_ACTOR_ALIGN_START: + return 0.0; + + case CLUTTER_ACTOR_ALIGN_END: + return 1.0; + + case CLUTTER_ACTOR_ALIGN_FILL: + break; + } + + return 0.0; +} + +static void +st_bin_allocate (ClutterActor *self, + const ClutterActorBox *box) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (self)); + + clutter_actor_set_allocation (self, box); + + if (priv->child && clutter_actor_is_visible (priv->child)) + { + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + ClutterActorAlign x_align = clutter_actor_get_x_align (priv->child); + ClutterActorAlign y_align = clutter_actor_get_y_align (priv->child); + ClutterActorBox childbox; + + st_theme_node_get_content_box (theme_node, box, &childbox); + clutter_actor_allocate_align_fill (priv->child, &childbox, + get_align_factor (x_align), + get_align_factor (y_align), + x_align == CLUTTER_ACTOR_ALIGN_FILL, + y_align == CLUTTER_ACTOR_ALIGN_FILL); + } +} + +static void +st_bin_get_preferred_width (ClutterActor *self, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (self)); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + + st_theme_node_adjust_for_height (theme_node, &for_height); + + if (priv->child == NULL || !clutter_actor_is_visible (priv->child)) + { + if (min_width_p) + *min_width_p = 0; + + if (natural_width_p) + *natural_width_p = 0; + } + else + { + ClutterActorAlign y_align = clutter_actor_get_y_align (priv->child); + + _st_actor_get_preferred_width (priv->child, for_height, + y_align == CLUTTER_ACTOR_ALIGN_FILL, + min_width_p, + natural_width_p); + } + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_bin_get_preferred_height (ClutterActor *self, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (self)); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + + st_theme_node_adjust_for_width (theme_node, &for_width); + + if (priv->child == NULL || !clutter_actor_is_visible (priv->child)) + { + if (min_height_p) + *min_height_p = 0; + + if (natural_height_p) + *natural_height_p = 0; + } + else + { + ClutterActorAlign x_align = clutter_actor_get_y_align (priv->child); + + _st_actor_get_preferred_height (priv->child, for_width, + x_align == CLUTTER_ACTOR_ALIGN_FILL, + min_height_p, + natural_height_p); + } + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_bin_destroy (ClutterActor *actor) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (actor)); + + if (priv->child) + clutter_actor_destroy (priv->child); + g_assert (priv->child == NULL); + + CLUTTER_ACTOR_CLASS (st_bin_parent_class)->destroy (actor); +} + +static void +st_bin_popup_menu (StWidget *widget) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (widget)); + + if (priv->child && ST_IS_WIDGET (priv->child)) + st_widget_popup_menu (ST_WIDGET (priv->child)); +} + +static gboolean +st_bin_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (widget)); + ClutterActor *bin_actor = CLUTTER_ACTOR (widget); + + if (st_widget_get_can_focus (widget)) + { + if (from && clutter_actor_contains (bin_actor, from)) + return FALSE; + + if (clutter_actor_is_mapped (bin_actor)) + { + clutter_actor_grab_key_focus (bin_actor); + return TRUE; + } + else + { + return FALSE; + } + } + else if (priv->child && ST_IS_WIDGET (priv->child)) + return st_widget_navigate_focus (ST_WIDGET (priv->child), from, direction, FALSE); + else + return FALSE; +} + +static void +st_bin_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StBin *bin = ST_BIN (gobject); + + switch (prop_id) + { + case PROP_CHILD: + st_bin_set_child (bin, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +st_bin_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StBinPrivate *priv = st_bin_get_instance_private (ST_BIN (gobject)); + + switch (prop_id) + { + case PROP_CHILD: + g_value_set_object (value, priv->child); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + } +} + +static void +st_bin_class_init (StBinClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + gobject_class->set_property = st_bin_set_property; + gobject_class->get_property = st_bin_get_property; + + actor_class->get_preferred_width = st_bin_get_preferred_width; + actor_class->get_preferred_height = st_bin_get_preferred_height; + actor_class->allocate = st_bin_allocate; + actor_class->destroy = st_bin_destroy; + + widget_class->popup_menu = st_bin_popup_menu; + widget_class->navigate_focus = st_bin_navigate_focus; + + /** + * StBin:child: + * + * The child #ClutterActor of the #StBin container. + */ + props[PROP_CHILD] = + g_param_spec_object ("child", + "Child", + "The child of the Bin", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +static void +st_bin_init (StBin *bin) +{ +} + +/** + * st_bin_new: + * + * Creates a new #StBin, a simple container for one child. + * + * Returns: the newly created #StBin actor + */ +StWidget * +st_bin_new (void) +{ + return g_object_new (ST_TYPE_BIN, NULL); +} + +/** + * st_bin_set_child: + * @bin: a #StBin + * @child: (nullable): a #ClutterActor, or %NULL + * + * Sets @child as the child of @bin. + * + * If @bin already has a child, the previous child is removed. + */ +void +st_bin_set_child (StBin *bin, + ClutterActor *child) +{ + StBinPrivate *priv; + + g_return_if_fail (ST_IS_BIN (bin)); + g_return_if_fail (child == NULL || CLUTTER_IS_ACTOR (child)); + + priv = st_bin_get_instance_private (bin); + + if (priv->child == child) + return; + + if (child) + { + ClutterActor *parent = clutter_actor_get_parent (child); + + if (parent) + { + g_warning ("%s: The provided 'child' actor %p already has a " + "(different) parent %p and can't be made a child of %p.", + G_STRFUNC, child, parent, bin); + return; + } + } + + if (priv->child) + clutter_actor_remove_child (CLUTTER_ACTOR (bin), priv->child); + + priv->child = NULL; + + if (child) + { + priv->child = child; + clutter_actor_add_child (CLUTTER_ACTOR (bin), child); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (bin)); + + g_object_notify_by_pspec (G_OBJECT (bin), props[PROP_CHILD]); +} + +/** + * st_bin_get_child: + * @bin: a #StBin + * + * Gets the #ClutterActor child for @bin. + * + * Returns: (transfer none) (nullable): a #ClutterActor, or %NULL + */ +ClutterActor * +st_bin_get_child (StBin *bin) +{ + g_return_val_if_fail (ST_IS_BIN (bin), NULL); + + return ((StBinPrivate *)st_bin_get_instance_private (bin))->child; +} diff --git a/src/st/st-bin.h b/src/st/st-bin.h new file mode 100644 index 0000000..7784f45 --- /dev/null +++ b/src/st/st-bin.h @@ -0,0 +1,53 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-bin.h: Basic container actor + * + * Copyright 2009, 2008 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_BIN_H__ +#define __ST_BIN_H__ + +#include <st/st-types.h> +#include <st/st-widget.h> + +G_BEGIN_DECLS + +#define ST_TYPE_BIN (st_bin_get_type ()) +G_DECLARE_DERIVABLE_TYPE (StBin, st_bin, ST, BIN, StWidget) + +/** + * StBinClass: + * + * The #StBinClass struct contains only private data + */ +struct _StBinClass +{ + /*< private >*/ + StWidgetClass parent_class; +}; + +StWidget * st_bin_new (void); +void st_bin_set_child (StBin *bin, + ClutterActor *child); +ClutterActor *st_bin_get_child (StBin *bin); + +G_END_DECLS + +#endif /* __ST_BIN_H__ */ diff --git a/src/st/st-border-image.c b/src/st/st-border-image.c new file mode 100644 index 0000000..ee09d02 --- /dev/null +++ b/src/st/st-border-image.c @@ -0,0 +1,171 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-border-image.c: store information about an image with borders + * + * 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 Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <config.h> + +#include <string.h> + +#include "st-border-image.h" + +struct _StBorderImage { + GObject parent; + + GFile *file; + int border_top; + int border_right; + int border_bottom; + int border_left; + + int scale_factor; +}; + +struct _StBorderImageClass { + GObjectClass parent_class; + +}; + +G_DEFINE_TYPE (StBorderImage, st_border_image, G_TYPE_OBJECT) + +static void +st_border_image_finalize (GObject *object) +{ + StBorderImage *image = ST_BORDER_IMAGE (object); + + g_object_unref (image->file); + + G_OBJECT_CLASS (st_border_image_parent_class)->finalize (object); +} + +static void +st_border_image_class_init (StBorderImageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = st_border_image_finalize; +} + +static void +st_border_image_init (StBorderImage *image) +{ +} + +/** + * st_border_image_new: + * @file: a #GFile + * @border_top: the top border + * @border_right: the right border + * @border_bottom: the bottom border + * @border_left: the left border + * @scale_factor: the scale factor + * + * Creates a new #StBorderImage. + * + * Returns: a new #StBorderImage. + */ +StBorderImage * +st_border_image_new (GFile *file, + int border_top, + int border_right, + int border_bottom, + int border_left, + int scale_factor) +{ + StBorderImage *image; + + image = g_object_new (ST_TYPE_BORDER_IMAGE, NULL); + + image->file = g_object_ref (file); + image->border_top = border_top; + image->border_right = border_right; + image->border_bottom = border_bottom; + image->border_left = border_left; + image->scale_factor = scale_factor; + + return image; +} + +/** + * st_border_image_get_file: + * @image: a #StBorderImage + * + * Get the #GFile for @image. + * + * Returns: (transfer none): a #GFile + */ +GFile * +st_border_image_get_file (StBorderImage *image) +{ + g_return_val_if_fail (ST_IS_BORDER_IMAGE (image), NULL); + + return image->file; +} + +/** + * st_border_image_get_border: + * @image: a #StBorderImage + * @border_top: (out) (optional): the top border + * @border_right: (out) (optional): the right border + * @border_bottom: (out) (optional): the bottom border + * @border_left: (out) (optional): the left border + * + * Get the border widths for @image, taking into account the scale factor + * provided at construction. + */ +void +st_border_image_get_borders (StBorderImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left) +{ + g_return_if_fail (ST_IS_BORDER_IMAGE (image)); + + if (border_top) + *border_top = image->border_top * image->scale_factor; + if (border_right) + *border_right = image->border_right * image->scale_factor; + if (border_bottom) + *border_bottom = image->border_bottom * image->scale_factor; + if (border_left) + *border_left = image->border_left * image->scale_factor; +} + +/** + * st_border_image_equal: + * @image: a #StBorderImage + * @other: a different #StBorderImage + * + * Check if two #StBorderImage objects are identical. + * + * Returns: %TRUE if the two border image objects are identical + */ +gboolean +st_border_image_equal (StBorderImage *image, + StBorderImage *other) +{ + g_return_val_if_fail (ST_IS_BORDER_IMAGE (image), FALSE); + g_return_val_if_fail (ST_IS_BORDER_IMAGE (other), FALSE); + + return (image->border_top == other->border_top && + image->border_right == other->border_right && + image->border_bottom == other->border_bottom && + image->border_left == other->border_left && + g_file_equal (image->file, other->file)); +} diff --git a/src/st/st-border-image.h b/src/st/st-border-image.h new file mode 100644 index 0000000..9c58152 --- /dev/null +++ b/src/st/st-border-image.h @@ -0,0 +1,54 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-border-image.h: store information about an image with borders + * + * 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 Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_BORDER_IMAGE_H__ +#define __ST_BORDER_IMAGE_H__ + +#include <glib-object.h> +#include <gio/gio.h> + +G_BEGIN_DECLS + +/* A StBorderImage encapsulates an image with specified unscaled borders on each edge. + */ + +#define ST_TYPE_BORDER_IMAGE (st_border_image_get_type ()) +G_DECLARE_FINAL_TYPE (StBorderImage, st_border_image, ST, BORDER_IMAGE, GObject) + +StBorderImage *st_border_image_new (GFile *file, + int border_top, + int border_right, + int border_bottom, + int border_left, + int scale_factor); + +GFile *st_border_image_get_file (StBorderImage *image); +void st_border_image_get_borders (StBorderImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left); + +gboolean st_border_image_equal (StBorderImage *image, + StBorderImage *other); + +G_END_DECLS + +#endif /* __ST_BORDER_IMAGE_H__ */ diff --git a/src/st/st-box-layout.c b/src/st/st-box-layout.c new file mode 100644 index 0000000..990fb9e --- /dev/null +++ b/src/st/st-box-layout.c @@ -0,0 +1,307 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-box-layout.h: box layout actor + * + * Copyright 2009 Intel Corporation. + * Copyright 2009 Abderrahim Kitouni + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Florian Muellner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/* Portions copied from Clutter: + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Authored By Matthew Allum <mallum@openedhand.com> + * + * Copyright (C) 2006 OpenedHand + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + */ + +/** + * SECTION:st-box-layout + * @short_description: a layout container arranging children in a single line + * + * The #StBoxLayout arranges its children along a single line, where each + * child can be allocated either its preferred size or larger if the expand + * option is set. If the fill option is set, the actor will be allocated more + * than its requested size. If the fill option is not set, but the expand option + * is enabled, then the position of the actor within the available space can + * be determined by the alignment child property. + * + */ + +#include <stdlib.h> + +#include "st-box-layout.h" + +#include "st-private.h" +#include "st-scrollable.h" + + +enum { + PROP_0, + + PROP_VERTICAL, + PROP_PACK_START, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +struct _StBoxLayoutPrivate +{ + StAdjustment *hadjustment; + StAdjustment *vadjustment; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StBoxLayout, st_box_layout, ST_TYPE_VIEWPORT); + + +static void +st_box_layout_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + ClutterLayoutManager *layout; + ClutterOrientation orientation; + + switch (property_id) + { + case PROP_VERTICAL: + layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (object)); + orientation = clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout)); + g_value_set_boolean (value, orientation == CLUTTER_ORIENTATION_VERTICAL); + break; + + case PROP_PACK_START: + g_value_set_boolean (value, FALSE); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +st_box_layout_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + StBoxLayout *box = ST_BOX_LAYOUT (object); + + switch (property_id) + { + case PROP_VERTICAL: + st_box_layout_set_vertical (box, g_value_get_boolean (value)); + break; + + case PROP_PACK_START: + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +st_box_layout_style_changed (StWidget *self) +{ + StThemeNode *theme_node = st_widget_get_theme_node (self); + ClutterBoxLayout *layout; + double spacing; + + layout = CLUTTER_BOX_LAYOUT (clutter_actor_get_layout_manager (CLUTTER_ACTOR (self))); + + spacing = st_theme_node_get_length (theme_node, "spacing"); + clutter_box_layout_set_spacing (layout, (int)(spacing + 0.5)); + + ST_WIDGET_CLASS (st_box_layout_parent_class)->style_changed (self); +} + +static void +layout_notify (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GObject *self = user_data; + const char *prop_name = g_param_spec_get_name (pspec); + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (self), prop_name)) + g_object_notify (self, prop_name); +} + +static void +on_layout_manager_notify (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + ClutterActor *actor = CLUTTER_ACTOR (object); + ClutterLayoutManager *layout = clutter_actor_get_layout_manager (actor); + + g_warn_if_fail (CLUTTER_IS_BOX_LAYOUT (layout)); + + if (layout == NULL) + return; + + g_signal_connect_swapped (layout, "layout-changed", + G_CALLBACK (clutter_actor_queue_relayout), actor); + g_signal_connect (layout, "notify", G_CALLBACK (layout_notify), object); +} + +static void +st_box_layout_class_init (StBoxLayoutClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + object_class->get_property = st_box_layout_get_property; + object_class->set_property = st_box_layout_set_property; + + widget_class->style_changed = st_box_layout_style_changed; + + /** + * StBoxLayout:vertical: + * + * A convenience property for the #ClutterBoxLayout:vertical property of the + * internal layout for #StBoxLayout. + */ + props[PROP_VERTICAL] = + g_param_spec_boolean ("vertical", + "Vertical", + "Whether the layout should be vertical, rather" + "than horizontal", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StBoxLayout:pack-start: + * + * A convenience property for the #ClutterBoxLayout:pack-start property of the + * internal layout for #StBoxLayout. + */ + props[PROP_PACK_START] = + g_param_spec_boolean ("pack-start", + "Pack Start", + "Whether to pack items at the start of the box", + FALSE, + ST_PARAM_READWRITE | G_PARAM_DEPRECATED); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +st_box_layout_init (StBoxLayout *self) +{ + self->priv = st_box_layout_get_instance_private (self); + + g_signal_connect (self, "notify::layout-manager", + G_CALLBACK (on_layout_manager_notify), NULL); + clutter_actor_set_layout_manager (CLUTTER_ACTOR (self), clutter_box_layout_new ()); +} + +/** + * st_box_layout_new: + * + * Create a new #StBoxLayout. + * + * Returns: a newly allocated #StBoxLayout + */ +StWidget * +st_box_layout_new (void) +{ + return g_object_new (ST_TYPE_BOX_LAYOUT, NULL); +} + +/** + * st_box_layout_set_vertical: + * @box: A #StBoxLayout + * @vertical: %TRUE if the layout should be vertical + * + * Set the value of the #StBoxLayout:vertical property + */ +void +st_box_layout_set_vertical (StBoxLayout *box, + gboolean vertical) +{ + ClutterLayoutManager *layout; + ClutterOrientation orientation; + + g_return_if_fail (ST_IS_BOX_LAYOUT (box)); + + layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (box)); + orientation = vertical ? CLUTTER_ORIENTATION_VERTICAL + : CLUTTER_ORIENTATION_HORIZONTAL; + + if (clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout)) != orientation) + { + clutter_box_layout_set_orientation (CLUTTER_BOX_LAYOUT (layout), orientation); + g_object_notify_by_pspec (G_OBJECT (box), props[PROP_VERTICAL]); + } +} + +/** + * st_box_layout_get_vertical: + * @box: A #StBoxLayout + * + * Get the value of the #StBoxLayout:vertical property. + * + * Returns: %TRUE if the layout is vertical + */ +gboolean +st_box_layout_get_vertical (StBoxLayout *box) +{ + ClutterLayoutManager *layout; + ClutterOrientation orientation; + + g_return_val_if_fail (ST_IS_BOX_LAYOUT (box), FALSE); + + layout = clutter_actor_get_layout_manager (CLUTTER_ACTOR (box)); + orientation = clutter_box_layout_get_orientation (CLUTTER_BOX_LAYOUT (layout)); + return orientation == CLUTTER_ORIENTATION_VERTICAL; +} + +/** + * st_box_layout_set_pack_start: + * @box: A #StBoxLayout + * @pack_start: %TRUE if the layout should use pack-start + * + * Deprecated: No longer has any effect + */ +void +st_box_layout_set_pack_start (StBoxLayout *box, + gboolean pack_start) +{ +} + +/** + * st_box_layout_get_pack_start: + * @box: A #StBoxLayout + * + * Returns: the value of the #StBoxLayout:pack-start property, + * always %FALSE + */ +gboolean +st_box_layout_get_pack_start (StBoxLayout *box) +{ + return FALSE; +} diff --git a/src/st/st-box-layout.h b/src/st/st-box-layout.h new file mode 100644 index 0000000..82f5a70 --- /dev/null +++ b/src/st/st-box-layout.h @@ -0,0 +1,65 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-box-layout.h: box layout actor + * + * Copyright 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef _ST_BOX_LAYOUT_H +#define _ST_BOX_LAYOUT_H + +#include <st/st-widget.h> +#include <st/st-viewport.h> + +G_BEGIN_DECLS + +#define ST_TYPE_BOX_LAYOUT st_box_layout_get_type() +G_DECLARE_FINAL_TYPE (StBoxLayout, st_box_layout, ST, BOX_LAYOUT, StViewport) + +typedef struct _StBoxLayout StBoxLayout; +typedef struct _StBoxLayoutPrivate StBoxLayoutPrivate; + +/** + * StBoxLayout: + * + * The contents of this structure are private and should only be accessed + * through the public API. + */ +struct _StBoxLayout +{ + /*< private >*/ + StViewport parent; + + StBoxLayoutPrivate *priv; +}; + +StWidget *st_box_layout_new (void); + +void st_box_layout_set_vertical (StBoxLayout *box, + gboolean vertical); +gboolean st_box_layout_get_vertical (StBoxLayout *box); + +void st_box_layout_set_pack_start (StBoxLayout *box, + gboolean pack_start); +gboolean st_box_layout_get_pack_start (StBoxLayout *box); + +G_END_DECLS + +#endif /* _ST_BOX_LAYOUT_H */ diff --git a/src/st/st-button.c b/src/st/st-button.c new file mode 100644 index 0000000..148197c --- /dev/null +++ b/src/st/st-button.c @@ -0,0 +1,1043 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-button.c: Plain button actor + * + * Copyright 2007 OpenedHand + * Copyright 2008, 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-button + * @short_description: Button widget + * + * A button widget with support for either a text label or icon, toggle mode + * and transitions effects between states. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <string.h> + +#include <glib.h> + +#include <clutter/clutter.h> + +#include "st-button.h" + +#include "st-icon.h" +#include "st-enum-types.h" +#include "st-texture-cache.h" +#include "st-private.h" + +#include <st/st-widget-accessible.h> + +enum +{ + PROP_0, + + PROP_LABEL, + PROP_ICON_NAME, + PROP_BUTTON_MASK, + PROP_TOGGLE_MODE, + PROP_CHECKED, + PROP_PRESSED, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum +{ + CLICKED, + + LAST_SIGNAL +}; + +typedef struct _StButtonPrivate StButtonPrivate; + +struct _StButtonPrivate +{ + gchar *text; + + ClutterInputDevice *device; + ClutterEventSequence *press_sequence; + ClutterGrab *grab; + + guint button_mask : 3; + guint is_toggle : 1; + + guint pressed : 3; + guint grabbed : 3; + guint is_checked : 1; +}; + +static guint button_signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE_WITH_PRIVATE (StButton, st_button, ST_TYPE_BIN); + +static GType st_button_accessible_get_type (void) G_GNUC_CONST; + +static void +st_button_update_label_style (StButton *button) +{ + ClutterActor *label; + + label = st_bin_get_child (ST_BIN (button)); + + /* check the child is really a label */ + if (!CLUTTER_IS_TEXT (label)) + return; + + _st_set_text_from_style (CLUTTER_TEXT (label), st_widget_get_theme_node (ST_WIDGET (button))); +} + +static void +st_button_style_changed (StWidget *widget) +{ + StButton *button = ST_BUTTON (widget); + StButtonClass *button_class = ST_BUTTON_GET_CLASS (button); + + ST_WIDGET_CLASS (st_button_parent_class)->style_changed (widget); + + /* update the label styling */ + st_button_update_label_style (button); + + /* run a transition if applicable */ + if (button_class->transition) + { + button_class->transition (button); + } +} + +static void +st_button_press (StButton *button, + ClutterInputDevice *device, + StButtonMask mask, + ClutterEventSequence *sequence) +{ + StButtonPrivate *priv = st_button_get_instance_private (button); + gboolean active_changed = priv->pressed == 0 || sequence; + + if (active_changed) + st_widget_add_style_pseudo_class (ST_WIDGET (button), "active"); + + priv->pressed |= mask; + priv->press_sequence = sequence; + priv->device = device; + + if (active_changed) + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_PRESSED]); +} + +static void +st_button_release (StButton *button, + ClutterInputDevice *device, + StButtonMask mask, + int clicked_button, + ClutterEventSequence *sequence) +{ + StButtonPrivate *priv = st_button_get_instance_private (button); + + if ((device && priv->device != device) || + (sequence && priv->press_sequence != sequence)) + return; + else if (!sequence) + { + priv->pressed &= ~mask; + + if (priv->pressed != 0) + return; + } + + priv->press_sequence = NULL; + priv->device = NULL; + st_widget_remove_style_pseudo_class (ST_WIDGET (button), "active"); + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_PRESSED]); + + if (clicked_button || sequence) + { + if (priv->is_toggle) + st_button_set_checked (button, !priv->is_checked); + + g_signal_emit (button, button_signals[CLICKED], 0, clicked_button); + } +} + +static gboolean +st_button_button_press (ClutterActor *actor, + ClutterButtonEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + StButtonMask mask = ST_BUTTON_MASK_FROM_BUTTON (event->button); + ClutterInputDevice *device = clutter_event_get_device ((ClutterEvent*) event); + + if (priv->press_sequence) + return CLUTTER_EVENT_PROPAGATE; + + if (priv->button_mask & mask) + { + ClutterActor *stage; + + stage = clutter_actor_get_stage (actor); + + if (priv->grabbed == 0) + priv->grab = clutter_stage_grab (CLUTTER_STAGE (stage), actor); + + priv->grabbed |= mask; + st_button_press (button, device, mask, NULL); + + return TRUE; + } + + return FALSE; +} + +static gboolean +st_button_button_release (ClutterActor *actor, + ClutterButtonEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + StButtonMask mask = ST_BUTTON_MASK_FROM_BUTTON (event->button); + ClutterInputDevice *device = clutter_event_get_device ((ClutterEvent*) event); + + if (priv->button_mask & mask) + { + ClutterStage *stage; + ClutterActor *target; + gboolean is_click; + + stage = clutter_event_get_stage ((ClutterEvent *) event); + target = clutter_stage_get_event_actor (stage, (ClutterEvent *) event); + + is_click = priv->grabbed && clutter_actor_contains (actor, target); + st_button_release (button, device, mask, is_click ? event->button : 0, NULL); + + priv->grabbed &= ~mask; + if (priv->grab && priv->grabbed == 0) + { + clutter_grab_dismiss (priv->grab); + g_clear_pointer (&priv->grab, clutter_grab_unref); + } + + return TRUE; + } + + return FALSE; +} + +static gboolean +st_button_touch_event (ClutterActor *actor, + ClutterTouchEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + StButtonMask mask = ST_BUTTON_MASK_FROM_BUTTON (1); + ClutterEventSequence *sequence; + ClutterInputDevice *device; + + if (priv->pressed != 0) + return CLUTTER_EVENT_PROPAGATE; + if ((priv->button_mask & mask) == 0) + return CLUTTER_EVENT_PROPAGATE; + + device = clutter_event_get_device ((ClutterEvent*) event); + sequence = clutter_event_get_event_sequence ((ClutterEvent*) event); + + if (event->type == CLUTTER_TOUCH_BEGIN && !priv->grab && !priv->press_sequence) + { + st_button_press (button, device, 0, sequence); + return CLUTTER_EVENT_STOP; + } + else if (event->type == CLUTTER_TOUCH_END && + priv->device == device && + priv->press_sequence == sequence) + { + st_button_release (button, device, mask, 0, sequence); + return CLUTTER_EVENT_STOP; + } + else if (event->type == CLUTTER_TOUCH_CANCEL) + { + st_button_fake_release (button); + } + + return CLUTTER_EVENT_PROPAGATE; +} + +static gboolean +st_button_key_press (ClutterActor *actor, + ClutterKeyEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + + if (priv->button_mask & ST_BUTTON_ONE) + { + if (event->keyval == CLUTTER_KEY_space || + event->keyval == CLUTTER_KEY_Return || + event->keyval == CLUTTER_KEY_KP_Enter || + event->keyval == CLUTTER_KEY_ISO_Enter) + { + st_button_press (button, NULL, ST_BUTTON_ONE, NULL); + return TRUE; + } + } + + return CLUTTER_ACTOR_CLASS (st_button_parent_class)->key_press_event (actor, event); +} + +static gboolean +st_button_key_release (ClutterActor *actor, + ClutterKeyEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + + if (priv->button_mask & ST_BUTTON_ONE) + { + if (event->keyval == CLUTTER_KEY_space || + event->keyval == CLUTTER_KEY_Return || + event->keyval == CLUTTER_KEY_KP_Enter || + event->keyval == CLUTTER_KEY_ISO_Enter) + { + gboolean is_click; + + is_click = (priv->pressed & ST_BUTTON_ONE); + st_button_release (button, NULL, ST_BUTTON_ONE, is_click ? 1 : 0, NULL); + return TRUE; + } + } + + return FALSE; +} + +static void +st_button_key_focus_out (ClutterActor *actor) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + + /* If we lose focus between a key press and release, undo the press */ + if ((priv->pressed & ST_BUTTON_ONE) && + !(priv->grabbed & ST_BUTTON_ONE)) + st_button_release (button, NULL, ST_BUTTON_ONE, 0, NULL); + + CLUTTER_ACTOR_CLASS (st_button_parent_class)->key_focus_out (actor); +} + +static gboolean +st_button_enter (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + gboolean ret; + + ret = CLUTTER_ACTOR_CLASS (st_button_parent_class)->enter_event (actor, event); + + if (priv->grabbed) + { + if (st_widget_get_hover (ST_WIDGET (button))) + st_button_press (button, priv->device, + priv->grabbed, NULL); + else + st_button_release (button, priv->device, + priv->grabbed, 0, NULL); + } + + return ret; +} + +static gboolean +st_button_leave (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + StButton *button = ST_BUTTON (actor); + StButtonPrivate *priv = st_button_get_instance_private (button); + gboolean ret; + + ret = CLUTTER_ACTOR_CLASS (st_button_parent_class)->leave_event (actor, event); + + if (priv->grabbed) + { + if (st_widget_get_hover (ST_WIDGET (button))) + st_button_press (button, priv->device, + priv->grabbed, NULL); + else + st_button_release (button, priv->device, + priv->grabbed, 0, NULL); + } + + return ret; +} + +static void +st_button_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StButton *button = ST_BUTTON (gobject); + + switch (prop_id) + { + case PROP_LABEL: + st_button_set_label (button, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + st_button_set_icon_name (button, g_value_get_string (value)); + break; + case PROP_BUTTON_MASK: + st_button_set_button_mask (button, g_value_get_flags (value)); + break; + case PROP_TOGGLE_MODE: + st_button_set_toggle_mode (button, g_value_get_boolean (value)); + break; + case PROP_CHECKED: + st_button_set_checked (button, g_value_get_boolean (value)); + break; + + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_button_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StButtonPrivate *priv = st_button_get_instance_private (ST_BUTTON (gobject)); + + switch (prop_id) + { + case PROP_LABEL: + g_value_set_string (value, priv->text); + break; + case PROP_ICON_NAME: + g_value_set_string (value, st_button_get_icon_name (ST_BUTTON (gobject))); + break; + case PROP_BUTTON_MASK: + g_value_set_flags (value, priv->button_mask); + break; + case PROP_TOGGLE_MODE: + g_value_set_boolean (value, priv->is_toggle); + break; + case PROP_CHECKED: + g_value_set_boolean (value, priv->is_checked); + break; + case PROP_PRESSED: + g_value_set_boolean (value, priv->pressed != 0 || priv->press_sequence != NULL); + break; + + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_button_finalize (GObject *gobject) +{ + StButtonPrivate *priv = st_button_get_instance_private (ST_BUTTON (gobject)); + + g_free (priv->text); + + G_OBJECT_CLASS (st_button_parent_class)->finalize (gobject); +} + +static void +st_button_class_init (StButtonClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + gobject_class->set_property = st_button_set_property; + gobject_class->get_property = st_button_get_property; + gobject_class->finalize = st_button_finalize; + + actor_class->button_press_event = st_button_button_press; + actor_class->button_release_event = st_button_button_release; + actor_class->key_press_event = st_button_key_press; + actor_class->key_release_event = st_button_key_release; + actor_class->key_focus_out = st_button_key_focus_out; + actor_class->enter_event = st_button_enter; + actor_class->leave_event = st_button_leave; + actor_class->touch_event = st_button_touch_event; + + widget_class->style_changed = st_button_style_changed; + widget_class->get_accessible_type = st_button_accessible_get_type; + + /** + * StButton:label: + * + * The label of the #StButton. + */ + props[PROP_LABEL] = + g_param_spec_string ("label", + "Label", + "Label of the button", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StButton:icon-name: + * + * The icon name of the #StButton. + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon name", + "Icon name of the button", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StButton:button-mask: + * + * Which buttons will trigger the #StButton::clicked signal. + */ + props[PROP_BUTTON_MASK] = + g_param_spec_flags ("button-mask", + "Button mask", + "Which buttons trigger the 'clicked' signal", + ST_TYPE_BUTTON_MASK, ST_BUTTON_ONE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StButton:toggle-mode: + * + * Whether the #StButton is operating in toggle mode (on/off). + */ + props[PROP_TOGGLE_MODE] = + g_param_spec_boolean ("toggle-mode", + "Toggle Mode", + "Enable or disable toggling", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StButton:checked: + * + * If #StButton:toggle-mode is %TRUE, indicates if the #StButton is toggled + * "on" or "off". + * + * When the value is %TRUE, the #StButton will have the `checked` CSS + * pseudo-class set. + */ + props[PROP_CHECKED] = + g_param_spec_boolean ("checked", + "Checked", + "Indicates if a toggle button is \"on\" or \"off\"", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StButton:pressed: + * + * In contrast to #StButton:checked, this property indicates whether the + * #StButton is being actively pressed, rather than just in the "on" state. + */ + props[PROP_PRESSED] = + g_param_spec_boolean ("pressed", + "Pressed", + "Indicates if the button is pressed in", + FALSE, + ST_PARAM_READABLE); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + + /** + * StButton::clicked: + * @button: the object that received the signal + * @clicked_button: the mouse button that was used + * + * Emitted when the user activates the button, either with a mouse press and + * release or with the keyboard. + */ + button_signals[CLICKED] = + g_signal_new ("clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StButtonClass, clicked), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + G_TYPE_INT); +} + +static void +st_button_init (StButton *button) +{ + StButtonPrivate *priv = st_button_get_instance_private (button); + + priv->button_mask = ST_BUTTON_ONE; + + clutter_actor_set_reactive (CLUTTER_ACTOR (button), TRUE); + st_widget_set_track_hover (ST_WIDGET (button), TRUE); +} + +/** + * st_button_new: + * + * Create a new button + * + * Returns: a new #StButton + */ +StWidget * +st_button_new (void) +{ + return g_object_new (ST_TYPE_BUTTON, NULL); +} + +/** + * st_button_new_with_label: + * @text: text to set the label to + * + * Create a new #StButton with the specified label + * + * Returns: a new #StButton + */ +StWidget * +st_button_new_with_label (const gchar *text) +{ + return g_object_new (ST_TYPE_BUTTON, "label", text, NULL); +} + +/** + * st_button_get_label: + * @button: a #StButton + * + * Get the text displayed on the button. If the label is empty, an empty string + * will be returned instead of %NULL. + * + * Returns: (transfer none): the text for the button + */ +const gchar * +st_button_get_label (StButton *button) +{ + g_return_val_if_fail (ST_IS_BUTTON (button), NULL); + + return ((StButtonPrivate *)st_button_get_instance_private (button))->text; +} + +/** + * st_button_set_label: + * @button: a #Stbutton + * @text: (nullable): text to set the label to + * + * Sets the text displayed on the button. + */ +void +st_button_set_label (StButton *button, + const gchar *text) +{ + StButtonPrivate *priv; + ClutterActor *label; + + g_return_if_fail (ST_IS_BUTTON (button)); + + priv = st_button_get_instance_private (button); + + if (g_strcmp0 (priv->text, text) == 0) + return; + + g_free (priv->text); + + if (text) + priv->text = g_strdup (text); + else + priv->text = g_strdup (""); + + label = st_bin_get_child (ST_BIN (button)); + + if (label && CLUTTER_IS_TEXT (label)) + { + clutter_text_set_text (CLUTTER_TEXT (label), priv->text); + } + else + { + label = g_object_new (CLUTTER_TYPE_TEXT, + "text", priv->text, + "line-alignment", PANGO_ALIGN_CENTER, + "ellipsize", PANGO_ELLIPSIZE_END, + "use-markup", TRUE, + "x-align", CLUTTER_ACTOR_ALIGN_CENTER, + "y-align", CLUTTER_ACTOR_ALIGN_CENTER, + NULL); + st_bin_set_child (ST_BIN (button), label); + } + + /* Fake a style change so that we reset the style properties on the label */ + st_widget_style_changed (ST_WIDGET (button)); + + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_LABEL]); +} + +/** + * st_button_get_icon_name: + * @button: a #StButton + * + * Get the icon name of the button. If the button isn't showing an icon, + * the return value will be %NULL. + * + * Returns: (transfer none) (nullable): the icon name of the button + */ +const char * +st_button_get_icon_name (StButton *button) +{ + ClutterActor *icon; + + g_return_val_if_fail (ST_IS_BUTTON (button), NULL); + + icon = st_bin_get_child (ST_BIN (button)); + if (ST_IS_ICON (icon)) + return st_icon_get_icon_name (ST_ICON (icon)); + return NULL; +} + +/** + * st_button_set_icon_name: + * @button: a #Stbutton + * @icon_name: an icon name + * + * Adds an `StIcon` with the given icon name as a child. + * + * If @button already contains a child actor, that child will + * be removed and replaced with the icon. + */ +void +st_button_set_icon_name (StButton *button, + const char *icon_name) +{ + ClutterActor *icon; + + g_return_if_fail (ST_IS_BUTTON (button)); + g_return_if_fail (icon_name != NULL); + + icon = st_bin_get_child (ST_BIN (button)); + + if (ST_IS_ICON (icon)) + { + if (g_strcmp0 (st_icon_get_icon_name (ST_ICON (icon)), icon_name) == 0) + return; + + st_icon_set_icon_name (ST_ICON (icon), icon_name); + } + else + { + icon = g_object_new (ST_TYPE_ICON, + "icon-name", icon_name, + "x-align", CLUTTER_ACTOR_ALIGN_CENTER, + "y-align", CLUTTER_ACTOR_ALIGN_CENTER, + NULL); + st_bin_set_child (ST_BIN (button), icon); + } + + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_ICON_NAME]); +} + +/** + * st_button_get_button_mask: + * @button: a #StButton + * + * Gets the mask of mouse buttons that @button emits the + * #StButton::clicked signal for. + * + * Returns: the mask of mouse buttons that @button emits the + * #StButton::clicked signal for. + */ +StButtonMask +st_button_get_button_mask (StButton *button) +{ + g_return_val_if_fail (ST_IS_BUTTON (button), 0); + + return ((StButtonPrivate *)st_button_get_instance_private (button))->button_mask; +} + +/** + * st_button_set_button_mask: + * @button: a #Stbutton + * @mask: the mask of mouse buttons that @button responds to + * + * Sets which mouse buttons @button emits #StButton::clicked for. + */ +void +st_button_set_button_mask (StButton *button, + StButtonMask mask) +{ + StButtonPrivate *priv; + + g_return_if_fail (ST_IS_BUTTON (button)); + + priv = st_button_get_instance_private (button); + + if (priv->button_mask == mask) + return; + + priv->button_mask = mask; + + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_BUTTON_MASK]); +} + +/** + * st_button_get_toggle_mode: + * @button: a #StButton + * + * Get the toggle mode status of the button. + * + * Returns: %TRUE if toggle mode is set, otherwise %FALSE + */ +gboolean +st_button_get_toggle_mode (StButton *button) +{ + g_return_val_if_fail (ST_IS_BUTTON (button), FALSE); + + return ((StButtonPrivate *)st_button_get_instance_private (button))->is_toggle; +} + +/** + * st_button_set_toggle_mode: + * @button: a #Stbutton + * @toggle: %TRUE or %FALSE + * + * Enables or disables toggle mode for the button. In toggle mode, the active + * state will be "toggled" when the user clicks the button. + */ +void +st_button_set_toggle_mode (StButton *button, + gboolean toggle) +{ + StButtonPrivate *priv; + + g_return_if_fail (ST_IS_BUTTON (button)); + + priv = st_button_get_instance_private (button); + + if (priv->is_toggle == toggle) + return; + + priv->is_toggle = toggle; + + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_TOGGLE_MODE]); +} + +/** + * st_button_get_checked: + * @button: a #StButton + * + * Get the #StButton:checked property of a #StButton that is in toggle mode. + * + * Returns: %TRUE if the button is checked, or %FALSE if not + */ +gboolean +st_button_get_checked (StButton *button) +{ + g_return_val_if_fail (ST_IS_BUTTON (button), FALSE); + + return ((StButtonPrivate *)st_button_get_instance_private (button))->is_checked; +} + +/** + * st_button_set_checked: + * @button: a #Stbutton + * @checked: %TRUE or %FALSE + * + * Set the #StButton:checked property of the button. This is only really useful + * if the button has #StButton:toggle-mode property set to %TRUE. + */ +void +st_button_set_checked (StButton *button, + gboolean checked) +{ + StButtonPrivate *priv; + + g_return_if_fail (ST_IS_BUTTON (button)); + + priv = st_button_get_instance_private (button); + if (priv->is_checked == checked) + return; + + priv->is_checked = checked; + + if (checked) + st_widget_add_style_pseudo_class (ST_WIDGET (button), "checked"); + else + st_widget_remove_style_pseudo_class (ST_WIDGET (button), "checked"); + + g_object_notify_by_pspec (G_OBJECT (button), props[PROP_CHECKED]); +} + +/** + * st_button_fake_release: + * @button: an #StButton + * + * If this widget is holding a pointer grab, this function will + * will ungrab it, and reset the #StButton:pressed state. The effect is + * similar to if the user had released the mouse button, but without + * emitting the #StButton::clicked signal. + * + * This function is useful if for example you want to do something + * after the user is holding the mouse button for a given period of + * time, breaking the grab. + */ +void +st_button_fake_release (StButton *button) +{ + StButtonPrivate *priv; + + g_return_if_fail (ST_IS_BUTTON (button)); + + priv = st_button_get_instance_private (button); + + if (priv->grab) + { + clutter_grab_dismiss (priv->grab); + g_clear_pointer (&priv->grab, clutter_grab_unref); + } + + priv->grabbed = 0; + + if (priv->pressed || priv->press_sequence) + st_button_release (button, priv->device, + priv->pressed, 0, NULL); +} + +/******************************************************************************/ +/*************************** ACCESSIBILITY SUPPORT ****************************/ +/******************************************************************************/ + +#define ST_TYPE_BUTTON_ACCESSIBLE st_button_accessible_get_type () + +#define ST_BUTTON_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + ST_TYPE_BUTTON_ACCESSIBLE, StButtonAccessible)) + +#define ST_IS_BUTTON_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + ST_TYPE_BUTTON_ACCESSIBLE)) + +#define ST_BUTTON_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + ST_TYPE_BUTTON_ACCESSIBLE, StButtonAccessibleClass)) + +#define ST_IS_BUTTON_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + ST_TYPE_BUTTON_ACCESSIBLE)) + +#define ST_BUTTON_ACCESSIBLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + ST_TYPE_BUTTON_ACCESSIBLE, StButtonAccessibleClass)) + +typedef struct _StButtonAccessible StButtonAccessible; +typedef struct _StButtonAccessibleClass StButtonAccessibleClass; + +struct _StButtonAccessible +{ + StWidgetAccessible parent; +}; + +struct _StButtonAccessibleClass +{ + StWidgetAccessibleClass parent_class; +}; + +/* AtkObject */ +static void st_button_accessible_initialize (AtkObject *obj, + gpointer data); + +G_DEFINE_TYPE (StButtonAccessible, st_button_accessible, ST_TYPE_WIDGET_ACCESSIBLE) + +static const gchar * +st_button_accessible_get_name (AtkObject *obj) +{ + StButton *button = NULL; + const gchar *name = NULL; + + button = ST_BUTTON (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (button == NULL) + return NULL; + + name = ATK_OBJECT_CLASS (st_button_accessible_parent_class)->get_name (obj); + if (name != NULL) + return name; + + return st_button_get_label (button); +} + +static void +st_button_accessible_class_init (StButtonAccessibleClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass); + + atk_class->initialize = st_button_accessible_initialize; + atk_class->get_name = st_button_accessible_get_name; +} + +static void +st_button_accessible_init (StButtonAccessible *self) +{ + /* initialization done on AtkObject->initialize */ +} + +static void +st_button_accessible_notify_label_cb (StButton *button, + GParamSpec *psec, + AtkObject *accessible) +{ + g_object_notify (G_OBJECT (accessible), "accessible-name"); +} + +static void +st_button_accessible_compute_role (AtkObject *accessible, + StButton *button) +{ + atk_object_set_role (accessible, st_button_get_toggle_mode (button) + ? ATK_ROLE_TOGGLE_BUTTON : ATK_ROLE_PUSH_BUTTON); +} + +static void +st_button_accessible_notify_toggle_mode_cb (StButton *button, + GParamSpec *psec, + AtkObject *accessible) +{ + st_button_accessible_compute_role (accessible, button); +} + +static void +st_button_accessible_initialize (AtkObject *obj, + gpointer data) +{ + ATK_OBJECT_CLASS (st_button_accessible_parent_class)->initialize (obj, data); + + st_button_accessible_compute_role (obj, ST_BUTTON (data)); + + g_signal_connect (data, "notify::label", + G_CALLBACK (st_button_accessible_notify_label_cb), obj); + g_signal_connect (data, "notify::toggle-mode", + G_CALLBACK (st_button_accessible_notify_toggle_mode_cb), obj); +} diff --git a/src/st/st-button.h b/src/st/st-button.h new file mode 100644 index 0000000..3f7dbb0 --- /dev/null +++ b/src/st/st-button.h @@ -0,0 +1,85 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-button.h: Plain button actor + * + * Copyright 2007 OpenedHand + * Copyright 2008, 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_BUTTON_H__ +#define __ST_BUTTON_H__ + +G_BEGIN_DECLS + +#include <st/st-bin.h> + +#define ST_TYPE_BUTTON (st_button_get_type ()) +G_DECLARE_DERIVABLE_TYPE (StButton, st_button, ST, BUTTON, StBin) + +struct _StButtonClass +{ + StBinClass parent_class; + + /* vfuncs, not signals */ + void (* transition) (StButton *button); + + /* signals */ + void (* clicked) (StButton *button, int clicked_button); +}; + +StWidget *st_button_new (void); +StWidget *st_button_new_with_label (const gchar *text); +const gchar *st_button_get_label (StButton *button); +void st_button_set_label (StButton *button, + const gchar *text); +const char *st_button_get_icon_name (StButton *button); +void st_button_set_icon_name (StButton *button, + const char *icon_name); +void st_button_set_toggle_mode (StButton *button, + gboolean toggle); +gboolean st_button_get_toggle_mode (StButton *button); +void st_button_set_checked (StButton *button, + gboolean checked); +gboolean st_button_get_checked (StButton *button); + +void st_button_fake_release (StButton *button); + +/** + * StButtonMask: + * @ST_BUTTON_ONE: button 1 (left) + * @ST_BUTTON_TWO: button 2 (middle) + * @ST_BUTTON_THREE: button 3 (right) + * + * A mask representing which mouse buttons an #StButton responds to. + */ +typedef enum { + ST_BUTTON_ONE = (1 << 0), + ST_BUTTON_TWO = (1 << 1), + ST_BUTTON_THREE = (1 << 2), +} StButtonMask; + +#define ST_BUTTON_MASK_FROM_BUTTON(button) (1 << ((button) - 1)) + +void st_button_set_button_mask (StButton *button, + StButtonMask mask); +StButtonMask st_button_get_button_mask (StButton *button); + +G_END_DECLS + +#endif /* __ST_BUTTON_H__ */ diff --git a/src/st/st-clipboard.c b/src/st/st-clipboard.c new file mode 100644 index 0000000..4b730c9 --- /dev/null +++ b/src/st/st-clipboard.c @@ -0,0 +1,348 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-clipboard.c: clipboard object + * + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-clipboard + * @short_description: a simple representation of the clipboard + * + * #StCliboard is a very simple object representation of the clipboard + * available to applications. Text is always assumed to be UTF-8 and non-text + * items are not handled. + */ + +#include "config.h" + +#include "st-clipboard.h" + +#include <meta/display.h> +#include <meta/meta-selection-source-memory.h> +#include <meta/meta-selection.h> + +G_DEFINE_TYPE (StClipboard, st_clipboard, G_TYPE_OBJECT) + +typedef struct _TransferData TransferData; +struct _TransferData +{ + StClipboard *clipboard; + GCallback callback; + gpointer user_data; + GOutputStream *stream; +}; + +const char *supported_mimetypes[] = { + "text/plain;charset=utf-8", + "UTF8_STRING", + "text/plain", + "STRING", +}; + +static MetaSelection *meta_selection = NULL; + +static void +st_clipboard_class_init (StClipboardClass *klass) +{ +} + +static void +st_clipboard_init (StClipboard *self) +{ +} + +/** + * st_clipboard_get_default: + * + * Get the global #StClipboard object that represents the clipboard. + * + * Returns: (transfer none): a #StClipboard owned by St and must not be + * unrefferenced or freed. + */ +StClipboard* +st_clipboard_get_default (void) +{ + static StClipboard *default_clipboard = NULL; + + if (!default_clipboard) + { + default_clipboard = g_object_new (ST_TYPE_CLIPBOARD, NULL); + } + + return default_clipboard; +} + +static gboolean +convert_type (StClipboardType type, + MetaSelectionType *type_out) +{ + if (type == ST_CLIPBOARD_TYPE_PRIMARY) + *type_out = META_SELECTION_PRIMARY; + else if (type == ST_CLIPBOARD_TYPE_CLIPBOARD) + *type_out = META_SELECTION_CLIPBOARD; + else + return FALSE; + + return TRUE; +} + +static const char * +pick_mimetype (MetaSelection *meta_selection, + MetaSelectionType selection_type) +{ + const char *selected_mimetype = NULL; + GList *mimetypes; + int i; + + mimetypes = meta_selection_get_mimetypes (meta_selection, selection_type); + + for (i = 0; i < G_N_ELEMENTS (supported_mimetypes); i++) + { + if (g_list_find_custom (mimetypes, supported_mimetypes[i], + (GCompareFunc) g_strcmp0)) + { + selected_mimetype = supported_mimetypes[i]; + break; + } + } + + g_list_free_full (mimetypes, g_free); + return selected_mimetype; +} + +static void +transfer_cb (MetaSelection *selection, + GAsyncResult *res, + TransferData *data) +{ + gchar *text = NULL; + + if (meta_selection_transfer_finish (selection, res, NULL)) + { + gsize data_size; + + data_size = + g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM (data->stream)); + text = g_new0 (char, data_size + 1); + memcpy (text, g_memory_output_stream_get_data (G_MEMORY_OUTPUT_STREAM (data->stream)), data_size); + } + + ((StClipboardCallbackFunc) data->callback) (data->clipboard, text, + data->user_data); + g_object_unref (data->stream); + g_free (data); + g_free (text); +} + +static void +transfer_bytes_cb (MetaSelection *selection, + GAsyncResult *res, + TransferData *data) +{ + GBytes *bytes = NULL; + + if (meta_selection_transfer_finish (selection, res, NULL)) + bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (data->stream)); + + ((StClipboardContentCallbackFunc) data->callback) (data->clipboard, bytes, + data->user_data); + g_object_unref (data->stream); + g_clear_pointer (&bytes, g_bytes_unref); +} + +/** + * st_clipboard_get_mimetypes: + * @clipboard: a #StClipboard + * + * Gets a list of the mimetypes supported by the default #StClipboard. + * + * Returns: (element-type utf8) (transfer full): the supported mimetypes + */ +GList * +st_clipboard_get_mimetypes (StClipboard *clipboard, + StClipboardType type) +{ + MetaSelectionType selection_type; + + g_return_val_if_fail (ST_IS_CLIPBOARD (clipboard), NULL); + g_return_val_if_fail (meta_selection != NULL, NULL); + + if (!convert_type (type, &selection_type)) + return NULL; + + return meta_selection_get_mimetypes (meta_selection, selection_type); +} + +/** + * st_clipboard_get_text: + * @clipboard: A #StCliboard + * @type: The type of clipboard data you want + * @callback: (scope async): function to be called when the text is retrieved + * @user_data: data to be passed to the callback + * + * Request the data from the clipboard in text form. @callback is executed + * when the data is retrieved. + */ +void +st_clipboard_get_text (StClipboard *clipboard, + StClipboardType type, + StClipboardCallbackFunc callback, + gpointer user_data) +{ + MetaSelectionType selection_type; + TransferData *data; + const char *mimetype = NULL; + + g_return_if_fail (ST_IS_CLIPBOARD (clipboard)); + g_return_if_fail (meta_selection != NULL); + g_return_if_fail (callback != NULL); + + if (convert_type (type, &selection_type)) + mimetype = pick_mimetype (meta_selection, selection_type); + + if (!mimetype) + { + callback (clipboard, NULL, user_data); + return; + } + + data = g_new0 (TransferData, 1); + data->clipboard = clipboard; + data->callback = G_CALLBACK (callback); + data->user_data = user_data; + data->stream = g_memory_output_stream_new_resizable (); + + meta_selection_transfer_async (meta_selection, + selection_type, + mimetype, -1, + data->stream, NULL, + (GAsyncReadyCallback) transfer_cb, + data); +} + +/** + * st_clipboard_get_content: + * @clipboard: A #StCliboard + * @type: The type of clipboard data you want + * @mimetype: The mimetype to get content for + * @callback: (scope async): function to be called when the type is retrieved + * @user_data: data to be passed to the callback + * + * Request the data from the clipboard in #GBytes form. @callback is executed + * when the data is retrieved. + */ +void +st_clipboard_get_content (StClipboard *clipboard, + StClipboardType type, + const gchar *mimetype, + StClipboardContentCallbackFunc callback, + gpointer user_data) +{ + MetaSelectionType selection_type; + TransferData *data; + + g_return_if_fail (ST_IS_CLIPBOARD (clipboard)); + g_return_if_fail (meta_selection != NULL); + g_return_if_fail (callback != NULL); + + if (!mimetype || !convert_type (type, &selection_type)) + { + callback (clipboard, NULL, user_data); + return; + } + + data = g_new0 (TransferData, 1); + data->clipboard = clipboard; + data->callback = G_CALLBACK (callback); + data->user_data = user_data; + data->stream = g_memory_output_stream_new_resizable (); + + meta_selection_transfer_async (meta_selection, + selection_type, + mimetype, -1, + data->stream, NULL, + (GAsyncReadyCallback) transfer_bytes_cb, + data); +} + +/** + * st_clipboard_set_content: + * @clipboard: A #StClipboard + * @type: The type of clipboard that you want to set + * @mimetype: content mimetype + * @bytes: content data + * + * Sets the clipboard content to @bytes. + * + * @mimetype is a semi-colon separated list of mime-type strings. + **/ +void +st_clipboard_set_content (StClipboard *clipboard, + StClipboardType type, + const gchar *mimetype, + GBytes *bytes) +{ + MetaSelectionType selection_type; + MetaSelectionSource *source; + + g_return_if_fail (ST_IS_CLIPBOARD (clipboard)); + g_return_if_fail (meta_selection != NULL); + g_return_if_fail (bytes != NULL); + + if (!convert_type (type, &selection_type)) + return; + + source = meta_selection_source_memory_new (mimetype, bytes); + meta_selection_set_owner (meta_selection, selection_type, source); + g_object_unref (source); +} + +/** + * st_clipboard_set_text: + * @clipboard: A #StClipboard + * @type: The type of clipboard that you want to set + * @text: text to copy to the clipboard + * + * Sets text as the current contents of the clipboard. + */ +void +st_clipboard_set_text (StClipboard *clipboard, + StClipboardType type, + const gchar *text) +{ + GBytes *bytes; + + g_return_if_fail (ST_IS_CLIPBOARD (clipboard)); + g_return_if_fail (meta_selection != NULL); + g_return_if_fail (text != NULL); + + bytes = g_bytes_new_take (g_strdup (text), strlen (text)); + st_clipboard_set_content (clipboard, type, "text/plain;charset=utf-8", bytes); + g_bytes_unref (bytes); +} + +/** + * st_clipboard_set_selection: (skip) + * + * Sets the #MetaSelection of the default #StClipboard. + * + * This function is called during the initialization of GNOME Shell. + */ +void +st_clipboard_set_selection (MetaSelection *selection) +{ + meta_selection = selection; +} diff --git a/src/st/st-clipboard.h b/src/st/st-clipboard.h new file mode 100644 index 0000000..022b832 --- /dev/null +++ b/src/st/st-clipboard.h @@ -0,0 +1,105 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-clipboard.h: clipboard object + * + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef _ST_CLIPBOARD_H +#define _ST_CLIPBOARD_H + +#include <glib-object.h> +#include <meta/meta-selection.h> + +G_BEGIN_DECLS + +#define ST_TYPE_CLIPBOARD st_clipboard_get_type() +G_DECLARE_FINAL_TYPE (StClipboard, st_clipboard, ST, CLIPBOARD, GObject) + +typedef struct _StClipboard StClipboard; + +/** + * StClipboard: + * + * The contents of this structure is private and should only be accessed using + * the provided API. + */ +struct _StClipboard +{ + /*< private >*/ + GObject parent; +}; + +typedef enum { + ST_CLIPBOARD_TYPE_PRIMARY, + ST_CLIPBOARD_TYPE_CLIPBOARD +} StClipboardType; + +/** + * StClipboardCallbackFunc: + * @clipboard: A #StClipboard + * @text: text from the clipboard + * @user_data: user data + * + * Callback function called when text is retrieved from the clipboard. + */ +typedef void (*StClipboardCallbackFunc) (StClipboard *clipboard, + const gchar *text, + gpointer user_data); + +/** + * StClipboardContentCallbackFunc: + * @clipboard: A #StClipboard + * @bytes: content from the clipboard + * @user_data: user data + * + * Callback function called when content is retrieved from the clipboard. + */ +typedef void (*StClipboardContentCallbackFunc) (StClipboard *clipboard, + GBytes *bytes, + gpointer user_data); + +StClipboard* st_clipboard_get_default (void); + +GList * st_clipboard_get_mimetypes (StClipboard *clipboard, + StClipboardType type); + +void st_clipboard_get_text (StClipboard *clipboard, + StClipboardType type, + StClipboardCallbackFunc callback, + gpointer user_data); +void st_clipboard_set_text (StClipboard *clipboard, + StClipboardType type, + const gchar *text); + +void st_clipboard_set_content (StClipboard *clipboard, + StClipboardType type, + const gchar *mimetype, + GBytes *bytes); +void st_clipboard_get_content (StClipboard *clipboard, + StClipboardType type, + const gchar *mimetype, + StClipboardContentCallbackFunc callback, + gpointer user_data); + +void st_clipboard_set_selection (MetaSelection *selection); + +G_END_DECLS + +#endif /* _ST_CLIPBOARD_H */ diff --git a/src/st/st-drawing-area.c b/src/st/st-drawing-area.c new file mode 100644 index 0000000..38737fd --- /dev/null +++ b/src/st/st-drawing-area.c @@ -0,0 +1,242 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-drawing-area.c: A dynamically-sized Cairo drawing area + * + * 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 Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-drawing-area + * @short_description: A dynamically-sized Cairo drawing area + * + * #StDrawingArea allows drawing via Cairo; the primary difference is that + * it is dynamically sized. To use, connect to the #StDrawingArea::repaint + * signal, and inside the signal handler, call + * st_drawing_area_get_context() to get the Cairo context to draw to. The + * #StDrawingArea::repaint signal will be emitted by default when the area is + * resized or the CSS style changes; you can use the + * st_drawing_area_queue_repaint() as well. + */ + +#include "st-drawing-area.h" + +#include <cairo.h> +#include <math.h> + +typedef struct _StDrawingAreaPrivate StDrawingAreaPrivate; +struct _StDrawingAreaPrivate { + cairo_t *context; + guint in_repaint : 1; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StDrawingArea, st_drawing_area, ST_TYPE_WIDGET); + +/* Signals */ +enum +{ + REPAINT, + LAST_SIGNAL +}; + +static guint st_drawing_area_signals [LAST_SIGNAL] = { 0 }; + +static gboolean +draw_content (ClutterCanvas *canvas, + cairo_t *cr, + int width, + int height, + gpointer user_data) +{ + StDrawingArea *area = ST_DRAWING_AREA (user_data); + StDrawingAreaPrivate *priv = st_drawing_area_get_instance_private (area); + + priv->context = cr; + priv->in_repaint = TRUE; + + clutter_cairo_clear (cr); + g_signal_emit (area, st_drawing_area_signals[REPAINT], 0); + + priv->context = NULL; + priv->in_repaint = FALSE; + + return TRUE; +} + +static void +st_drawing_area_allocate (ClutterActor *self, + const ClutterActorBox *box) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + ClutterContent *content = clutter_actor_get_content (self); + ClutterActorBox content_box; + int width, height; + float resource_scale; + + resource_scale = clutter_actor_get_resource_scale (self); + + clutter_actor_set_allocation (self, box); + st_theme_node_get_content_box (theme_node, box, &content_box); + + width = (int)(0.5 + content_box.x2 - content_box.x1); + height = (int)(0.5 + content_box.y2 - content_box.y1); + + clutter_canvas_set_scale_factor (CLUTTER_CANVAS (content), resource_scale); + clutter_canvas_set_size (CLUTTER_CANVAS (content), width, height); +} + +static void +st_drawing_area_style_changed (StWidget *self) +{ + (ST_WIDGET_CLASS (st_drawing_area_parent_class))->style_changed (self); + + st_drawing_area_queue_repaint (ST_DRAWING_AREA (self)); +} + +static void +st_drawing_area_resource_scale_changed (ClutterActor *self) +{ + float resource_scale; + ClutterContent *content = clutter_actor_get_content (self); + + resource_scale = clutter_actor_get_resource_scale (self); + clutter_canvas_set_scale_factor (CLUTTER_CANVAS (content), resource_scale); + + if (CLUTTER_ACTOR_CLASS (st_drawing_area_parent_class)->resource_scale_changed) + CLUTTER_ACTOR_CLASS (st_drawing_area_parent_class)->resource_scale_changed (self); +} + +static void +st_drawing_area_class_init (StDrawingAreaClass *klass) +{ + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + actor_class->allocate = st_drawing_area_allocate; + widget_class->style_changed = st_drawing_area_style_changed; + actor_class->resource_scale_changed = st_drawing_area_resource_scale_changed; + + st_drawing_area_signals[REPAINT] = + g_signal_new ("repaint", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StDrawingAreaClass, repaint), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +st_drawing_area_init (StDrawingArea *area) +{ + ClutterContent *content = clutter_canvas_new (); + g_signal_connect (content, "draw", G_CALLBACK (draw_content), area); + clutter_actor_set_content (CLUTTER_ACTOR (area), content); + g_object_unref (content); +} + +/** + * st_drawing_area_queue_repaint: + * @area: the #StDrawingArea + * + * Will cause the actor to emit a #StDrawingArea::repaint signal before it is + * next drawn to the scene. Useful if some parameters for the area being + * drawn other than the size or style have changed. Note that + * clutter_actor_queue_redraw() will simply result in the same + * contents being drawn to the scene again. + */ +void +st_drawing_area_queue_repaint (StDrawingArea *area) +{ + g_return_if_fail (ST_IS_DRAWING_AREA (area)); + + clutter_content_invalidate (clutter_actor_get_content (CLUTTER_ACTOR (area))); +} + +/** + * st_drawing_area_get_context: + * @area: the #StDrawingArea + * + * Gets the Cairo context to paint to. This function must only be called + * from a signal handler or virtual function for the #StDrawingArea::repaint + * signal. + * + * JavaScript code must call the special dispose function before returning from + * the signal handler or virtual function to avoid leaking memory: + * + * |[<!-- language="JavaScript" --> + * function onRepaint(area) { + * let cr = area.get_context(); + * + * // Draw to the context + * + * cr.$dispose(); + * } + * + * let area = new St.DrawingArea(); + * area.connect('repaint', onRepaint); + * ]| + * + * Returns: (transfer none): the Cairo context for the paint operation + */ +cairo_t * +st_drawing_area_get_context (StDrawingArea *area) +{ + StDrawingAreaPrivate *priv; + + g_return_val_if_fail (ST_IS_DRAWING_AREA (area), NULL); + + priv = st_drawing_area_get_instance_private (area); + g_return_val_if_fail (priv->in_repaint, NULL); + + return priv->context; +} + +/** + * st_drawing_area_get_surface_size: + * @area: the #StDrawingArea + * @width: (out) (optional): location to store the width of the painted area + * @height: (out) (optional): location to store the height of the painted area + * + * Gets the size of the cairo surface being painted to, which is equal + * to the size of the content area of the widget. This function must + * only be called from a signal handler for the #StDrawingArea::repaint signal. + */ +void +st_drawing_area_get_surface_size (StDrawingArea *area, + guint *width, + guint *height) +{ + StDrawingAreaPrivate *priv; + ClutterContent *content; + float w, h, resource_scale; + + g_return_if_fail (ST_IS_DRAWING_AREA (area)); + + priv = st_drawing_area_get_instance_private (area); + g_return_if_fail (priv->in_repaint); + + content = clutter_actor_get_content (CLUTTER_ACTOR (area)); + clutter_content_get_preferred_size (content, &w, &h); + + resource_scale = clutter_actor_get_resource_scale (CLUTTER_ACTOR (area)); + + w /= resource_scale; + h /= resource_scale; + + if (width) + *width = ceilf (w); + if (height) + *height = ceilf (h); +} diff --git a/src/st/st-drawing-area.h b/src/st/st-drawing-area.h new file mode 100644 index 0000000..e09f9c5 --- /dev/null +++ b/src/st/st-drawing-area.h @@ -0,0 +1,44 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-drawing-area.h: A dynamically-sized Cairo drawing area + * + * 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 Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_DRAWING_AREA_H__ +#define __ST_DRAWING_AREA_H__ + +#include "st-widget.h" +#include <cairo.h> + +#define ST_TYPE_DRAWING_AREA (st_drawing_area_get_type ()) +G_DECLARE_DERIVABLE_TYPE (StDrawingArea, st_drawing_area, + ST, DRAWING_AREA, StWidget) + +struct _StDrawingAreaClass +{ + StWidgetClass parent_class; + + void (*repaint) (StDrawingArea *area); +}; + +void st_drawing_area_queue_repaint (StDrawingArea *area); +cairo_t *st_drawing_area_get_context (StDrawingArea *area); +void st_drawing_area_get_surface_size (StDrawingArea *area, + guint *width, + guint *height); + +#endif /* __ST_DRAWING_AREA_H__ */ diff --git a/src/st/st-entry.c b/src/st/st-entry.c new file mode 100644 index 0000000..64f85fd --- /dev/null +++ b/src/st/st-entry.c @@ -0,0 +1,1626 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-entry.c: Plain entry actor + * + * Copyright 2008, 2009 Intel Corporation + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-entry + * @short_description: Widget for displaying text + * + * #StEntry is a simple widget for displaying text. It derives from + * #StWidget to add extra style and placement functionality over + * #ClutterText. The internal #ClutterText is publicly accessibly to allow + * applications to set further properties. + * + * #StEntry supports the following pseudo style states: + * + * - `focus`: the widget has focus + * - `indeterminate`: the widget is showing the hint text or actor + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <math.h> + +#include <stdlib.h> +#include <string.h> + +#include <glib.h> + +#include <clutter/clutter.h> + +#include "st-entry.h" + +#include "st-icon.h" +#include "st-label.h" +#include "st-settings.h" +#include "st-widget.h" +#include "st-texture-cache.h" +#include "st-clipboard.h" +#include "st-private.h" + +#include "st-widget-accessible.h" + + +/* properties */ +enum +{ + PROP_0, + + PROP_CLUTTER_TEXT, + PROP_PRIMARY_ICON, + PROP_SECONDARY_ICON, + PROP_HINT_TEXT, + PROP_HINT_ACTOR, + PROP_TEXT, + PROP_INPUT_PURPOSE, + PROP_INPUT_HINTS, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +/* signals */ +enum +{ + PRIMARY_ICON_CLICKED, + SECONDARY_ICON_CLICKED, + + LAST_SIGNAL +}; + +#define ST_ENTRY_PRIV(x) st_entry_get_instance_private ((StEntry *) x) + + +typedef struct _StEntryPrivate StEntryPrivate; +struct _StEntryPrivate +{ + ClutterActor *entry; + + ClutterActor *primary_icon; + ClutterActor *secondary_icon; + + ClutterActor *hint_actor; + + gfloat spacing; + + gboolean has_ibeam; + + StShadow *shadow_spec; + + CoglPipeline *text_shadow_material; + gfloat shadow_width; + gfloat shadow_height; +}; + +static guint entry_signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE_WITH_PRIVATE (StEntry, st_entry, ST_TYPE_WIDGET); + +static GType st_entry_accessible_get_type (void) G_GNUC_CONST; + +static void +st_entry_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StEntry *entry = ST_ENTRY (gobject); + + switch (prop_id) + { + case PROP_PRIMARY_ICON: + st_entry_set_primary_icon (entry, g_value_get_object (value)); + break; + + case PROP_SECONDARY_ICON: + st_entry_set_secondary_icon (entry, g_value_get_object (value)); + break; + + case PROP_HINT_TEXT: + st_entry_set_hint_text (entry, g_value_get_string (value)); + break; + + case PROP_HINT_ACTOR: + st_entry_set_hint_actor (entry, g_value_get_object (value)); + break; + + case PROP_TEXT: + st_entry_set_text (entry, g_value_get_string (value)); + break; + + case PROP_INPUT_PURPOSE: + st_entry_set_input_purpose (entry, g_value_get_enum (value)); + break; + + case PROP_INPUT_HINTS: + st_entry_set_input_hints (entry, g_value_get_flags (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_entry_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (gobject); + + switch (prop_id) + { + case PROP_CLUTTER_TEXT: + g_value_set_object (value, priv->entry); + break; + + case PROP_PRIMARY_ICON: + g_value_set_object (value, priv->primary_icon); + break; + + case PROP_SECONDARY_ICON: + g_value_set_object (value, priv->secondary_icon); + break; + + case PROP_HINT_TEXT: + g_value_set_string (value, st_entry_get_hint_text (ST_ENTRY (gobject))); + break; + + case PROP_HINT_ACTOR: + g_value_set_object (value, priv->hint_actor); + break; + + case PROP_TEXT: + g_value_set_string (value, clutter_text_get_text (CLUTTER_TEXT (priv->entry))); + break; + + case PROP_INPUT_PURPOSE: + g_value_set_enum (value, clutter_text_get_input_purpose (CLUTTER_TEXT (priv->entry))); + break; + + case PROP_INPUT_HINTS: + g_value_set_flags (value, clutter_text_get_input_hints (CLUTTER_TEXT (priv->entry))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_entry_dispose (GObject *object) +{ + StEntry *entry = ST_ENTRY (object); + StEntryPrivate *priv = ST_ENTRY_PRIV (entry); + + cogl_clear_object (&priv->text_shadow_material); + + G_OBJECT_CLASS (st_entry_parent_class)->dispose (object); +} + +static void +st_entry_update_hint_visibility (StEntry *self) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (self); + gboolean hint_visible = + priv->hint_actor != NULL && + !clutter_text_has_preedit (CLUTTER_TEXT (priv->entry)) && + strcmp (clutter_text_get_text (CLUTTER_TEXT (priv->entry)), "") == 0; + + if (priv->hint_actor) + g_object_set (priv->hint_actor, "visible", hint_visible, NULL); + + if (hint_visible) + st_widget_add_style_pseudo_class (ST_WIDGET (self), "indeterminate"); + else + st_widget_remove_style_pseudo_class (ST_WIDGET (self), "indeterminate"); +} + +static void +st_entry_style_changed (StWidget *self) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (self); + StThemeNode *theme_node; + StShadow *shadow_spec; + ClutterColor color; + gdouble size; + + theme_node = st_widget_get_theme_node (self); + + shadow_spec = st_theme_node_get_text_shadow (theme_node); + if (!priv->shadow_spec || !shadow_spec || + !st_shadow_equal (shadow_spec, priv->shadow_spec)) + { + g_clear_pointer (&priv->text_shadow_material, cogl_object_unref); + + g_clear_pointer (&priv->shadow_spec, st_shadow_unref); + if (shadow_spec) + priv->shadow_spec = st_shadow_ref (shadow_spec); + } + + _st_set_text_from_style (CLUTTER_TEXT (priv->entry), theme_node); + + if (st_theme_node_lookup_length (theme_node, "caret-size", TRUE, &size)) + clutter_text_set_cursor_size (CLUTTER_TEXT (priv->entry), (int)(.5 + size)); + + if (st_theme_node_lookup_color (theme_node, "caret-color", TRUE, &color)) + clutter_text_set_cursor_color (CLUTTER_TEXT (priv->entry), &color); + + if (st_theme_node_lookup_color (theme_node, "selection-background-color", TRUE, &color)) + clutter_text_set_selection_color (CLUTTER_TEXT (priv->entry), &color); + + if (st_theme_node_lookup_color (theme_node, "selected-color", TRUE, &color)) + clutter_text_set_selected_text_color (CLUTTER_TEXT (priv->entry), &color); + + ST_WIDGET_CLASS (st_entry_parent_class)->style_changed (self); +} + +static gboolean +st_entry_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (widget); + + /* This is basically the same as st_widget_real_navigate_focus(), + * except that widget is behaving as a proxy for priv->entry (which + * isn't an StWidget and so has no can-focus flag of its own). + */ + + if (from == priv->entry) + return FALSE; + else if (st_widget_get_can_focus (widget) && + clutter_actor_is_mapped (priv->entry)) + { + clutter_actor_grab_key_focus (priv->entry); + return TRUE; + } + else + return FALSE; +} + +static void +st_entry_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gfloat hint_w, hint_min_w, icon_w; + + st_theme_node_adjust_for_height (theme_node, &for_height); + + clutter_actor_get_preferred_width (priv->entry, for_height, + min_width_p, + natural_width_p); + + if (priv->hint_actor) + { + clutter_actor_get_preferred_width (priv->hint_actor, -1, + &hint_min_w, &hint_w); + + if (min_width_p && hint_min_w > *min_width_p) + *min_width_p = hint_min_w; + + if (natural_width_p && hint_w > *natural_width_p) + *natural_width_p = hint_w; + } + + if (priv->primary_icon) + { + clutter_actor_get_preferred_width (priv->primary_icon, -1, NULL, &icon_w); + + if (min_width_p) + *min_width_p += icon_w + priv->spacing; + + if (natural_width_p) + *natural_width_p += icon_w + priv->spacing; + } + + if (priv->secondary_icon) + { + clutter_actor_get_preferred_width (priv->secondary_icon, + -1, NULL, &icon_w); + + if (min_width_p) + *min_width_p += icon_w + priv->spacing; + + if (natural_width_p) + *natural_width_p += icon_w + priv->spacing; + } + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_entry_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gfloat hint_h, icon_h; + + st_theme_node_adjust_for_width (theme_node, &for_width); + + clutter_actor_get_preferred_height (priv->entry, for_width, + min_height_p, + natural_height_p); + + if (priv->hint_actor) + { + clutter_actor_get_preferred_height (priv->hint_actor, -1, NULL, &hint_h); + + if (min_height_p && hint_h > *min_height_p) + *min_height_p = hint_h; + + if (natural_height_p && hint_h > *natural_height_p) + *natural_height_p = hint_h; + } + + if (priv->primary_icon) + { + clutter_actor_get_preferred_height (priv->primary_icon, + -1, NULL, &icon_h); + + if (min_height_p && icon_h > *min_height_p) + *min_height_p = icon_h; + + if (natural_height_p && icon_h > *natural_height_p) + *natural_height_p = icon_h; + } + + if (priv->secondary_icon) + { + clutter_actor_get_preferred_height (priv->secondary_icon, + -1, NULL, &icon_h); + + if (min_height_p && icon_h > *min_height_p) + *min_height_p = icon_h; + + if (natural_height_p && icon_h > *natural_height_p) + *natural_height_p = icon_h; + } + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_entry_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox content_box, child_box, icon_box, hint_box; + gfloat icon_w, icon_h; + gfloat hint_w, hint_min_w, hint_h; + gfloat entry_h, min_h, pref_h, avail_h; + ClutterActor *left_icon, *right_icon; + gboolean is_rtl; + + is_rtl = clutter_actor_get_text_direction (actor) == CLUTTER_TEXT_DIRECTION_RTL; + + if (is_rtl) + { + right_icon = priv->primary_icon; + left_icon = priv->secondary_icon; + } + else + { + left_icon = priv->primary_icon; + right_icon = priv->secondary_icon; + } + + clutter_actor_set_allocation (actor, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + avail_h = content_box.y2 - content_box.y1; + + child_box.x1 = content_box.x1; + child_box.x2 = content_box.x2; + + if (left_icon) + { + clutter_actor_get_preferred_width (left_icon, -1, NULL, &icon_w); + clutter_actor_get_preferred_height (left_icon, -1, NULL, &icon_h); + + icon_box.x1 = content_box.x1; + icon_box.x2 = icon_box.x1 + icon_w; + + icon_box.y1 = (int) (content_box.y1 + avail_h / 2 - icon_h / 2); + icon_box.y2 = icon_box.y1 + icon_h; + + clutter_actor_allocate (left_icon, &icon_box); + + /* reduce the size for the entry */ + child_box.x1 = MIN (child_box.x2, child_box.x1 + icon_w + priv->spacing); + } + + if (right_icon) + { + clutter_actor_get_preferred_width (right_icon, -1, NULL, &icon_w); + clutter_actor_get_preferred_height (right_icon, -1, NULL, &icon_h); + + icon_box.x2 = content_box.x2; + icon_box.x1 = icon_box.x2 - icon_w; + + icon_box.y1 = (int) (content_box.y1 + avail_h / 2 - icon_h / 2); + icon_box.y2 = icon_box.y1 + icon_h; + + clutter_actor_allocate (right_icon, &icon_box); + + /* reduce the size for the entry */ + child_box.x2 = MAX (child_box.x1, child_box.x2 - icon_w - priv->spacing); + } + + if (priv->hint_actor) + { + /* now allocate the hint actor */ + hint_box = child_box; + + clutter_actor_get_preferred_width (priv->hint_actor, -1, &hint_min_w, &hint_w); + clutter_actor_get_preferred_height (priv->hint_actor, -1, NULL, &hint_h); + + hint_w = CLAMP (hint_w, hint_min_w, child_box.x2 - child_box.x1); + + if (is_rtl) + hint_box.x1 = hint_box.x2 - hint_w; + else + hint_box.x2 = hint_box.x1 + hint_w; + + hint_box.y1 = ceil (content_box.y1 + avail_h / 2 - hint_h / 2); + hint_box.y2 = hint_box.y1 + hint_h; + + clutter_actor_allocate (priv->hint_actor, &hint_box); + } + + clutter_actor_get_preferred_height (priv->entry, child_box.x2 - child_box.x1, + &min_h, &pref_h); + + entry_h = CLAMP (pref_h, min_h, avail_h); + + child_box.y1 = (int) (content_box.y1 + avail_h / 2 - entry_h / 2); + child_box.y2 = child_box.y1 + entry_h; + + clutter_actor_allocate (priv->entry, &child_box); +} + +static void +clutter_text_reactive_changed_cb (ClutterActor *text, + GParamSpec *pspec, + gpointer user_data) +{ + ClutterActor *stage; + + if (clutter_actor_get_reactive (text)) + return; + + if (!clutter_actor_has_key_focus (text)) + return; + + stage = clutter_actor_get_stage (text); + if (stage == NULL) + return; + + clutter_stage_set_key_focus (CLUTTER_STAGE (stage), NULL); +} + +static void +clutter_text_focus_in_cb (ClutterText *text, + ClutterActor *actor) +{ + st_widget_add_style_pseudo_class (ST_WIDGET (actor), "focus"); + clutter_text_set_cursor_visible (text, TRUE); +} + +static void +clutter_text_focus_out_cb (ClutterText *text, + ClutterActor *actor) +{ + st_widget_remove_style_pseudo_class (ST_WIDGET (actor), "focus"); + clutter_text_set_cursor_visible (text, FALSE); +} + +static void +clutter_text_cursor_changed (ClutterText *text, + StEntry *entry) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (entry); + + st_entry_update_hint_visibility (entry); + + g_clear_pointer (&priv->text_shadow_material, cogl_object_unref); +} + +static void +clutter_text_changed_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + StEntry *entry = ST_ENTRY (user_data); + StEntryPrivate *priv = ST_ENTRY_PRIV (entry); + + st_entry_update_hint_visibility (entry); + + /* Since the text changed, force a regen of the shadow texture */ + cogl_clear_object (&priv->text_shadow_material); + + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_TEXT]); +} + +static void +invalidate_shadow_pipeline (GObject *object, + GParamSpec *pspec, + StEntry *entry) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (entry); + + g_clear_pointer (&priv->text_shadow_material, cogl_object_unref); +} + +static void +st_entry_clipboard_callback (StClipboard *clipboard, + const gchar *text, + gpointer data) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (data); + ClutterText *ctext = (ClutterText*)priv->entry; + gint cursor_pos; + + if (!text) + return; + + /* delete the current selection before pasting */ + clutter_text_delete_selection (ctext); + + /* "paste" the clipboard text into the entry */ + cursor_pos = clutter_text_get_cursor_position (ctext); + clutter_text_insert_text (ctext, text, cursor_pos); +} + +static gboolean +clutter_text_button_press_event (ClutterActor *actor, + ClutterButtonEvent *event, + gpointer user_data) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (user_data); + + if (event->button == 2 && + clutter_text_get_editable (CLUTTER_TEXT (priv->entry))) + { + StSettings *settings; + gboolean primary_paste_enabled; + + settings = st_settings_get (); + g_object_get (settings, "primary-paste", &primary_paste_enabled, NULL); + + if (primary_paste_enabled) + { + StClipboard *clipboard; + + clipboard = st_clipboard_get_default (); + + /* By the time the clipboard callback is called, + * the rest of the signal handlers will have + * run, making the text cursor to be in the correct + * place. + */ + st_clipboard_get_text (clipboard, + ST_CLIPBOARD_TYPE_PRIMARY, + st_entry_clipboard_callback, + user_data); + } + } + + return FALSE; +} + +static gboolean +st_entry_key_press_event (ClutterActor *actor, + ClutterKeyEvent *event) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + + /* This is expected to handle events that were emitted for the inner + ClutterText. They only reach this function if the ClutterText + didn't handle them */ + + /* paste */ + if (((event->modifier_state & CLUTTER_CONTROL_MASK) + && event->keyval == CLUTTER_KEY_v) || + ((event->modifier_state & CLUTTER_CONTROL_MASK) + && event->keyval == CLUTTER_KEY_V) || + ((event->modifier_state & CLUTTER_SHIFT_MASK) + && event->keyval == CLUTTER_KEY_Insert)) + { + StClipboard *clipboard; + + clipboard = st_clipboard_get_default (); + + st_clipboard_get_text (clipboard, + ST_CLIPBOARD_TYPE_CLIPBOARD, + st_entry_clipboard_callback, + actor); + + return TRUE; + } + + /* copy */ + if ((event->modifier_state & CLUTTER_CONTROL_MASK) + && (event->keyval == CLUTTER_KEY_c || event->keyval == CLUTTER_KEY_C) && + clutter_text_get_password_char ((ClutterText*) priv->entry) == 0) + { + StClipboard *clipboard; + gchar *text; + + clipboard = st_clipboard_get_default (); + + text = clutter_text_get_selection ((ClutterText*) priv->entry); + + if (text && strlen (text)) + st_clipboard_set_text (clipboard, + ST_CLIPBOARD_TYPE_CLIPBOARD, + text); + + g_free (text); + + return TRUE; + } + + + /* cut */ + if ((event->modifier_state & CLUTTER_CONTROL_MASK) + && (event->keyval == CLUTTER_KEY_x || event->keyval == CLUTTER_KEY_X) && + clutter_text_get_password_char ((ClutterText*) priv->entry) == 0) + { + StClipboard *clipboard; + gchar *text; + + clipboard = st_clipboard_get_default (); + + text = clutter_text_get_selection ((ClutterText*) priv->entry); + + if (text && strlen (text)) + { + st_clipboard_set_text (clipboard, + ST_CLIPBOARD_TYPE_CLIPBOARD, + text); + + /* now delete the text */ + clutter_text_delete_selection ((ClutterText *) priv->entry); + } + + g_free (text); + + return TRUE; + } + + + /* delete to beginning of line */ + if ((event->modifier_state & CLUTTER_CONTROL_MASK) && + (event->keyval == CLUTTER_KEY_u || event->keyval == CLUTTER_KEY_U)) + { + int pos = clutter_text_get_cursor_position ((ClutterText *)priv->entry); + clutter_text_delete_text ((ClutterText *)priv->entry, 0, pos); + + return TRUE; + } + + + /* delete to end of line */ + if ((event->modifier_state & CLUTTER_CONTROL_MASK) && + (event->keyval == CLUTTER_KEY_k || event->keyval == CLUTTER_KEY_K)) + { + ClutterTextBuffer *buffer = clutter_text_get_buffer ((ClutterText *)priv->entry); + int pos = clutter_text_get_cursor_position ((ClutterText *)priv->entry); + clutter_text_buffer_delete_text (buffer, pos, -1); + + return TRUE; + } + + return CLUTTER_ACTOR_CLASS (st_entry_parent_class)->key_press_event (actor, event); +} + +static void +st_entry_key_focus_in (ClutterActor *actor) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + + /* We never want key focus. The ClutterText should be given first + pass for all key events */ + clutter_actor_grab_key_focus (priv->entry); +} + +static StEntryCursorFunc cursor_func = NULL; +static gpointer cursor_func_data = NULL; + +/** + * st_entry_set_cursor_func: (skip) + * + * This function is for private use by libgnome-shell. + * Do not ever use. + */ +void +st_entry_set_cursor_func (StEntryCursorFunc func, + gpointer data) +{ + cursor_func = func; + cursor_func_data = data; +} + +static void +st_entry_set_cursor (StEntry *entry, + gboolean use_ibeam) +{ + if (cursor_func) + cursor_func (entry, use_ibeam, cursor_func_data); + + ((StEntryPrivate *)ST_ENTRY_PRIV (entry))->has_ibeam = use_ibeam; +} + +static gboolean +st_entry_enter_event (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + ClutterStage *stage; + ClutterActor *target; + + stage = clutter_event_get_stage ((ClutterEvent *) event); + target = clutter_stage_get_event_actor (stage, (ClutterEvent *) event); + + if (target == priv->entry && event->related != NULL) + st_entry_set_cursor (ST_ENTRY (actor), TRUE); + + return CLUTTER_ACTOR_CLASS (st_entry_parent_class)->enter_event (actor, event); +} + +static gboolean +st_entry_leave_event (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + st_entry_set_cursor (ST_ENTRY (actor), FALSE); + + return CLUTTER_ACTOR_CLASS (st_entry_parent_class)->leave_event (actor, event); +} + +static void +st_entry_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + ClutterActorClass *parent_class; + + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + if (priv->shadow_spec) + { + ClutterActorBox allocation; + float width, height; + + clutter_actor_get_allocation_box (priv->entry, &allocation); + clutter_actor_box_get_size (&allocation, &width, &height); + + if (priv->text_shadow_material == NULL || + width != priv->shadow_width || + height != priv->shadow_height) + { + CoglPipeline *material; + + cogl_clear_object (&priv->text_shadow_material); + + material = _st_create_shadow_pipeline_from_actor (priv->shadow_spec, + priv->entry); + + priv->shadow_width = width; + priv->shadow_height = height; + priv->text_shadow_material = material; + } + + if (priv->text_shadow_material != NULL) + { + CoglFramebuffer *framebuffer = + clutter_paint_context_get_framebuffer (paint_context); + + _st_paint_shadow_with_opacity (priv->shadow_spec, + framebuffer, + priv->text_shadow_material, + &allocation, + clutter_actor_get_paint_opacity (priv->entry)); + } + } + + /* Since we paint the background ourselves, chain to the parent class + * of StWidget, to avoid painting it twice. + * This is needed as we still want to paint children. + */ + parent_class = g_type_class_peek_parent (st_entry_parent_class); + parent_class->paint (actor, paint_context); +} + +static void +st_entry_unmap (ClutterActor *actor) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (actor); + if (priv->has_ibeam) + st_entry_set_cursor (ST_ENTRY (actor), FALSE); + + CLUTTER_ACTOR_CLASS (st_entry_parent_class)->unmap (actor); +} + +static gboolean +st_entry_get_paint_volume (ClutterActor *actor, + ClutterPaintVolume *volume) +{ + return clutter_paint_volume_set_from_allocation (volume, actor); +} + +static void +st_entry_class_init (StEntryClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + gobject_class->set_property = st_entry_set_property; + gobject_class->get_property = st_entry_get_property; + gobject_class->dispose = st_entry_dispose; + + actor_class->get_preferred_width = st_entry_get_preferred_width; + actor_class->get_preferred_height = st_entry_get_preferred_height; + actor_class->allocate = st_entry_allocate; + actor_class->paint = st_entry_paint; + actor_class->unmap = st_entry_unmap; + actor_class->get_paint_volume = st_entry_get_paint_volume; + + actor_class->key_press_event = st_entry_key_press_event; + actor_class->key_focus_in = st_entry_key_focus_in; + + actor_class->enter_event = st_entry_enter_event; + actor_class->leave_event = st_entry_leave_event; + + widget_class->style_changed = st_entry_style_changed; + widget_class->navigate_focus = st_entry_navigate_focus; + widget_class->get_accessible_type = st_entry_accessible_get_type; + + /** + * StEntry:clutter-text: + * + * The internal #ClutterText actor supporting the #StEntry. + */ + props[PROP_CLUTTER_TEXT] = + g_param_spec_object ("clutter-text", + "Clutter Text", + "Internal ClutterText actor", + CLUTTER_TYPE_TEXT, + ST_PARAM_READABLE); + + /** + * StEntry:primary-icon: + * + * The #ClutterActor acting as the primary icon at the start of the #StEntry. + */ + props[PROP_PRIMARY_ICON] = + g_param_spec_object ("primary-icon", + "Primary Icon", + "Primary Icon actor", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:secondary-icon: + * + * The #ClutterActor acting as the secondary icon at the end of the #StEntry. + */ + props[PROP_SECONDARY_ICON] = + g_param_spec_object ("secondary-icon", + "Secondary Icon", + "Secondary Icon actor", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:hint-text: + * + * The text to display when the entry is empty and unfocused. Setting this + * will replace the actor of #StEntry::hint-actor. + */ + props[PROP_HINT_TEXT] = + g_param_spec_string ("hint-text", + "Hint Text", + "Text to display when the entry is not focused " + "and the text property is empty", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:hint-actor: + * + * A #ClutterActor to display when the entry is empty and unfocused. Setting + * this will replace the actor displaying #StEntry:hint-text. + */ + props[PROP_HINT_ACTOR] = + g_param_spec_object ("hint-actor", + "Hint Actor", + "An actor to display when the entry is not focused " + "and the text property is empty", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:text: + * + * The current text value of the #StEntry. + */ + props[PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "Text of the entry", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:input-purpose: + * + * The #ClutterInputContentPurpose that helps on-screen keyboards and similar + * input methods to decide which keys should be presented to the user. + */ + props[PROP_INPUT_PURPOSE] = + g_param_spec_enum ("input-purpose", + "Purpose", + "Purpose of the text field", + CLUTTER_TYPE_INPUT_CONTENT_PURPOSE, + CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StEntry:input-hints: + * + * The #ClutterInputContentHintFlags providing additional hints (beyond + * #StEntry:input-purpose) that allow input methods to fine-tune their + * behaviour. + */ + props[PROP_INPUT_HINTS] = + g_param_spec_flags ("input-hints", + "hints", + "Hints for the text field behaviour", + CLUTTER_TYPE_INPUT_CONTENT_HINT_FLAGS, + 0, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + /* signals */ + /** + * StEntry::primary-icon-clicked: + * @self: the #StEntry + * + * Emitted when the primary icon is clicked. + */ + entry_signals[PRIMARY_ICON_CLICKED] = + g_signal_new ("primary-icon-clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StEntryClass, primary_icon_clicked), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * StEntry::secondary-icon-clicked: + * @self: the #StEntry + * + * Emitted when the secondary icon is clicked. + */ + entry_signals[SECONDARY_ICON_CLICKED] = + g_signal_new ("secondary-icon-clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StEntryClass, secondary_icon_clicked), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +st_entry_init (StEntry *entry) +{ + StEntryPrivate *priv; + + priv = st_entry_get_instance_private (entry); + + priv->entry = g_object_new (CLUTTER_TYPE_TEXT, + "line-alignment", PANGO_ALIGN_LEFT, + "editable", TRUE, + "reactive", TRUE, + "single-line-mode", TRUE, + NULL); + + g_object_bind_property (G_OBJECT (entry), "reactive", + priv->entry, "reactive", + G_BINDING_DEFAULT); + + g_signal_connect(priv->entry, "notify::reactive", + G_CALLBACK (clutter_text_reactive_changed_cb), entry); + + g_signal_connect (priv->entry, "key-focus-in", + G_CALLBACK (clutter_text_focus_in_cb), entry); + + g_signal_connect (priv->entry, "key-focus-out", + G_CALLBACK (clutter_text_focus_out_cb), entry); + + g_signal_connect (priv->entry, "button-press-event", + G_CALLBACK (clutter_text_button_press_event), entry); + + g_signal_connect (priv->entry, "cursor-changed", + G_CALLBACK (clutter_text_cursor_changed), entry); + + g_signal_connect (priv->entry, "notify::text", + G_CALLBACK (clutter_text_changed_cb), entry); + + /* These properties might get set from CSS using _st_set_text_from_style */ + g_signal_connect (priv->entry, "notify::font-description", + G_CALLBACK (invalidate_shadow_pipeline), entry); + + g_signal_connect (priv->entry, "notify::attributes", + G_CALLBACK (invalidate_shadow_pipeline), entry); + + g_signal_connect (priv->entry, "notify::justify", + G_CALLBACK (invalidate_shadow_pipeline), entry); + + g_signal_connect (priv->entry, "notify::line-alignment", + G_CALLBACK (invalidate_shadow_pipeline), entry); + + + priv->spacing = 6.0f; + + priv->text_shadow_material = NULL; + priv->shadow_width = -1.; + priv->shadow_height = -1.; + + clutter_actor_add_child (CLUTTER_ACTOR (entry), priv->entry); + clutter_actor_set_reactive ((ClutterActor *) entry, TRUE); + + /* set cursor hidden until we receive focus */ + clutter_text_set_cursor_visible ((ClutterText *) priv->entry, FALSE); +} + +/** + * st_entry_new: + * @text: (nullable): text to set the entry to + * + * Create a new #StEntry with the specified text. + * + * Returns: a new #StEntry + */ +StWidget * +st_entry_new (const gchar *text) +{ + StWidget *entry; + + /* add the entry to the stage, but don't allow it to be visible */ + entry = g_object_new (ST_TYPE_ENTRY, + "text", text, + NULL); + + return entry; +} + +/** + * st_entry_get_text: + * @entry: a #StEntry + * + * Get the text displayed on the entry. If @entry is empty, an empty string will + * be returned instead of %NULL. + * + * Returns: (transfer none): the text for the entry + */ +const gchar * +st_entry_get_text (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), NULL); + + priv = st_entry_get_instance_private (entry); + + return clutter_text_get_text (CLUTTER_TEXT (priv->entry)); +} + +/** + * st_entry_set_text: + * @entry: a #StEntry + * @text: (nullable): text to set the entry to + * + * Sets the text displayed on the entry. If @text is %NULL, the #ClutterText + * will instead be set to an empty string. + */ +void +st_entry_set_text (StEntry *entry, + const gchar *text) +{ + StEntryPrivate *priv; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = st_entry_get_instance_private (entry); + + clutter_text_set_text (CLUTTER_TEXT (priv->entry), text); + + /* Note: PROP_TEXT will get notfied from our notify::text handler connected + * to priv->entry. */ +} + +/** + * st_entry_get_clutter_text: + * @entry: a #StEntry + * + * Retrieve the internal #ClutterText so that extra parameters can be set. + * + * Returns: (transfer none): the #ClutterText used by @entry + */ +ClutterActor* +st_entry_get_clutter_text (StEntry *entry) +{ + g_return_val_if_fail (ST_ENTRY (entry), NULL); + + return ((StEntryPrivate *)ST_ENTRY_PRIV (entry))->entry; +} + +/** + * st_entry_set_hint_text: + * @entry: a #StEntry + * @text: (nullable): text to set as the entry hint + * + * Sets the text to display when the entry is empty and unfocused. When the + * entry is displaying the hint, it has a pseudo class of `indeterminate`. + * A value of %NULL unsets the hint. + */ +void +st_entry_set_hint_text (StEntry *entry, + const gchar *text) +{ + StWidget *label; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + label = st_label_new (text); + st_widget_add_style_class_name (label, "hint-text"); + + st_entry_set_hint_actor (ST_ENTRY (entry), CLUTTER_ACTOR (label)); + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_HINT_TEXT]); +} + +/** + * st_entry_get_hint_text: + * @entry: a #StEntry + * + * Gets the text that is displayed when the entry is empty and unfocused or + * %NULL if the #StEntry:hint-actor was set to an actor that is not a #StLabel. + * + * Unlike st_entry_get_text() this function may return %NULL if + * #StEntry:hint-actor is not a #StLabel. + * + * Returns: (nullable) (transfer none): the current value of the hint property + */ +const gchar * +st_entry_get_hint_text (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), NULL); + + priv = ST_ENTRY_PRIV (entry); + + if (priv->hint_actor != NULL && ST_IS_LABEL (priv->hint_actor)) + return st_label_get_text (ST_LABEL (priv->hint_actor)); + + return NULL; +} + +/** + * st_entry_set_input_purpose: + * @entry: a #StEntry + * @purpose: the purpose + * + * Sets the #StEntry:input-purpose property which + * can be used by on-screen keyboards and other input + * methods to adjust their behaviour. + */ +void +st_entry_set_input_purpose (StEntry *entry, + ClutterInputContentPurpose purpose) +{ + StEntryPrivate *priv; + ClutterText *editable; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = st_entry_get_instance_private (entry); + editable = CLUTTER_TEXT (priv->entry); + + if (clutter_text_get_input_purpose (editable) != purpose) + { + clutter_text_set_input_purpose (editable, purpose); + + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_INPUT_PURPOSE]); + } +} + +/** + * st_entry_get_input_purpose: + * @entry: a #StEntry + * + * Gets the value of the #StEntry:input-purpose property. + * + * Returns: the input purpose of the entry + */ +ClutterInputContentPurpose +st_entry_get_input_purpose (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), CLUTTER_INPUT_CONTENT_PURPOSE_NORMAL); + + priv = st_entry_get_instance_private (entry); + return clutter_text_get_input_purpose (CLUTTER_TEXT (priv->entry)); +} + +/** + * st_entry_set_input_hints: + * @entry: a #StEntry + * @hints: the hints + * + * Sets the #StEntry:input-hints property, which + * allows input methods to fine-tune their behaviour. + */ +void +st_entry_set_input_hints (StEntry *entry, + ClutterInputContentHintFlags hints) +{ + StEntryPrivate *priv; + ClutterText *editable; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = st_entry_get_instance_private (entry); + editable = CLUTTER_TEXT (priv->entry); + + if (clutter_text_get_input_hints (editable) != hints) + { + clutter_text_set_input_hints (editable, hints); + + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_INPUT_HINTS]); + } +} + +/** + * st_entry_get_input_hints: + * @entry: a #StEntry + * + * Gets the value of the #StEntry:input-hints property. + * + * Returns: the input hints for the entry + */ +ClutterInputContentHintFlags +st_entry_get_input_hints (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), 0); + + priv = st_entry_get_instance_private (entry); + return clutter_text_get_input_hints (CLUTTER_TEXT (priv->entry)); +} + +static void +_st_entry_icon_clicked_cb (ClutterClickAction *action, + ClutterActor *actor, + StEntry *entry) +{ + StEntryPrivate *priv = ST_ENTRY_PRIV (entry); + + if (!clutter_actor_get_reactive (CLUTTER_ACTOR (entry))) + return; + + if (actor == priv->primary_icon) + g_signal_emit (entry, entry_signals[PRIMARY_ICON_CLICKED], 0); + else + g_signal_emit (entry, entry_signals[SECONDARY_ICON_CLICKED], 0); +} + +static void +_st_entry_set_icon (StEntry *entry, + ClutterActor **icon, + ClutterActor *new_icon) +{ + if (*icon) + { + clutter_actor_remove_action_by_name (*icon, "entry-icon-action"); + clutter_actor_remove_child (CLUTTER_ACTOR (entry), *icon); + *icon = NULL; + } + + if (new_icon) + { + ClutterAction *action; + + *icon = g_object_ref (new_icon); + + clutter_actor_set_reactive (*icon, TRUE); + clutter_actor_add_child (CLUTTER_ACTOR (entry), *icon); + + action = clutter_click_action_new (); + clutter_actor_add_action_with_name (*icon, "entry-icon-action", action); + g_signal_connect (action, "clicked", + G_CALLBACK (_st_entry_icon_clicked_cb), entry); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (entry)); +} + +/** + * st_entry_set_primary_icon: + * @entry: a #StEntry + * @icon: (nullable): a #ClutterActor + * + * Set the primary icon of the entry to @icon. + */ +void +st_entry_set_primary_icon (StEntry *entry, + ClutterActor *icon) +{ + StEntryPrivate *priv; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = st_entry_get_instance_private (entry); + + if (priv->primary_icon == icon) + return; + + _st_entry_set_icon (entry, &priv->primary_icon, icon); + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_PRIMARY_ICON]); +} + +/** + * st_entry_get_primary_icon: + * @entry: a #StEntry + * + * Get the value of the #StEntry:primary-icon property. + * + * Returns: (nullable) (transfer none): a #ClutterActor + */ +ClutterActor * +st_entry_get_primary_icon (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), NULL); + + priv = ST_ENTRY_PRIV (entry); + return priv->primary_icon; +} + +/** + * st_entry_set_secondary_icon: + * @entry: a #StEntry + * @icon: (nullable): an #ClutterActor + * + * Set the secondary icon of the entry to @icon. + */ +void +st_entry_set_secondary_icon (StEntry *entry, + ClutterActor *icon) +{ + StEntryPrivate *priv; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = st_entry_get_instance_private (entry); + + if (priv->secondary_icon == icon) + return; + + _st_entry_set_icon (entry, &priv->secondary_icon, icon); + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_SECONDARY_ICON]); +} + +/** + * st_entry_get_secondary_icon: + * @entry: a #StEntry + * + * Get the value of the #StEntry:secondary-icon property. + * + * Returns: (nullable) (transfer none): a #ClutterActor + */ +ClutterActor * +st_entry_get_secondary_icon (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), NULL); + + priv = ST_ENTRY_PRIV (entry); + return priv->secondary_icon; +} + +/** + * st_entry_set_hint_actor: + * @entry: a #StEntry + * @hint_actor: (nullable): a #ClutterActor + * + * Set the hint actor of the entry to @hint_actor. + */ +void +st_entry_set_hint_actor (StEntry *entry, + ClutterActor *hint_actor) +{ + StEntryPrivate *priv; + + g_return_if_fail (ST_IS_ENTRY (entry)); + + priv = ST_ENTRY_PRIV (entry); + + if (priv->hint_actor == hint_actor) + return; + + if (priv->hint_actor != NULL) + { + clutter_actor_remove_child (CLUTTER_ACTOR (entry), priv->hint_actor); + priv->hint_actor = NULL; + } + + if (hint_actor != NULL) + { + priv->hint_actor = hint_actor; + clutter_actor_add_child (CLUTTER_ACTOR (entry), priv->hint_actor); + } + + st_entry_update_hint_visibility (entry); + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_HINT_ACTOR]); + + clutter_actor_queue_relayout (CLUTTER_ACTOR (entry)); +} + +/** + * st_entry_get_hint_actor: + * @entry: a #StEntry + * + * Get the value of the #StEntry:hint-actor property. + * + * Returns: (nullable) (transfer none): a #ClutterActor + */ +ClutterActor * +st_entry_get_hint_actor (StEntry *entry) +{ + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY (entry), NULL); + + priv = ST_ENTRY_PRIV (entry); + return priv->hint_actor; +} + +/******************************************************************************/ +/*************************** ACCESSIBILITY SUPPORT ****************************/ +/******************************************************************************/ + +#define ST_TYPE_ENTRY_ACCESSIBLE (st_entry_accessible_get_type ()) +#define ST_ENTRY_ACCESSIBLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), ST_TYPE_ENTRY_ACCESSIBLE, StEntryAccessible)) +#define ST_IS_ENTRY_ACCESSIBLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), ST_TYPE_ENTRY_ACCESSIBLE)) +#define ST_ENTRY_ACCESSIBLE_CLASS(c) (G_TYPE_CHECK_CLASS_CAST ((c), ST_TYPE_ENTRY_ACCESSIBLE, StEntryAccessibleClass)) +#define ST_IS_ENTRY_ACCESSIBLE_CLASS(c) (G_TYPE_CHECK_CLASS_TYPE ((c), ST_TYPE_ENTRY_ACCESSIBLE)) +#define ST_ENTRY_ACCESSIBLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), ST_TYPE_ENTRY_ACCESSIBLE, StEntryAccessibleClass)) + +typedef struct _StEntryAccessible StEntryAccessible; +typedef struct _StEntryAccessibleClass StEntryAccessibleClass; + +struct _StEntryAccessible +{ + StWidgetAccessible parent; +}; + +struct _StEntryAccessibleClass +{ + StWidgetAccessibleClass parent_class; +}; + +G_DEFINE_TYPE (StEntryAccessible, st_entry_accessible, ST_TYPE_WIDGET_ACCESSIBLE) + +static void +st_entry_accessible_init (StEntryAccessible *self) +{ + /* initialization done on AtkObject->initialize */ +} + +static void +st_entry_accessible_initialize (AtkObject *obj, + gpointer data) +{ + ATK_OBJECT_CLASS (st_entry_accessible_parent_class)->initialize (obj, data); + + /* StEntry is behaving as a ClutterText container */ + atk_object_set_role (obj, ATK_ROLE_PANEL); +} + +static gint +st_entry_accessible_get_n_children (AtkObject *obj) +{ + StEntry *entry = NULL; + StEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_ENTRY_ACCESSIBLE (obj), 0); + + entry = ST_ENTRY (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (entry == NULL) + return 0; + + priv = st_entry_get_instance_private (entry); + if (priv->entry == NULL) + return 0; + else + return 1; +} + +static AtkObject* +st_entry_accessible_ref_child (AtkObject *obj, + gint i) +{ + StEntry *entry = NULL; + StEntryPrivate *priv; + AtkObject *result = NULL; + + g_return_val_if_fail (ST_IS_ENTRY_ACCESSIBLE (obj), NULL); + g_return_val_if_fail (i == 0, NULL); + + entry = ST_ENTRY (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (entry == NULL) + return NULL; + + priv = st_entry_get_instance_private (entry); + if (priv->entry == NULL) + return NULL; + + result = clutter_actor_get_accessible (priv->entry); + g_object_ref (result); + + return result; +} + + +static void +st_entry_accessible_class_init (StEntryAccessibleClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass); + + atk_class->initialize = st_entry_accessible_initialize; + atk_class->get_n_children = st_entry_accessible_get_n_children; + atk_class->ref_child= st_entry_accessible_ref_child; +} diff --git a/src/st/st-entry.h b/src/st/st-entry.h new file mode 100644 index 0000000..2a05759 --- /dev/null +++ b/src/st/st-entry.h @@ -0,0 +1,79 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-entry.h: Plain entry actor + * + * Copyright 2008, 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_ENTRY_H__ +#define __ST_ENTRY_H__ + +G_BEGIN_DECLS + +#include <st/st-widget.h> + +#define ST_TYPE_ENTRY (st_entry_get_type ()) +G_DECLARE_DERIVABLE_TYPE (StEntry, st_entry, ST, ENTRY, StWidget) + +struct _StEntryClass +{ + StWidgetClass parent_class; + + /* signals */ + void (*primary_icon_clicked) (StEntry *entry); + void (*secondary_icon_clicked) (StEntry *entry); +}; + +StWidget *st_entry_new (const gchar *text); +const gchar *st_entry_get_text (StEntry *entry); +void st_entry_set_text (StEntry *entry, + const gchar *text); +ClutterActor *st_entry_get_clutter_text (StEntry *entry); + +void st_entry_set_hint_text (StEntry *entry, + const gchar *text); +const gchar *st_entry_get_hint_text (StEntry *entry); + +void st_entry_set_input_purpose (StEntry *entry, + ClutterInputContentPurpose purpose); +void st_entry_set_input_hints (StEntry *entry, + ClutterInputContentHintFlags hints); + +ClutterInputContentPurpose st_entry_get_input_purpose (StEntry *entry); +ClutterInputContentHintFlags st_entry_get_input_hints (StEntry *entry); + +void st_entry_set_primary_icon (StEntry *entry, + ClutterActor *icon); +ClutterActor * st_entry_get_primary_icon (StEntry *entry); + +void st_entry_set_secondary_icon (StEntry *entry, + ClutterActor *icon); +ClutterActor * st_entry_get_secondary_icon (StEntry *entry); + +void st_entry_set_hint_actor (StEntry *entry, + ClutterActor *hint_actor); +ClutterActor * st_entry_get_hint_actor (StEntry *entry); + +typedef void (*StEntryCursorFunc) (StEntry *entry, gboolean use_ibeam, gpointer data); +void st_entry_set_cursor_func (StEntryCursorFunc func, + gpointer user_data); + +G_END_DECLS + +#endif /* __ST_ENTRY_H__ */ diff --git a/src/st/st-focus-manager.c b/src/st/st-focus-manager.c new file mode 100644 index 0000000..1ac6d28 --- /dev/null +++ b/src/st/st-focus-manager.c @@ -0,0 +1,256 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-focus-manager.c: Keyboard focus manager + * + * Copyright 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-focus-manager + * @short_description: Keyboard focus management + * + * #StFocusManager handles keyboard focus for all actors on the stage. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <clutter/clutter.h> + +#include "st-focus-manager.h" + +struct _StFocusManagerPrivate +{ + GHashTable *groups; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StFocusManager, st_focus_manager, G_TYPE_OBJECT) + +static void +st_focus_manager_dispose (GObject *object) +{ + StFocusManager *manager = ST_FOCUS_MANAGER (object); + + if (manager->priv->groups) + { + g_hash_table_destroy (manager->priv->groups); + manager->priv->groups = NULL; + } + + G_OBJECT_CLASS (st_focus_manager_parent_class)->dispose (object); +} + +static void +st_focus_manager_class_init (StFocusManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = st_focus_manager_dispose; +} + +static void +st_focus_manager_init (StFocusManager *manager) +{ + manager->priv = st_focus_manager_get_instance_private (manager); + manager->priv->groups = g_hash_table_new (NULL, NULL); +} + +static gboolean +st_focus_manager_stage_event (ClutterActor *stage, + ClutterEvent *event, + gpointer user_data) +{ + StFocusManager *manager = user_data; + StDirectionType direction; + gboolean wrap_around = FALSE; + ClutterActor *focused, *group; + + if (event->type != CLUTTER_KEY_PRESS) + return FALSE; + + switch (event->key.keyval) + { + case CLUTTER_KEY_Up: + direction = ST_DIR_UP; + break; + case CLUTTER_KEY_Down: + direction = ST_DIR_DOWN; + break; + case CLUTTER_KEY_Left: + direction = ST_DIR_LEFT; + break; + case CLUTTER_KEY_Right: + direction = ST_DIR_RIGHT; + break; + case CLUTTER_KEY_Tab: + if (event->key.modifier_state & CLUTTER_SHIFT_MASK) + direction = ST_DIR_TAB_BACKWARD; + else + direction = ST_DIR_TAB_FORWARD; + wrap_around = TRUE; + break; + case CLUTTER_KEY_ISO_Left_Tab: + direction = ST_DIR_TAB_BACKWARD; + wrap_around = TRUE; + break; + + default: + return FALSE; + } + + focused = clutter_stage_get_key_focus (CLUTTER_STAGE (stage)); + if (!focused) + return FALSE; + + for (group = focused; group != stage; group = clutter_actor_get_parent (group)) + { + if (g_hash_table_lookup (manager->priv->groups, group)) + { + return st_widget_navigate_focus (ST_WIDGET (group), focused, + direction, wrap_around); + } + } + return FALSE; +} + +/** + * st_focus_manager_get_for_stage: + * @stage: a #ClutterStage + * + * Gets the #StFocusManager for @stage, creating it if necessary. + * + * Returns: (transfer none): the focus manager for @stage + */ +StFocusManager * +st_focus_manager_get_for_stage (ClutterStage *stage) +{ + StFocusManager *manager; + + manager = g_object_get_data (G_OBJECT (stage), "st-focus-manager"); + if (!manager) + { + manager = g_object_new (ST_TYPE_FOCUS_MANAGER, NULL); + g_object_set_data_full (G_OBJECT (stage), "st-focus-manager", + manager, g_object_unref); + + g_signal_connect (stage, "event", + G_CALLBACK (st_focus_manager_stage_event), manager); + } + + return manager; +} + +static void +remove_destroyed_group (ClutterActor *actor, + gpointer user_data) +{ + StFocusManager *manager = user_data; + + st_focus_manager_remove_group (manager, ST_WIDGET (actor)); +} + +/** + * st_focus_manager_add_group: + * @manager: the #StFocusManager + * @root: the root container of the group + * + * Adds a new focus group to @manager. When the focus is in an actor + * that is a descendant of @root, @manager will handle moving focus + * from one actor to another within @root based on keyboard events. + */ +void +st_focus_manager_add_group (StFocusManager *manager, + StWidget *root) +{ + gpointer count_p = g_hash_table_lookup (manager->priv->groups, root); + int count = count_p ? GPOINTER_TO_INT (count_p) : 0; + + g_signal_connect (root, "destroy", + G_CALLBACK (remove_destroyed_group), + manager); + g_hash_table_insert (manager->priv->groups, root, GINT_TO_POINTER (++count)); +} + +/** + * st_focus_manager_remove_group: + * @manager: the #StFocusManager + * @root: the root container of the group + * + * Removes the group rooted at @root from @manager + */ +void +st_focus_manager_remove_group (StFocusManager *manager, + StWidget *root) +{ + gpointer count_p = g_hash_table_lookup (manager->priv->groups, root); + int count = count_p ? GPOINTER_TO_INT (count_p) : 0; + + if (count == 0) + return; + if (count == 1) + g_hash_table_remove (manager->priv->groups, root); + else + g_hash_table_insert (manager->priv->groups, root, GINT_TO_POINTER(--count)); +} + +/** + * st_focus_manager_get_group: + * @manager: the #StFocusManager + * @widget: an #StWidget + * + * Checks if @widget is inside a focus group, and if so, returns + * the root of that group. + * + * Returns: (transfer none): the focus group root, or %NULL if + * @widget is not in a focus group + */ +StWidget * +st_focus_manager_get_group (StFocusManager *manager, + StWidget *widget) +{ + ClutterActor *actor = CLUTTER_ACTOR (widget); + + while (actor && !g_hash_table_lookup (manager->priv->groups, actor)) + actor = clutter_actor_get_parent (actor); + + return ST_WIDGET (actor); +} + +/** + * st_focus_manager_navigate_from_event: + * @manager: the #StFocusManager + * @event: a #ClutterEvent + * + * Try to navigate from @event as if it bubbled all the way up to + * the stage. This is useful in complex event handling situations + * where you want key navigation, but a parent might be stopping + * the key navigation event from bubbling all the way up to the stage. + * + * Returns: Whether a new actor was navigated to + */ +gboolean +st_focus_manager_navigate_from_event (StFocusManager *manager, + ClutterEvent *event) +{ + ClutterActor *stage; + + if (event->type != CLUTTER_KEY_PRESS) + return FALSE; + + stage = CLUTTER_ACTOR (event->key.stage); + return st_focus_manager_stage_event (stage, event, manager); +} diff --git a/src/st/st-focus-manager.h b/src/st/st-focus-manager.h new file mode 100644 index 0000000..ba8442b --- /dev/null +++ b/src/st/st-focus-manager.h @@ -0,0 +1,65 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-focus-manager.h: Keyboard focus manager + * + * Copyright 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_FOCUS_MANAGER_H__ +#define __ST_FOCUS_MANAGER_H__ + +#include <st/st-types.h> +#include <st/st-widget.h> + +G_BEGIN_DECLS + +#define ST_TYPE_FOCUS_MANAGER (st_focus_manager_get_type ()) +G_DECLARE_FINAL_TYPE (StFocusManager, st_focus_manager, ST, FOCUS_MANAGER, GObject) + +typedef struct _StFocusManager StFocusManager; +typedef struct _StFocusManagerPrivate StFocusManagerPrivate; + +/** + * StFocusManager: + * + * The #StFocusManager struct contains only private data + */ +struct _StFocusManager +{ + /*< private >*/ + GObject parent_instance; + + StFocusManagerPrivate *priv; +}; + +StFocusManager *st_focus_manager_get_for_stage (ClutterStage *stage); + +void st_focus_manager_add_group (StFocusManager *manager, + StWidget *root); +void st_focus_manager_remove_group (StFocusManager *manager, + StWidget *root); +StWidget *st_focus_manager_get_group (StFocusManager *manager, + StWidget *widget); +gboolean st_focus_manager_navigate_from_event (StFocusManager *manager, + ClutterEvent *event); + +G_END_DECLS + +#endif /* __ST_FOCUS_MANAGER_H__ */ diff --git a/src/st/st-generic-accessible.c b/src/st/st-generic-accessible.c new file mode 100644 index 0000000..e6cb393 --- /dev/null +++ b/src/st/st-generic-accessible.c @@ -0,0 +1,246 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-generic-accessible.c: generic accessible + * + * Copyright 2013 Igalia, S.L. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-generic-accessible + * @short_description: An accessible class with signals for + * implementing specific Atk interfaces + * + * #StGenericAccessible is mainly a workaround for the current lack of + * of a proper support for GValue at javascript. See bug#703412 for + * more information. We implement the accessible interfaces, but proxy + * the virtual functions into signals, which gjs can catch. + * + * #StGenericAccessible is an #StWidgetAccessible + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "st-generic-accessible.h" + +static void atk_value_iface_init (AtkValueIface *iface); + +G_DEFINE_TYPE_WITH_CODE(StGenericAccessible, + st_generic_accessible, + ST_TYPE_WIDGET_ACCESSIBLE, + G_IMPLEMENT_INTERFACE (ATK_TYPE_VALUE, + atk_value_iface_init)); +/* Signals */ +enum +{ + GET_CURRENT_VALUE, + GET_MAXIMUM_VALUE, + GET_MINIMUM_VALUE, + SET_CURRENT_VALUE, + GET_MINIMUM_INCREMENT, + LAST_SIGNAL +}; + +static guint st_generic_accessible_signals [LAST_SIGNAL] = { 0 }; + +static void +st_generic_accessible_init (StGenericAccessible *accessible) +{ +} + +static void +st_generic_accessible_class_init (StGenericAccessibleClass *klass) +{ + /** + * StGenericAccessible::get-current-value: + * @self: the #StGenericAccessible + * + * Emitted when atk_value_get_current_value() is called on + * @self. Right now we only care about doubles, so the value is + * directly returned by the signal. + * + * Returns: value of the current element. + */ + st_generic_accessible_signals[GET_CURRENT_VALUE] = + g_signal_new ("get-current-value", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_DOUBLE, 0); + + /** + * StGenericAccessible::get-maximum-value: + * @self: the #StGenericAccessible + * + * Emitted when atk_value_get_maximum_value() is called on + * @self. Right now we only care about doubles, so the value is + * directly returned by the signal. + * + * Returns: maximum value of the accessible. + */ + st_generic_accessible_signals[GET_MAXIMUM_VALUE] = + g_signal_new ("get-maximum-value", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_DOUBLE, 0); + + /** + * StGenericAccessible::get-minimum-value: + * @self: the #StGenericAccessible + * + * Emitted when atk_value_get_current_value() is called on + * @self. Right now we only care about doubles, so the value is + * directly returned by the signal. + * + * Returns: minimum value of the accessible. + */ + st_generic_accessible_signals[GET_MINIMUM_VALUE] = + g_signal_new ("get-minimum-value", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_DOUBLE, 0); + + /** + * StGenericAccessible::get-minimum-increment: + * @self: the #StGenericAccessible + * + * Emitted when atk_value_get_minimum_increment() is called on + * @self. Right now we only care about doubles, so the value is + * directly returned by the signal. + * + * Returns: value of the current element. + */ + st_generic_accessible_signals[GET_MINIMUM_INCREMENT] = + g_signal_new ("get-minimum-increment", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_DOUBLE, 0); + + /** + * StGenericAccessible::set-current-value: + * @self: the #StGenericAccessible + * @new_value: the new value for the accessible + * + * Emitted when atk_value_set_current_value() is called on + * @self. Right now we only care about doubles, so the value is + * directly returned by the signal. + */ + st_generic_accessible_signals[SET_CURRENT_VALUE] = + g_signal_new ("set-current-value", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_DOUBLE); + +} + +static void +st_generic_accessible_get_current_value (AtkValue *obj, + GValue *value) +{ + gdouble current_value = 0; + + g_value_init (value, G_TYPE_DOUBLE); + g_signal_emit (G_OBJECT (obj), st_generic_accessible_signals[GET_CURRENT_VALUE], 0, ¤t_value); + g_value_set_double (value, current_value); +} + +static void +st_generic_accessible_get_maximum_value (AtkValue *obj, + GValue *value) +{ + gdouble current_value = 0; + + g_value_init (value, G_TYPE_DOUBLE); + g_signal_emit (G_OBJECT (obj), st_generic_accessible_signals[GET_MAXIMUM_VALUE], 0, ¤t_value); + g_value_set_double (value, current_value); +} + +static void +st_generic_accessible_get_minimum_value (AtkValue *obj, + GValue *value) +{ + gdouble current_value = 0; + + g_value_init (value, G_TYPE_DOUBLE); + g_signal_emit (G_OBJECT (obj), st_generic_accessible_signals[GET_MINIMUM_VALUE], 0, ¤t_value); + g_value_set_double (value, current_value); +} + +static void +st_generic_accessible_get_minimum_increment (AtkValue *obj, + GValue *value) +{ + gdouble current_value = 0; + + g_value_init (value, G_TYPE_DOUBLE); + g_signal_emit (G_OBJECT (obj), st_generic_accessible_signals[GET_MINIMUM_INCREMENT], 0, ¤t_value); + g_value_set_double (value, current_value); +} + +static gboolean +st_generic_accessible_set_current_value (AtkValue *obj, + const GValue *value) +{ + gdouble current_value = 0; + + current_value = g_value_get_double (value); + g_signal_emit (G_OBJECT (obj), st_generic_accessible_signals[SET_CURRENT_VALUE], 0, current_value); + + return TRUE; // we assume that the value was properly set +} + +static void +atk_value_iface_init (AtkValueIface *iface) +{ + iface->get_current_value = st_generic_accessible_get_current_value; + iface->get_maximum_value = st_generic_accessible_get_maximum_value; + iface->get_minimum_value = st_generic_accessible_get_minimum_value; + iface->get_minimum_increment = st_generic_accessible_get_minimum_increment; + iface->set_current_value = st_generic_accessible_set_current_value; +} + +/** + * st_generic_accessible_new_for_actor: + * @actor: a #Clutter Actor + * + * Create a new #StGenericAccessible for @actor. + * + * This is useful only for custom widgets that need a proxy for #AtkObject. + * + * Returns: (transfer full): a new #AtkObject + */ +AtkObject* +st_generic_accessible_new_for_actor (ClutterActor *actor) +{ + AtkObject *accessible = NULL; + + g_return_val_if_fail (CLUTTER_IS_ACTOR (actor), NULL); + + accessible = ATK_OBJECT (g_object_new (ST_TYPE_GENERIC_ACCESSIBLE, + NULL)); + atk_object_initialize (accessible, actor); + + return accessible; +} diff --git a/src/st/st-generic-accessible.h b/src/st/st-generic-accessible.h new file mode 100644 index 0000000..99a6a71 --- /dev/null +++ b/src/st/st-generic-accessible.h @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-generic-accessible.h: generic accessible + * + * Copyright 2013 Igalia, S.L. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_GENERIC_ACCESSIBLE_H__ +#define __ST_GENERIC_ACCESSIBLE_H__ + +#include <clutter/clutter.h> +#include <st/st-widget-accessible.h> + +G_BEGIN_DECLS + +#define ST_TYPE_GENERIC_ACCESSIBLE (st_generic_accessible_get_type ()) +#define ST_GENERIC_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), ST_TYPE_GENERIC_ACCESSIBLE, StGenericAccessible)) +#define ST_GENERIC_ACCESSIBLE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_GENERIC_ACCESSIBLE, StGenericAccessibleClass)) +#define ST_IS_GENERIC_ACCESSIBLE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), ST_TYPE_GENERIC_ACCESSIBLE)) +#define ST_IS_GENERIC_ACCESSIBLE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_GENERIC_ACCESSIBLE)) +#define ST_GENERIC_ACCESSIBLE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_GENERIC_ACCESSIBLE, StGenericAccessibleClass)) + +typedef struct _StGenericAccessible StGenericAccessible; +typedef struct _StGenericAccessibleClass StGenericAccessibleClass; + +typedef struct _StGenericAccessiblePrivate StGenericAccessiblePrivate; + +struct _StGenericAccessible +{ + StWidgetAccessible parent; + + StGenericAccessiblePrivate *priv; +}; + +struct _StGenericAccessibleClass +{ + StWidgetAccessibleClass parent_class; +}; + +GType st_generic_accessible_get_type (void) G_GNUC_CONST; + +AtkObject* st_generic_accessible_new_for_actor (ClutterActor *actor); + +G_END_DECLS + +#endif /* __ST_GENERIC_ACCESSIBLE_H__ */ diff --git a/src/st/st-icon-colors.c b/src/st/st-icon-colors.c new file mode 100644 index 0000000..c6a082a --- /dev/null +++ b/src/st/st-icon-colors.c @@ -0,0 +1,133 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-icon-colors.c: Colors for colorizing a symbolic icon + * + * Copyright 2010 Red Hat, Inc. + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "st-icon-colors.h" + +/** + * st_icon_colors_new: + * + * Creates a new #StIconColors. All colors are initialized to transparent black. + * + * Returns: a newly created #StIconColors. Free with st_icon_colors_unref() + */ +StIconColors * +st_icon_colors_new (void) +{ + StIconColors *colors; + + colors = g_new0 (StIconColors, 1); + colors->ref_count = 1; + + return colors; +} + +/** + * st_icon_colors_ref: + * @colors: a #StIconColors + * + * Atomically increments the reference count of @colors by one. + * + * Returns: the passed in #StIconColors. + */ +StIconColors * +st_icon_colors_ref (StIconColors *colors) +{ + g_return_val_if_fail (colors != NULL, NULL); + g_return_val_if_fail (colors->ref_count > 0, colors); + + g_atomic_int_inc ((volatile int *)&colors->ref_count); + return colors; +} + +/** + * st_icon_colors_unref: + * @colors: a #StIconColors + * + * Atomically decrements the reference count of @colors by one. + * If the reference count drops to 0, all memory allocated by the + * #StIconColors is released. + */ +void +st_icon_colors_unref (StIconColors *colors) +{ + g_return_if_fail (colors != NULL); + g_return_if_fail (colors->ref_count > 0); + + if (g_atomic_int_dec_and_test ((volatile int *)&colors->ref_count)) + g_free (colors); +} + +/** + * st_icon_colors_copy: + * @colors: a #StIconColors + * + * Creates a new StIconColors structure that is a copy of the passed + * in @colors. You would use this function instead of st_icon_colors_ref() + * if you were planning to change colors in the result. + * + * Returns: a newly created #StIconColors. + */ +StIconColors * +st_icon_colors_copy (StIconColors *colors) +{ + StIconColors *copy; + + g_return_val_if_fail (colors != NULL, NULL); + + copy = st_icon_colors_new (); + + copy->foreground = colors->foreground; + copy->warning = colors->warning; + copy->error = colors->error; + copy->success = colors->success; + + return copy; +} + +/** + * st_icon_colors_equal: + * @colors: a #StIconColors + * @other: another #StIconColors + * + * Check if two #StIconColors objects are identical. + * + * Returns: %TRUE if the #StIconColors are equal + */ +gboolean +st_icon_colors_equal (StIconColors *colors, + StIconColors *other) +{ + if (colors == other) + return TRUE; + + if (colors == NULL || other == NULL) + return FALSE; + + return clutter_color_equal (&colors->foreground, &other->foreground) && + clutter_color_equal (&colors->warning, &other->warning) && + clutter_color_equal (&colors->error, &other->error) && + clutter_color_equal (&colors->success, &other->success); +} + +G_DEFINE_BOXED_TYPE (StIconColors, + st_icon_colors, + st_icon_colors_ref, + st_icon_colors_unref) diff --git a/src/st/st-icon-colors.h b/src/st/st-icon-colors.h new file mode 100644 index 0000000..e994a75 --- /dev/null +++ b/src/st/st-icon-colors.h @@ -0,0 +1,43 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_ICON_COLORS__ +#define __ST_ICON_COLORS__ + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define ST_TYPE_ICON_COLORS (st_icon_colors_get_type ()) + +typedef struct _StIconColors StIconColors; + +/** + * StIconColors: + * @foreground: foreground color + * @warning: color indicating a warning state + * @error: color indicating an error state + * @success: color indicating a successful operation + * + * The #StIconColors structure encapsulates colors for colorizing a symbolic + * icon. + */ +struct _StIconColors { + volatile guint ref_count; + + ClutterColor foreground; + ClutterColor warning; + ClutterColor error; + ClutterColor success; +}; + +GType st_icon_colors_get_type (void) G_GNUC_CONST; + +StIconColors *st_icon_colors_new (void); +StIconColors *st_icon_colors_ref (StIconColors *colors); +void st_icon_colors_unref (StIconColors *colors); +StIconColors *st_icon_colors_copy (StIconColors *colors); +gboolean st_icon_colors_equal (StIconColors *colors, + StIconColors *other); + +G_END_DECLS + +#endif /* __ST_ICON_COLORS__ */ diff --git a/src/st/st-icon.c b/src/st/st-icon.c new file mode 100644 index 0000000..6009afe --- /dev/null +++ b/src/st/st-icon.c @@ -0,0 +1,833 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-icon.c: icon widget + * + * Copyright 2009, 2010 Intel Corporation. + * Copyright 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-icon + * @short_description: a simple styled icon actor + * + * #StIcon is a simple styled texture actor that displays an image from + * a stylesheet. + */ + +#include "st-enum-types.h" +#include "st-icon.h" +#include "st-texture-cache.h" +#include "st-theme-context.h" +#include "st-private.h" + +enum +{ + PROP_0, + + PROP_GICON, + PROP_FALLBACK_GICON, + + PROP_ICON_NAME, + PROP_ICON_SIZE, + PROP_FALLBACK_ICON_NAME, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +struct _StIconPrivate +{ + ClutterActor *icon_texture; + ClutterActor *pending_texture; + gulong opacity_handler_id; + + GIcon *gicon; + gint prop_icon_size; /* icon size set as property */ + gint theme_icon_size; /* icon size from theme node */ + gint icon_size; /* icon size we are using */ + GIcon *fallback_gicon; + gboolean needs_update; + + StIconColors *colors; + + CoglPipeline *shadow_pipeline; + StShadow *shadow_spec; + graphene_size_t shadow_size; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StIcon, st_icon, ST_TYPE_WIDGET) + +static void st_icon_update (StIcon *icon); +static gboolean st_icon_update_icon_size (StIcon *icon); +static void st_icon_update_shadow_pipeline (StIcon *icon); +static void st_icon_clear_shadow_pipeline (StIcon *icon); + +static GIcon *default_gicon = NULL; + +#define IMAGE_MISSING_ICON_NAME "image-missing" +#define DEFAULT_ICON_SIZE 48 + +static void +st_icon_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StIcon *icon = ST_ICON (gobject); + + switch (prop_id) + { + case PROP_GICON: + st_icon_set_gicon (icon, g_value_get_object (value)); + break; + + case PROP_FALLBACK_GICON: + st_icon_set_fallback_gicon (icon, g_value_get_object (value)); + break; + + case PROP_ICON_NAME: + st_icon_set_icon_name (icon, g_value_get_string (value)); + break; + + case PROP_ICON_SIZE: + st_icon_set_icon_size (icon, g_value_get_int (value)); + break; + + case PROP_FALLBACK_ICON_NAME: + st_icon_set_fallback_icon_name (icon, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_icon_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StIcon *icon = ST_ICON (gobject); + + switch (prop_id) + { + case PROP_GICON: + g_value_set_object (value, st_icon_get_gicon (icon)); + break; + + case PROP_FALLBACK_GICON: + g_value_set_object (value, st_icon_get_fallback_gicon (icon)); + break; + + case PROP_ICON_NAME: + g_value_set_string (value, st_icon_get_icon_name (icon)); + break; + + case PROP_ICON_SIZE: + g_value_set_int (value, st_icon_get_icon_size (icon)); + break; + + case PROP_FALLBACK_ICON_NAME: + g_value_set_string (value, st_icon_get_fallback_icon_name (icon)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_icon_dispose (GObject *gobject) +{ + StIconPrivate *priv = ST_ICON (gobject)->priv; + + if (priv->icon_texture) + { + clutter_actor_destroy (priv->icon_texture); + priv->icon_texture = NULL; + } + + if (priv->pending_texture) + { + clutter_actor_destroy (priv->pending_texture); + g_object_unref (priv->pending_texture); + priv->pending_texture = NULL; + } + + g_clear_object (&priv->gicon); + g_clear_object (&priv->fallback_gicon); + g_clear_pointer (&priv->colors, st_icon_colors_unref); + g_clear_pointer (&priv->shadow_pipeline, cogl_object_unref); + g_clear_pointer (&priv->shadow_spec, st_shadow_unref); + + G_OBJECT_CLASS (st_icon_parent_class)->dispose (gobject); +} + +static void +st_icon_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + StIcon *icon = ST_ICON (actor); + StIconPrivate *priv = icon->priv; + + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + if (priv->icon_texture) + { + st_icon_update_shadow_pipeline (icon); + + if (priv->shadow_pipeline) + { + ClutterActorBox allocation; + CoglFramebuffer *framebuffer; + + clutter_actor_get_allocation_box (priv->icon_texture, &allocation); + framebuffer = clutter_paint_context_get_framebuffer (paint_context); + _st_paint_shadow_with_opacity (priv->shadow_spec, + framebuffer, + priv->shadow_pipeline, + &allocation, + clutter_actor_get_paint_opacity (priv->icon_texture)); + } + + clutter_actor_paint (priv->icon_texture, paint_context); + } +} + +static void +st_icon_style_changed (StWidget *widget) +{ + StIcon *self = ST_ICON (widget); + StThemeNode *theme_node = st_widget_get_theme_node (widget); + StIconPrivate *priv = self->priv; + gboolean should_update = FALSE; + g_autoptr(StShadow) shadow_spec = NULL; + StIconColors *colors; + + shadow_spec = st_theme_node_get_shadow (theme_node, "icon-shadow"); + + if (shadow_spec && shadow_spec->inset) + { + g_warning ("The icon-shadow property does not support inset shadows"); + g_clear_pointer (&shadow_spec, st_shadow_unref); + } + + if ((shadow_spec && priv->shadow_spec && !st_shadow_equal (shadow_spec, priv->shadow_spec)) || + (shadow_spec && !priv->shadow_spec) || (!shadow_spec && priv->shadow_spec)) + { + st_icon_clear_shadow_pipeline (self); + + g_clear_pointer (&priv->shadow_spec, st_shadow_unref); + priv->shadow_spec = g_steal_pointer (&shadow_spec); + + should_update = TRUE; + } + + colors = st_theme_node_get_icon_colors (theme_node); + + if ((colors && priv->colors && !st_icon_colors_equal (colors, priv->colors)) || + (colors && !priv->colors) || (!colors && priv->colors)) + { + g_clear_pointer (&priv->colors, st_icon_colors_unref); + priv->colors = st_icon_colors_ref (colors); + + should_update = TRUE; + } + + priv->theme_icon_size = (int)(0.5 + st_theme_node_get_length (theme_node, "icon-size")); + + should_update |= st_icon_update_icon_size (self); + + if (priv->needs_update || should_update) + st_icon_update (self); + + ST_WIDGET_CLASS (st_icon_parent_class)->style_changed (widget); +} + +static void +st_icon_resource_scale_changed (ClutterActor *actor) +{ + st_icon_update (ST_ICON (actor)); + + if (CLUTTER_ACTOR_CLASS (st_icon_parent_class)->resource_scale_changed) + CLUTTER_ACTOR_CLASS (st_icon_parent_class)->resource_scale_changed (actor); +} + +static void +st_icon_class_init (StIconClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + object_class->get_property = st_icon_get_property; + object_class->set_property = st_icon_set_property; + object_class->dispose = st_icon_dispose; + + actor_class->paint = st_icon_paint; + + widget_class->style_changed = st_icon_style_changed; + actor_class->resource_scale_changed = st_icon_resource_scale_changed; + + /** + * StIcon:gicon: + * + * The #GIcon being displayed by this #StIcon. + */ + props[PROP_GICON] = + g_param_spec_object ("gicon", + "GIcon", + "The GIcon shown by this icon actor", + G_TYPE_ICON, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StIcon:fallback-gicon: + * + * The fallback #GIcon to display if #StIcon:gicon fails to load. + */ + props[PROP_FALLBACK_GICON] = + g_param_spec_object ("fallback-gicon", + "Fallback GIcon", + "The fallback GIcon shown if the normal icon fails to load", + G_TYPE_ICON, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StIcon:icon-name: + * + * The name of the icon if the icon being displayed is a #GThemedIcon. + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon name", + "An icon name", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StIcon:icon-size: + * + * The size of the icon, if greater than `0`. Other the icon size is derived + * from the current style. + */ + props[PROP_ICON_SIZE] = + g_param_spec_int ("icon-size", + "Icon size", + "The size if the icon, if positive. Otherwise the size will be derived from the current style", + -1, G_MAXINT, -1, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StIcon:fallback-icon-name: + * + * The fallback icon name of the #StIcon. See st_icon_set_fallback_icon_name() + * for details. + */ + props[PROP_FALLBACK_ICON_NAME] = + g_param_spec_string ("fallback-icon-name", + "Fallback icon name", + "A fallback icon name", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +st_icon_init (StIcon *self) +{ + ClutterLayoutManager *layout_manager; + + if (G_UNLIKELY (default_gicon == NULL)) + default_gicon = g_themed_icon_new (IMAGE_MISSING_ICON_NAME); + + self->priv = st_icon_get_instance_private (self); + + layout_manager = clutter_bin_layout_new (CLUTTER_BIN_ALIGNMENT_FILL, + CLUTTER_BIN_ALIGNMENT_FILL); + clutter_actor_set_layout_manager (CLUTTER_ACTOR (self), layout_manager); + + /* Set the icon size to -1 here to make sure we apply the scale to the + * default size on the first "style-changed" signal. */ + self->priv->icon_size = -1; + self->priv->prop_icon_size = -1; + + self->priv->shadow_pipeline = NULL; +} + +static void +st_icon_clear_shadow_pipeline (StIcon *icon) +{ + StIconPrivate *priv = icon->priv; + + g_clear_pointer (&priv->shadow_pipeline, cogl_object_unref); + graphene_size_init (&priv->shadow_size, 0, 0); +} + +static void +st_icon_update_shadow_pipeline (StIcon *icon) +{ + StIconPrivate *priv = icon->priv; + + if (priv->icon_texture && priv->shadow_spec) + { + ClutterActorBox box; + float width, height; + + clutter_actor_get_allocation_box (CLUTTER_ACTOR (priv->icon_texture), + &box); + clutter_actor_box_get_size (&box, &width, &height); + + if (priv->shadow_pipeline == NULL || + priv->shadow_size.width != width || + priv->shadow_size.height != height) + { + st_icon_clear_shadow_pipeline (icon); + + priv->shadow_pipeline = + _st_create_shadow_pipeline_from_actor (priv->shadow_spec, + priv->icon_texture); + + if (priv->shadow_pipeline) + graphene_size_init (&priv->shadow_size, width, height); + } + } +} + +static void +on_content_changed (ClutterActor *actor, + GParamSpec *pspec, + StIcon *icon) +{ + st_icon_clear_shadow_pipeline (icon); +} + +static void +st_icon_finish_update (StIcon *icon) +{ + StIconPrivate *priv = icon->priv; + + if (priv->icon_texture) + { + clutter_actor_destroy (priv->icon_texture); + priv->icon_texture = NULL; + } + + if (priv->pending_texture) + { + priv->icon_texture = priv->pending_texture; + priv->pending_texture = NULL; + clutter_actor_set_x_align (priv->icon_texture, CLUTTER_ACTOR_ALIGN_CENTER); + clutter_actor_set_y_align (priv->icon_texture, CLUTTER_ACTOR_ALIGN_CENTER); + clutter_actor_add_child (CLUTTER_ACTOR (icon), priv->icon_texture); + + /* Remove the temporary ref we added */ + g_object_unref (priv->icon_texture); + + st_icon_clear_shadow_pipeline (icon); + + g_signal_connect_object (priv->icon_texture, "notify::content", + G_CALLBACK (on_content_changed), icon, 0); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (icon)); +} + +static void +opacity_changed_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + StIcon *icon = user_data; + StIconPrivate *priv = icon->priv; + + g_clear_signal_handler (&priv->opacity_handler_id, priv->pending_texture); + + st_icon_finish_update (icon); +} + +static void +st_icon_update (StIcon *icon) +{ + StIconPrivate *priv = icon->priv; + StThemeNode *theme_node; + StTextureCache *cache; + gint paint_scale; + ClutterActor *stage; + StThemeContext *context; + float resource_scale; + + if (priv->pending_texture) + { + clutter_actor_destroy (priv->pending_texture); + g_object_unref (priv->pending_texture); + priv->pending_texture = NULL; + priv->opacity_handler_id = 0; + } + + if (priv->gicon == NULL && priv->fallback_gicon == NULL) + { + g_clear_pointer (&priv->icon_texture, clutter_actor_destroy); + return; + } + + priv->needs_update = TRUE; + + theme_node = st_widget_peek_theme_node (ST_WIDGET (icon)); + if (theme_node == NULL) + return; + + if (priv->icon_size <= 0) + return; + + resource_scale = clutter_actor_get_resource_scale (CLUTTER_ACTOR (icon)); + + stage = clutter_actor_get_stage (CLUTTER_ACTOR (icon)); + context = st_theme_context_get_for_stage (CLUTTER_STAGE (stage)); + g_object_get (context, "scale-factor", &paint_scale, NULL); + + cache = st_texture_cache_get_default (); + + if (priv->gicon != NULL) + priv->pending_texture = st_texture_cache_load_gicon (cache, + theme_node, + priv->gicon, + priv->icon_size / paint_scale, + paint_scale, + resource_scale); + + if (priv->pending_texture == NULL && priv->fallback_gicon != NULL) + priv->pending_texture = st_texture_cache_load_gicon (cache, + theme_node, + priv->fallback_gicon, + priv->icon_size / paint_scale, + paint_scale, + resource_scale); + + if (priv->pending_texture == NULL) + priv->pending_texture = st_texture_cache_load_gicon (cache, + theme_node, + default_gicon, + priv->icon_size / paint_scale, + paint_scale, + resource_scale); + priv->needs_update = FALSE; + + if (priv->pending_texture) + { + g_object_ref_sink (priv->pending_texture); + + if (clutter_actor_get_opacity (priv->pending_texture) != 0 || priv->icon_texture == NULL) + { + /* This icon is ready for showing, or nothing else is already showing */ + st_icon_finish_update (icon); + } + else + { + /* Will be shown when fully loaded */ + priv->opacity_handler_id = g_signal_connect_object (priv->pending_texture, "notify::opacity", G_CALLBACK (opacity_changed_cb), icon, 0); + } + } + else if (priv->icon_texture) + { + clutter_actor_destroy (priv->icon_texture); + priv->icon_texture = NULL; + } +} + +static gboolean +st_icon_update_icon_size (StIcon *icon) +{ + StIconPrivate *priv = icon->priv; + int new_size; + gint scale = 1; + ClutterActor *stage; + StThemeContext *context; + + stage = clutter_actor_get_stage (CLUTTER_ACTOR (icon)); + if (stage != NULL) + { + context = st_theme_context_get_for_stage (CLUTTER_STAGE (stage)); + g_object_get (context, "scale-factor", &scale, NULL); + } + + if (priv->prop_icon_size > 0) + new_size = priv->prop_icon_size * scale; + else if (priv->theme_icon_size > 0) + new_size = priv->theme_icon_size; + else + new_size = DEFAULT_ICON_SIZE * scale; + + if (new_size != priv->icon_size) + { + priv->icon_size = new_size; + return TRUE; + } + + return FALSE; +} + +/** + * st_icon_new: + * + * Create a newly allocated #StIcon. + * + * Returns: A newly allocated #StIcon + */ +ClutterActor * +st_icon_new (void) +{ + return g_object_new (ST_TYPE_ICON, NULL); +} + +/** + * st_icon_get_icon_name: + * @icon: an #StIcon + * + * This is a convenience method to get the icon name of the current icon, if it + * is currenyly a #GThemedIcon, or %NULL otherwise. + * + * Returns: (transfer none) (nullable): The name of the icon or %NULL + */ +const gchar * +st_icon_get_icon_name (StIcon *icon) +{ + StIconPrivate *priv; + + g_return_val_if_fail (ST_IS_ICON (icon), NULL); + + priv = icon->priv; + + if (priv->gicon && G_IS_THEMED_ICON (priv->gicon)) + return g_themed_icon_get_names (G_THEMED_ICON (priv->gicon)) [0]; + else + return NULL; +} + +/** + * st_icon_set_icon_name: + * @icon: an #StIcon + * @icon_name: (nullable): the name of the icon + * + * This is a convenience method to set the #GIcon to a #GThemedIcon created + * using the given icon name. If @icon_name is an empty string, %NULL or + * fails to load, the fallback icon will be shown. + */ +void +st_icon_set_icon_name (StIcon *icon, + const gchar *icon_name) +{ + g_autoptr(GIcon) gicon = NULL; + + g_return_if_fail (ST_IS_ICON (icon)); + + if (g_strcmp0 (icon_name, st_icon_get_icon_name (icon)) == 0) + return; + + if (icon_name && *icon_name) + gicon = g_themed_icon_new_with_default_fallbacks (icon_name); + + g_object_freeze_notify (G_OBJECT (icon)); + + st_icon_set_gicon (icon, gicon); + g_object_notify_by_pspec (G_OBJECT (icon), props[PROP_ICON_NAME]); + + g_object_thaw_notify (G_OBJECT (icon)); +} + +/** + * st_icon_get_gicon: + * @icon: an #StIcon + * + * Gets the current #GIcon in use. + * + * Returns: (nullable) (transfer none): The current #GIcon, if set, otherwise %NULL + */ +GIcon * +st_icon_get_gicon (StIcon *icon) +{ + g_return_val_if_fail (ST_IS_ICON (icon), NULL); + + return icon->priv->gicon; +} + +/** + * st_icon_set_gicon: + * @icon: an #StIcon + * @gicon: (nullable): a #GIcon + * + * Sets a #GIcon to show for the icon. If @gicon is %NULL or fails to load, + * the fallback icon set using st_icon_set_fallback_icon() will be shown. + */ +void +st_icon_set_gicon (StIcon *icon, GIcon *gicon) +{ + g_return_if_fail (ST_IS_ICON (icon)); + g_return_if_fail (gicon == NULL || G_IS_ICON (gicon)); + + if (g_icon_equal (icon->priv->gicon, gicon)) /* do nothing */ + return; + + g_set_object (&icon->priv->gicon, gicon); + g_object_notify_by_pspec (G_OBJECT (icon), props[PROP_GICON]); + + st_icon_update (icon); +} + +/** + * st_icon_get_fallback_gicon: + * @icon: a #StIcon + * + * Gets the currently set fallback #GIcon. + * + * Returns: (transfer none): The fallback #GIcon, if set, otherwise %NULL + */ +GIcon * +st_icon_get_fallback_gicon (StIcon *icon) +{ + g_return_val_if_fail (ST_IS_ICON (icon), NULL); + + return icon->priv->fallback_gicon; +} + +/** + * st_icon_set_fallback_gicon: + * @icon: a #StIcon + * @fallback_gicon: (nullable): the fallback #GIcon + * + * Sets a fallback #GIcon to show if the normal icon fails to load. + * If @fallback_gicon is %NULL or fails to load, the icon is unset and no + * texture will be visible for the fallback icon. + */ +void +st_icon_set_fallback_gicon (StIcon *icon, + GIcon *fallback_gicon) +{ + g_return_if_fail (ST_IS_ICON (icon)); + g_return_if_fail (fallback_gicon == NULL || G_IS_ICON (fallback_gicon)); + + if (g_icon_equal (icon->priv->fallback_gicon, fallback_gicon)) + return; + + g_set_object (&icon->priv->fallback_gicon, fallback_gicon); + g_object_notify_by_pspec (G_OBJECT (icon), props[PROP_FALLBACK_GICON]); + + st_icon_update (icon); +} + +/** + * st_icon_get_icon_size: + * @icon: an #StIcon + * + * Gets the explicit size set using st_icon_set_icon_size() for the icon. + * This is not necessarily the size that the icon will be displayed at. + * + * Returns: The explicitly set size, or -1 if no size has been set + */ +gint +st_icon_get_icon_size (StIcon *icon) +{ + g_return_val_if_fail (ST_IS_ICON (icon), -1); + + return icon->priv->prop_icon_size; +} + +/** + * st_icon_set_icon_size: + * @icon: an #StIcon + * @size: if positive, the new size, otherwise the size will be + * derived from the current style + * + * Sets an explicit size for the icon. Setting @size to -1 will use the size + * defined by the current style or the default icon size. + */ +void +st_icon_set_icon_size (StIcon *icon, + gint size) +{ + StIconPrivate *priv; + + g_return_if_fail (ST_IS_ICON (icon)); + + priv = icon->priv; + if (priv->prop_icon_size != size) + { + priv->prop_icon_size = size; + if (st_icon_update_icon_size (icon)) + st_icon_update (icon); + g_object_notify_by_pspec (G_OBJECT (icon), props[PROP_ICON_SIZE]); + } +} + +/** + * st_icon_get_fallback_icon_name: + * @icon: an #StIcon + * + * This is a convenience method to get the icon name of the fallback + * #GThemedIcon that is currently set. + * + * Returns: (transfer none): The name of the icon or %NULL if no icon is set + */ +const gchar * +st_icon_get_fallback_icon_name (StIcon *icon) +{ + StIconPrivate *priv; + + g_return_val_if_fail (ST_IS_ICON (icon), NULL); + + priv = icon->priv; + + if (priv->fallback_gicon && G_IS_THEMED_ICON (priv->fallback_gicon)) + return g_themed_icon_get_names (G_THEMED_ICON (priv->fallback_gicon)) [0]; + else + return NULL; +} + +/** + * st_icon_set_fallback_icon_name: + * @icon: an #StIcon + * @fallback_icon_name: (nullable): the name of the fallback icon + * + * This is a convenience method to set the fallback #GIcon to a #GThemedIcon + * created using the given icon name. If @fallback_icon_name is an empty + * string, %NULL or fails to load, the icon is unset and no texture will + * be visible for the fallback icon. + */ +void +st_icon_set_fallback_icon_name (StIcon *icon, + const gchar *fallback_icon_name) +{ + g_autoptr(GIcon) gicon = NULL; + + g_return_if_fail (ST_IS_ICON (icon)); + + if (g_strcmp0 (fallback_icon_name, st_icon_get_fallback_icon_name (icon)) == 0) + return; + + if (fallback_icon_name && *fallback_icon_name) + gicon = g_themed_icon_new_with_default_fallbacks (fallback_icon_name); + + g_object_freeze_notify (G_OBJECT (icon)); + + st_icon_set_fallback_gicon (icon, gicon); + g_object_notify_by_pspec (G_OBJECT (icon), props[PROP_FALLBACK_ICON_NAME]); + + g_object_thaw_notify (G_OBJECT (icon)); +} diff --git a/src/st/st-icon.h b/src/st/st-icon.h new file mode 100644 index 0000000..8714ef9 --- /dev/null +++ b/src/st/st-icon.h @@ -0,0 +1,82 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-icon.h: icon widget + * + * Copyright 2009, 2010 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + * + * Written by: Thomas Wood <thomas.wood@intel.com> + * + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef _ST_ICON +#define _ST_ICON + +#include <glib-object.h> +#include <gio/gio.h> +#include <st/st-widget.h> + +#include <st/st-types.h> + +G_BEGIN_DECLS + +#define ST_TYPE_ICON st_icon_get_type() +G_DECLARE_FINAL_TYPE (StIcon, st_icon, ST, ICON, StWidget) + +typedef struct _StIconPrivate StIconPrivate; + +/** + * StIcon: + * + * The contents of this structure are private and should only be accessed + * through the public API. + */ +struct _StIcon { + /*< private >*/ + StWidget parent; + + StIconPrivate *priv; +}; + +ClutterActor* st_icon_new (void); + +GIcon *st_icon_get_gicon (StIcon *icon); +void st_icon_set_gicon (StIcon *icon, + GIcon *gicon); + +GIcon *st_icon_get_fallback_gicon (StIcon *icon); +void st_icon_set_fallback_gicon (StIcon *icon, + GIcon *fallback_gicon); + +const gchar *st_icon_get_icon_name (StIcon *icon); +void st_icon_set_icon_name (StIcon *icon, + const gchar *icon_name); + +const gchar *st_icon_get_fallback_icon_name (StIcon *icon); +void st_icon_set_fallback_icon_name (StIcon *icon, + const gchar *fallback_icon_name); + +gint st_icon_get_icon_size (StIcon *icon); +void st_icon_set_icon_size (StIcon *icon, + gint size); + +G_END_DECLS + +#endif /* _ST_ICON */ + diff --git a/src/st/st-image-content.c b/src/st/st-image-content.c new file mode 100644 index 0000000..92f1c14 --- /dev/null +++ b/src/st/st-image-content.c @@ -0,0 +1,346 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-image-content.h: A content image with scaling support + * + * Copyright 2019 Canonical, Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "st-image-content.h" +#include "st-private.h" + +struct _StImageContent +{ + /*< private >*/ + ClutterImage parent_instance; +}; + +typedef struct _StImageContentPrivate StImageContentPrivate; +struct _StImageContentPrivate +{ + int width; + int height; +}; + +enum +{ + PROP_0, + PROP_PREFERRED_WIDTH, + PROP_PREFERRED_HEIGHT, +}; + +static void clutter_content_interface_init (ClutterContentInterface *iface); +static void g_icon_interface_init (GIconIface *iface); +static void g_loadable_icon_interface_init (GLoadableIconIface *iface); + +G_DEFINE_TYPE_WITH_CODE (StImageContent, st_image_content, CLUTTER_TYPE_IMAGE, + G_ADD_PRIVATE (StImageContent) + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTENT, + clutter_content_interface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_ICON, + g_icon_interface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_LOADABLE_ICON, + g_loadable_icon_interface_init)) + +static void +st_image_content_init (StImageContent *self) +{ +} + +static void +st_image_content_constructed (GObject *object) +{ + StImageContent *self = ST_IMAGE_CONTENT (object); + StImageContentPrivate *priv = st_image_content_get_instance_private (self); + + if (priv->width < 0 || priv->height < 0) + g_warning ("StImageContent initialized with invalid preferred size: %dx%d\n", + priv->width, priv->height); +} + +static void +st_image_content_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StImageContent *self = ST_IMAGE_CONTENT (object); + StImageContentPrivate *priv = st_image_content_get_instance_private (self); + + switch (prop_id) + { + case PROP_PREFERRED_WIDTH: + g_value_set_int (value, priv->width); + break; + + case PROP_PREFERRED_HEIGHT: + g_value_set_int (value, priv->height); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_image_content_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StImageContent *self = ST_IMAGE_CONTENT (object); + StImageContentPrivate *priv = st_image_content_get_instance_private (self); + + switch (prop_id) + { + case PROP_PREFERRED_WIDTH: + priv->width = g_value_get_int (value); + break; + + case PROP_PREFERRED_HEIGHT: + priv->height = g_value_get_int (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_image_content_class_init (StImageContentClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = st_image_content_constructed; + object_class->get_property = st_image_content_get_property; + object_class->set_property = st_image_content_set_property; + + pspec = g_param_spec_int ("preferred-width", + "Preferred Width", + "Preferred Width of the Content when painted", + -1, G_MAXINT, -1, + G_PARAM_CONSTRUCT_ONLY | ST_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PREFERRED_WIDTH, pspec); + + pspec = g_param_spec_int ("preferred-height", + "Preferred Height", + "Preferred Height of the Content when painted", + -1, G_MAXINT, -1, + G_PARAM_CONSTRUCT_ONLY | ST_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PREFERRED_HEIGHT, pspec); +} + +static gboolean +st_image_content_get_preferred_size (ClutterContent *content, + float *width, + float *height) +{ + StImageContent *self = ST_IMAGE_CONTENT (content); + StImageContentPrivate *priv = st_image_content_get_instance_private (self); + CoglTexture *texture; + + texture = clutter_image_get_texture (CLUTTER_IMAGE (content)); + + if (texture == NULL) + return FALSE; + + g_assert_cmpint (priv->width, >, -1); + g_assert_cmpint (priv->height, >, -1); + + if (width != NULL) + *width = (float) priv->width; + + if (height != NULL) + *height = (float) priv->height; + + return TRUE; +} + +static GdkPixbuf* +pixbuf_from_image (StImageContent *image) +{ + CoglTexture *texture; + int width, height, rowstride; + uint8_t *data; + + texture = clutter_image_get_texture (CLUTTER_IMAGE (image)); + if (!texture || !cogl_texture_is_get_data_supported (texture)) + return NULL; + + width = cogl_texture_get_width (texture); + height = cogl_texture_get_width (texture); + rowstride = 4 * width; + data = g_new (uint8_t, rowstride * height); + + cogl_texture_get_data (texture, COGL_PIXEL_FORMAT_RGBA_8888, rowstride, data); + + return gdk_pixbuf_new_from_data ((const guchar *)data, + GDK_COLORSPACE_RGB, + TRUE, 8, width, height, rowstride, + (GdkPixbufDestroyNotify)g_free, NULL); +} + +static void +clutter_content_interface_init (ClutterContentInterface *iface) +{ + iface->get_preferred_size = st_image_content_get_preferred_size; +} + +static guint +st_image_content_hash (GIcon *icon) +{ + return g_direct_hash (icon); +} + +static gboolean +st_image_content_equal (GIcon *icon1, + GIcon *icon2) +{ + return g_direct_equal (icon1, icon2); +} + +static GVariant * +st_image_content_serialize (GIcon *icon) +{ + g_autoptr (GdkPixbuf) pixbuf = NULL; + + pixbuf = pixbuf_from_image (ST_IMAGE_CONTENT (icon)); + if (!pixbuf) + return NULL; + + return g_icon_serialize (G_ICON (pixbuf)); +} + +static void +g_icon_interface_init (GIconIface *iface) +{ + iface->hash = st_image_content_hash; + iface->equal = st_image_content_equal; + iface->serialize = st_image_content_serialize; +} + +static GInputStream * +st_image_load (GLoadableIcon *icon, + int size, + char **type, + GCancellable *cancellable, + GError **error) +{ + g_autoptr (GdkPixbuf) pixbuf = NULL; + + pixbuf = pixbuf_from_image (ST_IMAGE_CONTENT (icon)); + if (!pixbuf) + { + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to read texture"); + return NULL; + } + + return g_loadable_icon_load (G_LOADABLE_ICON (pixbuf), + size, type, cancellable, error); +} + +static void +load_image_thread (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + GInputStream *stream; + GError *error = NULL; + char *type; + + stream = st_image_load (G_LOADABLE_ICON (object), + GPOINTER_TO_INT (task_data), + &type, + cancellable, + &error); + + if (error) + { + g_task_return_error (task, error); + } + else + { + g_task_set_task_data (task, type, g_free); + g_task_return_pointer (task, stream, g_object_unref); + } +} + +static void +st_image_load_async (GLoadableIcon *icon, + int size, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + + task = g_task_new (icon, cancellable, callback, user_data); + g_task_set_task_data (task, GINT_TO_POINTER (size), NULL); + g_task_run_in_thread (task, load_image_thread); +} + +static GInputStream * +st_image_load_finish (GLoadableIcon *icon, + GAsyncResult *res, + char **type, + GError **error) +{ + GInputStream *stream; + + stream = g_task_propagate_pointer (G_TASK (res), error); + if (!stream) + return NULL; + + if (type) + *type = g_strdup (g_task_get_task_data (G_TASK (res))); + + return stream; +} + +static void +g_loadable_icon_interface_init (GLoadableIconIface *iface) +{ + iface->load = st_image_load; + iface->load_async = st_image_load_async; + iface->load_finish = st_image_load_finish; +} + +/** + * st_image_content_new_with_preferred_size: + * @width: The preferred width to be used when drawing the content + * @height: The preferred width to be used when drawing the content + * + * Creates a new #StImageContent, a simple content for sized images. + * + * See #ClutterImage for setting the actual image to display or #StIcon for + * displaying icons. + * + * Returns: (transfer full): the newly created #StImageContent content + * Use g_object_unref() when done. + */ +ClutterContent * +st_image_content_new_with_preferred_size (int width, + int height) +{ + return g_object_new (ST_TYPE_IMAGE_CONTENT, + "preferred-width", width, + "preferred-height", height, + NULL); +} diff --git a/src/st/st-image-content.h b/src/st/st-image-content.h new file mode 100644 index 0000000..0ebb0b7 --- /dev/null +++ b/src/st/st-image-content.h @@ -0,0 +1,33 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-image-content.h: A content image with scaling support + * + * Copyright 2019 Canonical, Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_IMAGE_CONTENT_H__ +#define __ST_IMAGE_CONTENT_H__ + +#include <clutter/clutter.h> + +#define ST_TYPE_IMAGE_CONTENT (st_image_content_get_type ()) +G_DECLARE_FINAL_TYPE (StImageContent, st_image_content, + ST, IMAGE_CONTENT, ClutterImage) + +ClutterContent *st_image_content_new_with_preferred_size (int width, + int height); + +#endif /* __ST_IMAGE_CONTENT_H__ */ diff --git a/src/st/st-label.c b/src/st/st-label.c new file mode 100644 index 0000000..fe77743 --- /dev/null +++ b/src/st/st-label.c @@ -0,0 +1,549 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-label.c: Plain label actor + * + * Copyright 2008,2009 Intel Corporation + * Copyright 2009 Red Hat, Inc. + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-label + * @short_description: Widget for displaying text + * + * #StLabel is a simple widget for displaying text. It derives from + * #StWidget to add extra style and placement functionality over + * #ClutterText. The internal #ClutterText is publicly accessibly to allow + * applications to set further properties. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <string.h> + +#include <glib.h> + +#include <clutter/clutter.h> + +#include "st-label.h" +#include "st-private.h" +#include "st-widget.h" + +#include <st/st-widget-accessible.h> + +enum +{ + PROP_0, + + PROP_CLUTTER_TEXT, + PROP_TEXT, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +struct _StLabelPrivate +{ + ClutterActor *label; + + StShadow *shadow_spec; + + CoglPipeline *text_shadow_pipeline; + float shadow_width; + float shadow_height; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StLabel, st_label, ST_TYPE_WIDGET); + +static GType st_label_accessible_get_type (void) G_GNUC_CONST; + +static void +st_label_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StLabel *label = ST_LABEL (gobject); + + switch (prop_id) + { + case PROP_TEXT: + st_label_set_text (label, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_label_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StLabelPrivate *priv = ST_LABEL (gobject)->priv; + + switch (prop_id) + { + case PROP_CLUTTER_TEXT: + g_value_set_object (value, priv->label); + break; + + case PROP_TEXT: + g_value_set_string (value, clutter_text_get_text (CLUTTER_TEXT (priv->label))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_label_style_changed (StWidget *self) +{ + StLabelPrivate *priv = ST_LABEL(self)->priv; + StThemeNode *theme_node; + StShadow *shadow_spec; + + theme_node = st_widget_get_theme_node (self); + + shadow_spec = st_theme_node_get_text_shadow (theme_node); + if (!priv->shadow_spec || !shadow_spec || + !st_shadow_equal (shadow_spec, priv->shadow_spec)) + { + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); + + g_clear_pointer (&priv->shadow_spec, st_shadow_unref); + if (shadow_spec) + priv->shadow_spec = st_shadow_ref (shadow_spec); + } + + _st_set_text_from_style ((ClutterText *)priv->label, st_widget_get_theme_node (self)); + + ST_WIDGET_CLASS (st_label_parent_class)->style_changed (self); +} + +static void +st_label_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StLabelPrivate *priv = ST_LABEL (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + + st_theme_node_adjust_for_height (theme_node, &for_height); + + clutter_actor_get_preferred_width (priv->label, for_height, + min_width_p, + natural_width_p); + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_label_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StLabelPrivate *priv = ST_LABEL (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + + st_theme_node_adjust_for_width (theme_node, &for_width); + + clutter_actor_get_preferred_height (priv->label, for_width, + min_height_p, + natural_height_p); + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_label_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StLabelPrivate *priv = ST_LABEL (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox content_box; + + clutter_actor_set_allocation (actor, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + clutter_actor_allocate (priv->label, &content_box); +} + +static void +st_label_dispose (GObject *object) +{ + StLabelPrivate *priv = ST_LABEL (object)->priv; + + priv->label = NULL; + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); + + G_OBJECT_CLASS (st_label_parent_class)->dispose (object); +} + +static void +st_label_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + StLabelPrivate *priv = ST_LABEL (actor)->priv; + + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + if (priv->shadow_spec) + { + ClutterActorBox allocation; + float width, height; + float resource_scale; + + clutter_actor_get_allocation_box (priv->label, &allocation); + clutter_actor_box_get_size (&allocation, &width, &height); + + resource_scale = clutter_actor_get_resource_scale (priv->label); + + width *= resource_scale; + height *= resource_scale; + + if (priv->text_shadow_pipeline == NULL || + width != priv->shadow_width || + height != priv->shadow_height) + { + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); + + priv->shadow_width = width; + priv->shadow_height = height; + priv->text_shadow_pipeline = + _st_create_shadow_pipeline_from_actor (priv->shadow_spec, + priv->label); + } + + if (priv->text_shadow_pipeline != NULL) + { + CoglFramebuffer *framebuffer; + + framebuffer = + clutter_paint_context_get_framebuffer (paint_context); + _st_paint_shadow_with_opacity (priv->shadow_spec, + framebuffer, + priv->text_shadow_pipeline, + &allocation, + clutter_actor_get_paint_opacity (priv->label)); + } + } + + clutter_actor_paint (priv->label, paint_context); +} + +static void +st_label_resource_scale_changed (ClutterActor *actor) +{ + StLabelPrivate *priv = ST_LABEL (actor)->priv; + + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); + + if (CLUTTER_ACTOR_CLASS (st_label_parent_class)->resource_scale_changed) + CLUTTER_ACTOR_CLASS (st_label_parent_class)->resource_scale_changed (actor); +} + +static void +st_label_class_init (StLabelClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + gobject_class->set_property = st_label_set_property; + gobject_class->get_property = st_label_get_property; + gobject_class->dispose = st_label_dispose; + + actor_class->paint = st_label_paint; + actor_class->allocate = st_label_allocate; + actor_class->get_preferred_width = st_label_get_preferred_width; + actor_class->get_preferred_height = st_label_get_preferred_height; + actor_class->resource_scale_changed = st_label_resource_scale_changed; + + widget_class->style_changed = st_label_style_changed; + widget_class->get_accessible_type = st_label_accessible_get_type; + + /** + * StLabel:clutter-text: + * + * The internal #ClutterText actor supporting the label + */ + props[PROP_CLUTTER_TEXT] = + g_param_spec_object ("clutter-text", + "Clutter Text", + "Internal ClutterText actor", + CLUTTER_TYPE_TEXT, + ST_PARAM_READABLE); + + /** + * StLabel:text: + * + * The current text being display in the #StLabel. + */ + props[PROP_TEXT] = + g_param_spec_string ("text", + "Text", + "Text of the label", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +static void +invalidate_shadow_pipeline (GObject *object, + GParamSpec *pspec, + StLabel *label) +{ + StLabelPrivate *priv = st_label_get_instance_private (label); + + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); +} + +static void +st_label_init (StLabel *label) +{ + ClutterActor *actor = CLUTTER_ACTOR (label); + StLabelPrivate *priv; + + label->priv = priv = st_label_get_instance_private (label); + + label->priv->label = g_object_new (CLUTTER_TYPE_TEXT, + "ellipsize", PANGO_ELLIPSIZE_END, + NULL); + label->priv->text_shadow_pipeline = NULL; + label->priv->shadow_width = -1.; + label->priv->shadow_height = -1.; + + /* These properties might get set from CSS using _st_set_text_from_style */ + g_signal_connect (priv->label, "notify::font-description", + G_CALLBACK (invalidate_shadow_pipeline), label); + + g_signal_connect (priv->label, "notify::attributes", + G_CALLBACK (invalidate_shadow_pipeline), label); + + g_signal_connect (priv->label, "notify::justify", + G_CALLBACK (invalidate_shadow_pipeline), label); + + g_signal_connect (priv->label, "notify::line-alignment", + G_CALLBACK (invalidate_shadow_pipeline), label); + + clutter_actor_add_child (actor, priv->label); + + clutter_actor_set_offscreen_redirect (actor, + CLUTTER_OFFSCREEN_REDIRECT_ALWAYS); +} + +/** + * st_label_new: + * @text: (nullable): text to set the label to + * + * Create a new #StLabel with the label specified by @text. + * + * Returns: a new #StLabel + */ +StWidget * +st_label_new (const gchar *text) +{ + if (text == NULL || *text == '\0') + return g_object_new (ST_TYPE_LABEL, NULL); + else + return g_object_new (ST_TYPE_LABEL, + "text", text, + NULL); +} + +/** + * st_label_get_text: + * @label: a #StLabel + * + * Get the text displayed on the label. + * + * Returns: (transfer none): the text for the label. This must not be freed by + * the application + */ +const gchar * +st_label_get_text (StLabel *label) +{ + g_return_val_if_fail (ST_IS_LABEL (label), NULL); + + return clutter_text_get_text (CLUTTER_TEXT (label->priv->label)); +} + +/** + * st_label_set_text: + * @label: a #StLabel + * @text: (nullable): text to set the label to + * + * Sets the text displayed by the label. + */ +void +st_label_set_text (StLabel *label, + const gchar *text) +{ + StLabelPrivate *priv; + ClutterText *ctext; + + g_return_if_fail (ST_IS_LABEL (label)); + + priv = label->priv; + ctext = CLUTTER_TEXT (priv->label); + + if (clutter_text_get_editable (ctext) || + g_strcmp0 (clutter_text_get_text (ctext), text) != 0) + { + g_clear_pointer (&priv->text_shadow_pipeline, cogl_object_unref); + + clutter_text_set_text (ctext, text); + + g_object_notify_by_pspec (G_OBJECT (label), props[PROP_TEXT]); + } +} + +/** + * st_label_get_clutter_text: + * @label: a #StLabel + * + * Retrieve the internal #ClutterText used by @label so that extra parameters + * can be set. + * + * Returns: (transfer none): the #ClutterText used by #StLabel. The actor + * is owned by the #StLabel and should not be destroyed by the application. + */ +ClutterActor* +st_label_get_clutter_text (StLabel *label) +{ + g_return_val_if_fail (ST_LABEL (label), NULL); + + return label->priv->label; +} + + +/******************************************************************************/ +/*************************** ACCESSIBILITY SUPPORT ****************************/ +/******************************************************************************/ + +#define ST_TYPE_LABEL_ACCESSIBLE st_label_accessible_get_type () + +#define ST_LABEL_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + ST_TYPE_LABEL_ACCESSIBLE, StLabelAccessible)) + +#define ST_IS_LABEL_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + ST_TYPE_LABEL_ACCESSIBLE)) + +#define ST_LABEL_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + ST_TYPE_LABEL_ACCESSIBLE, StLabelAccessibleClass)) + +#define ST_IS_LABEL_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + ST_TYPE_LABEL_ACCESSIBLE)) + +#define ST_LABEL_ACCESSIBLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + ST_TYPE_LABEL_ACCESSIBLE, StLabelAccessibleClass)) + +typedef struct _StLabelAccessible StLabelAccessible; +typedef struct _StLabelAccessibleClass StLabelAccessibleClass; + +struct _StLabelAccessible +{ + StWidgetAccessible parent; +}; + +struct _StLabelAccessibleClass +{ + StWidgetAccessibleClass parent_class; +}; + +/* AtkObject */ +static void st_label_accessible_initialize (AtkObject *obj, + gpointer data); +static const gchar * st_label_accessible_get_name (AtkObject *obj); + +G_DEFINE_TYPE (StLabelAccessible, st_label_accessible, ST_TYPE_WIDGET_ACCESSIBLE) + +static void +st_label_accessible_class_init (StLabelAccessibleClass *klass) +{ + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass); + + atk_class->initialize = st_label_accessible_initialize; + atk_class->get_name = st_label_accessible_get_name; +} + +static void +st_label_accessible_init (StLabelAccessible *self) +{ + /* initialization done on AtkObject->initialize */ +} + +static void +label_text_notify_cb (StLabel *label, + GParamSpec *pspec, + AtkObject *accessible) +{ + g_object_notify (G_OBJECT (accessible), "accessible-name"); +} + +static void +st_label_accessible_initialize (AtkObject *obj, + gpointer data) +{ + ATK_OBJECT_CLASS (st_label_accessible_parent_class)->initialize (obj, data); + + g_signal_connect (data, "notify::text", + G_CALLBACK (label_text_notify_cb), + obj); + + obj->role = ATK_ROLE_LABEL; +} + +static const gchar * +st_label_accessible_get_name (AtkObject *obj) +{ + const gchar *name = NULL; + + g_return_val_if_fail (ST_IS_LABEL_ACCESSIBLE (obj), NULL); + + name = ATK_OBJECT_CLASS (st_label_accessible_parent_class)->get_name (obj); + if (name == NULL) + { + ClutterActor *actor = NULL; + + actor = CLUTTER_ACTOR (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (actor == NULL || st_widget_has_style_class_name (ST_WIDGET (actor), "hidden")) + name = NULL; + else + name = st_label_get_text (ST_LABEL (actor)); + } + + return name; +} diff --git a/src/st/st-label.h b/src/st/st-label.h new file mode 100644 index 0000000..456ad31 --- /dev/null +++ b/src/st/st-label.h @@ -0,0 +1,58 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-label.h: Plain label actor + * + * Copyright 2008, 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_LABEL_H__ +#define __ST_LABEL_H__ + +G_BEGIN_DECLS + +#include <st/st-widget.h> + +#define ST_TYPE_LABEL (st_label_get_type ()) +G_DECLARE_FINAL_TYPE (StLabel, st_label, ST, LABEL, StWidget) + +typedef struct _StLabelPrivate StLabelPrivate; + +/** + * StLabel: + * + * The contents of this structure is private and should only be accessed using + * the provided API. + */ +struct _StLabel +{ + /*< private >*/ + StWidget parent_instance; + + StLabelPrivate *priv; +}; + +StWidget * st_label_new (const gchar *text); +const gchar * st_label_get_text (StLabel *label); +void st_label_set_text (StLabel *label, + const gchar *text); +ClutterActor * st_label_get_clutter_text (StLabel *label); + +G_END_DECLS + +#endif /* __ST_LABEL_H__ */ diff --git a/src/st/st-password-entry.c b/src/st/st-password-entry.c new file mode 100644 index 0000000..43f9b52 --- /dev/null +++ b/src/st/st-password-entry.c @@ -0,0 +1,363 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-password-entry.c: Password entry actor based on st-entry + * + * Copyright 2019 Endless Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "st-private.h" +#include "st-password-entry.h" +#include "st-icon.h" +#include "st-settings.h" + +#define BLACK_CIRCLE 9679 + +#define ST_PASSWORD_ENTRY_PRIV(x) st_password_entry_get_instance_private ((StPasswordEntry *) x) + +typedef struct _StPasswordEntryPrivate StPasswordEntryPrivate; + +struct _StPasswordEntry +{ + /*< private >*/ + StEntry parent_instance; +}; + +struct _StPasswordEntryPrivate +{ + ClutterActor *peek_password_icon; + + gboolean password_visible; + gboolean show_peek_icon; +}; + +enum +{ + PROP_0, + + PROP_PASSWORD_VISIBLE, + PROP_SHOW_PEEK_ICON, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (StPasswordEntry, st_password_entry, ST_TYPE_ENTRY); + +static gboolean +show_password_locked_down (StPasswordEntry *entry) +{ + gboolean disable_show_password = FALSE; + + g_object_get (st_settings_get (), "disable-show-password", &disable_show_password, NULL); + + return disable_show_password; +} + +static void +st_password_entry_secondary_icon_clicked (StEntry *entry) +{ + StPasswordEntry *password_entry = ST_PASSWORD_ENTRY (entry); + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (password_entry); + + st_password_entry_set_password_visible (password_entry, !priv->password_visible); +} + +static void +st_password_entry_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (gobject); + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (gobject); + + switch (prop_id) + { + case PROP_PASSWORD_VISIBLE: + g_value_set_boolean (value, priv->password_visible); + break; + + case PROP_SHOW_PEEK_ICON: + g_value_set_boolean (value, st_password_entry_get_show_peek_icon (entry)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_password_entry_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (gobject); + + switch (prop_id) + { + case PROP_PASSWORD_VISIBLE: + st_password_entry_set_password_visible (entry, g_value_get_boolean (value)); + break; + + case PROP_SHOW_PEEK_ICON: + st_password_entry_set_show_peek_icon (entry, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_password_entry_dispose (GObject *gobject) +{ + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (gobject); + + g_clear_object (&priv->peek_password_icon); + + G_OBJECT_CLASS(st_password_entry_parent_class)->dispose (gobject); +} + +static void +st_password_entry_class_init (StPasswordEntryClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + StEntryClass *st_entry_class = ST_ENTRY_CLASS (klass); + + gobject_class->get_property = st_password_entry_get_property; + gobject_class->set_property = st_password_entry_set_property; + gobject_class->dispose = st_password_entry_dispose; + + st_entry_class->secondary_icon_clicked = st_password_entry_secondary_icon_clicked; + + /** + * StPasswordEntry:password-visible: + * + * Whether the text in the entry is masked for privacy. + */ + props[PROP_PASSWORD_VISIBLE] = g_param_spec_boolean ("password-visible", + "Password visible", + "Whether the text in the entry is masked or not", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StPasswordEntry:show-peek-icon: + * + * Whether to display an icon button to toggle the masking enabled by the + * #StPasswordEntry:password-visible property. + */ + props[PROP_SHOW_PEEK_ICON] = g_param_spec_boolean ("show-peek-icon", + "Show peek icon", + "Whether to show the password peek icon", + TRUE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +static void +update_peek_icon (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (entry); + gboolean show_peek_icon; + + show_peek_icon = st_password_entry_get_show_peek_icon (entry); + + if (show_peek_icon) + st_entry_set_secondary_icon (ST_ENTRY (entry), priv->peek_password_icon); + else + st_entry_set_secondary_icon (ST_ENTRY (entry), NULL); +} + +static void +on_disable_show_password_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (user_data); + + if (show_password_locked_down (entry)) + st_password_entry_set_password_visible (entry, FALSE); + + update_peek_icon (entry); + + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_SHOW_PEEK_ICON]); +} + +static void +clutter_text_password_char_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + StPasswordEntry *entry = ST_PASSWORD_ENTRY (user_data); + ClutterActor *clutter_text; + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + if (clutter_text_get_password_char (CLUTTER_TEXT (clutter_text)) == 0) + st_password_entry_set_password_visible (entry, TRUE); + else + st_password_entry_set_password_visible (entry, FALSE); +} + +static void +st_password_entry_init (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv = ST_PASSWORD_ENTRY_PRIV (entry); + ClutterActor *clutter_text; + + priv->peek_password_icon = g_object_new (ST_TYPE_ICON, + "style-class", "peek-password", + "icon-name", "view-reveal-symbolic", + NULL); + st_entry_set_secondary_icon (ST_ENTRY (entry), priv->peek_password_icon); + + st_password_entry_set_show_peek_icon (entry, TRUE); + + g_signal_connect_object (st_settings_get (), + "notify::disable-show-password", + G_CALLBACK (on_disable_show_password_changed), + entry, + 0); + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), BLACK_CIRCLE); + + st_entry_set_input_purpose (ST_ENTRY (entry), CLUTTER_INPUT_CONTENT_PURPOSE_PASSWORD); + + g_signal_connect (clutter_text, "notify::password-char", + G_CALLBACK (clutter_text_password_char_cb), entry); +} + +/** + * st_password_entry_new: + * + * Create a new #StPasswordEntry. + * + * Returns: a new #StEntry + */ +StEntry* +st_password_entry_new (void) +{ + return ST_ENTRY (g_object_new (ST_TYPE_PASSWORD_ENTRY, NULL)); +} + +/** + * st_password_entry_set_show_peek_icon: + * @entry: a #StPasswordEntry + * @value: %TRUE to show the peek-icon in the entry + * + * Sets whether to show or hide the peek-icon in the password entry. If %TRUE, + * a icon button for temporarily unmasking the password will be shown at the + * end of the entry. + */ +void +st_password_entry_set_show_peek_icon (StPasswordEntry *entry, + gboolean value) +{ + StPasswordEntryPrivate *priv; + + g_return_if_fail (ST_IS_PASSWORD_ENTRY (entry)); + + priv = ST_PASSWORD_ENTRY_PRIV (entry); + if (priv->show_peek_icon == value) + return; + + priv->show_peek_icon = value; + + update_peek_icon (entry); + + if (st_password_entry_get_show_peek_icon (entry) != value) + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_SHOW_PEEK_ICON]); +} + +/** + * st_password_entry_get_show_peek_icon: + * @entry: a #StPasswordEntry + * + * Gets whether peek-icon is shown or hidden in the password entry. + * + * Returns: %TRUE if visible + */ +gboolean +st_password_entry_get_show_peek_icon (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_PASSWORD_ENTRY (entry), TRUE); + + priv = ST_PASSWORD_ENTRY_PRIV (entry); + return priv->show_peek_icon && !show_password_locked_down (entry); +} + +/** + * st_password_entry_set_password_visible: + * @entry: a #StPasswordEntry + * @value: %TRUE to show the password in the entry, #FALSE otherwise + * + * Sets whether to show or hide text in the password entry. + */ +void +st_password_entry_set_password_visible (StPasswordEntry *entry, + gboolean value) +{ + StPasswordEntryPrivate *priv; + ClutterActor *clutter_text; + + g_return_if_fail (ST_IS_PASSWORD_ENTRY (entry)); + + priv = ST_PASSWORD_ENTRY_PRIV (entry); + if (priv->password_visible == value) + return; + + priv->password_visible = value; + + clutter_text = st_entry_get_clutter_text (ST_ENTRY (entry)); + if (priv->password_visible) + { + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), 0); + st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "view-conceal-symbolic"); + } + else + { + clutter_text_set_password_char (CLUTTER_TEXT (clutter_text), BLACK_CIRCLE); + st_icon_set_icon_name (ST_ICON (priv->peek_password_icon), "view-reveal-symbolic"); + } + + g_object_notify_by_pspec (G_OBJECT (entry), props[PROP_PASSWORD_VISIBLE]); +} + +/** + * st_password_entry_get_password_visible: + * @entry: a #StPasswordEntry + * + * Gets whether the text is masked in the password entry. + * + * Returns: %TRUE if visible + */ +gboolean +st_password_entry_get_password_visible (StPasswordEntry *entry) +{ + StPasswordEntryPrivate *priv; + + g_return_val_if_fail (ST_IS_PASSWORD_ENTRY (entry), FALSE); + + priv = ST_PASSWORD_ENTRY_PRIV (entry); + return priv->password_visible; +} diff --git a/src/st/st-password-entry.h b/src/st/st-password-entry.h new file mode 100644 index 0000000..3998068 --- /dev/null +++ b/src/st/st-password-entry.h @@ -0,0 +1,46 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-password-entry.h: Password entry actor based on st-entry + * + * Copyright 2019 Endless Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_PASSWORD_ENTRY_H__ +#define __ST_PASSWORD_ENTRY_H__ + +G_BEGIN_DECLS + +#include <st/st-entry.h> + +#define ST_TYPE_PASSWORD_ENTRY (st_password_entry_get_type ()) + +G_DECLARE_FINAL_TYPE (StPasswordEntry, st_password_entry, ST, PASSWORD_ENTRY, StEntry) + +StEntry *st_password_entry_new (void); +gboolean st_password_entry_get_password_visible (StPasswordEntry *entry); +void st_password_entry_set_password_visible (StPasswordEntry *entry, + gboolean value); +gboolean st_password_entry_get_show_peek_icon (StPasswordEntry *entry); +void st_password_entry_set_show_peek_icon (StPasswordEntry *entry, + gboolean value); + +G_END_DECLS + +#endif /* __ST_PASSWORD_ENTRY_H__ */ + diff --git a/src/st/st-private.c b/src/st/st-private.c new file mode 100644 index 0000000..bb98151 --- /dev/null +++ b/src/st/st-private.c @@ -0,0 +1,804 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-private.h: Private declarations and functions + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Florian Müllner + * Copyright 2010 Intel Corporation + * Copyright 2010 Giovanni Campagna + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#include <math.h> +#include <string.h> + +#include "st-private.h" + +/** + * _st_actor_get_preferred_width: + * @actor: a #ClutterActor + * @for_height: as with clutter_actor_get_preferred_width() + * @y_fill: %TRUE if @actor will fill its allocation vertically + * @min_width_p: as with clutter_actor_get_preferred_width() + * @natural_width_p: as with clutter_actor_get_preferred_width() + * + * Like clutter_actor_get_preferred_width(), but if @y_fill is %FALSE, + * then it will compute a width request based on the assumption that + * @actor will be given an allocation no taller than its natural + * height. + */ +void +_st_actor_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gboolean y_fill, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + if (!y_fill && for_height != -1) + { + ClutterRequestMode mode; + gfloat natural_height; + + mode = clutter_actor_get_request_mode (actor); + if (mode == CLUTTER_REQUEST_WIDTH_FOR_HEIGHT) + { + clutter_actor_get_preferred_height (actor, -1, NULL, &natural_height); + if (for_height > natural_height) + for_height = natural_height; + } + } + + clutter_actor_get_preferred_width (actor, for_height, min_width_p, natural_width_p); +} + +/** + * _st_actor_get_preferred_height: + * @actor: a #ClutterActor + * @for_width: as with clutter_actor_get_preferred_height() + * @x_fill: %TRUE if @actor will fill its allocation horizontally + * @min_height_p: as with clutter_actor_get_preferred_height() + * @natural_height_p: as with clutter_actor_get_preferred_height() + * + * Like clutter_actor_get_preferred_height(), but if @x_fill is + * %FALSE, then it will compute a height request based on the + * assumption that @actor will be given an allocation no wider than + * its natural width. + */ +void +_st_actor_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gboolean x_fill, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + if (!x_fill && for_width != -1) + { + ClutterRequestMode mode; + gfloat natural_width; + + mode = clutter_actor_get_request_mode (actor); + if (mode == CLUTTER_REQUEST_HEIGHT_FOR_WIDTH) + { + clutter_actor_get_preferred_width (actor, -1, NULL, &natural_width); + if (for_width > natural_width) + for_width = natural_width; + } + } + + clutter_actor_get_preferred_height (actor, for_width, min_height_p, natural_height_p); +} + +/** + * _st_set_text_from_style: + * @text: Target #ClutterText + * @theme_node: Source #StThemeNode + * + * Set various GObject properties of the @text object using + * CSS information from @theme_node. + */ +void +_st_set_text_from_style (ClutterText *text, + StThemeNode *theme_node) +{ + + ClutterColor color; + StTextDecoration decoration; + PangoAttrList *attribs = NULL; + const PangoFontDescription *font; + PangoAttribute *foreground; + StTextAlign align; + gdouble spacing; + gchar *font_features; + + font = st_theme_node_get_font (theme_node); + clutter_text_set_font_description (text, (PangoFontDescription *) font); + + attribs = pango_attr_list_new (); + + st_theme_node_get_foreground_color (theme_node, &color); + clutter_text_set_cursor_color (text, &color); + foreground = pango_attr_foreground_new (color.red * 255, + color.green * 255, + color.blue * 255); + pango_attr_list_insert (attribs, foreground); + + if (color.alpha != 255) + { + PangoAttribute *alpha; + + /* An alpha value of 0 means "system inherited", so the + * minimum regular value is 1. + */ + if (color.alpha == 0) + alpha = pango_attr_foreground_alpha_new (1); + else + alpha = pango_attr_foreground_alpha_new (color.alpha * 255); + + pango_attr_list_insert (attribs, alpha); + } + + decoration = st_theme_node_get_text_decoration (theme_node); + if (decoration) + { + if (decoration & ST_TEXT_DECORATION_UNDERLINE) + { + PangoAttribute *underline = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE); + pango_attr_list_insert (attribs, underline); + } + if (decoration & ST_TEXT_DECORATION_LINE_THROUGH) + { + PangoAttribute *strikethrough = pango_attr_strikethrough_new (TRUE); + pango_attr_list_insert (attribs, strikethrough); + } + /* Pango doesn't have an equivalent attribute for _OVERLINE, and we deliberately + * skip BLINK (for now...) + */ + } + + spacing = st_theme_node_get_letter_spacing (theme_node); + if (spacing) + { + PangoAttribute *letter_spacing = pango_attr_letter_spacing_new ((int)(.5 + spacing) * PANGO_SCALE); + pango_attr_list_insert (attribs, letter_spacing); + } + + font_features = st_theme_node_get_font_features (theme_node); + if (font_features) + { + pango_attr_list_insert (attribs, pango_attr_font_features_new (font_features)); + g_free (font_features); + } + + clutter_text_set_attributes (text, attribs); + + if (attribs) + pango_attr_list_unref (attribs); + + align = st_theme_node_get_text_align (theme_node); + if (align == ST_TEXT_ALIGN_JUSTIFY) + { + clutter_text_set_justify (text, TRUE); + clutter_text_set_line_alignment (text, PANGO_ALIGN_LEFT); + } + else + { + clutter_text_set_justify (text, FALSE); + clutter_text_set_line_alignment (text, (PangoAlignment) align); + } +} + +/** + * _st_create_texture_pipeline: + * @src_texture: The CoglTexture for the pipeline + * + * Creates a simple pipeline which contains the given texture as a + * single layer. + */ +CoglPipeline * +_st_create_texture_pipeline (CoglTexture *src_texture) +{ + static CoglPipeline *texture_pipeline_template = NULL; + CoglPipeline *pipeline; + + g_return_val_if_fail (src_texture != NULL, NULL); + + /* The only state used in the pipeline that would affect the shader + generation is the texture type on the layer. Therefore we create + a template pipeline which sets this state and all texture + pipelines are created as a copy of this. That way Cogl can find + the shader state for the pipeline more quickly by looking at the + pipeline ancestry instead of resorting to the shader cache. */ + if (G_UNLIKELY (texture_pipeline_template == NULL)) + { + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + texture_pipeline_template = cogl_pipeline_new (ctx); + cogl_pipeline_set_layer_null_texture (texture_pipeline_template, 0); + } + + pipeline = cogl_pipeline_copy (texture_pipeline_template); + + if (src_texture != NULL) + cogl_pipeline_set_layer_texture (pipeline, 0, src_texture); + + return pipeline; +} + +/***** + * Shadows + *****/ + +static gdouble * +calculate_gaussian_kernel (gdouble sigma, + guint n_values) +{ + gdouble *ret, sum; + gdouble exp_divisor; + int half, i; + + g_return_val_if_fail (sigma > 0, NULL); + + half = n_values / 2; + + ret = g_malloc (n_values * sizeof (gdouble)); + sum = 0.0; + + exp_divisor = 2 * sigma * sigma; + + /* n_values of 1D Gauss function */ + for (i = 0; i < (int)n_values; i++) + { + ret[i] = exp (-(i - half) * (i - half) / exp_divisor); + sum += ret[i]; + } + + /* normalize */ + for (i = 0; i < (int)n_values; i++) + ret[i] /= sum; + + return ret; +} + +static guchar * +blur_pixels (guchar *pixels_in, + gint width_in, + gint height_in, + gint rowstride_in, + gdouble blur, + gint *width_out, + gint *height_out, + size_t *rowstride_out) +{ + guchar *pixels_out; + gdouble sigma; + + /* The CSS specification defines (or will define) the blur radius as twice + * the Gaussian standard deviation. See: + * + * http://lists.w3.org/Archives/Public/www-style/2010Sep/0002.html + */ + sigma = blur / 2.; + + if ((guint) blur == 0) + { + *width_out = width_in; + *height_out = height_in; + *rowstride_out = rowstride_in; + pixels_out = g_memdup2 (pixels_in, *rowstride_out * *height_out); + } + else + { + gdouble *kernel; + guchar *line; + gint n_values, half; + gint x_in, y_in, x_out, y_out, i; + + n_values = (gint) 5 * sigma; + half = n_values / 2; + + *width_out = width_in + 2 * half; + *height_out = height_in + 2 * half; + *rowstride_out = (*width_out + 3) & ~3; + + pixels_out = g_malloc0 (*rowstride_out * *height_out); + line = g_malloc0 (*rowstride_out); + + kernel = calculate_gaussian_kernel (sigma, n_values); + + /* vertical blur */ + for (x_in = 0; x_in < width_in; x_in++) + for (y_out = 0; y_out < *height_out; y_out++) + { + guchar *pixel_in, *pixel_out; + gint i0, i1; + + y_in = y_out - half; + + /* We read from the source at 'y = y_in + i - half'; clamp the + * full i range [0, n_values) so that y is in [0, height_in). + */ + i0 = MAX (half - y_in, 0); + i1 = MIN (height_in + half - y_in, n_values); + + pixel_in = pixels_in + (y_in + i0 - half) * rowstride_in + x_in; + pixel_out = pixels_out + y_out * *rowstride_out + (x_in + half); + + for (i = i0; i < i1; i++) + { + *pixel_out += *pixel_in * kernel[i]; + pixel_in += rowstride_in; + } + } + + /* horizontal blur */ + for (y_out = 0; y_out < *height_out; y_out++) + { + memcpy (line, pixels_out + y_out * *rowstride_out, *rowstride_out); + + for (x_out = 0; x_out < *width_out; x_out++) + { + gint i0, i1; + guchar *pixel_out, *pixel_in; + + /* We read from the source at 'x = x_out + i - half'; clamp the + * full i range [0, n_values) so that x is in [0, width_out). + */ + i0 = MAX (half - x_out, 0); + i1 = MIN (*width_out + half - x_out, n_values); + + pixel_in = line + x_out + i0 - half; + pixel_out = pixels_out + *rowstride_out * y_out + x_out; + + *pixel_out = 0; + for (i = i0; i < i1; i++) + { + *pixel_out += *pixel_in * kernel[i]; + pixel_in++; + } + } + } + g_free (kernel); + g_free (line); + } + + return pixels_out; +} + +CoglPipeline * +_st_create_shadow_pipeline (StShadow *shadow_spec, + CoglTexture *src_texture, + float resource_scale) +{ + ClutterBackend *backend = clutter_get_default_backend (); + CoglContext *ctx = clutter_backend_get_cogl_context (backend); + g_autoptr (ClutterPaintNode) texture_node = NULL; + g_autoptr (ClutterPaintNode) blur_node = NULL; + g_autoptr (CoglOffscreen) offscreen = NULL; + g_autoptr (GError) error = NULL; + ClutterPaintContext *paint_context; + CoglFramebuffer *fb; + CoglPipeline *pipeline; + CoglTexture *texture; + float sampling_radius; + float sigma; + int src_height, dst_height; + int src_width, dst_width; + CoglPipeline *texture_pipeline; + + static CoglPipelineKey texture_pipeline_key = + "st-create-shadow-pipeline-saturate-alpha"; + static CoglPipeline *shadow_pipeline_template = NULL; + + g_return_val_if_fail (shadow_spec != NULL, NULL); + g_return_val_if_fail (src_texture != NULL, NULL); + + sampling_radius = resource_scale * shadow_spec->blur; + sigma = sampling_radius / 2.f; + sampling_radius = ceilf (sampling_radius); + + src_width = cogl_texture_get_width (src_texture); + src_height = cogl_texture_get_height (src_texture); + dst_width = src_width + 2 * sampling_radius; + dst_height = src_height + 2 * sampling_radius; + + texture = cogl_texture_2d_new_with_size (ctx, dst_width, dst_height); + if (!texture) + return NULL; + + offscreen = cogl_offscreen_new_with_texture (texture); + fb = COGL_FRAMEBUFFER (offscreen); + if (!cogl_framebuffer_allocate (fb, &error)) + { + cogl_clear_object (&texture); + return NULL; + } + + cogl_framebuffer_clear4f (fb, COGL_BUFFER_BIT_COLOR, 0.f, 0.f, 0.f, 0.f); + cogl_framebuffer_orthographic (fb, 0, 0, dst_width, dst_height, 0, 1.0); + + /* Blur */ + blur_node = clutter_blur_node_new (dst_width, dst_height, sigma); + clutter_paint_node_add_rectangle (blur_node, + &(ClutterActorBox) { + 0.f, 0.f, + dst_width, dst_height, + }); + + /* Texture */ + texture_pipeline = cogl_context_get_named_pipeline (ctx, + &texture_pipeline_key); + + if (G_UNLIKELY (texture_pipeline == NULL)) + { + CoglSnippet *snippet; + + snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_FRAGMENT, + "", + "if (cogl_color_out.a > 0.0)\n" + " cogl_color_out.a = 1.0;"); + + texture_pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_add_snippet (texture_pipeline, snippet); + cogl_object_unref (snippet); + + cogl_context_set_named_pipeline (ctx, + &texture_pipeline_key, + texture_pipeline); + } + + /* No need to unref texture_pipeline since the named pipeline hash + * doesn't change its ref count from 1. Also no need to copy texture_pipeline + * since we'll be completely finished with it after clutter_paint_node_paint. + */ + + cogl_pipeline_set_layer_texture (texture_pipeline, 0, src_texture); + texture_node = clutter_pipeline_node_new (texture_pipeline); + clutter_paint_node_add_child (blur_node, texture_node); + clutter_paint_node_add_rectangle (texture_node, + &(ClutterActorBox) { + .x1 = sampling_radius, + .y1 = sampling_radius, + .x2 = src_width + sampling_radius, + .y2 = src_height + sampling_radius, + }); + + paint_context = + clutter_paint_context_new_for_framebuffer (fb, NULL, CLUTTER_PAINT_FLAG_NONE); + clutter_paint_node_paint (blur_node, paint_context); + clutter_paint_context_destroy (paint_context); + + if (G_UNLIKELY (shadow_pipeline_template == NULL)) + { + shadow_pipeline_template = cogl_pipeline_new (ctx); + + /* We set up the pipeline to blend the shadow texture with the combine + * constant, but defer setting the latter until painting, so that we can + * take the actor's overall opacity into account. */ + cogl_pipeline_set_layer_combine (shadow_pipeline_template, 0, + "RGBA = MODULATE (CONSTANT, TEXTURE[A])", + NULL); + } + + pipeline = cogl_pipeline_copy (shadow_pipeline_template); + cogl_pipeline_set_layer_texture (pipeline, 0, texture); + + cogl_clear_object (&texture); + + return pipeline; +} + +CoglPipeline * +_st_create_shadow_pipeline_from_actor (StShadow *shadow_spec, + ClutterActor *actor) +{ + ClutterContent *image = NULL; + CoglPipeline *shadow_pipeline = NULL; + float resource_scale; + float width, height; + ClutterPaintContext *paint_context; + + g_return_val_if_fail (clutter_actor_has_allocation (actor), NULL); + + clutter_actor_get_size (actor, &width, &height); + + if (width == 0 || height == 0) + return NULL; + + resource_scale = clutter_actor_get_resource_scale (actor); + + width = ceilf (width * resource_scale); + height = ceilf (height * resource_scale); + + image = clutter_actor_get_content (actor); + if (image && CLUTTER_IS_IMAGE (image)) + { + CoglTexture *texture; + + texture = clutter_image_get_texture (CLUTTER_IMAGE (image)); + if (texture && + cogl_texture_get_width (texture) == width && + cogl_texture_get_height (texture) == height) + shadow_pipeline = _st_create_shadow_pipeline (shadow_spec, texture, + resource_scale); + } + + if (shadow_pipeline == NULL) + { + CoglTexture *buffer; + CoglOffscreen *offscreen; + CoglFramebuffer *fb; + CoglContext *ctx; + CoglColor clear_color; + GError *catch_error = NULL; + float x, y; + + ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ()); + buffer = cogl_texture_2d_new_with_size (ctx, width, height); + + if (buffer == NULL) + return NULL; + + offscreen = cogl_offscreen_new_with_texture (buffer); + fb = COGL_FRAMEBUFFER (offscreen); + + if (!cogl_framebuffer_allocate (fb, &catch_error)) + { + g_error_free (catch_error); + g_object_unref (offscreen); + cogl_object_unref (buffer); + return NULL; + } + + cogl_color_init_from_4ub (&clear_color, 0, 0, 0, 0); + clutter_actor_get_position (actor, &x, &y); + x *= resource_scale; + y *= resource_scale; + + cogl_framebuffer_clear (fb, COGL_BUFFER_BIT_COLOR, &clear_color); + cogl_framebuffer_translate (fb, -x, -y, 0); + cogl_framebuffer_orthographic (fb, 0, 0, width, height, 0, 1.0); + cogl_framebuffer_scale (fb, resource_scale, resource_scale, 1); + + clutter_actor_set_opacity_override (actor, 255); + + paint_context = + clutter_paint_context_new_for_framebuffer (fb, NULL, + CLUTTER_PAINT_FLAG_NONE); + clutter_actor_paint (actor, paint_context); + clutter_paint_context_destroy (paint_context); + + clutter_actor_set_opacity_override (actor, -1); + + g_object_unref (fb); + + shadow_pipeline = _st_create_shadow_pipeline (shadow_spec, buffer, + resource_scale); + + cogl_object_unref (buffer); + } + + return shadow_pipeline; +} + +/** + * _st_create_shadow_cairo_pattern: + * @shadow_spec: the definition of the shadow + * @src_pattern: surface pattern for which we create the shadow + * (must be a surface pattern) + * + * This is a utility function for creating shadows used by + * st-theme-node.c; it's in this file to share the gaussian + * blur implementation. The usage of this function is quite different + * depending on whether shadow_spec->inset is %TRUE or not. If + * shadow_spec->inset is %TRUE, the caller should pass in a @src_pattern + * which is the <i>inverse</i> of what they want shadowed, and must take + * care of the spread and offset from the shadow spec themselves. If + * shadow_spec->inset is %FALSE then the caller should pass in what they + * want shadowed directly, and this function takes care of the spread and + * the offset. + */ +cairo_pattern_t * +_st_create_shadow_cairo_pattern (StShadow *shadow_spec_in, + cairo_pattern_t *src_pattern) +{ + g_autoptr(StShadow) shadow_spec = NULL; + static cairo_user_data_key_t shadow_pattern_user_data; + cairo_t *cr; + cairo_surface_t *src_surface; + cairo_surface_t *surface_in; + cairo_surface_t *surface_out; + cairo_pattern_t *dst_pattern; + guchar *pixels_in, *pixels_out; + gint width_in, height_in, rowstride_in; + gint width_out, height_out; + size_t rowstride_out; + cairo_matrix_t shadow_matrix; + double xscale_in, yscale_in; + int i, j; + + g_return_val_if_fail (shadow_spec_in != NULL, NULL); + g_return_val_if_fail (src_pattern != NULL, NULL); + + if (cairo_pattern_get_surface (src_pattern, &src_surface) != CAIRO_STATUS_SUCCESS) + /* The most likely reason we can't get the pattern is that sizing went hairwire + * and the caller tried to create a surface too big for memory, leaving us with + * a pattern in an error state; we return a transparent pattern for the shadow. + */ + return cairo_pattern_create_rgba(1.0, 1.0, 1.0, 0.0); + + width_in = cairo_image_surface_get_width (src_surface); + height_in = cairo_image_surface_get_height (src_surface); + + cairo_surface_get_device_scale (src_surface, &xscale_in, &yscale_in); + + if (xscale_in != 1.0 || yscale_in != 1.0) + { + /* Scale the shadow specifications in a temporary copy so that + * we can work everywhere in absolute surface coordinates */ + double scale = (xscale_in + yscale_in) / 2.0; + shadow_spec = st_shadow_new (&shadow_spec_in->color, + shadow_spec_in->xoffset * xscale_in, + shadow_spec_in->yoffset * yscale_in, + shadow_spec_in->blur * scale, + shadow_spec_in->spread * scale, + shadow_spec_in->inset); + } + else + { + shadow_spec = st_shadow_ref (shadow_spec_in); + } + + /* We want the output to be a color agnostic alpha mask, + * so we need to strip the color channels from the input + */ + if (cairo_image_surface_get_format (src_surface) != CAIRO_FORMAT_A8) + { + surface_in = cairo_image_surface_create (CAIRO_FORMAT_A8, + width_in, height_in); + + cr = cairo_create (surface_in); + cairo_set_source_surface (cr, src_surface, 0, 0); + cairo_paint (cr); + cairo_destroy (cr); + } + else + { + surface_in = cairo_surface_reference (src_surface); + } + + pixels_in = cairo_image_surface_get_data (surface_in); + rowstride_in = cairo_image_surface_get_stride (surface_in); + + pixels_out = blur_pixels (pixels_in, width_in, height_in, rowstride_in, + shadow_spec->blur, + &width_out, &height_out, &rowstride_out); + cairo_surface_destroy (surface_in); + + /* Invert pixels for inset shadows */ + if (shadow_spec->inset) + { + for (j = 0; j < height_out; j++) + { + guchar *p = pixels_out + rowstride_out * j; + for (i = 0; i < width_out; i++, p++) + *p = ~*p; + } + } + + surface_out = cairo_image_surface_create_for_data (pixels_out, + CAIRO_FORMAT_A8, + width_out, + height_out, + rowstride_out); + cairo_surface_set_device_scale (surface_out, xscale_in, yscale_in); + cairo_surface_set_user_data (surface_out, &shadow_pattern_user_data, + pixels_out, (cairo_destroy_func_t) g_free); + + dst_pattern = cairo_pattern_create_for_surface (surface_out); + cairo_surface_destroy (surface_out); + + cairo_pattern_get_matrix (src_pattern, &shadow_matrix); + + if (shadow_spec->inset) + { + /* Scale the matrix in surface absolute coordinates */ + cairo_matrix_scale (&shadow_matrix, 1.0 / xscale_in, 1.0 / yscale_in); + + /* For inset shadows, offsets and spread radius have already been + * applied to the original pattern, so all left to do is shift the + * blurred image left, so that it aligns centered under the + * unblurred one + */ + cairo_matrix_translate (&shadow_matrix, + (width_out - width_in) / 2.0, + (height_out - height_in) / 2.0); + + /* Scale back the matrix in original coordinates */ + cairo_matrix_scale (&shadow_matrix, xscale_in, yscale_in); + + cairo_pattern_set_matrix (dst_pattern, &shadow_matrix); + return dst_pattern; + } + + /* Read all the code from the cairo_pattern_set_matrix call + * at the end of this function to here from bottom to top, + * because each new affine transformation is applied in + * front of all the previous ones */ + + /* 6. Invert the matrix back */ + cairo_matrix_invert (&shadow_matrix); + + /* Scale the matrix in surface absolute coordinates */ + cairo_matrix_scale (&shadow_matrix, 1.0 / xscale_in, 1.0 / yscale_in); + + /* 5. Adjust based on specified offsets */ + cairo_matrix_translate (&shadow_matrix, + shadow_spec->xoffset, + shadow_spec->yoffset); + + /* 4. Recenter the newly scaled image */ + cairo_matrix_translate (&shadow_matrix, + - shadow_spec->spread, + - shadow_spec->spread); + + /* 3. Scale up the blurred image to fill the spread */ + cairo_matrix_scale (&shadow_matrix, + (width_in + 2.0 * shadow_spec->spread) / width_in, + (height_in + 2.0 * shadow_spec->spread) / height_in); + + /* 2. Shift the blurred image left, so that it aligns centered + * under the unblurred one */ + cairo_matrix_translate (&shadow_matrix, + - (width_out - width_in) / 2.0, + - (height_out - height_in) / 2.0); + + /* Scale back the matrix in scaled coordinates */ + cairo_matrix_scale (&shadow_matrix, xscale_in, yscale_in); + + /* 1. Invert the matrix so we can work with it in pattern space + */ + cairo_matrix_invert (&shadow_matrix); + + cairo_pattern_set_matrix (dst_pattern, &shadow_matrix); + + return dst_pattern; +} + +void +_st_paint_shadow_with_opacity (StShadow *shadow_spec, + CoglFramebuffer *framebuffer, + CoglPipeline *shadow_pipeline, + ClutterActorBox *box, + guint8 paint_opacity) +{ + ClutterActorBox shadow_box; + CoglColor color; + + g_return_if_fail (shadow_spec != NULL); + g_return_if_fail (shadow_pipeline != NULL); + + st_shadow_get_box (shadow_spec, box, &shadow_box); + + cogl_color_init_from_4ub (&color, + shadow_spec->color.red * paint_opacity / 255, + shadow_spec->color.green * paint_opacity / 255, + shadow_spec->color.blue * paint_opacity / 255, + shadow_spec->color.alpha * paint_opacity / 255); + cogl_color_premultiply (&color); + cogl_pipeline_set_layer_combine_constant (shadow_pipeline, 0, &color); + cogl_framebuffer_draw_rectangle (framebuffer, + shadow_pipeline, + shadow_box.x1, shadow_box.y1, + shadow_box.x2, shadow_box.y2); +} diff --git a/src/st/st-private.h b/src/st/st-private.h new file mode 100644 index 0000000..3f1fd12 --- /dev/null +++ b/src/st/st-private.h @@ -0,0 +1,75 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-private.h: Private declarations and functions + * + * Copyright 2007 OpenedHand + * Copyright 2009 Intel Corporation. + * Copyright 2010 Red Hat, Inc. + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_PRIVATE_H__ +#define __ST_PRIVATE_H__ + +#include <glib.h> +#include <cairo.h> +#include "st-widget.h" +#include "st-bin.h" +#include "st-shadow.h" + +G_BEGIN_DECLS + +#define I_(str) (g_intern_static_string ((str))) + +#define ST_PARAM_READABLE (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS) +#define ST_PARAM_WRITABLE (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS) +#define ST_PARAM_READWRITE (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS) + +G_END_DECLS + +ClutterActor *_st_widget_get_dnd_clone (StWidget *widget); + +void _st_actor_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gboolean y_fill, + gfloat *min_width_p, + gfloat *natural_width_p); +void _st_actor_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gboolean x_fill, + gfloat *min_height_p, + gfloat *natural_height_p); + +void _st_set_text_from_style (ClutterText *text, + StThemeNode *theme_node); + +CoglPipeline * _st_create_texture_pipeline (CoglTexture *src_texture); + +/* Helper for widgets which need to draw additional shadows */ +CoglPipeline * _st_create_shadow_pipeline (StShadow *shadow_spec, + CoglTexture *src_texture, + float resource_scale); +CoglPipeline * _st_create_shadow_pipeline_from_actor (StShadow *shadow_spec, + ClutterActor *actor); +cairo_pattern_t *_st_create_shadow_cairo_pattern (StShadow *shadow_spec, + cairo_pattern_t *src_pattern); + +void _st_paint_shadow_with_opacity (StShadow *shadow_spec, + CoglFramebuffer *framebuffer, + CoglPipeline *shadow_pipeline, + ClutterActorBox *box, + guint8 paint_opacity); + +#endif /* __ST_PRIVATE_H__ */ diff --git a/src/st/st-scroll-bar.c b/src/st/st-scroll-bar.c new file mode 100644 index 0000000..72bcd55 --- /dev/null +++ b/src/st/st-scroll-bar.c @@ -0,0 +1,1014 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-bar.c: Scroll bar actor + * + * Copyright 2008 OpenedHand + * Copyright 2008, 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Maxim Ermilov + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-scroll-bar + * @short_description: a user interface element to control scrollable areas. + * + * The #StScrollBar allows users to scroll scrollable actors, either by + * the step or page amount, or by manually dragging the handle. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <math.h> +#include <clutter/clutter.h> + +#include "st-scroll-bar.h" +#include "st-bin.h" +#include "st-enum-types.h" +#include "st-private.h" +#include "st-button.h" +#include "st-settings.h" + +#define PAGING_INITIAL_REPEAT_TIMEOUT 500 +#define PAGING_SUBSEQUENT_REPEAT_TIMEOUT 200 + +typedef struct _StScrollBarPrivate StScrollBarPrivate; +struct _StScrollBarPrivate +{ + StAdjustment *adjustment; + + gfloat x_origin; + gfloat y_origin; + + ClutterInputDevice *grab_device; + ClutterGrab *grab; + + ClutterActor *trough; + ClutterActor *handle; + + gfloat move_x; + gfloat move_y; + + /* Trough-click handling. */ + enum { NONE, UP, DOWN } paging_direction; + guint paging_source_id; + guint paging_event_no; + + guint vertical : 1; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StScrollBar, st_scroll_bar, ST_TYPE_WIDGET) + +#define ST_SCROLL_BAR_PRIVATE(sb) st_scroll_bar_get_instance_private (ST_SCROLL_BAR (sb)) + +enum +{ + PROP_0, + + PROP_ADJUSTMENT, + PROP_VERTICAL, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum +{ + SCROLL_START, + SCROLL_STOP, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +static gboolean +handle_button_press_event_cb (ClutterActor *actor, + ClutterButtonEvent *event, + StScrollBar *bar); + +static void stop_scrolling (StScrollBar *bar); + +static void +st_scroll_bar_set_vertical (StScrollBar *bar, + gboolean vertical) +{ + StScrollBarPrivate *priv = ST_SCROLL_BAR_PRIVATE (bar); + + if (priv->vertical == vertical) + return; + + priv->vertical = vertical; + + if (priv->vertical) + clutter_actor_set_name (CLUTTER_ACTOR (priv->handle), + "vhandle"); + else + clutter_actor_set_name (CLUTTER_ACTOR (priv->handle), + "hhandle"); + clutter_actor_queue_relayout (CLUTTER_ACTOR (bar)); + g_object_notify_by_pspec (G_OBJECT (bar), props[PROP_VERTICAL]); +} + +static void +st_scroll_bar_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StScrollBarPrivate *priv = ST_SCROLL_BAR_PRIVATE (gobject); + + switch (prop_id) + { + case PROP_ADJUSTMENT: + g_value_set_object (value, priv->adjustment); + break; + + case PROP_VERTICAL: + g_value_set_boolean (value, priv->vertical); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_scroll_bar_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StScrollBar *bar = ST_SCROLL_BAR (gobject); + + switch (prop_id) + { + case PROP_ADJUSTMENT: + st_scroll_bar_set_adjustment (bar, g_value_get_object (value)); + break; + + case PROP_VERTICAL: + st_scroll_bar_set_vertical (bar, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_scroll_bar_dispose (GObject *gobject) +{ + StScrollBar *bar = ST_SCROLL_BAR (gobject); + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + + if (priv->adjustment) + st_scroll_bar_set_adjustment (bar, NULL); + + if (priv->handle) + { + clutter_actor_destroy (priv->handle); + priv->handle = NULL; + } + + if (priv->trough) + { + clutter_actor_destroy (priv->trough); + priv->trough = NULL; + } + + G_OBJECT_CLASS (st_scroll_bar_parent_class)->dispose (gobject); +} + +static void +st_scroll_bar_unmap (ClutterActor *actor) +{ + CLUTTER_ACTOR_CLASS (st_scroll_bar_parent_class)->unmap (actor); + + stop_scrolling (ST_SCROLL_BAR (actor)); +} + +static void +scroll_bar_allocate_children (StScrollBar *bar, + const ClutterActorBox *box) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (bar)); + ClutterActorBox content_box, trough_box; + + st_theme_node_get_content_box (theme_node, box, &content_box); + + trough_box.x1 = content_box.x1; + trough_box.y1 = content_box.y1; + trough_box.x2 = content_box.x2; + trough_box.y2 = content_box.y2; + clutter_actor_allocate (priv->trough, &trough_box); + + if (priv->adjustment) + { + float handle_size, position, avail_size; + gdouble value, lower, upper, page_size, increment, min_size, max_size; + ClutterActorBox handle_box = { 0, }; + + st_adjustment_get_values (priv->adjustment, + &value, + &lower, + &upper, + NULL, + NULL, + &page_size); + + if ((upper == lower) + || (page_size >= (upper - lower))) + increment = 1.0; + else + increment = page_size / (upper - lower); + + min_size = 32.; + st_theme_node_lookup_length (theme_node, "min-size", FALSE, &min_size); + max_size = G_MAXINT16; + st_theme_node_lookup_length (theme_node, "max-size", FALSE, &max_size); + + if (upper - lower - page_size <= 0) + position = 0; + else + position = (value - lower) / (upper - lower - page_size); + + if (priv->vertical) + { + avail_size = content_box.y2 - content_box.y1; + handle_size = increment * avail_size; + handle_size = CLAMP (handle_size, min_size, max_size); + + handle_box.x1 = content_box.x1; + handle_box.y1 = content_box.y1 + position * (avail_size - handle_size); + + handle_box.x2 = content_box.x2; + handle_box.y2 = handle_box.y1 + handle_size; + } + else + { + ClutterTextDirection direction; + + avail_size = content_box.x2 - content_box.x1; + handle_size = increment * avail_size; + handle_size = CLAMP (handle_size, min_size, max_size); + + direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (bar)); + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + { + handle_box.x2 = content_box.x2 - position * (avail_size - handle_size); + handle_box.x1 = handle_box.x2 - handle_size; + } + else + { + handle_box.x1 = content_box.x1 + position * (avail_size - handle_size); + handle_box.x2 = handle_box.x1 + handle_size; + } + + handle_box.y1 = content_box.y1; + handle_box.y2 = content_box.y2; + } + + clutter_actor_allocate (priv->handle, &handle_box); + } +} + +static void +st_scroll_bar_get_preferred_width (ClutterActor *self, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StScrollBar *bar = ST_SCROLL_BAR (self); + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + gfloat trough_min_width, trough_natural_width; + gfloat handle_min_width, handle_natural_width; + + st_theme_node_adjust_for_height (theme_node, &for_height); + + _st_actor_get_preferred_width (priv->trough, for_height, TRUE, + &trough_min_width, &trough_natural_width); + + _st_actor_get_preferred_width (priv->handle, for_height, TRUE, + &handle_min_width, &handle_natural_width); + + if (priv->vertical) + { + if (min_width_p) + *min_width_p = MAX (trough_min_width, handle_min_width); + + if (natural_width_p) + *natural_width_p = MAX (trough_natural_width, handle_natural_width); + } + else + { + if (min_width_p) + *min_width_p = trough_min_width + handle_min_width; + + if (natural_width_p) + *natural_width_p = trough_natural_width + handle_natural_width; + } + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_scroll_bar_get_preferred_height (ClutterActor *self, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StScrollBar *bar = ST_SCROLL_BAR (self); + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + gfloat trough_min_height, trough_natural_height; + gfloat handle_min_height, handle_natural_height; + + st_theme_node_adjust_for_width (theme_node, &for_width); + + _st_actor_get_preferred_height (priv->trough, for_width, TRUE, + &trough_min_height, &trough_natural_height); + + _st_actor_get_preferred_height (priv->handle, for_width, TRUE, + &handle_min_height, &handle_natural_height); + + if (priv->vertical) + { + if (min_height_p) + *min_height_p = trough_min_height + handle_min_height; + + if (natural_height_p) + *natural_height_p = trough_natural_height + handle_natural_height; + } + else + { + if (min_height_p) + *min_height_p = MAX (trough_min_height, handle_min_height); + + if (natural_height_p) + *natural_height_p = MAX (trough_natural_height, handle_natural_height); + } + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_scroll_bar_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StScrollBar *bar = ST_SCROLL_BAR (actor); + + clutter_actor_set_allocation (actor, box); + + scroll_bar_allocate_children (bar, box); +} + +static void +scroll_bar_update_positions (StScrollBar *bar) +{ + ClutterActorBox box; + + /* Due to a change in the adjustments, we need to reposition our + * children; since adjustments changes can come from allocation + * changes in the scrolled area, we can't just queue a new relayout - + * we may already be in a relayout cycle. On the other hand, if + * a relayout is already queued, we can't just go ahead and allocate + * our children, since we don't have a valid allocation, and calling + * clutter_actor_get_allocation_box() will trigger an immediate + * stage relayout. So what we do is go ahead and immediately + * allocate our children if we already have a valid allocation, and + * otherwise just wait for the queued relayout. + */ + if (!clutter_actor_has_allocation (CLUTTER_ACTOR (bar))) + return; + + clutter_actor_get_allocation_box (CLUTTER_ACTOR (bar), &box); + scroll_bar_allocate_children (bar, &box); +} + +static void +bar_reactive_notify_cb (GObject *gobject, + GParamSpec *arg1, + gpointer user_data) +{ + StScrollBar *bar = ST_SCROLL_BAR (gobject); + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + + clutter_actor_set_reactive (priv->handle, + clutter_actor_get_reactive (CLUTTER_ACTOR (bar))); +} + +static GObject* +st_scroll_bar_constructor (GType type, + guint n_properties, + GObjectConstructParam *properties) +{ + GObjectClass *gobject_class; + GObject *obj; + StScrollBar *bar; + + gobject_class = G_OBJECT_CLASS (st_scroll_bar_parent_class); + obj = gobject_class->constructor (type, n_properties, properties); + + bar = ST_SCROLL_BAR (obj); + + g_signal_connect (bar, "notify::reactive", + G_CALLBACK (bar_reactive_notify_cb), NULL); + + return obj; +} + +static void +adjust_with_direction (StAdjustment *adj, + ClutterScrollDirection direction) +{ + gdouble delta; + + switch (direction) + { + case CLUTTER_SCROLL_UP: + case CLUTTER_SCROLL_LEFT: + delta = -1.0; + break; + case CLUTTER_SCROLL_RIGHT: + case CLUTTER_SCROLL_DOWN: + delta = 1.0; + break; + case CLUTTER_SCROLL_SMOOTH: + default: + g_assert_not_reached (); + break; + } + + st_adjustment_adjust_for_scroll_event (adj, delta); +} + +static gboolean +st_scroll_bar_scroll_event (ClutterActor *actor, + ClutterScrollEvent *event) +{ + StScrollBarPrivate *priv = ST_SCROLL_BAR_PRIVATE (actor); + ClutterTextDirection direction; + ClutterScrollDirection scroll_dir; + + if (clutter_event_is_pointer_emulated ((ClutterEvent *) event)) + return TRUE; + + direction = clutter_actor_get_text_direction (actor); + scroll_dir = event->direction; + + switch (scroll_dir) + { + case CLUTTER_SCROLL_SMOOTH: + { + gdouble delta_x, delta_y; + clutter_event_get_scroll_delta ((ClutterEvent *)event, &delta_x, &delta_y); + + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + delta_x *= -1; + + if (priv->vertical) + st_adjustment_adjust_for_scroll_event (priv->adjustment, delta_y); + else + st_adjustment_adjust_for_scroll_event (priv->adjustment, delta_x); + } + break; + case CLUTTER_SCROLL_LEFT: + case CLUTTER_SCROLL_RIGHT: + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + scroll_dir = scroll_dir == CLUTTER_SCROLL_LEFT ? CLUTTER_SCROLL_RIGHT + : CLUTTER_SCROLL_LEFT; + /* Fall through */ + case CLUTTER_SCROLL_UP: + case CLUTTER_SCROLL_DOWN: + adjust_with_direction (priv->adjustment, scroll_dir); + break; + default: + g_return_val_if_reached (FALSE); + break; + } + + return TRUE; +} + +static void +st_scroll_bar_class_init (StScrollBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + object_class->get_property = st_scroll_bar_get_property; + object_class->set_property = st_scroll_bar_set_property; + object_class->dispose = st_scroll_bar_dispose; + object_class->constructor = st_scroll_bar_constructor; + + actor_class->get_preferred_width = st_scroll_bar_get_preferred_width; + actor_class->get_preferred_height = st_scroll_bar_get_preferred_height; + actor_class->allocate = st_scroll_bar_allocate; + actor_class->scroll_event = st_scroll_bar_scroll_event; + actor_class->unmap = st_scroll_bar_unmap; + + /** + * StScrollBar:adjustment: + * + * The #StAdjustment controlling the #StScrollBar. + */ + props[PROP_ADJUSTMENT] = + g_param_spec_object ("adjustment", "Adjustment", "The adjustment", + ST_TYPE_ADJUSTMENT, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollBar:vertical: + * + * Whether the #StScrollBar is vertical. If %FALSE it is horizontal. + */ + props[PROP_VERTICAL] = + g_param_spec_boolean ("vertical", + "Vertical Orientation", + "Vertical Orientation", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); + + + /** + * StScrollBar::scroll-start: + * @bar: a #StScrollBar + * + * Emitted when the #StScrollBar begins scrolling. + */ + signals[SCROLL_START] = + g_signal_new ("scroll-start", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StScrollBarClass, scroll_start), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * StScrollBar::scroll-stop: + * @bar: a #StScrollBar + * + * Emitted when the #StScrollBar finishes scrolling. + */ + signals[SCROLL_STOP] = + g_signal_new ("scroll-stop", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StScrollBarClass, scroll_stop), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +move_slider (StScrollBar *bar, + gfloat x, + gfloat y) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + ClutterTextDirection direction; + gdouble position, lower, upper, page_size; + gfloat ux, uy, pos, size; + + if (!priv->adjustment) + return; + + if (!clutter_actor_transform_stage_point (priv->trough, x, y, &ux, &uy)) + return; + + if (priv->vertical) + size = clutter_actor_get_height (priv->trough) + - clutter_actor_get_height (priv->handle); + else + size = clutter_actor_get_width (priv->trough) + - clutter_actor_get_width (priv->handle); + + if (size == 0) + return; + + if (priv->vertical) + pos = uy - priv->y_origin; + else + pos = ux - priv->x_origin; + pos = CLAMP (pos, 0, size); + + st_adjustment_get_values (priv->adjustment, + NULL, + &lower, + &upper, + NULL, + NULL, + &page_size); + + direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (bar)); + if (!priv->vertical && direction == CLUTTER_TEXT_DIRECTION_RTL) + pos = size - pos; + + position = ((pos / size) + * (upper - lower - page_size)) + + lower; + + st_adjustment_set_value (priv->adjustment, position); +} + +static void +stop_scrolling (StScrollBar *bar) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + if (!priv->grab_device) + return; + + st_widget_remove_style_pseudo_class (ST_WIDGET (priv->handle), "active"); + + if (priv->grab) + { + clutter_grab_dismiss (priv->grab); + g_clear_pointer (&priv->grab, clutter_grab_unref); + } + + priv->grab_device = NULL; + g_signal_emit (bar, signals[SCROLL_STOP], 0); +} + +static gboolean +handle_motion_event_cb (ClutterActor *trough, + ClutterMotionEvent *event, + StScrollBar *bar) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + if (!priv->grab_device) + return FALSE; + + move_slider (bar, event->x, event->y); + return TRUE; +} + +static gboolean +handle_button_release_event_cb (ClutterActor *trough, + ClutterButtonEvent *event, + StScrollBar *bar) +{ + if (event->button != 1) + return FALSE; + + stop_scrolling (bar); + return TRUE; +} + +static gboolean +handle_button_press_event_cb (ClutterActor *actor, + ClutterButtonEvent *event, + StScrollBar *bar) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (bar); + ClutterInputDevice *device = clutter_event_get_device ((ClutterEvent*) event); + ClutterActor *stage; + + if (event->button != 1) + return FALSE; + + if (!clutter_actor_transform_stage_point (priv->handle, + event->x, + event->y, + &priv->x_origin, + &priv->y_origin)) + return FALSE; + + st_widget_add_style_pseudo_class (ST_WIDGET (priv->handle), "active"); + + /* Account for the scrollbar-trough-handle nesting. */ + priv->x_origin += clutter_actor_get_x (priv->trough); + priv->y_origin += clutter_actor_get_y (priv->trough); + + g_assert (!priv->grab_device); + + stage = clutter_actor_get_stage (actor); + priv->grab = clutter_stage_grab (CLUTTER_STAGE (stage), priv->handle); + priv->grab_device = device; + g_signal_emit (bar, signals[SCROLL_START], 0); + + return TRUE; +} + +static gboolean +trough_paging_cb (StScrollBar *self) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self); + ClutterTextDirection direction; + g_autoptr (ClutterTransition) transition = NULL; + StSettings *settings; + gfloat handle_pos, event_pos, tx, ty; + gdouble value, new_value; + gdouble page_increment; + gdouble slow_down_factor; + gboolean ret; + + gulong mode; + + if (priv->paging_event_no == 0) + { + /* Scroll on after initial timeout. */ + mode = CLUTTER_EASE_OUT_CUBIC; + ret = FALSE; + priv->paging_event_no = 1; + priv->paging_source_id = g_timeout_add ( + PAGING_INITIAL_REPEAT_TIMEOUT, + (GSourceFunc) trough_paging_cb, + self); + g_source_set_name_by_id (priv->paging_source_id, "[gnome-shell] trough_paging_cb"); + } + else if (priv->paging_event_no == 1) + { + /* Scroll on after subsequent timeout. */ + ret = FALSE; + mode = CLUTTER_EASE_IN_CUBIC; + priv->paging_event_no = 2; + priv->paging_source_id = g_timeout_add ( + PAGING_SUBSEQUENT_REPEAT_TIMEOUT, + (GSourceFunc) trough_paging_cb, + self); + g_source_set_name_by_id (priv->paging_source_id, "[gnome-shell] trough_paging_cb"); + } + else + { + /* Keep scrolling. */ + ret = TRUE; + mode = CLUTTER_LINEAR; + priv->paging_event_no++; + } + + /* Do the scrolling */ + st_adjustment_get_values (priv->adjustment, + &value, NULL, NULL, + NULL, &page_increment, NULL); + + if (priv->vertical) + handle_pos = clutter_actor_get_y (priv->handle); + else + handle_pos = clutter_actor_get_x (priv->handle); + + clutter_actor_transform_stage_point (CLUTTER_ACTOR (priv->trough), + priv->move_x, + priv->move_y, + &tx, &ty); + + direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (self)); + if (!priv->vertical && direction == CLUTTER_TEXT_DIRECTION_RTL) + page_increment *= -1; + + if (priv->vertical) + event_pos = ty; + else + event_pos = tx; + + if (event_pos > handle_pos) + { + if (priv->paging_direction == NONE) + { + /* Remember direction. */ + priv->paging_direction = DOWN; + } + if (priv->paging_direction == UP) + { + /* Scrolled far enough. */ + return FALSE; + } + new_value = value + page_increment; + } + else + { + if (priv->paging_direction == NONE) + { + /* Remember direction. */ + priv->paging_direction = UP; + } + if (priv->paging_direction == DOWN) + { + /* Scrolled far enough. */ + return FALSE; + } + new_value = value - page_increment; + } + + /* Stop existing transition, if one exists */ + st_adjustment_remove_transition (priv->adjustment, "value"); + + settings = st_settings_get (); + g_object_get (settings, "slow-down-factor", &slow_down_factor, NULL); + + /* FIXME: Creating a new transition for each scroll is probably not the best + * idea, but it's a lot less involved than extending the current animation */ + transition = g_object_new (CLUTTER_TYPE_PROPERTY_TRANSITION, + "property-name", "value", + "interval", clutter_interval_new (G_TYPE_DOUBLE, value, new_value), + "duration", (guint)(PAGING_SUBSEQUENT_REPEAT_TIMEOUT * slow_down_factor), + "progress-mode", mode, + "remove-on-complete", TRUE, + NULL); + st_adjustment_add_transition (priv->adjustment, "value", transition); + + return ret; +} + +static gboolean +trough_button_press_event_cb (ClutterActor *actor, + ClutterButtonEvent *event, + StScrollBar *self) +{ + StScrollBarPrivate *priv; + + g_return_val_if_fail (self, FALSE); + + if (event->button != 1) + return FALSE; + + priv = st_scroll_bar_get_instance_private (self); + if (priv->adjustment == NULL) + return FALSE; + + priv->move_x = event->x; + priv->move_y = event->y; + priv->paging_direction = NONE; + priv->paging_event_no = 0; + trough_paging_cb (self); + + return TRUE; +} + +static gboolean +trough_button_release_event_cb (ClutterActor *actor, + ClutterButtonEvent *event, + StScrollBar *self) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self); + + if (event->button != 1) + return FALSE; + + g_clear_handle_id (&priv->paging_source_id, g_source_remove); + + return TRUE; +} + +static gboolean +trough_leave_event_cb (ClutterActor *actor, + ClutterEvent *event, + StScrollBar *self) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self); + + if (priv->paging_source_id) + { + g_clear_handle_id (&priv->paging_source_id, g_source_remove); + return TRUE; + } + + return FALSE; +} + +static void +st_scroll_bar_notify_reactive (StScrollBar *self) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self); + + gboolean reactive = clutter_actor_get_reactive (CLUTTER_ACTOR (self)); + + clutter_actor_set_reactive (CLUTTER_ACTOR (priv->trough), reactive); + clutter_actor_set_reactive (CLUTTER_ACTOR (priv->handle), reactive); +} + +static void +st_scroll_bar_init (StScrollBar *self) +{ + StScrollBarPrivate *priv = st_scroll_bar_get_instance_private (self); + + priv->trough = (ClutterActor *) st_bin_new (); + clutter_actor_set_reactive ((ClutterActor *) priv->trough, TRUE); + clutter_actor_set_name (CLUTTER_ACTOR (priv->trough), "trough"); + clutter_actor_add_child (CLUTTER_ACTOR (self), + CLUTTER_ACTOR (priv->trough)); + g_signal_connect (priv->trough, "button-press-event", + G_CALLBACK (trough_button_press_event_cb), self); + g_signal_connect (priv->trough, "button-release-event", + G_CALLBACK (trough_button_release_event_cb), self); + g_signal_connect (priv->trough, "leave-event", + G_CALLBACK (trough_leave_event_cb), self); + + priv->handle = (ClutterActor *) st_button_new (); + clutter_actor_set_name (CLUTTER_ACTOR (priv->handle), "hhandle"); + clutter_actor_add_child (CLUTTER_ACTOR (self), + CLUTTER_ACTOR (priv->handle)); + g_signal_connect (priv->handle, "button-press-event", + G_CALLBACK (handle_button_press_event_cb), self); + g_signal_connect (priv->handle, "button-release-event", + G_CALLBACK (handle_button_release_event_cb), self); + g_signal_connect (priv->handle, "motion-event", + G_CALLBACK (handle_motion_event_cb), self); + + clutter_actor_set_reactive (CLUTTER_ACTOR (self), TRUE); + + g_signal_connect (self, "notify::reactive", + G_CALLBACK (st_scroll_bar_notify_reactive), NULL); +} + +StWidget * +st_scroll_bar_new (StAdjustment *adjustment) +{ + return g_object_new (ST_TYPE_SCROLL_BAR, + "adjustment", adjustment, + NULL); +} + +static void +on_notify_value (GObject *object, + GParamSpec *pspec, + StScrollBar *bar) +{ + scroll_bar_update_positions (bar); +} + +static void +on_changed (StAdjustment *adjustment, + StScrollBar *bar) +{ + scroll_bar_update_positions (bar); +} + +void +st_scroll_bar_set_adjustment (StScrollBar *bar, + StAdjustment *adjustment) +{ + StScrollBarPrivate *priv; + + g_return_if_fail (ST_IS_SCROLL_BAR (bar)); + + priv = st_scroll_bar_get_instance_private (bar); + + if (adjustment == priv->adjustment) + return; + + if (priv->adjustment) + { + g_signal_handlers_disconnect_by_func (priv->adjustment, + on_notify_value, + bar); + g_signal_handlers_disconnect_by_func (priv->adjustment, + on_changed, + bar); + g_object_unref (priv->adjustment); + priv->adjustment = NULL; + } + + if (adjustment) + { + priv->adjustment = g_object_ref (adjustment); + + g_signal_connect (priv->adjustment, "notify::value", + G_CALLBACK (on_notify_value), + bar); + g_signal_connect (priv->adjustment, "changed", + G_CALLBACK (on_changed), + bar); + + clutter_actor_queue_relayout (CLUTTER_ACTOR (bar)); + } + + g_object_notify_by_pspec (G_OBJECT (bar), props[PROP_ADJUSTMENT]); +} + +/** + * st_scroll_bar_get_adjustment: + * @bar: a #StScrollbar + * + * Gets the #StAdjustment that controls the current position of @bar. + * + * Returns: (transfer none): an #StAdjustment + */ +StAdjustment * +st_scroll_bar_get_adjustment (StScrollBar *bar) +{ + g_return_val_if_fail (ST_IS_SCROLL_BAR (bar), NULL); + + return ((StScrollBarPrivate *)ST_SCROLL_BAR_PRIVATE (bar))->adjustment; +} + diff --git a/src/st/st-scroll-bar.h b/src/st/st-scroll-bar.h new file mode 100644 index 0000000..2c69fdd --- /dev/null +++ b/src/st/st-scroll-bar.h @@ -0,0 +1,53 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-bar.h: Scroll bar actor + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_SCROLL_BAR_H__ +#define __ST_SCROLL_BAR_H__ + +#include <st/st-adjustment.h> +#include <st/st-widget.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SCROLL_BAR (st_scroll_bar_get_type()) +G_DECLARE_DERIVABLE_TYPE (StScrollBar, st_scroll_bar, ST, SCROLL_BAR, StWidget) + +struct _StScrollBarClass +{ + StWidgetClass parent_class; + + /* signals */ + void (*scroll_start) (StScrollBar *bar); + void (*scroll_stop) (StScrollBar *bar); +}; + +StWidget *st_scroll_bar_new (StAdjustment *adjustment); + +void st_scroll_bar_set_adjustment (StScrollBar *bar, + StAdjustment *adjustment); +StAdjustment *st_scroll_bar_get_adjustment (StScrollBar *bar); + +G_END_DECLS + +#endif /* __ST_SCROLL_BAR_H__ */ diff --git a/src/st/st-scroll-view-fade.c b/src/st/st-scroll-view-fade.c new file mode 100644 index 0000000..77b1d0b --- /dev/null +++ b/src/st/st-scroll-view-fade.c @@ -0,0 +1,461 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-view-fade.h: Edge fade effect for StScrollView + * + * Copyright 2010 Intel Corporation. + * Copyright 2011 Adel Gadllah + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + + +#include "st-private.h" +#include "st-scroll-view-fade.h" +#include "st-scroll-view.h" +#include "st-widget.h" +#include "st-theme-node.h" +#include "st-scroll-bar.h" +#include "st-scrollable.h" + +#include <clutter/clutter.h> +#include <cogl/cogl.h> + +#define DEFAULT_FADE_OFFSET 68.0f + +#include "st-scroll-view-fade-generated.h" + +struct _StScrollViewFade +{ + ClutterShaderEffect parent_instance; + + /* a back pointer to our actor, so that we can query it */ + ClutterActor *actor; + + StAdjustment *vadjustment; + StAdjustment *hadjustment; + + guint fade_edges : 1; + guint extend_fade_area: 1; + + ClutterMargin fade_margins; +}; + +G_DEFINE_TYPE (StScrollViewFade, + st_scroll_view_fade, + CLUTTER_TYPE_SHADER_EFFECT); + +enum { + PROP_0, + + PROP_FADE_MARGINS, + PROP_FADE_EDGES, + PROP_EXTEND_FADE_AREA, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +static CoglTexture * +st_scroll_view_fade_create_texture (ClutterOffscreenEffect *effect, + gfloat min_width, + gfloat min_height) +{ + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + return COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, min_width, min_height)); +} + +static char * +st_scroll_view_fade_get_static_shader_source (ClutterShaderEffect *effect) +{ + return g_strdup (st_scroll_view_fade_glsl); +} + + +static void +st_scroll_view_fade_paint_target (ClutterOffscreenEffect *effect, + ClutterPaintNode *node, + ClutterPaintContext *paint_context) +{ + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (effect); + ClutterShaderEffect *shader = CLUTTER_SHADER_EFFECT (effect); + ClutterOffscreenEffectClass *parent; + + gdouble value, lower, upper, page_size; + ClutterActor *vscroll = st_scroll_view_get_vscroll_bar (ST_SCROLL_VIEW (self->actor)); + ClutterActor *hscroll = st_scroll_view_get_hscroll_bar (ST_SCROLL_VIEW (self->actor)); + gboolean h_scroll_visible, v_scroll_visible, rtl; + + ClutterActorBox allocation, content_box, paint_box; + + float fade_area_topleft[2]; + float fade_area_bottomright[2]; + graphene_point3d_t verts[4]; + + clutter_actor_get_paint_box (self->actor, &paint_box); + clutter_actor_get_abs_allocation_vertices (self->actor, verts); + + clutter_actor_get_allocation_box (self->actor, &allocation); + st_theme_node_get_content_box (st_widget_get_theme_node (ST_WIDGET (self->actor)), + (const ClutterActorBox *)&allocation, &content_box); + + /* + * The FBO is based on the paint_volume's size which can be larger then the actual + * allocation, so we have to account for that when passing the positions + */ + fade_area_topleft[0] = content_box.x1 + (verts[0].x - paint_box.x1); + fade_area_topleft[1] = content_box.y1 + (verts[0].y - paint_box.y1); + fade_area_bottomright[0] = content_box.x2 + (verts[3].x - paint_box.x2) + 1; + fade_area_bottomright[1] = content_box.y2 + (verts[3].y - paint_box.y2) + 1; + + g_object_get (ST_SCROLL_VIEW (self->actor), + "hscrollbar-visible", &h_scroll_visible, + "vscrollbar-visible", &v_scroll_visible, + NULL); + + if (v_scroll_visible) + { + if (clutter_actor_get_text_direction (self->actor) == CLUTTER_TEXT_DIRECTION_RTL) + fade_area_topleft[0] += clutter_actor_get_width (vscroll); + + fade_area_bottomright[0] -= clutter_actor_get_width (vscroll); + } + + if (h_scroll_visible) + fade_area_bottomright[1] -= clutter_actor_get_height (hscroll); + + if (self->fade_margins.left < 0) + fade_area_topleft[0] -= ABS (self->fade_margins.left); + if (self->fade_margins.right < 0) + fade_area_bottomright[0] += ABS (self->fade_margins.right); + if (self->fade_margins.top < 0) + fade_area_topleft[1] -= ABS (self->fade_margins.top); + if (self->fade_margins.bottom < 0) + fade_area_bottomright[1] += ABS (self->fade_margins.bottom); + + st_adjustment_get_values (self->vadjustment, &value, &lower, &upper, NULL, NULL, &page_size); + value = (value - lower) / (upper - page_size - lower); + clutter_shader_effect_set_uniform (shader, "fade_edges_top", G_TYPE_INT, 1, self->fade_edges ? value >= 0.0 : value > 0.0); + clutter_shader_effect_set_uniform (shader, "fade_edges_bottom", G_TYPE_INT, 1, self->fade_edges ? value <= 1.0 : value < 1.0); + + st_adjustment_get_values (self->hadjustment, &value, &lower, &upper, NULL, NULL, &page_size); + value = (value - lower) / (upper - page_size - lower); + rtl = clutter_actor_get_text_direction (self->actor) == CLUTTER_TEXT_DIRECTION_RTL; + clutter_shader_effect_set_uniform (shader, "fade_edges_left", G_TYPE_INT, 1, + self->fade_edges ? + value >= 0.0 : + (rtl ? value < 1.0 : value > 0.0)); + clutter_shader_effect_set_uniform (shader, "fade_edges_right", G_TYPE_INT, 1, + self->fade_edges ? + value <= 1.0 : + (rtl ? value > 0.0 : value < 1.0)); + + clutter_shader_effect_set_uniform (shader, "extend_fade_area", G_TYPE_INT, 1, self->extend_fade_area); + clutter_shader_effect_set_uniform (shader, "fade_offset_top", G_TYPE_FLOAT, 1, ABS (self->fade_margins.top)); + clutter_shader_effect_set_uniform (shader, "fade_offset_bottom", G_TYPE_FLOAT, 1, ABS (self->fade_margins.bottom)); + clutter_shader_effect_set_uniform (shader, "fade_offset_left", G_TYPE_FLOAT, 1, ABS (self->fade_margins.left)); + clutter_shader_effect_set_uniform (shader, "fade_offset_right", G_TYPE_FLOAT, 1, ABS (self->fade_margins.right)); + clutter_shader_effect_set_uniform (shader, "tex", G_TYPE_INT, 1, 0); + clutter_shader_effect_set_uniform (shader, "height", G_TYPE_FLOAT, 1, clutter_actor_get_height (self->actor)); + clutter_shader_effect_set_uniform (shader, "width", G_TYPE_FLOAT, 1, clutter_actor_get_width (self->actor)); + clutter_shader_effect_set_uniform (shader, "fade_area_topleft", CLUTTER_TYPE_SHADER_FLOAT, 2, fade_area_topleft); + clutter_shader_effect_set_uniform (shader, "fade_area_bottomright", CLUTTER_TYPE_SHADER_FLOAT, 2, fade_area_bottomright); + + parent = CLUTTER_OFFSCREEN_EFFECT_CLASS (st_scroll_view_fade_parent_class); + parent->paint_target (effect, node, paint_context); +} + +static void +on_adjustment_changed (StAdjustment *adjustment, + ClutterEffect *effect) +{ + gdouble value, lower, upper, page_size; + gboolean needs_fade; + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (effect); + + st_adjustment_get_values (self->vadjustment, &value, &lower, &upper, NULL, NULL, &page_size); + needs_fade = (value > lower + 0.1) || (value < upper - page_size - 0.1); + + if (!needs_fade) + { + st_adjustment_get_values (self->hadjustment, &value, &lower, &upper, NULL, NULL, &page_size); + needs_fade = (value > lower + 0.1) || (value < upper - page_size - 0.1); + } + + clutter_actor_meta_set_enabled (CLUTTER_ACTOR_META (effect), needs_fade); +} + +static void +st_scroll_view_fade_set_actor (ClutterActorMeta *meta, + ClutterActor *actor) +{ + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (meta); + ClutterActorMetaClass *parent; + + g_return_if_fail (actor == NULL || ST_IS_SCROLL_VIEW (actor)); + + if (self->vadjustment) + { + g_signal_handlers_disconnect_by_func (self->vadjustment, + (gpointer)on_adjustment_changed, + self); + self->vadjustment = NULL; + } + + if (self->hadjustment) + { + g_signal_handlers_disconnect_by_func (self->hadjustment, + (gpointer)on_adjustment_changed, + self); + self->hadjustment = NULL; + } + + + if (actor) + { + StScrollView *scroll_view = ST_SCROLL_VIEW (actor); + StScrollBar *vscroll = ST_SCROLL_BAR (st_scroll_view_get_vscroll_bar (scroll_view)); + StScrollBar *hscroll = ST_SCROLL_BAR (st_scroll_view_get_hscroll_bar (scroll_view)); + self->vadjustment = ST_ADJUSTMENT (st_scroll_bar_get_adjustment (vscroll)); + self->hadjustment = ST_ADJUSTMENT (st_scroll_bar_get_adjustment (hscroll)); + + g_signal_connect (self->vadjustment, "changed", + G_CALLBACK (on_adjustment_changed), + self); + + g_signal_connect (self->hadjustment, "changed", + G_CALLBACK (on_adjustment_changed), + self); + + on_adjustment_changed (NULL, CLUTTER_EFFECT (self)); + } + + parent = CLUTTER_ACTOR_META_CLASS (st_scroll_view_fade_parent_class); + parent->set_actor (meta, actor); + + /* we keep a back pointer here, to avoid going through the ActorMeta */ + self->actor = clutter_actor_meta_get_actor (meta); +} + +static void +st_scroll_view_fade_dispose (GObject *gobject) +{ + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (gobject); + + if (self->vadjustment) + { + g_signal_handlers_disconnect_by_func (self->vadjustment, + (gpointer)on_adjustment_changed, + self); + self->vadjustment = NULL; + } + + if (self->hadjustment) + { + g_signal_handlers_disconnect_by_func (self->hadjustment, + (gpointer)on_adjustment_changed, + self); + self->hadjustment = NULL; + } + + self->actor = NULL; + + G_OBJECT_CLASS (st_scroll_view_fade_parent_class)->dispose (gobject); +} + +static void +st_scroll_view_set_fade_margins (StScrollViewFade *self, + ClutterMargin *fade_margins) +{ + if (self->fade_margins.left == fade_margins->left && + self->fade_margins.right == fade_margins->right && + self->fade_margins.top == fade_margins->top && + self->fade_margins.bottom == fade_margins->bottom) + return; + + self->fade_margins = *fade_margins; + + if (self->actor != NULL) + clutter_actor_queue_redraw (self->actor); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FADE_MARGINS]); +} + +static void +st_scroll_view_fade_set_fade_edges (StScrollViewFade *self, + gboolean fade_edges) +{ + if (self->fade_edges == fade_edges) + return; + + g_object_freeze_notify (G_OBJECT (self)); + + self->fade_edges = fade_edges; + + if (self->actor != NULL) + clutter_actor_queue_redraw (self->actor); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FADE_EDGES]); + g_object_thaw_notify (G_OBJECT (self)); +} + +static void +st_scroll_view_fade_set_extend_fade_area (StScrollViewFade *self, + gboolean extend_fade_area) +{ + if (self->extend_fade_area == extend_fade_area) + return; + + self->extend_fade_area = extend_fade_area; + + if (self->actor != NULL) + clutter_actor_queue_redraw (self->actor); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXTEND_FADE_AREA]); +} + +static void +st_scroll_view_fade_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (object); + + switch (prop_id) + { + case PROP_FADE_MARGINS: + st_scroll_view_set_fade_margins (self, g_value_get_boxed (value)); + break; + case PROP_FADE_EDGES: + st_scroll_view_fade_set_fade_edges (self, g_value_get_boolean (value)); + break; + case PROP_EXTEND_FADE_AREA: + st_scroll_view_fade_set_extend_fade_area (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_scroll_view_fade_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StScrollViewFade *self = ST_SCROLL_VIEW_FADE (object); + + switch (prop_id) + { + case PROP_FADE_MARGINS: + g_value_set_boxed (value, &self->fade_margins); + break; + case PROP_FADE_EDGES: + g_value_set_boolean (value, self->fade_edges); + break; + case PROP_EXTEND_FADE_AREA: + g_value_set_boolean (value, self->extend_fade_area); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_scroll_view_fade_class_init (StScrollViewFadeClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterShaderEffectClass *shader_class; + ClutterOffscreenEffectClass *offscreen_class; + ClutterActorMetaClass *meta_class = CLUTTER_ACTOR_META_CLASS (klass); + + gobject_class->dispose = st_scroll_view_fade_dispose; + gobject_class->get_property = st_scroll_view_fade_get_property; + gobject_class->set_property = st_scroll_view_fade_set_property; + + meta_class->set_actor = st_scroll_view_fade_set_actor; + + shader_class = CLUTTER_SHADER_EFFECT_CLASS (klass); + shader_class->get_static_shader_source = st_scroll_view_fade_get_static_shader_source; + + offscreen_class = CLUTTER_OFFSCREEN_EFFECT_CLASS (klass); + offscreen_class->create_texture = st_scroll_view_fade_create_texture; + offscreen_class->paint_target = st_scroll_view_fade_paint_target; + + /** + * StScrollViewFade:fade-margins: + * + * The margins widths that are faded. + */ + props[PROP_FADE_MARGINS] = + g_param_spec_boxed ("fade-margins", + "Fade margins", + "The margin widths that are faded", + CLUTTER_TYPE_MARGIN, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollViewFade:fade-edges: + * + * Whether the faded area should extend to the edges of the #StScrollViewFade. + */ + props[PROP_FADE_EDGES] = + g_param_spec_boolean ("fade-edges", + "Fade Edges", + "Whether the faded area should extend to the edges", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollViewFade:extend-fade-area: + * + * Whether faded edges should extend beyond the faded area of the #StScrollViewFade. + */ + props[PROP_EXTEND_FADE_AREA] = + g_param_spec_boolean ("extend-fade-area", + "Extend Fade Area", + "Whether faded edges should extend beyond the faded area", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +static void +st_scroll_view_fade_init (StScrollViewFade *self) +{ + self->fade_margins = (ClutterMargin) { + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + DEFAULT_FADE_OFFSET, + }; +} + +/** + * st_scroll_view_fade_new: + * + * Create a new #StScrollViewFade. + * + * Returns: (transfer full): a new #StScrollViewFade + */ +ClutterEffect * +st_scroll_view_fade_new (void) +{ + return g_object_new (ST_TYPE_SCROLL_VIEW_FADE, NULL); +} diff --git a/src/st/st-scroll-view-fade.glsl b/src/st/st-scroll-view-fade.glsl new file mode 100644 index 0000000..ba6582f --- /dev/null +++ b/src/st/st-scroll-view-fade.glsl @@ -0,0 +1,77 @@ +/* + * st-scroll-view-fade.glsl: Edge fade effect for StScrollView + * + * Copyright 2010 Intel Corporation. + * Copyright 2011 Adel Gadllah + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +uniform sampler2D tex; +uniform float height; +uniform float width; +uniform float fade_offset_top; +uniform float fade_offset_bottom; +uniform float fade_offset_left; +uniform float fade_offset_right; +uniform bool fade_edges_top; +uniform bool fade_edges_right; +uniform bool fade_edges_bottom; +uniform bool fade_edges_left; +uniform bool extend_fade_area; + +uniform vec2 fade_area_topleft; +uniform vec2 fade_area_bottomright; + +void main () +{ + cogl_color_out = cogl_color_in * texture2D (tex, vec2 (cogl_tex_coord_in[0].xy)); + + float y = height * cogl_tex_coord_in[0].y; + float x = width * cogl_tex_coord_in[0].x; + float ratio = 1.0; + + if (x > fade_area_topleft[0] && x < fade_area_bottomright[0] && + y > fade_area_topleft[1] && y < fade_area_bottomright[1]) + { + float after_left = x - fade_area_topleft[0]; + float before_right = fade_area_bottomright[0] - x; + float after_top = y - fade_area_topleft[1]; + float before_bottom = fade_area_bottomright[1] - y; + + if (after_top < fade_offset_top && fade_edges_top) { + ratio *= after_top / fade_offset_top; + } + + if (before_bottom < fade_offset_bottom && fade_edges_bottom) { + ratio *= before_bottom / fade_offset_bottom; + } + + if (after_left < fade_offset_left && fade_edges_left) { + ratio *= after_left / fade_offset_left; + } + + if (before_right < fade_offset_right && fade_edges_right) { + ratio *= before_right / fade_offset_right; + } + } else if (extend_fade_area) { + if (x <= fade_area_topleft[0] && fade_edges_left || + x >= fade_area_bottomright[0] && fade_edges_right || + y <= fade_area_topleft[1] && fade_edges_top || + y >= fade_area_bottomright[1] && fade_edges_bottom) { + ratio = 0.0; + } + } + + cogl_color_out *= ratio; +} diff --git a/src/st/st-scroll-view-fade.h b/src/st/st-scroll-view-fade.h new file mode 100644 index 0000000..2c65a77 --- /dev/null +++ b/src/st/st-scroll-view-fade.h @@ -0,0 +1,36 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-view-fade.h: Edge fade effect for StScrollView + * + * Copyright 2010 Intel Corporation. + * Copyright 2011 Adel Gadllah + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_SCROLL_VIEW_FADE_H__ +#define __ST_SCROLL_VIEW_FADE_H__ + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SCROLL_VIEW_FADE (st_scroll_view_fade_get_type ()) +G_DECLARE_FINAL_TYPE (StScrollViewFade, st_scroll_view_fade, + ST, SCROLL_VIEW_FADE, ClutterShaderEffect) + +ClutterEffect *st_scroll_view_fade_new (void); + +G_END_DECLS + +#endif /* __ST_SCROLL_VIEW_FADE_H__ */ diff --git a/src/st/st-scroll-view.c b/src/st/st-scroll-view.c new file mode 100644 index 0000000..50de481 --- /dev/null +++ b/src/st/st-scroll-view.c @@ -0,0 +1,1327 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-view.h: Container with scroll-bars + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Maxim Ermilov + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * SECTION:st-scroll-view + * @short_description: a container for scrollable children + * + * #StScrollView is a single child container for actors that implement + * #StScrollable. It provides scrollbars around the edge of the child to + * allow the user to move around the scrollable area. + */ + +/* TODO: The code here currently only deals with height-for-width + * allocation; width-for-height allocation would need a second set of + * code paths through get_preferred_height()/get_preferred_width()/allocate() + * that reverse the roles of the horizontal and vertical scrollbars. + * + * TODO: The multiple layout passes with and without scrollbars when + * using the automatic policy causes considerable inefficiency because + * it breaks request caching; we should saved the last size passed + * into allocate() and if it's the same as previous size not repeat + * the determination of scrollbar visibility. This requires overriding + * queue_relayout() so we know when to discard the saved value. + * + * The size negotiation between the #StScrollView and the child is + * described in the documentation for #StScrollable; the significant + * part to note there is that reported minimum sizes for a scrolled + * child are the minimum sizes when no scrollbar is needed. This allows + * us to determine what scrollbars are visible without a need to look + * inside the #StAdjustment. + * + * The second simplification that we make that allows us to implement + * a straightforward height-for-width negotiation without multiple + * allocate passes is that when the scrollbar policy is + * AUTO, we always reserve space for the scrollbar in the + * reported minimum and natural size. + * + * See https://bugzilla.gnome.org/show_bug.cgi?id=611740 for a more + * detailed description of the considerations involved. + */ + +#include "st-enum-types.h" +#include "st-private.h" +#include "st-scroll-view.h" +#include "st-scroll-bar.h" +#include "st-scrollable.h" +#include "st-scroll-view-fade.h" +#include <clutter/clutter.h> +#include <math.h> + +static void clutter_container_iface_init (ClutterContainerIface *iface); + +static ClutterContainerIface *st_scroll_view_parent_iface = NULL; + +struct _StScrollViewPrivate +{ + /* a pointer to the child; this is actually stored + * inside StBin:child, but we keep it to avoid + * calling st_bin_get_child() every time we need it + */ + ClutterActor *child; + + StAdjustment *hadjustment; + ClutterActor *hscroll; + StAdjustment *vadjustment; + ClutterActor *vscroll; + + StPolicyType hscrollbar_policy; + StPolicyType vscrollbar_policy; + + gfloat row_size; + gfloat column_size; + + StScrollViewFade *fade_effect; + + guint row_size_set : 1; + guint column_size_set : 1; + guint mouse_scroll : 1; + guint overlay_scrollbars : 1; + guint hscrollbar_visible : 1; + guint vscrollbar_visible : 1; +}; + +G_DEFINE_TYPE_WITH_CODE (StScrollView, st_scroll_view, ST_TYPE_BIN, + G_ADD_PRIVATE (StScrollView) + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER, + clutter_container_iface_init)) + +enum { + PROP_0, + + PROP_HSCROLL, + PROP_VSCROLL, + PROP_HSCROLLBAR_POLICY, + PROP_VSCROLLBAR_POLICY, + PROP_HSCROLLBAR_VISIBLE, + PROP_VSCROLLBAR_VISIBLE, + PROP_MOUSE_SCROLL, + PROP_OVERLAY_SCROLLBARS, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +static void +st_scroll_view_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + StScrollViewPrivate *priv = ((StScrollView *) object)->priv; + + switch (property_id) + { + case PROP_HSCROLL: + g_value_set_object (value, priv->hscroll); + break; + case PROP_VSCROLL: + g_value_set_object (value, priv->vscroll); + break; + case PROP_HSCROLLBAR_POLICY: + g_value_set_enum (value, priv->hscrollbar_policy); + break; + case PROP_VSCROLLBAR_POLICY: + g_value_set_enum (value, priv->vscrollbar_policy); + break; + case PROP_HSCROLLBAR_VISIBLE: + g_value_set_boolean (value, priv->hscrollbar_visible); + break; + case PROP_VSCROLLBAR_VISIBLE: + g_value_set_boolean (value, priv->vscrollbar_visible); + break; + case PROP_MOUSE_SCROLL: + g_value_set_boolean (value, priv->mouse_scroll); + break; + case PROP_OVERLAY_SCROLLBARS: + g_value_set_boolean (value, priv->overlay_scrollbars); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +/** + * st_scroll_view_update_fade_effect: + * @scroll: a #StScrollView + * @fade_margins: a #ClutterMargin defining the vertical fade effects, in pixels. + * + * Sets the fade effects in all four edges of the view. A value of 0 + * disables the effect. + */ +void +st_scroll_view_update_fade_effect (StScrollView *scroll, + ClutterMargin *fade_margins) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (scroll)->priv; + + /* A fade amount of other than 0 enables the effect. */ + if (fade_margins->left != 0. || fade_margins->right != 0. || + fade_margins->top != 0. || fade_margins->bottom != 0.) + { + if (priv->fade_effect == NULL) + { + priv->fade_effect = g_object_new (ST_TYPE_SCROLL_VIEW_FADE, NULL); + + clutter_actor_add_effect_with_name (CLUTTER_ACTOR (scroll), "fade", + CLUTTER_EFFECT (priv->fade_effect)); + } + + g_object_set (priv->fade_effect, + "fade-margins", fade_margins, + NULL); + } + else + { + if (priv->fade_effect != NULL) + { + clutter_actor_remove_effect (CLUTTER_ACTOR (scroll), + CLUTTER_EFFECT (priv->fade_effect)); + priv->fade_effect = NULL; + } + } +} + +static void +st_scroll_view_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + StScrollView *self = ST_SCROLL_VIEW (object); + StScrollViewPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_MOUSE_SCROLL: + st_scroll_view_set_mouse_scrolling (self, + g_value_get_boolean (value)); + break; + case PROP_OVERLAY_SCROLLBARS: + st_scroll_view_set_overlay_scrollbars (self, + g_value_get_boolean (value)); + break; + case PROP_HSCROLLBAR_POLICY: + st_scroll_view_set_policy (self, + g_value_get_enum (value), + priv->vscrollbar_policy); + break; + case PROP_VSCROLLBAR_POLICY: + st_scroll_view_set_policy (self, + priv->hscrollbar_policy, + g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +st_scroll_view_dispose (GObject *object) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (object)->priv; + + if (priv->fade_effect) + { + clutter_actor_remove_effect (CLUTTER_ACTOR (object), CLUTTER_EFFECT (priv->fade_effect)); + priv->fade_effect = NULL; + } + + g_clear_pointer (&priv->vscroll, clutter_actor_destroy); + g_clear_pointer (&priv->hscroll, clutter_actor_destroy); + + /* For most reliable freeing of memory, an object with signals + * like StAdjustment should be explicitly disposed. Since we own + * the adjustments, we take care of that. This also disconnects + * the signal handlers that we established on creation. + */ + if (priv->hadjustment) + { + g_object_run_dispose (G_OBJECT (priv->hadjustment)); + g_object_unref (priv->hadjustment); + priv->hadjustment = NULL; + } + + if (priv->vadjustment) + { + g_object_run_dispose (G_OBJECT (priv->vadjustment)); + g_object_unref (priv->vadjustment); + priv->vadjustment = NULL; + } + + G_OBJECT_CLASS (st_scroll_view_parent_class)->dispose (object); +} + +static void +st_scroll_view_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; + + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + if (priv->child) + clutter_actor_paint (priv->child, paint_context); + if (priv->hscrollbar_visible) + clutter_actor_paint (priv->hscroll, paint_context); + if (priv->vscrollbar_visible) + clutter_actor_paint (priv->vscroll, paint_context); +} + +static void +st_scroll_view_pick (ClutterActor *actor, + ClutterPickContext *pick_context) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; + + /* Chain up so we get a bounding box pained (if we are reactive) */ + CLUTTER_ACTOR_CLASS (st_scroll_view_parent_class)->pick (actor, pick_context); + + if (priv->child) + clutter_actor_pick (priv->child, pick_context); + if (priv->hscrollbar_visible) + clutter_actor_pick (priv->hscroll, pick_context); + if (priv->vscrollbar_visible) + clutter_actor_pick (priv->vscroll, pick_context); +} + +static gboolean +st_scroll_view_get_paint_volume (ClutterActor *actor, + ClutterPaintVolume *volume) +{ + return clutter_paint_volume_set_from_allocation (volume, actor); +} + +static double +get_scrollbar_width (StScrollView *scroll, + gfloat for_height) +{ + StScrollViewPrivate *priv = scroll->priv; + + if (clutter_actor_is_visible (priv->vscroll)) + { + gfloat min_size; + + clutter_actor_get_preferred_width (CLUTTER_ACTOR (priv->vscroll), for_height, + &min_size, NULL); + return min_size; + } + else + return 0; +} + +static double +get_scrollbar_height (StScrollView *scroll, + gfloat for_width) +{ + StScrollViewPrivate *priv = scroll->priv; + + if (clutter_actor_is_visible (priv->hscroll)) + { + gfloat min_size; + + clutter_actor_get_preferred_height (CLUTTER_ACTOR (priv->hscroll), for_width, + &min_size, NULL); + + return min_size; + } + else + return 0; +} + +static void +st_scroll_view_get_preferred_width (ClutterActor *actor, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean account_for_vscrollbar = FALSE; + gfloat min_width = 0, natural_width; + gfloat child_min_width, child_natural_width; + + if (!priv->child) + return; + + st_theme_node_adjust_for_height (theme_node, &for_height); + + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, &child_natural_width); + + natural_width = child_natural_width; + + switch (priv->hscrollbar_policy) + { + case ST_POLICY_NEVER: + min_width = child_min_width; + break; + case ST_POLICY_ALWAYS: + case ST_POLICY_AUTOMATIC: + case ST_POLICY_EXTERNAL: + /* Should theoretically use the min width of the hscrollbar, + * but that's not cleanly defined at the moment */ + min_width = 0; + break; + default: + g_warn_if_reached(); + break; + } + + switch (priv->vscrollbar_policy) + { + case ST_POLICY_NEVER: + case ST_POLICY_EXTERNAL: + account_for_vscrollbar = FALSE; + break; + case ST_POLICY_ALWAYS: + account_for_vscrollbar = !priv->overlay_scrollbars; + break; + case ST_POLICY_AUTOMATIC: + /* For automatic scrollbars, we always request space for the vertical + * scrollbar; we won't know whether we actually need one until our + * height is assigned in allocate(). + */ + account_for_vscrollbar = !priv->overlay_scrollbars; + break; + default: + g_warn_if_reached(); + break; + } + + if (account_for_vscrollbar) + { + float sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor), for_height); + + min_width += sb_width; + natural_width += sb_width; + } + + if (min_width_p) + *min_width_p = min_width; + + if (natural_width_p) + *natural_width_p = natural_width; + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_scroll_view_get_preferred_height (ClutterActor *actor, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + gboolean account_for_hscrollbar = FALSE; + gfloat min_height = 0, natural_height; + gfloat child_min_height, child_natural_height; + gfloat child_min_width; + gfloat sb_width; + + if (!priv->child) + return; + + st_theme_node_adjust_for_width (theme_node, &for_width); + + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, NULL); + + if (min_height_p) + *min_height_p = 0; + + sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor), -1); + + switch (priv->vscrollbar_policy) + { + case ST_POLICY_NEVER: + case ST_POLICY_EXTERNAL: + break; + case ST_POLICY_ALWAYS: + case ST_POLICY_AUTOMATIC: + /* We've requested space for the scrollbar, subtract it back out */ + for_width -= sb_width; + break; + default: + g_warn_if_reached(); + break; + } + + switch (priv->hscrollbar_policy) + { + case ST_POLICY_NEVER: + case ST_POLICY_EXTERNAL: + account_for_hscrollbar = FALSE; + break; + case ST_POLICY_ALWAYS: + account_for_hscrollbar = !priv->overlay_scrollbars; + break; + case ST_POLICY_AUTOMATIC: + /* For automatic scrollbars, we always request space for the horizontal + * scrollbar; we won't know whether we actually need one until our + * width is assigned in allocate(). + */ + account_for_hscrollbar = !priv->overlay_scrollbars; + break; + default: + g_warn_if_reached(); + break; + } + + clutter_actor_get_preferred_height (priv->child, for_width, + &child_min_height, &child_natural_height); + + natural_height = child_natural_height; + + switch (priv->vscrollbar_policy) + { + case ST_POLICY_NEVER: + min_height = child_min_height; + break; + case ST_POLICY_ALWAYS: + case ST_POLICY_AUTOMATIC: + case ST_POLICY_EXTERNAL: + /* Should theoretically use the min height of the vscrollbar, + * but that's not cleanly defined at the moment */ + min_height = 0; + break; + default: + g_warn_if_reached(); + break; + } + + if (account_for_hscrollbar) + { + float sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor), for_width); + + min_height += sb_height; + natural_height += sb_height; + } + + if (min_height_p) + *min_height_p = min_height; + + if (natural_height_p) + *natural_height_p = natural_height; + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_scroll_view_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + ClutterActorBox content_box, child_box; + gfloat avail_width, avail_height, sb_width, sb_height; + gboolean hscrollbar_visible, vscrollbar_visible; + + StScrollViewPrivate *priv = ST_SCROLL_VIEW (actor)->priv; + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + + clutter_actor_set_allocation (actor, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + avail_width = content_box.x2 - content_box.x1; + avail_height = content_box.y2 - content_box.y1; + + if (clutter_actor_get_request_mode (actor) == CLUTTER_REQUEST_HEIGHT_FOR_WIDTH) + { + sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor), -1); + sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor), sb_width); + } + else + { + sb_height = get_scrollbar_height (ST_SCROLL_VIEW (actor), -1); + sb_width = get_scrollbar_width (ST_SCROLL_VIEW (actor), sb_height); + } + + /* Determine what scrollbars are visible. The basic idea of the + * handling of an automatic scrollbars is that we start off with the + * assumption that we don't need any scrollbars, see if that works, + * and if not add horizontal and vertical scrollbars until we are no + * longer overflowing. + */ + if (priv->child) + { + gfloat child_min_width; + gfloat child_min_height; + + clutter_actor_get_preferred_width (priv->child, -1, + &child_min_width, NULL); + + if (priv->vscrollbar_policy == ST_POLICY_AUTOMATIC) + { + if (priv->hscrollbar_policy == ST_POLICY_AUTOMATIC) + { + /* Pass one, try without a vertical scrollbar */ + clutter_actor_get_preferred_height (priv->child, avail_width, &child_min_height, NULL); + vscrollbar_visible = child_min_height > avail_height; + hscrollbar_visible = child_min_width > avail_width - (vscrollbar_visible ? sb_width : 0); + vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0); + + /* Pass two - if we needed a vertical scrollbar, get a new preferred height */ + if (vscrollbar_visible) + { + clutter_actor_get_preferred_height (priv->child, MAX (avail_width - sb_width, 0), + &child_min_height, NULL); + hscrollbar_visible = child_min_width > avail_width - sb_width; + } + } + else + { + hscrollbar_visible = priv->hscrollbar_policy == ST_POLICY_ALWAYS; + + /* try without a vertical scrollbar */ + clutter_actor_get_preferred_height (priv->child, avail_width, &child_min_height, NULL); + vscrollbar_visible = child_min_height > avail_height - (hscrollbar_visible ? sb_height : 0); + } + } + else + { + vscrollbar_visible = priv->vscrollbar_policy == ST_POLICY_ALWAYS; + + if (priv->hscrollbar_policy == ST_POLICY_AUTOMATIC) + hscrollbar_visible = child_min_width > avail_height - (vscrollbar_visible ? 0 : sb_width); + else + hscrollbar_visible = priv->hscrollbar_policy == ST_POLICY_ALWAYS; + } + } + else + { + hscrollbar_visible = priv->hscrollbar_policy != ST_POLICY_NEVER && + priv->hscrollbar_policy != ST_POLICY_EXTERNAL; + vscrollbar_visible = priv->vscrollbar_policy != ST_POLICY_NEVER && + priv->vscrollbar_policy != ST_POLICY_EXTERNAL; + } + + /* Whether or not we show the scrollbars, if the scrollbars are visible + * actors, we need to give them some allocation, so we unconditionally + * give them the "right" allocation; that might overlap the child when + * the scrollbars are not visible, but it doesn't matter because we + * don't include them in pick or paint. + */ + + /* Vertical scrollbar */ + if (clutter_actor_get_text_direction (actor) == CLUTTER_TEXT_DIRECTION_RTL) + { + child_box.x1 = content_box.x1; + child_box.x2 = content_box.x1 + sb_width; + } + else + { + child_box.x1 = content_box.x2 - sb_width; + child_box.x2 = content_box.x2; + } + child_box.y1 = content_box.y1; + child_box.y2 = content_box.y2 - (hscrollbar_visible ? sb_height : 0); + + clutter_actor_allocate (priv->vscroll, &child_box); + + /* Horizontal scrollbar */ + if (clutter_actor_get_text_direction (actor) == CLUTTER_TEXT_DIRECTION_RTL) + { + child_box.x1 = content_box.x1 + (vscrollbar_visible ? sb_width : 0); + child_box.x2 = content_box.x2; + } + else + { + child_box.x1 = content_box.x1; + child_box.x2 = content_box.x2 - (vscrollbar_visible ? sb_width : 0); + } + child_box.y1 = content_box.y2 - sb_height; + child_box.y2 = content_box.y2; + + clutter_actor_allocate (priv->hscroll, &child_box); + + /* In case the scrollbar policy is NEVER or EXTERNAL or scrollbars + * should be overlaid, we don't trim the content box allocation by + * the scrollbar size. + * Fold this into the scrollbar sizes to simplify the rest of the + * computations. + */ + if (priv->hscrollbar_policy == ST_POLICY_NEVER || + priv->hscrollbar_policy == ST_POLICY_EXTERNAL || + priv->overlay_scrollbars) + sb_height = 0; + if (priv->vscrollbar_policy == ST_POLICY_NEVER || + priv->vscrollbar_policy == ST_POLICY_EXTERNAL || + priv->overlay_scrollbars) + sb_width = 0; + + /* Child */ + if (clutter_actor_get_text_direction (actor) == CLUTTER_TEXT_DIRECTION_RTL) + { + child_box.x1 = content_box.x1 + sb_width; + child_box.x2 = content_box.x2; + } + else + { + child_box.x1 = content_box.x1; + child_box.x2 = content_box.x2 - sb_width; + } + child_box.y1 = content_box.y1; + child_box.y2 = content_box.y2 - sb_height; + + if (priv->child) + clutter_actor_allocate (priv->child, &child_box); + + if (priv->hscrollbar_visible != hscrollbar_visible) + { + g_object_freeze_notify (G_OBJECT (actor)); + priv->hscrollbar_visible = hscrollbar_visible; + g_object_notify_by_pspec (G_OBJECT (actor), + props[PROP_HSCROLLBAR_VISIBLE]); + g_object_thaw_notify (G_OBJECT (actor)); + } + + if (priv->vscrollbar_visible != vscrollbar_visible) + { + g_object_freeze_notify (G_OBJECT (actor)); + priv->vscrollbar_visible = vscrollbar_visible; + g_object_notify_by_pspec (G_OBJECT (actor), + props[PROP_VSCROLLBAR_VISIBLE]); + g_object_thaw_notify (G_OBJECT (actor)); + } + +} + +static void +adjust_with_direction (StAdjustment *adj, + ClutterScrollDirection direction) +{ + gdouble delta; + + switch (direction) + { + case CLUTTER_SCROLL_UP: + case CLUTTER_SCROLL_LEFT: + delta = -1.0; + break; + case CLUTTER_SCROLL_RIGHT: + case CLUTTER_SCROLL_DOWN: + delta = 1.0; + break; + case CLUTTER_SCROLL_SMOOTH: + default: + g_assert_not_reached (); + break; + } + + st_adjustment_adjust_for_scroll_event (adj, delta); +} + +static void +st_scroll_view_style_changed (StWidget *widget) +{ + StScrollView *self = ST_SCROLL_VIEW (widget); + gboolean has_vfade, has_hfade; + double vfade_offset = 0.0; + double hfade_offset = 0.0; + + StThemeNode *theme_node = st_widget_get_theme_node (widget); + + has_vfade = st_theme_node_lookup_length (theme_node, "-st-vfade-offset", FALSE, &vfade_offset); + has_hfade = st_theme_node_lookup_length (theme_node, "-st-hfade-offset", FALSE, &hfade_offset); + if (has_vfade || has_hfade) + { + st_scroll_view_update_fade_effect (self, + &(ClutterMargin) { + .top = vfade_offset, + .bottom = vfade_offset, + .left = hfade_offset, + .right = hfade_offset, + }); + } + + ST_WIDGET_CLASS (st_scroll_view_parent_class)->style_changed (widget); +} + +static gboolean +st_scroll_view_scroll_event (ClutterActor *self, + ClutterScrollEvent *event) +{ + StScrollViewPrivate *priv = ST_SCROLL_VIEW (self)->priv; + ClutterTextDirection direction; + + /* don't handle scroll events if requested not to */ + if (!priv->mouse_scroll) + return FALSE; + + if (clutter_event_is_pointer_emulated ((ClutterEvent *) event)) + return TRUE; + + direction = clutter_actor_get_text_direction (self); + + switch (event->direction) + { + case CLUTTER_SCROLL_SMOOTH: + { + gdouble delta_x, delta_y; + clutter_event_get_scroll_delta ((ClutterEvent *)event, &delta_x, &delta_y); + + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + delta_x *= -1; + + st_adjustment_adjust_for_scroll_event (priv->hadjustment, delta_x); + st_adjustment_adjust_for_scroll_event (priv->vadjustment, delta_y); + } + break; + case CLUTTER_SCROLL_UP: + case CLUTTER_SCROLL_DOWN: + adjust_with_direction (priv->vadjustment, event->direction); + break; + case CLUTTER_SCROLL_LEFT: + case CLUTTER_SCROLL_RIGHT: + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + { + ClutterScrollDirection dir; + + dir = event->direction == CLUTTER_SCROLL_LEFT ? CLUTTER_SCROLL_RIGHT + : CLUTTER_SCROLL_LEFT; + adjust_with_direction (priv->hadjustment, dir); + } + else + { + adjust_with_direction (priv->hadjustment, event->direction); + } + break; + default: + g_warn_if_reached(); + break; + } + + return TRUE; +} + +static void +st_scroll_view_class_init (StScrollViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + StWidgetClass *widget_class = ST_WIDGET_CLASS (klass); + + object_class->get_property = st_scroll_view_get_property; + object_class->set_property = st_scroll_view_set_property; + object_class->dispose = st_scroll_view_dispose; + + actor_class->paint = st_scroll_view_paint; + actor_class->pick = st_scroll_view_pick; + actor_class->get_paint_volume = st_scroll_view_get_paint_volume; + actor_class->get_preferred_width = st_scroll_view_get_preferred_width; + actor_class->get_preferred_height = st_scroll_view_get_preferred_height; + actor_class->allocate = st_scroll_view_allocate; + actor_class->scroll_event = st_scroll_view_scroll_event; + + widget_class->style_changed = st_scroll_view_style_changed; + + /** + * StScrollView:hscroll: + * + * The horizontal #StScrollBar for the #StScrollView. + */ + props[PROP_HSCROLL] = + g_param_spec_object ("hscroll", + "StScrollBar", + "Horizontal scroll indicator", + ST_TYPE_SCROLL_BAR, + ST_PARAM_READABLE); + + /** + * StScrollView:vscroll: + * + * The vertical #StScrollBar for the #StScrollView. + */ + props[PROP_VSCROLL] = + g_param_spec_object ("vscroll", + "StScrollBar", + "Vertical scroll indicator", + ST_TYPE_SCROLL_BAR, + ST_PARAM_READABLE); + + /** + * StScrollView:vscrollbar-policy: + * + * The #StPolicyType for when to show the vertical #StScrollBar. + */ + props[PROP_VSCROLLBAR_POLICY] = + g_param_spec_enum ("vscrollbar-policy", + "Vertical Scrollbar Policy", + "When the vertical scrollbar is displayed", + ST_TYPE_POLICY_TYPE, + ST_POLICY_AUTOMATIC, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollView:hscrollbar-policy: + * + * The #StPolicyType for when to show the horizontal #StScrollBar. + */ + props[PROP_HSCROLLBAR_POLICY] = + g_param_spec_enum ("hscrollbar-policy", + "Horizontal Scrollbar Policy", + "When the horizontal scrollbar is displayed", + ST_TYPE_POLICY_TYPE, + ST_POLICY_AUTOMATIC, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollView:hscrollbar-visible: + * + * Whether the horizontal #StScrollBar is visible. + */ + props[PROP_HSCROLLBAR_VISIBLE] = + g_param_spec_boolean ("hscrollbar-visible", + "Horizontal Scrollbar Visibility", + "Whether the horizontal scrollbar is visible", + TRUE, + ST_PARAM_READABLE); + + /** + * StScrollView:vscrollbar-visible: + * + * Whether the vertical #StScrollBar is visible. + */ + props[PROP_VSCROLLBAR_VISIBLE] = + g_param_spec_boolean ("vscrollbar-visible", + "Vertical Scrollbar Visibility", + "Whether the vertical scrollbar is visible", + TRUE, + ST_PARAM_READABLE); + + /** + * StScrollView:enable-mouse-scrolling: + * + * Whether to enable automatic mouse wheel scrolling. + */ + props[PROP_MOUSE_SCROLL] = + g_param_spec_boolean ("enable-mouse-scrolling", + "Enable Mouse Scrolling", + "Enable automatic mouse wheel scrolling", + TRUE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StScrollView:overlay-scrollbars: + * + * Whether scrollbars are painted on top of the content. + */ + props[PROP_OVERLAY_SCROLLBARS] = + g_param_spec_boolean ("overlay-scrollbars", + "Use Overlay Scrollbars", + "Overlay scrollbars over the content", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +st_scroll_view_init (StScrollView *self) +{ + StScrollViewPrivate *priv = self->priv = st_scroll_view_get_instance_private (self); + + priv->hscrollbar_policy = ST_POLICY_AUTOMATIC; + priv->vscrollbar_policy = ST_POLICY_AUTOMATIC; + + priv->hadjustment = g_object_new (ST_TYPE_ADJUSTMENT, + "actor", self, + NULL); + priv->hscroll = g_object_new (ST_TYPE_SCROLL_BAR, + "adjustment", priv->hadjustment, + "vertical", FALSE, + NULL); + + priv->vadjustment = g_object_new (ST_TYPE_ADJUSTMENT, + "actor", self, + NULL); + priv->vscroll = g_object_new (ST_TYPE_SCROLL_BAR, + "adjustment", priv->vadjustment, + "vertical", TRUE, + NULL); + + clutter_actor_add_child (CLUTTER_ACTOR (self), priv->hscroll); + clutter_actor_add_child (CLUTTER_ACTOR (self), priv->vscroll); + + /* mouse scroll is enabled by default, so we also need to be reactive */ + priv->mouse_scroll = TRUE; + g_object_set (G_OBJECT (self), "reactive", TRUE, NULL); +} + +static void +st_scroll_view_add (ClutterContainer *container, + ClutterActor *actor) +{ + StScrollView *self = ST_SCROLL_VIEW (container); + StScrollViewPrivate *priv = self->priv; + + if (ST_IS_SCROLLABLE (actor)) + { + priv->child = actor; + + /* chain up to StBin::add() */ + st_scroll_view_parent_iface->add (container, actor); + + st_scrollable_set_adjustments (ST_SCROLLABLE (actor), + priv->hadjustment, priv->vadjustment); + } + else + { + g_warning ("Attempting to add an actor of type %s to " + "a StScrollView, but the actor does " + "not implement StScrollable.", + g_type_name (G_OBJECT_TYPE (actor))); + } +} + +static void +st_scroll_view_remove (ClutterContainer *container, + ClutterActor *actor) +{ + StScrollView *self = ST_SCROLL_VIEW (container); + StScrollViewPrivate *priv = self->priv; + + if (actor == priv->child) + { + g_object_ref (priv->child); + + /* chain up to StBin::remove() */ + st_scroll_view_parent_iface->remove (container, actor); + + st_scrollable_set_adjustments (ST_SCROLLABLE (priv->child), + NULL, NULL); + + g_object_unref (priv->child); + priv->child = NULL; + } + else + { + if (actor == priv->vscroll) + priv->vscroll = NULL; + else if (actor == priv->hscroll) + priv->hscroll = NULL; + else + g_assert ("Unknown child removed from StScrollView"); + + clutter_actor_remove_child (CLUTTER_ACTOR (container), actor); + } +} + +static void +clutter_container_iface_init (ClutterContainerIface *iface) +{ + /* store a pointer to the StBin implementation of + * ClutterContainer so that we can chain up when + * overriding the methods + */ + st_scroll_view_parent_iface = g_type_interface_peek_parent (iface); + + iface->add = st_scroll_view_add; + iface->remove = st_scroll_view_remove; +} + +/** + * st_scroll_view_new: + * + * Create a new #StScrollView. + * + * Returns: (transfer full): a new #StScrollView + */ +StWidget * +st_scroll_view_new (void) +{ + return g_object_new (ST_TYPE_SCROLL_VIEW, NULL); +} + +/** + * st_scroll_view_get_hscroll_bar: + * @scroll: a #StScrollView + * + * Gets the horizontal #StScrollBar of the #StScrollView. + * + * Returns: (transfer none): the horizontal scrollbar + */ +ClutterActor * +st_scroll_view_get_hscroll_bar (StScrollView *scroll) +{ + g_return_val_if_fail (ST_IS_SCROLL_VIEW (scroll), NULL); + + return scroll->priv->hscroll; +} + +/** + * st_scroll_view_get_vscroll_bar: + * @scroll: a #StScrollView + * + * Gets the vertical scrollbar of the #StScrollView. + * + * Returns: (transfer none): the vertical #StScrollBar + */ +ClutterActor * +st_scroll_view_get_vscroll_bar (StScrollView *scroll) +{ + g_return_val_if_fail (ST_IS_SCROLL_VIEW (scroll), NULL); + + return scroll->priv->vscroll; +} + +/** + * st_scroll_view_get_column_size: + * @scroll: a #StScrollView + * + * Get the step increment of the horizontal plane. + * + * Returns: the horizontal step increment + */ +gfloat +st_scroll_view_get_column_size (StScrollView *scroll) +{ + gdouble column_size; + + g_return_val_if_fail (scroll, 0); + + g_object_get (scroll->priv->hadjustment, + "step-increment", &column_size, + NULL); + + return column_size; +} + +/** + * st_scroll_view_set_column_size: + * @scroll: a #StScrollView + * @column_size: horizontal step increment + * + * Set the step increment of the horizontal plane to @column_size. + */ +void +st_scroll_view_set_column_size (StScrollView *scroll, + gfloat column_size) +{ + g_return_if_fail (scroll); + + if (column_size < 0) + { + scroll->priv->column_size_set = FALSE; + scroll->priv->column_size = -1; + } + else + { + scroll->priv->column_size_set = TRUE; + scroll->priv->column_size = column_size; + + g_object_set (scroll->priv->hadjustment, + "step-increment", (gdouble) scroll->priv->column_size, + NULL); + } +} + +/** + * st_scroll_view_get_row_size: + * @scroll: a #StScrollView + * + * Get the step increment of the vertical plane. + * + * Returns: the vertical step increment + */ +gfloat +st_scroll_view_get_row_size (StScrollView *scroll) +{ + gdouble row_size; + + g_return_val_if_fail (scroll, 0); + + g_object_get (scroll->priv->vadjustment, + "step-increment", &row_size, + NULL); + + return row_size; +} + +/** + * st_scroll_view_set_row_size: + * @scroll: a #StScrollView + * @row_size: vertical step increment + * + * Set the step increment of the vertical plane to @row_size. + */ +void +st_scroll_view_set_row_size (StScrollView *scroll, + gfloat row_size) +{ + g_return_if_fail (scroll); + + if (row_size < 0) + { + scroll->priv->row_size_set = FALSE; + scroll->priv->row_size = -1; + } + else + { + scroll->priv->row_size_set = TRUE; + scroll->priv->row_size = row_size; + + g_object_set (scroll->priv->vadjustment, + "step-increment", (gdouble) scroll->priv->row_size, + NULL); + } +} + +/** + * st_scroll_view_set_mouse_scrolling: + * @scroll: a #StScrollView + * @enabled: %TRUE or %FALSE + * + * Sets automatic mouse wheel scrolling to enabled or disabled. + */ +void +st_scroll_view_set_mouse_scrolling (StScrollView *scroll, + gboolean enabled) +{ + StScrollViewPrivate *priv; + + g_return_if_fail (ST_IS_SCROLL_VIEW (scroll)); + + priv = ST_SCROLL_VIEW (scroll)->priv; + + if (priv->mouse_scroll != enabled) + { + priv->mouse_scroll = enabled; + + /* make sure we can receive mouse wheel events */ + if (enabled) + clutter_actor_set_reactive ((ClutterActor *) scroll, TRUE); + + g_object_notify_by_pspec (G_OBJECT (scroll), props[PROP_MOUSE_SCROLL]); + } +} + +/** + * st_scroll_view_get_mouse_scrolling: + * @scroll: a #StScrollView + * + * Get whether automatic mouse wheel scrolling is enabled or disabled. + * + * Returns: %TRUE if enabled, %FALSE otherwise + */ +gboolean +st_scroll_view_get_mouse_scrolling (StScrollView *scroll) +{ + StScrollViewPrivate *priv; + + g_return_val_if_fail (ST_IS_SCROLL_VIEW (scroll), FALSE); + + priv = ST_SCROLL_VIEW (scroll)->priv; + + return priv->mouse_scroll; +} + +/** + * st_scroll_view_set_overlay_scrollbars: + * @scroll: A #StScrollView + * @enabled: Whether to enable overlay scrollbars + * + * Sets whether scrollbars are painted on top of the content. + */ +void +st_scroll_view_set_overlay_scrollbars (StScrollView *scroll, + gboolean enabled) +{ + StScrollViewPrivate *priv; + + g_return_if_fail (ST_IS_SCROLL_VIEW (scroll)); + + priv = ST_SCROLL_VIEW (scroll)->priv; + + if (priv->overlay_scrollbars != enabled) + { + priv->overlay_scrollbars = enabled; + g_object_notify_by_pspec (G_OBJECT (scroll), + props[PROP_OVERLAY_SCROLLBARS]); + clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll)); + } +} + +/** + * st_scroll_view_get_overlay_scrollbars: + * @scroll: A #StScrollView + * + * Gets whether scrollbars are painted on top of the content. + * + * Returns: %TRUE if enabled, %FALSE otherwise + */ +gboolean +st_scroll_view_get_overlay_scrollbars (StScrollView *scroll) +{ + StScrollViewPrivate *priv; + + g_return_val_if_fail (ST_IS_SCROLL_VIEW (scroll), FALSE); + + priv = ST_SCROLL_VIEW (scroll)->priv; + + return priv->overlay_scrollbars; +} + +/** + * st_scroll_view_set_policy: + * @scroll: A #StScrollView + * @hscroll: Whether to enable horizontal scrolling + * @vscroll: Whether to enable vertical scrolling + * + * Set the scroll policy. + */ +void +st_scroll_view_set_policy (StScrollView *scroll, + StPolicyType hscroll, + StPolicyType vscroll) +{ + StScrollViewPrivate *priv; + + g_return_if_fail (ST_IS_SCROLL_VIEW (scroll)); + + priv = ST_SCROLL_VIEW (scroll)->priv; + + if (priv->hscrollbar_policy == hscroll && priv->vscrollbar_policy == vscroll) + return; + + g_object_freeze_notify ((GObject *) scroll); + + if (priv->hscrollbar_policy != hscroll) + { + priv->hscrollbar_policy = hscroll; + g_object_notify_by_pspec ((GObject *) scroll, + props[PROP_HSCROLLBAR_POLICY]); + } + + if (priv->vscrollbar_policy != vscroll) + { + priv->vscrollbar_policy = vscroll; + g_object_notify_by_pspec ((GObject *) scroll, + props[PROP_VSCROLLBAR_POLICY]); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (scroll)); + + g_object_thaw_notify ((GObject *) scroll); +} diff --git a/src/st/st-scroll-view.h b/src/st/st-scroll-view.h new file mode 100644 index 0000000..e2acaca --- /dev/null +++ b/src/st/st-scroll-view.h @@ -0,0 +1,90 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scroll-view.h: Container with scroll-bars + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * Copyright 2010 Red Hat, Inc. + * Copyright 2010 Maxim Ermilov + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_SCROLL_VIEW_H__ +#define __ST_SCROLL_VIEW_H__ + +#include <st/st-bin.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SCROLL_VIEW (st_scroll_view_get_type()) +G_DECLARE_FINAL_TYPE (StScrollView, st_scroll_view, ST, SCROLL_VIEW, StBin) + +typedef enum +{ + ST_POLICY_ALWAYS, + ST_POLICY_AUTOMATIC, + ST_POLICY_NEVER, + ST_POLICY_EXTERNAL, +} StPolicyType; + +typedef struct _StScrollViewPrivate StScrollViewPrivate; + +/** + * StScrollView: + * + * The contents of this structure are private and should only be accessed + * through the public API. + */ +struct _StScrollView +{ + /*< private >*/ + StBin parent_instance; + + StScrollViewPrivate *priv; +}; + +StWidget *st_scroll_view_new (void); + +ClutterActor *st_scroll_view_get_hscroll_bar (StScrollView *scroll); +ClutterActor *st_scroll_view_get_vscroll_bar (StScrollView *scroll); + +gfloat st_scroll_view_get_column_size (StScrollView *scroll); +void st_scroll_view_set_column_size (StScrollView *scroll, + gfloat column_size); + +gfloat st_scroll_view_get_row_size (StScrollView *scroll); +void st_scroll_view_set_row_size (StScrollView *scroll, + gfloat row_size); + +void st_scroll_view_set_mouse_scrolling (StScrollView *scroll, + gboolean enabled); +gboolean st_scroll_view_get_mouse_scrolling (StScrollView *scroll); + +void st_scroll_view_set_overlay_scrollbars (StScrollView *scroll, + gboolean enabled); +gboolean st_scroll_view_get_overlay_scrollbars (StScrollView *scroll); + +void st_scroll_view_set_policy (StScrollView *scroll, + StPolicyType hscroll, + StPolicyType vscroll); +void st_scroll_view_update_fade_effect (StScrollView *scroll, + ClutterMargin *fade_margins); + +G_END_DECLS + +#endif /* __ST_SCROLL_VIEW_H__ */ diff --git a/src/st/st-scrollable.c b/src/st/st-scrollable.c new file mode 100644 index 0000000..3a77052 --- /dev/null +++ b/src/st/st-scrollable.c @@ -0,0 +1,196 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scrollable.c: Scrollable interface + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * Copyright 2010 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "st-private.h" +#include "st-scrollable.h" + +G_DEFINE_INTERFACE (StScrollable, st_scrollable, G_TYPE_OBJECT) + +/** + * SECTION:st-scrollable + * @short_description: A #ClutterActor that can be scrolled + * + * The #StScrollable interface is exposed by actors that support scrolling. + * + * The interface contains methods for getting and setting the adjustments + * for scrolling; these adjustments will be used to hook the scrolled + * position up to scrollbars or other external controls. When a #StScrollable + * is added to a parent container, the parent container is responsible + * for setting the adjustments. The parent container then sets the adjustments + * back to %NULL when the scrollable is removed. + * + * For #StScrollable supporting height-for-width size negotiation, size + * negotiation works as follows: + * + * In response to get_preferred_width(), the scrollable should report + * the minimum width at which horizontal scrolling is needed for the + * preferred width, and natural width of the actor when not + * horizontally scrolled as the natural width. + * + * The for_width passed into get_preferred_height() is the width at which + * the scrollable will be allocated; this will be smaller than the minimum + * width when scrolling horizontally, so the scrollable may want to adjust + * it up to the minimum width before computing a preferred height. (Other + * scrollables may want to fit as much content into the allocated area + * as possible and only scroll what absolutely needs to scroll - consider, + * for example, the line-wrapping behavior of a text editor where there + * is a long line without any spaces.) As for width, get_preferred_height() + * should return the minimum size at which no scrolling is needed for the + * minimum height, and the natural size of the actor when not vertically scrolled + * as the natural height. + * + * In allocate() the allocation box passed in will be actual allocated + * size of the actor so will be smaller than the reported minimum + * width and/or height when scrolling is present. Any scrollable actor + * must support being allocated at any size down to 0x0 without + * crashing, however if the actor has content around the scrolled area + * and has an absolute minimum size that's bigger than 0x0 its + * acceptable for it to misdraw between 0x0 and the absolute minimum + * size. It's up to the application author to avoid letting the user + * resize the scroll view small enough so that the scrolled area + * vanishes. + * + * In response to allocate, in addition to normal handling, the + * scrollable should also set the limits of the the horizontal and + * vertical adjustments that were set on it earlier. The standard + * settings are: + * + * lower: 0 + * page_size: allocated size (width or height) + * upper: MAX (total size of the scrolled area,allocated_size) + * step_increment: natural row/column height or a fixed fraction of the page size + * page_increment: page_size - step_increment + */ +static void +st_scrollable_default_init (StScrollableInterface *g_iface) +{ + static gboolean initialized = FALSE; + + if (!initialized) + { + /** + * StScrollable:hadjustment: + * + * The horizontal #StAdjustment used by the #StScrollable. + * + * Implementations should override this property to provide read-write + * access to the #StAdjustment. + * + * JavaScript code may override this as demonstrated below: + * + * |[<!-- language="JavaScript" --> + * var MyScrollable = GObject.registerClass({ + * Properties: { + * 'hadjustment': GObject.ParamSpec.override( + * 'hadjustment', + * St.Scrollable + * ) + * } + * }, class MyScrollable extends St.Scrollable { + * + * get hadjustment() { + * return this._hadjustment || null; + * } + * + * set hadjustment(adjustment) { + * if (this.hadjustment === adjustment) + * return; + * + * this._hadjustment = adjustment; + * this.notify('hadjustment'); + * } + * }); + * ]| + */ + g_object_interface_install_property (g_iface, + g_param_spec_object ("hadjustment", + "StAdjustment", + "Horizontal adjustment", + ST_TYPE_ADJUSTMENT, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); + + /** + * StScrollable:vadjustment: + * + * The vertical #StAdjustment used by the #StScrollable. + * + * Implementations should override this property to provide read-write + * access to the #StAdjustment. + * + * See #StScrollable:hadjustment for an example of how to override this + * property in JavaScript code. + */ + g_object_interface_install_property (g_iface, + g_param_spec_object ("vadjustment", + "StAdjustment", + "Vertical adjustment", + ST_TYPE_ADJUSTMENT, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); + + initialized = TRUE; + } +} + +/** + * st_scrollable_set_adjustments: + * @scrollable: a #StScrollable + * @hadjustment: the horizontal #StAdjustment + * @vadjustment: the vertical #StAdjustment + * + * This method should be implemented by classes implementing the #StScrollable + * interface. + * + * JavaScript code should do this by overriding the `vfunc_set_adjustments()` + * method. + */ +void +st_scrollable_set_adjustments (StScrollable *scrollable, + StAdjustment *hadjustment, + StAdjustment *vadjustment) +{ + ST_SCROLLABLE_GET_IFACE (scrollable)->set_adjustments (scrollable, + hadjustment, + vadjustment); +} + +/** + * st_scroll_bar_get_adjustments: + * @hadjustment: (transfer none) (out) (optional): location to store the horizontal adjustment, or %NULL + * @vadjustment: (transfer none) (out) (optional): location to store the vertical adjustment, or %NULL + * + * Gets the adjustment objects that store the offsets of the scrollable widget + * into its possible scrolling area. + * + * This method should be implemented by classes implementing the #StScrollable + * interface. + * + * JavaScript code should do this by overriding the `vfunc_get_adjustments()` + * method. + */ +void +st_scrollable_get_adjustments (StScrollable *scrollable, + StAdjustment **hadjustment, + StAdjustment **vadjustment) +{ + ST_SCROLLABLE_GET_IFACE (scrollable)->get_adjustments (scrollable, + hadjustment, + vadjustment); +} diff --git a/src/st/st-scrollable.h b/src/st/st-scrollable.h new file mode 100644 index 0000000..797ec7d --- /dev/null +++ b/src/st/st-scrollable.h @@ -0,0 +1,59 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scrollable.h: Scrollable interface + * + * Copyright 2008 OpenedHand + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_SCROLLABLE_H__ +#define __ST_SCROLLABLE_H__ + +#include <glib-object.h> +#include <st/st-adjustment.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SCROLLABLE (st_scrollable_get_type ()) +G_DECLARE_INTERFACE (StScrollable, st_scrollable, ST, SCROLLABLE, GObject) + +typedef struct _StScrollableInterface StScrollableInterface; + +struct _StScrollableInterface +{ + GTypeInterface parent; + + void (* set_adjustments) (StScrollable *scrollable, + StAdjustment *hadjustment, + StAdjustment *vadjustment); + void (* get_adjustments) (StScrollable *scrollable, + StAdjustment **hadjustment, + StAdjustment **vadjustment); +}; + +void st_scrollable_set_adjustments (StScrollable *scrollable, + StAdjustment *hadjustment, + StAdjustment *vadjustment); +void st_scrollable_get_adjustments (StScrollable *scrollable, + StAdjustment **hadjustment, + StAdjustment **vadjustment); + +G_END_DECLS + +#endif /* __ST_SCROLLABLE_H__ */ diff --git a/src/st/st-settings.c b/src/st/st-settings.c new file mode 100644 index 0000000..04bf68f --- /dev/null +++ b/src/st/st-settings.c @@ -0,0 +1,451 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-settings.c: Global settings + * + * Copyright 2019 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <math.h> +#include <gio/gio.h> + +#include "st-private.h" +#include "st-settings.h" + +#define KEY_ENABLE_ANIMATIONS "enable-animations" +#define KEY_PRIMARY_PASTE "gtk-enable-primary-paste" +#define KEY_DRAG_THRESHOLD "drag-threshold" +#define KEY_FONT_NAME "font-name" +#define KEY_HIGH_CONTRAST "high-contrast" +#define KEY_GTK_ICON_THEME "icon-theme" +#define KEY_MAGNIFIER_ACTIVE "screen-magnifier-enabled" +#define KEY_DISABLE_SHOW_PASSWORD "disable-show-password" + +enum { + PROP_0, + PROP_ENABLE_ANIMATIONS, + PROP_PRIMARY_PASTE, + PROP_DRAG_THRESHOLD, + PROP_FONT_NAME, + PROP_HIGH_CONTRAST, + PROP_GTK_ICON_THEME, + PROP_MAGNIFIER_ACTIVE, + PROP_SLOW_DOWN_FACTOR, + PROP_DISABLE_SHOW_PASSWORD, + N_PROPS +}; + +GParamSpec *props[N_PROPS] = { 0 }; + +struct _StSettings +{ + GObject parent_object; + GSettings *interface_settings; + GSettings *mouse_settings; + GSettings *a11y_applications_settings; + GSettings *a11y_interface_settings; + GSettings *lockdown_settings; + + gchar *font_name; + gboolean high_contrast; + gchar *gtk_icon_theme; + int inhibit_animations_count; + gboolean enable_animations; + gboolean primary_paste; + gboolean magnifier_active; + gboolean disable_show_password; + gint drag_threshold; + double slow_down_factor; +}; + +G_DEFINE_TYPE (StSettings, st_settings, G_TYPE_OBJECT) + +#define EPSILON (1e-10) + +static void +st_settings_set_slow_down_factor (StSettings *settings, + double factor) +{ + if (fabs (settings->slow_down_factor - factor) < EPSILON) + return; + + settings->slow_down_factor = factor; + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_SLOW_DOWN_FACTOR]); +} + +static gboolean +get_enable_animations (StSettings *settings) +{ + if (settings->inhibit_animations_count > 0) + return FALSE; + else + return settings->enable_animations; +} + +void +st_settings_inhibit_animations (StSettings *settings) +{ + gboolean enable_animations; + + enable_animations = get_enable_animations (settings); + settings->inhibit_animations_count++; + + if (enable_animations != get_enable_animations (settings)) + g_object_notify_by_pspec (G_OBJECT (settings), + props[PROP_ENABLE_ANIMATIONS]); +} + +void +st_settings_uninhibit_animations (StSettings *settings) +{ + gboolean enable_animations; + + enable_animations = get_enable_animations (settings); + settings->inhibit_animations_count--; + + if (enable_animations != get_enable_animations (settings)) + g_object_notify_by_pspec (G_OBJECT (settings), + props[PROP_ENABLE_ANIMATIONS]); +} + +static void +st_settings_finalize (GObject *object) +{ + StSettings *settings = ST_SETTINGS (object); + + g_object_unref (settings->interface_settings); + g_object_unref (settings->mouse_settings); + g_object_unref (settings->a11y_applications_settings); + g_object_unref (settings->a11y_interface_settings); + g_object_unref (settings->lockdown_settings); + g_free (settings->font_name); + g_free (settings->gtk_icon_theme); + + G_OBJECT_CLASS (st_settings_parent_class)->finalize (object); +} + +static void +st_settings_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StSettings *settings = ST_SETTINGS (object); + + switch (prop_id) + { + case PROP_SLOW_DOWN_FACTOR: + st_settings_set_slow_down_factor (settings, g_value_get_double (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +st_settings_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StSettings *settings = ST_SETTINGS (object); + + switch (prop_id) + { + case PROP_ENABLE_ANIMATIONS: + g_value_set_boolean (value, get_enable_animations (settings)); + break; + case PROP_PRIMARY_PASTE: + g_value_set_boolean (value, settings->primary_paste); + break; + case PROP_DRAG_THRESHOLD: + g_value_set_int (value, settings->drag_threshold); + break; + case PROP_FONT_NAME: + g_value_set_string (value, settings->font_name); + break; + case PROP_HIGH_CONTRAST: + g_value_set_boolean (value, settings->high_contrast); + break; + case PROP_GTK_ICON_THEME: + g_value_set_string (value, settings->gtk_icon_theme); + break; + case PROP_MAGNIFIER_ACTIVE: + g_value_set_boolean (value, settings->magnifier_active); + break; + case PROP_SLOW_DOWN_FACTOR: + g_value_set_double (value, settings->slow_down_factor); + break; + case PROP_DISABLE_SHOW_PASSWORD: + g_value_set_boolean (value, settings->disable_show_password); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +st_settings_class_init (StSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = st_settings_finalize; + object_class->set_property = st_settings_set_property; + object_class->get_property = st_settings_get_property; + + /** + * StSettings:enable-animations: + * + * Whether animations are enabled. + */ + props[PROP_ENABLE_ANIMATIONS] = g_param_spec_boolean ("enable-animations", + "Enable animations", + "Enable animations", + TRUE, + ST_PARAM_READABLE); + + /** + * StSettings:primary-paste: + * + * Whether pasting from the `PRIMARY` selection is supported (eg. middle-click + * paste). + */ + props[PROP_PRIMARY_PASTE] = g_param_spec_boolean ("primary-paste", + "Primary paste", + "Primary paste", + TRUE, + ST_PARAM_READABLE); + + /** + * StSettings:drag-threshold: + * + * The threshold before a drag operation begins. + */ + props[PROP_DRAG_THRESHOLD] = g_param_spec_int ("drag-threshold", + "Drag threshold", + "Drag threshold", + 0, G_MAXINT, 8, + ST_PARAM_READABLE); + + /** + * StSettings:font-name: + * + * The current font name. + */ + props[PROP_FONT_NAME] = g_param_spec_string ("font-name", + "font name", + "font name", + "", + ST_PARAM_READABLE); + + /** + * StSettings:high-contrast: + * + * Whether the accessibility high contrast mode is enabled. + */ + props[PROP_HIGH_CONTRAST] = g_param_spec_boolean ("high-contrast", + "High contrast", + "High contrast", + FALSE, + ST_PARAM_READABLE); + + /** + * StSettings:gtk-icon-theme: + * + * The current GTK icon theme + */ + props[PROP_GTK_ICON_THEME] = g_param_spec_string ("gtk-icon-theme", + "GTK Icon Theme", + "GTK Icon Theme", + "", + ST_PARAM_READABLE); + + /** + * StSettings:magnifier-active: + * + * Whether the accessibility magnifier is active. + */ + props[PROP_MAGNIFIER_ACTIVE] = g_param_spec_boolean("magnifier-active", + "Magnifier is active", + "Whether the a11y magnifier is active", + FALSE, + ST_PARAM_READABLE); + + /** + * StSettings:slow-down-factor: + * + * The slow-down factor applied to all animation durations. + */ + props[PROP_SLOW_DOWN_FACTOR] = g_param_spec_double("slow-down-factor", + "Slow down factor", + "Factor applied to all animation durations", + EPSILON, G_MAXDOUBLE, 1.0, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StSettings:disable-show-password: + * + * Whether password showing can be locked down + */ + props[PROP_DISABLE_SHOW_PASSWORD] = g_param_spec_boolean("disable-show-password", + "'Show Password' is disabled", + "Whether user can request to see their password", + FALSE, + ST_PARAM_READABLE); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +on_interface_settings_changed (GSettings *g_settings, + const gchar *key, + StSettings *settings) +{ + if (g_str_equal (key, KEY_ENABLE_ANIMATIONS)) + { + settings->enable_animations = g_settings_get_boolean (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_ENABLE_ANIMATIONS]); + } + else if (g_str_equal (key, KEY_PRIMARY_PASTE)) + { + settings->primary_paste = g_settings_get_boolean (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_PRIMARY_PASTE]); + } + else if (g_str_equal (key, KEY_FONT_NAME)) + { + g_free (settings->font_name); + settings->font_name = g_settings_get_string (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_FONT_NAME]); + } + else if (g_str_equal (key, KEY_GTK_ICON_THEME)) + { + g_free (settings->gtk_icon_theme); + settings->gtk_icon_theme = g_settings_get_string (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), + props[PROP_GTK_ICON_THEME]); + } +} + +static void +on_mouse_settings_changed (GSettings *g_settings, + const gchar *key, + StSettings *settings) +{ + if (g_str_equal (key, KEY_DRAG_THRESHOLD)) + { + settings->drag_threshold = g_settings_get_int (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_DRAG_THRESHOLD]); + } +} + +static void +on_a11y_applications_settings_changed (GSettings *g_settings, + const gchar *key, + StSettings *settings) +{ + if (g_str_equal (key, KEY_MAGNIFIER_ACTIVE)) + { + settings->magnifier_active = g_settings_get_boolean (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_MAGNIFIER_ACTIVE]); + } +} + +static void +on_a11y_interface_settings_changed (GSettings *g_settings, + const gchar *key, + StSettings *settings) +{ + if (g_str_equal (key, KEY_HIGH_CONTRAST)) + { + settings->high_contrast = g_settings_get_boolean (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_HIGH_CONTRAST]); + + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_GTK_ICON_THEME]); + } +} + +static void +on_lockdown_settings_changed (GSettings *g_settings, + const gchar *key, + StSettings *settings) +{ + if (g_str_equal (key, KEY_DISABLE_SHOW_PASSWORD)) + { + settings->disable_show_password = g_settings_get_boolean (g_settings, key); + g_object_notify_by_pspec (G_OBJECT (settings), props[PROP_DISABLE_SHOW_PASSWORD]); + } +} + +static void +st_settings_init (StSettings *settings) +{ + settings->interface_settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect (settings->interface_settings, "changed", + G_CALLBACK (on_interface_settings_changed), settings); + + settings->mouse_settings = g_settings_new ("org.gnome.desktop.peripherals.mouse"); + g_signal_connect (settings->mouse_settings, "changed", + G_CALLBACK (on_mouse_settings_changed), settings); + + settings->a11y_applications_settings = g_settings_new ("org.gnome.desktop.a11y.applications"); + g_signal_connect (settings->a11y_applications_settings, "changed", + G_CALLBACK (on_a11y_applications_settings_changed), settings); + + settings->a11y_interface_settings = g_settings_new ("org.gnome.desktop.a11y.interface"); + g_signal_connect (settings->a11y_interface_settings, "changed", + G_CALLBACK (on_a11y_interface_settings_changed), settings); + + settings->lockdown_settings = g_settings_new ("org.gnome.desktop.lockdown"); + g_signal_connect (settings->lockdown_settings, "changed", + G_CALLBACK (on_lockdown_settings_changed), settings); + + settings->enable_animations = g_settings_get_boolean (settings->interface_settings, + KEY_ENABLE_ANIMATIONS); + settings->primary_paste = g_settings_get_boolean (settings->interface_settings, + KEY_PRIMARY_PASTE); + settings->font_name = g_settings_get_string (settings->interface_settings, + KEY_FONT_NAME); + settings->gtk_icon_theme = g_settings_get_string (settings->interface_settings, + KEY_GTK_ICON_THEME); + settings->drag_threshold = g_settings_get_int (settings->mouse_settings, + KEY_DRAG_THRESHOLD); + settings->magnifier_active = g_settings_get_boolean (settings->a11y_applications_settings, + KEY_MAGNIFIER_ACTIVE); + settings->high_contrast = g_settings_get_boolean (settings->a11y_interface_settings, + KEY_HIGH_CONTRAST); + settings->slow_down_factor = 1.; + settings->disable_show_password = g_settings_get_boolean (settings->lockdown_settings, KEY_DISABLE_SHOW_PASSWORD); +} + +/** + * st_settings_get: + * + * Gets the global #StSettings object. + * + * Returns: (transfer none): the global #StSettings object + **/ +StSettings * +st_settings_get (void) +{ + static StSettings *settings = NULL; + + if (!settings) + settings = g_object_new (ST_TYPE_SETTINGS, NULL); + + return settings; +} diff --git a/src/st/st-settings.h b/src/st/st-settings.h new file mode 100644 index 0000000..8b25494 --- /dev/null +++ b/src/st/st-settings.h @@ -0,0 +1,42 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-settings.h: Global settings + * + * Copyright 2019 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_SETTINGS_H__ +#define __ST_SETTINGS_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SETTINGS (st_settings_get_type ()) +G_DECLARE_FINAL_TYPE (StSettings, st_settings, ST, SETTINGS, GObject) + +StSettings * st_settings_get (void); + +void st_settings_inhibit_animations (StSettings *settings); + +void st_settings_uninhibit_animations (StSettings *settings); + +G_END_DECLS + +#endif /* __ST_SETTINGS_H__ */ diff --git a/src/st/st-shadow.c b/src/st/st-shadow.c new file mode 100644 index 0000000..0a8e319 --- /dev/null +++ b/src/st/st-shadow.c @@ -0,0 +1,307 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-shadow.c: Boxed type holding for -st-shadow attributes + * + * Copyright 2009, 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "st-shadow.h" +#include "st-private.h" + +G_DEFINE_BOXED_TYPE (StShadow, st_shadow, st_shadow_ref, st_shadow_unref) +G_DEFINE_BOXED_TYPE (StShadowHelper, st_shadow_helper, st_shadow_helper_copy, st_shadow_helper_free) + +/** + * SECTION: st-shadow + * @short_description: Boxed type for -st-shadow attributes + * + * #StShadow is a boxed type for storing attributes of the -st-shadow + * property, modelled liberally after the CSS3 box-shadow property. + * See http://www.css3.info/preview/box-shadow/ + * + */ + +/** + * st_shadow_new: + * @color: shadow's color + * @xoffset: horizontal offset + * @yoffset: vertical offset + * @blur: blur radius + * @spread: spread radius + * @inset: whether the shadow should be inset + * + * Creates a new #StShadow + * + * Returns: the newly allocated shadow. Use st_shadow_free() when done + */ +StShadow * +st_shadow_new (ClutterColor *color, + gdouble xoffset, + gdouble yoffset, + gdouble blur, + gdouble spread, + gboolean inset) +{ + StShadow *shadow; + + shadow = g_new (StShadow, 1); + + shadow->color = *color; + shadow->xoffset = xoffset; + shadow->yoffset = yoffset; + shadow->blur = blur; + shadow->spread = spread; + shadow->inset = inset; + shadow->ref_count = 1; + + return shadow; +} + +/** + * st_shadow_ref: + * @shadow: a #StShadow + * + * Atomically increments the reference count of @shadow by one. + * + * Returns: the passed in #StShadow. + */ +StShadow * +st_shadow_ref (StShadow *shadow) +{ + g_return_val_if_fail (shadow != NULL, NULL); + g_return_val_if_fail (shadow->ref_count > 0, shadow); + + g_atomic_int_inc (&shadow->ref_count); + return shadow; +} + +/** + * st_shadow_unref: + * @shadow: a #StShadow + * + * Atomically decrements the reference count of @shadow by one. + * If the reference count drops to 0, all memory allocated by the + * #StShadow is released. + */ +void +st_shadow_unref (StShadow *shadow) +{ + g_return_if_fail (shadow != NULL); + g_return_if_fail (shadow->ref_count > 0); + + if (g_atomic_int_dec_and_test (&shadow->ref_count)) + g_free (shadow); +} + +/** + * st_shadow_equal: + * @shadow: a #StShadow + * @other: a different #StShadow + * + * Check if two shadow objects are identical. Note that two shadows may + * compare non-identically if they differ only by floating point rounding + * errors. + * + * Returns: %TRUE if the two shadows are identical + */ +gboolean +st_shadow_equal (StShadow *shadow, + StShadow *other) +{ + g_return_val_if_fail (shadow != NULL, FALSE); + g_return_val_if_fail (other != NULL, FALSE); + + if (shadow == other) + return TRUE; + + /* We use strict equality to compare double quantities; this means + * that, for example, a shadow offset of 0.25in does not necessarily + * compare equal to a shadow offset of 18pt in this test. Assume + * that a few false negatives are mostly harmless. + */ + + return (clutter_color_equal (&shadow->color, &other->color) && + shadow->xoffset == other->xoffset && + shadow->yoffset == other->yoffset && + shadow->blur == other->blur && + shadow->spread == other->spread && + shadow->inset == other->inset); +} + +/** + * st_shadow_get_box: + * @shadow: a #StShadow + * @actor_box: the box allocated to a #ClutterAlctor + * @shadow_box: computed box occupied by @shadow + * + * Gets the box used to paint @shadow, which will be partly + * outside of @actor_box + */ +void +st_shadow_get_box (StShadow *shadow, + const ClutterActorBox *actor_box, + ClutterActorBox *shadow_box) +{ + g_return_if_fail (shadow != NULL); + g_return_if_fail (actor_box != NULL); + g_return_if_fail (shadow_box != NULL); + + /* Inset shadows are drawn below the border, so returning + * the original box is not actually correct; still, it's + * good enough for the purpose of determining additional space + * required outside the actor box. + */ + if (shadow->inset) + { + *shadow_box = *actor_box; + return; + } + + shadow_box->x1 = actor_box->x1 + shadow->xoffset + - shadow->blur - shadow->spread; + shadow_box->x2 = actor_box->x2 + shadow->xoffset + + shadow->blur + shadow->spread; + shadow_box->y1 = actor_box->y1 + shadow->yoffset + - shadow->blur - shadow->spread; + shadow_box->y2 = actor_box->y2 + shadow->yoffset + + shadow->blur + shadow->spread; +} + +/** + * SECTION: st-shadow-helper + * + * An helper for implementing a drop shadow on a actor. + * The actor is expected to recreate the helper whenever its contents + * or size change. Then, it would call st_shadow_helper_paint() inside + * its paint() virtual function. + */ + +struct _StShadowHelper { + StShadow *shadow; + CoglPipeline *pipeline; + + gfloat width; + gfloat height; +}; + +/** + * st_shadow_helper_new: + * @shadow: a #StShadow representing the shadow properties + * + * Builds a #StShadowHelper that will build a drop shadow + * using @source as the mask. + * + * Returns: (transfer full): a new #StShadowHelper + */ +StShadowHelper * +st_shadow_helper_new (StShadow *shadow) +{ + StShadowHelper *helper; + + helper = g_new0 (StShadowHelper, 1); + helper->shadow = st_shadow_ref (shadow); + + return helper; +} + +/** + * st_shadow_helper_update: + * @helper: a #StShadowHelper + * @source: a #ClutterActor + * + * Update @helper from @source. + */ +void +st_shadow_helper_update (StShadowHelper *helper, + ClutterActor *source) +{ + gfloat width, height; + + clutter_actor_get_size (source, &width, &height); + + if (helper->pipeline == NULL || + helper->width != width || + helper->height != height) + { + if (helper->pipeline) + cogl_object_unref (helper->pipeline); + + helper->pipeline = _st_create_shadow_pipeline_from_actor (helper->shadow, source); + helper->width = width; + helper->height = height; + } +} + +/** + * st_shadow_helper_copy: + * @helper: the #StShadowHelper to copy + * + * Returns: (transfer full): a copy of @helper + */ +StShadowHelper * +st_shadow_helper_copy (StShadowHelper *helper) +{ + StShadowHelper *copy; + + copy = g_new (StShadowHelper, 1); + *copy = *helper; + if (copy->pipeline) + cogl_object_ref (copy->pipeline); + st_shadow_ref (copy->shadow); + + return copy; +} + +/** + * st_shadow_helper_free: + * @helper: a #StShadowHelper + * + * Free resources associated with @helper. + */ +void +st_shadow_helper_free (StShadowHelper *helper) +{ + if (helper->pipeline) + cogl_object_unref (helper->pipeline); + st_shadow_unref (helper->shadow); + + g_free (helper); +} + +/** + * st_shadow_helper_paint: + * @helper: a #StShadowHelper + * @framebuffer: a #CoglFramebuffer + * @actor_box: the bounding box of the shadow + * @paint_opacity: the opacity at which the shadow is painted + * + * Paints the shadow associated with @helper This must only + * be called from the implementation of ClutterActor::paint(). + */ +void +st_shadow_helper_paint (StShadowHelper *helper, + CoglFramebuffer *framebuffer, + ClutterActorBox *actor_box, + guint8 paint_opacity) +{ + _st_paint_shadow_with_opacity (helper->shadow, + framebuffer, + helper->pipeline, + actor_box, + paint_opacity); +} diff --git a/src/st/st-shadow.h b/src/st/st-shadow.h new file mode 100644 index 0000000..267d48f --- /dev/null +++ b/src/st/st-shadow.h @@ -0,0 +1,95 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-shadow.h: Boxed type holding for -st-shadow attributes + * + * Copyright 2009, 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_SHADOW__ +#define __ST_SHADOW__ + +#include <clutter/clutter.h> + +G_BEGIN_DECLS + +#define ST_TYPE_SHADOW (st_shadow_get_type ()) +#define ST_TYPE_SHADOW_HELPER (st_shadow_get_type ()) + +typedef struct _StShadow StShadow; +typedef struct _StShadowHelper StShadowHelper; + +/** + * StShadow: + * @color: shadow's color + * @xoffset: horizontal offset - positive values mean placement to the right, + * negative values placement to the left of the element. + * @yoffset: vertical offset - positive values mean placement below, negative + * values placement above the element. + * @blur: shadow's blur radius - a value of 0.0 will result in a hard shadow. + * @spread: shadow's spread radius - grow the shadow without enlarging the + * blur. + * + * Attributes of the -st-shadow property. + */ +struct _StShadow { + ClutterColor color; + gdouble xoffset; + gdouble yoffset; + gdouble blur; + gdouble spread; + gboolean inset; + volatile int ref_count; +}; + +GType st_shadow_get_type (void) G_GNUC_CONST; + +StShadow *st_shadow_new (ClutterColor *color, + gdouble xoffset, + gdouble yoffset, + gdouble blur, + gdouble spread, + gboolean inset); +StShadow *st_shadow_ref (StShadow *shadow); +void st_shadow_unref (StShadow *shadow); + +gboolean st_shadow_equal (StShadow *shadow, + StShadow *other); + +void st_shadow_get_box (StShadow *shadow, + const ClutterActorBox *actor_box, + ClutterActorBox *shadow_box); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (StShadow, st_shadow_unref) + + +GType st_shadow_helper_get_type (void) G_GNUC_CONST; + +StShadowHelper *st_shadow_helper_new (StShadow *shadow); + +StShadowHelper *st_shadow_helper_copy (StShadowHelper *helper); +void st_shadow_helper_free (StShadowHelper *helper); + +void st_shadow_helper_update (StShadowHelper *helper, + ClutterActor *source); + +void st_shadow_helper_paint (StShadowHelper *helper, + CoglFramebuffer *framebuffer, + ClutterActorBox *actor_box, + guint8 paint_opacity); + +G_END_DECLS + +#endif /* __ST_SHADOW__ */ diff --git a/src/st/st-texture-cache.c b/src/st/st-texture-cache.c new file mode 100644 index 0000000..7062221 --- /dev/null +++ b/src/st/st-texture-cache.c @@ -0,0 +1,1688 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-texture-cache.h: Object for loading and caching images as textures + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010, Maxim Ermilov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include "st-image-content.h" +#include "st-texture-cache.h" +#include "st-private.h" +#include "st-settings.h" +#include <gtk/gtk.h> +#include <math.h> +#include <string.h> +#include <glib.h> + +#define CACHE_PREFIX_ICON "icon:" +#define CACHE_PREFIX_FILE "file:" +#define CACHE_PREFIX_FILE_FOR_CAIRO "file-for-cairo:" + +struct _StTextureCachePrivate +{ + GtkIconTheme *icon_theme; + GSettings *settings; + + /* Things that were loaded with a cache policy != NONE */ + GHashTable *keyed_cache; /* char * -> ClutterImage* */ + GHashTable *keyed_surface_cache; /* char * -> cairo_surface_t* */ + + GHashTable *used_scales; /* Set: double */ + + /* Presently this is used to de-duplicate requests for GIcons and async URIs. */ + GHashTable *outstanding_requests; /* char * -> AsyncTextureLoadData * */ + + /* File monitors to evict cache data on changes */ + GHashTable *file_monitors; /* char * -> GFileMonitor * */ + + GCancellable *cancellable; +}; + +static void st_texture_cache_dispose (GObject *object); +static void st_texture_cache_finalize (GObject *object); + +enum +{ + ICON_THEME_CHANGED, + TEXTURE_FILE_CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; +G_DEFINE_TYPE(StTextureCache, st_texture_cache, G_TYPE_OBJECT); + +/* We want to preserve the aspect ratio by default, also the default + * pipeline for an empty texture is full opacity white, which we + * definitely don't want. Skip that by setting 0 opacity. + */ +static ClutterActor * +create_invisible_actor (void) +{ + return g_object_new (CLUTTER_TYPE_ACTOR, + "opacity", 0, + "request-mode", CLUTTER_REQUEST_CONTENT_SIZE, + NULL); +} + +/* Reverse the opacity we added while loading */ +static void +set_content_from_image (ClutterActor *actor, + ClutterContent *image) +{ + g_assert (image && CLUTTER_IS_IMAGE (image)); + + clutter_actor_set_content (actor, image); + clutter_actor_set_opacity (actor, 255); +} + +static void +st_texture_cache_class_init (StTextureCacheClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *)klass; + + gobject_class->dispose = st_texture_cache_dispose; + gobject_class->finalize = st_texture_cache_finalize; + + /** + * StTextureCache::icon-theme-changed: + * @self: a #StTextureCache + * + * Emitted when the icon theme is changed. + */ + signals[ICON_THEME_CHANGED] = + g_signal_new ("icon-theme-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* no default handler slot */ + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * StTextureCache::texture-file-changed: + * @self: a #StTextureCache + * @file: a #GFile + * + * Emitted when the source file of a texture is changed. + */ + signals[TEXTURE_FILE_CHANGED] = + g_signal_new ("texture-file-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* no default handler slot */ + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_FILE); +} + +/* Evicts all cached textures for named icons */ +static void +st_texture_cache_evict_icons (StTextureCache *cache) +{ + GHashTableIter iter; + gpointer key; + gpointer value; + + g_hash_table_iter_init (&iter, cache->priv->keyed_cache); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *cache_key = key; + + /* This is too conservative - it takes out all cached textures + * for GIcons even when they aren't named icons, but it's not + * worth the complexity of parsing the key and calling + * g_icon_new_for_string(); icon theme changes aren't normal */ + if (g_str_has_prefix (cache_key, CACHE_PREFIX_ICON)) + g_hash_table_iter_remove (&iter); + } +} + +static void +on_icon_theme_changed (StSettings *settings, + GParamSpec *pspec, + StTextureCache *cache) +{ + g_autofree gchar *theme = NULL; + + g_cancellable_cancel (cache->priv->cancellable); + g_cancellable_reset (cache->priv->cancellable); + + st_texture_cache_evict_icons (cache); + + g_object_get (settings, "gtk-icon-theme", &theme, NULL); + gtk_icon_theme_set_custom_theme (cache->priv->icon_theme, theme); + + g_signal_emit (cache, signals[ICON_THEME_CHANGED], 0); +} + +static void +on_gtk_icon_theme_changed (GtkIconTheme *icon_theme, + StTextureCache *self) +{ + st_texture_cache_evict_icons (self); + g_signal_emit (self, signals[ICON_THEME_CHANGED], 0); +} + +static void +st_texture_cache_init (StTextureCache *self) +{ + StSettings *settings; + + self->priv = g_new0 (StTextureCachePrivate, 1); + + self->priv->icon_theme = gtk_icon_theme_new (); + gtk_icon_theme_add_resource_path (self->priv->icon_theme, + "/org/gnome/shell/icons"); + g_signal_connect (self->priv->icon_theme, "changed", + G_CALLBACK (on_gtk_icon_theme_changed), self); + + settings = st_settings_get (); + g_signal_connect (settings, "notify::gtk-icon-theme", + G_CALLBACK (on_icon_theme_changed), self); + + self->priv->keyed_cache = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_object_unref); + self->priv->keyed_surface_cache = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) cairo_surface_destroy); + self->priv->used_scales = g_hash_table_new_full (g_double_hash, g_double_equal, + g_free, NULL); + self->priv->outstanding_requests = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + self->priv->file_monitors = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, + g_object_unref, g_object_unref); + + self->priv->cancellable = g_cancellable_new (); + + on_icon_theme_changed (settings, NULL, self); +} + +static void +st_texture_cache_dispose (GObject *object) +{ + StTextureCache *self = (StTextureCache*)object; + + g_cancellable_cancel (self->priv->cancellable); + + g_clear_object (&self->priv->settings); + g_clear_object (&self->priv->icon_theme); + g_clear_object (&self->priv->cancellable); + + g_clear_pointer (&self->priv->keyed_cache, g_hash_table_destroy); + g_clear_pointer (&self->priv->keyed_surface_cache, g_hash_table_destroy); + g_clear_pointer (&self->priv->used_scales, g_hash_table_destroy); + g_clear_pointer (&self->priv->outstanding_requests, g_hash_table_destroy); + g_clear_pointer (&self->priv->file_monitors, g_hash_table_destroy); + + G_OBJECT_CLASS (st_texture_cache_parent_class)->dispose (object); +} + +static void +st_texture_cache_finalize (GObject *object) +{ + G_OBJECT_CLASS (st_texture_cache_parent_class)->finalize (object); +} + +static void +compute_pixbuf_scale (gint width, + gint height, + gint available_width, + gint available_height, + gint *new_width, + gint *new_height) +{ + int scaled_width, scaled_height; + + if (width == 0 || height == 0) + { + *new_width = *new_height = 0; + return; + } + + if (available_width >= 0 && available_height >= 0) + { + /* This should keep the aspect ratio of the image intact, because if + * available_width < (available_height * width) / height + * then + * (available_width * height) / width < available_height + * So we are guaranteed to either scale the image to have an available_width + * for width and height scaled accordingly OR have the available_height + * for height and width scaled accordingly, whichever scaling results + * in the image that can fit both available dimensions. + */ + scaled_width = MIN (available_width, (available_height * width) / height); + scaled_height = MIN (available_height, (available_width * height) / width); + } + else if (available_width >= 0) + { + scaled_width = available_width; + scaled_height = (available_width * height) / width; + } + else if (available_height >= 0) + { + scaled_width = (available_height * width) / height; + scaled_height = available_height; + } + else + { + scaled_width = scaled_height = 0; + } + + /* Scale the image only if that will not increase its original dimensions. */ + if (scaled_width > 0 && scaled_height > 0 && scaled_width < width && scaled_height < height) + { + *new_width = scaled_width; + *new_height = scaled_height; + } + else + { + *new_width = width; + *new_height = height; + } +} + +static void +rgba_from_clutter (GdkRGBA *rgba, + ClutterColor *color) +{ + rgba->red = color->red / 255.; + rgba->green = color->green / 255.; + rgba->blue = color->blue / 255.; + rgba->alpha = color->alpha / 255.; +} + +/* A private structure for keeping width, height and scale. */ +typedef struct { + int width; + int height; + int scale; +} Dimensions; + +/* This struct corresponds to a request for an texture. + * It's creasted when something needs a new texture, + * and destroyed when the texture data is loaded. */ +typedef struct { + StTextureCache *cache; + StTextureCachePolicy policy; + char *key; + + guint width; + guint height; + guint paint_scale; + gfloat resource_scale; + GSList *actors; + + GtkIconInfo *icon_info; + StIconColors *colors; + GFile *file; +} AsyncTextureLoadData; + +static void +texture_load_data_free (gpointer p) +{ + AsyncTextureLoadData *data = p; + + if (data->icon_info) + { + g_object_unref (data->icon_info); + if (data->colors) + st_icon_colors_unref (data->colors); + } + else if (data->file) + g_object_unref (data->file); + + if (data->key) + g_free (data->key); + + if (data->actors) + g_slist_free_full (data->actors, (GDestroyNotify) g_object_unref); + + g_free (data); +} + +/** + * on_image_size_prepared: + * @pixbuf_loader: #GdkPixbufLoader loading the image + * @width: the original width of the image + * @height: the original height of the image + * @data: pointer to the #Dimensions structure containing available width and height for the image, + * available width or height can be -1 if the dimension is not limited + * + * Private function. + * + * Sets the size of the image being loaded to fit the available width and height dimensions, + * but never scales up the image beyond its actual size. + * Intended to be used as a callback for #GdkPixbufLoader "size-prepared" signal. + */ +static void +on_image_size_prepared (GdkPixbufLoader *pixbuf_loader, + gint width, + gint height, + gpointer data) +{ + Dimensions *available_dimensions = data; + int available_width = available_dimensions->width; + int available_height = available_dimensions->height; + int scale_factor = available_dimensions->scale; + int scaled_width; + int scaled_height; + + compute_pixbuf_scale (width, height, available_width, available_height, + &scaled_width, &scaled_height); + + gdk_pixbuf_loader_set_size (pixbuf_loader, + scaled_width * scale_factor, + scaled_height * scale_factor); +} + +static GdkPixbuf * +impl_load_pixbuf_data (const guchar *data, + gsize size, + int available_width, + int available_height, + int scale, + GError **error) +{ + GdkPixbufLoader *pixbuf_loader = NULL; + GdkPixbuf *rotated_pixbuf = NULL; + GdkPixbuf *pixbuf; + gboolean success; + Dimensions available_dimensions; + int width_before_rotation, width_after_rotation; + + pixbuf_loader = gdk_pixbuf_loader_new (); + + available_dimensions.width = available_width; + available_dimensions.height = available_height; + available_dimensions.scale = scale; + g_signal_connect (pixbuf_loader, "size-prepared", + G_CALLBACK (on_image_size_prepared), &available_dimensions); + + success = gdk_pixbuf_loader_write (pixbuf_loader, data, size, error); + if (!success) + goto out; + success = gdk_pixbuf_loader_close (pixbuf_loader, error); + if (!success) + goto out; + + pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader); + + width_before_rotation = gdk_pixbuf_get_width (pixbuf); + + rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf); + width_after_rotation = gdk_pixbuf_get_width (rotated_pixbuf); + + /* There is currently no way to tell if the pixbuf will need to be rotated before it is loaded, + * so we only check that once it is loaded, and reload it again if it needs to be rotated in order + * to use the available width and height correctly. + * See http://bugzilla.gnome.org/show_bug.cgi?id=579003 + */ + if (width_before_rotation != width_after_rotation) + { + g_object_unref (pixbuf_loader); + g_object_unref (rotated_pixbuf); + rotated_pixbuf = NULL; + + pixbuf_loader = gdk_pixbuf_loader_new (); + + /* We know that the image will later be rotated, so we reverse the available dimensions. */ + available_dimensions.width = available_height; + available_dimensions.height = available_width; + available_dimensions.scale = scale; + g_signal_connect (pixbuf_loader, "size-prepared", + G_CALLBACK (on_image_size_prepared), &available_dimensions); + + success = gdk_pixbuf_loader_write (pixbuf_loader, data, size, error); + if (!success) + goto out; + + success = gdk_pixbuf_loader_close (pixbuf_loader, error); + if (!success) + goto out; + + pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader); + + rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf); + } + +out: + if (pixbuf_loader) + g_object_unref (pixbuf_loader); + return rotated_pixbuf; +} + +static GdkPixbuf * +impl_load_pixbuf_file (GFile *file, + int available_width, + int available_height, + int paint_scale, + float resource_scale, + GError **error) +{ + GdkPixbuf *pixbuf = NULL; + char *contents = NULL; + gsize size; + + if (g_file_load_contents (file, NULL, &contents, &size, NULL, error)) + { + int scale = ceilf (paint_scale * resource_scale); + pixbuf = impl_load_pixbuf_data ((const guchar *) contents, size, + available_width, available_height, + scale, + error); + } + + g_free (contents); + + return pixbuf; +} + +static void +load_pixbuf_thread (GTask *result, + gpointer source, + gpointer task_data, + GCancellable *cancellable) +{ + GdkPixbuf *pixbuf; + AsyncTextureLoadData *data = task_data; + GError *error = NULL; + + g_assert (data != NULL); + g_assert (data->file != NULL); + + pixbuf = impl_load_pixbuf_file (data->file, data->width, data->height, + data->paint_scale, data->resource_scale, + &error); + + if (error != NULL) + g_task_return_error (result, error); + else if (pixbuf) + g_task_return_pointer (result, g_object_ref (pixbuf), g_object_unref); + + g_clear_object (&pixbuf); +} + +static GdkPixbuf * +load_pixbuf_async_finish (StTextureCache *cache, GAsyncResult *result, GError **error) +{ + return g_task_propagate_pointer (G_TASK (result), error); +} + +static ClutterContent * +pixbuf_to_st_content_image (GdkPixbuf *pixbuf, + int width, + int height, + int paint_scale, + float resource_scale) +{ + ClutterContent *image; + g_autoptr(GError) error = NULL; + + float native_width, native_height; + + native_width = ceilf (gdk_pixbuf_get_width (pixbuf) / resource_scale); + native_height = ceilf (gdk_pixbuf_get_height (pixbuf) / resource_scale); + + if (width < 0 && height < 0) + { + width = native_width; + height = native_height; + } + else if (width < 0) + { + height *= paint_scale; + width = native_width * (height / native_height); + } + else if (height < 0) + { + width *= paint_scale; + height = native_height * (width / native_width); + } + else + { + width *= paint_scale; + height *= paint_scale; + } + + image = st_image_content_new_with_preferred_size (width, height); + clutter_image_set_data (CLUTTER_IMAGE (image), + gdk_pixbuf_get_pixels (pixbuf), + gdk_pixbuf_get_has_alpha (pixbuf) ? + COGL_PIXEL_FORMAT_RGBA_8888 : COGL_PIXEL_FORMAT_RGB_888, + gdk_pixbuf_get_width (pixbuf), + gdk_pixbuf_get_height (pixbuf), + gdk_pixbuf_get_rowstride (pixbuf), + &error); + + if (error) + { + g_warning ("Failed to allocate texture: %s", error->message); + g_clear_object (&image); + } + + return image; +} + +static cairo_surface_t * +pixbuf_to_cairo_surface (GdkPixbuf *pixbuf) +{ + cairo_surface_t *dummy_surface; + cairo_pattern_t *pattern; + cairo_surface_t *surface; + cairo_t *cr; + + dummy_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 1, 1); + + cr = cairo_create (dummy_surface); + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + pattern = cairo_get_source (cr); + cairo_pattern_get_surface (pattern, &surface); + cairo_surface_reference (surface); + cairo_destroy (cr); + cairo_surface_destroy (dummy_surface); + + return surface; +} + +static void +finish_texture_load (AsyncTextureLoadData *data, + GdkPixbuf *pixbuf) +{ + g_autoptr(ClutterContent) image = NULL; + GSList *iter; + StTextureCache *cache; + + cache = data->cache; + + g_hash_table_remove (cache->priv->outstanding_requests, data->key); + + if (pixbuf == NULL) + goto out; + + if (data->policy != ST_TEXTURE_CACHE_POLICY_NONE) + { + gpointer orig_key = NULL, value = NULL; + + if (!g_hash_table_lookup_extended (cache->priv->keyed_cache, data->key, + &orig_key, &value)) + { + image = pixbuf_to_st_content_image (pixbuf, + data->width, data->height, + data->paint_scale, + data->resource_scale); + if (!image) + goto out; + + g_hash_table_insert (cache->priv->keyed_cache, g_strdup (data->key), + g_object_ref (image)); + } + else + { + image = g_object_ref (value); + } + } + else + { + image = pixbuf_to_st_content_image (pixbuf, + data->width, data->height, + data->paint_scale, + data->resource_scale); + if (!image) + goto out; + } + + for (iter = data->actors; iter; iter = iter->next) + { + ClutterActor *actor = iter->data; + set_content_from_image (actor, image); + } + +out: + texture_load_data_free (data); +} + +static void +on_symbolic_icon_loaded (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GdkPixbuf *pixbuf; + pixbuf = gtk_icon_info_load_symbolic_finish (GTK_ICON_INFO (source), result, NULL, NULL); + finish_texture_load (user_data, pixbuf); + g_clear_object (&pixbuf); +} + +static void +on_icon_loaded (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GdkPixbuf *pixbuf; + pixbuf = gtk_icon_info_load_icon_finish (GTK_ICON_INFO (source), result, NULL); + finish_texture_load (user_data, pixbuf); + g_clear_object (&pixbuf); +} + +static void +on_pixbuf_loaded (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GdkPixbuf *pixbuf; + pixbuf = load_pixbuf_async_finish (ST_TEXTURE_CACHE (source), result, NULL); + finish_texture_load (user_data, pixbuf); + g_clear_object (&pixbuf); +} + +static void +load_texture_async (StTextureCache *cache, + AsyncTextureLoadData *data) +{ + if (data->file) + { + GTask *task = g_task_new (cache, NULL, on_pixbuf_loaded, data); + g_task_set_task_data (task, data, NULL); + g_task_run_in_thread (task, load_pixbuf_thread); + g_object_unref (task); + } + else if (data->icon_info) + { + StIconColors *colors = data->colors; + if (colors) + { + GdkRGBA foreground_color; + GdkRGBA success_color; + GdkRGBA warning_color; + GdkRGBA error_color; + + rgba_from_clutter (&foreground_color, &colors->foreground); + rgba_from_clutter (&success_color, &colors->success); + rgba_from_clutter (&warning_color, &colors->warning); + rgba_from_clutter (&error_color, &colors->error); + + gtk_icon_info_load_symbolic_async (data->icon_info, + &foreground_color, &success_color, + &warning_color, &error_color, + cache->priv->cancellable, + on_symbolic_icon_loaded, data); + } + else + { + gtk_icon_info_load_icon_async (data->icon_info, + cache->priv->cancellable, + on_icon_loaded, data); + } + } + else + g_assert_not_reached (); +} + +typedef struct { + StTextureCache *cache; + ClutterContent *image; + GObject *source; + gulong notify_signal_id; + gboolean weakref_active; +} StTextureCachePropertyBind; + +static void +st_texture_cache_load_surface (ClutterContent **image, + cairo_surface_t *surface) +{ + g_return_if_fail (image != NULL); + + if (surface != NULL && + cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE && + (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32 || + cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24)) + { + g_autoptr(GError) error = NULL; + int width, height, size; + + width = cairo_image_surface_get_width (surface); + height = cairo_image_surface_get_width (surface); + size = MAX(width, height); + + if (*image == NULL) + *image = st_image_content_new_with_preferred_size (size, size); + + clutter_image_set_data (CLUTTER_IMAGE (*image), + cairo_image_surface_get_data (surface), + cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32 ? + COGL_PIXEL_FORMAT_BGRA_8888 : COGL_PIXEL_FORMAT_BGR_888, + width, + height, + cairo_image_surface_get_stride (surface), + &error); + + if (error) + g_warning ("Failed to allocate texture: %s", error->message); + } + else if (*image == NULL) + { + *image = st_image_content_new_with_preferred_size (0, 0); + } +} + +static void +st_texture_cache_reset_texture (StTextureCachePropertyBind *bind, + const char *propname) +{ + cairo_surface_t *surface; + + g_object_get (bind->source, propname, &surface, NULL); + + st_texture_cache_load_surface (&bind->image, surface); +} + +static void +st_texture_cache_on_pixbuf_notify (GObject *object, + GParamSpec *paramspec, + gpointer data) +{ + StTextureCachePropertyBind *bind = data; + st_texture_cache_reset_texture (bind, paramspec->name); +} + +static void +st_texture_cache_bind_weak_notify (gpointer data, + GObject *source_location) +{ + StTextureCachePropertyBind *bind = data; + bind->weakref_active = FALSE; + g_signal_handler_disconnect (bind->source, bind->notify_signal_id); +} + +static void +st_texture_cache_free_bind (gpointer data) +{ + StTextureCachePropertyBind *bind = data; + if (bind->weakref_active) + g_object_weak_unref (G_OBJECT (bind->image), st_texture_cache_bind_weak_notify, bind); + g_free (bind); +} + +/** + * st_texture_cache_bind_cairo_surface_property: + * @cache: A #StTextureCache + * @object: A #GObject with a property @property_name of type #cairo_surface_t + * @property_name: Name of a property + * + * Create a #GIcon which tracks the #cairo_surface_t value of a GObject property + * named by @property_name. Unlike other methods in StTextureCache, the underlying + * #CoglTexture is not shared by default with other invocations to this method. + * + * If the source object is destroyed, the texture will continue to show the last + * value of the property. + * + * Returns: (transfer full): A new #GIcon + */ +GIcon * +st_texture_cache_bind_cairo_surface_property (StTextureCache *cache, + GObject *object, + const char *property_name) +{ + gchar *notify_key; + StTextureCachePropertyBind *bind; + + bind = g_new0 (StTextureCachePropertyBind, 1); + bind->cache = cache; + bind->source = object; + + st_texture_cache_reset_texture (bind, property_name); + + g_object_weak_ref (G_OBJECT (bind->image), st_texture_cache_bind_weak_notify, bind); + bind->weakref_active = TRUE; + + notify_key = g_strdup_printf ("notify::%s", property_name); + bind->notify_signal_id = g_signal_connect_data (object, notify_key, G_CALLBACK(st_texture_cache_on_pixbuf_notify), + bind, (GClosureNotify)st_texture_cache_free_bind, 0); + g_free (notify_key); + + return G_ICON (bind->image); +} + +/** + * st_texture_cache_load_cairo_surface_to_gicon: + * @cache: A #StTextureCache + * @surface: A #cairo_surface_t + * + * Create a #GIcon from @surface. + * + * Returns: (transfer full): A new #GIcon + */ +GIcon * +st_texture_cache_load_cairo_surface_to_gicon (StTextureCache *cache, + cairo_surface_t *surface) +{ + ClutterContent *image = NULL; + st_texture_cache_load_surface (&image, surface); + + return G_ICON (image); +} + +/** + * st_texture_cache_load: (skip) + * @cache: A #StTextureCache + * @key: Arbitrary string used to refer to item + * @policy: Caching policy + * @load: Function to create the texture, if not already cached + * @data: User data passed to @load + * @error: A #GError + * + * Load an arbitrary texture, caching it. The string chosen for @key + * should be of the form "type-prefix:type-uuid". For example, + * "url:file:///usr/share/icons/hicolor/48x48/apps/firefox.png", or + * "stock-icon:gtk-ok". + * + * Returns: (transfer full): A newly-referenced handle to the texture + */ +CoglTexture * +st_texture_cache_load (StTextureCache *cache, + const char *key, + StTextureCachePolicy policy, + StTextureCacheLoader load, + void *data, + GError **error) +{ + CoglTexture *texture; + + texture = g_hash_table_lookup (cache->priv->keyed_cache, key); + if (!texture) + { + texture = load (cache, key, data, error); + if (texture && policy == ST_TEXTURE_CACHE_POLICY_FOREVER) + g_hash_table_insert (cache->priv->keyed_cache, g_strdup (key), texture); + } + + if (texture && policy == ST_TEXTURE_CACHE_POLICY_FOREVER) + cogl_object_ref (texture); + + return texture; +} + +/** + * ensure_request: + * @cache: A #StTextureCache + * @key: A cache key + * @policy: Cache policy + * @request: (out): If no request is outstanding, one will be created and returned here + * @texture: A texture to be added to the request + * + * Check for any outstanding load for the data represented by @key. If there + * is already a request pending, append it to that request to avoid loading + * the data multiple times. + * + * Returns: %TRUE if there is already a request pending + */ +static gboolean +ensure_request (StTextureCache *cache, + const char *key, + StTextureCachePolicy policy, + AsyncTextureLoadData **request, + ClutterActor *actor) +{ + ClutterContent *image; + AsyncTextureLoadData *pending; + gboolean had_pending; + + image = g_hash_table_lookup (cache->priv->keyed_cache, key); + + if (image != NULL) + { + /* We had this cached already, just set the texture and we're done. */ + set_content_from_image (actor, image); + return TRUE; + } + + pending = g_hash_table_lookup (cache->priv->outstanding_requests, key); + had_pending = pending != NULL; + + if (pending == NULL) + { + /* Not cached and no pending request, create it */ + *request = g_new0 (AsyncTextureLoadData, 1); + if (policy != ST_TEXTURE_CACHE_POLICY_NONE) + g_hash_table_insert (cache->priv->outstanding_requests, g_strdup (key), *request); + } + else + *request = pending; + + /* Regardless of whether there was a pending request, prepend our texture here. */ + (*request)->actors = g_slist_prepend ((*request)->actors, g_object_ref (actor)); + + return had_pending; +} + +/** + * st_texture_cache_load_gicon: + * @cache: A #StTextureCache + * @theme_node: (nullable): The #StThemeNode to use for colors, or %NULL + * if the icon must not be recolored + * @icon: the #GIcon to load + * @size: Size of themed + * @paint_scale: Scale factor of display + * @resource_scale: Resource scale factor + * + * This method returns a new #ClutterActor for a given #GIcon. If the + * icon isn't loaded already, the texture will be filled + * asynchronously. + * + * Returns: (transfer none) (nullable): A new #ClutterActor for the icon, or %NULL if not found + */ +ClutterActor * +st_texture_cache_load_gicon (StTextureCache *cache, + StThemeNode *theme_node, + GIcon *icon, + gint size, + gint paint_scale, + gfloat resource_scale) +{ + AsyncTextureLoadData *request; + ClutterActor *actor; + gint scale; + char *gicon_string; + g_autofree char *key = NULL; + float actor_size; + GtkIconTheme *theme; + StTextureCachePolicy policy; + StIconColors *colors = NULL; + StIconStyle icon_style = ST_ICON_STYLE_REQUESTED; + GtkIconLookupFlags lookup_flags; + + actor_size = size * paint_scale; + + if (ST_IS_IMAGE_CONTENT (icon)) + { + int width, height; + + g_object_get (G_OBJECT (icon), + "preferred-width", &width, + "preferred-height", &height, + NULL); + if (width == 0 && height == 0) + return NULL; + + return g_object_new (CLUTTER_TYPE_ACTOR, + "content-gravity", CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT, + "width", actor_size, + "height", actor_size, + "content", CLUTTER_CONTENT (icon), + NULL); + } + + if (theme_node) + { + colors = st_theme_node_get_icon_colors (theme_node); + icon_style = st_theme_node_get_icon_style (theme_node); + } + + /* Do theme lookups in the main thread to avoid thread-unsafety */ + theme = cache->priv->icon_theme; + + lookup_flags = GTK_ICON_LOOKUP_USE_BUILTIN; + + if (icon_style == ST_ICON_STYLE_REGULAR) + lookup_flags |= GTK_ICON_LOOKUP_FORCE_REGULAR; + else if (icon_style == ST_ICON_STYLE_SYMBOLIC) + lookup_flags |= GTK_ICON_LOOKUP_FORCE_SYMBOLIC; + + if (clutter_get_default_text_direction () == CLUTTER_TEXT_DIRECTION_RTL) + lookup_flags |= GTK_ICON_LOOKUP_DIR_RTL; + else + lookup_flags |= GTK_ICON_LOOKUP_DIR_LTR; + + scale = ceilf (paint_scale * resource_scale); + + gicon_string = g_icon_to_string (icon); + /* A return value of NULL indicates that the icon can not be serialized, + * so don't have a unique identifier for it as a cache key, and thus can't + * be cached. If it is cacheable, we hardcode a policy of FOREVER here for + * now; we should actually blow this away on icon theme changes probably */ + policy = gicon_string != NULL ? ST_TEXTURE_CACHE_POLICY_FOREVER + : ST_TEXTURE_CACHE_POLICY_NONE; + if (colors) + { + /* This raises some doubts about the practice of using string keys */ + key = g_strdup_printf (CACHE_PREFIX_ICON "%s,size=%d,scale=%d,style=%d,colors=%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x,%2x%2x%2x%2x", + gicon_string, size, scale, icon_style, + colors->foreground.red, colors->foreground.blue, colors->foreground.green, colors->foreground.alpha, + colors->warning.red, colors->warning.blue, colors->warning.green, colors->warning.alpha, + colors->error.red, colors->error.blue, colors->error.green, colors->error.alpha, + colors->success.red, colors->success.blue, colors->success.green, colors->success.alpha); + } + else + { + key = g_strdup_printf (CACHE_PREFIX_ICON "%s,size=%d,scale=%d,style=%d", + gicon_string, size, scale, icon_style); + } + g_free (gicon_string); + + actor = create_invisible_actor (); + clutter_actor_set_content_gravity (actor, CLUTTER_CONTENT_GRAVITY_RESIZE_ASPECT); + clutter_actor_set_size (actor, actor_size, actor_size); + if (!ensure_request (cache, key, policy, &request, actor)) + { + /* Else, make a new request */ + GtkIconInfo *info; + + info = gtk_icon_theme_lookup_by_gicon_for_scale (theme, icon, + size, scale, + lookup_flags); + if (info == NULL) + { + g_hash_table_remove (cache->priv->outstanding_requests, key); + texture_load_data_free (request); + g_object_unref (actor); + return NULL; + } + + request->cache = cache; + /* Transfer ownership of key */ + request->key = g_steal_pointer (&key); + request->policy = policy; + request->colors = colors ? st_icon_colors_ref (colors) : NULL; + request->icon_info = info; + request->width = request->height = size; + request->paint_scale = paint_scale; + request->resource_scale = resource_scale; + + load_texture_async (cache, request); + } + + return actor; +} + +static ClutterActor * +load_from_pixbuf (GdkPixbuf *pixbuf, + int paint_scale, + float resource_scale) +{ + g_autoptr(ClutterContent) image = NULL; + ClutterActor *actor; + + image = pixbuf_to_st_content_image (pixbuf, -1, -1, paint_scale, resource_scale); + + actor = g_object_new (CLUTTER_TYPE_ACTOR, + "request-mode", CLUTTER_REQUEST_CONTENT_SIZE, + NULL); + clutter_actor_set_content (actor, image); + + return actor; +} + +static void +hash_table_remove_with_scales (GHashTable *hash, + GList *scales, + const char *base_key) +{ + GList *l; + + for (l = scales; l; l = l->next) + { + double scale = *((double *)l->data); + g_autofree char *key = NULL; + key = g_strdup_printf ("%s%f", base_key, scale); + g_hash_table_remove (hash, key); + } +} + +static void +hash_table_insert_scale (GHashTable *hash, + double scale) +{ + double *saved_scale; + + if (g_hash_table_contains (hash, &scale)) + return; + + saved_scale = g_new (double, 1); + *saved_scale = scale; + + g_hash_table_add (hash, saved_scale); +} + +static void +file_changed_cb (GFileMonitor *monitor, + GFile *file, + GFile *other, + GFileMonitorEvent event_type, + gpointer user_data) +{ + StTextureCache *cache = user_data; + char *key; + guint file_hash; + g_autoptr (GList) scales = NULL; + + if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) + return; + + file_hash = g_file_hash (file); + scales = g_hash_table_get_keys (cache->priv->used_scales); + + key = g_strdup_printf (CACHE_PREFIX_FILE "%u", file_hash); + g_hash_table_remove (cache->priv->keyed_cache, key); + hash_table_remove_with_scales (cache->priv->keyed_cache, scales, key); + g_free (key); + + key = g_strdup_printf (CACHE_PREFIX_FILE_FOR_CAIRO "%u", file_hash); + g_hash_table_remove (cache->priv->keyed_surface_cache, key); + hash_table_remove_with_scales (cache->priv->keyed_surface_cache, scales, key); + g_free (key); + + g_signal_emit (cache, signals[TEXTURE_FILE_CHANGED], 0, file); +} + +static void +ensure_monitor_for_file (StTextureCache *cache, + GFile *file) +{ + StTextureCachePrivate *priv = cache->priv; + + /* No point in trying to monitor files that are part of a + * GResource, since it does not support file monitoring. + */ + if (g_file_has_uri_scheme (file, "resource")) + return; + + if (g_hash_table_lookup (priv->file_monitors, file) == NULL) + { + GFileMonitor *monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, + NULL, NULL); + g_signal_connect (monitor, "changed", + G_CALLBACK (file_changed_cb), cache); + g_hash_table_insert (priv->file_monitors, g_object_ref (file), monitor); + } +} + +typedef struct { + GFile *gfile; + gint grid_width, grid_height; + gint paint_scale; + gfloat resource_scale; + ClutterActor *actor; + GCancellable *cancellable; + GFunc load_callback; + gpointer load_callback_data; +} AsyncImageData; + +static void +on_data_destroy (gpointer data) +{ + AsyncImageData *d = (AsyncImageData *)data; + g_object_unref (d->gfile); + g_object_unref (d->actor); + g_object_unref (d->cancellable); + g_free (d); +} + +static void +on_sliced_image_actor_destroyed (ClutterActor *actor, + gpointer data) +{ + GTask *task = data; + GCancellable *cancellable = g_task_get_cancellable (task); + + g_cancellable_cancel (cancellable); +} + +static void +on_sliced_image_loaded (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GObject *cache = source_object; + AsyncImageData *data = (AsyncImageData *)user_data; + GTask *task = G_TASK (res); + GList *list, *pixbufs; + + if (g_task_had_error (task) || g_cancellable_is_cancelled (data->cancellable)) + return; + + pixbufs = g_task_propagate_pointer (task, NULL); + + for (list = pixbufs; list; list = list->next) + { + ClutterActor *actor = load_from_pixbuf (GDK_PIXBUF (list->data), + data->paint_scale, + data->resource_scale); + clutter_actor_hide (actor); + clutter_actor_add_child (data->actor, actor); + } + + g_list_free_full (pixbufs, g_object_unref); + + g_signal_handlers_disconnect_by_func (data->actor, + on_sliced_image_actor_destroyed, + task); + + if (data->load_callback != NULL) + data->load_callback (cache, data->load_callback_data); +} + +static void +free_glist_unref_gobjects (gpointer p) +{ + g_list_free_full (p, g_object_unref); +} + +static void +on_loader_size_prepared (GdkPixbufLoader *loader, + gint width, + gint height, + gpointer user_data) +{ + AsyncImageData *data = user_data; + int scale = ceilf (data->paint_scale * data->resource_scale); + + gdk_pixbuf_loader_set_size (loader, width * scale, height * scale); +} + +static void +load_sliced_image (GTask *result, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + AsyncImageData *data; + GList *res = NULL; + GdkPixbuf *pix; + gint width, height, y, x; + gint scale_factor; + GdkPixbufLoader *loader; + GError *error = NULL; + gchar *buffer = NULL; + gsize length; + + g_assert (cancellable); + + data = task_data; + g_assert (data); + + loader = gdk_pixbuf_loader_new (); + g_signal_connect (loader, "size-prepared", G_CALLBACK (on_loader_size_prepared), data); + + if (!g_file_load_contents (data->gfile, cancellable, &buffer, &length, NULL, &error)) + { + g_warning ("Failed to open sliced image: %s", error->message); + goto out; + } + + if (!gdk_pixbuf_loader_write (loader, (const guchar *) buffer, length, &error)) + { + g_warning ("Failed to load image: %s", error->message); + goto out; + } + + if (!gdk_pixbuf_loader_close (loader, NULL)) + goto out; + + pix = gdk_pixbuf_loader_get_pixbuf (loader); + width = gdk_pixbuf_get_width (pix); + height = gdk_pixbuf_get_height (pix); + scale_factor = ceilf (data->paint_scale * data->resource_scale); + for (y = 0; y < height; y += data->grid_height * scale_factor) + { + for (x = 0; x < width; x += data->grid_width * scale_factor) + { + GdkPixbuf *pixbuf = gdk_pixbuf_new_subpixbuf (pix, x, y, + data->grid_width * scale_factor, + data->grid_height * scale_factor); + g_assert (pixbuf != NULL); + res = g_list_append (res, pixbuf); + } + } + + out: + /* We don't need the original pixbuf anymore, which is owned by the loader, + * though the subpixbufs will hold a reference. */ + g_object_unref (loader); + g_free (buffer); + g_clear_pointer (&error, g_error_free); + g_task_return_pointer (result, res, free_glist_unref_gobjects); +} + +/** + * st_texture_cache_load_sliced_image: + * @cache: A #StTextureCache + * @file: A #GFile + * @grid_width: Width in pixels + * @grid_height: Height in pixels + * @paint_scale: Scale factor of the display + * @load_callback: (scope async) (nullable): Function called when the image is loaded, or %NULL + * @user_data: Data to pass to the load callback + * + * This function reads a single image file which contains multiple images internally. + * The image file will be divided using @grid_width and @grid_height; + * note that the dimensions of the image loaded from @path + * should be a multiple of the specified grid dimensions. + * + * Returns: (transfer none): A new #ClutterActor + */ +ClutterActor * +st_texture_cache_load_sliced_image (StTextureCache *cache, + GFile *file, + gint grid_width, + gint grid_height, + gint paint_scale, + gfloat resource_scale, + GFunc load_callback, + gpointer user_data) +{ + AsyncImageData *data; + GTask *result; + ClutterActor *actor = clutter_actor_new (); + GCancellable *cancellable = g_cancellable_new (); + + g_return_val_if_fail (G_IS_FILE (file), NULL); + g_assert (paint_scale > 0); + g_assert (resource_scale > 0); + + data = g_new0 (AsyncImageData, 1); + data->grid_width = grid_width; + data->grid_height = grid_height; + data->paint_scale = paint_scale; + data->resource_scale = resource_scale; + data->gfile = g_object_ref (file); + data->actor = actor; + data->cancellable = cancellable; + data->load_callback = load_callback; + data->load_callback_data = user_data; + g_object_ref (G_OBJECT (actor)); + + result = g_task_new (cache, cancellable, on_sliced_image_loaded, data); + + g_signal_connect (actor, "destroy", + G_CALLBACK (on_sliced_image_actor_destroyed), result); + + g_task_set_task_data (result, data, on_data_destroy); + g_task_run_in_thread (result, load_sliced_image); + + g_object_unref (result); + + return actor; +} + +/** + * st_texture_cache_load_file_async: + * @cache: A #StTextureCache + * @file: a #GFile of the image file from which to create a pixbuf + * @available_width: available width for the image, can be -1 if not limited + * @available_height: available height for the image, can be -1 if not limited + * @paint_scale: scale factor of the display + * @resource_scale: Resource scale factor + * + * Asynchronously load an image. Initially, the returned texture will have a natural + * size of zero. At some later point, either the image will be loaded successfully + * and at that point size will be negotiated, or upon an error, no image will be set. + * + * Returns: (transfer none): A new #ClutterActor with no image loaded initially. + */ +ClutterActor * +st_texture_cache_load_file_async (StTextureCache *cache, + GFile *file, + int available_width, + int available_height, + int paint_scale, + gfloat resource_scale) +{ + ClutterActor *actor; + AsyncTextureLoadData *request; + StTextureCachePolicy policy; + gchar *key; + int scale; + + scale = ceilf (paint_scale * resource_scale); + key = g_strdup_printf (CACHE_PREFIX_FILE "%u%d", g_file_hash (file), scale); + + policy = ST_TEXTURE_CACHE_POLICY_NONE; /* XXX */ + + actor = create_invisible_actor (); + + if (ensure_request (cache, key, policy, &request, actor)) + { + /* If there's an outstanding request, we've just added ourselves to it */ + g_free (key); + } + else + { + /* Else, make a new request */ + + request->cache = cache; + /* Transfer ownership of key */ + request->key = key; + request->file = g_object_ref (file); + request->policy = policy; + request->width = available_width; + request->height = available_height; + request->paint_scale = paint_scale; + request->resource_scale = resource_scale; + + load_texture_async (cache, request); + } + + ensure_monitor_for_file (cache, file); + + return actor; +} + +static CoglTexture * +st_texture_cache_load_file_sync_to_cogl_texture (StTextureCache *cache, + StTextureCachePolicy policy, + GFile *file, + int available_width, + int available_height, + int paint_scale, + gfloat resource_scale, + GError **error) +{ + ClutterContent *image; + CoglTexture *texdata; + GdkPixbuf *pixbuf; + char *key; + + key = g_strdup_printf (CACHE_PREFIX_FILE "%u%f", g_file_hash (file), resource_scale); + + texdata = NULL; + image = g_hash_table_lookup (cache->priv->keyed_cache, key); + + if (image == NULL) + { + pixbuf = impl_load_pixbuf_file (file, available_width, available_height, + paint_scale, resource_scale, error); + if (!pixbuf) + goto out; + + image = pixbuf_to_st_content_image (pixbuf, + available_height, available_width, + paint_scale, resource_scale); + g_object_unref (pixbuf); + + if (!image) + goto out; + + if (policy == ST_TEXTURE_CACHE_POLICY_FOREVER) + { + g_hash_table_insert (cache->priv->keyed_cache, g_strdup (key), image); + hash_table_insert_scale (cache->priv->used_scales, (double)resource_scale); + } + } + + /* Because the texture is loaded synchronously, we won't call + * clutter_image_set_data(), so it's safe to use the texture + * of ClutterImage here. */ + texdata = clutter_image_get_texture (CLUTTER_IMAGE (image)); + cogl_object_ref (texdata); + + ensure_monitor_for_file (cache, file); + +out: + g_free (key); + return texdata; +} + +static cairo_surface_t * +st_texture_cache_load_file_sync_to_cairo_surface (StTextureCache *cache, + StTextureCachePolicy policy, + GFile *file, + int available_width, + int available_height, + int paint_scale, + gfloat resource_scale, + GError **error) +{ + cairo_surface_t *surface; + GdkPixbuf *pixbuf; + char *key; + + key = g_strdup_printf (CACHE_PREFIX_FILE_FOR_CAIRO "%u%f", g_file_hash (file), resource_scale); + + surface = g_hash_table_lookup (cache->priv->keyed_surface_cache, key); + + if (surface == NULL) + { + pixbuf = impl_load_pixbuf_file (file, available_width, available_height, + paint_scale, resource_scale, error); + if (!pixbuf) + goto out; + + surface = pixbuf_to_cairo_surface (pixbuf); + g_object_unref (pixbuf); + + if (policy == ST_TEXTURE_CACHE_POLICY_FOREVER) + { + cairo_surface_reference (surface); + g_hash_table_insert (cache->priv->keyed_surface_cache, + g_strdup (key), surface); + hash_table_insert_scale (cache->priv->used_scales, (double)resource_scale); + } + } + else + cairo_surface_reference (surface); + + ensure_monitor_for_file (cache, file); + +out: + g_free (key); + return surface; +} + +/** + * st_texture_cache_load_file_to_cogl_texture: (skip) + * @cache: A #StTextureCache + * @file: A #GFile in supported image format + * @paint_scale: Scale factor of the display + * @resource_scale: Resource scale factor + * + * This function synchronously loads the given file path + * into a COGL texture. On error, a warning is emitted + * and %NULL is returned. + * + * Returns: (transfer full): a new #CoglTexture + */ +CoglTexture * +st_texture_cache_load_file_to_cogl_texture (StTextureCache *cache, + GFile *file, + gint paint_scale, + gfloat resource_scale) +{ + CoglTexture *texture; + GError *error = NULL; + + texture = st_texture_cache_load_file_sync_to_cogl_texture (cache, ST_TEXTURE_CACHE_POLICY_FOREVER, + file, -1, -1, paint_scale, resource_scale, + &error); + + if (texture == NULL) + { + char *uri = g_file_get_uri (file); + g_warning ("Failed to load %s: %s", uri, error->message); + g_clear_error (&error); + g_free (uri); + } + + return texture; +} + +/** + * st_texture_cache_load_file_to_cairo_surface: + * @cache: A #StTextureCache + * @file: A #GFile in supported image format + * @paint_scale: Scale factor of the display + * @resource_scale: Resource scale factor + * + * This function synchronously loads the given file path + * into a cairo surface. On error, a warning is emitted + * and %NULL is returned. + * + * Returns: (transfer full): a new #cairo_surface_t + */ +cairo_surface_t * +st_texture_cache_load_file_to_cairo_surface (StTextureCache *cache, + GFile *file, + gint paint_scale, + gfloat resource_scale) +{ + cairo_surface_t *surface; + GError *error = NULL; + + surface = st_texture_cache_load_file_sync_to_cairo_surface (cache, ST_TEXTURE_CACHE_POLICY_FOREVER, + file, -1, -1, paint_scale, resource_scale, + &error); + + if (surface == NULL) + { + char *uri = g_file_get_uri (file); + g_warning ("Failed to load %s: %s", uri, error->message); + g_clear_error (&error); + g_free (uri); + } + + return surface; +} + +static StTextureCache *instance = NULL; + +/** + * st_texture_cache_get_default: + * + * Returns: (transfer none): The global texture cache + */ +StTextureCache* +st_texture_cache_get_default (void) +{ + if (instance == NULL) + instance = g_object_new (ST_TYPE_TEXTURE_CACHE, NULL); + return instance; +} + +/** + * st_texture_cache_rescan_icon_theme: + * + * Rescan the current icon theme, if necessary. + * + * Returns: %TRUE if the icon theme has changed and needed to be reloaded. + */ +gboolean +st_texture_cache_rescan_icon_theme (StTextureCache *cache) +{ + StTextureCachePrivate *priv = cache->priv; + + return gtk_icon_theme_rescan_if_needed (priv->icon_theme); +} diff --git a/src/st/st-texture-cache.h b/src/st/st-texture-cache.h new file mode 100644 index 0000000..55d8495 --- /dev/null +++ b/src/st/st-texture-cache.h @@ -0,0 +1,120 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-texture-cache.h: Object for loading and caching images as textures + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010, Maxim Ermilov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_TEXTURE_CACHE_H__ +#define __ST_TEXTURE_CACHE_H__ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <clutter/clutter.h> + +#include <st/st-types.h> +#include <st/st-theme-node.h> +#include <st/st-widget.h> + +#define ST_TYPE_TEXTURE_CACHE (st_texture_cache_get_type ()) +G_DECLARE_FINAL_TYPE (StTextureCache, st_texture_cache, + ST, TEXTURE_CACHE, GObject) + +typedef struct _StTextureCachePrivate StTextureCachePrivate; + +struct _StTextureCache +{ + GObject parent; + + StTextureCachePrivate *priv; +}; + +typedef enum { + ST_TEXTURE_CACHE_POLICY_NONE, + ST_TEXTURE_CACHE_POLICY_FOREVER +} StTextureCachePolicy; + +StTextureCache* st_texture_cache_get_default (void); + +ClutterActor * +st_texture_cache_load_sliced_image (StTextureCache *cache, + GFile *file, + gint grid_width, + gint grid_height, + gint paint_scale, + gfloat resource_scale, + GFunc load_callback, + gpointer user_data); + +GIcon *st_texture_cache_bind_cairo_surface_property (StTextureCache *cache, + GObject *object, + const char *property_name); +GIcon * +st_texture_cache_load_cairo_surface_to_gicon (StTextureCache *cache, + cairo_surface_t *surface); + +ClutterActor *st_texture_cache_load_gicon (StTextureCache *cache, + StThemeNode *theme_node, + GIcon *icon, + gint size, + gint paint_scale, + gfloat resource_scale); + +ClutterActor *st_texture_cache_load_file_async (StTextureCache *cache, + GFile *file, + int available_width, + int available_height, + int paint_scale, + gfloat resource_scale); + +CoglTexture *st_texture_cache_load_file_to_cogl_texture (StTextureCache *cache, + GFile *file, + gint paint_scale, + gfloat resource_scale); + +cairo_surface_t *st_texture_cache_load_file_to_cairo_surface (StTextureCache *cache, + GFile *file, + gint paint_scale, + gfloat resource_scale); + +/** + * StTextureCacheLoader: (skip) + * @cache: a #StTextureCache + * @key: Unique identifier for this texture + * @data: Callback user data + * @error: A #GError + * + * See st_texture_cache_load(). Implementations should return a + * texture handle for the given key, or set @error. + * + */ +typedef CoglTexture * (*StTextureCacheLoader) (StTextureCache *cache, const char *key, void *data, GError **error); + +CoglTexture * st_texture_cache_load (StTextureCache *cache, + const char *key, + StTextureCachePolicy policy, + StTextureCacheLoader load, + void *data, + GError **error); + +gboolean st_texture_cache_rescan_icon_theme (StTextureCache *cache); + +#endif /* __ST_TEXTURE_CACHE_H__ */ diff --git a/src/st/st-theme-context.c b/src/st/st-theme-context.c new file mode 100644 index 0000000..4055786 --- /dev/null +++ b/src/st/st-theme-context.c @@ -0,0 +1,492 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-context.c: holds global information about a tree of styled objects + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2009 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <config.h> + +#include "st-private.h" +#include "st-settings.h" +#include "st-texture-cache.h" +#include "st-theme.h" +#include "st-theme-context.h" +#include "st-theme-node-private.h" + +struct _StThemeContext { + GObject parent; + + PangoFontDescription *font; + StThemeNode *root_node; + StTheme *theme; + + /* set of StThemeNode */ + GHashTable *nodes; + + gulong stylesheets_changed_id; + + int scale_factor; +}; + +enum +{ + PROP_0, + PROP_SCALE_FACTOR, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum +{ + CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (StThemeContext, st_theme_context, G_TYPE_OBJECT) + +static PangoFontDescription *get_interface_font_description (void); +static void on_font_name_changed (StSettings *settings, + GParamSpec *pspec, + StThemeContext *context); +static void on_icon_theme_changed (StTextureCache *cache, + StThemeContext *context); +static void st_theme_context_changed (StThemeContext *context); + +static void st_theme_context_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void st_theme_context_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +static void +st_theme_context_set_scale_factor (StThemeContext *context, + int scale_factor) +{ + if (scale_factor == context->scale_factor) + return; + + context->scale_factor = scale_factor; + g_object_notify_by_pspec (G_OBJECT (context), props[PROP_SCALE_FACTOR]); + st_theme_context_changed (context); +} + + +static void +st_theme_context_finalize (GObject *object) +{ + StThemeContext *context = ST_THEME_CONTEXT (object); + + g_signal_handlers_disconnect_by_func (st_settings_get (), + (gpointer) on_font_name_changed, + context); + g_signal_handlers_disconnect_by_func (st_texture_cache_get_default (), + (gpointer) on_icon_theme_changed, + context); + g_signal_handlers_disconnect_by_func (clutter_get_default_backend (), + (gpointer) st_theme_context_changed, + context); + + g_clear_signal_handler (&context->stylesheets_changed_id, context->theme); + + if (context->nodes) + g_hash_table_unref (context->nodes); + if (context->root_node) + g_object_unref (context->root_node); + if (context->theme) + g_object_unref (context->theme); + + pango_font_description_free (context->font); + + G_OBJECT_CLASS (st_theme_context_parent_class)->finalize (object); +} + +static void +st_theme_context_class_init (StThemeContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = st_theme_context_set_property; + object_class->get_property = st_theme_context_get_property; + object_class->finalize = st_theme_context_finalize; + + /** + * StThemeContext:scale-factor: + * + * The scaling factor used for HiDPI scaling. + */ + props[PROP_SCALE_FACTOR] = + g_param_spec_int ("scale-factor", + "Scale factor", + "Integer scale factor used for HiDPI scaling", + 0, G_MAXINT, 1, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, props); + + /** + * StThemeContext::changed: + * @self: a #StThemeContext + * + * Emitted when the icon theme, font, resolution, scale factor or the current + * theme's custom stylesheets change. + */ + signals[CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* no default handler slot */ + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +st_theme_context_init (StThemeContext *context) +{ + context->font = get_interface_font_description (); + + g_signal_connect (st_settings_get (), + "notify::font-name", + G_CALLBACK (on_font_name_changed), + context); + g_signal_connect (st_texture_cache_get_default (), + "icon-theme-changed", + G_CALLBACK (on_icon_theme_changed), + context); + g_signal_connect_swapped (clutter_get_default_backend (), + "resolution-changed", + G_CALLBACK (st_theme_context_changed), + context); + + context->nodes = g_hash_table_new_full ((GHashFunc) st_theme_node_hash, + (GEqualFunc) st_theme_node_equal, + g_object_unref, NULL); + context->scale_factor = 1; +} + +static void +st_theme_context_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StThemeContext *context = ST_THEME_CONTEXT (object); + + switch (prop_id) + { + case PROP_SCALE_FACTOR: + st_theme_context_set_scale_factor (context, g_value_get_int (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_theme_context_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StThemeContext *context = ST_THEME_CONTEXT (object); + + switch (prop_id) + { + case PROP_SCALE_FACTOR: + g_value_set_int (value, context->scale_factor); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * st_theme_context_new: + * + * Create a new theme context not associated with any #ClutterStage. + * This can be useful in testing scenarios, or if using StThemeContext + * with something other than #ClutterActor objects, but you generally + * should use st_theme_context_get_for_stage() instead. + * + * Returns: (transfer full): a new #StThemeContext + */ +StThemeContext * +st_theme_context_new (void) +{ + StThemeContext *context; + + context = g_object_new (ST_TYPE_THEME_CONTEXT, NULL); + + return context; +} + +static PangoFontDescription * +get_interface_font_description (void) +{ + StSettings *settings = st_settings_get (); + g_autofree char *font_name = NULL; + + g_object_get (settings, "font-name", &font_name, NULL); + return pango_font_description_from_string (font_name); +} + +static void +on_stage_destroy (ClutterStage *stage) +{ + StThemeContext *context = st_theme_context_get_for_stage (stage); + + g_object_set_data (G_OBJECT (stage), "st-theme-context", NULL); + g_object_unref (context); +} + +static void +st_theme_context_changed (StThemeContext *context) +{ + StThemeNode *old_root = context->root_node; + context->root_node = NULL; + g_hash_table_remove_all (context->nodes); + + g_signal_emit (context, signals[CHANGED], 0); + + if (old_root) + g_object_unref (old_root); +} + +static void +on_font_name_changed (StSettings *settings, + GParamSpec *pspect, + StThemeContext *context) +{ + PangoFontDescription *font_desc = get_interface_font_description (); + st_theme_context_set_font (context, font_desc); + + pango_font_description_free (font_desc); +} + +static gboolean +changed_idle (gpointer userdata) +{ + st_theme_context_changed (userdata); + return FALSE; +} + +static void +on_icon_theme_changed (StTextureCache *cache, + StThemeContext *context) +{ + guint id; + + /* Note that an icon theme change isn't really a change of the StThemeContext; + * the style information has changed. But since the style factors into the + * icon_name => icon lookup, faking a theme context change is a good way + * to force users such as StIcon to look up icons again. + */ + id = g_idle_add ((GSourceFunc) changed_idle, context); + g_source_set_name_by_id (id, "[gnome-shell] changed_idle"); +} + +/** + * st_theme_context_get_for_stage: + * @stage: a #ClutterStage + * + * Gets a singleton theme context associated with the stage. + * + * Returns: (transfer none): the singleton theme context for the stage + */ +StThemeContext * +st_theme_context_get_for_stage (ClutterStage *stage) +{ + StThemeContext *context; + + g_return_val_if_fail (CLUTTER_IS_STAGE (stage), NULL); + + context = g_object_get_data (G_OBJECT (stage), "st-theme-context"); + if (context) + return context; + + context = st_theme_context_new (); + g_object_set_data (G_OBJECT (stage), "st-theme-context", context); + g_signal_connect (stage, "destroy", + G_CALLBACK (on_stage_destroy), NULL); + + return context; +} + +/** + * st_theme_context_set_theme: + * @context: a #StThemeContext + * @theme: a #StTheme + * + * Sets the default set of theme stylesheets for the context. This theme will + * be used for the root node and for nodes descending from it, unless some other + * style is explicitly specified. + */ +void +st_theme_context_set_theme (StThemeContext *context, + StTheme *theme) +{ + g_return_if_fail (ST_IS_THEME_CONTEXT (context)); + g_return_if_fail (theme == NULL || ST_IS_THEME (theme)); + + if (context->theme != theme) + { + if (context->theme) + g_clear_signal_handler (&context->stylesheets_changed_id, context->theme); + + g_set_object (&context->theme, theme); + + if (context->theme) + { + context->stylesheets_changed_id = + g_signal_connect_swapped (context->theme, + "custom-stylesheets-changed", + G_CALLBACK (st_theme_context_changed), + context); + } + + st_theme_context_changed (context); + } +} + +/** + * st_theme_context_get_theme: + * @context: a #StThemeContext + * + * Gets the default theme for the context. See st_theme_context_set_theme() + * + * Returns: (transfer none): the default theme for the context + */ +StTheme * +st_theme_context_get_theme (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + + return context->theme; +} + +/** + * st_theme_context_set_font: + * @context: a #StThemeContext + * @font: the default font for theme context + * + * Sets the default font for the theme context. This is the font that + * is inherited by the root node of the tree of theme nodes. If the + * font is not overridden, then this font will be used. If the font is + * partially modified (for example, with 'font-size: 110%'), then that + * modification is based on this font. + */ +void +st_theme_context_set_font (StThemeContext *context, + const PangoFontDescription *font) +{ + g_return_if_fail (ST_IS_THEME_CONTEXT (context)); + g_return_if_fail (font != NULL); + + if (context->font == font || + pango_font_description_equal (context->font, font)) + return; + + pango_font_description_free (context->font); + context->font = pango_font_description_copy (font); + st_theme_context_changed (context); +} + +/** + * st_theme_context_get_font: + * @context: a #StThemeContext + * + * Gets the default font for the theme context. See st_theme_context_set_font(). + * + * Returns: the default font for the theme context. + */ +const PangoFontDescription * +st_theme_context_get_font (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + + return context->font; +} + +/** + * st_theme_context_get_root_node: + * @context: a #StThemeContext + * + * Gets the root node of the tree of theme style nodes that associated with this + * context. For the node tree associated with a stage, this node represents + * styles applied to the stage itself. + * + * Returns: (transfer none): the root node of the context's style tree + */ +StThemeNode * +st_theme_context_get_root_node (StThemeContext *context) +{ + if (context->root_node == NULL) + context->root_node = st_theme_node_new (context, NULL, context->theme, + G_TYPE_NONE, NULL, NULL, NULL, NULL); + + return context->root_node; +} + +/** + * st_theme_context_intern_node: + * @context: a #StThemeContext + * @node: a #StThemeNode + * + * Return an existing node matching @node, or if that isn't possible, + * @node itself. + * + * Returns: (transfer none): a node with the same properties as @node + */ +StThemeNode * +st_theme_context_intern_node (StThemeContext *context, + StThemeNode *node) +{ + StThemeNode *mine = g_hash_table_lookup (context->nodes, node); + + /* this might be node or not - it doesn't actually matter */ + if (mine != NULL) + return mine; + + g_hash_table_add (context->nodes, g_object_ref (node)); + return node; +} + +/** + * st_theme_context_get_scale_factor: + * @context: a #StThemeContext + * + * Return the current scale factor of @context. + * + * Returns: an integer scale factor + */ +int +st_theme_context_get_scale_factor (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), -1); + + return context->scale_factor; +} diff --git a/src/st/st-theme-context.h b/src/st/st-theme-context.h new file mode 100644 index 0000000..165ce25 --- /dev/null +++ b/src/st/st-theme-context.h @@ -0,0 +1,65 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-context.c: holds global information about a tree of styled objects + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2009 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_THEME_CONTEXT_H__ +#define __ST_THEME_CONTEXT_H__ + +#include <clutter/clutter.h> +#include <pango/pango.h> +#include "st-theme-node.h" + +G_BEGIN_DECLS + +/** + * SECTION:st-theme-context + * @short_description: holds global information about a tree of styled objects + * + * #StThemeContext is responsible for managing information global to a tree of styled objects, + * such as the set of stylesheets or the default font. In normal usage, a #StThemeContext + * is bound to a #ClutterStage; a singleton #StThemeContext can be obtained for a #ClutterStage + * by using st_theme_context_get_for_stage(). + */ + +#define ST_TYPE_THEME_CONTEXT (st_theme_context_get_type ()) +G_DECLARE_FINAL_TYPE (StThemeContext, st_theme_context, + ST, THEME_CONTEXT, GObject) + +StThemeContext *st_theme_context_new (void); +StThemeContext *st_theme_context_get_for_stage (ClutterStage *stage); + +void st_theme_context_set_theme (StThemeContext *context, + StTheme *theme); +StTheme * st_theme_context_get_theme (StThemeContext *context); + +void st_theme_context_set_font (StThemeContext *context, + const PangoFontDescription *font); +const PangoFontDescription *st_theme_context_get_font (StThemeContext *context); + +StThemeNode * st_theme_context_get_root_node (StThemeContext *context); + +StThemeNode * st_theme_context_intern_node (StThemeContext *context, + StThemeNode *node); + +int st_theme_context_get_scale_factor (StThemeContext *context); + +G_END_DECLS + +#endif /* __ST_THEME_CONTEXT_H__ */ diff --git a/src/st/st-theme-node-drawing.c b/src/st/st-theme-node-drawing.c new file mode 100644 index 0000000..72745ed --- /dev/null +++ b/src/st/st-theme-node-drawing.c @@ -0,0 +1,2864 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node-drawing.c: Code to draw themed elements + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2009, 2010 Florian Müllner + * Copyright 2010 Intel Corporation. + * Copyright 2011 Quentin "Sardem FF7" Glidic + * + * Contains code derived from: + * rectangle.c: Rounded rectangle. + * Copyright 2008 litl, LLC. + * st-texture-frame.h: Expandible texture actor + * Copyright 2007 OpenedHand + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <math.h> + +#include "st-shadow.h" +#include "st-private.h" +#include "st-theme-private.h" +#include "st-theme-context.h" +#include "st-texture-cache.h" +#include "st-theme-node-private.h" + +/**** + * Rounded corners + ****/ + +typedef struct { + ClutterColor color; + ClutterColor border_color_1; + ClutterColor border_color_2; + guint radius; + guint border_width_1; + guint border_width_2; + float resource_scale; +} StCornerSpec; + +typedef enum { + ST_PAINT_BORDERS_MODE_COLOR, + ST_PAINT_BORDERS_MODE_SILHOUETTE +} StPaintBordersMode; + +static void +elliptical_arc (cairo_t *cr, + double x_center, + double y_center, + double x_radius, + double y_radius, + double angle1, + double angle2) +{ + cairo_save (cr); + cairo_translate (cr, x_center, y_center); + cairo_scale (cr, x_radius, y_radius); + cairo_arc (cr, 0, 0, 1.0, angle1, angle2); + cairo_restore (cr); +} + +static CoglTexture * +create_corner_material (StCornerSpec *corner) +{ + ClutterBackend *backend = clutter_get_default_backend (); + CoglContext *ctx = clutter_backend_get_cogl_context (backend); + GError *error = NULL; + CoglTexture *texture; + cairo_t *cr; + cairo_surface_t *surface; + guint rowstride; + guint8 *data; + guint size; + guint logical_size; + guint max_border_width; + double device_scaling; + + max_border_width = MAX(corner->border_width_2, corner->border_width_1); + logical_size = 2 * MAX(max_border_width, corner->radius); + size = ceilf (logical_size * corner->resource_scale); + rowstride = size * 4; + data = g_new0 (guint8, size * rowstride); + + surface = cairo_image_surface_create_for_data (data, + CAIRO_FORMAT_ARGB32, + size, size, + rowstride); + device_scaling = (double) size / logical_size; + cairo_surface_set_device_scale (surface, device_scaling, device_scaling); + cr = cairo_create (surface); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_scale (cr, logical_size, logical_size); + + if (max_border_width <= corner->radius) + { + double x_radius, y_radius; + + if (max_border_width != 0) + { + cairo_set_source_rgba (cr, + corner->border_color_1.red / 255., + corner->border_color_1.green / 255., + corner->border_color_1.blue / 255., + corner->border_color_1.alpha / 255.); + + cairo_arc (cr, 0.5, 0.5, 0.5, 0, 2 * M_PI); + cairo_fill (cr); + } + + cairo_set_source_rgba (cr, + corner->color.red / 255., + corner->color.green / 255., + corner->color.blue / 255., + corner->color.alpha / 255.); + + x_radius = 0.5 * (1.0 - (double) corner->border_width_2 / corner->radius); + y_radius = 0.5 * (1.0 - (double) corner->border_width_1 / corner->radius); + + /* TOPRIGHT */ + elliptical_arc (cr, + 0.5, 0.5, + x_radius, y_radius, + 3 * M_PI / 2, 2 * M_PI); + + /* BOTTOMRIGHT */ + elliptical_arc (cr, + 0.5, 0.5, + x_radius, y_radius, + 0, M_PI / 2); + + /* TOPLEFT */ + elliptical_arc (cr, + 0.5, 0.5, + x_radius, y_radius, + M_PI, 3 * M_PI / 2); + + /* BOTTOMLEFT */ + elliptical_arc (cr, + 0.5, 0.5, + x_radius, y_radius, + M_PI / 2, M_PI); + + cairo_fill (cr); + } + else + { + double radius; + + radius = (gdouble)corner->radius / max_border_width; + + cairo_set_source_rgba (cr, + corner->border_color_1.red / 255., + corner->border_color_1.green / 255., + corner->border_color_1.blue / 255., + corner->border_color_1.alpha / 255.); + + cairo_arc (cr, radius, radius, radius, M_PI, 3 * M_PI / 2); + cairo_line_to (cr, 1.0 - radius, 0.0); + cairo_arc (cr, 1.0 - radius, radius, radius, 3 * M_PI / 2, 2 * M_PI); + cairo_line_to (cr, 1.0, 1.0 - radius); + cairo_arc (cr, 1.0 - radius, 1.0 - radius, radius, 0, M_PI / 2); + cairo_line_to (cr, radius, 1.0); + cairo_arc (cr, radius, 1.0 - radius, radius, M_PI / 2, M_PI); + cairo_fill (cr); + } + cairo_destroy (cr); + + cairo_surface_destroy (surface); + + texture = COGL_TEXTURE (cogl_texture_2d_new_from_data (ctx, size, size, + CLUTTER_CAIRO_FORMAT_ARGB32, + rowstride, + data, + &error)); + + if (error) + { + g_warning ("Failed to allocate texture: %s", error->message); + g_error_free (error); + } + + g_free (data); + + return texture; +} + +static char * +corner_to_string (StCornerSpec *corner) +{ + return g_strdup_printf ("st-theme-node-corner:%02x%02x%02x%02x,%02x%02x%02x%02x,%02x%02x%02x%02x,%u,%u,%u,%.4f", + corner->color.red, corner->color.blue, corner->color.green, corner->color.alpha, + corner->border_color_1.red, corner->border_color_1.green, corner->border_color_1.blue, corner->border_color_1.alpha, + corner->border_color_2.red, corner->border_color_2.green, corner->border_color_2.blue, corner->border_color_2.alpha, + corner->radius, + corner->border_width_1, + corner->border_width_2, + corner->resource_scale); +} + +static CoglTexture * +load_corner (StTextureCache *cache, + const char *key, + void *datap, + GError **error) +{ + return create_corner_material ((StCornerSpec *) datap); +} + +/* To match the CSS specification, we want the border to look like it was + * drawn over the background. But actually drawing the border over the + * background will produce slightly bad antialiasing at the edges, so + * compute the effective border color instead. + */ +#define NORM(x) (t = (x) + 127, (t + (t >> 8)) >> 8) +#define MULT(c,a) NORM(c*a) + +static void +premultiply (ClutterColor *color) +{ + guint t; + color->red = MULT (color->red, color->alpha); + color->green = MULT (color->green, color->alpha); + color->blue = MULT (color->blue, color->alpha); +} + +static void +unpremultiply (ClutterColor *color) +{ + if (color->alpha != 0) + { + color->red = MIN((color->red * 255 + 127) / color->alpha, 255); + color->green = MIN((color->green * 255 + 127) / color->alpha, 255); + color->blue = MIN((color->blue * 255 + 127) / color->alpha, 255); + } +} + +static void +over (const ClutterColor *source, + const ClutterColor *destination, + ClutterColor *result) +{ + guint t; + ClutterColor src = *source; + ClutterColor dst = *destination; + + premultiply (&src); + premultiply (&dst); + + result->alpha = src.alpha + NORM ((255 - src.alpha) * dst.alpha); + result->red = src.red + NORM ((255 - src.alpha) * dst.red); + result->green = src.green + NORM ((255 - src.alpha) * dst.green); + result->blue = src.blue + NORM ((255 - src.alpha) * dst.blue); + + unpremultiply (result); +} + +/* + * st_theme_node_reduce_border_radius: + * @node: a #StThemeNode + * @width: The width of the box + * @height: The height of the box + * @corners: (array length=4) (out): reduced corners + * + * Implements the corner overlap algorithm mentioned at + * http://www.w3.org/TR/css3-background/#corner-overlap + */ +static void +st_theme_node_reduce_border_radius (StThemeNode *node, + float width, + float height, + guint *corners) +{ + gfloat scale; + guint sum; + + scale = 1.0; + + /* top */ + sum = node->border_radius[ST_CORNER_TOPLEFT] + + node->border_radius[ST_CORNER_TOPRIGHT]; + + if (sum > 0) + scale = MIN (width / sum, scale); + + /* right */ + sum = node->border_radius[ST_CORNER_TOPRIGHT] + + node->border_radius[ST_CORNER_BOTTOMRIGHT]; + + if (sum > 0) + scale = MIN (height / sum, scale); + + /* bottom */ + sum = node->border_radius[ST_CORNER_BOTTOMLEFT] + + node->border_radius[ST_CORNER_BOTTOMRIGHT]; + + if (sum > 0) + scale = MIN (width / sum, scale); + + /* left */ + sum = node->border_radius[ST_CORNER_BOTTOMLEFT] + + node->border_radius[ST_CORNER_TOPLEFT]; + + if (sum > 0) + scale = MIN (height / sum, scale); + + corners[ST_CORNER_TOPLEFT] = node->border_radius[ST_CORNER_TOPLEFT] * scale; + corners[ST_CORNER_TOPRIGHT] = node->border_radius[ST_CORNER_TOPRIGHT] * scale; + corners[ST_CORNER_BOTTOMLEFT] = node->border_radius[ST_CORNER_BOTTOMLEFT] * scale; + corners[ST_CORNER_BOTTOMRIGHT] = node->border_radius[ST_CORNER_BOTTOMRIGHT] * scale; +} + +static void +st_theme_node_get_corner_border_widths (StThemeNode *node, + StCorner corner_id, + guint *border_width_1, + guint *border_width_2) +{ + switch (corner_id) + { + case ST_CORNER_TOPLEFT: + if (border_width_1) + *border_width_1 = node->border_width[ST_SIDE_TOP]; + if (border_width_2) + *border_width_2 = node->border_width[ST_SIDE_LEFT]; + break; + case ST_CORNER_TOPRIGHT: + if (border_width_1) + *border_width_1 = node->border_width[ST_SIDE_TOP]; + if (border_width_2) + *border_width_2 = node->border_width[ST_SIDE_RIGHT]; + break; + case ST_CORNER_BOTTOMRIGHT: + if (border_width_1) + *border_width_1 = node->border_width[ST_SIDE_BOTTOM]; + if (border_width_2) + *border_width_2 = node->border_width[ST_SIDE_RIGHT]; + break; + case ST_CORNER_BOTTOMLEFT: + if (border_width_1) + *border_width_1 = node->border_width[ST_SIDE_BOTTOM]; + if (border_width_2) + *border_width_2 = node->border_width[ST_SIDE_LEFT]; + break; + default: + g_assert_not_reached(); + break; + } +} + +static CoglPipeline * +st_theme_node_lookup_corner (StThemeNode *node, + float width, + float height, + float resource_scale, + StCorner corner_id) +{ + CoglTexture *texture = NULL; + CoglPipeline *material = NULL; + char *key; + StTextureCache *cache; + StCornerSpec corner; + guint radius[4]; + + cache = st_texture_cache_get_default (); + + st_theme_node_reduce_border_radius (node, width, height, radius); + + if (radius[corner_id] == 0) + return NULL; + + corner.radius = radius[corner_id]; + corner.color = node->background_color; + corner.resource_scale = resource_scale; + st_theme_node_get_corner_border_widths (node, corner_id, + &corner.border_width_1, + &corner.border_width_2); + + switch (corner_id) + { + case ST_CORNER_TOPLEFT: + over (&node->border_color[ST_SIDE_TOP], &corner.color, &corner.border_color_1); + over (&node->border_color[ST_SIDE_LEFT], &corner.color, &corner.border_color_2); + break; + case ST_CORNER_TOPRIGHT: + over (&node->border_color[ST_SIDE_TOP], &corner.color, &corner.border_color_1); + over (&node->border_color[ST_SIDE_RIGHT], &corner.color, &corner.border_color_2); + break; + case ST_CORNER_BOTTOMRIGHT: + over (&node->border_color[ST_SIDE_BOTTOM], &corner.color, &corner.border_color_1); + over (&node->border_color[ST_SIDE_RIGHT], &corner.color, &corner.border_color_2); + break; + case ST_CORNER_BOTTOMLEFT: + over (&node->border_color[ST_SIDE_BOTTOM], &corner.color, &corner.border_color_1); + over (&node->border_color[ST_SIDE_LEFT], &corner.color, &corner.border_color_2); + break; + default: + g_assert_not_reached(); + break; + } + + if (corner.color.alpha == 0 && + corner.border_color_1.alpha == 0 && + corner.border_color_2.alpha == 0) + { + if (node->box_shadow == NULL) + return NULL; + else /* We still need a corner texture to render the box-shadow */ + corner.color = (ClutterColor) {0, 0, 0, 255}; + } + + key = corner_to_string (&corner); + texture = st_texture_cache_load (cache, key, ST_TEXTURE_CACHE_POLICY_FOREVER, load_corner, &corner, NULL); + + if (texture) + { + material = _st_create_texture_pipeline (texture); + cogl_object_unref (texture); + } + + g_free (key); + + return material; +} + +static void +get_background_scale (StThemeNode *node, + gdouble painting_area_width, + gdouble painting_area_height, + gdouble background_image_width, + gdouble background_image_height, + gdouble *scale_w, + gdouble *scale_h) +{ + *scale_w = -1.0; + *scale_h = -1.0; + + switch (node->background_size) + { + case ST_BACKGROUND_SIZE_AUTO: + *scale_w = 1.0f; + break; + case ST_BACKGROUND_SIZE_CONTAIN: + *scale_w = MIN (painting_area_width / background_image_width, + painting_area_height / background_image_height); + break; + case ST_BACKGROUND_SIZE_COVER: + *scale_w = MAX (painting_area_width / background_image_width, + painting_area_height / background_image_height); + break; + case ST_BACKGROUND_SIZE_FIXED: + if (node->background_size_w > -1) + { + *scale_w = node->background_size_w / background_image_width; + if (node->background_size_h > -1) + *scale_h = node->background_size_h / background_image_height; + } + else if (node->background_size_h > -1) + *scale_w = node->background_size_h / background_image_height; + break; + default: + g_assert_not_reached(); + break; + } + if (*scale_h < 0.0) + *scale_h = *scale_w; +} + +static void +get_background_coordinates (StThemeNode *node, + gdouble painting_area_width, + gdouble painting_area_height, + gdouble background_image_width, + gdouble background_image_height, + gdouble *x, + gdouble *y) +{ + /* honor the specified position if any */ + if (node->background_position_set) + { + *x = node->background_position_x; + *y = node->background_position_y; + } + else + { + /* center the background on the widget */ + *x = (painting_area_width / 2.0) - (background_image_width / 2.0); + *y = (painting_area_height / 2.0) - (background_image_height / 2.0); + } +} + +static void +get_background_position (StThemeNode *self, + const ClutterActorBox *allocation, + float resource_scale, + ClutterActorBox *result, + ClutterActorBox *texture_coords) +{ + gdouble painting_area_width, painting_area_height; + gdouble background_image_width, background_image_height; + gdouble x1, y1; + gdouble scale_w, scale_h; + + /* get the background image size */ + background_image_width = cogl_texture_get_width (self->background_texture); + background_image_height = cogl_texture_get_height (self->background_texture); + + background_image_width /= resource_scale; + background_image_height /= resource_scale; + + /* get the painting area size */ + painting_area_width = allocation->x2 - allocation->x1; + painting_area_height = allocation->y2 - allocation->y1; + + /* scale if requested */ + get_background_scale (self, + painting_area_width, painting_area_height, + background_image_width, background_image_height, + &scale_w, &scale_h); + + background_image_width *= scale_w; + background_image_height *= scale_h; + + /* get coordinates */ + get_background_coordinates (self, + painting_area_width, painting_area_height, + background_image_width, background_image_height, + &x1, &y1); + + if (self->background_repeat) + { + gdouble width = allocation->x2 - allocation->x1 + x1; + gdouble height = allocation->y2 - allocation->y1 + y1; + + *result = *allocation; + + /* reference image is at x1, y1 */ + texture_coords->x1 = x1 / background_image_width; + texture_coords->y1 = y1 / background_image_height; + texture_coords->x2 = width / background_image_width; + texture_coords->y2 = height / background_image_height; + } + else + { + result->x1 = x1; + result->y1 = y1; + result->x2 = x1 + background_image_width; + result->y2 = y1 + background_image_height; + + texture_coords->x1 = texture_coords->y1 = 0; + texture_coords->x2 = texture_coords->y2 = 1; + } +} + +/* Use of this function marks code which doesn't support + * non-uniform colors. + */ +static void +get_arbitrary_border_color (StThemeNode *node, + ClutterColor *color) +{ + if (color) + st_theme_node_get_border_color (node, ST_SIDE_TOP, color); +} + +static gboolean +st_theme_node_has_visible_outline (StThemeNode *node) +{ + if (node->background_color.alpha > 0) + return TRUE; + + if (node->background_gradient_end.alpha > 0) + return TRUE; + + if (node->border_radius[ST_CORNER_TOPLEFT] > 0 || + node->border_radius[ST_CORNER_TOPRIGHT] > 0 || + node->border_radius[ST_CORNER_BOTTOMLEFT] > 0 || + node->border_radius[ST_CORNER_BOTTOMRIGHT] > 0) + return TRUE; + + if (node->border_width[ST_SIDE_TOP] > 0 || + node->border_width[ST_SIDE_LEFT] > 0 || + node->border_width[ST_SIDE_RIGHT] > 0 || + node->border_width[ST_SIDE_BOTTOM] > 0) + return TRUE; + + return FALSE; +} + +static cairo_pattern_t * +create_cairo_pattern_of_background_gradient (StThemeNode *node, + float width, + float height) +{ + cairo_pattern_t *pattern; + + g_return_val_if_fail (node->background_gradient_type != ST_GRADIENT_NONE, + NULL); + + if (node->background_gradient_type == ST_GRADIENT_VERTICAL) + pattern = cairo_pattern_create_linear (0, 0, 0, height); + else if (node->background_gradient_type == ST_GRADIENT_HORIZONTAL) + pattern = cairo_pattern_create_linear (0, 0, width, 0); + else + { + gdouble cx, cy; + + cx = width / 2.; + cy = height / 2.; + pattern = cairo_pattern_create_radial (cx, cy, 0, cx, cy, MIN (cx, cy)); + } + + cairo_pattern_add_color_stop_rgba (pattern, 0, + node->background_color.red / 255., + node->background_color.green / 255., + node->background_color.blue / 255., + node->background_color.alpha / 255.); + cairo_pattern_add_color_stop_rgba (pattern, 1, + node->background_gradient_end.red / 255., + node->background_gradient_end.green / 255., + node->background_gradient_end.blue / 255., + node->background_gradient_end.alpha / 255.); + return pattern; +} + +static cairo_pattern_t * +create_cairo_pattern_of_background_image (StThemeNode *node, + float width, + float height, + float resource_scale, + gboolean *needs_background_fill) +{ + cairo_surface_t *surface; + cairo_pattern_t *pattern; + cairo_content_t content; + cairo_matrix_t matrix; + GFile *file; + + StTextureCache *texture_cache; + + gdouble background_image_width, background_image_height; + gdouble x, y; + gdouble scale_w, scale_h; + + file = st_theme_node_get_background_image (node); + + texture_cache = st_texture_cache_get_default (); + + surface = st_texture_cache_load_file_to_cairo_surface (texture_cache, file, + node->cached_scale_factor, + resource_scale); + + if (surface == NULL) + return NULL; + + g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE); + + content = cairo_surface_get_content (surface); + pattern = cairo_pattern_create_for_surface (surface); + + background_image_width = cairo_image_surface_get_width (surface); + background_image_height = cairo_image_surface_get_height (surface); + + *needs_background_fill = TRUE; + + cairo_matrix_init_identity (&matrix); + + if (resource_scale != 1.0) + { + background_image_width /= resource_scale; + background_image_height /= resource_scale; + + cairo_matrix_scale (&matrix, resource_scale, resource_scale); + } + + get_background_scale (node, + width, height, + background_image_width, background_image_height, + &scale_w, &scale_h); + + if ((scale_w != 1) || (scale_h != 1)) + cairo_matrix_scale (&matrix, 1.0/scale_w, 1.0/scale_h); + + background_image_width *= scale_w; + background_image_height *= scale_h; + + get_background_coordinates (node, + width, height, + background_image_width, background_image_height, + &x, &y); + cairo_matrix_translate (&matrix, -x, -y); + + if (node->background_repeat) + cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT); + + /* If it's opaque, fills up the entire allocated + * area, then don't bother doing a background fill first + */ + if (content != CAIRO_CONTENT_COLOR_ALPHA) + { + if (node->background_repeat || + (x >= 0 && + y >= 0 && + background_image_width - x >= width && + background_image_height -y >= height)) + *needs_background_fill = FALSE; + } + + cairo_pattern_set_matrix (pattern, &matrix); + + return pattern; +} + +/* fill_exterior = TRUE means that pattern is a surface pattern and + * we should extend the pattern with a solid fill from its edges. + * This is a bit of a hack; the alternative would be to make the + * surface of the surface pattern 1 pixel bigger and use CAIRO_EXTEND_PAD. + */ +static void +paint_shadow_pattern_to_cairo_context (StShadow *shadow_spec, + cairo_pattern_t *pattern, + gboolean fill_exterior, + cairo_t *cr, + cairo_path_t *interior_path, + cairo_path_t *outline_path) +{ + /* If there are borders, clip the shadow to the interior + * of the borders; if there is a visible outline, clip the shadow to + * that outline + */ + cairo_path_t *path = (interior_path != NULL) ? interior_path : outline_path; + double x1, x2, y1, y2; + + /* fill_exterior only makes sense if we're clipping the shadow - filling + * to the edges of the surface would be silly */ + g_assert (!(fill_exterior && path == NULL)); + + cairo_save (cr); + if (path != NULL) + { + cairo_append_path (cr, path); + + /* There's no way to invert a path in cairo, so we need bounds for + * the area we are drawing in order to create the "exterior" region. + * Pixel align to hit fast paths. + */ + if (fill_exterior) + { + cairo_path_extents (cr, &x1, &y1, &x2, &y2); + x1 = floor (x1); + y1 = floor (y1); + x2 = ceil (x2); + y2 = ceil (y2); + } + + cairo_clip (cr); + } + + cairo_set_source_rgba (cr, + shadow_spec->color.red / 255.0, + shadow_spec->color.green / 255.0, + shadow_spec->color.blue / 255.0, + shadow_spec->color.alpha / 255.0); + if (fill_exterior) + { + cairo_surface_t *surface; + int width, height; + double xscale, yscale; + cairo_matrix_t matrix; + + cairo_save (cr); + + /* Start with a rectangle enclosing the bounds of the clipped + * region */ + cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1); + + /* Then subtract out the bounds of the surface in the surface + * pattern; we transform the context by the inverse of the + * pattern matrix to get to surface coordinates */ + + if (cairo_pattern_get_surface (pattern, &surface) != CAIRO_STATUS_SUCCESS) + /* Something went wrong previously */ + goto no_surface; + + cairo_surface_get_device_scale (surface, &xscale, &yscale); + width = cairo_image_surface_get_width (surface); + height = cairo_image_surface_get_height (surface); + + cairo_pattern_get_matrix (pattern, &matrix); + cairo_matrix_invert (&matrix); + cairo_matrix_scale (&matrix, 1.0 / xscale, 1.0 / yscale); + cairo_transform (cr, &matrix); + + cairo_rectangle (cr, 0, height, width, - height); + cairo_fill (cr); + + no_surface: + cairo_restore (cr); + } + + cairo_mask (cr, pattern); + cairo_restore (cr); +} + +static void +paint_background_image_shadow_to_cairo_context (StThemeNode *node, + StShadow *shadow_spec, + cairo_pattern_t *pattern, + cairo_t *cr, + cairo_path_t *interior_path, + cairo_path_t *outline_path, + int x, + int y, + int width, + int height, + float resource_scale) +{ + cairo_pattern_t *shadow_pattern; + + g_assert (shadow_spec != NULL); + g_assert (pattern != NULL); + + if (outline_path != NULL) + { + cairo_surface_t *clipped_surface; + cairo_pattern_t *clipped_pattern; + cairo_t *temp_cr; + + /* Prerender the pattern to a temporary surface, + * so it's properly clipped before we create a shadow from it + */ + width = ceilf (width * resource_scale); + height = ceilf (height * resource_scale); + clipped_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height); + cairo_surface_set_device_scale (clipped_surface, resource_scale, resource_scale); + temp_cr = cairo_create (clipped_surface); + + cairo_set_operator (temp_cr, CAIRO_OPERATOR_CLEAR); + cairo_paint (temp_cr); + cairo_set_operator (temp_cr, CAIRO_OPERATOR_SOURCE); + + if (interior_path != NULL) + { + cairo_append_path (temp_cr, interior_path); + cairo_clip (temp_cr); + } + + cairo_append_path (temp_cr, outline_path); + cairo_translate (temp_cr, x, y); + cairo_set_source (temp_cr, pattern); + cairo_clip (temp_cr); + cairo_paint (temp_cr); + cairo_destroy (temp_cr); + + clipped_pattern = cairo_pattern_create_for_surface (clipped_surface); + cairo_surface_destroy (clipped_surface); + + shadow_pattern = _st_create_shadow_cairo_pattern (shadow_spec, + clipped_pattern); + cairo_pattern_destroy (clipped_pattern); + } + else + { + shadow_pattern = _st_create_shadow_cairo_pattern (shadow_spec, + pattern); + } + + paint_shadow_pattern_to_cairo_context (shadow_spec, + shadow_pattern, FALSE, + cr, + interior_path, + outline_path); + cairo_pattern_destroy (shadow_pattern); +} + +/* gets the extents of a cairo_path_t; slightly inefficient, but much simpler than + * computing from the raw path data */ +static void +path_extents (cairo_path_t *path, + double *x1, + double *y1, + double *x2, + double *y2) + +{ + cairo_surface_t *dummy = cairo_image_surface_create (CAIRO_FORMAT_A8, 1, 1); + cairo_t *cr = cairo_create (dummy); + + cairo_append_path (cr, path); + cairo_path_extents (cr, x1, y1, x2, y2); + + cairo_destroy (cr); + cairo_surface_destroy (dummy); +} + +static void +paint_inset_box_shadow_to_cairo_context (StThemeNode *node, + StShadow *shadow_spec, + float resource_scale, + cairo_t *cr, + cairo_path_t *shadow_outline) +{ + cairo_surface_t *shadow_surface; + cairo_pattern_t *shadow_pattern; + double extents_x1, extents_y1, extents_x2, extents_y2; + double shrunk_extents_x1, shrunk_extents_y1, shrunk_extents_x2, shrunk_extents_y2; + gboolean fill_exterior; + + g_assert (shadow_spec != NULL); + g_assert (shadow_outline != NULL); + + /* Create the pattern used to create the inset shadow; as the shadow + * should be drawn as if everything outside the outline was opaque, + * we use a temporary surface to draw the background as a solid shape, + * which is inverted when creating the shadow pattern. + */ + + /* First we need to find the size of the temporary surface + */ + path_extents (shadow_outline, + &extents_x1, &extents_y1, &extents_x2, &extents_y2); + + /* Shrink the extents by the spread, and offset */ + shrunk_extents_x1 = extents_x1 + shadow_spec->xoffset + shadow_spec->spread; + shrunk_extents_y1 = extents_y1 + shadow_spec->yoffset + shadow_spec->spread; + shrunk_extents_x2 = extents_x2 + shadow_spec->xoffset - shadow_spec->spread; + shrunk_extents_y2 = extents_y2 + shadow_spec->yoffset - shadow_spec->spread; + + if (shrunk_extents_x1 >= shrunk_extents_x2 || shrunk_extents_y1 >= shrunk_extents_y2) + { + /* Shadow occupies entire area within border */ + shadow_pattern = cairo_pattern_create_rgb (0., 0., 0.); + fill_exterior = FALSE; + } + else + { + /* Bounds of temporary surface */ + int surface_x = floor (shrunk_extents_x1); + int surface_y = floor (shrunk_extents_y1); + int surface_width = ceil ((shrunk_extents_x2 - surface_x) * resource_scale); + int surface_height = ceil ((shrunk_extents_y2 - surface_y) * resource_scale); + + /* Center of the original path */ + double x_center = (extents_x1 + extents_x2) / 2; + double y_center = (extents_y1 + extents_y2) / 2; + + cairo_pattern_t *pattern; + cairo_t *temp_cr; + cairo_matrix_t matrix; + + shadow_surface = cairo_image_surface_create (CAIRO_FORMAT_A8, surface_width, surface_height); + cairo_surface_set_device_scale (shadow_surface, resource_scale, resource_scale); + temp_cr = cairo_create (shadow_surface); + + /* Match the coordinates in the temporary context to the parent context */ + cairo_translate (temp_cr, - surface_x, - surface_y); + + /* Shadow offset */ + cairo_translate (temp_cr, shadow_spec->xoffset, shadow_spec->yoffset); + + /* Scale the path around the center to match the shrunk bounds */ + cairo_translate (temp_cr, x_center, y_center); + cairo_scale (temp_cr, + (shrunk_extents_x2 - shrunk_extents_x1) / (extents_x2 - extents_x1), + (shrunk_extents_y2 - shrunk_extents_y1) / (extents_y2 - extents_y1)); + cairo_translate (temp_cr, - x_center, - y_center); + + cairo_append_path (temp_cr, shadow_outline); + cairo_fill (temp_cr); + cairo_destroy (temp_cr); + + pattern = cairo_pattern_create_for_surface (shadow_surface); + cairo_surface_destroy (shadow_surface); + + /* The pattern needs to be offset back to coordinates in the parent context */ + cairo_matrix_init_translate (&matrix, - surface_x, - surface_y); + cairo_pattern_set_matrix (pattern, &matrix); + + shadow_pattern = _st_create_shadow_cairo_pattern (shadow_spec, pattern); + fill_exterior = TRUE; + + cairo_pattern_destroy (pattern); + } + + paint_shadow_pattern_to_cairo_context (shadow_spec, + shadow_pattern, fill_exterior, + cr, + shadow_outline, + NULL); + + cairo_pattern_destroy (shadow_pattern); +} + +/* In order for borders to be smoothly blended with non-solid backgrounds, + * we need to use cairo. This function is a slow fallback path for those + * cases (gradients, background images, etc). + */ +static CoglTexture * +st_theme_node_prerender_background (StThemeNode *node, + float actor_width, + float actor_height, + float resource_scale) +{ + ClutterBackend *backend = clutter_get_default_backend (); + CoglContext *ctx = clutter_backend_get_cogl_context (backend); + GError *error = NULL; + StBorderImage *border_image; + CoglTexture *texture; + guint radius[4]; + int i; + cairo_t *cr; + cairo_surface_t *surface; + StShadow *shadow_spec; + StShadow *box_shadow_spec; + cairo_pattern_t *pattern = NULL; + cairo_path_t *outline_path = NULL; + gboolean draw_solid_background = TRUE; + gboolean background_is_translucent; + gboolean interior_dirty; + gboolean draw_background_image_shadow = FALSE; + gboolean has_visible_outline; + ClutterColor border_color; + guint border_width[4]; + guint rowstride; + guchar *data; + ClutterActorBox actor_box; + ClutterActorBox paint_box; + cairo_path_t *interior_path = NULL; + float width, height; + int texture_width; + int texture_height; + + border_image = st_theme_node_get_border_image (node); + + shadow_spec = st_theme_node_get_background_image_shadow (node); + box_shadow_spec = st_theme_node_get_box_shadow (node); + + actor_box.x1 = 0; + actor_box.x2 = actor_width; + actor_box.y1 = 0; + actor_box.y2 = actor_height; + + /* If there's a background image shadow, we + * may need to create an image bigger than the nodes + * allocation + */ + st_theme_node_get_background_paint_box (node, &actor_box, &paint_box); + + /* translate the boxes so the paint box is at 0,0 + */ + actor_box.x1 += - paint_box.x1; + actor_box.x2 += - paint_box.x1; + actor_box.y1 += - paint_box.y1; + actor_box.y2 += - paint_box.y1; + + width = paint_box.x2 - paint_box.x1; + height = paint_box.y2 - paint_box.y1; + + texture_width = ceilf (width * resource_scale); + texture_height = ceilf (height * resource_scale); + + rowstride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, texture_width); + data = g_new0 (guchar, texture_height * rowstride); + + /* We zero initialize the destination memory, so it's fully transparent + * by default. + */ + interior_dirty = FALSE; + + surface = cairo_image_surface_create_for_data (data, + CAIRO_FORMAT_ARGB32, + texture_width, texture_height, + rowstride); + cairo_surface_set_device_scale (surface, resource_scale, resource_scale); + cr = cairo_create (surface); + + /* TODO - support non-uniform border colors */ + get_arbitrary_border_color (node, &border_color); + + st_theme_node_reduce_border_radius (node, width, height, radius); + + for (i = 0; i < 4; i++) + border_width[i] = st_theme_node_get_border_width (node, i); + + /* Note we don't support translucent background images on top + * of gradients. It's strictly either/or. + */ + if (node->background_gradient_type != ST_GRADIENT_NONE) + { + pattern = create_cairo_pattern_of_background_gradient (node, width, height); + draw_solid_background = FALSE; + + /* If the gradient has any translucent areas, we need to + * erase the interior region before drawing, so that we show + * what's actually under the gradient and not whatever is + * left over from filling the border, etc. + */ + if (node->background_color.alpha < 255 || + node->background_gradient_end.alpha < 255) + background_is_translucent = TRUE; + else + background_is_translucent = FALSE; + } + else + { + GFile *background_image; + + background_image = st_theme_node_get_background_image (node); + + if (background_image != NULL) + { + pattern = create_cairo_pattern_of_background_image (node, + width, height, + resource_scale, + &draw_solid_background); + if (shadow_spec && pattern != NULL) + draw_background_image_shadow = TRUE; + } + + /* We never need to clear the interior region before drawing the + * background image, because it either always fills the entire area + * opaquely, or we draw the solid background behind it. + */ + background_is_translucent = FALSE; + } + + if (pattern == NULL) + draw_solid_background = TRUE; + + /* drawing the solid background implicitly clears the interior + * region, so if we're going to draw a solid background before drawing + * the background pattern, then we don't need to bother also clearing the + * background region. + */ + if (draw_solid_background) + background_is_translucent = FALSE; + + has_visible_outline = st_theme_node_has_visible_outline (node); + + /* Create a path for the background's outline first */ + if (radius[ST_CORNER_TOPLEFT] > 0) + cairo_arc (cr, + actor_box.x1 + radius[ST_CORNER_TOPLEFT], + actor_box.y1 + radius[ST_CORNER_TOPLEFT], + radius[ST_CORNER_TOPLEFT], M_PI, 3 * M_PI / 2); + else + cairo_move_to (cr, actor_box.x1, actor_box.y1); + cairo_line_to (cr, actor_box.x2 - radius[ST_CORNER_TOPRIGHT], actor_box.x1); + if (radius[ST_CORNER_TOPRIGHT] > 0) + cairo_arc (cr, + actor_box.x2 - radius[ST_CORNER_TOPRIGHT], + actor_box.x1 + radius[ST_CORNER_TOPRIGHT], + radius[ST_CORNER_TOPRIGHT], 3 * M_PI / 2, 2 * M_PI); + cairo_line_to (cr, actor_box.x2, actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT]); + if (radius[ST_CORNER_BOTTOMRIGHT] > 0) + cairo_arc (cr, + actor_box.x2 - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT], + radius[ST_CORNER_BOTTOMRIGHT], 0, M_PI / 2); + cairo_line_to (cr, actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], actor_box.y2); + if (radius[ST_CORNER_BOTTOMLEFT] > 0) + cairo_arc (cr, + actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], + actor_box.y2 - radius[ST_CORNER_BOTTOMLEFT], + radius[ST_CORNER_BOTTOMLEFT], M_PI / 2, M_PI); + cairo_close_path (cr); + + outline_path = cairo_copy_path (cr); + + /* If we have a solid border, we fill the outline shape with the border + * color and create the inline shape for the background; + * otherwise the outline shape is filled with the background + * directly + */ + if (border_image == NULL && + (border_width[ST_SIDE_TOP] > 0 || + border_width[ST_SIDE_RIGHT] > 0 || + border_width[ST_SIDE_BOTTOM] > 0 || + border_width[ST_SIDE_LEFT] > 0)) + { + cairo_set_source_rgba (cr, + border_color.red / 255., + border_color.green / 255., + border_color.blue / 255., + border_color.alpha / 255.); + cairo_fill (cr); + + /* We were sloppy when filling in the border, and now the interior + * is filled with the border color, too. + */ + interior_dirty = TRUE; + + if (radius[ST_CORNER_TOPLEFT] > MAX(border_width[ST_SIDE_TOP], + border_width[ST_SIDE_LEFT])) + elliptical_arc (cr, + actor_box.x1 + radius[ST_CORNER_TOPLEFT], + actor_box.y1 + radius[ST_CORNER_TOPLEFT], + radius[ST_CORNER_TOPLEFT] - border_width[ST_SIDE_LEFT], + radius[ST_CORNER_TOPLEFT] - border_width[ST_SIDE_TOP], + M_PI, 3 * M_PI / 2); + else + cairo_move_to (cr, + actor_box.x1 + border_width[ST_SIDE_LEFT], + actor_box.y1 + border_width[ST_SIDE_TOP]); + + cairo_line_to (cr, + actor_box.x2 - MAX(radius[ST_CORNER_TOPRIGHT], border_width[ST_SIDE_RIGHT]), + actor_box.y1 + border_width[ST_SIDE_TOP]); + + if (radius[ST_CORNER_TOPRIGHT] > MAX(border_width[ST_SIDE_TOP], + border_width[ST_SIDE_RIGHT])) + elliptical_arc (cr, + actor_box.x2 - radius[ST_CORNER_TOPRIGHT], + actor_box.y1 + radius[ST_CORNER_TOPRIGHT], + radius[ST_CORNER_TOPRIGHT] - border_width[ST_SIDE_RIGHT], + radius[ST_CORNER_TOPRIGHT] - border_width[ST_SIDE_TOP], + 3 * M_PI / 2, 2 * M_PI); + else + cairo_line_to (cr, + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y1 + border_width[ST_SIDE_TOP]); + + cairo_line_to (cr, + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y2 - MAX(radius[ST_CORNER_BOTTOMRIGHT], border_width[ST_SIDE_BOTTOM])); + + if (radius[ST_CORNER_BOTTOMRIGHT] > MAX(border_width[ST_SIDE_BOTTOM], + border_width[ST_SIDE_RIGHT])) + elliptical_arc (cr, + actor_box.x2 - radius[ST_CORNER_BOTTOMRIGHT], + actor_box.y2 - radius[ST_CORNER_BOTTOMRIGHT], + radius[ST_CORNER_BOTTOMRIGHT] - border_width[ST_SIDE_RIGHT], + radius[ST_CORNER_BOTTOMRIGHT] - border_width[ST_SIDE_BOTTOM], + 0, M_PI / 2); + else + cairo_line_to (cr, + actor_box.x2 - border_width[ST_SIDE_RIGHT], + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); + + cairo_line_to (cr, + MAX(radius[ST_CORNER_BOTTOMLEFT], border_width[ST_SIDE_LEFT]), + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); + + if (radius[ST_CORNER_BOTTOMLEFT] > MAX(border_width[ST_SIDE_BOTTOM], + border_width[ST_SIDE_LEFT])) + elliptical_arc (cr, + actor_box.x1 + radius[ST_CORNER_BOTTOMLEFT], + actor_box.y2 - radius[ST_CORNER_BOTTOMLEFT], + radius[ST_CORNER_BOTTOMLEFT] - border_width[ST_SIDE_LEFT], + radius[ST_CORNER_BOTTOMLEFT] - border_width[ST_SIDE_BOTTOM], + M_PI / 2, M_PI); + else + cairo_line_to (cr, + actor_box.x1 + border_width[ST_SIDE_LEFT], + actor_box.y2 - border_width[ST_SIDE_BOTTOM]); + + cairo_close_path (cr); + + interior_path = cairo_copy_path (cr); + + /* clip drawing to the region inside of the borders + */ + cairo_clip (cr); + + /* But fill the pattern as if it started at the edge of outline, + * behind the borders. This is similar to + * background-clip: border-box; semantics. + */ + cairo_append_path (cr, outline_path); + } + + if (interior_dirty && background_is_translucent) + { + cairo_set_operator (cr, CAIRO_OPERATOR_CLEAR); + cairo_fill_preserve (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + } + + if (draw_solid_background) + { + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + + cairo_set_source_rgba (cr, + node->background_color.red / 255., + node->background_color.green / 255., + node->background_color.blue / 255., + node->background_color.alpha / 255.); + cairo_fill_preserve (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + } + + if (draw_background_image_shadow) + { + paint_background_image_shadow_to_cairo_context (node, + shadow_spec, + pattern, + cr, + interior_path, + has_visible_outline? outline_path : NULL, + actor_box.x1, + actor_box.y1, + width, height, + resource_scale); + cairo_append_path (cr, outline_path); + } + + cairo_translate (cr, actor_box.x1, actor_box.y1); + + if (pattern != NULL) + { + cairo_set_source (cr, pattern); + cairo_fill (cr); + cairo_pattern_destroy (pattern); + } + + if (box_shadow_spec && box_shadow_spec->inset) + { + paint_inset_box_shadow_to_cairo_context (node, + box_shadow_spec, + resource_scale, + cr, + interior_path ? interior_path + : outline_path); + } + + if (outline_path != NULL) + cairo_path_destroy (outline_path); + + if (interior_path != NULL) + cairo_path_destroy (interior_path); + + texture = COGL_TEXTURE (cogl_texture_2d_new_from_data (ctx, + texture_width, + texture_height, + CLUTTER_CAIRO_FORMAT_ARGB32, + rowstride, + data, + &error)); + + if (error) + { + g_warning ("Failed to allocate texture: %s", error->message); + g_error_free (error); + } + + cairo_destroy (cr); + cairo_surface_destroy (surface); + g_free (data); + + return texture; +} + +static void st_theme_node_paint_borders (StThemeNodePaintState *state, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + StPaintBordersMode mode, + guint8 paint_opacity); + +void +st_theme_node_invalidate_border_image (StThemeNode *node) +{ + cogl_clear_object (&node->border_slices_texture); + cogl_clear_object (&node->border_slices_pipeline); +} + +static gboolean +st_theme_node_load_border_image (StThemeNode *node, + gfloat resource_scale) +{ + if (node->border_slices_texture == NULL) + { + StBorderImage *border_image; + GFile *file; + + border_image = st_theme_node_get_border_image (node); + if (border_image == NULL) + goto out; + + file = st_border_image_get_file (border_image); + + node->border_slices_texture = st_texture_cache_load_file_to_cogl_texture (st_texture_cache_get_default (), + file, + node->cached_scale_factor, + resource_scale); + if (node->border_slices_texture == NULL) + goto out; + + node->border_slices_pipeline = _st_create_texture_pipeline (node->border_slices_texture); + } + + out: + return node->border_slices_texture != NULL; +} + +void +st_theme_node_invalidate_background_image (StThemeNode *node) +{ + cogl_clear_object (&node->background_texture); + cogl_clear_object (&node->background_pipeline); + cogl_clear_object (&node->background_shadow_pipeline); +} + +static gboolean +st_theme_node_load_background_image (StThemeNode *node, + gfloat resource_scale) +{ + if (node->background_texture == NULL) + { + GFile *background_image; + StShadow *background_image_shadow_spec; + + background_image = st_theme_node_get_background_image (node); + if (background_image == NULL) + goto out; + + background_image_shadow_spec = st_theme_node_get_background_image_shadow (node); + node->background_texture = st_texture_cache_load_file_to_cogl_texture (st_texture_cache_get_default (), + background_image, + node->cached_scale_factor, + resource_scale); + if (node->background_texture == NULL) + goto out; + + node->background_pipeline = _st_create_texture_pipeline (node->background_texture); + + if (node->background_repeat) + cogl_pipeline_set_layer_wrap_mode (node->background_pipeline, 0, + COGL_PIPELINE_WRAP_MODE_REPEAT); + + if (background_image_shadow_spec) + { + node->background_shadow_pipeline = _st_create_shadow_pipeline (background_image_shadow_spec, + node->background_texture, + resource_scale); + } + } + + out: + return node->background_texture != NULL; +} + +static gboolean +st_theme_node_invalidate_resources_for_file (StThemeNode *node, + GFile *file) +{ + StBorderImage *border_image; + gboolean changed = FALSE; + GFile *theme_file; + + theme_file = st_theme_node_get_background_image (node); + if ((theme_file != NULL) && g_file_equal (theme_file, file)) + { + st_theme_node_invalidate_background_image (node); + changed = TRUE; + } + + border_image = st_theme_node_get_border_image (node); + theme_file = border_image ? st_border_image_get_file (border_image) : NULL; + if ((theme_file != NULL) && g_file_equal (theme_file, file)) + { + st_theme_node_invalidate_border_image (node); + changed = TRUE; + } + + return changed; +} + +static void st_theme_node_compute_maximum_borders (StThemeNodePaintState *state); +static void st_theme_node_prerender_shadow (StThemeNodePaintState *state); + +static void +st_theme_node_render_resources (StThemeNodePaintState *state, + StThemeNode *node, + float width, + float height, + float resource_scale) +{ + gboolean has_border; + gboolean has_border_radius; + gboolean has_inset_box_shadow; + gboolean has_large_corners; + StShadow *box_shadow_spec; + + g_return_if_fail (width > 0 && height > 0); + + /* FIXME - need to separate this into things that need to be recomputed on + * geometry change versus things that can be cached regardless, such as + * a background image. + */ + st_theme_node_paint_state_free (state); + + st_theme_node_paint_state_set_node (state, node); + state->alloc_width = width; + state->alloc_height = height; + state->resource_scale = resource_scale; + + _st_theme_node_ensure_background (node); + _st_theme_node_ensure_geometry (node); + + box_shadow_spec = st_theme_node_get_box_shadow (node); + has_inset_box_shadow = box_shadow_spec && box_shadow_spec->inset; + + if (node->border_width[ST_SIDE_TOP] > 0 || + node->border_width[ST_SIDE_LEFT] > 0 || + node->border_width[ST_SIDE_RIGHT] > 0 || + node->border_width[ST_SIDE_BOTTOM] > 0) + has_border = TRUE; + else + has_border = FALSE; + + if (node->border_radius[ST_CORNER_TOPLEFT] > 0 || + node->border_radius[ST_CORNER_TOPRIGHT] > 0 || + node->border_radius[ST_CORNER_BOTTOMLEFT] > 0 || + node->border_radius[ST_CORNER_BOTTOMRIGHT] > 0) + has_border_radius = TRUE; + else + has_border_radius = FALSE; + + /* The cogl code pads each corner to the maximum border radius, + * which results in overlapping corner areas if the radius + * exceeds the actor's halfsize, causing rendering errors. + * Fall back to cairo in these cases. */ + has_large_corners = FALSE; + + if (has_border_radius) { + guint border_radius[4]; + int corner; + + st_theme_node_reduce_border_radius (node, width, height, border_radius); + + for (corner = 0; corner < 4; corner ++) { + if (border_radius[corner] * 2 > height || + border_radius[corner] * 2 > width) { + has_large_corners = TRUE; + break; + } + } + } + + state->corner_material[ST_CORNER_TOPLEFT] = + st_theme_node_lookup_corner (node, width, height, resource_scale, ST_CORNER_TOPLEFT); + state->corner_material[ST_CORNER_TOPRIGHT] = + st_theme_node_lookup_corner (node, width, height, resource_scale, ST_CORNER_TOPRIGHT); + state->corner_material[ST_CORNER_BOTTOMRIGHT] = + st_theme_node_lookup_corner (node, width, height, resource_scale, ST_CORNER_BOTTOMRIGHT); + state->corner_material[ST_CORNER_BOTTOMLEFT] = + st_theme_node_lookup_corner (node, width, height, resource_scale, ST_CORNER_BOTTOMLEFT); + + /* Use cairo to prerender the node if there is a gradient, or + * background image with borders and/or rounded corners, + * or large corners, since we can't do those things + * easily with cogl. + * + * FIXME: if we could figure out ahead of time that a + * background image won't overlap with the node borders, + * then we could use cogl for that case. + */ + if ((node->background_gradient_type != ST_GRADIENT_NONE) + || (has_inset_box_shadow && (has_border || node->background_color.alpha > 0)) + || (st_theme_node_get_background_image (node) && (has_border || has_border_radius)) + || has_large_corners) + state->prerendered_texture = st_theme_node_prerender_background (node, width, height, + resource_scale); + + if (state->prerendered_texture) + state->prerendered_pipeline = _st_create_texture_pipeline (state->prerendered_texture); + else + state->prerendered_pipeline = NULL; + + if (box_shadow_spec && !has_inset_box_shadow) + { + st_theme_node_compute_maximum_borders (state); + + if (st_theme_node_load_border_image (node, resource_scale)) + state->box_shadow_pipeline = _st_create_shadow_pipeline (box_shadow_spec, + node->border_slices_texture, + state->resource_scale); + else if (state->prerendered_texture != NULL) + state->box_shadow_pipeline = _st_create_shadow_pipeline (box_shadow_spec, + state->prerendered_texture, + state->resource_scale); + else + st_theme_node_prerender_shadow (state); + } + + /* If we don't have cached textures yet, check whether we can cache + them. */ + if (!node->cached_textures) + { + if (state->prerendered_pipeline == NULL && + width >= node->box_shadow_min_width && + height >= node->box_shadow_min_height) + { + st_theme_node_paint_state_copy (&node->cached_state, state); + node->cached_textures = TRUE; + } + } +} + +static void +st_theme_node_update_resources (StThemeNodePaintState *state, + StThemeNode *node, + float width, + float height, + float resource_scale) +{ + gboolean had_prerendered_texture = FALSE; + gboolean had_box_shadow = FALSE; + StShadow *box_shadow_spec; + + g_return_if_fail (width > 0 && height > 0); + + /* Free handles we can't reuse */ + had_prerendered_texture = (state->prerendered_texture != NULL); + cogl_clear_object (&state->prerendered_texture); + + if (state->prerendered_pipeline != NULL) + { + cogl_clear_object (&state->prerendered_pipeline); + + if (node->border_slices_texture == NULL && + state->box_shadow_pipeline != NULL) + { + cogl_clear_object (&state->box_shadow_pipeline); + had_box_shadow = TRUE; + } + } + + st_theme_node_paint_state_set_node (state, node); + state->alloc_width = width; + state->alloc_height = height; + state->resource_scale = resource_scale; + + box_shadow_spec = st_theme_node_get_box_shadow (node); + + if (had_prerendered_texture) + { + state->prerendered_texture = st_theme_node_prerender_background (node, width, height, resource_scale); + state->prerendered_pipeline = _st_create_texture_pipeline (state->prerendered_texture); + } + else + { + int corner_id; + + for (corner_id = 0; corner_id < 4; corner_id++) + if (state->corner_material[corner_id] == NULL) + state->corner_material[corner_id] = + st_theme_node_lookup_corner (node, width, height, resource_scale, corner_id); + } + + if (had_box_shadow) + state->box_shadow_pipeline = _st_create_shadow_pipeline (box_shadow_spec, + state->prerendered_texture, + state->resource_scale); +} + +static void +paint_material_with_opacity (CoglPipeline *material, + CoglFramebuffer *framebuffer, + ClutterActorBox *box, + ClutterActorBox *coords, + guint8 paint_opacity) +{ + cogl_pipeline_set_color4ub (material, + paint_opacity, paint_opacity, paint_opacity, paint_opacity); + + if (coords) + cogl_framebuffer_draw_textured_rectangle (framebuffer, material, + box->x1, box->y1, box->x2, box->y2, + coords->x1, coords->y1, coords->x2, coords->y2); + else + cogl_framebuffer_draw_rectangle (framebuffer, material, + box->x1, box->y1, box->x2, box->y2); +} + +static void +st_theme_node_ensure_color_pipeline (StThemeNode *node) +{ + static CoglPipeline *color_pipeline_template = NULL; + + if (node->color_pipeline != NULL) + return; + + if (G_UNLIKELY (color_pipeline_template == NULL)) + { + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + color_pipeline_template = cogl_pipeline_new (ctx); + } + + node->color_pipeline = cogl_pipeline_copy (color_pipeline_template); +} + +static void +st_theme_node_paint_borders (StThemeNodePaintState *state, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + StPaintBordersMode mode, + guint8 paint_opacity) +{ + StThemeNode *node = state->node; + float width, height; + guint border_width[4]; + guint border_radius[4]; + guint max_border_radius = 0; + guint max_width_radius[4]; + int corner_id, side_id; + ClutterColor border_color; + guint8 alpha; + gboolean corners_are_transparent; + + width = box->x2 - box->x1; + height = box->y2 - box->y1; + + /* TODO - support non-uniform border colors */ + get_arbitrary_border_color (node, &border_color); + + for (side_id = 0; side_id < 4; side_id++) + border_width[side_id] = st_theme_node_get_border_width(node, side_id); + + st_theme_node_reduce_border_radius (node, width, height, border_radius); + + for (corner_id = 0; corner_id < 4; corner_id++) + { + guint border_width_1, border_width_2; + + st_theme_node_get_corner_border_widths (node, corner_id, + &border_width_1, &border_width_2); + + if (border_radius[corner_id] > max_border_radius) + max_border_radius = border_radius[corner_id]; + max_width_radius[corner_id] = MAX(MAX(border_width_1, border_width_2), + border_radius[corner_id]); + } + + /* borders */ + if (border_width[ST_SIDE_TOP] > 0 || + border_width[ST_SIDE_RIGHT] > 0 || + border_width[ST_SIDE_BOTTOM] > 0 || + border_width[ST_SIDE_LEFT] > 0) + { + ClutterColor effective_border; + gboolean skip_corner_1, skip_corner_2; + float rects[16]; + + over (&border_color, &node->background_color, &effective_border); + alpha = paint_opacity * effective_border.alpha / 255; + + if (alpha > 0) + { + st_theme_node_ensure_color_pipeline (node); + cogl_pipeline_set_color4ub (node->color_pipeline, + effective_border.red * alpha / 255, + effective_border.green * alpha / 255, + effective_border.blue * alpha / 255, + alpha); + + /* NORTH */ + skip_corner_1 = border_radius[ST_CORNER_TOPLEFT] > 0; + skip_corner_2 = border_radius[ST_CORNER_TOPRIGHT] > 0; + + rects[0] = skip_corner_1 ? max_width_radius[ST_CORNER_TOPLEFT] : 0; + rects[1] = 0; + rects[2] = skip_corner_2 ? width - max_width_radius[ST_CORNER_TOPRIGHT] : width; + rects[3] = border_width[ST_SIDE_TOP]; + + /* EAST */ + skip_corner_1 = border_radius[ST_CORNER_TOPRIGHT] > 0; + skip_corner_2 = border_radius[ST_CORNER_BOTTOMRIGHT] > 0; + + rects[4] = width - border_width[ST_SIDE_RIGHT]; + rects[5] = skip_corner_1 ? max_width_radius[ST_CORNER_TOPRIGHT] + : border_width[ST_SIDE_TOP]; + rects[6] = width; + rects[7] = skip_corner_2 ? height - max_width_radius[ST_CORNER_BOTTOMRIGHT] + : height - border_width[ST_SIDE_BOTTOM]; + + /* SOUTH */ + skip_corner_1 = border_radius[ST_CORNER_BOTTOMLEFT] > 0; + skip_corner_2 = border_radius[ST_CORNER_BOTTOMRIGHT] > 0; + + rects[8] = skip_corner_1 ? max_width_radius[ST_CORNER_BOTTOMLEFT] : 0; + rects[9] = height - border_width[ST_SIDE_BOTTOM]; + rects[10] = skip_corner_2 ? width - max_width_radius[ST_CORNER_BOTTOMRIGHT] + : width; + rects[11] = height; + + /* WEST */ + skip_corner_1 = border_radius[ST_CORNER_TOPLEFT] > 0; + skip_corner_2 = border_radius[ST_CORNER_BOTTOMLEFT] > 0; + + rects[12] = 0; + rects[13] = skip_corner_1 ? max_width_radius[ST_CORNER_TOPLEFT] + : border_width[ST_SIDE_TOP]; + rects[14] = border_width[ST_SIDE_LEFT]; + rects[15] = skip_corner_2 ? height - max_width_radius[ST_CORNER_BOTTOMLEFT] + : height - border_width[ST_SIDE_BOTTOM]; + + cogl_framebuffer_draw_rectangles (framebuffer, + node->color_pipeline, + rects, 4); + } + } + + corners_are_transparent = mode == ST_PAINT_BORDERS_MODE_COLOR && + node->background_color.alpha == 0 && + node->border_color[0].alpha == 0; + + /* corners */ + if (max_border_radius > 0 && paint_opacity > 0 && !corners_are_transparent) + { + for (corner_id = 0; corner_id < 4; corner_id++) + { + if (state->corner_material[corner_id] == NULL) + continue; + + cogl_pipeline_set_color4ub (state->corner_material[corner_id], + paint_opacity, paint_opacity, + paint_opacity, paint_opacity); + + switch (corner_id) + { + case ST_CORNER_TOPLEFT: + cogl_framebuffer_draw_textured_rectangle (framebuffer, + state->corner_material[corner_id], 0, 0, + max_width_radius[ST_CORNER_TOPLEFT], max_width_radius[ST_CORNER_TOPLEFT], + 0, 0, 0.5, 0.5); + break; + case ST_CORNER_TOPRIGHT: + cogl_framebuffer_draw_textured_rectangle (framebuffer, + state->corner_material[corner_id], + width - max_width_radius[ST_CORNER_TOPRIGHT], 0, + width, max_width_radius[ST_CORNER_TOPRIGHT], + 0.5, 0, 1, 0.5); + break; + case ST_CORNER_BOTTOMRIGHT: + cogl_framebuffer_draw_textured_rectangle (framebuffer, + state->corner_material[corner_id], + width - max_width_radius[ST_CORNER_BOTTOMRIGHT], + height - max_width_radius[ST_CORNER_BOTTOMRIGHT], + width, height, + 0.5, 0.5, 1, 1); + break; + case ST_CORNER_BOTTOMLEFT: + cogl_framebuffer_draw_textured_rectangle (framebuffer, + state->corner_material[corner_id], + 0, height - max_width_radius[ST_CORNER_BOTTOMLEFT], + max_width_radius[ST_CORNER_BOTTOMLEFT], height, + 0, 0.5, 0.5, 1); + break; + default: + g_assert_not_reached(); + break; + } + } + } + + /* background color */ + alpha = mode == ST_PAINT_BORDERS_MODE_SILHOUETTE ? + 255 : + paint_opacity * node->background_color.alpha / 255; + if (alpha > 0) + { + st_theme_node_ensure_color_pipeline (node); + cogl_pipeline_set_color4ub (node->color_pipeline, + node->background_color.red * alpha / 255, + node->background_color.green * alpha / 255, + node->background_color.blue * alpha / 255, + alpha); + + /* We add padding to each corner, so that all corners end up as if they + * had a border-radius of max_border_radius, which allows us to treat + * corners as uniform further on. + */ + for (corner_id = 0; corner_id < 4; corner_id++) + { + float verts[8]; + int n_rects; + + /* corner texture does not need padding */ + if (max_border_radius == border_radius[corner_id]) + continue; + + n_rects = border_radius[corner_id] == 0 ? 1 : 2; + + switch (corner_id) + { + case ST_CORNER_TOPLEFT: + verts[0] = border_width[ST_SIDE_LEFT]; + verts[1] = MAX(border_radius[corner_id], + border_width[ST_SIDE_TOP]); + verts[2] = max_border_radius; + verts[3] = max_border_radius; + if (n_rects == 2) + { + verts[4] = MAX(border_radius[corner_id], + border_width[ST_SIDE_LEFT]); + verts[5] = border_width[ST_SIDE_TOP]; + verts[6] = max_border_radius; + verts[7] = MAX(border_radius[corner_id], + border_width[ST_SIDE_TOP]); + } + break; + case ST_CORNER_TOPRIGHT: + verts[0] = width - max_border_radius; + verts[1] = MAX(border_radius[corner_id], + border_width[ST_SIDE_TOP]); + verts[2] = width - border_width[ST_SIDE_RIGHT]; + verts[3] = max_border_radius; + if (n_rects == 2) + { + verts[4] = width - max_border_radius; + verts[5] = border_width[ST_SIDE_TOP]; + verts[6] = width - MAX(border_radius[corner_id], + border_width[ST_SIDE_RIGHT]); + verts[7] = MAX(border_radius[corner_id], + border_width[ST_SIDE_TOP]); + } + break; + case ST_CORNER_BOTTOMRIGHT: + verts[0] = width - max_border_radius; + verts[1] = height - max_border_radius; + verts[2] = width - border_width[ST_SIDE_RIGHT]; + verts[3] = height - MAX(border_radius[corner_id], + border_width[ST_SIDE_BOTTOM]); + if (n_rects == 2) + { + verts[4] = width - max_border_radius; + verts[5] = height - MAX(border_radius[corner_id], + border_width[ST_SIDE_BOTTOM]); + verts[6] = width - MAX(border_radius[corner_id], + border_width[ST_SIDE_RIGHT]); + verts[7] = height - border_width[ST_SIDE_BOTTOM]; + } + break; + case ST_CORNER_BOTTOMLEFT: + verts[0] = border_width[ST_SIDE_LEFT]; + verts[1] = height - max_border_radius; + verts[2] = max_border_radius; + verts[3] = height - MAX(border_radius[corner_id], + border_width[ST_SIDE_BOTTOM]); + if (n_rects == 2) + { + verts[4] = MAX(border_radius[corner_id], + border_width[ST_SIDE_LEFT]); + verts[5] = height - MAX(border_radius[corner_id], + border_width[ST_SIDE_BOTTOM]); + verts[6] = max_border_radius; + verts[7] = height - border_width[ST_SIDE_BOTTOM]; + } + break; + default: + g_assert_not_reached(); + break; + } + cogl_framebuffer_draw_rectangles (framebuffer, + node->color_pipeline, + verts, n_rects); + } + + /* Once we've drawn the borders and corners, if the corners are bigger + * then the border width, the remaining area is shaped like + * + * ######## + * ########## + * ########## + * ######## + * + * We draw it in at most 3 pieces - first the top and bottom if + * necessary, then the main rectangle + */ + if (max_border_radius > border_width[ST_SIDE_TOP]) + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + MAX(max_border_radius, border_width[ST_SIDE_LEFT]), + border_width[ST_SIDE_TOP], + width - MAX(max_border_radius, border_width[ST_SIDE_RIGHT]), + max_border_radius); + if (max_border_radius > border_width[ST_SIDE_BOTTOM]) + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + MAX(max_border_radius, border_width[ST_SIDE_LEFT]), + height - max_border_radius, + width - MAX(max_border_radius, border_width[ST_SIDE_RIGHT]), + height - border_width[ST_SIDE_BOTTOM]); + + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + border_width[ST_SIDE_LEFT], + MAX(border_width[ST_SIDE_TOP], max_border_radius), + width - border_width[ST_SIDE_RIGHT], + height - MAX(border_width[ST_SIDE_BOTTOM], max_border_radius)); + } +} + +static void +st_theme_node_paint_sliced_shadow (StThemeNodePaintState *state, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + guint8 paint_opacity) +{ + StThemeNode *node = state->node; + guint border_radius[4]; + CoglColor color; + StShadow *box_shadow_spec; + gfloat xoffset, yoffset; + gfloat width, height; + gfloat shadow_width, shadow_height; + gfloat xend, yend, top, bottom, left, right; + gfloat s_top, s_bottom, s_left, s_right; + gfloat shadow_blur_radius, x_spread_factor, y_spread_factor; + float rectangles[8 * 9]; + gint idx; + ClutterColor background_color; + static const ClutterColor invisible_occluded = {3, 2, 1, 0}; + + if (paint_opacity == 0) + return; + + st_theme_node_reduce_border_radius (node, box->x2 - box->x1, box->y2 - box->y1, border_radius); + + box_shadow_spec = st_theme_node_get_box_shadow (node); + + /* Compute input & output areas : + * + * yoffset ---------------------------- + * | | | | + * | | | | + * | | | | + * top ---------------------------- + * | | | | + * | | | | + * | | | | + * bottom ---------------------------- + * | | | | + * | | | | + * | | | | + * yend ---------------------------- + * xoffset left right xend + * + * s_top = top in offscreen's coordinates (0.0 - 1.0) + * s_bottom = bottom in offscreen's coordinates (0.0 - 1.0) + * s_left = left in offscreen's coordinates (0.0 - 1.0) + * s_right = right in offscreen's coordinates (0.0 - 1.0) + */ + if (box_shadow_spec->blur == 0) + shadow_blur_radius = 0; + else + shadow_blur_radius = ceilf (1.5 * box_shadow_spec->blur / 2.0) * 2.0; + + shadow_width = state->box_shadow_width + 2 * shadow_blur_radius; + shadow_height = state->box_shadow_height + 2 * shadow_blur_radius; + + /* Compute input regions parameters */ + s_top = shadow_blur_radius + box_shadow_spec->blur + + MAX (node->border_radius[ST_CORNER_TOPLEFT], + node->border_radius[ST_CORNER_TOPRIGHT]); + s_bottom = shadow_blur_radius + box_shadow_spec->blur + + MAX (node->border_radius[ST_CORNER_BOTTOMLEFT], + node->border_radius[ST_CORNER_BOTTOMRIGHT]); + s_left = shadow_blur_radius + box_shadow_spec->blur + + MAX (node->border_radius[ST_CORNER_TOPLEFT], + node->border_radius[ST_CORNER_BOTTOMLEFT]); + s_right = shadow_blur_radius + box_shadow_spec->blur + + MAX (node->border_radius[ST_CORNER_TOPRIGHT], + node->border_radius[ST_CORNER_BOTTOMRIGHT]); + + /* Compute output regions parameters */ + xoffset = box->x1 + box_shadow_spec->xoffset - shadow_blur_radius - box_shadow_spec->spread; + yoffset = box->y1 + box_shadow_spec->yoffset - shadow_blur_radius - box_shadow_spec->spread; + width = box->x2 - box->x1 + 2 * shadow_blur_radius; + height = box->y2 - box->y1 + 2 * shadow_blur_radius; + + x_spread_factor = (width + 2 * box_shadow_spec->spread) / width; + y_spread_factor = (height + 2 * box_shadow_spec->spread) / height; + + width += 2 * box_shadow_spec->spread; + height += 2 * box_shadow_spec->spread; + + xend = xoffset + width; + yend = yoffset + height; + + top = s_top * y_spread_factor; + bottom = s_bottom * y_spread_factor; + left = s_left * x_spread_factor; + right = s_right * x_spread_factor; + + bottom = height - bottom; + right = width - right; + + /* Final adjustments */ + s_top /= shadow_height; + s_bottom /= shadow_height; + s_left /= shadow_width; + s_right /= shadow_width; + + s_bottom = 1.0 - s_bottom; + s_right = 1.0 - s_right; + + top += yoffset; + bottom += yoffset; + left += xoffset; + right += xoffset; + + /* Setup pipeline */ + cogl_color_init_from_4ub (&color, + box_shadow_spec->color.red * paint_opacity / 255, + box_shadow_spec->color.green * paint_opacity / 255, + box_shadow_spec->color.blue * paint_opacity / 255, + box_shadow_spec->color.alpha * paint_opacity / 255); + cogl_color_premultiply (&color); + + cogl_pipeline_set_layer_combine_constant (state->box_shadow_pipeline, 0, &color); + + idx = 0; + + if (yoffset < top) + { + if (xoffset < left) + { + /* Top left corner */ + rectangles[idx++] = xoffset; + rectangles[idx++] = yoffset; + rectangles[idx++] = left; + rectangles[idx++] = top; + + rectangles[idx++] = 0; + rectangles[idx++] = 0; + rectangles[idx++] = s_left; + rectangles[idx++] = s_top; + } + + /* Top middle */ + rectangles[idx++] = left; + rectangles[idx++] = yoffset; + rectangles[idx++] = right; + rectangles[idx++] = top; + + rectangles[idx++] = s_left; + rectangles[idx++] = 0; + rectangles[idx++] = s_right; + rectangles[idx++] = s_top; + + if (xend > right) + { + /* Top right corner */ + rectangles[idx++] = right; + rectangles[idx++] = yoffset; + rectangles[idx++] = xend; + rectangles[idx++] = top; + + rectangles[idx++] = s_right; + rectangles[idx++] = 0; + rectangles[idx++] = 1; + rectangles[idx++] = s_top; + } + } + + if (xoffset < left) + { + /* Left middle */ + rectangles[idx++] = xoffset; + rectangles[idx++] = top; + rectangles[idx++] = left; + rectangles[idx++] = bottom; + + rectangles[idx++] = 0; + rectangles[idx++] = s_top; + rectangles[idx++] = s_left; + rectangles[idx++] = s_bottom; + } + + /* Center middle is not definitely occluded? */ + st_theme_node_get_background_color (node, &background_color); + if (!clutter_color_equal (&background_color, &invisible_occluded) || + paint_opacity < 255 || + xoffset > shadow_blur_radius || left < 0 || + yoffset > shadow_blur_radius || top < 0) + { + rectangles[idx++] = left; + rectangles[idx++] = top; + rectangles[idx++] = right; + rectangles[idx++] = bottom; + + rectangles[idx++] = s_left; + rectangles[idx++] = s_top; + rectangles[idx++] = s_right; + rectangles[idx++] = s_bottom; + } + + if (xend > right) + { + /* Right middle */ + rectangles[idx++] = right; + rectangles[idx++] = top; + rectangles[idx++] = xend; + rectangles[idx++] = bottom; + + rectangles[idx++] = s_right; + rectangles[idx++] = s_top; + rectangles[idx++] = 1; + rectangles[idx++] = s_bottom; + } + + if (yend > bottom) + { + if (xoffset < left) + { + /* Bottom left corner */ + rectangles[idx++] = xoffset; + rectangles[idx++] = bottom; + rectangles[idx++] = left; + rectangles[idx++] = yend; + + rectangles[idx++] = 0; + rectangles[idx++] = s_bottom; + rectangles[idx++] = s_left; + rectangles[idx++] = 1; + } + + /* Bottom middle */ + rectangles[idx++] = left; + rectangles[idx++] = bottom; + rectangles[idx++] = right; + rectangles[idx++] = yend; + + rectangles[idx++] = s_left; + rectangles[idx++] = s_bottom; + rectangles[idx++] = s_right; + rectangles[idx++] = 1; + + if (xend > right) + { + /* Bottom right corner */ + rectangles[idx++] = right; + rectangles[idx++] = bottom; + rectangles[idx++] = xend; + rectangles[idx++] = yend; + + rectangles[idx++] = s_right; + rectangles[idx++] = s_bottom; + rectangles[idx++] = 1; + rectangles[idx++] = 1; + } + } + + cogl_framebuffer_draw_textured_rectangles (framebuffer, state->box_shadow_pipeline, + rectangles, idx / 8); + +#if 0 + /* Visual feedback on shadow's 9-slice and original offscreen buffer, + for debug purposes */ + cogl_framebuffer_draw_rectangle (framebuffer, state->box_shadow_pipeline, + xend, yoffset, xend + shadow_width, yoffset + shadow_height); + + st_theme_node_ensure_color_pipeline (node); + cogl_pipeline_set_color4ub (node->color_pipeline, 0xff, 0x0, 0x0, 0xff); + + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xoffset, top, xend, top + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xoffset, bottom, xend, bottom + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + left, yoffset, left + 1, yend); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + right, yoffset, right + 1, yend); + + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend, yoffset, xend + shadow_width, yoffset + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend, yoffset + shadow_height, xend + shadow_width, yoffset + shadow_height + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend, yoffset, xend + 1, yoffset + shadow_height); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend + shadow_width, yoffset, xend + shadow_width + 1, yoffset + shadow_height); + + s_top *= shadow_height; + s_bottom *= shadow_height; + s_left *= shadow_width; + s_right *= shadow_width; + + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend, yoffset + s_top, xend + shadow_width, yoffset + s_top + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend, yoffset + s_bottom, xend + shadow_width, yoffset + s_bottom + 1); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend + s_left, yoffset, xend + s_left + 1, yoffset + shadow_height); + cogl_framebuffer_draw_rectangle (framebuffer, node->color_pipeline, + xend + s_right, yoffset, xend + s_right + 1, yoffset + shadow_height); + +#endif +} + +static void +st_theme_node_prerender_shadow (StThemeNodePaintState *state) +{ + StThemeNode *node = state->node; + CoglContext *ctx; + int fb_width, fb_height; + CoglTexture *buffer; + CoglOffscreen *offscreen = NULL; + CoglFramebuffer *framebuffer; + GError *error = NULL; + + ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + /* Render offscreen */ + fb_width = ceilf (state->box_shadow_width * state->resource_scale); + fb_height = ceilf (state->box_shadow_height * state->resource_scale); + buffer = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, fb_width, fb_height)); + if (buffer == NULL) + return; + + offscreen = cogl_offscreen_new_with_texture (buffer); + framebuffer = COGL_FRAMEBUFFER (offscreen); + + if (cogl_framebuffer_allocate (framebuffer, &error)) + { + ClutterActorBox box = { 0, 0, state->box_shadow_width, state->box_shadow_height}; + + cogl_framebuffer_orthographic (framebuffer, 0, 0, + fb_width, fb_height, 0, 1.0); + cogl_framebuffer_scale (framebuffer, + state->resource_scale, + state->resource_scale, 1); + cogl_framebuffer_clear4f (framebuffer, COGL_BUFFER_BIT_COLOR, 0, 0, 0, 0); + + st_theme_node_paint_borders (state, framebuffer, &box, ST_PAINT_BORDERS_MODE_SILHOUETTE, 0xFF); + + state->box_shadow_pipeline = _st_create_shadow_pipeline (st_theme_node_get_box_shadow (node), + buffer, state->resource_scale); + } + + g_clear_error (&error); + g_clear_object (&offscreen); + cogl_clear_object (&buffer); +} + +static void +st_theme_node_compute_maximum_borders (StThemeNodePaintState *state) +{ + int max_borders[4], center_radius; + StThemeNode * node = state->node; + + /* Compute maximum borders sizes */ + max_borders[ST_SIDE_TOP] = MAX (node->border_radius[ST_CORNER_TOPLEFT], + node->border_radius[ST_CORNER_TOPRIGHT]); + max_borders[ST_SIDE_BOTTOM] = MAX (node->border_radius[ST_CORNER_BOTTOMLEFT], + node->border_radius[ST_CORNER_BOTTOMRIGHT]); + max_borders[ST_SIDE_LEFT] = MAX (node->border_radius[ST_CORNER_TOPLEFT], + node->border_radius[ST_CORNER_BOTTOMLEFT]); + max_borders[ST_SIDE_RIGHT] = MAX (node->border_radius[ST_CORNER_TOPRIGHT], + node->border_radius[ST_CORNER_BOTTOMRIGHT]); + + center_radius = (node->box_shadow->blur > 0) ? (2 * node->box_shadow->blur + 1) : 1; + + node->box_shadow_min_width = max_borders[ST_SIDE_LEFT] + max_borders[ST_SIDE_RIGHT] + center_radius; + node->box_shadow_min_height = max_borders[ST_SIDE_TOP] + max_borders[ST_SIDE_BOTTOM] + center_radius; + if (state->alloc_width < node->box_shadow_min_width || + state->alloc_height < node->box_shadow_min_height) + { + state->box_shadow_width = state->alloc_width; + state->box_shadow_height = state->alloc_height; + } + else + { + state->box_shadow_width = node->box_shadow_min_width; + state->box_shadow_height = node->box_shadow_min_height; + } +} + +static void +st_theme_node_paint_sliced_border_image (StThemeNode *node, + CoglFramebuffer *framebuffer, + float width, + float height, + guint8 paint_opacity) +{ + gfloat ex, ey; + gfloat tx1, ty1, tx2, ty2; + gint border_left, border_right, border_top, border_bottom; + float img_width, img_height; + StBorderImage *border_image; + CoglPipeline *pipeline; + + border_image = st_theme_node_get_border_image (node); + g_assert (border_image != NULL); + + st_border_image_get_borders (border_image, + &border_left, &border_right, &border_top, &border_bottom); + + img_width = cogl_texture_get_width (node->border_slices_texture); + img_height = cogl_texture_get_height (node->border_slices_texture); + + tx1 = border_left / img_width; + tx2 = (img_width - border_right) / img_width; + ty1 = border_top / img_height; + ty2 = (img_height - border_bottom) / img_height; + + ex = width - border_right; + if (ex < 0) + ex = border_right; /* FIXME ? */ + + ey = height - border_bottom; + if (ey < 0) + ey = border_bottom; /* FIXME ? */ + + pipeline = node->border_slices_pipeline; + cogl_pipeline_set_color4ub (pipeline, + paint_opacity, paint_opacity, paint_opacity, paint_opacity); + + { + float rectangles[] = + { + /* top left corner */ + 0, 0, border_left, border_top, + 0.0, 0.0, + tx1, ty1, + + /* top middle */ + border_left, 0, ex, border_top, + tx1, 0.0, + tx2, ty1, + + /* top right */ + ex, 0, width, border_top, + tx2, 0.0, + 1.0, ty1, + + /* mid left */ + 0, border_top, border_left, ey, + 0.0, ty1, + tx1, ty2, + + /* center */ + border_left, border_top, ex, ey, + tx1, ty1, + tx2, ty2, + + /* mid right */ + ex, border_top, width, ey, + tx2, ty1, + 1.0, ty2, + + /* bottom left */ + 0, ey, border_left, height, + 0.0, ty2, + tx1, 1.0, + + /* bottom center */ + border_left, ey, ex, height, + tx1, ty2, + tx2, 1.0, + + /* bottom right */ + ex, ey, width, height, + tx2, ty2, + 1.0, 1.0 + }; + + cogl_framebuffer_draw_textured_rectangles (framebuffer, pipeline, rectangles, 9); + } +} + +static void +st_theme_node_paint_outline (StThemeNode *node, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + guint8 paint_opacity) + +{ + float width, height; + int outline_width; + float rects[16]; + ClutterColor outline_color, effective_outline; + guint8 alpha; + + width = box->x2 - box->x1; + height = box->y2 - box->y1; + + outline_width = st_theme_node_get_outline_width (node); + if (outline_width == 0) + return; + + st_theme_node_get_outline_color (node, &outline_color); + over (&outline_color, &node->background_color, &effective_outline); + + alpha = paint_opacity * outline_color.alpha / 255; + + st_theme_node_ensure_color_pipeline (node); + cogl_pipeline_set_color4ub (node->color_pipeline, + effective_outline.red * alpha / 255, + effective_outline.green * alpha / 255, + effective_outline.blue * alpha / 255, + alpha); + + /* The outline is drawn just outside the border, which means just + * outside the allocation box. This means that in some situations + * involving clip_to_allocation or the screen edges, you won't be + * able to see the outline. In practice, it works well enough. + */ + + /* NORTH */ + rects[0] = -outline_width; + rects[1] = -outline_width; + rects[2] = width + outline_width; + rects[3] = 0; + + /* EAST */ + rects[4] = width; + rects[5] = 0; + rects[6] = width + outline_width; + rects[7] = height; + + /* SOUTH */ + rects[8] = -outline_width; + rects[9] = height; + rects[10] = width + outline_width; + rects[11] = height + outline_width; + + /* WEST */ + rects[12] = -outline_width; + rects[13] = 0; + rects[14] = 0; + rects[15] = height; + + cogl_framebuffer_draw_rectangles (framebuffer, node->color_pipeline, rects, 4); +} + +static gboolean +st_theme_node_needs_new_box_shadow_for_size (StThemeNodePaintState *state, + StThemeNode *node, + float width, + float height, + float resource_scale) +{ + if (!node->rendered_once) + return TRUE; + + /* The resource scale changed, so need to recompute a new box-shadow */ + if (fabsf (state->resource_scale - resource_scale) > FLT_EPSILON) + return TRUE; + + /* The allocation hasn't changed, no need to recompute a new + box-shadow. */ + if (state->alloc_width == width && + state->alloc_height == height) + return FALSE; + + /* If there is no shadow, no need to recompute a new box-shadow. */ + if (node->box_shadow_min_width == 0 || + node->box_shadow_min_height == 0) + return FALSE; + + /* If the new size is inferior to the box-shadow minimum size (we + already know the size has changed), we need to recompute the + box-shadow. */ + if (width < node->box_shadow_min_width || + height < node->box_shadow_min_height) + return TRUE; + + /* Now checking whether the size of the node has crossed the minimum + box-shadow size boundary, from below to above the minimum size . + If that's the case, we need to recompute the box-shadow */ + if (state->alloc_width < node->box_shadow_min_width || + state->alloc_height < node->box_shadow_min_height) + return TRUE; + + return FALSE; +} + +void +st_theme_node_paint (StThemeNode *node, + StThemeNodePaintState *state, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + guint8 paint_opacity, + float resource_scale) +{ + float width, height; + ClutterActorBox allocation; + + /* Some things take an ActorBox, some things just width/height */ + width = box->x2 - box->x1; + height = box->y2 - box->y1; + allocation.x1 = allocation.y1 = 0; + allocation.x2 = width; + allocation.y2 = height; + + if (width <= 0 || height <= 0 || resource_scale <= 0.0f) + return; + + /* Check whether we need to recreate the textures of the paint + * state, either because : + * 1) the theme node associated to the paint state has changed + * 2) the allocation size change requires recreating textures + */ + if (state->node != node || + st_theme_node_needs_new_box_shadow_for_size (state, node, width, height, + resource_scale)) + { + /* If we had the ability to cache textures on the node, then we + can just copy them over to the paint state and avoid all + rendering. We end up sharing textures a cross different + widgets. */ + if (node->rendered_once && node->cached_textures && + width >= node->box_shadow_min_width && height >= node->box_shadow_min_height && + fabsf (resource_scale - state->resource_scale) < FLT_EPSILON) + st_theme_node_paint_state_copy (state, &node->cached_state); + else + st_theme_node_render_resources (state, node, width, height, resource_scale); + + node->rendered_once = TRUE; + } + else if (state->alloc_width != width || state->alloc_height != height || + fabsf (state->resource_scale - resource_scale) > FLT_EPSILON) + st_theme_node_update_resources (state, node, width, height, resource_scale); + + /* Rough notes about the relationship of borders and backgrounds in CSS3; + * see http://www.w3.org/TR/css3-background/ for more accurate details. + * + * - Things are drawn in 4 layers, from the bottom: + * Background color + * Background image + * Border color or border image + * Content + * - The background color, gradient and image extend to and are clipped by + * the edge of the border area, so will be rounded if the border is + * rounded. (CSS3 background-clip property modifies this) + * - The border image replaces what would normally be drawn by the border + * - The border image is not clipped by a rounded border-radius + * - The border radius rounds the background even if the border is + * zero width or a border image is being used. + * + * Deviations from the above as implemented here: + * - The combination of border image and a non-zero border radius is + * not supported; the background color will be drawn with square + * corners. + * - The background image is drawn above the border color, not below it. + * - We clip the background image to the inside edges of the border + * instead of the outside edges of the border (but position the image + * such that it's aligned to the outside edges) + */ + + if (state->box_shadow_pipeline) + { + if (state->alloc_width < node->box_shadow_min_width || + state->alloc_height < node->box_shadow_min_height) + _st_paint_shadow_with_opacity (node->box_shadow, + framebuffer, + state->box_shadow_pipeline, + &allocation, + paint_opacity); + else + st_theme_node_paint_sliced_shadow (state, + framebuffer, + &allocation, + paint_opacity); + } + + if (state->prerendered_pipeline != NULL || + st_theme_node_load_border_image (node, resource_scale)) + { + if (state->prerendered_pipeline != NULL) + { + ClutterActorBox paint_box; + + st_theme_node_get_background_paint_box (node, + &allocation, + &paint_box); + + paint_material_with_opacity (state->prerendered_pipeline, + framebuffer, + &paint_box, + NULL, + paint_opacity); + } + + if (node->border_slices_pipeline != NULL) + st_theme_node_paint_sliced_border_image (node, framebuffer, width, height, paint_opacity); + } + else + { + st_theme_node_paint_borders (state, framebuffer, box, ST_PAINT_BORDERS_MODE_COLOR, paint_opacity); + } + + st_theme_node_paint_outline (node, framebuffer, box, paint_opacity); + + if (state->prerendered_pipeline == NULL && + st_theme_node_load_background_image (node, resource_scale)) + { + ClutterActorBox background_box; + ClutterActorBox texture_coords; + gboolean has_visible_outline; + + /* If the node doesn't have an opaque or repeating background or + * a border then we let its background image shadows leak out, + * but otherwise we clip it. + */ + has_visible_outline = st_theme_node_has_visible_outline (node); + + get_background_position (node, &allocation, resource_scale, + &background_box, &texture_coords); + + if (has_visible_outline || node->background_repeat) + cogl_framebuffer_push_rectangle_clip (framebuffer, + allocation.x1, allocation.y1, + allocation.x2, allocation.y2); + + /* CSS based drop shadows + * + * Drop shadows in ST are modelled after the CSS3 box-shadow property; + * see http://www.css3.info/preview/box-shadow/ for a detailed description. + * + * While the syntax of the property is mostly identical - we do not support + * multiple shadows and allow for a more liberal placement of the color + * parameter - its interpretation defers significantly in that the shadow's + * shape is not determined by the bounding box, but by the CSS background + * image. The drop shadows are allowed to escape the nodes allocation if + * there is nothing (like a border, or the edge of the background color) + * to logically confine it. + */ + if (node->background_shadow_pipeline != NULL) + _st_paint_shadow_with_opacity (node->background_image_shadow, + framebuffer, + node->background_shadow_pipeline, + &background_box, + paint_opacity); + + paint_material_with_opacity (node->background_pipeline, + framebuffer, + &background_box, + &texture_coords, + paint_opacity); + + if (has_visible_outline || node->background_repeat) + cogl_framebuffer_pop_clip (framebuffer); + } +} + +static void +st_theme_node_paint_state_node_free_internal (StThemeNodePaintState *state, + gboolean unref_node) +{ + int corner_id; + + cogl_clear_object (&state->prerendered_texture); + cogl_clear_object (&state->prerendered_pipeline); + cogl_clear_object (&state->box_shadow_pipeline); + + for (corner_id = 0; corner_id < 4; corner_id++) + cogl_clear_object (&state->corner_material[corner_id]); + + if (unref_node) + st_theme_node_paint_state_set_node (state, NULL); + + st_theme_node_paint_state_init (state); +} + +static void +st_theme_node_paint_state_node_freed (StThemeNodePaintState *state) +{ + st_theme_node_paint_state_node_free_internal (state, FALSE); +} + +void +st_theme_node_paint_state_set_node (StThemeNodePaintState *state, StThemeNode *node) +{ + if (state->node) + g_object_weak_unref (G_OBJECT (state->node), + (GWeakNotify) st_theme_node_paint_state_node_freed, + state); + + state->node = node; + if (state->node) + g_object_weak_ref (G_OBJECT (state->node), + (GWeakNotify) st_theme_node_paint_state_node_freed, + state); +} + +void +st_theme_node_paint_state_free (StThemeNodePaintState *state) +{ + st_theme_node_paint_state_node_free_internal (state, TRUE); +} + +void +st_theme_node_paint_state_init (StThemeNodePaintState *state) +{ + int corner_id; + + state->alloc_width = 0; + state->alloc_height = 0; + state->resource_scale = -1; + state->node = NULL; + state->box_shadow_pipeline = NULL; + state->prerendered_texture = NULL; + state->prerendered_pipeline = NULL; + + for (corner_id = 0; corner_id < 4; corner_id++) + state->corner_material[corner_id] = NULL; +} + +void +st_theme_node_paint_state_copy (StThemeNodePaintState *state, + StThemeNodePaintState *other) +{ + int corner_id; + + if (state == other) + return; + + st_theme_node_paint_state_free (state); + + st_theme_node_paint_state_set_node (state, other->node); + + state->alloc_width = other->alloc_width; + state->alloc_height = other->alloc_height; + state->resource_scale = other->resource_scale; + state->box_shadow_width = other->box_shadow_width; + state->box_shadow_height = other->box_shadow_height; + + if (other->box_shadow_pipeline) + state->box_shadow_pipeline = cogl_object_ref (other->box_shadow_pipeline); + if (other->prerendered_texture) + state->prerendered_texture = cogl_object_ref (other->prerendered_texture); + if (other->prerendered_pipeline) + state->prerendered_pipeline = cogl_object_ref (other->prerendered_pipeline); + for (corner_id = 0; corner_id < 4; corner_id++) + if (other->corner_material[corner_id]) + state->corner_material[corner_id] = cogl_object_ref (other->corner_material[corner_id]); +} + +void +st_theme_node_paint_state_invalidate (StThemeNodePaintState *state) +{ + state->alloc_width = 0; + state->alloc_height = 0; + state->resource_scale = -1.0f; +} + +gboolean +st_theme_node_paint_state_invalidate_for_file (StThemeNodePaintState *state, + GFile *file) +{ + if (state->node != NULL && + st_theme_node_invalidate_resources_for_file (state->node, file)) + { + st_theme_node_paint_state_invalidate (state); + return TRUE; + } + + return FALSE; +} diff --git a/src/st/st-theme-node-private.h b/src/st/st-theme-node-private.h new file mode 100644 index 0000000..7533482 --- /dev/null +++ b/src/st/st-theme-node-private.h @@ -0,0 +1,131 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node-private.h: private structures and functions for StThemeNode + * + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2011 Quentin "Sardem FF7" Glidic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_THEME_NODE_PRIVATE_H__ +#define __ST_THEME_NODE_PRIVATE_H__ + +#include <gdk/gdk.h> + +#include "st-theme-node.h" +#include "croco/libcroco.h" +#include "st-types.h" + +G_BEGIN_DECLS + +struct _StThemeNode { + GObject parent; + + StThemeContext *context; + StThemeNode *parent_node; + StTheme *theme; + + PangoFontDescription *font_desc; + + ClutterColor background_color; + /* If gradient is set, then background_color is the gradient start */ + StGradientType background_gradient_type; + ClutterColor background_gradient_end; + + int background_position_x; + int background_position_y; + + StBackgroundSize background_size; + gint background_size_w; + gint background_size_h; + + ClutterColor foreground_color; + ClutterColor border_color[4]; + ClutterColor outline_color; + + int border_width[4]; + int border_radius[4]; + int outline_width; + guint padding[4]; + guint margin[4]; + + int width; + int height; + int min_width; + int min_height; + int max_width; + int max_height; + + int transition_duration; + + GFile *background_image; + StBorderImage *border_image; + StShadow *box_shadow; + StShadow *background_image_shadow; + StShadow *text_shadow; + StIconColors *icon_colors; + + GType element_type; + char *element_id; + GStrv element_classes; + GStrv pseudo_classes; + char *inline_style; + + CRDeclaration **properties; + int n_properties; + + /* We hold onto these separately so we can destroy them on finalize */ + CRDeclaration *inline_properties; + + guint background_position_set : 1; + guint background_repeat : 1; + + guint properties_computed : 1; + guint geometry_computed : 1; + guint background_computed : 1; + guint foreground_computed : 1; + guint border_image_computed : 1; + guint box_shadow_computed : 1; + guint background_image_shadow_computed : 1; + guint text_shadow_computed : 1; + guint link_type : 2; + guint rendered_once : 1; + guint cached_textures : 1; + + int box_shadow_min_width; + int box_shadow_min_height; + + guint stylesheets_changed_id; + + CoglPipeline *border_slices_texture; + CoglPipeline *border_slices_pipeline; + CoglPipeline *background_texture; + CoglPipeline *background_pipeline; + CoglPipeline *background_shadow_pipeline; + CoglPipeline *color_pipeline; + + StThemeNodePaintState cached_state; + + int cached_scale_factor; +}; + +void _st_theme_node_ensure_background (StThemeNode *node); +void _st_theme_node_ensure_geometry (StThemeNode *node); +void _st_theme_node_apply_margins (StThemeNode *node, + ClutterActor *actor); + +G_END_DECLS + +#endif /* __ST_THEME_NODE_PRIVATE_H__ */ diff --git a/src/st/st-theme-node-transition.c b/src/st/st-theme-node-transition.c new file mode 100644 index 0000000..20b1476 --- /dev/null +++ b/src/st/st-theme-node-transition.c @@ -0,0 +1,470 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node-transition.c: Theme node transitions for StWidget. + * + * Copyright 2010 Florian Müllner + * Copyright 2010 Adel Gadllah + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <math.h> + +#include "st-theme-node-transition.h" + +enum { + COMPLETED, + NEW_FRAME, + LAST_SIGNAL +}; + +typedef struct _StThemeNodeTransitionPrivate StThemeNodeTransitionPrivate; + +struct _StThemeNodeTransition { + GObject parent; + + StThemeNodeTransitionPrivate *priv; +}; + +struct _StThemeNodeTransitionPrivate { + StThemeNode *old_theme_node; + StThemeNode *new_theme_node; + + StThemeNodePaintState old_paint_state; + StThemeNodePaintState new_paint_state; + + CoglTexture *old_texture; + CoglTexture *new_texture; + + CoglFramebuffer *old_offscreen; + CoglFramebuffer *new_offscreen; + + CoglPipeline *material; + + ClutterTimeline *timeline; + + gulong timeline_completed_id; + gulong timeline_new_frame_id; + + ClutterActorBox last_allocation; + ClutterActorBox offscreen_box; + + gboolean needs_setup; +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +G_DEFINE_TYPE_WITH_PRIVATE (StThemeNodeTransition, st_theme_node_transition, G_TYPE_OBJECT); + + +static void +on_timeline_completed (ClutterTimeline *timeline, + StThemeNodeTransition *transition) +{ + g_signal_emit (transition, signals[COMPLETED], 0); +} + +static void +on_timeline_new_frame (ClutterTimeline *timeline, + gint frame_num, + StThemeNodeTransition *transition) +{ + g_signal_emit (transition, signals[NEW_FRAME], 0); +} + +StThemeNodeTransition * +st_theme_node_transition_new (ClutterActor *actor, + StThemeNode *from_node, + StThemeNode *to_node, + StThemeNodePaintState *old_paint_state, + unsigned int duration) +{ + StThemeNodeTransition *transition; + g_return_val_if_fail (ST_IS_THEME_NODE (from_node), NULL); + g_return_val_if_fail (ST_IS_THEME_NODE (to_node), NULL); + + duration = st_theme_node_get_transition_duration (to_node); + + transition = g_object_new (ST_TYPE_THEME_NODE_TRANSITION, NULL); + + transition->priv->old_theme_node = g_object_ref (from_node); + transition->priv->new_theme_node = g_object_ref (to_node); + + st_theme_node_paint_state_copy (&transition->priv->old_paint_state, + old_paint_state); + + transition->priv->timeline = clutter_timeline_new_for_actor (actor, duration); + + transition->priv->timeline_completed_id = + g_signal_connect (transition->priv->timeline, "completed", + G_CALLBACK (on_timeline_completed), transition); + transition->priv->timeline_new_frame_id = + g_signal_connect (transition->priv->timeline, "new-frame", + G_CALLBACK (on_timeline_new_frame), transition); + + clutter_timeline_set_progress_mode (transition->priv->timeline, CLUTTER_EASE_IN_OUT_QUAD); + + clutter_timeline_start (transition->priv->timeline); + + return transition; +} + +/** + * st_theme_node_transition_get_new_paint_state: (skip) + * + */ +StThemeNodePaintState * +st_theme_node_transition_get_new_paint_state (StThemeNodeTransition *transition) +{ + return &transition->priv->new_paint_state; +} + +void +st_theme_node_transition_update (StThemeNodeTransition *transition, + StThemeNode *new_node) +{ + StThemeNodeTransitionPrivate *priv; + StThemeNode *old_node; + ClutterTimelineDirection direction; + + g_return_if_fail (ST_IS_THEME_NODE_TRANSITION (transition)); + g_return_if_fail (ST_IS_THEME_NODE (new_node)); + + priv = transition->priv; + direction = clutter_timeline_get_direction (priv->timeline); + old_node = (direction == CLUTTER_TIMELINE_FORWARD) ? priv->old_theme_node + : priv->new_theme_node; + + /* If the update is the reversal of the current transition, + * we reverse the timeline. + * Otherwise, we should initiate a new transition from the + * current state to the new one; this is hard to do if the + * transition is in an intermediate state, so we just cancel + * the ongoing transition in that case. + * Note that reversing a timeline before any time elapsed + * results in the timeline's time position being set to the + * full duration - this is not what we want, so we cancel the + * transition as well in that case. + */ + if (st_theme_node_equal (new_node, old_node)) + { + { + StThemeNodePaintState tmp; + + st_theme_node_paint_state_init (&tmp); + st_theme_node_paint_state_copy (&tmp, &priv->old_paint_state); + st_theme_node_paint_state_copy (&priv->old_paint_state, &priv->new_paint_state); + st_theme_node_paint_state_copy (&priv->new_paint_state, &tmp); + st_theme_node_paint_state_free (&tmp); + } + + if (clutter_timeline_get_elapsed_time (priv->timeline) > 0) + { + if (direction == CLUTTER_TIMELINE_FORWARD) + clutter_timeline_set_direction (priv->timeline, + CLUTTER_TIMELINE_BACKWARD); + else + clutter_timeline_set_direction (priv->timeline, + CLUTTER_TIMELINE_FORWARD); + } + else + { + clutter_timeline_stop (priv->timeline); + g_signal_emit (transition, signals[COMPLETED], 0); + } + } + else + { + if (clutter_timeline_get_elapsed_time (priv->timeline) > 0) + { + clutter_timeline_stop (priv->timeline); + g_signal_emit (transition, signals[COMPLETED], 0); + } + else + { + guint new_duration = st_theme_node_get_transition_duration (new_node); + + clutter_timeline_set_duration (priv->timeline, new_duration); + + g_object_unref (priv->new_theme_node); + priv->new_theme_node = g_object_ref (new_node); + + st_theme_node_paint_state_invalidate (&priv->new_paint_state); + } + } +} + +static void +calculate_offscreen_box (StThemeNodeTransition *transition, + const ClutterActorBox *allocation) +{ + ClutterActorBox paint_box; + + st_theme_node_transition_get_paint_box (transition, + allocation, + &paint_box); + transition->priv->offscreen_box.x1 = paint_box.x1 - allocation->x1; + transition->priv->offscreen_box.y1 = paint_box.y1 - allocation->y1; + transition->priv->offscreen_box.x2 = paint_box.x2 - allocation->x1; + transition->priv->offscreen_box.y2 = paint_box.y2 - allocation->y1; +} + +void +st_theme_node_transition_get_paint_box (StThemeNodeTransition *transition, + const ClutterActorBox *allocation, + ClutterActorBox *paint_box) +{ + StThemeNodeTransitionPrivate *priv = transition->priv; + ClutterActorBox old_node_box, new_node_box; + + st_theme_node_get_paint_box (priv->old_theme_node, + allocation, + &old_node_box); + + st_theme_node_get_paint_box (priv->new_theme_node, + allocation, + &new_node_box); + + paint_box->x1 = MIN (old_node_box.x1, new_node_box.x1); + paint_box->y1 = MIN (old_node_box.y1, new_node_box.y1); + paint_box->x2 = MAX (old_node_box.x2, new_node_box.x2); + paint_box->y2 = MAX (old_node_box.y2, new_node_box.y2); +} + +static gboolean +setup_framebuffers (StThemeNodeTransition *transition, + const ClutterActorBox *allocation, + float resource_scale) +{ + StThemeNodeTransitionPrivate *priv = transition->priv; + CoglContext *ctx; + guint width, height; + GError *catch_error = NULL; + + /* template material to avoid unnecessary shader compilation */ + static CoglPipeline *material_template = NULL; + + ctx = clutter_backend_get_cogl_context (clutter_get_default_backend ()); + width = ceilf ((priv->offscreen_box.x2 - priv->offscreen_box.x1) * resource_scale); + height = ceilf ((priv->offscreen_box.y2 - priv->offscreen_box.y1) * resource_scale); + + g_return_val_if_fail (width > 0, FALSE); + g_return_val_if_fail (height > 0, FALSE); + + cogl_clear_object (&priv->old_texture); + priv->old_texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, width, height)); + + cogl_clear_object (&priv->new_texture); + priv->new_texture = COGL_TEXTURE (cogl_texture_2d_new_with_size (ctx, width, height)); + + if (priv->old_texture == NULL) + return FALSE; + + if (priv->new_texture == NULL) + return FALSE; + + g_clear_object (&priv->old_offscreen); + priv->old_offscreen = COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (priv->old_texture)); + if (!cogl_framebuffer_allocate (priv->old_offscreen, &catch_error)) + { + g_error_free (catch_error); + g_clear_object (&priv->old_offscreen); + return FALSE; + } + + g_clear_object (&priv->new_offscreen); + priv->new_offscreen = COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (priv->new_texture)); + if (!cogl_framebuffer_allocate (priv->new_offscreen, &catch_error)) + { + g_error_free (catch_error); + g_clear_object (&priv->new_offscreen); + return FALSE; + } + + if (priv->material == NULL) + { + if (G_UNLIKELY (material_template == NULL)) + { + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + material_template = cogl_pipeline_new (ctx); + + cogl_pipeline_set_layer_combine (material_template, 0, + "RGBA = REPLACE (TEXTURE)", + NULL); + cogl_pipeline_set_layer_combine (material_template, 1, + "RGBA = INTERPOLATE (PREVIOUS, " + "TEXTURE, " + "CONSTANT[A])", + NULL); + cogl_pipeline_set_layer_combine (material_template, 2, + "RGBA = MODULATE (PREVIOUS, " + "PRIMARY)", + NULL); + } + priv->material = cogl_pipeline_copy (material_template); + } + + cogl_pipeline_set_layer_texture (priv->material, 0, priv->new_texture); + cogl_pipeline_set_layer_texture (priv->material, 1, priv->old_texture); + + cogl_framebuffer_clear4f (priv->old_offscreen, COGL_BUFFER_BIT_COLOR, + 0, 0, 0, 0); + cogl_framebuffer_orthographic (priv->old_offscreen, + priv->offscreen_box.x1, + priv->offscreen_box.y1, + priv->offscreen_box.x2, + priv->offscreen_box.y2, 0.0, 1.0); + + st_theme_node_paint (priv->old_theme_node, &priv->old_paint_state, + priv->old_offscreen, allocation, 255, resource_scale); + + cogl_framebuffer_clear4f (priv->new_offscreen, COGL_BUFFER_BIT_COLOR, + 0, 0, 0, 0); + cogl_framebuffer_orthographic (priv->new_offscreen, + priv->offscreen_box.x1, + priv->offscreen_box.y1, + priv->offscreen_box.x2, + priv->offscreen_box.y2, 0.0, 1.0); + st_theme_node_paint (priv->new_theme_node, &priv->new_paint_state, + priv->new_offscreen, allocation, 255, resource_scale); + + return TRUE; +} + +void +st_theme_node_transition_paint (StThemeNodeTransition *transition, + CoglFramebuffer *framebuffer, + ClutterActorBox *allocation, + guint8 paint_opacity, + float resource_scale) +{ + StThemeNodeTransitionPrivate *priv = transition->priv; + + CoglColor constant; + float tex_coords[] = { + 0.0, 0.0, 1.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + }; + + g_return_if_fail (ST_IS_THEME_NODE (priv->old_theme_node)); + g_return_if_fail (ST_IS_THEME_NODE (priv->new_theme_node)); + + if (!clutter_actor_box_equal (allocation, &priv->last_allocation)) + priv->needs_setup = TRUE; + + if (priv->needs_setup) + { + priv->last_allocation = *allocation; + + calculate_offscreen_box (transition, allocation); + priv->needs_setup = clutter_actor_box_get_area (&priv->offscreen_box) == 0 || + !setup_framebuffers (transition, allocation, + resource_scale); + + if (priv->needs_setup) /* setting up framebuffers failed */ + return; + } + + cogl_color_init_from_4f (&constant, 0., 0., 0., + clutter_timeline_get_progress (priv->timeline)); + cogl_pipeline_set_layer_combine_constant (priv->material, 1, &constant); + + cogl_pipeline_set_color4ub (priv->material, + paint_opacity, paint_opacity, + paint_opacity, paint_opacity); + + cogl_framebuffer_draw_multitextured_rectangle (framebuffer, + priv->material, + priv->offscreen_box.x1, + priv->offscreen_box.y1, + priv->offscreen_box.x2, + priv->offscreen_box.y2, + tex_coords, 8); +} + +static void +st_theme_node_transition_dispose (GObject *object) +{ + StThemeNodeTransitionPrivate *priv = ST_THEME_NODE_TRANSITION (object)->priv; + + g_clear_object (&priv->old_theme_node); + g_clear_object (&priv->new_theme_node); + + cogl_clear_object (&priv->old_texture); + cogl_clear_object (&priv->new_texture); + + g_clear_object (&priv->old_offscreen); + g_clear_object (&priv->new_offscreen); + + cogl_clear_object (&priv->material); + + if (priv->timeline) + { + g_clear_signal_handler (&priv->timeline_completed_id, priv->timeline); + g_clear_signal_handler (&priv->timeline_new_frame_id, priv->timeline); + + g_clear_object (&priv->timeline); + } + + priv->timeline_completed_id = 0; + priv->timeline_new_frame_id = 0; + + st_theme_node_paint_state_free (&priv->old_paint_state); + st_theme_node_paint_state_free (&priv->new_paint_state); + + G_OBJECT_CLASS (st_theme_node_transition_parent_class)->dispose (object); +} + +static void +st_theme_node_transition_init (StThemeNodeTransition *transition) +{ + transition->priv = st_theme_node_transition_get_instance_private (transition); + + transition->priv->old_theme_node = NULL; + transition->priv->new_theme_node = NULL; + + transition->priv->old_texture = NULL; + transition->priv->new_texture = NULL; + + transition->priv->old_offscreen = NULL; + transition->priv->new_offscreen = NULL; + + st_theme_node_paint_state_init (&transition->priv->old_paint_state); + st_theme_node_paint_state_init (&transition->priv->new_paint_state); + + transition->priv->needs_setup = TRUE; +} + +static void +st_theme_node_transition_class_init (StThemeNodeTransitionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = st_theme_node_transition_dispose; + + signals[COMPLETED] = + g_signal_new ("completed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[NEW_FRAME] = + g_signal_new ("new-frame", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/src/st/st-theme-node-transition.h b/src/st/st-theme-node-transition.h new file mode 100644 index 0000000..e7420e6 --- /dev/null +++ b/src/st/st-theme-node-transition.h @@ -0,0 +1,58 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node-transition.h: Theme node transitions for StWidget. + * + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_THEME_NODE_TRANSITION_H__ +#define __ST_THEME_NODE_TRANSITION_H__ + +#include <clutter/clutter.h> + +#include "st-widget.h" +#include "st-theme-node.h" + +G_BEGIN_DECLS + +#define ST_TYPE_THEME_NODE_TRANSITION (st_theme_node_transition_get_type ()) +G_DECLARE_FINAL_TYPE (StThemeNodeTransition, st_theme_node_transition, + ST, THEME_NODE_TRANSITION, GObject) + +StThemeNodeTransition *st_theme_node_transition_new (ClutterActor *actor, + StThemeNode *from_node, + StThemeNode *to_node, + StThemeNodePaintState *old_paint_state, + guint duration); + +void st_theme_node_transition_update (StThemeNodeTransition *transition, + StThemeNode *new_node); + +void st_theme_node_transition_paint (StThemeNodeTransition *transition, + CoglFramebuffer *framebuffer, + ClutterActorBox *allocation, + guint8 paint_opacity, + float resource_scale); + +void st_theme_node_transition_get_paint_box (StThemeNodeTransition *transition, + const ClutterActorBox *allocation, + ClutterActorBox *paint_box); + +StThemeNodePaintState * st_theme_node_transition_get_new_paint_state (StThemeNodeTransition *transition); + +G_END_DECLS + +#endif diff --git a/src/st/st-theme-node.c b/src/st/st-theme-node.c new file mode 100644 index 0000000..6e09c39 --- /dev/null +++ b/src/st/st-theme-node.c @@ -0,0 +1,4325 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node.c: style information for one node in a tree of themed objects + * + * Copyright 2008-2010 Red Hat, Inc. + * Copyright 2009 Steve Frécinaux + * Copyright 2009, 2010 Florian Müllner + * Copyright 2010 Adel Gadllah + * Copyright 2010 Giovanni Campagna + * Copyright 2011 Quentin "Sardem FF7" Glidic + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <string.h> + +#include "st-settings.h" +#include "st-theme-private.h" +#include "st-theme-context.h" +#include "st-theme-node-private.h" + +static void st_theme_node_dispose (GObject *object); +static void st_theme_node_finalize (GObject *object); + +static const ClutterColor BLACK_COLOR = { 0, 0, 0, 0xff }; +static const ClutterColor TRANSPARENT_COLOR = { 0, 0, 0, 0 }; +static const ClutterColor DEFAULT_SUCCESS_COLOR = { 0x4e, 0x9a, 0x06, 0xff }; +static const ClutterColor DEFAULT_WARNING_COLOR = { 0xf5, 0x79, 0x3e, 0xff }; +static const ClutterColor DEFAULT_ERROR_COLOR = { 0xcc, 0x00, 0x00, 0xff }; + +G_DEFINE_TYPE (StThemeNode, st_theme_node, G_TYPE_OBJECT) + +static void +st_theme_node_init (StThemeNode *node) +{ + node->transition_duration = -1; + + st_theme_node_paint_state_init (&node->cached_state); +} + +static void +st_theme_node_class_init (StThemeNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = st_theme_node_dispose; + object_class->finalize = st_theme_node_finalize; +} + +static void +maybe_free_properties (StThemeNode *node) +{ + if (node->properties) + { + g_free (node->properties); + node->properties = NULL; + node->n_properties = 0; + } + + if (node->inline_properties) + { + /* This destroys the list, not just the head of the list */ + cr_declaration_destroy (node->inline_properties); + node->inline_properties = NULL; + } +} + +static void +st_theme_node_dispose (GObject *gobject) +{ + StThemeNode *node = ST_THEME_NODE (gobject); + + if (node->parent_node) + { + g_object_unref (node->parent_node); + node->parent_node = NULL; + } + + if (node->border_image) + { + g_object_unref (node->border_image); + node->border_image = NULL; + } + + if (node->icon_colors) + { + st_icon_colors_unref (node->icon_colors); + node->icon_colors = NULL; + } + + st_theme_node_paint_state_free (&node->cached_state); + + g_clear_object (&node->theme); + + G_OBJECT_CLASS (st_theme_node_parent_class)->dispose (gobject); +} + +static void +st_theme_node_finalize (GObject *object) +{ + StThemeNode *node = ST_THEME_NODE (object); + + g_free (node->element_id); + g_strfreev (node->element_classes); + g_strfreev (node->pseudo_classes); + g_free (node->inline_style); + + maybe_free_properties (node); + + g_clear_pointer (&node->font_desc, pango_font_description_free); + + g_clear_pointer (&node->box_shadow, st_shadow_unref); + g_clear_pointer (&node->background_image_shadow, st_shadow_unref); + g_clear_pointer (&node->text_shadow, st_shadow_unref); + + g_clear_object (&node->background_image); + + cogl_clear_object (&node->background_texture); + cogl_clear_object (&node->background_pipeline); + cogl_clear_object (&node->background_shadow_pipeline); + cogl_clear_object (&node->border_slices_texture); + cogl_clear_object (&node->border_slices_pipeline); + cogl_clear_object (&node->color_pipeline); + + G_OBJECT_CLASS (st_theme_node_parent_class)->finalize (object); +} + +static GStrv +split_on_whitespace (const gchar *s) +{ + gchar *cur; + gchar *l; + gchar *temp; + GPtrArray *arr; + + if (s == NULL) + return NULL; + + arr = g_ptr_array_new (); + l = g_strdup (s); + + cur = strtok_r (l, " \t\f\r\n", &temp); + + while (cur != NULL) + { + g_ptr_array_add (arr, g_strdup (cur)); + cur = strtok_r (NULL, " \t\f\r\n", &temp); + } + + g_free (l); + g_ptr_array_add (arr, NULL); + return (GStrv) g_ptr_array_free (arr, FALSE); +} + +/** + * st_theme_node_new: + * @context: the context representing global state for this themed tree + * @parent_node: (nullable): the parent node of this node + * @theme: (nullable): a theme (stylesheet set) that overrides the + * theme inherited from the parent node + * @element_type: the type of the GObject represented by this node + * in the tree (corresponding to an element if we were theming an XML + * document. %G_TYPE_NONE means this style was created for the stage + * actor and matches a selector element name of 'stage'. + * @element_id: (nullable): the ID to match CSS rules against + * @element_class: (nullable): a whitespace-separated list of classes + * to match CSS rules against + * @pseudo_class: (nullable): a whitespace-separated list of pseudo-classes + * (like 'hover' or 'visited') to match CSS rules against + * + * Creates a new #StThemeNode. Once created, a node is immutable. If any + * of the attributes of the node (like the @element_class) change the node + * and its child nodes must be destroyed and recreated. + * + * Returns: (transfer full): a new #StThemeNode + */ +StThemeNode * +st_theme_node_new (StThemeContext *context, + StThemeNode *parent_node, + StTheme *theme, + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class, + const char *inline_style) +{ + StThemeNode *node; + + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + g_return_val_if_fail (parent_node == NULL || ST_IS_THEME_NODE (parent_node), NULL); + + node = g_object_new (ST_TYPE_THEME_NODE, NULL); + + node->context = context; + if (parent_node != NULL) + node->parent_node = g_object_ref (parent_node); + else + node->parent_node = NULL; + + if (theme == NULL && parent_node != NULL) + theme = parent_node->theme; + + g_set_object (&node->theme, theme); + node->element_type = element_type; + node->element_id = g_strdup (element_id); + node->element_classes = split_on_whitespace (element_class); + node->pseudo_classes = split_on_whitespace (pseudo_class); + node->inline_style = g_strdup (inline_style); + node->cached_scale_factor = st_theme_context_get_scale_factor (context); + + return node; +} + +/** + * st_theme_node_get_parent: + * @node: a #StThemeNode + * + * Gets the parent themed element node. + * + * Returns: (nullable) (transfer none): the parent #StThemeNode, or %NULL if + * this is the root node of the tree of theme elements. + */ +StThemeNode * +st_theme_node_get_parent (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->parent_node; +} + +/** + * st_theme_node_get_theme: + * @node: a #StThemeNode + * + * Gets the theme stylesheet set that styles this node + * + * Returns: (transfer none): the theme stylesheet set + */ +StTheme * +st_theme_node_get_theme (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->theme; +} + +/** + * st_theme_node_get_element_type: + * @node: a #StThemeNode + * + * Get the element #GType for @node. + * + * Returns: the element type + */ +GType +st_theme_node_get_element_type (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), G_TYPE_NONE); + + return node->element_type; +} + +/** + * st_theme_node_get_element_id: + * @node: a #StThemeNode + * + * Get the unique element ID for @node. + * + * Returns: (transfer none): the element's ID + */ +const char * +st_theme_node_get_element_id (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->element_id; +} + +/** + * st_theme_node_get_element_classes: + * @node: a #StThemeNode + * + * Get the list of element classes for @node. + * + * Returns: (transfer none): the element's classes + */ +GStrv +st_theme_node_get_element_classes (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->element_classes; +} + +/** + * st_theme_node_get_pseudo_classes: + * @node: a #StThemeNode + * + * Get the list of pseudo-classes for @node (eg. `:focused`). + * + * Returns: (transfer none): the element's pseudo-classes + */ +GStrv +st_theme_node_get_pseudo_classes (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->pseudo_classes; +} + +/** + * st_theme_node_equal: + * @node_a: first #StThemeNode + * @node_b: second #StThemeNode + * + * Compare two #StThemeNodes. Two nodes which compare equal will match + * the same CSS rules and have the same style properties. However, two + * nodes that have ended up with identical style properties do not + * necessarily compare equal. + * + * In detail, @node_a and @node_b are considered equal if and only if: + * + * - they share the same #StTheme and #StThemeContext + * - they have the same parent + * - they have the same element type + * - their id, class, pseudo-class and inline-style match + * + * Returns: %TRUE if @node_a equals @node_b + */ +gboolean +st_theme_node_equal (StThemeNode *node_a, StThemeNode *node_b) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node_a), FALSE); + + if (node_a == node_b) + return TRUE; + + g_return_val_if_fail (ST_IS_THEME_NODE (node_b), FALSE); + + if (node_a->parent_node != node_b->parent_node || + node_a->context != node_b->context || + node_a->theme != node_b->theme || + node_a->element_type != node_b->element_type || + node_a->cached_scale_factor != node_b->cached_scale_factor || + g_strcmp0 (node_a->element_id, node_b->element_id) || + g_strcmp0 (node_a->inline_style, node_b->inline_style)) + return FALSE; + + if ((node_a->element_classes == NULL) != (node_b->element_classes == NULL)) + return FALSE; + + if ((node_a->pseudo_classes == NULL) != (node_b->pseudo_classes == NULL)) + return FALSE; + + if (node_a->element_classes != NULL) + { + int i; + + for (i = 0; ; i++) + { + if (g_strcmp0 (node_a->element_classes[i], + node_b->element_classes[i])) + return FALSE; + + if (node_a->element_classes[i] == NULL) + break; + } + } + + if (node_a->pseudo_classes != NULL) + { + int i; + + for (i = 0; ; i++) + { + if (g_strcmp0 (node_a->pseudo_classes[i], + node_b->pseudo_classes[i])) + return FALSE; + + if (node_a->pseudo_classes[i] == NULL) + break; + } + } + + return TRUE; +} + +/** + * st_theme_node_hash: + * @node: a #StThemeNode + * + * Converts @node to a hash value. + * + * Returns: a hash value corresponding to @node + */ +guint +st_theme_node_hash (StThemeNode *node) +{ + guint hash; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), 0); + + hash = GPOINTER_TO_UINT (node->parent_node); + + hash = hash * 33 + GPOINTER_TO_UINT (node->context); + hash = hash * 33 + GPOINTER_TO_UINT (node->theme); + hash = hash * 33 + ((guint) node->element_type); + hash = hash * 33 + ((guint) node->cached_scale_factor); + + if (node->element_id != NULL) + hash = hash * 33 + g_str_hash (node->element_id); + + if (node->inline_style != NULL) + hash = hash * 33 + g_str_hash (node->inline_style); + + if (node->element_classes != NULL) + { + gchar **it; + + for (it = node->element_classes; *it != NULL; it++) + hash = hash * 33 + g_str_hash (*it) + 1; + } + + if (node->pseudo_classes != NULL) + { + gchar **it; + + for (it = node->pseudo_classes; *it != NULL; it++) + hash = hash * 33 + g_str_hash (*it) + 1; + } + + return hash; +} + +static void +ensure_properties (StThemeNode *node) +{ + if (!node->properties_computed) + { + GPtrArray *properties = NULL; + + node->properties_computed = TRUE; + + if (node->theme) + properties = _st_theme_get_matched_properties (node->theme, node); + + if (node->inline_style && *node->inline_style != '\0') + { + CRDeclaration *cur_decl; + + if (!properties) + properties = g_ptr_array_new (); + + node->inline_properties = _st_theme_parse_declaration_list (node->inline_style); + for (cur_decl = node->inline_properties; cur_decl; cur_decl = cur_decl->next) + g_ptr_array_add (properties, cur_decl); + } + + if (properties) + { + node->n_properties = properties->len; + node->properties = (CRDeclaration **)g_ptr_array_free (properties, FALSE); + } + } +} + +typedef enum { + VALUE_FOUND, + VALUE_NOT_FOUND, + VALUE_INHERIT +} GetFromTermResult; + +static gboolean +term_is_inherit (CRTerm *term) +{ + return (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "inherit") == 0); +} + +static gboolean +term_is_none (CRTerm *term) +{ + return (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "none") == 0); +} + +static gboolean +term_is_transparent (CRTerm *term) +{ + return (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "transparent") == 0); +} + +static int +color_component_from_double (double component) +{ + /* We want to spread the range 0-1 equally over 0..255, but + * 1.0 should map to 255 not 256, so we need to special-case it. + * See http://people.redhat.com/otaylor/pixel-converting.html + * for (very) detailed discussion of related issues. */ + if (component >= 1.0) + return 255; + else + return (int)(component * 256); +} + +static GetFromTermResult +get_color_from_rgba_term (CRTerm *term, + ClutterColor *color) +{ + CRTerm *arg = term->ext_content.func_param; + CRNum *num; + double r = 0, g = 0, b = 0, a = 0; + int i; + + for (i = 0; i < 4; i++) + { + double value; + + if (arg == NULL) + return VALUE_NOT_FOUND; + + if ((i == 0 && arg->the_operator != NO_OP) || + (i > 0 && arg->the_operator != COMMA)) + return VALUE_NOT_FOUND; + + if (arg->type != TERM_NUMBER) + return VALUE_NOT_FOUND; + + num = arg->content.num; + + /* For simplicity, we convert a,r,g,b to [0,1.0] floats and then + * convert them back below. Then when we set them on a cairo content + * we convert them back to floats, and then cairo converts them + * back to integers to pass them to X, and so forth... + */ + if (i < 3) + { + if (num->type == NUM_PERCENTAGE) + value = num->val / 100; + else if (num->type == NUM_GENERIC) + value = num->val / 255; + else + return VALUE_NOT_FOUND; + } + else + { + if (num->type != NUM_GENERIC) + return VALUE_NOT_FOUND; + + value = num->val; + } + + value = CLAMP (value, 0, 1); + + switch (i) + { + case 0: + r = value; + break; + case 1: + g = value; + break; + case 2: + b = value; + break; + case 3: + a = value; + break; + default: + g_assert_not_reached(); + break; + } + + arg = arg->next; + } + + color->red = color_component_from_double (r); + color->green = color_component_from_double (g); + color->blue = color_component_from_double (b); + color->alpha = color_component_from_double (a); + + return VALUE_FOUND; +} + +static GetFromTermResult +get_color_from_term (StThemeNode *node, + CRTerm *term, + ClutterColor *color) +{ + CRRgb rgb; + enum CRStatus status; + + if (term_is_inherit (term)) + { + return VALUE_INHERIT; + } + /* Since libcroco doesn't know about rgba colors, it can't handle + * the transparent keyword + */ + else if (term_is_transparent (term)) + { + *color = TRANSPARENT_COLOR; + return VALUE_FOUND; + } + /* rgba () colors - a CSS3 addition, are not supported by libcroco, + * but they are parsed as a "function", so we can emulate the + * functionality. + */ + else if (term->type == TERM_FUNCTION && + term->content.str && + term->content.str->stryng && + term->content.str->stryng->str && + strcmp (term->content.str->stryng->str, "rgba") == 0) + { + return get_color_from_rgba_term (term, color); + } + + status = cr_rgb_set_from_term (&rgb, term); + if (status != CR_OK) + return VALUE_NOT_FOUND; + + if (rgb.is_percentage) + cr_rgb_compute_from_percentage (&rgb); + + color->red = rgb.red; + color->green = rgb.green; + color->blue = rgb.blue; + color->alpha = 0xff; + + return VALUE_FOUND; +} + +/** + * st_theme_node_lookup_color: + * @node: a #StThemeNode + * @property_name: The name of the color property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @color: (out caller-allocates): location to store the color that was + * determined. If the property is not found, the value in this location + * will not be changed. + * + * Generically looks up a property containing a single color value. When + * specific getters (like st_theme_node_get_background_color()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * See also st_theme_node_get_color(), which provides a simpler API. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.) + */ +gboolean +st_theme_node_lookup_color (StThemeNode *node, + const char *property_name, + gboolean inherit, + ClutterColor *color) +{ + + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + GetFromTermResult result = get_color_from_term (node, decl->value, color); + if (result == VALUE_FOUND) + { + return TRUE; + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + return st_theme_node_lookup_color (node->parent_node, property_name, inherit, color); + else + break; + } + } + } + + if (inherit && node->parent_node) + return st_theme_node_lookup_color (node->parent_node, property_name, inherit, color); + + return FALSE; +} + +/** + * st_theme_node_get_color: + * @node: a #StThemeNode + * @property_name: The name of the color property + * @color: (out caller-allocates): location to store the color that + * was determined. + * + * Generically looks up a property containing a single color value. When + * specific getters (like st_theme_node_get_background_color()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * If @property_name is not found, a warning will be logged and a + * default color returned. + * + * See also st_theme_node_lookup_color(), which provides more options, + * and lets you handle the case where the theme does not specify the + * indicated color. + */ +void +st_theme_node_get_color (StThemeNode *node, + const char *property_name, + ClutterColor *color) +{ + if (!st_theme_node_lookup_color (node, property_name, FALSE, color)) + { + g_warning ("Did not find color property '%s'", property_name); + memset (color, 0, sizeof (ClutterColor)); + } +} + +/** + * st_theme_node_lookup_double: + * @node: a #StThemeNode + * @property_name: The name of the numeric property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @value: (out): location to store the value that was determined. + * If the property is not found, the value in this location + * will not be changed. + * + * Generically looks up a property containing a single numeric value + * without units. + * + * See also st_theme_node_get_double(), which provides a simpler API. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.) + */ +gboolean +st_theme_node_lookup_double (StThemeNode *node, + const char *property_name, + gboolean inherit, + double *value) +{ + gboolean result = FALSE; + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + CRTerm *term = decl->value; + + if (term->type != TERM_NUMBER || term->content.num->type != NUM_GENERIC) + continue; + + *value = term->content.num->val; + result = TRUE; + break; + } + } + + if (!result && inherit && node->parent_node) + result = st_theme_node_lookup_double (node->parent_node, property_name, inherit, value); + + return result; +} + +/** + * st_theme_node_lookup_time: + * @node: a #StThemeNode + * @property_name: The name of the time property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @value: (out): location to store the value that was determined. + * If the property is not found, the value in this location + * will not be changed. + * + * Generically looks up a property containing a single time value, + * which is converted to milliseconds. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.) + */ +gboolean +st_theme_node_lookup_time (StThemeNode *node, + const char *property_name, + gboolean inherit, + double *value) +{ + gboolean result = FALSE; + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + CRTerm *term = decl->value; + int factor = 1; + + if (term->type != TERM_NUMBER) + continue; + + if (term->content.num->type != NUM_TIME_S && + term->content.num->type != NUM_TIME_MS) + continue; + + if (term->content.num->type == NUM_TIME_S) + factor = 1000; + + *value = factor * term->content.num->val; + result = TRUE; + break; + } + } + + if (!result && inherit && node->parent_node) + result = st_theme_node_lookup_time (node->parent_node, property_name, inherit, value); + + return result; +} + +/** + * st_theme_node_get_double: + * @node: a #StThemeNode + * @property_name: The name of the numeric property + * + * Generically looks up a property containing a single numeric value + * without units. + * + * See also st_theme_node_lookup_double(), which provides more options, + * and lets you handle the case where the theme does not specify the + * indicated value. + * + * Returns: the value found. If @property_name is not + * found, a warning will be logged and 0 will be returned. + */ +gdouble +st_theme_node_get_double (StThemeNode *node, + const char *property_name) +{ + gdouble value; + + if (st_theme_node_lookup_double (node, property_name, FALSE, &value)) + return value; + else + { + g_warning ("Did not find double property '%s'", property_name); + return 0.0; + } +} + +/** + * st_theme_node_lookup_url: + * @node: a #StThemeNode + * @property_name: The name of the string property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @file: (out) (transfer full): location to store the newly allocated value that was + * determined. If the property is not found, the value in this location + * will not be changed. + * + * Looks up a property containing a single URL value. + * + * See also st_theme_node_get_url(), which provides a simpler API. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.) + */ +gboolean +st_theme_node_lookup_url (StThemeNode *node, + const char *property_name, + gboolean inherit, + GFile **file) +{ + gboolean result = FALSE; + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + CRTerm *term = decl->value; + CRStyleSheet *base_stylesheet; + + if (term->type != TERM_URI && term->type != TERM_STRING) + continue; + + if (decl->parent_statement != NULL) + base_stylesheet = decl->parent_statement->parent_sheet; + else + base_stylesheet = NULL; + + *file = _st_theme_resolve_url (node->theme, + base_stylesheet, + decl->value->content.str->stryng->str); + result = TRUE; + break; + } + } + + if (!result && inherit && node->parent_node) + result = st_theme_node_lookup_url (node->parent_node, property_name, inherit, file); + + return result; +} + +/** + * st_theme_node_get_url: + * @node: a #StThemeNode + * @property_name: The name of the string property + * + * Looks up a property containing a single URL value. + * + * See also st_theme_node_lookup_url(), which provides more options, + * and lets you handle the case where the theme does not specify the + * indicated value. + * + * Returns: (nullable) (transfer full): the newly allocated value if found. + * If @property_name is not found, a warning will be logged and %NULL + * will be returned. + */ +GFile * +st_theme_node_get_url (StThemeNode *node, + const char *property_name) +{ + GFile *file; + + if (st_theme_node_lookup_url (node, property_name, FALSE, &file)) + return file; + else + { + g_warning ("Did not find string property '%s'", property_name); + return NULL; + } +} + +static const PangoFontDescription * +get_parent_font (StThemeNode *node) +{ + if (node->parent_node) + return st_theme_node_get_font (node->parent_node); + else + return st_theme_context_get_font (node->context); +} + +static GetFromTermResult +get_length_from_term (StThemeNode *node, + CRTerm *term, + gboolean use_parent_font, + gdouble *length) +{ + CRNum *num; + + enum { + ABSOLUTE, + POINTS, + FONT_RELATIVE, + } type = ABSOLUTE; + + double multiplier = 1.0; + + + if (term->type != TERM_NUMBER) + { + g_warning ("Ignoring length property that isn't a number at line %d, col %d", + term->location.line, term->location.column); + return VALUE_NOT_FOUND; + } + + num = term->content.num; + + switch (num->type) + { + case NUM_LENGTH_PX: + type = ABSOLUTE; + multiplier = 1 * node->cached_scale_factor; + break; + case NUM_LENGTH_PT: + type = POINTS; + multiplier = 1; + break; + case NUM_LENGTH_IN: + type = POINTS; + multiplier = 72; + break; + case NUM_LENGTH_CM: + type = POINTS; + multiplier = 72. / 2.54; + break; + case NUM_LENGTH_MM: + type = POINTS; + multiplier = 72. / 25.4; + break; + case NUM_LENGTH_PC: + type = POINTS; + multiplier = 12. / 25.4; + break; + case NUM_LENGTH_EM: + { + type = FONT_RELATIVE; + multiplier = 1; + break; + } + case NUM_LENGTH_EX: + { + /* Doing better would require actually resolving the font description + * to a specific font, and Pango doesn't have an ex metric anyways, + * so we'd have to try and synthesize it by complicated means. + * + * The 0.5em is the CSS spec suggested thing to use when nothing + * better is available. + */ + type = FONT_RELATIVE; + multiplier = 0.5; + break; + } + + case NUM_INHERIT: + return VALUE_INHERIT; + + case NUM_AUTO: + g_warning ("'auto' not supported for lengths"); + return VALUE_NOT_FOUND; + + case NUM_GENERIC: + { + if (num->val != 0) + { + g_warning ("length values must specify a unit"); + return VALUE_NOT_FOUND; + } + else + { + type = ABSOLUTE; + multiplier = 0; + } + break; + } + + case NUM_PERCENTAGE: + g_warning ("percentage lengths not currently supported"); + return VALUE_NOT_FOUND; + + case NUM_ANGLE_DEG: + case NUM_ANGLE_RAD: + case NUM_ANGLE_GRAD: + case NUM_TIME_MS: + case NUM_TIME_S: + case NUM_FREQ_HZ: + case NUM_FREQ_KHZ: + case NUM_UNKNOWN_TYPE: + case NB_NUM_TYPE: + default: + g_warning ("Ignoring invalid type of number of length property"); + return VALUE_NOT_FOUND; + } + + switch (type) + { + case ABSOLUTE: + *length = num->val * multiplier; + break; + case POINTS: + { + double resolution = clutter_backend_get_resolution (clutter_get_default_backend ()); + *length = num->val * multiplier * (resolution / 72.); + } + break; + case FONT_RELATIVE: + { + const PangoFontDescription *desc; + double font_size; + + if (use_parent_font) + desc = get_parent_font (node); + else + desc = st_theme_node_get_font (node); + + font_size = (double)pango_font_description_get_size (desc) / PANGO_SCALE; + + if (pango_font_description_get_size_is_absolute (desc)) + { + *length = num->val * multiplier * font_size; + } + else + { + double resolution = clutter_backend_get_resolution (clutter_get_default_backend ()); + *length = num->val * multiplier * (resolution / 72.) * font_size; + } + } + break; + default: + g_assert_not_reached (); + } + + return VALUE_FOUND; +} + +static GetFromTermResult +get_length_from_term_int (StThemeNode *node, + CRTerm *term, + gboolean use_parent_font, + gint *length) +{ + double value; + GetFromTermResult result; + + result = get_length_from_term (node, term, use_parent_font, &value); + if (result == VALUE_FOUND) + *length = (int) ((value / node->cached_scale_factor) + 0.5) * node->cached_scale_factor; + return result; +} + +static GetFromTermResult +get_length_internal (StThemeNode *node, + const char *property_name, + gdouble *length) +{ + int i; + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + GetFromTermResult result = get_length_from_term (node, decl->value, FALSE, length); + if (result != VALUE_NOT_FOUND) + return result; + } + } + + return VALUE_NOT_FOUND; +} + +/** + * st_theme_node_lookup_length: + * @node: a #StThemeNode + * @property_name: The name of the length property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @length: (out): location to store the length that was determined. + * If the property is not found, the value in this location + * will not be changed. The returned length is resolved + * to pixels. + * + * Generically looks up a property containing a single length value. When + * specific getters (like st_theme_node_get_border_width()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * See also st_theme_node_get_length(), which provides a simpler API. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.) + */ +gboolean +st_theme_node_lookup_length (StThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *length) +{ + GetFromTermResult result; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + result = get_length_internal (node, property_name, length); + + if (result == VALUE_FOUND) + return TRUE; + else if (result == VALUE_INHERIT) + inherit = TRUE; + + if (inherit && node->parent_node) + return st_theme_node_lookup_length (node->parent_node, property_name, inherit, length); + + return FALSE; +} + +/** + * st_theme_node_get_length: + * @node: a #StThemeNode + * @property_name: The name of the length property + * + * Generically looks up a property containing a single length value. When + * specific getters (like st_theme_node_get_border_width()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * Unlike st_theme_node_get_color() and st_theme_node_get_double(), + * this does not print a warning if the property is not found; it just + * returns 0. + * + * See also st_theme_node_lookup_length(), which provides more options. The + * returned value is in physical pixels, as opposed to logical pixels. + * + * Returns: the length, in pixels, or 0 if the property was not found. + */ +gdouble +st_theme_node_get_length (StThemeNode *node, + const char *property_name) +{ + gdouble length; + + if (st_theme_node_lookup_length (node, property_name, FALSE, &length)) + return length; + else + return 0.0; +} + +static void +do_border_radius_term (StThemeNode *node, + CRTerm *term, + gboolean topleft, + gboolean topright, + gboolean bottomright, + gboolean bottomleft) +{ + int value; + + if (get_length_from_term_int (node, term, FALSE, &value) != VALUE_FOUND) + return; + + if (topleft) + node->border_radius[ST_CORNER_TOPLEFT] = value; + if (topright) + node->border_radius[ST_CORNER_TOPRIGHT] = value; + if (bottomright) + node->border_radius[ST_CORNER_BOTTOMRIGHT] = value; + if (bottomleft) + node->border_radius[ST_CORNER_BOTTOMLEFT] = value; +} + +static void +do_border_radius (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 13; /* Skip 'border-radius' */ + + if (strcmp (property_name, "") == 0) + { + /* Slight deviation ... if we don't understand some of the terms and understand others, + * then we set the ones we understand and ignore the others instead of ignoring the + * whole thing + */ + if (decl->value == NULL) /* 0 values */ + return; + else if (decl->value->next == NULL) /* 1 value */ + { + do_border_radius_term (node, decl->value, TRUE, TRUE, TRUE, TRUE); /* all corners */ + return; + } + else if (decl->value->next->next == NULL) /* 2 values */ + { + do_border_radius_term (node, decl->value, TRUE, FALSE, TRUE, FALSE); /* topleft/bottomright */ + do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, TRUE); /* topright/bottomleft */ + } + else if (decl->value->next->next->next == NULL) /* 3 values */ + { + do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); /* topleft */ + do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, TRUE); /* topright/bottomleft */ + do_border_radius_term (node, decl->value->next->next, FALSE, FALSE, TRUE, FALSE); /* bottomright */ + } + else if (decl->value->next->next->next->next == NULL) /* 4 values */ + { + do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); /* topleft */ + do_border_radius_term (node, decl->value->next, FALSE, TRUE, FALSE, FALSE); /* topright */ + do_border_radius_term (node, decl->value->next->next, FALSE, FALSE, TRUE, FALSE); /* bottomright */ + do_border_radius_term (node, decl->value->next->next->next, FALSE, FALSE, FALSE, TRUE); /* bottomleft */ + } + else + { + g_warning ("Too many values for border-radius property"); + return; + } + } + else + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (strcmp (property_name, "-topleft") == 0) + do_border_radius_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); + else if (strcmp (property_name, "-topright") == 0) + do_border_radius_term (node, decl->value, FALSE, TRUE, FALSE, FALSE); + else if (strcmp (property_name, "-bottomright") == 0) + do_border_radius_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); + else if (strcmp (property_name, "-bottomleft") == 0) + do_border_radius_term (node, decl->value, FALSE, FALSE, FALSE, TRUE); + } +} + +static void +do_border_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 6; /* Skip 'border' */ + StSide side = (StSide)-1; + ClutterColor color; + gboolean color_set = FALSE; + int width = 0; /* suppress warning */ + gboolean width_set = FALSE; + int j; + + if (g_str_has_prefix (property_name, "-radius")) + { + do_border_radius (node, decl); + return; + } + + if (g_str_has_prefix (property_name, "-left")) + { + side = ST_SIDE_LEFT; + property_name += 5; + } + else if (g_str_has_prefix (property_name, "-right")) + { + side = ST_SIDE_RIGHT; + property_name += 6; + } + else if (g_str_has_prefix (property_name, "-top")) + { + side = ST_SIDE_TOP; + property_name += 4; + } + else if (g_str_has_prefix (property_name, "-bottom")) + { + side = ST_SIDE_BOTTOM; + property_name += 7; + } + + if (strcmp (property_name, "") == 0) + { + /* Set value for width/color/style in any order */ + CRTerm *term; + + for (term = decl->value; term; term = term->next) + { + GetFromTermResult result; + + if (term->type == TERM_IDENT) + { + const char *ident = term->content.str->stryng->str; + if (strcmp (ident, "none") == 0 || strcmp (ident, "hidden") == 0) + { + width = 0; + width_set = TRUE; + continue; + } + else if (strcmp (ident, "solid") == 0) + { + /* The only thing we support */ + continue; + } + else if (strcmp (ident, "dotted") == 0 || + strcmp (ident, "dashed") == 0 || + strcmp (ident, "double") == 0 || + strcmp (ident, "groove") == 0 || + strcmp (ident, "ridge") == 0 || + strcmp (ident, "inset") == 0 || + strcmp (ident, "outset") == 0) + { + /* Treat the same as solid */ + continue; + } + + /* Presumably a color, fall through */ + } + + if (term->type == TERM_NUMBER) + { + result = get_length_from_term_int (node, term, FALSE, &width); + if (result != VALUE_NOT_FOUND) + { + width_set = result == VALUE_FOUND; + continue; + } + } + + result = get_color_from_term (node, term, &color); + if (result != VALUE_NOT_FOUND) + { + color_set = result == VALUE_FOUND; + continue; + } + } + + } + else if (strcmp (property_name, "-color") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (get_color_from_term (node, decl->value, &color) == VALUE_FOUND) + /* Ignore inherit */ + color_set = TRUE; + } + else if (strcmp (property_name, "-width") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (get_length_from_term_int (node, decl->value, FALSE, &width) == VALUE_FOUND) + /* Ignore inherit */ + width_set = TRUE; + } + + if (side == (StSide)-1) + { + for (j = 0; j < 4; j++) + { + if (color_set) + node->border_color[j] = color; + if (width_set) + node->border_width[j] = width; + } + } + else + { + if (color_set) + node->border_color[side] = color; + if (width_set) + node->border_width[side] = width; + } +} + +static void +do_outline_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 7; /* Skip 'outline' */ + ClutterColor color; + gboolean color_set = FALSE; + int width = 0; /* suppress warning */ + gboolean width_set = FALSE; + + if (strcmp (property_name, "") == 0) + { + /* Set value for width/color/style in any order */ + CRTerm *term; + + for (term = decl->value; term; term = term->next) + { + GetFromTermResult result; + + if (term->type == TERM_IDENT) + { + const char *ident = term->content.str->stryng->str; + if (strcmp (ident, "none") == 0 || strcmp (ident, "hidden") == 0) + { + width = 0; + width_set = TRUE; + continue; + } + else if (strcmp (ident, "solid") == 0) + { + /* The only thing we support */ + continue; + } + else if (strcmp (ident, "dotted") == 0 || + strcmp (ident, "dashed") == 0 || + strcmp (ident, "double") == 0 || + strcmp (ident, "groove") == 0 || + strcmp (ident, "ridge") == 0 || + strcmp (ident, "inset") == 0 || + strcmp (ident, "outset") == 0) + { + /* Treat the same as solid */ + continue; + } + + /* Presumably a color, fall through */ + } + + if (term->type == TERM_NUMBER) + { + result = get_length_from_term_int (node, term, FALSE, &width); + if (result != VALUE_NOT_FOUND) + { + width_set = result == VALUE_FOUND; + continue; + } + } + + result = get_color_from_term (node, term, &color); + if (result != VALUE_NOT_FOUND) + { + color_set = result == VALUE_FOUND; + continue; + } + } + + } + else if (strcmp (property_name, "-color") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (get_color_from_term (node, decl->value, &color) == VALUE_FOUND) + /* Ignore inherit */ + color_set = TRUE; + } + else if (strcmp (property_name, "-width") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (get_length_from_term_int (node, decl->value, FALSE, &width) == VALUE_FOUND) + /* Ignore inherit */ + width_set = TRUE; + } + + if (color_set) + node->outline_color = color; + if (width_set) + node->outline_width = width; +} + +static void +do_padding_property_term (StThemeNode *node, + CRTerm *term, + gboolean left, + gboolean right, + gboolean top, + gboolean bottom) +{ + int value; + + if (get_length_from_term_int (node, term, FALSE, &value) != VALUE_FOUND) + return; + + if (left) + node->padding[ST_SIDE_LEFT] = value; + if (right) + node->padding[ST_SIDE_RIGHT] = value; + if (top) + node->padding[ST_SIDE_TOP] = value; + if (bottom) + node->padding[ST_SIDE_BOTTOM] = value; +} + +static void +do_padding_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 7; /* Skip 'padding' */ + + if (strcmp (property_name, "") == 0) + { + /* Slight deviation ... if we don't understand some of the terms and understand others, + * then we set the ones we understand and ignore the others instead of ignoring the + * whole thing + */ + if (decl->value == NULL) /* 0 values */ + return; + else if (decl->value->next == NULL) /* 1 value */ + { + do_padding_property_term (node, decl->value, TRUE, TRUE, TRUE, TRUE); /* left/right/top/bottom */ + return; + } + else if (decl->value->next->next == NULL) /* 2 values */ + { + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, TRUE); /* top/bottom */ + do_padding_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + } + else if (decl->value->next->next->next == NULL) /* 3 values */ + { + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); /* top */ + do_padding_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + do_padding_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); /* bottom */ + } + else if (decl->value->next->next->next->next == NULL) /* 4 values */ + { + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); /* top */ + do_padding_property_term (node, decl->value->next, FALSE, TRUE, FALSE, FALSE); /* right */ + do_padding_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); /* bottom */ + do_padding_property_term (node, decl->value->next->next->next, TRUE, FALSE, FALSE, FALSE); /* left */ + } + else + { + g_warning ("Too many values for padding property"); + return; + } + } + else + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (strcmp (property_name, "-left") == 0) + do_padding_property_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); + else if (strcmp (property_name, "-right") == 0) + do_padding_property_term (node, decl->value, FALSE, TRUE, FALSE, FALSE); + else if (strcmp (property_name, "-top") == 0) + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); + else if (strcmp (property_name, "-bottom") == 0) + do_padding_property_term (node, decl->value, FALSE, FALSE, FALSE, TRUE); + } +} + +static void +do_margin_property_term (StThemeNode *node, + CRTerm *term, + gboolean left, + gboolean right, + gboolean top, + gboolean bottom) +{ + int value; + + if (get_length_from_term_int (node, term, FALSE, &value) != VALUE_FOUND) + return; + + if (left) + node->margin[ST_SIDE_LEFT] = value; + if (right) + node->margin[ST_SIDE_RIGHT] = value; + if (top) + node->margin[ST_SIDE_TOP] = value; + if (bottom) + node->margin[ST_SIDE_BOTTOM] = value; +} + +static void +do_margin_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 6; /* Skip 'margin' */ + + if (strcmp (property_name, "") == 0) + { + /* Slight deviation ... if we don't understand some of the terms and understand others, + * then we set the ones we understand and ignore the others instead of ignoring the + * whole thing + */ + if (decl->value == NULL) /* 0 values */ + return; + else if (decl->value->next == NULL) /* 1 value */ + { + do_margin_property_term (node, decl->value, TRUE, TRUE, TRUE, TRUE); /* left/right/top/bottom */ + return; + } + else if (decl->value->next->next == NULL) /* 2 values */ + { + do_margin_property_term (node, decl->value, FALSE, FALSE, TRUE, TRUE); /* top/bottom */ + do_margin_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + } + else if (decl->value->next->next->next == NULL) /* 3 values */ + { + do_margin_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); /* top */ + do_margin_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + do_margin_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); /* bottom */ + } + else if (decl->value->next->next->next->next == NULL) /* 4 values */ + { + do_margin_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); /* top */ + do_margin_property_term (node, decl->value->next, FALSE, TRUE, FALSE, FALSE); /* right */ + do_margin_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); /* bottom */ + do_margin_property_term (node, decl->value->next->next->next, TRUE, FALSE, FALSE, FALSE); /* left */ + } + else + { + g_warning ("Too many values for margin property"); + return; + } + } + else + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (strcmp (property_name, "-left") == 0) + do_margin_property_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); + else if (strcmp (property_name, "-right") == 0) + do_margin_property_term (node, decl->value, FALSE, TRUE, FALSE, FALSE); + else if (strcmp (property_name, "-top") == 0) + do_margin_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); + else if (strcmp (property_name, "-bottom") == 0) + do_margin_property_term (node, decl->value, FALSE, FALSE, FALSE, TRUE); + } +} + +static void +do_size_property (StThemeNode *node, + CRDeclaration *decl, + int *node_value) +{ + CRTerm *term = decl->value; + + if (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "auto") == 0) + *node_value = -1; + else + get_length_from_term_int (node, term, FALSE, node_value); +} + +void +_st_theme_node_ensure_geometry (StThemeNode *node) +{ + int i, j; + int width, height; + + if (node->geometry_computed) + return; + + node->geometry_computed = TRUE; + + ensure_properties (node); + + for (j = 0; j < 4; j++) + { + node->border_width[j] = 0; + node->border_color[j] = TRANSPARENT_COLOR; + } + + node->outline_width = 0; + node->outline_color = TRANSPARENT_COLOR; + + width = -1; + height = -1; + node->width = -1; + node->height = -1; + node->min_width = -1; + node->min_height = -1; + node->max_width = -1; + node->max_height = -1; + + for (i = 0; i < node->n_properties; i++) + { + CRDeclaration *decl = node->properties[i]; + const char *property_name = decl->property->stryng->str; + + if (g_str_has_prefix (property_name, "border")) + do_border_property (node, decl); + else if (g_str_has_prefix (property_name, "outline")) + do_outline_property (node, decl); + else if (g_str_has_prefix (property_name, "padding")) + do_padding_property (node, decl); + else if (g_str_has_prefix (property_name, "margin")) + do_margin_property (node, decl); + else if (strcmp (property_name, "width") == 0) + do_size_property (node, decl, &width); + else if (strcmp (property_name, "height") == 0) + do_size_property (node, decl, &height); + else if (strcmp (property_name, "-st-natural-width") == 0) + do_size_property (node, decl, &node->width); + else if (strcmp (property_name, "-st-natural-height") == 0) + do_size_property (node, decl, &node->height); + else if (strcmp (property_name, "min-width") == 0) + do_size_property (node, decl, &node->min_width); + else if (strcmp (property_name, "min-height") == 0) + do_size_property (node, decl, &node->min_height); + else if (strcmp (property_name, "max-width") == 0) + do_size_property (node, decl, &node->max_width); + else if (strcmp (property_name, "max-height") == 0) + do_size_property (node, decl, &node->max_height); + } + + /* + * Setting width sets max-width, min-width and -st-natural-width, + * unless one of them is set individually. + * Setting min-width sets natural width too, so that the minimum + * width reported by get_preferred_width() is always not greater + * than the natural width. + * The natural width in node->width is actually a lower bound, the + * actor is allowed to request something greater than that, but + * not greater than max-width. + * We don't need to clamp node->width to be less than max_width, + * that's done by adjust_preferred_width. + */ + if (width != -1) + { + if (node->width == -1) + node->width = width; + if (node->min_width == -1) + node->min_width = width; + if (node->max_width == -1) + node->max_width = width; + } + + if (node->width < node->min_width) + node->width = node->min_width; + + if (height != -1) + { + if (node->height == -1) + node->height = height; + if (node->min_height == -1) + node->min_height = height; + if (node->max_height == -1) + node->max_height = height; + } + + if (node->height < node->min_height) + node->height = node->min_height; +} + +/** + * st_theme_node_get_border_width: + * @node: a #StThemeNode + * @side: a #StCorner + * + * Get the border width for @node on @side, in physical pixels. + * + * Returns: the border width in physical pixels + */ +int +st_theme_node_get_border_width (StThemeNode *node, + StSide side) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT, 0.); + + _st_theme_node_ensure_geometry (node); + + return node->border_width[side]; +} + +/** + * st_theme_node_get_border_radius: + * @node: a #StThemeNode + * @corner: a #StCorner + * + * Get the border radius for @node at @corner, in physical pixels. + * + * Returns: the border radius in physical pixels + */ +int +st_theme_node_get_border_radius (StThemeNode *node, + StCorner corner) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (corner >= ST_CORNER_TOPLEFT && corner <= ST_CORNER_BOTTOMLEFT, 0.); + + _st_theme_node_ensure_geometry (node); + + return node->border_radius[corner]; +} + +/** + * st_theme_node_get_outline_width: + * @node: a #StThemeNode + * + * Get the width of the outline for @node, in physical pixels. + * + * Returns: the width in physical pixels + */ +int +st_theme_node_get_outline_width (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0); + + _st_theme_node_ensure_geometry (node); + + return node->outline_width; +} + +/** + * st_theme_node_get_outline_color: + * @node: a #StThemeNode + * @color: (out caller-allocates): location to store the color + * + * Gets the color of @node's outline. + */ +void +st_theme_node_get_outline_color (StThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_geometry (node); + + *color = node->outline_color; +} + +/** + * st_theme_node_get_width: + * @node: a #StThemeNode + * + * Get the width for @node, in physical pixels. + * + * Returns: the width in physical pixels + */ +int +st_theme_node_get_width (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->width; +} + +/** + * st_theme_node_get_height: + * @node: a #StThemeNode + * + * Get the height for @node, in physical pixels. + * + * Returns: the height in physical pixels + */ +int +st_theme_node_get_height (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->height; +} + +/** + * st_theme_node_get_min_width: + * @node: a #StThemeNode + * + * Get the minimum width for @node, in physical pixels. + * + * Returns: the minimum width in physical pixels + */ +int +st_theme_node_get_min_width (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->min_width; +} + +/** + * st_theme_node_get_min_height: + * @node: a #StThemeNode + * + * Get the minimum height for @node, in physical pixels. + * + * Returns: the minimum height in physical pixels + */ +int +st_theme_node_get_min_height (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->min_height; +} + +/** + * st_theme_node_get_max_width: + * @node: a #StThemeNode + * + * Get the maximum width for @node, in physical pixels. + * + * Returns: the maximum width in physical pixels + */ +int +st_theme_node_get_max_width (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->max_width; +} + +/** + * st_theme_node_get_max_height: + * @node: a #StThemeNode + * + * Get the maximum height for @node, in physical pixels. + * + * Returns: the maximum height in physical pixels + */ +int +st_theme_node_get_max_height (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), -1); + + _st_theme_node_ensure_geometry (node); + return node->max_height; +} + +void +_st_theme_node_ensure_background (StThemeNode *node) +{ + int i; + + if (node->background_computed) + return; + + node->background_repeat = FALSE; + node->background_computed = TRUE; + node->background_color = TRANSPARENT_COLOR; + node->background_gradient_type = ST_GRADIENT_NONE; + node->background_position_set = FALSE; + node->background_size = ST_BACKGROUND_SIZE_AUTO; + + ensure_properties (node); + + for (i = 0; i < node->n_properties; i++) + { + CRDeclaration *decl = node->properties[i]; + const char *property_name = decl->property->stryng->str; + + if (g_str_has_prefix (property_name, "background")) + property_name += 10; + else + continue; + + if (strcmp (property_name, "") == 0) + { + /* We're very liberal here ... if we recognize any term in the expression we take it, and + * we ignore the rest. The actual specification is: + * + * background: [<'background-color'> || <'background-image'> || <'background-repeat'> || <'background-attachment'> || <'background-position'>] | inherit + */ + + CRTerm *term; + /* background: property sets all terms to specified or default values */ + node->background_color = TRANSPARENT_COLOR; + g_clear_object (&node->background_image); + node->background_position_set = FALSE; + node->background_size = ST_BACKGROUND_SIZE_AUTO; + + for (term = decl->value; term; term = term->next) + { + GetFromTermResult result = get_color_from_term (node, term, &node->background_color); + if (result == VALUE_FOUND) + { + /* color stored in node->background_color */ + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + { + st_theme_node_get_background_color (node->parent_node, &node->background_color); + node->background_image = g_object_ref (st_theme_node_get_background_image (node->parent_node)); + } + } + else if (term_is_none (term)) + { + /* leave node->background_color as transparent */ + } + else if (term->type == TERM_URI) + { + CRStyleSheet *base_stylesheet; + GFile *file; + + if (decl->parent_statement != NULL) + base_stylesheet = decl->parent_statement->parent_sheet; + else + base_stylesheet = NULL; + + file = _st_theme_resolve_url (node->theme, + base_stylesheet, + term->content.str->stryng->str); + + node->background_image = file; + } + } + } + else if (strcmp (property_name, "-position") == 0) + { + GetFromTermResult result = get_length_from_term_int (node, decl->value, FALSE, &node->background_position_x); + if (result == VALUE_NOT_FOUND) + { + node->background_position_set = FALSE; + continue; + } + else + node->background_position_set = TRUE; + + result = get_length_from_term_int (node, decl->value->next, FALSE, &node->background_position_y); + + if (result == VALUE_NOT_FOUND) + { + node->background_position_set = FALSE; + continue; + } + else + node->background_position_set = TRUE; + } + else if (strcmp (property_name, "-repeat") == 0) + { + if (decl->value->type == TERM_IDENT) + { + if (strcmp (decl->value->content.str->stryng->str, "repeat") == 0) + node->background_repeat = TRUE; + } + } + else if (strcmp (property_name, "-size") == 0) + { + if (decl->value->type == TERM_IDENT) + { + if (strcmp (decl->value->content.str->stryng->str, "contain") == 0) + node->background_size = ST_BACKGROUND_SIZE_CONTAIN; + else if (strcmp (decl->value->content.str->stryng->str, "cover") == 0) + node->background_size = ST_BACKGROUND_SIZE_COVER; + else if ((strcmp (decl->value->content.str->stryng->str, "auto") == 0) && (decl->value->next) && (decl->value->next->type == TERM_NUMBER)) + { + GetFromTermResult result = get_length_from_term_int (node, decl->value->next, FALSE, &node->background_size_h); + + node->background_size_w = -1; + node->background_size = (result == VALUE_FOUND) ? ST_BACKGROUND_SIZE_FIXED : ST_BACKGROUND_SIZE_AUTO; + } + else + node->background_size = ST_BACKGROUND_SIZE_AUTO; + } + else if (decl->value->type == TERM_NUMBER) + { + GetFromTermResult result = get_length_from_term_int (node, decl->value, FALSE, &node->background_size_w); + if (result == VALUE_NOT_FOUND) + continue; + + node->background_size = ST_BACKGROUND_SIZE_FIXED; + + if ((decl->value->next) && (decl->value->next->type == TERM_NUMBER)) + { + result = get_length_from_term_int (node, decl->value->next, FALSE, &node->background_size_h); + + if (result == VALUE_FOUND) + continue; + } + node->background_size_h = -1; + } + else + node->background_size = ST_BACKGROUND_SIZE_AUTO; + } + else if (strcmp (property_name, "-color") == 0) + { + GetFromTermResult result; + + if (decl->value == NULL || decl->value->next != NULL) + continue; + + result = get_color_from_term (node, decl->value, &node->background_color); + if (result == VALUE_FOUND) + { + /* color stored in node->background_color */ + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + st_theme_node_get_background_color (node->parent_node, &node->background_color); + } + } + else if (strcmp (property_name, "-image") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + continue; + + if (decl->value->type == TERM_URI) + { + CRStyleSheet *base_stylesheet; + + if (decl->parent_statement != NULL) + base_stylesheet = decl->parent_statement->parent_sheet; + else + base_stylesheet = NULL; + + g_clear_object (&node->background_image); + node->background_image = _st_theme_resolve_url (node->theme, + base_stylesheet, + decl->value->content.str->stryng->str); + } + else if (term_is_inherit (decl->value)) + { + g_clear_object (&node->background_image); + node->background_image = g_object_ref (st_theme_node_get_background_image (node->parent_node)); + } + else if (term_is_none (decl->value)) + { + g_clear_object (&node->background_image); + } + } + else if (strcmp (property_name, "-gradient-direction") == 0) + { + CRTerm *term = decl->value; + if (strcmp (term->content.str->stryng->str, "vertical") == 0) + { + node->background_gradient_type = ST_GRADIENT_VERTICAL; + } + else if (strcmp (term->content.str->stryng->str, "horizontal") == 0) + { + node->background_gradient_type = ST_GRADIENT_HORIZONTAL; + } + else if (strcmp (term->content.str->stryng->str, "radial") == 0) + { + node->background_gradient_type = ST_GRADIENT_RADIAL; + } + else if (strcmp (term->content.str->stryng->str, "none") == 0) + { + node->background_gradient_type = ST_GRADIENT_NONE; + } + else + { + g_warning ("Unrecognized background-gradient-direction \"%s\"", + term->content.str->stryng->str); + } + } + else if (strcmp (property_name, "-gradient-start") == 0) + { + get_color_from_term (node, decl->value, &node->background_color); + } + else if (strcmp (property_name, "-gradient-end") == 0) + { + get_color_from_term (node, decl->value, &node->background_gradient_end); + } + } +} + +/** + * st_theme_node_get_background_color: + * @node: a #StThemeNode + * @color: (out caller-allocates): location to store the color + * + * Gets @node's background color. + */ +void +st_theme_node_get_background_color (StThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_background (node); + + *color = node->background_color; +} + +/** + * st_theme_node_get_background_image: + * @node: a #StThemeNode + * + * Returns: (transfer none): @node's background image. + */ +GFile * +st_theme_node_get_background_image (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + _st_theme_node_ensure_background (node); + + return node->background_image; +} + +/** + * st_theme_node_get_foreground_color: + * @node: a #StThemeNode + * @color: (out caller-allocates): location to store the color + * + * Gets @node's foreground color. + */ +void +st_theme_node_get_foreground_color (StThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + if (!node->foreground_computed) + { + int i; + + node->foreground_computed = TRUE; + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "color") == 0) + { + GetFromTermResult result = get_color_from_term (node, decl->value, &node->foreground_color); + if (result == VALUE_FOUND) + goto out; + else if (result == VALUE_INHERIT) + break; + } + } + + if (node->parent_node) + st_theme_node_get_foreground_color (node->parent_node, &node->foreground_color); + else + node->foreground_color = BLACK_COLOR; /* default to black */ + } + + out: + *color = node->foreground_color; +} + + +/** + * st_theme_node_get_background_gradient: + * @node: A #StThemeNode + * @type: (out): Type of gradient + * @start: (out caller-allocates): Color at start of gradient + * @end: (out caller-allocates): Color at end of gradient + * + * The @start and @end arguments will only be set if @type is not #ST_GRADIENT_NONE. + */ +void +st_theme_node_get_background_gradient (StThemeNode *node, + StGradientType *type, + ClutterColor *start, + ClutterColor *end) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_background (node); + + *type = node->background_gradient_type; + if (*type != ST_GRADIENT_NONE) + { + *start = node->background_color; + *end = node->background_gradient_end; + } +} + +/** + * st_theme_node_get_border_color: + * @node: a #StThemeNode + * @side: a #StSide + * @color: (out caller-allocates): location to store the color + * + * Gets the color of @node's border on @side + */ +void +st_theme_node_get_border_color (StThemeNode *node, + StSide side, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT); + + _st_theme_node_ensure_geometry (node); + + *color = node->border_color[side]; +} + +/** + * st_theme_node_get_padding: + * @node: a #StThemeNode + * @side: a #StSide + * + * Get the padding for @node on @side, in physical pixels. This corresponds to + * the CSS properties such as `padding-top`. + * + * Returns: the padding size in physical pixels + */ +double +st_theme_node_get_padding (StThemeNode *node, + StSide side) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT, 0.); + + _st_theme_node_ensure_geometry (node); + + return node->padding[side]; +} + +/** + * st_theme_node_get_margin: + * @node: a #StThemeNode + * @side: a #StSide + * + * Get the margin for @node on @side, in physical pixels. This corresponds to + * the CSS properties such as `margin-top`. + * + * Returns: the margin size in physical pixels + */ +double +st_theme_node_get_margin (StThemeNode *node, + StSide side) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT, 0.); + + _st_theme_node_ensure_geometry (node); + + return node->margin[side]; +} + +/** + * st_theme_node_get_transition_duration: + * @node: an #StThemeNode + * + * Get the value of the transition-duration property, which + * specifies the transition time between the previous #StThemeNode + * and @node. + * + * Returns: the node's transition duration in milliseconds + */ +int +st_theme_node_get_transition_duration (StThemeNode *node) +{ + StSettings *settings; + gdouble value = 0.0; + gdouble factor; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0); + + settings = st_settings_get (); + g_object_get (settings, "slow-down-factor", &factor, NULL); + + if (node->transition_duration > -1) + return factor * node->transition_duration; + + st_theme_node_lookup_time (node, "transition-duration", FALSE, &value); + + node->transition_duration = (int)value; + + return factor * node->transition_duration; +} + +/** + * st_theme_node_get_icon_style: + * @node: a #StThemeNode + * + * Get the icon style for @node (eg. symbolic, regular). This corresponds to the + * special `-st-icon-style` CSS property. + * + * Returns: the icon style for @node + */ +StIconStyle +st_theme_node_get_icon_style (StThemeNode *node) +{ + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), ST_ICON_STYLE_REQUESTED); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "-st-icon-style") == 0) + { + CRTerm *term; + + for (term = decl->value; term; term = term->next) + { + if (term->type != TERM_IDENT) + goto next_decl; + + if (strcmp (term->content.str->stryng->str, "requested") == 0) + return ST_ICON_STYLE_REQUESTED; + else if (strcmp (term->content.str->stryng->str, "regular") == 0) + return ST_ICON_STYLE_REGULAR; + else if (strcmp (term->content.str->stryng->str, "symbolic") == 0) + return ST_ICON_STYLE_SYMBOLIC; + else + g_warning ("Unknown -st-icon-style \"%s\"", + term->content.str->stryng->str); + } + } + + next_decl: + ; + } + + if (node->parent_node) + return st_theme_node_get_icon_style (node->parent_node); + + return ST_ICON_STYLE_REQUESTED; +} + +/** + * st_theme_node_get_text_decoration + * @node: a #StThemeNode + * + * Get the text decoration for @node (eg. underline, line-through, etc). + * + * Returns: the text decoration for @node + */ +StTextDecoration +st_theme_node_get_text_decoration (StThemeNode *node) +{ + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), 0); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "text-decoration") == 0) + { + CRTerm *term = decl->value; + StTextDecoration decoration = 0; + + /* Specification is none | [ underline || overline || line-through || blink ] | inherit + * + * We're a bit more liberal, and for example treat 'underline none' as the same as + * none. + */ + for (; term; term = term->next) + { + if (term->type != TERM_IDENT) + goto next_decl; + + if (strcmp (term->content.str->stryng->str, "none") == 0) + { + return 0; + } + else if (strcmp (term->content.str->stryng->str, "inherit") == 0) + { + if (node->parent_node) + return st_theme_node_get_text_decoration (node->parent_node); + } + else if (strcmp (term->content.str->stryng->str, "underline") == 0) + { + decoration |= ST_TEXT_DECORATION_UNDERLINE; + } + else if (strcmp (term->content.str->stryng->str, "overline") == 0) + { + decoration |= ST_TEXT_DECORATION_OVERLINE; + } + else if (strcmp (term->content.str->stryng->str, "line-through") == 0) + { + decoration |= ST_TEXT_DECORATION_LINE_THROUGH; + } + else if (strcmp (term->content.str->stryng->str, "blink") == 0) + { + decoration |= ST_TEXT_DECORATION_BLINK; + } + else + { + goto next_decl; + } + } + + return decoration; + } + + next_decl: + ; + } + + return 0; +} + +/** + * st_theme_node_get_text_align: + * @node: a #StThemeNode + * + * Get the text alignment of @node. + * + * Returns: the alignment of text for @node + */ +StTextAlign +st_theme_node_get_text_align(StThemeNode *node) +{ + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE(node), ST_TEXT_ALIGN_LEFT); + + ensure_properties(node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp(decl->property->stryng->str, "text-align") == 0) + { + CRTerm *term = decl->value; + + if (term->type != TERM_IDENT || term->next) + continue; + + if (strcmp(term->content.str->stryng->str, "inherit") == 0) + { + if (node->parent_node) + return st_theme_node_get_text_align(node->parent_node); + return ST_TEXT_ALIGN_LEFT; + } + else if (strcmp(term->content.str->stryng->str, "left") == 0) + { + return ST_TEXT_ALIGN_LEFT; + } + else if (strcmp(term->content.str->stryng->str, "right") == 0) + { + return ST_TEXT_ALIGN_RIGHT; + } + else if (strcmp(term->content.str->stryng->str, "center") == 0) + { + return ST_TEXT_ALIGN_CENTER; + } + else if (strcmp(term->content.str->stryng->str, "justify") == 0) + { + return ST_TEXT_ALIGN_JUSTIFY; + } + } + } + if(node->parent_node) + return st_theme_node_get_text_align(node->parent_node); + + if (clutter_get_default_text_direction () == CLUTTER_TEXT_DIRECTION_RTL) + return ST_TEXT_ALIGN_RIGHT; + return ST_TEXT_ALIGN_LEFT; +} + +/** + * st_theme_node_get_letter_spacing: + * @node: a #StThemeNode + * + * Gets the value for the letter-spacing style property, in physical pixels. + * + * Returns: the value of the letter-spacing property, if + * found, or zero if such property has not been found. + */ +gdouble +st_theme_node_get_letter_spacing (StThemeNode *node) +{ + gdouble spacing = 0.; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), spacing); + + ensure_properties (node); + + st_theme_node_lookup_length (node, "letter-spacing", FALSE, &spacing); + return spacing; +} + +static gboolean +font_family_from_terms (CRTerm *term, + char **family) +{ + GString *family_string; + gboolean result = FALSE; + gboolean last_was_quoted = FALSE; + + if (!term) + return FALSE; + + family_string = g_string_new (NULL); + + while (term) + { + if (term->type != TERM_STRING && term->type != TERM_IDENT) + { + goto out; + } + + if (family_string->len > 0) + { + if (term->the_operator != COMMA && term->the_operator != NO_OP) + goto out; + /* Can concatenate two bare words, but not two quoted strings */ + if ((term->the_operator == NO_OP && last_was_quoted) || term->type == TERM_STRING) + goto out; + + if (term->the_operator == NO_OP) + g_string_append (family_string, " "); + else + g_string_append (family_string, ","); + } + else + { + if (term->the_operator != NO_OP) + goto out; + } + + g_string_append (family_string, term->content.str->stryng->str); + + term = term->next; + } + + result = TRUE; + + out: + if (result) + { + *family = g_string_free (family_string, FALSE); + return TRUE; + } + else + { + *family = g_string_free (family_string, TRUE); + return FALSE; + } +} + +/* In points */ +static int font_sizes[] = { + 6 * 1024, /* xx-small */ + 8 * 1024, /* x-small */ + 10 * 1024, /* small */ + 12 * 1024, /* medium */ + 16 * 1024, /* large */ + 20 * 1024, /* x-large */ + 24 * 1024, /* xx-large */ +}; + +static gboolean +font_size_from_term (StThemeNode *node, + CRTerm *term, + double *size) +{ + if (term->type == TERM_IDENT) + { + double resolution = clutter_backend_get_resolution (clutter_get_default_backend ()); + /* We work in integers to avoid double comparisons when converting back + * from a size in pixels to a logical size. + */ + int size_points = (int)(0.5 + *size * (72. / resolution)); + + if (strcmp (term->content.str->stryng->str, "xx-small") == 0) + size_points = font_sizes[0]; + else if (strcmp (term->content.str->stryng->str, "x-small") == 0) + size_points = font_sizes[1]; + else if (strcmp (term->content.str->stryng->str, "small") == 0) + size_points = font_sizes[2]; + else if (strcmp (term->content.str->stryng->str, "medium") == 0) + size_points = font_sizes[3]; + else if (strcmp (term->content.str->stryng->str, "large") == 0) + size_points = font_sizes[4]; + else if (strcmp (term->content.str->stryng->str, "x-large") == 0) + size_points = font_sizes[5]; + else if (strcmp (term->content.str->stryng->str, "xx-large") == 0) + size_points = font_sizes[6]; + else if (strcmp (term->content.str->stryng->str, "smaller") == 0) + { + /* Find the standard size equal to or smaller than the current size */ + int i = 0; + + while (i <= 6 && font_sizes[i] < size_points) + i++; + + if (i > 6) + { + /* original size greater than any standard size */ + size_points = (int)(0.5 + size_points / 1.2); + } + else + { + /* Go one smaller than that, if possible */ + if (i > 0) + i--; + + size_points = font_sizes[i]; + } + } + else if (strcmp (term->content.str->stryng->str, "larger") == 0) + { + /* Find the standard size equal to or larger than the current size */ + int i = 6; + + while (i >= 0 && font_sizes[i] > size_points) + i--; + + if (i < 0) /* original size smaller than any standard size */ + i = 0; + + /* Go one larger than that, if possible */ + if (i < 6) + i++; + + size_points = font_sizes[i]; + } + else + { + return FALSE; + } + + *size = size_points * (resolution / 72.); + return TRUE; + + } + else if (term->type == TERM_NUMBER && term->content.num->type == NUM_PERCENTAGE) + { + *size *= term->content.num->val / 100.; + return TRUE; + } + else if (get_length_from_term (node, term, TRUE, size) == VALUE_FOUND) + { + /* Convert from pixels to Pango units */ + *size *= 1024; + return TRUE; + } + + return FALSE; +} + +static gboolean +font_weight_from_term (CRTerm *term, + PangoWeight *weight, + gboolean *weight_absolute) +{ + if (term->type == TERM_NUMBER) + { + int weight_int; + + /* The spec only allows numeric weights from 100-900, though Pango + * will handle any number. We just let anything through. + */ + if (term->content.num->type != NUM_GENERIC) + return FALSE; + + weight_int = (int)(0.5 + term->content.num->val); + + *weight = weight_int; + *weight_absolute = TRUE; + + } + else if (term->type == TERM_IDENT) + { + /* FIXME: handle INHERIT */ + + if (strcmp (term->content.str->stryng->str, "bold") == 0) + { + *weight = PANGO_WEIGHT_BOLD; + *weight_absolute = TRUE; + } + else if (strcmp (term->content.str->stryng->str, "normal") == 0) + { + *weight = PANGO_WEIGHT_NORMAL; + *weight_absolute = TRUE; + } + else if (strcmp (term->content.str->stryng->str, "bolder") == 0) + { + *weight = PANGO_WEIGHT_BOLD; + *weight_absolute = FALSE; + } + else if (strcmp (term->content.str->stryng->str, "lighter") == 0) + { + *weight = PANGO_WEIGHT_LIGHT; + *weight_absolute = FALSE; + } + else + { + return FALSE; + } + + } + else + { + return FALSE; + } + + return TRUE; +} + +static gboolean +font_style_from_term (CRTerm *term, + PangoStyle *style) +{ + if (term->type != TERM_IDENT) + return FALSE; + + /* FIXME: handle INHERIT */ + + if (strcmp (term->content.str->stryng->str, "normal") == 0) + *style = PANGO_STYLE_NORMAL; + else if (strcmp (term->content.str->stryng->str, "oblique") == 0) + *style = PANGO_STYLE_OBLIQUE; + else if (strcmp (term->content.str->stryng->str, "italic") == 0) + *style = PANGO_STYLE_ITALIC; + else + return FALSE; + + return TRUE; +} + +static gboolean +font_variant_from_term (CRTerm *term, + PangoVariant *variant) +{ + if (term->type != TERM_IDENT) + return FALSE; + + /* FIXME: handle INHERIT */ + + if (strcmp (term->content.str->stryng->str, "normal") == 0) + *variant = PANGO_VARIANT_NORMAL; + else if (strcmp (term->content.str->stryng->str, "small-caps") == 0) + *variant = PANGO_VARIANT_SMALL_CAPS; + else + return FALSE; + + return TRUE; +} + +/** + * st_theme_node_get_font: + * @node: a #StThemeNode + * + * Get the current font of @node as a #PangoFontDescription + * + * Returns: (transfer none): the current font + */ +const PangoFontDescription * +st_theme_node_get_font (StThemeNode *node) +{ + /* Initialized despite _set flags to suppress compiler warnings */ + PangoStyle font_style = PANGO_STYLE_NORMAL; + gboolean font_style_set = FALSE; + PangoVariant variant = PANGO_VARIANT_NORMAL; + gboolean variant_set = FALSE; + PangoWeight weight = PANGO_WEIGHT_NORMAL; + gboolean weight_absolute = TRUE; + gboolean weight_set = FALSE; + double size = 0.; + gboolean size_set = FALSE; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + char *family = NULL; + double parent_size; + int i; + + if (node->font_desc) + return node->font_desc; + + node->font_desc = pango_font_description_copy (get_parent_font (node)); + parent_size = pango_font_description_get_size (node->font_desc); + if (!pango_font_description_get_size_is_absolute (node->font_desc)) + { + double resolution = clutter_backend_get_resolution (clutter_get_default_backend ()); + parent_size *= (resolution / 72.); + } + + ensure_properties (node); + + for (i = 0; i < node->n_properties; i++) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "font") == 0) + { + PangoStyle tmp_style = PANGO_STYLE_NORMAL; + PangoVariant tmp_variant = PANGO_VARIANT_NORMAL; + PangoWeight tmp_weight = PANGO_WEIGHT_NORMAL; + gboolean tmp_weight_absolute = TRUE; + double tmp_size; + CRTerm *term = decl->value; + + /* A font specification starts with node/variant/weight + * in any order. Each is allowed to be specified only once, + * but we don't enforce that. + */ + for (; term; term = term->next) + { + if (font_style_from_term (term, &tmp_style)) + continue; + if (font_variant_from_term (term, &tmp_variant)) + continue; + if (font_weight_from_term (term, &tmp_weight, &tmp_weight_absolute)) + continue; + + break; + } + + /* The size is mandatory */ + + if (term == NULL || term->type != TERM_NUMBER) + { + g_warning ("Size missing from font property"); + continue; + } + + tmp_size = parent_size; + if (!font_size_from_term (node, term, &tmp_size)) + { + g_warning ("Couldn't parse size in font property"); + continue; + } + + term = term->next; + + if (term != NULL && term->type && TERM_NUMBER && term->the_operator == DIVIDE) + { + /* Ignore line-height specification */ + term = term->next; + } + + /* the font family is mandatory - it is a comma-separated list of + * names. + */ + if (!font_family_from_terms (term, &family)) + { + g_warning ("Couldn't parse family in font property"); + continue; + } + + font_style = tmp_style; + font_style_set = TRUE; + weight = tmp_weight; + weight_absolute = tmp_weight_absolute; + weight_set = TRUE; + variant = tmp_variant; + variant_set = TRUE; + + size = tmp_size; + size_set = TRUE; + + } + else if (strcmp (decl->property->stryng->str, "font-family") == 0) + { + if (!font_family_from_terms (decl->value, &family)) + { + g_warning ("Couldn't parse family in font property"); + continue; + } + } + else if (strcmp (decl->property->stryng->str, "font-weight") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + continue; + + if (font_weight_from_term (decl->value, &weight, &weight_absolute)) + weight_set = TRUE; + } + else if (strcmp (decl->property->stryng->str, "font-style") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + continue; + + if (font_style_from_term (decl->value, &font_style)) + font_style_set = TRUE; + } + else if (strcmp (decl->property->stryng->str, "font-variant") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + continue; + + if (font_variant_from_term (decl->value, &variant)) + variant_set = TRUE; + } + else if (strcmp (decl->property->stryng->str, "font-size") == 0) + { + gdouble tmp_size; + if (decl->value == NULL || decl->value->next != NULL) + continue; + + tmp_size = parent_size; + if (font_size_from_term (node, decl->value, &tmp_size)) + { + size = tmp_size; + size_set = TRUE; + } + } + } + + if (family) + { + pango_font_description_set_family (node->font_desc, family); + g_free (family); + } + + if (size_set) + pango_font_description_set_absolute_size (node->font_desc, size); + + if (weight_set) + { + if (!weight_absolute) + { + /* bolder/lighter are supposed to switch between available styles, but with + * font substitution, that gets to be a pretty fuzzy concept. So we use + * a fixed step of 200. (The spec says 100, but that might not take us from + * normal to bold. + */ + + PangoWeight old_weight = pango_font_description_get_weight (node->font_desc); + if (weight == PANGO_WEIGHT_BOLD) + weight = old_weight + 200; + else + weight = old_weight - 200; + + if (weight < 100) + weight = 100; + if (weight > 900) + weight = 900; + } + + pango_font_description_set_weight (node->font_desc, weight); + } + + if (font_style_set) + pango_font_description_set_style (node->font_desc, font_style); + if (variant_set) + pango_font_description_set_variant (node->font_desc, variant); + + return node->font_desc; +} + +/** + * st_theme_node_get_font_features: + * @node: a #StThemeNode + * + * Get the CSS font-features for @node. + * + * Returns: (transfer full): font-features as a string + */ +gchar * +st_theme_node_get_font_features (StThemeNode *node) +{ + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "font-feature-settings") == 0) + { + CRTerm *term = decl->value; + + if (!term->next && term->type == TERM_IDENT) + { + gchar *ident = term->content.str->stryng->str; + + if (strcmp (ident, "inherit") == 0) + break; + + if (strcmp (ident, "normal") == 0) + return NULL; + } + + return (gchar *)cr_term_to_string (term); + } + } + + return node->parent_node ? st_theme_node_get_font_features (node->parent_node) : NULL; +} + +/** + * st_theme_node_get_border_image: + * @node: a #StThemeNode + * + * Gets the value for the border-image style property + * + * Returns: (transfer none): the border image, or %NULL + * if there is no border image. + */ +StBorderImage * +st_theme_node_get_border_image (StThemeNode *node) +{ + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + if (node->border_image_computed) + return node->border_image; + + node->border_image = NULL; + node->border_image_computed = TRUE; + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "border-image") == 0) + { + CRTerm *term = decl->value; + CRStyleSheet *base_stylesheet; + int borders[4]; + int n_borders = 0; + int j; + + const char *url; + int border_top; + int border_right; + int border_bottom; + int border_left; + + GFile *file; + + /* Support border-image: none; to suppress a previously specified border image */ + if (term_is_none (term)) + { + if (term->next == NULL) + return NULL; + else + goto next_property; + } + + /* First term must be the URL to the image */ + if (term->type != TERM_URI) + goto next_property; + + url = term->content.str->stryng->str; + + term = term->next; + + /* Followed by 0 to 4 numbers or percentages. *Not lengths*. The interpretation + * of a number is supposed to be pixels if the image is pixel based, otherwise CSS pixels. + */ + for (j = 0; j < 4; j++) + { + if (term == NULL) + break; + + if (term->type != TERM_NUMBER) + goto next_property; + + if (term->content.num->type == NUM_GENERIC) + { + borders[n_borders] = (int)(0.5 + term->content.num->val); + n_borders++; + } + else if (term->content.num->type == NUM_PERCENTAGE) + { + /* This would be easiest to support if we moved image handling into StBorderImage */ + g_warning ("Percentages not supported for border-image"); + goto next_property; + } + else + goto next_property; + + term = term->next; + } + + switch (n_borders) + { + case 0: + border_top = border_right = border_bottom = border_left = 0; + break; + case 1: + border_top = border_right = border_bottom = border_left = borders[0]; + break; + case 2: + border_top = border_bottom = borders[0]; + border_left = border_right = borders[1]; + break; + case 3: + border_top = borders[0]; + border_left = border_right = borders[1]; + border_bottom = borders[2]; + break; + case 4: + default: + border_top = borders[0]; + border_right = borders[1]; + border_bottom = borders[2]; + border_left = borders[3]; + break; + } + + if (decl->parent_statement != NULL) + base_stylesheet = decl->parent_statement->parent_sheet; + else + base_stylesheet = NULL; + + file = _st_theme_resolve_url (node->theme, base_stylesheet, url); + + if (file == NULL) + goto next_property; + + node->border_image = st_border_image_new (file, + border_top, border_right, border_bottom, border_left, + node->cached_scale_factor); + + g_object_unref (file); + + return node->border_image; + } + + next_property: + ; + } + + return NULL; +} + +/** + * st_theme_node_get_horizontal_padding: + * @node: a #StThemeNode + * + * Gets the total horizontal padding (left + right padding), in physical pixels. + * + * Returns: the total horizontal padding in physical pixels + */ +double +st_theme_node_get_horizontal_padding (StThemeNode *node) +{ + double padding = 0.0; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), padding); + + padding += st_theme_node_get_padding (node, ST_SIDE_LEFT); + padding += st_theme_node_get_padding (node, ST_SIDE_RIGHT); + + return padding; +} + +/** + * st_theme_node_get_vertical_padding: + * @node: a #StThemeNode + * + * Gets the total vertical padding (top + bottom padding), in physical pixels. + * + * Returns: the total vertical padding in physical pixels + */ +double +st_theme_node_get_vertical_padding (StThemeNode *node) +{ + double padding = 0.0; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), padding); + + padding += st_theme_node_get_padding (node, ST_SIDE_TOP); + padding += st_theme_node_get_padding (node, ST_SIDE_BOTTOM); + + return padding; +} + +void +_st_theme_node_apply_margins (StThemeNode *node, + ClutterActor *actor) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_geometry (node); + + clutter_actor_set_margin_left (actor, st_theme_node_get_margin(node, ST_SIDE_LEFT)); + clutter_actor_set_margin_right (actor, st_theme_node_get_margin(node, ST_SIDE_RIGHT)); + clutter_actor_set_margin_top (actor, st_theme_node_get_margin(node, ST_SIDE_TOP)); + clutter_actor_set_margin_bottom (actor, st_theme_node_get_margin(node, ST_SIDE_BOTTOM)); +} + +static GetFromTermResult +parse_shadow_property (StThemeNode *node, + CRDeclaration *decl, + ClutterColor *color, + gdouble *xoffset, + gdouble *yoffset, + gdouble *blur, + gdouble *spread, + gboolean *inset, + gboolean *is_none) +{ + GetFromTermResult result; + CRTerm *term; + int n_offsets = 0; + *is_none = FALSE; + + /* default values */ + color->red = 0x0; color->green = 0x0; color->blue = 0x0; color->alpha = 0xff; + *xoffset = 0.; + *yoffset = 0.; + *blur = 0.; + *spread = 0.; + *inset = FALSE; + + /* The CSS3 draft of the box-shadow property[0] is a lot stricter + * regarding the order of terms: + * If the 'inset' keyword is specified, it has to be first or last, + * and the color may not be mixed with the lengths; while we parse + * length values in the correct order, we allow for arbitrary + * placement of the color and 'inset' keyword. + * + * [0] http://www.w3.org/TR/css3-background/#box-shadow + */ + for (term = decl->value; term; term = term->next) + { + /* if we found "none", we're all set with the default values */ + if (term_is_none (term)) { + *is_none = TRUE; + return VALUE_FOUND; + } + + if (term->type == TERM_NUMBER) + { + gdouble value; + gdouble multiplier; + + multiplier = (term->unary_op == MINUS_UOP) ? -1. : 1.; + result = get_length_from_term (node, term, FALSE, &value); + + if (result == VALUE_INHERIT) + { + /* we only allow inherit on the line by itself */ + if (n_offsets > 0) + return VALUE_NOT_FOUND; + else + return VALUE_INHERIT; + } + else if (result == VALUE_FOUND) + { + switch (n_offsets++) + { + case 0: + *xoffset = multiplier * value; + break; + case 1: + *yoffset = multiplier * value; + break; + case 2: + if (multiplier < 0) + g_warning ("Negative blur values are " + "not allowed"); + *blur = value; + break; + case 3: + if (multiplier < 0) + g_warning ("Negative spread values are " + "not allowed"); + *spread = value; + break; + default: + g_warning ("Ignoring excess values in shadow definition"); + break; + } + continue; + } + } + else if (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "inset") == 0) + { + *inset = TRUE; + continue; + } + + result = get_color_from_term (node, term, color); + + if (result == VALUE_INHERIT) + { + if (n_offsets > 0) + return VALUE_NOT_FOUND; + else + return VALUE_INHERIT; + } + else if (result == VALUE_FOUND) + { + continue; + } + } + + /* The only required terms are the x and y offsets + */ + if (n_offsets >= 2) + return VALUE_FOUND; + else + return VALUE_NOT_FOUND; +} + +/** + * st_theme_node_lookup_shadow: + * @node: a #StThemeNode + * @property_name: The name of the shadow property + * @inherit: if %TRUE, if a value is not found for the property on the + * node, then it will be looked up on the parent node, and then on the + * parent's parent, and so forth. Note that if the property has a + * value of 'inherit' it will be inherited even if %FALSE is passed + * in for @inherit; this only affects the default behavior for inheritance. + * @shadow: (out): location to store the shadow + * + * If the property is not found, the value in the shadow variable will not + * be changed. + * + * Generically looks up a property containing a set of shadow values. When + * specific getters (like st_theme_node_get_box_shadow ()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * See also st_theme_node_get_shadow(), which provides a simpler API. + * + * Returns: %TRUE if the property was found in the properties for this + * theme node (or in the properties of parent nodes when inheriting.), %FALSE + * if the property was not found, or was explicitly set to 'none'. + */ +gboolean +st_theme_node_lookup_shadow (StThemeNode *node, + const char *property_name, + gboolean inherit, + StShadow **shadow) +{ + ClutterColor color = { 0., }; + gdouble xoffset = 0.; + gdouble yoffset = 0.; + gdouble blur = 0.; + gdouble spread = 0.; + gboolean inset = FALSE; + gboolean is_none = FALSE; + + int i; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), FALSE); + g_return_val_if_fail (property_name != NULL, FALSE); + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, property_name) == 0) + { + GetFromTermResult result = parse_shadow_property (node, + decl, + &color, + &xoffset, + &yoffset, + &blur, + &spread, + &inset, + &is_none); + if (result == VALUE_FOUND) + { + if (is_none) + return FALSE; + + *shadow = st_shadow_new (&color, + xoffset, yoffset, + blur, spread, + inset); + return TRUE; + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + return st_theme_node_lookup_shadow (node->parent_node, + property_name, + inherit, + shadow); + else + break; + } + } + } + + if (inherit && node->parent_node) + return st_theme_node_lookup_shadow (node->parent_node, + property_name, + inherit, + shadow); + + return FALSE; +} + +/** + * st_theme_node_get_shadow: + * @node: a #StThemeNode + * @property_name: The name of the shadow property + * + * Generically looks up a property containing a set of shadow values. When + * specific getters (like st_theme_node_get_box_shadow()) exist, they + * should be used instead. They are cached, so more efficient, and have + * handling for shortcut properties and other details of CSS. + * + * Like st_theme_get_length(), this does not print a warning if the property is + * not found; it just returns %NULL + * + * See also st_theme_node_lookup_shadow (), which provides more options. + * + * Returns: (nullable) (transfer full): the shadow, or %NULL if the property was + * not found. + */ +StShadow * +st_theme_node_get_shadow (StThemeNode *node, + const char *property_name) +{ + StShadow *shadow; + + if (st_theme_node_lookup_shadow (node, property_name, FALSE, &shadow)) + return shadow; + else + return NULL; +} + +/** + * st_theme_node_get_box_shadow: + * @node: a #StThemeNode + * + * Gets the value for the box-shadow style property + * + * Returns: (nullable) (transfer none): the node's shadow, or %NULL + * if node has no shadow + */ +StShadow * +st_theme_node_get_box_shadow (StThemeNode *node) +{ + StShadow *shadow; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + if (node->box_shadow_computed) + return node->box_shadow; + + node->box_shadow = NULL; + node->box_shadow_computed = TRUE; + + if (st_theme_node_lookup_shadow (node, + "box-shadow", + FALSE, + &shadow)) + { + node->box_shadow = shadow; + + return node->box_shadow; + } + + return NULL; +} + +/** + * st_theme_node_get_background_image_shadow: + * @node: a #StThemeNode + * + * Gets the value for the -st-background-image-shadow style property + * + * Returns: (nullable) (transfer none): the node's background image shadow, or + * %NULL if node has no such shadow + */ +StShadow * +st_theme_node_get_background_image_shadow (StThemeNode *node) +{ + StShadow *shadow; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + if (node->background_image_shadow_computed) + return node->background_image_shadow; + + node->background_image_shadow = NULL; + node->background_image_shadow_computed = TRUE; + + if (st_theme_node_lookup_shadow (node, + "-st-background-image-shadow", + FALSE, + &shadow)) + { + if (shadow->inset) + { + g_warning ("The -st-background-image-shadow property does not " + "support inset shadows"); + st_shadow_unref (shadow); + shadow = NULL; + } + + node->background_image_shadow = shadow; + + return node->background_image_shadow; + } + + return NULL; +} + +/** + * st_theme_node_get_text_shadow: + * @node: a #StThemeNode + * + * Gets the value for the text-shadow style property + * + * Returns: (nullable) (transfer none): the node's text-shadow, or %NULL + * if node has no text-shadow + */ +StShadow * +st_theme_node_get_text_shadow (StThemeNode *node) +{ + StShadow *result = NULL; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + if (node->text_shadow_computed) + return node->text_shadow; + + ensure_properties (node); + + if (!st_theme_node_lookup_shadow (node, + "text-shadow", + FALSE, + &result)) + { + if (node->parent_node) + { + result = st_theme_node_get_text_shadow (node->parent_node); + if (result) + st_shadow_ref (result); + } + } + + if (result && result->inset) + { + g_warning ("The text-shadow property does not support inset shadows"); + st_shadow_unref (result); + result = NULL; + } + + node->text_shadow = result; + node->text_shadow_computed = TRUE; + + return result; +} + +/** + * st_theme_node_get_icon_colors: + * @node: a #StThemeNode + * + * Gets the colors that should be used for colorizing symbolic icons according + * the style of this node. + * + * Returns: (transfer none): the icon colors to use for this theme node + */ +StIconColors * +st_theme_node_get_icon_colors (StThemeNode *node) +{ + /* Foreground here will always be the same as st_theme_node_get_foreground_color(), + * but there's a loss of symmetry and little efficiency win if we try to exploit + * that. */ + + enum { + FOREGROUND = 1 << 0, + WARNING = 1 << 1, + ERROR = 1 << 2, + SUCCESS = 1 << 3 + }; + + gboolean shared_with_parent; + int i; + ClutterColor color = { 0, }; + + guint still_need = FOREGROUND | WARNING | ERROR | SUCCESS; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + if (node->icon_colors) + return node->icon_colors; + + if (node->parent_node) + { + node->icon_colors = st_theme_node_get_icon_colors (node->parent_node); + shared_with_parent = TRUE; + } + else + { + node->icon_colors = st_icon_colors_new (); + node->icon_colors->foreground = BLACK_COLOR; + node->icon_colors->warning = DEFAULT_WARNING_COLOR; + node->icon_colors->error = DEFAULT_ERROR_COLOR; + node->icon_colors->success = DEFAULT_SUCCESS_COLOR; + shared_with_parent = FALSE; + } + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0 && still_need != 0; i--) + { + CRDeclaration *decl = node->properties[i]; + GetFromTermResult result = VALUE_NOT_FOUND; + guint found = 0; + + if ((still_need & FOREGROUND) != 0 && + strcmp (decl->property->stryng->str, "color") == 0) + { + found = FOREGROUND; + result = get_color_from_term (node, decl->value, &color); + } + else if ((still_need & WARNING) != 0 && + strcmp (decl->property->stryng->str, "warning-color") == 0) + { + found = WARNING; + result = get_color_from_term (node, decl->value, &color); + } + else if ((still_need & ERROR) != 0 && + strcmp (decl->property->stryng->str, "error-color") == 0) + { + found = ERROR; + result = get_color_from_term (node, decl->value, &color); + } + else if ((still_need & SUCCESS) != 0 && + strcmp (decl->property->stryng->str, "success-color") == 0) + { + found = SUCCESS; + result = get_color_from_term (node, decl->value, &color); + } + + if (result == VALUE_INHERIT) + { + still_need &= ~found; + } + else if (result == VALUE_FOUND) + { + still_need &= ~found; + if (shared_with_parent) + { + node->icon_colors = st_icon_colors_copy (node->icon_colors); + shared_with_parent = FALSE; + } + + switch (found) + { + case FOREGROUND: + node->icon_colors->foreground = color; + break; + case WARNING: + node->icon_colors->warning = color; + break; + case ERROR: + node->icon_colors->error = color; + break; + case SUCCESS: + node->icon_colors->success = color; + break; + default: + g_assert_not_reached(); + break; + } + } + } + + if (shared_with_parent) + st_icon_colors_ref (node->icon_colors); + + return node->icon_colors; +} + +static float +get_width_inc (StThemeNode *node) +{ + return ((int)(0.5 + node->border_width[ST_SIDE_LEFT]) + node->padding[ST_SIDE_LEFT] + + (int)(0.5 + node->border_width[ST_SIDE_RIGHT]) + node->padding[ST_SIDE_RIGHT]); +} + +static float +get_height_inc (StThemeNode *node) +{ + return ((int)(0.5 + node->border_width[ST_SIDE_TOP]) + node->padding[ST_SIDE_TOP] + + (int)(0.5 + node->border_width[ST_SIDE_BOTTOM]) + node->padding[ST_SIDE_BOTTOM]); +} + +/** + * st_theme_node_adjust_for_height: + * @node: a #StThemeNode + * @for_height: (inout): the "for height" to adjust + * + * Adjusts a "for height" passed to clutter_actor_get_preferred_width() to + * account for borders and padding. This is a convenience function meant + * to be called from a get_preferred_width() method of a #ClutterActor + * subclass. The value after adjustment is the height available for the actor's + * content. + */ +void +st_theme_node_adjust_for_height (StThemeNode *node, + float *for_height) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (for_height != NULL); + + if (*for_height >= 0) + { + float height_inc = get_height_inc (node); + *for_height = MAX (0, *for_height - height_inc); + } +} + +/** + * st_theme_node_adjust_preferred_width: + * @node: a #StThemeNode + * @min_width_p: (inout) (nullable): the minimum width to adjust + * @natural_width_p: (inout): the natural width to adjust + * + * Adjusts the minimum and natural width computed for an actor by + * adding on the necessary space for borders and padding and taking + * into account any minimum or maximum width. This is a convenience + * function meant to be called from the get_preferred_width() method + * of a #ClutterActor subclass + */ +void +st_theme_node_adjust_preferred_width (StThemeNode *node, + float *min_width_p, + float *natural_width_p) +{ + float width_inc; + + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_geometry (node); + + width_inc = get_width_inc (node); + + if (min_width_p) + { + if (node->min_width != -1) + *min_width_p = node->min_width; + *min_width_p += width_inc; + } + + if (natural_width_p) + { + if (node->width != -1) + *natural_width_p = MAX (*natural_width_p, node->width); + if (node->max_width != -1) + *natural_width_p = MIN (*natural_width_p, node->max_width); + *natural_width_p += width_inc; + } +} + +/** + * st_theme_node_adjust_for_width: + * @node: a #StThemeNode + * @for_width: (inout): the "for width" to adjust + * + * Adjusts a "for width" passed to clutter_actor_get_preferred_height() to + * account for borders and padding. This is a convenience function meant + * to be called from a get_preferred_height() method of a #ClutterActor + * subclass. The value after adjustment is the width available for the actor's + * content. + */ +void +st_theme_node_adjust_for_width (StThemeNode *node, + float *for_width) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (for_width != NULL); + + if (*for_width >= 0) + { + float width_inc = get_width_inc (node); + *for_width = MAX (0, *for_width - width_inc); + } +} + +/** + * st_theme_node_adjust_preferred_height: + * @node: a #StThemeNode + * @min_height_p: (inout) (nullable): the minimum height to adjust + * @natural_height_p: (inout): the natural height to adjust + * + * Adjusts the minimum and natural height computed for an actor by + * adding on the necessary space for borders and padding and taking + * into account any minimum or maximum height. This is a convenience + * function meant to be called from the get_preferred_height() method + * of a #ClutterActor subclass + */ +void +st_theme_node_adjust_preferred_height (StThemeNode *node, + float *min_height_p, + float *natural_height_p) +{ + float height_inc; + + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_geometry (node); + + height_inc = get_height_inc (node); + + if (min_height_p) + { + if (node->min_height != -1) + *min_height_p = node->min_height; + *min_height_p += height_inc; + } + if (natural_height_p) + { + if (node->height != -1) + *natural_height_p = MAX (*natural_height_p, node->height); + if (node->max_height != -1) + *natural_height_p = MIN (*natural_height_p, node->max_height); + *natural_height_p += height_inc; + } +} + +/** + * st_theme_node_get_content_box: + * @node: a #StThemeNode + * @allocation: the box allocated to a #ClutterAlctor + * @content_box: (out caller-allocates): computed box occupied by the actor's content + * + * Gets the box within an actor's allocation that contents the content + * of an actor (excluding borders and padding). This is a convenience function + * meant to be used from the allocate() or paint() methods of a #ClutterActor + * subclass. + */ +void +st_theme_node_get_content_box (StThemeNode *node, + const ClutterActorBox *allocation, + ClutterActorBox *content_box) +{ + double noncontent_left, noncontent_top, noncontent_right, noncontent_bottom; + double avail_width, avail_height, content_width, content_height; + + g_return_if_fail (ST_IS_THEME_NODE (node)); + + _st_theme_node_ensure_geometry (node); + + avail_width = allocation->x2 - allocation->x1; + avail_height = allocation->y2 - allocation->y1; + + noncontent_left = node->border_width[ST_SIDE_LEFT] + node->padding[ST_SIDE_LEFT]; + noncontent_top = node->border_width[ST_SIDE_TOP] + node->padding[ST_SIDE_TOP]; + noncontent_right = node->border_width[ST_SIDE_RIGHT] + node->padding[ST_SIDE_RIGHT]; + noncontent_bottom = node->border_width[ST_SIDE_BOTTOM] + node->padding[ST_SIDE_BOTTOM]; + + content_box->x1 = (int)(0.5 + noncontent_left); + content_box->y1 = (int)(0.5 + noncontent_top); + + content_width = avail_width - noncontent_left - noncontent_right; + if (content_width < 0) + content_width = 0; + content_height = avail_height - noncontent_top - noncontent_bottom; + if (content_height < 0) + content_height = 0; + + content_box->x2 = (int)(0.5 + content_box->x1 + content_width); + content_box->y2 = (int)(0.5 + content_box->y1 + content_height); +} + +/** + * st_theme_node_get_background_paint_box: + * @node: a #StThemeNode + * @allocation: the box allocated to a #ClutterActor + * @paint_box: (out caller-allocates): computed box occupied when painting the actor's background + * + * Gets the box used to paint the actor's background, including the area + * occupied by properties which paint outside the actor's assigned allocation. + */ +void +st_theme_node_get_background_paint_box (StThemeNode *node, + const ClutterActorBox *actor_box, + ClutterActorBox *paint_box) +{ + StShadow *background_image_shadow; + ClutterActorBox shadow_box; + + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (actor_box != NULL); + g_return_if_fail (paint_box != NULL); + + background_image_shadow = st_theme_node_get_background_image_shadow (node); + + *paint_box = *actor_box; + + if (!background_image_shadow) + return; + + st_shadow_get_box (background_image_shadow, actor_box, &shadow_box); + + paint_box->x1 = MIN (paint_box->x1, shadow_box.x1); + paint_box->x2 = MAX (paint_box->x2, shadow_box.x2); + paint_box->y1 = MIN (paint_box->y1, shadow_box.y1); + paint_box->y2 = MAX (paint_box->y2, shadow_box.y2); +} + +/** + * st_theme_node_get_paint_box: + * @node: a #StThemeNode + * @allocation: the box allocated to a #ClutterActor + * @paint_box: (out caller-allocates): computed box occupied when painting the actor + * + * Gets the box used to paint the actor, including the area occupied + * by properties which paint outside the actor's assigned allocation. + * When painting @node to an offscreen buffer, this function can be + * used to determine the necessary size of the buffer. + */ +void +st_theme_node_get_paint_box (StThemeNode *node, + const ClutterActorBox *actor_box, + ClutterActorBox *paint_box) +{ + StShadow *box_shadow; + ClutterActorBox shadow_box; + int outline_width; + + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (actor_box != NULL); + g_return_if_fail (paint_box != NULL); + + box_shadow = st_theme_node_get_box_shadow (node); + outline_width = st_theme_node_get_outline_width (node); + + st_theme_node_get_background_paint_box (node, actor_box, paint_box); + + if (!box_shadow && !outline_width) + return; + + paint_box->x1 -= outline_width; + paint_box->x2 += outline_width; + paint_box->y1 -= outline_width; + paint_box->y2 += outline_width; + + if (box_shadow) + { + st_shadow_get_box (box_shadow, actor_box, &shadow_box); + + paint_box->x1 = MIN (paint_box->x1, shadow_box.x1); + paint_box->x2 = MAX (paint_box->x2, shadow_box.x2); + paint_box->y1 = MIN (paint_box->y1, shadow_box.y1); + paint_box->y2 = MAX (paint_box->y2, shadow_box.y2); + } +} + +/** + * st_theme_node_geometry_equal: + * @node: a #StThemeNode + * @other: a different #StThemeNode + * + * Tests if two theme nodes have the same borders and padding; this can be + * used to optimize having to relayout when the style applied to a Clutter + * actor changes colors without changing the geometry. + * + * Returns: %TRUE if equal, %FALSE otherwise + */ +gboolean +st_theme_node_geometry_equal (StThemeNode *node, + StThemeNode *other) +{ + StSide side; + + g_return_val_if_fail (ST_IS_THEME_NODE (node), FALSE); + + if (node == other) + return TRUE; + + g_return_val_if_fail (ST_IS_THEME_NODE (other), FALSE); + + if (node->cached_scale_factor != other->cached_scale_factor) + return FALSE; + + _st_theme_node_ensure_geometry (node); + _st_theme_node_ensure_geometry (other); + + for (side = ST_SIDE_TOP; side <= ST_SIDE_LEFT; side++) + { + if (node->border_width[side] != other->border_width[side]) + return FALSE; + if (node->padding[side] != other->padding[side]) + return FALSE; + } + + if (node->width != other->width || node->height != other->height) + return FALSE; + if (node->min_width != other->min_width || node->min_height != other->min_height) + return FALSE; + if (node->max_width != other->max_width || node->max_height != other->max_height) + return FALSE; + + return TRUE; +} + +/** + * st_theme_node_paint_equal: + * @node: (nullable): a #StThemeNode + * @other: (nullable): a different #StThemeNode + * + * Check if st_theme_node_paint() will paint identically for @node as it does + * for @other. Note that in some cases this function may return %TRUE even + * if there is no visible difference in the painting. + * + * Returns: %TRUE if the two theme nodes paint identically. %FALSE if the + * two nodes potentially paint differently. + */ +gboolean +st_theme_node_paint_equal (StThemeNode *node, + StThemeNode *other) +{ + StBorderImage *border_image, *other_border_image; + StShadow *shadow, *other_shadow; + int i; + + /* Make sure NULL != NULL */ + if (node == NULL || other == NULL) + return FALSE; + + if (node == other) + return TRUE; + + _st_theme_node_ensure_background (node); + _st_theme_node_ensure_background (other); + + if (!clutter_color_equal (&node->background_color, &other->background_color)) + return FALSE; + + if (node->background_gradient_type != other->background_gradient_type) + return FALSE; + + if (node->background_gradient_type != ST_GRADIENT_NONE && + !clutter_color_equal (&node->background_gradient_end, &other->background_gradient_end)) + return FALSE; + + if ((node->background_image != NULL) && + (other->background_image != NULL) && + !g_file_equal (node->background_image, other->background_image)) + return FALSE; + + _st_theme_node_ensure_geometry (node); + _st_theme_node_ensure_geometry (other); + + for (i = 0; i < 4; i++) + { + if (node->border_width[i] != other->border_width[i]) + return FALSE; + + if (node->border_width[i] > 0 && + !clutter_color_equal (&node->border_color[i], &other->border_color[i])) + return FALSE; + + if (node->border_radius[i] != other->border_radius[i]) + return FALSE; + } + + if (node->outline_width != other->outline_width) + return FALSE; + + if (node->outline_width > 0 && + !clutter_color_equal (&node->outline_color, &other->outline_color)) + return FALSE; + + border_image = st_theme_node_get_border_image (node); + other_border_image = st_theme_node_get_border_image (other); + + if ((border_image == NULL) != (other_border_image == NULL)) + return FALSE; + + if (border_image != NULL && !st_border_image_equal (border_image, other_border_image)) + return FALSE; + + shadow = st_theme_node_get_box_shadow (node); + other_shadow = st_theme_node_get_box_shadow (other); + + if ((shadow == NULL) != (other_shadow == NULL)) + return FALSE; + + if (shadow != NULL && !st_shadow_equal (shadow, other_shadow)) + return FALSE; + + shadow = st_theme_node_get_background_image_shadow (node); + other_shadow = st_theme_node_get_background_image_shadow (other); + + if ((shadow == NULL) != (other_shadow == NULL)) + return FALSE; + + if (shadow != NULL && !st_shadow_equal (shadow, other_shadow)) + return FALSE; + + return TRUE; +} + +/** + * st_theme_node_to_string: + * @node: a #StThemeNode + * + * Serialize @node to a string of its #GType name, CSS ID, classes and + * pseudo-classes. + * + * Returns: the serialized theme node + */ +gchar * +st_theme_node_to_string (StThemeNode *node) +{ + GString *desc; + gchar **it; + + if (!node) + return g_strdup ("[null]"); + + desc = g_string_new (NULL); + g_string_append_printf (desc, + "[%p %s#%s", + node, + g_type_name (node->element_type), + node->element_id); + + for (it = node->element_classes; it && *it; it++) + g_string_append_printf (desc, ".%s", *it); + + for (it = node->pseudo_classes; it && *it; it++) + g_string_append_printf (desc, ":%s", *it); + + g_string_append_c (desc, ']'); + + return g_string_free (desc, FALSE); +} diff --git a/src/st/st-theme-node.h b/src/st/st-theme-node.h new file mode 100644 index 0000000..520e29f --- /dev/null +++ b/src/st/st-theme-node.h @@ -0,0 +1,368 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-node.h: style information for one node in a tree of themed objects + * + * Copyright 2008-2010 Red Hat, Inc. + * Copyright 2009, 2010 Florian Müllner + * Copyright 2010 Giovanni Campagna + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_THEME_NODE_H__ +#define __ST_THEME_NODE_H__ + +#include <clutter/clutter.h> +#include "st-border-image.h" +#include "st-icon-colors.h" +#include "st-shadow.h" + +G_BEGIN_DECLS + +/** + * SECTION:st-theme-node + * @short_description: style information for one node in a tree of themed objects + * + * A #StThemeNode represents the CSS style information (the set of CSS properties) for one + * node in a tree of themed objects. In typical usage, it represents the style information + * for a single #ClutterActor. A #StThemeNode is immutable: attributes such as the + * CSS classes for the node are passed in at construction. If the attributes of the node + * or any parent node change, the node should be discarded and a new node created. + * #StThemeNode has generic accessors to look up properties by name and specific + * accessors for standard CSS properties that add caching and handling of various + * details of the CSS specification. #StThemeNode also has convenience functions to help + * in implementing a #ClutterActor with borders and padding. + * + * Note that pixel measurements take the #StThemeContext:scale-factor into + * account so all values are in physical pixels, as opposed to logical pixels. + * Physical pixels correspond to actor sizes, not necessarily to pixels on + * display devices (eg. when `scale-monitor-framebuffer` is enabled). + */ + +typedef struct _StTheme StTheme; +typedef struct _StThemeContext StThemeContext; + +#define ST_TYPE_THEME_NODE (st_theme_node_get_type ()) +G_DECLARE_FINAL_TYPE (StThemeNode, st_theme_node, ST, THEME_NODE, GObject) + +/** + * StSide: + * @ST_SIDE_TOP: The top side. + * @ST_SIDE_RIGHT: The right side. + * @ST_SIDE_BOTTOM: The bottom side. + * @ST_SIDE_LEFT: The left side. + * + * Used to target a particular side of a #StThemeNode element. + */ +typedef enum { + ST_SIDE_TOP, + ST_SIDE_RIGHT, + ST_SIDE_BOTTOM, + ST_SIDE_LEFT +} StSide; + +/** + * StCorner: + * @ST_CORNER_TOPLEFT: The top-right corner. + * @ST_CORNER_TOPRIGHT: The top-right corner. + * @ST_CORNER_BOTTOMRIGHT: The bottom-right corner. + * @ST_CORNER_BOTTOMLEFT: The bottom-left corner. + * + * Used to target a particular corner of a #StThemeNode element. + */ +typedef enum { + ST_CORNER_TOPLEFT, + ST_CORNER_TOPRIGHT, + ST_CORNER_BOTTOMRIGHT, + ST_CORNER_BOTTOMLEFT +} StCorner; + +/* These are the CSS values; that doesn't mean we have to implement blink... */ +/** + * StTextDecoration: + * @ST_TEXT_DECORATION_: Text is underlined + * @ST_TEXT_DECORATION_OVERLINE: Text is overlined + * @ST_TEXT_DECORATION_LINE_THROUGH: Text is striked out + * @ST_TEXT_DECORATION_BLINK: Text blinks + * + * Flags used to determine the decoration of text. + * + * Not that neither %ST_TEXT_DECORATION_OVERLINE or %ST_TEXT_DECORATION_BLINK + * are implemented, currently. + */ +typedef enum { + ST_TEXT_DECORATION_UNDERLINE = 1 << 0, + ST_TEXT_DECORATION_OVERLINE = 1 << 1, + ST_TEXT_DECORATION_LINE_THROUGH = 1 << 2, + ST_TEXT_DECORATION_BLINK = 1 << 3 +} StTextDecoration; + +/** + * StTextAlign: + * @ST_TEXT_ALIGN_LEFT: Text is aligned at the beginning of the label. + * @ST_TEXT_ALIGN_CENTER: Text is aligned in the middle of the label. + * @ST_TEXT_ALIGN_RIGHT: Text is aligned at the end of the label. + * @ST_GRADIENT_JUSTIFY: Text is justified in the label. + * + * Used to align text in a label. + */ +typedef enum { + ST_TEXT_ALIGN_LEFT = PANGO_ALIGN_LEFT, + ST_TEXT_ALIGN_CENTER = PANGO_ALIGN_CENTER, + ST_TEXT_ALIGN_RIGHT = PANGO_ALIGN_RIGHT, + ST_TEXT_ALIGN_JUSTIFY +} StTextAlign; + +/** + * StGradientType: + * @ST_GRADIENT_NONE: No gradient. + * @ST_GRADIENT_VERTICAL: A vertical gradient. + * @ST_GRADIENT_HORIZONTAL: A horizontal gradient. + * @ST_GRADIENT_RADIAL: Lookup the style requested in the icon name. + * + * Used to specify options when rendering gradients. + */ +typedef enum { + ST_GRADIENT_NONE, + ST_GRADIENT_VERTICAL, + ST_GRADIENT_HORIZONTAL, + ST_GRADIENT_RADIAL +} StGradientType; + +/** + * StIconStyle: + * @ST_ICON_STYLE_REQUESTED: Lookup the style requested in the icon name. + * @ST_ICON_STYLE_REGULAR: Try to always load regular icons, even when symbolic + * icon names are given. + * @ST_ICON_STYLE_SYMBOLIC: Try to always load symbolic icons, even when regular + * icon names are given. + * + * Used to specify options when looking up icons. + */ +typedef enum { + ST_ICON_STYLE_REQUESTED, + ST_ICON_STYLE_REGULAR, + ST_ICON_STYLE_SYMBOLIC +} StIconStyle; + +typedef struct _StThemeNodePaintState StThemeNodePaintState; + +struct _StThemeNodePaintState { + StThemeNode *node; + + float alloc_width; + float alloc_height; + + float box_shadow_width; + float box_shadow_height; + + float resource_scale; + + CoglPipeline *box_shadow_pipeline; + CoglPipeline *prerendered_texture; + CoglPipeline *prerendered_pipeline; + CoglPipeline *corner_material[4]; +}; + +StThemeNode *st_theme_node_new (StThemeContext *context, + StThemeNode *parent_node, /* can be null */ + StTheme *theme, /* can be null */ + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class, + const char *inline_style); + +StThemeNode *st_theme_node_get_parent (StThemeNode *node); + +StTheme *st_theme_node_get_theme (StThemeNode *node); + +gboolean st_theme_node_equal (StThemeNode *node_a, StThemeNode *node_b); +guint st_theme_node_hash (StThemeNode *node); + +GType st_theme_node_get_element_type (StThemeNode *node); +const char *st_theme_node_get_element_id (StThemeNode *node); +GStrv st_theme_node_get_element_classes (StThemeNode *node); +GStrv st_theme_node_get_pseudo_classes (StThemeNode *node); + +/* Generic getters ... these are not cached so are less efficient. The other + * reason for adding the more specific version is that we can handle the + * details of the actual CSS rules, which can be complicated, especially + * for fonts + */ +gboolean st_theme_node_lookup_color (StThemeNode *node, + const char *property_name, + gboolean inherit, + ClutterColor *color); +gboolean st_theme_node_lookup_double (StThemeNode *node, + const char *property_name, + gboolean inherit, + double *value); +gboolean st_theme_node_lookup_length (StThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *length); +gboolean st_theme_node_lookup_time (StThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *value); +gboolean st_theme_node_lookup_shadow (StThemeNode *node, + const char *property_name, + gboolean inherit, + StShadow **shadow); +gboolean st_theme_node_lookup_url (StThemeNode *node, + const char *property_name, + gboolean inherit, + GFile **file); + +/* Easier-to-use variants of the above, for application-level use */ +void st_theme_node_get_color (StThemeNode *node, + const char *property_name, + ClutterColor *color); +gdouble st_theme_node_get_double (StThemeNode *node, + const char *property_name); +gdouble st_theme_node_get_length (StThemeNode *node, + const char *property_name); +StShadow *st_theme_node_get_shadow (StThemeNode *node, + const char *property_name); +GFile *st_theme_node_get_url (StThemeNode *node, + const char *property_name); + +/* Specific getters for particular properties: cached + */ +void st_theme_node_get_background_color (StThemeNode *node, + ClutterColor *color); +void st_theme_node_get_foreground_color (StThemeNode *node, + ClutterColor *color); +void st_theme_node_get_background_gradient (StThemeNode *node, + StGradientType *type, + ClutterColor *start, + ClutterColor *end); + +GFile *st_theme_node_get_background_image (StThemeNode *node); + +int st_theme_node_get_border_width (StThemeNode *node, + StSide side); +int st_theme_node_get_border_radius (StThemeNode *node, + StCorner corner); +void st_theme_node_get_border_color (StThemeNode *node, + StSide side, + ClutterColor *color); + +int st_theme_node_get_outline_width (StThemeNode *node); +void st_theme_node_get_outline_color (StThemeNode *node, + ClutterColor *color); + +double st_theme_node_get_padding (StThemeNode *node, + StSide side); + +double st_theme_node_get_horizontal_padding (StThemeNode *node); +double st_theme_node_get_vertical_padding (StThemeNode *node); + +double st_theme_node_get_margin (StThemeNode *node, + StSide side); + +int st_theme_node_get_width (StThemeNode *node); +int st_theme_node_get_height (StThemeNode *node); +int st_theme_node_get_min_width (StThemeNode *node); +int st_theme_node_get_min_height (StThemeNode *node); +int st_theme_node_get_max_width (StThemeNode *node); +int st_theme_node_get_max_height (StThemeNode *node); + +int st_theme_node_get_transition_duration (StThemeNode *node); + +StIconStyle st_theme_node_get_icon_style (StThemeNode *node); + +StTextDecoration st_theme_node_get_text_decoration (StThemeNode *node); + +StTextAlign st_theme_node_get_text_align (StThemeNode *node); + +double st_theme_node_get_letter_spacing (StThemeNode *node); + +/* Font rule processing is pretty complicated, so we just hardcode it + * under the standard font/font-family/font-size/etc names. This means + * you can't have multiple separate styled fonts for a single item, + * but that should be OK. + */ +const PangoFontDescription *st_theme_node_get_font (StThemeNode *node); + +gchar *st_theme_node_get_font_features (StThemeNode *node); + +StBorderImage *st_theme_node_get_border_image (StThemeNode *node); +StShadow *st_theme_node_get_box_shadow (StThemeNode *node); +StShadow *st_theme_node_get_text_shadow (StThemeNode *node); + +StShadow *st_theme_node_get_background_image_shadow (StThemeNode *node); + +StIconColors *st_theme_node_get_icon_colors (StThemeNode *node); + +/* Helpers for get_preferred_width()/get_preferred_height() ClutterActor vfuncs */ +void st_theme_node_adjust_for_height (StThemeNode *node, + float *for_height); +void st_theme_node_adjust_preferred_width (StThemeNode *node, + float *min_width_p, + float *natural_width_p); +void st_theme_node_adjust_for_width (StThemeNode *node, + float *for_width); +void st_theme_node_adjust_preferred_height (StThemeNode *node, + float *min_height_p, + float *natural_height_p); + +/* Helper for allocate() ClutterActor vfunc */ +void st_theme_node_get_content_box (StThemeNode *node, + const ClutterActorBox *allocation, + ClutterActorBox *content_box); +/* Helper for StThemeNodeTransition */ +void st_theme_node_get_paint_box (StThemeNode *node, + const ClutterActorBox *allocation, + ClutterActorBox *paint_box); +/* Helper for background prerendering */ +void st_theme_node_get_background_paint_box (StThemeNode *node, + const ClutterActorBox *allocation, + ClutterActorBox *paint_box); + +gboolean st_theme_node_geometry_equal (StThemeNode *node, + StThemeNode *other); +gboolean st_theme_node_paint_equal (StThemeNode *node, + StThemeNode *other); + +/** + * st_theme_node_paint: (skip) + */ +void st_theme_node_paint (StThemeNode *node, + StThemeNodePaintState *state, + CoglFramebuffer *framebuffer, + const ClutterActorBox *box, + guint8 paint_opacity, + float resource_scale); + +void st_theme_node_invalidate_background_image (StThemeNode *node); +void st_theme_node_invalidate_border_image (StThemeNode *node); + +gchar * st_theme_node_to_string (StThemeNode *node); + +void st_theme_node_paint_state_init (StThemeNodePaintState *state); +void st_theme_node_paint_state_free (StThemeNodePaintState *state); +void st_theme_node_paint_state_copy (StThemeNodePaintState *state, + StThemeNodePaintState *other); +void st_theme_node_paint_state_invalidate (StThemeNodePaintState *state); +gboolean st_theme_node_paint_state_invalidate_for_file (StThemeNodePaintState *state, + GFile *file); + +void st_theme_node_paint_state_set_node (StThemeNodePaintState *state, + StThemeNode *node); + +G_END_DECLS + +#endif /* __ST_THEME_NODE_H__ */ diff --git a/src/st/st-theme-private.h b/src/st/st-theme-private.h new file mode 100644 index 0000000..2083a7c --- /dev/null +++ b/src/st/st-theme-private.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme-private.h: Private StThemeMethods + * + * Copyright 2008, 2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __ST_THEME_PRIVATE_H__ +#define __ST_THEME_PRIVATE_H__ + +#include "croco/libcroco.h" +#include "st-theme.h" + +G_BEGIN_DECLS + +GPtrArray *_st_theme_get_matched_properties (StTheme *theme, + StThemeNode *node); + +/* Resolve an URL from the stylesheet to a file */ +GFile *_st_theme_resolve_url (StTheme *theme, + CRStyleSheet *base_stylesheet, + const char *url); + +CRDeclaration *_st_theme_parse_declaration_list (const char *str); + +G_END_DECLS + +#endif /* __ST_THEME_PRIVATE_H__ */ diff --git a/src/st/st-theme.c b/src/st/st-theme.c new file mode 100644 index 0000000..20d42fd --- /dev/null +++ b/src/st/st-theme.c @@ -0,0 +1,1085 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme.c: A set of CSS stylesheets used for rule matching + * + * Copyright 2003-2004 Dodji Seketeli + * Copyright 2008, 2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * This file started as a cut-and-paste of cr-sel-eng.c from libcroco. + * + * In moving it to hippo-canvas: + * - Reformatted and otherwise edited to match our coding style + * - Switched from handling xmlNode to handling HippoStyle + * - Simplified by removing things that we don't need or that don't + * make sense in our context. + * - The code to get a list of matching properties works quite differently; + * we order things in priority order, but we don't actually try to + * coalesce properties with the same name. + * + * In moving it to GNOME Shell: + * - Renamed again to StTheme + * - Reformatted to match the gnome-shell coding style + * - Removed notion of "theme engine" from hippo-canvas + * - pseudo-class matching changed from link enum to strings + * - Some code simplification + */ + + +#include <stdlib.h> +#include <string.h> + +#include <gio/gio.h> + +#include "st-private.h" +#include "st-theme-node.h" +#include "st-theme-private.h" + +static void st_theme_constructed (GObject *object); +static void st_theme_finalize (GObject *object); +static void st_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void st_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +struct _StTheme +{ + GObject parent; + + GFile *application_stylesheet; + GFile *default_stylesheet; + GFile *theme_stylesheet; + GSList *custom_stylesheets; + + GHashTable *stylesheets_by_file; + GHashTable *files_by_stylesheet; + + CRCascade *cascade; +}; + +enum +{ + PROP_0, + PROP_APPLICATION_STYLESHEET, + PROP_THEME_STYLESHEET, + PROP_DEFAULT_STYLESHEET +}; + +enum +{ + STYLESHEETS_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (StTheme, st_theme, G_TYPE_OBJECT) + +/* Quick strcmp. Test only for == 0 or != 0, not < 0 or > 0. */ +#define strqcmp(str,lit,lit_len) \ + (strlen (str) != (lit_len) || memcmp (str, lit, lit_len)) + +static gboolean +file_equal0 (GFile *file1, + GFile *file2) +{ + if (file1 == file2) + return TRUE; + + if ((file1 == NULL) || (file2 == NULL)) + return FALSE; + + return g_file_equal (file1, file2); +} + +static void +st_theme_init (StTheme *theme) +{ + theme->stylesheets_by_file = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, + (GDestroyNotify)g_object_unref, (GDestroyNotify)cr_stylesheet_unref); + theme->files_by_stylesheet = g_hash_table_new (g_direct_hash, g_direct_equal); +} + +static void +st_theme_class_init (StThemeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = st_theme_constructed; + object_class->finalize = st_theme_finalize; + object_class->set_property = st_theme_set_property; + object_class->get_property = st_theme_get_property; + + /** + * StTheme:application-stylesheet: + * + * The highest priority stylesheet, representing application-specific + * styling; this is associated with the CSS "author" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_APPLICATION_STYLESHEET, + g_param_spec_object ("application-stylesheet", + "Application Stylesheet", + "Stylesheet with application-specific styling", + G_TYPE_FILE, + ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + /** + * StTheme:theme-stylesheet: + * + * The second priority stylesheet, representing theme-specific styling; + * this is associated with the CSS "user" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_THEME_STYLESHEET, + g_param_spec_object ("theme-stylesheet", + "Theme Stylesheet", + "Stylesheet with theme-specific styling", + G_TYPE_FILE, + ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + /** + * StTheme:default-stylesheet: + * + * The lowest priority stylesheet, representing global default + * styling; this is associated with the CSS "user agent" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_DEFAULT_STYLESHEET, + g_param_spec_object ("default-stylesheet", + "Default Stylesheet", + "Stylesheet with global default styling", + G_TYPE_FILE, + ST_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); + + signals[STYLESHEETS_CHANGED] = + g_signal_new ("custom-stylesheets-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* no default handler slot */ + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static CRStyleSheet * +parse_stylesheet (GFile *file, + GError **error) +{ + enum CRStatus status; + CRStyleSheet *stylesheet; + char *contents; + gsize length; + + if (file == NULL) + return NULL; + + if (!g_file_load_contents (file, NULL, &contents, &length, NULL, error)) + return NULL; + + status = cr_om_parser_simply_parse_buf ((const guchar *) contents, + length, + CR_UTF_8, + &stylesheet); + g_free (contents); + + if (status != CR_OK) + { + char *uri = g_file_get_uri (file); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Error parsing stylesheet '%s'; errcode:%d", uri, status); + g_free (uri); + return NULL; + } + + /* Extension stylesheet */ + stylesheet->app_data = GUINT_TO_POINTER (FALSE); + + return stylesheet; +} + +CRDeclaration * +_st_theme_parse_declaration_list (const char *str) +{ + return cr_declaration_parse_list_from_buf ((const guchar *)str, + CR_UTF_8); +} + +/* Just g_warning for now until we have something nicer to do */ +static CRStyleSheet * +parse_stylesheet_nofail (GFile *file) +{ + GError *error = NULL; + CRStyleSheet *result; + + result = parse_stylesheet (file, &error); + if (error) + { + g_warning ("%s", error->message); + g_clear_error (&error); + } + return result; +} + +static void +insert_stylesheet (StTheme *theme, + GFile *file, + CRStyleSheet *stylesheet) +{ + if (stylesheet == NULL) + return; + + g_object_ref (file); + cr_stylesheet_ref (stylesheet); + + g_hash_table_insert (theme->stylesheets_by_file, file, stylesheet); + g_hash_table_insert (theme->files_by_stylesheet, stylesheet, file); +} + +/** + * st_theme_load_stylesheet: + * @theme: a #StTheme + * @file: a #GFile + * @error: a #GError + * + * Load the stylesheet associated with @file. + * + * Returns: %TRUE if successful + */ +gboolean +st_theme_load_stylesheet (StTheme *theme, + GFile *file, + GError **error) +{ + CRStyleSheet *stylesheet; + + stylesheet = parse_stylesheet (file, error); + if (!stylesheet) + return FALSE; + + stylesheet->app_data = GUINT_TO_POINTER (TRUE); + + insert_stylesheet (theme, file, stylesheet); + cr_stylesheet_ref (stylesheet); + theme->custom_stylesheets = g_slist_prepend (theme->custom_stylesheets, stylesheet); + g_signal_emit (theme, signals[STYLESHEETS_CHANGED], 0); + + return TRUE; +} + +/** + * st_theme_unload_stylesheet: + * @theme: a #StTheme + * @file: a #GFile + * + * Unload the stylesheet associated with @file. If @file was not loaded this + * function does nothing. + */ +void +st_theme_unload_stylesheet (StTheme *theme, + GFile *file) +{ + CRStyleSheet *stylesheet; + + stylesheet = g_hash_table_lookup (theme->stylesheets_by_file, file); + if (!stylesheet) + return; + + if (!g_slist_find (theme->custom_stylesheets, stylesheet)) + return; + + theme->custom_stylesheets = g_slist_remove (theme->custom_stylesheets, stylesheet); + + g_signal_emit (theme, signals[STYLESHEETS_CHANGED], 0); + + /* We need to remove the entry from the hashtable after emitting the signal + * since we might still access the files_by_stylesheet hashtable in + * _st_theme_resolve_url() during the signal emission. + */ + g_hash_table_remove (theme->stylesheets_by_file, file); + g_hash_table_remove (theme->files_by_stylesheet, stylesheet); + cr_stylesheet_unref (stylesheet); +} + +/** + * st_theme_get_custom_stylesheets: + * @theme: an #StTheme + * + * Get a list of the stylesheet files loaded with st_theme_load_stylesheet(). + * + * Returns: (transfer full) (element-type GFile): the list of stylesheet files + * that were loaded with st_theme_load_stylesheet() + */ +GSList* +st_theme_get_custom_stylesheets (StTheme *theme) +{ + GSList *result = NULL; + GSList *iter; + + for (iter = theme->custom_stylesheets; iter; iter = iter->next) + { + CRStyleSheet *stylesheet = iter->data; + GFile *file = g_hash_table_lookup (theme->files_by_stylesheet, stylesheet); + + result = g_slist_prepend (result, g_object_ref (file)); + } + + return result; +} + +static void +st_theme_constructed (GObject *object) +{ + StTheme *theme = ST_THEME (object); + CRStyleSheet *application_stylesheet; + CRStyleSheet *theme_stylesheet; + CRStyleSheet *default_stylesheet; + + G_OBJECT_CLASS (st_theme_parent_class)->constructed (object); + + application_stylesheet = parse_stylesheet_nofail (theme->application_stylesheet); + theme_stylesheet = parse_stylesheet_nofail (theme->theme_stylesheet); + default_stylesheet = parse_stylesheet_nofail (theme->default_stylesheet); + + theme->cascade = cr_cascade_new (application_stylesheet, + theme_stylesheet, + default_stylesheet); + + if (theme->cascade == NULL) + g_error ("Out of memory when creating cascade object"); + + insert_stylesheet (theme, theme->application_stylesheet, application_stylesheet); + insert_stylesheet (theme, theme->theme_stylesheet, theme_stylesheet); + insert_stylesheet (theme, theme->default_stylesheet, default_stylesheet); +} + +static void +st_theme_finalize (GObject * object) +{ + StTheme *theme = ST_THEME (object); + + g_slist_foreach (theme->custom_stylesheets, (GFunc) cr_stylesheet_unref, NULL); + g_slist_free (theme->custom_stylesheets); + theme->custom_stylesheets = NULL; + + g_hash_table_destroy (theme->stylesheets_by_file); + g_hash_table_destroy (theme->files_by_stylesheet); + + g_clear_object (&theme->application_stylesheet); + g_clear_object (&theme->theme_stylesheet); + g_clear_object (&theme->default_stylesheet); + + if (theme->cascade) + { + cr_cascade_unref (theme->cascade); + theme->cascade = NULL; + } + + G_OBJECT_CLASS (st_theme_parent_class)->finalize (object); +} + +static void +st_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StTheme *theme = ST_THEME (object); + + switch (prop_id) + { + case PROP_APPLICATION_STYLESHEET: + { + GFile *file = g_value_get_object (value); + + if (!file_equal0 (file, theme->application_stylesheet)) + { + g_clear_object (&theme->application_stylesheet); + if (file != NULL) + theme->application_stylesheet = g_object_ref (file); + } + + break; + } + case PROP_THEME_STYLESHEET: + { + GFile *file = g_value_get_object (value); + + if (!file_equal0 (file, theme->theme_stylesheet)) + { + g_clear_object (&theme->theme_stylesheet); + if (file != NULL) + theme->theme_stylesheet = g_object_ref (file); + } + + break; + } + case PROP_DEFAULT_STYLESHEET: + { + GFile *file = g_value_get_object (value); + + if (!file_equal0 (file, theme->default_stylesheet)) + { + g_clear_object (&theme->default_stylesheet); + if (file != NULL) + theme->default_stylesheet = g_object_ref (file); + } + + break; + } + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StTheme *theme = ST_THEME (object); + + switch (prop_id) + { + case PROP_APPLICATION_STYLESHEET: + g_value_set_object (value, theme->application_stylesheet); + break; + case PROP_THEME_STYLESHEET: + g_value_set_object (value, theme->theme_stylesheet); + break; + case PROP_DEFAULT_STYLESHEET: + g_value_set_object (value, theme->default_stylesheet); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * st_theme_new: + * @application_stylesheet: The highest priority stylesheet, representing application-specific + * styling; this is associated with the CSS "author" stylesheet, may be %NULL + * @theme_stylesheet: The second priority stylesheet, representing theme-specific styling ; + * this is associated with the CSS "user" stylesheet, may be %NULL + * @default_stylesheet: The lowest priority stylesheet, representing global default styling; + * this is associated with the CSS "user agent" stylesheet, may be %NULL + * + * Returns: the newly created theme object + **/ +StTheme * +st_theme_new (GFile *application_stylesheet, + GFile *theme_stylesheet, + GFile *default_stylesheet) +{ + StTheme *theme = g_object_new (ST_TYPE_THEME, + "application-stylesheet", application_stylesheet, + "theme-stylesheet", theme_stylesheet, + "default-stylesheet", default_stylesheet, + NULL); + + return theme; +} + +static gboolean +string_in_list (GString *stryng, + GStrv list) +{ + gchar **it; + + if (list == NULL) + return FALSE; + + for (it = list; *it != NULL; it++) + { + if (!strqcmp (*it, stryng->str, stryng->len)) + return TRUE; + } + + return FALSE; +} + +static gboolean +pseudo_class_add_sel_matches_style (StTheme *a_this, + CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + GStrv node_pseudo_classes; + + g_return_val_if_fail (a_this + && a_add_sel + && a_add_sel->content.pseudo + && a_add_sel->content.pseudo->name + && a_add_sel->content.pseudo->name->stryng + && a_add_sel->content.pseudo->name->stryng->str + && a_node, FALSE); + + node_pseudo_classes = st_theme_node_get_pseudo_classes (a_node); + + return string_in_list (a_add_sel->content.pseudo->name->stryng, + node_pseudo_classes); +} + +/** + * class_add_sel_matches_style: + * @a_add_sel: The class additional selector to consider. + * @a_node: The style node to consider. + * + * Returns: %TRUE if the class additional selector matches + * the style node given in argument, %FALSE otherwise. + */ +static gboolean +class_add_sel_matches_style (CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + GStrv element_classes; + + g_return_val_if_fail (a_add_sel + && a_add_sel->type == CLASS_ADD_SELECTOR + && a_add_sel->content.class_name + && a_add_sel->content.class_name->stryng + && a_add_sel->content.class_name->stryng->str + && a_node, FALSE); + + element_classes = st_theme_node_get_element_classes (a_node); + + return string_in_list (a_add_sel->content.class_name->stryng, + element_classes); +} + +/* + *@return TRUE if the additional attribute selector matches + *the current style node given in argument, FALSE otherwise. + *@param a_add_sel the additional attribute selector to consider. + *@param a_node the style node to consider. + */ +static gboolean +id_add_sel_matches_style (CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + gboolean result = FALSE; + const char *id; + + g_return_val_if_fail (a_add_sel + && a_add_sel->type == ID_ADD_SELECTOR + && a_add_sel->content.id_name + && a_add_sel->content.id_name->stryng + && a_add_sel->content.id_name->stryng->str + && a_node, FALSE); + g_return_val_if_fail (a_add_sel + && a_add_sel->type == ID_ADD_SELECTOR + && a_node, FALSE); + + id = st_theme_node_get_element_id (a_node); + + if (id != NULL) + { + if (!strqcmp (id, a_add_sel->content.id_name->stryng->str, + a_add_sel->content.id_name->stryng->len)) + { + result = TRUE; + } + } + + return result; +} + +/** + *additional_selector_matches_style: + *Evaluates if a given additional selector matches an style node. + *@param a_add_sel the additional selector to consider. + *@param a_node the style node to consider. + *@return TRUE is a_add_sel matches a_node, FALSE otherwise. + */ +static gboolean +additional_selector_matches_style (StTheme *a_this, + CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + CRAdditionalSel *cur_add_sel = NULL; + + g_return_val_if_fail (a_add_sel, FALSE); + + for (cur_add_sel = a_add_sel; cur_add_sel; cur_add_sel = cur_add_sel->next) + { + switch (cur_add_sel->type) + { + case NO_ADD_SELECTOR: + return FALSE; + case CLASS_ADD_SELECTOR: + if (!class_add_sel_matches_style (cur_add_sel, a_node)) + return FALSE; + break; + case ID_ADD_SELECTOR: + if (!id_add_sel_matches_style (cur_add_sel, a_node)) + return FALSE; + break; + case ATTRIBUTE_ADD_SELECTOR: + g_warning ("Attribute selectors not supported"); + return FALSE; + case PSEUDO_CLASS_ADD_SELECTOR: + if (!pseudo_class_add_sel_matches_style (a_this, cur_add_sel, a_node)) + return FALSE; + break; + default: + g_warning ("Unhandled selector type %d", cur_add_sel->type); + return FALSE; + } + } + + return TRUE; +} + +static gboolean +element_name_matches_type (const char *element_name, + GType element_type) +{ + if (element_type == G_TYPE_NONE) + { + return strcmp (element_name, "stage") == 0; + } + else + { + GType match_type = g_type_from_name (element_name); + if (match_type == G_TYPE_INVALID) + return FALSE; + + return g_type_is_a (element_type, match_type); + } +} + +/* + *Evaluate a selector (a simple selectors list) and says + *if it matches the style node given in parameter. + *The algorithm used here is the following: + *Walk the combinator separated list of simple selectors backward, starting + *from the end of the list. For each simple selector, looks if + *if matches the current style. + * + *@param a_this the selection engine. + *@param a_sel the simple selection list. + *@param a_node the style node. + *@param a_result out parameter. Set to true if the + *selector matches the style node, FALSE otherwise. + *@param a_recurse if set to TRUE, the function will walk to + *the next simple selector (after the evaluation of the current one) + *and recursively evaluate it. Must be usually set to TRUE unless you + *know what you are doing. + */ +static enum CRStatus +sel_matches_style_real (StTheme *a_this, + CRSimpleSel *a_sel, + StThemeNode *a_node, + gboolean *a_result, + gboolean a_eval_sel_list_from_end, + gboolean a_recurse) +{ + CRSimpleSel *cur_sel = NULL; + StThemeNode *cur_node = NULL; + GType cur_type; + + *a_result = FALSE; + + if (a_eval_sel_list_from_end) + { + /*go and get the last simple selector of the list */ + for (cur_sel = a_sel; cur_sel && cur_sel->next; cur_sel = cur_sel->next) + ; + } + else + { + cur_sel = a_sel; + } + + cur_node = a_node; + cur_type = st_theme_node_get_element_type (cur_node); + + while (cur_sel) + { + if (((cur_sel->type_mask & TYPE_SELECTOR) + && (cur_sel->name + && cur_sel->name->stryng + && cur_sel->name->stryng->str) + && + (element_name_matches_type (cur_sel->name->stryng->str, cur_type))) + || (cur_sel->type_mask & UNIVERSAL_SELECTOR)) + { + /* + *this simple selector + *matches the current style node + *Let's see if the preceding + *simple selectors also match + *their style node counterpart. + */ + if (cur_sel->add_sel) + { + if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node)) + goto walk_a_step_in_expr; + else + goto done; + } + else + goto walk_a_step_in_expr; + } + if (!(cur_sel->type_mask & TYPE_SELECTOR) + && !(cur_sel->type_mask & UNIVERSAL_SELECTOR)) + { + if (!cur_sel->add_sel) + goto done; + if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node)) + goto walk_a_step_in_expr; + else + goto done; + } + else + { + goto done; + } + + walk_a_step_in_expr: + if (a_recurse == FALSE) + { + *a_result = TRUE; + goto done; + } + + /* + *here, depending on the combinator of cur_sel + *choose the axis of the element tree traversal + *and walk one step in the element tree. + */ + if (!cur_sel->prev) + break; + + switch (cur_sel->combinator) + { + case NO_COMBINATOR: + break; + + case COMB_WS: /*descendant selector */ + { + StThemeNode *n = NULL; + + /* + *walk the element tree upward looking for a parent + *style that matches the preceding selector. + */ + for (n = st_theme_node_get_parent (a_node); n; n = st_theme_node_get_parent (n)) + { + enum CRStatus status; + gboolean matches = FALSE; + + status = sel_matches_style_real (a_this, cur_sel->prev, n, &matches, FALSE, TRUE); + + if (status != CR_OK) + goto done; + + if (matches) + { + cur_node = n; + cur_type = st_theme_node_get_element_type (cur_node); + break; + } + } + + if (!n) + { + /* + *didn't find any ancestor that matches + *the previous simple selector. + */ + goto done; + } + /* + *in this case, the preceding simple sel + *will have been interpreted twice, which + *is a cpu and mem waste ... I need to find + *another way to do this. Anyway, this is + *my first attempt to write this function and + *I am a bit clueless. + */ + break; + } + + case COMB_PLUS: + g_warning ("+ combinators are not supported"); + goto done; + + case COMB_GT: + cur_node = st_theme_node_get_parent (cur_node); + if (!cur_node) + goto done; + cur_type = st_theme_node_get_element_type (cur_node); + break; + + default: + goto done; + } + + cur_sel = cur_sel->prev; + } + + /* + *if we reached this point, it means the selector matches + *the style node. + */ + *a_result = TRUE; + +done: + return CR_OK; +} + +static void +add_matched_properties (StTheme *a_this, + CRStyleSheet *a_nodesheet, + StThemeNode *a_node, + GPtrArray *props) +{ + CRStatement *cur_stmt = NULL; + CRSelector *sel_list = NULL; + CRSelector *cur_sel = NULL; + gboolean matches = FALSE; + enum CRStatus status = CR_OK; + + /* + *walk through the list of statements and, + *get the selectors list inside the statements that + *contain some, and try to match our style node in these + *selectors lists. + */ + for (cur_stmt = a_nodesheet->statements; cur_stmt; cur_stmt = cur_stmt->next) + { + /* + *initialize the selector list in which we will + *really perform the search. + */ + sel_list = NULL; + + /* + *get the the damn selector list in + *which we have to look + */ + switch (cur_stmt->type) + { + case RULESET_STMT: + if (cur_stmt->kind.ruleset && cur_stmt->kind.ruleset->sel_list) + { + sel_list = cur_stmt->kind.ruleset->sel_list; + } + break; + + case AT_IMPORT_RULE_STMT: + { + CRAtImportRule *import_rule = cur_stmt->kind.import_rule; + + if (import_rule->sheet == NULL) + { + GFile *file = NULL; + + if (import_rule->url->stryng && import_rule->url->stryng->str) + { + file = _st_theme_resolve_url (a_this, + a_nodesheet, + import_rule->url->stryng->str); + import_rule->sheet = parse_stylesheet (file, NULL); + } + + if (import_rule->sheet) + { + insert_stylesheet (a_this, file, import_rule->sheet); + /* refcount of stylesheets starts off at zero, so we don't need to unref! */ + } + else + { + /* Set a marker to avoid repeatedly trying to parse a non-existent or + * broken stylesheet + */ + import_rule->sheet = (CRStyleSheet *) - 1; + } + + if (file) + g_object_unref (file); + } + + if (import_rule->sheet != (CRStyleSheet *) - 1) + { + add_matched_properties (a_this, import_rule->sheet, + a_node, props); + } + } + break; + case AT_MEDIA_RULE_STMT: + case AT_RULE_STMT: + case AT_PAGE_RULE_STMT: + case AT_CHARSET_RULE_STMT: + case AT_FONT_FACE_RULE_STMT: + default: + break; + } + + if (!sel_list) + continue; + + /* + *now, we have a comma separated selector list to look in. + *let's walk it and try to match the style node + *on each item of the list. + */ + for (cur_sel = sel_list; cur_sel; cur_sel = cur_sel->next) + { + if (!cur_sel->simple_sel) + continue; + + status = sel_matches_style_real (a_this, cur_sel->simple_sel, a_node, &matches, TRUE, TRUE); + + if (status == CR_OK && matches) + { + CRDeclaration *cur_decl = NULL; + + /* In order to sort the matching properties, we need to compute the + * specificity of the selector that actually matched this + * element. In a non-thread-safe fashion, we store it in the + * ruleset. (Fixing this would mean cut-and-pasting + * cr_simple_sel_compute_specificity(), and have no need for + * thread-safety anyways.) + * + * Once we've sorted the properties, the specificity no longer + * matters and it can be safely overridden. + */ + cr_simple_sel_compute_specificity (cur_sel->simple_sel); + + cur_stmt->specificity = cur_sel->simple_sel->specificity; + + for (cur_decl = cur_stmt->kind.ruleset->decl_list; cur_decl; cur_decl = cur_decl->next) + g_ptr_array_add (props, cur_decl); + } + } + } +} + +#define ORIGIN_OFFSET_IMPORTANT (NB_ORIGINS) +#define ORIGIN_OFFSET_EXTENSION (NB_ORIGINS * 2) + +static inline int +get_origin (const CRDeclaration * decl) +{ + enum CRStyleOrigin origin = decl->parent_statement->parent_sheet->origin; + gboolean is_extension_sheet = GPOINTER_TO_UINT (decl->parent_statement->parent_sheet->app_data); + + if (decl->important) + origin += ORIGIN_OFFSET_IMPORTANT; + + if (is_extension_sheet) + origin += ORIGIN_OFFSET_EXTENSION; + + return origin; +} + +/* Order of comparison is so that higher priority statements compare after + * lower priority statements */ +static int +compare_declarations (gconstpointer a, + gconstpointer b) +{ + /* g_ptr_array_sort() is broooken */ + CRDeclaration *decl_a = *(CRDeclaration **) a; + CRDeclaration *decl_b = *(CRDeclaration **) b; + + int origin_a = get_origin (decl_a); + int origin_b = get_origin (decl_b); + + if (origin_a != origin_b) + return origin_a - origin_b; + + if (decl_a->parent_statement->specificity != decl_b->parent_statement->specificity) + return decl_a->parent_statement->specificity - decl_b->parent_statement->specificity; + + return 0; +} + +GPtrArray * +_st_theme_get_matched_properties (StTheme *theme, + StThemeNode *node) +{ + enum CRStyleOrigin origin = 0; + CRStyleSheet *sheet = NULL; + GPtrArray *props = g_ptr_array_new (); + GSList *iter; + + g_return_val_if_fail (ST_IS_THEME (theme), NULL); + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + for (origin = ORIGIN_UA; origin < NB_ORIGINS; origin++) + { + sheet = cr_cascade_get_sheet (theme->cascade, origin); + if (!sheet) + continue; + + add_matched_properties (theme, sheet, node, props); + } + + for (iter = theme->custom_stylesheets; iter; iter = iter->next) + add_matched_properties (theme, iter->data, node, props); + + /* We count on a stable sort here so that later declarations come + * after earlier declarations */ + g_ptr_array_sort (props, compare_declarations); + + return props; +} + +/* Resolve an url from an url() reference in a stylesheet into a GFile, + * if possible. The resolution here is distinctly lame and + * will fail on many examples. + */ +GFile * +_st_theme_resolve_url (StTheme *theme, + CRStyleSheet *base_stylesheet, + const char *url) +{ + char *scheme; + GFile *resource; + + if ((scheme = g_uri_parse_scheme (url))) + { + g_free (scheme); + resource = g_file_new_for_uri (url); + } + else if (base_stylesheet != NULL) + { + GFile *base_file = NULL, *parent; + + base_file = g_hash_table_lookup (theme->files_by_stylesheet, base_stylesheet); + + /* This is an internal function, if we get here with + a bad @base_stylesheet we have a problem. */ + g_assert (base_file); + + parent = g_file_get_parent (base_file); + resource = g_file_resolve_relative_path (parent, url); + + g_object_unref (parent); + } + else + { + resource = g_file_new_for_path (url); + } + + return resource; +} diff --git a/src/st/st-theme.h b/src/st/st-theme.h new file mode 100644 index 0000000..d3f242c --- /dev/null +++ b/src/st/st-theme.h @@ -0,0 +1,51 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-theme.h: A set of CSS stylesheets used for rule matching + * + * Copyright 2008, 2009 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef __ST_THEME_H__ +#define __ST_THEME_H__ + +#include <glib-object.h> + +#include "st-theme-node.h" + +G_BEGIN_DECLS + +/** + * SECTION:st-theme + * @short_description: a set of stylesheets + * + * #StTheme holds a set of stylesheets. (The "cascade" of the name + * Cascading Stylesheets.) A #StTheme can be set to apply to all the actors + * in a stage using st_theme_context_set_theme(). + */ + +#define ST_TYPE_THEME (st_theme_get_type ()) +G_DECLARE_FINAL_TYPE (StTheme, st_theme, ST, THEME, GObject) + +StTheme *st_theme_new (GFile *application_stylesheet, + GFile *theme_stylesheet, + GFile *default_stylesheet); + +gboolean st_theme_load_stylesheet (StTheme *theme, GFile *file, GError **error); +void st_theme_unload_stylesheet (StTheme *theme, GFile *file); +GSList *st_theme_get_custom_stylesheets (StTheme *theme); + +G_END_DECLS + +#endif /* __ST_THEME_H__ */ diff --git a/src/st/st-types.h b/src/st/st-types.h new file mode 100644 index 0000000..d204041 --- /dev/null +++ b/src/st/st-types.h @@ -0,0 +1,52 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2009 Intel Corporation. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef __ST_TYPES_H__ +#define __ST_TYPES_H__ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#include <glib-object.h> +#include <clutter/clutter.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +/** + * SECTION:st-types + * @short_description: type definitions used throughout St + * + * Common types for StWidgets. + */ + +typedef enum { + ST_ALIGN_START, + ST_ALIGN_MIDDLE, + ST_ALIGN_END +} StAlign; + +typedef enum { + ST_BACKGROUND_SIZE_AUTO, + ST_BACKGROUND_SIZE_CONTAIN, + ST_BACKGROUND_SIZE_COVER, + ST_BACKGROUND_SIZE_FIXED +} StBackgroundSize; + +G_END_DECLS + +#endif /* __ST_TYPES_H__ */ diff --git a/src/st/st-viewport.c b/src/st/st-viewport.c new file mode 100644 index 0000000..e6b9127 --- /dev/null +++ b/src/st/st-viewport.c @@ -0,0 +1,600 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-scrollable-wiget.c: a scrollable actor + * + * Copyright 2009 Intel Corporation. + * Copyright 2009 Abderrahim Kitouni + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2010 Florian Muellner + * Copyright 2019 Endless, Inc + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/* Portions copied from Clutter: + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Authored By Matthew Allum <mallum@openedhand.com> + * + * Copyright (C) 2006 OpenedHand + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + */ + +/** + * SECTION:st-viewport + * @short_description: a scrollable container + * + * The #StViewport is a generic #StScrollable implementation. + * + */ + +#include <stdlib.h> + +#include "st-viewport.h" + +#include "st-private.h" +#include "st-scrollable.h" + + +static void st_viewport_scrollable_interface_init (StScrollableInterface *iface); + +enum { + PROP_0, + + PROP_CLIP_TO_VIEW, + + N_PROPS, + + /* StScrollable */ + PROP_HADJUST, + PROP_VADJUST +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +typedef struct +{ + StAdjustment *hadjustment; + StAdjustment *vadjustment; + gboolean clip_to_view; +} StViewportPrivate; + +G_DEFINE_TYPE_WITH_CODE (StViewport, st_viewport, ST_TYPE_WIDGET, + G_ADD_PRIVATE (StViewport) + G_IMPLEMENT_INTERFACE (ST_TYPE_SCROLLABLE, + st_viewport_scrollable_interface_init)); + +/* + * StScrollable Interface Implementation + */ +static void +adjustment_value_notify_cb (StAdjustment *adjustment, + GParamSpec *pspec, + StViewport *viewport) +{ + clutter_actor_invalidate_transform (CLUTTER_ACTOR (viewport)); + clutter_actor_invalidate_paint_volume (CLUTTER_ACTOR (viewport)); + clutter_actor_queue_relayout (CLUTTER_ACTOR (viewport)); +} + +static void +scrollable_set_adjustments (StScrollable *scrollable, + StAdjustment *hadjustment, + StAdjustment *vadjustment) +{ + StViewport *viewport = ST_VIEWPORT (scrollable); + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + g_object_freeze_notify (G_OBJECT (scrollable)); + + if (hadjustment != priv->hadjustment) + { + if (priv->hadjustment) + { + g_signal_handlers_disconnect_by_func (priv->hadjustment, + adjustment_value_notify_cb, + scrollable); + g_object_unref (priv->hadjustment); + } + + if (hadjustment) + { + g_object_ref (hadjustment); + g_signal_connect (hadjustment, "notify::value", + G_CALLBACK (adjustment_value_notify_cb), + scrollable); + } + + priv->hadjustment = hadjustment; + g_object_notify (G_OBJECT (scrollable), "hadjustment"); + } + + if (vadjustment != priv->vadjustment) + { + if (priv->vadjustment) + { + g_signal_handlers_disconnect_by_func (priv->vadjustment, + adjustment_value_notify_cb, + scrollable); + g_object_unref (priv->vadjustment); + } + + if (vadjustment) + { + g_object_ref (vadjustment); + g_signal_connect (vadjustment, "notify::value", + G_CALLBACK (adjustment_value_notify_cb), + scrollable); + } + + priv->vadjustment = vadjustment; + g_object_notify (G_OBJECT (scrollable), "vadjustment"); + } + + g_object_thaw_notify (G_OBJECT (scrollable)); +} + +static void +scrollable_get_adjustments (StScrollable *scrollable, + StAdjustment **hadjustment, + StAdjustment **vadjustment) +{ + StViewport *viewport = ST_VIEWPORT (scrollable); + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + if (hadjustment) + *hadjustment = priv->hadjustment; + + if (vadjustment) + *vadjustment = priv->vadjustment; +} + +static void +st_viewport_scrollable_interface_init (StScrollableInterface *iface) +{ + iface->set_adjustments = scrollable_set_adjustments; + iface->get_adjustments = scrollable_get_adjustments; +} + +static void +st_viewport_set_clip_to_view (StViewport *viewport, + gboolean clip_to_view) +{ + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + if (!!priv->clip_to_view != !!clip_to_view) + { + priv->clip_to_view = clip_to_view; + clutter_actor_queue_redraw (CLUTTER_ACTOR (viewport)); + g_object_notify_by_pspec (G_OBJECT (viewport), props[PROP_CLIP_TO_VIEW]); + } +} + +static void +st_viewport_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + StViewportPrivate *priv = + st_viewport_get_instance_private (ST_VIEWPORT (object)); + StAdjustment *adjustment; + + switch (property_id) + { + case PROP_HADJUST: + scrollable_get_adjustments (ST_SCROLLABLE (object), &adjustment, NULL); + g_value_set_object (value, adjustment); + break; + + case PROP_VADJUST: + scrollable_get_adjustments (ST_SCROLLABLE (object), NULL, &adjustment); + g_value_set_object (value, adjustment); + break; + + case PROP_CLIP_TO_VIEW: + g_value_set_boolean (value, priv->clip_to_view); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +st_viewport_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + StViewport *viewport = ST_VIEWPORT (object); + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + switch (property_id) + { + case PROP_HADJUST: + scrollable_set_adjustments (ST_SCROLLABLE (object), + g_value_get_object (value), + priv->vadjustment); + break; + + case PROP_VADJUST: + scrollable_set_adjustments (ST_SCROLLABLE (object), + priv->hadjustment, + g_value_get_object (value)); + break; + + case PROP_CLIP_TO_VIEW: + st_viewport_set_clip_to_view (viewport, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +st_viewport_dispose (GObject *object) +{ + StViewport *viewport = ST_VIEWPORT (object); + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + + g_clear_object (&priv->hadjustment); + g_clear_object (&priv->vadjustment); + + G_OBJECT_CLASS (st_viewport_parent_class)->dispose (object); +} + +static void +st_viewport_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StViewport *viewport = ST_VIEWPORT (actor); + StViewportPrivate *priv = + st_viewport_get_instance_private (viewport); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterLayoutManager *layout = clutter_actor_get_layout_manager (actor); + ClutterActorBox viewport_box; + ClutterActorBox content_box; + float avail_width, avail_height; + float min_width, natural_width; + float min_height, natural_height; + + st_theme_node_get_content_box (theme_node, box, &viewport_box); + clutter_actor_box_get_size (&viewport_box, &avail_width, &avail_height); + + clutter_layout_manager_get_preferred_width (layout, CLUTTER_CONTAINER (actor), + avail_height, + &min_width, &natural_width); + clutter_layout_manager_get_preferred_height (layout, CLUTTER_CONTAINER (actor), + MAX (avail_width, min_width), + &min_height, &natural_height); + + /* Because StViewport implements StScrollable, the allocation box passed here + * may not match the minimum sizes reported by the layout manager. When that + * happens, the content box needs to be adjusted to match the reported minimum + * sizes before being passed to clutter_layout_manager_allocate() */ + clutter_actor_set_allocation (actor, box); + + content_box = viewport_box; + if (priv->hadjustment) + content_box.x2 += MAX (0, min_width - avail_width); + if (priv->vadjustment) + content_box.y2 += MAX (0, min_height - avail_height); + + clutter_layout_manager_allocate (layout, CLUTTER_CONTAINER (actor), + &content_box); + + /* update adjustments for scrolling */ + if (priv->vadjustment) + { + double prev_value; + + g_object_set (G_OBJECT (priv->vadjustment), + "lower", 0.0, + "upper", MAX (min_height, avail_height), + "page-size", avail_height, + "step-increment", avail_height / 6, + "page-increment", avail_height - avail_height / 6, + NULL); + + prev_value = st_adjustment_get_value (priv->vadjustment); + st_adjustment_set_value (priv->vadjustment, prev_value); + } + + if (priv->hadjustment) + { + double prev_value; + + g_object_set (G_OBJECT (priv->hadjustment), + "lower", 0.0, + "upper", MAX (min_width, avail_width), + "page-size", avail_width, + "step-increment", avail_width / 6, + "page-increment", avail_width - avail_width / 6, + NULL); + + prev_value = st_adjustment_get_value (priv->hadjustment); + st_adjustment_set_value (priv->hadjustment, prev_value); + } +} + +static double +get_hadjustment_value (StViewport *viewport) +{ + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + ClutterTextDirection direction; + double x, upper, page_size; + + if (!priv->hadjustment) + return 0; + + st_adjustment_get_values (priv->hadjustment, + &x, NULL, &upper, NULL, NULL, &page_size); + + direction = clutter_actor_get_text_direction (CLUTTER_ACTOR (viewport)); + if (direction == CLUTTER_TEXT_DIRECTION_RTL) + return upper - page_size - x; + + return x; +} + +static void +st_viewport_apply_transform (ClutterActor *actor, + graphene_matrix_t *matrix) +{ + StViewport *viewport = ST_VIEWPORT (actor); + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + ClutterActorClass *parent_class = + CLUTTER_ACTOR_CLASS (st_viewport_parent_class); + graphene_point3d_t p = GRAPHENE_POINT3D_INIT_ZERO; + + if (priv->hadjustment) + p.x = -(int)get_hadjustment_value (viewport); + + if (priv->vadjustment) + p.y = -(int)st_adjustment_get_value (priv->vadjustment); + + graphene_matrix_translate (matrix, &p); + + parent_class->apply_transform (actor, matrix); +} + +/* If we are translated, then we need to translate back before chaining + * up or the background and borders will be drawn in the wrong place */ +static void +get_border_paint_offsets (StViewport *viewport, + int *x, + int *y) +{ + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + + if (priv->hadjustment) + *x = get_hadjustment_value (viewport); + else + *x = 0; + + if (priv->vadjustment) + *y = st_adjustment_get_value (priv->vadjustment); + else + *y = 0; +} + + +static void +st_viewport_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + StViewport *viewport = ST_VIEWPORT (actor); + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + int x, y; + ClutterActorBox allocation_box; + ClutterActorBox content_box; + ClutterActor *child; + CoglFramebuffer *fb = clutter_paint_context_get_framebuffer (paint_context); + + get_border_paint_offsets (viewport, &x, &y); + if (x != 0 || y != 0) + { + cogl_framebuffer_push_matrix (fb); + cogl_framebuffer_translate (fb, x, y, 0); + } + + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + if (x != 0 || y != 0) + cogl_framebuffer_pop_matrix (fb); + + if (clutter_actor_get_n_children (actor) == 0) + return; + + clutter_actor_get_allocation_box (actor, &allocation_box); + st_theme_node_get_content_box (theme_node, &allocation_box, &content_box); + + content_box.x1 += x; + content_box.y1 += y; + content_box.x2 += x; + content_box.y2 += y; + + /* The content area forms the viewport into the scrolled contents, while + * the borders and background stay in place; after drawing the borders and + * background, we clip to the content area */ + if (priv->clip_to_view && (priv->hadjustment || priv->vadjustment)) + { + cogl_framebuffer_push_rectangle_clip (fb, + (int)content_box.x1, + (int)content_box.y1, + (int)content_box.x2, + (int)content_box.y2); + } + + for (child = clutter_actor_get_first_child (actor); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + clutter_actor_paint (child, paint_context); + + if (priv->clip_to_view && (priv->hadjustment || priv->vadjustment)) + cogl_framebuffer_pop_clip (fb); +} + +static void +st_viewport_pick (ClutterActor *actor, + ClutterPickContext *pick_context) +{ + StViewport *viewport = ST_VIEWPORT (actor); + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + int x, y; + g_autoptr (ClutterActorBox) allocation_box = NULL; + ClutterActorBox content_box; + ClutterActor *child; + + CLUTTER_ACTOR_CLASS (st_viewport_parent_class)->pick (actor, pick_context); + + if (clutter_actor_get_n_children (actor) == 0) + return; + + g_object_get (actor, "allocation", &allocation_box, NULL); + st_theme_node_get_content_box (theme_node, allocation_box, &content_box); + + get_border_paint_offsets (viewport, &x, &y); + + content_box.x1 += x; + content_box.y1 += y; + content_box.x2 += x; + content_box.y2 += y; + + if (priv->hadjustment || priv->vadjustment) + clutter_pick_context_push_clip (pick_context, &content_box); + + for (child = clutter_actor_get_first_child (actor); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + clutter_actor_pick (child, pick_context); + + if (priv->hadjustment || priv->vadjustment) + clutter_pick_context_pop_clip (pick_context); +} + +static gboolean +st_viewport_get_paint_volume (ClutterActor *actor, + ClutterPaintVolume *volume) +{ + StViewport *viewport = ST_VIEWPORT (actor); + StViewportPrivate *priv = st_viewport_get_instance_private (viewport); + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox allocation_box; + ClutterActorBox content_box; + int x, y; + + /* Setting the paint volume does not make sense when we don't have any allocation */ + if (!clutter_actor_has_allocation (actor)) + return FALSE; + + if (!priv->clip_to_view) + return CLUTTER_ACTOR_CLASS (st_viewport_parent_class)->get_paint_volume (actor, volume); + + /* When have an adjustment we are clipped to the content box, so base + * our paint volume on that. */ + if (priv->hadjustment || priv->vadjustment) + { + double width, height; + + clutter_actor_get_allocation_box (actor, &allocation_box); + st_theme_node_get_content_box (theme_node, &allocation_box, &content_box); + + width = content_box.x2 - content_box.x1; + height = content_box.y2 - content_box.y1; + + clutter_paint_volume_set_width (volume, width); + clutter_paint_volume_set_height (volume, height); + } + else if (!CLUTTER_ACTOR_CLASS (st_viewport_parent_class)->get_paint_volume (actor, volume)) + { + return FALSE; + } + + /* When scrolled, st_viewport_apply_transform() includes the scroll offset + * and affects paint volumes. This is right for our children, but our paint volume + * is determined by our allocation and borders and doesn't scroll, so we need + * to reverse-compensate here, the same as we do when painting. + */ + get_border_paint_offsets (viewport, &x, &y); + if (x != 0 || y != 0) + { + graphene_point3d_t origin; + + clutter_paint_volume_get_origin (volume, &origin); + origin.x += x; + origin.y += y; + clutter_paint_volume_set_origin (volume, &origin); + } + + return TRUE; +} + +static void +st_viewport_class_init (StViewportClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + object_class->get_property = st_viewport_get_property; + object_class->set_property = st_viewport_set_property; + object_class->dispose = st_viewport_dispose; + + actor_class->allocate = st_viewport_allocate; + actor_class->apply_transform = st_viewport_apply_transform; + + actor_class->paint = st_viewport_paint; + actor_class->get_paint_volume = st_viewport_get_paint_volume; + actor_class->pick = st_viewport_pick; + + props[PROP_CLIP_TO_VIEW] = + g_param_spec_boolean ("clip-to-view", + "Clip to view", + "Clip to view", + TRUE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /* StScrollable properties */ + g_object_class_override_property (object_class, + PROP_HADJUST, + "hadjustment"); + + g_object_class_override_property (object_class, + PROP_VADJUST, + "vadjustment"); + + g_object_class_install_properties (object_class, N_PROPS, props); +} + +static void +st_viewport_init (StViewport *self) +{ + StViewportPrivate *priv = + st_viewport_get_instance_private (self); + + priv->clip_to_view = TRUE; +} diff --git a/src/st/st-viewport.h b/src/st/st-viewport.h new file mode 100644 index 0000000..8f0b16b --- /dev/null +++ b/src/st/st-viewport.h @@ -0,0 +1,40 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-viewport.h: viewport actor + * + * Copyright 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2019 Endless, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#pragma once + +#include <st/st-widget.h> + +G_BEGIN_DECLS + +#define ST_TYPE_VIEWPORT (st_viewport_get_type()) +G_DECLARE_DERIVABLE_TYPE (StViewport, st_viewport, ST, VIEWPORT, StWidget) + +struct _StViewportClass +{ + StWidgetClass parent_class; +}; + +G_END_DECLS diff --git a/src/st/st-widget-accessible.h b/src/st/st-widget-accessible.h new file mode 100644 index 0000000..c60f778 --- /dev/null +++ b/src/st/st-widget-accessible.h @@ -0,0 +1,76 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-widget-accessible.h: Accessible object for StWidget + * + * Copyright 2010 Igalia, S.L. + * Author: Alejandro Piñeiro Iglesias <apinheiro@igalia.com> + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_WIDGET_ACCESSIBLE_H__ +#define __ST_WIDGET_ACCESSIBLE_H__ + +G_BEGIN_DECLS + +#include <st/st-widget.h> +#include <cally/cally.h> + +#define ST_TYPE_WIDGET_ACCESSIBLE st_widget_accessible_get_type () + +#define ST_WIDGET_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + ST_TYPE_WIDGET_ACCESSIBLE, StWidgetAccessible)) + +#define ST_IS_WIDGET_ACCESSIBLE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + ST_TYPE_WIDGET_ACCESSIBLE)) + +#define ST_WIDGET_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), \ + ST_TYPE_WIDGET_ACCESSIBLE, StWidgetAccessibleClass)) + +#define ST_IS_WIDGET_ACCESSIBLE_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + ST_TYPE_WIDGET_ACCESSIBLE)) + +#define ST_WIDGET_ACCESSIBLE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + ST_TYPE_WIDGET_ACCESSIBLE, StWidgetAccessibleClass)) + +typedef struct _StWidgetAccessible StWidgetAccessible; +typedef struct _StWidgetAccessibleClass StWidgetAccessibleClass; +typedef struct _StWidgetAccessiblePrivate StWidgetAccessiblePrivate; + +struct _StWidgetAccessible +{ + CallyActor parent; + + /*< private >*/ + StWidgetAccessiblePrivate *priv; +}; + +struct _StWidgetAccessibleClass +{ + CallyActorClass parent_class; +}; + +GType st_widget_accessible_get_type (void) G_GNUC_CONST; + +G_END_DECLS + +#endif /* __ST_WIDGET_ACCESSIBLE_H__ */ diff --git a/src/st/st-widget.c b/src/st/st-widget.c new file mode 100644 index 0000000..31c400b --- /dev/null +++ b/src/st/st-widget.c @@ -0,0 +1,3049 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-widget.c: Base class for St actors + * + * Copyright 2007 OpenedHand + * Copyright 2008, 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2009 Abderrahim Kitouni + * Copyright 2009, 2010 Florian Müllner + * Copyright 2010 Adel Gadllah + * Copyright 2012 Igalia, S.L. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#include <clutter/clutter.h> + +#include "st-widget.h" + +#include "st-label.h" +#include "st-private.h" +#include "st-settings.h" +#include "st-texture-cache.h" +#include "st-theme-context.h" +#include "st-theme-node-transition.h" +#include "st-theme-node-private.h" +#include "st-drawing-area.h" + +#include "st-widget-accessible.h" + +#include <atk/atk-enum-types.h> + +/* This is set in stone and also hard-coded in GDK. */ +#define VIRTUAL_CORE_POINTER_ID 2 + +/* + * Forward declaration for sake of StWidgetChild + */ +typedef struct _StWidgetPrivate StWidgetPrivate; +struct _StWidgetPrivate +{ + StThemeNode *theme_node; + gchar *pseudo_class; + gchar *style_class; + gchar *inline_style; + + StThemeNodeTransition *transition_animation; + + guint is_style_dirty : 1; + guint first_child_dirty : 1; + guint last_child_dirty : 1; + guint draw_bg_color : 1; + guint draw_border_internal : 1; + guint track_hover : 1; + guint hover : 1; + guint can_focus : 1; + + gulong texture_file_changed_id; + guint update_child_styles_id; + + AtkObject *accessible; + AtkRole accessible_role; + AtkStateSet *local_state_set; + + ClutterActor *label_actor; + gchar *accessible_name; + + StWidget *last_visible_child; + StWidget *first_visible_child; + + StThemeNodePaintState paint_states[2]; + int current_paint_state : 2; +}; + +/** + * SECTION:st-widget + * @short_description: Base class for stylable actors + * + * #StWidget is a simple abstract class on top of #ClutterActor. It + * provides basic themeing properties. + * + * Actors in the St library should subclass #StWidget if they plan + * to obey to a certain #StStyle. + */ + +enum +{ + PROP_0, + + PROP_PSEUDO_CLASS, + PROP_STYLE_CLASS, + PROP_STYLE, + PROP_TRACK_HOVER, + PROP_HOVER, + PROP_CAN_FOCUS, + PROP_LABEL_ACTOR, + PROP_ACCESSIBLE_ROLE, + PROP_ACCESSIBLE_NAME, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum +{ + STYLE_CHANGED, + POPUP_MENU, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE_WITH_PRIVATE (StWidget, st_widget, CLUTTER_TYPE_ACTOR); +#define ST_WIDGET_PRIVATE(w) ((StWidgetPrivate *)st_widget_get_instance_private (w)) + +static void st_widget_recompute_style (StWidget *widget, + StThemeNode *old_theme_node); +static gboolean st_widget_real_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction); + +static AtkObject * st_widget_get_accessible (ClutterActor *actor); +static gboolean st_widget_has_accessible (ClutterActor *actor); + +static void +st_widget_update_insensitive (StWidget *widget) +{ + if (clutter_actor_get_reactive (CLUTTER_ACTOR (widget))) + st_widget_remove_style_pseudo_class (widget, "insensitive"); + else + st_widget_add_style_pseudo_class (widget, "insensitive"); +} + +static void +st_widget_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StWidget *actor = ST_WIDGET (gobject); + + switch (prop_id) + { + case PROP_PSEUDO_CLASS: + st_widget_set_style_pseudo_class (actor, g_value_get_string (value)); + break; + + case PROP_STYLE_CLASS: + st_widget_set_style_class_name (actor, g_value_get_string (value)); + break; + + case PROP_STYLE: + st_widget_set_style (actor, g_value_get_string (value)); + break; + + case PROP_TRACK_HOVER: + st_widget_set_track_hover (actor, g_value_get_boolean (value)); + break; + + case PROP_HOVER: + st_widget_set_hover (actor, g_value_get_boolean (value)); + break; + + case PROP_CAN_FOCUS: + st_widget_set_can_focus (actor, g_value_get_boolean (value)); + break; + + case PROP_LABEL_ACTOR: + st_widget_set_label_actor (actor, g_value_get_object (value)); + break; + + case PROP_ACCESSIBLE_ROLE: + st_widget_set_accessible_role (actor, g_value_get_enum (value)); + break; + + case PROP_ACCESSIBLE_NAME: + st_widget_set_accessible_name (actor, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_widget_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StWidget *actor = ST_WIDGET (gobject); + StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (gobject)); + + switch (prop_id) + { + case PROP_PSEUDO_CLASS: + g_value_set_string (value, priv->pseudo_class); + break; + + case PROP_STYLE_CLASS: + g_value_set_string (value, priv->style_class); + break; + + case PROP_STYLE: + g_value_set_string (value, priv->inline_style); + break; + + case PROP_TRACK_HOVER: + g_value_set_boolean (value, priv->track_hover); + break; + + case PROP_HOVER: + g_value_set_boolean (value, priv->hover); + break; + + case PROP_CAN_FOCUS: + g_value_set_boolean (value, priv->can_focus); + break; + + case PROP_LABEL_ACTOR: + g_value_set_object (value, priv->label_actor); + break; + + case PROP_ACCESSIBLE_ROLE: + g_value_set_enum (value, st_widget_get_accessible_role (actor)); + break; + + case PROP_ACCESSIBLE_NAME: + g_value_set_string (value, priv->accessible_name); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +st_widget_constructed (GObject *gobject) +{ + G_OBJECT_CLASS (st_widget_parent_class)->constructed (gobject); + + st_widget_update_insensitive (ST_WIDGET (gobject)); +} + +static void +st_widget_remove_transition (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->transition_animation) + { + g_object_run_dispose (G_OBJECT (priv->transition_animation)); + g_object_unref (priv->transition_animation); + priv->transition_animation = NULL; + } +} + +static void +next_paint_state (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + priv->current_paint_state = (priv->current_paint_state + 1) % G_N_ELEMENTS (priv->paint_states); +} + +static StThemeNodePaintState * +current_paint_state (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + return &priv->paint_states[priv->current_paint_state]; +} + +static void +st_widget_texture_cache_changed (StTextureCache *cache, + GFile *file, + gpointer user_data) +{ + StWidget *actor = ST_WIDGET (user_data); + StWidgetPrivate *priv = st_widget_get_instance_private (actor); + gboolean changed = FALSE; + int i; + + for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++) + { + StThemeNodePaintState *paint_state = &priv->paint_states[i]; + changed |= st_theme_node_paint_state_invalidate_for_file (paint_state, file); + } + + if (changed && clutter_actor_is_mapped (CLUTTER_ACTOR (actor))) + clutter_actor_queue_redraw (CLUTTER_ACTOR (actor)); +} + +static void +st_widget_dispose (GObject *gobject) +{ + StWidget *actor = ST_WIDGET (gobject); + StWidgetPrivate *priv = st_widget_get_instance_private (actor); + + g_clear_pointer (&priv->theme_node, g_object_unref); + + st_widget_remove_transition (actor); + + g_clear_pointer (&priv->label_actor, g_object_unref); + + g_clear_signal_handler (&priv->texture_file_changed_id, + st_texture_cache_get_default ()); + + g_clear_object (&priv->first_visible_child); + g_clear_object (&priv->last_visible_child); + + G_OBJECT_CLASS (st_widget_parent_class)->dispose (gobject); + + g_clear_handle_id (&priv->update_child_styles_id, g_source_remove); +} + +static void +st_widget_finalize (GObject *gobject) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (gobject)); + guint i; + + g_free (priv->style_class); + g_free (priv->pseudo_class); + g_object_unref (priv->local_state_set); + g_free (priv->accessible_name); + g_free (priv->inline_style); + + for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++) + st_theme_node_paint_state_free (&priv->paint_states[i]); + + G_OBJECT_CLASS (st_widget_parent_class)->finalize (gobject); +} + + +static void +st_widget_get_preferred_width (ClutterActor *self, + gfloat for_height, + gfloat *min_width_p, + gfloat *natural_width_p) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + + st_theme_node_adjust_for_width (theme_node, &for_height); + + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->get_preferred_width (self, for_height, min_width_p, natural_width_p); + + st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p); +} + +static void +st_widget_get_preferred_height (ClutterActor *self, + gfloat for_width, + gfloat *min_height_p, + gfloat *natural_height_p) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + + st_theme_node_adjust_for_width (theme_node, &for_width); + + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->get_preferred_height (self, for_width, min_height_p, natural_height_p); + + st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p); +} + +static void +st_widget_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor)); + ClutterActorBox content_box; + + /* Note that we can't just chain up to clutter_actor_real_allocate -- + * Clutter does some dirty tricks for backwards compatibility. + * Clutter also passes the actor's allocation directly to the layout + * manager, meaning that we can't modify it for children only. + */ + + clutter_actor_set_allocation (actor, box); + + st_theme_node_get_content_box (theme_node, box, &content_box); + + /* If we've chained up to here, we want to allocate the children using the + * currently installed layout manager */ + clutter_layout_manager_allocate (clutter_actor_get_layout_manager (actor), + CLUTTER_CONTAINER (actor), + &content_box); +} + +/** + * st_widget_paint_background: + * @widget: The #StWidget + * + * Paint the background of the widget. This is meant to be called by + * subclasses of StWidget that need to paint the background without + * painting children. + */ +void +st_widget_paint_background (StWidget *widget, + ClutterPaintContext *paint_context) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + CoglFramebuffer *framebuffer; + StThemeNode *theme_node; + ClutterActorBox allocation; + float resource_scale; + guint8 opacity; + + resource_scale = clutter_actor_get_resource_scale (CLUTTER_ACTOR (widget)); + + framebuffer = clutter_paint_context_get_framebuffer (paint_context); + theme_node = st_widget_get_theme_node (widget); + + clutter_actor_get_allocation_box (CLUTTER_ACTOR (widget), &allocation); + + opacity = clutter_actor_get_paint_opacity (CLUTTER_ACTOR (widget)); + + if (priv->transition_animation) + st_theme_node_transition_paint (priv->transition_animation, + framebuffer, + &allocation, + opacity, + resource_scale); + else + st_theme_node_paint (theme_node, + current_paint_state (widget), + framebuffer, + &allocation, + opacity, + resource_scale); +} + +static void +st_widget_paint (ClutterActor *actor, + ClutterPaintContext *paint_context) +{ + st_widget_paint_background (ST_WIDGET (actor), paint_context); + + /* Chain up so we paint children. */ + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->paint (actor, paint_context); +} + +static void +st_widget_parent_set (ClutterActor *widget, + ClutterActor *old_parent) +{ + StWidget *self = ST_WIDGET (widget); + ClutterActorClass *parent_class; + + parent_class = CLUTTER_ACTOR_CLASS (st_widget_parent_class); + if (parent_class->parent_set) + parent_class->parent_set (widget, old_parent); + + st_widget_style_changed (self); +} + +static void +st_widget_map (ClutterActor *actor) +{ + StWidget *self = ST_WIDGET (actor); + + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->map (actor); + + st_widget_ensure_style (self); +} + +static void +st_widget_unmap (ClutterActor *actor) +{ + StWidget *self = ST_WIDGET (actor); + StWidgetPrivate *priv = st_widget_get_instance_private (self); + + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->unmap (actor); + + st_widget_remove_transition (self); + + if (priv->track_hover && priv->hover) + st_widget_set_hover (self, FALSE); +} + +static void +notify_children_of_style_change (ClutterActor *self) +{ + ClutterActorIter iter; + ClutterActor *actor; + + clutter_actor_iter_init (&iter, self); + while (clutter_actor_iter_next (&iter, &actor)) + { + if (ST_IS_WIDGET (actor)) + st_widget_style_changed (ST_WIDGET (actor)); + else + notify_children_of_style_change (actor); + } +} + +static void +st_widget_real_style_changed (StWidget *self) +{ + clutter_actor_queue_redraw ((ClutterActor *) self); +} + +void +st_widget_style_changed (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + StThemeNode *old_theme_node = NULL; + + priv->is_style_dirty = TRUE; + if (priv->theme_node) + { + old_theme_node = priv->theme_node; + priv->theme_node = NULL; + } + + /* update the style only if we are mapped */ + if (clutter_actor_is_mapped (CLUTTER_ACTOR (widget))) + st_widget_recompute_style (widget, old_theme_node); + + /* Descend through all children. If the actor is not mapped, + * children will clear their theme node without recomputing style. + */ + notify_children_of_style_change (CLUTTER_ACTOR (widget)); + + if (old_theme_node) + g_object_unref (old_theme_node); +} + +static void +on_theme_context_changed (StThemeContext *context, + ClutterStage *stage) +{ + notify_children_of_style_change (CLUTTER_ACTOR (stage)); +} + +static StThemeNode * +get_root_theme_node (ClutterStage *stage) +{ + StThemeContext *context = st_theme_context_get_for_stage (stage); + + if (!g_object_get_data (G_OBJECT (context), "st-theme-initialized")) + { + g_object_set_data (G_OBJECT (context), "st-theme-initialized", GUINT_TO_POINTER (1)); + g_signal_connect (G_OBJECT (context), "changed", + G_CALLBACK (on_theme_context_changed), stage); + } + + return st_theme_context_get_root_node (context); +} + +/** + * st_widget_get_theme_node: + * @widget: a #StWidget + * + * Gets the theme node holding style information for the widget. + * The theme node is used to access standard and custom CSS + * properties of the widget. + * + * Note: it is a fatal error to call this on a widget that is + * not been added to a stage. + * + * Returns: (transfer none): the theme node for the widget. + * This is owned by the widget. When attributes of the widget + * or the environment that affect the styling change (for example + * the style_class property of the widget), it will be recreated, + * and the ::style-changed signal will be emitted on the widget. + */ +StThemeNode * +st_widget_get_theme_node (StWidget *widget) +{ + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET (widget), NULL); + + priv = st_widget_get_instance_private (widget); + + if (priv->theme_node == NULL) + { + StThemeContext *context; + StThemeNode *tmp_node; + StThemeNode *parent_node = NULL; + ClutterStage *stage = NULL; + ClutterActor *parent; + char *pseudo_class, *direction_pseudo_class; + + parent = clutter_actor_get_parent (CLUTTER_ACTOR (widget)); + while (parent != NULL) + { + if (parent_node == NULL && ST_IS_WIDGET (parent)) + parent_node = st_widget_get_theme_node (ST_WIDGET (parent)); + else if (CLUTTER_IS_STAGE (parent)) + stage = CLUTTER_STAGE (parent); + + parent = clutter_actor_get_parent (parent); + } + + if (stage == NULL) + { + g_autofree char *desc = st_describe_actor (CLUTTER_ACTOR (widget)); + + g_critical ("st_widget_get_theme_node called on the widget %s which is not in the stage.", + desc); + + return g_object_new (ST_TYPE_THEME_NODE, NULL); + } + + if (parent_node == NULL) + parent_node = get_root_theme_node (CLUTTER_STAGE (stage)); + + /* Always append a "magic" pseudo class indicating the text + * direction, to allow to adapt the CSS when necessary without + * requiring separate style sheets. + */ + if (clutter_actor_get_text_direction (CLUTTER_ACTOR (widget)) == CLUTTER_TEXT_DIRECTION_RTL) + direction_pseudo_class = (char *)"rtl"; + else + direction_pseudo_class = (char *)"ltr"; + + if (priv->pseudo_class) + pseudo_class = g_strconcat(priv->pseudo_class, " ", + direction_pseudo_class, NULL); + else + pseudo_class = direction_pseudo_class; + + context = st_theme_context_get_for_stage (stage); + tmp_node = st_theme_node_new (context, parent_node, NULL, + G_OBJECT_TYPE (widget), + clutter_actor_get_name (CLUTTER_ACTOR (widget)), + priv->style_class, + pseudo_class, + priv->inline_style); + + if (pseudo_class != direction_pseudo_class) + g_free (pseudo_class); + + priv->theme_node = g_object_ref (st_theme_context_intern_node (context, + tmp_node)); + g_object_unref (tmp_node); + } + + return priv->theme_node; +} + +/** + * st_widget_peek_theme_node: + * @widget: a #StWidget + * + * Returns the theme node for the widget if it has already been + * computed, %NULL if the widget hasn't been added to a stage or the theme + * node hasn't been computed. If %NULL is returned, then ::style-changed + * will be reliably emitted before the widget is allocated or painted. + * + * Returns: (transfer none): the theme node for the widget. + * This is owned by the widget. When attributes of the widget + * or the environment that affect the styling change (for example + * the style_class property of the widget), it will be recreated, + * and the ::style-changed signal will be emitted on the widget. + */ +StThemeNode * +st_widget_peek_theme_node (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), NULL); + + return ST_WIDGET_PRIVATE (widget)->theme_node; +} + +static gboolean +st_widget_enter (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (actor)); + + if (priv->track_hover) + { + ClutterStage *stage; + ClutterActor *target; + + stage = clutter_event_get_stage ((ClutterEvent *) event); + target = clutter_stage_get_event_actor (stage, (ClutterEvent *) event); + + if (clutter_actor_contains (actor, target)) + st_widget_set_hover (ST_WIDGET (actor), TRUE); + else + { + /* The widget has a grab and is being told about an + * enter-event outside its hierarchy. Hopefully we already + * got a leave-event, but if not, handle it now. + */ + st_widget_set_hover (ST_WIDGET (actor), FALSE); + } + } + + if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->enter_event) + return CLUTTER_ACTOR_CLASS (st_widget_parent_class)->enter_event (actor, event); + else + return FALSE; +} + +static gboolean +st_widget_leave (ClutterActor *actor, + ClutterCrossingEvent *event) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (actor)); + + if (priv->track_hover) + { + if (!event->related || !clutter_actor_contains (actor, event->related)) + st_widget_set_hover (ST_WIDGET (actor), FALSE); + } + + if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->leave_event) + return CLUTTER_ACTOR_CLASS (st_widget_parent_class)->leave_event (actor, event); + else + return FALSE; +} + +static void +st_widget_key_focus_in (ClutterActor *actor) +{ + StWidget *widget = ST_WIDGET (actor); + + st_widget_add_style_pseudo_class (widget, "focus"); +} + +static void +st_widget_key_focus_out (ClutterActor *actor) +{ + StWidget *widget = ST_WIDGET (actor); + + st_widget_remove_style_pseudo_class (widget, "focus"); +} + +static gboolean +st_widget_key_press_event (ClutterActor *actor, + ClutterKeyEvent *event) +{ + if (event->keyval == CLUTTER_KEY_Menu || + (event->keyval == CLUTTER_KEY_F10 && + (event->modifier_state & CLUTTER_SHIFT_MASK))) + { + st_widget_popup_menu (ST_WIDGET (actor)); + return TRUE; + } + + return FALSE; +} + +static gboolean +st_widget_get_paint_volume (ClutterActor *self, + ClutterPaintVolume *volume) +{ + ClutterActorBox paint_box, alloc_box; + StThemeNode *theme_node; + StWidgetPrivate *priv; + graphene_point3d_t origin; + + /* Setting the paint volume does not make sense when we don't have any allocation */ + if (!clutter_actor_has_allocation (self)) + return FALSE; + + priv = st_widget_get_instance_private (ST_WIDGET (self)); + + theme_node = st_widget_get_theme_node (ST_WIDGET (self)); + clutter_actor_get_allocation_box (self, &alloc_box); + + if (priv->transition_animation) + st_theme_node_transition_get_paint_box (priv->transition_animation, + &alloc_box, &paint_box); + else + st_theme_node_get_paint_box (theme_node, &alloc_box, &paint_box); + + origin.x = paint_box.x1 - alloc_box.x1; + origin.y = paint_box.y1 - alloc_box.y1; + origin.z = 0.0f; + + clutter_paint_volume_set_origin (volume, &origin); + clutter_paint_volume_set_width (volume, paint_box.x2 - paint_box.x1); + clutter_paint_volume_set_height (volume, paint_box.y2 - paint_box.y1); + + if (!clutter_actor_get_clip_to_allocation (self)) + { + ClutterActor *child; + StShadow *shadow_spec = st_theme_node_get_text_shadow (theme_node); + + if (shadow_spec) + { + ClutterActorBox shadow_box; + + st_shadow_get_box (shadow_spec, &alloc_box, &shadow_box); + clutter_paint_volume_union_box (volume, &shadow_box); + } + + /* Based on ClutterGroup/ClutterBox; include the children's + * paint volumes, since they may paint outside our allocation. + */ + for (child = clutter_actor_get_first_child (self); + child != NULL; + child = clutter_actor_get_next_sibling (child)) + { + const ClutterPaintVolume *child_volume; + + if (!clutter_actor_is_visible (child)) + continue; + + child_volume = clutter_actor_get_transformed_paint_volume (child, self); + if (!child_volume) + return FALSE; + + clutter_paint_volume_union (volume, child_volume); + } + } + + return TRUE; +} + +static GList * +st_widget_real_get_focus_chain (StWidget *widget) +{ + GList *children, *l, *visible = NULL; + + children = clutter_actor_get_children (CLUTTER_ACTOR (widget)); + + for (l = children; l; l = l->next) + { + if (clutter_actor_is_visible (CLUTTER_ACTOR (l->data))) + visible = g_list_prepend (visible, l->data); + } + + g_list_free (children); + + return g_list_reverse (visible); +} + +static void +st_widget_resource_scale_changed (ClutterActor *actor) +{ + StWidget *widget = ST_WIDGET (actor); + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + int i; + + for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++) + st_theme_node_paint_state_invalidate (&priv->paint_states[i]); + + if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->resource_scale_changed) + CLUTTER_ACTOR_CLASS (st_widget_parent_class)->resource_scale_changed (actor); +} + +static void +st_widget_class_init (StWidgetClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + gobject_class->set_property = st_widget_set_property; + gobject_class->get_property = st_widget_get_property; + gobject_class->constructed = st_widget_constructed; + gobject_class->dispose = st_widget_dispose; + gobject_class->finalize = st_widget_finalize; + + actor_class->get_preferred_width = st_widget_get_preferred_width; + actor_class->get_preferred_height = st_widget_get_preferred_height; + actor_class->allocate = st_widget_allocate; + actor_class->paint = st_widget_paint; + actor_class->get_paint_volume = st_widget_get_paint_volume; + actor_class->parent_set = st_widget_parent_set; + actor_class->map = st_widget_map; + actor_class->unmap = st_widget_unmap; + + actor_class->enter_event = st_widget_enter; + actor_class->leave_event = st_widget_leave; + actor_class->key_focus_in = st_widget_key_focus_in; + actor_class->key_focus_out = st_widget_key_focus_out; + actor_class->key_press_event = st_widget_key_press_event; + + actor_class->get_accessible = st_widget_get_accessible; + actor_class->has_accessible = st_widget_has_accessible; + + actor_class->resource_scale_changed = st_widget_resource_scale_changed; + + klass->style_changed = st_widget_real_style_changed; + klass->navigate_focus = st_widget_real_navigate_focus; + klass->get_accessible_type = st_widget_accessible_get_type; + klass->get_focus_chain = st_widget_real_get_focus_chain; + + /** + * StWidget:pseudo-class: + * + * The pseudo-class of the actor. Typical values include "hover", "active", + * "focus". + */ + props[PROP_PSEUDO_CLASS] = + g_param_spec_string ("pseudo-class", + "Pseudo Class", + "Pseudo class for styling", + "", + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:style-class: + * + * The style-class of the actor for use in styling. + */ + props[PROP_STYLE_CLASS] = + g_param_spec_string ("style-class", + "Style Class", + "Style class for styling", + "", + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:style: + * + * Inline style information for the actor as a ';'-separated list of + * CSS properties. + */ + props[PROP_STYLE] = + g_param_spec_string ("style", + "Style", + "Inline style string", + "", + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:track-hover: + * + * Determines whether the widget tracks pointer hover state. If + * %TRUE (and the widget is visible and reactive), the + * #StWidget:hover property and "hover" style pseudo class will be + * adjusted automatically as the pointer moves in and out of the + * widget. + */ + props[PROP_TRACK_HOVER] = + g_param_spec_boolean ("track-hover", + "Track hover", + "Determines whether the widget tracks hover state", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:hover: + * + * Whether or not the pointer is currently hovering over the widget. This is + * only tracked automatically if #StWidget:track-hover is %TRUE, but you can + * adjust it manually in any case. + */ + props[PROP_HOVER] = + g_param_spec_boolean ("hover", + "Hover", + "Whether the pointer is hovering over the widget", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:can-focus: + * + * Whether or not the widget can be focused via keyboard navigation. + */ + props[PROP_CAN_FOCUS] = + g_param_spec_boolean ("can-focus", + "Can focus", + "Whether the widget can be focused via keyboard navigation", + FALSE, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:label-actor: + * + * An actor that labels this widget. + */ + props[PROP_LABEL_ACTOR] = + g_param_spec_object ("label-actor", + "Label", + "Label that identifies this widget", + CLUTTER_TYPE_ACTOR, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:accessible-role: + * + * The accessible role of this object + */ + props[PROP_ACCESSIBLE_ROLE] = + g_param_spec_enum ("accessible-role", + "Accessible Role", + "The accessible role of this object", + ATK_TYPE_ROLE, + ATK_ROLE_INVALID, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * StWidget:accessible-name: + * + * Object instance's name for assistive technology access. + */ + props[PROP_ACCESSIBLE_NAME] = + g_param_spec_string ("accessible-name", + "Accessible name", + "Object instance's name for assistive technology access.", + NULL, + ST_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + /** + * StWidget::style-changed: + * @widget: the #StWidget + * + * Emitted when the style information that the widget derives from the + * theme changes + */ + signals[STYLE_CHANGED] = + g_signal_new ("style-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StWidgetClass, style_changed), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * StWidget::popup-menu: + * @widget: the #StWidget + * + * Emitted when the user has requested a context menu (eg, via a keybinding) + */ + signals[POPUP_MENU] = + g_signal_new ("popup-menu", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (StWidgetClass, popup_menu), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static const gchar * +find_class_name (const gchar *class_list, + const gchar *class_name) +{ + gint len = strlen (class_name); + const gchar *match; + + if (!class_list) + return NULL; + + for (match = strstr (class_list, class_name); match; match = strstr (match + 1, class_name)) + { + if ((match == class_list || g_ascii_isspace (match[-1])) && + (match[len] == '\0' || g_ascii_isspace (match[len]))) + return match; + } + + return NULL; +} + +static gboolean +set_class_list (gchar **class_list, + const gchar *new_class_list) +{ + if (g_strcmp0 (*class_list, new_class_list) != 0) + { + g_free (*class_list); + *class_list = g_strdup (new_class_list); + return TRUE; + } + else + return FALSE; +} + +static gboolean +add_class_name (gchar **class_list, + const gchar *class_name) +{ + gchar *new_class_list; + + if (*class_list) + { + if (find_class_name (*class_list, class_name)) + return FALSE; + + new_class_list = g_strdup_printf ("%s %s", *class_list, class_name); + g_free (*class_list); + *class_list = new_class_list; + } + else + *class_list = g_strdup (class_name); + + return TRUE; +} + +static gboolean +remove_class_name (gchar **class_list, + const gchar *class_name) +{ + const gchar *match, *end; + gchar *new_class_list; + + if (!*class_list) + return FALSE; + + if (strcmp (*class_list, class_name) == 0) + { + g_free (*class_list); + *class_list = NULL; + return TRUE; + } + + match = find_class_name (*class_list, class_name); + if (!match) + return FALSE; + end = match + strlen (class_name); + + /* Adjust either match or end to include a space as well. + * (One or the other must be possible at this point.) + */ + if (match != *class_list) + match--; + else + end++; + + new_class_list = g_strdup_printf ("%.*s%s", (int)(match - *class_list), + *class_list, end); + g_free (*class_list); + *class_list = new_class_list; + + return TRUE; +} + +/** + * st_widget_set_style_class_name: + * @actor: a #StWidget + * @style_class_list: (nullable): a new style class list string + * + * Set the style class name list. @style_class_list can either be + * %NULL, for no classes, or a space-separated list of style class + * names. See also st_widget_add_style_class_name() and + * st_widget_remove_style_class_name(). + */ +void +st_widget_set_style_class_name (StWidget *actor, + const gchar *style_class_list) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + + priv = st_widget_get_instance_private (actor); + + if (set_class_list (&priv->style_class, style_class_list)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_STYLE_CLASS]); + } +} + +/** + * st_widget_add_style_class_name: + * @actor: a #StWidget + * @style_class: a style class name string + * + * Adds @style_class to @actor's style class name list, if it is not + * already present. + */ +void +st_widget_add_style_class_name (StWidget *actor, + const gchar *style_class) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + g_return_if_fail (style_class != NULL); + + priv = st_widget_get_instance_private (actor); + + if (add_class_name (&priv->style_class, style_class)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_STYLE_CLASS]); + } +} + +/** + * st_widget_remove_style_class_name: + * @actor: a #StWidget + * @style_class: a style class name string + * + * Removes @style_class from @actor's style class name, if it is + * present. + */ +void +st_widget_remove_style_class_name (StWidget *actor, + const gchar *style_class) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + g_return_if_fail (style_class != NULL); + + priv = st_widget_get_instance_private (actor); + + if (remove_class_name (&priv->style_class, style_class)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_STYLE_CLASS]); + } +} + +/** + * st_widget_get_style_class_name: + * @actor: a #StWidget + * + * Get the current style class name + * + * Returns: the class name string. The string is owned by the #StWidget and + * should not be modified or freed. + */ +const gchar* +st_widget_get_style_class_name (StWidget *actor) +{ + g_return_val_if_fail (ST_IS_WIDGET (actor), NULL); + + return ST_WIDGET_PRIVATE (actor)->style_class; +} + +/** + * st_widget_has_style_class_name: + * @actor: a #StWidget + * @style_class: a style class string + * + * Tests if @actor's style class list includes @style_class. + * + * Returns: whether or not @actor's style class list includes + * @style_class. + */ +gboolean +st_widget_has_style_class_name (StWidget *actor, + const gchar *style_class) +{ + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE); + + priv = st_widget_get_instance_private (actor); + + return find_class_name (priv->style_class, style_class) != NULL; +} + +/** + * st_widget_get_style_pseudo_class: + * @actor: a #StWidget + * + * Get the current style pseudo class list. + * + * Note that an actor can have multiple pseudo classes; if you just + * want to test for the presence of a specific pseudo class, use + * st_widget_has_style_pseudo_class(). + * + * Returns: the pseudo class list string. The string is owned by the + * #StWidget and should not be modified or freed. + */ +const gchar* +st_widget_get_style_pseudo_class (StWidget *actor) +{ + g_return_val_if_fail (ST_IS_WIDGET (actor), NULL); + + return ST_WIDGET_PRIVATE (actor)->pseudo_class; +} + +/** + * st_widget_has_style_pseudo_class: + * @actor: a #StWidget + * @pseudo_class: a pseudo class string + * + * Tests if @actor's pseudo class list includes @pseudo_class. + * + * Returns: whether or not @actor's pseudo class list includes + * @pseudo_class. + */ +gboolean +st_widget_has_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class) +{ + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE); + + priv = st_widget_get_instance_private (actor); + + return find_class_name (priv->pseudo_class, pseudo_class) != NULL; +} + +/** + * st_widget_set_style_pseudo_class: + * @actor: a #StWidget + * @pseudo_class_list: (nullable): a new pseudo class list string + * + * Set the style pseudo class list. @pseudo_class_list can either be + * %NULL, for no classes, or a space-separated list of pseudo class + * names. See also st_widget_add_style_pseudo_class() and + * st_widget_remove_style_pseudo_class(). + */ +void +st_widget_set_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class_list) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + + priv = st_widget_get_instance_private (actor); + + if (set_class_list (&priv->pseudo_class, pseudo_class_list)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_PSEUDO_CLASS]); + } +} + +/** + * st_widget_add_style_pseudo_class: + * @actor: a #StWidget + * @pseudo_class: a pseudo class string + * + * Adds @pseudo_class to @actor's pseudo class list, if it is not + * already present. + */ +void +st_widget_add_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + g_return_if_fail (pseudo_class != NULL); + + priv = st_widget_get_instance_private (actor); + + if (add_class_name (&priv->pseudo_class, pseudo_class)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_PSEUDO_CLASS]); + } +} + +/** + * st_widget_remove_style_pseudo_class: + * @actor: a #StWidget + * @pseudo_class: a pseudo class string + * + * Removes @pseudo_class from @actor's pseudo class, if it is present. + */ +void +st_widget_remove_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + g_return_if_fail (pseudo_class != NULL); + + priv = st_widget_get_instance_private (actor); + + if (remove_class_name (&priv->pseudo_class, pseudo_class)) + { + st_widget_style_changed (actor); + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_PSEUDO_CLASS]); + } +} + +/** + * st_widget_set_style: + * @actor: a #StWidget + * @style: (nullable): a inline style string, or %NULL + * + * Set the inline style string for this widget. The inline style string is an + * optional ';'-separated list of CSS properties that override the style as + * determined from the stylesheets of the current theme. + */ +void +st_widget_set_style (StWidget *actor, + const gchar *style) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (actor)); + + priv = st_widget_get_instance_private (actor); + + if (g_strcmp0 (style, priv->inline_style)) + { + g_free (priv->inline_style); + priv->inline_style = g_strdup (style); + + st_widget_style_changed (actor); + + g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_STYLE]); + } +} + +/** + * st_widget_get_style: + * @actor: a #StWidget + * + * Get the current inline style string. See st_widget_set_style(). + * + * Returns: (transfer none) (nullable): The inline style string, or %NULL. The + * string is owned by the #StWidget and should not be modified or freed. + */ +const gchar* +st_widget_get_style (StWidget *actor) +{ + g_return_val_if_fail (ST_IS_WIDGET (actor), NULL); + + return ST_WIDGET_PRIVATE (actor)->inline_style; +} + +static void +st_widget_set_first_visible_child (StWidget *widget, + ClutterActor *actor) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->first_visible_child == NULL && actor == NULL) + return; + + if (priv->first_visible_child != NULL && + CLUTTER_ACTOR (priv->first_visible_child) == actor) + return; + + if (priv->first_visible_child != NULL) + { + st_widget_remove_style_pseudo_class (priv->first_visible_child, "first-child"); + g_clear_object (&priv->first_visible_child); + } + + if (actor == NULL) + return; + + if (ST_IS_WIDGET (actor)) + { + st_widget_add_style_pseudo_class (ST_WIDGET (actor), "first-child"); + priv->first_visible_child = g_object_ref (ST_WIDGET (actor)); + } +} + +static void +st_widget_set_last_visible_child (StWidget *widget, + ClutterActor *actor) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->last_visible_child == NULL && actor == NULL) + return; + + if (priv->last_visible_child != NULL && + CLUTTER_ACTOR (priv->last_visible_child) == actor) + return; + + if (priv->last_visible_child != NULL) + { + st_widget_remove_style_pseudo_class (priv->last_visible_child, "last-child"); + g_clear_object (&priv->last_visible_child); + } + + if (actor == NULL) + return; + + if (ST_IS_WIDGET (actor)) + { + st_widget_add_style_pseudo_class (ST_WIDGET (actor), "last-child"); + priv->last_visible_child = g_object_ref (ST_WIDGET (actor)); + } +} + +static void +st_widget_name_notify (StWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + st_widget_style_changed (widget); +} + +static void +st_widget_reactive_notify (StWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + st_widget_update_insensitive (widget); + + if (priv->track_hover) + st_widget_sync_hover(widget); +} + +static ClutterActor * +find_nearest_visible_backwards (ClutterActor *actor) +{ + ClutterActor *prev = actor; + + while (prev != NULL && !clutter_actor_is_visible (prev)) + prev = clutter_actor_get_previous_sibling (prev); + return prev; +} + +static ClutterActor * +find_nearest_visible_forward (ClutterActor *actor) +{ + ClutterActor *next = actor; + + while (next != NULL && !clutter_actor_is_visible (next)) + next = clutter_actor_get_next_sibling (next); + return next; +} + +static gboolean +st_widget_update_child_styles (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->first_child_dirty) + { + ClutterActor *first_child; + + priv->first_child_dirty = FALSE; + + first_child = clutter_actor_get_first_child (CLUTTER_ACTOR (widget)); + st_widget_set_first_visible_child (widget, + find_nearest_visible_forward (first_child)); + } + + if (priv->last_child_dirty) + { + ClutterActor *last_child; + + priv->last_child_dirty = FALSE; + + last_child = clutter_actor_get_last_child (CLUTTER_ACTOR (widget)); + st_widget_set_last_visible_child (widget, + find_nearest_visible_backwards (last_child)); + } + + priv->update_child_styles_id = 0; + return G_SOURCE_REMOVE; +} + +static void +st_widget_queue_child_styles_update (StWidget *widget) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->update_child_styles_id != 0) + return; + + priv->update_child_styles_id = g_idle_add ((GSourceFunc) st_widget_update_child_styles, widget); +} + +static void +st_widget_visible_notify (StWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + StWidgetPrivate *parent_priv; + ClutterActor *actor = CLUTTER_ACTOR (widget); + ClutterActor *parent = clutter_actor_get_parent (actor); + + if (parent == NULL || !ST_IS_WIDGET (parent)) + return; + + parent_priv = st_widget_get_instance_private (ST_WIDGET (parent)); + + if (clutter_actor_is_visible (actor)) + { + ClutterActor *before, *after; + + before = clutter_actor_get_previous_sibling (actor); + if (find_nearest_visible_backwards (before) == NULL) + parent_priv->first_child_dirty = TRUE; + + after = clutter_actor_get_next_sibling (actor); + if (find_nearest_visible_forward (after) == NULL) + parent_priv->last_child_dirty = TRUE; + } + else + { + if (st_widget_has_style_pseudo_class (widget, "first-child")) + parent_priv->first_child_dirty = TRUE; + + if (st_widget_has_style_pseudo_class (widget, "last-child")) + parent_priv->last_child_dirty = TRUE; + } + + if (parent_priv->first_child_dirty || parent_priv->last_child_dirty) + st_widget_queue_child_styles_update (ST_WIDGET (parent)); +} + +static void +st_widget_first_child_notify (StWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + priv->first_child_dirty = TRUE; + st_widget_queue_child_styles_update (widget); +} + +static void +st_widget_last_child_notify (StWidget *widget, + GParamSpec *pspec, + gpointer data) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + priv->last_child_dirty = TRUE; + st_widget_queue_child_styles_update (widget); +} + +static void +st_widget_init (StWidget *actor) +{ + StWidgetPrivate *priv; + guint i; + + priv = st_widget_get_instance_private (actor); + priv->transition_animation = NULL; + priv->local_state_set = atk_state_set_new (); + + /* connect style changed */ + g_signal_connect (actor, "notify::name", G_CALLBACK (st_widget_name_notify), NULL); + g_signal_connect (actor, "notify::reactive", G_CALLBACK (st_widget_reactive_notify), NULL); + + g_signal_connect (actor, "notify::visible", G_CALLBACK (st_widget_visible_notify), NULL); + g_signal_connect (actor, "notify::first-child", G_CALLBACK (st_widget_first_child_notify), NULL); + g_signal_connect (actor, "notify::last-child", G_CALLBACK (st_widget_last_child_notify), NULL); + priv->texture_file_changed_id = g_signal_connect (st_texture_cache_get_default (), "texture-file-changed", + G_CALLBACK (st_widget_texture_cache_changed), actor); + + for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++) + st_theme_node_paint_state_init (&priv->paint_states[i]); +} + +static void +on_transition_completed (StThemeNodeTransition *transition, + StWidget *widget) +{ + next_paint_state (widget); + + st_theme_node_paint_state_copy (current_paint_state (widget), + st_theme_node_transition_get_new_paint_state (transition)); + + st_widget_remove_transition (widget); +} + +static void +st_widget_recompute_style (StWidget *widget, + StThemeNode *old_theme_node) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + StThemeNode *new_theme_node = st_widget_get_theme_node (widget); + int transition_duration; + StSettings *settings; + gboolean paint_equal, geometry_equal = FALSE; + gboolean animations_enabled; + + if (new_theme_node == old_theme_node) + { + priv->is_style_dirty = FALSE; + return; + } + + _st_theme_node_apply_margins (new_theme_node, CLUTTER_ACTOR (widget)); + + if (old_theme_node) + geometry_equal = st_theme_node_geometry_equal (old_theme_node, new_theme_node); + if (!geometry_equal) + clutter_actor_queue_relayout ((ClutterActor *) widget); + + transition_duration = st_theme_node_get_transition_duration (new_theme_node); + + paint_equal = st_theme_node_paint_equal (old_theme_node, new_theme_node); + + settings = st_settings_get (); + g_object_get (settings, "enable-animations", &animations_enabled, NULL); + + if (animations_enabled && transition_duration > 0) + { + if (priv->transition_animation != NULL) + { + st_theme_node_transition_update (priv->transition_animation, + new_theme_node); + } + else if (old_theme_node && !paint_equal) + { + /* Since our transitions are only of the painting done by StThemeNode, we + * only want to start a transition when what is painted changes; if + * other visual aspects like the foreground color of a label change, + * we can't animate that anyways. + */ + + priv->transition_animation = + st_theme_node_transition_new (CLUTTER_ACTOR (widget), + old_theme_node, + new_theme_node, + current_paint_state (widget), + transition_duration); + + g_signal_connect (priv->transition_animation, "completed", + G_CALLBACK (on_transition_completed), widget); + g_signal_connect_swapped (priv->transition_animation, + "new-frame", + G_CALLBACK (clutter_actor_queue_redraw), + widget); + } + } + else if (priv->transition_animation) + { + st_widget_remove_transition (widget); + } + + if (!paint_equal) + { + clutter_actor_invalidate_paint_volume (CLUTTER_ACTOR (widget)); + + next_paint_state (widget); + + if (!st_theme_node_paint_equal (new_theme_node, current_paint_state (widget)->node)) + st_theme_node_paint_state_invalidate (current_paint_state (widget)); + } + + g_signal_emit (widget, signals[STYLE_CHANGED], 0); + + priv->is_style_dirty = FALSE; +} + +/** + * st_widget_ensure_style: + * @widget: A #StWidget + * + * Ensures that @widget has read its style information and propagated any + * changes to its children. + */ +void +st_widget_ensure_style (StWidget *widget) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->is_style_dirty) + { + st_widget_recompute_style (widget, NULL); + notify_children_of_style_change (CLUTTER_ACTOR (widget)); + } +} + +/** + * st_widget_set_track_hover: + * @widget: A #StWidget + * @track_hover: %TRUE if the widget should track the pointer hover state + * + * Enables hover tracking on the #StWidget. + * + * If hover tracking is enabled, and the widget is visible and + * reactive, then @widget's #StWidget:hover property will be updated + * automatically to reflect whether the pointer is in @widget (or one + * of its children), and @widget's #StWidget:pseudo-class will have + * the "hover" class added and removed from it accordingly. + * + * Note that currently it is not possible to correctly track the hover + * state when another actor has a pointer grab. You can use + * st_widget_sync_hover() to update the property manually in this + * case. + */ +void +st_widget_set_track_hover (StWidget *widget, + gboolean track_hover) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->track_hover != track_hover) + { + priv->track_hover = track_hover; + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_TRACK_HOVER]); + + if (priv->track_hover) + st_widget_sync_hover (widget); + else + st_widget_set_hover (widget, FALSE); + } +} + +/** + * st_widget_get_track_hover: + * @widget: A #StWidget + * + * Returns the current value of the #StWidget:track-hover property. See + * st_widget_set_track_hover() for more information. + * + * Returns: current value of track-hover on @widget + */ +gboolean +st_widget_get_track_hover (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE); + + return ST_WIDGET_PRIVATE (widget)->track_hover; +} + +/** + * st_widget_set_hover: + * @widget: A #StWidget + * @hover: whether the pointer is hovering over the widget + * + * Sets @widget's hover property and adds or removes "hover" from its + * pseudo class accordingly. + * + * If you have set #StWidget:track-hover, you should not need to call + * this directly. You can call st_widget_sync_hover() if the hover + * state might be out of sync due to another actor's pointer grab. + */ +void +st_widget_set_hover (StWidget *widget, + gboolean hover) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->hover != hover) + { + priv->hover = hover; + if (priv->hover) + st_widget_add_style_pseudo_class (widget, "hover"); + else + st_widget_remove_style_pseudo_class (widget, "hover"); + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_HOVER]); + } +} + +/** + * st_widget_sync_hover: + * @widget: A #StWidget + * + * Sets @widget's hover state according to the current pointer + * position. This can be used to ensure that it is correct after + * (or during) a pointer grab. + */ +void +st_widget_sync_hover (StWidget *widget) +{ + ClutterInputDevice *pointer; + ClutterActor *stage; + ClutterActor *pointer_actor; + ClutterSeat *seat; + + seat = clutter_backend_get_default_seat (clutter_get_default_backend ()); + pointer = clutter_seat_get_pointer (seat); + stage = clutter_actor_get_stage (CLUTTER_ACTOR (widget)); + if (!stage) + return; + + pointer_actor = clutter_stage_get_device_actor (CLUTTER_STAGE (stage), pointer, NULL); + if (pointer_actor && clutter_actor_get_reactive (CLUTTER_ACTOR (widget))) + st_widget_set_hover (widget, clutter_actor_contains (CLUTTER_ACTOR (widget), pointer_actor)); + else + st_widget_set_hover (widget, FALSE); +} + +/** + * st_widget_get_hover: + * @widget: A #StWidget + * + * If #StWidget:track-hover is set, this returns whether the pointer + * is currently over the widget. + * + * Returns: current value of hover on @widget + */ +gboolean +st_widget_get_hover (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE); + + return ST_WIDGET_PRIVATE (widget)->hover; +} + +/** + * st_widget_set_can_focus: + * @widget: A #StWidget + * @can_focus: %TRUE if the widget can receive keyboard focus + * via keyboard navigation + * + * Marks @widget as being able to receive keyboard focus via + * keyboard navigation. + */ +void +st_widget_set_can_focus (StWidget *widget, + gboolean can_focus) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->can_focus != can_focus) + { + priv->can_focus = can_focus; + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_CAN_FOCUS]); + } +} + +/** + * st_widget_get_can_focus: + * @widget: A #StWidget + * + * Returns the current value of the can-focus property. See + * st_widget_set_can_focus() for more information. + * + * Returns: current value of can-focus on @widget + */ +gboolean +st_widget_get_can_focus (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE); + + return ST_WIDGET_PRIVATE (widget)->can_focus; +} + +/** + * st_widget_popup_menu: + * @self: A #StWidget + * + * Asks the widget to pop-up a context menu by emitting #StWidget::popup-menu. + */ +void +st_widget_popup_menu (StWidget *self) +{ + g_signal_emit (self, signals[POPUP_MENU], 0); +} + +/* filter @children to contain only only actors that overlap @rbox + * when moving in @direction. (Assuming no transformations.) + */ +static GList * +filter_by_position (GList *children, + ClutterActorBox *rbox, + StDirectionType direction) +{ + ClutterActorBox cbox; + graphene_point3d_t abs_vertices[4]; + GList *l, *ret; + ClutterActor *child; + + for (l = children, ret = NULL; l; l = l->next) + { + child = l->data; + clutter_actor_get_abs_allocation_vertices (child, abs_vertices); + clutter_actor_box_from_vertices (&cbox, abs_vertices); + + /* Filter out children if they are in the wrong direction from + * @rbox, or if they don't overlap it. To account for floating- + * point imprecision, an actor is "down" (etc.) from an another + * actor even if it overlaps it by up to 0.1 pixels. + */ + switch (direction) + { + case ST_DIR_UP: + if (cbox.y2 > rbox->y1 + 0.1) + continue; + break; + + case ST_DIR_DOWN: + if (cbox.y1 < rbox->y2 - 0.1) + continue; + break; + + case ST_DIR_LEFT: + if (cbox.x2 > rbox->x1 + 0.1) + continue; + break; + + case ST_DIR_RIGHT: + if (cbox.x1 < rbox->x2 - 0.1) + continue; + break; + + case ST_DIR_TAB_BACKWARD: + case ST_DIR_TAB_FORWARD: + default: + g_return_val_if_reached (NULL); + } + + ret = g_list_prepend (ret, child); + } + + g_list_free (children); + return ret; +} + + +static void +get_midpoint (ClutterActorBox *box, + int *x, + int *y) +{ + *x = (box->x1 + box->x2) / 2; + *y = (box->y1 + box->y2) / 2; +} + +static double +get_distance (ClutterActor *actor, + ClutterActorBox *bbox) +{ + int ax, ay, bx, by, dx, dy; + ClutterActorBox abox; + graphene_point3d_t abs_vertices[4]; + + clutter_actor_get_abs_allocation_vertices (actor, abs_vertices); + clutter_actor_box_from_vertices (&abox, abs_vertices); + + get_midpoint (&abox, &ax, &ay); + get_midpoint (bbox, &bx, &by); + dx = ax - bx; + dy = ay - by; + + /* Not the exact distance, but good enough to sort by. */ + return dx*dx + dy*dy; +} + +static int +sort_by_distance (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + ClutterActor *actor_a = (ClutterActor *)a; + ClutterActor *actor_b = (ClutterActor *)b; + ClutterActorBox *box = user_data; + + return get_distance (actor_a, box) - get_distance (actor_b, box); +} + +static gboolean +st_widget_real_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + ClutterActor *widget_actor, *focus_child; + GList *children, *l; + + widget_actor = CLUTTER_ACTOR (widget); + if (from == widget_actor) + return FALSE; + + /* Figure out if @from is a descendant of @widget, and if so, + * set @focus_child to the immediate child of @widget that + * contains (or *is*) @from. + */ + focus_child = from; + while (focus_child && clutter_actor_get_parent (focus_child) != widget_actor) + focus_child = clutter_actor_get_parent (focus_child); + + if (priv->can_focus) + { + if (!focus_child) + { + if (clutter_actor_is_mapped (widget_actor)) + { + /* Accept focus from outside */ + clutter_actor_grab_key_focus (widget_actor); + return TRUE; + } + else + { + /* Refuse to set focus on hidden actors */ + return FALSE; + } + } + else + { + /* Yield focus from within: since @widget itself is + * focusable we don't allow the focus to be navigated + * within @widget. + */ + return FALSE; + } + } + + /* See if we can navigate within @focus_child */ + if (focus_child && ST_IS_WIDGET (focus_child)) + { + if (st_widget_navigate_focus (ST_WIDGET (focus_child), from, direction, FALSE)) + return TRUE; + } + + children = st_widget_get_focus_chain (widget); + if (direction == ST_DIR_TAB_FORWARD || + direction == ST_DIR_TAB_BACKWARD) + { + /* At this point we know that we want to navigate focus to one of + * @widget's immediate children; the next one after @focus_child, or the + * first one if @focus_child is %NULL. (With "next" and "first" being + * determined by @direction.) + */ + if (direction == ST_DIR_TAB_BACKWARD) + children = g_list_reverse (children); + + if (focus_child) + { + /* Remove focus_child and any earlier children */ + while (children && children->data != focus_child) + children = g_list_delete_link (children, children); + if (children) + children = g_list_delete_link (children, children); + } + } + else /* direction is an arrow key, not tab */ + { + ClutterActorBox sort_box; + graphene_point3d_t abs_vertices[4]; + + /* Compute the allocation box of the previous focused actor. If there + * was no previous focus, use the coordinates of the appropriate edge of + * @widget. + * + * Note that all of this code assumes the actors are not + * transformed (or at most, they are all scaled by the same + * amount). If @widget or any of its children is rotated, or + * any child is inconsistently scaled, then the focus chain will + * probably be unpredictable. + */ + if (from) + { + clutter_actor_get_abs_allocation_vertices (from, abs_vertices); + clutter_actor_box_from_vertices (&sort_box, abs_vertices); + } + else + { + clutter_actor_get_abs_allocation_vertices (widget_actor, abs_vertices); + clutter_actor_box_from_vertices (&sort_box, abs_vertices); + switch (direction) + { + case ST_DIR_UP: + sort_box.y1 = sort_box.y2; + break; + case ST_DIR_DOWN: + sort_box.y2 = sort_box.y1; + break; + case ST_DIR_LEFT: + sort_box.x1 = sort_box.x2; + break; + case ST_DIR_RIGHT: + sort_box.x2 = sort_box.x1; + break; + case ST_DIR_TAB_FORWARD: + case ST_DIR_TAB_BACKWARD: + default: + g_warn_if_reached (); + } + } + + if (from) + children = filter_by_position (children, &sort_box, direction); + if (children) + children = g_list_sort_with_data (children, sort_by_distance, &sort_box); + } + + /* Now try each child in turn */ + for (l = children; l; l = l->next) + { + if (ST_IS_WIDGET (l->data)) + { + if (st_widget_navigate_focus (l->data, from, direction, FALSE)) + { + g_list_free (children); + return TRUE; + } + } + } + + g_list_free (children); + return FALSE; +} + + +/** + * st_widget_navigate_focus: + * @widget: the "top level" container + * @from: (nullable): the actor that the focus is coming from + * @direction: the direction focus is moving in + * @wrap_around: whether focus should wrap around + * + * Tries to update the keyboard focus within @widget in response to a + * keyboard event. + * + * If @from is a descendant of @widget, this attempts to move the + * keyboard focus to the next descendant of @widget (in the order + * implied by @direction) that has the #StWidget:can-focus property + * set. If @from is %NULL, this attempts to focus either @widget + * itself, or its first descendant in the order implied by + * @direction. If @from is outside of @widget, it behaves as if it was + * a descendant if @direction is one of the directional arrows and as + * if it was %NULL otherwise. + * + * If a container type is marked #StWidget:can-focus, the expected + * behavior is that it will only take up a single slot on the focus + * chain as a whole, rather than allowing navigation between its child + * actors (or having a distinction between itself being focused and + * one of its children being focused). + * + * Some widget classes might have slightly different behavior from the + * above, where that would make more sense. + * + * If @wrap_around is %TRUE and @from is a child of @widget, but the + * widget has no further children that can accept the focus in the + * given direction, then st_widget_navigate_focus() will try a second + * time, using a %NULL @from, which should cause it to reset the focus + * to the first available widget in the given direction. + * + * Returns: %TRUE if clutter_actor_grab_key_focus() has been + * called on an actor. %FALSE if not. + */ +gboolean +st_widget_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction, + gboolean wrap_around) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE); + + if (ST_WIDGET_GET_CLASS (widget)->navigate_focus (widget, from, direction)) + return TRUE; + if (wrap_around && from && clutter_actor_contains (CLUTTER_ACTOR (widget), from)) + return ST_WIDGET_GET_CLASS (widget)->navigate_focus (widget, NULL, direction); + return FALSE; +} + +static gboolean +append_actor_text (GString *desc, + ClutterActor *actor) +{ + if (CLUTTER_IS_TEXT (actor)) + { + g_string_append_printf (desc, " (\"%s\")", + clutter_text_get_text (CLUTTER_TEXT (actor))); + return TRUE; + } + else if (ST_IS_LABEL (actor)) + { + g_string_append_printf (desc, " (\"%s\")", + st_label_get_text (ST_LABEL (actor))); + return TRUE; + } + else + return FALSE; +} + +/** + * st_describe_actor: + * @actor: a #ClutterActor + * + * Creates a string describing @actor, for use in debugging. This + * includes the class name and actor name (if any), plus if @actor + * is an #StWidget, its style class and pseudo class names. + * + * Returns: the debug name. + */ +char * +st_describe_actor (ClutterActor *actor) +{ + GString *desc; + const char *name; + int i; + + if (!actor) + return g_strdup ("[null]"); + + desc = g_string_new (NULL); + g_string_append_printf (desc, "[%p %s", actor, + G_OBJECT_TYPE_NAME (actor)); + + if (ST_IS_WIDGET (actor)) + { + const char *style_class = st_widget_get_style_class_name (ST_WIDGET (actor)); + const char *pseudo_class = st_widget_get_style_pseudo_class (ST_WIDGET (actor)); + char **classes; + + if (style_class) + { + classes = g_strsplit (style_class, ",", -1); + for (i = 0; classes[i]; i++) + { + g_strchug (classes[i]); + g_string_append_printf (desc, ".%s", classes[i]); + } + g_strfreev (classes); + } + + if (pseudo_class) + { + classes = g_strsplit (pseudo_class, ",", -1); + for (i = 0; classes[i]; i++) + { + g_strchug (classes[i]); + g_string_append_printf (desc, ":%s", classes[i]); + } + g_strfreev (classes); + } + } + + name = clutter_actor_get_name (actor); + if (name) + g_string_append_printf (desc, " \"%s\"", name); + + if (!append_actor_text (desc, actor)) + { + GList *children, *l; + + /* Do a limited search of @actor's children looking for a label */ + children = clutter_actor_get_children (actor); + for (l = children, i = 0; l && i < 20; l = l->next, i++) + { + if (append_actor_text (desc, l->data)) + break; + children = g_list_concat (children, clutter_actor_get_children (l->data)); + } + g_list_free (children); + } + + g_string_append_c (desc, ']'); + + return g_string_free (desc, FALSE); +} + +/** + * st_widget_get_label_actor: + * @widget: a #StWidget + * + * Gets the label that identifies @widget if it is defined + * + * Returns: (transfer none): the label that identifies the widget + */ +ClutterActor * +st_widget_get_label_actor (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), NULL); + + return ST_WIDGET_PRIVATE (widget)->label_actor; +} + +/** + * st_widget_set_label_actor: + * @widget: a #StWidget + * @label: a #ClutterActor + * + * Sets @label as the #ClutterActor that identifies (labels) + * @widget. @label can be %NULL to indicate that @widget is not + * labelled any more + */ + +void +st_widget_set_label_actor (StWidget *widget, + ClutterActor *label) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->label_actor != label) + { + if (priv->label_actor) + g_object_unref (priv->label_actor); + + if (label != NULL) + priv->label_actor = g_object_ref (label); + else + priv->label_actor = NULL; + + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_LABEL_ACTOR]); + } +} + +/** + * st_widget_set_accessible_name: + * @widget: widget to set the accessible name for + * @name: (nullable): a character string to be set as the accessible name + * + * This method sets @name as the accessible name for @widget. + * + * Usually you will have no need to set the accessible name for an + * object, as usually there is a label for most of the interface + * elements. So in general it is better to just use + * @st_widget_set_label_actor. This method is only required when you + * need to set an accessible name and there is no available label + * object. + * + */ +void +st_widget_set_accessible_name (StWidget *widget, + const gchar *name) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (g_strcmp0 (name, priv->accessible_name) == 0) + return; + + if (priv->accessible_name != NULL) + g_free (priv->accessible_name); + + priv->accessible_name = g_strdup (name); + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_ACCESSIBLE_NAME]); +} + +/** + * st_widget_get_accessible_name: + * @widget: widget to get the accessible name for + * + * Gets the accessible name for this widget. See + * st_widget_set_accessible_name() for more information. + * + * Returns: a character string representing the accessible name + * of the widget. + */ +const gchar * +st_widget_get_accessible_name (StWidget *widget) +{ + g_return_val_if_fail (ST_IS_WIDGET (widget), NULL); + + return ST_WIDGET_PRIVATE (widget)->accessible_name; +} + +/** + * st_widget_set_accessible_role: + * @widget: widget to set the accessible role for + * @role: The role to use + * + * This method sets @role as the accessible role for @widget. This + * role describes what kind of user interface element @widget is and + * is provided so that assistive technologies know how to present + * @widget to the user. + * + * Usually you will have no need to set the accessible role for an + * object, as this information is extracted from the context of the + * object (ie: a #StButton has by default a push button role). This + * method is only required when you need to redefine the role + * currently associated with the widget, for instance if it is being + * used in an unusual way (ie: a #StButton used as a togglebutton), or + * if a generic object is used directly (ie: a container as a menu + * item). + * + * If @role is #ATK_ROLE_INVALID, the role will not be changed + * and the accessible's default role will be used instead. + */ +void +st_widget_set_accessible_role (StWidget *widget, + AtkRole role) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (priv->accessible_role == role) + return; + + priv->accessible_role = role; + + g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_ACCESSIBLE_ROLE]); +} + + +/** + * st_widget_get_accessible_role: + * @widget: widget to get the accessible role for + * + * Gets the #AtkRole for this widget. See + * st_widget_set_accessible_role() for more information. + * + * Returns: accessible #AtkRole for this widget + */ +AtkRole +st_widget_get_accessible_role (StWidget *widget) +{ + StWidgetPrivate *priv; + AtkRole role = ATK_ROLE_INVALID; + + g_return_val_if_fail (ST_IS_WIDGET (widget), ATK_ROLE_INVALID); + + priv = st_widget_get_instance_private (widget); + + if (priv->accessible_role != ATK_ROLE_INVALID) + role = priv->accessible_role; + else if (priv->accessible != NULL) + role = atk_object_get_role (priv->accessible); + + return role; +} + +static void +notify_accessible_state_change (StWidget *widget, + AtkStateType state, + gboolean value) +{ + StWidgetPrivate *priv = st_widget_get_instance_private (widget); + + if (priv->accessible != NULL) + atk_object_notify_state_change (priv->accessible, state, value); +} + +/** + * st_widget_add_accessible_state: + * @widget: A #StWidget + * @state: #AtkStateType state to add + * + * This method adds @state as one of the accessible states for + * @widget. The list of states of a widget describes the current state + * of user interface element @widget and is provided so that assistive + * technologies know how to present @widget to the user. + * + * Usually you will have no need to add accessible states for an + * object, as the accessible object can extract most of the states + * from the object itself (ie: a #StButton knows when it is pressed). + * This method is only required when one cannot extract the + * information automatically from the object itself (i.e.: a generic + * container used as a toggle menu item will not automatically include + * the toggled state). + * + */ +void +st_widget_add_accessible_state (StWidget *widget, + AtkStateType state) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (atk_state_set_add_state (priv->local_state_set, state)) + notify_accessible_state_change (widget, state, TRUE); +} + +/** + * st_widget_remove_accessible_state: + * @widget: A #StWidget + * @state: #AtkState state to remove + * + * This method removes @state as on of the accessible states for + * @widget. See st_widget_add_accessible_state() for more information. + * + */ +void +st_widget_remove_accessible_state (StWidget *widget, + AtkStateType state) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + + priv = st_widget_get_instance_private (widget); + + if (atk_state_set_remove_state (priv->local_state_set, state)) + notify_accessible_state_change (widget, state, FALSE); +} + +/******************************************************************************/ +/*************************** ACCESSIBILITY SUPPORT ****************************/ +/******************************************************************************/ + +/* GObject */ + +static void st_widget_accessible_dispose (GObject *gobject); + +/* AtkObject */ +static AtkStateSet *st_widget_accessible_ref_state_set (AtkObject *obj); +static void st_widget_accessible_initialize (AtkObject *obj, + gpointer data); +static AtkRole st_widget_accessible_get_role (AtkObject *obj); + +/* Private methods */ +static void on_pseudo_class_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data); +static void on_can_focus_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data); +static void on_label_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data); +static void check_pseudo_class (StWidgetAccessible *self, + StWidget *widget); +static void check_labels (StWidgetAccessible *self, + StWidget *widget); + +struct _StWidgetAccessiblePrivate +{ + /* Cached values (used to avoid extra notifications) */ + gboolean selected; + gboolean checked; + + /* The current_label. Right now there are the proper atk + * relationships between this object and the label + */ + AtkObject *current_label; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (StWidgetAccessible, st_widget_accessible, CALLY_TYPE_ACTOR) + +static gboolean +st_widget_has_accessible (ClutterActor *actor) +{ + StWidget *widget; + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE); + + widget = ST_WIDGET (actor); + priv = st_widget_get_instance_private (widget); + + return priv->accessible != NULL; +} + +static AtkObject * +st_widget_get_accessible (ClutterActor *actor) +{ + StWidget *widget = NULL; + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET (actor), NULL); + + widget = ST_WIDGET (actor); + priv = st_widget_get_instance_private (widget); + + if (priv->accessible == NULL) + { + priv->accessible = + g_object_new (ST_WIDGET_GET_CLASS (widget)->get_accessible_type (), + NULL); + + atk_object_initialize (priv->accessible, actor); + + /* AtkGObjectAccessible, which StWidgetAccessible derives from, clears + * the back reference to the object in a weak notify for the object; + * weak-ref notification, which occurs during g_object_real_dispose(), + * is then the optimal time to clear the forward reference. We + * can't clear the reference in dispose() before chaining up, since + * clutter_actor_dispose() causes notifications to be sent out, which + * will result in a new accessible object being created. + */ + g_object_add_weak_pointer (G_OBJECT (actor), + (gpointer *)&priv->accessible); + } + + return priv->accessible; +} + +/** + * st_widget_set_accessible: + * @widget: A #StWidget + * @accessible: an accessible (#AtkObject) + * + * This method allows to set a customly created accessible object to + * this widget. For example if you define a new subclass of + * #StWidgetAccessible at the javascript code. + * + * NULL is a valid value for @accessible. That contemplates the + * hypothetical case of not needing anymore a custom accessible object + * for the widget. Next call of st_widget_get_accessible() would + * create and return a default accessible. + * + * It assumes that the call to atk_object_initialize that bound the + * gobject with the custom accessible object was already called, so + * not a responsibility of this method. + * + */ +void +st_widget_set_accessible (StWidget *widget, + AtkObject *accessible) +{ + StWidgetPrivate *priv; + + g_return_if_fail (ST_IS_WIDGET (widget)); + g_return_if_fail (accessible == NULL || ATK_IS_GOBJECT_ACCESSIBLE (accessible)); + + priv = st_widget_get_instance_private (widget); + + if (priv->accessible != accessible) + { + if (priv->accessible) + { + g_object_remove_weak_pointer (G_OBJECT (widget), + (gpointer *)&priv->accessible); + g_object_unref (priv->accessible); + priv->accessible = NULL; + } + + if (accessible) + { + priv->accessible = g_object_ref (accessible); + /* See note in st_widget_get_accessible() */ + g_object_add_weak_pointer (G_OBJECT (widget), + (gpointer *)&priv->accessible); + } + else + priv->accessible = NULL; + } +} + +static const gchar * +st_widget_accessible_get_name (AtkObject *obj) +{ + const gchar* name = NULL; + + g_return_val_if_fail (ST_IS_WIDGET_ACCESSIBLE (obj), NULL); + + name = ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->get_name (obj); + if (name == NULL) + { + StWidget *widget = NULL; + + widget = ST_WIDGET (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (widget == NULL) + name = NULL; + else + name = st_widget_get_accessible_name (widget); + } + + return name; +} + +static void +st_widget_accessible_class_init (StWidgetAccessibleClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass); + + gobject_class->dispose = st_widget_accessible_dispose; + + atk_class->ref_state_set = st_widget_accessible_ref_state_set; + atk_class->initialize = st_widget_accessible_initialize; + atk_class->get_role = st_widget_accessible_get_role; + atk_class->get_name = st_widget_accessible_get_name; +} + +static void +st_widget_accessible_init (StWidgetAccessible *self) +{ + StWidgetAccessiblePrivate *priv = st_widget_accessible_get_instance_private (self); + + self->priv = priv; +} + +static void +st_widget_accessible_dispose (GObject *gobject) +{ + StWidgetAccessible *self = ST_WIDGET_ACCESSIBLE (gobject); + + if (self->priv->current_label) + { + g_object_unref (self->priv->current_label); + self->priv->current_label = NULL; + } + + G_OBJECT_CLASS (st_widget_accessible_parent_class)->dispose (gobject); +} + +static void +on_accessible_name_notify (GObject *gobject, + GParamSpec *pspec, + AtkObject *accessible) +{ + g_object_notify (G_OBJECT (accessible), "accessible-name"); +} + +static void +st_widget_accessible_initialize (AtkObject *obj, + gpointer data) +{ + ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->initialize (obj, data); + + g_signal_connect (data, "notify::pseudo-class", + G_CALLBACK (on_pseudo_class_notify), + obj); + + g_signal_connect (data, "notify::can-focus", + G_CALLBACK (on_can_focus_notify), + obj); + + g_signal_connect (data, "notify::label-actor", + G_CALLBACK (on_label_notify), + obj); + + g_signal_connect (data, "notify::accessible-name", + G_CALLBACK (on_accessible_name_notify), + obj); + + /* Check the cached selected state and notify the first selection. + * Ie: it is required to ensure a first notification when Alt+Tab + * popup appears + */ + check_pseudo_class (ST_WIDGET_ACCESSIBLE (obj), ST_WIDGET (data)); + check_labels (ST_WIDGET_ACCESSIBLE (obj), ST_WIDGET (data)); +} + +static AtkStateSet * +st_widget_accessible_ref_state_set (AtkObject *obj) +{ + AtkStateSet *result = NULL; + AtkStateSet *aux_set = NULL; + ClutterActor *actor = NULL; + StWidget *widget = NULL; + StWidgetPrivate *widget_priv; + StWidgetAccessible *self = NULL; + + result = ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->ref_state_set (obj); + + actor = CLUTTER_ACTOR (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (actor == NULL) /* State is defunct */ + return result; + + widget = ST_WIDGET (actor); + self = ST_WIDGET_ACCESSIBLE (obj); + widget_priv = st_widget_get_instance_private (widget); + + /* priv->selected should be properly updated on the + * ATK_STATE_SELECTED notification callbacks + */ + if (self->priv->selected) + atk_state_set_add_state (result, ATK_STATE_SELECTED); + + if (self->priv->checked) + atk_state_set_add_state (result, ATK_STATE_CHECKED); + + /* On clutter there isn't any tip to know if a actor is focusable or + * not, anyone can receive the key_focus. For this reason + * cally_actor sets any actor as FOCUSABLE. This is not the case on + * St, where we have can_focus. But this means that we need to + * remove the state FOCUSABLE if it is not focusable + */ + if (st_widget_get_can_focus (widget)) + atk_state_set_add_state (result, ATK_STATE_FOCUSABLE); + else + atk_state_set_remove_state (result, ATK_STATE_FOCUSABLE); + + /* We add the states added externally if required */ + if (!atk_state_set_is_empty (widget_priv->local_state_set)) + { + aux_set = atk_state_set_or_sets (result, widget_priv->local_state_set); + + g_object_unref (result); /* previous result will not be used */ + result = aux_set; + } + + return result; +} + +static AtkRole +st_widget_accessible_get_role (AtkObject *obj) +{ + StWidget *widget = NULL; + StWidgetPrivate *priv; + + g_return_val_if_fail (ST_IS_WIDGET_ACCESSIBLE (obj), ATK_ROLE_INVALID); + + widget = ST_WIDGET (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj))); + + if (widget == NULL) + return ATK_ROLE_INVALID; + + priv = st_widget_get_instance_private (widget); + if (priv->accessible_role != ATK_ROLE_INVALID) + return priv->accessible_role; + + return ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->get_role (obj); +} + +static void +on_pseudo_class_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + check_pseudo_class (ST_WIDGET_ACCESSIBLE (data), + ST_WIDGET (gobject)); +} + +/* + * In some cases the only way to check some states are checking the + * pseudo-class. Like if the object is selected (see bug 637830) or if + * the object is toggled. This method also notifies a state change if + * the value is different to the one cached. + * + * We also assume that if the object uses that pseudo-class, it makes + * sense to notify that state change. It would be possible to refine + * that behaviour checking the role (ie: notify CHECKED changes only + * for CHECK_BUTTON roles). + * + * In a ideal world we would have a more standard way to get the + * state, like the widget-context (as in the case of + * gtktreeview-cells), or something like the property "can-focus". But + * for the moment this is enough, and we can update that in the future + * if required. + */ +static void +check_pseudo_class (StWidgetAccessible *self, + StWidget *widget) +{ + gboolean found = FALSE; + + found = st_widget_has_style_pseudo_class (widget, + "selected"); + + if (found != self->priv->selected) + { + self->priv->selected = found; + atk_object_notify_state_change (ATK_OBJECT (self), + ATK_STATE_SELECTED, + found); + } + + found = st_widget_has_style_pseudo_class (widget, + "checked"); + if (found != self->priv->checked) + { + self->priv->checked = found; + atk_object_notify_state_change (ATK_OBJECT (self), + ATK_STATE_CHECKED, + found); + } +} + +static void +on_can_focus_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + gboolean can_focus = st_widget_get_can_focus (ST_WIDGET (gobject)); + + atk_object_notify_state_change (ATK_OBJECT (data), + ATK_STATE_FOCUSABLE, can_focus); +} + +static void +on_label_notify (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + check_labels (ST_WIDGET_ACCESSIBLE (data), ST_WIDGET (gobject)); +} + +static void +check_labels (StWidgetAccessible *widget_accessible, + StWidget *widget) +{ + ClutterActor *label = NULL; + AtkObject *label_accessible = NULL; + + /* We only call this method at startup, and when the label changes, + * so it is fine to remove the previous relationships if we have the + * current_label by default + */ + if (widget_accessible->priv->current_label != NULL) + { + AtkObject *previous_label = widget_accessible->priv->current_label; + + atk_object_remove_relationship (ATK_OBJECT (widget_accessible), + ATK_RELATION_LABELLED_BY, + previous_label); + + atk_object_remove_relationship (previous_label, + ATK_RELATION_LABEL_FOR, + ATK_OBJECT (widget_accessible)); + + g_object_unref (previous_label); + } + + label = st_widget_get_label_actor (widget); + if (label == NULL) + { + widget_accessible->priv->current_label = NULL; + } + else + { + label_accessible = clutter_actor_get_accessible (label); + widget_accessible->priv->current_label = g_object_ref (label_accessible); + + atk_object_add_relationship (ATK_OBJECT (widget_accessible), + ATK_RELATION_LABELLED_BY, + label_accessible); + + atk_object_add_relationship (label_accessible, + ATK_RELATION_LABEL_FOR, + ATK_OBJECT (widget_accessible)); + } +} + +/** + * st_widget_get_focus_chain: + * @widget: An #StWidget + * + * Gets a list of the focusable children of @widget, in "Tab" + * order. By default, this returns all visible + * (as in clutter_actor_is_visible()) children of @widget. + * + * Returns: (element-type Clutter.Actor) (transfer container): + * @widget's focusable children + */ +GList * +st_widget_get_focus_chain (StWidget *widget) +{ + return ST_WIDGET_GET_CLASS (widget)->get_focus_chain (widget); +} diff --git a/src/st/st-widget.h b/src/st/st-widget.h new file mode 100644 index 0000000..f00c987 --- /dev/null +++ b/src/st/st-widget.h @@ -0,0 +1,167 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * st-widget.h: Base class for St actors + * + * Copyright 2007 OpenedHand + * Copyright 2008, 2009 Intel Corporation. + * Copyright 2009, 2010 Red Hat, Inc. + * Copyright 2009 Abderrahim Kitouni + * Copyright 2010 Florian Müllner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#if !defined(ST_H_INSIDE) && !defined(ST_COMPILATION) +#error "Only <st/st.h> can be included directly.h" +#endif + +#ifndef __ST_WIDGET_H__ +#define __ST_WIDGET_H__ + +#include <clutter/clutter.h> +#include <st/st-types.h> +#include <st/st-theme.h> +#include <st/st-theme-node.h> + +G_BEGIN_DECLS + +#define ST_TYPE_WIDGET (st_widget_get_type ()) +G_DECLARE_DERIVABLE_TYPE (StWidget, st_widget, ST, WIDGET, ClutterActor) + +/** + * StDirectionType: + * @ST_DIR_TAB_FORWARD: Move forward. + * @ST_DIR_TAB_BACKWARD: Move backward. + * @ST_DIR_UP: Move up. + * @ST_DIR_DOWN: Move down. + * @ST_DIR_LEFT: Move left. + * @ST_DIR_RIGHT: Move right. + * + * Enumeration for focus direction. + */ +typedef enum +{ + ST_DIR_TAB_FORWARD, + ST_DIR_TAB_BACKWARD, + ST_DIR_UP, + ST_DIR_DOWN, + ST_DIR_LEFT, + ST_DIR_RIGHT, +} StDirectionType; + +typedef struct _StWidgetClass StWidgetClass; + +/** + * StWidgetClass: + * + * Base class for stylable actors. + */ +struct _StWidgetClass +{ + /*< private >*/ + ClutterActorClass parent_class; + + /* signals */ + void (* style_changed) (StWidget *self); + void (* popup_menu) (StWidget *self); + + /* vfuncs */ + + /** + * StWidgetClass::navigate_focus: + * @self: the "top level" container + * @from: (nullable): the actor that the focus is coming from + * @direction: the direction focus is moving in + */ + gboolean (* navigate_focus) (StWidget *self, + ClutterActor *from, + StDirectionType direction); + GType (* get_accessible_type) (void); + + GList * (* get_focus_chain) (StWidget *widget); +}; + +void st_widget_set_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class_list); +void st_widget_add_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class); +void st_widget_remove_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class); +const gchar * st_widget_get_style_pseudo_class (StWidget *actor); +gboolean st_widget_has_style_pseudo_class (StWidget *actor, + const gchar *pseudo_class); + +void st_widget_set_style_class_name (StWidget *actor, + const gchar *style_class_list); +void st_widget_add_style_class_name (StWidget *actor, + const gchar *style_class); +void st_widget_remove_style_class_name (StWidget *actor, + const gchar *style_class); +const gchar * st_widget_get_style_class_name (StWidget *actor); +gboolean st_widget_has_style_class_name (StWidget *actor, + const gchar *style_class); + +void st_widget_set_style (StWidget *actor, + const gchar *style); +const gchar * st_widget_get_style (StWidget *actor); +void st_widget_set_track_hover (StWidget *widget, + gboolean track_hover); +gboolean st_widget_get_track_hover (StWidget *widget); +void st_widget_set_hover (StWidget *widget, + gboolean hover); +void st_widget_sync_hover (StWidget *widget); +gboolean st_widget_get_hover (StWidget *widget); +void st_widget_popup_menu (StWidget *self); + +void st_widget_ensure_style (StWidget *widget); + +void st_widget_set_can_focus (StWidget *widget, + gboolean can_focus); +gboolean st_widget_get_can_focus (StWidget *widget); +gboolean st_widget_navigate_focus (StWidget *widget, + ClutterActor *from, + StDirectionType direction, + gboolean wrap_around); + +ClutterActor * st_widget_get_label_actor (StWidget *widget); +void st_widget_set_label_actor (StWidget *widget, + ClutterActor *label); + +/* Only to be used by sub-classes of StWidget */ +void st_widget_style_changed (StWidget *widget); +StThemeNode * st_widget_get_theme_node (StWidget *widget); +StThemeNode * st_widget_peek_theme_node (StWidget *widget); + +GList * st_widget_get_focus_chain (StWidget *widget); +void st_widget_paint_background (StWidget *widget, + ClutterPaintContext *paint_context); + +/* debug methods */ +char *st_describe_actor (ClutterActor *actor); + +/* accessibility methods */ +void st_widget_set_accessible_role (StWidget *widget, + AtkRole role); +AtkRole st_widget_get_accessible_role (StWidget *widget); +void st_widget_add_accessible_state (StWidget *widget, + AtkStateType state); +void st_widget_remove_accessible_state (StWidget *widget, + AtkStateType state); +void st_widget_set_accessible_name (StWidget *widget, + const gchar *name); +const gchar * st_widget_get_accessible_name (StWidget *widget); +void st_widget_set_accessible (StWidget *widget, + AtkObject *accessible); +G_END_DECLS + +#endif /* __ST_WIDGET_H__ */ diff --git a/src/st/st.h.in b/src/st/st.h.in new file mode 100644 index 0000000..825c820 --- /dev/null +++ b/src/st/st.h.in @@ -0,0 +1,3 @@ +#define ST_H_INSIDE 1 +@includes@ +#undef ST_H_INSIDE diff --git a/src/st/test-theme.c b/src/st/test-theme.c new file mode 100644 index 0000000..3fcbd99 --- /dev/null +++ b/src/st/test-theme.c @@ -0,0 +1,637 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * test-theme.c: test program for CSS styling code + * + * 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 Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for + * more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <clutter/clutter.h> +#include "st-theme.h" +#include "st-theme-context.h" +#include "st-label.h" +#include "st-button.h" +#include <math.h> +#include <string.h> +#include <meta-test/meta-context-test.h> +#include <meta/meta-backend.h> + +static ClutterActor *stage; +static StThemeNode *root; +static StThemeNode *group1; +static StThemeNode *text1; +static StThemeNode *text2; +static StThemeNode *group2; +static StThemeNode *text3; +static StThemeNode *text4; +static StThemeNode *group3; +static StThemeNode *group4; +static StThemeNode *group5; +static StThemeNode *group6; +static StThemeNode *button; +static gboolean fail; + +static const char *test; + +static void +assert_font (StThemeNode *node, + const char *node_description, + const char *expected) +{ + char *value = pango_font_description_to_string (st_theme_node_get_font (node)); + + if (strcmp (expected, value) != 0) + { + g_print ("%s: %s.font: expected: %s, got: %s\n", + test, node_description, expected, value); + fail = TRUE; + } + + g_free (value); +} + +static void +assert_font_features (StThemeNode *node, + const char *node_description, + const char *expected) +{ + char *value = st_theme_node_get_font_features (node); + + if (g_strcmp0 (expected, value) != 0) + { + g_print ("%s: %s.font-feature-settings: expected: %s, got: %s\n", + test, node_description, expected, value); + fail = TRUE; + } + + if (value) + g_free (value); +} + +static char * +text_decoration_to_string (StTextDecoration decoration) +{ + GString *result = g_string_new (NULL); + + if (decoration & ST_TEXT_DECORATION_UNDERLINE) + g_string_append(result, " underline"); + if (decoration & ST_TEXT_DECORATION_OVERLINE) + g_string_append(result, " overline"); + if (decoration & ST_TEXT_DECORATION_LINE_THROUGH) + g_string_append(result, " line_through"); + if (decoration & ST_TEXT_DECORATION_BLINK) + g_string_append(result, " blink"); + + if (result->len > 0) + g_string_erase (result, 0, 1); + else + g_string_append(result, "none"); + + return g_string_free (result, FALSE); +} + +static void +assert_text_decoration (StThemeNode *node, + const char *node_description, + StTextDecoration expected) +{ + StTextDecoration value = st_theme_node_get_text_decoration (node); + if (expected != value) + { + char *es = text_decoration_to_string (expected); + char *vs = text_decoration_to_string (value); + + g_print ("%s: %s.text-decoration: expected: %s, got: %s\n", + test, node_description, es, vs); + fail = TRUE; + + g_free (es); + g_free (vs); + } +} + +static void +assert_foreground_color (StThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + guint32 value; + + st_theme_node_get_foreground_color (node, &color); + value = clutter_color_to_pixel (&color); + + if (expected != value) + { + g_print ("%s: %s.color: expected: #%08x, got: #%08x\n", + test, node_description, expected, value); + fail = TRUE; + } +} + +static void +assert_background_color (StThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + guint32 value; + + st_theme_node_get_background_color (node, &color); + value = clutter_color_to_pixel (&color); + + if (expected != value) + { + g_print ("%s: %s.background-color: expected: #%08x, got: #%08x\n", + test, node_description, expected, value); + fail = TRUE; + } +} + +static const char * +side_to_string (StSide side) +{ + switch (side) + { + case ST_SIDE_TOP: + return "top"; + case ST_SIDE_RIGHT: + return "right"; + case ST_SIDE_BOTTOM: + return "bottom"; + case ST_SIDE_LEFT: + return "left"; + default: + return "<unknown>"; + } +} + +static void +assert_border_color (StThemeNode *node, + const char *node_description, + StSide side, + guint32 expected) +{ + ClutterColor color; + guint32 value; + + st_theme_node_get_border_color (node, side, &color); + value = clutter_color_to_pixel (&color); + + if (expected != value) + { + g_print ("%s: %s.border-%s-color: expected: #%08x, got: #%08x\n", + test, node_description, side_to_string (side), expected, value); + fail = TRUE; + } +} + +static void +assert_background_image (StThemeNode *node, + const char *node_description, + const char *expected) +{ + GFile *value = st_theme_node_get_background_image (node); + GFile *expected_file; + + if (expected != NULL && value != NULL) + { + expected_file = g_file_new_for_path (expected); + + if (!g_file_equal (expected_file, value)) + { + char *uri = g_file_get_uri (expected_file); + g_print ("%s: %s.background-image: expected: %s, got: %s\n", + test, node_description, expected, uri); + fail = TRUE; + g_free (uri); + } + } +} + +#define LENGTH_EPSILON 0.001 + +static void +assert_length (const char *node_description, + const char *property_description, + double expected, + double value) +{ + if (fabs (expected - value) > LENGTH_EPSILON) + { + g_print ("%s %s.%s: expected: %3f, got: %3f\n", + test, node_description, property_description, expected, value); + fail = TRUE; + } +} + +static void +test_defaults (void) +{ + test = "defaults"; + /* font comes from context */ + assert_font (root, "stage", "sans-serif 12"); + /* black is the default foreground color */ + assert_foreground_color (root, "stage", 0x00000ff); +} + +static void +test_lengths (void) +{ + test = "lengths"; + /* 12pt == 16px at 96dpi */ + assert_length ("group1", "padding-top", 16., + st_theme_node_get_padding (group1, ST_SIDE_TOP)); + /* 12px == 12px */ + assert_length ("group1", "padding-right", 12., + st_theme_node_get_padding (group1, ST_SIDE_RIGHT)); + /* 2em == 32px (with a 12pt font) */ + assert_length ("group1", "padding-bottom", 32., + st_theme_node_get_padding (group1, ST_SIDE_BOTTOM)); + /* 1in == 72pt == 96px, at 96dpi */ + assert_length ("group1", "padding-left", 96., + st_theme_node_get_padding (group1, ST_SIDE_LEFT)); + + /* 12pt == 16px at 96dpi */ + assert_length ("group1", "margin-top", 16., + st_theme_node_get_margin (group1, ST_SIDE_TOP)); + /* 12px == 12px */ + assert_length ("group1", "margin-right", 12., + st_theme_node_get_margin (group1, ST_SIDE_RIGHT)); + /* 2em == 32px (with a 12pt font) */ + assert_length ("group1", "margin-bottom", 32., + st_theme_node_get_margin (group1, ST_SIDE_BOTTOM)); + /* 1in == 72pt == 96px, at 96dpi */ + assert_length ("group1", "margin-left", 96., + st_theme_node_get_margin (group1, ST_SIDE_LEFT)); +} + +static void +test_classes (void) +{ + test = "classes"; + /* .special-text class overrides size and style; + * the StBin.special-text selector doesn't match */ + assert_font (text1, "text1", "sans-serif Italic 32px"); +} + +static void +test_type_inheritance (void) +{ + test = "type_inheritance"; + /* From StBin element selector */ + assert_length ("button", "padding-top", 10., + st_theme_node_get_padding (button, ST_SIDE_TOP)); + /* From StButton element selector */ + assert_length ("button", "padding-right", 20., + st_theme_node_get_padding (button, ST_SIDE_RIGHT)); +} + +static void +test_adjacent_selector (void) +{ + test = "adjacent_selector"; + /* #group1 > #text1 matches text1 */ + assert_foreground_color (text1, "text1", 0x00ff00ff); + /* stage > #text2 doesn't match text2 */ + assert_foreground_color (text2, "text2", 0x000000ff); +} + +static void +test_padding (void) +{ + test = "padding"; + /* Test that a 4-sided padding property assigns the right paddings to + * all sides */ + assert_length ("group2", "padding-top", 1., + st_theme_node_get_padding (group2, ST_SIDE_TOP)); + assert_length ("group2", "padding-right", 2., + st_theme_node_get_padding (group2, ST_SIDE_RIGHT)); + assert_length ("group2", "padding-bottom", 3., + st_theme_node_get_padding (group2, ST_SIDE_BOTTOM)); + assert_length ("group2", "padding-left", 4., + st_theme_node_get_padding (group2, ST_SIDE_LEFT)); +} + +static void +test_margin (void) +{ + test = "margin"; + /* Test that a 4-sided margin property assigns the right margin to + * all sides */ + assert_length ("group2", "margin-top", 1., + st_theme_node_get_margin (group2, ST_SIDE_TOP)); + assert_length ("group2", "margin-right", 2., + st_theme_node_get_margin (group2, ST_SIDE_RIGHT)); + assert_length ("group2", "margin-bottom", 3., + st_theme_node_get_margin (group2, ST_SIDE_BOTTOM)); + assert_length ("group2", "margin-left", 4., + st_theme_node_get_margin (group2, ST_SIDE_LEFT)); + + /* Test that a 3-sided margin property assigns the right margin to + * all sides */ + assert_length ("group4", "margin-top", 1., + st_theme_node_get_margin (group4, ST_SIDE_TOP)); + assert_length ("group4", "margin-right", 2., + st_theme_node_get_margin (group4, ST_SIDE_RIGHT)); + assert_length ("group4", "margin-bottom", 3., + st_theme_node_get_margin (group4, ST_SIDE_BOTTOM)); + assert_length ("group4", "margin-left", 2., + st_theme_node_get_margin (group4, ST_SIDE_LEFT)); + + /* Test that a 2-sided margin property assigns the right margin to + * all sides */ + assert_length ("group5", "margin-top", 1., + st_theme_node_get_margin (group5, ST_SIDE_TOP)); + assert_length ("group5", "margin-right", 2., + st_theme_node_get_margin (group5, ST_SIDE_RIGHT)); + assert_length ("group5", "margin-bottom", 1., + st_theme_node_get_margin (group5, ST_SIDE_BOTTOM)); + assert_length ("group5", "margin-left", 2., + st_theme_node_get_margin (group5, ST_SIDE_LEFT)); + + /* Test that all sides have a margin of 0 when not specified */ + assert_length ("group6", "margin-top", 0., + st_theme_node_get_margin (group6, ST_SIDE_TOP)); + assert_length ("group6", "margin-right", 0., + st_theme_node_get_margin (group6, ST_SIDE_RIGHT)); + assert_length ("group6", "margin-bottom", 0., + st_theme_node_get_margin (group6, ST_SIDE_BOTTOM)); + assert_length ("group6", "margin-left", 0., + st_theme_node_get_margin (group6, ST_SIDE_LEFT)); +} + +static void +test_border (void) +{ + test = "border"; + + /* group2 is defined as having a thin black border along the top three + * sides with rounded joins, then a square-joined green border at the + * bottom + */ + + assert_length ("group2", "border-top-width", 2., + st_theme_node_get_border_width (group2, ST_SIDE_TOP)); + assert_length ("group2", "border-right-width", 2., + st_theme_node_get_border_width (group2, ST_SIDE_RIGHT)); + assert_length ("group2", "border-bottom-width", 5., + st_theme_node_get_border_width (group2, ST_SIDE_BOTTOM)); + assert_length ("group2", "border-left-width", 2., + st_theme_node_get_border_width (group2, ST_SIDE_LEFT)); + + assert_border_color (group2, "group2", ST_SIDE_TOP, 0x000000ff); + assert_border_color (group2, "group2", ST_SIDE_RIGHT, 0x000000ff); + assert_border_color (group2, "group2", ST_SIDE_BOTTOM, 0x0000ffff); + assert_border_color (group2, "group2", ST_SIDE_LEFT, 0x000000ff); + + assert_length ("group2", "border-radius-topleft", 10., + st_theme_node_get_border_radius (group2, ST_CORNER_TOPLEFT)); + assert_length ("group2", "border-radius-topright", 10., + st_theme_node_get_border_radius (group2, ST_CORNER_TOPRIGHT)); + assert_length ("group2", "border-radius-bottomright", 0., + st_theme_node_get_border_radius (group2, ST_CORNER_BOTTOMRIGHT)); + assert_length ("group2", "border-radius-bottomleft", 0., + st_theme_node_get_border_radius (group2, ST_CORNER_BOTTOMLEFT)); +} + +static void +test_background (void) +{ + test = "background"; + /* group1 has a background: shortcut property setting color and image */ + assert_background_color (group1, "group1", 0xff0000ff); + assert_background_image (group1, "group1", "some-background.png"); + /* text1 inherits the background image but not the color */ + assert_background_color (text1, "text1", 0x00000000); + assert_background_image (text1, "text1", "some-background.png"); + /* text2 inherits both, but then background: none overrides both */ + assert_background_color (text2, "text2", 0x00000000); + assert_background_image (text2, "text2", NULL); + /* background-image property */ + assert_background_image (group2, "group2", "other-background.png"); +} + +static void +test_font (void) +{ + test = "font"; + /* font specified with font: */ + assert_font (group2, "group2", "serif Italic 12px"); + /* text3 inherits and overrides individually properties */ + assert_font (text3, "text3", "serif Bold Oblique Small-Caps 24px"); +} + +static void +test_font_features (void) +{ + test = "font_features"; + /* group1 has font-feature-settings: "tnum" */ + assert_font_features (group1, "group1", "\"tnum\""); + /* text2 should inherit from group1 */ + assert_font_features (text2, "text2", "\"tnum\""); + /* group2 has font-feature-settings: "tnum", "zero" */ + assert_font_features (group2, "group2", "\"tnum\", \"zero\""); + /* text3 should inherit from group2 using the inherit keyword */ + assert_font_features (text3, "text3", "\"tnum\", \"zero\""); + /* text4 has font-feature-settings: normal */ + assert_font_features (text4, "text4", NULL); +} + +static void +test_pseudo_class (void) +{ + StWidget *label; + StThemeNode *labelNode; + + test = "pseudo_class"; + /* text4 has :visited and :hover pseudo-classes, so should pick up both of these */ + assert_foreground_color (text4, "text4", 0x888888ff); + assert_text_decoration (text4, "text4", ST_TEXT_DECORATION_UNDERLINE); + /* :hover pseudo-class matches, but class doesn't match */ + assert_text_decoration (group3, "group3", 0); + + /* Test the StWidget add/remove pseudo_class interfaces */ + label = st_label_new ("foo"); + clutter_actor_add_child (stage, CLUTTER_ACTOR (label)); + + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x000000ff); + assert_text_decoration (labelNode, "label", 0); + assert_length ("label", "border-width", 0., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_add_style_pseudo_class (label, "visited"); + g_assert (st_widget_has_style_pseudo_class (label, "visited")); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x888888ff); + assert_text_decoration (labelNode, "label", 0); + assert_length ("label", "border-width", 0., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_add_style_pseudo_class (label, "hover"); + g_assert (st_widget_has_style_pseudo_class (label, "hover")); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x888888ff); + assert_text_decoration (labelNode, "label", ST_TEXT_DECORATION_UNDERLINE); + assert_length ("label", "border-width", 0., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_remove_style_pseudo_class (label, "visited"); + g_assert (!st_widget_has_style_pseudo_class (label, "visited")); + g_assert (st_widget_has_style_pseudo_class (label, "hover")); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x000000ff); + assert_text_decoration (labelNode, "label", ST_TEXT_DECORATION_UNDERLINE); + assert_length ("label", "border-width", 0., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_add_style_pseudo_class (label, "boxed"); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x000000ff); + assert_text_decoration (labelNode, "label", ST_TEXT_DECORATION_UNDERLINE); + assert_length ("label", "border-width", 1., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_remove_style_pseudo_class (label, "hover"); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x000000ff); + assert_text_decoration (labelNode, "label", 0); + assert_length ("label", "border-width", 1., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + st_widget_remove_style_pseudo_class (label, "boxed"); + g_assert (!st_widget_has_style_pseudo_class (label, "boxed")); + g_assert (st_widget_has_style_pseudo_class (label, "insensitive")); + labelNode = st_widget_get_theme_node (label); + assert_foreground_color (labelNode, "label", 0x000000ff); + assert_text_decoration (labelNode, "label", 0); + assert_length ("label", "border-width", 0., + st_theme_node_get_border_width (labelNode, ST_SIDE_TOP)); + + clutter_actor_set_reactive (CLUTTER_ACTOR (label), TRUE); + g_assert (st_widget_get_style_pseudo_class (label) == NULL); +} + +static void +test_inline_style (void) +{ + test = "inline_style"; + /* These properties come from the inline-style specified when creating the node */ + assert_foreground_color (text3, "text3", 0x00000ffff); + assert_length ("text3", "padding-bottom", 12., + st_theme_node_get_padding (text3, ST_SIDE_BOTTOM)); +} + +int +main (int argc, char **argv) +{ + MetaContext *context; + g_autoptr (GError) error = NULL; + MetaBackend *backend; + StTheme *theme; + StThemeContext *theme_context; + PangoFontDescription *font_desc; + GFile *file; + g_autofree char *cwd = NULL; + + gtk_init (&argc, &argv); + + /* meta_init() cds to $HOME */ + cwd = g_get_current_dir (); + + context = meta_create_test_context (META_CONTEXT_TEST_TYPE_NESTED, + META_CONTEXT_TEST_FLAG_NONE); + if (!meta_context_configure (context, &argc, &argv, &error)) + g_error ("Failed to configure: %s", error->message); + + if (!meta_context_setup (context, &error)) + g_error ("Failed to setup: %s", error->message); + + if (chdir (cwd) < 0) + g_error ("chdir('%s') failed: %s", cwd, g_strerror (errno)); + + /* Make sure our assumptions about resolution are correct */ + g_object_set (clutter_settings_get_default (), "font-dpi", -1, NULL); + + file = g_file_new_for_path ("test-theme.css"); + theme = st_theme_new (file, NULL, NULL); + g_object_unref (file); + + backend = meta_get_backend (); + stage = meta_backend_get_stage (backend); + theme_context = st_theme_context_get_for_stage (CLUTTER_STAGE (stage)); + st_theme_context_set_theme (theme_context, theme); + + font_desc = pango_font_description_from_string ("sans-serif 12"); + st_theme_context_set_font (theme_context, font_desc); + pango_font_description_free (font_desc); + + root = st_theme_context_get_root_node (theme_context); + group1 = st_theme_node_new (theme_context, root, NULL, + CLUTTER_TYPE_ACTOR, "group1", NULL, NULL, NULL); + text1 = st_theme_node_new (theme_context, group1, NULL, + CLUTTER_TYPE_TEXT, "text1", "special-text", NULL, NULL); + text2 = st_theme_node_new (theme_context, group1, NULL, + CLUTTER_TYPE_TEXT, "text2", NULL, NULL, NULL); + group2 = st_theme_node_new (theme_context, root, NULL, + CLUTTER_TYPE_ACTOR, "group2", NULL, NULL, NULL); + group4 = st_theme_node_new (theme_context, root, NULL, + CLUTTER_TYPE_ACTOR, "group4", NULL, NULL, NULL); + group5 = st_theme_node_new (theme_context, root, NULL, + CLUTTER_TYPE_ACTOR, "group5", NULL, NULL, NULL); + group6 = st_theme_node_new (theme_context, root, NULL, + CLUTTER_TYPE_ACTOR, "group6", NULL, NULL, NULL); + text3 = st_theme_node_new (theme_context, group2, NULL, + CLUTTER_TYPE_TEXT, "text3", NULL, NULL, + "color: #0000ff; padding-bottom: 12px;"); + text4 = st_theme_node_new (theme_context, group2, NULL, + CLUTTER_TYPE_TEXT, "text4", NULL, "visited hover", NULL); + group3 = st_theme_node_new (theme_context, group2, NULL, + CLUTTER_TYPE_ACTOR, "group3", NULL, "hover", NULL); + button = st_theme_node_new (theme_context, root, NULL, + ST_TYPE_BUTTON, "button", NULL, NULL, NULL); + + test_defaults (); + test_lengths (); + test_classes (); + test_type_inheritance (); + test_adjacent_selector (); + test_padding (); + test_margin (); + test_border (); + test_background (); + test_font (); + test_font_features (); + test_pseudo_class (); + test_inline_style (); + + g_object_unref (button); + g_object_unref (group1); + g_object_unref (group2); + g_object_unref (group3); + g_object_unref (group4); + g_object_unref (group5); + g_object_unref (group6); + g_object_unref (text1); + g_object_unref (text2); + g_object_unref (text3); + g_object_unref (text4); + g_object_unref (theme); + + g_object_unref (context); + + return fail ? 1 : 0; +} diff --git a/src/st/test-theme.css b/src/st/test-theme.css new file mode 100644 index 0000000..d180255 --- /dev/null +++ b/src/st/test-theme.css @@ -0,0 +1,107 @@ +stage { +} + +#group1 { + padding: 12pt; + padding-right: 12px; + padding-bottom: 2em; + padding-left: 1in; + + margin: 12pt; + margin-right: 12px; + margin-bottom: 2em; + margin-left: 1in; + + background: #ff0000 url('some-background.png'); + + font-feature-settings: "tnum"; +} + +#text1 { + background-image: inherit; +} + +.special-text { + font-size: 24pt; + font-style: italic; +} + +StBin.special-text { + font-weight: bold; +} + +#text2 { + background: inherit; + background: none; /* also overrides the color */ +} + +#group2 { + font: italic 12px serif; + font-feature-settings: "tnum", "zero"; +} + +#text3 { + font-variant: small-caps; + font-weight: bold; + font-style: oblique; + font-size: 200%; + font-feature-settings: "pnum"; +} + +#text4 { + font-feature-settings: normal; +} + +StBin { + padding: 10px; +} + +StButton { + padding-right: 20px; +} + +#group1 > #text1 { + color: #00ff00; +} + +stage > #text2 { + color: #ff0000; +} + +#group2 > #text3 { + font-feature-settings: inherit; +} + +#group2 { + background-image: url('other-background.png'); + padding: 1px 2px 3px 4px; + margin: 1px 2px 3px 4px; + + border: 2px solid #000000; + border-bottom: 5px solid #0000ff; + border-radius: 10px 10px 0px 0px; +} + +ClutterText:hover, StLabel:hover { + text-decoration: underline; +} + +ClutterText:visited, StLabel:visited { + color: #888888; +} + +StLabel:boxed { + border: 1px; +} + +#group4 { + margin: 1px 2px 3px; +} + +#group5 { + margin: 1px 2px; +} + +#group6 { + padding: 5px; +} diff --git a/src/tray/meson.build b/src/tray/meson.build new file mode 100644 index 0000000..139e3b2 --- /dev/null +++ b/src/tray/meson.build @@ -0,0 +1,12 @@ +tray_sources = [ + 'na-tray-child.c', + 'na-tray-child.h', + 'na-tray-manager.c', + 'na-tray-manager.h' +] + +libtray = static_library('tray', tray_sources, + c_args: ['-DG_LOG_DOMAIN="notification_area"'], + dependencies: [clutter_dep, gtk_dep], + include_directories: conf_inc +) diff --git a/src/tray/na-tray-child.c b/src/tray/na-tray-child.c new file mode 100644 index 0000000..175ac8a --- /dev/null +++ b/src/tray/na-tray-child.c @@ -0,0 +1,504 @@ +/* na-tray-child.c + * Copyright (C) 2002 Anders Carlsson <andersca@gnu.org> + * Copyright (C) 2003-2006 Vincent Untz + * Copyright (C) 2008 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +#include <config.h> +#include <string.h> + +#include "na-tray-child.h" + +#include <gdk/gdk.h> +#include <gdk/gdkx.h> +#include <X11/Xatom.h> + +G_DEFINE_TYPE (NaTrayChild, na_tray_child, GTK_TYPE_SOCKET) + +static void +na_tray_child_finalize (GObject *object) +{ + G_OBJECT_CLASS (na_tray_child_parent_class)->finalize (object); +} + +static void +na_tray_child_realize (GtkWidget *widget) +{ + NaTrayChild *child = NA_TRAY_CHILD (widget); + GdkVisual *visual = gtk_widget_get_visual (widget); + GdkWindow *window; + + GTK_WIDGET_CLASS (na_tray_child_parent_class)->realize (widget); + + window = gtk_widget_get_window (widget); + + if (child->has_alpha) + { + /* We have real transparency with an ARGB visual and the Composite + * extension. */ + + /* Set a transparent background */ + cairo_pattern_t *transparent = cairo_pattern_create_rgba (0, 0, 0, 0); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gdk_window_set_background_pattern (window, transparent); +G_GNUC_END_IGNORE_DEPRECATIONS + cairo_pattern_destroy (transparent); + + child->parent_relative_bg = FALSE; + } + else if (visual == gdk_window_get_visual (gdk_window_get_parent (window))) + { + /* Otherwise, if the visual matches the visual of the parent window, we + * can use a parent-relative background and fake transparency. */ +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gdk_window_set_background_pattern (window, NULL); +G_GNUC_END_IGNORE_DEPRECATIONS + + child->parent_relative_bg = TRUE; + } + else + { + /* Nothing to do; the icon will sit on top of an ugly gray box */ + child->parent_relative_bg = FALSE; + } + + gtk_widget_set_app_paintable (GTK_WIDGET (child), + child->parent_relative_bg || child->has_alpha); + + /* Double-buffering will interfere with the parent-relative-background fake + * transparency, since the double-buffer code doesn't know how to fill in the + * background of the double-buffer correctly. + * The function is deprecated because it is only meaningful on X11 - the + * same is true for XEmbed of course, so just ignore the warning. + */ +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + gtk_widget_set_double_buffered (GTK_WIDGET (child), + child->parent_relative_bg); +G_GNUC_END_IGNORE_DEPRECATIONS +} + +static void +na_tray_child_style_set (GtkWidget *widget, + GtkStyle *previous_style) +{ + /* The default handler resets the background according to the new style. + * We either use a transparent background or a parent-relative background + * and ignore the style background. So, just don't chain up. + */ +} + +#if 0 +/* This is adapted from code that was commented out in na-tray-manager.c; the + * code in na-tray-manager.c wouldn't have worked reliably, this will. So maybe + * it can be re-enabled. On other hand, things seem to be working fine without + * it. + * + * If reenabling, you need to hook it up in na_tray_child_class_init(). + */ +static void +na_tray_child_size_request (GtkWidget *widget, + GtkRequisition *request) +{ + GTK_WIDGET_CLASS (na_tray_child_parent_class)->size_request (widget, request); + + /* + * Make sure the icons have a meaningful size .. + */ + if ((request->width < 16) || (request->height < 16)) + { + gint nw = MAX (24, request->width); + gint nh = MAX (24, request->height); + g_warning ("Tray icon has requested a size of (%ix%i), resizing to (%ix%i)", + req.width, req.height, nw, nh); + request->width = nw; + request->height = nh; + } +} +#endif + +static void +na_tray_child_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + NaTrayChild *child = NA_TRAY_CHILD (widget); + GtkAllocation widget_allocation; + gboolean moved, resized; + + gtk_widget_get_allocation (widget, &widget_allocation); + + moved = (allocation->x != widget_allocation.x || + allocation->y != widget_allocation.y); + resized = (allocation->width != widget_allocation.width || + allocation->height != widget_allocation.height); + + /* When we are allocating the widget while mapped we need special handling + * for both real and fake transparency. + * + * Real transparency: we need to invalidate and trigger a redraw of the old + * and new areas. (GDK really should handle this for us, but doesn't as of + * GTK+-2.14) + * + * Fake transparency: if the widget moved, we need to force the contents to + * be redrawn with the new offset for the parent-relative background. + */ + if ((moved || resized) && gtk_widget_get_mapped (widget)) + { + if (na_tray_child_has_alpha (child)) + gdk_window_invalidate_rect (gdk_window_get_parent (gtk_widget_get_window (widget)), + &widget_allocation, FALSE); + } + + GTK_WIDGET_CLASS (na_tray_child_parent_class)->size_allocate (widget, + allocation); + + if ((moved || resized) && gtk_widget_get_mapped (widget)) + { + if (na_tray_child_has_alpha (NA_TRAY_CHILD (widget))) + gdk_window_invalidate_rect (gdk_window_get_parent (gtk_widget_get_window (widget)), + &widget_allocation, FALSE); + else if (moved && child->parent_relative_bg) + na_tray_child_force_redraw (child); + } +} + +/* The plug window should completely occupy the area of the child, so we won't + * get a draw event. But in case we do (the plug unmaps itself, say), this + * draw handler draws with real or fake transparency. + */ +static gboolean +na_tray_child_draw (GtkWidget *widget, + cairo_t *cr) +{ + NaTrayChild *child = NA_TRAY_CHILD (widget); + + if (na_tray_child_has_alpha (child)) + { + /* Clear to transparent */ + cairo_set_source_rgba (cr, 0, 0, 0, 0); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + cairo_paint (cr); + } + else if (child->parent_relative_bg) + { + GdkWindow *window; + cairo_surface_t *target; + GdkRectangle clip_rect; + + window = gtk_widget_get_window (widget); + target = cairo_get_group_target (cr); + + gdk_cairo_get_clip_rectangle (cr, &clip_rect); + + /* Clear to parent-relative pixmap + * We need to use direct X access here because GDK doesn't know about + * the parent relative pixmap. */ + cairo_surface_flush (target); + + XClearArea (GDK_WINDOW_XDISPLAY (window), + GDK_WINDOW_XID (window), + clip_rect.x, clip_rect.y, + clip_rect.width, clip_rect.height, + False); + cairo_surface_mark_dirty_rectangle (target, + clip_rect.x, clip_rect.y, + clip_rect.width, clip_rect.height); + } + + return FALSE; +} + +static void +na_tray_child_init (NaTrayChild *child) +{ +} + +static void +na_tray_child_class_init (NaTrayChildClass *klass) +{ + GObjectClass *gobject_class; + GtkWidgetClass *widget_class; + + gobject_class = (GObjectClass *)klass; + widget_class = (GtkWidgetClass *)klass; + + gobject_class->finalize = na_tray_child_finalize; + widget_class->style_set = na_tray_child_style_set; + widget_class->realize = na_tray_child_realize; + widget_class->size_allocate = na_tray_child_size_allocate; + widget_class->draw = na_tray_child_draw; +} + +GtkWidget * +na_tray_child_new (GdkScreen *screen, + Window icon_window) +{ + XWindowAttributes window_attributes; + GdkDisplay *display; + Display *xdisplay; + NaTrayChild *child; + GdkVisual *visual; + gboolean visual_has_alpha; + int red_prec, green_prec, blue_prec, depth; + int result; + + g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL); + g_return_val_if_fail (icon_window != None, NULL); + + xdisplay = GDK_SCREEN_XDISPLAY (screen); + display = gdk_x11_lookup_xdisplay (xdisplay); + + /* We need to determine the visual of the window we are embedding and create + * the socket in the same visual. + */ + + gdk_x11_display_error_trap_push (display); + result = XGetWindowAttributes (xdisplay, icon_window, + &window_attributes); + gdk_x11_display_error_trap_pop_ignored (display); + + if (!result) /* Window already gone */ + return NULL; + + visual = gdk_x11_screen_lookup_visual (screen, + window_attributes.visual->visualid); + if (!visual) /* Icon window is on another screen? */ + return NULL; + + child = g_object_new (NA_TYPE_TRAY_CHILD, NULL); + child->icon_window = icon_window; + + gtk_widget_set_visual (GTK_WIDGET (child), visual); + + /* We have alpha if the visual has something other than red, green, + * and blue */ + gdk_visual_get_red_pixel_details (visual, NULL, NULL, &red_prec); + gdk_visual_get_green_pixel_details (visual, NULL, NULL, &green_prec); + gdk_visual_get_blue_pixel_details (visual, NULL, NULL, &blue_prec); + depth = gdk_visual_get_depth (visual); + + visual_has_alpha = red_prec + blue_prec + green_prec < depth; + child->has_alpha = visual_has_alpha; + + return GTK_WIDGET (child); +} + +char * +na_tray_child_get_title (NaTrayChild *child) +{ + char *retval = NULL; + GdkDisplay *display; + Atom utf8_string, atom, type; + int result; + int format; + gulong nitems; + gulong bytes_after; + gchar *val; + + g_return_val_if_fail (NA_IS_TRAY_CHILD (child), NULL); + + display = gtk_widget_get_display (GTK_WIDGET (child)); + + utf8_string = gdk_x11_get_xatom_by_name_for_display (display, "UTF8_STRING"); + atom = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"); + + gdk_x11_display_error_trap_push (display); + + result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), + child->icon_window, + atom, + 0, G_MAXLONG, + False, utf8_string, + &type, &format, &nitems, + &bytes_after, (guchar **)&val); + + if (gdk_x11_display_error_trap_pop (display) || result != Success) + return NULL; + + if (type != utf8_string || + format != 8 || + nitems == 0) + { + if (val) + XFree (val); + return NULL; + } + + if (!g_utf8_validate (val, nitems, NULL)) + { + XFree (val); + return NULL; + } + + retval = g_strndup (val, nitems); + + XFree (val); + + return retval; +} + +/** + * na_tray_child_has_alpha; + * @child: a #NaTrayChild + * + * Checks if the child has an ARGB visual and real alpha transparence. + * (as opposed to faked alpha transparency with an parent-relative + * background) + * + * Return value: %TRUE if the child has an alpha transparency + */ +gboolean +na_tray_child_has_alpha (NaTrayChild *child) +{ + g_return_val_if_fail (NA_IS_TRAY_CHILD (child), FALSE); + + return child->has_alpha; +} + +/* If we are faking transparency with a window-relative background, force a + * redraw of the icon. This should be called if the background changes or if + * the child is shifted with respect to the background. + */ +void +na_tray_child_force_redraw (NaTrayChild *child) +{ + GtkWidget *widget = GTK_WIDGET (child); + + if (gtk_widget_get_mapped (widget) && child->parent_relative_bg) + { +#if 1 + /* Sending an ExposeEvent might cause redraw problems if the + * icon is expecting the server to clear-to-background before + * the redraw. It should be ok for GtkStatusIcon or EggTrayIcon. + */ + GdkDisplay *display = gtk_widget_get_display (widget); + Display *xdisplay = GDK_DISPLAY_XDISPLAY (display); + XEvent xev; + GdkWindow *plug_window; + GtkAllocation allocation; + + plug_window = gtk_socket_get_plug_window (GTK_SOCKET (child)); + gtk_widget_get_allocation (widget, &allocation); + + xev.xexpose.type = Expose; + xev.xexpose.window = GDK_WINDOW_XID (plug_window); + xev.xexpose.x = 0; + xev.xexpose.y = 0; + xev.xexpose.width = allocation.width; + xev.xexpose.height = allocation.height; + xev.xexpose.count = 0; + + gdk_x11_display_error_trap_push (display); + XSendEvent (xdisplay, + xev.xexpose.window, + False, ExposureMask, + &xev); + gdk_x11_display_error_trap_pop_ignored (display); +#else + /* Hiding and showing is the safe way to do it, but can result in more + * flickering. + */ + gdk_window_hide (widget->window); + gdk_window_show (widget->window); +#endif + } +} + +/* from libwnck/xutils.c, comes as LGPLv2+ */ +static char * +latin1_to_utf8 (const char *latin1) +{ + GString *str; + const char *p; + + str = g_string_new (NULL); + + p = latin1; + while (*p) + { + g_string_append_unichar (str, (gunichar) *p); + ++p; + } + + return g_string_free (str, FALSE); +} + +/* derived from libwnck/xutils.c, comes as LGPLv2+ */ +static void +_get_wmclass (Display *xdisplay, + Window xwindow, + char **res_class, + char **res_name) +{ + GdkDisplay *display; + XClassHint ch; + + ch.res_name = NULL; + ch.res_class = NULL; + + display = gdk_x11_lookup_xdisplay (xdisplay); + gdk_x11_display_error_trap_push (display); + XGetClassHint (xdisplay, xwindow, &ch); + gdk_x11_display_error_trap_pop_ignored (display); + + if (res_class) + *res_class = NULL; + + if (res_name) + *res_name = NULL; + + if (ch.res_name) + { + if (res_name) + *res_name = latin1_to_utf8 (ch.res_name); + + XFree (ch.res_name); + } + + if (ch.res_class) + { + if (res_class) + *res_class = latin1_to_utf8 (ch.res_class); + + XFree (ch.res_class); + } +} + +/** + * na_tray_child_get_wm_class; + * @child: a #NaTrayChild + * @res_name: return location for a string containing the application name of + * @child, or %NULL + * @res_class: return location for a string containing the application class of + * @child, or %NULL + * + * Fetches the resource associated with @child. + */ +void +na_tray_child_get_wm_class (NaTrayChild *child, + char **res_name, + char **res_class) +{ + GdkDisplay *display; + + g_return_if_fail (NA_IS_TRAY_CHILD (child)); + + display = gtk_widget_get_display (GTK_WIDGET (child)); + + _get_wmclass (GDK_DISPLAY_XDISPLAY (display), + child->icon_window, + res_class, + res_name); +} diff --git a/src/tray/na-tray-child.h b/src/tray/na-tray-child.h new file mode 100644 index 0000000..d143676 --- /dev/null +++ b/src/tray/na-tray-child.h @@ -0,0 +1,66 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* na-tray-child.h + * Copyright (C) 2002 Anders Carlsson <andersca@gnu.org> + * Copyright (C) 2003-2006 Vincent Untz + * Copyright (C) 2008 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef __NA_TRAY_CHILD_H__ +#define __NA_TRAY_CHILD_H__ + +#include <gtk/gtk.h> +#include <gtk/gtkx.h> + +G_BEGIN_DECLS + +#define NA_TYPE_TRAY_CHILD (na_tray_child_get_type ()) +#define NA_TRAY_CHILD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NA_TYPE_TRAY_CHILD, NaTrayChild)) +#define NA_TRAY_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NA_TYPE_TRAY_CHILD, NaTrayChildClass)) +#define NA_IS_TRAY_CHILD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NA_TYPE_TRAY_CHILD)) +#define NA_IS_TRAY_CHILD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NA_TYPE_TRAY_CHILD)) +#define NA_TRAY_CHILD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NA_TYPE_TRAY_CHILD, NaTrayChildClass)) + +typedef struct _NaTrayChild NaTrayChild; +typedef struct _NaTrayChildClass NaTrayChildClass; +typedef struct _NaTrayChildChild NaTrayChildChild; + +struct _NaTrayChild +{ + GtkSocket parent_instance; + Window icon_window; + guint has_alpha : 1; + guint parent_relative_bg : 1; +}; + +struct _NaTrayChildClass +{ + GtkSocketClass parent_class; +}; + +GType na_tray_child_get_type (void); + +GtkWidget *na_tray_child_new (GdkScreen *screen, + Window icon_window); +char *na_tray_child_get_title (NaTrayChild *child); +gboolean na_tray_child_has_alpha (NaTrayChild *child); +void na_tray_child_force_redraw (NaTrayChild *child); +void na_tray_child_get_wm_class (NaTrayChild *child, + char **res_name, + char **res_class); + +G_END_DECLS + +#endif /* __NA_TRAY_CHILD_H__ */ diff --git a/src/tray/na-tray-manager.c b/src/tray/na-tray-manager.c new file mode 100644 index 0000000..15e1d80 --- /dev/null +++ b/src/tray/na-tray-manager.c @@ -0,0 +1,888 @@ +/* na-tray-manager.c + * Copyright (C) 2002 Anders Carlsson <andersca@gnu.org> + * Copyright (C) 2003-2006 Vincent Untz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Used to be: eggtraymanager.c + */ + +#include <config.h> +#include <string.h> +#include <libintl.h> + +#include "na-tray-manager.h" + +#if defined (GDK_WINDOWING_X11) +#include <gdk/gdkx.h> +#include <X11/Xatom.h> +#elif defined (GDK_WINDOWING_WIN32) +#include <gdk/gdkwin32.h> +#endif +#include <gtk/gtk.h> + +/* Signals */ +enum +{ + TRAY_ICON_ADDED, + TRAY_ICON_REMOVED, + MESSAGE_SENT, + MESSAGE_CANCELLED, + LOST_SELECTION, + LAST_SIGNAL +}; + +enum { + PROP_0, + PROP_ORIENTATION +}; + +typedef struct +{ + long id, len; + long remaining_len; + + long timeout; + char *str; +#ifdef GDK_WINDOWING_X11 + Window window; +#endif +} PendingMessage; + +static guint manager_signals[LAST_SIGNAL]; + +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 + +#define SYSTEM_TRAY_ORIENTATION_HORZ 0 +#define SYSTEM_TRAY_ORIENTATION_VERT 1 + +#ifdef GDK_WINDOWING_X11 +static gboolean na_tray_manager_check_running_screen_x11 (void); +#endif + +static void na_tray_manager_finalize (GObject *object); +static void na_tray_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void na_tray_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +static void na_tray_manager_unmanage (NaTrayManager *manager); + +G_DEFINE_TYPE (NaTrayManager, na_tray_manager, G_TYPE_OBJECT) + +static void +na_tray_manager_init (NaTrayManager *manager) +{ + manager->invisible = NULL; + manager->socket_table = g_hash_table_new (NULL, NULL); + + manager->fg.red = 0; + manager->fg.green = 0; + manager->fg.blue = 0; + + manager->error.red = 0xff; + manager->error.green = 0; + manager->error.blue = 0; + + manager->warning.red = 0xff; + manager->warning.green = 0xff; + manager->warning.blue = 0; + + manager->success.red = 0; + manager->success.green = 0xff; + manager->success.blue = 0; +} + +static void +na_tray_manager_class_init (NaTrayManagerClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = (GObjectClass *)klass; + + gobject_class->finalize = na_tray_manager_finalize; + gobject_class->set_property = na_tray_manager_set_property; + gobject_class->get_property = na_tray_manager_get_property; + + g_object_class_install_property (gobject_class, + PROP_ORIENTATION, + g_param_spec_enum ("orientation", + "orientation", + "orientation", + GTK_TYPE_ORIENTATION, + GTK_ORIENTATION_HORIZONTAL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT)); + + manager_signals[TRAY_ICON_ADDED] = + g_signal_new ("tray_icon_added", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NaTrayManagerClass, tray_icon_added), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + GTK_TYPE_SOCKET); + + manager_signals[TRAY_ICON_REMOVED] = + g_signal_new ("tray_icon_removed", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NaTrayManagerClass, tray_icon_removed), + NULL, NULL, NULL, + G_TYPE_NONE, 1, + GTK_TYPE_SOCKET); + manager_signals[MESSAGE_SENT] = + g_signal_new ("message_sent", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NaTrayManagerClass, message_sent), + NULL, NULL, NULL, + G_TYPE_NONE, 4, + GTK_TYPE_SOCKET, + G_TYPE_STRING, + G_TYPE_LONG, + G_TYPE_LONG); + manager_signals[MESSAGE_CANCELLED] = + g_signal_new ("message_cancelled", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NaTrayManagerClass, message_cancelled), + NULL, NULL, NULL, + G_TYPE_NONE, 2, + GTK_TYPE_SOCKET, + G_TYPE_LONG); + manager_signals[LOST_SELECTION] = + g_signal_new ("lost_selection", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (NaTrayManagerClass, lost_selection), + NULL, NULL, NULL, + G_TYPE_NONE, 0); + +#if defined (GDK_WINDOWING_X11) + /* Nothing */ +#elif defined (GDK_WINDOWING_WIN32) + g_warning ("Port NaTrayManager to Win32"); +#else + g_warning ("Port NaTrayManager to this GTK+ backend"); +#endif +} + +static void +na_tray_manager_finalize (GObject *object) +{ + NaTrayManager *manager; + + manager = NA_TRAY_MANAGER (object); + + na_tray_manager_unmanage (manager); + + g_list_free (manager->messages); + g_hash_table_destroy (manager->socket_table); + + G_OBJECT_CLASS (na_tray_manager_parent_class)->finalize (object); +} + +static void +na_tray_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + NaTrayManager *manager = NA_TRAY_MANAGER (object); + + switch (prop_id) + { + case PROP_ORIENTATION: + na_tray_manager_set_orientation (manager, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +na_tray_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + NaTrayManager *manager = NA_TRAY_MANAGER (object); + + switch (prop_id) + { + case PROP_ORIENTATION: + g_value_set_enum (value, manager->orientation); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +NaTrayManager * +na_tray_manager_new (void) +{ + NaTrayManager *manager; + + manager = g_object_new (NA_TYPE_TRAY_MANAGER, NULL); + + return manager; +} + +#ifdef GDK_WINDOWING_X11 + +static gboolean +na_tray_manager_plug_removed (GtkSocket *socket, + NaTrayManager *manager) +{ + NaTrayChild *child = NA_TRAY_CHILD (socket); + + g_hash_table_remove (manager->socket_table, + GINT_TO_POINTER (child->icon_window)); + g_signal_emit (manager, manager_signals[TRAY_ICON_REMOVED], 0, child); + + /* This destroys the socket. */ + return FALSE; +} + +static void +na_tray_manager_handle_dock_request (NaTrayManager *manager, + XClientMessageEvent *xevent) +{ + Window icon_window = xevent->data.l[2]; + GtkWidget *child; + + if (g_hash_table_lookup (manager->socket_table, + GINT_TO_POINTER (icon_window))) + { + /* We already got this notification earlier, ignore this one */ + return; + } + + child = na_tray_child_new (manager->screen, icon_window); + if (child == NULL) /* already gone or other error */ + return; + + g_signal_emit (manager, manager_signals[TRAY_ICON_ADDED], 0, + child); + + /* If the child wasn't attached, then destroy it */ + + if (!GTK_IS_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (child)))) + { + gtk_widget_destroy (child); + return; + } + + g_signal_connect (child, "plug_removed", + G_CALLBACK (na_tray_manager_plug_removed), manager); + + gtk_socket_add_id (GTK_SOCKET (child), icon_window); + + if (!gtk_socket_get_plug_window (GTK_SOCKET (child))) + { + /* Embedding failed, we won't get a plug-removed signal */ + /* This signal destroys the socket */ + g_signal_emit (manager, manager_signals[TRAY_ICON_REMOVED], 0, child); + return; + } + + g_hash_table_insert (manager->socket_table, + GINT_TO_POINTER (icon_window), child); + gtk_widget_show (child); +} + +static void +pending_message_free (PendingMessage *message) +{ + g_free (message->str); + g_free (message); +} + +static void +na_tray_manager_handle_message_data (NaTrayManager *manager, + XClientMessageEvent *xevent) +{ + GList *p; + int len; + + /* Try to see if we can find the pending message in the list */ + for (p = manager->messages; p; p = p->next) + { + PendingMessage *msg = p->data; + + if (xevent->window == msg->window) + { + /* Append the message */ + len = MIN (msg->remaining_len, 20); + + memcpy ((msg->str + msg->len - msg->remaining_len), + &xevent->data, len); + msg->remaining_len -= len; + + if (msg->remaining_len == 0) + { + GtkSocket *socket; + + socket = g_hash_table_lookup (manager->socket_table, + GINT_TO_POINTER (msg->window)); + + if (socket) + g_signal_emit (manager, manager_signals[MESSAGE_SENT], 0, + socket, msg->str, msg->id, msg->timeout); + + pending_message_free (msg); + manager->messages = g_list_remove_link (manager->messages, p); + g_list_free_1 (p); + } + + break; + } + } +} + +static void +na_tray_manager_handle_begin_message (NaTrayManager *manager, + XClientMessageEvent *xevent) +{ + GtkSocket *socket; + GList *p; + PendingMessage *msg; + long timeout; + long len; + long id; + + socket = g_hash_table_lookup (manager->socket_table, + GINT_TO_POINTER (xevent->window)); + /* we don't know about this tray icon, so ignore the message */ + if (!socket) + return; + + timeout = xevent->data.l[2]; + len = xevent->data.l[3]; + id = xevent->data.l[4]; + + /* Check if the same message is already in the queue and remove it if so */ + for (p = manager->messages; p; p = p->next) + { + PendingMessage *pmsg = p->data; + + if (xevent->window == pmsg->window && + id == pmsg->id) + { + /* Hmm, we found it, now remove it */ + pending_message_free (pmsg); + manager->messages = g_list_remove_link (manager->messages, p); + g_list_free_1 (p); + break; + } + } + + if (len == 0) + { + g_signal_emit (manager, manager_signals[MESSAGE_SENT], 0, + socket, "", id, timeout); + } + else + { + /* Now add the new message to the queue */ + msg = g_new0 (PendingMessage, 1); + msg->window = xevent->window; + msg->timeout = timeout; + msg->len = len; + msg->id = id; + msg->remaining_len = msg->len; + msg->str = g_malloc (msg->len + 1); + msg->str[msg->len] = '\0'; + manager->messages = g_list_prepend (manager->messages, msg); + } +} + +static void +na_tray_manager_handle_cancel_message (NaTrayManager *manager, + XClientMessageEvent *xevent) +{ + GList *p; + GtkSocket *socket; + long id; + + id = xevent->data.l[2]; + + /* Check if the message is in the queue and remove it if so */ + for (p = manager->messages; p; p = p->next) + { + PendingMessage *msg = p->data; + + if (xevent->window == msg->window && + id == msg->id) + { + pending_message_free (msg); + manager->messages = g_list_remove_link (manager->messages, p); + g_list_free_1 (p); + break; + } + } + + socket = g_hash_table_lookup (manager->socket_table, + GINT_TO_POINTER (xevent->window)); + + if (socket) + { + g_signal_emit (manager, manager_signals[MESSAGE_CANCELLED], 0, + socket, xevent->data.l[2]); + } +} + +static GdkFilterReturn +na_tray_manager_window_filter (GdkXEvent *xev, + GdkEvent *event, + gpointer data) +{ + XEvent *xevent = (GdkXEvent *)xev; + NaTrayManager *manager = data; + + if (xevent->type == ClientMessage) + { + /* _NET_SYSTEM_TRAY_OPCODE: SYSTEM_TRAY_REQUEST_DOCK */ + if (xevent->xclient.message_type == manager->opcode_atom && + xevent->xclient.data.l[1] == SYSTEM_TRAY_REQUEST_DOCK) + { + na_tray_manager_handle_dock_request (manager, + (XClientMessageEvent *) xevent); + return GDK_FILTER_REMOVE; + } + /* _NET_SYSTEM_TRAY_OPCODE: SYSTEM_TRAY_BEGIN_MESSAGE */ + else if (xevent->xclient.message_type == manager->opcode_atom && + xevent->xclient.data.l[1] == SYSTEM_TRAY_BEGIN_MESSAGE) + { + na_tray_manager_handle_begin_message (manager, + (XClientMessageEvent *) event); + return GDK_FILTER_REMOVE; + } + /* _NET_SYSTEM_TRAY_OPCODE: SYSTEM_TRAY_CANCEL_MESSAGE */ + else if (xevent->xclient.message_type == manager->opcode_atom && + xevent->xclient.data.l[1] == SYSTEM_TRAY_CANCEL_MESSAGE) + { + na_tray_manager_handle_cancel_message (manager, + (XClientMessageEvent *) event); + return GDK_FILTER_REMOVE; + } + /* _NET_SYSTEM_TRAY_MESSAGE_DATA */ + else if (xevent->xclient.message_type == manager->message_data_atom) + { + na_tray_manager_handle_message_data (manager, + (XClientMessageEvent *) event); + return GDK_FILTER_REMOVE; + } + } + else if (xevent->type == SelectionClear) + { + g_signal_emit (manager, manager_signals[LOST_SELECTION], 0); + na_tray_manager_unmanage (manager); + } + + return GDK_FILTER_CONTINUE; +} + +#if 0 +//FIXME investigate why this doesn't work +static gboolean +na_tray_manager_selection_clear_event (GtkWidget *widget, + GdkEventSelection *event, + NaTrayManager *manager) +{ + g_signal_emit (manager, manager_signals[LOST_SELECTION], 0); + na_tray_manager_unmanage (manager); + + return FALSE; +} +#endif +#endif + +static void +na_tray_manager_unmanage (NaTrayManager *manager) +{ +#ifdef GDK_WINDOWING_X11 + GdkDisplay *display; + guint32 timestamp; + GtkWidget *invisible; + GdkWindow *window; + + if (manager->invisible == NULL) + return; + + invisible = manager->invisible; + window = gtk_widget_get_window (invisible); + + g_assert (GTK_IS_INVISIBLE (invisible)); + g_assert (gtk_widget_get_realized (invisible)); + g_assert (GDK_IS_WINDOW (window)); + + display = gtk_widget_get_display (invisible); + + if (gdk_selection_owner_get_for_display (display, manager->selection_atom) == + window) + { + timestamp = gdk_x11_get_server_time (window); + gdk_selection_owner_set_for_display (display, + NULL, + manager->selection_atom, + timestamp, + TRUE); + } + + gdk_window_remove_filter (window, + na_tray_manager_window_filter, manager); + + manager->invisible = NULL; /* prior to destroy for reentrancy paranoia */ + gtk_widget_destroy (invisible); + g_object_unref (G_OBJECT (invisible)); +#endif +} + +static void +na_tray_manager_set_orientation_property (NaTrayManager *manager) +{ +#ifdef GDK_WINDOWING_X11 + GdkWindow *window; + GdkDisplay *display; + Atom orientation_atom; + gulong data[1]; + + g_return_if_fail (manager->invisible != NULL); + window = gtk_widget_get_window (manager->invisible); + g_return_if_fail (window != NULL); + + display = gtk_widget_get_display (manager->invisible); + orientation_atom = gdk_x11_get_xatom_by_name_for_display (display, + "_NET_SYSTEM_TRAY_ORIENTATION"); + + data[0] = manager->orientation == GTK_ORIENTATION_HORIZONTAL ? + SYSTEM_TRAY_ORIENTATION_HORZ : + SYSTEM_TRAY_ORIENTATION_VERT; + + XChangeProperty (GDK_DISPLAY_XDISPLAY (display), + GDK_WINDOW_XID (window), + orientation_atom, + XA_CARDINAL, 32, + PropModeReplace, + (guchar *) &data, 1); +#endif +} + +static void +na_tray_manager_set_visual_property (NaTrayManager *manager) +{ +#ifdef GDK_WINDOWING_X11 + GdkWindow *window; + GdkDisplay *display; + Visual *xvisual; + Atom visual_atom; + gulong data[1]; + + g_return_if_fail (manager->invisible != NULL); + window = gtk_widget_get_window (manager->invisible); + g_return_if_fail (window != NULL); + + /* The visual property is a hint to the tray icons as to what visual they + * should use for their windows. If the X server has RGBA colormaps, then + * we tell the tray icons to use a RGBA colormap and we'll composite the + * icon onto its parents with real transparency. Otherwise, we just tell + * the icon to use our colormap, and we'll do some hacks with parent + * relative backgrounds to simulate transparency. + */ + + display = gtk_widget_get_display (manager->invisible); + visual_atom = gdk_x11_get_xatom_by_name_for_display (display, + "_NET_SYSTEM_TRAY_VISUAL"); + + if (gdk_screen_get_rgba_visual (manager->screen) != NULL) + xvisual = GDK_VISUAL_XVISUAL (gdk_screen_get_rgba_visual (manager->screen)); + else + { + /* We actually want the visual of the tray where the icons will + * be embedded. In almost all cases, this will be the same as the visual + * of the screen. + */ + xvisual = GDK_VISUAL_XVISUAL (gdk_screen_get_system_visual (manager->screen)); + } + + data[0] = XVisualIDFromVisual (xvisual); + + XChangeProperty (GDK_DISPLAY_XDISPLAY (display), + GDK_WINDOW_XID (window), + visual_atom, + XA_VISUALID, 32, + PropModeReplace, + (guchar *) &data, 1); +#endif +} + +static void +na_tray_manager_set_colors_property (NaTrayManager *manager) +{ +#ifdef GDK_WINDOWING_X11 + GdkWindow *window; + GdkDisplay *display; + Atom atom; + gulong data[12]; + + g_return_if_fail (manager->invisible != NULL); + window = gtk_widget_get_window (manager->invisible); + g_return_if_fail (window != NULL); + + display = gtk_widget_get_display (manager->invisible); + atom = gdk_x11_get_xatom_by_name_for_display (display, + "_NET_SYSTEM_TRAY_COLORS"); + + data[0] = manager->fg.red * 0x101; + data[1] = manager->fg.green * 0x101; + data[2] = manager->fg.blue * 0x101; + data[3] = manager->error.red * 0x101; + data[4] = manager->error.green * 0x101; + data[5] = manager->error.blue * 0x101; + data[6] = manager->warning.red * 0x101; + data[7] = manager->warning.green * 0x101; + data[8] = manager->warning.blue * 0x101; + data[9] = manager->success.red * 0x101; + data[10] = manager->success.green * 0x101; + data[11] = manager->success.blue * 0x101; + + XChangeProperty (GDK_DISPLAY_XDISPLAY (display), + GDK_WINDOW_XID (window), + atom, + XA_CARDINAL, 32, + PropModeReplace, + (guchar *) &data, 12); +#endif +} + +#ifdef GDK_WINDOWING_X11 + +static gboolean +na_tray_manager_manage_screen_x11 (NaTrayManager *manager) +{ + GdkDisplay *display; + GdkScreen *screen; + Screen *xscreen; + GtkWidget *invisible; + GdkWindow *window; + char *selection_atom_name; + guint32 timestamp; + + g_return_val_if_fail (NA_IS_TRAY_MANAGER (manager), FALSE); + g_return_val_if_fail (manager->screen == NULL, FALSE); + + /* If there's already a manager running on the screen + * we can't create another one. + */ +#if 0 + if (na_tray_manager_check_running_screen_x11 ()) + return FALSE; +#endif + + screen = gdk_screen_get_default (); + manager->screen = screen; + + display = gdk_screen_get_display (screen); + xscreen = GDK_SCREEN_XSCREEN (screen); + + invisible = gtk_invisible_new_for_screen (screen); + gtk_widget_realize (invisible); + + gtk_widget_add_events (invisible, + GDK_PROPERTY_CHANGE_MASK | GDK_STRUCTURE_MASK); + + selection_atom_name = g_strdup_printf ("_NET_SYSTEM_TRAY_S%d", + gdk_x11_get_default_screen ()); + manager->selection_atom = gdk_atom_intern (selection_atom_name, FALSE); + g_free (selection_atom_name); + + manager->invisible = invisible; + g_object_ref (G_OBJECT (manager->invisible)); + + na_tray_manager_set_orientation_property (manager); + na_tray_manager_set_visual_property (manager); + na_tray_manager_set_colors_property (manager); + + window = gtk_widget_get_window (invisible); + + timestamp = gdk_x11_get_server_time (window); + + /* Check if we could set the selection owner successfully */ + if (gdk_selection_owner_set_for_display (display, + window, + manager->selection_atom, + timestamp, + TRUE)) + { + XClientMessageEvent xev; + GdkAtom opcode_atom; + GdkAtom message_data_atom; + + xev.type = ClientMessage; + xev.window = RootWindowOfScreen (xscreen); + xev.message_type = gdk_x11_get_xatom_by_name_for_display (display, + "MANAGER"); + + xev.format = 32; + xev.data.l[0] = timestamp; + xev.data.l[1] = gdk_x11_atom_to_xatom_for_display (display, + manager->selection_atom); + xev.data.l[2] = GDK_WINDOW_XID (window); + xev.data.l[3] = 0; /* manager specific data */ + xev.data.l[4] = 0; /* manager specific data */ + + XSendEvent (GDK_DISPLAY_XDISPLAY (display), + RootWindowOfScreen (xscreen), + False, StructureNotifyMask, (XEvent *)&xev); + + opcode_atom = gdk_atom_intern ("_NET_SYSTEM_TRAY_OPCODE", FALSE); + manager->opcode_atom = gdk_x11_atom_to_xatom_for_display (display, + opcode_atom); + + message_data_atom = gdk_atom_intern ("_NET_SYSTEM_TRAY_MESSAGE_DATA", + FALSE); + manager->message_data_atom = gdk_x11_atom_to_xatom_for_display (display, + message_data_atom); + + /* Add a window filter */ +#if 0 + /* This is for when we lose the selection of _NET_SYSTEM_TRAY_Sx */ + g_signal_connect (invisible, "selection-clear-event", + G_CALLBACK (na_tray_manager_selection_clear_event), + manager); +#endif + gdk_window_add_filter (window, + na_tray_manager_window_filter, manager); + return TRUE; + } + else + { + gtk_widget_destroy (invisible); + g_object_unref (invisible); + manager->invisible = NULL; + + manager->screen = NULL; + + return FALSE; + } +} + +#endif + +gboolean +na_tray_manager_manage_screen (NaTrayManager *manager) +{ + g_return_val_if_fail (manager->screen == NULL, FALSE); + +#ifdef GDK_WINDOWING_X11 + return na_tray_manager_manage_screen_x11 (manager); +#else + return FALSE; +#endif +} + +#ifdef GDK_WINDOWING_X11 + +static gboolean +na_tray_manager_check_running_screen_x11 (void) +{ + GdkDisplay *display; + GdkScreen *screen; + Atom selection_atom; + char *selection_atom_name; + + screen = gdk_screen_get_default (); + display = gdk_screen_get_display (screen); + selection_atom_name = g_strdup_printf ("_NET_SYSTEM_TRAY_S%d", + gdk_x11_get_default_screen ()); + selection_atom = gdk_x11_get_xatom_by_name_for_display (display, + selection_atom_name); + g_free (selection_atom_name); + + if (XGetSelectionOwner (GDK_DISPLAY_XDISPLAY (display), + selection_atom) != None) + return TRUE; + else + return FALSE; +} + +#endif + +gboolean +na_tray_manager_check_running (void) +{ +#ifdef GDK_WINDOWING_X11 + return na_tray_manager_check_running_screen_x11 (); +#else + return FALSE; +#endif +} + +void +na_tray_manager_set_orientation (NaTrayManager *manager, + GtkOrientation orientation) +{ + g_return_if_fail (NA_IS_TRAY_MANAGER (manager)); + + if (manager->orientation != orientation) + { + manager->orientation = orientation; + + na_tray_manager_set_orientation_property (manager); + + g_object_notify (G_OBJECT (manager), "orientation"); + } +} + +void +na_tray_manager_set_colors (NaTrayManager *manager, + ClutterColor *fg, + ClutterColor *error, + ClutterColor *warning, + ClutterColor *success) +{ + g_return_if_fail (NA_IS_TRAY_MANAGER (manager)); + + if (!clutter_color_equal (&manager->fg, fg) || + !clutter_color_equal (&manager->error, error) || + !clutter_color_equal (&manager->warning, warning) || + !clutter_color_equal (&manager->success, success)) + { + manager->fg = *fg; + manager->error = *error; + manager->warning = *warning; + manager->success = *success; + + na_tray_manager_set_colors_property (manager); + } +} + +GtkOrientation +na_tray_manager_get_orientation (NaTrayManager *manager) +{ + g_return_val_if_fail (NA_IS_TRAY_MANAGER (manager), GTK_ORIENTATION_HORIZONTAL); + + return manager->orientation; +} diff --git a/src/tray/na-tray-manager.h b/src/tray/na-tray-manager.h new file mode 100644 index 0000000..727b02d --- /dev/null +++ b/src/tray/na-tray-manager.h @@ -0,0 +1,106 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* na-tray-manager.h + * Copyright (C) 2002 Anders Carlsson <andersca@gnu.org> + * Copyright (C) 2003-2006 Vincent Untz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Used to be: eggtraymanager.h + */ + +#ifndef __NA_TRAY_MANAGER_H__ +#define __NA_TRAY_MANAGER_H__ + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif +#include <gtk/gtk.h> +#include <clutter/clutter.h> + +#include "na-tray-child.h" + +G_BEGIN_DECLS + +#define NA_TYPE_TRAY_MANAGER (na_tray_manager_get_type ()) +#define NA_TRAY_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NA_TYPE_TRAY_MANAGER, NaTrayManager)) +#define NA_TRAY_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), NA_TYPE_TRAY_MANAGER, NaTrayManagerClass)) +#define NA_IS_TRAY_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), NA_TYPE_TRAY_MANAGER)) +#define NA_IS_TRAY_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), NA_TYPE_TRAY_MANAGER)) +#define NA_TRAY_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), NA_TYPE_TRAY_MANAGER, NaTrayManagerClass)) + +typedef struct _NaTrayManager NaTrayManager; +typedef struct _NaTrayManagerClass NaTrayManagerClass; + +struct _NaTrayManager +{ + GObject parent_instance; + +#ifdef GDK_WINDOWING_X11 + GdkAtom selection_atom; + Atom opcode_atom; + Atom message_data_atom; +#endif + + GtkWidget *invisible; + GdkScreen *screen; + GtkOrientation orientation; + ClutterColor fg; + ClutterColor error; + ClutterColor warning; + ClutterColor success; + + GList *messages; + GHashTable *socket_table; +}; + +struct _NaTrayManagerClass +{ + GObjectClass parent_class; + + void (* tray_icon_added) (NaTrayManager *manager, + NaTrayChild *child); + void (* tray_icon_removed) (NaTrayManager *manager, + NaTrayChild *child); + + void (* message_sent) (NaTrayManager *manager, + NaTrayChild *child, + const gchar *message, + glong id, + glong timeout); + + void (* message_cancelled) (NaTrayManager *manager, + NaTrayChild *child, + glong id); + + void (* lost_selection) (NaTrayManager *manager); +}; + +GType na_tray_manager_get_type (void); + +gboolean na_tray_manager_check_running (void); +NaTrayManager *na_tray_manager_new (void); +gboolean na_tray_manager_manage_screen (NaTrayManager *manager); +void na_tray_manager_set_orientation (NaTrayManager *manager, + GtkOrientation orientation); +GtkOrientation na_tray_manager_get_orientation (NaTrayManager *manager); +void na_tray_manager_set_colors (NaTrayManager *manager, + ClutterColor *fg, + ClutterColor *error, + ClutterColor *warning, + ClutterColor *success); + + +G_END_DECLS + +#endif /* __NA_TRAY_MANAGER_H__ */ |