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 "window.h"
27#include "css-editor.h"
28
29#include "gtkcssprovider.h"
30#include "gtkstyleprovider.h"
31#include "gtkstylecontext.h"
32#include "gtktextview.h"
33#include "gtkmessagedialog.h"
34#include "gtkfilechooserdialog.h"
35#include "gtktogglebutton.h"
36#include "gtklabel.h"
37#include "gtktooltip.h"
38#include "gtktextiter.h"
39
40#include "gtk/css/gtkcss.h"
41
42struct _GtkInspectorCssEditorPrivate
43{
44 GtkWidget *view;
45 GtkTextBuffer *text;
46 GdkDisplay *display;
47 GtkCssProvider *provider;
48 GtkToggleButton *disable_button;
49 guint timeout;
50 GList *errors;
51};
52
53typedef struct {
54 GError *error;
55 GtkTextIter start;
56 GtkTextIter end;
57} CssError;
58
59static void
60css_error_free (gpointer data)
61{
62 CssError *error = data;
63 g_error_free (error: error->error);
64 g_free (mem: error);
65}
66
67static gboolean
68query_tooltip_cb (GtkWidget *widget,
69 int x,
70 int y,
71 gboolean keyboard_tip,
72 GtkTooltip *tooltip,
73 GtkInspectorCssEditor *ce)
74{
75 GtkTextIter iter;
76 GList *l;
77
78 if (keyboard_tip)
79 {
80 int offset;
81
82 g_object_get (object: ce->priv->text, first_property_name: "cursor-position", &offset, NULL);
83 gtk_text_buffer_get_iter_at_offset (buffer: ce->priv->text, iter: &iter, char_offset: offset);
84 }
85 else
86 {
87 int bx, by, trailing;
88
89 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (ce->priv->view), win: GTK_TEXT_WINDOW_TEXT,
90 window_x: x, window_y: y, buffer_x: &bx, buffer_y: &by);
91 gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (ce->priv->view), iter: &iter, trailing: &trailing, x: bx, y: by);
92 }
93
94 for (l = ce->priv->errors; l; l = l->next)
95 {
96 CssError *css_error = l->data;
97
98 if (gtk_text_iter_in_range (iter: &iter, start: &css_error->start, end: &css_error->end))
99 {
100 gtk_tooltip_set_text (tooltip, text: css_error->error->message);
101 return TRUE;
102 }
103 }
104
105 return FALSE;
106}
107
108G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorCssEditor, gtk_inspector_css_editor, GTK_TYPE_BOX)
109
110static char *
111get_autosave_path (void)
112{
113 return g_build_filename (first_element: g_get_user_cache_dir (), "gtk-4.0", "inspector-css-autosave", NULL);
114}
115
116static void
117set_initial_text (GtkInspectorCssEditor *ce)
118{
119 char *initial_text = NULL;
120 char *autosave_file = NULL;
121 gsize len;
122
123 autosave_file = get_autosave_path ();
124
125 if (g_file_get_contents (filename: autosave_file, contents: &initial_text, length: &len, NULL))
126 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ce->priv->disable_button), TRUE);
127 else
128 initial_text = g_strconcat (string1: "/*\n",
129 _("You can type here any CSS rule recognized by GTK."), "\n",
130 _("You can temporarily disable this custom CSS by clicking on the “Pause” button above."), "\n\n",
131 _("Changes are applied instantly and globally, for the whole application."), "\n",
132 "*/\n\n", NULL);
133 gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), text: initial_text, len: -1);
134 g_free (mem: initial_text);
135 g_free (mem: autosave_file);
136}
137
138static void
139autosave_contents (GtkInspectorCssEditor *ce)
140{
141 char *autosave_file = NULL;
142 char *dir = NULL;
143 char *contents;
144 GtkTextIter start, end;
145
146 gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (ce->priv->text), start: &start, end: &end);
147 contents = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (ce->priv->text), start: &start, end: &end, TRUE);
148 autosave_file = get_autosave_path ();
149 dir = g_path_get_dirname (file_name: autosave_file);
150 g_mkdir_with_parents (pathname: dir, mode: 0755);
151 g_file_set_contents (filename: autosave_file, contents, length: -1, NULL);
152
153 g_free (mem: dir);
154 g_free (mem: autosave_file);
155 g_free (mem: contents);
156}
157
158static void
159disable_toggled (GtkToggleButton *button,
160 GtkInspectorCssEditor *ce)
161{
162 if (!ce->priv->display)
163 return;
164
165 if (gtk_toggle_button_get_active (toggle_button: button))
166 gtk_style_context_remove_provider_for_display (display: ce->priv->display,
167 GTK_STYLE_PROVIDER (ce->priv->provider));
168 else
169 gtk_style_context_add_provider_for_display (display: ce->priv->display,
170 GTK_STYLE_PROVIDER (ce->priv->provider),
171 GTK_STYLE_PROVIDER_PRIORITY_USER);
172}
173
174static char *
175get_current_text (GtkTextBuffer *buffer)
176{
177 GtkTextIter start, end;
178
179 gtk_text_buffer_get_start_iter (buffer, iter: &start);
180 gtk_text_buffer_get_end_iter (buffer, iter: &end);
181 gtk_text_buffer_remove_all_tags (buffer, start: &start, end: &end);
182
183 return gtk_text_buffer_get_text (buffer, start: &start, end: &end, FALSE);
184}
185
186static void
187save_to_file (GtkInspectorCssEditor *ce,
188 GFile *file)
189{
190 GError *error = NULL;
191 char *text;
192
193 text = get_current_text (buffer: ce->priv->text);
194
195 g_file_replace_contents (file, contents: text, length: strlen (s: text),
196 NULL,
197 FALSE,
198 flags: G_FILE_CREATE_NONE,
199 NULL,
200 NULL,
201 error: &error);
202
203 if (error != NULL)
204 {
205 GtkWidget *dialog;
206
207 dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (ce))),
208 flags: GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
209 type: GTK_MESSAGE_INFO,
210 buttons: GTK_BUTTONS_OK,
211 _("Saving CSS failed"));
212 gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
213 message_format: "%s", error->message);
214 g_signal_connect (dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
215 gtk_widget_show (widget: dialog);
216 g_error_free (error);
217 }
218
219 g_free (mem: text);
220}
221
222static void
223save_response (GtkWidget *dialog,
224 int response,
225 GtkInspectorCssEditor *ce)
226{
227 gtk_widget_hide (widget: dialog);
228
229 if (response == GTK_RESPONSE_ACCEPT)
230 {
231 GFile *file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
232 save_to_file (ce, file);
233 g_object_unref (object: file);
234 }
235
236 gtk_window_destroy (GTK_WINDOW (dialog));
237}
238
239static void
240save_clicked (GtkButton *button,
241 GtkInspectorCssEditor *ce)
242{
243 GtkWidget *dialog;
244
245 dialog = gtk_file_chooser_dialog_new (title: "",
246 GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (ce))),
247 action: GTK_FILE_CHOOSER_ACTION_SAVE,
248 _("_Cancel"), GTK_RESPONSE_CANCEL,
249 _("_Save"), GTK_RESPONSE_ACCEPT,
250 NULL);
251 gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), name: "custom.css");
252 gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id: GTK_RESPONSE_ACCEPT);
253 gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
254 g_signal_connect (dialog, "response", G_CALLBACK (save_response), ce);
255 gtk_widget_show (widget: dialog);
256}
257
258static void
259update_style (GtkInspectorCssEditor *ce)
260{
261 char *text;
262
263 g_list_free_full (list: ce->priv->errors, free_func: css_error_free);
264 ce->priv->errors = NULL;
265
266 text = get_current_text (buffer: ce->priv->text);
267 gtk_css_provider_load_from_data (css_provider: ce->priv->provider, data: text, length: -1);
268 g_free (mem: text);
269}
270
271static gboolean
272update_timeout (gpointer data)
273{
274 GtkInspectorCssEditor *ce = data;
275
276 ce->priv->timeout = 0;
277
278 autosave_contents (ce);
279 update_style (ce);
280
281 return G_SOURCE_REMOVE;
282}
283
284static void
285text_changed (GtkTextBuffer *buffer,
286 GtkInspectorCssEditor *ce)
287{
288 if (ce->priv->timeout != 0)
289 g_source_remove (tag: ce->priv->timeout);
290
291 ce->priv->timeout = g_timeout_add (interval: 100, function: update_timeout, data: ce);
292
293 g_list_free_full (list: ce->priv->errors, free_func: css_error_free);
294 ce->priv->errors = NULL;
295}
296
297static void
298show_parsing_error (GtkCssProvider *provider,
299 GtkCssSection *section,
300 const GError *error,
301 GtkInspectorCssEditor *ce)
302{
303 const char *tag_name;
304 GtkTextBuffer *buffer = GTK_TEXT_BUFFER (ce->priv->text);
305 const GtkCssLocation *start, *end;
306 CssError *css_error;
307
308 css_error = g_new (CssError, 1);
309 css_error->error = g_error_copy (error);
310
311 start = gtk_css_section_get_start_location (section);
312 gtk_text_buffer_get_iter_at_line_index (buffer,
313 iter: &css_error->start,
314 line_number: start->lines,
315 byte_index: start->line_bytes);
316 end = gtk_css_section_get_end_location (section);
317 gtk_text_buffer_get_iter_at_line_index (buffer,
318 iter: &css_error->end,
319 line_number: end->lines,
320 byte_index: end->line_bytes);
321
322 if (error->domain == GTK_CSS_PARSER_WARNING)
323 tag_name = "warning";
324 else
325 tag_name = "error";
326
327 if (gtk_text_iter_equal (lhs: &css_error->start, rhs: &css_error->end))
328 gtk_text_iter_forward_char (iter: &css_error->end);
329
330 gtk_text_buffer_apply_tag_by_name (buffer, name: tag_name, start: &css_error->start, end: &css_error->end);
331
332 ce->priv->errors = g_list_prepend (list: ce->priv->errors, data: css_error);
333}
334
335static void
336create_provider (GtkInspectorCssEditor *ce)
337{
338 ce->priv->provider = gtk_css_provider_new ();
339 g_signal_connect (ce->priv->provider, "parsing-error",
340 G_CALLBACK (show_parsing_error), ce);
341
342}
343
344static void
345destroy_provider (GtkInspectorCssEditor *ce)
346{
347 g_signal_handlers_disconnect_by_func (ce->priv->provider, show_parsing_error, ce);
348 g_clear_object (&ce->priv->provider);
349}
350
351static void
352add_provider (GtkInspectorCssEditor *ce,
353 GdkDisplay *display)
354{
355 gtk_style_context_add_provider_for_display (display,
356 GTK_STYLE_PROVIDER (ce->priv->provider),
357 GTK_STYLE_PROVIDER_PRIORITY_USER);
358}
359
360static void
361remove_provider (GtkInspectorCssEditor *ce,
362 GdkDisplay *display)
363{
364 gtk_style_context_remove_provider_for_display (display,
365 GTK_STYLE_PROVIDER (ce->priv->provider));
366}
367
368static void
369gtk_inspector_css_editor_init (GtkInspectorCssEditor *ce)
370{
371 ce->priv = gtk_inspector_css_editor_get_instance_private (self: ce);
372 gtk_widget_init_template (GTK_WIDGET (ce));
373}
374
375static void
376constructed (GObject *object)
377{
378 GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);
379
380 create_provider (ce);
381}
382
383static void
384finalize (GObject *object)
385{
386 GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);
387
388 if (ce->priv->timeout != 0)
389 g_source_remove (tag: ce->priv->timeout);
390
391 if (ce->priv->display)
392 remove_provider (ce, display: ce->priv->display);
393 destroy_provider (ce);
394
395 g_list_free_full (list: ce->priv->errors, free_func: css_error_free);
396
397 G_OBJECT_CLASS (gtk_inspector_css_editor_parent_class)->finalize (object);
398}
399
400static void
401gtk_inspector_css_editor_class_init (GtkInspectorCssEditorClass *klass)
402{
403 GObjectClass *object_class = G_OBJECT_CLASS (klass);
404 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
405
406 object_class->constructed = constructed;
407 object_class->finalize = finalize;
408
409 gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/org/gtk/libgtk/inspector/css-editor.ui");
410 gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, text);
411 gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, view);
412 gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, disable_button);
413 gtk_widget_class_bind_template_callback (widget_class, disable_toggled);
414 gtk_widget_class_bind_template_callback (widget_class, save_clicked);
415 gtk_widget_class_bind_template_callback (widget_class, text_changed);
416 gtk_widget_class_bind_template_callback (widget_class, query_tooltip_cb);
417}
418
419void
420gtk_inspector_css_editor_set_display (GtkInspectorCssEditor *ce,
421 GdkDisplay *display)
422{
423 ce->priv->display = display;
424 add_provider (ce, display);
425 set_initial_text (ce);
426}
427
428// vim: set et sw=2 ts=2:
429

source code of gtk/gtk/inspector/css-editor.c