Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Allow any control to be an input #3

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion addons/form/nodes/FormLabel.gd
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,52 @@ class_name FormLabel extends Label
if input != null:
input.gui_input.disconnect(_on_gui_input)
input = new_val
nonstandard_input = false
mode = mode # run setter
indicate_required()
if validate_on_input && input != null:
input.gui_input.connect(_on_gui_input)
else:
printerr(get_class(),": input must be a input button or input field")
nonstandard_input = true

## "The selected input is not an input according to Form.is_input() or null"
var nonstandard_input := false

## Must be compatible with the type of subject
@export var validator: Validator:
set(new_val):
if validator != null && input != null:
if has_property(input, subject) && typeof(input[subject]) != new_val.get_type():
push_error("Validator must be compatible with the type of subject")
return
if input.get_theme_stylebox("normal") == null:
validator.style_valid = input.get_theme_stylebox("normal")
validator = new_val

## Name of the property to validate
## if not set, value property will be searched in
## - Button: button_pressed
## - LineEdit, TextEdit: text
## - Range: value
@export var subject: StringName:
set(new_val):
# can't do any validation here because every keystroke in the editor would trigger it
_subject = new_val
get:
if _subject == &"" && input != null:
if input is BaseButton:
subject = &"button_pressed"
elif input is LineEdit || input is TextEdit:
subject = &"text"
elif input is Range:
subject = &"value"
else:
subject = &""
return _subject

## Internal storage subject
var _subject: StringName

## "Input value must not be empty"
@export var input_required := false:
set(new_val):
Expand Down
176 changes: 10 additions & 166 deletions addons/form/nodes/Validator.gd
Original file line number Diff line number Diff line change
@@ -1,175 +1,19 @@
@tool
## Validates Text Input according to rules about length and content
## Validates Input according to rules
class_name Validator extends Resource

## A collection of predefined regular expression patterns.
## Items correspond to the PredefinedRegEx enum.
const REGEX_LIB = [
"^[a-zA-Z]+$",
"^[0-9]+$",
"^[a-zA-Z0-9]+$",
"^(?!(?:(?:\\x22?\\x5C[\\x00-\\x7E]\\x22?)|(?:\\x22?[^\\x5C\\x22]\\x22?)){255,})(?!(?:(?:\\x22?\\x5C[\\x00-\\x7E]\\x22?)|(?:\\x22?[^\\x5C\\x22]\\x22?)){65,}@)(?:(?:[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2F-\\x39\\x3D\\x3F\\x5E-\\x7E]+)|(?:\\x22(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x21\\x23-\\x5B\\x5D-\\x7F]|(?:\\x5C[\\x00-\\x7F]))*\\x22))(?:\\.(?:(?:[\\x21\\x23-\\x27\\x2A\\x2B\\x2D\\x2F-\\x39\\x3D\\x3F\\x5E-\\x7E]+)|(?:\\x22(?:[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x21\\x23-\\x5B\\x5D-\\x7F]|(?:\\x5C[\\x00-\\x7F]))*\\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\\]))$",
# source: emailregex.com
"^[\\+]?[(]?[0-9]{3}[)]?[-\\s\\.]?[0-9]{3}[-\\s\\.]?[0-9]{4,6}$"
# source: ihateregex.io/expr/phone
]


@export_group("Rules")

@export_subgroup("Simple Rules")
## "Input must have a value"
## min_length will be adjusted if needed
@export var required := false:
set(new_val):
if new_val:
if min_length < 1:
_prev_min_length = min_length
min_length = 1
else:
min_length = _prev_min_length
required = new_val

## temporary storage of min_length used by the setter of required
var _prev_min_length := 0
## Minimum number of characters
@export var min_length := 0:
set(new_val):
if new_val < 0:
new_val = 0
if new_val == 0 && required:
required = false
min_length = new_val
## Minimum and Maximum number of matches for \w+ allowed
@export var word_range: Boundaries
## List of allowed strings
@export var whitelist: ListFilter
## List of prohibited strings
@export var blacklist: ListFilter

