/* * Copyright © 2019 Red Hat, Inc. * * 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: Matthias Clasen */ #include "config.h" #include "constraint-editor-window.h" #include "constraint-view.h" #include "constraint-editor.h" #include "guide-editor.h" struct _ConstraintEditorWindow { GtkApplicationWindow parent_instance; GtkWidget *paned; GtkWidget *view; GtkWidget *list; }; G_DEFINE_TYPE(ConstraintEditorWindow, constraint_editor_window, GTK_TYPE_APPLICATION_WINDOW); static GtkConstraintTarget * find_target (GListModel *model, GtkConstraintTarget *orig) { const char *name; const char *model_name; gpointer item; int i; if (orig == NULL) return NULL; if (GTK_IS_LABEL (orig)) name = gtk_label_get_label (GTK_LABEL (orig)); else if (GTK_IS_CONSTRAINT_GUIDE (orig)) name = gtk_constraint_guide_get_name (GTK_CONSTRAINT_GUIDE (orig)); else { g_warning ("Don't know how to handle %s targets", G_OBJECT_TYPE_NAME (orig)); return NULL; } for (i = 0; i < g_list_model_get_n_items (model); i++) { item = g_list_model_get_item (model, i); g_object_unref (item); if (GTK_IS_WIDGET (item)) model_name = gtk_widget_get_name (GTK_WIDGET (item)); else model_name = gtk_constraint_guide_get_name (GTK_CONSTRAINT_GUIDE (item)); if (strcmp (name, model_name) == 0) return GTK_CONSTRAINT_TARGET (item); } g_warning ("Failed to find target '%s'", name); return NULL; } gboolean constraint_editor_window_load (ConstraintEditorWindow *self, GFile *file) { char *path; GtkBuilder *builder; GError *error = NULL; GtkWidget *view; GtkLayoutManager *layout; GtkWidget *child; const char *name; gpointer item; int i; GListModel *list; path = g_file_get_path (file); builder = gtk_builder_new (); if (!gtk_builder_add_from_file (builder, path, &error)) { g_print ("Could not load %s: %s", path, error->message); g_error_free (error); g_free (path); g_object_unref (builder); return FALSE; } view = GTK_WIDGET (gtk_builder_get_object (builder, "view")); if (!GTK_IS_BOX (view)) { g_print ("Could not load %s: No GtkBox named 'view'", path); g_free (path); g_object_unref (builder); return FALSE; } layout = gtk_widget_get_layout_manager (view); if (!GTK_IS_CONSTRAINT_LAYOUT (layout)) { g_print ("Could not load %s: Widget 'view' does not use GtkConstraintLayout", path); g_free (path); g_object_unref (builder); return FALSE; } for (child = gtk_widget_get_first_child (view); child; child = gtk_widget_get_next_sibling (child)) { if (!GTK_IS_LABEL (child)) { g_print ("Skipping non-GtkLabel child\n"); continue; } name = gtk_label_get_label (GTK_LABEL (child)); constraint_view_add_child (CONSTRAINT_VIEW (self->view), name); } list = gtk_constraint_layout_observe_guides (GTK_CONSTRAINT_LAYOUT (layout)); for (i = 0; i < g_list_model_get_n_items (list); i++) { GtkConstraintGuide *guide, *clone; int w, h; item = g_list_model_get_item (list, i); guide = GTK_CONSTRAINT_GUIDE (item); /* need to clone here, to attach to the right targets */ clone = gtk_constraint_guide_new (); gtk_constraint_guide_set_name (clone, gtk_constraint_guide_get_name (guide)); gtk_constraint_guide_set_strength (clone, gtk_constraint_guide_get_strength (guide)); gtk_constraint_guide_get_min_size (guide, &w, &h); gtk_constraint_guide_set_min_size (clone, w, h); gtk_constraint_guide_get_nat_size (guide, &w, &h); gtk_constraint_guide_set_nat_size (clone, w, h); gtk_constraint_guide_get_max_size (guide, &w, &h); gtk_constraint_guide_set_max_size (clone, w, h); constraint_view_add_guide (CONSTRAINT_VIEW (self->view), clone); g_object_unref (guide); g_object_unref (clone); } g_object_unref (list); list = gtk_constraint_layout_observe_constraints (GTK_CONSTRAINT_LAYOUT (layout)); for (i = 0; i < g_list_model_get_n_items (list); i++) { GtkConstraint *constraint; GtkConstraint *clone; GtkConstraintTarget *target; GtkConstraintTarget *source; GtkConstraintAttribute source_attr; item = g_list_model_get_item (list, i); constraint = GTK_CONSTRAINT (item); target = gtk_constraint_get_target (constraint); source = gtk_constraint_get_source (constraint); source_attr = gtk_constraint_get_source_attribute (constraint); if (source == NULL && source_attr == GTK_CONSTRAINT_ATTRIBUTE_NONE) clone = gtk_constraint_new_constant (find_target (constraint_view_get_model (CONSTRAINT_VIEW (self->view)), target), gtk_constraint_get_target_attribute (constraint), gtk_constraint_get_relation (constraint), gtk_constraint_get_constant (constraint), gtk_constraint_get_strength (constraint)); else clone = gtk_constraint_new (find_target (constraint_view_get_model (CONSTRAINT_VIEW (self->view)), target), gtk_constraint_get_target_attribute (constraint), gtk_constraint_get_relation (constraint), find_target (constraint_view_get_model (CONSTRAINT_VIEW (self->view)), source), source_attr, gtk_constraint_get_multiplier (constraint), gtk_constraint_get_constant (constraint), gtk_constraint_get_strength (constraint)); constraint_view_add_constraint (CONSTRAINT_VIEW (self->view), clone); g_object_unref (constraint); g_object_unref (clone); } g_object_unref (list); g_free (path); g_object_unref (builder); return TRUE; } static void open_response_cb (GtkNativeDialog *dialog, int response, ConstraintEditorWindow *self) { gtk_native_dialog_hide (dialog); if (response == GTK_RESPONSE_ACCEPT) { GFile *file; file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); constraint_editor_window_load (self, file); g_object_unref (file); } gtk_native_dialog_destroy (dialog); } static void open_cb (GtkWidget *button, ConstraintEditorWindow *self) { GtkFileChooserNative *dialog; dialog = gtk_file_chooser_native_new ("Open file", GTK_WINDOW (self), GTK_FILE_CHOOSER_ACTION_OPEN, "_Load", "_Cancel"); gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (dialog), TRUE); GFile *cwd = g_file_new_for_path ("."); gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd, NULL); g_object_unref (cwd); g_signal_connect (dialog, "response", G_CALLBACK (open_response_cb), self); gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog)); } static void serialize_child (GString *str, int indent, GtkWidget *child) { const char *name; name = gtk_widget_get_name (child); g_string_append_printf (str, "%*s\n", indent, ""); g_string_append_printf (str, "%*s \n", indent, "", name); g_string_append_printf (str, "%*s %s\n", indent, "", name); g_string_append_printf (str, "%*s \n", indent, ""); g_string_append_printf (str, "%*s\n", indent, ""); } static char * serialize_model (GListModel *list) { GString *str = g_string_new (""); int i; g_string_append (str, "\n"); g_string_append (str, " \n"); g_string_append (str, " \n"); g_string_append (str, " \n"); g_string_append (str, " \n"); for (i = 0; i < g_list_model_get_n_items (list); i++) { gpointer item = g_list_model_get_item (list, i); g_object_unref (item); if (GTK_IS_CONSTRAINT (item)) constraint_editor_serialize_constraint (str, 10, GTK_CONSTRAINT (item)); else if (GTK_IS_CONSTRAINT_GUIDE (item)) guide_editor_serialize_guide (str, 10, GTK_CONSTRAINT_GUIDE (item)); } g_string_append (str, " \n"); g_string_append (str, " \n"); g_string_append (str, " \n"); for (i = 0; i < g_list_model_get_n_items (list); i++) { gpointer item = g_list_model_get_item (list, i); g_object_unref (item); if (GTK_IS_WIDGET (item)) serialize_child (str, 4, GTK_WIDGET (item)); } g_string_append (str, " \n"); g_string_append (str, "\n"); return g_string_free (str, FALSE); } static void save_response_cb (GtkNativeDialog *dialog, int response, ConstraintEditorWindow *self) { gtk_native_dialog_hide (dialog); if (response == GTK_RESPONSE_ACCEPT) { GListModel *model; GFile *file; char *text; GError *error = NULL; model = constraint_view_get_model (CONSTRAINT_VIEW (self->view)); text = serialize_model (model); file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)); g_file_replace_contents (file, text, strlen (text), NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); if (error != NULL) { GtkWidget *message_dialog; message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (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 (message_dialog), "%s", error->message); g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); gtk_widget_show (message_dialog); g_error_free (error); } g_free (text); g_object_unref (file); } gtk_native_dialog_destroy (dialog); } static void save_cb (GtkWidget *button, ConstraintEditorWindow *self) { GtkFileChooserNative *dialog; dialog = gtk_file_chooser_native_new ("Save constraints", GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))), GTK_FILE_CHOOSER_ACTION_SAVE, "_Save", "_Cancel"); gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (dialog), TRUE); GFile *cwd = g_file_new_for_path ("."); gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd, NULL); g_object_unref (cwd); g_signal_connect (dialog, "response", G_CALLBACK (save_response_cb), self); gtk_native_dialog_show (GTK_NATIVE_DIALOG (dialog)); } static void constraint_editor_window_finalize (GObject *object) { //ConstraintEditorWindow *self = (ConstraintEditorWindow *)object; G_OBJECT_CLASS (constraint_editor_window_parent_class)->finalize (object); } static int child_counter; static int guide_counter; static void add_child (ConstraintEditorWindow *win) { char *name; child_counter++; name = g_strdup_printf ("Child %d", child_counter); constraint_view_add_child (CONSTRAINT_VIEW (win->view), name); g_free (name); } static void add_guide (ConstraintEditorWindow *win) { char *name; GtkConstraintGuide *guide; guide_counter++; name = g_strdup_printf ("Guide %d", guide_counter); guide = gtk_constraint_guide_new (); gtk_constraint_guide_set_name (guide, name); g_free (name); constraint_view_add_guide (CONSTRAINT_VIEW (win->view), guide); } static void constraint_editor_done (ConstraintEditor *editor, GtkConstraint *constraint, ConstraintEditorWindow *win) { GtkConstraint *old_constraint; g_object_get (editor, "constraint", &old_constraint, NULL); if (old_constraint) constraint_view_remove_constraint (CONSTRAINT_VIEW (win->view), old_constraint); constraint_view_add_constraint (CONSTRAINT_VIEW (win->view), constraint); g_clear_object (&old_constraint); gtk_window_destroy (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW))); } static void edit_constraint (ConstraintEditorWindow *win, GtkConstraint *constraint) { GtkWidget *window; ConstraintEditor *editor; GListModel *model; window = gtk_window_new (); gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (win)); gtk_window_set_resizable (GTK_WINDOW (window), FALSE); if (constraint) gtk_window_set_title (GTK_WINDOW (window), "Edit Constraint"); else gtk_window_set_title (GTK_WINDOW (window), "Create Constraint"); model = constraint_view_get_model (CONSTRAINT_VIEW (win->view)); editor = constraint_editor_new (model, constraint); gtk_window_set_child (GTK_WINDOW (window), GTK_WIDGET (editor)); g_signal_connect (editor, "done", G_CALLBACK (constraint_editor_done), win); gtk_widget_show (window); } static void add_constraint (ConstraintEditorWindow *win) { edit_constraint (win, NULL); } static void guide_editor_done (GuideEditor *editor, GtkConstraintGuide *guide, ConstraintEditorWindow *win) { gtk_window_destroy (GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW))); } static void edit_guide (ConstraintEditorWindow *win, GtkConstraintGuide *guide) { GtkWidget *window; GuideEditor *editor; window = gtk_window_new (); gtk_window_set_resizable (GTK_WINDOW (window), FALSE); gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (win)); gtk_window_set_title (GTK_WINDOW (window), "Edit Guide"); editor = guide_editor_new (guide); gtk_window_set_child (GTK_WINDOW (window), GTK_WIDGET (editor)); g_signal_connect (editor, "done", G_CALLBACK (guide_editor_done), win); gtk_widget_show (window); } static void row_activated (GtkListBox *list, GtkListBoxRow *row, ConstraintEditorWindow *win) { GObject *item; item = G_OBJECT (g_object_get_data (G_OBJECT (row), "item")); if (GTK_IS_CONSTRAINT (item)) edit_constraint (win, GTK_CONSTRAINT (item)); else if (GTK_IS_CONSTRAINT_GUIDE (item)) edit_guide (win, GTK_CONSTRAINT_GUIDE (item)); } static void constraint_editor_window_class_init (ConstraintEditorWindowClass *class) { GObjectClass *object_class = G_OBJECT_CLASS (class); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); object_class->finalize = constraint_editor_window_finalize; gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/gtk4/constraint-editor/constraint-editor-window.ui"); gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, paned); gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, view); gtk_widget_class_bind_template_child (widget_class, ConstraintEditorWindow, list); 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, add_child); gtk_widget_class_bind_template_callback (widget_class, add_guide); gtk_widget_class_bind_template_callback (widget_class, add_constraint); gtk_widget_class_bind_template_callback (widget_class, row_activated); } static void row_edit (GtkButton *button, ConstraintEditorWindow *win) { GtkWidget *row; GObject *item; row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW); item = (GObject *)g_object_get_data (G_OBJECT (row), "item"); if (GTK_IS_CONSTRAINT (item)) edit_constraint (win, GTK_CONSTRAINT (item)); else if (GTK_IS_CONSTRAINT_GUIDE (item)) edit_guide (win, GTK_CONSTRAINT_GUIDE (item)); } static void mark_constraints_invalid (ConstraintEditorWindow *win, gpointer removed) { GtkWidget *child; GObject *item; for (child = gtk_widget_get_first_child (win->list); child; child = gtk_widget_get_next_sibling (child)) { item = (GObject *)g_object_get_data (G_OBJECT (child), "item"); if (GTK_IS_CONSTRAINT (item)) { GtkConstraint *constraint = GTK_CONSTRAINT (item); if (gtk_constraint_get_target (constraint) == (GtkConstraintTarget *)removed || gtk_constraint_get_source (constraint) == (GtkConstraintTarget *)removed) { GtkWidget *button; button = (GtkWidget *)g_object_get_data (G_OBJECT (child), "edit"); gtk_button_set_icon_name (GTK_BUTTON (button), "dialog-warning-symbolic"); gtk_widget_set_tooltip_text (button, "Constraint is invalid"); } } } } static void row_delete (GtkButton *button, ConstraintEditorWindow *win) { GtkWidget *row; GObject *item; row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW); item = (GObject *)g_object_get_data (G_OBJECT (row), "item"); if (GTK_IS_CONSTRAINT (item)) constraint_view_remove_constraint (CONSTRAINT_VIEW (win->view), GTK_CONSTRAINT (item)); else if (GTK_IS_CONSTRAINT_GUIDE (item)) { mark_constraints_invalid (win, item); constraint_view_remove_guide (CONSTRAINT_VIEW (win->view), GTK_CONSTRAINT_GUIDE (item)); } else if (GTK_IS_WIDGET (item)) { mark_constraints_invalid (win, item); constraint_view_remove_child (CONSTRAINT_VIEW (win->view), GTK_WIDGET (item)); } } static GtkWidget * create_widget_func (gpointer item, gpointer user_data) { ConstraintEditorWindow *win = user_data; const char *name; char *freeme = NULL; GtkWidget *row, *box, *label, *button; if (GTK_IS_WIDGET (item)) name = gtk_widget_get_name (GTK_WIDGET (item)); else if (GTK_IS_CONSTRAINT_GUIDE (item)) name = gtk_constraint_guide_get_name (GTK_CONSTRAINT_GUIDE (item)); else if (GTK_IS_CONSTRAINT (item)) name = freeme = constraint_editor_constraint_to_string (GTK_CONSTRAINT (item)); else name = ""; row = gtk_list_box_row_new (); g_object_set_data_full (G_OBJECT (row), "item", g_object_ref (item), g_object_unref); box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); label = gtk_label_new (name); if (GTK_IS_WIDGET (item) || GTK_IS_CONSTRAINT_GUIDE (item)) g_object_bind_property (item, "name", label, "label", G_BINDING_DEFAULT); gtk_widget_set_margin_start (label, 10); gtk_widget_set_margin_end (label, 10); gtk_widget_set_margin_top (label, 10); gtk_widget_set_margin_bottom (label, 10); gtk_label_set_xalign (GTK_LABEL (label), 0.0); gtk_widget_set_hexpand (label, TRUE); gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box); gtk_box_append (GTK_BOX (box), label); if (GTK_IS_CONSTRAINT (item) || GTK_IS_CONSTRAINT_GUIDE (item)) { button = gtk_button_new_from_icon_name ("document-edit-symbolic"); gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); g_signal_connect (button, "clicked", G_CALLBACK (row_edit), win); g_object_set_data (G_OBJECT (row), "edit", button); gtk_box_append (GTK_BOX (box), button); button = gtk_button_new_from_icon_name ("edit-delete-symbolic"); gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); g_signal_connect (button, "clicked", G_CALLBACK (row_delete), win); gtk_box_append (GTK_BOX (box), button); } else if (GTK_IS_WIDGET (item)) { button = gtk_button_new_from_icon_name ("edit-delete-symbolic"); gtk_button_set_has_frame (GTK_BUTTON (button), FALSE); g_signal_connect (button, "clicked", G_CALLBACK (row_delete), win); gtk_box_append (GTK_BOX (box), button); } g_free (freeme); return row; } static void constraint_editor_window_init (ConstraintEditorWindow *self) { gtk_widget_init_template (GTK_WIDGET (self)); gtk_list_box_bind_model (GTK_LIST_BOX (self->list), constraint_view_get_model (CONSTRAINT_VIEW (self->view)), create_widget_func, self, NULL); } ConstraintEditorWindow * constraint_editor_window_new (ConstraintEditorApplication *application) { return g_object_new (CONSTRAINT_EDITOR_WINDOW_TYPE, "application", application, NULL); }