OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / room_settings.gd

652 lines · gd ref:
extends Window
## Room Settings window — configure room, manage members, notifications.

const SETTINGS_VERSION := "v14"  # Bump on each deploy to verify build

signal room_left(room_id: String)

@onready var room_title_label: Label = %RoomTitle
@onready var avatar_preview: TextureRect = %AvatarPreview
@onready var change_avatar_btn: Button = %ChangeAvatarBtn
@onready var title_field: LineEdit = %TitleField
@onready var desc_field: TextEdit = %DescField
@onready var calls_check: CheckButton = %CallsCheck
@onready var lock_check: CheckButton = %LockCheck
@onready var member_events_check: CheckButton = %MemberEventsCheck
@onready var topics_field: LineEdit = %TopicsField
@onready var discoverable_check: CheckButton = %DiscoverableCheck
@onready var discovery_list: VBoxContainer = %DiscoveryList
@onready var discovery_status: Label = %DiscoveryStatus
@onready var save_btn: Button = %SaveBtn
@onready var save_status: Label = %SaveStatus

@onready var invite_btn: Button = %InviteBtn
@onready var invite_result: LineEdit = %InviteResult
@onready var member_list: VBoxContainer = %MemberList

@onready var mute_all_check: CheckButton = %MuteAllCheck
@onready var mentions_only_check: CheckButton = %MentionsOnlyCheck
@onready var bookmark_check: CheckButton = %BookmarkCheck
@onready var notif_save_btn: Button = %NotifSaveBtn

@onready var leave_btn: Button = %LeaveBtn
@onready var purge_btn: Button = %PurgeBtn

var _room_id: String = ""
var _room_title: String = ""
var _config: Dictionary = {}
var _meta: Dictionary = {}  # Custom metadata from _hs:room-meta pinned message
# Per-registry discovery state: { registry_key: { label, is_default, registered } }
var _registry_state: Dictionary = {}
var _registries: Array = []
var _avatar_picker: AvatarPicker = null
var _my_member_id: String = ""
var _my_room_role: String = ""  # admin, mod, peer, etc.


func _ready() -> void:
	close_requested.connect(_on_close_requested)
	save_btn.pressed.connect(_on_save)
	invite_btn.pressed.connect(_on_invite)
	change_avatar_btn.pressed.connect(_on_change_avatar)
	notif_save_btn.pressed.connect(_on_save_notifications)
	leave_btn.pressed.connect(_on_leave)
	purge_btn.pressed.connect(_on_purge)
	save_status.text = ""

	# Placeholder avatar
	var placeholder := Image.create(64, 64, false, Image.FORMAT_RGBA8)
	placeholder.fill(Color(0.25, 0.25, 0.3))
	avatar_preview.texture = ImageTexture.create_from_image(placeholder)