@export_subgroup("Regular Expression")
enum PredefinedRegEx {NONE = -1,
ALPHABETICAL, NUMERICAL, ALPHANUMERICAL, EMAIL_ADDRESS, PHONE_NUMBER
}
## Predefined pattern to match against
@export var predefined: PredefinedRegEx = -1

enum Behaviour {
## Input must match both the predefined and the custom regex (if both are set)
MUST_MATCH_BOTH = 0,
## Input can match either the predefined or the custom regex (if at least one is set)
CAN_MATCH_EITHER = 1
}
## How predefined and custom regexes are checked against in relation to each other.
@export var behaviour: Behaviour

## Custom Pattern to match against
@export var custom := ".*"
## Normalise the case before matching
@export var normalise := false
## Remove these strings from the subject before matching
@export var remove: Array[String]
## Don't allow any more than one match
@export var require_single_match := false

## Types that can be validated
## See Variant.Type for possible return values
func get_type() -> int:
return TYPE_OBJECT

## Validation passed (updated on text change)
var valid := false
## Broken Rules with names and relevant value
var broken_rules := {}
## Compiled custom regex
var user_regex: RegEx

## Compiles the custom regex
func _init() -> void:
user_regex = RegEx.new()
user_regex.compile(custom)

## Validates given text and updates valid property
func _on_text_changed(
## New content of the input
new_text: String
) -> void:
valid = validate(new_text)

## Validates given text against all rules and returns validity
func validate(
## Text to validate
subject: String
) -> bool:
## Validates subject against all rules and returns validity
func validate(subject) -> bool:
if typeof(subject) != get_type():
push_error("Subject must be of type ", get_type(), " (see Variant.Type)")
broken_rules = {}
var _regex := RegEx.new()


##-- min_length --##
var length = subject.length()
if length < min_length:
broken_rules["min_length"] = str(length)
return false


##-- filter_list --##
if (
blacklist != null && blacklist.size() > 0
&& blacklist.is_represented_in(subject)
):
broken_rules["filter_list"] = "blacklist"
return false

if (
whitelist != null && whitelist.size() > 0
&& !whitelist.is_represented_in(subject)
):
broken_rules["filter_list"] = "whitelist"
return false


##-- word_range --##
if word_range != null && (word_range.min != 0 || word_range.max != 0):
_regex.compile("\\w+") # word
var matches = _regex.search_all(subject)
if matches == null:
if word_range.min:
broken_rules["word_range"] = "min"
return false
matches = [] # size() = 0
var count = matches.size()
if count > word_range.max:
broken_rules["word_range"] = "max"
return false
elif count < word_range.min:
broken_rules["word_range"] = "min"
return false

##-- predefined_regex --##
var predefined_regex_result := false

if normalise:
subject = subject.to_lower()
if remove.size() > 0:
for item in remove:
subject = subject.replace(item, "")

if predefined != PredefinedRegEx.NONE:
_regex.compile(REGEX_LIB[predefined])
if _regex.search(subject) != null: # if there is a match
predefined_regex_result = true
if behaviour == Behaviour.CAN_MATCH_EITHER:# if we only need one match
return true # return true, since this is the last check before user_regex, which we don't need to run, since we only need one match
elif behaviour == Behaviour.MUST_MATCH_BOTH: # if no match is found and we need both
broken_rules["predefined"] = Behaviour.keys()[behaviour]
return false
# else we need both and there is no match, so we keep predefined_regex_result as false and continue to user_regex
else: # if there is no predefined regex, we don't need to run it
predefined_regex_result = true

##-- user_regex --##
if user_regex not in ["", null, ".*"]:
if require_single_match:
var matches = user_regex.search_all(subject)
if matches != null && matches.size() == 1 && matches[0].get_string() == subject.strip_edges():
return true
return predefined_regex_result || bool(behaviour)
elif user_regex.search(subject) != null:
return true
return true
return valid
Loading