3378 lines
147 KiB
Python
3378 lines
147 KiB
Python
import os
|
||
import sys
|
||
import struct
|
||
from pathlib import Path
|
||
import panda3d.core as p3d
|
||
from panda3d.core import NodePath, CardMaker
|
||
from ui.lui_function import luiFunction
|
||
|
||
# Ensure ui directory is in path and lui is correctly patched
|
||
UI_DIR = Path(__file__).resolve().parent
|
||
if str(UI_DIR) not in sys.path:
|
||
sys.path.insert(0, str(UI_DIR))
|
||
|
||
# Add Builtin to path
|
||
BUILTIN_DIR = UI_DIR / "Builtin"
|
||
if str(BUILTIN_DIR) not in sys.path:
|
||
sys.path.insert(0, str(BUILTIN_DIR))
|
||
|
||
# Handle DLL loading for lui.pyd
|
||
import panda3d
|
||
panda_dir = os.path.dirname(panda3d.__file__)
|
||
if hasattr(os, "add_dll_directory"):
|
||
try:
|
||
os.add_dll_directory(panda_dir)
|
||
os.add_dll_directory(str(UI_DIR))
|
||
except Exception as e:
|
||
print(f"Warning: Failed to add DLL directory: {e}")
|
||
|
||
try:
|
||
import lui
|
||
# Monkey patch for Builtin components
|
||
import panda3d
|
||
panda3d.lui = lui
|
||
sys.modules["panda3d.lui"] = lui
|
||
|
||
from Builtin.LUIRegion import LUIRegion
|
||
from Builtin.LUIInputHandler import LUIInputHandler
|
||
from Builtin.LUIButton import LUIButton
|
||
from Builtin.LUILabel import LUILabel
|
||
from Builtin.LUIInputField import LUIInputField
|
||
from Builtin.LUISlider import LUISlider
|
||
from Builtin.LUIFrame import LUIFrame
|
||
from Builtin.LUISkin import LUIDefaultSkin
|
||
from Builtin.LUISprite import LUISprite
|
||
from Builtin.LUIObject import LUIObject
|
||
except ImportError as e:
|
||
print(f"Error: Failed to import LUI: {e}")
|
||
lui = None
|
||
|
||
from imgui_bundle import imgui, imgui_ctx
|
||
|
||
class LUIManager:
|
||
"""Manager for LUI system integration"""
|
||
def __init__(self, world):
|
||
self.world = world
|
||
self.lui_enabled = (lui is not None)
|
||
# Always initialize selection fields, even when LUI is unavailable.
|
||
self.selected_index = -1
|
||
self._last_selected_index = -1
|
||
|
||
self.luiFunction = luiFunction
|
||
|
||
# Editor State
|
||
self.show_editor = True
|
||
self.play_mode = False
|
||
self._selected_type_index = 0
|
||
|
||
if not self.lui_enabled:
|
||
print("LUI is not available, LUIManager will be disabled.")
|
||
return
|
||
|
||
# 1. 优先注册中文字体 (必须在皮肤加载前)
|
||
self._register_chinese_font()
|
||
|
||
# 2. 初始化核心 LUI 区域
|
||
self._init_ui_regions()
|
||
|
||
# 3. 加载皮肤资源
|
||
self.lui_skin = LUIDefaultSkin()
|
||
self.lui_skin.load()
|
||
|
||
# 4. 状态管理 - 与engine保持一致
|
||
self.canvases = []
|
||
self.current_canvas_index = -1
|
||
self.canvas_counter = 0
|
||
self.components = []
|
||
self.root_order = []
|
||
self._tree_drag_src = None
|
||
self.selected_index = -1
|
||
self._last_selected_index = -1
|
||
|
||
# 拖拽和选择状态
|
||
self.dragging_comp = None
|
||
self.pending_drag_comp = None
|
||
self.pending_drag_index = -1
|
||
self.pending_drag_start_mouse = None
|
||
self.pending_drag_start_abs = None
|
||
self.pending_drag_start_parent = None
|
||
self.dragging_index = -1
|
||
self.drag_start_threshold = 6.0
|
||
self.drag_offset = (0, 0)
|
||
|
||
# 锚点功能相关变量
|
||
self.anchor_popup_open = False
|
||
self.pending_child_creation = False
|
||
self.pending_parent_index = -1
|
||
|
||
# 子父对齐功能
|
||
self.child_parent_alignment_enabled = True
|
||
|
||
# Resize handles 相关变量
|
||
self.resize_handles = []
|
||
self.selection_box = []
|
||
self.debug_outlines = {} # comp_index -> [border sprites]
|
||
self.resizing_handle = None
|
||
self.resize_start_pos = (0, 0)
|
||
self.resize_start_bounds = {}
|
||
self.min_size = 20
|
||
|
||
# 使用外部字典管理状态,避免修改 C++ 对象属性导致 AttributeError
|
||
self.drag_states = {}
|
||
self._last_interaction_frame = -1 # 记录最后一次交互的帧数,用于防止事件穿透
|
||
self._input_enabled = True
|
||
|
||
# 创建resize handles和selection box(延迟创建,确保在Canvas之后)
|
||
if hasattr(self.world, 'taskMgr'):
|
||
self.world.taskMgr.doMethodLater(0.1, self._delayed_create_resize_handles, "delayed_create_resize_handles")
|
||
else:
|
||
self._create_resize_handles()
|
||
|
||
# 添加任务来处理拖动和resize handles更新
|
||
if hasattr(self.world, 'taskMgr'):
|
||
self.world.taskMgr.add(self._update_drag, "update_lui_drag")
|
||
self.world.taskMgr.add(self._update_resize_handles, "update_resize_handles")
|
||
self.world.taskMgr.add(self._update_component_outlines, "update_lui_outlines")
|
||
|
||
print("✓ LUIManager initialized with complete functionality")
|
||
|
||
def set_input_enabled(self, enabled):
|
||
"""Enable/disable LUI input handling (used to avoid ImGui click-through)."""
|
||
if not self.lui_enabled:
|
||
return
|
||
|
||
if enabled == self._input_enabled:
|
||
return
|
||
|
||
self._input_enabled = enabled
|
||
|
||
try:
|
||
if enabled:
|
||
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
|
||
self.overlay_handler_node.reparent_to(self.world.mouseWatcher)
|
||
if hasattr(self, "overlay_region") and self.overlay_region:
|
||
self.overlay_region.set_input_handler(self.overlay_handler)
|
||
else:
|
||
if hasattr(self, "overlay_region") and self.overlay_region:
|
||
self.overlay_region.set_input_handler(None)
|
||
if hasattr(self, "overlay_handler_node") and self.overlay_handler_node:
|
||
self.overlay_handler_node.detach_node()
|
||
except Exception as e:
|
||
print(f"Warning: Failed to toggle LUI input state: {e}")
|
||
|
||
def _register_chinese_font(self):
|
||
"""注册支持中文的字体"""
|
||
from panda3d.lui import LUIFontPool
|
||
import os
|
||
|
||
# 尝试常见的系统路径,确保使用完整的 OS 路径
|
||
candidate_fonts = [
|
||
"C:/Windows/Fonts/msyh.ttc",
|
||
"C:/Windows/Fonts/simhei.ttf",
|
||
"C:/Windows/Fonts/arial.ttf"
|
||
]
|
||
|
||
font_registered = False
|
||
for fpath in candidate_fonts:
|
||
if os.path.exists(fpath):
|
||
try:
|
||
# 在 LUI 中,建议直接用 loader.loadFont 载入后获取动态库指针
|
||
# 或者直接传递路径字符串
|
||
font = self.world.loader.loadFont(fpath)
|
||
if font:
|
||
LUIFontPool.get_global_ptr().register_font("default", font)
|
||
LUIFontPool.get_global_ptr().register_font("label", font)
|
||
print(f"✓ LUI 成功注册字体: {fpath}")
|
||
font_registered = True
|
||
break
|
||
except Exception as e:
|
||
print(f"LUI 字体注册失败 {fpath}: {e}")
|
||
|
||
if not font_registered:
|
||
# 如果没有系统字体,至少注册一个默认的避免崩溃
|
||
print("⚠️ 警告: 未能找到合适的 LUI 中文字体")
|
||
|
||
def _init_ui_regions(self):
|
||
"""初始化 LUI 区域 (Overlay, Camera, WorldSpace)"""
|
||
# 渲染层级说明(参考engine/main.py的设置):
|
||
# - 3D场景(render)默认sort=0
|
||
# - LUI组件设置sort=5,在3D场景之上
|
||
# - pixel2d (ImGui) 默认sort=20,在LUI之上
|
||
# 这样:3D场景 < LUI组件 < ImGui面板
|
||
|
||
self.overlay_region = LUIRegion.make("OverlayUI", self.world.win)
|
||
self.overlay_handler = LUIInputHandler()
|
||
self.overlay_handler_node = self.world.mouseWatcher.attach_new_node(self.overlay_handler)
|
||
self.overlay_region.set_input_handler(self.overlay_handler)
|
||
self.overlay_region.set_sort(5) # 在3D场景之上,在ImGui之下(与engine保持一致)
|
||
self.overlay_root = self.overlay_region.root
|
||
|
||
# 调试输出:确认层级设置
|
||
actual_sort = self.overlay_region.getSort()
|
||
print(f"✓ LUI Overlay Region 层级设置: {actual_sort}")
|
||
|
||
# 显示所有DisplayRegion的层级信息
|
||
num_regions = self.world.win.getNumDisplayRegions()
|
||
print(f"✓ 窗口总共有 {num_regions} 个DisplayRegion:")
|
||
for i in range(num_regions):
|
||
dr = self.world.win.getDisplayRegion(i)
|
||
sort_value = dr.getSort()
|
||
camera = dr.getCamera()
|
||
camera_name = camera.getName() if not camera.isEmpty() else "无摄像机"
|
||
print(f" Region {i}: sort={sort_value}, camera={camera_name}")
|
||
|
||
# 当前操作的根节点
|
||
self.root = self.overlay_root
|
||
|
||
def _make_canvas_draggable(self, canvas_panel):
|
||
"""使Canvas可拖拽"""
|
||
def on_canvas_drag_start(event):
|
||
# 检查是否有组件或手柄在当前帧被点击
|
||
current_frame = 0
|
||
if hasattr(p3d, 'ClockObject'):
|
||
current_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
|
||
else:
|
||
current_frame = self.world.taskMgr.globalClock.getFrameCount()
|
||
|
||
if self._last_interaction_frame == current_frame:
|
||
# 当前帧已经处理了组件或手柄的点击,忽略Canvas点击事件
|
||
return
|
||
|
||
# 点击空白处:不自动取消选中,避免事件穿透导致闪退
|
||
# self.deselect_all()
|
||
|
||
# 如果启用填充模式,则自动禁用,以便用户可以自由拖动
|
||
canvas_index = -1
|
||
for i, c in enumerate(self.canvases):
|
||
if c['panel'] == canvas_panel:
|
||
canvas_index = i
|
||
break
|
||
|
||
if canvas_index >= 0:
|
||
canvas_data = self.canvases[canvas_index]
|
||
|
||
# 如果是固定模式,则禁止拖动初始化
|
||
if canvas_data.get('fixed', False):
|
||
return
|
||
|
||
if canvas_data.get('fill_mode', False):
|
||
canvas_data['fill_mode'] = False
|
||
print("✓ 自动禁用填充模式以允许自由拖动")
|
||
|
||
# 记录Canvas拖拽状态
|
||
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
pos = canvas_panel.get_pos()
|
||
self.canvas_drag_offset = (pixel_x - pos.x, pixel_y - pos.y)
|
||
self.dragging_canvas = canvas_panel
|
||
|
||
canvas_panel.bind("mousedown", on_canvas_drag_start)
|
||
|
||
def switch_canvas(self, index):
|
||
"""切换到指定Canvas并显示/隐藏相应的UI组件"""
|
||
if index < 0 or index >= len(self.canvases):
|
||
return
|
||
|
||
self.current_canvas_index = index
|
||
|
||
# 显示选中的Canvas,隐藏其他Canvas
|
||
for i, canvas in enumerate(self.canvases):
|
||
if i == index:
|
||
canvas['panel'].show()
|
||
canvas['visible'] = True
|
||
else:
|
||
canvas['panel'].hide()
|
||
canvas['visible'] = False
|
||
|
||
# 显示/隐藏属于不同Canvas的组件(包括子组件)
|
||
visible_count = 0
|
||
hidden_count = 0
|
||
|
||
for comp in self.components:
|
||
comp_canvas_index = comp.get('canvas_index')
|
||
comp_obj = comp['object']
|
||
comp_name = comp.get('name', 'Unknown')
|
||
|
||
should_be_visible = (comp_canvas_index == index)
|
||
|
||
if should_be_visible:
|
||
if hasattr(comp_obj, 'show'):
|
||
comp_obj.show()
|
||
elif hasattr(comp_obj, 'visible'):
|
||
comp_obj.visible = True
|
||
visible_count += 1
|
||
else:
|
||
if hasattr(comp_obj, 'hide'):
|
||
comp_obj.hide()
|
||
elif hasattr(comp_obj, 'visible'):
|
||
comp_obj.visible = False
|
||
hidden_count += 1
|
||
|
||
# 更新当前根节点
|
||
if index >= 0:
|
||
self.root = self.canvases[index]['panel']
|
||
|
||
# 如果选中的组件不属于当前Canvas,取消选中
|
||
if self.selected_index >= 0 and self.selected_index < len(self.components):
|
||
selected_comp = self.components[self.selected_index]
|
||
if selected_comp.get('canvas_index') != index:
|
||
if getattr(self, '_force_keep_selection_frames', 0) > 0:
|
||
pass
|
||
else:
|
||
self.selected_index = -1
|
||
self._hide_selection()
|
||
|
||
print(f"✓ Switched to Canvas: {self.canvases[index]['name']}")
|
||
print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}")
|
||
|
||
def _update_canvas_geometry(self, canvas_data):
|
||
"""根据填充模式和边距更新Canvas几何信息"""
|
||
if not canvas_data.get('fill_mode', False):
|
||
return
|
||
|
||
panel = canvas_data['panel']
|
||
margins = canvas_data.get('margins', {'left':0,'right':0,'top':0,'bottom':0})
|
||
|
||
win_width = 800
|
||
win_height = 600
|
||
if hasattr(self.world, 'win'):
|
||
win_width = self.world.win.getXSize()
|
||
win_height = self.world.win.getYSize()
|
||
|
||
new_x = margins['left']
|
||
new_y = margins['top']
|
||
new_width = max(10, win_width - margins['left'] - margins['right'])
|
||
new_height = max(10, win_height - margins['top'] - margins['bottom'])
|
||
|
||
panel.set_pos(new_x, new_y)
|
||
panel.width = new_width
|
||
panel.height = new_height
|
||
|
||
if 'background' in canvas_data:
|
||
bg = canvas_data['background']
|
||
bg.width = new_width
|
||
bg.height = new_height
|
||
|
||
def _setup_component_drag(self, comp_data, comp_index):
|
||
"""为组件设置拖动功能 - 支持Canvas相对坐标系"""
|
||
comp_obj = comp_data['object']
|
||
|
||
def on_mouse_down(event):
|
||
comp_type = comp_data.get('type')
|
||
if getattr(self, 'play_mode', False):
|
||
# Let runtime widgets handle their own events (especially Slider)
|
||
if comp_type == 'Slider':
|
||
return
|
||
try:
|
||
if hasattr(comp_obj, 'on_mousedown'):
|
||
comp_obj.on_mousedown(event)
|
||
except Exception:
|
||
pass
|
||
return
|
||
# Prevent re-entrant drag
|
||
if self.dragging_comp is not None:
|
||
return
|
||
|
||
# Find current index
|
||
current_index = -1
|
||
for i, c in enumerate(self.components):
|
||
if c is comp_data:
|
||
current_index = i
|
||
break
|
||
|
||
if current_index == -1:
|
||
return
|
||
|
||
# Mark interaction frame
|
||
if hasattr(p3d, 'ClockObject'):
|
||
self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
|
||
else:
|
||
self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount()
|
||
|
||
# Select component
|
||
self.selected_index = current_index
|
||
self._last_selected_index = current_index
|
||
self._force_keep_selection_frames = 3
|
||
|
||
# Force component to current canvas to avoid immediate deselect
|
||
try:
|
||
comp_data['canvas_index'] = self.current_canvas_index
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
if comp_data.get('canvas_index') is None:
|
||
comp_data['canvas_index'] = self.current_canvas_index
|
||
except Exception:
|
||
pass
|
||
try:
|
||
self._show_and_update_handles(comp_data)
|
||
except Exception:
|
||
pass
|
||
|
||
# Slider: allow knob/bar drag in edit mode without hijacking
|
||
if comp_type == 'Slider':
|
||
sender = getattr(event, 'sender', None)
|
||
knob = getattr(comp_obj, '_knob', None)
|
||
bg = getattr(comp_obj, '_slider_bg', None)
|
||
if sender is not None and (sender == knob or sender == bg):
|
||
# Let slider handle its own drag logic
|
||
try:
|
||
if hasattr(comp_obj, '_start_drag'):
|
||
comp_obj._start_drag(event)
|
||
except Exception:
|
||
pass
|
||
return
|
||
|
||
parent_idx = comp_data.get('parent_index')
|
||
if parent_idx is not None and parent_idx >= 0:
|
||
parent_type = self.components[parent_idx].get('type')
|
||
if parent_type in ['VerticalLayout', 'HorizontalLayout']:
|
||
return
|
||
|
||
if not comp_data.get('draggable', True):
|
||
return
|
||
|
||
self.pending_drag_comp = comp_data
|
||
self.pending_drag_index = current_index
|
||
self.pending_drag_start_abs = self._get_component_accumulated_pos(current_index)
|
||
self.pending_drag_start_parent = comp_obj.parent
|
||
self._is_drag_reparented = False
|
||
|
||
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
self.pending_drag_start_mouse = (pixel_x, pixel_y)
|
||
else:
|
||
self.pending_drag_start_mouse = None
|
||
|
||
# Bind mouse event
|
||
comp_obj.bind("mousedown", on_mouse_down)
|
||
|
||
# Slider selection bindings: allow clicking knob/bar to select in edit mode
|
||
try:
|
||
if comp_data.get('type') == 'Slider':
|
||
knob = getattr(comp_obj, '_knob', None)
|
||
bg = getattr(comp_obj, '_slider_bg', None)
|
||
except Exception:
|
||
pass
|
||
|
||
def on_mouse_up(event):
|
||
if getattr(self, 'play_mode', False):
|
||
try:
|
||
if hasattr(comp_obj, 'on_mouseup'):
|
||
comp_obj.on_mouseup(event)
|
||
except Exception:
|
||
pass
|
||
return
|
||
|
||
if comp_data.get('type') == 'Slider':
|
||
sender = getattr(event, 'sender', None)
|
||
knob = getattr(comp_obj, '_knob', None)
|
||
if sender is not None and sender == knob:
|
||
try:
|
||
if hasattr(comp_obj, '_stop_drag'):
|
||
comp_obj._stop_drag(event)
|
||
except Exception:
|
||
pass
|
||
|
||
comp_obj.bind("mouseup", on_mouse_up)
|
||
|
||
def _make_draggable(self, lui_obj):
|
||
"""启用 LUI 对象的鼠标拖拽功能 - 兼容旧接口"""
|
||
# 查找对应的组件索引
|
||
comp_index = -1
|
||
for i, comp in enumerate(self.components):
|
||
if comp.get('object') == lui_obj:
|
||
comp_index = i
|
||
break
|
||
|
||
if comp_index >= 0:
|
||
self._setup_component_drag(self.components[comp_index], comp_index)
|
||
|
||
def _get_component_accumulated_pos(self, comp_index):
|
||
"""递归计算组件相对于Canvas的累积位置"""
|
||
if comp_index < 0 or comp_index >= len(self.components):
|
||
return 0, 0
|
||
|
||
comp_data = self.components[comp_index]
|
||
comp_obj = comp_data['object']
|
||
|
||
# 优先使用递归计算(更可靠)
|
||
left = comp_data.get('left', 0)
|
||
top = comp_data.get('top', 0)
|
||
|
||
parent_index = comp_data.get('parent_index')
|
||
if parent_index is not None and parent_index >= 0:
|
||
parent_left, parent_top = self._get_component_accumulated_pos(parent_index)
|
||
result_x = parent_left + left
|
||
result_y = parent_top + top
|
||
print(f"[_get_component_accumulated_pos] Component #{comp_index} (has parent #{parent_index}): local=({left:.1f}, {top:.1f}), parent_offset=({parent_left:.1f}, {parent_top:.1f}), result=({result_x:.1f}, {result_y:.1f})")
|
||
return result_x, result_y
|
||
|
||
# 没有父组件,直接返回局部坐标(相对于Canvas)
|
||
print(f"[_get_component_accumulated_pos] Component #{comp_index} (root): local=({left:.1f}, {top:.1f})")
|
||
return left, top
|
||
|
||
def _update_drag(self, task):
|
||
"""每帧更新拖动状态 - 支持Canvas边界约束和局部坐标"""
|
||
if getattr(self, 'play_mode', False):
|
||
# Disable editor drag logic while in play mode
|
||
return task.cont
|
||
|
||
# 1. 处理Canvas拖动
|
||
if hasattr(self, 'dragging_canvas') and self.dragging_canvas:
|
||
# 查找对应的Canvas数据
|
||
canvas_data = None
|
||
for c in self.canvases:
|
||
if c['panel'] == self.dragging_canvas:
|
||
canvas_data = c
|
||
break
|
||
|
||
# 检查Canvas是否锁定
|
||
if canvas_data and canvas_data.get('fixed', False):
|
||
return task.cont
|
||
|
||
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
|
||
return task.cont
|
||
|
||
import panda3d.core as p3d
|
||
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
|
||
self.dragging_canvas = None
|
||
return task.cont
|
||
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
new_x = pixel_x - self.canvas_drag_offset[0]
|
||
new_y = pixel_y - self.canvas_drag_offset[1]
|
||
|
||
self.dragging_canvas.set_pos(new_x, new_y)
|
||
return task.cont
|
||
|
||
# 2. 处理组件拖动
|
||
if self.dragging_comp is None:
|
||
# Pending drag: only start after threshold
|
||
if self.pending_drag_comp is None:
|
||
return task.cont
|
||
|
||
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
|
||
return task.cont
|
||
|
||
import panda3d.core as p3d
|
||
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
|
||
# click without drag
|
||
self.pending_drag_comp = None
|
||
self.pending_drag_index = -1
|
||
self.pending_drag_start_mouse = None
|
||
self.pending_drag_start_abs = None
|
||
self.pending_drag_start_parent = None
|
||
return task.cont
|
||
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
if self.pending_drag_start_mouse is None:
|
||
self.pending_drag_start_mouse = (pixel_x, pixel_y)
|
||
return task.cont
|
||
|
||
dx = pixel_x - self.pending_drag_start_mouse[0]
|
||
dy = pixel_y - self.pending_drag_start_mouse[1]
|
||
if (dx * dx + dy * dy) < (self.drag_start_threshold * self.drag_start_threshold):
|
||
return task.cont
|
||
|
||
# Start drag now
|
||
comp_data = self.pending_drag_comp
|
||
self.dragging_comp = comp_data
|
||
self.dragging_index = self.pending_drag_index
|
||
self._original_parent_obj = self.pending_drag_start_parent
|
||
self._is_drag_reparented = False
|
||
|
||
acc_left, acc_top = self.pending_drag_start_abs
|
||
if acc_left is None or acc_top is None:
|
||
acc_left, acc_top = self._get_component_accumulated_pos(self.pending_drag_index)
|
||
|
||
# If child, reparent to canvas for free drag
|
||
p_idx = comp_data.get('parent_index')
|
||
if p_idx is not None and p_idx >= 0 and self.current_canvas_index >= 0:
|
||
canvas_panel = self.canvases[self.current_canvas_index]['panel']
|
||
comp_obj = comp_data['object']
|
||
if not comp_data.get('visual_parent_canvas'):
|
||
try:
|
||
if getattr(comp_obj, 'parent', None) == canvas_panel:
|
||
pass
|
||
elif hasattr(comp_obj, 'reparent_to'):
|
||
comp_obj.reparent_to(canvas_panel)
|
||
if hasattr(comp_obj, 'set_pos'):
|
||
comp_obj.set_pos(acc_left, acc_top)
|
||
else:
|
||
comp_obj.left = acc_left
|
||
comp_obj.top = acc_top
|
||
self._is_drag_reparented = True
|
||
except Exception:
|
||
pass
|
||
|
||
# Compute drag offset
|
||
canvas_abs_left = 0
|
||
canvas_abs_top = 0
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_panel = self.canvases[self.current_canvas_index]['panel']
|
||
canvas_abs_left = getattr(canvas_panel, 'left', 0)
|
||
canvas_abs_top = getattr(canvas_panel, 'top', 0)
|
||
if canvas_abs_left == 0 and canvas_abs_top == 0:
|
||
try:
|
||
canvas_pos = canvas_panel.get_pos()
|
||
canvas_abs_left = canvas_pos.x
|
||
canvas_abs_top = canvas_pos.y
|
||
except:
|
||
canvas_abs_left = 300
|
||
canvas_abs_top = 100
|
||
|
||
comp_abs_left = canvas_abs_left + acc_left
|
||
comp_abs_top = canvas_abs_top + acc_top
|
||
self.drag_offset = (pixel_x - comp_abs_left, pixel_y - comp_abs_top)
|
||
|
||
# Clear pending state
|
||
self.pending_drag_comp = None
|
||
self.pending_drag_index = -1
|
||
self.pending_drag_start_mouse = None
|
||
self.pending_drag_start_abs = None
|
||
self.pending_drag_start_parent = None
|
||
|
||
# Continue with drag in same frame
|
||
|
||
|
||
# 检查鼠标状态
|
||
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
|
||
return task.cont
|
||
|
||
# 检查鼠标左键是否释放
|
||
import panda3d.core as p3d
|
||
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
|
||
if self.dragging_comp:
|
||
comp_data = self.dragging_comp
|
||
print(f"组件移动到: left={comp_data.get('left', 0):.1f}, top={comp_data.get('top', 0):.1f} (局部坐标)")
|
||
|
||
# 拖动结束:检查是否需要接触父子关系 (Auto-unparent)
|
||
should_unparent = False
|
||
if getattr(self, '_is_drag_reparented', False):
|
||
comp_obj = comp_data['object']
|
||
|
||
# Check if center of component is outside parent bounds
|
||
p_idx = comp_data.get('parent_index')
|
||
if p_idx is not None and p_idx >= 0:
|
||
parent_data = self.components[p_idx]
|
||
p_w = parent_data.get('width', 100)
|
||
p_h = parent_data.get('height', 100)
|
||
|
||
c_w = comp_data.get('width', 0)
|
||
c_h = comp_data.get('height', 0)
|
||
|
||
# Current comp_data['left'] is LOCAL to parent
|
||
local_l = comp_data.get('left', 0)
|
||
local_t = comp_data.get('top', 0)
|
||
|
||
center_x = local_l + c_w / 2
|
||
center_y = local_t + c_h / 2
|
||
|
||
# Check bounds: center must be in 0..p_w, 0..p_h
|
||
# Allow some leniency or strict? Let's say if center is outside, unparent.
|
||
if center_x < 0 or center_x > p_w or center_y < 0 or center_y > p_h:
|
||
should_unparent = True
|
||
|
||
if should_unparent:
|
||
# Unparent!
|
||
# The component is currently visually attached to Canvas (from drag start)
|
||
# We just need to update the data to reflect this permanent change.
|
||
|
||
# Calculate the new global (canvas-relative) position
|
||
# current local + parent offset
|
||
p_off_x, p_off_y = self._get_component_accumulated_pos(p_idx)
|
||
new_canvas_left = local_l + p_off_x
|
||
new_canvas_top = local_t + p_off_y
|
||
|
||
comp_data['left'] = new_canvas_left
|
||
comp_data['top'] = new_canvas_top
|
||
|
||
# Remove parent linkage
|
||
if 'parent_index' in comp_data:
|
||
del comp_data['parent_index']
|
||
if 'anchored_to_parent' in comp_data:
|
||
del comp_data['anchored_to_parent']
|
||
|
||
print(f"✓ Auto-unparented component from #{p_idx} (Dragged out)")
|
||
|
||
elif hasattr(comp_obj, 'reparent_to') and hasattr(self, '_original_parent_obj'):
|
||
comp_obj.reparent_to(self._original_parent_obj)
|
||
# 重新应用计算出的局部坐标
|
||
comp_obj.left = comp_data.get('left', 0)
|
||
comp_obj.top = comp_data.get('top', 0)
|
||
print(f"✓ 组件归位到逻辑父级")
|
||
|
||
self.dragging_comp = None
|
||
return task.cont
|
||
|
||
# 获取鼠标位置
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
# 转换为像素坐标
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
# 计算新的绝对位置(减去偏移量)
|
||
new_abs_left = pixel_x - self.drag_offset[0]
|
||
new_abs_top = pixel_y - self.drag_offset[1]
|
||
|
||
# 获取Canvas信息
|
||
canvas_abs_left = 0
|
||
canvas_abs_top = 0
|
||
canvas_width = 800
|
||
canvas_height = 600
|
||
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_data = self.canvases[self.current_canvas_index]
|
||
canvas_panel = canvas_data['panel']
|
||
|
||
# 获取Canvas的绝对位置
|
||
canvas_abs_left = getattr(canvas_panel, 'left', 0)
|
||
canvas_abs_top = getattr(canvas_panel, 'top', 0)
|
||
|
||
if canvas_abs_left == 0 and canvas_abs_top == 0:
|
||
try:
|
||
pos = canvas_panel.get_pos()
|
||
canvas_abs_left = pos.x
|
||
canvas_abs_top = pos.y
|
||
except:
|
||
# 如果获取失败,使用默认值
|
||
canvas_abs_left = canvas_data.get('margins', {}).get('left', 240)
|
||
canvas_abs_top = canvas_data.get('margins', {}).get('top', 89)
|
||
|
||
# 获取Canvas的实际尺寸
|
||
try:
|
||
if hasattr(canvas_panel, 'width') and canvas_panel.width is not None:
|
||
canvas_width = float(canvas_panel.width)
|
||
elif hasattr(canvas_panel, 'get_width'):
|
||
canvas_width = float(canvas_panel.get_width())
|
||
|
||
if hasattr(canvas_panel, 'height') and canvas_panel.height is not None:
|
||
canvas_height = float(canvas_panel.height)
|
||
elif hasattr(canvas_panel, 'get_height'):
|
||
canvas_height = float(canvas_panel.get_height())
|
||
except Exception as e:
|
||
print(f"Warning: Failed to get canvas size: {e}")
|
||
# 使用窗口尺寸减去边距作为后备
|
||
margins = canvas_data.get('margins', {'left': 240, 'right': 480, 'top': 89, 'bottom': 220})
|
||
canvas_width = win_x - margins.get('left', 0) - margins.get('right', 0)
|
||
canvas_height = win_y - margins.get('top', 0) - margins.get('bottom', 0)
|
||
|
||
# 转换为Canvas相对坐标 (Global Canvas Pos)
|
||
canvas_relative_left = new_abs_left - canvas_abs_left
|
||
canvas_relative_top = new_abs_top - canvas_abs_top
|
||
|
||
# 获取当前拖拽的组件数据
|
||
comp_data = self.dragging_comp
|
||
comp_obj = comp_data['object']
|
||
|
||
# 获取组件宽高 (用于边界限制)
|
||
comp_width = comp_data.get('width', 0)
|
||
comp_height = comp_data.get('height', 0)
|
||
|
||
# 尝试从对象获取更准确的宽高
|
||
if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0:
|
||
comp_width = comp_obj.width
|
||
if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0:
|
||
comp_height = comp_obj.height
|
||
|
||
# 限制在Canvas范围内
|
||
# 确保左边界不小于0
|
||
if canvas_relative_left < 0:
|
||
canvas_relative_left = 0
|
||
|
||
# 确保上边界不小于0
|
||
if canvas_relative_top < 0:
|
||
canvas_relative_top = 0
|
||
|
||
# 确保右边界不超出Canvas宽度
|
||
if canvas_relative_left + comp_width > canvas_width:
|
||
canvas_relative_left = max(0, canvas_width - comp_width)
|
||
|
||
# 确保下边界不超出Canvas高度
|
||
if canvas_relative_top + comp_height > canvas_height:
|
||
canvas_relative_top = max(0, canvas_height - comp_height)
|
||
|
||
# 如果有父组件,需要转换为相对于父组件的局部坐标
|
||
comp_data = self.dragging_comp
|
||
parent_index = comp_data.get('parent_index')
|
||
parent_offset_x = 0
|
||
parent_offset_y = 0
|
||
|
||
if parent_index is not None and parent_index >= 0:
|
||
# 获取父组件相对于Canvas的累积位置
|
||
parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index)
|
||
|
||
# 计算新的局部坐标
|
||
new_local_left = canvas_relative_left - parent_offset_x
|
||
new_local_top = canvas_relative_top - parent_offset_y
|
||
|
||
# 如果物理上已脱离父级(挂在Canvas上),则物理坐标就是Canvas相对坐标
|
||
comp_obj = comp_data['object']
|
||
if getattr(self, '_is_drag_reparented', False):
|
||
# 子组件被重定向到Canvas,使其可以移动到负数局部坐标(即父组件外侧)
|
||
if hasattr(comp_obj, 'set_pos'):
|
||
comp_obj.set_pos(canvas_relative_left, canvas_relative_top)
|
||
else:
|
||
comp_obj.left = canvas_relative_left
|
||
comp_obj.top = canvas_relative_top
|
||
# print(f"Dragging Child (Canvas Rel): {canvas_relative_left:.1f}, {canvas_relative_top:.1f} | Local: {new_local_left:.1f}, {new_local_top:.1f}")
|
||
else:
|
||
# If visual parent is canvas, keep world position while storing local
|
||
if comp_data.get('visual_parent_canvas'):
|
||
abs_left = parent_offset_x + new_local_left
|
||
abs_top = parent_offset_y + new_local_top
|
||
if hasattr(comp_obj, 'set_pos'):
|
||
comp_obj.set_pos(abs_left, abs_top)
|
||
else:
|
||
comp_obj.left = abs_left
|
||
comp_obj.top = abs_top
|
||
else:
|
||
if hasattr(comp_obj, 'set_pos'):
|
||
comp_obj.set_pos(new_local_left, new_local_top)
|
||
else:
|
||
comp_obj.left = new_local_left
|
||
comp_obj.top = new_local_top
|
||
|
||
# 更新存储的逻辑坐标数据 (保持始终相对于逻辑父级)
|
||
comp_data['left'] = new_local_left
|
||
comp_data['top'] = new_local_top
|
||
|
||
# Sync visually-canvas children for non-container parents
|
||
if self.dragging_index is not None and self.dragging_index >= 0:
|
||
self._sync_canvas_children(self.dragging_index)
|
||
|
||
return task.cont
|
||
|
||
def _create_resize_handles(self):
|
||
"""创建选择框和8个resize手柄"""
|
||
from Builtin.LUISprite import LUISprite
|
||
from Builtin.LUIObject import LUIObject
|
||
|
||
print("开始创建resize handles...")
|
||
|
||
# 先创建4条选择框边线(蓝色,2px宽)
|
||
for i in range(4):
|
||
border = LUISprite(self.overlay_root, "blank", "skin")
|
||
border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色
|
||
border.visible = False
|
||
|
||
# 设置选择框层级,在组件之上但在LUI内部
|
||
if hasattr(border, 'z_offset'):
|
||
border.z_offset = 40 # 选中框在普通边框(25)之上
|
||
elif hasattr(border, 'set_sort'):
|
||
border.set_sort(40)
|
||
|
||
# 确保边框在最后创建,这样在渲染顺序上会在前面
|
||
self.selection_box.append(border)
|
||
|
||
# 最后创建8个resize手柄(白色圆形,蓝色边框)
|
||
handle_positions = [
|
||
'top-left', 'top', 'top-right', 'right',
|
||
'bottom-right', 'bottom', 'bottom-left', 'left'
|
||
]
|
||
|
||
for i, pos in enumerate(handle_positions):
|
||
# 创建容器对象用于接收鼠标事件
|
||
handle_container = LUIObject(parent=self.overlay_root)
|
||
handle_container.width = 10
|
||
handle_container.height = 10
|
||
handle_container.solid = True
|
||
handle_container.visible = False
|
||
|
||
# 设置手柄层级,在选择框之上但在LUI内部
|
||
if hasattr(handle_container, 'z_offset'):
|
||
handle_container.z_offset = 50 # 在选择框之上
|
||
elif hasattr(handle_container, 'set_sort'):
|
||
handle_container.set_sort(50) # 在选择框之上
|
||
|
||
# 创建蓝色边框(外圈)
|
||
handle_border = LUISprite(handle_container, "blank", "skin")
|
||
handle_border.width = 10
|
||
handle_border.height = 10
|
||
handle_border.left = 0
|
||
handle_border.top = 0
|
||
handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框
|
||
|
||
# 创建白色圆形背景(内圈)
|
||
handle_bg = LUISprite(handle_container, "blank", "skin")
|
||
handle_bg.width = 8
|
||
handle_bg.height = 8
|
||
handle_bg.left = 1
|
||
handle_bg.top = 1
|
||
handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色
|
||
|
||
# 存储手柄位置信息
|
||
handle_container._resize_position = pos
|
||
handle_container._resize_index = i
|
||
handle_container._bg = handle_bg
|
||
handle_container._border = handle_border
|
||
|
||
# 绑定鼠标事件
|
||
handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h))
|
||
|
||
self.resize_handles.append(handle_container)
|
||
|
||
print(f"✓ 创建了{len(self.selection_box)}条边框和{len(self.resize_handles)}个手柄")
|
||
|
||
# 强制将手柄移到最前面(在LUI中,后创建的元素通常在前面)
|
||
for handle in self.resize_handles:
|
||
# 尝试重新设置父节点来强制更新渲染顺序
|
||
if hasattr(handle, 'reparent_to'):
|
||
handle.reparent_to(self.overlay_root)
|
||
|
||
for border in self.selection_box:
|
||
if hasattr(border, 'reparent_to'):
|
||
border.reparent_to(self.overlay_root)
|
||
|
||
def _delayed_create_resize_handles(self, task):
|
||
"""延迟创建resize handles,确保在Canvas之后创建"""
|
||
self._create_resize_handles()
|
||
return task.done
|
||
|
||
def _on_handle_mousedown(self, event, handle):
|
||
"""手柄鼠标按下事件"""
|
||
# 标记交互帧,防止Canvas点击事件触发取消选中
|
||
if hasattr(p3d, 'ClockObject'):
|
||
self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount()
|
||
else:
|
||
# Fallback if specific import not avail in this scope (though p3d is imported)
|
||
self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount()
|
||
|
||
if self.selected_index < 0:
|
||
return
|
||
|
||
comp_data = self.components[self.selected_index]
|
||
print(f"✓ 开始resize: 手柄位置={handle._resize_position}")
|
||
|
||
# 记录开始调整大小的状态
|
||
self.resizing_handle = handle
|
||
|
||
# 获取鼠标位置
|
||
if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse():
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
self.resize_start_pos = (pixel_x, pixel_y)
|
||
|
||
# 记录组件的初始边界
|
||
self.resize_start_bounds = {
|
||
'left': comp_data.get('left', 0),
|
||
'top': comp_data.get('top', 0),
|
||
'width': comp_data.get('width', 100),
|
||
'height': comp_data.get('height', 30),
|
||
}
|
||
|
||
def _update_resize_handles(self, task):
|
||
# Restore selection if it was cleared immediately
|
||
if self.selected_index < 0 and getattr(self, '_force_keep_selection_frames', 0) > 0:
|
||
if getattr(self, '_last_selected_index', -1) >= 0:
|
||
self.selected_index = self._last_selected_index
|
||
|
||
if getattr(self, '_force_keep_selection_frames', 0) > 0:
|
||
self._force_keep_selection_frames -= 1
|
||
|
||
"""更新resize handles的位置和可见性"""
|
||
# 检查当前Canvas是否可见
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas = self.canvases[self.current_canvas_index]
|
||
if not canvas.get('visible', True):
|
||
self._hide_resize_handles()
|
||
return task.cont
|
||
|
||
if self.selected_index < 0 or self.selected_index >= len(self.components):
|
||
# 隐藏所有handles
|
||
self._hide_resize_handles()
|
||
return task.cont
|
||
|
||
comp_data = self.components[self.selected_index]
|
||
comp_type = comp_data['type']
|
||
|
||
# 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles
|
||
if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout']:
|
||
self._hide_resize_handles()
|
||
return task.cont
|
||
|
||
# 显示并更新handles位置
|
||
self._show_and_update_handles(comp_data)
|
||
|
||
# 处理resize拖拽
|
||
if self.resizing_handle is not None:
|
||
self._handle_resize_drag()
|
||
|
||
return task.cont
|
||
|
||
def _hide_resize_handles(self):
|
||
"""隐藏所有resize handles和selection box"""
|
||
for border in self.selection_box:
|
||
border.visible = False
|
||
for handle in self.resize_handles:
|
||
handle.visible = False
|
||
|
||
def _ensure_component_outline(self, comp_index):
|
||
'Create a simple 4-edge outline for a component if not exists.'
|
||
if comp_index in self.debug_outlines:
|
||
return self.debug_outlines[comp_index]
|
||
|
||
from Builtin.LUISprite import LUISprite
|
||
|
||
borders = []
|
||
for _ in range(4):
|
||
border = LUISprite(self.overlay_root, "blank", "skin")
|
||
border.color = (0.2, 0.7, 1.0, 1.0)
|
||
border.visible = False
|
||
|
||
# Ensure outline is above components
|
||
if hasattr(border, 'z_offset'):
|
||
border.z_offset = 25
|
||
elif hasattr(border, 'set_sort'):
|
||
border.set_sort(25)
|
||
|
||
borders.append(border)
|
||
|
||
self.debug_outlines[comp_index] = borders
|
||
return borders
|
||
|
||
def _update_component_outlines(self, task):
|
||
'Show component outlines in edit mode, hide in play mode.'
|
||
if getattr(self, 'play_mode', False) or not self.lui_enabled or not self.show_editor:
|
||
for borders in self.debug_outlines.values():
|
||
for b in borders:
|
||
b.visible = False
|
||
return task.cont
|
||
|
||
for idx, comp_data in enumerate(self.components):
|
||
# If selected, let the resize handles/selection box handle the outline (Bright Blue)
|
||
# We hide the "debug" outline for the selected item to avoid double-drawing or z-fighting
|
||
if idx == self.selected_index:
|
||
borders = self.debug_outlines.get(idx)
|
||
if borders:
|
||
for b in borders:
|
||
b.visible = False
|
||
continue
|
||
|
||
comp_obj = comp_data.get('object')
|
||
if comp_obj is None:
|
||
continue
|
||
|
||
canvas_index = comp_data.get('canvas_index', self.current_canvas_index)
|
||
if canvas_index is None or canvas_index < 0 or canvas_index >= len(self.canvases):
|
||
# Component not assigned to a valid canvas
|
||
borders = self.debug_outlines.get(idx)
|
||
if borders:
|
||
for b in borders: b.visible = False
|
||
continue
|
||
|
||
canvas = self.canvases[canvas_index]
|
||
if not canvas.get('visible', True):
|
||
# Hide outlines for components on hidden canvases
|
||
borders = self.debug_outlines.get(idx)
|
||
if borders:
|
||
for b in borders:
|
||
b.visible = False
|
||
continue
|
||
|
||
# Determine size
|
||
width = comp_data.get('width', 0)
|
||
height = comp_data.get('height', 0)
|
||
try:
|
||
if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0:
|
||
width = comp_obj.width
|
||
if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0:
|
||
height = comp_obj.height
|
||
except Exception:
|
||
pass
|
||
|
||
if width <= 0 or height <= 0:
|
||
continue
|
||
|
||
# Determine Position (Absolute)
|
||
canvas_abs_left = 0
|
||
canvas_abs_top = 0
|
||
canvas_panel = self.canvases[canvas_index]['panel']
|
||
canvas_abs_left = getattr(canvas_panel, 'left', 0)
|
||
canvas_abs_top = getattr(canvas_panel, 'top', 0)
|
||
if canvas_abs_left == 0 and canvas_abs_top == 0:
|
||
try:
|
||
canvas_pos = canvas_panel.get_pos()
|
||
canvas_abs_left = canvas_pos.x
|
||
canvas_abs_top = canvas_pos.y
|
||
except Exception:
|
||
canvas_abs_left = 300
|
||
canvas_abs_top = 100
|
||
|
||
if hasattr(comp_obj, 'get_abs_pos'):
|
||
try:
|
||
abs_pos = comp_obj.get_abs_pos()
|
||
abs_left = abs_pos.x
|
||
abs_top = abs_pos.y
|
||
except Exception:
|
||
acc_left, acc_top = self._get_component_accumulated_pos(idx)
|
||
abs_left = canvas_abs_left + acc_left
|
||
abs_top = canvas_abs_top + acc_top
|
||
else:
|
||
acc_left, acc_top = self._get_component_accumulated_pos(idx)
|
||
abs_left = canvas_abs_left + acc_left
|
||
abs_top = canvas_abs_top + acc_top
|
||
|
||
# Draw Outline
|
||
borders = self._ensure_component_outline(idx)
|
||
|
||
# Style for unselected components: Gray, thinner or same thickness
|
||
for b in borders:
|
||
b.color = (0.7, 0.7, 0.7, 0.5) # Light gray, semi-transparent
|
||
if hasattr(b, 'z_offset'):
|
||
b.z_offset = 25
|
||
elif hasattr(b, 'set_sort'):
|
||
b.set_sort(25)
|
||
b.visible = True
|
||
|
||
t = 1.0 # Thickness
|
||
|
||
borders[0].left = abs_left
|
||
borders[0].top = abs_top - t
|
||
borders[0].width = width
|
||
borders[0].height = t
|
||
|
||
borders[1].left = abs_left
|
||
borders[1].top = abs_top + height
|
||
borders[1].width = width
|
||
borders[1].height = t
|
||
|
||
borders[2].left = abs_left - t
|
||
borders[2].top = abs_top
|
||
borders[2].width = t
|
||
borders[2].height = height
|
||
|
||
borders[3].left = abs_left + width
|
||
borders[3].top = abs_top
|
||
borders[3].width = t
|
||
borders[3].height = height
|
||
|
||
return task.cont
|
||
|
||
def _show_and_update_handles(self, comp_data):
|
||
"""显示并更新handles位置 - 修复坐标系不一致问题"""
|
||
# 每次显示时重新创建手柄,确保它们在最前面
|
||
if not hasattr(self, '_handles_recreated'):
|
||
self._recreate_handles_on_top()
|
||
self._handles_recreated = True
|
||
|
||
# 获取组件的Canvas相对位置和尺寸
|
||
comp_obj = comp_data['object']
|
||
|
||
# 获取Canvas的绝对位置用于坐标转换
|
||
canvas_abs_left = 0
|
||
canvas_abs_top = 0
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_panel = self.canvases[self.current_canvas_index]['panel']
|
||
# 使用LUI对象的left和top属性,而不是get_pos()方法
|
||
canvas_abs_left = getattr(canvas_panel, 'left', 0)
|
||
canvas_abs_top = getattr(canvas_panel, 'top', 0)
|
||
|
||
# 如果left/top属性不存在,尝试使用get_pos()
|
||
if canvas_abs_left == 0 and canvas_abs_top == 0:
|
||
try:
|
||
canvas_pos = canvas_panel.get_pos()
|
||
canvas_abs_left = canvas_pos.x
|
||
canvas_abs_top = canvas_pos.y
|
||
except:
|
||
# 如果get_pos()也失败,使用默认值
|
||
canvas_abs_left = 300 # 默认Canvas位置
|
||
canvas_abs_top = 100
|
||
|
||
# 尝试直接获取组件的绝对屏幕坐标
|
||
if hasattr(comp_obj, 'get_abs_pos'):
|
||
try:
|
||
abs_pos = comp_obj.get_abs_pos()
|
||
abs_left = abs_pos.x
|
||
abs_top = abs_pos.y
|
||
# print(f"使用 get_abs_pos(): {abs_left}, {abs_top}")
|
||
except Exception as e:
|
||
print(f"get_abs_pos() 失败: {e}")
|
||
# Fallback to manual calculation
|
||
acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index)
|
||
abs_left = canvas_abs_left + acc_left
|
||
abs_top = canvas_abs_top + acc_top
|
||
else:
|
||
# 递归计算组件的 Canvas 相对位置(累加所有父组件的相对位置)
|
||
# 使用 self.selected_index 似乎最安全,或者反查 comp_data
|
||
if self.selected_index >= 0 and self.components[self.selected_index] == comp_data:
|
||
acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index)
|
||
else:
|
||
acc_left = comp_data.get('left', 0)
|
||
acc_top = comp_data.get('top', 0)
|
||
|
||
# 转换为绝对屏幕坐标(用于手柄和边框显示)
|
||
abs_left = canvas_abs_left + acc_left
|
||
abs_top = canvas_abs_top + acc_top
|
||
|
||
# 获取组件尺寸 - 优先从LUI对象获取实际尺寸
|
||
comp_type = comp_data['type']
|
||
width = comp_data.get('width', 100)
|
||
height = comp_data.get('height', 30)
|
||
|
||
# 尝试从LUI对象获取实际尺寸
|
||
try:
|
||
if hasattr(comp_obj, 'width') and comp_obj.width is not None:
|
||
actual_width = comp_obj.width
|
||
if actual_width > 0:
|
||
width = actual_width
|
||
if hasattr(comp_obj, 'height') and comp_obj.height is not None:
|
||
actual_height = comp_obj.height
|
||
if actual_height > 0:
|
||
height = actual_height
|
||
except:
|
||
pass
|
||
|
||
# 确保宽高是数值类型
|
||
try:
|
||
width = float(width)
|
||
height = float(height)
|
||
except (TypeError, ValueError):
|
||
width = 100.0
|
||
height = 30.0
|
||
|
||
# 更新comp_data中的尺寸
|
||
comp_data['width'] = width
|
||
comp_data['height'] = height
|
||
|
||
# 更新4条边框(使用绝对位置,因为边框在overlay_root下)
|
||
# 确保 selection_box 存在
|
||
if not self.selection_box:
|
||
self._create_resize_handles()
|
||
if not self.selection_box: # Still empty?
|
||
return
|
||
|
||
# 上边框
|
||
self.selection_box[0].left = abs_left
|
||
self.selection_box[0].top = abs_top - 2
|
||
self.selection_box[0].width = width
|
||
self.selection_box[0].height = 2
|
||
self.selection_box[0].visible = True
|
||
|
||
# 下边框
|
||
self.selection_box[1].left = abs_left
|
||
self.selection_box[1].top = abs_top + height
|
||
self.selection_box[1].width = width
|
||
self.selection_box[1].height = 2
|
||
self.selection_box[1].visible = True
|
||
|
||
# 左边框
|
||
self.selection_box[2].left = abs_left - 2
|
||
self.selection_box[2].top = abs_top
|
||
self.selection_box[2].width = 2
|
||
self.selection_box[2].height = height
|
||
self.selection_box[2].visible = True
|
||
|
||
# 右边框
|
||
self.selection_box[3].left = abs_left + width
|
||
self.selection_box[3].top = abs_top
|
||
self.selection_box[3].width = 2
|
||
self.selection_box[3].height = height
|
||
self.selection_box[3].visible = True
|
||
|
||
# 更新8个手柄位置(使用绝对位置,因为手柄在overlay_root下)
|
||
handle_positions = [
|
||
(abs_left - 5, abs_top - 5), # 左上
|
||
(abs_left + width/2 - 5, abs_top - 5), # 上
|
||
(abs_left + width - 5, abs_top - 5), # 右上
|
||
(abs_left + width - 5, abs_top + height/2 - 5), # 右
|
||
(abs_left + width - 5, abs_top + height - 5), # 右下
|
||
(abs_left + width/2 - 5, abs_top + height - 5), # 下
|
||
(abs_left - 5, abs_top + height - 5), # 左下
|
||
(abs_left - 5, abs_top + height/2 - 5), # 左
|
||
]
|
||
|
||
for i, (h_left, h_top) in enumerate(handle_positions):
|
||
if i < len(self.resize_handles):
|
||
handle = self.resize_handles[i]
|
||
handle.left = h_left
|
||
handle.top = h_top
|
||
handle.visible = True
|
||
|
||
def _recreate_handles_on_top(self):
|
||
"""重新创建手柄,确保它们在最前面"""
|
||
from Builtin.LUISprite import LUISprite
|
||
from Builtin.LUIObject import LUIObject
|
||
|
||
# 清除旧的手柄
|
||
for handle in self.resize_handles:
|
||
if hasattr(handle, 'remove'):
|
||
handle.remove()
|
||
for border in self.selection_box:
|
||
if hasattr(border, 'remove'):
|
||
border.remove()
|
||
|
||
self.resize_handles.clear()
|
||
self.selection_box.clear()
|
||
|
||
# 重新创建边框
|
||
for i in range(4):
|
||
border = LUISprite(self.overlay_root, "blank", "skin")
|
||
border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色
|
||
border.visible = False
|
||
|
||
# 设置选择框层级,在组件之上但在LUI内部
|
||
if hasattr(border, 'z_offset'):
|
||
border.z_offset = 40
|
||
elif hasattr(border, 'set_sort'):
|
||
border.set_sort(40)
|
||
|
||
self.selection_box.append(border)
|
||
|
||
# 重新创建手柄
|
||
handle_positions = [
|
||
'top-left', 'top', 'top-right', 'right',
|
||
'bottom-right', 'bottom', 'bottom-left', 'left'
|
||
]
|
||
|
||
for i, pos in enumerate(handle_positions):
|
||
handle_container = LUIObject(parent=self.overlay_root)
|
||
handle_container.width = 10
|
||
handle_container.height = 10
|
||
handle_container.solid = True
|
||
handle_container.visible = False
|
||
|
||
# 设置手柄层级,在选择框之上但在LUI内部
|
||
if hasattr(handle_container, 'z_offset'):
|
||
handle_container.z_offset = 50
|
||
elif hasattr(handle_container, 'set_sort'):
|
||
handle_container.set_sort(50)
|
||
|
||
# 创建蓝色边框(外圈)
|
||
handle_border = LUISprite(handle_container, "blank", "skin")
|
||
handle_border.width = 10
|
||
handle_border.height = 10
|
||
handle_border.left = 0
|
||
handle_border.top = 0
|
||
handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框
|
||
|
||
# 创建白色圆形背景(内圈)
|
||
handle_bg = LUISprite(handle_container, "blank", "skin")
|
||
handle_bg.width = 8
|
||
handle_bg.height = 8
|
||
handle_bg.left = 1
|
||
handle_bg.top = 1
|
||
handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色
|
||
|
||
# 存储手柄位置信息
|
||
handle_container._resize_position = pos
|
||
handle_container._resize_index = i
|
||
handle_container._bg = handle_bg
|
||
handle_container._border = handle_border
|
||
|
||
# 绑定鼠标事件
|
||
handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h))
|
||
|
||
self.resize_handles.append(handle_container)
|
||
|
||
print("✓ 重新创建了手柄,确保在最前面")
|
||
|
||
def _handle_resize_drag(self):
|
||
"""处理resize拖拽 - 完整功能实现,支持Shift和Alt键操作"""
|
||
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
|
||
return
|
||
|
||
# 检查鼠标左键是否释放
|
||
import panda3d.core as p3d
|
||
if not self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
|
||
if self.resizing_handle:
|
||
comp_data = self.components[self.selected_index]
|
||
width_val = comp_data.get('width', 0)
|
||
height_val = comp_data.get('height', 0)
|
||
print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}")
|
||
|
||
# 更新锚点位置(如果有锚点)
|
||
if comp_data.get('anchored_to_parent'):
|
||
anchor_pos = comp_data.get('anchor_position')
|
||
if anchor_pos:
|
||
self._update_component_anchor_position(self.selected_index, anchor_pos)
|
||
|
||
self.resizing_handle = None
|
||
return
|
||
|
||
# 获取当前鼠标位置
|
||
mouse_x = self.world.mouseWatcherNode.getMouseX()
|
||
mouse_y = self.world.mouseWatcherNode.getMouseY()
|
||
|
||
win_x = self.world.win.getXSize()
|
||
win_y = self.world.win.getYSize()
|
||
pixel_x = (mouse_x + 1) * win_x / 2
|
||
pixel_y = (1 - mouse_y) * win_y / 2
|
||
|
||
# 计算鼠标移动距离
|
||
delta_x = pixel_x - self.resize_start_pos[0]
|
||
delta_y = pixel_y - self.resize_start_pos[1]
|
||
|
||
# 检查修饰键
|
||
shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift())
|
||
alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt())
|
||
|
||
# 获取组件数据
|
||
comp_data = self.components[self.selected_index]
|
||
comp_obj = comp_data['object']
|
||
comp_type = comp_data['type']
|
||
|
||
# 获取初始边界(Canvas相对坐标)
|
||
start_left = self.resize_start_bounds['left']
|
||
start_top = self.resize_start_bounds['top']
|
||
start_width = self.resize_start_bounds['width']
|
||
start_height = self.resize_start_bounds['height']
|
||
|
||
# 计算初始宽高比
|
||
aspect_ratio = start_width / start_height if start_height > 0 else 1
|
||
|
||
# 根据手柄位置计算新的边界
|
||
handle_pos = self.resizing_handle._resize_position
|
||
|
||
new_left = start_left
|
||
new_top = start_top
|
||
new_width = start_width
|
||
new_height = start_height
|
||
|
||
# 根据手柄位置调整尺寸
|
||
if handle_pos == 'top-left':
|
||
new_left = start_left + delta_x
|
||
new_top = start_top + delta_y
|
||
new_width = start_width - delta_x
|
||
new_height = start_height - delta_y
|
||
elif handle_pos == 'top':
|
||
new_top = start_top + delta_y
|
||
new_height = start_height - delta_y
|
||
elif handle_pos == 'top-right':
|
||
new_top = start_top + delta_y
|
||
new_width = start_width + delta_x
|
||
new_height = start_height - delta_y
|
||
elif handle_pos == 'right':
|
||
new_width = start_width + delta_x
|
||
elif handle_pos == 'bottom-right':
|
||
new_width = start_width + delta_x
|
||
new_height = start_height + delta_y
|
||
elif handle_pos == 'bottom':
|
||
new_height = start_height + delta_y
|
||
elif handle_pos == 'bottom-left':
|
||
new_left = start_left + delta_x
|
||
new_width = start_width - delta_x
|
||
new_height = start_height + delta_y
|
||
elif handle_pos == 'left':
|
||
new_left = start_left + delta_x
|
||
new_width = start_width - delta_x
|
||
|
||
# 应用最小尺寸限制
|
||
if new_width < self.min_size:
|
||
if handle_pos in ['top-left', 'left', 'bottom-left']:
|
||
new_left = start_left + start_width - self.min_size
|
||
new_width = self.min_size
|
||
|
||
if new_height < self.min_size:
|
||
if handle_pos in ['top-left', 'top', 'top-right']:
|
||
new_top = start_top + start_height - self.min_size
|
||
new_height = self.min_size
|
||
|
||
# Shift键:保持宽高比
|
||
if shift_held:
|
||
width_change = abs(new_width - start_width)
|
||
height_change = abs(new_height - start_height)
|
||
|
||
if width_change > height_change:
|
||
new_height = new_width / aspect_ratio
|
||
if handle_pos in ['top-left', 'top', 'top-right']:
|
||
new_top = start_top + start_height - new_height
|
||
else:
|
||
new_width = new_height * aspect_ratio
|
||
if handle_pos in ['top-left', 'left', 'bottom-left']:
|
||
new_left = start_left + start_width - new_width
|
||
|
||
# Alt键:从中心点缩放
|
||
if alt_held:
|
||
center_x = start_left + start_width / 2
|
||
center_y = start_top + start_height / 2
|
||
|
||
width_diff = new_width - start_width
|
||
height_diff = new_height - start_height
|
||
|
||
new_width = start_width + width_diff * 2
|
||
new_height = start_height + height_diff * 2
|
||
new_left = center_x - new_width / 2
|
||
new_top = center_y - new_height / 2
|
||
|
||
# 再次应用最小尺寸限制
|
||
if new_width < self.min_size:
|
||
new_width = self.min_size
|
||
new_left = center_x - new_width / 2
|
||
if new_height < self.min_size:
|
||
new_height = self.min_size
|
||
new_top = center_y - new_height / 2
|
||
|
||
# 获取Canvas边界约束
|
||
canvas_width = 800 # 默认Canvas宽度
|
||
canvas_height = 600 # 默认Canvas高度
|
||
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_panel = self.canvases[self.current_canvas_index]['panel']
|
||
canvas_width = canvas_panel.get_width()
|
||
canvas_height = canvas_panel.get_height()
|
||
|
||
|
||
# 最后的安全检查,防止宽高变为负数
|
||
if new_width < 1.0: new_width = 1.0
|
||
if new_height < 1.0: new_height = 1.0
|
||
|
||
if self.selected_index < 0:
|
||
return
|
||
|
||
comp_data = self.components[self.selected_index]
|
||
parent_index = comp_data.get('parent_index')
|
||
parent_offset_x = 0
|
||
parent_offset_y = 0
|
||
|
||
if parent_index is not None and parent_index >= 0:
|
||
parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index)
|
||
|
||
# Update component data
|
||
comp_data['left'] = new_left
|
||
comp_data['top'] = new_top
|
||
comp_data['width'] = new_width
|
||
comp_data['height'] = new_height
|
||
|
||
# Update component position
|
||
child_parent = getattr(comp_obj, 'parent', None)
|
||
parent_obj = None
|
||
if parent_index is not None and parent_index >= 0:
|
||
parent_obj = self.components[parent_index].get('object')
|
||
|
||
# Check if actually parented in LUI scene graph
|
||
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
|
||
|
||
if is_scene_parented:
|
||
# Component is physically parented to its logical parent -> Use Relative coords
|
||
comp_obj.left = new_left
|
||
comp_obj.top = new_top
|
||
else:
|
||
# Component is physically root/canvas -> Use Absolute coords (Parent Abs + Relative)
|
||
comp_obj.left = parent_offset_x + new_left
|
||
comp_obj.top = parent_offset_y + new_top
|
||
|
||
# Update component size
|
||
try:
|
||
if hasattr(comp_obj, 'width'):
|
||
comp_obj.width = new_width
|
||
if hasattr(comp_obj, 'height'):
|
||
comp_obj.height = new_height
|
||
|
||
if comp_type == 'Frame':
|
||
pass
|
||
elif comp_type in ['Plane', 'Image', 'Video']:
|
||
sprite = comp_data.get('sprite')
|
||
if sprite is not None:
|
||
sprite.width = new_width
|
||
sprite.height = new_height
|
||
elif comp_type == 'Button':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
if hasattr(comp_obj, 'set_height'):
|
||
comp_obj.set_height(new_height)
|
||
if hasattr(comp_obj, '_apply_stretch_sizes'):
|
||
comp_obj._apply_stretch_sizes()
|
||
elif comp_type == 'InputField':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
if hasattr(comp_obj, 'set_height'):
|
||
comp_obj.set_height(new_height)
|
||
if hasattr(comp_obj, 'width'):
|
||
comp_obj.width = new_width
|
||
if hasattr(comp_obj, 'height'):
|
||
comp_obj.height = new_height
|
||
try:
|
||
layout = getattr(comp_obj, '_layout', None)
|
||
if layout is not None:
|
||
if hasattr(layout, 'width'):
|
||
layout.width = '100%'
|
||
if hasattr(layout, 'height'):
|
||
layout.height = '100%'
|
||
inner = getattr(layout, '_layout', None)
|
||
if inner is not None:
|
||
if hasattr(inner, 'width'):
|
||
inner.width = '100%'
|
||
if hasattr(inner, 'height'):
|
||
inner.height = '100%'
|
||
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
|
||
spr = getattr(layout, attr, None)
|
||
if spr is not None and hasattr(spr, 'height'):
|
||
spr.height = '100%'
|
||
if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'):
|
||
spr.width = '100%'
|
||
except Exception:
|
||
pass
|
||
elif comp_type == 'Slider':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
if hasattr(comp_obj, 'set_height'):
|
||
comp_obj.set_height(new_height)
|
||
|
||
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
|
||
self._update_layout_inner(self.selected_index)
|
||
|
||
self._update_anchored_children(self.selected_index)
|
||
|
||
modifier_info = []
|
||
if shift_held:
|
||
modifier_info.append('Shift(keep ratio)')
|
||
if alt_held:
|
||
modifier_info.append('Alt(center scale)')
|
||
modifier_str = ' + '.join(modifier_info) if modifier_info else 'Normal'
|
||
except Exception as e:
|
||
print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}")
|
||
|
||
def _add_to_scene_tree(self, comp_data):
|
||
"""Add a virtual node to represent the LUI component in the scene tree"""
|
||
from panda3d.core import NodePath
|
||
node_name = f"UI_{comp_data['type']}_{len(self.components)}"
|
||
ui_node = NodePath(node_name)
|
||
|
||
# Reparent to current canvas node if available
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_node = self.canvases[self.current_canvas_index]['node']
|
||
ui_node.reparent_to(canvas_node)
|
||
comp_data['canvas_index'] = self.current_canvas_index
|
||
else:
|
||
ui_node.reparent_to(self.world.render)
|
||
comp_data['canvas_index'] = None
|
||
|
||
comp_data['ui_node'] = ui_node
|
||
comp_data['parent_index'] = None
|
||
comp_data['children_indices'] = []
|
||
|
||
def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False):
|
||
# Set parent-child relationship
|
||
if (child_index < 0 or child_index >= len(self.components) or
|
||
parent_index < 0 or parent_index >= len(self.components)):
|
||
print("Error: invalid component index")
|
||
return
|
||
|
||
child_data = self.components[child_index]
|
||
parent_data = self.components[parent_index]
|
||
|
||
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
|
||
child_data['draggable'] = False
|
||
|
||
if child_data.get('parent_index') == parent_index:
|
||
print(f"Component {child_index} already under {parent_index}")
|
||
return
|
||
|
||
current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index)
|
||
|
||
print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}")
|
||
print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})")
|
||
|
||
old_parent_index = child_data.get('parent_index')
|
||
if old_parent_index is not None and old_parent_index >= 0:
|
||
old_parent_data = self.components[old_parent_index]
|
||
if child_index in old_parent_data.get('children_indices', []):
|
||
old_parent_data['children_indices'].remove(child_index)
|
||
|
||
child_data['parent_index'] = parent_index
|
||
if 'children_indices' not in parent_data:
|
||
parent_data['children_indices'] = []
|
||
if child_index not in parent_data['children_indices']:
|
||
parent_data['children_indices'].append(child_index)
|
||
|
||
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
|
||
new_local_left = current_abs_left - parent_abs_left
|
||
new_local_top = current_abs_top - parent_abs_top
|
||
|
||
print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})")
|
||
print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})")
|
||
|
||
# 在数据结构中存储局部坐标(相对于父组件)
|
||
child_data['left'] = new_local_left
|
||
child_data['top'] = new_local_top
|
||
|
||
child_obj = child_data['object']
|
||
parent_obj = parent_data['object']
|
||
parent_type = parent_data.get('type')
|
||
visual_container_types = [
|
||
'Frame', 'Plane', 'Video',
|
||
'ScrollableRegion', 'TabbedFrame',
|
||
'VerticalLayout', 'HorizontalLayout'
|
||
]
|
||
if parent_data.get('type') == 'ScrollableRegion':
|
||
parent_obj = parent_data['object'].content_node
|
||
elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'):
|
||
if parent_data.get('layout_wrap', True):
|
||
parent_obj = parent_data.get('object')
|
||
else:
|
||
parent_obj = parent_data.get('layout_obj').cell()
|
||
|
||
try:
|
||
if parent_type in visual_container_types:
|
||
child_data['visual_parent_canvas'] = False
|
||
# 关键修改:不要调用 reparent_to(),避免破坏内部结构
|
||
# 只更新数据结构,保持原有的父对象关系
|
||
print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)")
|
||
else:
|
||
# Keep visual parent on canvas to avoid clipping, but keep logical parent
|
||
child_data['visual_parent_canvas'] = True
|
||
|
||
# If not preserving world position, clamp into parent bounds for visibility
|
||
if not keep_world:
|
||
try:
|
||
parent_w = parent_data.get('width')
|
||
parent_h = parent_data.get('height')
|
||
if parent_w is None and hasattr(parent_obj, 'width'):
|
||
parent_w = parent_obj.width
|
||
if parent_h is None and hasattr(parent_obj, 'height'):
|
||
parent_h = parent_obj.height
|
||
child_w = child_data.get('width')
|
||
child_h = child_data.get('height')
|
||
if child_w is None and hasattr(child_obj, 'width'):
|
||
child_w = child_obj.width
|
||
if child_h is None and hasattr(child_obj, 'height'):
|
||
child_h = child_obj.height
|
||
if parent_w and parent_h and child_w and child_h:
|
||
max_x = max(0.0, float(parent_w) - float(child_w))
|
||
max_y = max(0.0, float(parent_h) - float(child_h))
|
||
new_local_left = max(0.0, min(float(new_local_left), max_x))
|
||
new_local_top = max(0.0, min(float(new_local_top), max_y))
|
||
child_data['left'] = new_local_left
|
||
child_data['top'] = new_local_top
|
||
except Exception:
|
||
pass
|
||
|
||
# 关键修改:由于我们不实际重新父化对象,所有组件都保持相对于Canvas的绝对位置
|
||
# 只在数据结构中记录相对于父组件的局部坐标
|
||
if child_data.get('visual_parent_canvas'):
|
||
# Keep world position
|
||
try:
|
||
child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index)
|
||
except Exception:
|
||
pass
|
||
|
||
# 根据keep_world参数决定是否保持世界位置
|
||
if keep_world:
|
||
# 保持世界位置:使用绝对坐标
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = current_abs_left
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = current_abs_top
|
||
print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
|
||
else:
|
||
# 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变)
|
||
# 为了避免跳变,我们仍然使用绝对坐标
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = current_abs_left
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = current_abs_top
|
||
print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})")
|
||
|
||
# Ensure child visuals are visible above parent
|
||
if hasattr(child_obj, 'z_offset'):
|
||
try:
|
||
parent_z = getattr(parent_obj, 'z_offset', 0)
|
||
child_obj.z_offset = float(parent_z) + 2.0
|
||
except Exception:
|
||
pass
|
||
|
||
# Special handling for Button: raise internal sprites/label above parent
|
||
if child_data.get('type') == 'Button':
|
||
try:
|
||
layout = getattr(child_obj, '_layout', None)
|
||
if layout is not None:
|
||
for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'):
|
||
spr = getattr(layout, attr, None)
|
||
if spr is not None and hasattr(spr, 'z_offset'):
|
||
spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0
|
||
lbl = getattr(child_obj, '_label', None)
|
||
if lbl is not None and hasattr(lbl, 'z_offset'):
|
||
lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0
|
||
# Ensure label visible
|
||
if lbl is not None and hasattr(lbl, 'show'):
|
||
lbl.show()
|
||
except Exception:
|
||
pass
|
||
|
||
# Special handling for Frame: ensure visibility
|
||
elif child_data.get('type') == 'Frame':
|
||
try:
|
||
if hasattr(child_obj, 'show'):
|
||
child_obj.show()
|
||
if hasattr(child_obj, 'visible'):
|
||
child_obj.visible = True
|
||
print(f"[_set_parent_child_relationship] Frame visibility ensured")
|
||
except Exception as e:
|
||
print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}")
|
||
|
||
# Special handling for Plane, Image, Video: ensure sprite visibility
|
||
elif child_data.get('type') in ['Plane', 'Image', 'Video']:
|
||
try:
|
||
spr = child_data.get('sprite')
|
||
if spr is not None:
|
||
if hasattr(spr, 'show'):
|
||
spr.show()
|
||
if hasattr(spr, 'visible'):
|
||
spr.visible = True
|
||
|
||
# Ensure sprite color is not transparent
|
||
if child_data.get('type') == 'Plane':
|
||
color = child_data.get('color', (1, 1, 1, 1))
|
||
if hasattr(spr, 'color'):
|
||
spr.color = color
|
||
|
||
print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured")
|
||
except Exception as e:
|
||
print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}")
|
||
|
||
child_sprite = child_data.get('sprite')
|
||
if child_sprite is not None:
|
||
try:
|
||
if hasattr(child_sprite, 'show'):
|
||
child_sprite.show()
|
||
# Ensure sprite is attached to the child object
|
||
if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj:
|
||
child_sprite.parent = child_obj
|
||
# Restore sprite color if stored
|
||
if hasattr(child_sprite, 'color') and 'color' in child_data:
|
||
child_sprite.color = child_data.get('color', child_sprite.color)
|
||
if hasattr(child_sprite, 'z_offset'):
|
||
parent_sprite = parent_data.get('sprite')
|
||
parent_sprite_z = 0.0
|
||
if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'):
|
||
parent_sprite_z = float(parent_sprite.z_offset)
|
||
child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0)
|
||
except Exception:
|
||
pass
|
||
# Keep child visible above parent visuals
|
||
if hasattr(child_obj, 'z_offset'):
|
||
parent_z = getattr(parent_obj, 'z_offset', 0)
|
||
try:
|
||
child_obj.z_offset = float(parent_z) + 2.0
|
||
except Exception:
|
||
pass
|
||
# Ensure child sprite renders above parent visuals
|
||
child_sprite = child_data.get('sprite')
|
||
if child_sprite is not None and hasattr(child_sprite, 'z_offset'):
|
||
parent_sprite_z = 0
|
||
try:
|
||
parent_sprite = parent_data.get('sprite')
|
||
if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'):
|
||
parent_sprite_z = float(parent_sprite.z_offset)
|
||
except Exception:
|
||
parent_sprite_z = 0
|
||
try:
|
||
child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0)
|
||
except Exception:
|
||
pass
|
||
if hasattr(child_obj, 'show'):
|
||
child_obj.show()
|
||
|
||
except Exception as e:
|
||
print(f"Reparenting error: {e}")
|
||
|
||
# Ensure canvas_index follows parent canvas (visibility)
|
||
parent_canvas = parent_data.get('canvas_index')
|
||
if parent_canvas is not None:
|
||
self._update_canvas_index_recursive(child_index, parent_canvas)
|
||
|
||
if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True):
|
||
self._apply_wrap_layout(parent_index)
|
||
|
||
print(f"Set parent: child #{child_index} -> parent #{parent_index}")
|
||
|
||
|
||
def _set_parent_root(self, child_index, keep_world=False):
|
||
"""Detach a component from its parent and move it to root (canvas)."""
|
||
if child_index < 0 or child_index >= len(self.components):
|
||
print("Error: invalid component index")
|
||
return
|
||
|
||
child_data = self.components[child_index]
|
||
|
||
# 保存当前的累积位置(相对于Canvas的绝对位置)
|
||
current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index)
|
||
|
||
print(f"[_set_parent_root] Component #{child_index} current accumulated pos: ({current_abs_left:.1f}, {current_abs_top:.1f})")
|
||
|
||
old_parent_index = child_data.get('parent_index')
|
||
if old_parent_index is not None and old_parent_index >= 0:
|
||
old_parent_data = self.components[old_parent_index]
|
||
if child_index in old_parent_data.get('children_indices', []):
|
||
old_parent_data['children_indices'].remove(child_index)
|
||
|
||
# 更新数据结构
|
||
child_data['parent_index'] = None
|
||
child_data['visual_parent_canvas'] = False
|
||
child_data['draggable'] = True
|
||
|
||
# 获取Canvas panel
|
||
parent_obj = None
|
||
canvas_index = self.current_canvas_index
|
||
if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases):
|
||
parent_obj = self.canvases[canvas_index]['panel']
|
||
child_data['canvas_index'] = canvas_index
|
||
else:
|
||
parent_obj = getattr(self, 'overlay_root', None) or getattr(self.world, 'render', None)
|
||
|
||
child_obj = child_data.get('object')
|
||
|
||
# 关键修改:对于 Button 等复杂组件,不要重新父化!
|
||
# 只更新位置和数据结构
|
||
if child_obj is not None:
|
||
# 更新位置数据
|
||
if keep_world:
|
||
child_data['left'] = current_abs_left
|
||
child_data['top'] = current_abs_top
|
||
|
||
# 直接设置对象位置,不改变父对象
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(current_abs_left, current_abs_top)
|
||
else:
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = current_abs_left
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = current_abs_top
|
||
|
||
print(f"[_set_parent_root] Set position to: ({current_abs_left:.1f}, {current_abs_top:.1f}) without reparenting")
|
||
else:
|
||
# 不保持世界位置:使用原有的局部坐标
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = child_data.get('left', 0)
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = child_data.get('top', 0)
|
||
|
||
# 确保可见性
|
||
if child_obj is not None:
|
||
try:
|
||
if hasattr(child_obj, 'show'):
|
||
child_obj.show()
|
||
elif hasattr(child_obj, 'visible'):
|
||
child_obj.visible = True
|
||
except Exception:
|
||
pass
|
||
|
||
# 对于 Button,确保内部组件可见
|
||
comp_type = child_data.get('type')
|
||
if comp_type == 'Button' and child_obj is not None:
|
||
try:
|
||
layout = getattr(child_obj, '_layout', None)
|
||
lbl = getattr(child_obj, '_label', None)
|
||
|
||
if layout is not None:
|
||
if hasattr(layout, 'show'):
|
||
layout.show()
|
||
if hasattr(layout, 'visible'):
|
||
layout.visible = True
|
||
|
||
if lbl is not None:
|
||
if hasattr(lbl, 'show'):
|
||
lbl.show()
|
||
if hasattr(lbl, 'visible'):
|
||
lbl.visible = True
|
||
|
||
# 重新设置文本
|
||
current_text = child_data.get('text', 'Button')
|
||
if hasattr(lbl, 'set_text'):
|
||
lbl.set_text(current_text)
|
||
elif hasattr(lbl, 'text'):
|
||
lbl.text = current_text
|
||
|
||
print(f"[_set_parent_root] Button components visibility ensured")
|
||
except Exception as e:
|
||
print(f"[_set_parent_root] Error ensuring button visibility: {e}")
|
||
|
||
# 对于 Plane, Image, Video - 确保内部 sprite 可见
|
||
elif comp_type in ['Plane', 'Image', 'Video'] and child_obj is not None:
|
||
try:
|
||
spr = child_data.get('sprite')
|
||
if spr is not None:
|
||
if hasattr(spr, 'show'):
|
||
spr.show()
|
||
if hasattr(spr, 'visible'):
|
||
spr.visible = True
|
||
|
||
# 确保 sprite 的颜色不是透明的
|
||
if comp_type == 'Plane':
|
||
color = child_data.get('color', (1, 1, 1, 1))
|
||
if hasattr(spr, 'color'):
|
||
spr.color = color
|
||
|
||
print(f"[_set_parent_root] {comp_type} sprite visibility ensured")
|
||
except Exception as e:
|
||
print(f"[_set_parent_root] Error ensuring {comp_type} visibility: {e}")
|
||
|
||
# 对于 Frame - 直接确保可见
|
||
elif comp_type == 'Frame' and child_obj is not None:
|
||
try:
|
||
if hasattr(child_obj, 'show'):
|
||
child_obj.show()
|
||
if hasattr(child_obj, 'visible'):
|
||
child_obj.visible = True
|
||
print(f"[_set_parent_root] Frame visibility ensured")
|
||
except Exception as e:
|
||
print(f"[_set_parent_root] Error ensuring Frame visibility: {e}")
|
||
|
||
# Update canvas index recursively
|
||
self._update_canvas_index_recursive(child_index, child_data.get('canvas_index'))
|
||
|
||
# 调试输出:确认组件状态
|
||
print(f"[_set_parent_root] Component #{child_index} ({child_data.get('name', 'Unknown')}) moved to root (data only)")
|
||
print(f" - Position: ({child_data.get('left', 0):.1f}, {child_data.get('top', 0):.1f})")
|
||
print(f" - Size: ({child_data.get('width', 0):.1f}, {child_data.get('height', 0):.1f})")
|
||
print(f" - Canvas index: {child_data.get('canvas_index')}")
|
||
print(f" - Visible: {getattr(child_obj, 'visible', 'N/A') if child_obj else 'N/A'}")
|
||
print(f" - Parent (unchanged): {getattr(child_obj, 'parent', 'N/A') if child_obj else 'N/A'}")
|
||
def create_child_component(self, parent_index, comp_type, anchor_position=None):
|
||
# Create child component under parent
|
||
print("\n=== Create child component ===")
|
||
print(f"Parent index: {parent_index}, Type: {comp_type}")
|
||
|
||
if parent_index < 0 or parent_index >= len(self.components):
|
||
print("Error: invalid parent index")
|
||
return None
|
||
|
||
parent_data = self.components[parent_index]
|
||
parent_obj = parent_data['object']
|
||
if parent_data.get('type') == 'ScrollableRegion':
|
||
parent_obj = parent_data['object'].content_node
|
||
elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'):
|
||
if parent_data.get('layout_wrap', True):
|
||
parent_obj = parent_data.get('object')
|
||
else:
|
||
parent_obj = parent_data.get('layout_obj').cell()
|
||
|
||
parent_width = parent_data.get('width', 100)
|
||
parent_height = parent_data.get('height', 30)
|
||
|
||
comp_sizes = {
|
||
'Button': (100, 30),
|
||
'Text': (80, 20),
|
||
'Label': (80, 20),
|
||
'CheckBox': (120, 20),
|
||
'InputField': (200, 24),
|
||
'Slider': (200, 16),
|
||
'Frame': (300, 200),
|
||
'Plane': (100, 100),
|
||
'Image': (100, 100),
|
||
'Video': (320, 240),
|
||
'Progressbar': (200, 30),
|
||
'Selectbox': (200, 30),
|
||
'ScrollableRegion': (200, 200),
|
||
'TabbedFrame': (300, 200),
|
||
'VerticalLayout': (300, 200),
|
||
'HorizontalLayout': (300, 200),
|
||
}
|
||
|
||
child_w, child_h = comp_sizes.get(comp_type, (80, 20))
|
||
child_x = (parent_width - child_w) / 2
|
||
child_y = (parent_height - child_h) / 2
|
||
|
||
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
|
||
child_x = 0
|
||
child_y = 0
|
||
|
||
child_obj = None
|
||
try:
|
||
if comp_type == 'Button':
|
||
child_obj = self.luiFunction.create_button(self, text="ChildButton", x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type in ['Text', 'Label']:
|
||
child_obj = self.luiFunction.create_label(self, text="ChildText", x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'CheckBox':
|
||
child_obj = self.luiFunction.create_checkbox(self, label="ChildCheckbox", x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'InputField':
|
||
child_obj = self.luiFunction.create_input_field(self, text="ChildInput", x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Slider':
|
||
child_obj = self.luiFunction.create_slider(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Frame':
|
||
child_obj = self.luiFunction.create_frame(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Plane':
|
||
child_obj = self.luiFunction.create_plane(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Image':
|
||
child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Video':
|
||
child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Progressbar':
|
||
child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'Selectbox':
|
||
child_obj = self.luiFunction.create_selectbox(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'ScrollableRegion':
|
||
child_obj = self.luiFunction.create_scrollable_region(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'TabbedFrame':
|
||
child_obj = self.luiFunction.create_tabbed_frame(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'VerticalLayout':
|
||
child_obj = self.luiFunction.create_vertical_layout(self, x=child_x, y=child_y, parent=parent_obj)
|
||
elif comp_type == 'HorizontalLayout':
|
||
child_obj = self.luiFunction.create_horizontal_layout(self, x=child_x, y=child_y, parent=parent_obj)
|
||
else:
|
||
print(f"Error: unsupported component type {comp_type}")
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"Error: create component failed: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
if child_obj:
|
||
child_index = -1
|
||
for i, comp in enumerate(self.components):
|
||
if comp.get('object') == child_obj:
|
||
child_index = i
|
||
break
|
||
|
||
if child_index >= 0:
|
||
child_data = self.components[child_index]
|
||
if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True):
|
||
self._apply_wrap_layout(parent_index)
|
||
|
||
if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']:
|
||
child_data['draggable'] = False
|
||
|
||
child_data['parent_index'] = parent_index
|
||
if 'children_indices' not in parent_data:
|
||
parent_data['children_indices'] = []
|
||
if child_index not in parent_data['children_indices']:
|
||
parent_data['children_indices'].append(child_index)
|
||
|
||
print(f"Linked child #{child_index} -> parent #{parent_index}")
|
||
return child_index
|
||
else:
|
||
print("Child creation failed")
|
||
return None
|
||
|
||
def _get_descendants(self, idx):
|
||
"""Return set of all descendant indices for a component."""
|
||
desc = set()
|
||
if idx < 0 or idx >= len(self.components):
|
||
return desc
|
||
comp = self.components[idx]
|
||
for c_idx in comp.get('children_indices', []):
|
||
if c_idx in desc:
|
||
continue
|
||
desc.add(c_idx)
|
||
desc.update(self._get_descendants(c_idx))
|
||
return desc
|
||
|
||
def _update_canvas_index_recursive(self, idx, canvas_index):
|
||
"""Update canvas_index for component and its descendants."""
|
||
if idx < 0 or idx >= len(self.components):
|
||
return
|
||
comp = self.components[idx]
|
||
comp['canvas_index'] = canvas_index
|
||
for c_idx in comp.get('children_indices', []):
|
||
self._update_canvas_index_recursive(c_idx, canvas_index)
|
||
|
||
def _reorder_list(self, items, src, target):
|
||
"""Move src before target within items list."""
|
||
if src == target:
|
||
return items
|
||
if src not in items or target not in items:
|
||
return items
|
||
items = list(items)
|
||
items.remove(src)
|
||
insert_at = items.index(target)
|
||
items.insert(insert_at, src)
|
||
return items
|
||
|
||
def _sync_canvas_children(self, parent_index):
|
||
"""Update positions for children that keep visual parent on canvas."""
|
||
if parent_index < 0 or parent_index >= len(self.components):
|
||
return
|
||
|
||
# Get parent's absolute position
|
||
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
|
||
parent_data = self.components[parent_index]
|
||
parent_obj = parent_data.get('object')
|
||
|
||
for child_idx in parent_data.get('children_indices', []):
|
||
if child_idx < 0 or child_idx >= len(self.components):
|
||
continue
|
||
child = self.components[child_idx]
|
||
child_obj = child.get('object')
|
||
|
||
if child_obj is None:
|
||
continue
|
||
|
||
# Check actual parenting
|
||
child_parent = getattr(child_obj, 'parent', None)
|
||
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
|
||
|
||
if not is_scene_parented:
|
||
# If child is NOT physically parented to the parent object (e.g. it is on Canvas),
|
||
# we MUST manually update its absolute position to follow the parent.
|
||
abs_left = parent_abs_left + float(child.get('left', 0))
|
||
abs_top = parent_abs_top + float(child.get('top', 0))
|
||
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = abs_left
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = abs_top
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(abs_left, abs_top)
|
||
|
||
# Recurse for grandchildren
|
||
self._sync_canvas_children(child_idx)
|
||
|
||
|
||
|
||
|
||
|
||
def draw_editor(self):
|
||
"""Draw the LUI Editor Window"""
|
||
if not self.lui_enabled or not self.show_editor:
|
||
return
|
||
|
||
imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever)
|
||
with imgui_ctx.begin("LUI编辑器", True) as (opened, show):
|
||
self.show_editor = opened
|
||
if not show:
|
||
return
|
||
|
||
# Play/Stop
|
||
if imgui.button("Play" if not self.play_mode else "Stop", (120, 25)):
|
||
self.play_mode = not self.play_mode
|
||
if self.play_mode:
|
||
# Clear editor-only state when entering play mode
|
||
self.selected_index = -1
|
||
self.dragging_comp = None
|
||
self.pending_drag_comp = None
|
||
self.pending_drag_index = -1
|
||
self.pending_drag_start_mouse = None
|
||
self.pending_drag_start_abs = None
|
||
self.pending_drag_start_parent = None
|
||
self._hide_resize_handles()
|
||
# Keep LUI input enabled for runtime UI interaction
|
||
self.set_input_enabled(True)
|
||
imgui.same_line()
|
||
imgui.text_colored((0.2, 1.0, 0.2, 1.0) if self.play_mode else (1.0, 0.8, 0.2, 1.0),
|
||
"Running" if self.play_mode else "Editing")
|
||
imgui.separator()
|
||
|
||
# In play mode, show toolbar only
|
||
if self.play_mode:
|
||
return
|
||
|
||
# Canvas Management
|
||
imgui.text("Canvas 管理")
|
||
imgui.separator()
|
||
|
||
if imgui.button("创建新Canvas", (150, 25)):
|
||
self.luiFunction.create_canvas(self)
|
||
|
||
imgui.same_line()
|
||
|
||
# 显示当前Canvas信息
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
current_canvas_name = self.canvases[self.current_canvas_index]['name']
|
||
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前: {current_canvas_name}")
|
||
else:
|
||
imgui.text_colored((1.0, 0.0, 0.0, 1.0), "无Canvas")
|
||
|
||
if len(self.canvases) > 0:
|
||
canvas_names = [c['name'] for c in self.canvases]
|
||
if self.current_canvas_index >= len(canvas_names):
|
||
self.current_canvas_index = len(canvas_names) - 1
|
||
|
||
changed, new_index = imgui.combo("选择Canvas", self.current_canvas_index, canvas_names)
|
||
if changed:
|
||
self.switch_canvas(new_index)
|
||
|
||
# Canvas Properties
|
||
if self.current_canvas_index >= 0:
|
||
canvas = self.canvases[self.current_canvas_index]
|
||
panel = canvas['panel']
|
||
imgui.indent()
|
||
|
||
# Visibility
|
||
is_visible = canvas.get('visible', True)
|
||
changed, visible = imgui.checkbox("显示Canvas", is_visible)
|
||
if changed:
|
||
canvas['visible'] = visible
|
||
if visible:
|
||
panel.show()
|
||
else:
|
||
panel.hide()
|
||
|
||
|
||
# Fill Mode & Margins
|
||
is_fill_mode = canvas.get('fill_mode', False)
|
||
imgui.separator()
|
||
changed, fill_mode = imgui.checkbox("场景填充模式", is_fill_mode)
|
||
if changed:
|
||
canvas['fill_mode'] = fill_mode
|
||
if fill_mode:
|
||
# 刚切换到填充模式时,更新一次
|
||
self._update_canvas_geometry(canvas)
|
||
|
||
if fill_mode:
|
||
imgui.text("场景边距设置 (调整以匹配视口)")
|
||
margins = canvas.get('margins', {'left':0,'right':0,'top':0,'bottom':0})
|
||
geometry_changed = False
|
||
|
||
changed, margins['left'] = imgui.drag_float("左边距", margins['left'], 1.0, 0, 1000)
|
||
if changed: geometry_changed = True
|
||
|
||
changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000)
|
||
if changed: geometry_changed = True
|
||
|
||
changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500)
|
||
if changed: geometry_changed = True
|
||
|
||
changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500)
|
||
if changed: geometry_changed = True
|
||
|
||
if geometry_changed:
|
||
canvas['margins'] = margins
|
||
self._update_canvas_geometry(canvas)
|
||
else:
|
||
# Manual Position
|
||
curr_pos_val = panel.get_pos()
|
||
curr_pos = (curr_pos_val.x, curr_pos_val.y)
|
||
|
||
# 固定开关
|
||
is_fixed = canvas.get('fixed', False)
|
||
changed, fixed = imgui.checkbox("锁定位置 (禁止拖动)", is_fixed)
|
||
if changed:
|
||
canvas['fixed'] = fixed
|
||
|
||
if not is_fixed:
|
||
changed, pos = imgui.drag_float2("Canvas位置", curr_pos)
|
||
if changed:
|
||
panel.set_pos(pos[0], pos[1])
|
||
|
||
|
||
# Manual Size
|
||
curr_size = (panel.get_width(), panel.get_height())
|
||
changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000)
|
||
if changed:
|
||
panel.width = size[0]
|
||
panel.height = size[1]
|
||
# 同时更新背景尺寸
|
||
if 'background' in canvas:
|
||
bg = canvas['background']
|
||
bg.width = size[0]
|
||
bg.height = size[1]
|
||
|
||
# Canvas Color
|
||
if 'background' in canvas:
|
||
imgui.separator()
|
||
bg = canvas['background']
|
||
# 获取当前颜色 (RGBA)
|
||
current_color = bg.color
|
||
# 转换为可编辑的格式 (list)
|
||
edit_color = [current_color[0], current_color[1], current_color[2], current_color[3]]
|
||
|
||
changed, new_color = imgui.color_edit4("背景颜色/透明度", edit_color)
|
||
if changed:
|
||
bg.color = (new_color[0], new_color[1], new_color[2], new_color[3])
|
||
|
||
# Title settings
|
||
if 'title_label' in canvas:
|
||
imgui.separator()
|
||
imgui.text("标题设置")
|
||
title = canvas['title_label']
|
||
|
||
# Visible
|
||
is_title_visible = title.visible
|
||
changed, title_visible = imgui.checkbox("显示标题", is_title_visible)
|
||
if changed:
|
||
try:
|
||
if title_visible:
|
||
title.show()
|
||
else:
|
||
title.hide()
|
||
except:
|
||
title.visible = title_visible
|
||
|
||
# Text content
|
||
curr_text = title.text
|
||
changed, new_text = imgui.input_text("标题文本", curr_text, 128)
|
||
if changed:
|
||
title.text = new_text
|
||
|
||
# Position
|
||
curr_pos_val = title.get_pos()
|
||
curr_pos = (curr_pos_val.x, curr_pos_val.y)
|
||
changed, new_pos = imgui.drag_float2("标题位置", curr_pos)
|
||
if changed:
|
||
title.set_pos(new_pos[0], new_pos[1])
|
||
|
||
imgui.unindent()
|
||
else:
|
||
imgui.text_colored((1, 1, 0, 1), "请先创建一个Canvas")
|
||
|
||
imgui.spacing()
|
||
imgui.text("创建组件")
|
||
imgui.separator()
|
||
|
||
if len(self.canvases) == 0:
|
||
imgui.begin_disabled()
|
||
|
||
# Component Type Selection
|
||
component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)",
|
||
"滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)",
|
||
"进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)",
|
||
"垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"]
|
||
|
||
changed, self._selected_type_index = imgui.combo("组件类型", self._selected_type_index, component_types)
|
||
|
||
if imgui.button("创建组件", (120, 25)):
|
||
self._create_component_by_type(self._selected_type_index)
|
||
|
||
if len(self.canvases) == 0:
|
||
imgui.end_disabled()
|
||
|
||
imgui.spacing()
|
||
imgui.text(f"已创建组件 ({len(self.components)})")
|
||
imgui.separator()
|
||
|
||
# Component Tree Helper
|
||
# self.draw_component_tree()
|
||
|
||
imgui.spacing()
|
||
imgui.separator()
|
||
|
||
# Anchor Popup Handling
|
||
self._handle_anchor_popup()
|
||
|
||
def draw_component_tree(self):
|
||
"""Draw the component tree (can be used in other windows)"""
|
||
# Track tree item rects for drop hit-testing (allows drop on earlier items)
|
||
self._tree_item_rects = {}
|
||
|
||
def _store_item_rect(item_key):
|
||
try:
|
||
rmin = imgui.get_item_rect_min()
|
||
rmax = imgui.get_item_rect_max()
|
||
self._tree_item_rects[item_key] = (rmin, rmax)
|
||
except Exception:
|
||
pass
|
||
|
||
def _hit_test_tree_item(mx, my):
|
||
for key, (rmin, rmax) in self._tree_item_rects.items():
|
||
try:
|
||
min_x = rmin.x
|
||
min_y = rmin.y
|
||
max_x = rmax.x
|
||
max_y = rmax.y
|
||
except Exception:
|
||
min_x = rmin[0]
|
||
min_y = rmin[1]
|
||
max_x = rmax[0]
|
||
max_y = rmax[1]
|
||
if min_x <= mx <= max_x and min_y <= my <= max_y:
|
||
return key
|
||
return None
|
||
|
||
# Root drop target
|
||
if imgui.selectable("ROOT", self.selected_index < 0):
|
||
# ????????
|
||
pass
|
||
_store_item_rect(-1)
|
||
|
||
def render_component_tree(idx):
|
||
if idx >= len(self.components): return True
|
||
comp = self.components[idx]
|
||
comp_type = comp.get('type', 'Unknown')
|
||
|
||
label = f"{comp.get('name', 'N/A')}"
|
||
|
||
# Determine flags
|
||
flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open
|
||
if self.selected_index == idx:
|
||
flags |= imgui.TreeNodeFlags_.selected
|
||
|
||
# Check for children
|
||
children = list(comp.get('children_indices', []))
|
||
if not children:
|
||
flags |= imgui.TreeNodeFlags_.leaf
|
||
else:
|
||
label += f" ({len(children)})"
|
||
|
||
# Draw tree node
|
||
is_open = imgui.tree_node_ex(f"##node_{idx}", flags, label)
|
||
_store_item_rect(idx)
|
||
|
||
# Custom drag/drop (avoid payload capsule issues)
|
||
if imgui.is_item_active() and imgui.is_mouse_dragging(0):
|
||
self._tree_drag_src = idx
|
||
imgui.begin_tooltip()
|
||
imgui.text(f"Move: {label}")
|
||
imgui.end_tooltip()
|
||
|
||
# Handle selection
|
||
if imgui.is_item_clicked():
|
||
self.selected_index = idx
|
||
# Clear 3D scene selection
|
||
if hasattr(self.world, 'selection') and self.world.selection:
|
||
self.world.selection.selectedNode = None
|
||
self.world.selection.clearSelectionBox()
|
||
self.world.selection.clearGizmo()
|
||
|
||
# Right-click menu
|
||
if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"):
|
||
if imgui.menu_item("删除", "", False)[0]:
|
||
self.luiFunction.delete_component(self, idx)
|
||
imgui.end_popup()
|
||
if is_open: imgui.tree_pop()
|
||
return False
|
||
|
||
imgui.separator()
|
||
if imgui.begin_menu("创建子组件"):
|
||
child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video','Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout']
|
||
cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频','进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组']
|
||
for ct_idx, ct in enumerate(child_types):
|
||
if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]:
|
||
self.create_child_component(idx, ct)
|
||
imgui.end_menu()
|
||
imgui.end_popup()
|
||
|
||
# Draw children if open
|
||
if is_open:
|
||
for child_idx in children:
|
||
# After a deletion, the list might have shifted,
|
||
# so we check bounds again or just return False to stop this frame's recursion
|
||
if child_idx < len(self.components):
|
||
if not render_component_tree(child_idx):
|
||
# If a child (or sub-child) was deleted, indices shifted.
|
||
# Stopping current rendering frame's branch is safest.
|
||
imgui.tree_pop()
|
||
return False
|
||
imgui.tree_pop()
|
||
return True
|
||
|
||
# Find root components
|
||
roots = []
|
||
for i, comp in enumerate(self.components):
|
||
p_idx = comp.get('parent_index')
|
||
if p_idx is None or p_idx < 0:
|
||
roots.append(i)
|
||
|
||
# Maintain root order for sorting
|
||
if not self.root_order:
|
||
self.root_order = list(roots)
|
||
else:
|
||
self.root_order = [r for r in self.root_order if r in roots]
|
||
for r in roots:
|
||
if r not in self.root_order:
|
||
self.root_order.append(r)
|
||
|
||
# Render roots
|
||
for root_idx in self.root_order:
|
||
if not render_component_tree(root_idx):
|
||
break
|
||
|
||
# Handle tree drop after full render (allows drop on earlier items)
|
||
if self._tree_drag_src is not None and imgui.is_mouse_released(0):
|
||
src_idx = self._tree_drag_src
|
||
self._tree_drag_src = None
|
||
mx, my = imgui.get_mouse_pos()
|
||
target_idx = _hit_test_tree_item(mx, my)
|
||
if target_idx is None:
|
||
target_idx = -1
|
||
if 0 <= src_idx < len(self.components):
|
||
if target_idx == -1:
|
||
self._set_parent_root(src_idx, keep_world=True)
|
||
elif target_idx != src_idx:
|
||
if target_idx in self._get_descendants(src_idx):
|
||
print("Invalid drop: cannot parent to descendant")
|
||
else:
|
||
src_parent = self.components[src_idx].get('parent_index', -1)
|
||
tgt_parent = self.components[target_idx].get('parent_index', -1)
|
||
io = imgui.get_io()
|
||
want_reorder = bool(getattr(io, 'key_shift', False))
|
||
if want_reorder and src_parent == tgt_parent:
|
||
if src_parent is None or src_parent < 0:
|
||
if not self.root_order:
|
||
self.root_order = [i for i, c in enumerate(self.components) if c.get('parent_index') is None or c.get('parent_index') < 0]
|
||
self.root_order = self._reorder_list(self.root_order, src_idx, target_idx)
|
||
else:
|
||
parent_data = self.components[src_parent]
|
||
children = parent_data.get('children_indices', [])
|
||
parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx)
|
||
else:
|
||
self._set_parent_child_relationship(src_idx, target_idx, keep_world=True)
|
||
|
||
# Clear drag state if released outside any item
|
||
if self._tree_drag_src is not None and imgui.is_mouse_released(0):
|
||
self._tree_drag_src = None
|
||
|
||
imgui.spacing()
|
||
imgui.separator()
|
||
|
||
# Property Editing
|
||
if self.selected_index >= 0 and self.selected_index < len(self.components):
|
||
imgui.text("属性编辑")
|
||
imgui.separator()
|
||
self.luiFunction._draw_component_properties(self,self.selected_index)
|
||
else:
|
||
imgui.text_disabled("未选中任何组件")
|
||
|
||
# Anchor popup
|
||
if self.anchor_popup_open:
|
||
imgui.open_popup("选择锚点位置")
|
||
|
||
if imgui.begin_popup("选择锚点位置"):
|
||
imgui.text("选择锚点位置:")
|
||
|
||
# 中文显示名称
|
||
anchor_names = {
|
||
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
|
||
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
|
||
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
|
||
}
|
||
|
||
anchor_positions = [
|
||
['top-left', 'top-center', 'top-right'],
|
||
['middle-left', 'center', 'middle-right'],
|
||
['bottom-left', 'bottom-center', 'bottom-right']
|
||
]
|
||
|
||
for row in anchor_positions:
|
||
for i, pos in enumerate(row):
|
||
button_text = anchor_names.get(pos, pos)
|
||
if imgui.button(f"{button_text}##popup_{pos}", (50, 25)):
|
||
# 使用新的锚点更新方法
|
||
if hasattr(self, '_temp_selected_index_for_anchor'):
|
||
selected_idx = self._temp_selected_index_for_anchor
|
||
self._update_component_anchor_position(selected_idx, pos)
|
||
|
||
self.anchor_popup_open = False
|
||
if hasattr(self, '_temp_selected_index_for_anchor'):
|
||
delattr(self, '_temp_selected_index_for_anchor')
|
||
imgui.close_current_popup()
|
||
if i < len(row) - 1:
|
||
imgui.same_line()
|
||
|
||
imgui.spacing()
|
||
if imgui.button("取消"):
|
||
self.anchor_popup_open = False
|
||
if hasattr(self, '_temp_selected_index_for_anchor'):
|
||
delattr(self, '_temp_selected_index_for_anchor')
|
||
imgui.close_current_popup()
|
||
|
||
imgui.end_popup()
|
||
|
||
def _create_component_by_type(self, type_index):
|
||
"""Create LUI component by index - 默认生成在Canvas中心"""
|
||
|
||
# 1. 确定生成位置 (默认 Canvas 中心)
|
||
canvas_relative_x = 100
|
||
canvas_relative_y = 100
|
||
|
||
if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases):
|
||
canvas_panel = self.canvases[self.current_canvas_index]['panel']
|
||
canvas_width = canvas_panel.get_width()
|
||
canvas_height = canvas_panel.get_height()
|
||
|
||
# 根据组件类型预测尺寸,以便进行中心点对齐
|
||
# 注意:这些是 lui_function.py 中定义的默认值
|
||
comp_sizes = {
|
||
0: (100, 30), # Button
|
||
1: (80, 20), # Text
|
||
2: (120, 20), # Checkbox
|
||
3: (200, 24), # InputField
|
||
4: (200, 16), # Slider
|
||
5: (300, 200), # Frame
|
||
6: (100, 100), # Plane
|
||
7: (100, 100), # Image
|
||
8: (320, 240), # Video
|
||
9: (200, 30), # Progressbar
|
||
10: (200, 30), # Selectbox
|
||
11: (200, 200), # ScrollableRegion
|
||
12: (300, 200), # TabbedFrame
|
||
13: (300, 200), # VerticalLayout
|
||
14: (300, 200) # HorizontalLayout
|
||
}
|
||
|
||
|
||
w, h = comp_sizes.get(type_index, (100, 100))
|
||
|
||
# 计算中心位置
|
||
canvas_relative_x = (canvas_width - w) / 2
|
||
canvas_relative_y = (canvas_height - h) / 2
|
||
|
||
# 确保不超出合理范围 (防止 Canvas 太小时位置为负)
|
||
canvas_relative_x = max(10, canvas_relative_x)
|
||
canvas_relative_y = max(10, canvas_relative_y)
|
||
|
||
try:
|
||
if type_index == 0: # Button
|
||
self.luiFunction.create_button(self, text="New Button", x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 1: # Text
|
||
self.luiFunction.create_label(self, text="New Text", x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 2: # Checkbox
|
||
self.luiFunction.create_checkbox(self, label="Check box", x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 3: # InputField
|
||
self.luiFunction.create_input_field(self, text="", x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 4: # Slider
|
||
self.luiFunction.create_slider(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 5: # Frame
|
||
self.luiFunction.create_frame(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 6: # Plane
|
||
self.luiFunction.create_plane(self, x=canvas_relative_x, y=canvas_relative_y, color=(0.8, 0.8, 0.8, 1))
|
||
elif type_index == 7: # Image
|
||
self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 8: # Video
|
||
self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 9: # Progressbar
|
||
self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 10: # Selectbox
|
||
self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 11: # ScrollableRegion
|
||
self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 12: # TabbedFrame
|
||
self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 13: # VerticalLayout
|
||
self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
elif type_index == 14: # HorizontalLayout
|
||
self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y)
|
||
|
||
print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})")
|
||
if hasattr(self.world, 'updateSceneTree'):
|
||
self.world.updateSceneTree()
|
||
except Exception as e:
|
||
print(f"创建组件失败: {str(e)}")
|
||
|
||
|
||
|
||
def set_component_anchor(self, comp_index, anchor_position):
|
||
"""设置组件锚点 - 保持向后兼容"""
|
||
self._update_component_anchor_position(comp_index, anchor_position)
|
||
|
||
def _update_anchored_children(self, parent_index):
|
||
"""Recursively update anchored/fill children."""
|
||
if parent_index < 0 or parent_index >= len(self.components):
|
||
return
|
||
|
||
print(f"[_update_anchored_children] Updating children of #{parent_index}")
|
||
parent_data = self.components[parent_index]
|
||
children = parent_data.get('children_indices', [])
|
||
for child_idx in children:
|
||
if child_idx < len(self.components):
|
||
child_data = self.components[child_idx]
|
||
# Fill layout has priority over anchored positioning
|
||
if child_data.get('layout_mode') == 'fill':
|
||
print(f" -> Child #{child_idx} is FILL mode")
|
||
self._apply_fill_layout(child_idx)
|
||
elif child_data.get('anchored_to_parent'):
|
||
anchor_pos = child_data.get('anchor_position')
|
||
manual_offset = (
|
||
child_data.get('anchor_manual_offset_x', 0.0),
|
||
child_data.get('anchor_manual_offset_y', 0.0)
|
||
)
|
||
if anchor_pos:
|
||
self._update_component_anchor_position(child_idx, anchor_pos, manual_offset)
|
||
|
||
# Recurse into children
|
||
self._update_anchored_children(child_idx)
|
||
|
||
def _apply_fill_layout(self, comp_index):
|
||
"""Apply fill layout to component: match parent/canvas size with margins."""
|
||
if comp_index < 0 or comp_index >= len(self.components):
|
||
return
|
||
|
||
comp_data = self.components[comp_index]
|
||
comp_obj = comp_data.get('object')
|
||
comp_type = comp_data.get('type', '')
|
||
|
||
parent_width = None
|
||
parent_height = None
|
||
|
||
parent_index = comp_data.get('parent_index')
|
||
if parent_index is not None and parent_index >= 0 and parent_index < len(self.components):
|
||
parent_data = self.components[parent_index]
|
||
parent_width = parent_data.get('width')
|
||
parent_height = parent_data.get('height')
|
||
parent_obj = parent_data.get('object')
|
||
if parent_width is None and parent_obj is not None and hasattr(parent_obj, 'width'):
|
||
parent_width = parent_obj.width
|
||
if parent_height is None and parent_obj is not None and hasattr(parent_obj, 'height'):
|
||
parent_height = parent_obj.height
|
||
else:
|
||
canvas_index = comp_data.get('canvas_index', self.current_canvas_index)
|
||
if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases):
|
||
canvas_panel = self.canvases[canvas_index].get('panel')
|
||
if canvas_panel is not None:
|
||
if hasattr(canvas_panel, 'width'):
|
||
parent_width = canvas_panel.width
|
||
elif hasattr(canvas_panel, 'get_width'):
|
||
parent_width = canvas_panel.get_width()
|
||
if hasattr(canvas_panel, 'height'):
|
||
parent_height = canvas_panel.height
|
||
elif hasattr(canvas_panel, 'get_height'):
|
||
parent_height = canvas_panel.get_height()
|
||
|
||
# DEBUG: Trace fill layout calculations
|
||
# print(f"[_apply_fill_layout] Comp #{comp_index} Parent #{parent_index}: PW={parent_width} PH={parent_height}")
|
||
|
||
if parent_width is None or parent_height is None:
|
||
# print(f"[_apply_fill_layout] Failed to get parent dimensions for Comp #{comp_index}")
|
||
return
|
||
|
||
margin_left = float(comp_data.get('fill_margin_left', 0.0))
|
||
margin_right = float(comp_data.get('fill_margin_right', 0.0))
|
||
margin_top = float(comp_data.get('fill_margin_top', 0.0))
|
||
margin_bottom = float(comp_data.get('fill_margin_bottom', 0.0))
|
||
|
||
new_left = margin_left
|
||
new_top = margin_top
|
||
new_width = float(parent_width) - (margin_left + margin_right)
|
||
new_height = float(parent_height) - (margin_top + margin_bottom)
|
||
|
||
if new_width < 1.0:
|
||
new_width = 1.0
|
||
if new_height < 1.0:
|
||
new_height = 1.0
|
||
|
||
comp_data['left'] = new_left
|
||
comp_data['top'] = new_top
|
||
comp_data['width'] = new_width
|
||
comp_data['height'] = new_height
|
||
|
||
if comp_obj is not None:
|
||
# Handle visual_parent_canvas case: convert local to absolute
|
||
abs_left = new_left
|
||
abs_top = new_top
|
||
|
||
if parent_index is not None and parent_index >= 0 and comp_data.get('visual_parent_canvas'):
|
||
p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index)
|
||
abs_left = p_abs_x + new_left
|
||
abs_top = p_abs_y + new_top
|
||
|
||
if hasattr(comp_obj, 'set_pos'):
|
||
comp_obj.set_pos(abs_left, abs_top)
|
||
else:
|
||
if hasattr(comp_obj, 'left'):
|
||
comp_obj.left = abs_left
|
||
if hasattr(comp_obj, 'top'):
|
||
comp_obj.top = abs_top
|
||
if hasattr(comp_obj, 'width'):
|
||
comp_obj.width = new_width
|
||
if hasattr(comp_obj, 'height'):
|
||
comp_obj.height = new_height
|
||
|
||
if comp_type in ['Plane', 'Image', 'Video'] and 'sprite' in comp_data:
|
||
comp_data['sprite'].width = new_width
|
||
comp_data['sprite'].height = new_height
|
||
elif comp_type == 'Button':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
if hasattr(comp_obj, 'set_height'):
|
||
comp_obj.set_height(new_height)
|
||
|
||
# Force update internal layout for Button to ensure fill works
|
||
try:
|
||
layout = getattr(comp_obj, '_layout', None)
|
||
if layout:
|
||
if hasattr(layout, 'width'): layout.width = new_width
|
||
if hasattr(layout, 'height'): layout.height = new_height
|
||
|
||
# Inner layout
|
||
inner = getattr(layout, '_layout', None)
|
||
if inner:
|
||
if hasattr(inner, 'width'): inner.width = new_width
|
||
if hasattr(inner, 'height'): inner.height = new_height
|
||
|
||
# Mid sprite usually stretches
|
||
spr_mid = getattr(layout, '_sprite_mid', None)
|
||
if spr_mid:
|
||
spr_mid.width = new_width
|
||
spr_mid.height = new_height
|
||
except Exception:
|
||
pass
|
||
elif comp_type == 'InputField':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
elif comp_type == 'Slider':
|
||
if hasattr(comp_obj, 'set_width'):
|
||
comp_obj.set_width(new_width)
|
||
|
||
|
||
def _apply_layout_spacing(self, layout_obj, spacing):
|
||
try:
|
||
if hasattr(layout_obj, 'set_spacing'):
|
||
layout_obj.set_spacing(spacing)
|
||
elif hasattr(layout_obj, 'setSpacing'):
|
||
layout_obj.setSpacing(spacing)
|
||
elif hasattr(layout_obj, 'spacing'):
|
||
layout_obj.spacing = spacing
|
||
elif hasattr(layout_obj, '_spacing'):
|
||
layout_obj._spacing = spacing
|
||
except Exception:
|
||
pass
|
||
|
||
def _apply_layout_alignment(self, layout_obj, align):
|
||
try:
|
||
if hasattr(layout_obj, 'set_alignment'):
|
||
layout_obj.set_alignment(align)
|
||
elif hasattr(layout_obj, 'alignment'):
|
||
layout_obj.alignment = align
|
||
elif hasattr(layout_obj, 'align'):
|
||
layout_obj.align = align
|
||
except Exception:
|
||
pass
|
||
|
||
def _update_layout_inner(self, comp_index):
|
||
if comp_index < 0 or comp_index >= len(self.components):
|
||
return
|
||
|
||
comp_data = self.components[comp_index]
|
||
layout_obj = comp_data.get('layout_obj')
|
||
if layout_obj is None:
|
||
return
|
||
|
||
width = float(comp_data.get('width', 0.0))
|
||
height = float(comp_data.get('height', 0.0))
|
||
|
||
pad_left = float(comp_data.get('layout_padding_left', 0.0))
|
||
pad_right = float(comp_data.get('layout_padding_right', 0.0))
|
||
pad_top = float(comp_data.get('layout_padding_top', 0.0))
|
||
pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0))
|
||
|
||
inner_w = max(1.0, width - (pad_left + pad_right))
|
||
inner_h = max(1.0, height - (pad_top + pad_bottom))
|
||
|
||
try:
|
||
if hasattr(layout_obj, 'left'):
|
||
layout_obj.left = pad_left
|
||
if hasattr(layout_obj, 'top'):
|
||
layout_obj.top = pad_top
|
||
if hasattr(layout_obj, 'width'):
|
||
layout_obj.width = inner_w
|
||
if hasattr(layout_obj, 'height'):
|
||
layout_obj.height = inner_h
|
||
if hasattr(layout_obj, 'set_size'):
|
||
layout_obj.set_size(inner_w, inner_h)
|
||
except Exception:
|
||
pass
|
||
|
||
spacing = float(comp_data.get('layout_spacing', 0.0))
|
||
self._apply_layout_spacing(layout_obj, spacing)
|
||
|
||
align = comp_data.get('layout_align', None)
|
||
if align:
|
||
self._apply_layout_alignment(layout_obj, align)
|
||
|
||
|
||
if comp_data.get('type') in ['HorizontalLayout', 'VerticalLayout']:
|
||
self._apply_wrap_layout(comp_index)
|
||
|
||
try:
|
||
if hasattr(layout_obj, 'update'):
|
||
layout_obj.update()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _reparent_scrollable_children(self, parent_index):
|
||
if parent_index < 0 or parent_index >= len(self.components):
|
||
return
|
||
parent_data = self.components[parent_index]
|
||
if parent_data.get('type') != 'ScrollableRegion':
|
||
return
|
||
parent_obj = parent_data['object']
|
||
try:
|
||
content_node = parent_obj.content_node
|
||
except Exception:
|
||
return
|
||
|
||
for child_idx in parent_data.get('children_indices', []):
|
||
if child_idx < 0 or child_idx >= len(self.components):
|
||
continue
|
||
child_data = self.components[child_idx]
|
||
child_obj = child_data.get('object')
|
||
if child_obj is None:
|
||
continue
|
||
try:
|
||
if hasattr(child_obj, 'reparent_to'):
|
||
child_obj.reparent_to(content_node)
|
||
elif hasattr(child_obj, 'parent'):
|
||
child_obj.parent = content_node
|
||
except Exception:
|
||
pass
|
||
|
||
def _apply_wrap_layout(self, parent_index):
|
||
if parent_index < 0 or parent_index >= len(self.components):
|
||
return
|
||
|
||
parent_data = self.components[parent_index]
|
||
layout_type = parent_data.get('type')
|
||
if layout_type not in ['HorizontalLayout', 'VerticalLayout']:
|
||
return
|
||
if not parent_data.get('layout_wrap', True):
|
||
return
|
||
|
||
# Get parent absolute position for coordinate conversion
|
||
parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index)
|
||
|
||
width = float(parent_data.get('width', 0.0))
|
||
height = float(parent_data.get('height', 0.0))
|
||
|
||
pad_left = float(parent_data.get('layout_padding_left', 0.0))
|
||
pad_right = float(parent_data.get('layout_padding_right', 0.0))
|
||
pad_top = float(parent_data.get('layout_padding_top', 0.0))
|
||
pad_bottom = float(parent_data.get('layout_padding_bottom', 0.0))
|
||
spacing = float(parent_data.get('layout_spacing', 0.0))
|
||
line_spacing = float(parent_data.get('layout_line_spacing', 0.0))
|
||
|
||
inner_w = max(1.0, width - (pad_left + pad_right))
|
||
inner_h = max(1.0, height - (pad_top + pad_bottom))
|
||
|
||
if layout_type == 'HorizontalLayout':
|
||
x = pad_left
|
||
y = pad_top
|
||
row_h = 0.0
|
||
|
||
for child_idx in parent_data.get('children_indices', []):
|
||
if child_idx < 0 or child_idx >= len(self.components):
|
||
continue
|
||
child = self.components[child_idx]
|
||
child_obj = child.get('object')
|
||
|
||
cw = float(child.get('width', 0.0))
|
||
ch = float(child.get('height', 0.0))
|
||
# Allow layout even if size is 0, but usually skip
|
||
# if cw <= 0 or ch <= 0: continue
|
||
|
||
if x > pad_left and (x + cw) > (pad_left + inner_w):
|
||
x = pad_left
|
||
y += row_h + line_spacing
|
||
row_h = 0.0
|
||
|
||
# Data stores relative position (to parent)
|
||
child['left'] = x
|
||
child['top'] = y
|
||
|
||
# Update visual object
|
||
try:
|
||
child_parent = getattr(child_obj, 'parent', None)
|
||
parent_obj = parent_data.get('object')
|
||
|
||
if child_parent is not None and child_parent == parent_obj:
|
||
# Child is parented to layout object: use relative coordinates
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = x
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = y
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(x, y)
|
||
else:
|
||
# Child is NOT parented to layout object (e.g. at root): use absolute coordinates
|
||
abs_x = parent_abs_left + x
|
||
abs_y = parent_abs_top + y
|
||
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = abs_x
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = abs_y
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(abs_x, abs_y)
|
||
except Exception:
|
||
pass
|
||
|
||
# Recursively update child layout/visuals
|
||
if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True):
|
||
self._apply_wrap_layout(child_idx)
|
||
elif child.get('children_indices'):
|
||
self._sync_canvas_children(child_idx)
|
||
|
||
row_h = max(row_h, ch)
|
||
x += cw + spacing
|
||
else:
|
||
# Vertical wrap: flow top->bottom, then next column
|
||
x = pad_left
|
||
y = pad_top
|
||
col_w = 0.0
|
||
|
||
for child_idx in parent_data.get('children_indices', []):
|
||
if child_idx < 0 or child_idx >= len(self.components):
|
||
continue
|
||
child = self.components[child_idx]
|
||
child_obj = child.get('object')
|
||
|
||
cw = float(child.get('width', 0.0))
|
||
ch = float(child.get('height', 0.0))
|
||
|
||
if y > pad_top and (y + ch) > (pad_top + inner_h):
|
||
y = pad_top
|
||
x += col_w + line_spacing
|
||
col_w = 0.0
|
||
|
||
child['left'] = x
|
||
child['top'] = y
|
||
|
||
try:
|
||
child_parent = getattr(child_obj, 'parent', None)
|
||
parent_obj = parent_data.get('object')
|
||
|
||
if child_parent is not None and child_parent == parent_obj:
|
||
# Child is parented to layout object: use relative coordinates
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = x
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = y
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(x, y)
|
||
else:
|
||
# Child is NOT parented to layout object (e.g. at root): use absolute coordinates
|
||
abs_x = parent_abs_left + x
|
||
abs_y = parent_abs_top + y
|
||
|
||
if hasattr(child_obj, 'left'):
|
||
child_obj.left = abs_x
|
||
if hasattr(child_obj, 'top'):
|
||
child_obj.top = abs_y
|
||
if hasattr(child_obj, 'set_pos'):
|
||
child_obj.set_pos(abs_x, abs_y)
|
||
except Exception:
|
||
pass
|
||
|
||
# If child is also a layout, recursively update it
|
||
if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True):
|
||
self._apply_wrap_layout(child_idx)
|
||
# If child is NOT a layout but has children (like a Frame with children), update their visual text
|
||
elif child.get('children_indices'):
|
||
self._sync_canvas_children(child_idx)
|
||
|
||
col_w = max(col_w, cw)
|
||
y += ch + spacing
|
||
|
||
def get_selected_component(self):
|
||
"""获取当前选中的组件"""
|
||
if self.selected_index >= 0 and self.selected_index < len(self.components):
|
||
return self.components[self.selected_index]
|
||
return None
|
||
|
||
def select_component(self, index):
|
||
"""选中指定索引的组件"""
|
||
if 0 <= index < len(self.components):
|
||
self.selected_index = index
|
||
print(f"✓ 选中组件 #{index}: {self.components[index]['type']}")
|
||
else:
|
||
self.selected_index = -1
|
||
|
||
def deselect_all(self):
|
||
"""取消选中所有组件"""
|
||
if getattr(self, '_force_keep_selection_frames', 0) > 0:
|
||
return
|
||
self.selected_index = -1
|
||
print("✓ 已取消选中所有组件")
|
||
|
||
def _change_image_texture(self, title = "选择文件",filetypes=None,initialdir=None):
|
||
"""更改图像组件的纹理"""
|
||
try:
|
||
import tkinter as tk
|
||
from tkinter import filedialog
|
||
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
root.attributes('-topmost',True)
|
||
|
||
if filetypes is None:
|
||
filetypes = [("所有文件","*.*")]
|
||
|
||
file_path = filedialog.askopenfilename(
|
||
title = title,
|
||
filetypes = filetypes,
|
||
initialdir = initialdir
|
||
)
|
||
root.destroy()
|
||
|
||
if file_path:
|
||
return file_path
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"更改纹理失败: {e}")
|
||
|
||
def _update_component_anchor_position(self, comp_index, anchor_position, manual_offset=(0.0, 0.0)):
|
||
"""更新组件的锚点位置"""
|
||
if comp_index < 0 or comp_index >= len(self.components):
|
||
return
|
||
|
||
|
||
|
||
comp_data = self.components[comp_index]
|
||
comp_obj = comp_data['object']
|
||
|
||
# Update data
|
||
comp_data['anchor_position'] = anchor_position
|
||
comp_data['anchored_to_parent'] = True
|
||
comp_data['anchor_manual_offset_x'] = manual_offset[0]
|
||
comp_data['anchor_manual_offset_y'] = manual_offset[1]
|
||
|
||
# Determine strict parent reference
|
||
parent_index = comp_data.get('parent_index')
|
||
parent_width = 0
|
||
parent_height = 0
|
||
|
||
if parent_index is not None and parent_index >= 0:
|
||
parent_data = self.components[parent_index]
|
||
parent_width = parent_data.get('width', 100)
|
||
parent_height = parent_data.get('height', 30)
|
||
# Try to get actual object size if available
|
||
p_obj = parent_data.get('object')
|
||
if p_obj:
|
||
if hasattr(p_obj, 'width') and p_obj.width > 0: parent_width = p_obj.width
|
||
if hasattr(p_obj, 'height') and p_obj.height > 0: parent_height = p_obj.height
|
||
elif self.current_canvas_index >= 0:
|
||
# Canvas anchor
|
||
canvas = self.canvases[self.current_canvas_index]['panel']
|
||
if hasattr(canvas, 'get_width'):
|
||
parent_width = canvas.get_width()
|
||
parent_height = canvas.get_height()
|
||
else:
|
||
parent_width = canvas.width
|
||
parent_height = canvas.height
|
||
|
||
# Calculate anchor point in parent local space
|
||
anchor_ratios = {
|
||
'top-left': (0, 0), 'top-center': (0.5, 0), 'top-right': (1, 0),
|
||
'middle-left': (0, 0.5), 'center': (0.5, 0.5), 'middle-right': (1, 0.5),
|
||
'bottom-left': (0, 1), 'bottom-center': (0.5, 1), 'bottom-right': (1, 1)
|
||
}
|
||
rx, ry = anchor_ratios.get(anchor_position, (0, 0))
|
||
|
||
anchor_x = parent_width * rx
|
||
anchor_y = parent_height * ry
|
||
|
||
child_width = comp_data.get('width', 0)
|
||
child_height = comp_data.get('height', 0)
|
||
# Try to get actual object size
|
||
if hasattr(comp_obj, 'width') and comp_obj.width > 0: child_width = comp_obj.width
|
||
if hasattr(comp_obj, 'height') and comp_obj.height > 0: child_height = comp_obj.height
|
||
|
||
# Center child on anchor (pivot logic)
|
||
child_left = anchor_x - (child_width * rx) + manual_offset[0]
|
||
child_top = anchor_y - (child_height * ry) + manual_offset[1]
|
||
|
||
# Update logical position data
|
||
comp_data['left'] = child_left
|
||
comp_data['top'] = child_top
|
||
|
||
# Update visual position
|
||
if parent_index is not None and parent_index >= 0:
|
||
# Check if visually parented to canvas (absolute positioning needed)
|
||
if comp_data.get('visual_parent_canvas'):
|
||
p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index)
|
||
abs_left = p_abs_x + child_left
|
||
abs_top = p_abs_y + child_top
|
||
|
||
# Prefer left/top properties for LUI objects
|
||
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
|
||
comp_obj.left = abs_left
|
||
comp_obj.top = abs_top
|
||
elif hasattr(comp_obj, 'set_pos'):
|
||
try:
|
||
comp_obj.set_pos(abs_left, abs_top)
|
||
except TypeError:
|
||
# Fallback for NodePath-like objects requiring 3 args
|
||
comp_obj.set_pos(abs_left, abs_top, 0)
|
||
else:
|
||
# Standard local parenting
|
||
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
|
||
comp_obj.left = child_left
|
||
comp_obj.top = child_top
|
||
elif hasattr(comp_obj, 'set_pos'):
|
||
try:
|
||
comp_obj.set_pos(child_left, child_top)
|
||
except TypeError:
|
||
comp_obj.set_pos(child_left, child_top, 0)
|
||
else:
|
||
# Canvas root
|
||
if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'):
|
||
comp_obj.left = child_left
|
||
comp_obj.top = child_top
|
||
elif hasattr(comp_obj, 'set_pos'):
|
||
try:
|
||
comp_obj.set_pos(child_left, child_top)
|
||
except TypeError:
|
||
comp_obj.set_pos(child_left, child_top, 0)
|
||
|
||
print(f"✓ 已更新锚点位置: {anchor_position} -> ({child_left:.1f}, {child_top:.1f})")
|
||
|
||
# Update children positions (for visual_parent_canvas or anchored children)
|
||
self._sync_canvas_children(comp_index)
|
||
self._update_anchored_children(comp_index)
|
||
|
||
def _handle_anchor_popup(self):
|
||
"""Handle component anchor selection popup"""
|
||
if imgui.begin_popup("选择锚点位置"):
|
||
imgui.text("选择锚点位置:")
|
||
|
||
anchor_names = {
|
||
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
|
||
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
|
||
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
|
||
}
|
||
|
||
anchor_positions = [
|
||
['top-left', 'top-center', 'top-right'],
|
||
['middle-left', 'center', 'middle-right'],
|
||
['bottom-left', 'bottom-center', 'bottom-right']
|
||
]
|
||
|
||
for row in anchor_positions:
|
||
for i, pos in enumerate(row):
|
||
button_text = anchor_names.get(pos, pos)
|
||
if imgui.button(f"{button_text}##popup_{pos}", (50, 25)):
|
||
if hasattr(self, '_temp_selected_index_for_anchor'):
|
||
selected_idx = self._temp_selected_index_for_anchor
|
||
# Reset manual offset by passing (0,0) explicitly to match original behavior logic
|
||
self._update_component_anchor_position(selected_idx, pos, manual_offset=(0.0, 0.0))
|
||
|
||
# Clean up temp attribute
|
||
delattr(self, '_temp_selected_index_for_anchor')
|
||
|
||
# Reset flag if used elsewhere
|
||
self.anchor_popup_open = False
|
||
imgui.close_current_popup()
|
||
|
||
if i < len(row) - 1:
|
||
imgui.same_line()
|
||
|
||
imgui.spacing()
|
||
if imgui.button("取消", (160, 25)):
|
||
self.anchor_popup_open = False
|
||
if hasattr(self, '_temp_selected_index_for_anchor'):
|
||
delattr(self, '_temp_selected_index_for_anchor')
|
||
imgui.close_current_popup()
|
||
|
||
imgui.end_popup()
|