func open_for_room(room_id: String, room_title: String) -> void:
	print("[room_settings %s] open_for_room: room=%s title='%s'" % [SETTINGS_VERSION, room_id.left(12), room_title])
	_room_id = room_id
	_room_title = room_title
	title = "Settings: %s" % room_title
	room_title_label.text = room_title
	save_status.text = ""
	invite_result.visible = false

	# Load room config (includes avatar)
	KeetService.room_mgr.get_config(room_id, func(config):
		_config = config if config is Dictionary else {}
		_populate_general()
		_load_room_avatar()
	)

	# Load custom metadata (topics, discoverable)
	_meta = {}
	KeetService.client.call_rpc("meta.get", {"roomId": room_id}, func(result, _err):
		_meta = result if result is Dictionary else {}
		_populate_meta()
	)

	# Get our identity then load members
	_my_room_role = ""
	KeetService.identity.get_identity(func(ident):
		_my_member_id = ident.get("memberId", "") if ident is Dictionary else ""
		KeetService.room_mgr.list_members(room_id, func(members):
			_populate_members(members)
		)
	)

	# Load preferences
	KeetService.room_mgr.get_preferences(room_id, func(prefs):
		_populate_notifications(prefs)
	)

	# Load all registries and check which ones this room is in
	_registry_state.clear()
	_registries.clear()
	discovery_status.text = "Loading groups..."
	KeetService.discovery.list_registries(func(regs):
		_registries = regs if regs is Array else []
		if _registries.is_empty():
			discovery_status.text = "No groups configured"
			_build_discovery_list()
			return
		# Build registry state and show checkboxes immediately (default unchecked)
		for reg: Variant in _registries:
			if not reg is Dictionary:
				continue
			var reg_key: String = reg.get("roomId", "")
			if reg_key == "":
				continue
			var reg_label: String = reg.get("title", reg_key.left(12) + "...")
			var is_default: bool = false
			var is_available: bool = true
			print("[room_settings] registry: key=%s label=%s" % [reg_key.left(12), reg_label])
			_registry_state[reg_key] = {
				"label": reg_label,
				"is_default": is_default,
				"registered": false,
				"available": is_available
			}
		_build_discovery_list()
		discovery_status.text = "Checking registrations..."
		# Now check which registries this room is listed in (skip unavailable)
		var check_keys: Array = []
		for reg_key: String in _registry_state:
			if _registry_state[reg_key].get("available", true):
				check_keys.append(reg_key)
		# Use Dictionary so lambdas share the counter by reference (int would be captured by value)
		var counter := {"pending": check_keys.size()}
		if counter["pending"] == 0:
			var avail: int = 0
			for _k: String in _registry_state:
				if _registry_state[_k].get("available", true):
					avail += 1
			discovery_status.text = "Listed in 0 of %d groups (%d unavailable)" % [avail, _registry_state.size() - avail]
			_build_discovery_list()
			return
		for reg_key: String in check_keys:
			KeetService.discovery.list_rooms(reg_key, "", func(rooms: Array, _rk = reg_key):
				for rm: Variant in rooms:
					if rm is Dictionary and rm.get("roomId", "") == _room_id:
						if _registry_state.has(_rk):
							_registry_state[_rk]["registered"] = true
						break
				counter["pending"] -= 1
				_build_discovery_list()
				if counter["pending"] <= 0:
					var count: int = 0
					var avail: int = 0
					for _k: String in _registry_state:
						if _registry_state[_k].get("registered", false):
							count += 1
						if _registry_state[_k].get("available", true):
							avail += 1
					if avail < _registry_state.size():
						discovery_status.text = "Listed in %d of %d groups (%d unavailable)" % [count, avail, _registry_state.size() - avail]
					else:
						discovery_status.text = "Listed in %d of %d groups" % [count, _registry_state.size()]
			)
		# Timeout fallback — if some registries have fd-lock issues, don't wait forever
		get_tree().create_timer(8.0).timeout.connect(func():
			if discovery_status.text == "Checking registrations...":
				var count: int = 0
				var avail: int = 0
				for _k: String in _registry_state:
					if _registry_state[_k].get("registered", false):
						count += 1
					if _registry_state[_k].get("available", true):
						avail += 1
				discovery_status.text = "Listed in %d of %d groups (%d unavailable)" % [count, avail, _registry_state.size() - avail]
				_build_discovery_list()
		)
	)

	popup_centered()


# --- General Tab ---

func _build_discovery_list() -> void:
	# Remove old children IMMEDIATELY (not deferred) so new ones are the only visible ones
	var old_children: Array = discovery_list.get_children()
	for child in old_children:
		discovery_list.remove_child(child)
		child.queue_free()

	if _registry_state.is_empty():
		discovery_status.text = "No groups configured"
		print("[room_settings %s] _build_discovery_list: empty registry_state" % SETTINGS_VERSION)
		return

	var count: int = 0
	for reg_key: String in _registry_state:
		var state: Dictionary = _registry_state[reg_key]
		var is_available: bool = state.get("available", true)
		var is_registered: bool = state.get("registered", false)
		var check := CheckButton.new()
		var label: String = state.get("label", reg_key.left(12) + "...")
		if state.get("is_default", false):
			label += " (default)"
		if not is_available:
			label += " (unavailable)"
			check.disabled = true
		check.text = label
		check.set_pressed_no_signal(is_registered)
		check.set_meta("registry_key", reg_key)
		check.add_theme_font_size_override("font_size", 12)
		if not is_available:
			check.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5))
		discovery_list.add_child(check)
		if is_registered:
			count += 1
		print("[room_settings %s] _build_discovery_list: key=%s registered=%s label=%s" % [SETTINGS_VERSION, reg_key.left(12), str(is_registered), label])

	# Update discovery_status to reflect current registration state
	var avail: int = 0
	for _k: String in _registry_state:
		if _registry_state[_k].get("available", true):
			avail += 1
	if avail < _registry_state.size():
		discovery_status.text = "Listed in %d of %d groups (%d unavailable)" % [count, avail, _registry_state.size() - avail]
	else:
		discovery_status.text = "Listed in %d of %d groups" % [count, _registry_state.size()]
	print("[room_settings %s] _build_discovery_list: status='%s'" % [SETTINGS_VERSION, discovery_status.text])


