X Tutup
Skip to content

WIP: form validation#111440

Draft
xuhuisheng wants to merge 1 commit intogodotengine:masterfrom
xuhuisheng:dev/form_validation
Draft

WIP: form validation#111440
xuhuisheng wants to merge 1 commit intogodotengine:masterfrom
xuhuisheng:dev/form_validation

Conversation

@xuhuisheng
Copy link
Contributor

@xuhuisheng xuhuisheng commented Oct 9, 2025

I am working on adding form validation feature in Editor Inspector.

With this feature, we can get a warning color when updating property value is invalid.

And this feature allow us editing unsaved invalid value, without revert invalid value to previous every key pressed.

It is graceful for any suggestion. Thank you very much.

Without validation, we cannot get 'foo'.

without-valid.webm

With validation, we can modify property value to an invalid value and keep editing it.

form-validation.webm

MRP

mrp-validation.zip

@xuhuisheng xuhuisheng requested a review from a team as a code owner October 9, 2025 06:37
@xuhuisheng xuhuisheng requested a review from a team October 9, 2025 06:37
@xuhuisheng xuhuisheng requested a review from a team as a code owner October 9, 2025 06:37
@xuhuisheng xuhuisheng marked this pull request as draft October 9, 2025 06:39
@AThousandShips AThousandShips added this to the 4.6 milestone Oct 9, 2025
@xuhuisheng xuhuisheng force-pushed the dev/form_validation branch 4 times, most recently from ab218fd to da9d80d Compare October 10, 2025 02:05
@chocola-mint
Copy link
Contributor

is_valid_property_value() should also be implementable by GDScript/GDExtension. I suggest taking a look at #94047 for inspiration.

@xuhuisheng
Copy link
Contributor Author

@chocola-mint
I am not familiar with GDExtension. Should I define a private _is_valid_property_value() for child class?

Something like this?

bool Resource::is_valid_property_value(const String &p_path, const Variant &p_value, String &p_error_message) const
  return _is_valid_property_value(); 
}

bool Resource::_is_valid_prooperty_value(const String &p_path, const Variant &p_value, String &p_error_message) const
  return true;
}

@chocola-mint
Copy link
Contributor

chocola-mint commented Oct 13, 2025

@xuhuisheng

I would write it like this:

bool Resource::is_valid_property_value(const String &p_path, const Variant &p_value, String &p_error_message) const {
  if (script_instance) {
    bool valid;
    String ret = script_instance->is_valid_property_value(&valid);
    if (valid) {
      return ret;
    }
  }
  if (_extension && _extension->is_valid_property_value) {
    bool ret;
    GDExtensionBool is_valid;
    _extension->is_valid_property_value(_extension_instance, &is_valid, &ret);
    if (is_valid) {
      return ret;
    }
  }
  return _is_valid_property_value(); 
}

bool Resource::_is_valid_property_value(const String &p_path, const Variant &p_value, String &p_error_message) const
  return true;
}

You'll need to declare a function pointer called GDExtensionClassIsValidPropertyValue here. Something like: typedef void (*GDExtensionClassIsValidPropertyValue)(GDExtensionClassInstancePtr p_instance, GDExtensionBool *r_is_valid, GDExtensionBool* p_out);

