Ver código fonte

feat: hand-curated GtkBuilder binding, test and example

- src/GtkBuilder.def: GtkBuilder with GError-aware add_from_* loaders
  (the generator drops GError**), error domain/codes, closure flags,
  object/type lookup and template helpers
- tests/test_builder.mod: loads a .ui string, checks object/type lookup,
  and verifies malformed XML reports GTK_BUILDER_ERROR
- examples/builder.{mod,ui}: load a window from a .ui file and wire a
  button signal to a status label

17 tests pass.
Eric Streit 3 dias atrás
pai
commit
29f1c65f14
7 arquivos alterados com 438 adições e 5 exclusões
  1. 2 2
      Makefile
  2. 3 1
      README.md
  3. 73 0
      examples/builder.mod
  4. 34 0
      examples/builder.ui
  5. 213 0
      src/GtkBuilder.def
  6. 2 2
      tests/run_tests.sh
  7. 111 0
      tests/test_builder.mod

+ 2 - 2
Makefile

@@ -18,8 +18,8 @@ OBJS    := $(OBJDIR)/GtkUtils.o $(OBJDIR)/GtkClosures.o
 
 INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
 
-TESTS    := test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_application
-EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views
+TESTS    := test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_application
+EXAMPLES := hello counter inputs containers actions headerbar editor layouts files views builder
 
 # Include the libadwaita test/example only when libadwaita is installed.
 ifneq ($(ADWLIBS),)

+ 3 - 1
README.md

@@ -20,7 +20,7 @@ Verified with `gm2` 16.0.1 (experimental), GTK 4.18.6 and libadwaita 1.7.6.
 - **Pure-Modula-2 helpers**: `GtkUtils` (string/number conversion,
   `Connect`, `SetAccel`, ownership and `GError` helpers) and
   `GtkClosures` (per-connection signal contexts with cleanup).
-- **16 tests** and **11 runnable examples**.
+- **17 tests** and **11 runnable examples**.
 - **Generator + verifier**: `tools/gir2def.py` turns a `.gir` file into
   `.def` files; `tools/gir_verify.py` checks every generated identifier
   against the installed library. Verified clean across 8602 identifiers.
@@ -112,6 +112,7 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `layouts` | frame, expander, paned, overlay, list box |
 | `files` | native file dialog → `GFile` |
 | `views` | `GtkListView` / `GtkGridView` / `GtkColumnView` from a store |
+| `builder` | load a window from a `.ui` file with `GtkBuilder` |
 | `adw` | libadwaita preferences app (toolbar view, rows, toast, about) |
 
 ## Module reference
@@ -148,6 +149,7 @@ gm2 -fiso -Isrc -Ilib myprog.mod \
 | `GtkLabel`, `GtkButton`, `GtkCheckButton`, `GtkSwitch`, `GtkScale`, `GtkRange`, `GtkSpinButton`, `GtkEntry`, `GtkEditable`, `GtkSearchEntry`, `GtkDropDown` | controls |
 | `GtkTextView`, `GtkTextBuffer`, `GtkTextIter` | multi-line text |
 | `GtkHeaderBar`, `GtkMenuButton`, `GtkPopoverMenuBar` | window chrome and menus |
+| `GtkBuilder` | load widget trees from `.ui` XML (with `GError`) |
 | `GtkListBox`, `GtkListView`, `GtkGridView`, `GtkColumnView`, `GtkColumnViewColumn`, `GtkColumnViewCell`, `GtkListItem`, `GtkSignalListItemFactory`, `GtkSelectionModel`, `GtkSingleSelection`, `GtkNoSelection` | lists, grids and tables |
 
 ### libadwaita

+ 73 - 0
examples/builder.mod