## Config values from keet-core may be String "true"/"false" instead of bool.
## Comparing String == bool crashes silently in Godot 4.6 exports.
static func _to_bool(val: Variant) -> bool:
	if val is bool: return val
	if val is String: return val.to_lower() == "true"
	if val is int: return val != 0
	return false


func _populate_general() -> void:
	title_field.text = str(_config.get("title", _room_title))
	desc_field.text = str(_config.get("description", ""))
	calls_check.set_pressed_no_signal(_to_bool(_config.get("canCall", false)))
	lock_check.set_pressed_no_signal(_to_bool(_config.get("locked", false)))
	var events_disabled: bool = _to_bool(_config.get("isMembershipEventsDisabled", false))
	member_events_check.set_pressed_no_signal(not events_disabled)


func _populate_meta() -> void:
	# Topics: array of strings → comma-separated display
	var topics: Variant = _meta.get("topics", [])
	if topics is Array:
		var parts: PackedStringArray = PackedStringArray()
		for t: Variant in topics:
			var s: String = str(t).strip_edges()
			if s != "":
				parts.append(s)
		topics_field.text = ", ".join(parts)
	else:
		topics_field.text = ""
	# Discoverable toggle
	discoverable_check.set_pressed_no_signal(_to_bool(_meta.get("discoverable", false)))


func _parse_topics() -> Array:
	## Parse comma-separated topics from the field into a clean array.
	var raw: String = topics_field.text.strip_edges()
	if raw == "":
		return []
	var result: Array = []
	for part: String in raw.split(","):
		var t: String = part.strip_edges().to_lower()
		if t != "" and not result.has(t):
			result.append(t)
	return result


func _on_close_requested() -> void:
	hide()


