diff --git a/demos/meson.build b/demos/meson.build index e45a7c8b4a..6f1905ddb6 100644 --- a/demos/meson.build +++ b/demos/meson.build @@ -1,3 +1,4 @@ subdir('gtk-demo') subdir('icon-browser') +subdir('node-editor') subdir('widget-factory') diff --git a/demos/node-editor/main.c b/demos/node-editor/main.c new file mode 100644 index 0000000000..8aec52a759 --- /dev/null +++ b/demos/node-editor/main.c @@ -0,0 +1,28 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include + +int +main (int argc, char *argv[]) +{ + return g_application_run (G_APPLICATION (node_editor_application_new ()), argc, argv); +} diff --git a/demos/node-editor/meson.build b/demos/node-editor/meson.build new file mode 100644 index 0000000000..b12bc326e6 --- /dev/null +++ b/demos/node-editor/meson.build @@ -0,0 +1,17 @@ +node_editor_sources = [ + 'main.c', + 'node-editor-application.c', + 'node-editor-window.c', +] + +node_editor_resources = gnome.compile_resources('node_editor_resources', + 'node-editor.gresource.xml', + source_dir: '.') + +executable('gtk4-node-editor', + node_editor_sources, node_editor_resources, + dependencies: libgtk_dep, + include_directories: confinc, + gui_app: true, + link_args: extra_demo_ldflags, + install: false) diff --git a/demos/node-editor/node-editor-application.c b/demos/node-editor/node-editor-application.c new file mode 100644 index 0000000000..9455cf3bc3 --- /dev/null +++ b/demos/node-editor/node-editor-application.c @@ -0,0 +1,114 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "node-editor-application.h" + +#include "node-editor-window.h" + +struct _NodeEditorApplication +{ + GtkApplication parent; +}; + +struct _NodeEditorApplicationClass +{ + GtkApplicationClass parent_class; +}; + +G_DEFINE_TYPE(NodeEditorApplication, node_editor_application, GTK_TYPE_APPLICATION); + +static void +node_editor_application_init (NodeEditorApplication *app) +{ +} + +static void +quit_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + g_application_quit (G_APPLICATION (data)); +} + +static GActionEntry app_entries[] = +{ + { "quit", quit_activated, NULL, NULL, NULL } +}; + +static void +node_editor_application_startup (GApplication *app) +{ + const gchar *quit_accels[2] = { "Q", NULL }; + + G_APPLICATION_CLASS (node_editor_application_parent_class)->startup (app); + + g_action_map_add_action_entries (G_ACTION_MAP (app), + app_entries, G_N_ELEMENTS (app_entries), + app); + gtk_application_set_accels_for_action (GTK_APPLICATION (app), + "app.quit", + quit_accels); +} + +static void +node_editor_application_activate (GApplication *app) +{ + NodeEditorWindow *win; + + win = node_editor_window_new (NODE_EDITOR_APPLICATION (app)); + gtk_window_present (GTK_WINDOW (win)); +} + +static void +node_editor_application_open (GApplication *app, + GFile **files, + gint n_files, + const gchar *hint) +{ + NodeEditorWindow *win; + gint i; + + for (i = 0; i < n_files; i++) + { + win = node_editor_window_new (NODE_EDITOR_APPLICATION (app)); + node_editor_window_load (win, files[i]); + gtk_window_present (GTK_WINDOW (win)); + } +} + +static void +node_editor_application_class_init (NodeEditorApplicationClass *class) +{ + GApplicationClass *application_class = G_APPLICATION_CLASS (class); + + application_class->startup = node_editor_application_startup; + application_class->activate = node_editor_application_activate; + application_class->open = node_editor_application_open; +} + +NodeEditorApplication * +node_editor_application_new (void) +{ + return g_object_new (NODE_EDITOR_APPLICATION_TYPE, + "application-id", "org.gtk.gtk4.NodeEditor", + "flags", G_APPLICATION_HANDLES_OPEN, + NULL); +} diff --git a/demos/node-editor/node-editor-application.h b/demos/node-editor/node-editor-application.h new file mode 100644 index 0000000000..90fbb24cb6 --- /dev/null +++ b/demos/node-editor/node-editor-application.h @@ -0,0 +1,38 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __NODE_EDITOR_APPLICATION_H__ +#define __NODE_EDITOR_APPLICATION_H__ + +#include + + +#define NODE_EDITOR_APPLICATION_TYPE (node_editor_application_get_type ()) +#define NODE_EDITOR_APPLICATION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NODE_EDITOR_APPLICATION_TYPE, NodeEditorApplication)) + + +typedef struct _NodeEditorApplication NodeEditorApplication; +typedef struct _NodeEditorApplicationClass NodeEditorApplicationClass; + + +GType node_editor_application_get_type (void); +NodeEditorApplication *node_editor_application_new (void); + + +#endif /* __NODE_EDITOR_APPLICATION_H__ */ diff --git a/demos/node-editor/node-editor-window.c b/demos/node-editor/node-editor-window.c new file mode 100644 index 0000000000..ac276dd3f1 --- /dev/null +++ b/demos/node-editor/node-editor-window.c @@ -0,0 +1,431 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "node-editor-window.h" + +#include "gsk/gskrendernodeparserprivate.h" + +typedef struct +{ + gsize start_chars; + gsize end_chars; + char *message; +} TextViewError; + +struct _NodeEditorWindow +{ + GtkApplicationWindow parent; + + GtkWidget *picture; + GtkWidget *text_view; + GtkTextBuffer *text_buffer; + + GArray *errors; +}; + +struct _NodeEditorWindowClass +{ + GtkApplicationWindowClass parent_class; +}; + +G_DEFINE_TYPE(NodeEditorWindow, node_editor_window, GTK_TYPE_APPLICATION_WINDOW); + +static void +text_view_error_free (TextViewError *e) +{ + g_free (e->message); +} + +static gchar * +get_current_text (GtkTextBuffer *buffer) +{ + GtkTextIter start, end; + + gtk_text_buffer_get_start_iter (buffer, &start); + gtk_text_buffer_get_end_iter (buffer, &end); + gtk_text_buffer_remove_all_tags (buffer, &start, &end); + + return gtk_text_buffer_get_text (buffer, &start, &end, FALSE); +} + +static void +deserialize_error_func (const GtkCssSection *section, + const GError *error, + gpointer user_data) +{ + const GtkCssLocation *start_location = gtk_css_section_get_start_location (section); + const GtkCssLocation *end_location = gtk_css_section_get_end_location (section); + NodeEditorWindow *self = user_data; + GtkTextIter start_iter, end_iter; + TextViewError text_view_error; + + gtk_text_buffer_get_iter_at_line_offset (self->text_buffer, &start_iter, + start_location->lines, + start_location->line_chars); + gtk_text_buffer_get_iter_at_line_offset (self->text_buffer, &end_iter, + end_location->lines, + end_location->line_chars); + + gtk_text_buffer_apply_tag_by_name (self->text_buffer, "error", + &start_iter, &end_iter); + + text_view_error.start_chars = start_location->chars; + text_view_error.end_chars = end_location->chars; + text_view_error.message = g_strdup (error->message); + g_array_append_val (self->errors, text_view_error); +} + +static void +text_changed (GtkTextBuffer *buffer, + NodeEditorWindow *self) +{ + GskRenderNode *node; + char *text; + GBytes *bytes; + + g_array_remove_range (self->errors, 0, self->errors->len); + text = get_current_text (self->text_buffer); + bytes = g_bytes_new_take (text, strlen (text)); + + /* If this is too slow, go fix the parser performance */ + node = gsk_render_node_deserialize (bytes, deserialize_error_func, self); + g_bytes_unref (bytes); + if (node) + { + /* XXX: Is this code necessary or can we have API to turn nodes into paintables? */ + GtkSnapshot *snapshot; + GdkPaintable *paintable; + graphene_rect_t bounds; + + snapshot = gtk_snapshot_new (); + gsk_render_node_get_bounds (node, &bounds); + gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- bounds.origin.x, - bounds.origin.y)); + gtk_snapshot_append_node (snapshot, node); + gsk_render_node_unref (node); + paintable = gtk_snapshot_free_to_paintable (snapshot, &bounds.size); + gtk_picture_set_paintable (GTK_PICTURE (self->picture), paintable); + g_clear_object (&paintable); + } + else + { + gtk_picture_set_paintable (GTK_PICTURE (self->picture), NULL); + } +} + +static gboolean +text_view_query_tooltip_cb (GtkWidget *widget, + int x, + int y, + gboolean keyboard_tip, + GtkTooltip *tooltip, + NodeEditorWindow *self) +{ + GtkTextIter iter; + guint i; + + if (keyboard_tip) + { + gint offset; + + g_object_get (self->text_buffer, "cursor-position", &offset, NULL); + gtk_text_buffer_get_iter_at_offset (self->text_buffer, &iter, offset); + } + else + { + gint bx, by, trailing; + + gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (self->text_view), GTK_TEXT_WINDOW_TEXT, + x, y, &bx, &by); + gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (self->text_view), &iter, &trailing, bx, by); + } + + for (i = 0; i < self->errors->len; i ++) + { + const TextViewError *e = &g_array_index (self->errors, TextViewError, i); + GtkTextIter start_iter, end_iter; + + gtk_text_buffer_get_iter_at_offset (self->text_buffer, &start_iter, e->start_chars); + gtk_text_buffer_get_iter_at_offset (self->text_buffer, &end_iter, e->end_chars); + + if (gtk_text_iter_in_range (&iter, &start_iter, &end_iter)) + { + gtk_tooltip_set_text (tooltip, e->message); + return TRUE; + } + } + + return FALSE; +} + +gboolean +node_editor_window_load (NodeEditorWindow *self, + GFile *file) +{ + GtkTextIter end; + GBytes *bytes; + + bytes = g_file_load_bytes (file, NULL, NULL, NULL); + if (bytes == NULL) + return FALSE; + + if (!g_utf8_validate (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL)) + { + g_bytes_unref (bytes); + return FALSE; + } + + gtk_text_buffer_get_end_iter (self->text_buffer, &end); + gtk_text_buffer_insert (self->text_buffer, + &end, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes)); + + return TRUE; +} + +static void +open_response_cb (GtkWidget *dialog, + gint response, + NodeEditorWindow *self) +{ + gtk_widget_hide (dialog); + + if (response == GTK_RESPONSE_ACCEPT) + { + GFile *file; + + file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); + node_editor_window_load (self, file); + g_object_unref (file); + } + + gtk_widget_destroy (dialog); +} + +static void +open_cb (GtkWidget *button, + NodeEditorWindow *self) +{ + GtkWidget *dialog; + + dialog = gtk_file_chooser_dialog_new ("Open node file", + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button))), + GTK_FILE_CHOOSER_ACTION_OPEN, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Load", GTK_RESPONSE_ACCEPT, + NULL); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), "."); + g_signal_connect (dialog, "response", G_CALLBACK (open_response_cb), self); + gtk_widget_show (dialog); +} + +static void +save_response_cb (GtkWidget *dialog, + gint response, + NodeEditorWindow *self) +{ + gtk_widget_hide (dialog); + + if (response == GTK_RESPONSE_ACCEPT) + { + char *text, *filename; + GError *error = NULL; + + text = get_current_text (self->text_buffer); + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); + if (!g_file_set_contents (filename, text, -1, &error)) + { + GtkWidget *dialog; + + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (self))), + GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "Saving failed"); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", error->message); + g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); + gtk_widget_show (dialog); + g_error_free (error); + } + g_free (filename); + } + + gtk_widget_destroy (dialog); +} + +static void +save_cb (GtkWidget *button, + NodeEditorWindow *self) +{ + GtkWidget *dialog; + + dialog = gtk_file_chooser_dialog_new ("Save node", + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button))), + GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Save", GTK_RESPONSE_ACCEPT, + NULL); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE); + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), "."); + g_signal_connect (dialog, "response", G_CALLBACK (save_response_cb), self); + gtk_widget_show (dialog); +} + +static GdkTexture * +create_texture (NodeEditorWindow *self) +{ + GdkPaintable *paintable; + GtkSnapshot *snapshot; + GskRenderer *renderer; + GskRenderNode *node; + GdkTexture *texture; + + paintable = gtk_picture_get_paintable (GTK_PICTURE (self->picture)); + if (paintable == NULL || + gdk_paintable_get_intrinsic_width (paintable) <= 0 || + gdk_paintable_get_intrinsic_height (paintable) <= 0) + return NULL; + snapshot = gtk_snapshot_new (); + gdk_paintable_snapshot (paintable, snapshot, gdk_paintable_get_intrinsic_width (paintable), gdk_paintable_get_intrinsic_height (paintable)); + node = gtk_snapshot_free_to_node (snapshot); + if (node == NULL) + return NULL; + + /* ahem */ + renderer = GTK_ROOT_GET_IFACE (gtk_widget_get_root (GTK_WIDGET (self)))->get_renderer (gtk_widget_get_root (GTK_WIDGET (self))); + texture = gsk_renderer_render_texture (renderer, node, NULL); + gsk_render_node_unref (node); + + return texture; +} + +static void +export_image_response_cb (GtkWidget *dialog, + gint response, + GdkTexture *texture) +{ + gtk_widget_hide (dialog); + + if (response == GTK_RESPONSE_ACCEPT) + { + char *filename; + + filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); + if (!gdk_texture_save_to_png (texture, filename)) + { + GtkWidget *message_dialog; + + message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_window_get_transient_for (GTK_WINDOW (dialog))), + GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "Exporting to image failed"); + g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); + gtk_widget_show (message_dialog); + } + g_free (filename); + } + + gtk_widget_destroy (dialog); + g_object_unref (texture); +} + +static void +export_image_cb (GtkWidget *button, + NodeEditorWindow *self) +{ + GdkTexture *texture; + GtkWidget *dialog; + + texture = create_texture (self); + if (texture == NULL) + return; + + dialog = gtk_file_chooser_dialog_new ("", + GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (button))), + GTK_FILE_CHOOSER_ACTION_SAVE, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Save", GTK_RESPONSE_ACCEPT, + NULL); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE); + g_signal_connect (dialog, "response", G_CALLBACK (export_image_response_cb), texture); + gtk_widget_show (dialog); +} + +static void +node_editor_window_finalize (GObject *object) +{ + NodeEditorWindow *self = (NodeEditorWindow *)object; + + g_array_free (self->errors, TRUE); + + G_OBJECT_CLASS (node_editor_window_parent_class)->finalize (object); +} + +static void +node_editor_window_class_init (NodeEditorWindowClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->finalize = node_editor_window_finalize; + + gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class), + "/org/gtk/gtk4/node-editor/node-editor-window.ui"); + + gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, text_buffer); + gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, text_view); + gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, picture); + + gtk_widget_class_bind_template_callback (widget_class, text_changed); + gtk_widget_class_bind_template_callback (widget_class, text_view_query_tooltip_cb); + gtk_widget_class_bind_template_callback (widget_class, open_cb); + gtk_widget_class_bind_template_callback (widget_class, save_cb); + gtk_widget_class_bind_template_callback (widget_class, export_image_cb); +} + +static void +node_editor_window_init (NodeEditorWindow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->errors = g_array_new (FALSE, TRUE, sizeof (TextViewError)); + g_array_set_clear_func (self->errors, (GDestroyNotify)text_view_error_free); +} + +NodeEditorWindow * +node_editor_window_new (NodeEditorApplication *application) +{ + return g_object_new (NODE_EDITOR_WINDOW_TYPE, + "application", application, + NULL); +} diff --git a/demos/node-editor/node-editor-window.h b/demos/node-editor/node-editor-window.h new file mode 100644 index 0000000000..9eaf6aed3f --- /dev/null +++ b/demos/node-editor/node-editor-window.h @@ -0,0 +1,42 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __NODE_EDITOR_WINDOW_H__ +#define __NODE_EDITOR_WINDOW_H__ + +#include + +#include "node-editor-application.h" + +#define NODE_EDITOR_WINDOW_TYPE (node_editor_window_get_type ()) +#define NODE_EDITOR_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), NODE_EDITOR_WINDOW_TYPE, NodeEditorWindow)) + + +typedef struct _NodeEditorWindow NodeEditorWindow; +typedef struct _NodeEditorWindowClass NodeEditorWindowClass; + + +GType node_editor_window_get_type (void); + +NodeEditorWindow * node_editor_window_new (NodeEditorApplication *application); + +gboolean node_editor_window_load (NodeEditorWindow *self, + GFile *file); + +#endif /* __NODE_EDITOR_WINDOW_H__ */ diff --git a/demos/node-editor/node-editor-window.ui b/demos/node-editor/node-editor-window.ui new file mode 100644 index 0000000000..e886b53ee0 --- /dev/null +++ b/demos/node-editor/node-editor-window.ui @@ -0,0 +1,99 @@ + + + + + + error + error + + + + + tags + + + + diff --git a/demos/node-editor/node-editor.gresource.xml b/demos/node-editor/node-editor.gresource.xml new file mode 100644 index 0000000000..ca6cdd2c1b --- /dev/null +++ b/demos/node-editor/node-editor.gresource.xml @@ -0,0 +1,6 @@ + + + + node-editor-window.ui + +