EG/ui/LUI/lui_manager_interaction.py

1564 lines
64 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""LUIManager interaction and resize mixin."""
import struct
from pathlib import Path
import panda3d.core as p3d
from imgui_bundle import imgui, imgui_ctx
class LUIManagerInteractionMixin:
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 _clear_pending_drag_state(self):
"""清空 pending 拖拽状态。"""
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
def _has_mouse_for_drag(self):
"""检查是否存在可用鼠标输入。"""
return hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse()
def _is_left_mouse_down(self):
"""检查鼠标左键是否按下。"""
import panda3d.core as p3d
return self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one())
def _get_mouse_pixel_data(self):
"""获取鼠标像素坐标及窗口尺寸。"""
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
return pixel_x, pixel_y, win_x, win_y
def _find_canvas_data_by_panel(self, canvas_panel):
"""按 panel 对象查找 canvas 配置。"""
for c in self.canvases:
if c['panel'] == canvas_panel:
return c
return None
def _process_canvas_drag(self):
"""处理 Canvas 拖动,返回是否已处理该帧。"""
if not (hasattr(self, 'dragging_canvas') and self.dragging_canvas):
return False
canvas_data = self._find_canvas_data_by_panel(self.dragging_canvas)
if canvas_data and canvas_data.get('fixed', False):
return True
if not self._has_mouse_for_drag():
return True
if not self._is_left_mouse_down():
self.dragging_canvas = None
return True
pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data()
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 True
def _try_start_pending_component_drag(self):
"""处理 pending 拖拽转正,返回是否需要本帧提前结束。"""
if self.dragging_comp is not None:
return False
if self.pending_drag_comp is None:
return True
if not self._has_mouse_for_drag():
return True
if not self._is_left_mouse_down():
# click without drag
self._clear_pending_drag_state()
return True
pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data()
if self.pending_drag_start_mouse is None:
self.pending_drag_start_mouse = (pixel_x, pixel_y)
return True
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 True
# 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 Exception:
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)
self._clear_pending_drag_state()
return False
def _finish_component_drag_if_released(self):
"""在鼠标释放时完成拖拽收尾,返回是否已处理该帧。"""
if not self._is_left_mouse_down():
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 True
return False
def _resolve_canvas_metrics_for_drag(self, win_x, win_y):
"""获取当前 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 Exception:
# 如果获取失败,使用默认值
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)
return canvas_abs_left, canvas_abs_top, canvas_width, canvas_height
def _update_active_component_drag(self, pixel_x, pixel_y, win_x, win_y):
"""更新已开始拖拽的组件位置。"""
# 计算新的绝对位置(减去偏移量)
new_abs_left = pixel_x - self.drag_offset[0]
new_abs_top = pixel_y - self.drag_offset[1]
# 获取Canvas信息
canvas_abs_left, canvas_abs_top, canvas_width, canvas_height = self._resolve_canvas_metrics_for_drag(win_x, win_y)
# 转换为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)
# 如果有父组件,需要转换为相对于父组件的局部坐标
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相对坐标
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)
def _update_drag(self, task):
"""每帧更新拖动状态 - 支持Canvas边界约束和局部坐标"""
if getattr(self, 'play_mode', False):
# Disable editor drag logic while in play mode
return task.cont
if self._process_canvas_drag():
return task.cont
if self._try_start_pending_component_drag():
return task.cont
if not self._has_mouse_for_drag():
return task.cont
if self._finish_component_drag_if_released():
return task.cont
pixel_x, pixel_y, win_x, win_y = self._get_mouse_pixel_data()
self._update_active_component_drag(pixel_x, pixel_y, win_x, win_y)
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', 'HttpText']:
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 _update_http_components_task(self, task):
"""Drive async updates for HttpText components."""
if not self.lui_enabled:
return task.cont
try:
self.luiFunction.update_http_components(self)
except Exception:
pass
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 拖拽(编排入口)。"""
self._handle_resize_drag_core()
def _handle_resize_drag_core(self):
"""处理resize拖拽 - 完整功能实现支持Shift和Alt键操作"""
if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse():
return
import panda3d.core as p3d
if self._finish_resize_drag_if_released(p3d):
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']
new_left, new_top, new_width, new_height = self._compute_resize_drag_bounds(
delta_x, delta_y, shift_held, alt_held
)
if self.selected_index < 0:
return
comp_data = self.components[self.selected_index]
self._apply_resize_drag_updates(
comp_data, comp_obj, comp_type,
new_left, new_top, new_width, new_height,
shift_held, alt_held,
)
def _finish_resize_drag_if_released(self, p3d):
"""鼠标释放时结束缩放并执行收尾同步。"""
if self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()):
return False
if self.resizing_handle and self.selected_index >= 0 and self.selected_index < len(self.components):
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 True
def _compute_resize_drag_bounds(self, delta_x, delta_y, shift_held, alt_held):
"""根据当前手柄/修饰键计算新的 left/top/width/height。"""
start_left, start_top, start_width, start_height, aspect_ratio = self._get_resize_start_values()
handle_pos = self.resizing_handle._resize_position
new_left, new_top, new_width, new_height = self._apply_resize_handle_delta(
handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y
)
new_left, new_top, new_width, new_height = self._apply_resize_min_size_clamp(
handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
)
if shift_held:
new_left, new_top, new_width, new_height = self._apply_shift_keep_ratio_resize(
handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height, aspect_ratio,
)
if alt_held:
new_left, new_top, new_width, new_height = self._apply_alt_center_resize(
start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
)
self._cache_resize_canvas_bounds()
return self._sanitize_resize_dimensions(new_left, new_top, new_width, new_height)
def _apply_resize_drag_updates(
self, comp_data, comp_obj, comp_type,
new_left, new_top, new_width, new_height,
shift_held, alt_held,
):
"""将缩放结果写回组件数据与渲染对象。"""
parent_index, parent_offset_x, parent_offset_y, is_scene_parented = self._resolve_resize_parent_context(
comp_data, comp_obj
)
_ = parent_index
self._write_resize_component_data(comp_data, new_left, new_top, new_width, new_height)
self._apply_resize_component_position(
comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented
)
try:
self._apply_resize_component_size_by_type(comp_data, comp_obj, comp_type, new_width, new_height)
self._post_resize_component_sync(comp_type, shift_held, alt_held)
except Exception as e:
print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}")
def _get_resize_start_values(self):
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
return start_left, start_top, start_width, start_height, aspect_ratio
def _apply_resize_handle_delta(
self, handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y
):
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
return new_left, new_top, new_width, new_height
def _apply_resize_min_size_clamp(
self, handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
):
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
return new_left, new_top, new_width, new_height
def _apply_shift_keep_ratio_resize(
self, handle_pos, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height, aspect_ratio,
):
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
return new_left, new_top, new_width, new_height
def _apply_alt_center_resize(
self, start_left, start_top, start_width, start_height,
new_left, new_top, new_width, new_height,
):
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
return new_left, new_top, new_width, new_height
def _cache_resize_canvas_bounds(self):
canvas_width = 800
canvas_height = 600
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()
_ = (canvas_width, canvas_height)
def _sanitize_resize_dimensions(self, new_left, new_top, new_width, new_height):
if new_width < 1.0:
new_width = 1.0
if new_height < 1.0:
new_height = 1.0
return new_left, new_top, new_width, new_height
def _resolve_resize_parent_context(self, comp_data, comp_obj):
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)
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')
is_scene_parented = (child_parent is not None and child_parent == parent_obj)
return parent_index, parent_offset_x, parent_offset_y, is_scene_parented
def _write_resize_component_data(self, comp_data, new_left, new_top, new_width, new_height):
comp_data['left'] = new_left
comp_data['top'] = new_top
comp_data['width'] = new_width
comp_data['height'] = new_height
def _apply_resize_component_position(
self, comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented
):
if is_scene_parented:
comp_obj.left = new_left
comp_obj.top = new_top
else:
comp_obj.left = parent_offset_x + new_left
comp_obj.top = parent_offset_y + new_top
def _apply_resize_component_size_by_type(self, comp_data, comp_obj, comp_type, new_width, new_height):
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', 'HttpText']:
sprite = comp_data.get('sprite')
if sprite is not None:
sprite.width = new_width
sprite.height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
elif comp_type == 'Button':
self._apply_button_resize_size(comp_obj, new_width, new_height)
elif comp_type == 'InputField':
self._apply_input_field_resize_size(comp_obj, new_width, new_height)
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)
def _apply_button_resize_size(self, comp_obj, new_width, new_height):
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()
def _apply_input_field_resize_size(self, comp_obj, new_width, new_height):
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
self._sync_input_field_layout_stretch(comp_obj)
def _sync_input_field_layout_stretch(self, comp_obj):
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
def _post_resize_component_sync(self, comp_type, shift_held, alt_held):
if comp_type in ['VerticalLayout', 'HorizontalLayout']:
self._update_layout_inner(self.selected_index)
self._update_anchored_children(self.selected_index)
modifier_str = self._get_resize_modifier_info(shift_held, alt_held)
_ = modifier_str
def _get_resize_modifier_info(self, shift_held, alt_held):
modifier_info = []
if shift_held:
modifier_info.append('Shift(keep ratio)')
if alt_held:
modifier_info.append('Alt(center scale)')
return ' + '.join(modifier_info) if modifier_info else 'Normal'