1/*
2 * Copyright © 2019 Benjamin Otte
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Benjamin Otte <otte@gnome.org>
18 */
19
20#include "config.h"
21
22#include "node-editor-window.h"
23
24#include "gtkrendererpaintableprivate.h"
25
26#include "gsk/gskrendernodeparserprivate.h"
27#include "gsk/gl/gskglrenderer.h"
28#ifdef GDK_WINDOWING_BROADWAY
29#include "gsk/broadway/gskbroadwayrenderer.h"
30#endif
31#ifdef GDK_RENDERING_VULKAN
32#include "gsk/vulkan/gskvulkanrenderer.h"
33#endif
34
35#ifndef NODE_EDITOR_SOURCE_DIR
36#define NODE_EDITOR_SOURCE_DIR "." /* Fallback */
37#endif
38
39typedef struct
40{
41 gsize start_chars;
42 gsize end_chars;
43 char *message;
44} TextViewError;
45
46struct _NodeEditorWindow
47{
48 GtkApplicationWindow parent;
49
50 GtkWidget *picture;
51 GtkWidget *text_view;
52 GtkTextBuffer *text_buffer;
53 GtkTextTagTable *tag_table;
54
55 GtkWidget *testcase_popover;
56 GtkWidget *testcase_error_label;
57 GtkWidget *testcase_cairo_checkbutton;
58 GtkWidget *testcase_name_entry;
59 GtkWidget *testcase_save_button;
60
61 GtkWidget *renderer_listbox;
62 GListStore *renderers;
63 GskRenderNode *node;
64
65 GFileMonitor *file_monitor;
66
67 GArray *errors;
68};
69
70struct _NodeEditorWindowClass
71{
72 GtkApplicationWindowClass parent_class;
73};
74
75G_DEFINE_TYPE(NodeEditorWindow, node_editor_window, GTK_TYPE_APPLICATION_WINDOW);
76
77static void
78text_view_error_free (TextViewError *e)
79{
80 g_free (mem: e->message);
81}
82
83static char *
84get_current_text (GtkTextBuffer *buffer)
85{
86 GtkTextIter start, end;
87
88 gtk_text_buffer_get_start_iter (buffer, iter: &start);
89 gtk_text_buffer_get_end_iter (buffer, iter: &end);
90
91 return gtk_text_buffer_get_text (buffer, start: &start, end: &end, FALSE);
92}
93
94static void
95text_buffer_remove_all_tags (GtkTextBuffer *buffer)
96{
97 GtkTextIter start, end;
98
99 gtk_text_buffer_get_start_iter (buffer, iter: &start);
100 gtk_text_buffer_get_end_iter (buffer, iter: &end);
101 gtk_text_buffer_remove_all_tags (buffer, start: &start, end: &end);
102}
103
104static void
105deserialize_error_func (const GskParseLocation *start_location,
106 const GskParseLocation *end_location,
107 const GError *error,
108 gpointer user_data)
109{
110 NodeEditorWindow *self = user_data;
111 GtkTextIter start_iter, end_iter;
112 TextViewError text_view_error;
113
114 gtk_text_buffer_get_iter_at_line_offset (buffer: self->text_buffer, iter: &start_iter,
115 line_number: start_location->lines,
116 char_offset: start_location->line_chars);
117 gtk_text_buffer_get_iter_at_line_offset (buffer: self->text_buffer, iter: &end_iter,
118 line_number: end_location->lines,
119 char_offset: end_location->line_chars);
120
121 gtk_text_buffer_apply_tag_by_name (buffer: self->text_buffer, name: "error",
122 start: &start_iter, end: &end_iter);
123
124 text_view_error.start_chars = start_location->chars;
125 text_view_error.end_chars = end_location->chars;
126 text_view_error.message = g_strdup (str: error->message);
127 g_array_append_val (self->errors, text_view_error);
128}
129
130static void
131text_iter_skip_alpha_backward (GtkTextIter *iter)
132{
133 /* Just skip to the previous non-whitespace char */
134
135 while (!gtk_text_iter_is_start (iter))
136 {
137 gunichar c = gtk_text_iter_get_char (iter);
138
139 if (g_unichar_isspace (c))
140 {
141 gtk_text_iter_forward_char (iter);
142 break;
143 }
144
145 gtk_text_iter_backward_char (iter);
146 }
147}
148
149static void
150text_iter_skip_whitespace_backward (GtkTextIter *iter)
151{
152 while (!gtk_text_iter_is_start (iter))
153 {
154 gunichar c = gtk_text_iter_get_char (iter);
155
156 if (g_unichar_isalpha (c))
157 {
158 gtk_text_iter_forward_char (iter);
159 break;
160 }
161
162 gtk_text_iter_backward_char (iter);
163 }
164}
165
166static void
167text_changed (GtkTextBuffer *buffer,
168 NodeEditorWindow *self)
169{
170 char *text;
171 GBytes *bytes;
172 GtkTextIter iter;
173 GtkTextIter start, end;
174
175 g_array_remove_range (array: self->errors, index_: 0, length: self->errors->len);
176 text = get_current_text (buffer: self->text_buffer);
177 text_buffer_remove_all_tags (buffer: self->text_buffer);
178 bytes = g_bytes_new_take (data: text, size: strlen (s: text));
179
180 g_clear_pointer (&self->node, gsk_render_node_unref);
181
182 /* If this is too slow, go fix the parser performance */
183 self->node = gsk_render_node_deserialize (bytes, error_func: deserialize_error_func, user_data: self);
184 g_bytes_unref (bytes);
185 if (self->node)
186 {
187 /* XXX: Is this code necessary or can we have API to turn nodes into paintables? */
188 GtkSnapshot *snapshot;
189 GdkPaintable *paintable;
190 graphene_rect_t bounds;
191 guint i;
192
193 snapshot = gtk_snapshot_new ();
194 gsk_render_node_get_bounds (node: self->node, bounds: &bounds);
195 gtk_snapshot_translate (snapshot, point: &GRAPHENE_POINT_INIT (- bounds.origin.x, - bounds.origin.y));
196 gtk_snapshot_append_node (snapshot, node: self->node);
197 paintable = gtk_snapshot_free_to_paintable (snapshot, size: &bounds.size);
198 gtk_picture_set_paintable (self: GTK_PICTURE (ptr: self->picture), paintable);
199 for (i = 0; i < g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->renderers)); i++)
200 {
201 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->renderers), position: i);
202 gtk_renderer_paintable_set_paintable (self: item, paintable);
203 g_object_unref (object: item);
204 }
205 g_clear_object (&paintable);
206 }
207 else
208 {
209 gtk_picture_set_paintable (self: GTK_PICTURE (ptr: self->picture), NULL);
210 }
211
212 gtk_text_buffer_get_start_iter (buffer: self->text_buffer, iter: &iter);
213
214 while (!gtk_text_iter_is_end (iter: &iter))
215 {
216 gunichar c = gtk_text_iter_get_char (iter: &iter);
217
218 if (c == '{')
219 {
220 GtkTextIter word_end = iter;
221 GtkTextIter word_start;
222
223 gtk_text_iter_backward_char (iter: &word_end);
224 text_iter_skip_whitespace_backward (iter: &word_end);
225
226 word_start = word_end;
227 gtk_text_iter_backward_word_start (iter: &word_start);
228 text_iter_skip_alpha_backward (iter: &word_start);
229
230 gtk_text_buffer_apply_tag_by_name (buffer: self->text_buffer, name: "nodename",
231 start: &word_start, end: &word_end);
232 }
233 else if (c == ':')
234 {
235 GtkTextIter word_end = iter;
236 GtkTextIter word_start;
237
238 gtk_text_iter_backward_char (iter: &word_end);
239 text_iter_skip_whitespace_backward (iter: &word_end);
240
241 word_start = word_end;
242 gtk_text_iter_backward_word_start (iter: &word_start);
243 text_iter_skip_alpha_backward (iter: &word_start);
244
245 gtk_text_buffer_apply_tag_by_name (buffer: self->text_buffer, name: "propname",
246 start: &word_start, end: &word_end);
247 }
248 else if (c == '"')
249 {
250 GtkTextIter string_start = iter;
251 GtkTextIter string_end = iter;
252
253 gtk_text_iter_forward_char (iter: &iter);
254 while (!gtk_text_iter_is_end (iter: &iter))
255 {
256 c = gtk_text_iter_get_char (iter: &iter);
257
258 if (c == '"')
259 {
260 gtk_text_iter_forward_char (iter: &iter);
261 string_end = iter;
262 break;
263 }
264
265 gtk_text_iter_forward_char (iter: &iter);
266 }
267
268 gtk_text_buffer_apply_tag_by_name (buffer: self->text_buffer, name: "string",
269 start: &string_start, end: &string_end);
270 }
271
272 gtk_text_iter_forward_char (iter: &iter);
273 }
274
275 gtk_text_buffer_get_bounds (buffer: self->text_buffer, start: &start, end: &end);
276 gtk_text_buffer_apply_tag_by_name (buffer: self->text_buffer, name: "no-hyphens",
277 start: &start, end: &end);
278}
279
280static gboolean
281text_view_query_tooltip_cb (GtkWidget *widget,
282 int x,
283 int y,
284 gboolean keyboard_tip,
285 GtkTooltip *tooltip,
286 NodeEditorWindow *self)
287{
288 GtkTextIter iter;
289 guint i;
290 GString *text;
291
292 if (keyboard_tip)
293 {
294 int offset;
295
296 g_object_get (object: self->text_buffer, first_property_name: "cursor-position", &offset, NULL);
297 gtk_text_buffer_get_iter_at_offset (buffer: self->text_buffer, iter: &iter, char_offset: offset);
298 }
299 else
300 {
301 int bx, by, trailing;
302
303 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (self->text_view), win: GTK_TEXT_WINDOW_TEXT,
304 window_x: x, window_y: y, buffer_x: &bx, buffer_y: &by);
305 gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (self->text_view), iter: &iter, trailing: &trailing, x: bx, y: by);
306 }
307
308 text = g_string_new (init: "");
309
310 for (i = 0; i < self->errors->len; i ++)
311 {
312 const TextViewError *e = &g_array_index (self->errors, TextViewError, i);
313 GtkTextIter start_iter, end_iter;
314
315 gtk_text_buffer_get_iter_at_offset (buffer: self->text_buffer, iter: &start_iter, char_offset: e->start_chars);
316 gtk_text_buffer_get_iter_at_offset (buffer: self->text_buffer, iter: &end_iter, char_offset: e->end_chars);
317
318 if (gtk_text_iter_in_range (iter: &iter, start: &start_iter, end: &end_iter))
319 {
320 if (text->len > 0)
321 g_string_append (string: text, val: "\n");
322 g_string_append (string: text, val: e->message);
323 }
324 }
325
326 if (text->len > 0)
327 {
328 gtk_tooltip_set_text (tooltip, text: text->str);
329 g_string_free (string: text, TRUE);
330 return TRUE;
331 }
332 else
333 {
334 g_string_free (string: text, TRUE);
335 return FALSE;
336 }
337}
338
339static gboolean
340load_bytes (NodeEditorWindow *self,
341 GBytes *bytes);
342
343static void
344load_error (NodeEditorWindow *self,
345 const char *error_message)
346{
347 PangoLayout *layout;
348 GtkSnapshot *snapshot;
349 GskRenderNode *node;
350 GBytes *bytes;
351
352 layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), text: error_message);
353 pango_layout_set_width (layout, width: 300 * PANGO_SCALE);
354 snapshot = gtk_snapshot_new ();
355 gtk_snapshot_append_layout (snapshot, layout, color: &(GdkRGBA) { 0.7, 0.13, 0.13, 1.0 });
356 node = gtk_snapshot_free_to_node (snapshot);
357 bytes = gsk_render_node_serialize (node);
358
359 load_bytes (self, bytes);
360
361 gsk_render_node_unref (node);
362 g_object_unref (object: layout);
363}
364
365static gboolean
366load_bytes (NodeEditorWindow *self,
367 GBytes *bytes)
368{
369 if (!g_utf8_validate (str: g_bytes_get_data (bytes, NULL), max_len: g_bytes_get_size (bytes), NULL))
370 {
371 load_error (self, error_message: "Invalid UTF-8");
372 g_bytes_unref (bytes);
373 return FALSE;
374 }
375
376 gtk_text_buffer_set_text (buffer: self->text_buffer,
377 text: g_bytes_get_data (bytes, NULL),
378 len: g_bytes_get_size (bytes));
379
380 g_bytes_unref (bytes);
381
382 return TRUE;
383}
384
385static gboolean
386load_file_contents (NodeEditorWindow *self,
387 GFile *file)
388{
389 GError *error = NULL;
390 GBytes *bytes;
391
392 bytes = g_file_load_bytes (file, NULL, NULL, error: &error);
393 if (bytes == NULL)
394 {
395 load_error (self, error_message: error->message);
396 g_clear_error (err: &error);
397 return FALSE;
398 }
399
400 return load_bytes (self, bytes);
401}
402
403static GdkContentProvider *
404on_picture_drag_prepare_cb (GtkDragSource *source,
405 double x,
406 double y,
407 NodeEditorWindow *self)
408{
409 if (self->node == NULL)
410 return NULL;
411
412 return gdk_content_provider_new_typed (GSK_TYPE_RENDER_NODE, self->node);
413}
414
415static void
416on_picture_drop_read_done_cb (GObject *source,
417 GAsyncResult *res,
418 gpointer data)
419{
420 NodeEditorWindow *self = data;
421 GOutputStream *stream = G_OUTPUT_STREAM (source);
422 GdkDrop *drop = g_object_get_data (object: source, key: "drop");
423 GdkDragAction action = 0;
424 GBytes *bytes;
425
426 if (g_output_stream_splice_finish (stream, result: res, NULL) >= 0)
427 {
428 bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (stream));
429 if (load_bytes (self, bytes))
430 action = GDK_ACTION_COPY;
431 }
432
433 g_object_unref (object: self);
434 gdk_drop_finish (self: drop, action);
435 g_object_unref (object: drop);
436 return;
437}
438
439static void
440on_picture_drop_read_cb (GObject *source,
441 GAsyncResult *res,
442 gpointer data)
443{
444 NodeEditorWindow *self = data;
445 GdkDrop *drop = GDK_DROP (source);
446 GInputStream *input;
447 GOutputStream *output;
448
449 input = gdk_drop_read_finish (self: drop, result: res, NULL, NULL);
450 if (input == NULL)
451 {
452 g_object_unref (object: self);
453 gdk_drop_finish (self: drop, action: 0);
454 return;
455 }
456
457 output = g_memory_output_stream_new_resizable ();
458 g_object_set_data (G_OBJECT (output), key: "drop", data: drop);
459 g_object_ref (drop);
460
461 g_output_stream_splice_async (stream: output,
462 source: input,
463 flags: G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
464 G_PRIORITY_DEFAULT,
465 NULL,
466 callback: on_picture_drop_read_done_cb,
467 user_data: self);
468 g_object_unref (object: output);
469 g_object_unref (object: input);
470}
471
472static gboolean
473on_picture_drop_cb (GtkDropTargetAsync *dest,
474 GdkDrop *drop,
475 double x,
476 double y,
477 NodeEditorWindow *self)
478{
479 gdk_drop_read_async (self: drop,
480 mime_types: (const char *[2]) { "application/x-gtk-render-node", NULL },
481 G_PRIORITY_DEFAULT,
482 NULL,
483 callback: on_picture_drop_read_cb,
484 g_object_ref (self));
485
486 return TRUE;
487}
488
489static void
490file_changed_cb (GFileMonitor *monitor,
491 GFile *file,
492 GFile *other_file,
493 GFileMonitorEvent event_type,
494 gpointer user_data)
495{
496 NodeEditorWindow *self = user_data;
497
498 if (event_type == G_FILE_MONITOR_EVENT_CHANGED)
499 load_file_contents (self, file);
500}
501
502gboolean
503node_editor_window_load (NodeEditorWindow *self,
504 GFile *file)
505{
506 GError *error = NULL;
507
508 g_clear_object (&self->file_monitor);
509
510 if (!load_file_contents (self, file))
511 return FALSE;
512
513 self->file_monitor = g_file_monitor_file (file, flags: G_FILE_MONITOR_NONE, NULL, error: &error);
514
515 if (error)
516 {
517 g_warning ("couldn't monitor file: %s", error->message);
518 g_error_free (error);
519 g_clear_object (&self->file_monitor);
520 }
521 else
522 {
523 g_signal_connect (self->file_monitor, "changed", G_CALLBACK (file_changed_cb), self);
524 }
525
526 return TRUE;
527}
528
529static void
530open_response_cb (GtkWidget *dialog,
531 int response,
532 NodeEditorWindow *self)
533{
534 gtk_widget_hide (widget: dialog);
535
536 if (response == GTK_RESPONSE_ACCEPT)
537 {
538 GFile *file;
539
540 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
541 node_editor_window_load (self, file);
542 g_object_unref (object: file);
543 }
544
545 gtk_window_destroy (GTK_WINDOW (dialog));
546}
547
548static void
549show_open_filechooser (NodeEditorWindow *self)
550{
551 GtkWidget *dialog;
552
553 dialog = gtk_file_chooser_dialog_new (title: "Open node file",
554 GTK_WINDOW (self),
555 action: GTK_FILE_CHOOSER_ACTION_OPEN,
556 first_button_text: "_Cancel", GTK_RESPONSE_CANCEL,
557 "_Load", GTK_RESPONSE_ACCEPT,
558 NULL);
559
560 gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id: GTK_RESPONSE_ACCEPT);
561 gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
562
563 GFile *cwd = g_file_new_for_path (path: ".");
564 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), file: cwd, NULL);
565 g_object_unref (object: cwd);
566
567 g_signal_connect (dialog, "response", G_CALLBACK (open_response_cb), self);
568 gtk_widget_show (widget: dialog);
569}
570
571static void
572open_cb (GtkWidget *button,
573 NodeEditorWindow *self)
574{
575 show_open_filechooser (self);
576}
577
578static void
579save_response_cb (GtkWidget *dialog,
580 int response,
581 NodeEditorWindow *self)
582{
583 gtk_widget_hide (widget: dialog);
584
585 if (response == GTK_RESPONSE_ACCEPT)
586 {
587 GFile *file;
588 char *text;
589 GError *error = NULL;
590
591 text = get_current_text (buffer: self->text_buffer);
592
593 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
594 g_file_replace_contents (file, contents: text, length: strlen (s: text),
595 NULL, FALSE,
596 flags: G_FILE_CREATE_NONE,
597 NULL,
598 NULL,
599 error: &error);
600 if (error != NULL)
601 {
602 GtkWidget *message_dialog;
603
604 message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
605 flags: GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
606 type: GTK_MESSAGE_INFO,
607 buttons: GTK_BUTTONS_OK,
608 message_format: "Saving failed");
609 gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message_dialog),
610 message_format: "%s", error->message);
611 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
612 gtk_widget_show (widget: message_dialog);
613 g_error_free (error);
614 }
615
616 g_free (mem: text);
617 g_object_unref (object: file);
618 }
619
620 gtk_window_destroy (GTK_WINDOW (dialog));
621}
622
623static void
624save_cb (GtkWidget *button,
625 NodeEditorWindow *self)
626{
627 GtkWidget *dialog;
628
629 dialog = gtk_file_chooser_dialog_new (title: "Save node",
630 GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
631 action: GTK_FILE_CHOOSER_ACTION_SAVE,
632 first_button_text: "_Cancel", GTK_RESPONSE_CANCEL,
633 "_Save", GTK_RESPONSE_ACCEPT,
634 NULL);
635
636 gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id: GTK_RESPONSE_ACCEPT);
637 gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
638
639 GFile *cwd = g_file_new_for_path (path: ".");
640 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), file: cwd, NULL);
641 g_object_unref (object: cwd);
642
643 g_signal_connect (dialog, "response", G_CALLBACK (save_response_cb), self);
644 gtk_widget_show (widget: dialog);
645}
646
647static GdkTexture *
648create_texture (NodeEditorWindow *self)
649{
650 GdkPaintable *paintable;
651 GtkSnapshot *snapshot;
652 GskRenderer *renderer;
653 GskRenderNode *node;
654 GdkTexture *texture;
655
656 paintable = gtk_picture_get_paintable (self: GTK_PICTURE (ptr: self->picture));
657 if (paintable == NULL ||
658 gdk_paintable_get_intrinsic_width (paintable) <= 0 ||
659 gdk_paintable_get_intrinsic_height (paintable) <= 0)
660 return NULL;
661 snapshot = gtk_snapshot_new ();
662 gdk_paintable_snapshot (paintable, snapshot, width: gdk_paintable_get_intrinsic_width (paintable), height: gdk_paintable_get_intrinsic_height (paintable));
663 node = gtk_snapshot_free_to_node (snapshot);
664 if (node == NULL)
665 return NULL;
666
667 renderer = gtk_native_get_renderer (self: gtk_widget_get_native (GTK_WIDGET (self)));
668 texture = gsk_renderer_render_texture (renderer, root: node, NULL);
669 gsk_render_node_unref (node);
670
671 return texture;
672}
673
674static GdkTexture *
675create_cairo_texture (NodeEditorWindow *self)
676{
677 GdkPaintable *paintable;
678 GtkSnapshot *snapshot;
679 GskRenderer *renderer;
680 GskRenderNode *node;
681 GdkTexture *texture;
682
683 paintable = gtk_picture_get_paintable (self: GTK_PICTURE (ptr: self->picture));
684 if (paintable == NULL ||
685 gdk_paintable_get_intrinsic_width (paintable) <= 0 ||
686 gdk_paintable_get_intrinsic_height (paintable) <= 0)
687 return NULL;
688 snapshot = gtk_snapshot_new ();
689 gdk_paintable_snapshot (paintable, snapshot, width: gdk_paintable_get_intrinsic_width (paintable), height: gdk_paintable_get_intrinsic_height (paintable));
690 node = gtk_snapshot_free_to_node (snapshot);
691 if (node == NULL)
692 return NULL;
693
694 renderer = gsk_cairo_renderer_new ();
695 gsk_renderer_realize (renderer, NULL, NULL);
696
697 texture = gsk_renderer_render_texture (renderer, root: node, NULL);
698 gsk_render_node_unref (node);
699 gsk_renderer_unrealize (renderer);
700 g_object_unref (object: renderer);
701
702 return texture;
703}
704
705static void
706export_image_response_cb (GtkWidget *dialog,
707 int response,
708 GdkTexture *texture)
709{
710 gtk_widget_hide (widget: dialog);
711
712 if (response == GTK_RESPONSE_ACCEPT)
713 {
714 GFile *file;
715
716 file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
717 if (!gdk_texture_save_to_png (texture, filename: g_file_peek_path (file)))
718 {
719 GtkWidget *message_dialog;
720
721 message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_window_get_transient_for (GTK_WINDOW (dialog))),
722 flags: GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
723 type: GTK_MESSAGE_INFO,
724 buttons: GTK_BUTTONS_OK,
725 message_format: "Exporting to image failed");
726 g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
727 gtk_widget_show (widget: message_dialog);
728 }
729
730 g_object_unref (object: file);
731 }
732
733 gtk_window_destroy (GTK_WINDOW (dialog));
734 g_object_unref (object: texture);
735}
736
737static void
738export_image_cb (GtkWidget *button,
739 NodeEditorWindow *self)
740{
741 GdkTexture *texture;
742 GtkWidget *dialog;
743
744 texture = create_texture (self);
745 if (texture == NULL)
746 return;
747
748 dialog = gtk_file_chooser_dialog_new (title: "",
749 GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
750 action: GTK_FILE_CHOOSER_ACTION_SAVE,
751 first_button_text: "_Cancel", GTK_RESPONSE_CANCEL,
752 "_Save", GTK_RESPONSE_ACCEPT,
753 NULL);
754
755 gtk_dialog_set_default_response (GTK_DIALOG (dialog), response_id: GTK_RESPONSE_ACCEPT);
756 gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
757 g_signal_connect (dialog, "response", G_CALLBACK (export_image_response_cb), texture);
758 gtk_widget_show (widget: dialog);
759}
760
761static void
762clip_image_cb (GtkWidget *button,
763 NodeEditorWindow *self)
764{
765 GdkTexture *texture;
766 GdkClipboard *clipboard;
767
768 texture = create_texture (self);
769 if (texture == NULL)
770 return;
771
772 clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self));
773
774 gdk_clipboard_set_texture (clipboard, texture);
775
776 g_object_unref (object: texture);
777}
778
779static void
780testcase_name_entry_changed_cb (GtkWidget *button,
781 GParamSpec *pspec,
782 NodeEditorWindow *self)
783
784{
785 const char *text = gtk_editable_get_text (GTK_EDITABLE (self->testcase_name_entry));
786
787 if (strlen (s: text) > 0)
788 gtk_widget_set_sensitive (widget: self->testcase_save_button, TRUE);
789 else
790 gtk_widget_set_sensitive (widget: self->testcase_save_button, FALSE);
791}
792
793static void
794testcase_save_clicked_cb (GtkWidget *button,
795 NodeEditorWindow *self)
796{
797 const char *testcase_name = gtk_editable_get_text (GTK_EDITABLE (self->testcase_name_entry));
798 char *source_dir = g_canonicalize_filename (NODE_EDITOR_SOURCE_DIR, NULL);
799 char *node_file_name;
800 char *node_file;
801 char *png_file_name;
802 char *png_file;
803 char *text = NULL;
804 GdkTexture *texture;
805 GError *error = NULL;
806
807 node_file_name = g_strconcat (string1: testcase_name, ".node", NULL);
808 node_file = g_build_filename (first_element: source_dir, node_file_name, NULL);
809 g_free (mem: node_file_name);
810
811 png_file_name = g_strconcat (string1: testcase_name, ".png", NULL);
812 png_file = g_build_filename (first_element: source_dir, png_file_name, NULL);
813 g_free (mem: png_file_name);
814
815 if (gtk_check_button_get_active (GTK_CHECK_BUTTON (self->testcase_cairo_checkbutton)))
816 texture = create_cairo_texture (self);
817 else
818 texture = create_texture (self);
819
820 if (!gdk_texture_save_to_png (texture, filename: png_file))
821 {
822 gtk_label_set_label (GTK_LABEL (self->testcase_error_label),
823 str: "Could not save texture file");
824 goto out;
825 }
826
827 text = get_current_text (buffer: self->text_buffer);
828 if (!g_file_set_contents (filename: node_file, contents: text, length: -1, error: &error))
829 {
830 gtk_label_set_label (GTK_LABEL (self->testcase_error_label), str: error->message);
831 /* TODO: Remove texture file again? */
832 goto out;
833 }
834
835 gtk_editable_set_text (GTK_EDITABLE (self->testcase_name_entry), text: "");
836 gtk_popover_popdown (GTK_POPOVER (self->testcase_popover));
837
838out:
839 g_free (mem: text);
840 g_free (mem: png_file);
841 g_free (mem: node_file);
842 g_free (mem: source_dir);
843}
844
845static void
846dark_mode_cb (GtkToggleButton *button,
847 GParamSpec *pspec,
848 NodeEditorWindow *self)
849{
850 g_object_set (object: gtk_widget_get_settings (GTK_WIDGET (self)),
851 first_property_name: "gtk-application-prefer-dark-theme", gtk_toggle_button_get_active (toggle_button: button),
852 NULL);
853}
854
855static void
856node_editor_window_finalize (GObject *object)
857{
858 NodeEditorWindow *self = (NodeEditorWindow *)object;
859
860 g_array_free (array: self->errors, TRUE);
861
862 g_clear_pointer (&self->node, gsk_render_node_unref);
863 g_clear_object (&self->renderers);
864
865 G_OBJECT_CLASS (node_editor_window_parent_class)->finalize (object);
866}
867
868static void
869node_editor_window_add_renderer (NodeEditorWindow *self,
870 GskRenderer *renderer,
871 const char *description)
872{
873 GdkPaintable *paintable;
874
875 if (!gsk_renderer_realize (renderer, NULL, NULL))
876 {
877 GdkSurface *surface = gtk_native_get_surface (self: GTK_NATIVE (ptr: self));
878 g_assert (surface != NULL);
879
880 if (!gsk_renderer_realize (renderer, surface, NULL))
881 {
882 g_object_unref (object: renderer);
883 return;
884 }
885 }
886
887 paintable = gtk_renderer_paintable_new (renderer, paintable: gtk_picture_get_paintable (self: GTK_PICTURE (ptr: self->picture)));
888 g_object_set_data_full (G_OBJECT (paintable), key: "description", data: g_strdup (str: description), destroy: g_free);
889 g_clear_object (&renderer);
890
891 g_list_store_append (store: self->renderers, item: paintable);
892 g_object_unref (object: paintable);
893}
894
895static void
896node_editor_window_realize (GtkWidget *widget)
897{
898 NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
899
900 GTK_WIDGET_CLASS (node_editor_window_parent_class)->realize (widget);
901
902#if 0
903 node_editor_window_add_renderer (self,
904 NULL,
905 "Default");
906#endif
907 node_editor_window_add_renderer (self,
908 renderer: gsk_gl_renderer_new (),
909 description: "OpenGL");
910#ifdef GDK_RENDERING_VULKAN
911 node_editor_window_add_renderer (self,
912 gsk_vulkan_renderer_new (),
913 "Vulkan");
914#endif
915#ifdef GDK_WINDOWING_BROADWAY
916 node_editor_window_add_renderer (self,
917 gsk_broadway_renderer_new (),
918 "Broadway");
919#endif
920 node_editor_window_add_renderer (self,
921 renderer: gsk_cairo_renderer_new (),
922 description: "Cairo");
923}
924
925static void
926node_editor_window_unrealize (GtkWidget *widget)
927{
928 NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
929 guint i;
930
931 for (i = 0; i < g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->renderers)); i ++)
932 {
933 gpointer item = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->renderers), position: i);
934 gsk_renderer_unrealize (renderer: gtk_renderer_paintable_get_renderer (self: item));
935 g_object_unref (object: item);
936 }
937
938 g_list_store_remove_all (store: self->renderers);
939
940 GTK_WIDGET_CLASS (node_editor_window_parent_class)->unrealize (widget);
941}
942
943static void
944node_editor_window_class_init (NodeEditorWindowClass *class)
945{
946 GObjectClass *object_class = G_OBJECT_CLASS (class);
947 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
948
949 object_class->finalize = node_editor_window_finalize;
950
951 gtk_widget_class_set_template_from_resource (widget_class,
952 resource_name: "/org/gtk/gtk4/node-editor/node-editor-window.ui");
953
954 widget_class->realize = node_editor_window_realize;
955 widget_class->unrealize = node_editor_window_unrealize;
956
957 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, text_view);
958 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, picture);
959 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, renderer_listbox);
960 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_popover);
961 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_error_label);
962 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_cairo_checkbutton);
963 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_name_entry);
964 gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_save_button);
965
966 gtk_widget_class_bind_template_callback (widget_class, text_view_query_tooltip_cb);
967 gtk_widget_class_bind_template_callback (widget_class, open_cb);
968 gtk_widget_class_bind_template_callback (widget_class, save_cb);
969 gtk_widget_class_bind_template_callback (widget_class, export_image_cb);
970 gtk_widget_class_bind_template_callback (widget_class, clip_image_cb);
971 gtk_widget_class_bind_template_callback (widget_class, testcase_save_clicked_cb);
972 gtk_widget_class_bind_template_callback (widget_class, testcase_name_entry_changed_cb);
973 gtk_widget_class_bind_template_callback (widget_class, dark_mode_cb);
974 gtk_widget_class_bind_template_callback (widget_class, on_picture_drag_prepare_cb);
975 gtk_widget_class_bind_template_callback (widget_class, on_picture_drop_cb);
976}
977
978static GtkWidget *
979node_editor_window_create_renderer_widget (gpointer item,
980 gpointer user_data)
981{
982 GdkPaintable *paintable = item;
983 GtkWidget *box, *label, *picture;
984 GtkWidget *row;
985
986 box = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 0);
987 gtk_widget_set_size_request (widget: box, width: 120, height: 90);
988
989 label = gtk_label_new (str: g_object_get_data (G_OBJECT (paintable), key: "description"));
990 gtk_widget_add_css_class (widget: label, css_class: "title-4");
991 gtk_box_append (GTK_BOX (box), child: label);
992
993 picture = gtk_picture_new_for_paintable (paintable);
994 /* don't ever scale up, we want to be as accurate as possible */
995 gtk_widget_set_halign (widget: picture, align: GTK_ALIGN_CENTER);
996 gtk_widget_set_valign (widget: picture, align: GTK_ALIGN_CENTER);
997 gtk_box_append (GTK_BOX (box), child: picture);
998
999 row = gtk_list_box_row_new ();
1000 gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), child: box);
1001 gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
1002
1003 return row;
1004}
1005
1006static void
1007window_open (GSimpleAction *action,
1008 GVariant *parameter,
1009 gpointer user_data)
1010{
1011 NodeEditorWindow *self = user_data;
1012
1013 show_open_filechooser (self);
1014}
1015
1016static GActionEntry win_entries[] = {
1017 { "open", window_open, NULL, NULL, NULL },
1018};
1019
1020static void
1021node_editor_window_init (NodeEditorWindow *self)
1022{
1023 gtk_widget_init_template (GTK_WIDGET (self));
1024
1025 self->renderers = g_list_store_new (GDK_TYPE_PAINTABLE);
1026 gtk_list_box_bind_model (GTK_LIST_BOX (self->renderer_listbox),
1027 model: G_LIST_MODEL (ptr: self->renderers),
1028 create_widget_func: node_editor_window_create_renderer_widget,
1029 user_data: self,
1030 NULL);
1031
1032 self->errors = g_array_new (FALSE, TRUE, element_size: sizeof (TextViewError));
1033 g_array_set_clear_func (array: self->errors, clear_func: (GDestroyNotify)text_view_error_free);
1034
1035 g_action_map_add_action_entries (G_ACTION_MAP (self), entries: win_entries, G_N_ELEMENTS (win_entries), user_data: self);
1036
1037 self->tag_table = gtk_text_tag_table_new ();
1038 gtk_text_tag_table_add (table: self->tag_table,
1039 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1040 first_property_name: "name", "error",
1041 "underline", PANGO_UNDERLINE_ERROR,
1042 NULL));
1043 gtk_text_tag_table_add (table: self->tag_table,
1044 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1045 first_property_name: "name", "nodename",
1046 "foreground-rgba", &(GdkRGBA) { 0.9, 0.78, 0.53, 1},
1047 NULL));
1048 gtk_text_tag_table_add (table: self->tag_table,
1049 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1050 first_property_name: "name", "propname",
1051 "foreground-rgba", &(GdkRGBA) { 0.7, 0.55, 0.67, 1},
1052 NULL));
1053 gtk_text_tag_table_add (table: self->tag_table,
1054 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1055 first_property_name: "name", "string",
1056 "foreground-rgba", &(GdkRGBA) { 0.63, 0.73, 0.54, 1},
1057 NULL));
1058 gtk_text_tag_table_add (table: self->tag_table,
1059 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1060 first_property_name: "name", "number",
1061 "foreground-rgba", &(GdkRGBA) { 0.8, 0.52, 0.43, 1},
1062 NULL));
1063 gtk_text_tag_table_add (table: self->tag_table,
1064 tag: g_object_new (GTK_TYPE_TEXT_TAG,
1065 first_property_name: "name", "no-hyphens",
1066 "insert-hyphens", FALSE,
1067 NULL));
1068
1069 self->text_buffer = gtk_text_buffer_new (table: self->tag_table);
1070 g_signal_connect (self->text_buffer, "changed", G_CALLBACK (text_changed), self);
1071 gtk_text_view_set_buffer (GTK_TEXT_VIEW (self->text_view), buffer: self->text_buffer);
1072
1073 /* Default */
1074 gtk_text_buffer_set_text (buffer: self->text_buffer,
1075 text: "shadow {\n"
1076 " child: texture {\n"
1077 " bounds: 0 0 128 128;\n"
1078 " texture: url(\"resource:///org/gtk/gtk4/node-editor/icons/apps/org.gtk.gtk4.NodeEditor.svg\");\n"
1079 " }\n"
1080 " shadows: rgba(0,0,0,0.5) 0 1 12;\n"
1081 "}\n"
1082 "\n"
1083 "transform {\n"
1084 " child: text {\n"
1085 " color: rgb(46,52,54);\n"
1086 " font: \"Cantarell Bold 11\";\n"
1087 " glyphs: \"GTK Node Editor\";\n"
1088 " offset: 8 14.418;\n"
1089 " }\n"
1090 " transform: translate(0, 140);\n"
1091 "}", len: -1);
1092}
1093
1094NodeEditorWindow *
1095node_editor_window_new (NodeEditorApplication *application)
1096{
1097 return g_object_new (NODE_EDITOR_WINDOW_TYPE,
1098 first_property_name: "application", application,
1099 NULL);
1100}
1101

source code of gtk/demos/node-editor/node-editor-window.c