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

1248 lines
45 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 and HTTP creation mixin."""
import json
import threading
import time
import urllib.error
import urllib.request
from pathlib import Path
import panda3d.core as p3d
from panda3d.core import CardMaker, NodePath
from imgui_bundle import imgui, imgui_ctx
from ui.LUI.lui_shared import (
LUICheckbox,
LUIFrame,
LUIHorizontalLayout,
LUIButton,
LUILabel,
LUIInputField,
LUISlider,
LUISprite,
LUIObject,
LUIProgressbar,
LUISelectbox,
LUIScrollableRegion,
LUITabbedFrame,
LUIVerticalLayout,
)
class LUIFunctionComponentsMixin:
def create_canvas(manager, name=None):
"""创建一个可视化的 UI 面板作为画布"""
manager.canvas_counter += 1
canvas_name = name or f"{manager.canvas_counter}"
# 获取窗口尺寸
win_width = 800
win_height = 600
if hasattr(manager.world, 'win'):
win_width = manager.world.win.getXSize()
win_height = manager.world.win.getYSize()
# 计算场景窗口区域排除Hierarchy面板
left_offset = 0
if hasattr(manager.world, 'hierarchy_size'):
left_offset = manager.world.hierarchy_size.x
target_width = win_width - left_offset
target_height = win_height
canvas_panel = LUIObject(parent=manager.overlay_root, x=left_offset, y=0)
# 显式设置尺寸
canvas_panel.width = target_width
canvas_panel.height = target_height
canvas_panel.solid = True # 接收事件
# 创建背景 Sprite
bg = LUISprite(canvas_panel, "blank", "skin")
bg.width = target_width
bg.height = target_height
bg.color = (0.5, 0.5, 0.5, 1.0) # 半透明灰色背景可见但不遮挡ImGui
# Canvas背景在LUI内部的底层
if hasattr(bg, 'set_sort'):
bg.set_sort(-1) # LUI内部的底层
# Canvas面板使用默认层级继承自LUI Region的sort=5
# 不需要额外设置sort会自动继承LUI Region的层级
canvas_panel.solid = True # 接收事件用于Canvas交互
# 创建Canvas的NodePath节点用于层级树显示
canvas_node = NodePath(canvas_name)
canvas_node.reparent_to(manager.world.render)
# 存储Canvas数据
canvas_data = {
'name': canvas_name,
'node': canvas_node,
'panel': canvas_panel,
'visible': True,
'background': bg,
'fill_mode': True, # 默认开启填充模式
'margins': {
'left': 240,
'right': 480, # 默认右侧面板宽度
'top': 89,
'bottom': 220 # 默认底部面板高度
}
}
# 应用初始几何计算 (调用 manager 的方法)
# 注意:这里需要 manager 是 public 的或者我们访问 protected 方法
if hasattr(manager, '_update_canvas_geometry'):
manager._update_canvas_geometry(canvas_data)
# 添加面板标题
title_text = f"Canvas: {canvas_name}"
title = LUILabel(text=title_text, font_size=16)
title.set_parent(canvas_panel)
title.set_pos(10, 10) # 内部 padding
if hasattr(title, 'set_color'):
title.set_color((0.8, 0.8, 0.8, 1.0))
# 存储标题引用以便后续修改
canvas_data['title_label'] = title
# 启用Canvas拖动
if hasattr(manager, '_make_canvas_draggable'):
manager._make_canvas_draggable(canvas_panel)
manager.canvases.append(canvas_data)
manager.current_canvas_index = len(manager.canvases) - 1
manager.switch_canvas(manager.current_canvas_index)
# 调试输出确认Canvas层级
print(f"✓ Created Canvas: {canvas_name}")
print(f" Canvas panel sort: {getattr(canvas_panel, 'sort', 'N/A')}")
print(f" Background sprite sort: {getattr(bg, 'sort', 'N/A')}")
if hasattr(manager.world, 'updateSceneTree'):
manager.world.updateSceneTree()
return canvas_data
def create_button(manager, text="Button", x=100, y=100, parent=None):
"""Create a LUI Button"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
btn = LUIButton(parent=parent, text=text, x=x, y=y)
# Ensure size is initialized so selection bounds match the actual button
default_w = 100
default_h = 30
try:
if not hasattr(btn, 'width') or btn.width is None or btn.width <= 0:
btn.width = default_w
if not hasattr(btn, 'height') or btn.height is None or btn.height <= 0:
btn.height = default_h
except Exception:
pass
width = getattr(btn, 'width', default_w) or default_w
height = getattr(btn, 'height', default_h) or default_h
# 设置组件层级在Canvas背景之上
if hasattr(btn, 'set_sort'):
btn.set_sort(1) # 在Canvas背景之上
comp_data = {
'type': 'Button',
'object': btn,
'widget': btn,
'text': text,
'left': x,
'top': y,
'width': float(width),
'height': float(height),
'canvas_index': manager.current_canvas_index,
'name': f"Button_{len(manager.components)}",
'color': (1.0, 1.0, 1.0, 1.0),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return btn
def create_label(manager,text="Text", x=100, y=100, font_size=14, parent=None):
"""Create a LUI Text"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
lbl = LUILabel(parent=parent, text=text, x=x, y=y, solid=True, font_size=font_size)
# 设置组件层级在Canvas背景之上
if hasattr(lbl, 'set_sort'):
lbl.set_sort(1) # 在Canvas背景之上
comp_data = {
'type': 'Text',
'object': lbl,
'text': text,
'left': x,
'top': y,
'width': 80,
'height': 20,
'canvas_index': manager.current_canvas_index,
'name': f"Text_{len(manager.components)}",
'color': (1, 1, 1, 1),
'font_size': font_size,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return lbl
def create_input_field(manager,text="", x=100, y=100, width=200, parent=None):
"""Create a LUI InputField"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
inf = LUIInputField(parent=parent, value=text, x=x, y=y, width=width)
comp_data = {
'type': 'InputField',
'object': inf,
'text': text,
'left': x,
'top': y,
'width': width,
'height': 24,
'canvas_index': manager.current_canvas_index,
'name': f"InputField_{len(manager.components)}",
'value': text,
'placeholder': "输入文本...",
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return inf
def create_slider(manager,x=100, y=100, width=200, parent=None):
"""Create a LUI Slider"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
sld = LUISlider(parent=parent, x=x, y=y, width=width)
if hasattr(sld, 'set_height'):
sld.set_height(16)
if hasattr(sld, 'height'):
sld.height = 16
comp_data = {
'type': 'Slider',
'object': sld,
'left': x,
'top': y,
'width': width,
'height': 16,
'canvas_index': manager.current_canvas_index,
'name': f"Slider_{len(manager.components)}",
'min_value': 0.0,
'max_value': 100.0,
'value': 50.0,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sld
def create_frame(manager,x=100, y=100, width=300, height=200, parent=None):
"""Create a LUI Frame"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
frm = LUIFrame(parent=parent, x=x, y=y, width=width, height=height)
comp_data = {
'type': 'Frame',
'object': frm,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Frame_{len(manager.components)}",
'color': (0.7, 0.7, 0.7, 0.8),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return frm
def create_vertical_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None):
"""Create a LUI Vertical Layout"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
container = LUIObject(parent=parent, x=x, y=y)
if hasattr(container, 'width'):
container.width = width
if hasattr(container, 'height'):
container.height = height
if hasattr(container, 'solid'):
container.solid = True
layout = LUIVerticalLayout(parent=container, spacing=spacing)
padding_left = 0.0
padding_right = 0.0
padding_top = 0.0
padding_bottom = 0.0
inner_w = max(1.0, float(width) - (padding_left + padding_right))
inner_h = max(1.0, float(height) - (padding_top + padding_bottom))
if hasattr(layout, 'left'):
layout.left = padding_left
if hasattr(layout, 'top'):
layout.top = padding_top
if hasattr(layout, 'width'):
layout.width = inner_w
if hasattr(layout, 'height'):
layout.height = inner_h
comp_data = {
'type': 'VerticalLayout',
'object': container,
'layout_obj': layout,
'left': x,
'top': y,
'width': width,
'height': height,
'layout_spacing': spacing,
'layout_padding_left': padding_left,
'layout_padding_right': padding_right,
'layout_padding_top': padding_top,
'layout_padding_bottom': padding_bottom,
'layout_align': 'start',
'layout_wrap': True,
'layout_line_spacing': 0.0,
'canvas_index': manager.current_canvas_index,
'name': f"VerticalLayout_{len(manager.components)}",
'sort': 1,
'draggable': True
}
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return container
def create_horizontal_layout(manager, x=100, y=100, width=300, height=200, spacing=0.0, parent=None):
"""Create a LUI Horizontal Layout"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
container = LUIObject(parent=parent, x=x, y=y)
if hasattr(container, 'width'):
container.width = width
if hasattr(container, 'height'):
container.height = height
if hasattr(container, 'solid'):
container.solid = True
layout = LUIHorizontalLayout(parent=container, spacing=spacing)
padding_left = 0.0
padding_right = 0.0
padding_top = 0.0
padding_bottom = 0.0
inner_w = max(1.0, float(width) - (padding_left + padding_right))
inner_h = max(1.0, float(height) - (padding_top + padding_bottom))
if hasattr(layout, 'left'):
layout.left = padding_left
if hasattr(layout, 'top'):
layout.top = padding_top
if hasattr(layout, 'width'):
layout.width = inner_w
if hasattr(layout, 'height'):
layout.height = inner_h
comp_data = {
'type': 'HorizontalLayout',
'object': container,
'layout_obj': layout,
'left': x,
'top': y,
'width': width,
'height': height,
'layout_spacing': spacing,
'layout_padding_left': padding_left,
'layout_padding_right': padding_right,
'layout_padding_top': padding_top,
'layout_padding_bottom': padding_bottom,
'layout_align': 'start',
'layout_wrap': True,
'layout_line_spacing': 0.0,
'canvas_index': manager.current_canvas_index,
'name': f"HorizontalLayout_{len(manager.components)}",
'sort': 1,
'draggable': True
}
comp_index = len(manager.components)
if hasattr(manager, '_setup_component_drag'):
manager._setup_component_drag(comp_data, comp_index)
if hasattr(manager, '_add_to_scene_tree'):
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return container
def create_checkbox(manager, label="Checkbox", x=100, y=100, parent=None):
"""Create a LUI Checkbox"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
chk = LUICheckbox(parent=parent, label=label, x=x, y=y)
comp_data = {
'type': 'Checkbox',
'object': chk,
'text': label,
'left': x,
'top': y,
'width': 120,
'height': 20,
'canvas_index': manager.current_canvas_index,
'name': f"Checkbox_{len(manager.components)}",
'checked': False,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return chk
def create_plane(manager, x=100, y=100, width=100, height=100, color=(1,1,1,1), parent=None):
"""Create a LUI Plane (using Sprite with blank texture)"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 创建LUIObject容器
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True # 重要设置为solid才能接收鼠标事件
# 设置组件层级在Canvas背景之上 (Plane)
if hasattr(obj, 'set_sort'):
obj.set_sort(1) # 在Canvas背景之上
# 创建Sprite作为平面背景
spr = LUISprite(obj, "blank", "skin")
spr.width = width
spr.height = height
spr.color = color
spr.left = 0
spr.top = 0
comp_data = {
'type': 'Plane',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Plane_{len(manager.components)}",
'color': color,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_image(manager, texture_path="blank", x=100, y=100, width=100, height=100, parent=None):
"""Create a LUI Image"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 创建LUIObject容器
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True # 重要设置为solid才能接收鼠标事件
# 设置组件层级在Canvas背景之上 (Image)
if hasattr(obj, 'set_sort'):
obj.set_sort(1) # 在Canvas背景之上
# 加载纹理 (使用皮肤中的'blank'或外部路径)
# 注意: LUISprite通常使用图集图像名称或纹理
# 对于外部图像可能需要加载纹理并使用不同的LUI元素
# 或将其注册到图集中。LUISprite(parent, image, atlas)
# 为了简单起见我们使用默认皮肤sprite它有'blank'
img_name = "blank"
spr = LUISprite(obj, img_name, "skin")
spr.width = width
spr.height = height
spr.left = 0
spr.top = 0
# 设置默认图像颜色(浅灰色,表示这是一个图像占位符)
spr.color = (1.0, 1.0, 1.0, 1.0)
comp_data = {
'type': 'Image',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Image_{len(manager.components)}",
'texture_path': texture_path,
'color': (1.0, 1.0, 1.0, 1.0),
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_video(manager, video_path="", x=100, y=100, width=320, height=240, parent=None):
"""Create a LUI Video Player component"""
if not manager.lui_enabled: return None
# 如果没有指定parent使用当前Canvas的panel作为parent
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
# 1. 创建容器对象
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True
# 设置层级
if hasattr(obj, 'set_sort'):
obj.set_sort(1)
# 2. 准备视频纹理
video_texture = None
if video_path:
try:
from panda3d.core import Filename, MovieTexture, MovieVideo, loadPrcFileData
# Trim whitespace which could cause "file not found" errors
video_path = video_path.strip()
# Allow network protocols
loadPrcFileData("", "ffmpeg-protocol-whitelist http,https,tcp,tls,file")
if "://" in video_path:
# 尝试更稳定的加载序列
try:
video_src = MovieVideo.get(Filename(video_path))
video_texture = MovieTexture("RemoteVideo")
if not video_texture.load(video_src):
video_texture = manager.world.loader.loadTexture(video_path)
except:
video_texture = manager.world.loader.loadTexture(video_path)
else:
video_texture = manager.world.loader.loadTexture(Filename.from_os_specific(video_path))
if video_texture:
print(f"✓ 视频加载成功: {video_path}")
# 尝试同步视频尺寸比例
orig_w = video_texture.getOrigFileXSize()
orig_h = video_texture.getOrigFileYSize()
if orig_w > 0 and orig_h > 0:
ratio = orig_w / orig_h
# 保持宽度,调整高度
new_height = width / ratio
obj.height = new_height
height = new_height # Update local var
except Exception as e:
print(f"⚠ 加载视频失败: {e}")
# 2.1 ???????????????????????????????????????
audio_sound = None
audio_path = ""
audio_from_video = False
if video_path and "://" not in video_path:
try:
audio_path = video_path
audio_sound = manager.world.loader.loadSfx(p3d.Filename.from_os_specific(video_path))
if audio_sound:
audio_from_video = True
if hasattr(audio_sound, 'setLoop'):
audio_sound.setLoop(True)
if hasattr(audio_sound, 'setVolume'):
audio_sound.setVolume(1.0)
except Exception:
audio_sound = None
audio_path = ""
# 3. 创建Sprite
if video_texture:
# 直接使用纹理创建不使用Atlas
spr = LUISprite(obj, video_texture)
spr.color = (1, 1, 1, 1) # Video needs white to show original colors
else:
# 使用默认黑色背景
spr = LUISprite(obj, "blank", "skin")
spr.color = (0.0, 0.0, 0.0, 1.0)
spr.width = width
spr.height = height
if video_texture:
obj.video_texture = video_texture # Prevent GC
# Ensure full UVs
if hasattr(spr, 'set_uv_range'):
spr.set_uv_range(0, 0, 1, 1)
# 如果支持,自动播放
if hasattr(video_texture, 'play'):
# Stop first to reset state, critical for some video formats
if hasattr(video_texture, 'stop'):
video_texture.stop()
video_texture.play()
if hasattr(video_texture, 'setLoop'):
video_texture.setLoop(True)
if audio_sound:
try:
if hasattr(audio_sound, 'stop'):
audio_sound.stop()
if hasattr(audio_sound, 'play'):
audio_sound.play()
except Exception:
pass
# 4. 创建简单的控制栏 (可选,这里先做个占位符)
# TODO: Play/Pause controls
comp_data = {
'type': 'Video',
'object': obj,
'sprite': spr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"Video_{len(manager.components)}",
'video_path': video_path,
'texture': video_texture,
'audio': audio_sound,
'audio_path': audio_path,
'audio_from_video': audio_from_video,
'volume': 1.0,
'is_playing': True,
'loop': True,
'sort': 1
}
# 设置拖拽功能
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
# 添加到层级树
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return obj
def create_http_text(manager, x=100, y=100, width=320, height=120, 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", parent=None):
"""Create a HTTP text component that fetches data from URL and renders it."""
if not manager.lui_enabled:
return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
obj = LUIObject(parent=parent, x=x, y=y)
obj.width = width
obj.height = height
obj.solid = True
if hasattr(obj, 'set_sort'):
obj.set_sort(1)
# Panel background
spr = LUISprite(obj, "blank", "skin")
spr.width = width
spr.height = height
spr.left = 0
spr.top = 0
spr.color = (0.08, 0.14, 0.20, 0.92)
# Text content
text_label = LUILabel(
parent=obj,
text="HTTP组件\n点击“立即请求”获取数据",
x=8,
y=8,
font_size=14,
wordwrap=True
)
text_label.color = (0.92, 0.95, 1.0, 1.0)
comp_data = {
'type': 'HttpText',
'object': obj,
'sprite': spr,
'text_label': text_label,
'text': "HTTP组件\n点击“立即请求”获取数据",
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"HttpText_{len(manager.components)}",
'sort': 1,
'color': (0.08, 0.14, 0.20, 0.92),
'text_color': (0.92, 0.95, 1.0, 1.0),
'error_color': (1.0, 0.40, 0.40, 1.0),
'text_padding': 8.0,
# HTTP settings
'http_url': url,
'http_method': 'GET',
'http_headers': '{}',
'http_body': '',
'http_timeout': 8.0,
'http_json_path': 'current',
'auto_refresh': True,
'refresh_interval': 60.0,
'max_chars': 300,
'http_status': '未请求',
'last_error': '',
'last_response_raw': '',
'last_refresh_time': 0.0,
# Runtime flags
'_http_inflight': False,
'_http_pending_result': None,
}
manager.luiFunction.sync_http_text_layout(manager, comp_data)
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
# Trigger first request in background
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
return obj
def sync_http_text_layout(manager, comp_data):
"""Keep HttpText internal label in sync with panel size and padding."""
if comp_data.get('type') != 'HttpText':
return
label = comp_data.get('text_label')
if label is None:
return
pad = float(comp_data.get('text_padding', 8.0))
width = float(comp_data.get('width', 100.0))
height = float(comp_data.get('height', 40.0))
inner_w = max(1.0, width - pad * 2.0)
inner_h = max(1.0, height - pad * 2.0)
try:
label.left = pad
label.top = pad
if hasattr(label, 'width'):
label.width = inner_w
if hasattr(label, 'height'):
label.height = inner_h
except Exception:
pass
def _extract_json_by_path(path_text, payload):
"""Extract value from JSON payload by a simple path: a.b[0].c"""
if not path_text:
return payload
path = str(path_text).strip()
if not path:
return payload
tokens = []
current = ""
i = 0
while i < len(path):
ch = path[i]
if ch == '.':
if current:
tokens.append(('key', current))
current = ""
i += 1
continue
if ch == '[':
if current:
tokens.append(('key', current))
current = ""
j = path.find(']', i + 1)
if j < 0:
raise ValueError(f"JSON路径非法: {path_text}")
idx_text = path[i + 1:j].strip()
if not idx_text:
raise ValueError(f"JSON路径非法: {path_text}")
tokens.append(('index', int(idx_text)))
i = j + 1
continue
current += ch
i += 1
if current:
tokens.append(('key', current))
node = payload
for token_type, token_val in tokens:
if token_type == 'key':
if not isinstance(node, dict):
raise KeyError(f"JSON路径错误期望对象: {token_val}")
node = node[token_val]
else:
if not isinstance(node, (list, tuple)):
raise KeyError(f"JSON路径错误期望数组: [{token_val}]")
node = node[token_val]
return node
def _set_http_text_display(manager, comp_data, text, is_error=False):
"""Update HttpText display text and color."""
msg = text if text is not None else ""
comp_data['text'] = msg
label = comp_data.get('text_label')
if label is None:
return
try:
label.text = msg
if is_error:
color = comp_data.get('error_color', (1.0, 0.4, 0.4, 1.0))
else:
color = comp_data.get('text_color', (0.92, 0.95, 1.0, 1.0))
if hasattr(label, 'set_color'):
label.set_color(color)
else:
label.color = color
except Exception:
pass
def trigger_http_request(manager, comp_data, force=False):
"""Start one async HTTP request for HttpText component."""
if comp_data.get('type') != 'HttpText':
return False
if comp_data.get('_http_inflight'):
return False
now_ts = time.time()
if not force:
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
last_ts = float(comp_data.get('last_refresh_time', 0.0))
if now_ts - last_ts < interval:
return False
url = str(comp_data.get('http_url', '')).strip()
if not url:
comp_data['http_status'] = "URL为空"
comp_data['last_error'] = "URL为空"
manager.luiFunction._set_http_text_display(manager, comp_data, "HTTP请求失败: URL为空", is_error=True)
return False
method = str(comp_data.get('http_method', 'GET')).upper().strip()
if method not in ('GET', 'POST'):
method = 'GET'
comp_data['http_method'] = 'GET'
timeout_sec = max(1.0, float(comp_data.get('http_timeout', 8.0)))
headers_raw = str(comp_data.get('http_headers', '{}'))
body_raw = str(comp_data.get('http_body', ''))
json_path = str(comp_data.get('http_json_path', '')).strip()
max_chars = max(32, int(comp_data.get('max_chars', 300)))
comp_data['_http_inflight'] = True
comp_data['http_status'] = "请求中..."
def _worker():
result = {
'ok': False,
'message': '',
'status_code': None,
'error': '',
'time': time.time(),
'raw': '',
}
try:
headers = {}
if headers_raw.strip():
parsed_headers = json.loads(headers_raw)
if not isinstance(parsed_headers, dict):
raise ValueError("Headers 必须是 JSON 对象")
headers = {str(k): str(v) for k, v in parsed_headers.items()}
data_bytes = None
if body_raw:
data_bytes = body_raw.encode('utf-8')
request = urllib.request.Request(
url=url,
data=data_bytes,
headers=headers,
method=method
)
with urllib.request.urlopen(request, timeout=timeout_sec) as response:
status_code = getattr(response, 'status', 200)
raw_bytes = response.read()
charset = None
response_headers = getattr(response, 'headers', None)
if response_headers is not None and hasattr(response_headers, 'get_content_charset'):
charset = response_headers.get_content_charset()
if not charset:
charset = "utf-8"
raw_text = raw_bytes.decode(charset, errors='replace')
display_text = raw_text.strip()
if json_path:
json_payload = json.loads(raw_text)
extracted = manager.luiFunction._extract_json_by_path(json_path, json_payload)
if isinstance(extracted, (dict, list)):
display_text = json.dumps(extracted, ensure_ascii=False)
else:
display_text = str(extracted)
if len(display_text) > max_chars:
display_text = display_text[:max_chars] + "..."
result['ok'] = True
result['message'] = display_text
result['status_code'] = status_code
result['raw'] = raw_text
except Exception as err:
result['error'] = str(err)
comp_data['_http_pending_result'] = result
thread = threading.Thread(target=_worker, daemon=True)
thread.start()
return True
def update_http_components(manager):
"""Consume async HTTP results and run auto-refresh."""
now_ts = time.time()
for comp_data in manager.components:
if comp_data.get('type') != 'HttpText':
continue
pending = comp_data.get('_http_pending_result')
if pending is not None:
comp_data['_http_pending_result'] = None
comp_data['_http_inflight'] = False
comp_data['last_refresh_time'] = float(pending.get('time', now_ts))
comp_data['last_response_raw'] = pending.get('raw', '')
if pending.get('ok'):
status_code = pending.get('status_code')
stamp = time.strftime("%H:%M:%S", time.localtime(comp_data['last_refresh_time']))
comp_data['http_status'] = f"HTTP {status_code} @ {stamp}"
comp_data['last_error'] = ''
manager.luiFunction._set_http_text_display(
manager,
comp_data,
pending.get('message', ''),
is_error=False
)
else:
error_msg = pending.get('error', '未知错误')
comp_data['last_error'] = error_msg
comp_data['http_status'] = "请求失败"
manager.luiFunction._set_http_text_display(
manager,
comp_data,
f"HTTP请求失败: {error_msg}",
is_error=True
)
# Auto refresh
if comp_data.get('auto_refresh', False) and not comp_data.get('_http_inflight'):
interval = max(1.0, float(comp_data.get('refresh_interval', 60.0)))
last_ts = float(comp_data.get('last_refresh_time', 0.0))
if now_ts - last_ts >= interval:
manager.luiFunction.trigger_http_request(manager, comp_data, force=True)
def create_progressbar(manager, value=50, x=100, y=100, width=200, parent=None):
"""Create a LUI Progressbar"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
pg = LUIProgressbar(parent=parent, width=width, value=value)
pg.left = x
pg.top = y
pg.solid = True # Ensure it receives input
if hasattr(pg, 'set_sort'):
pg.set_sort(1)
comp_data = {
'type': 'Progressbar',
'object': pg,
'left': x,
'top': y,
'width': width,
'height': 30, # default height
'canvas_index': manager.current_canvas_index,
'name': f"Progressbar_{len(manager.components)}",
'value': value,
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return pg
def create_selectbox(manager, options=None, x=100, y=100, width=200, parent=None):
"""Create a LUI Selectbox"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
if options is None:
options = [(0, "Option 1"), (1, "Option 2"), (2, "Option 3")]
sb = LUISelectbox(width=width, options=options, selected_option=0)
sb.parent = parent
sb.left = x
sb.top = y
sb.solid = True # Ensure it receives input
if hasattr(sb, 'set_sort'):
sb.set_sort(1)
comp_data = {
'type': 'Selectbox',
'object': sb,
'left': x,
'top': y,
'width': width,
'height': 30, # default height
'canvas_index': manager.current_canvas_index,
'name': f"Selectbox_{len(manager.components)}",
'options': options,
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sb
def create_scrollable_region(manager, x=100, y=100, width=200, height=200, parent=None):
"""Create a LUI Scrollable Region"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
sr = LUIScrollableRegion(parent=parent, width=width, height=height)
sr.left = x
sr.top = y
sr.solid = True # Ensure it receives input
if hasattr(sr, 'set_sort'):
sr.set_sort(1)
comp_data = {
'type': 'ScrollableRegion',
'object': sr,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"ScrollableRegion_{len(manager.components)}",
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return sr
def create_tabbed_frame(manager, x=100, y=100, width=300, height=200, parent=None):
"""Create a LUI Tabbed Frame"""
if not manager.lui_enabled: return None
if parent is None and manager.current_canvas_index >= 0:
parent = manager.canvases[manager.current_canvas_index]['panel']
elif parent is None:
parent = manager.overlay_root
tf = LUITabbedFrame(parent=parent, width=width, height=height)
tf.left = x
tf.top = y
tf.solid = True # Ensure it receives input
# Add default tabs
f1 = LUIFrame(width=width, height=height)
tf.add("Tab 1", f1)
f2 = LUIFrame(width=width, height=height)
tf.add("Tab 2", f2)
if hasattr(tf, 'set_sort'):
tf.set_sort(1)
comp_data = {
'type': 'TabbedFrame',
'object': tf,
'left': x,
'top': y,
'width': width,
'height': height,
'canvas_index': manager.current_canvas_index,
'name': f"TabbedFrame_{len(manager.components)}",
'sort': 1
}
comp_index = len(manager.components)
manager._setup_component_drag(comp_data, comp_index)
manager._add_to_scene_tree(comp_data)
manager.components.append(comp_data)
return tf