@@ -0,0 +1,73 @@
+MODULE builder ;
+
+(*
+   m2-GTK4 example - building a window from a .ui file with GtkBuilder.
+
+   Loads examples/builder.ui (object ids: window, button, status), wires
+   the button's "clicked" signal to update the status label, and shows
+   the window.
+
+   Build:  make examples
+   Run:    ./build/examples/builder      (from the repository root)
+*)
+
+FROM Gio IMPORT g_application_run;
+FROM GtkApplication IMPORT gtk_application_new;
+FROM GtkBuilder IMPORT GtkBuilder, gtk_builder_new, gtk_builder_add_from_file,
+   gtk_builder_get_object;
+FROM GtkWindow IMPORT gtk_window_set_application, gtk_window_present;
+FROM GtkLabel IMPORT gtk_label_set_text;
+FROM GError IMPORT GError;
+FROM GtkClosures IMPORT Connect2;
+FROM GtkUtils IMPORT ErrorMessage, ClearError;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.Builder";
+
+VAR
+   builder: ADDRESS;
+
+PROCEDURE OnClicked (button: ADDRESS; user: ADDRESS);
+BEGIN
+   gtk_label_set_text(user, "Button clicked!")
+END OnClicked;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   window, button, status: ADDRESS;
+   err: GError;
+   buf: ARRAY [0..255] OF CHAR;
+BEGIN
+   builder := gtk_builder_new();
+   err := NIL;
+   IF gtk_builder_add_from_file(builder, "examples/builder.ui", err) = 0 THEN
+      ErrorMessage(err, buf);
+      printf("failed to load examples/builder.ui: %s\n", buf);
+      printf("(run this example from the repository root)\n");
+      ClearError(err);
+      HALT(1)
+   END;
+
+   window := gtk_builder_get_object(builder, "window");
+   button := gtk_builder_get_object(builder, "button");
+   status := gtk_builder_get_object(builder, "status");
+
+   Connect2(button, "clicked", OnClicked, status);
+
+   gtk_window_set_application(window, application);
+   gtk_window_present(window)
+END OnActivate;
+
+VAR
+   app: ADDRESS;
+BEGIN
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("gtk_application_new failed\n");
+      HALT(1)
+   END;
+   Connect2(app, "activate", OnActivate, NIL);
+   g_application_run(app, 0, NIL)
+END builder.

+ 34 - 0
examples/builder.ui

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   UI description used by examples/builder.mod.
+   Loaded at run time with gtk_builder_add_from_file(); run the example
+   from the repository root so the relative path resolves.
+-->
+<interface>
+  <object class="GtkWindow" id="window">
+    <property name="title">m2-GTK4 Builder</property>
+    <property name="default-width">420</property>
+    <property name="default-height">200</property>
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="orientation">vertical</property>
+        <property name="spacing">10</property>
+        <child>
+          <object class="GtkLabel" id="heading">
+            <property name="label">Loaded from builder.ui</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="button">
+            <property name="label">Click me</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="status">
+            <property name="label">Ready</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>

+ 213 - 0
src/GtkBuilder.def