typedef const GDExtensionPropertyInfo *(*GDExtensionClassGetPropertyList)(GDExtensionClassInstancePtr p_instance, uint32_t *r_count);
typedef void (*GDExtensionClassFreePropertyList)(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list);
typedef void (*GDExtensionClassFreePropertyList2)(GDExtensionClassInstancePtr p_instance, const GDExtensionPropertyInfo *p_list, uint32_t p_count);
typedef GDExtensionBool (*GDExtensionClassPropertyCanRevert)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name);
typedef GDExtensionBool (*GDExtensionClassPropertyGetRevert)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, GDExtensionVariantPtr r_ret);
typedef GDExtensionBool (*GDExtensionClassValidateProperty)(GDExtensionClassInstancePtr p_instance, GDExtensionPropertyInfo *p_property);
typedef void (*GDExtensionClassNotification)(GDExtensionClassInstancePtr p_instance, int32_t p_what); // Deprecated. Use GDExtensionClassNotification2 instead.
typedef void (*GDExtensionClassNotification2)(GDExtensionClassInstancePtr p_instance, int32_t p_what, GDExtensionBool p_reversed);
typedef void (*GDExtensionClassToString)(GDExtensionClassInstancePtr p_instance, GDExtensionBool *r_is_valid, GDExtensionStringPtr p_out);
typedef void (*GDExtensionClassReference)(GDExtensionClassInstancePtr p_instance);
typedef void (*GDExtensionClassUnreference)(GDExtensionClassInstancePtr p_instance);
typedef void (*GDExtensionClassCallVirtual)(GDExtensionClassInstancePtr p_instance, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);
typedef GDExtensionObjectPtr (*GDExtensionClassCreateInstance)(void *p_class_userdata);
typedef GDExtensionObjectPtr (*GDExtensionClassCreateInstance2)(void *p_class_userdata, GDExtensionBool p_notify_postinitialize);
typedef void (*GDExtensionClassFreeInstance)(void *p_class_userdata, GDExtensionClassInstancePtr p_instance);
typedef GDExtensionClassInstancePtr (*GDExtensionClassRecreateInstance)(void *p_class_userdata, GDExtensionObjectPtr p_object);
typedef GDExtensionClassCallVirtual (*GDExtensionClassGetVirtual)(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
typedef GDExtensionClassCallVirtual (*GDExtensionClassGetVirtual2)(void *p_class_userdata, GDExtensionConstStringNamePtr p_name, uint32_t p_hash);
typedef void *(*GDExtensionClassGetVirtualCallData)(void *p_class_userdata, GDExtensionConstStringNamePtr p_name);
typedef void *(*GDExtensionClassGetVirtualCallData2)(void *p_class_userdata, GDExtensionConstStringNamePtr p_name, uint32_t p_hash);
typedef void (*GDExtensionClassCallVirtualWithData)(GDExtensionClassInstancePtr p_instance, GDExtensionConstStringNamePtr p_name, void *p_virtual_call_userdata, const GDExtensionConstTypePtr *p_args, GDExtensionTypePtr r_ret);

Then, add a placeholder function with the same signature in PlaceholderExtensionInstance. Something like this:

static void placeholder_instance_is_valid_property_value(GDExtensionClassInstancePtr p_instance, GDExtensionBool *r_is_valid, GDExtensionBool *p_out) {
  *r_is_valid = true;
}

static void placeholder_instance_notification(GDExtensionClassInstancePtr p_instance, int32_t p_what, GDExtensionBool p_reversed) {
}
static void placeholder_instance_to_string(GDExtensionClassInstancePtr p_instance, GDExtensionBool *r_is_valid, GDExtensionStringPtr p_out) {
*r_is_valid = true;
}
static void placeholder_instance_reference(GDExtensionClassInstancePtr p_instance) {
}
static void placeholder_instance_unreference(GDExtensionClassInstancePtr p_instance) {
}
static uint64_t placeholder_instance_get_rid(GDExtensionClassInstancePtr p_instance) {
return 0;
}

You'll then need to add GDExtensionClassIsValidPropertyValue is_valid_property_value_func to GDExtensionClassCreationInfo4. (I think you only need to do it for the non-deprecated version but I'm not sure)

typedef struct {
GDExtensionBool is_virtual;
GDExtensionBool is_abstract;
GDExtensionBool is_exposed;
GDExtensionBool is_runtime;
GDExtensionConstStringPtr icon_path;
GDExtensionClassSet set_func;
GDExtensionClassGet get_func;
GDExtensionClassGetPropertyList get_property_list_func;
GDExtensionClassFreePropertyList2 free_property_list_func;
GDExtensionClassPropertyCanRevert property_can_revert_func;
GDExtensionClassPropertyGetRevert property_get_revert_func;
GDExtensionClassValidateProperty validate_property_func;
GDExtensionClassNotification2 notification_func;
GDExtensionClassToString to_string_func;
GDExtensionClassReference reference_func;
GDExtensionClassUnreference unreference_func;
GDExtensionClassCreateInstance2 create_instance_func; // (Default) constructor; mandatory. If the class is not instantiable, consider making it virtual or abstract.
GDExtensionClassFreeInstance free_instance_func; // Destructor; mandatory.
GDExtensionClassRecreateInstance recreate_instance_func;
// Queries a virtual function by name and returns a callback to invoke the requested virtual function.
GDExtensionClassGetVirtual2 get_virtual_func;
// Paired with `call_virtual_with_data_func`, this is an alternative to `get_virtual_func` for extensions that
// need or benefit from extra data when calling virtual functions.
// Returns user data that will be passed to `call_virtual_with_data_func`.
// Returning `NULL` from this function signals to Godot that the virtual function is not overridden.
// Data returned from this function should be managed by the extension and must be valid until the extension is deinitialized.
// You should supply either `get_virtual_func`, or `get_virtual_call_data_func` with `call_virtual_with_data_func`.
GDExtensionClassGetVirtualCallData2 get_virtual_call_data_func;
// Used to call virtual functions when `get_virtual_call_data_func` is not null.
GDExtensionClassCallVirtualWithData call_virtual_with_data_func;
void *class_userdata; // Per-class user data, later accessible in instance bindings.
} GDExtensionClassCreationInfo4;

