diff --git a/README.md b/README.md index 64e0f8a2..974ca24d 100644 --- a/README.md +++ b/README.md @@ -176,24 +176,24 @@ public class MyWidget extends Widget { } ``` -You can define custom GObject properties and signals using annotations: +You can define custom GObject properties and signals using annotations. The following example defines an `int` property named `"lives"` and a `"game-over"` signal with a `String` parameter: ```java - @Property(name="lives") + @Property public int getLives() { return lives; } - @Property(name="lives") + @Property public void setLives(int value) { this.lives = value; - if (value == 0) emit("game-over", limit); + if (value == 0) + emit("game-over", player.name()); } - @Signal(name="game-over") - @FunctionalInterface + @Signal public interface GameOver { - public void apply(int limit); + void apply(String playerName); } ``` diff --git a/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/annotations/Property.java b/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/annotations/Property.java index 37f72a29..fcbf8754 100644 --- a/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/annotations/Property.java +++ b/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/annotations/Property.java @@ -29,7 +29,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Property { - String name(); + String name() default ""; Class type() default ParamSpec.class; boolean readable() default true; boolean writable() default true; diff --git a/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/types/Properties.java b/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/types/Properties.java index 8df2bf6d..7117a28a 100644 --- a/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/types/Properties.java +++ b/modules/gobject/src/main/java/io/github/jwharm/javagi/gobject/types/Properties.java @@ -151,6 +151,17 @@ public static T newGObjectWithProperties(Type objectType, Ob } } + // Convert "CamelCase" to "kebab-case" + private static String getPropertyName(String methodName) { + if (methodName.startsWith("get") || methodName.startsWith("set")) { + String value = methodName.substring(3); + return value.replaceAll("([a-z0-9])([A-Z])", "$1-$2") + .toLowerCase().replaceAll("\\.", ""); + } + throw new IllegalArgumentException("Cannot infer property name from method named %s" + .formatted(methodName)); + } + // Infer the ParamSpec class from the Java class that is used in the getter/setter method. private static Class inferType(Method method) { // Determine the Java class of the property @@ -243,8 +254,11 @@ public static Consumer i continue; } + // Name is specified with the annotation, or infer it form the method name + String name = p.name().isEmpty() ? getPropertyName(method.getName()) : p.name(); + // Check if this property has already been added from another method - if (propertyNames.contains(p.name())) { + if (propertyNames.contains(name)) { continue; } @@ -261,35 +275,35 @@ public static Consumer i ParamSpec ps; if (paramspec.equals(ParamSpecBoolean.class)) { - ps = GObjects.paramSpecBoolean(p.name(), p.name(), p.name(), false, getFlags(p)); + ps = GObjects.paramSpecBoolean(name, name, name, false, getFlags(p)); } else if (paramspec.equals(ParamSpecChar.class)) { - ps = GObjects.paramSpecChar(p.name(), p.name(), p.name(), Byte.MIN_VALUE, Byte.MAX_VALUE, (byte) 0, getFlags(p)); + ps = GObjects.paramSpecChar(name, name, name, Byte.MIN_VALUE, Byte.MAX_VALUE, (byte) 0, getFlags(p)); } else if (paramspec.equals(ParamSpecDouble.class)) { - ps = GObjects.paramSpecDouble(p.name(), p.name(), p.name(), -Double.MAX_VALUE, Double.MAX_VALUE, 0.0d, getFlags(p)); + ps = GObjects.paramSpecDouble(name, name, name, -Double.MAX_VALUE, Double.MAX_VALUE, 0.0d, getFlags(p)); } else if (paramspec.equals(ParamSpecFloat.class)) { - ps = GObjects.paramSpecFloat(p.name(), p.name(), p.name(), -Float.MAX_VALUE, Float.MAX_VALUE, 0.0f, getFlags(p)); + ps = GObjects.paramSpecFloat(name, name, name, -Float.MAX_VALUE, Float.MAX_VALUE, 0.0f, getFlags(p)); } else if (paramspec.equals(ParamSpecGType.class)) { - ps = GObjects.paramSpecGtype(p.name(), p.name(), p.name(), Types.NONE, getFlags(p)); + ps = GObjects.paramSpecGtype(name, name, name, Types.NONE, getFlags(p)); } else if (paramspec.equals(ParamSpecInt.class)) { - ps = GObjects.paramSpecInt(p.name(), p.name(), p.name(), Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecInt(name, name, name, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecInt64.class)) { - ps = GObjects.paramSpecInt64(p.name(), p.name(), p.name(), Long.MIN_VALUE, Long.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecInt64(name, name, name, Long.MIN_VALUE, Long.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecLong.class)) { - ps = GObjects.paramSpecLong(p.name(), p.name(), p.name(), Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecLong(name, name, name, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecPointer.class)) { - ps = GObjects.paramSpecPointer(p.name(), p.name(), p.name(), getFlags(p)); + ps = GObjects.paramSpecPointer(name, name, name, getFlags(p)); } else if (paramspec.equals(ParamSpecString.class)) { - ps = GObjects.paramSpecString(p.name(), p.name(), p.name(), null, getFlags(p)); + ps = GObjects.paramSpecString(name, name, name, null, getFlags(p)); } else if (paramspec.equals(ParamSpecUChar.class)) { - ps = GObjects.paramSpecUchar(p.name(), p.name(), p.name(), (byte) 0, Byte.MAX_VALUE, (byte) 0, getFlags(p)); + ps = GObjects.paramSpecUchar(name, name, name, (byte) 0, Byte.MAX_VALUE, (byte) 0, getFlags(p)); } else if (paramspec.equals(ParamSpecUInt.class)) { - ps = GObjects.paramSpecUint(p.name(), p.name(), p.name(), 0, Integer.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecUint(name, name, name, 0, Integer.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecUInt64.class)) { - ps = GObjects.paramSpecUint64(p.name(), p.name(), p.name(), 0, Long.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecUint64(name, name, name, 0, Long.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecULong.class)) { - ps = GObjects.paramSpecUlong(p.name(), p.name(), p.name(), 0, Integer.MAX_VALUE, 0, getFlags(p)); + ps = GObjects.paramSpecUlong(name, name, name, 0, Integer.MAX_VALUE, 0, getFlags(p)); } else if (paramspec.equals(ParamSpecUnichar.class)) { - ps = GObjects.paramSpecUnichar(p.name(), p.name(), p.name(), 0, getFlags(p)); + ps = GObjects.paramSpecUnichar(name, name, name, 0, getFlags(p)); } else { GLib.log(LOG_DOMAIN, LogLevelFlags.LEVEL_CRITICAL, "Unsupported ParamSpec %s in class %s:\n", @@ -297,7 +311,7 @@ public static Consumer i return null; } propertySpecs.add(ps); - propertyNames.add(p.name()); + propertyNames.add(name); } // No properties found? @@ -314,7 +328,11 @@ public static Consumer i continue; } Property property = method.getDeclaredAnnotation(Property.class); - int idx = propertyNames.indexOf(property.name()); + + // Name is specified with the annotation, or infer it form the method name + String name = property.name().isEmpty() ? getPropertyName(method.getName()) : property.name(); + + int idx = propertyNames.indexOf(name); // Returns void -> setter, else -> getter if (method.getReturnType().equals(void.class)) { diff --git a/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java b/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java index 038c2a68..9bdc3c27 100644 --- a/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java +++ b/modules/gobject/src/test/java/io/github/jwharm/javagi/test/gobject/DerivedClassTest.java @@ -145,12 +145,12 @@ public void setStringProperty(String value) { private boolean boolProperty = false; - @Property(name="bool-property") + @Property // name will be inferred: "bool-property" public boolean getBoolProperty() { return boolProperty; } - @Property(name="bool-property") + @Property public void setBoolProperty(boolean boolProperty) { this.boolProperty = boolProperty; } diff --git a/website/docs/register.md b/website/docs/register.md index 8896a089..b200c461 100644 --- a/website/docs/register.md +++ b/website/docs/register.md @@ -43,10 +43,12 @@ Finally, add the default memory-address-constructor for Java-GI Proxy objects: This constructor must exist in all Java-GI Proxy objects. It enables a Proxy class to be instantiated automatically for new instances returned from native function calls. -If your application is module-based, you must export your package to the `org.gnome.glib` module in your `module-info.java` file, to allow the reflection to work: +If your application is module-based, you must export your package to the `org.gnome.gobject` module in your `module-info.java` file, to allow the reflection to work: ``` -exports [package.name] to org.gnome.glib; +module my.module.name { + exports my.package.name to org.gnome.gobject; +} ``` ## Specifying the name of the GType @@ -84,19 +86,19 @@ public void finalize_() { ## Properties -You can define GObject properties with the `@Property` annotation on the getter and setter methods. You must annotate both the getter and setter methods (if applicable). The `@Property` annotation must always specify the `name` parameter; all other parameters are optional. +You can define GObject properties with the `@Property` annotation on the getter and setter methods. You must annotate both the getter and setter methods (if applicable). The `@Property` annotation can optionally specify the `name` parameter; all other parameters are optional. Example definition of an `int` property with name `n-items`: ```java private int size; -@Property(name="n-items") +@Property public int getNItems() { return size; } -@Property(name="n-items") +@Property public void setNItems(int nItems) { size = nItems; } @@ -106,7 +108,7 @@ The `@Property` annotation accepts the following parameters: | Parameter | Type | Default value | |----------------|-----------|---------------| -| name | Mandatory | n/a | +| name | String | inferred | | type | ParamSpec | inferred | | readable | Boolean | true | | writable | Boolean | true | @@ -115,6 +117,8 @@ The `@Property` annotation accepts the following parameters: | explicitNotify | Boolean | false | | deprecated | Boolean | false | +When the name is not specified, it will be inferred from the name of the method (provided that the method names follow the `getX`/`setX` pattern), stripping the "get" or "set" prefix and converting CamelCase to kebab-case. If you do specify a name, it must be present on **both** the getter and setter methods (otherwise Java-GI will create two properties, with different names). + When the type is not specified, it will be inferred from the parameter or return-type of the method. When the type is specified, it must be one of the subclasses of `GParamSpec`. The boolean parameters are `GParamFlags` arguments, and are documented [here](https://docs.gtk.org/gobject/flags.ParamFlags.html). ## Class and instance init functions @@ -163,7 +167,7 @@ public class Counter extends GObject { } ``` -The "limit-reached" signal is defined with a functional interface annotated as `@Signal`. The method signature of the functional interface is used to define the signal parameters and return value. The signal name is inferred from the interface too (converting CamelCase to kebab-case) but can be overridden. +The "limit-reached" signal in the example is defined with a functional interface annotated as `@Signal`. The method signature of the functional interface is used to define the signal parameters and return value. The signal name is inferred from the interface too (converting CamelCase to kebab-case) but can be overridden. You can connect to the custom signal, like this: @@ -173,7 +177,7 @@ counter.connect("limit-reached", (Counter.LimitReached) (limit) -> { } ``` -Because the signal declaration is an ordinary functional interface, the declaration in the above example can also be declared as an `IntConsumer`: +Because the signal declaration is an ordinary functional interface, it is equally valid to extend from a standard functional interface like `Runnable`, `BooleanSupplier`, or any other one, like (in the above example) an `IntConsumer`: ``` @Signal