Skip to content

Commit

Permalink
Add optimized LineEditButton control (#731)
Browse files Browse the repository at this point in the history
  • Loading branch information
MewPurPur authored May 14, 2024
1 parent efb8dae commit 3d5dffd
Show file tree
Hide file tree
Showing 40 changed files with 464 additions and 346 deletions.
1 change: 1 addition & 0 deletions godot_only/icons/LineEditButton.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions godot_only/icons/LineEditButton.svg.import
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[remap]

importer="texture"
type="CompressedTexture2D"
uid="uid://bfpat62uepne8"
path="res://.godot/imported/LineEditButton.svg-8852cdccf228ac979cde671d6750b2e2.ctex"
metadata={
"vram_texture": false
}

[deps]

source_file="res://godot_only/icons/LineEditButton.svg"
dest_files=["res://.godot/imported/LineEditButton.svg-8852cdccf228ac979cde671d6750b2e2.ctex"]

[params]

compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
2 changes: 1 addition & 1 deletion src/FileUtils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ static func apply_svg_from_path(path: String) -> int:
elif extension != "svg":
error = TranslationServer.translate(
"\"{passed_extension}\" is a unsupported file extension. Only \"svg\" files are supported.").format({"passed_extension": extension})
elif svg_file == null:
elif !is_instance_valid(svg_file):
error = TranslationServer.translate(
"The file couldn't be opened.\nTry checking the file path, ensure that the file is not deleted, or choose a different file.")

Expand Down
27 changes: 15 additions & 12 deletions src/HandlerGUI.gd
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func remove_overlay(overlay_ref: ColorRect = null) -> void:
return
# If an overlay_ref is passed but doesn't match, do nothing.
# This is a hack against exiting overlay menus closing other menus than their own.
if overlay_ref != null and overlay_ref != overlay_stack.back():
if is_instance_valid(overlay_ref) and overlay_ref != overlay_stack.back():
return

overlay_ref = overlay_stack.pop_back()
Expand Down Expand Up @@ -87,7 +87,7 @@ func remove_popup_overlay(overlay_ref: Control = null) -> void:
if popup_overlay_stack.is_empty():
return
# Refer to remove_overlay() for why the logic is like this.
if overlay_ref != null and overlay_ref != popup_overlay_stack.back():
if is_instance_valid(overlay_ref) and overlay_ref != popup_overlay_stack.back():
return

overlay_ref = popup_overlay_stack.pop_back()
Expand Down Expand Up @@ -157,6 +157,16 @@ func _parse_popup_overlay_event(event: InputEvent) -> void:
var last_mouse_click_double := false

func _input(event: InputEvent) -> void:
# Clear popups or overlays.
if not popup_overlay_stack.is_empty() and event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
remove_popup_overlay()
return
elif not overlay_stack.is_empty() and event.is_action_pressed("ui_cancel"):
get_viewport().set_input_as_handled()
remove_overlay()
return

# So, it turns out that when you double click, only the press will count as such.
# I don't like that, and it causes problems! So mark the release as double_click too.
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
Expand All @@ -173,18 +183,11 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
FileUtils.open_save_dialog("svg", FileUtils.native_file_save, FileUtils.save_svg_to_file)


func _unhandled_input(event: InputEvent) -> void:
# Clear popups or overlays.
if not popup_overlay_stack.is_empty():
# Stop stuff from propagating when there's overlays.
if not popup_overlay_stack.is_empty() or not overlay_stack.is_empty():
get_viewport().set_input_as_handled()
if event.is_action_pressed("ui_cancel"):
remove_popup_overlay()
return
elif not overlay_stack.is_empty():
get_viewport().set_input_as_handled()
if event.is_action_pressed("ui_cancel"):
remove_overlay()
return

if event.is_action_pressed("redo"):
get_viewport().set_input_as_handled()
Expand Down
2 changes: 1 addition & 1 deletion src/Utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ static func create_btn(text: String, press_action: Callable, disabled := false,
icon: Texture2D = null) -> Button:
var btn := Button.new()
btn.text = text
if icon != null:
if is_instance_valid(icon):
btn.icon = icon
if disabled:
btn.disabled = true
Expand Down
2 changes: 1 addition & 1 deletion src/data_classes/TagSVG.gd
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func delete_tags(tids: Array[PackedInt32Array]) -> void:
for tid in tids:
var parent_tid := Utils.get_parent_tid(tid)
var parent_tag := get_tag(parent_tid)
if parent_tag != null:
if is_instance_valid(parent_tag):
var tag_idx := tid[-1]
if tag_idx < parent_tag.get_child_count():
parent_tag.child_tags.remove_at(tag_idx)
Expand Down
29 changes: 16 additions & 13 deletions src/ui_elements/BetterLineEdit.gd
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,43 @@ func _ready() -> void:
func _input(event: InputEvent) -> void:
if has_focus():
if event is InputEventMouseButton:
if event.is_pressed() and not get_global_rect().has_point(event.position):
release_focus()
text_submitted.emit(text)
elif event.is_released() and first_click and not has_selection():
if event.is_released() and first_click and not has_selection():
first_click = false
select_all()
elif first_click:
first_click = false
select_all()
elif event.is_action_pressed("ui_focus_next") || event.is_action_pressed("ui_focus_prev"):
elif event.is_action_pressed("ui_focus_next") or\
event.is_action_pressed("ui_focus_prev"):
text_submitted.emit(text)

var tree_was_paused_before := false
func _unhandled_input(event: InputEvent) -> void:
# Context menu items need the LineEdit to exist in order to ask it for things
# like its text, so this needs to be in unhandled_input so it doesn't unfocus
# when you click on buttons.
if has_focus() and event is InputEventMouseButton and event.is_pressed() and\
not get_global_rect().has_point(event.position):
release_focus()
text_submitted.emit(text)

var first_click := false
var text_before_focus := ""

func _on_base_class_focus_entered() -> void:
process_mode = PROCESS_MODE_ALWAYS
tree_was_paused_before = get_tree().paused
first_click = true
text_before_focus = text
if not tree_was_paused_before:
get_tree().paused = true

func _on_base_class_focus_exited() -> void:
process_mode = PROCESS_MODE_INHERIT
first_click = false
if not tree_was_paused_before:
get_tree().paused = false
if Input.is_action_pressed("ui_cancel"):
text = text_before_focus
text_change_canceled.emit()

func _on_base_class_text_submitted(_submitted_text) -> void:
if not Input.is_action_just_pressed("ui_focus_next") and not Input.is_action_just_pressed("ui_focus_prev"):
if not Input.is_action_just_pressed("ui_focus_next") and\
not Input.is_action_just_pressed("ui_focus_prev"):
release_focus()


Expand Down Expand Up @@ -126,4 +128,5 @@ func _gui_input(event: InputEvent) -> void:
HandlerGUI.popup_under_pos(context_popup, vp.get_mouse_position(), vp)
accept_event()
# Wow, no way to find out the column of a given click? Okay...
# TODO Make it so LineEdit caret automatically moves to the clicked position.
# TODO Make it so LineEdit caret automatically moves to the clicked position
# to finish the right-click logic.
2 changes: 1 addition & 1 deletion src/ui_elements/BetterTextEdit.gd
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func _draw() -> void:
if editable and _hovered and has_theme_stylebox("hover"):
draw_style_box(get_theme_stylebox("hover"), Rect2(Vector2.ZERO, size))

func _input(event: InputEvent) -> void:
func _unhandled_input(event: InputEvent) -> void:
if (has_focus() and event is InputEventMouseButton and event.is_pressed() and\
not get_global_rect().has_point(event.position)):
release_focus()
Expand Down
3 changes: 2 additions & 1 deletion src/ui_elements/BetterToggleButton.gd
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ func _on_mouse_exited() -> void:
queue_redraw()

func _draw() -> void:
if not disabled and button_pressed and _hovered and hover_pressed_stylebox != null:
if not disabled and button_pressed and _hovered and\
is_instance_valid(hover_pressed_stylebox):
draw_style_box(hover_pressed_stylebox, Rect2(Vector2.ZERO, size))
188 changes: 188 additions & 0 deletions src/ui_elements/LineEditButton.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
@icon("res://godot_only/icons/LineEditButton.svg")
class_name LineEditButton extends Control
## An optimized control representing a LineEdit with a button attached to it.

# A fake-out is drawn to avoid adding unnecessary nodes.
# The real controls are only created when necessary, such as when hovered or focused.

const BUTTON_WIDTH = 14.0

signal pressed
signal text_change_canceled
signal text_changed
signal text_submitted
signal button_gui_input

var _should_stay_active_outside := false
var _is_mouse_outside := true

var active := false
var temp_line_edit: BetterLineEdit
var temp_button: Button

@export var placeholder_text: String:
set(new_value):
if placeholder_text != new_value:
placeholder_text = new_value
if active:
temp_line_edit.placeholder_text = new_value
else:
queue_redraw()

@export var text: String:
set(new_value):
if text != new_value:
text = new_value
if active:
temp_line_edit.text = new_value
else:
queue_redraw()

@export var font_color := Color.TRANSPARENT:
set(new_value):
if font_color != new_value:
font_color = new_value
if active:
temp_line_edit.add_theme_color_override("font_color", _get_font_color())
else:
queue_redraw()

@export var icon: Texture2D
@export var button_visuals := true
@export var code_font := true

var ci := get_canvas_item()


func reset_font_color() -> void:
font_color = Color.TRANSPARENT # This is the value treated as invalid.


func _init() -> void:
custom_minimum_size.y = 22
set_anchors_and_offsets_preset(PRESET_TOP_LEFT)
focus_mode = Control.FOCUS_ALL
focus_entered.connect(_on_base_class_focus_entered)
mouse_entered.connect(_on_base_class_mouse_entered)
mouse_exited.connect(_on_base_class_mouse_exited)

func _on_base_class_mouse_entered() -> void:
_is_mouse_outside = false
_setup()

func _on_base_class_focus_entered() -> void:
_setup()

func _on_base_class_mouse_exited() -> void:
_is_mouse_outside = true
if not _should_stay_active_outside:
_setdown()

func _on_underlying_control_focused() -> void:
_should_stay_active_outside = true
focus_entered.emit()

func _on_underlying_control_unfocused() -> void:
_should_stay_active_outside = false
if _is_mouse_outside:
_setdown()

func _setup() -> void:
if not active:
active = true
temp_line_edit = BetterLineEdit.new()
temp_line_edit.custom_minimum_size =\
Vector2(custom_minimum_size.x - BUTTON_WIDTH, 22)
temp_line_edit.tooltip_text = tooltip_text
temp_line_edit.placeholder_text = placeholder_text
temp_line_edit.text = text
temp_line_edit.mouse_filter = Control.MOUSE_FILTER_PASS
temp_line_edit.theme_type_variation = "RightConnectedLineEdit"
if font_color != Color.TRANSPARENT:
temp_line_edit.add_theme_color_override("font_color", _get_font_color())
temp_line_edit.text_change_canceled.connect(emit_text_change_canceled)
temp_line_edit.text_changed.connect(emit_text_changed)
temp_line_edit.text_submitted.connect(emit_text_submitted)
temp_line_edit.focus_entered.connect(_on_underlying_control_focused)
temp_line_edit.focus_exited.connect(_on_underlying_control_unfocused)
add_child(temp_line_edit)
temp_button = Button.new()
temp_button.show_behind_parent = true # Lets the icon draw in front.
temp_button.custom_minimum_size = Vector2(BUTTON_WIDTH, 22)
temp_button.position.x = size.x - BUTTON_WIDTH
temp_button.focus_mode = Control.FOCUS_NONE
temp_button.mouse_filter = Control.MOUSE_FILTER_PASS
temp_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
if button_visuals:
temp_button.theme_type_variation = "LeftConnectedButton"
else:
temp_button.flat = true
temp_button.pressed.connect(emit_pressed)
temp_button.gui_input.connect(emit_button_gui_input)
temp_button.button_down.connect(_on_underlying_control_focused)
temp_button.button_up.connect(_on_underlying_control_unfocused)
add_child(temp_button)
queue_redraw()
# If there aren't button visuals, then they are probably
# handed off to draw functions which need to be aware of the hover state.
if not button_visuals:
temp_button.mouse_exited.connect(queue_redraw)
temp_line_edit.mouse_exited.connect(queue_redraw)

func _setdown() -> void:
if active:
active = false
temp_line_edit.queue_free()
temp_button.queue_free()
queue_redraw()


func _draw() -> void:
var sb: StyleBoxFlat = get_theme_stylebox("normal", "LineEdit")
var horizontal_margin_width := sb.content_margin_left + sb.content_margin_right
if not active:
draw_style_box(sb, Rect2(Vector2.ZERO, size))
draw_line(Vector2(size.x - BUTTON_WIDTH, 0),
Vector2(size.x - BUTTON_WIDTH, size.y), sb.border_color, 2)
# The default overrun behavior couldn't be changed for the simplest draw methods.
var text_line_object := TextLine.new()
text_line_object.text_overrun_behavior = TextServer.OVERRUN_TRIM_CHAR
text_line_object.width = size.x - BUTTON_WIDTH - horizontal_margin_width
text_line_object.add_string(placeholder_text if text.is_empty() else text,
get_theme_font("font", "LineEdit"), get_theme_font_size("font_size", "LineEdit"))
text_line_object.draw(ci, Vector2(5, 2), get_theme_color("font_placeholder_color",
"LineEdit") if text.is_empty() else _get_font_color())

if is_instance_valid(icon):
var icon_side := BUTTON_WIDTH - horizontal_margin_width + 2
icon.draw_rect(ci, Rect2(size.x - (BUTTON_WIDTH + 0.5 + icon_side) / 2,
(size.y - icon_side) / 2, icon_side, icon_side), false)


func emit_pressed() -> void:
pressed.emit()

func emit_text_change_canceled() -> void:
text_change_canceled.emit()

func emit_text_changed(new_text: String) -> void:
text_changed.emit(new_text)

func emit_text_submitted(new_text: String) -> void:
text_submitted.emit(new_text)

func emit_button_gui_input(event: InputEvent) -> void:
button_gui_input.emit(event)


# Helpers

func _get_font_color() -> Color:
return get_theme_color("font_color", "LineEdit") if font_color == Color.TRANSPARENT\
else font_color

func draw_button_border(theme_name: String) -> void:
var button_outline: StyleBoxFlat =\
get_theme_stylebox(theme_name, "LeftConnectedButton").duplicate()
button_outline.draw_center = false
button_outline.draw(ci, Rect2(size.x - BUTTON_WIDTH, 0, BUTTON_WIDTH, size.y))
Loading

0 comments on commit 3d5dffd

Please sign in to comment.