first commit
This commit is contained in:
53
addons/zylann.hterrain/tools/util/dialog_fitter.gd
Executable file
53
addons/zylann.hterrain/tools/util/dialog_fitter.gd
Executable file
@@ -0,0 +1,53 @@
|
||||
|
||||
# If you make a container-based UI inside a WindowDialog, there is a chance it will overflow
|
||||
# because WindowDialogs don't adjust by themselves. This happens when the user has a different
|
||||
# font size than yours, and can cause controls to be unusable (like buttons at the bottom).
|
||||
# This script adjusts the size of the parent WindowDialog based on the first Container it finds
|
||||
# when the node becomes visible.
|
||||
|
||||
@tool
|
||||
# Needs to be a Control, otherwise we don't receive the notification...
|
||||
extends Control
|
||||
|
||||
const HT_Util = preload("../../util/util.gd")
|
||||
|
||||
|
||||
func _notification(what: int):
|
||||
if HT_Util.is_in_edited_scene(self):
|
||||
return
|
||||
if is_inside_tree() and what == Control.NOTIFICATION_VISIBILITY_CHANGED:
|
||||
#print("Visible ", is_visible_in_tree(), ", ", visible)
|
||||
call_deferred("_fit_to_contents")
|
||||
|
||||
|
||||
func _fit_to_contents():
|
||||
var dialog : Window = get_parent()
|
||||
for child in dialog.get_children():
|
||||
if child is Container:
|
||||
var child_rect : Rect2 = child.get_global_rect()
|
||||
var dialog_rect := Rect2(Vector2(), dialog.size)
|
||||
#print("Dialog: ", dialog_rect, ", contents: ", child_rect, " ", child.get_path())
|
||||
if not dialog_rect.encloses(child_rect):
|
||||
var margin : Vector2 = child.get_rect().position
|
||||
#print("Fitting ", dialog.get_path(), " from ", dialog.rect_size,
|
||||
# " to ", child_rect.size + margin * 2.0)
|
||||
dialog.min_size = child_rect.size + margin * 2.0
|
||||
|
||||
|
||||
#func _process(delta):
|
||||
# update()
|
||||
|
||||
# DEBUG
|
||||
#func _draw():
|
||||
# var self_global_pos = get_global_rect().position
|
||||
#
|
||||
# var dialog : Control = get_parent()
|
||||
# var dialog_rect := dialog.get_global_rect()
|
||||
# dialog_rect.position -= self_global_pos
|
||||
# draw_rect(dialog_rect, Color(1,1,0), false)
|
||||
#
|
||||
# for child in dialog.get_children():
|
||||
# if child is Container:
|
||||
# var child_rect : Rect2 = child.get_global_rect()
|
||||
# child_rect.position -= self_global_pos
|
||||
# draw_rect(child_rect, Color(1,1,0,0.1))
|
||||
1
addons/zylann.hterrain/tools/util/dialog_fitter.gd.uid
Normal file
1
addons/zylann.hterrain/tools/util/dialog_fitter.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://civlwde0pyfx8
|
||||
10
addons/zylann.hterrain/tools/util/dialog_fitter.tscn
Normal file
10
addons/zylann.hterrain/tools/util/dialog_fitter.tscn
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://hf4jllhtne1j"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://civlwde0pyfx8" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.gd" id="1"]
|
||||
|
||||
[node name="DialogFitter" type="Control"]
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1")
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
104
addons/zylann.hterrain/tools/util/editor_util.gd
Executable file
104
addons/zylann.hterrain/tools/util/editor_util.gd
Executable file
@@ -0,0 +1,104 @@
|
||||
|
||||
# Editor-specific utilities.
|
||||
# This script cannot be loaded in an exported game.
|
||||
|
||||
@tool
|
||||
|
||||
|
||||
# This is normally an `EditorFileDialog`. I can't type-hint this one properly,
|
||||
# because when I test UI in isolation, I can't use `EditorFileDialog`.
|
||||
static func create_open_file_dialog() -> ConfirmationDialog:
|
||||
var d
|
||||
if Engine.is_editor_hint():
|
||||
# TODO Workaround bug when editor-only classes are created in source code, even if not run
|
||||
# https://github.com/godotengine/godot/issues/73525
|
||||
# d = EditorFileDialog.new()
|
||||
d = ClassDB.instantiate(&"EditorFileDialog")
|
||||
d.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
|
||||
d.access = EditorFileDialog.ACCESS_RESOURCES
|
||||
else:
|
||||
d = FileDialog.new()
|
||||
d.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
d.access = FileDialog.ACCESS_RESOURCES
|
||||
d.unresizable = false
|
||||
return d
|
||||
|
||||
|
||||
static func create_open_dir_dialog() -> ConfirmationDialog:
|
||||
var d
|
||||
if Engine.is_editor_hint():
|
||||
# TODO Workaround bug when editor-only classes are created in source code, even if not run
|
||||
# https://github.com/godotengine/godot/issues/73525
|
||||
# d = EditorFileDialog.new()
|
||||
d = ClassDB.instantiate(&"EditorFileDialog")
|
||||
d.file_mode = EditorFileDialog.FILE_MODE_OPEN_DIR
|
||||
d.access = EditorFileDialog.ACCESS_RESOURCES
|
||||
else:
|
||||
d = FileDialog.new()
|
||||
d.file_mode = FileDialog.FILE_MODE_OPEN_DIR
|
||||
d.access = FileDialog.ACCESS_RESOURCES
|
||||
d.unresizable = false
|
||||
return d
|
||||
|
||||
|
||||
# If you want to open using Image.load()
|
||||
static func create_open_image_dialog() -> ConfirmationDialog:
|
||||
var d = create_open_file_dialog()
|
||||
_add_image_filters(d)
|
||||
return d
|
||||
|
||||
|
||||
# If you want to open using load(),
|
||||
# although it might still fail if the file is imported as Image...
|
||||
static func create_open_texture_dialog() -> ConfirmationDialog:
|
||||
var d = create_open_file_dialog()
|
||||
_add_texture_filters(d)
|
||||
return d
|
||||
|
||||
|
||||
static func create_open_texture_array_dialog() -> ConfirmationDialog:
|
||||
var d = create_open_file_dialog()
|
||||
_add_texture_array_filters(d)
|
||||
return d
|
||||
|
||||
# TODO Post a proposal, we need a file dialog filtering on resource types, not on file extensions!
|
||||
|
||||
static func _add_image_filters(file_dialog):
|
||||
file_dialog.add_filter("*.png ; PNG files")
|
||||
file_dialog.add_filter("*.jpg ; JPG files")
|
||||
#file_dialog.add_filter("*.exr ; EXR files")
|
||||
|
||||
|
||||
static func _add_texture_filters(file_dialog):
|
||||
_add_image_filters(file_dialog)
|
||||
# Godot
|
||||
file_dialog.add_filter("*.ctex ; CompressedTexture files")
|
||||
# Packed textures
|
||||
file_dialog.add_filter("*.packed_tex ; HTerrainPackedTexture files")
|
||||
|
||||
|
||||
static func _add_texture_array_filters(file_dialog):
|
||||
_add_image_filters(file_dialog)
|
||||
# Godot
|
||||
file_dialog.add_filter("*.ctexarray ; TextureArray files")
|
||||
# Packed textures
|
||||
file_dialog.add_filter("*.packed_texarr ; HTerrainPackedTextureArray files")
|
||||
|
||||
|
||||
# Tries to load a texture with the ResourceLoader, and if it fails, attempts
|
||||
# to load it manually as an ImageTexture
|
||||
static func load_texture(path: String, logger) -> Texture:
|
||||
var tex : Texture = load(path)
|
||||
if tex != null:
|
||||
return tex
|
||||
# This can unfortunately happen when the editor didn't import assets yet.
|
||||
# See https://github.com/godotengine/godot/issues/17483
|
||||
logger.error(str("Failed to load texture ", path, ", attempting to load manually"))
|
||||
var im := Image.new()
|
||||
var err = im.load(path)
|
||||
if err != OK:
|
||||
logger.error(str("Failed to load image ", path))
|
||||
return null
|
||||
var itex := ImageTexture.create_from_image(im)
|
||||
return itex
|
||||
|
||||
1
addons/zylann.hterrain/tools/util/editor_util.gd.uid
Normal file
1
addons/zylann.hterrain/tools/util/editor_util.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://br7gfr80jqfq2
|
||||
197
addons/zylann.hterrain/tools/util/interval_slider.gd
Executable file
197
addons/zylann.hterrain/tools/util/interval_slider.gd
Executable file
@@ -0,0 +1,197 @@
|
||||
|
||||
# Slider with two handles representing an interval.
|
||||
|
||||
@tool
|
||||
extends Control
|
||||
|
||||
const VALUE_LOW = 0
|
||||
const VALUE_HIGH = 1
|
||||
const VALUE_COUNT = 2
|
||||
|
||||
const FG_MARGIN = 1
|
||||
|
||||
signal changed
|
||||
|
||||
var _min_value := 0.0
|
||||
var _max_value := 1.0
|
||||
var _values = [0.2, 0.6]
|
||||
var _grabbing := false
|
||||
|
||||
|
||||
func _get_property_list():
|
||||
return [
|
||||
{
|
||||
"name": "min_value",
|
||||
"type": TYPE_FLOAT,
|
||||
"usage": PROPERTY_USAGE_EDITOR
|
||||
},
|
||||
{
|
||||
"name": "max_value",
|
||||
"type": TYPE_FLOAT,
|
||||
"usage": PROPERTY_USAGE_EDITOR
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"type": TYPE_VECTOR2,
|
||||
"usage": PROPERTY_USAGE_STORAGE
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
func _get(key: StringName):
|
||||
match key:
|
||||
&"min_value":
|
||||
return _min_value
|
||||
&"max_value":
|
||||
return _max_value
|
||||
&"range":
|
||||
return Vector2(_min_value, _max_value)
|
||||
|
||||
|
||||
func _set(key: StringName, value):
|
||||
match key:
|
||||
&"min_value":
|
||||
_min_value = min(value, _max_value)
|
||||
queue_redraw()
|
||||
&"max_value":
|
||||
_max_value = max(value, _min_value)
|
||||
queue_redraw()
|
||||
&"range":
|
||||
_min_value = value.x
|
||||
_max_value = value.y
|
||||
|
||||
|
||||
func set_values(low: float, high: float):
|
||||
if low > high:
|
||||
low = high
|
||||
if high < low:
|
||||
high = low
|
||||
_values[VALUE_LOW] = low
|
||||
_values[VALUE_HIGH] = high
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_value(i: int, v: float, notify_change: bool):
|
||||
var min_value = _min_value
|
||||
var max_value = _max_value
|
||||
|
||||
match i:
|
||||
VALUE_LOW:
|
||||
max_value = _values[VALUE_HIGH]
|
||||
VALUE_HIGH:
|
||||
min_value = _values[VALUE_LOW]
|
||||
_:
|
||||
assert(false)
|
||||
|
||||
v = clampf(v, min_value, max_value)
|
||||
if v != _values[i]:
|
||||
_values[i] = v
|
||||
queue_redraw()
|
||||
if notify_change:
|
||||
changed.emit()
|
||||
|
||||
|
||||
func get_value(i: int) -> float:
|
||||
return _values[i]
|
||||
|
||||
|
||||
func get_low_value() -> float:
|
||||
return _values[VALUE_LOW]
|
||||
|
||||
|
||||
func get_high_value() -> float:
|
||||
return _values[VALUE_HIGH]
|
||||
|
||||
|
||||
func get_ratio(i: int) -> float:
|
||||
return _value_to_ratio(_values[i])
|
||||
|
||||
|
||||
func get_low_ratio() -> float:
|
||||
return get_ratio(VALUE_LOW)
|
||||
|
||||
|
||||
func get_high_ratio() -> float:
|
||||
return get_ratio(VALUE_HIGH)
|
||||
|
||||
|
||||
func _ratio_to_value(r: float) -> float:
|
||||
return r * (_max_value - _min_value) + _min_value
|
||||
|
||||
|
||||
func _value_to_ratio(v: float) -> float:
|
||||
if absf(_max_value - _min_value) < 0.001:
|
||||
return 0.0
|
||||
return (v - _min_value) / (_max_value - _min_value)
|
||||
|
||||
|
||||
func _get_closest_index(ratio: float) -> int:
|
||||
var distance_low := absf(ratio - get_low_ratio())
|
||||
var distance_high := absf(ratio - get_high_ratio())
|
||||
if distance_low < distance_high:
|
||||
return VALUE_LOW
|
||||
return VALUE_HIGH
|
||||
|
||||
|
||||
func _set_from_pixel(px: float):
|
||||
var r := (px - FG_MARGIN) / (size.x - FG_MARGIN * 2.0)
|
||||
var i := _get_closest_index(r)
|
||||
var v := _ratio_to_value(r)
|
||||
set_value(i, v, true)
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_grabbing = true
|
||||
_set_from_pixel(event.position.x)
|
||||
else:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_grabbing = false
|
||||
|
||||
elif event is InputEventMouseMotion:
|
||||
if _grabbing:
|
||||
_set_from_pixel(event.position.x)
|
||||
|
||||
|
||||
func _draw():
|
||||
var grabber_width := 3
|
||||
var background_v_margin := 0
|
||||
var foreground_margin := FG_MARGIN
|
||||
var grabber_color := Color(0.8, 0.8, 0.8)
|
||||
var interval_color := Color(0.4,0.4,0.4)
|
||||
var background_color := Color(0.1, 0.1, 0.1)
|
||||
|
||||
var control_rect := Rect2(Vector2(), size)
|
||||
|
||||
var bg_rect := Rect2(
|
||||
control_rect.position.x,
|
||||
control_rect.position.y + background_v_margin,
|
||||
control_rect.size.x,
|
||||
control_rect.size.y - 2 * background_v_margin)
|
||||
draw_rect(bg_rect, background_color)
|
||||
|
||||
var fg_rect := control_rect.grow(-foreground_margin)
|
||||
|
||||
var low_ratio := get_low_ratio()
|
||||
var high_ratio := get_high_ratio()
|
||||
|
||||
var low_x := fg_rect.position.x + low_ratio * fg_rect.size.x
|
||||
var high_x := fg_rect.position.x + high_ratio * fg_rect.size.x
|
||||
|
||||
var interval_rect := Rect2(
|
||||
low_x, fg_rect.position.y, high_x - low_x, fg_rect.size.y)
|
||||
draw_rect(interval_rect, interval_color)
|
||||
|
||||
low_x = fg_rect.position.x + low_ratio * (fg_rect.size.x - grabber_width)
|
||||
high_x = fg_rect.position.x + high_ratio * (fg_rect.size.x - grabber_width)
|
||||
|
||||
for x in [low_x, high_x]:
|
||||
var grabber_rect := Rect2(
|
||||
x,
|
||||
fg_rect.position.y,
|
||||
grabber_width,
|
||||
fg_rect.size.y)
|
||||
draw_rect(grabber_rect, grabber_color)
|
||||
|
||||
1
addons/zylann.hterrain/tools/util/interval_slider.gd.uid
Normal file
1
addons/zylann.hterrain/tools/util/interval_slider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dnqvfdgwxl85k
|
||||
19
addons/zylann.hterrain/tools/util/resource_importer_texture.gd
Executable file
19
addons/zylann.hterrain/tools/util/resource_importer_texture.gd
Executable file
@@ -0,0 +1,19 @@
|
||||
@tool
|
||||
|
||||
# Stuff not exposed by Godot for making .import files
|
||||
|
||||
const COMPRESS_LOSSLESS = 0
|
||||
const COMPRESS_LOSSY = 1
|
||||
const COMPRESS_VRAM_COMPRESSED = 2
|
||||
const COMPRESS_VRAM_UNCOMPRESSED = 3
|
||||
const COMPRESS_BASIS_UNIVERSAL = 4
|
||||
|
||||
const ROUGHNESS_DETECT = 0
|
||||
const ROUGHNESS_DISABLED = 1
|
||||
# Godot internally subtracts 2 to magically obtain a `Image.RoughnessChannel` enum
|
||||
# (also not exposed)
|
||||
const ROUGHNESS_RED = 2
|
||||
const ROUGHNESS_GREEN = 3
|
||||
const ROUGHNESS_BLUE = 4
|
||||
const ROUGHNESS_ALPHA = 5
|
||||
const ROUGHNESS_GRAY = 6
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1gdap1s7mjqx
|
||||
9
addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd
Executable file
9
addons/zylann.hterrain/tools/util/resource_importer_texture_layered.gd
Executable file
@@ -0,0 +1,9 @@
|
||||
@tool
|
||||
|
||||
# Stuff not exposed by Godot for making .import files
|
||||
|
||||
const COMPRESS_LOSSLESS = 0
|
||||
const COMPRESS_LOSSY = 1
|
||||
const COMPRESS_VRAM_COMPRESSED = 2
|
||||
const COMPRESS_VRAM_UNCOMPRESSED = 3
|
||||
const COMPRESS_BASIS_UNIVERSAL = 4
|
||||
@@ -0,0 +1 @@
|
||||
uid://cluv1frdusgml
|
||||
36
addons/zylann.hterrain/tools/util/result.gd
Executable file
36
addons/zylann.hterrain/tools/util/result.gd
Executable file
@@ -0,0 +1,36 @@
|
||||
# Data structure to hold the result of a function that can be expected to fail.
|
||||
# The use case is to report errors back to the GUI and act accordingly,
|
||||
# instead of forgetting them to the console or having the script break on an assertion.
|
||||
# This is a C-like way of things, where the result can bubble, and does not require globals.
|
||||
|
||||
@tool
|
||||
|
||||
# Replace `success` with `error : int`?
|
||||
var success := false
|
||||
var value = null
|
||||
var message := ""
|
||||
var inner_result = null
|
||||
|
||||
|
||||
func _init(p_success: bool, p_message := "", p_inner = null):
|
||||
success = p_success
|
||||
message = p_message
|
||||
inner_result = p_inner
|
||||
|
||||
|
||||
# TODO Can't type-hint self return
|
||||
func with_value(v):
|
||||
value = v
|
||||
return self
|
||||
|
||||
|
||||
func get_message() -> String:
|
||||
var msg := message
|
||||
if inner_result != null:
|
||||
msg += "\n"
|
||||
msg += inner_result.get_message()
|
||||
return msg
|
||||
|
||||
|
||||
func is_ok() -> bool:
|
||||
return success
|
||||
1
addons/zylann.hterrain/tools/util/result.gd.uid
Normal file
1
addons/zylann.hterrain/tools/util/result.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jawgqo7nvu7m
|
||||
11
addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd
Executable file
11
addons/zylann.hterrain/tools/util/rich_text_label_hyperlinks.gd
Executable file
@@ -0,0 +1,11 @@
|
||||
@tool
|
||||
extends RichTextLabel
|
||||
|
||||
|
||||
func _init():
|
||||
meta_clicked.connect(_on_meta_clicked)
|
||||
|
||||
|
||||
func _on_meta_clicked(meta):
|
||||
OS.shell_open(meta)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://ict65vmutips
|
||||
385
addons/zylann.hterrain/tools/util/spin_slider.gd
Executable file
385
addons/zylann.hterrain/tools/util/spin_slider.gd
Executable file
@@ -0,0 +1,385 @@
|
||||
@tool
|
||||
extends Control
|
||||
|
||||
const FG_MARGIN = 2
|
||||
const MAX_DECIMALS_VISUAL = 3
|
||||
|
||||
signal value_changed(value)
|
||||
|
||||
|
||||
var _value := 0.0
|
||||
@export var value: float:
|
||||
get:
|
||||
return _value
|
||||
set(v):
|
||||
set_value_no_notify(v)
|
||||
|
||||
|
||||
var _min_value := 0.0
|
||||
@export var min_value: float:
|
||||
get:
|
||||
return _min_value
|
||||
set(v):
|
||||
set_min_value(v)
|
||||
|
||||
|
||||
var _max_value := 100.0
|
||||
@export var max_value: float:
|
||||
get:
|
||||
return _max_value
|
||||
set(v):
|
||||
set_max_value(v)
|
||||
|
||||
|
||||
var _prefix := ""
|
||||
@export var prefix: String:
|
||||
get:
|
||||
return _prefix
|
||||
set(v):
|
||||
set_prefix(v)
|
||||
|
||||
|
||||
var _suffix := ""
|
||||
@export var suffix: String:
|
||||
get:
|
||||
return _suffix
|
||||
set(v):
|
||||
set_suffix(v)
|
||||
|
||||
|
||||
var _rounded := false
|
||||
@export var rounded: bool:
|
||||
get:
|
||||
return _rounded
|
||||
set(v):
|
||||
set_rounded(v)
|
||||
|
||||
|
||||
var _centered := true
|
||||
@export var centered: bool:
|
||||
get:
|
||||
return _centered
|
||||
set(v):
|
||||
set_centered(v)
|
||||
|
||||
|
||||
var _allow_greater := false
|
||||
@export var allow_greater: bool:
|
||||
get:
|
||||
return _allow_greater
|
||||
set(v):
|
||||
set_allow_greater(v)
|
||||
|
||||
|
||||
# There is still a limit when typing a larger value, but this one is to prevent software
|
||||
# crashes or freezes. The regular min and max values are for slider UX. Exceeding it should be
|
||||
# a corner case.
|
||||
var _greater_max_value := 10000.0
|
||||
@export var greater_max_value: float:
|
||||
get:
|
||||
return _greater_max_value
|
||||
set(v):
|
||||
set_greater_max_value(v)
|
||||
|
||||
|
||||
var _label : Label
|
||||
var _label2 : Label
|
||||
var _line_edit : LineEdit
|
||||
var _ignore_line_edit := false
|
||||
var _pressing := false
|
||||
var _grabbing := false
|
||||
var _press_pos := Vector2()
|
||||
|
||||
|
||||
func _init():
|
||||
custom_minimum_size = Vector2(32, 28)
|
||||
|
||||
_label = Label.new()
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label.clip_text = true
|
||||
#_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_label.anchor_top = 0
|
||||
_label.anchor_left = 0
|
||||
_label.anchor_right = 1
|
||||
_label.anchor_bottom = 1
|
||||
_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_label.add_theme_color_override("font_color_shadow", Color(0,0,0,0.5))
|
||||
_label.add_theme_constant_override("shadow_offset_x", 1)
|
||||
_label.add_theme_constant_override("shadow_offset_y", 1)
|
||||
add_child(_label)
|
||||
|
||||
_label2 = Label.new()
|
||||
_label2.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
_label2.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_label2.clip_text = true
|
||||
#_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_label2.anchor_top = 0
|
||||
_label2.anchor_left = 0
|
||||
_label2.anchor_right = 1
|
||||
_label2.anchor_bottom = 1
|
||||
_label2.offset_left = 8
|
||||
_label2.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_label2.add_theme_color_override("font_color_shadow", Color(0,0,0,0.5))
|
||||
_label2.add_theme_constant_override("shadow_offset_x", 1)
|
||||
_label2.add_theme_constant_override("shadow_offset_y", 1)
|
||||
_label2.hide()
|
||||
add_child(_label2)
|
||||
|
||||
_line_edit = LineEdit.new()
|
||||
_line_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_line_edit.anchor_top = 0
|
||||
_line_edit.anchor_left = 0
|
||||
_line_edit.anchor_right = 1
|
||||
_line_edit.anchor_bottom = 1
|
||||
_line_edit.gui_input.connect(_on_LineEdit_gui_input)
|
||||
_line_edit.focus_exited.connect(_on_LineEdit_focus_exited)
|
||||
_line_edit.text_submitted.connect(_on_LineEdit_text_submitted)
|
||||
_line_edit.hide()
|
||||
add_child(_line_edit)
|
||||
|
||||
mouse_default_cursor_shape = Control.CURSOR_HSIZE
|
||||
|
||||
|
||||
func _ready():
|
||||
pass # Replace with function body.
|
||||
|
||||
|
||||
func set_centered(p_centered: bool):
|
||||
_centered = p_centered
|
||||
if _centered:
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_label.offset_right = 0
|
||||
_label2.hide()
|
||||
else:
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_label.offset_right = -8
|
||||
_label2.show()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_centered() -> bool:
|
||||
return _centered
|
||||
|
||||
|
||||
func set_value_no_notify(v: float):
|
||||
set_value(v, false, false)
|
||||
|
||||
|
||||
func set_value(v: float, notify_change: bool, use_slider_maximum: bool = false):
|
||||
if _allow_greater and not use_slider_maximum:
|
||||
v = clampf(v, _min_value, _greater_max_value)
|
||||
else:
|
||||
v = clampf(v, _min_value, _max_value)
|
||||
|
||||
if v != _value:
|
||||
_value = v
|
||||
|
||||
queue_redraw()
|
||||
|
||||
if notify_change:
|
||||
value_changed.emit(get_value())
|
||||
|
||||
|
||||
func get_value():
|
||||
if _rounded:
|
||||
return int(roundf(_value))
|
||||
return _value
|
||||
|
||||
|
||||
func set_min_value(minv: float):
|
||||
_min_value = minv
|
||||
#queue_redraw()
|
||||
|
||||
|
||||
func get_min_value() -> float:
|
||||
return _min_value
|
||||
|
||||
|
||||
func set_max_value(maxv: float):
|
||||
_max_value = maxv
|
||||
#queue_redraw()
|
||||
|
||||
|
||||
func get_max_value() -> float:
|
||||
return _max_value
|
||||
|
||||
|
||||
func set_greater_max_value(gmax: float):
|
||||
_greater_max_value = gmax
|
||||
|
||||
|
||||
func get_greater_max_value() -> float:
|
||||
return _greater_max_value
|
||||
|
||||
|
||||
func set_rounded(b: bool):
|
||||
_rounded = b
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func is_rounded() -> bool:
|
||||
return _rounded
|
||||
|
||||
|
||||
func set_prefix(p_prefix: String):
|
||||
_prefix = p_prefix
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_prefix() -> String:
|
||||
return _prefix
|
||||
|
||||
|
||||
func set_suffix(p_suffix: String):
|
||||
_suffix = p_suffix
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_suffix() -> String:
|
||||
return _suffix
|
||||
|
||||
|
||||
func set_allow_greater(allow: bool):
|
||||
_allow_greater = allow
|
||||
|
||||
|
||||
func is_allowing_greater() -> bool:
|
||||
return _allow_greater
|
||||
|
||||
|
||||
func _set_from_pixel(px: float):
|
||||
var r := (px - FG_MARGIN) / (size.x - FG_MARGIN * 2.0)
|
||||
var v := _ratio_to_value(r)
|
||||
set_value(v, true, true)
|
||||
|
||||
|
||||
func get_ratio() -> float:
|
||||
return _value_to_ratio(get_value())
|
||||
|
||||
|
||||
func _ratio_to_value(r: float) -> float:
|
||||
return r * (_max_value - _min_value) + _min_value
|
||||
|
||||
|
||||
func _value_to_ratio(v: float) -> float:
|
||||
if absf(_max_value - _min_value) < 0.001:
|
||||
return 0.0
|
||||
return (v - _min_value) / (_max_value - _min_value)
|
||||
|
||||
|
||||
func _on_LineEdit_gui_input(event: InputEvent):
|
||||
if event is InputEventKey:
|
||||
if event.pressed:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
_ignore_line_edit = true
|
||||
_hide_line_edit()
|
||||
grab_focus()
|
||||
_ignore_line_edit = false
|
||||
|
||||
|
||||
func _on_LineEdit_focus_exited():
|
||||
if _ignore_line_edit:
|
||||
return
|
||||
_enter_text()
|
||||
|
||||
|
||||
func _on_LineEdit_text_submitted(text: String):
|
||||
_enter_text()
|
||||
|
||||
|
||||
func _enter_text():
|
||||
var s = _line_edit.text.strip_edges()
|
||||
if s.is_valid_float():
|
||||
var v := s.to_float()
|
||||
if not _allow_greater:
|
||||
v = minf(v, _max_value)
|
||||
set_value(v, true, false)
|
||||
_hide_line_edit()
|
||||
|
||||
|
||||
func _hide_line_edit():
|
||||
_line_edit.hide()
|
||||
_label.show()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _show_line_edit():
|
||||
_line_edit.show()
|
||||
_line_edit.text = str(get_value())
|
||||
_line_edit.select_all()
|
||||
_line_edit.grab_focus()
|
||||
_label.hide()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_press_pos = event.position
|
||||
_pressing = true
|
||||
else:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
_pressing = false
|
||||
if _grabbing:
|
||||
_grabbing = false
|
||||
_set_from_pixel(event.position.x)
|
||||
else:
|
||||
_show_line_edit()
|
||||
|
||||
elif event is InputEventMouseMotion:
|
||||
if _pressing and _press_pos.distance_to(event.position) > 2.0:
|
||||
_grabbing = true
|
||||
if _grabbing:
|
||||
_set_from_pixel(event.position.x)
|
||||
|
||||
|
||||
func _draw():
|
||||
if _line_edit.visible:
|
||||
return
|
||||
|
||||
#var grabber_width := 3
|
||||
var background_v_margin := 0
|
||||
var foreground_margin := FG_MARGIN
|
||||
#var grabber_color := Color(0.8, 0.8, 0.8)
|
||||
var interval_color := Color(0.4,0.4,0.4)
|
||||
var background_color := Color(0.1, 0.1, 0.1)
|
||||
|
||||
var control_rect := Rect2(Vector2(), size)
|
||||
|
||||
var bg_rect := Rect2(
|
||||
control_rect.position.x,
|
||||
control_rect.position.y + background_v_margin,
|
||||
control_rect.size.x,
|
||||
control_rect.size.y - 2 * background_v_margin)
|
||||
draw_rect(bg_rect, background_color)
|
||||
|
||||
var fg_rect := control_rect.grow(-foreground_margin)
|
||||
# Clamping the ratio because the value can be allowed to exceed the slider's boundaries
|
||||
var ratio := clampf(get_ratio(), 0.0, 1.0)
|
||||
fg_rect.size.x *= ratio
|
||||
draw_rect(fg_rect, interval_color)
|
||||
|
||||
var value_text := str(get_value())
|
||||
|
||||
var dot_pos := value_text.find(".")
|
||||
if dot_pos != -1:
|
||||
var decimal_count := len(value_text) - dot_pos
|
||||
if decimal_count > MAX_DECIMALS_VISUAL:
|
||||
value_text = value_text.substr(0, dot_pos + MAX_DECIMALS_VISUAL + 1)
|
||||
|
||||
if _centered:
|
||||
var text := value_text
|
||||
if _prefix != "":
|
||||
text = str(_prefix, " ", text)
|
||||
if _suffix != "":
|
||||
text = str(text, " ", _suffix)
|
||||
_label.text = text
|
||||
|
||||
else:
|
||||
_label2.text = _prefix
|
||||
var text := value_text
|
||||
if _suffix != "":
|
||||
text = str(text, " ", _suffix)
|
||||
_label.text = text
|
||||
1
addons/zylann.hterrain/tools/util/spin_slider.gd.uid
Normal file
1
addons/zylann.hterrain/tools/util/spin_slider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b2xd6th11r23e
|
||||
13
addons/zylann.hterrain/tools/util/spin_slider.tscn
Normal file
13
addons/zylann.hterrain/tools/util/spin_slider.tscn
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cq247pyah78ey"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b2xd6th11r23e" path="res://addons/zylann.hterrain/tools/util/spin_slider.gd" id="1"]
|
||||
|
||||
[node name="SpinSlider" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
rect_min_size = Vector2(32, 28)
|
||||
mouse_default_cursor_shape = 10
|
||||
script = ExtResource("1")
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
Reference in New Issue
Block a user