/* * Copyright (C) 2016 Alberts Muktupāvels * * 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 . */ #include "config.h" #include #include #include "meta-enum-types.h" #include "meta-frame-layout-private.h" #include "meta-theme.h" #include "meta-theme-gtk-private.h" #include "meta-theme-impl-private.h" #include "meta-theme-metacity-private.h" #include "meta-style-info-private.h" struct _MetaTheme { GObject parent; MetaThemeType type; MetaThemeImpl *impl; gulong gtk_theme_name_id; gulong gtk_xft_antialias_id; gulong gtk_xft_hinting_id; gulong gtk_xft_hintstyle_id; gulong gtk_xft_rgba_id; MetaButtonLayout *button_layout; gboolean composited; gint scale; gdouble dpi; PangoFontDescription *titlebar_font; gchar *gtk_theme_name; GHashTable *variants; PangoContext *context; GHashTable *font_descs; GHashTable *title_heights; }; enum { PROP_0, PROP_TYPE, LAST_PROP }; static GParamSpec *properties[LAST_PROP] = { NULL }; G_DEFINE_TYPE (MetaTheme, meta_theme, G_TYPE_OBJECT) static void update_composited_func (gpointer key, gpointer value, gpointer user_data) { MetaTheme *theme; MetaStyleInfo *style_info; theme = META_THEME (user_data); style_info = META_STYLE_INFO (value); meta_style_info_set_composited (style_info, theme->composited); } static void update_scale_func (gpointer key, gpointer value, gpointer user_data) { MetaTheme *theme; MetaStyleInfo *style_info; theme = META_THEME (user_data); style_info = META_STYLE_INFO (value); meta_style_info_set_scale (style_info, theme->scale); } static MetaStyleInfo * get_style_info (MetaTheme *theme, const gchar *variant) { const gchar *key; MetaStyleInfo *style_info; key = variant; if (variant == NULL) key = "default"; style_info = g_hash_table_lookup (theme->variants, key); if (style_info == NULL) { style_info = meta_style_info_new (theme->gtk_theme_name, variant, theme->composited, theme->scale); g_hash_table_insert (theme->variants, g_strdup (key), style_info); } return style_info; } static void font_desc_apply_scale (PangoFontDescription *font_desc, MetaTheme *theme, MetaFrameType type, MetaFrameFlags flags) { gint old_size; MetaFrameStyle *style; gint new_size; old_size = pango_font_description_get_size (font_desc); style = META_THEME_IMPL_GET_CLASS (theme->impl)->get_frame_style (theme->impl, type, flags); new_size = MAX (old_size * (style->layout->title_scale / theme->scale), 1); pango_font_description_set_size (font_desc, new_size); } static PangoFontDescription * get_title_font_desc (MetaTheme *theme, const gchar *variant, MetaFrameType type, MetaFrameFlags flags) { gchar *key; PangoFontDescription *font_desc; MetaStyleInfo *style_info; GtkStyleContext *context; key = g_strdup_printf ("%s_%d_%x", variant ? variant : "default", type, flags); font_desc = g_hash_table_lookup (theme->font_descs, key); if (font_desc != NULL) { g_free (key); return font_desc; } style_info = get_style_info (theme, variant); context = meta_style_info_get_style (style_info, META_STYLE_ELEMENT_TITLE); gtk_style_context_save (context); gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL); gtk_style_context_get (context, GTK_STATE_FLAG_NORMAL, "font", &font_desc, NULL); gtk_style_context_restore (context); if (theme->titlebar_font) pango_font_description_merge (font_desc, theme->titlebar_font, TRUE); font_desc_apply_scale (font_desc, theme, type, flags); g_hash_table_insert (theme->font_descs, key, font_desc); return font_desc; } static void ensure_pango_context (MetaTheme *theme) { GdkScreen *screen; PangoFontMap *fontmap; PangoContext *context; const cairo_font_options_t *options; gdouble dpi; if (theme->context != NULL) return; screen = gdk_screen_get_default (); fontmap = pango_cairo_font_map_get_default (); context = pango_font_map_create_context (fontmap); options = gdk_screen_get_font_options (screen); pango_cairo_context_set_font_options (context, options); dpi = theme->dpi * theme->scale; pango_cairo_context_set_resolution (context, dpi); theme->context = context; } static gint get_title_height (MetaTheme *theme, const gchar *variant, MetaFrameType type, MetaFrameFlags flags) { PangoFontDescription *description; gpointer size; gpointer height; gint title_height; description = get_title_font_desc (theme, variant, type, flags); g_assert (description != NULL); size = GINT_TO_POINTER (pango_font_description_get_size (description)); height = g_hash_table_lookup (theme->title_heights, size); if (height != NULL) { title_height = GPOINTER_TO_INT (height); } else { PangoLanguage *lang; PangoFontMetrics *metrics; gint ascent; gint descent; ensure_pango_context (theme); lang = pango_context_get_language (theme->context); metrics = pango_context_get_metrics (theme->context, description, lang); ascent = pango_font_metrics_get_ascent (metrics); descent = pango_font_metrics_get_descent (metrics); pango_font_metrics_unref (metrics); title_height = PANGO_PIXELS (ascent + descent); title_height *= theme->scale; height = GINT_TO_POINTER (title_height); g_hash_table_insert (theme->title_heights, size, height); } return title_height; } static PangoLayout * create_title_layout (MetaTheme *theme, const gchar *variant, MetaFrameType type, MetaFrameFlags flags, const gchar *title) { PangoLayout *layout; PangoFontDescription *font_desc; ensure_pango_context (theme); layout = pango_layout_new (theme->context); if (title) pango_layout_set_text (layout, title, -1); pango_layout_set_auto_dir (layout, FALSE); pango_layout_set_ellipsize (layout, PANGO_ELLIPSIZE_END); pango_layout_set_single_paragraph_mode (layout, TRUE); font_desc = get_title_font_desc (theme, variant, type, flags); pango_layout_set_font_description (layout, font_desc); return layout; } static void update_font_options (MetaTheme *theme) { g_clear_object (&theme->context); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } static void notify_gtk_theme_name_cb (GtkSettings *settings, GParamSpec *pspec, MetaTheme *theme) { g_free (theme->gtk_theme_name); g_object_get (settings, "gtk-theme-name", &theme->gtk_theme_name, NULL); meta_theme_invalidate (theme); } static void notify_gtk_xft_antialias_cb (GtkSettings *settings, GParamSpec *pspec, MetaTheme *theme) { update_font_options (theme); } static void notify_gtk_xft_hinting_cb (GtkSettings *settings, GParamSpec *pspec, MetaTheme *theme) { update_font_options (theme); } static void notify_gtk_xft_hintstyle_cb (GtkSettings *settings, GParamSpec *pspec, MetaTheme *theme) { update_font_options (theme); } static void notify_gtk_xft_rgba_cb (GtkSettings *settings, GParamSpec *pspec, MetaTheme *theme) { update_font_options (theme); } static void meta_theme_constructed (GObject *object) { MetaTheme *theme; GtkSettings *settings; const gchar *button_layout; G_OBJECT_CLASS (meta_theme_parent_class)->constructed (object); theme = META_THEME (object); settings = gtk_settings_get_default (); if (theme->type == META_THEME_TYPE_GTK) theme->impl = g_object_new (META_TYPE_THEME_GTK, NULL); else if (theme->type == META_THEME_TYPE_METACITY) theme->impl = g_object_new (META_TYPE_THEME_METACITY, NULL); else g_assert_not_reached (); meta_theme_impl_set_composited (theme->impl, theme->composited); meta_theme_impl_set_scale (theme->impl, theme->scale); button_layout = "appmenu:minimize,maximize,close"; meta_theme_set_button_layout (theme, button_layout, FALSE); if (theme->type == META_THEME_TYPE_METACITY) { theme->gtk_theme_name_id = g_signal_connect (settings, "notify::gtk-theme-name", G_CALLBACK (notify_gtk_theme_name_cb), theme); } theme->gtk_xft_antialias_id = g_signal_connect (settings, "notify::gtk-xft-antialias", G_CALLBACK (notify_gtk_xft_antialias_cb), theme); theme->gtk_xft_hinting_id = g_signal_connect (settings, "notify::gtk-xft-hinting", G_CALLBACK (notify_gtk_xft_hinting_cb), theme); theme->gtk_xft_hintstyle_id = g_signal_connect (settings, "notify::gtk-xft-hintstyle", G_CALLBACK (notify_gtk_xft_hintstyle_cb), theme); theme->gtk_xft_rgba_id = g_signal_connect (settings, "notify::gtk-xft-rgba", G_CALLBACK (notify_gtk_xft_rgba_cb), theme); } static void meta_theme_dispose (GObject *object) { MetaTheme *theme; GtkSettings *settings; theme = META_THEME (object); settings = gtk_settings_get_default (); g_clear_object (&theme->impl); if (theme->gtk_theme_name_id > 0) { g_signal_handler_disconnect (settings, theme->gtk_theme_name_id); theme->gtk_theme_name_id = 0; } if (theme->gtk_xft_antialias_id > 0) { g_signal_handler_disconnect (settings, theme->gtk_xft_antialias_id); theme->gtk_xft_antialias_id = 0; } if (theme->gtk_xft_hinting_id > 0) { g_signal_handler_disconnect (settings, theme->gtk_xft_hinting_id); theme->gtk_xft_hinting_id = 0; } if (theme->gtk_xft_hintstyle_id > 0) { g_signal_handler_disconnect (settings, theme->gtk_xft_hintstyle_id); theme->gtk_xft_hintstyle_id = 0; } if (theme->gtk_xft_rgba_id > 0) { g_signal_handler_disconnect (settings, theme->gtk_xft_rgba_id); theme->gtk_xft_rgba_id = 0; } g_clear_pointer (&theme->variants, g_hash_table_destroy); g_clear_object (&theme->context); g_clear_pointer (&theme->font_descs, g_hash_table_destroy); g_clear_pointer (&theme->title_heights, g_hash_table_destroy); G_OBJECT_CLASS (meta_theme_parent_class)->dispose (object); } static void meta_theme_finalize (GObject *object) { MetaTheme *theme; theme = META_THEME (object); if (theme->button_layout != NULL) { meta_button_layout_free (theme->button_layout); theme->button_layout = NULL; } if (theme->titlebar_font) { pango_font_description_free (theme->titlebar_font); theme->titlebar_font = NULL; } g_free (theme->gtk_theme_name); G_OBJECT_CLASS (meta_theme_parent_class)->finalize (object); } static void meta_theme_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { MetaTheme *theme; theme = META_THEME (object); switch (property_id) { case PROP_TYPE: theme->type = g_value_get_enum (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void meta_theme_install_properties (GObjectClass *object_class) { properties[PROP_TYPE] = g_param_spec_enum ("type", "type", "type", META_TYPE_THEME_TYPE, META_THEME_TYPE_GTK, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, properties); } static void meta_theme_class_init (MetaThemeClass *theme_class) { GObjectClass *object_class; object_class = G_OBJECT_CLASS (theme_class); object_class->constructed = meta_theme_constructed; object_class->dispose = meta_theme_dispose; object_class->finalize = meta_theme_finalize; object_class->set_property = meta_theme_set_property; meta_theme_install_properties (object_class); } static void meta_theme_init (MetaTheme *theme) { theme->composited = TRUE; theme->scale = 1; theme->dpi = 96.0; theme->variants = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); theme->font_descs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) pango_font_description_free); theme->title_heights = g_hash_table_new (NULL, NULL); } /** * meta_theme_error_quark: * * Domain for #MetaThemeError errors. * * Returns: the #GQuark identifying the #MetaThemeError domain. */ GQuark meta_theme_error_quark (void) { return g_quark_from_static_string ("meta-theme-error-quark"); } MetaTheme * meta_theme_new (MetaThemeType type) { return g_object_new (META_TYPE_THEME, "type", type, NULL); } gboolean meta_theme_load (MetaTheme *theme, const gchar *name, GError **error) { if (theme->type == META_THEME_TYPE_GTK) { g_free (theme->gtk_theme_name); theme->gtk_theme_name = g_strdup (name); } else if (theme->type == META_THEME_TYPE_METACITY) { GtkSettings *settings; settings = gtk_settings_get_default (); g_free (theme->gtk_theme_name); g_object_get (settings, "gtk-theme-name", &theme->gtk_theme_name, NULL); } else { g_assert_not_reached (); } return META_THEME_IMPL_GET_CLASS (theme->impl)->load (theme->impl, name, error); } void meta_theme_invalidate (MetaTheme *theme) { g_hash_table_remove_all (theme->variants); g_clear_object (&theme->context); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } void meta_theme_set_button_layout (MetaTheme *theme, const gchar *button_layout, gboolean invert) { if (theme->button_layout != NULL) meta_button_layout_free (theme->button_layout); theme->button_layout = meta_button_layout_new (button_layout, invert); } MetaButton * meta_theme_get_button (MetaTheme *theme, gint x, gint y) { gint side; for (side = 0; side < 2; side++) { MetaButton *buttons; gint n_buttons; gint i; if (side == 0) { buttons = theme->button_layout->left_buttons; n_buttons = theme->button_layout->n_left_buttons; } else if (side == 1) { buttons = theme->button_layout->right_buttons; n_buttons = theme->button_layout->n_right_buttons; } else { g_assert_not_reached (); } for (i = 0; i < n_buttons; i++) { MetaButton *btn; GdkRectangle rect; btn = &buttons[i]; rect = btn->rect.visible; if (!btn->visible || btn->type == META_BUTTON_TYPE_SPACER || rect.width <= 0 || rect.height <= 0) { continue; } rect = btn->rect.clickable; if (x >= rect.x && x < (rect.x + rect.width) && y >= rect.y && y < (rect.y + rect.height)) { return btn; } } } return NULL; } MetaButton ** meta_theme_get_buttons (MetaTheme *theme) { MetaButtonLayout *layout; gint size; MetaButton **retval; gint index; gint side; layout = theme->button_layout; size = layout->n_left_buttons + layout->n_right_buttons + 1; retval = g_new0 (MetaButton *, size); index = 0; for (side = 0; side < 2; side++) { MetaButton *buttons; gint n_buttons; gint i; if (side == 0) { buttons = layout->left_buttons; n_buttons = layout->n_left_buttons; } else if (side == 1) { buttons = layout->right_buttons; n_buttons = layout->n_right_buttons; } else { g_assert_not_reached (); } for (i = 0; i < n_buttons; i++) { MetaButton *btn; GdkRectangle rect; btn = &buttons[i]; rect = btn->rect.visible; if (!btn->visible || btn->type == META_BUTTON_TYPE_SPACER || rect.width <= 0 || rect.height <= 0) { continue; } retval[index++] = btn; } } retval[index] = NULL; return retval; } void meta_theme_set_composited (MetaTheme *theme, gboolean composited) { if (theme->composited == composited) return; theme->composited = composited; meta_theme_impl_set_composited (theme->impl, composited); g_hash_table_foreach (theme->variants, update_composited_func, theme); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } void meta_theme_set_dpi (MetaTheme *theme, gdouble dpi) { if (theme->dpi == dpi) return; theme->dpi = dpi; g_clear_object (&theme->context); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } void meta_theme_set_scale (MetaTheme *theme, gint scale) { if (theme->scale == scale) return; theme->scale = scale; meta_theme_impl_set_scale (theme->impl, scale); g_hash_table_foreach (theme->variants, update_scale_func, theme); g_clear_object (&theme->context); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } void meta_theme_set_titlebar_font (MetaTheme *theme, const PangoFontDescription *titlebar_font) { pango_font_description_free (theme->titlebar_font); theme->titlebar_font = pango_font_description_copy (titlebar_font); g_hash_table_remove_all (theme->font_descs); g_hash_table_remove_all (theme->title_heights); } void meta_theme_get_frame_borders (MetaTheme *theme, const gchar *variant, MetaFrameType type, MetaFrameFlags flags, MetaFrameBorders *borders) { MetaThemeImplClass *impl_class; MetaFrameStyle *style; MetaStyleInfo *style_info; gint title_height; g_return_if_fail (type < META_FRAME_TYPE_LAST); meta_frame_borders_clear (borders); impl_class = META_THEME_IMPL_GET_CLASS (theme->impl); style = impl_class->get_frame_style (theme->impl, type, flags); /* Parser is not supposed to allow this currently */ if (style == NULL) return; style_info = get_style_info (theme, variant); title_height = get_title_height (theme, variant, type, flags); impl_class->get_frame_borders (theme->impl, style->layout, style_info, title_height, flags, type, borders); } void meta_theme_calc_geometry (MetaTheme *theme, const gchar *variant, MetaFrameType type, MetaFrameFlags flags, gint client_width, gint client_height, MetaFrameGeometry *fgeom) { MetaThemeImplClass *impl_class; MetaFrameStyle *style; MetaStyleInfo *style_info; gint title_height; g_return_if_fail (type < META_FRAME_TYPE_LAST); impl_class = META_THEME_IMPL_GET_CLASS (theme->impl); style = impl_class->get_frame_style (theme->impl, type, flags); /* Parser is not supposed to allow this currently */ if (style == NULL) return; style_info = get_style_info (theme, variant); title_height = get_title_height (theme, variant, type, flags); impl_class->calc_geometry (theme->impl, style->layout, style_info, title_height, flags, client_width, client_height, theme->button_layout, type, fgeom); } void meta_theme_draw_frame (MetaTheme *theme, const gchar *variant, cairo_t *cr, MetaFrameType type, MetaFrameFlags flags, gint client_width, gint client_height, const gchar *title, MetaButtonStateFunc func, gpointer user_data, GdkPixbuf *mini_icon, GdkPixbuf *icon) { MetaThemeImplClass *impl_class; MetaFrameStyle *style; MetaStyleInfo *style_info; gint title_height; PangoLayout *title_layout; MetaFrameGeometry fgeom; gint i; g_return_if_fail (type < META_FRAME_TYPE_LAST); impl_class = META_THEME_IMPL_GET_CLASS (theme->impl); style = impl_class->get_frame_style (theme->impl, type, flags); /* Parser is not supposed to allow this currently */ if (style == NULL) return; style_info = get_style_info (theme, variant); title_height = get_title_height (theme, variant, type, flags); title_layout = create_title_layout (theme, variant, type, flags, title); impl_class->calc_geometry (theme->impl, style->layout, style_info, title_height, flags, client_width, client_height, theme->button_layout, type, &fgeom); for (i = 0; i < 2; i++) { MetaButton *buttons; gint n_buttons; gint j; if (i == 0) { buttons = theme->button_layout->left_buttons; n_buttons = theme->button_layout->n_left_buttons; } else if (i == 1) { buttons = theme->button_layout->right_buttons; n_buttons = theme->button_layout->n_right_buttons; } else { g_assert_not_reached (); } for (j = 0; j < n_buttons; j++) { MetaButton *button; MetaButtonState state; GdkRectangle rect; button = &buttons[j]; state = META_BUTTON_STATE_NORMAL; rect = button->rect.visible; if (!button->visible || button->type == META_BUTTON_TYPE_SPACER || rect.width <= 0 || rect.height <= 0) { button->state = state; continue; } if (func != NULL) state = (* func) (button->type, button->rect.clickable, user_data); g_assert (state >= META_BUTTON_STATE_NORMAL && state < META_BUTTON_STATE_LAST); button->state = state; } } impl_class->draw_frame (theme->impl, style, style_info, cr, &fgeom, title_layout, flags, theme->button_layout, mini_icon, icon); g_object_unref (title_layout); }