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 | |
39 | typedef struct |
40 | { |
41 | gsize start_chars; |
42 | gsize end_chars; |
43 | char *message; |
44 | } TextViewError; |
45 | |
46 | struct _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 | |
70 | struct _NodeEditorWindowClass |
71 | { |
72 | GtkApplicationWindowClass parent_class; |
73 | }; |
74 | |
75 | G_DEFINE_TYPE(NodeEditorWindow, node_editor_window, GTK_TYPE_APPLICATION_WINDOW); |
76 | |
77 | static void |
78 | text_view_error_free (TextViewError *e) |
79 | { |
80 | g_free (mem: e->message); |
81 | } |
82 | |
83 | static char * |
84 | get_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 | |
94 | static void |
95 | text_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 | |
104 | static void |
105 | deserialize_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 | |
130 | static void |
131 | text_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 | |
149 | static void |
150 | text_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 | |
166 | static void |
167 | text_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 | |
280 | static gboolean |
281 | text_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 | |
339 | static gboolean |
340 | load_bytes (NodeEditorWindow *self, |
341 | GBytes *bytes); |
342 | |
343 | static void |
344 | load_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 | |
365 | static gboolean |
366 | load_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 | |
385 | static gboolean |
386 | load_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 | |
403 | static GdkContentProvider * |
404 | on_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 | |
415 | static void |
416 | on_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 | |
439 | static void |
440 | on_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 | |
472 | static gboolean |
473 | on_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 | |
489 | static void |
490 | file_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 | |
502 | gboolean |
503 | node_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 | |
529 | static void |
530 | open_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 | |
548 | static void |
549 | show_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 | |
571 | static void |
572 | open_cb (GtkWidget *button, |
573 | NodeEditorWindow *self) |
574 | { |
575 | show_open_filechooser (self); |
576 | } |
577 | |
578 | static void |
579 | save_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 | |
623 | static void |
624 | save_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 | |
647 | static GdkTexture * |
648 | create_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 | |
674 | static GdkTexture * |
675 | create_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 | |
705 | static void |
706 | export_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 | |
737 | static void |
738 | export_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 | |
761 | static void |
762 | clip_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 | |
779 | static void |
780 | testcase_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 | |
793 | static void |
794 | testcase_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 | |
838 | out: |
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 | |
845 | static void |
846 | dark_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 | |
855 | static void |
856 | node_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 | |
868 | static void |
869 | node_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 | |
895 | static void |
896 | node_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 | |
925 | static void |
926 | node_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 | |
943 | static void |
944 | node_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 | |
978 | static GtkWidget * |
979 | node_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 | |
1006 | static void |
1007 | window_open (GSimpleAction *action, |
1008 | GVariant *parameter, |
1009 | gpointer user_data) |
1010 | { |
1011 | NodeEditorWindow *self = user_data; |
1012 | |
1013 | show_open_filechooser (self); |
1014 | } |
1015 | |
1016 | static GActionEntry win_entries[] = { |
1017 | { "open" , window_open, NULL, NULL, NULL }, |
1018 | }; |
1019 | |
1020 | static void |
1021 | node_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 | |
1094 | NodeEditorWindow * |
1095 | node_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 | |