EG/ui/LUI/lui_manager_editor.py
2026-02-27 16:52:00 +08:00

1725 lines
79 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 tree/editor/layout mixin."""
import struct
from pathlib import Path
import panda3d.core as p3d
from imgui_bundle import imgui, imgui_ctx
class LUIManagerEditorMixin:
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', 'HttpText',
'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', 'HttpText']:
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', 'HttpText'] 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),
'HttpText': (320, 120),
'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 == 'HttpText':
child_obj = self.luiFunction.create_http_text(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)", "HTTP文本 (HttpText)",
"进度条 (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', 'HttpText', 'Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout']
cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频', 'HTTP文本', '进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组']
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: (320, 120), # HttpText
10: (200, 30), # Progressbar
11: (200, 30), # Selectbox
12: (200, 200), # ScrollableRegion
13: (300, 200), # TabbedFrame
14: (300, 200), # VerticalLayout
15: (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: # HttpText
self.luiFunction.create_http_text(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 10: # Progressbar
self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 11: # Selectbox
self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 12: # ScrollableRegion
self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 13: # TabbedFrame
self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 14: # VerticalLayout
self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y)
elif type_index == 15: # 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', 'HttpText'] and 'sprite' in comp_data:
comp_data['sprite'].width = new_width
comp_data['sprite'].height = new_height
if comp_type == 'HttpText':
self.luiFunction.sync_http_text_layout(self, comp_data)
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()