And then, add GDExtensionClassIsValidProeprtyValue is_valid_property_value to ObjectGDExtension.

godot/core/object/object.h

Lines 335 to 350 in cb7cd81

GDExtensionClassSet set;
GDExtensionClassGet get;
GDExtensionClassGetPropertyList get_property_list;
GDExtensionClassFreePropertyList2 free_property_list2;
GDExtensionClassPropertyCanRevert property_can_revert;
GDExtensionClassPropertyGetRevert property_get_revert;
GDExtensionClassValidateProperty validate_property;
#ifndef DISABLE_DEPRECATED
GDExtensionClassNotification notification;
GDExtensionClassFreePropertyList free_property_list;
#endif // DISABLE_DEPRECATED
GDExtensionClassNotification2 notification2;
GDExtensionClassToString to_string;
GDExtensionClassReference reference;
GDExtensionClassReference unreference;
GDExtensionClassGetRID get_rid;

You'll also need to assign is_valid_property_value here:

extension->gdextension.to_string = p_extension_funcs->to_string_func;

And also assign the placeholder &PlaceholderExtensionInstance::placeholder_instance_is_valid_property_value here.

placeholder_extension->set = &PlaceholderExtensionInstance::placeholder_instance_set;
placeholder_extension->get = &PlaceholderExtensionInstance::placeholder_instance_get;
placeholder_extension->get_property_list = &PlaceholderExtensionInstance::placeholder_instance_get_property_list;
placeholder_extension->free_property_list2 = &PlaceholderExtensionInstance::placeholder_instance_free_property_list;
placeholder_extension->property_can_revert = &PlaceholderExtensionInstance::placeholder_instance_property_can_revert;
placeholder_extension->property_get_revert = &PlaceholderExtensionInstance::placeholder_instance_property_get_revert;
placeholder_extension->validate_property = &PlaceholderExtensionInstance::placeholder_instance_validate_property;
#ifndef DISABLE_DEPRECATED
placeholder_extension->notification = nullptr;
placeholder_extension->free_property_list = nullptr;
#endif // DISABLE_DEPRECATED
placeholder_extension->notification2 = &PlaceholderExtensionInstance::placeholder_instance_notification;
placeholder_extension->to_string = &PlaceholderExtensionInstance::placeholder_instance_to_string;
placeholder_extension->reference = &PlaceholderExtensionInstance::placeholder_instance_reference;
placeholder_extension->unreference = &PlaceholderExtensionInstance::placeholder_instance_unreference;
placeholder_extension->get_rid = &PlaceholderExtensionInstance::placeholder_instance_get_rid;

Take a look at how to_string() is implemented (GDExtensionClassToString) and it shouldn't be too hard to figure out where to go next from there.

@xuhuisheng xuhuisheng force-pushed the dev/form_validation branch 4 times, most recently from 11dd819 to 00c0265 Compare October 18, 2025 08:58
@xuhuisheng xuhuisheng force-pushed the dev/form_validation branch 2 times, most recently from 6117231 to 31478b2 Compare October 18, 2025 14:49
@Repiteo Repiteo modified the milestones: 4.6, 4.x Jan 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support validating property value before real update the property in the Inspector TileSet custom data layers check for name clashes while typing

4 participants

X Tutup