func _on_save() -> void:
	print("[room_settings %s] === _on_save START ===" % SETTINGS_VERSION)
	save_status.text = "Saving..."
	save_status.remove_theme_color_override("font_color")
	var counter := {"pending": 0}

	# Title
	var new_title: String = title_field.text.strip_edges()
	var old_title: String = str(_config.get("title", ""))
	if new_title != "" and new_title != old_title:
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "title", new_title, func(_r):
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Description
	var new_desc: String = desc_field.text.strip_edges()
	var old_desc: String = str(_config.get("description", ""))
	if new_desc != old_desc:
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "description", new_desc, func(_r):
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Calls — use _to_bool() because keet-core returns String "true"/"false"
	var new_calls: bool = calls_check.button_pressed
	var old_calls: bool = _to_bool(_config.get("canCall", false))
	print("[room_settings %s]   calls: new=%s old=%s" % [SETTINGS_VERSION, str(new_calls), str(old_calls)])
	if new_calls != old_calls:
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "canCall", new_calls, func(_r):
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Lock — same String/"true" issue
	var new_lock: bool = lock_check.button_pressed
	var old_lock: bool = _to_bool(_config.get("locked", false))
	print("[room_settings %s]   lock: new=%s old=%s" % [SETTINGS_VERSION, str(new_lock), str(old_lock)])
	if new_lock != old_lock:
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "locked", new_lock, func(_r):
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Membership events — correct key is isMembershipEventsDisabled (not disableMembershipEvents)
	var new_events: bool = member_events_check.button_pressed
	var current_events_disabled: bool = _to_bool(_config.get("isMembershipEventsDisabled", false))
	print("[room_settings %s]   events: show=%s disabled=%s toggled=%s" % [SETTINGS_VERSION, str(new_events), str(current_events_disabled), str(new_events == current_events_disabled)])
	if new_events == current_events_disabled:  # user wants events but they're disabled, or vice versa
		counter["pending"] += 1
		KeetService.room_mgr.set_config(_room_id, "isMembershipEventsDisabled", not new_events, func(_r):
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Topics + discoverable metadata
	var new_topics: Array = _parse_topics()
	var old_topics: Array = _meta.get("topics", []) if _meta.get("topics", []) is Array else []
	var new_discoverable: bool = discoverable_check.button_pressed
	var old_discoverable: bool = _to_bool(_meta.get("discoverable", false))
	if new_topics != old_topics or new_discoverable != old_discoverable:
		counter["pending"] += 1
		var meta_update := {"topics": new_topics, "discoverable": new_discoverable}
		KeetService.client.call_rpc("meta.set", {"roomId": _room_id, "metadata": meta_update}, func(_r, _e):
			_meta["topics"] = new_topics
			_meta["discoverable"] = new_discoverable
			# Auto-announce/unannounce on DHT based on discoverable toggle
			if new_discoverable and new_topics.size() > 0:
				KeetService.discovery.announce_room(_room_id)
			elif not new_discoverable and old_discoverable:
				KeetService.discovery.unannounce_room(_room_id)
			counter["pending"] -= 1
			if counter["pending"] <= 0:
				_on_save_done()
		)

	# Discovery group opt-in/opt-out per registry
	var reg_title: String = new_title if new_title != "" else _room_title
	var disc_children: Array = discovery_list.get_children()
	print("[room_settings %s]   discovery: reg_title='%s' children=%d registry_state_keys=%s" % [SETTINGS_VERSION, reg_title, disc_children.size(), str(_registry_state.keys())])
	for check_node: Variant in disc_children:
		if not check_node is CheckButton:
			continue
		if not check_node.is_inside_tree():
			print("[room_settings]   skip: not in tree")
			continue
		var cb: CheckButton = check_node
		var reg_key: String = cb.get_meta("registry_key", "")
		if reg_key == "":
			print("[room_settings]   skip: empty reg_key")
			continue
		var was_registered: bool = _registry_state.get(reg_key, {}).get("registered", false)
		var now_checked: bool = cb.button_pressed
		var is_avail: bool = _registry_state.get(reg_key, {}).get("available", true)
		print("[room_settings]   reg_key=%s was=%s now=%s avail=%s label=%s" % [reg_key.left(12), str(was_registered), str(now_checked), str(is_avail), cb.text])
		if now_checked and not was_registered:
			if not is_avail:
				continue
			counter["pending"] += 1
			KeetService.discovery.register_room(reg_key, _room_id, reg_title, new_desc, new_topics, "", func(_r, _rk = reg_key):
				if _r == null:
					save_status.text = "Failed to register in group"
					save_status.add_theme_color_override("font_color", Color(0.9, 0.4, 0.3))
				else:
					_registry_state[_rk]["registered"] = true
				counter["pending"] -= 1
				if counter["pending"] <= 0:
					_on_save_done()
			)
		elif not now_checked and was_registered:
			counter["pending"] += 1
			KeetService.discovery.unregister_room(reg_key, _room_id, func(_r, _rk = reg_key):
				_registry_state[_rk]["registered"] = false
				counter["pending"] -= 1
				if counter["pending"] <= 0:
					_on_save_done()
			)

	print("[room_settings %s] === _on_save END: pending=%d ===" % [SETTINGS_VERSION, counter["pending"]])
	if counter["pending"] == 0:
		save_status.text = "No changes"


func _on_save_done() -> void:
	print("[room_settings %s] _on_save_done called! status='%s'" % [SETTINGS_VERSION, save_status.text])
	# Only show "Saved!" if no error message was set (status still says "Saving...")
	if save_status.text == "Saving...":
		save_status.text = "Saved!"
		save_status.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
	# Refresh room list + discovery state
	KeetService.room_mgr.list_rooms()
	_build_discovery_list()
	get_tree().create_timer(2.0).timeout.connect(func():
		if save_status.text == "Saved!":
			save_status.text = ""
	)


# --- Room Avatar ---

func _load_room_avatar() -> void:
	var avatar: Variant = _config.get("avatar", null)
	if avatar is Dictionary:
		# Try medium first (288x288), then small (165x165)
		var b64: String = str(avatar.get("medium", ""))
		if b64 == "":
			b64 = str(avatar.get("small", ""))
		if b64 != "":
			var img_data: PackedByteArray = Marshalls.base64_to_raw(b64)
			var img := Image.new()
			if img.load_png_from_buffer(img_data) == OK:
				avatar_preview.texture = ImageTexture.create_from_image(img)
				return
			# Try WebP if PNG fails
			if img.load_webp_from_buffer(img_data) == OK:
				avatar_preview.texture = ImageTexture.create_from_image(img)


func _on_change_avatar() -> void:
	if _avatar_picker != null:
		_avatar_picker.queue_free()
	_avatar_picker = AvatarPicker.new()
	_avatar_picker.avatar_selected.connect(func(small_b64: String, large_b64: String):
		# Keet room avatar sizes: 165x165 (small), 288x288 (medium)
		var source_img := Image.new()
		var raw: PackedByteArray = Marshalls.base64_to_raw(large_b64)
		if source_img.load_png_from_buffer(raw) != OK:
			return
		# Scale to keet room sizes
		var small_img: Image = source_img.duplicate()
		small_img.resize(165, 165, Image.INTERPOLATE_LANCZOS)
		var medium_img: Image = source_img.duplicate()
		medium_img.resize(288, 288, Image.INTERPOLATE_LANCZOS)

		var small_val: String = Marshalls.raw_to_base64(small_img.save_png_to_buffer())
		var medium_val: String = Marshalls.raw_to_base64(medium_img.save_png_to_buffer())

		var avatar_val := {"small": small_val, "medium": medium_val}
		KeetService.room_mgr.set_config(_room_id, "avatar", avatar_val, func(_r):
			_config["avatar"] = avatar_val
			_load_room_avatar()
			save_status.text = "Avatar updated!"
			save_status.add_theme_color_override("font_color", Color(0.3, 0.9, 0.4))
			get_tree().create_timer(2.0).timeout.connect(func():
				if save_status.text == "Avatar updated!":
					save_status.text = ""
			)
		)
	)
	add_child(_avatar_picker)
	_avatar_picker.popup_centered()


# --- Members Tab ---

func _populate_members(members: Array) -> void:
	for child in member_list.get_children():
		child.queue_free()

	# Find our role in this room
	_my_room_role = ""
	for m: Variant in members:
		if m is Dictionary and m.get("memberId", "") == _my_member_id:
			_my_room_role = m.get("role", "peer")
			break
	var is_admin: bool = _my_room_role == "admin"

	for m: Variant in members:
		if not m is Dictionary:
			continue
		var row := _create_member_row(m, is_admin)
		member_list.add_child(row)


func _create_member_row(m: Dictionary, can_manage: bool = false) -> HBoxContainer:
	var hbox := HBoxContainer.new()
	var member_id: String = m.get("memberId", "?")
	var name_text: String = m.get("name", member_id.left(8))
	var role: String = m.get("role", "peer")
	var online: bool = m.get("online", false)
	var is_me: bool = member_id == _my_member_id

	# Name + online indicator
	var name_label := Label.new()
	var suffix: String = ""
	if online:
		suffix += " [ON]"
	if is_me:
		suffix += " (you)"
	name_label.text = "%s%s" % [name_text, suffix]
	name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
	name_label.add_theme_font_size_override("font_size", 13)
	if online:
		name_label.add_theme_color_override("font_color", Color(0.4, 0.9, 0.5))
	hbox.add_child(name_label)

	# Role label (always shown)
	var role_label := Label.new()
	role_label.text = role
	role_label.add_theme_font_size_override("font_size", 11)
	role_label.add_theme_color_override("font_color", Color(0.6, 0.65, 0.7))
	role_label.custom_minimum_size.x = 50

	if can_manage and not is_me:
		# Admin can change other members' roles
		var role_btn := OptionButton.new()
		role_btn.add_item("Admin", 1)
		role_btn.add_item("Mod", 2)
		role_btn.add_item("Peer", 3)
		role_btn.add_item("Read-only", 4)
		var role_map := {"admin": 0, "mod": 1, "peer": 2, "read-only": 3}
		role_btn.selected = role_map.get(role, 2)
		role_btn.add_theme_font_size_override("font_size", 11)
		role_btn.item_selected.connect(func(idx: int):
			var new_role: int = role_btn.get_item_id(idx)
			role_btn.disabled = true
			KeetService.room_mgr.change_role(_room_id, member_id, new_role, func(_r):
				role_btn.disabled = false
			)
		)
		hbox.add_child(role_btn)

		# Kick button
		var kick_btn := Button.new()
		kick_btn.text = "Kick"
		kick_btn.add_theme_font_size_override("font_size", 11)
		kick_btn.pressed.connect(func():
			var dialog := ConfirmationDialog.new()
			dialog.title = "Kick Member"
			dialog.dialog_text = "Remove %s from this room?" % name_text
			dialog.confirmed.connect(func():
				KeetService.room_mgr.kick_member(_room_id, member_id, func(_r):
					KeetService.room_mgr.list_members(_room_id, func(members):
						_populate_members(members)
					)
				)
				dialog.queue_free()
			)
			dialog.canceled.connect(func(): dialog.queue_free())
			add_child(dialog)
			dialog.popup_centered()
		)
		hbox.add_child(kick_btn)
	else:
		hbox.add_child(role_label)

	return hbox


func _on_invite() -> void:
	invite_btn.disabled = true
	invite_btn.text = "Creating..."
	KeetService.room_mgr.create_invite(_room_id, func(token):
		invite_btn.disabled = false
		invite_btn.text = "Create Invite Link"
		if token is String and token != "":
			invite_result.text = token
			invite_result.visible = true
		else:
			invite_result.text = "Failed to create invite"
			invite_result.visible = true
	)


# --- Notifications Tab ---

func _populate_notifications(prefs: Dictionary) -> void:
	mute_all_check.set_pressed_no_signal(prefs.get("isFullyMuted", false) == true)
	mentions_only_check.set_pressed_no_signal(prefs.get("isMentionsOnlyMuted", false) == true)
	# Bookmark state not available from preferences — skip for now


func _on_save_notifications() -> void:
	var prefs := {
		"isFullyMuted": mute_all_check.button_pressed,
		"isMentionsOnlyMuted": mentions_only_check.button_pressed
	}
	KeetService.room_mgr.set_preferences(_room_id, prefs)

	if bookmark_check.button_pressed:
		KeetService.room_mgr.bookmark(_room_id)
	else:
		KeetService.room_mgr.unbookmark(_room_id)


# --- Danger Zone ---

func _on_leave() -> void:
	var dialog := ConfirmationDialog.new()
	dialog.title = "Leave Room"
	dialog.dialog_text = "Leave '%s'?\n\nYou will need a new invite to rejoin." % _room_title
	dialog.min_size = Vector2i(320, 100)
	dialog.confirmed.connect(func():
		KeetService.room_mgr.leave_room(_room_id, func(_r):
			room_left.emit(_room_id)
			hide()
		)
		dialog.queue_free()
	)
	dialog.canceled.connect(func(): dialog.queue_free())
	add_child(dialog)
	dialog.popup_centered()


func _on_purge() -> void:
	var dialog := ConfirmationDialog.new()
	dialog.title = "Purge Room"
	dialog.dialog_text = "Leave '%s' and DELETE all local data?\n\nThis cannot be undone." % _room_title
	dialog.min_size = Vector2i(320, 120)
	dialog.confirmed.connect(func():
		KeetService.room_mgr.purge_room(_room_id, func(_r):
			room_left.emit(_room_id)
			hide()
		)
		dialog.queue_free()
	)
	dialog.canceled.connect(func(): dialog.queue_free())
	add_child(dialog)
	dialog.popup_centered()