EG/ui/LUI/lui_function_properties.py

1779 lines
81 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.

"""luiFunction component property drawing mixin."""
import os
from pathlib import Path
import panda3d.core as p3d
from imgui_bundle import imgui, imgui_ctx
class LUIFunctionPropertiesMixin:
def delete_component(manager, index):
"""Delete a LUI component and update all references"""
if index < 0 or index >= len(manager.components):
return
# 1. Identify all components to delete (recursive)
to_delete = {index}
queue = list(manager.components[index].get('children_indices', []))
while queue:
curr = queue.pop(0)
if curr not in to_delete:
to_delete.add(curr)
if curr < len(manager.components):
queue.extend(manager.components[curr].get('children_indices', []))
# Cleanup any cached outlines for deleted components
if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict):
for del_idx in list(to_delete):
borders = manager.debug_outlines.pop(del_idx, None)
if borders:
for b in borders:
try:
if hasattr(b, 'remove'):
b.remove()
else:
b.visible = False
except Exception:
pass
# Sort indices descending to delete from end first
sorted_indices = sorted(list(to_delete), reverse=True)
# Clear selection if affected
if manager.selected_index in to_delete:
manager.selected_index = -1
if hasattr(manager, '_hide_resize_handles'):
manager._hide_resize_handles()
# Process deletions
for i in sorted_indices:
if i >= len(manager.components): continue
deleted_comp = manager.components.pop(i)
# Cleanup physical objects
if 'object' in deleted_comp:
obj = deleted_comp['object']
if hasattr(obj, 'parent') and obj.parent:
if hasattr(obj.parent, 'remove_child'):
obj.parent.remove_child(obj)
# Cleanup audio if any
audio = deleted_comp.get("audio")
if audio:
try:
if hasattr(audio, "stop"): audio.stop()
except Exception:
pass
# Cleanup associated node handles if any
if hasattr(manager, 'resize_handles') and manager.resizing_handle and getattr(manager.resizing_handle, '_resize_index', -1) == i:
manager.resizing_handle = None
print(f"✓ Deleted LUI component: {deleted_comp.get('name', 'Unknown')}")
# Shift indices > i down by 1
if manager.selected_index > i:
manager.selected_index -= 1
for c in manager.components:
# Update parent_index
p_idx = c.get('parent_index')
if p_idx is not None and p_idx > i:
c['parent_index'] = p_idx - 1
elif p_idx == i:
c['parent_index'] = -1
# Update children_indices
if 'children_indices' in c:
new_children = []
for child_idx in c['children_indices']:
if child_idx == i: continue
elif child_idx > i: new_children.append(child_idx - 1)
else: new_children.append(child_idx)
c['children_indices'] = new_children
# Reindex remaining outlines after component indices shift
if hasattr(manager, 'debug_outlines') and isinstance(manager.debug_outlines, dict) and manager.debug_outlines:
deleted_sorted = sorted(to_delete)
new_outlines = {}
for old_idx, borders in manager.debug_outlines.items():
shift = 0
for d in deleted_sorted:
if d < old_idx:
shift += 1
else:
break
new_outlines[old_idx - shift] = borders
manager.debug_outlines = new_outlines
def _load_ui_texture(manager, file_path, name_hint="", max_size=1024):
"""Load image as a standalone Panda3D Texture (avoid LUI atlas)."""
try:
panda_path = p3d.Filename.from_os_specific(file_path)
pnm = p3d.PNMImage()
if not pnm.read(panda_path):
print(f"⚠ Failed to read image: {file_path}")
return None
# Ensure alpha exists to avoid unexpected black/transparent results.
if not pnm.hasAlpha():
pnm.addAlpha()
pnm.alphaFill(1.0)
orig_x = pnm.getXSize()
orig_y = pnm.getYSize()
# Power-of-two resize for better compatibility and to avoid atlas issues.
def next_pow2(v):
v = max(1, int(v))
return 1 << (v - 1).bit_length()
pot_x = min(next_pow2(orig_x), max_size)
pot_y = min(next_pow2(orig_y), max_size)
if pot_x != orig_x or pot_y != orig_y:
pnm_scaled = p3d.PNMImage(pot_x, pot_y, pnm.getNumChannels())
pnm_scaled.quickFilterFrom(pnm)
pnm = pnm_scaled
print(f"⚠ Resized to POT: {orig_x}x{orig_y} -> {pot_x}x{pot_y}")
tex = p3d.Texture()
safe_name = f"{name_hint}_{os.path.basename(file_path)}" if name_hint else os.path.basename(file_path)
tex.setName(safe_name)
tex.load(pnm)
tex.setKeepRamImage(True)
tex.setMinfilter(p3d.Texture.FT_linear)
tex.setMagfilter(p3d.Texture.FT_linear)
tex.setWrapU(p3d.Texture.WM_clamp)
tex.setWrapV(p3d.Texture.WM_clamp)
tex.setCompression(p3d.Texture.CM_off)
tex.setQualityLevel(p3d.Texture.QL_best)
return tex
except Exception as e:
print(f"⚠ Texture load failed: {e}")
try:
panda_path = p3d.Filename.from_os_specific(file_path)
return manager.world.loader.loadTexture(panda_path)
except Exception as e2:
print(f"⚠ Texture fallback load failed: {e2}")
return None
def _get_image_atlas_page(manager, size=2048):
"""Get or create an atlas page for UI images."""
if not hasattr(manager, "_image_atlas_pages"):
manager._image_atlas_pages = []
# Try existing pages first
for page in manager._image_atlas_pages:
if page.get("size") == size and not page.get("full", False):
return page
# Create a new page
pnm = p3d.PNMImage(size, size, 4)
pnm.fill(0, 0, 0)
pnm.alphaFill(0.0)
tex = p3d.Texture()
tex.setName(f"ui_atlas_{len(manager._image_atlas_pages)}_{size}")
tex.load(pnm)
tex.setKeepRamImage(True)
tex.setMinfilter(p3d.Texture.FT_linear)
tex.setMagfilter(p3d.Texture.FT_linear)
tex.setWrapU(p3d.Texture.WM_clamp)
tex.setWrapV(p3d.Texture.WM_clamp)
tex.setCompression(p3d.Texture.CM_off)
tex.setQualityLevel(p3d.Texture.QL_best)
page = {
"size": size,
"pnm": pnm,
"tex": tex,
"cursor_x": 0,
"cursor_y": 0,
"row_h": 0,
"full": False,
}
manager._image_atlas_pages.append(page)
return page
def _add_image_to_atlas(manager, file_path, atlas_size=2048):
"""Add image to atlas and return (tex, u0, v0, u1, v1, w, h, orig_w, orig_h)."""
try:
panda_path = p3d.Filename.from_os_specific(file_path)
src = p3d.PNMImage()
if not src.read(panda_path):
print(f"⚠ Failed to read image: {file_path}")
return None
if not src.hasAlpha():
src.addAlpha()
src.alphaFill(1.0)
w = src.getXSize()
h = src.getYSize()
orig_w = w
orig_h = h
# Downscale if too large for atlas
if w > atlas_size or h > atlas_size:
scale = min(atlas_size / max(1, w), atlas_size / max(1, h))
new_w = max(1, int(w * scale))
new_h = max(1, int(h * scale))
scaled = p3d.PNMImage(new_w, new_h, src.getNumChannels())
scaled.quickFilterFrom(src)
src = scaled
w, h = new_w, new_h
print(f"⚠ Downscaled for atlas: {file_path} -> {w}x{h}")
page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size)
# Shelf packing
if page["cursor_x"] + w > atlas_size:
page["cursor_x"] = 0
page["cursor_y"] += page["row_h"]
page["row_h"] = 0
if page["cursor_y"] + h > atlas_size:
page["full"] = True
# Create a new page and try again
page = manager.luiFunction._get_image_atlas_page(manager, size=atlas_size)
if page["cursor_x"] + w > atlas_size or page["cursor_y"] + h > atlas_size:
print(f"⚠ Atlas full for image: {file_path}")
return None
x = page["cursor_x"]
y = page["cursor_y"]
page["pnm"].copySubImage(src, x, y, 0, 0, w, h)
page["tex"].load(page["pnm"])
page["cursor_x"] += w
page["row_h"] = max(page["row_h"], h)
size = float(atlas_size)
u0 = x / size
v0 = y / size
u1 = (x + w) / size
v1 = (y + h) / size
return (page["tex"], u0, v0, u1, v1, w, h, orig_w, orig_h)
except Exception as e:
print(f"⚠ Atlas insert failed: {e}")
return None
def _draw_component_properties(manager, index):
"""Orchestrator entry for component property rendering."""
return manager.luiFunction._draw_component_properties_core(manager, index)
def _draw_component_properties_core(manager, index):
"""绘制组件属性编辑面板"""
if index < 0 or index >= len(manager.components):
return
# Keep async HTTP results synchronized even when not in dedicated task loop.
if hasattr(manager.luiFunction, 'update_http_components'):
try:
manager.luiFunction.update_http_components(manager)
except Exception:
pass
comp_data = manager.components[index]
comp_obj = comp_data['object']
comp_type = comp_data['type']
widget = comp_data.get('widget',comp_obj)
# 文本属性
# Text content
# Text content
manager.luiFunction._draw_text_and_button_properties(
manager, comp_data, comp_obj, comp_type
)
manager.luiFunction._draw_layout_transform_properties(
manager, index, comp_data, comp_obj, comp_type
)
manager.luiFunction._draw_type_specific_properties(
manager, index, comp_data, comp_obj, comp_type
)
manager.luiFunction._draw_anchor_and_hierarchy_properties(
manager, index, comp_data, comp_obj
)
def _draw_text_and_button_properties(manager, comp_data, comp_obj, comp_type):
if comp_type not in ['Button', 'Text', 'Label']:
return
# 编排入口:文本编辑/字号/颜色与按钮贴图拆分,降低单函数复杂度。
manager.luiFunction._draw_text_content_editor(comp_data, comp_obj)
if comp_type in ['Text', 'Label']:
manager.luiFunction._draw_text_font_size_editor(comp_data, comp_obj)
draw_color = manager.luiFunction._draw_text_color_editor(comp_data)
manager.luiFunction._sync_text_or_button_color(comp_obj, comp_type, draw_color)
if comp_type == 'Button':
manager.luiFunction._draw_button_texture_controls(manager, comp_data, comp_obj)
def _draw_text_content_editor(manager, comp_data, comp_obj):
imgui.text("Text Content")
imgui.separator()
if 'text' not in comp_data:
return
imgui.push_item_width(-1)
changed, new_text = imgui.input_text("##text_input", comp_data['text'], 256)
imgui.pop_item_width()
if changed:
comp_data['text'] = new_text
comp_obj.text = new_text
print(f"Text updated: {new_text}")
def _draw_text_font_size_editor(manager, comp_data, comp_obj):
imgui.spacing()
imgui.text("Font Size")
font_size = int(comp_data.get('font_size', 14))
changed, new_size = imgui.slider_int("##font_size", font_size, 8, 96)
if not changed:
return
comp_data['font_size'] = int(new_size)
try:
if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'):
comp_obj._text.font_size = int(new_size)
if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'):
comp_obj._shadow_text.font_size = int(new_size)
if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'):
comp_obj.text_handle.font_size = int(new_size)
except Exception as e:
print(f"Font size update failed: {e}")
def _draw_text_color_editor(manager, comp_data):
imgui.spacing()
imgui.text("Color")
color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("##color_edit", color)
draw_color = tuple(new_color)
if changed:
comp_data['color'] = draw_color
imgui.separator()
return draw_color
def _sync_text_or_button_color(manager, comp_obj, comp_type, draw_color):
# 保留原行为:每帧同步当前编辑色,避免按钮状态贴图切换后颜色不同步。
if comp_type == 'Button':
manager.luiFunction._apply_button_layout_color(comp_obj, draw_color)
else:
manager.luiFunction._apply_text_color(comp_obj, draw_color)
imgui.separator()
imgui.spacing()
imgui.text("颜色")
def _apply_button_layout_color(manager, comp_obj, color_value):
if not hasattr(comp_obj, '_layout'):
return
try:
if hasattr(comp_obj._layout, '_sprite_left'):
comp_obj._layout._sprite_left.color = color_value
if hasattr(comp_obj._layout, '_sprite_mid'):
comp_obj._layout._sprite_mid.color = color_value
if hasattr(comp_obj._layout, '_sprite_right'):
comp_obj._layout._sprite_right.color = color_value
except Exception as e:
print(f"Button color set failed: {e}")
def _apply_text_color(manager, comp_obj, color_value):
if hasattr(comp_obj, '_text'):
comp_obj._text.color = color_value
def _draw_button_texture_controls(manager, comp_data, comp_obj):
imgui.text("按钮图片")
manager.luiFunction._draw_button_texture_pick_button(
manager, comp_data, comp_obj, "更改默认图##button_img_norm", "normal"
)
manager.luiFunction._draw_button_texture_pick_button(
manager, comp_data, comp_obj, "更改悬停图##button_img_hover", "hover"
)
manager.luiFunction._draw_button_texture_pick_button(
manager, comp_data, comp_obj, "更改按下图##button_img_pressed", "pressed"
)
if imgui.button("使用默认图尺寸##button_fit", (160, 20)):
manager.luiFunction._fit_button_to_default_texture_size(comp_data, comp_obj)
if imgui.button("恢复默认按钮##button_default", (160, 20)):
manager.luiFunction._clear_button_custom_textures(comp_data, comp_obj)
manager.luiFunction._draw_button_texture_path_labels(comp_data)
def _draw_button_texture_pick_button(manager, comp_data, comp_obj, button_label, variant):
if not imgui.button(button_label, (160, 20)):
return
selected_path = manager._change_image_texture(
title="选择图片文件",
filetypes=[
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("所有文件", "*.*"),
],
)
if not selected_path:
return
try:
manager.luiFunction._apply_button_texture_variant(
manager, comp_data, comp_obj, selected_path, variant
)
except Exception as e:
print(f"Button {variant} texture set failed: {e}")
def _apply_button_texture_variant(manager, comp_data, comp_obj, selected_path, variant):
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if not atlas_result:
return
tex, u0, v0, u1, v1, w, h = manager.luiFunction._unpack_button_atlas_result(atlas_result)
if variant == "normal":
comp_data['button_texture_path'] = selected_path
comp_data['button_texture_path_normal'] = selected_path
comp_data['button_atlas_uv'] = (u0, v0, u1, v1)
comp_data['button_atlas_uv_normal'] = (u0, v0, u1, v1)
comp_data['button_atlas_tex'] = tex
comp_data['button_texture_ref'] = tex
comp_data['button_texture_ref_normal'] = tex
comp_data['button_image_size'] = (int(w), int(h))
comp_data['button_image_size_normal'] = (int(w), int(h))
elif variant == "hover":
comp_data['button_texture_path_hover'] = selected_path
comp_data['button_atlas_uv_hover'] = (u0, v0, u1, v1)
comp_data['button_texture_ref_hover'] = tex
comp_data['button_image_size_hover'] = (int(w), int(h))
elif variant == "pressed":
comp_data['button_texture_path_pressed'] = selected_path
comp_data['button_atlas_uv_pressed'] = (u0, v0, u1, v1)
comp_data['button_texture_ref_pressed'] = tex
comp_data['button_image_size_pressed'] = (int(w), int(h))
manager.luiFunction._apply_button_custom_textures(comp_data, comp_obj)
def _unpack_button_atlas_result(manager, atlas_result):
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
return tex, u0, v0, u1, v1, w, h
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
return tex, u0, v0, u1, v1, w, h
def _apply_button_custom_textures(manager, comp_data, comp_obj):
normal_tex = comp_data.get('button_texture_ref_normal') or comp_data.get('button_texture_ref')
normal_uv = comp_data.get('button_atlas_uv_normal') or comp_data.get('button_atlas_uv')
hover_tex = comp_data.get('button_texture_ref_hover')
hover_uv = comp_data.get('button_atlas_uv_hover')
pressed_tex = comp_data.get('button_texture_ref_pressed')
pressed_uv = comp_data.get('button_atlas_uv_pressed')
if hasattr(comp_obj, 'set_custom_textures') and normal_tex is not None:
comp_obj.set_custom_textures(normal_tex, normal_uv, hover_tex, hover_uv, pressed_tex, pressed_uv)
elif hasattr(comp_obj, 'set_custom_texture') and normal_tex is not None:
comp_obj.set_custom_texture(normal_tex, normal_uv)
def _fit_button_to_default_texture_size(manager, comp_data, comp_obj):
w = h = None
if comp_data.get('button_image_size_normal'):
try:
w, h = comp_data['button_image_size_normal']
except Exception:
w = h = None
elif comp_data.get('button_image_size'):
try:
w, h = comp_data['button_image_size']
except Exception:
w = h = None
if w is None or h is None or w <= 0 or h <= 0:
return
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
if hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
def _clear_button_custom_textures(manager, comp_data, comp_obj):
if hasattr(comp_obj, 'clear_custom_texture'):
comp_obj.clear_custom_texture()
for key in [
'button_texture_path', 'button_texture_path_normal', 'button_texture_path_hover',
'button_texture_path_pressed', 'button_texture_ref', 'button_texture_ref_normal',
'button_texture_ref_hover', 'button_texture_ref_pressed', 'button_atlas_uv',
'button_atlas_uv_normal', 'button_atlas_uv_hover', 'button_atlas_uv_pressed',
'button_image_size', 'button_image_size_normal', 'button_image_size_hover', 'button_image_size_pressed',
]:
if key in comp_data:
comp_data[key] = None
def _draw_button_texture_path_labels(manager, comp_data):
if comp_data.get('button_texture_path_normal'):
imgui.text(f"默认图: {comp_data['button_texture_path_normal']}")
if comp_data.get('button_texture_path_hover'):
imgui.text(f"悬停图: {comp_data['button_texture_path_hover']}")
if comp_data.get('button_texture_path_pressed'):
imgui.text(f"按下图: {comp_data['button_texture_path_pressed']}")
def _draw_layout_transform_properties(manager, index, comp_data, comp_obj, comp_type):
# 编排入口:布局模式/坐标尺寸/布局组/Z-Offset 采用分步函数,降低维护成本。
is_fill = manager.luiFunction._draw_fill_layout_controls(manager, index, comp_data)
imgui.separator()
manager.luiFunction._draw_position_controls(manager, comp_data, comp_obj, is_fill)
manager.luiFunction._draw_size_controls(
manager, index, comp_data, comp_obj, comp_type, is_fill
)
manager.luiFunction._draw_layout_group_controls(manager, index, comp_data, comp_type)
manager.luiFunction._draw_z_offset_controls(manager, index, comp_data, comp_obj)
def _draw_fill_layout_controls(manager, index, comp_data):
layout_mode = comp_data.get('layout_mode', 'manual')
is_fill = (layout_mode == 'fill')
changed, new_fill = imgui.checkbox("填充父级", is_fill)
if changed:
comp_data['layout_mode'] = 'fill' if new_fill else 'manual'
if new_fill:
comp_data['anchored_to_parent'] = False
comp_data.setdefault('fill_margin_left', 0.0)
comp_data.setdefault('fill_margin_right', 0.0)
comp_data.setdefault('fill_margin_top', 0.0)
comp_data.setdefault('fill_margin_bottom', 0.0)
if hasattr(manager, '_apply_fill_layout'):
manager._apply_fill_layout(index)
is_fill = (comp_data.get('layout_mode') == 'fill')
if not is_fill:
return False
imgui.text_disabled("填充模式下,位置/尺寸由父级决定")
imgui.text("边距")
m_left = float(comp_data.get('fill_margin_left', 0.0))
m_right = float(comp_data.get('fill_margin_right', 0.0))
m_top = float(comp_data.get('fill_margin_top', 0.0))
m_bottom = float(comp_data.get('fill_margin_bottom', 0.0))
changed_l, new_l = imgui.input_float("", m_left, 1.0, 10.0, "%.1f")
changed_r, new_r = imgui.input_float("", m_right, 1.0, 10.0, "%.1f")
changed_t, new_t = imgui.input_float("", m_top, 1.0, 10.0, "%.1f")
changed_b, new_b = imgui.input_float("", m_bottom, 1.0, 10.0, "%.1f")
if changed_l or changed_r or changed_t or changed_b:
comp_data['fill_margin_left'] = new_l
comp_data['fill_margin_right'] = new_r
comp_data['fill_margin_top'] = new_t
comp_data['fill_margin_bottom'] = new_b
if hasattr(manager, '_apply_fill_layout'):
manager._apply_fill_layout(index)
return True
def _draw_position_controls(manager, comp_data, comp_obj, is_fill):
imgui.text("位置")
if is_fill:
imgui.text(f"Left: {comp_data.get('left', 0.0):.1f}")
imgui.text(f"Top: {comp_data.get('top', 0.0):.1f}")
return
if 'left' in comp_data:
changed, new_left = imgui.input_float("Left", comp_data['left'], 1.0, 10.0, "%.1f")
if changed:
comp_data['left'] = new_left
comp_obj.left = new_left
if 'top' in comp_data:
changed, new_top = imgui.input_float("Top", comp_data['top'], 1.0, 10.0, "%.1f")
if changed:
comp_data['top'] = new_top
comp_obj.top = new_top
def _draw_size_controls(manager, index, comp_data, comp_obj, comp_type, is_fill):
imgui.spacing()
imgui.text("尺寸")
if is_fill:
imgui.text(f"Width: {comp_data.get('width', 0.0):.1f}")
imgui.text(f"Height: {comp_data.get('height', 0.0):.1f}")
return
width_changed = manager.luiFunction._draw_width_control(
manager, comp_data, comp_obj, comp_type
)
height_changed = manager.luiFunction._draw_height_control(
manager, comp_data, comp_obj, comp_type
)
manager.luiFunction._post_size_control_sync(
manager, index, comp_data, comp_type, width_changed, height_changed
)
def _draw_width_control(manager, comp_data, comp_obj, comp_type):
if 'width' not in comp_data:
return False
changed, new_width = imgui.input_float("Width", comp_data['width'], 1.0, 10.0, "%.1f")
if not changed or new_width <= 0:
return False
comp_data['width'] = new_width
if hasattr(comp_obj, 'width'):
comp_obj.width = new_width
if 'sprite' in comp_data:
comp_data['sprite'].width = new_width
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(float(comp_data.get('height', 16)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_height'):
comp_obj.set_height(float(comp_data.get('height', 24)))
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(new_width)
return True
def _draw_height_control(manager, comp_data, comp_obj, comp_type):
if 'height' not in comp_data:
return False
changed, new_height = imgui.input_float("Height", comp_data['height'], 1.0, 10.0, "%.1f")
if not changed or new_height <= 0:
return False
comp_data['height'] = new_height
if hasattr(comp_obj, 'height'):
comp_obj.height = new_height
if 'sprite' in comp_data:
comp_data['sprite'].height = new_height
if comp_type == 'Button' and hasattr(comp_obj, '_apply_stretch_sizes'):
comp_obj._apply_stretch_sizes()
if comp_type == 'Slider' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(float(comp_data.get('width', 200)))
if comp_type == 'InputField' and hasattr(comp_obj, 'set_width'):
comp_obj.set_width(float(comp_data.get('width', 200)))
return True
def _post_size_control_sync(manager, index, comp_data, comp_type, width_changed, height_changed):
if width_changed or height_changed:
if comp_type == 'HttpText' and hasattr(manager.luiFunction, 'sync_http_text_layout'):
manager.luiFunction.sync_http_text_layout(manager, comp_data)
if hasattr(manager, '_update_anchored_children'):
manager._update_anchored_children(index)
if comp_type in ['VerticalLayout', 'HorizontalLayout'] and hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
def _draw_layout_group_controls(manager, index, comp_data, comp_type):
if comp_type not in ['VerticalLayout', 'HorizontalLayout']:
return
imgui.spacing()
imgui.text("Layout Group")
spacing = float(comp_data.get('layout_spacing', 0.0))
changed, new_spacing = imgui.input_float("Spacing", spacing, 1.0, 10.0, "%.1f")
if changed:
comp_data['layout_spacing'] = new_spacing
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
imgui.text("Padding")
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))
changed_l, new_l = imgui.input_float("Pad Left", pad_left, 1.0, 10.0, "%.1f")
changed_r, new_r = imgui.input_float("Pad Right", pad_right, 1.0, 10.0, "%.1f")
changed_t, new_t = imgui.input_float("Pad Top", pad_top, 1.0, 10.0, "%.1f")
changed_b, new_b = imgui.input_float("Pad Bottom", pad_bottom, 1.0, 10.0, "%.1f")
if changed_l or changed_r or changed_t or changed_b:
comp_data['layout_padding_left'] = new_l
comp_data['layout_padding_right'] = new_r
comp_data['layout_padding_top'] = new_t
comp_data['layout_padding_bottom'] = new_b
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
wrap_enabled = bool(comp_data.get('layout_wrap', True))
changed_wrap, new_wrap = imgui.checkbox("Wrap", wrap_enabled)
if changed_wrap:
comp_data['layout_wrap'] = new_wrap
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
line_spacing = float(comp_data.get('layout_line_spacing', 0.0))
changed_line, new_line_spacing = imgui.input_float("Line Spacing (Wrap)", line_spacing, 1.0, 10.0, "%.1f")
if changed_line:
comp_data['layout_line_spacing'] = new_line_spacing
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
align_options = ["start", "center", "end", "stretch"]
current_align = comp_data.get('layout_align', 'start')
if imgui.begin_combo("Align", current_align):
for opt in align_options:
if imgui.selectable(opt, current_align == opt)[0]:
comp_data['layout_align'] = opt
if hasattr(manager, '_update_layout_inner'):
manager._update_layout_inner(index)
imgui.end_combo()
def _draw_z_offset_controls(manager, index, comp_data, comp_obj):
imgui.spacing()
imgui.text("层级与显示顺序")
current_z = comp_data.get('z_offset')
if current_z is None:
current_z = comp_data.get('sort', 0.0)
if current_z == 0 and hasattr(comp_obj, 'z_offset'):
current_z = comp_obj.z_offset
comp_data['z_offset'] = float(current_z)
layer_val = int(current_z)
changed_layer, new_layer = imgui.input_int("渲染层级 (Layer)", layer_val)
if changed_layer:
comp_data['z_offset'] = float(new_layer)
if hasattr(comp_obj, 'set_z_offset'):
comp_obj.set_z_offset(float(new_layer))
print(f"✓ 组件 #{index} 层级已映射到 Z-Offset: {new_layer}")
changed_z, new_z = imgui.input_float("深度微调 (Z-Offset)", comp_data['z_offset'], 0.1, 1.0, "%.2f")
if changed_z:
comp_data['z_offset'] = new_z
if hasattr(comp_obj, 'set_z_offset'):
comp_obj.set_z_offset(new_z)
elif hasattr(comp_obj, 'z_offset'):
comp_obj.z_offset = new_z
print(f"✓ 组件 #{index} Z-Offset 已微调为: {new_z}")
imgui.text_disabled("(注: LUI 内部组件通过 Z-Offset 决定遮挡关系)")
# 特定类型的属性
def _draw_type_specific_properties(manager, index, comp_data, comp_obj, comp_type):
if comp_type == 'InputField':
manager.luiFunction._draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'Slider':
manager.luiFunction._draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'Checkbox':
manager.luiFunction._draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type in ['Plane', 'Image']:
manager.luiFunction._draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'Frame':
manager.luiFunction._draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'Selectbox':
manager.luiFunction._draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'HttpText':
manager.luiFunction._draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type)
return
if comp_type == 'Video':
manager.luiFunction._draw_type_props_video(manager, index, comp_data, comp_obj, comp_type)
return
def _draw_type_props_input_field(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("输入框属性")
imgui.text("输入框图片")
if imgui.button("更改图片##input_img", (120, 20)):
selected_path = manager._change_image_texture(
title = "选择图片文件",
filetypes = [
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("所有文件", "*.*")
]
)
if selected_path:
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, selected_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
comp_data['input_texture_path'] = selected_path
comp_data['input_atlas_uv'] = (u0, v0, u1, v1)
comp_data['input_texture_ref'] = tex
comp_data['input_image_size'] = (int(w), int(h))
layout = getattr(comp_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, 'set_texture'):
spr.set_texture(tex, resize=False)
if hasattr(spr, 'set_uv_range'):
spr.set_uv_range(u0, v0, u1, v1)
except Exception as e:
print(f"InputField texture set failed: {e}")
if imgui.button("使用图片尺寸##input_fit", (120, 20)):
w = h = None
if 'input_image_size' in comp_data and comp_data['input_image_size']:
try:
w, h = comp_data['input_image_size']
except Exception:
w = h = None
elif 'input_atlas_uv' in comp_data and 'input_texture_ref' in comp_data and comp_data['input_texture_ref']:
try:
u0, v0, u1, v1 = comp_data['input_atlas_uv']
tex = comp_data['input_texture_ref']
tw = tex.getXSize() if hasattr(tex, 'getXSize') else 0
th = tex.getYSize() if hasattr(tex, 'getYSize') else 0
w = int(max(1, round((u1 - u0) * tw)))
h = int(max(1, round((v1 - v0) * th)))
except Exception:
w = h = None
if w is not None and h is not None and w > 0 and h > 0:
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
if hasattr(comp_obj, 'set_width'):
comp_obj.set_width(float(w))
if hasattr(comp_obj, 'set_height'):
comp_obj.set_height(float(h))
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%"
imgui.text("输入框颜色")
in_color = comp_data.get('input_color', (1.0, 1.0, 1.0, 1.0))
in_color = list(in_color) if isinstance(in_color, tuple) else in_color
changed, new_in_color = imgui.color_edit4("##input_color", in_color)
if changed:
comp_data['input_color'] = tuple(new_in_color)
layout = getattr(comp_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:
spr.color = tuple(new_in_color)
if 'value' in comp_data:
changed, new_value = imgui.input_text("当前值", comp_data['value'], 256)
if changed:
comp_data['value'] = new_value
comp_obj.value = new_value
def _draw_type_props_slider(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("滑块属性")
if 'value' in comp_data:
min_val = comp_data.get('min_value', 0.0)
max_val = comp_data.get('max_value', 100.0)
changed, new_value = imgui.slider_float("当前值", comp_data['value'], min_val, max_val, "%.1f")
if changed:
comp_data['value'] = new_value
if hasattr(comp_obj, 'set_value'):
comp_obj.set_value(new_value)
def _draw_type_props_checkbox(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("复选框属性")
if 'text' in comp_data:
changed, new_text = imgui.input_text("标签文本", comp_data['text'], 256)
if changed:
comp_data['text'] = new_text
# Checkbox uses a child label for text
if hasattr(comp_obj, 'label') and hasattr(comp_obj.label, 'text'):
comp_obj.label.text = new_text
elif hasattr(comp_obj, 'text'):
comp_obj.text = new_text
if 'checked' in comp_data:
changed, new_checked = imgui.checkbox("选中状态", comp_data['checked'])
if changed:
comp_data['checked'] = new_checked
if hasattr(comp_obj, 'checked'):
comp_obj.checked = new_checked
def _draw_type_props_plane_image(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text(f"{comp_type}属性")
# 颜色属性
if 'color' in comp_data:
color = comp_data.get('color', (1.0, 1.0, 1.0, 1.0))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("颜色", color)
if changed:
comp_data['color'] = tuple(new_color)
# 更新sprite颜色
if 'sprite' in comp_data:
comp_data['sprite'].color = tuple(new_color)
# 如果是Image显示纹理路径
if comp_type == 'Image' and 'texture_path' in comp_data:
# Fit size to image texture
if imgui.button("使用图片尺寸##fit_image"):
tex = None
# Prefer direct texture reference if available
if 'texture_ref' in comp_data and comp_data['texture_ref']:
tex = comp_data['texture_ref']
elif 'texture' in comp_data and comp_data['texture']:
tex = comp_data['texture']
else:
# Try to read from sprite python tag if present
try:
target = comp_data.get('sprite', comp_obj)
if target is not None and hasattr(target, 'get_python_tag'):
tex = target.get_python_tag('texture_ref')
except Exception:
tex = None
w = h = None
if 'image_size' in comp_data and comp_data['image_size']:
try:
w, h = comp_data['image_size']
except Exception:
w = h = None
elif 'atlas_uv' in comp_data and 'atlas_tex' in comp_data and comp_data['atlas_tex']:
try:
u0, v0, u1, v1 = comp_data['atlas_uv']
atlas_tex = comp_data['atlas_tex']
atlas_w = atlas_tex.getXSize() if hasattr(atlas_tex, 'getXSize') else 0
atlas_h = atlas_tex.getYSize() if hasattr(atlas_tex, 'getYSize') else 0
w = int(max(1, round((u1 - u0) * atlas_w)))
h = int(max(1, round((v1 - v0) * atlas_h)))
except Exception:
w = h = None
elif tex is not None:
if hasattr(tex, 'getOrigFileXSize') and hasattr(tex, 'getOrigFileYSize'):
ow = tex.getOrigFileXSize()
oh = tex.getOrigFileYSize()
if ow and oh:
w, h = ow, oh
if w is None or h is None:
if hasattr(tex, 'getXSize') and hasattr(tex, 'getYSize'):
w = tex.getXSize()
h = tex.getYSize()
if w is not None and h is not None and w > 0 and h > 0:
comp_data['width'] = float(w)
comp_data['height'] = float(h)
comp_obj.width = float(w)
comp_obj.height = float(h)
# Sync sprite size (Image/Plane/Video)
if 'sprite' in comp_data and comp_data['sprite']:
spr = comp_data['sprite']
if hasattr(spr, 'set_size'):
spr.set_size(float(w), float(h))
else:
spr.width = float(w)
spr.height = float(h)
if hasattr(manager, '_hide_resize_handles'):
manager._hide_resize_handles()
else:
print("Image size not available: missing texture")
imgui.text(f"纹理路径: {comp_data['texture_path']}")
if imgui.button("更改纹理", (100, 20)):
# 实现纹理更改功能
selected_path = manager._change_image_texture(
title = "选择图片文件",
filetypes = [
("图片文件", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.tga"),
("PNG", "*.png"),
("JPEG", "*.jpg;*.jpeg"),
("所有文件", "*.*")
]
)
if selected_path:
new_path = selected_path
comp_data['texture_path'] = new_path
try:
atlas_result = manager.luiFunction._add_image_to_atlas(manager, new_path, atlas_size=2048)
if atlas_result:
if len(atlas_result) >= 7:
tex, u0, v0, u1, v1, w, h = atlas_result[:7]
else:
tex, u0, v0, u1, v1 = atlas_result
w = int(max(1, round((u1 - u0) * tex.getXSize())))
h = int(max(1, round((v1 - v0) * tex.getYSize())))
print(f"? Texture loaded: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}")
comp_data["current_texture"] = os.path.basename(new_path)
comp_data["atlas_uv"] = (u0, v0, u1, v1)
comp_data["atlas_tex"] = tex
comp_data["image_size"] = (int(w), int(h))
old_spr = comp_data.get("sprite")
if old_spr:
try:
old_spr.parent = None
if hasattr(old_spr, "set_texture"): old_spr.set_texture(None)
if hasattr(old_spr, "destroy"): old_spr.destroy()
except:
pass
try:
new_spr = LUISprite(comp_obj, tex)
if hasattr(new_spr, "set_texture"):
new_spr.set_texture(tex, resize=False)
if hasattr(new_spr, "set_uv_range"):
new_spr.set_uv_range(u0, v0, u1, v1)
new_spr.width = comp_data.get("width", 100)
new_spr.height = comp_data.get("height", 100)
new_spr.left = 0
new_spr.top = 0
new_spr.z_offset = comp_data.get("z_offset", 0)
new_spr.color = comp_data.get("color", (1,1,1,1))
if hasattr(new_spr, "set_uv_range"):
new_spr.set_uv_range(u0, v0, u1, v1)
comp_data["sprite"] = new_spr
comp_data["texture_ref"] = tex
if not hasattr(manager, "texture_refs"):
manager.texture_refs = []
manager.texture_refs.append(tex)
if hasattr(new_spr, "set_python_tag"):
new_spr.set_python_tag("texture_ref", tex)
if hasattr(comp_obj, "set_python_tag"):
comp_obj.set_python_tag("texture_ref", tex)
print(f"? Replaced sprite with atlas texture image: {tex.getName()}, Size: {tex.getXSize()}x{tex.getYSize()}")
except Exception as ex:
print(f"? Failed to create new sprite: {ex}")
target = comp_data.get("widget", comp_data.get("object"))
if hasattr(target, "set_texture"):
target.set_texture(tex)
else:
print(f"? Texture loading returned None for: {new_path}")
except Exception as e:
print(f"Image texture update failed: {e}")
if 'sprite' in comp_data:
sprite = comp_data['sprite']
imgui.text(f"当前纹理: {comp_data.get('current_texture', 'blank')}")
def _draw_type_props_frame(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("框架属性")
# 框架特有的颜色属性
if 'color' in comp_data:
color = comp_data.get('color', (0.7, 0.7, 0.7, 0.8))
color = list(color) if isinstance(color, tuple) else color
changed, new_color = imgui.color_edit4("背景颜色", color)
if changed:
comp_data['color'] = tuple(new_color)
# 更新Frame颜色
if hasattr(comp_obj, 'set_color'):
comp_obj.set_color(tuple(new_color))
def _draw_type_props_selectbox(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("Selectbox Options")
imgui.separator()
options = comp_data.get('options', []) or []
# normalize to list of (id,label)
normalized = []
for i, opt in enumerate(options):
try:
opt_id, opt_label = opt
except Exception:
opt_id, opt_label = i, str(opt)
normalized.append((opt_id, str(opt_label)))
options = normalized
comp_data['options'] = options
current_selected = comp_data.get('selected_option_id')
if hasattr(comp_obj, 'get_selected_option'):
try:
current_selected = comp_obj.get_selected_option()
except Exception:
pass
opt_labels = [label for _, label in options] if options else ["(empty)"]
opt_ids = [oid for oid, _ in options] if options else [None]
try:
current_index = opt_ids.index(current_selected)
except Exception:
current_index = 0
changed_sel, new_index = imgui.combo("Selected", current_index, opt_labels)
if changed_sel and options:
sel_id = opt_ids[new_index]
comp_data['selected_option_id'] = sel_id
try:
if hasattr(comp_obj, '_select_option'):
comp_obj._select_option(sel_id)
except Exception:
pass
imgui.spacing()
imgui.text("Edit Options")
imgui.separator()
new_labels = []
dirty = False
for i, (opt_id, opt_label) in enumerate(options):
imgui.push_item_width(-40)
changed, new_label = imgui.input_text(f"##opt_label_{i}", opt_label, 128)
imgui.pop_item_width()
if changed:
opt_label = new_label
dirty = True
imgui.same_line()
if imgui.button(f"Delete##opt_del_{i}", (60, 20)):
dirty = True
continue
new_labels.append(opt_label)
if imgui.button("Add Option", (100, 24)):
new_labels.append(f"Option {len(new_labels)+1}")
dirty = True
if dirty:
# rebuild options with sequential ids to avoid duplicates
options = [(i, label) for i, label in enumerate(new_labels)]
comp_data['options'] = options
# keep selection if possible
if options:
if current_selected not in [oid for oid, _ in options]:
current_selected = options[0][0]
comp_data['selected_option_id'] = current_selected
else:
comp_data['selected_option_id'] = None
try:
if hasattr(comp_obj, 'set_options'):
comp_obj.set_options(options)
elif hasattr(comp_obj, 'options'):
comp_obj.options = options
if options and hasattr(comp_obj, '_select_option'):
comp_obj._select_option(comp_data['selected_option_id'])
except Exception as e:
print(f"Selectbox options update failed: {e}")
def _draw_type_props_http_text(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("HTTP通信")
imgui.separator()
http_url = str(comp_data.get('http_url', ''))
changed_url, new_url = imgui.input_text("URL", http_url, 512)
if changed_url:
comp_data['http_url'] = new_url
method_options = ["GET", "POST"]
curr_method = str(comp_data.get('http_method', 'GET')).upper()
curr_method_idx = 1 if curr_method == "POST" else 0
changed_method, new_method_idx = imgui.combo("Method", curr_method_idx, method_options)
if changed_method:
comp_data['http_method'] = method_options[new_method_idx]
timeout_val = float(comp_data.get('http_timeout', 8.0))
changed_timeout, new_timeout = imgui.input_float("Timeout(s)", timeout_val, 1.0, 5.0, "%.1f")
if changed_timeout:
comp_data['http_timeout'] = max(1.0, new_timeout)
auto_refresh = bool(comp_data.get('auto_refresh', True))
changed_auto, new_auto = imgui.checkbox("自动刷新", auto_refresh)
if changed_auto:
comp_data['auto_refresh'] = new_auto
interval_val = float(comp_data.get('refresh_interval', 60.0))
changed_interval, new_interval = imgui.input_float("刷新间隔(s)", interval_val, 1.0, 10.0, "%.1f")
if changed_interval:
comp_data['refresh_interval'] = max(1.0, new_interval)
json_path = str(comp_data.get('http_json_path', ''))
changed_path, new_path = imgui.input_text("JSON路径(可选)", json_path, 256)
if changed_path:
comp_data['http_json_path'] = new_path
headers_text = str(comp_data.get('http_headers', '{}'))
changed_headers, new_headers = imgui.input_text("Headers(JSON)", headers_text, 512)
if changed_headers:
comp_data['http_headers'] = new_headers
body_text = str(comp_data.get('http_body', ''))
changed_body, new_body = imgui.input_text("Body", body_text, 512)
if changed_body:
comp_data['http_body'] = new_body
max_chars = int(comp_data.get('max_chars', 300))
changed_max, new_max = imgui.input_int("最大显示字符", max_chars)
if changed_max:
comp_data['max_chars'] = max(32, int(new_max))
imgui.text(f"状态: {comp_data.get('http_status', '未请求')}")
last_error = comp_data.get('last_error', '')
if last_error:
imgui.text_colored((1.0, 0.4, 0.4, 1.0), f"错误: {last_error}")
if comp_data.get('_http_inflight'):
imgui.text("请求中...")
if imgui.button("北京天气示例", (120, 24)):
comp_data['http_url'] = "https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m&timezone=Asia%2FShanghai"
comp_data['http_method'] = "GET"
comp_data['http_json_path'] = "current"
comp_data['http_headers'] = "{}"
comp_data['http_body'] = ""
comp_data['refresh_interval'] = 60.0
comp_data['auto_refresh'] = True
imgui.same_line()
if imgui.button("立即请求", (100, 24)):
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
def _draw_type_props_video(manager, index, comp_data, comp_obj, comp_type):
imgui.spacing()
imgui.text("视频属性")
imgui.separator()
manager.luiFunction._draw_video_source_and_audio_controls(
manager, index, comp_data, comp_obj, comp_type
)
manager.luiFunction._draw_video_playback_controls(
manager, index, comp_data, comp_obj, comp_type
)
def _draw_video_source_and_audio_controls(manager, index, comp_data, comp_obj, comp_type):
manager.luiFunction._draw_video_audio_controls(manager, comp_data)
manager.luiFunction._draw_video_source_controls(manager, comp_data)
manager.luiFunction._process_pending_video_source(manager, comp_data, comp_obj)
def _draw_video_audio_controls(manager, comp_data):
imgui.spacing()
imgui.text("Audio")
imgui.separator()
current_audio = comp_data.get("audio_path", "")
audio_display = os.path.basename(current_audio) if current_audio else ""
imgui.text(f"Current Audio: {audio_display if audio_display else 'None'}")
if imgui.button("Select Audio", (110, 24)):
selected_audio = manager._change_image_texture(
title="Select Audio File",
filetypes=[("Audio", "*.mp3;*.wav;*.ogg;*.flac;*.m4a"), ("All Files", "*.*")],
)
if selected_audio:
manager.luiFunction._load_audio_for_component(
manager, comp_data, selected_audio, from_video=False, stop_current=False
)
vol = comp_data.get("volume", 1.0)
changed, new_vol = imgui.slider_float("Volume", vol, 0.0, 1.0, "%.2f")
if changed:
comp_data["volume"] = new_vol
audio_sound = comp_data.get("audio")
if audio_sound and hasattr(audio_sound, "setVolume"):
audio_sound.setVolume(new_vol)
def _draw_video_source_controls(manager, comp_data):
if "video_path" not in comp_data:
return
current_source = comp_data.get("video_path", "")
is_url = "://" in current_source
source_display = os.path.basename(current_source) if (current_source and not is_url) else current_source
imgui.text(f"当前源: {source_display if source_display else '未设置'}")
if imgui.button("选择本地视频", (110, 24)):
selected_path = manager._change_image_texture(
title="选择视频文件",
filetypes=[
("视频文件", "*.mp4;*.avi;*.mov;*.mkv;*.webm;*.ogv"),
("所有文件", "*.*"),
],
)
if selected_path:
comp_data["_pending_video_source"] = selected_path
def _process_pending_video_source(manager, comp_data, comp_obj):
if "_pending_video_source" not in comp_data:
return
selected_path = comp_data.pop("_pending_video_source")
if not selected_path:
return
selected_path = selected_path.strip()
comp_data["video_path"] = selected_path
comp_data["duration"] = 0.0
print(f"✓ 尝试加载视频源: {selected_path}")
try:
video_texture = manager.luiFunction._load_video_texture_from_source(manager, selected_path)
if not video_texture:
return
manager.luiFunction._apply_loaded_video_texture(
manager, comp_data, comp_obj, video_texture, selected_path
)
except Exception as e:
print(f"⚠ 加载视频失败: {e}")
def _load_video_texture_from_source(manager, selected_path):
from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData
loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file")
loadPrcFileData("", "ffmpeg-show-error #t")
if "://" in selected_path:
print(f"🔍 尝试加载 URL: [{selected_path}]")
try:
video_src = MovieVideo.get(Filename(selected_path))
video_texture = MovieTexture("RemoteVideo")
if not video_texture.load(video_src):
print("⚠ MovieVideo 加载失败,回退到 Texture.read...")
if not video_texture.read(Filename(selected_path)):
raise Exception("MovieTexture 无法读取 URL")
return video_texture
except Exception as e:
print(f"⚠ 显式流加载失败: {e},尝试使用 Loader...")
return manager.world.loader.loadTexture(selected_path)
return manager.world.loader.loadTexture(Filename.from_os_specific(selected_path))
def _apply_loaded_video_texture(manager, comp_data, comp_obj, video_texture, selected_path):
manager.luiFunction._sync_video_size_and_sprite(
manager, comp_data, comp_obj, video_texture
)
comp_data["texture"] = video_texture
comp_obj.video_texture = video_texture
if comp_data.get("audio_from_video"):
manager.luiFunction._load_audio_for_component(
manager, comp_data, selected_path, from_video=True, stop_current=True
)
manager.luiFunction._start_video_texture_playback(manager, video_texture, comp_data)
comp_data["is_playing"] = True
def _sync_video_size_and_sprite(manager, comp_data, comp_obj, video_texture):
orig_w = video_texture.getOrigFileXSize()
orig_h = video_texture.getOrigFileYSize()
if orig_w <= 0 or orig_h <= 0:
return
ratio = orig_w / orig_h
width = comp_data.get("width", 320)
new_height = width / ratio
print(f"✓Loaded Video Texture: {orig_w}x{orig_h}")
from panda3d.core import ConfigVariableBool
if not ConfigVariableBool("support-threads").getValue():
print("⚠ 警告: support-threads 为 False, 视频播放可能失败!")
if "sprite" in comp_data:
spr = comp_data["sprite"]
if hasattr(spr, "set_uv_range"):
spr.set_uv_range(0, 0, 1, 1)
comp_data["height"] = new_height
comp_obj.height = new_height
if "sprite" in comp_data:
comp_data["sprite"].height = new_height
manager.luiFunction._replace_video_sprite(
manager, comp_data, comp_obj, video_texture, width, new_height
)
def _replace_video_sprite(manager, comp_data, comp_obj, video_texture, width, new_height):
if "sprite" in comp_data:
old_spr = comp_data["sprite"]
if old_spr:
try:
old_spr.parent = None
if hasattr(old_spr, "hide"):
old_spr.hide()
except Exception as ex:
print(f"⚠ Failed to detach old sprite: {ex}")
new_spr = LUISprite(comp_obj, video_texture)
if hasattr(new_spr, "set_texture"):
new_spr.set_texture(video_texture, resize=False)
new_spr.width = width
new_spr.height = new_height
new_spr.color = (1, 1, 1, 1)
new_spr.z_offset = 0
if hasattr(new_spr, "set_uv_range"):
new_spr.set_uv_range(0, 0, 1, 1)
comp_data["sprite"] = new_spr
manager.luiFunction._refresh_video_keep_alive_node(manager, comp_data, video_texture)
def _refresh_video_keep_alive_node(manager, comp_data, video_texture):
if "keep_alive" in comp_data and comp_data["keep_alive"]:
try:
comp_data["keep_alive"].destroy()
except Exception:
pass
try:
from direct.gui.OnscreenImage import OnscreenImage
from panda3d.core import TransparencyAttrib
dummy = OnscreenImage(image=video_texture, parent=manager.world.render2d)
dummy.setScale(0.1)
dummy.setPos(0, 0, 0)
dummy.setTransparency(TransparencyAttrib.MAlpha)
dummy.setColorScale(1, 1, 1, 0.01)
dummy.setBin("background", -10)
comp_data["keep_alive"] = dummy
print("✓ Created keep-alive node for video")
except Exception as e:
print(f"⚠ Keep-alive creation failed: {e}")
def _load_audio_for_component(manager, comp_data, audio_path, from_video, stop_current):
if stop_current:
old_audio = comp_data.get("audio")
if old_audio and hasattr(old_audio, "stop"):
old_audio.stop()
try:
audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(audio_path))
if not audio_sound:
return
comp_data["audio"] = audio_sound
comp_data["audio_path"] = audio_path
comp_data["audio_from_video"] = from_video
if hasattr(audio_sound, "setLoop"):
audio_sound.setLoop(comp_data.get("loop", True))
if hasattr(audio_sound, "setVolume"):
audio_sound.setVolume(comp_data.get("volume", 1.0))
if comp_data.get("is_playing") and hasattr(audio_sound, "play"):
audio_sound.play()
except Exception as e:
print(f"Audio load failed: {e}")
def _start_video_texture_playback(manager, video_texture, comp_data):
if hasattr(video_texture, "play"):
if hasattr(video_texture, "stop"):
video_texture.stop()
video_texture.play()
if hasattr(video_texture, "setLoop"):
video_texture.setLoop(comp_data.get("loop", True))
def _draw_video_playback_controls(manager, index, comp_data, comp_obj, comp_type):
# 播放控制
imgui.spacing()
imgui.text("播放控制")
video_tex = comp_data.get('texture')
audio_sound = comp_data.get('audio')
if video_tex:
# Play/Pause Button
is_playing = comp_data.get('is_playing', False)
if imgui.button("暂停" if is_playing else "播放", (60, 24)):
if is_playing:
if hasattr(video_tex, 'stop'): video_tex.stop()
if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop()
comp_data['is_playing'] = False
else:
if hasattr(video_tex, 'play'): video_tex.play()
if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play()
comp_data['is_playing'] = True
imgui.same_line()
if imgui.button("重播", (60, 24)):
if hasattr(video_tex, 'stop'): video_tex.stop()
if audio_sound and hasattr(audio_sound, 'stop'): audio_sound.stop()
if hasattr(video_tex, 'play'): video_tex.play()
if audio_sound and hasattr(audio_sound, 'play'): audio_sound.play()
comp_data['is_playing'] = True
# Loop Checkbox
loop = comp_data.get('loop', True)
changed, new_loop = imgui.checkbox("循环播放", loop)
if changed:
comp_data['loop'] = new_loop
if hasattr(video_tex, 'setLoop'):
video_tex.setLoop(new_loop)
if audio_sound and hasattr(audio_sound, 'setLoop'): audio_sound.setLoop(new_loop)
# Time Display
if hasattr(video_tex, 'getTime'):
t = video_tex.getTime()
# 检查是否需要循环播放
duration = comp_data.get('duration', 0.0)
if duration > 0 and t >= duration:
# 播放时间超过总时长,重置到开头
if hasattr(video_tex, 'setTime'):
video_tex.setTime(0.0)
t = 0.0
print(f"✓ 视频播放完毕,重新开始循环播放")
imgui.text(f"当前时间: {t:.2f}s")
# Force refresh if stuck (hack)
# if t == 0.0 and comp_data.get('is_playing'):
# video_tex.play()
# imgui.separator()
# imgui.text("调试工具")
# if imgui.button("在Render2D中测试播放"):
# # Create a standard OnscreenImage to test playback capability
# from direct.gui.OnscreenImage import OnscreenImage
# from panda3d.core import TransparencyAttrib
# # Remove previous test if any
# if hasattr(manager, '_debug_video_node') and manager._debug_video_node:
# manager._debug_video_node.destroy()
# manager._debug_video_node = OnscreenImage(image=video_tex, pos=(0, 0, 0), scale=0.5, parent=manager.world.render2d)
# manager._debug_video_node.setTransparency(TransparencyAttrib.MAlpha)
# print(f"Debug: Created OnscreenImage with texture {video_tex}")
# if imgui.button("清除Render2D测试"):
# if hasattr(manager, '_debug_video_node') and manager._debug_video_node:
# manager._debug_video_node.destroy()
# manager._debug_video_node = None
# print("Debug: Cleared OnscreenImage")
# Time Display and Control
if hasattr(video_tex, 'getTime'):
current_time = video_tex.getTime()
if 'duration' not in comp_data or comp_data['duration'] <= 0.0:
# Attempt to detect duration automatically
detected_duration = 0.0
# Method 1: standard MovieTexture interface (if available)
if hasattr(video_tex, 'getVideoLength'):
detected_duration = video_tex.getVideoLength()
# Method 2: check if it has a 'length' property
elif hasattr(video_tex, 'length'):
detected_duration = video_tex.length
if detected_duration > 0:
comp_data['duration'] = detected_duration
print(f"✓ Automatically detected video duration: {detected_duration}s")
# Manual Duration Input (Essential if auto-detection fails)
# We only show this if we really don't know the duration
current_dur = comp_data.get('duration', 0.0)
if current_dur <= 0.1:
imgui.text_colored((1, 1, 0, 1), "⚠未知时长,请手动设置:")
changed, new_dur = imgui.input_float("总时长(s)", current_dur, 1.0, 10.0, "%.1f")
if changed:
comp_data['duration'] = new_dur
duration = comp_data.get('duration', 0.0)
# 检查是否需要循环播放
if duration > 0 and current_time >= duration:
# 播放时间超过总时长,重置到开头
if hasattr(video_tex, 'setTime'):
video_tex.setTime(0.0)
current_time = 0.0
print(f"✓ 视频播放完毕,重新开始循环播放")
# If we still don't have a valid duration, use a fallback for display but warn user
# REVERT: Do not auto-extend duration, it causes confusion. Trust detection or user.
display_max = duration if duration > 0 else max(current_time + 10.0, 60.0)
# Format function for time
def format_time(seconds):
m = int(seconds // 60)
s = int(seconds % 60)
return f"{m:02d}:{s:02d}"
# Progress Slider
imgui.text(f"进度: {format_time(current_time)} / {format_time(display_max)}")
imgui.same_line()
# Use push_item_width to make it fit
imgui.push_item_width(-1)
# Use a custom format for the slider to show seconds, but maybe we can show MM:SS in the text
changed, seek_time = imgui.slider_float("##seek_slider", current_time, 0.0, display_max, "%.2fs")
imgui.pop_item_width()
if changed:
if hasattr(video_tex, 'setTime'):
video_tex.setTime(seek_time)
if audio_sound and hasattr(audio_sound, 'setTime'): audio_sound.setTime(seek_time)
# If paused, we might need to show the frame.
# Usually setTime works.
print(f"Seek to {seek_time}")
# Update cached duration if we find a way, or if user inputs it?
# For now just show time
# imgui.text(f"时间: {current_time:.2f}s") # Already shown in slider
else:
imgui.text_colored((1, 0, 0, 1), "无有效视频纹理")
def _draw_anchor_and_hierarchy_properties(manager, index, comp_data, comp_obj):
imgui.spacing()
imgui.separator()
imgui.text("锚点设置")
parent_index = comp_data.get('parent_index')
has_parent = (parent_index is not None and parent_index >= 0 and parent_index < len(manager.components))
# 允许对父组件或Canvas进行锚点设置
if has_parent or manager.current_canvas_index >= 0:
is_anchored = comp_data.get('anchored_to_parent', False)
anchor_pos = comp_data.get('anchor_position', '未设置')
if has_parent:
parent_comp = manager.components[parent_index]
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"父组件: {parent_comp['type']}")
else:
imgui.text_colored((0.0, 1.0, 0.0, 1.0), "父容器: Canvas")
if is_anchored:
imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前锚点: {anchor_pos}")
# 锚点位置选择器 - 9宫格布局
imgui.spacing()
imgui.text("锚点位置:")
# 定义9个锚点位置
anchor_positions = [
['top-left', 'top-center', 'top-right'],
['middle-left', 'center', 'middle-right'],
['bottom-left', 'bottom-center', 'bottom-right']
]
# 中文显示名称
anchor_names = {
'top-left': '左上', 'top-center': '中上', 'top-right': '右上',
'middle-left': '左中', 'center': '中心', 'middle-right': '右中',
'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下'
}
# 绘制3x3网格按钮
for row_idx, row in enumerate(anchor_positions):
for col_idx, pos in enumerate(row):
# 检查是否是当前选中的锚点
is_current = (anchor_pos == pos)
# 如果是当前锚点,使用不同颜色
if is_current:
imgui.push_style_color(imgui.Col_.button, (0.2, 0.7, 0.2, 1.0))
imgui.push_style_color(imgui.Col_.button_hovered, (0.3, 0.8, 0.3, 1.0))
imgui.push_style_color(imgui.Col_.button_active, (0.1, 0.6, 0.1, 1.0))
# 绘制按钮
button_size = (50, 25)
if imgui.button(f"{anchor_names[pos]}##anchor_{pos}", button_size):
# 更新锚点位置
manager._update_component_anchor_position(index, pos)
if is_current:
imgui.pop_style_color(3)
# 同一行的按钮在同一行显示
if col_idx < len(row) - 1:
imgui.same_line()
imgui.spacing()
# 锚点偏移微调
imgui.text("位置微调:")
anchor_offset_x = comp_data.get('anchor_manual_offset_x', 0.0)
anchor_offset_y = comp_data.get('anchor_manual_offset_y', 0.0)
changed_x, new_offset_x = imgui.input_float("X偏移", anchor_offset_x, 1.0, 10.0, "%.1f")
if changed_x:
comp_data['anchor_manual_offset_x'] = new_offset_x
manager._update_component_anchor_position(index, anchor_pos, manual_offset=(new_offset_x, anchor_offset_y))
changed_y, new_offset_y = imgui.input_float("Y偏移", anchor_offset_y, 1.0, 10.0, "%.1f")
if changed_y:
comp_data['anchor_manual_offset_y'] = new_offset_y
manager._update_component_anchor_position(index, anchor_pos, manual_offset=(anchor_offset_x, new_offset_y))
imgui.spacing()
# 取消锚点按钮
if imgui.button("取消锚点", (100, 20)):
comp_data['anchored_to_parent'] = False
comp_data['anchor_manual_offset_x'] = 0.0
comp_data['anchor_manual_offset_y'] = 0.0
print("✓ 已取消组件的锚点关系")
else:
imgui.text_colored((1.0, 0.0, 0.0, 1.0), "未使用锚点定位")
if imgui.button("设置锚点", (100, 20)):
imgui.open_popup("选择锚点位置")
manager._temp_selected_index_for_anchor = index
else:
imgui.text_disabled("无法设置锚点 (无父组件且无有效Canvas)")
# Hierarchy & Parent Management
imgui.spacing()
imgui.text("层级与父级管理")
imgui.separator()
current_parent_index = comp_data.get('parent_index')
parent_text = "ROOT (无父组件)" if current_parent_index is None or current_parent_index < 0 else f"{manager.components[current_parent_index]['type']} #{current_parent_index}"
imgui.text(f"当前父级: {parent_text}")
# Create list of potential parents (exclude self and any children to avoid circular loops)
def get_all_descendants(idx):
desc = set()
comp = manager.components[idx]
for c_idx in comp.get('children_indices', []):
desc.add(c_idx)
desc.update(get_all_descendants(c_idx))
return desc
descendants = get_all_descendants(index)
parent_options = ["(无 / ROOT)"]
parent_indices = [-1]
for i, comp in enumerate(manager.components):
if i != index and i not in descendants:
parent_options.append(f"{comp['type']} #{i}: {comp.get('name', '')}")
parent_indices.append(i)
# Current selection in combo
try:
current_p_select = parent_indices.index(current_parent_index if current_parent_index is not None else -1)
except:
current_p_select = 0
changed, new_p_select = imgui.combo("更改父级", current_p_select, parent_options)
if changed:
new_parent_idx = parent_indices[new_p_select]
if new_parent_idx == -1:
# Unparent to Root
if current_parent_index is not None and current_parent_index >= 0:
# Remove from old parent
old_p = manager.components[current_parent_index]
if index in old_p.get('children_indices', []):
old_p['children_indices'].remove(index)
comp_data['parent_index'] = -1
# Physical reparent to Canvas or Root
if manager.current_canvas_index >= 0:
canvas_panel = manager.canvases[manager.current_canvas_index]['panel']
comp_obj.reparent_to(canvas_panel)
else:
# Fallback to overlay root
comp_obj.reparent_to(manager.overlay_root)
print(f"✓ 已将组件 #{index} 设为根节点")
else:
# Set New Parent
manager._set_parent_child_relationship(index, new_parent_idx, keep_world=True)
print(f"✓ 已更改父级: 组件 #{index} -> 父级 #{new_parent_idx}")
# 删除按钮
imgui.spacing()
imgui.separator()
imgui.push_style_color(imgui.Col_.button, (0.8, 0.2, 0.2, 1.0))
imgui.push_style_color(imgui.Col_.button_hovered, (0.9, 0.3, 0.3, 1.0))
imgui.push_style_color(imgui.Col_.button_active, (0.7, 0.1, 0.1, 1.0))
if imgui.button("删除组件", (-1, 30)):
manager.luiFunction.delete_component(manager,index)
imgui.pop_style_color(3)
# Draw the anchor popup if requested
manager._handle_anchor_popup()