node-editor: Add some editor smarts
authorMatthias Clasen <mclasen@redhat.com>
Sun, 7 May 2023 02:31:58 +0000 (22:31 -0400)
committerMatthias Clasen <mclasen@redhat.com>
Sun, 7 May 2023 13:19:44 +0000 (09:19 -0400)
Allow control-clicks on some fields to bring up
a more specific UI. This functionality is also
available via Ctrl-E and the context menu.

At this point, it can edit colors, fonts and
files in some places, as well as a few enums.

demos/node-editor/node-editor-window.c
demos/node-editor/node-editor-window.ui

index 86b529734708b0d5d3b5d5c68fad37725db935db..40f03ae4878d9232515d7e8423eca53cfe955538 100644 (file)
@@ -1190,11 +1190,367 @@ node_editor_window_unrealize (GtkWidget *widget)
   GTK_WIDGET_CLASS (node_editor_window_parent_class)->unrealize (widget);
 }
 
+typedef struct
+{
+  NodeEditorWindow *self;
+  GtkTextIter start, end;
+} Selection;
+
+static void
+color_cb (GObject *source,
+          GAsyncResult *result,
+          gpointer data)
+{
+  GtkColorDialog *dialog = GTK_COLOR_DIALOG (source);
+  Selection *selection = data;
+  NodeEditorWindow *self = selection->self;
+  GdkRGBA *color;
+  char *text;
+  GError *error = NULL;
+  GtkTextBuffer *buffer;
+
+  color = gtk_color_dialog_choose_rgba_finish (dialog, result, &error);
+  if (!color)
+    {
+      g_print ("%s\n", error->message);
+      g_error_free (error);
+      g_free (selection);
+      return;
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+
+  text = gdk_rgba_to_string (color);
+  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
+  gtk_text_buffer_insert (buffer, &selection->start, text, -1);
+
+  g_free (text);
+  gdk_rgba_free (color);
+  g_free (selection);
+}
+
+static void
+font_cb (GObject *source,
+          GAsyncResult *result,
+          gpointer data)
+{
+  GtkFontDialog *dialog = GTK_FONT_DIALOG (source);
+  Selection *selection = data;
+  NodeEditorWindow *self = selection->self;
+  GError *error = NULL;
+  PangoFontDescription *desc;
+  GtkTextBuffer *buffer;
+  char *text;
+
+  desc = gtk_font_dialog_choose_font_finish (dialog, result, &error);
+  if (!desc)
+    {
+      g_print ("%s\n", error->message);
+      g_error_free (error);
+      g_free (selection);
+      return;
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+
+  text = pango_font_description_to_string (desc);
+  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
+  gtk_text_buffer_insert (buffer, &selection->start, text, -1);
+
+  g_free (text);
+  pango_font_description_free (desc);
+  g_free (selection);
+}
+
+static void
+file_cb (GObject *source,
+          GAsyncResult *result,
+          gpointer data)
+{
+  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
+  Selection *selection = data;
+  NodeEditorWindow *self = selection->self;
+  GError *error = NULL;
+  GFile *file;
+  GtkTextBuffer *buffer;
+  char *text;
+
+  file = gtk_file_dialog_open_finish (dialog, result, &error);
+  if (!file)
+    {
+      g_print ("%s\n", error->message);
+      g_error_free (error);
+      g_free (selection);
+      return;
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+
+  text = g_file_get_uri (file);
+  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
+  gtk_text_buffer_insert (buffer, &selection->start, text, -1);
+
+  g_free (text);
+  g_object_unref (file);
+  g_free (selection);
+}
+
+static void
+key_pressed (GtkEventControllerKey *controller,
+             unsigned int keyval,
+             unsigned int keycode,
+             GdkModifierType state,
+             gpointer data)
+{
+  GtkWidget *dd = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (controller));
+  Selection *selection = data;
+  NodeEditorWindow *self = selection->self;
+  unsigned int selected;
+  GtkStringList *strings;
+  GtkTextBuffer *buffer;
+  const char *text;
+
+  if (keyval != GDK_KEY_Escape)
+    return;
+
+  strings = GTK_STRING_LIST (gtk_drop_down_get_model (GTK_DROP_DOWN (dd)));
+  selected = gtk_drop_down_get_selected (GTK_DROP_DOWN (dd));
+  text = gtk_string_list_get_string (strings, selected);
+
+  gtk_text_view_remove (GTK_TEXT_VIEW (self->text_view), dd);
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+  gtk_text_iter_backward_search (&selection->start, "mode:", 0, NULL, &selection->start, NULL);
+  gtk_text_iter_forward_search (&selection->start, ";", 0, &selection->end, NULL, NULL);
+  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
+  gtk_text_buffer_insert (buffer, &selection->start, " ", -1);
+  gtk_text_buffer_insert (buffer, &selection->start, text, -1);
+}
+
+static void
+node_editor_window_edit (NodeEditorWindow *self,
+                         GtkTextIter      *iter)
+{
+  GtkTextIter start, end;
+  GtkTextBuffer *buffer;
+  Selection *selection;
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+
+  gtk_text_iter_set_line_offset (iter, 0);
+
+  if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
+      gtk_text_iter_forward_search (iter, "color:", 0, NULL, &start, &end))
+    {
+      GtkColorDialog *dialog;
+      GdkRGBA color;
+      char *text;
+
+      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
+        gtk_text_iter_forward_char (&start);
+
+      gtk_text_buffer_select_range (buffer, &start, &end);
+      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
+      gdk_rgba_parse (&color, text);
+      g_free (text);
+
+      selection = g_new0 (Selection, 1);
+      selection->self = self;
+      selection->start = start;
+      selection->end = end;
+
+      dialog = gtk_color_dialog_new ();
+      gtk_color_dialog_choose_rgba (dialog, GTK_WINDOW (self), &color, NULL, color_cb, selection);
+    }
+  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
+           gtk_text_iter_forward_search (iter, "font:", 0, NULL, &start, &end))
+    {
+      GtkFontDialog *dialog;
+      PangoFontDescription *desc;
+      char *text;
+
+      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
+        gtk_text_iter_forward_char (&start);
+
+      /* Skip the quotes */
+      gtk_text_iter_forward_char (&start);
+      gtk_text_iter_backward_char (&end);
+
+      gtk_text_buffer_select_range (buffer, &start, &end);
+
+      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
+      desc = pango_font_description_from_string (text);
+      g_free (text);
+
+      selection = g_new0 (Selection, 1);
+      selection->self = self;
+      selection->start = start;
+      selection->end = end;
+
+      dialog = gtk_font_dialog_new ();
+      gtk_font_dialog_choose_font (dialog, GTK_WINDOW (self), desc, NULL, font_cb, selection);
+      pango_font_description_free (desc);
+    }
+  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
+           gtk_text_iter_forward_search (iter, "mode:", 0, NULL, &start, &end))
+    {
+      /* Assume we have a blend node, for now */
+      GEnumClass *class;
+      GtkStringList *strings;
+      GtkWidget *dd;
+      GtkTextChildAnchor *anchor;
+      unsigned int selected = 0;
+      GtkEventController *key_controller;
+      gboolean is_blend_mode = FALSE;
+      char *text;
+
+      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
+        gtk_text_iter_forward_char (&start);
+
+      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
+
+      strings = gtk_string_list_new (NULL);
+      class = g_type_class_ref (GSK_TYPE_BLEND_MODE);
+      for (unsigned int i = 0; i < class->n_values; i++)
+        {
+          if (strcmp (class->values[i].value_nick, text) == 0)
+            is_blend_mode = TRUE;
+        }
+      g_type_class_unref (class);
+
+      if (is_blend_mode)
+        class = g_type_class_ref (GSK_TYPE_BLEND_MODE);
+      else
+        class = g_type_class_ref (GSK_TYPE_MASK_MODE);
+
+       for (unsigned int i = 0; i < class->n_values; i++)
+         {
+           if (i == 0 && is_blend_mode)
+             gtk_string_list_append (strings, "normal");
+           else
+             gtk_string_list_append (strings, class->values[i].value_nick);
+
+           if (strcmp (class->values[i].value_nick, text) == 0)
+             selected = i;
+         }
+      g_type_class_unref (class);
+
+      gtk_text_buffer_delete (buffer, &start, &end);
+
+      anchor = gtk_text_buffer_create_child_anchor (buffer, &start);
+      dd = gtk_drop_down_new (G_LIST_MODEL (strings), NULL);
+      gtk_drop_down_set_selected (GTK_DROP_DOWN (dd), selected);
+      gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (self->text_view), dd, anchor);
+
+      selection = g_new0 (Selection, 1);
+      selection->self = self;
+      selection->start = start;
+      selection->end = end;
+
+      key_controller = gtk_event_controller_key_new ();
+      g_signal_connect (key_controller, "key-pressed", G_CALLBACK (key_pressed), selection);
+      gtk_widget_add_controller (dd, key_controller);
+    }
+  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
+           gtk_text_iter_forward_search (iter, "texture:", 0, NULL, &start, &end))
+    {
+      GtkFileDialog *dialog;
+      GtkTextIter skip;
+      char *text;
+      GFile *file;
+
+      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
+        gtk_text_iter_forward_char (&start);
+
+      skip = start;
+      gtk_text_iter_forward_chars (&skip, strlen ("url(\""));
+      text = gtk_text_iter_get_text (&start, &skip);
+      if (strcmp (text, "url(\"") != 0)
+        {
+          g_free (text);
+          return;
+        }
+      g_free (text);
+      start = skip;
+
+      skip = end;
+      gtk_text_iter_backward_chars (&skip, strlen ("\")"));
+      text = gtk_text_iter_get_text (&skip, &end);
+      if (strcmp (text, "\")") != 0)
+        {
+          g_free (text);
+          return;
+        }
+      g_free (text);
+      end = skip;
+
+      gtk_text_buffer_select_range (buffer, &start, &end);
+
+      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
+      file = g_file_new_for_uri (text);
+      g_free (text);
+
+      selection = g_new0 (Selection, 1);
+      selection->self = self;
+      selection->start = start;
+      selection->end = end;
+
+      dialog = gtk_file_dialog_new ();
+      gtk_file_dialog_set_initial_file (dialog, file);
+      gtk_file_dialog_open (dialog, GTK_WINDOW (self), NULL, file_cb, selection);
+      g_object_unref (file);
+    }
+}
+
+static void
+click_gesture_pressed (GtkGestureClick  *gesture,
+                       int               n_press,
+                       double            x,
+                       double            y,
+                       NodeEditorWindow *self)
+{
+  GtkTextIter iter;
+  int bx, by, trailing;
+  GdkModifierType state;
+
+  state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture));
+  if ((state & GDK_CONTROL_MASK) == 0)
+    return;
+
+  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);
+
+  node_editor_window_edit (self, &iter);
+}
+
+static void
+edit_action_cb (GtkWidget  *widget,
+                const char *action_name,
+                GVariant   *parameter)
+{
+  NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
+  GtkTextBuffer *buffer;
+  GtkTextIter start, end;
+
+#if 0
+  if (gtk_window_get_focus (GTK_WINDOW (self)) != self->text_view)
+    return;
+#endif
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
+  gtk_text_buffer_get_selection_bounds (buffer, &start, &end);
+
+  node_editor_window_edit (self, &start);
+}
+
 static void
 node_editor_window_class_init (NodeEditorWindowClass *class)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (class);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+  GtkShortcutTrigger *trigger;
