1 /*
2  * Copyright (c) 2013 Intel Corporation
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a
5  * copy of this software and associated documentation files (the "Software"),
6  * to deal in the Software without restriction, including without limitation
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8  * and/or sell copies of the Software, and to permit persons to whom the
9  * Software is furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included
12  * in all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20  * THE SOFTWARE.
21  */
22 
23 #include "config.h"
24 #include <glib/gi18n-lib.h>
25 
26 #include "css-editor.h"
27 
28 #include "gtkcssprovider.h"
29 #include "gtkstyleprovider.h"
30 #include "gtkstylecontext.h"
31 #include "gtktextview.h"
32 #include "gtkmessagedialog.h"
33 #include "gtkfilechooserdialog.h"
34 #include "gtktogglebutton.h"
35 #include "gtklabel.h"
36 #include "gtktooltip.h"
37 #include "gtktextiter.h"
38 
39 
40 struct _GtkInspectorCssEditorPrivate
41 {
42   GtkWidget *view;
43   GtkTextBuffer *text;
44   GtkCssProvider *provider;
45   GtkToggleButton *disable_button;
46   guint timeout;
47   GList *errors;
48 };
49 
50 typedef struct {
51   GError *error;
52   GtkTextIter start;
53   GtkTextIter end;
54 } CssError;
55 
56 static void
css_error_free(gpointer data)57 css_error_free (gpointer data)
58 {
59   CssError *error = data;
60   g_error_free (error->error);
61   g_free (error);
62 }
63 
64 static gboolean
query_tooltip_cb(GtkWidget * widget,gint x,gint y,gboolean keyboard_tip,GtkTooltip * tooltip,GtkInspectorCssEditor * ce)65 query_tooltip_cb (GtkWidget             *widget,
66                   gint                   x,
67                   gint                   y,
68                   gboolean               keyboard_tip,
69                   GtkTooltip            *tooltip,
70                   GtkInspectorCssEditor *ce)
71 {
72   GtkTextIter iter;
73   GList *l;
74 
75   if (keyboard_tip)
76     {
77       gint offset;
78 
79       g_object_get (ce->priv->text, "cursor-position", &offset, NULL);
80       gtk_text_buffer_get_iter_at_offset (ce->priv->text, &iter, offset);
81     }
82   else
83     {
84       gint bx, by, trailing;
85 
86       gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (ce->priv->view), GTK_TEXT_WINDOW_TEXT,
87                                              x, y, &bx, &by);
88       gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (ce->priv->view), &iter, &trailing, bx, by);
89     }
90 
91   for (l = ce->priv->errors; l; l = l->next)
92     {
93       CssError *css_error = l->data;
94 
95       if (gtk_text_iter_in_range (&iter, &css_error->start, &css_error->end))
96         {
97           gtk_tooltip_set_text (tooltip, css_error->error->message);
98           return TRUE;
99         }
100     }
101 
102   return FALSE;
103 }
104 
G_DEFINE_TYPE_WITH_PRIVATE(GtkInspectorCssEditor,gtk_inspector_css_editor,GTK_TYPE_BOX)105 G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorCssEditor, gtk_inspector_css_editor, GTK_TYPE_BOX)
106 
107 static void
108 set_initial_text (GtkInspectorCssEditor *ce)
109 {
110   gchar *initial_text;
111   initial_text = g_strconcat ("/*\n",
112                               _("You can type here any CSS rule recognized by GTK+."), "\n",
113                               _("You can temporarily disable this custom CSS by clicking on the “Pause” button above."), "\n\n",
114                               _("Changes are applied instantly and globally, for the whole application."), "\n",
115                               "*/\n\n", NULL);
116   gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), initial_text, -1);
117   g_free (initial_text);
118 }
119 
120 static void
disable_toggled(GtkToggleButton * button,GtkInspectorCssEditor * ce)121 disable_toggled (GtkToggleButton       *button,
122                  GtkInspectorCssEditor *ce)
123 {
124   if (gtk_toggle_button_get_active (button))
125     gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (),
126                                                   GTK_STYLE_PROVIDER (ce->priv->provider));
127   else
128     gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
129                                                GTK_STYLE_PROVIDER (ce->priv->provider),
130                                                GTK_STYLE_PROVIDER_PRIORITY_USER);
131 }
132 
133 static gchar *
get_current_text(GtkTextBuffer * buffer)134 get_current_text (GtkTextBuffer *buffer)
135 {
136   GtkTextIter start, end;
137 
138   gtk_text_buffer_get_start_iter (buffer, &start);
139   gtk_text_buffer_get_end_iter (buffer, &end);
140   gtk_text_buffer_remove_all_tags (buffer, &start, &end);
141 
142   return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
143 }
144 
145 static void
save_to_file(GtkInspectorCssEditor * ce,const gchar * filename)146 save_to_file (GtkInspectorCssEditor *ce,
147               const gchar           *filename)
148 {
149   gchar *text;
150   GError *error = NULL;
151 
152   text = get_current_text (ce->priv->text);
153 
154   if (!g_file_set_contents (filename, text, -1, &error))
155     {
156       GtkWidget *dialog;
157 
158       dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))),
159                                        GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
160                                        GTK_MESSAGE_INFO,
161                                        GTK_BUTTONS_OK,
162                                        _("Saving CSS failed"));
163       gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
164                                                 "%s", error->message);
165       g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
166       gtk_widget_show (dialog);
167       g_error_free (error);
168     }
169 
170   g_free (text);
171 }
172 
173 static void
save_response(GtkWidget * dialog,gint response,GtkInspectorCssEditor * ce)174 save_response (GtkWidget             *dialog,
175                gint                   response,
176                GtkInspectorCssEditor *ce)
177 {
178   gtk_widget_hide (dialog);
179 
180   if (response == GTK_RESPONSE_ACCEPT)
181     {
182       gchar *filename;
183 
184       filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
185       save_to_file (ce, filename);
186       g_free (filename);
187     }
188 
189   gtk_widget_destroy (dialog);
190 }
191 
192 static void
save_clicked(GtkButton * button,GtkInspectorCssEditor * ce)193 save_clicked (GtkButton             *button,
194               GtkInspectorCssEditor *ce)
195 {
196   GtkWidget *dialog;
197 
198   dialog = gtk_file_chooser_dialog_new ("",
199                                         GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))),
200                                         GTK_FILE_CHOOSER_ACTION_SAVE,
201                                         _("_Cancel"), GTK_RESPONSE_CANCEL,
202                                         _("_Save"), GTK_RESPONSE_ACCEPT,
203                                         NULL);
204   gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), "custom.css");
205   gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
206   gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
207   gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
208   g_signal_connect (dialog, "response", G_CALLBACK (save_response), ce);
209   gtk_widget_show (dialog);
210 }
211 
212 static void
update_style(GtkInspectorCssEditor * ce)213 update_style (GtkInspectorCssEditor *ce)
214 {
215   gchar *text;
216 
217   g_list_free_full (ce->priv->errors, css_error_free);
218   ce->priv->errors = NULL;
219 
220   text = get_current_text (ce->priv->text);
221   gtk_css_provider_load_from_data (ce->priv->provider, text, -1, NULL);
222   g_free (text);
223 }
224 
225 static gboolean
update_timeout(gpointer data)226 update_timeout (gpointer data)
227 {
228   GtkInspectorCssEditor *ce = data;
229 
230   ce->priv->timeout = 0;
231 
232   update_style (ce);
233 
234   return G_SOURCE_REMOVE;
235 }
236 
237 static void
text_changed(GtkTextBuffer * buffer,GtkInspectorCssEditor * ce)238 text_changed (GtkTextBuffer         *buffer,
239               GtkInspectorCssEditor *ce)
240 {
241   if (ce->priv->timeout != 0)
242     g_source_remove (ce->priv->timeout);
243 
244   ce->priv->timeout = g_timeout_add (100, update_timeout, ce);
245 
246   g_list_free_full (ce->priv->errors, css_error_free);
247   ce->priv->errors = NULL;
248 }
249 
250 static void
show_parsing_error(GtkCssProvider * provider,GtkCssSection * section,const GError * error,GtkInspectorCssEditor * ce)251 show_parsing_error (GtkCssProvider        *provider,
252                     GtkCssSection         *section,
253                     const GError          *error,
254                     GtkInspectorCssEditor *ce)
255 {
256   const char *tag_name;
257   GtkTextBuffer *buffer = GTK_TEXT_BUFFER (ce->priv->text);
258   CssError *css_error;
259 
260   css_error = g_new (CssError, 1);
261   css_error->error = g_error_copy (error);
262 
263   gtk_text_buffer_get_iter_at_line_index (buffer,
264                                           &css_error->start,
265                                           gtk_css_section_get_start_line (section),
266                                           gtk_css_section_get_start_position (section));
267   gtk_text_buffer_get_iter_at_line_index (buffer,
268                                           &css_error->end,
269                                           gtk_css_section_get_end_line (section),
270                                           gtk_css_section_get_end_position (section));
271 
272   if (g_error_matches (error, GTK_CSS_PROVIDER_ERROR, GTK_CSS_PROVIDER_ERROR_DEPRECATED))
273     tag_name = "warning";
274   else
275     tag_name = "error";
276 
277   if (gtk_text_iter_equal (&css_error->start, &css_error->end))
278     gtk_text_iter_forward_char (&css_error->end);
279 
280   gtk_text_buffer_apply_tag_by_name (buffer, tag_name, &css_error->start, &css_error->end);
281 
282   ce->priv->errors = g_list_prepend (ce->priv->errors, css_error);
283 }
284 
285 static void
create_provider(GtkInspectorCssEditor * ce)286 create_provider (GtkInspectorCssEditor *ce)
287 {
288   ce->priv->provider = gtk_css_provider_new ();
289   gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
290                                              GTK_STYLE_PROVIDER (ce->priv->provider),
291                                              GTK_STYLE_PROVIDER_PRIORITY_USER);
292 
293   g_signal_connect (ce->priv->provider, "parsing-error",
294                     G_CALLBACK (show_parsing_error), ce);
295 }
296 
297 static void
destroy_provider(GtkInspectorCssEditor * ce)298 destroy_provider (GtkInspectorCssEditor *ce)
299 {
300   gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (),
301                                                 GTK_STYLE_PROVIDER (ce->priv->provider));
302   g_clear_object (&ce->priv->provider);
303 }
304 
305 static void
gtk_inspector_css_editor_init(GtkInspectorCssEditor * ce)306 gtk_inspector_css_editor_init (GtkInspectorCssEditor *ce)
307 {
308   ce->priv = gtk_inspector_css_editor_get_instance_private (ce);
309   gtk_widget_init_template (GTK_WIDGET (ce));
310 }
311 
312 static void
constructed(GObject * object)313 constructed (GObject *object)
314 {
315   GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);
316 
317   create_provider (ce);
318   set_initial_text (ce);
319 }
320 
321 static void
finalize(GObject * object)322 finalize (GObject *object)
323 {
324   GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);
325 
326   if (ce->priv->timeout != 0)
327     g_source_remove (ce->priv->timeout);
328 
329   destroy_provider (ce);
330 
331   g_list_free_full (ce->priv->errors, css_error_free);
332 
333   G_OBJECT_CLASS (gtk_inspector_css_editor_parent_class)->finalize (object);
334 }
335 
336 static void
gtk_inspector_css_editor_class_init(GtkInspectorCssEditorClass * klass)337 gtk_inspector_css_editor_class_init (GtkInspectorCssEditorClass *klass)
338 {
339   GObjectClass *object_class = G_OBJECT_CLASS (klass);
340   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
341 
342   object_class->constructed = constructed;
343   object_class->finalize = finalize;
344 
345   gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/css-editor.ui");
346   gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, text);
347   gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, view);
348   gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, disable_button);
349   gtk_widget_class_bind_template_callback (widget_class, disable_toggled);
350   gtk_widget_class_bind_template_callback (widget_class, save_clicked);
351   gtk_widget_class_bind_template_callback (widget_class, text_changed);
352   gtk_widget_class_bind_template_callback (widget_class, query_tooltip_cb);
353 }
354 
355 // vim: set et sw=2 ts=2:
356