@@ -0,0 +1,213 @@
+DEFINITION MODULE FOR "C" GtkBuilder ;
+
+(*
+   m2-GTK4 - GtkBuilder: build a widget tree from UI description XML
+   (".ui" files).  Mapped onto <gtk/gtkbuild.h>.
+
+   Two styles of API:
+
+     * gtk_builder_new_from_file/_from_resource/_from_string are
+       convenience constructors.  They return NIL on failure but do not
+       report the error message.
+     * gtk_builder_add_from_* take a VAR error: GError, so callers can
+       inspect parse failures:
+
+          VAR err: GError;
+          err := NIL;
+          IF gtk_builder_add_from_string(b, ui, -1, err) = 0 THEN ... END;
+          IF err # NIL THEN GtkUtils.ErrorMessage(err, msg); ... END;
+
+       The length argument is gssize; pass -1 for a NUL-terminated
+       string.
+
+   Objects are fetched by id with gtk_builder_get_object (an opaque
+   handle - cast/assign to the concrete type).  GtkBuilder is a GObject:
+   release it with GtkUtils.Unref / g_object_unref.
+
+   The gtk_builder_value_from_string* functions are omitted because they
+   need a GValue, which is not bound (yet).
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM GError IMPORT GError;
+
+EXPORT UNQUALIFIED
+   GtkBuilder, GtkBuilderError, GtkBuilderClosureFlags,
+   GtkBuilderErrorInvalidTypeFunction, GtkBuilderErrorUnhandledTag,
+   GtkBuilderErrorMissingAttribute, GtkBuilderErrorInvalidAttribute,
+   GtkBuilderErrorInvalidTag, GtkBuilderErrorMissingPropertyValue,
+   GtkBuilderErrorInvalidValue, GtkBuilderErrorVersionMismatch,
+   GtkBuilderErrorDuplicateId, GtkBuilderErrorObjectTypeRefused,
+   GtkBuilderErrorTemplateMismatch, GtkBuilderErrorInvalidProperty,
+   GtkBuilderErrorInvalidSignal, GtkBuilderErrorInvalidId,
+   GtkBuilderErrorInvalidFunction,
+   GtkBuilderClosureSwapped,
+   gtk_builder_new,
+   gtk_builder_new_from_file, gtk_builder_new_from_resource,
+   gtk_builder_new_from_string,
+   gtk_builder_add_from_file, gtk_builder_add_from_resource,
+   gtk_builder_add_from_string,
+   gtk_builder_add_objects_from_file, gtk_builder_add_objects_from_resource,
+   gtk_builder_add_objects_from_string,
+   gtk_builder_get_object, gtk_builder_get_objects,
+   gtk_builder_get_type_from_name,
+   gtk_builder_expose_object,
+   gtk_builder_get_current_object, gtk_builder_set_current_object,
+   gtk_builder_set_translation_domain, gtk_builder_get_translation_domain,
+   gtk_builder_get_scope, gtk_builder_set_scope,
+   gtk_builder_create_closure, gtk_builder_extend_with_template,
+   gtk_builder_error_quark;
+
+
+TYPE
+   GtkBuilder = ADDRESS;
+   GtkBuilderError = CARDINAL;
+   GtkBuilderClosureFlags = CARDINAL;
+
+CONST
+   GtkBuilderErrorInvalidTypeFunction = 0;
+   GtkBuilderErrorUnhandledTag         = 1;
+   GtkBuilderErrorMissingAttribute     = 2;
+   GtkBuilderErrorInvalidAttribute     = 3;
+   GtkBuilderErrorInvalidTag           = 4;
+   GtkBuilderErrorMissingPropertyValue = 5;
+   GtkBuilderErrorInvalidValue         = 6;
+   GtkBuilderErrorVersionMismatch      = 7;
+   GtkBuilderErrorDuplicateId          = 8;
+   GtkBuilderErrorObjectTypeRefused    = 9;
+   GtkBuilderErrorTemplateMismatch     = 10;
+   GtkBuilderErrorInvalidProperty      = 11;
+   GtkBuilderErrorInvalidSignal        = 12;
+   GtkBuilderErrorInvalidId            = 13;
+   GtkBuilderErrorInvalidFunction      = 14;
+
+   GtkBuilderClosureSwapped = 1;   (* GTK_BUILDER_CLOSURE_SWAPPED *)
+
+(* GQuark gtk_builder_error_quark(void) *)
+PROCEDURE gtk_builder_error_quark () : CARDINAL;
+
+(* GtkBuilder *gtk_builder_new(void) *)
+PROCEDURE gtk_builder_new () : GtkBuilder;
+
+(* GtkBuilder *gtk_builder_new_from_file(const char *filename) *)
+PROCEDURE gtk_builder_new_from_file (filename: ARRAY OF CHAR) : GtkBuilder;
+
+(* GtkBuilder *gtk_builder_new_from_resource(const char *resource_path) *)
+PROCEDURE gtk_builder_new_from_resource (resource_path: ARRAY OF CHAR) : GtkBuilder;
+
+(* GtkBuilder *gtk_builder_new_from_string(const char *string, gssize length) *)
+PROCEDURE gtk_builder_new_from_string (string: ARRAY OF CHAR;
+                                       length: LONGINT) : GtkBuilder;
+
+(* gboolean gtk_builder_add_from_file(GtkBuilder *builder,
+                                       const char *filename, GError **error) *)
+PROCEDURE gtk_builder_add_from_file (builder: GtkBuilder;
+                                     filename: ARRAY OF CHAR;
+                                     VAR error: GError) : [ INTEGER ];
+
+(* gboolean gtk_builder_add_from_resource(GtkBuilder *builder,
+                                           const char *resource_path,
+                                           GError **error) *)
+PROCEDURE gtk_builder_add_from_resource (builder: GtkBuilder;
+                                         resource_path: ARRAY OF CHAR;
+                                         VAR error: GError) : [ INTEGER ];
+
+(* gboolean gtk_builder_add_from_string(GtkBuilder *builder,
+                                         const char *buffer, gssize length,
+                                         GError **error) *)
+PROCEDURE gtk_builder_add_from_string (builder: GtkBuilder;
+                                       buffer: ARRAY OF CHAR;
+                                       length: LONGINT;
+                                       VAR error: GError) : [ INTEGER ];
+
+(* gboolean gtk_builder_add_objects_from_file(GtkBuilder *builder,
+                                               const char *filename,
+                                               const char **object_ids,
+                                               GError **error) *)
+PROCEDURE gtk_builder_add_objects_from_file (builder: GtkBuilder;
+                                             filename: ARRAY OF CHAR;
+                                             object_ids: ADDRESS;
+                                             VAR error: GError) : [ INTEGER ];
+
+(* gboolean gtk_builder_add_objects_from_resource(GtkBuilder *builder,
+                                                   const char *resource_path,
+                                                   const char **object_ids,
+                                                   GError **error) *)
+PROCEDURE gtk_builder_add_objects_from_resource (builder: GtkBuilder;
+                                                 resource_path: ARRAY OF CHAR;
+                                                 object_ids: ADDRESS;
+                                                 VAR error: GError) : [ INTEGER ];
+
+(* gboolean gtk_builder_add_objects_from_string(GtkBuilder *builder,
+                                                 const char *buffer,
+                                                 gssize length,
+                                                 const char **object_ids,
+                                                 GError **error) *)
+PROCEDURE gtk_builder_add_objects_from_string (builder: GtkBuilder;
+                                               buffer: ARRAY OF CHAR;
+                                               length: LONGINT;
+                                               object_ids: ADDRESS;
+                                               VAR error: GError) : [ INTEGER ];
+
+(* GObject *gtk_builder_get_object(GtkBuilder *builder, const char *name) *)
+PROCEDURE gtk_builder_get_object (builder: GtkBuilder;
+                                  name: ARRAY OF CHAR) : ADDRESS;
+
+(* GSList *gtk_builder_get_objects(GtkBuilder *builder) *)
+PROCEDURE gtk_builder_get_objects (builder: GtkBuilder) : ADDRESS;
+
+(* GType gtk_builder_get_type_from_name(GtkBuilder *builder,
+                                         const char *type_name) *)
+PROCEDURE gtk_builder_get_type_from_name (builder: GtkBuilder;
+                                          type_name: ARRAY OF CHAR) : LONGCARD;
+
+(* void gtk_builder_expose_object(GtkBuilder *builder, const char *name,
+                                   GObject *object) *)
+PROCEDURE gtk_builder_expose_object (builder: GtkBuilder;
+                                     name: ARRAY OF CHAR; object: ADDRESS);
+
+(* GObject *gtk_builder_get_current_object(GtkBuilder *builder) *)
+PROCEDURE gtk_builder_get_current_object (builder: GtkBuilder) : ADDRESS;
+
+(* void gtk_builder_set_current_object(GtkBuilder *builder,
+                                        GObject *current_object) *)
+PROCEDURE gtk_builder_set_current_object (builder: GtkBuilder;
+                                          current_object: ADDRESS);
+
+(* void gtk_builder_set_translation_domain(GtkBuilder *builder,
+                                            const char *domain) *)
+PROCEDURE gtk_builder_set_translation_domain (builder: GtkBuilder;
+                                              domain: ARRAY OF CHAR);
+
+(* const char *gtk_builder_get_translation_domain(GtkBuilder *builder) *)
+PROCEDURE gtk_builder_get_translation_domain (builder: GtkBuilder) : ADDRESS;
+
+(* GtkBuilderScope *gtk_builder_get_scope(GtkBuilder *builder) *)
+PROCEDURE gtk_builder_get_scope (builder: GtkBuilder) : ADDRESS;
+
+(* void gtk_builder_set_scope(GtkBuilder *builder, GtkBuilderScope *scope) *)
+PROCEDURE gtk_builder_set_scope (builder: GtkBuilder; scope: ADDRESS);
+
+(* GClosure *gtk_builder_create_closure(GtkBuilder *builder,
+                                         const char *function_name,
+                                         GtkBuilderClosureFlags flags,
+                                         GObject *object, GError **error) *)
+PROCEDURE gtk_builder_create_closure (builder: GtkBuilder;
+                                      function_name: ARRAY OF CHAR;
+                                      flags: GtkBuilderClosureFlags;
+                                      object: ADDRESS;
+                                      VAR error: GError) : ADDRESS;
+
+(* gboolean gtk_builder_extend_with_template(GtkBuilder *builder,
+                                              GObject *object,
+                                              GType template_type,
+                                              const char *buffer,
+                                              gssize length, GError **error) *)
+PROCEDURE gtk_builder_extend_with_template (builder: GtkBuilder;
+                                            object: ADDRESS;
+                                            template_type: LONGCARD;
+                                            buffer: ARRAY OF CHAR;
+                                            length: LONGINT;
+                                            VAR error: GError) : [ INTEGER ];
+
+END GtkBuilder.

+ 2 - 2
tests/run_tests.sh

@@ -13,7 +13,7 @@ CFLAGS=$(pkg-config --cflags gtk4)
 LIBS=$(pkg-config --libs gtk4)
 
 # libadwaita is optional: add its flags and test when available.
-TESTS="test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_application"
+TESTS="test_glib test_version test_widgets test_inputs test_inputs2 test_containers test_containers2 test_actions test_closure test_headerbar test_gio test_gerror test_filedialog test_views test_builder test_application"
 if pkg-config --exists libadwaita-1 2>/dev/null; then
   CFLAGS="$CFLAGS $(pkg-config --cflags libadwaita-1)"
   LIBS="$LIBS $(pkg-config --libs libadwaita-1)"
@@ -45,7 +45,7 @@ for t in $TESTS; do
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS $CFLAGS \
        "$ROOT/tests/$t.mod" "$OBJ/GtkUtils.o" "$OBJ/GtkClosures.o" -o "$BIN/$t" $LIBS
   case "$t" in
-    test_widgets|test_inputs|test_inputs2|test_containers|test_containers2|test_actions|test_closure|test_headerbar|test_filedialog|test_views|test_adw|test_application) needs_display=1 ;;
+    test_widgets|test_inputs|test_inputs2|test_containers|test_containers2|test_actions|test_closure|test_headerbar|test_filedialog|test_views|test_builder|test_adw|test_application) needs_display=1 ;;
     *) needs_display=0 ;;
   esac
   if [ "$needs_display" = 1 ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then

+ 111 - 0
tests/test_builder.mod

@@ -0,0 +1,111 @@
+MODULE test_builder ;
+
+(*
+   m2-GTK4 test - GtkBuilder.
+
+   Loads a widget tree from an inline UI string, fetches an object by id,
+   looks up a type name, and checks that malformed XML reports a
+   GTK_BUILDER_ERROR via the GError out-parameter.  Requires a display.
+*)
+
+FROM Gio IMPORT g_application_run, g_application_quit;
+FROM GtkApplication IMPORT gtk_application_new;
+FROM GtkBuilder IMPORT GtkBuilder, gtk_builder_new,
+   gtk_builder_add_from_string, gtk_builder_get_object,
+   gtk_builder_get_objects, gtk_builder_get_type_from_name,
+   gtk_builder_error_quark;
+FROM GError IMPORT GError;
+FROM GtkUtils IMPORT Connect, ErrorMessage, ClearError, Unref;
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM libc IMPORT printf;
+
+CONST
+   AppId = "org.example.m2gtk4.TestBuilder";
+   GoodUi = "<interface><object class='GtkWindow' id='win'><property name='title'>T</property><child><object class='GtkLabel' id='label1'><property name='label'>Hi</property></object></child></object></interface>";
+
+VAR
+   app: ADDRESS;
+   passed: BOOLEAN;
+
+PROCEDURE OnActivate (application: ADDRESS; data: ADDRESS);
+VAR
+   builder: GtkBuilder;
+   err: GError;
+   buf: ARRAY [0..255] OF CHAR;
+   ok: BOOLEAN;
+BEGIN
+   ok := TRUE;
+
+   (* --- success path --- *)
+   builder := gtk_builder_new();
+   err := NIL;
+   IF gtk_builder_add_from_string(builder, GoodUi, -1, err) = 0 THEN
+      ErrorMessage(err, buf);
+      printf("good UI failed: %s\n", buf);
+      ClearError(err);
+      ok := FALSE
+   END;
+   IF err # NIL THEN
+      ErrorMessage(err, buf);
+      printf("unexpected error: %s\n", buf);
+      ClearError(err);
+      ok := FALSE
+   END;
+   IF gtk_builder_get_object(builder, "label1") = NIL THEN
+      printf("label1 not found\n");
+      ok := FALSE
+   END;
+   IF gtk_builder_get_type_from_name(builder, "GtkLabel") = 0 THEN
+      printf("type lookup failed\n");
+      ok := FALSE
+   END;
+   IF gtk_builder_get_objects(builder) = NIL THEN
+      printf("objects list is NIL\n");
+      ok := FALSE
+   END;
+   Unref(builder);
+
+   (* --- failure path: malformed XML must set a GError --- *)
+   builder := gtk_builder_new();
+   err := NIL;
+   IF gtk_builder_add_from_string(builder, "<interface><bad/></interface>",
+                                  -1, err) # 0 THEN
+      printf("bad UI unexpectedly parsed\n");
+      ok := FALSE
+   END;
+   IF err = NIL THEN
+      printf("bad UI did not set an error\n");
+      ok := FALSE
+   ELSE
+      IF err^.domain # gtk_builder_error_quark() THEN
+         printf("wrong error domain\n");
+         ok := FALSE
+      END;
+      ClearError(err)
+   END;
+   Unref(builder);
+
+   passed := ok;
+   g_application_quit(application)
+END OnActivate;
+
+VAR
+   rc: INTEGER;
+BEGIN
+   passed := FALSE;
+   app := gtk_application_new(AppId, 0);
+   IF app = NIL THEN
+      printf("test_builder: FAIL (no app)\n");
+      HALT(1)
+   END;
+   Connect(app, "activate", ADR(OnActivate), NIL);
+   rc := g_application_run(app, 0, NIL);
+
+   IF passed THEN
+      printf("test_builder: PASS\n");
+      HALT(0)
+   ELSE
+      printf("test_builder: FAIL\n");
+      HALT(1)
+   END
+END test_builder.