├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── addons └── godot-form-validator │ ├── control_validator.gd │ ├── editor_icon.png │ ├── editor_icon.png.import │ ├── example │ ├── example.gd │ └── example.tscn │ ├── form_validator.gd │ ├── plugin.cfg │ ├── plugin.gd │ ├── rules │ ├── alpha_rule.gd │ ├── alphanumeric_rule.gd │ ├── boolean_rule.gd │ ├── custom_rule.gd │ ├── does_not_match_rule.gd │ ├── email_rule.gd │ ├── equals_rule.gd │ ├── greater_than_rule.gd │ ├── length_rule.gd │ ├── less_than_rule.gd │ ├── matches_rule.gd │ ├── not_blank_rule.gd │ ├── not_empty_rule.gd │ ├── numeric_rule.gd │ ├── required_rule.gd │ ├── rule_result.gd │ └── validator_rule.gd │ ├── validation.gd │ ├── validator_functions.gd │ └── validators │ ├── button_validator.gd │ ├── line_edit_validator.gd │ ├── range_validator.gd │ ├── text_edit_validator.gd │ └── validator.gd ├── example_rules.png ├── example_rules.png.import ├── example_tree.png ├── example_tree.png.import ├── icon.png ├── icon.png.import ├── icon.svg ├── icon.svg.import ├── logo.png ├── logo.png.import └── project.godot /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 deadpixelsociety 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # godot-form-validator 2 | 3 | ![logo](logo.png) 4 | 5 | A Godot 4 plugin for adding validation logic to any control. 6 | 7 | An [example scene](addons/godot-form-validator/example/example.tscn) is provided. 8 | 9 | ![example scene tree](example_tree.png) 10 | 11 | ## FormValidator 12 | 13 | The ```FormValidator``` control should sit as a parent (direct or indirect) to the controls you wish to validate. 14 | 15 | ### Signals 16 | * ```control_validated``` - Emitted when a control has been validated either through auto validation or after calling ```FormValidator.validate()```. 17 | * ```control``` - The control that was validated. 18 | * ```passed``` - True if the control passed validation; otherwise, false. 19 | * ```messages``` - A ```PackedStringArray``` of failed validation messages, if any. 20 | 21 | ### Properties 22 | 23 | * Auto Validate - If true then controls will trigger validation logic when they lose focus (or their value is changed in the case of ```Range``` controls or controls that provide a ```value_changed``` signal.). Otherwise, validation must be invoked manually via ```FormValidator.validate()```. 24 | * Validation Method - Determines how validation logic is processed for each ```ControlValidator``` and the ```FormValidator``` as a whole. 25 | * Batch - Processes all validators and rules before returning a result. 26 | * Fail Fast - Processes validators until a rule failure is encountered, then proceeds to the next validator. 27 | * Immediate - Processes validators until a rule failure is encountered and returns immediately. 28 | 29 | 30 | ## ControlValidator 31 | 32 | The ```ControlValidator``` node should sit as a child to the control node you wish to validate. 33 | 34 | ### Properties 35 | 36 | * Validator - The ```Validator``` that is invoked to process validation logic for it's parent control. This will be automatically assigned if a compatible validator is registered with the ```Validation``` class. Otherwise a default ```Validator``` instance will be assigned, but can be replaced with a more specific or custom one as you see fit. 37 | 38 | Refer to ```ButtonValidator```, ```LineEditValidator```, ```RangeValidator``` and ```TextEditValidator``` for examples. 39 | 40 | See ```Validation.add_validator()``` to add custom validators. 41 | 42 | 43 | ## Validator 44 | 45 | The ```Validator``` determines how validation logic is processed for a given control. 46 | 47 | ### Properties 48 | * Validation Order - Determines the order in which this validator is processed relative to others. Most useful for the Immediate validation method as this will determine which control fails first. 49 | * Validation Method - Matches the value of ```FormValidator.validation_method``` by default, but can be overridden for control-specific needs. 50 | * Skip Validation - If true validation for this control will be skipped. 51 | * Rules - The validation rules applied to the control. See ```Rule```. 52 | 53 | 54 | ## Rule 55 | 56 | Rules provide the actual validation logic by comparing the validating control's current value to the expected result. Several rules are provided by default and custom rules can be implemented via the ```CustomRule``` expression or by extending the ```ValidatorRule``` class. 57 | 58 | ![example rules](example_rules.png) 59 | 60 | ### Provided Rules 61 | 62 | * Alphanumeric - Text must be alphanumeric (having letters and numbers only). 63 | * Alpha - Text must contain letters only. 64 | * Boolean - The control value must equal the specified boolean value. Useful for buttons or checkboxes. 65 | * Custom - The custom expression is evaluated and must return true for passing, or false. Two arguments are passed: 66 | * ```control``` - The control being validated. 67 | * ```value``` - The control value being validated. 68 | * Does Not Match - Text must not match the specified regular expression. 69 | * Email - Text must be a valid email address. 70 | * Equals - The control value must equal the specified value. 71 | * Greater Than - The control value must be greater than the specified value. 72 | * Length - Text must have a string length between the specified minimum and maximum. 73 | * Less Than - The control value must be less than the specified value. 74 | * Matches - Text must match the specified regular expression. 75 | * Not Blank - The control value must not be blank (not empty and not only whitespace). 76 | * Not Empty - The control value must not be empty (has a string length greater than 0, whitespace included). 77 | * Numeric - Text must contain numbers only. 78 | * Required - The control value is required. Same as Not Blank, with a different validation message. 79 | -------------------------------------------------------------------------------- /addons/godot-form-validator/control_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | class_name ControlValidator 4 | 5 | @export var validator: Validator: 6 | set(value): 7 | validator = value 8 | _on_validator_added() 9 | 10 | 11 | func _ready() -> void: 12 | tree_entered.connect(_on_tree_entered) 13 | tree_exiting.connect(_on_tree_exiting) 14 | _configure_validator() 15 | _on_validator_added() 16 | 17 | 18 | func _configure_validator() -> void: 19 | if validator != null: 20 | return 21 | var parent = get_parent() 22 | if not parent: 23 | return 24 | var found = Validation.find_validator(parent) 25 | if found: 26 | validator = found.duplicate(true) 27 | else: 28 | validator = Validator.new() 29 | 30 | 31 | func _get_configuration_warnings() -> PackedStringArray: 32 | if not validator: 33 | return [] 34 | var list = PackedStringArray() 35 | for rule in validator.rules: 36 | if not rule.is_valid(): 37 | list.append(rule.get_invalid_message()) 38 | return list 39 | 40 | 41 | func _on_tree_entered() -> void: 42 | _configure_validator() 43 | _on_validator_added() 44 | 45 | 46 | func _on_tree_exiting() -> void: 47 | _on_validator_removed() 48 | 49 | 50 | func _on_validator_added() -> void: 51 | var parent = get_parent() 52 | if not parent or not validator: 53 | return 54 | Validation.validator_added.emit(parent, validator) 55 | update_configuration_warnings() 56 | 57 | 58 | func _on_validator_removed() -> void: 59 | var parent = get_parent() 60 | if not parent: 61 | return 62 | Validation.validator_removed.emit(parent) 63 | -------------------------------------------------------------------------------- /addons/godot-form-validator/editor_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadpixelsociety/godot-form-validator/24d1742aa09967bd46c25243af91efb064a7c803/addons/godot-form-validator/editor_icon.png -------------------------------------------------------------------------------- /addons/godot-form-validator/editor_icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://hohynkx70xob" 6 | path="res://.godot/imported/editor_icon.png-6f130a2788dfaa185bebbdd85ed98d66.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/godot-form-validator/editor_icon.png" 14 | dest_files=["res://.godot/imported/editor_icon.png-6f130a2788dfaa185bebbdd85ed98d66.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /addons/godot-form-validator/example/example.gd: -------------------------------------------------------------------------------- 1 | extends PanelContainer 2 | 3 | @onready var form_validator_control: FormValidator = $MarginContainer/FormValidator 4 | 5 | func _on_form_validator_control_control_validated(control, passed, messages) -> void: 6 | var error_label = _get_error_label(control) 7 | var valid_label = _get_valid_label(control) 8 | if passed: 9 | if error_label: 10 | error_label.hide() 11 | if valid_label: 12 | valid_label.show() 13 | else: 14 | if error_label: 15 | error_label.text = ", ".join(messages) 16 | error_label.show() 17 | if valid_label: 18 | valid_label.hide() 19 | 20 | 21 | func _get_error_label(control: Control) -> Label: 22 | return find_child(control.name + "Error", true, false) as Label 23 | 24 | 25 | func _get_valid_label(control: Control) -> Label: 26 | return find_child(control.name + "Valid", true, false) as Label 27 | 28 | 29 | func _on_validate_pressed() -> void: 30 | form_validator_control.validate() 31 | 32 | 33 | func _on_exit_pressed() -> void: 34 | get_tree().quit() 35 | -------------------------------------------------------------------------------- /addons/godot-form-validator/example/example.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=41 format=3 uid="uid://dpi5vv8r80o7r"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot-form-validator/example/example.gd" id="1_8caq6"] 4 | [ext_resource type="Script" path="res://addons/godot-form-validator/form_validator.gd" id="2_mtxo0"] 5 | [ext_resource type="Script" path="res://addons/godot-form-validator/control_validator.gd" id="3_hkwup"] 6 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/alpha_rule.gd" id="4_dq1df"] 7 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/required_rule.gd" id="5_cu56g"] 8 | [ext_resource type="Script" path="res://addons/godot-form-validator/validators/line_edit_validator.gd" id="5_rknpr"] 9 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/numeric_rule.gd" id="7_6vtuw"] 10 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/alphanumeric_rule.gd" id="8_mu0ia"] 11 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/email_rule.gd" id="9_4ynyw"] 12 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/length_rule.gd" id="10_iuueu"] 13 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/greater_than_rule.gd" id="11_1tya3"] 14 | [ext_resource type="Script" path="res://addons/godot-form-validator/validators/range_validator.gd" id="11_51cp7"] 15 | [ext_resource type="Script" path="res://addons/godot-form-validator/validators/button_validator.gd" id="13_eri47"] 16 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/custom_rule.gd" id="13_ibg52"] 17 | [ext_resource type="Script" path="res://addons/godot-form-validator/rules/boolean_rule.gd" id="15_xjpko"] 18 | 19 | [sub_resource type="LabelSettings" id="LabelSettings_nw8ds"] 20 | font_size = 24 21 | 22 | [sub_resource type="Resource" id="Resource_i25hd"] 23 | script = ExtResource("5_cu56g") 24 | fail_message = "A value is required." 25 | 26 | [sub_resource type="Resource" id="Resource_4mimn"] 27 | script = ExtResource("4_dq1df") 28 | fail_message = "Value must contain only alpha characters." 29 | 30 | [sub_resource type="Resource" id="Resource_15man"] 31 | script = ExtResource("5_rknpr") 32 | validation_order = 1 33 | validation_method = 0 34 | skip_validation = false 35 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_i25hd"), SubResource("Resource_4mimn")]) 36 | 37 | [sub_resource type="LabelSettings" id="LabelSettings_su7t3"] 38 | font_color = Color(1, 0.568627, 0.580392, 1) 39 | 40 | [sub_resource type="LabelSettings" id="LabelSettings_urp4f"] 41 | font_color = Color(0.254902, 0.831373, 0.556863, 1) 42 | 43 | [sub_resource type="Resource" id="Resource_j4pud"] 44 | script = ExtResource("5_cu56g") 45 | fail_message = "A value is required." 46 | 47 | [sub_resource type="Resource" id="Resource_xxhfq"] 48 | script = ExtResource("7_6vtuw") 49 | fail_message = "Value must contain only numbers." 50 | 51 | [sub_resource type="Resource" id="Resource_1t3q0"] 52 | script = ExtResource("5_rknpr") 53 | validation_order = 1 54 | validation_method = 0 55 | skip_validation = false 56 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_j4pud"), SubResource("Resource_xxhfq")]) 57 | 58 | [sub_resource type="Resource" id="Resource_ftvbf"] 59 | script = ExtResource("8_mu0ia") 60 | fail_message = "Value must contain only alphanumeric cahracters." 61 | 62 | [sub_resource type="Resource" id="Resource_my5ss"] 63 | script = ExtResource("5_cu56g") 64 | fail_message = "A value is required." 65 | 66 | [sub_resource type="Resource" id="Resource_61y5a"] 67 | script = ExtResource("5_rknpr") 68 | validation_order = 1 69 | validation_method = 0 70 | skip_validation = false 71 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_ftvbf"), SubResource("Resource_my5ss")]) 72 | 73 | [sub_resource type="Resource" id="Resource_f3i8j"] 74 | script = ExtResource("9_4ynyw") 75 | fail_message = "Value must be a valid email address." 76 | 77 | [sub_resource type="Resource" id="Resource_l5e3w"] 78 | script = ExtResource("5_cu56g") 79 | fail_message = "A value is required." 80 | 81 | [sub_resource type="Resource" id="Resource_q4oax"] 82 | script = ExtResource("5_rknpr") 83 | validation_order = 1 84 | validation_method = 0 85 | skip_validation = false 86 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_f3i8j"), SubResource("Resource_l5e3w")]) 87 | 88 | [sub_resource type="Resource" id="Resource_d1g5r"] 89 | script = ExtResource("5_cu56g") 90 | fail_message = "A value is required." 91 | 92 | [sub_resource type="Resource" id="Resource_jqdfw"] 93 | script = ExtResource("10_iuueu") 94 | min_length = 5 95 | max_length = 10 96 | fail_message = "" 97 | 98 | [sub_resource type="Resource" id="Resource_62odm"] 99 | script = ExtResource("5_rknpr") 100 | validation_order = 1 101 | validation_method = 0 102 | skip_validation = false 103 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_d1g5r"), SubResource("Resource_jqdfw")]) 104 | 105 | [sub_resource type="Resource" id="Resource_qm2iv"] 106 | script = ExtResource("5_cu56g") 107 | fail_message = "A value is required." 108 | 109 | [sub_resource type="Resource" id="Resource_brbah"] 110 | script = ExtResource("11_1tya3") 111 | target_value = "3" 112 | fail_message = "" 113 | 114 | [sub_resource type="Resource" id="Resource_smogv"] 115 | script = ExtResource("11_51cp7") 116 | validation_order = 1 117 | validation_method = 0 118 | skip_validation = false 119 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_qm2iv"), SubResource("Resource_brbah")]) 120 | 121 | [sub_resource type="Resource" id="Resource_4riv3"] 122 | script = ExtResource("13_ibg52") 123 | expression = "control.color.r > 0.5" 124 | fail_message = "The red channel must be greater than 0.5." 125 | 126 | [sub_resource type="Resource" id="Resource_mh7td"] 127 | script = ExtResource("13_eri47") 128 | validation_order = 1 129 | validation_method = 0 130 | skip_validation = false 131 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_4riv3")]) 132 | 133 | [sub_resource type="Resource" id="Resource_3nuq2"] 134 | script = ExtResource("15_xjpko") 135 | target_value = true 136 | fail_message = "The check box must be checked." 137 | 138 | [sub_resource type="Resource" id="Resource_kbsj8"] 139 | script = ExtResource("13_eri47") 140 | validation_order = -1 141 | validation_method = 0 142 | skip_validation = false 143 | rules = Array[Resource("res://addons/godot-form-validator/rules/validator_rule.gd")]([SubResource("Resource_3nuq2")]) 144 | 145 | [node name="Example" type="PanelContainer"] 146 | anchors_preset = 15 147 | anchor_right = 1.0 148 | anchor_bottom = 1.0 149 | grow_horizontal = 2 150 | grow_vertical = 2 151 | script = ExtResource("1_8caq6") 152 | 153 | [node name="MarginContainer" type="MarginContainer" parent="."] 154 | layout_mode = 2 155 | theme_override_constants/margin_left = 32 156 | theme_override_constants/margin_top = 32 157 | theme_override_constants/margin_right = 32 158 | theme_override_constants/margin_bottom = 32 159 | 160 | [node name="FormValidator" type="Control" parent="MarginContainer"] 161 | layout_mode = 2 162 | script = ExtResource("2_mtxo0") 163 | auto_validate = true 164 | 165 | [node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/FormValidator"] 166 | layout_mode = 1 167 | anchors_preset = 15 168 | anchor_right = 1.0 169 | anchor_bottom = 1.0 170 | grow_horizontal = 2 171 | grow_vertical = 2 172 | 173 | [node name="Title" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2"] 174 | layout_mode = 2 175 | text = "Godot Form Validator Examples" 176 | label_settings = SubResource("LabelSettings_nw8ds") 177 | horizontal_alignment = 1 178 | vertical_alignment = 1 179 | 180 | [node name="GridContainer" type="GridContainer" parent="MarginContainer/FormValidator/VBoxContainer2"] 181 | layout_mode = 2 182 | size_flags_vertical = 3 183 | theme_override_constants/h_separation = 16 184 | theme_override_constants/v_separation = 16 185 | columns = 4 186 | 187 | [node name="Label" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 188 | layout_mode = 2 189 | text = "Alpha" 190 | vertical_alignment = 1 191 | 192 | [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 193 | layout_mode = 2 194 | size_flags_horizontal = 3 195 | 196 | [node name="Alpha" type="LineEdit" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer"] 197 | layout_mode = 2 198 | 199 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer/Alpha"] 200 | script = ExtResource("3_hkwup") 201 | validator = SubResource("Resource_15man") 202 | 203 | [node name="AlphaError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer"] 204 | visible = false 205 | layout_mode = 2 206 | text = "ERROR" 207 | label_settings = SubResource("LabelSettings_su7t3") 208 | autowrap_mode = 2 209 | 210 | [node name="AlphaValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer"] 211 | visible = false 212 | layout_mode = 2 213 | text = "Valid!" 214 | label_settings = SubResource("LabelSettings_urp4f") 215 | 216 | [node name="Label2" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 217 | layout_mode = 2 218 | text = "Numeric" 219 | vertical_alignment = 1 220 | 221 | [node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 222 | layout_mode = 2 223 | size_flags_horizontal = 3 224 | 225 | [node name="Numeric" type="LineEdit" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer2"] 226 | layout_mode = 2 227 | 228 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer2/Numeric"] 229 | script = ExtResource("3_hkwup") 230 | validator = SubResource("Resource_1t3q0") 231 | 232 | [node name="NumericError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer2"] 233 | visible = false 234 | layout_mode = 2 235 | text = "ERROR" 236 | label_settings = SubResource("LabelSettings_su7t3") 237 | autowrap_mode = 2 238 | 239 | [node name="NumericValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer2"] 240 | visible = false 241 | layout_mode = 2 242 | text = "Valid!" 243 | label_settings = SubResource("LabelSettings_urp4f") 244 | 245 | [node name="Label3" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 246 | layout_mode = 2 247 | text = "Alphanumeric" 248 | vertical_alignment = 1 249 | 250 | [node name="VBoxContainer3" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 251 | layout_mode = 2 252 | size_flags_horizontal = 3 253 | 254 | [node name="Alphanumeric" type="LineEdit" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer3"] 255 | layout_mode = 2 256 | 257 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer3/Alphanumeric"] 258 | script = ExtResource("3_hkwup") 259 | validator = SubResource("Resource_61y5a") 260 | 261 | [node name="AlphanumericError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer3"] 262 | visible = false 263 | layout_mode = 2 264 | text = "ERROR" 265 | label_settings = SubResource("LabelSettings_su7t3") 266 | autowrap_mode = 2 267 | 268 | [node name="AlphanumericValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer3"] 269 | visible = false 270 | layout_mode = 2 271 | text = "Valid!" 272 | label_settings = SubResource("LabelSettings_urp4f") 273 | 274 | [node name="Label4" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 275 | layout_mode = 2 276 | text = "Email" 277 | vertical_alignment = 1 278 | 279 | [node name="VBoxContainer4" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 280 | layout_mode = 2 281 | size_flags_horizontal = 3 282 | 283 | [node name="Email" type="LineEdit" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer4"] 284 | layout_mode = 2 285 | 286 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer4/Email"] 287 | script = ExtResource("3_hkwup") 288 | validator = SubResource("Resource_q4oax") 289 | 290 | [node name="EmailError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer4"] 291 | visible = false 292 | layout_mode = 2 293 | text = "ERROR" 294 | label_settings = SubResource("LabelSettings_su7t3") 295 | autowrap_mode = 2 296 | 297 | [node name="EmailValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer4"] 298 | visible = false 299 | layout_mode = 2 300 | text = "Valid!" 301 | label_settings = SubResource("LabelSettings_urp4f") 302 | 303 | [node name="Label5" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 304 | layout_mode = 2 305 | text = "Length" 306 | vertical_alignment = 1 307 | 308 | [node name="VBoxContainer5" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 309 | layout_mode = 2 310 | size_flags_horizontal = 3 311 | 312 | [node name="Length" type="LineEdit" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer5"] 313 | layout_mode = 2 314 | 315 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer5/Length"] 316 | script = ExtResource("3_hkwup") 317 | validator = SubResource("Resource_62odm") 318 | 319 | [node name="LengthError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer5"] 320 | visible = false 321 | layout_mode = 2 322 | text = "ERROR" 323 | label_settings = SubResource("LabelSettings_su7t3") 324 | autowrap_mode = 2 325 | 326 | [node name="LengthValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer5"] 327 | visible = false 328 | layout_mode = 2 329 | text = "Valid!" 330 | label_settings = SubResource("LabelSettings_urp4f") 331 | 332 | [node name="Label6" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 333 | layout_mode = 2 334 | text = "Value" 335 | vertical_alignment = 1 336 | 337 | [node name="VBoxContainer6" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 338 | layout_mode = 2 339 | size_flags_horizontal = 3 340 | 341 | [node name="Value" type="SpinBox" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer6"] 342 | layout_mode = 2 343 | 344 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer6/Value"] 345 | script = ExtResource("3_hkwup") 346 | validator = SubResource("Resource_smogv") 347 | 348 | [node name="ValueError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer6"] 349 | visible = false 350 | layout_mode = 2 351 | text = "ERROR" 352 | label_settings = SubResource("LabelSettings_su7t3") 353 | autowrap_mode = 2 354 | 355 | [node name="ValueValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer6"] 356 | visible = false 357 | layout_mode = 2 358 | text = "Valid!" 359 | label_settings = SubResource("LabelSettings_urp4f") 360 | 361 | [node name="Label7" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 362 | layout_mode = 2 363 | text = "Custom" 364 | vertical_alignment = 1 365 | 366 | [node name="VBoxContainer7" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 367 | layout_mode = 2 368 | size_flags_horizontal = 3 369 | 370 | [node name="ColorPicker" type="ColorPickerButton" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer7"] 371 | custom_minimum_size = Vector2(0, 32) 372 | layout_mode = 2 373 | size_flags_vertical = 3 374 | 375 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer7/ColorPicker"] 376 | script = ExtResource("3_hkwup") 377 | validator = SubResource("Resource_mh7td") 378 | 379 | [node name="ColorPickerError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer7"] 380 | visible = false 381 | layout_mode = 2 382 | text = "ERROR" 383 | label_settings = SubResource("LabelSettings_su7t3") 384 | autowrap_mode = 2 385 | 386 | [node name="ColorPickerValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer7"] 387 | visible = false 388 | layout_mode = 2 389 | text = "Valid!" 390 | label_settings = SubResource("LabelSettings_urp4f") 391 | 392 | [node name="Label8" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 393 | layout_mode = 2 394 | text = "Checked" 395 | vertical_alignment = 1 396 | 397 | [node name="VBoxContainer8" type="VBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer"] 398 | layout_mode = 2 399 | size_flags_horizontal = 3 400 | 401 | [node name="Checked" type="CheckBox" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer8"] 402 | custom_minimum_size = Vector2(0, 32) 403 | layout_mode = 2 404 | size_flags_vertical = 3 405 | 406 | [node name="ControlValidator" type="Node" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer8/Checked"] 407 | script = ExtResource("3_hkwup") 408 | validator = SubResource("Resource_kbsj8") 409 | 410 | [node name="CheckedError" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer8"] 411 | visible = false 412 | layout_mode = 2 413 | text = "ERROR" 414 | label_settings = SubResource("LabelSettings_su7t3") 415 | autowrap_mode = 2 416 | 417 | [node name="CheckedValid" type="Label" parent="MarginContainer/FormValidator/VBoxContainer2/GridContainer/VBoxContainer8"] 418 | visible = false 419 | layout_mode = 2 420 | text = "Valid!" 421 | label_settings = SubResource("LabelSettings_urp4f") 422 | 423 | [node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/FormValidator/VBoxContainer2"] 424 | layout_mode = 2 425 | theme_override_constants/separation = 32 426 | 427 | [node name="Validate" type="Button" parent="MarginContainer/FormValidator/VBoxContainer2/HBoxContainer"] 428 | layout_mode = 2 429 | size_flags_horizontal = 3 430 | theme_override_font_sizes/font_size = 16 431 | text = "Validate Form" 432 | 433 | [node name="Exit" type="Button" parent="MarginContainer/FormValidator/VBoxContainer2/HBoxContainer"] 434 | layout_mode = 2 435 | size_flags_horizontal = 3 436 | theme_override_font_sizes/font_size = 16 437 | text = "Exit" 438 | 439 | [connection signal="control_validated" from="MarginContainer/FormValidator" to="." method="_on_form_validator_control_control_validated"] 440 | [connection signal="pressed" from="MarginContainer/FormValidator/VBoxContainer2/HBoxContainer/Validate" to="." method="_on_validate_pressed"] 441 | [connection signal="pressed" from="MarginContainer/FormValidator/VBoxContainer2/HBoxContainer/Exit" to="." method="_on_exit_pressed"] 442 | -------------------------------------------------------------------------------- /addons/godot-form-validator/form_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Control 3 | class_name FormValidator 4 | 5 | signal control_validated(control, passed, messages) 6 | 7 | class ValidatorInfo extends RefCounted: 8 | var control: Control 9 | var validator: Validator 10 | 11 | @export var auto_validate: bool = false 12 | @export var validation_method: Validation.Method = Validation.Method.BATCH: 13 | set(value): 14 | validation_method = value 15 | _update_validation_methods() 16 | 17 | var _control_validator_map: Dictionary = {} 18 | var _control_messages_map: Dictionary = {} 19 | 20 | 21 | func _ready() -> void: 22 | Validation.validator_added.connect(_on_validator_added) 23 | Validation.validator_removed.connect(_on_validator_removed) 24 | _find_validators(self) 25 | 26 | 27 | func get_messages() -> Dictionary: 28 | return _control_messages_map 29 | 30 | 31 | func get_messages_for_control(control: Control) -> PackedStringArray: 32 | if not _control_messages_map.has(control): 33 | return PackedStringArray() 34 | return _control_messages_map[control] 35 | 36 | 37 | func validate() -> bool: 38 | _control_messages_map.clear() 39 | var list = _get_validator_info_list() 40 | var valid = true 41 | for info in list: 42 | if info.validator.skip_validation: 43 | continue 44 | var passed = info.validator.validate(info.control) 45 | var messages = info.validator.get_messages() 46 | if not passed: 47 | _control_messages_map[info.control] = messages 48 | control_validated.emit(info.control, passed, messages) 49 | valid = valid and passed 50 | if not valid and validation_method == Validation.Method.IMMEDIATE: 51 | return valid 52 | return valid 53 | 54 | 55 | func _get_validator_info_list() -> Array[ValidatorInfo]: 56 | var list: Array[ValidatorInfo] = [] 57 | for control in _control_validator_map.keys(): 58 | var validator = _control_validator_map[control] 59 | if not validator: 60 | continue 61 | var info = ValidatorInfo.new() 62 | info.control = control 63 | info.validator = validator 64 | list.append(info) 65 | list.sort_custom(func (a: ValidatorInfo, b: ValidatorInfo): 66 | return a.validator.validation_order < b.validator.validation_order 67 | ) 68 | return list 69 | 70 | 71 | func _update_validation_methods() -> void: 72 | var validators = _control_validator_map.values() 73 | for item in validators: 74 | var validator = item as Validator 75 | if not validator: 76 | continue 77 | validator.validation_method = validation_method 78 | 79 | 80 | func _find_validators(node: Node) -> void: 81 | for child in node.get_children(): 82 | var control_validator = child as ControlValidator 83 | if control_validator: 84 | control_validator._on_validator_added() 85 | _find_validators(child) 86 | 87 | 88 | func _auto_validate(control: Control) -> void: 89 | if not auto_validate: 90 | return 91 | var validator = _control_validator_map[control] as Validator 92 | if not validator: 93 | return 94 | var passed = validator.validate(control) 95 | control_validated.emit(control, passed, validator.get_messages()) 96 | 97 | 98 | func _on_validator_added(control: Control, validator: Validator) -> void: 99 | if not control: 100 | return 101 | _control_validator_map[control] = validator 102 | if not control.focus_exited.is_connected(_on_control_focus_exited): 103 | control.focus_exited.connect(_on_control_focus_exited.bind(control)) 104 | # Special case to handle range-type controls that don't respond to focus 105 | # events for editing. 106 | if control.has_signal("value_changed"): 107 | if not control.value_changed.is_connected(_on_control_value_changed): 108 | control.value_changed.connect(_on_control_value_changed.bind(control)) 109 | 110 | 111 | func _on_validator_removed(control: Control) -> void: 112 | _control_validator_map.erase(control) 113 | if control.focus_exited.is_connected(_on_control_focus_exited): 114 | control.focus_exited.disconnect(_on_control_focus_exited) 115 | if control.has_signal("value_changed"): 116 | if control.value_changed.is_connected(_on_control_value_changed): 117 | control.value_changed.disconnect(_on_control_value_changed) 118 | 119 | 120 | func _on_control_focus_exited(control: Control) -> void: 121 | _auto_validate(control) 122 | 123 | 124 | func _on_control_value_changed(value: float, control: Control) -> void: 125 | _auto_validate(control) 126 | -------------------------------------------------------------------------------- /addons/godot-form-validator/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot Form Validator" 4 | description="A plugin for Godot 4 that adds robust validation logic to any control." 5 | author="deadpixelsociety" 6 | version="1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/godot-form-validator/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree() -> void: 6 | add_autoload_singleton("Validation", "res://addons/godot-form-validator/validation.gd") 7 | add_custom_type("FormValidator", "Control", preload("form_validator.gd"), preload("editor_icon.png")) 8 | add_custom_type("ControlValidator", "Node", preload("control_validator.gd"), preload("editor_icon.png")) 9 | 10 | 11 | func _exit_tree() -> void: 12 | remove_custom_type("ControlValidator") 13 | remove_custom_type("FormValidator") 14 | remove_autoload_singleton("Validation") 15 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/alpha_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name AlphaRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must contain only alpha characters." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.alpha(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/alphanumeric_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name AlphanumericRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must contain only alphanumeric cahracters." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.alphanumeric(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/boolean_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name BooleanRule 4 | 5 | @export var target_value: bool 6 | 7 | 8 | func apply(control: Control, value) -> RuleResult: 9 | if ValidatorFunctions.empty(fail_message): 10 | fail_message = "The value must equal %s." % target_value 11 | var result = RuleResult.new() 12 | result.passed = value is bool and value == target_value 13 | if not result.passed: 14 | result.message = fail_message 15 | return result 16 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/custom_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name CustomRule 4 | 5 | @export_multiline var expression: String = "" 6 | 7 | var _expression: Expression = Expression.new() 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if not _validate_expression(): 13 | result.passed = false 14 | result.message = "Specified expression is not valid." 15 | return result 16 | var res = _expression.execute([ control, value ]) 17 | if _expression.has_execute_failed(): 18 | result.passed = false 19 | result.message = "Execution of the specified expression has failed." 20 | return result 21 | result.passed = res == true 22 | if not result.passed: 23 | result.message = fail_message 24 | return result 25 | 26 | 27 | func is_valid() -> bool: 28 | return _validate_expression() 29 | 30 | 31 | func get_invalid_message() -> String: 32 | return "Specified expression is not valid." 33 | 34 | 35 | func _validate_expression() -> bool: 36 | return _expression.parse(expression, [ "control", "value" ]) == OK 37 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/does_not_match_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name DoesNotMatchRule 4 | 5 | @export var pattern: String 6 | 7 | 8 | func _init() -> void: 9 | fail_message = "Invalid value." 10 | 11 | 12 | func apply(control: Control, value) -> RuleResult: 13 | var result = RuleResult.new() 14 | if value is String: 15 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.does_not_match(pattern, value) 16 | if not result.passed: 17 | result.message = fail_message 18 | return result 19 | 20 | 21 | func is_valid() -> bool: 22 | return RegEx.create_from_string(pattern).is_valid() 23 | 24 | 25 | func get_invalid_message() -> String: 26 | return "Specified regular expression is not valid." 27 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/email_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name EmailRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must be a valid email address." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.email(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/equals_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name EqualsRule 4 | 5 | @export var target_value: String 6 | 7 | 8 | func apply(control: Control, value) -> RuleResult: 9 | if ValidatorFunctions.empty(fail_message): 10 | fail_message = "The value must equal %s." % target_value 11 | var result = RuleResult.new() 12 | result.passed = str(value) == target_value 13 | if not result.passed: 14 | result.message = fail_message 15 | return result 16 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/greater_than_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name GreaterThanRule 4 | 5 | @export var target_value: String 6 | 7 | 8 | func apply(control: Control, value) -> RuleResult: 9 | if ValidatorFunctions.empty(fail_message): 10 | fail_message = "The value must be greater than %s." % target_value 11 | var result = RuleResult.new() 12 | result.passed = str(value) > target_value 13 | if not result.passed: 14 | result.message = fail_message 15 | return result 16 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/length_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name LengthRule 4 | 5 | const FAIL_MESSAGE: String = "The value must be between %d and %d characters long." 6 | 7 | @export var min_length: int = 0 8 | @export var max_length: int = 9999 9 | 10 | 11 | func apply(control: Control, value) -> RuleResult: 12 | var result = RuleResult.new() 13 | if value is String: 14 | result.passed = ValidatorFunctions.length(value, min_length, max_length) 15 | if not result.passed: 16 | if ValidatorFunctions.empty(fail_message): 17 | result.message = FAIL_MESSAGE % [ min_length, max_length ] 18 | else: 19 | result.message = fail_message 20 | return result 21 | 22 | 23 | func is_valid() -> bool: 24 | return min_length <= max_length 25 | 26 | 27 | func get_invalid_message() -> String: 28 | return "Minimum length must be less than or equal to maximum length." 29 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/less_than_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name LessThanRule 4 | 5 | @export var target_value: String 6 | 7 | 8 | func apply(control: Control, value) -> RuleResult: 9 | if ValidatorFunctions.empty(fail_message): 10 | fail_message = "The value must be less than %s." % target_value 11 | var result = RuleResult.new() 12 | result.passed = str(value) < target_value 13 | if not result.passed: 14 | result.message = fail_message 15 | return result 16 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/matches_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name MatchesRule 4 | 5 | @export var pattern: String 6 | 7 | 8 | func _init() -> void: 9 | fail_message = "Invalid value." 10 | 11 | 12 | func apply(control: Control, value) -> RuleResult: 13 | var result = RuleResult.new() 14 | if value is String: 15 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.matches(pattern, value) 16 | if not result.passed: 17 | result.message = fail_message 18 | return result 19 | 20 | 21 | func is_valid() -> bool: 22 | return RegEx.create_from_string(pattern).is_valid() 23 | 24 | 25 | func get_invalid_message() -> String: 26 | return "Specified regular expression is not valid." 27 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/not_blank_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name NotBlankRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must not be blank." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.not_blank(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/not_empty_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name NotEmptyRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must not be empty." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.not_empty(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/numeric_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name NumericRule 4 | 5 | 6 | func _init() -> void: 7 | fail_message = "Value must contain only numbers." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.empty(value) or ValidatorFunctions.numeric(value) 14 | if not result.passed: 15 | result.message = fail_message 16 | return result 17 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/required_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends ValidatorRule 3 | class_name RequiredRule 4 | 5 | 6 | func _init(): 7 | fail_message = "A value is required." 8 | 9 | 10 | func apply(control: Control, value) -> RuleResult: 11 | var result = RuleResult.new() 12 | if value is String: 13 | result.passed = ValidatorFunctions.not_blank(value) 14 | else: 15 | result.passed = value != null 16 | if not result.passed: 17 | result.message = fail_message 18 | return result 19 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/rule_result.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | class_name RuleResult 3 | 4 | var passed: bool = false 5 | var message: String = "" 6 | -------------------------------------------------------------------------------- /addons/godot-form-validator/rules/validator_rule.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name ValidatorRule 4 | 5 | @export var fail_message: String = "" 6 | 7 | 8 | func apply(control: Control, value) -> RuleResult: 9 | return RuleResult.new() 10 | 11 | 12 | func is_valid() -> bool: 13 | return true 14 | 15 | 16 | func get_invalid_message() -> String: 17 | return "" 18 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validation.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | 4 | signal validator_added(control, validator) 5 | signal validator_removed(control) 6 | 7 | enum Method { 8 | # Processes all validators and rules before returning a result. 9 | BATCH, 10 | # Processes validators until a rule failure is encountered, then proceeds to the next validator. 11 | FAIL_FAST, 12 | # Processes validators until a rule failure is encountered and returns immediately. 13 | IMMEDIATE 14 | } 15 | 16 | var _validators: Array[Validator] = [] 17 | 18 | 19 | func _ready() -> void: 20 | add_validator(ButtonValidator.new()) 21 | add_validator(LineEditValidator.new()) 22 | add_validator(RangeValidator.new()) 23 | add_validator(TextEditValidator.new()) 24 | 25 | 26 | func add_validator(validator: Validator) -> void: 27 | if not _validators.has(validator): 28 | _validators.append(validator) 29 | 30 | 31 | func get_validators() -> Array[Validator]: 32 | return _validators 33 | 34 | 35 | func find_validator(node: Node) -> Validator: 36 | for validator in get_validators(): 37 | if validator.is_type(node): 38 | return validator 39 | return null 40 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validator_functions.gd: -------------------------------------------------------------------------------- 1 | extends Object 2 | class_name ValidatorFunctions 3 | 4 | 5 | static func matches(pattern: String, text: String) -> bool: 6 | var regex = RegEx.create_from_string(pattern) 7 | if not regex.is_valid(): 8 | print_debug("WARNING: Invalid RegEx pattern supplied to matches function: ", pattern) 9 | return false 10 | var result = regex.search(text) 11 | return result != null and result.strings.size() > 0 12 | 13 | 14 | static func does_not_match(pattern: String, text: String) -> bool: 15 | var regex = RegEx.create_from_string(pattern) 16 | if not regex.is_valid(): 17 | print_debug("WARNING: Invalid RegEx pattern supplied to matches function: ", pattern) 18 | return false 19 | var result = regex.search(text) 20 | return result == null 21 | 22 | 23 | static func not_empty(text: String) -> bool: 24 | return text != null and not text.is_empty() 25 | 26 | 27 | static func empty(text: String) -> bool: 28 | return not not_empty(text) 29 | 30 | 31 | static func not_blank(text: String) -> bool: 32 | if text == null: 33 | return false 34 | # Remove escape sequences 35 | text = text.strip_escapes() 36 | # Remove spaces 37 | text = text.replace(" ", "") 38 | return not text.is_empty() 39 | 40 | 41 | static func alpha(text: String) -> bool: 42 | var regex = RegEx.create_from_string("[^a-zA-Z]+") 43 | var result = regex.search(text) 44 | return result == null 45 | 46 | 47 | static func numeric(text: String) -> bool: 48 | var regex = RegEx.create_from_string("[^0-9]+") 49 | var result = regex.search(text) 50 | return result == null 51 | 52 | 53 | static func alphanumeric(text: String) -> bool: 54 | var regex = RegEx.create_from_string("[^a-zA-Z0-9]+") 55 | var result = regex.search(text) 56 | return result == null 57 | 58 | 59 | static func email(text: String) -> bool: 60 | var regex = RegEx.create_from_string("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$") 61 | var result = regex.search(text) 62 | return result != null and result.strings.size() > 0 63 | 64 | 65 | static func length(text: String, min_length: int = 0, max_length: int = 9999) -> bool: 66 | return text != null and text.length() >= min_length and text.length() <= max_length 67 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validators/button_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Validator 3 | class_name ButtonValidator 4 | 5 | 6 | func get_value(control: Control): 7 | var button = control as Button 8 | if not button: 9 | return null 10 | return button.button_pressed 11 | 12 | 13 | func is_type(node) -> bool: 14 | return node is Button 15 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validators/line_edit_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Validator 3 | class_name LineEditValidator 4 | 5 | 6 | func get_value(control: Control): 7 | var line_edit = control as LineEdit 8 | if not line_edit: 9 | return null 10 | return line_edit.text 11 | 12 | 13 | func is_type(node) -> bool: 14 | return node is LineEdit 15 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validators/range_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Validator 3 | class_name RangeValidator 4 | 5 | 6 | func get_value(control: Control): 7 | var range_control = control as Range 8 | if not range_control: 9 | return null 10 | return range_control.value 11 | 12 | 13 | func is_type(node) -> bool: 14 | return node is Range and \ 15 | not (node is ScrollBar or node is ProgressBar or node is TextureProgressBar) 16 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validators/text_edit_validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Validator 3 | class_name TextEditValidator 4 | 5 | 6 | func get_value(control: Control): 7 | var text_edit = control as TextEdit 8 | if not text_edit: 9 | return null 10 | return text_edit.text 11 | 12 | 13 | func is_type(node) -> bool: 14 | return node is TextEdit 15 | -------------------------------------------------------------------------------- /addons/godot-form-validator/validators/validator.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Resource 3 | class_name Validator 4 | 5 | @export var validation_order: int = 1 6 | @export var validation_method: Validation.Method = Validation.Method.BATCH 7 | @export var skip_validation: bool = false 8 | @export var rules: Array[ValidatorRule] = [] 9 | 10 | var _messages: PackedStringArray = PackedStringArray() 11 | 12 | 13 | func get_value(control: Control): 14 | return null 15 | 16 | 17 | func is_type(node) -> bool: 18 | return node is Control 19 | 20 | 21 | func get_messages() -> PackedStringArray: 22 | return _messages 23 | 24 | 25 | func validate(control: Control) -> bool: 26 | _messages.clear() 27 | var valid = true 28 | for rule in rules: 29 | var result = rule.apply(control, get_value(control)) 30 | if not result.passed: 31 | _messages.append(result.message) 32 | valid = valid and result.passed 33 | if not valid: 34 | # Not BATCH, exit early. 35 | if validation_method != Validation.Method.BATCH: 36 | return valid 37 | return valid 38 | -------------------------------------------------------------------------------- /example_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadpixelsociety/godot-form-validator/24d1742aa09967bd46c25243af91efb064a7c803/example_rules.png -------------------------------------------------------------------------------- /example_rules.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c13odbsfv53he" 6 | path="res://.godot/imported/example_rules.png-faa9d2703b3ffa116338aa78beef009e.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://example_rules.png" 14 | dest_files=["res://.godot/imported/example_rules.png-faa9d2703b3ffa116338aa78beef009e.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /example_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadpixelsociety/godot-form-validator/24d1742aa09967bd46c25243af91efb064a7c803/example_tree.png -------------------------------------------------------------------------------- /example_tree.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://b1ynfmrih7ira" 6 | path="res://.godot/imported/example_tree.png-f962c768c2ced2db3f35399da01825e8.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://example_tree.png" 14 | dest_files=["res://.godot/imported/example_tree.png-f962c768c2ced2db3f35399da01825e8.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadpixelsociety/godot-form-validator/24d1742aa09967bd46c25243af91efb064a7c803/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://d3n0k21jubf3p" 6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.png" 14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 33 | 53 | 57 | 58 | -------------------------------------------------------------------------------- /icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://tptd2sbrrfi5" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadpixelsociety/godot-form-validator/24d1742aa09967bd46c25243af91efb064a7c803/logo.png -------------------------------------------------------------------------------- /logo.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://c3ykco3r3cbm4" 6 | path="res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://logo.png" 14 | dest_files=["res://.godot/imported/logo.png-cca8726399059c8d4f806e28e356b14d.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="godot-form-validator" 14 | config/features=PackedStringArray("4.0", "Forward Plus") 15 | config/icon="res://icon.svg" 16 | 17 | [autoload] 18 | 19 | Validation="*res://addons/godot-form-validator/validation.gd" 20 | 21 | [display] 22 | 23 | window/size/viewport_width=1280 24 | window/size/viewport_height=720 25 | window/stretch/mode="canvas_items" 26 | window/stretch/aspect="expand" 27 | 28 | [editor_plugins] 29 | 30 | enabled=PackedStringArray("res://addons/godot-form-validator/plugin.cfg") 31 | --------------------------------------------------------------------------------