+  GtkShortcutAction *action;
+  GtkShortcut *shortcut;
 
   object_class->dispose = node_editor_window_dispose;
   object_class->finalize = node_editor_window_finalize;
@@ -1225,6 +1581,14 @@ node_editor_window_class_init (NodeEditorWindowClass *class)
   gtk_widget_class_bind_template_callback (widget_class, dark_mode_cb);
   gtk_widget_class_bind_template_callback (widget_class, on_picture_drag_prepare_cb);
   gtk_widget_class_bind_template_callback (widget_class, on_picture_drop_cb);
+  gtk_widget_class_bind_template_callback (widget_class, click_gesture_pressed);
+
+  gtk_widget_class_install_action (widget_class, "smart-edit", NULL, edit_action_cb);
+
+  trigger = gtk_keyval_trigger_new (GDK_KEY_e, GDK_CONTROL_MASK);
+  action = gtk_named_action_new ("smart-edit");
+  shortcut = gtk_shortcut_new (trigger, action);
+  gtk_widget_class_add_shortcut (widget_class, shortcut);
 }
 
 static GtkWidget *
index 45a37cd952349258fc4cafa44354234da9da57e6..deeeb98de3ce090365bd8a0930f5c81b06c9203c 100644 (file)
       </item>
     </section>
   </menu>
+  <menu id="extra_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Assisted _Edit</attribute>
+        <attribute name="action">smart-edit</attribute>
+      </item>
+    </section>
+  </menu>
 
   <object class="GtkPopover" id="testcase_popover">
     <child>
                 <property name="right-margin">6</property>
                 <property name="bottom-margin">6</property>
                 <property name="has-tooltip">1</property>
+                <property name="extra-menu">extra_menu</property>
                 <signal name="query-tooltip" handler="text_view_query_tooltip_cb"/>
                 <style>
                   <class name="editor" />
                 </style>
+                <child>
+                  <object class="GtkGestureClick">
+                    <property name="button">1</property>
+                    <signal name="pressed" handler="click_gesture_pressed"/>
+                  </object>
+                </child>
               </object>
             </child>
           </object>