/* * Copyright (c) 2013 Intel Corporation * * 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 #include "css-editor.h" #include "gtkcssprovider.h" #include "gtkstyleprovider.h" #include "gtkstylecontext.h" #include "gtktextview.h" #include "gtkmessagedialog.h" #include "gtkfilechooserdialog.h" #include "gtktogglebutton.h" #include "gtklabel.h" #include "gtktooltip.h" #include "gtktextiter.h" #define GTK_INSPECTOR_CSS_EDITOR_TEXT "inspector-css-editor-text" #define GTK_INSPECTOR_CSS_EDITOR_PROVIDER "inspector-css-editor-provider" enum { COLUMN_ENABLED, COLUMN_NAME, COLUMN_USER }; enum { PROP_0, PROP_GLOBAL }; typedef struct { gboolean enabled; gboolean user; } GtkInspectorCssEditorByContext; struct _GtkInspectorCssEditorPrivate { GtkWidget *view; GtkWidget *object_title; GtkTextBuffer *text; GtkCssProvider *provider; gboolean global; GtkStyleContext *context; GtkToggleButton *disable_button; guint timeout; GList *errors; }; typedef struct { GError *error; GtkTextIter start; GtkTextIter end; } CssError; static void css_error_free (gpointer data) { CssError *error = data; g_error_free (error->error); g_free (error); } static gboolean query_tooltip_cb (GtkWidget *widget, gint x, gint y, gboolean keyboard_tip, GtkTooltip *tooltip, GtkInspectorCssEditor *ce) { GtkTextIter iter; GList *l; if (keyboard_tip) { gint offset; g_object_get (ce->priv->text, "cursor-position", &offset, NULL); gtk_text_buffer_get_iter_at_offset (ce->priv->text, &iter, offset); } else { gint bx, by, trailing; gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (ce->priv->view), GTK_TEXT_WINDOW_TEXT, x, y, &bx, &by); gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (ce->priv->view), &iter, &trailing, bx, by); } for (l = ce->priv->errors; l; l = l->next) { CssError *css_error = l->data; if (gtk_text_iter_in_range (&iter, &css_error->start, &css_error->end)) { gtk_tooltip_set_text (tooltip, css_error->error->message); return TRUE; } } return FALSE; } static void gtk_inspector_css_editor_remove_dead_object (gpointer data, GObject *dead_object); G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorCssEditor, gtk_inspector_css_editor, GTK_TYPE_BOX) static void set_initial_text (GtkInspectorCssEditor *ce) { const gchar *text = NULL; if (ce->priv->context) text = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_TEXT); if (text) gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), text, -1); else { gchar *initial_text; if (ce->priv->global) initial_text = g_strconcat ("/*\n", _("You can type here any CSS rule recognized by GTK+."), "\n", _("You can temporarily disable this custom CSS by clicking on the “Pause” button above."), "\n\n", _("Changes are applied instantly and globally, for the whole application."), "\n", "*/\n\n", NULL); else initial_text = g_strconcat ("/*\n", _("You can type here any CSS rule recognized by GTK+."), "\n", _("You can temporarily disable this custom CSS by clicking on the “Pause” button above."), "\n\n", _("Changes are applied instantly, only for this selected widget."), "\n", "*/\n\n", NULL); gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), initial_text, -1); g_free (initial_text); } } static void disable_toggled (GtkToggleButton *button, GtkInspectorCssEditor *ce) { if (gtk_toggle_button_get_active (button)) { if (ce->priv->global) gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (ce->priv->provider)); else if (ce->priv->context) gtk_style_context_remove_provider (ce->priv->context, GTK_STYLE_PROVIDER (g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER))); } else { if (ce->priv->global) gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (ce->priv->provider), GTK_STYLE_PROVIDER_PRIORITY_USER); else if (ce->priv->context) gtk_style_context_add_provider (ce->priv->context, GTK_STYLE_PROVIDER (g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER)), G_MAXUINT); } } static gchar * get_current_text (GtkTextBuffer *buffer) { GtkTextIter start, end; gtk_text_buffer_get_start_iter (buffer, &start); gtk_text_buffer_get_end_iter (buffer, &end); gtk_text_buffer_remove_all_tags (buffer, &start, &end); return gtk_text_buffer_get_text (buffer, &start, &end, FALSE); } static void save_to_file (GtkInspectorCssEditor *ce, const gchar *filename) { gchar *text; GError *error = NULL; text = get_current_text (ce->priv->text); if (!g_file_set_contents (filename, text, -1, &error)) { GtkWidget *dialog; dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))), GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, _("Saving CSS failed")); gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", error->message); g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); gtk_widget_show (dialog); g_error_free (error); } g_free (text); } static void save_response (GtkWidget *dialog, gint response, GtkInspectorCssEditor *ce) { gtk_widget_hide (dialog); if (response == GTK_RESPONSE_ACCEPT) { gchar *filename; filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); save_to_file (ce, filename); g_free (filename); } gtk_widget_destroy (dialog); } static void save_clicked (GtkButton *button, GtkInspectorCssEditor *ce) { GtkWidget *dialog; dialog = gtk_file_chooser_dialog_new ("", GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))), GTK_FILE_CHOOSER_ACTION_SAVE, _("_Cancel"), GTK_RESPONSE_CANCEL, _("_Save"), GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), "custom.css"); gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE); g_signal_connect (dialog, "response", G_CALLBACK (save_response), ce); gtk_widget_show (dialog); } static void update_style (GtkInspectorCssEditor *ce) { GtkCssProvider *provider; gchar *text; if (ce->priv->global) provider = ce->priv->provider; else if (ce->priv->context) provider = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER); else return; g_list_free_full (ce->priv->errors, css_error_free); ce->priv->errors = NULL; text = get_current_text (ce->priv->text); gtk_css_provider_load_from_data (provider, text, -1, NULL); g_free (text); gtk_style_context_reset_widgets (gdk_screen_get_default ()); } static gboolean update_timeout (gpointer data) { GtkInspectorCssEditor *ce = data; ce->priv->timeout = 0; update_style (ce); return G_SOURCE_REMOVE; } static void text_changed (GtkTextBuffer *buffer, GtkInspectorCssEditor *ce) { if (ce->priv->timeout != 0) g_source_remove (ce->priv->timeout); ce->priv->timeout = g_timeout_add (100, update_timeout, ce); } static void show_parsing_error (GtkCssProvider *provider, GtkCssSection *section, const GError *error, GtkInspectorCssEditor *ce) { const char *tag_name; GtkTextBuffer *buffer = GTK_TEXT_BUFFER (ce->priv->text); CssError *css_error; css_error = g_new (CssError, 1); css_error->error = g_error_copy (error); gtk_text_buffer_get_iter_at_line_index (buffer, &css_error->start, gtk_css_section_get_start_line (section), gtk_css_section_get_start_position (section)); gtk_text_buffer_get_iter_at_line_index (buffer, &css_error->end, gtk_css_section_get_end_line (section), gtk_css_section_get_end_position (section)); if (g_error_matches (error, GTK_CSS_PROVIDER_ERROR, GTK_CSS_PROVIDER_ERROR_DEPRECATED)) tag_name = "warning"; else tag_name = "error"; if (gtk_text_iter_equal (&css_error->start, &css_error->end)) gtk_text_iter_forward_char (&css_error->end); gtk_text_buffer_apply_tag_by_name (buffer, tag_name, &css_error->start, &css_error->end); ce->priv->errors = g_list_prepend (ce->priv->errors, css_error); } static void create_provider (GtkInspectorCssEditor *ce) { GtkCssProvider *provider = gtk_css_provider_new (); if (ce->priv->global) { ce->priv->provider = provider; gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (ce->priv->provider), GTK_STYLE_PROVIDER_PRIORITY_USER); } else if (ce->priv->context) { gtk_style_context_add_provider (ce->priv->context, GTK_STYLE_PROVIDER (provider), G_MAXUINT); g_object_set_data_full (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER, provider, g_object_unref); } g_signal_connect (provider, "parsing-error", G_CALLBACK (show_parsing_error), ce); } static void destroy_provider (GtkInspectorCssEditor *ce) { if (ce->priv->global) { gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), GTK_STYLE_PROVIDER (ce->priv->provider)); g_object_unref (ce->priv->provider); ce->priv->provider = NULL; } else if (ce->priv->context) { GtkStyleProvider *provider; provider = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER); gtk_style_context_remove_provider (ce->priv->context, provider); g_object_set_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER, NULL); } } static void gtk_inspector_css_editor_init (GtkInspectorCssEditor *ce) { ce->priv = gtk_inspector_css_editor_get_instance_private (ce); gtk_widget_init_template (GTK_WIDGET (ce)); } static void constructed (GObject *object) { GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object); create_provider (ce); set_initial_text (ce); } static void get_property (GObject *object, guint param_id, GValue *value, GParamSpec *pspec) { GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object); switch (param_id) { case PROP_GLOBAL: g_value_set_boolean (value, ce->priv->global); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec); break; } } static void set_property (GObject *object, guint param_id, const GValue *value, GParamSpec *pspec) { GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object); switch (param_id) { case PROP_GLOBAL: ce->priv->global = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec); break; } } static void finalize (GObject *object) { GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object); if (ce->priv->timeout != 0) g_source_remove (ce->priv->timeout); destroy_provider (ce); if (ce->priv->context) { g_object_weak_unref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce); g_object_set_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_TEXT, NULL); ce->priv->context = NULL; } g_list_free_full (ce->priv->errors, css_error_free); G_OBJECT_CLASS (gtk_inspector_css_editor_parent_class)->finalize (object); } static void gtk_inspector_css_editor_class_init (GtkInspectorCssEditorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->get_property = get_property; object_class->set_property = set_property; object_class->constructed = constructed; object_class->finalize = finalize; g_object_class_install_property (object_class, PROP_GLOBAL, g_param_spec_boolean ("global", "Global", "Whether this editor changes the whole application or just the selected widget", TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/css-editor.ui"); gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, text); gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, view); gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, disable_button); gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, object_title); gtk_widget_class_bind_template_callback (widget_class, disable_toggled); gtk_widget_class_bind_template_callback (widget_class, save_clicked); gtk_widget_class_bind_template_callback (widget_class, text_changed); gtk_widget_class_bind_template_callback (widget_class, query_tooltip_cb); } static void gtk_inspector_css_editor_remove_dead_object (gpointer data, GObject *dead_object) { GtkInspectorCssEditor *ce = data; ce->priv->context = NULL; gtk_widget_hide (GTK_WIDGET (ce)); } void gtk_inspector_css_editor_set_object (GtkInspectorCssEditor *ce, GObject *object) { gchar *text; GtkCssProvider *provider; const gchar *title; g_return_if_fail (GTK_INSPECTOR_IS_CSS_EDITOR (ce)); g_return_if_fail (!ce->priv->global); if (ce->priv->context) { g_object_weak_unref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce); text = get_current_text (GTK_TEXT_BUFFER (ce->priv->text)); g_object_set_data_full (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_TEXT, text, g_free); ce->priv->context = NULL; } if (!GTK_IS_WIDGET (object)) { gtk_widget_hide (GTK_WIDGET (ce)); return; } gtk_widget_show (GTK_WIDGET (ce)); title = (const gchar *)g_object_get_data (object, "gtk-inspector-object-title"); gtk_label_set_label (GTK_LABEL (ce->priv->object_title), title); ce->priv->context = gtk_widget_get_style_context (GTK_WIDGET (object)); provider = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER); if (!provider) create_provider (ce); set_initial_text (ce); disable_toggled (ce->priv->disable_button, ce); g_object_weak_ref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce); } // vim: set et sw=2 ts=2: