extends HFlowContainer
## THE TOOL SETTINGS ROW (editor redesign E4, docs/editor-redesign.md §8b): the active
## tool's own settings, under the top bar — TrenchBroom's tool book, Blender's tool header,
## Godot's context toolbar slot.
##
## GENERATED FROM DATA. Every row is a LevelOperator.tool_settings() descriptor; this file
## knows how to draw a descriptor type, never what any tool's settings are. A new tool, or a
## new shape with its own params, shows up here with no edit to this file.
##
## IT FOLLOWS THE OPERATOR, like the top bar follows the map: keys ([ ], Tab, X Y Z) change
## values without telling the bar, so each frame it re-reads the descriptors and updates the
## controls without their signals (set_*_no_signal), and rebuilds only when the SHAPE of the
## list changes (another tool, or a shape whose params differ).
##
## FOCUS: buttons and dropdowns take none — a focused button eats the arrows before the map
## sees them (the UV "works a bit then stops" bug). A SpinBox's text field takes click focus
## so a value can be typed; Enter or Escape hands the keyboard straight back.
const UNIT_SUFFIX := {"deg": "°"}
var map: Node = null
var _stack: Node = null
var _op: LevelOperator = null
var _sig := ""
var _controls: Dictionary = {} # setting id -> {type, node, group (enum buttons), extra}
var _more_btn: Button = null
var _more_pop: PopupPanel = null
var _more_box: GridContainer = null
func setup(map_node: Node) -> void:
map = map_node
add_theme_constant_override("h_separation", 6)
add_theme_constant_override("v_separation", 4)
_rebuild()
## ADJUST MODE (E5 Adjust Last): the same renderer pointed at a FIXED operator that is no
## longer active, showing only the settings it can re-run with, and routing changes to
## OperatorStack.adjust instead of the operator.
var target: LevelOperator = null
var apply_override: Callable = Callable()
func active_op() -> LevelOperator:
if target != null:
return target
if _stack == null and map != null and map.has_method("_ensure_ops"):
_stack = map.call("_ensure_ops")
return _stack.active if _stack != null else null
func _rows(op: LevelOperator) -> Array:
if op == null:
return []
var rows: Array = op.tool_settings()
if target == null:
return rows
var keep: Array = op.adjust_ids()
var out: Array = []
for d: Dictionary in rows:
if keep.has(String(d.get("id"))):
out.append(d)
return out
## The control drawn for a setting id (gates drive it).
func control(sid: String) -> Control:
return (_controls.get(sid, {}) as Dictionary).get("node") as Control
func setting_ids() -> Array:
return _controls.keys()
func _process(_d: float) -> void:
if map == null or not is_visible_in_tree():
return
var op := active_op()
var rows: Array = _rows(op)
var sig := _signature(op, rows)
if op != _op or sig != _sig:
_op = op
_sig = sig
_rebuild(rows)
else:
_sync(rows)
static func _signature(op: LevelOperator, rows: Array) -> String:
if op == null:
return ""
var parts: Array[String] = [op.op_name()]
for d: Dictionary in rows:
parts.append("%s:%s:%s" % [String(d.get("id")), String(d.get("type")), str(d.get("choices", []))])
return "|".join(parts)
func _clear() -> void:
for c in get_children():
remove_child(c)
c.queue_free()
_controls.clear()
_more_btn = null
_more_pop = null
_more_box = null
func _rebuild(rows: Array = []) -> void:
_clear()
if _op == null:
if target == null:
_idle_hint()
return
if target != null:
rows = _rows(_op) # adjust mode: never the tool's own action buttons
var title := Label.new()
title.text = _op.op_name().to_upper()
title.add_theme_color_override("font_color", Color(1.0, 0.4, 0.35))
add_child(title)
for d: Dictionary in rows:
if bool(d.get("more", false)):
_ensure_more()
_more_box.add_child(_label(d))
_more_box.add_child(_make(d))
else:
if String(d.get("type")) != "action":
add_child(_label(d))
add_child(_make(d))
if _more_btn != null:
move_child(_more_btn, get_child_count() - 1)
## No tool up: name the tools and their LIVE keys, so the row teaches rather than sits empty.
func _idle_hint() -> void:
var l := Label.new()
var parts: Array[String] = []
for a: Array in [["builder.add_menu", "add"], ["builder.knife", "knife"], ["builder.move", "move"],
["builder.rotate", "rotate"], ["builder.scale", "scale"], ["builder.extrude", "extrude"]]:
var k: String = Keymap.describe(String(a[0]))
if k != "":
parts.append("%s %s" % [k, String(a[1])])
l.text = "No tool — " + " · ".join(parts)
l.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
# SHRINKS, never widens the band: an unclipped Label's minimum width is its whole text,
# which pushed the top band past the gap between the columns of a 1280-px window
# (gate_editor_e1). Ellipsis on a narrow window; the tooltip keeps the full line.
l.clip_text = true
l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
l.custom_minimum_size = Vector2(160, 0)
l.tooltip_text = l.text
l.mouse_filter = Control.MOUSE_FILTER_PASS
add_child(l)
func _ensure_more() -> void:
if _more_btn != null:
return
_more_btn = Button.new()
_more_btn.text = "⋯"
_more_btn.flat = true
_more_btn.focus_mode = Control.FOCUS_NONE
_more_btn.tooltip_text = "More settings"
_more_btn.pressed.connect(_on_more)
add_child(_more_btn)
_more_pop = PopupPanel.new()
_more_box = GridContainer.new()
_more_box.columns = 2
_more_pop.add_child(_more_box)
add_child(_more_pop)
func _on_more() -> void:
var r := _more_btn.get_global_rect()
_more_pop.popup(Rect2i(Vector2i(r.position + Vector2(0, r.size.y)), Vector2i.ZERO))
func _label(d: Dictionary) -> Label:
var l := Label.new()
l.text = String(d.get("label", d.get("id")))
l.tooltip_text = String(d.get("tip", ""))
l.mouse_filter = Control.MOUSE_FILTER_PASS
return l
func _make(d: Dictionary) -> Control:
var sid := String(d.get("id"))
var tip := String(d.get("tip", ""))
match String(d.get("type")):
"enum":
var choices: Array = d.get("choices", [])
if choices.size() <= 4:
var row := HBoxContainer.new()
var group := ButtonGroup.new()
var buttons: Array = []
for i in choices.size():
var b := Button.new()
b.text = String(choices[i])
b.toggle_mode = true
b.button_group = group
b.focus_mode = Control.FOCUS_NONE
b.tooltip_text = tip
b.button_pressed = int(d.get("value", 0)) == i
b.pressed.connect(_on_enum.bind(sid, i))
row.add_child(b)
buttons.append(b)
_controls[sid] = {"type": "enum_buttons", "node": row, "buttons": buttons}
return row
var ob := OptionButton.new()
ob.focus_mode = Control.FOCUS_NONE
ob.tooltip_text = tip
for c in choices:
ob.add_item(String(c))
ob.select(int(d.get("value", 0)))
ob.item_selected.connect(_on_option.bind(sid))
_controls[sid] = {"type": "enum_option", "node": ob}
return ob
"int", "float":
var row := HBoxContainer.new()
var sb := SpinBox.new()
sb.min_value = float(d.get("min", -1.0e9))
sb.max_value = float(d.get("max", 1.0e9))
sb.step = float(d.get("step", 1.0 if String(d.get("type")) == "int" else 0.01))
sb.allow_greater = not d.has("max")
sb.allow_lesser = not d.has("min")
sb.suffix = String(UNIT_SUFFIX.get(String(d.get("unit", "")), ""))
sb.tooltip_text = tip
sb.custom_minimum_size = Vector2(76, 0)
sb.set_value_no_signal(float(d.get("value", 0)))
sb.focus_mode = Control.FOCUS_NONE
sb.get_line_edit().focus_mode = Control.FOCUS_CLICK
sb.get_line_edit().text_submitted.connect(_on_submitted.bind(sb))
sb.value_changed.connect(_on_number.bind(sid))
row.add_child(sb)
# A grid-step count is read in the player's length unit beside it ("= 16u").
var extra: Label = null
if String(d.get("unit", "")) == "grid":
extra = Label.new()
extra.add_theme_color_override("font_color", Color(0.75, 0.75, 0.75))
row.add_child(extra)
_set_grid_text(extra, float(d.get("value", 0)))
_controls[sid] = {"type": "number", "node": row, "spin": sb, "extra": extra}
return row
"bool":
var cb := CheckButton.new()
cb.text = ""
cb.focus_mode = Control.FOCUS_NONE
cb.tooltip_text = tip
cb.button_pressed = bool(d.get("value", false))
cb.toggled.connect(_on_bool.bind(sid))
_controls[sid] = {"type": "bool", "node": cb}
return cb
"action":
var b := Button.new()
b.text = String(d.get("label", sid))
b.focus_mode = Control.FOCUS_NONE
b.tooltip_text = tip
b.pressed.connect(_on_action.bind(sid))
_controls[sid] = {"type": "action", "node": b}
return b
var unknown := Label.new()
unknown.text = "?"
return unknown
func _set_grid_text(l: Label, steps: float) -> void:
if l == null or map == null or not map.has_method("fmt_units") or not map.has_method("grid_units"):
return
l.text = "= %s" % String(map.fmt_units(steps * float(map.grid_units())))
## Values follow the operator without firing the controls' own signals.
func _sync(rows: Array) -> void:
for d: Dictionary in rows:
var c: Dictionary = _controls.get(String(d.get("id")), {})
if c.is_empty():
continue
match String(c["type"]):
"enum_buttons":
var buttons: Array = c["buttons"]
for i in buttons.size():
var b: Button = buttons[i]
if b.button_pressed != (i == int(d.get("value", 0))):
b.set_pressed_no_signal(i == int(d.get("value", 0)))
"enum_option":
var ob: OptionButton = c["node"]
if ob.selected != int(d.get("value", 0)):
ob.select(int(d.get("value", 0)))
"number":
var sb: SpinBox = c["spin"]
# Not while the player is typing into it — that would overwrite the keystrokes.
if not sb.get_line_edit().has_focus() and not is_equal_approx(sb.value, float(d.get("value", 0))):
sb.set_value_no_signal(float(d.get("value", 0)))
_set_grid_text(c.get("extra") as Label, float(d.get("value", 0)))
"bool":
var cb: CheckButton = c["node"]
if cb.button_pressed != bool(d.get("value", false)):
cb.set_pressed_no_signal(bool(d.get("value", false)))
func _apply(sid: String, v: Variant) -> void:
if apply_override.is_valid():
apply_override.call(sid, v)
return
var op := active_op()
if op != null:
op.set_tool_setting(sid, v)
if map != null and map.has_method("_update_op_hud"):
map.call("_update_op_hud")
func _on_enum(sid: String, i: int) -> void:
_apply(sid, i)
func _on_option(i: int, sid: String) -> void:
_apply(sid, i)
func _on_number(v: float, sid: String) -> void:
_apply(sid, v)
func _on_bool(on: bool, sid: String) -> void:
_apply(sid, on)
func _on_action(sid: String) -> void:
_apply(sid, true)
## Enter in a typed value: apply it (SpinBox does on submit) and give the keyboard back.
func _on_submitted(_t: String, sb: SpinBox) -> void:
sb.apply()
sb.get_line_edit().release_focus()