"""LUIManager tree/editor/layout mixin.""" import copy import struct from pathlib import Path import panda3d.core as p3d from imgui_bundle import imgui, imgui_ctx class LUIManagerEditorMixin: def _ensure_component_edit_sessions(self): if not hasattr(self, "_component_edit_sessions"): self._component_edit_sessions = {} return self._component_edit_sessions def _capture_component_snapshot(self, index): if index < 0 or index >= len(self.components): return None comp_data = self.components[index] excluded_keys = { "object", "widget", "sprite", "ui_node", "layout_obj", "texture", "texture_ref", "atlas_tex", "button_texture_ref", "button_texture_ref_normal", "button_texture_ref_hover", "button_texture_ref_pressed", "input_texture_ref", "video_texture", "keep_alive_node", "keep_alive", "_input_default_runtime", } snapshot = {} for key, value in comp_data.items(): if key in excluded_keys or key.endswith("_ref") or key.endswith("_tex"): continue if isinstance(value, (str, int, float, bool, type(None))): snapshot[key] = value continue if isinstance(value, (tuple, list, dict)): try: snapshot[key] = copy.deepcopy(value) except Exception: pass return snapshot def _component_snapshots_equal(self, before_snapshot, after_snapshot): before_snapshot = before_snapshot or {} after_snapshot = after_snapshot or {} return before_snapshot == after_snapshot def _begin_component_snapshot_session(self, index, session_key): sessions = self._ensure_component_edit_sessions() sessions.setdefault((index, session_key), self._capture_component_snapshot(index)) def _record_component_snapshot_command(self, index, before_snapshot, after_snapshot): if not hasattr(self, "world") or not hasattr(self.world, "command_manager") or not self.world.command_manager: return if self._component_snapshots_equal(before_snapshot, after_snapshot): return try: from core.Command_System import SnapshotStateCommand self.world.command_manager.record_command( SnapshotStateCommand( lambda state, target_index=index: self._apply_component_snapshot(target_index, state), before_snapshot, after_snapshot, ) ) except Exception: pass def _finish_component_snapshot_session(self, index, session_key): if not imgui.is_item_deactivated_after_edit(): return sessions = self._ensure_component_edit_sessions() before_snapshot = sessions.pop((index, session_key), None) if before_snapshot is None: return after_snapshot = self._capture_component_snapshot(index) self._record_component_snapshot_command(index, before_snapshot, after_snapshot) def _apply_component_change_with_history(self, index, callback): before_snapshot = self._capture_component_snapshot(index) callback() after_snapshot = self._capture_component_snapshot(index) self._record_component_snapshot_command(index, before_snapshot, after_snapshot) def _ensure_lui_structure_edit_sessions(self): if not hasattr(self, "_lui_structure_edit_sessions"): self._lui_structure_edit_sessions = {} return self._lui_structure_edit_sessions def _capture_canvas_snapshot(self, canvas_data): if not canvas_data: return None panel = canvas_data.get("panel") background = canvas_data.get("background") title_label = canvas_data.get("title_label") left = float(getattr(panel, "left", 0.0) or 0.0) if panel is not None else 0.0 top = float(getattr(panel, "top", 0.0) or 0.0) if panel is not None else 0.0 width = float(getattr(panel, "width", 0.0) or 0.0) if panel is not None else 0.0 height = float(getattr(panel, "height", 0.0) or 0.0) if panel is not None else 0.0 if panel is not None and hasattr(panel, "get_pos") and left == 0.0 and top == 0.0: try: pos = panel.get_pos() left = float(pos.x) top = float(pos.y) except Exception: pass return { "name": canvas_data.get("name"), "visible": bool(canvas_data.get("visible", True)), "fill_mode": bool(canvas_data.get("fill_mode", False)), "fixed": bool(canvas_data.get("fixed", False)), "margins": copy.deepcopy(canvas_data.get("margins", {})), "left": left, "top": top, "width": width, "height": height, "background_color": tuple(getattr(background, "color", (0.5, 0.5, 0.5, 1.0))) if background is not None else None, "title_visible": bool(getattr(title_label, "visible", True)) if title_label is not None else None, "title_text": getattr(title_label, "text", None) if title_label is not None else None, "title_pos": ( float(getattr(title_label, "left", 0.0) or 0.0), float(getattr(title_label, "top", 0.0) or 0.0), ) if title_label is not None else None, } def _capture_lui_structure_snapshot(self): return { "canvases": [ snapshot for snapshot in (self._capture_canvas_snapshot(canvas) for canvas in getattr(self, "canvases", [])) if snapshot is not None ], "components": [ snapshot for snapshot in (self._capture_component_snapshot(index) for index in range(len(getattr(self, "components", [])))) if snapshot is not None ], "current_canvas_index": int(getattr(self, "current_canvas_index", -1)), "selected_index": int(getattr(self, "selected_index", -1)), "root_order": copy.deepcopy(getattr(self, "root_order", [])), } def _lui_structure_snapshots_equal(self, before_snapshot, after_snapshot): before_snapshot = before_snapshot or {} after_snapshot = after_snapshot or {} return before_snapshot == after_snapshot def _record_lui_structure_snapshot_command(self, before_snapshot, after_snapshot): if not hasattr(self, "world") or not hasattr(self.world, "command_manager") or not self.world.command_manager: return if self._lui_structure_snapshots_equal(before_snapshot, after_snapshot): return try: from core.Command_System import SnapshotStateCommand self.world.command_manager.record_command( SnapshotStateCommand( self._apply_lui_structure_snapshot, before_snapshot, after_snapshot, ) ) except Exception: pass def _begin_lui_structure_snapshot_session(self, session_key): sessions = self._ensure_lui_structure_edit_sessions() sessions.setdefault(session_key, self._capture_lui_structure_snapshot()) def _finish_lui_structure_snapshot_session(self, session_key): sessions = self._ensure_lui_structure_edit_sessions() before_snapshot = sessions.pop(session_key, None) if before_snapshot is None: return after_snapshot = self._capture_lui_structure_snapshot() self._record_lui_structure_snapshot_command(before_snapshot, after_snapshot) def _apply_lui_structure_change_with_history(self, callback): if getattr(self, "_restoring_lui_structure", False): return callback() before_snapshot = self._capture_lui_structure_snapshot() result = callback() after_snapshot = self._capture_lui_structure_snapshot() self._record_lui_structure_snapshot_command(before_snapshot, after_snapshot) return result def _remove_lui_runtime_object(self, obj): if obj is None: return try: parent = getattr(obj, "parent", None) if parent is not None and hasattr(parent, "remove_child"): parent.remove_child(obj) return except Exception: pass for method_name in ("remove", "destroy", "detach_node", "hide"): try: method = getattr(obj, method_name, None) if method: method() break except Exception: pass def _clear_lui_structure_runtime(self): if hasattr(self, "_hide_resize_handles"): try: self._hide_resize_handles() except Exception: pass if hasattr(self, "debug_outlines") and isinstance(self.debug_outlines, dict): for borders in list(self.debug_outlines.values()): for border in borders or []: try: if hasattr(border, "remove"): border.remove() else: border.visible = False except Exception: pass self.debug_outlines = {} for comp_data in reversed(list(getattr(self, "components", []))): audio = comp_data.get("audio") if audio is not None: try: if hasattr(audio, "stop"): audio.stop() except Exception: pass ui_node = comp_data.get("ui_node") if ui_node is not None: try: if not ui_node.isEmpty(): ui_node.removeNode() except Exception: pass keep_alive_node = comp_data.get("keep_alive_node") if keep_alive_node is not None: try: if hasattr(keep_alive_node, "removeNode") and not keep_alive_node.isEmpty(): keep_alive_node.removeNode() except Exception: pass keep_alive = comp_data.get("keep_alive") if keep_alive is not None: try: if hasattr(keep_alive, "destroy"): keep_alive.destroy() elif hasattr(keep_alive, "removeNode") and not keep_alive.isEmpty(): keep_alive.removeNode() except Exception: pass self._remove_lui_runtime_object(comp_data.get("object")) for canvas_data in reversed(list(getattr(self, "canvases", []))): node = canvas_data.get("node") if node is not None: try: if not node.isEmpty(): node.removeNode() except Exception: pass self._remove_lui_runtime_object(canvas_data.get("panel")) self.components = [] self.canvases = [] self.current_canvas_index = -1 self.canvas_counter = 0 self.selected_index = -1 self._last_selected_index = -1 self.root_order = [] self.dragging_comp = None self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None self.dragging_index = -1 self.resizing_handle = None self.resize_start_pos = (0, 0) self.resize_start_bounds = {} self.root = getattr(self, "overlay_root", None) def _apply_canvas_snapshot(self, canvas_index, snapshot): if canvas_index < 0 or canvas_index >= len(self.canvases): return snapshot = snapshot or {} canvas_data = self.canvases[canvas_index] canvas_data["name"] = snapshot.get("name", canvas_data.get("name")) canvas_data["fill_mode"] = bool(snapshot.get("fill_mode", canvas_data.get("fill_mode", False))) canvas_data["fixed"] = bool(snapshot.get("fixed", canvas_data.get("fixed", False))) canvas_data["visible"] = bool(snapshot.get("visible", canvas_data.get("visible", True))) canvas_data["margins"] = copy.deepcopy(snapshot.get("margins", canvas_data.get("margins", {}))) panel = canvas_data.get("panel") background = canvas_data.get("background") title_label = canvas_data.get("title_label") canvas_node = canvas_data.get("node") if canvas_node is not None and hasattr(canvas_node, "setName"): try: canvas_node.setName(canvas_data["name"]) except Exception: pass if title_label is not None and hasattr(title_label, "text"): try: title_label.text = f"Canvas: {canvas_data['name']}" except Exception: pass if panel is None: return if canvas_data["fill_mode"] and hasattr(self, "_update_canvas_geometry"): try: self._update_canvas_geometry(canvas_data) except Exception: pass else: left = float(snapshot.get("left", 0.0) or 0.0) top = float(snapshot.get("top", 0.0) or 0.0) width = float(snapshot.get("width", getattr(panel, "width", 0.0)) or 0.0) height = float(snapshot.get("height", getattr(panel, "height", 0.0)) or 0.0) try: if hasattr(panel, "set_pos"): panel.set_pos(left, top) else: panel.left = left panel.top = top except Exception: pass try: if hasattr(panel, "width"): panel.width = width if hasattr(panel, "height"): panel.height = height if background is not None: background.width = width background.height = height except Exception: pass try: if canvas_data["visible"] and hasattr(panel, "show"): panel.show() elif not canvas_data["visible"] and hasattr(panel, "hide"): panel.hide() except Exception: pass if background is not None and snapshot.get("background_color") is not None: try: background.color = tuple(snapshot.get("background_color")) except Exception: pass if title_label is not None: try: title_text = snapshot.get("title_text") if title_text is not None: title_label.text = title_text title_pos = snapshot.get("title_pos") if title_pos is not None: if hasattr(title_label, "set_pos"): title_label.set_pos(title_pos[0], title_pos[1]) else: title_label.left = title_pos[0] title_label.top = title_pos[1] title_visible = snapshot.get("title_visible") if title_visible is not None: if title_visible and hasattr(title_label, "show"): title_label.show() elif not title_visible and hasattr(title_label, "hide"): title_label.hide() else: title_label.visible = bool(title_visible) except Exception: pass def _compute_lui_structure_absolute_position(self, index, component_snapshots, memo=None): if memo is None: memo = {} if index in memo: return memo[index] if index < 0 or index >= len(component_snapshots): return 0.0, 0.0 snapshot = component_snapshots[index] or {} left = float(snapshot.get("left", 0.0) or 0.0) top = float(snapshot.get("top", 0.0) or 0.0) parent_index = snapshot.get("parent_index") if parent_index is not None and parent_index >= 0: parent_left, parent_top = self._compute_lui_structure_absolute_position(parent_index, component_snapshots, memo) left += parent_left top += parent_top memo[index] = (left, top) return memo[index] def _resolve_lui_restore_parent(self, snapshot): parent_index = snapshot.get("parent_index") if parent_index is None or parent_index < 0 or snapshot.get("visual_parent_canvas"): return None if parent_index >= len(self.components): return None try: return self._resolve_parent_visual_object(self.components[parent_index]) except Exception: return self.components[parent_index].get("object") def _create_component_from_structure_snapshot(self, index, component_snapshots, memo=None): snapshot = component_snapshots[index] or {} canvas_index = snapshot.get("canvas_index") if canvas_index is not None and 0 <= canvas_index < len(self.canvases): self.current_canvas_index = canvas_index parent = self._resolve_lui_restore_parent(snapshot) abs_left, abs_top = self._compute_lui_structure_absolute_position(index, component_snapshots, memo) comp_type = snapshot.get("type") width = float(snapshot.get("width", 100.0) or 100.0) height = float(snapshot.get("height", 30.0) or 30.0) if comp_type == "Button": created = self.luiFunction.create_button(self, text=snapshot.get("text", "Button"), x=abs_left, y=abs_top, parent=parent) elif comp_type in ["Text", "Label"]: created = self.luiFunction.create_label( self, text=snapshot.get("text", "Text"), x=abs_left, y=abs_top, font_size=int(snapshot.get("font_size", 14) or 14), parent=parent, ) elif comp_type == "Checkbox": created = self.luiFunction.create_checkbox(self, label=snapshot.get("text", "Checkbox"), x=abs_left, y=abs_top, parent=parent) elif comp_type == "InputField": created = self.luiFunction.create_input_field( self, text=snapshot.get("value", snapshot.get("text", "")), x=abs_left, y=abs_top, width=width, parent=parent, ) elif comp_type == "Slider": created = self.luiFunction.create_slider(self, x=abs_left, y=abs_top, width=width, parent=parent) elif comp_type == "Frame": created = self.luiFunction.create_frame(self, x=abs_left, y=abs_top, width=width, height=height, parent=parent) elif comp_type == "Plane": created = self.luiFunction.create_plane( self, x=abs_left, y=abs_top, width=width, height=height, color=tuple(snapshot.get("color", (1.0, 1.0, 1.0, 1.0))), parent=parent, ) elif comp_type == "Image": created = self.luiFunction.create_image( self, texture_path=snapshot.get("texture_path", "blank"), x=abs_left, y=abs_top, width=width, height=height, parent=parent, ) elif comp_type == "Video": created = self.luiFunction.create_video( self, video_path=snapshot.get("video_path", ""), x=abs_left, y=abs_top, width=width, height=height, parent=parent, ) elif comp_type == "HttpText": created = self.luiFunction.create_http_text( self, x=abs_left, y=abs_top, width=width, height=height, url=snapshot.get("http_url", ""), parent=parent, ) elif comp_type == "Progressbar": created = self.luiFunction.create_progressbar( self, value=float(snapshot.get("value", 50.0) or 50.0), x=abs_left, y=abs_top, width=width, parent=parent, ) elif comp_type == "Selectbox": created = self.luiFunction.create_selectbox( self, options=copy.deepcopy(snapshot.get("options", [])), x=abs_left, y=abs_top, width=width, parent=parent, ) elif comp_type == "ScrollableRegion": created = self.luiFunction.create_scrollable_region( self, x=abs_left, y=abs_top, width=width, height=height, parent=parent, ) elif comp_type == "TabbedFrame": created = self.luiFunction.create_tabbed_frame( self, x=abs_left, y=abs_top, width=width, height=height, parent=parent, ) elif comp_type == "VerticalLayout": created = self.luiFunction.create_vertical_layout( self, x=abs_left, y=abs_top, width=width, height=height, spacing=float(snapshot.get("layout_spacing", 0.0) or 0.0), parent=parent, ) elif comp_type == "HorizontalLayout": created = self.luiFunction.create_horizontal_layout( self, x=abs_left, y=abs_top, width=width, height=height, spacing=float(snapshot.get("layout_spacing", 0.0) or 0.0), parent=parent, ) else: raise ValueError(f"Unsupported LUI component type: {comp_type}") if created is None: raise RuntimeError(f"Failed to recreate LUI component: {comp_type}") return len(self.components) - 1 def _restore_lui_structure_postprocess(self): for index, comp_data in enumerate(self.components): comp_type = comp_data.get("type") if comp_type in ["VerticalLayout", "HorizontalLayout"] and hasattr(self, "_update_layout_inner"): try: self._update_layout_inner(index) except Exception: pass if comp_type == "ScrollableRegion" and hasattr(self, "_reparent_scrollable_children"): try: self._reparent_scrollable_children(index) except Exception: pass for index, comp_data in enumerate(self.components): if comp_data.get("layout_mode") == "fill" and hasattr(self, "_apply_fill_layout"): try: self._apply_fill_layout(index) except Exception: pass for index, comp_data in enumerate(self.components): if comp_data.get("type") in ["HorizontalLayout", "VerticalLayout"] and comp_data.get("layout_wrap", True): try: self._apply_wrap_layout(index) except Exception: pass root_indices = [ index for index, comp_data in enumerate(self.components) if comp_data.get("parent_index") is None or comp_data.get("parent_index") < 0 ] for index in root_indices: try: self._sync_canvas_children(index) except Exception: pass try: self._update_anchored_children(index) except Exception: pass def _apply_lui_structure_snapshot(self, snapshot): if getattr(self, "_restoring_lui_structure", False): return self._restoring_lui_structure = True try: snapshot = snapshot or {} canvas_snapshots = snapshot.get("canvases", []) or [] component_snapshots = snapshot.get("components", []) or [] self._clear_lui_structure_runtime() for canvas_snapshot in canvas_snapshots: self.luiFunction.create_canvas(self, name=canvas_snapshot.get("name")) self._apply_canvas_snapshot(len(self.canvases) - 1, canvas_snapshot) abs_pos_memo = {} for index in range(len(component_snapshots)): self._create_component_from_structure_snapshot(index, component_snapshots, abs_pos_memo) for index, comp_snapshot in enumerate(component_snapshots): if index >= len(self.components): break self._apply_component_snapshot(index, comp_snapshot) restored_root_order = snapshot.get("root_order", []) or [] self.root_order = [index for index in restored_root_order if 0 <= index < len(self.components)] if not self.root_order: self.root_order = [ index for index, comp_data in enumerate(self.components) if comp_data.get("parent_index") is None or comp_data.get("parent_index") < 0 ] self._restore_lui_structure_postprocess() target_canvas = snapshot.get("current_canvas_index", -1) if 0 <= target_canvas < len(self.canvases): self.switch_canvas(target_canvas) elif self.canvases: self.switch_canvas(0) else: self.current_canvas_index = -1 selected_index = snapshot.get("selected_index", -1) if 0 <= selected_index < len(self.components): self.selected_index = selected_index self._last_selected_index = selected_index if hasattr(self, "_show_and_update_handles"): try: self._show_and_update_handles(self.components[selected_index]) except Exception: pass else: self.selected_index = -1 self._last_selected_index = -1 self.canvas_counter = len(self.canvases) if hasattr(self.world, "updateSceneTree"): try: self.world.updateSceneTree() except Exception: pass finally: self._restoring_lui_structure = False def _create_canvas_with_history(self, name=None): return self._apply_lui_structure_change_with_history( lambda: self.luiFunction.create_canvas(self, name=name) ) def _apply_component_snapshot(self, index, snapshot): if index < 0 or index >= len(self.components): return comp_data = self.components[index] removable_state_keys = { "button_texture_path", "button_texture_path_normal", "button_texture_path_hover", "button_texture_path_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", "input_texture_path", "input_atlas_uv", "input_image_size", "current_texture", "atlas_uv", "image_size", "keep_alive", } preserved_refs = {} for key in ( "object", "widget", "sprite", "ui_node", "layout_obj", "texture", "texture_ref", "atlas_tex", "button_texture_ref", "button_texture_ref_normal", "button_texture_ref_hover", "button_texture_ref_pressed", "input_texture_ref", "video_texture", "keep_alive_node", "keep_alive", "_input_default_runtime", ): if key in comp_data: preserved_refs[key] = comp_data[key] snapshot = snapshot or {} for key in removable_state_keys: if key not in snapshot and key in comp_data: comp_data.pop(key, None) for key, value in snapshot.items(): try: comp_data[key] = copy.deepcopy(value) except Exception: comp_data[key] = value for key, value in preserved_refs.items(): comp_data[key] = value self._sync_component_from_snapshot(index) def _sync_component_from_snapshot(self, index): if index < 0 or index >= len(self.components): return comp_data = self.components[index] comp_obj = comp_data.get("object") comp_type = comp_data.get("type") if comp_obj is None: return text_value = comp_data.get("text") if text_value is not None: try: if comp_type == "Checkbox" and hasattr(comp_obj, "label") and hasattr(comp_obj.label, "text"): comp_obj.label.text = text_value elif comp_type == "HttpText" and comp_data.get("text_label") is not None and hasattr(comp_data["text_label"], "text"): comp_data["text_label"].text = text_value elif hasattr(comp_obj, "set_text"): comp_obj.set_text(text_value) elif hasattr(comp_obj, "text"): comp_obj.text = text_value except Exception: pass if "font_size" in comp_data: font_size = int(comp_data.get("font_size", 14)) try: if hasattr(comp_obj, '_text') and hasattr(comp_obj._text, 'font_size'): comp_obj._text.font_size = font_size if hasattr(comp_obj, '_shadow_text') and hasattr(comp_obj._shadow_text, 'font_size'): comp_obj._shadow_text.font_size = font_size if hasattr(comp_obj, 'text_handle') and hasattr(comp_obj.text_handle, 'font_size'): comp_obj.text_handle.font_size = font_size except Exception: pass color = comp_data.get("color") if color is not None: try: if comp_type == "Button": self.luiFunction._apply_button_layout_color(self, comp_obj, tuple(color)) elif comp_type in ["Text", "Label"]: self.luiFunction._apply_text_color(self, comp_obj, tuple(color)) elif comp_type in ["Plane", "Image"] and comp_data.get("sprite") is not None: comp_data["sprite"].color = tuple(color) elif comp_type == "Frame" and hasattr(comp_obj, "set_color"): comp_obj.set_color(tuple(color)) except Exception: pass input_color = comp_data.get("input_color") if input_color is not None: try: 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(input_color) except Exception: pass if "value" in comp_data: try: if comp_type == "Slider" and hasattr(comp_obj, "set_value"): comp_obj.set_value(comp_data["value"]) elif hasattr(comp_obj, "value"): comp_obj.value = comp_data["value"] except Exception: pass if "checked" in comp_data and hasattr(comp_obj, "checked"): try: comp_obj.checked = comp_data["checked"] except Exception: pass if "selected_option_id" in comp_data: try: options = comp_data.get("options", []) 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.get("selected_option_id")) except Exception: pass layout_mode = comp_data.get("layout_mode", "manual") if layout_mode != "fill": target_left = float(comp_data.get("left", 0.0) or 0.0) target_top = float(comp_data.get("top", 0.0) or 0.0) parent_index = comp_data.get("parent_index") if parent_index is not None and parent_index >= 0 and comp_data.get("visual_parent_canvas"): try: parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) target_left += parent_abs_left target_top += parent_abs_top except Exception: pass try: if hasattr(comp_obj, "set_pos"): comp_obj.set_pos(target_left, target_top) else: if hasattr(comp_obj, "left"): comp_obj.left = target_left if hasattr(comp_obj, "top"): comp_obj.top = target_top except Exception: pass if "width" in comp_data: width = float(comp_data["width"]) try: if hasattr(comp_obj, "width"): comp_obj.width = width if comp_data.get("sprite") is not None: comp_data["sprite"].width = width if comp_type in ["Slider", "InputField"] and hasattr(comp_obj, "set_width"): comp_obj.set_width(width) except Exception: pass if "height" in comp_data: height = float(comp_data["height"]) try: if hasattr(comp_obj, "height"): comp_obj.height = height if comp_data.get("sprite") is not None: comp_data["sprite"].height = height if comp_type == "Button" and hasattr(comp_obj, "_apply_stretch_sizes"): comp_obj._apply_stretch_sizes() if comp_type in ["Slider", "InputField"] and hasattr(comp_obj, "set_height"): comp_obj.set_height(height) except Exception: pass if "z_offset" in comp_data: z_offset = float(comp_data["z_offset"]) try: if hasattr(comp_obj, "set_z_offset"): comp_obj.set_z_offset(z_offset) elif hasattr(comp_obj, "z_offset"): comp_obj.z_offset = z_offset except Exception: pass if layout_mode == "fill" and hasattr(self, "_apply_fill_layout"): try: self._apply_fill_layout(index) except Exception: pass if comp_type == "HttpText" and hasattr(self.luiFunction, "sync_http_text_layout"): try: text_label = comp_data.get("text_label") if text_label is not None: if "text_color" in comp_data and hasattr(text_label, "color"): text_label.color = tuple(comp_data.get("text_color", (1.0, 1.0, 1.0, 1.0))) if "text" in comp_data and hasattr(text_label, "text"): text_label.text = comp_data["text"] self.luiFunction.sync_http_text_layout(self, comp_data) except Exception: pass if hasattr(self.luiFunction, "_reapply_component_resource_state"): try: self.luiFunction._reapply_component_resource_state(self, comp_data, comp_obj, comp_type) except Exception as e: print(f"LUI资源状态回放失败: {e}") if comp_type in ["VerticalLayout", "HorizontalLayout"] and hasattr(self, "_update_layout_inner"): try: self._update_layout_inner(index) except Exception: pass parent_index = comp_data.get("parent_index") try: if parent_index is not None and parent_index >= 0 and hasattr(self, "_update_anchored_children"): self._update_anchored_children(parent_index) if hasattr(self, "_update_anchored_children"): self._update_anchored_children(index) except Exception: pass 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): if not self._is_valid_relationship_index(child_index, parent_index): print("Error: invalid component index") return child_data = self.components[child_index] parent_data = self.components[parent_index] if not self._prepare_parent_child_link(child_index, parent_index, child_data, parent_data): return # 编排入口:仅更新逻辑父子关系,不执行真实 reparent,避免破坏复杂组件内部结构。 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})") self._remove_child_from_old_parent(child_data, child_index) self._attach_child_to_new_parent(child_data, parent_data, child_index, parent_index) new_local_left, new_local_top = self._compute_new_child_local_position( child_index, parent_index, current_abs_left, current_abs_top ) child_data['left'] = new_local_left child_data['top'] = new_local_top child_obj = child_data['object'] parent_obj = self._resolve_parent_visual_object(parent_data) try: self._set_child_visual_parent_policy(child_data, parent_data) if not keep_world: new_local_left, new_local_top = self._clamp_child_local_within_parent( child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top ) child_data['left'] = new_local_left child_data['top'] = new_local_top self._sync_child_canvas_index_with_parent(child_data, parent_data) self._apply_child_absolute_position(child_obj, current_abs_left, current_abs_top, keep_world) self._ensure_child_visibility_and_layer(child_data, parent_data, child_obj, parent_obj) except Exception as e: print(f"Reparenting error: {e}") self._finalize_parent_child_link(child_index, parent_index, parent_data) def _is_valid_relationship_index(self, child_index, parent_index): return ( 0 <= child_index < len(self.components) and 0 <= parent_index < len(self.components) ) def _prepare_parent_child_link(self, child_index, parent_index, child_data, parent_data): 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 False return True def _remove_child_from_old_parent(self, child_data, child_index): old_parent_index = child_data.get('parent_index') if old_parent_index is None or old_parent_index < 0: return 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) def _attach_child_to_new_parent(self, child_data, parent_data, child_index, parent_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) def _compute_new_child_local_position(self, child_index, parent_index, current_abs_left, current_abs_top): 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})") return new_local_left, new_local_top def _resolve_parent_visual_object(self, parent_data): parent_obj = parent_data['object'] parent_type = parent_data.get('type') if parent_type == 'ScrollableRegion': return parent_data['object'].content_node if parent_type in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): if parent_data.get('layout_wrap', True): return parent_data.get('object') return parent_data.get('layout_obj').cell() return parent_obj def _set_child_visual_parent_policy(self, child_data, parent_data): visual_container_types = [ 'Frame', 'Plane', 'Video', 'HttpText', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout', ] if parent_data.get('type') in visual_container_types: child_data['visual_parent_canvas'] = False print("[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") else: # 非视觉容器保持画布父级显示,避免被父组件裁剪。 child_data['visual_parent_canvas'] = True def _clamp_child_local_within_parent( self, child_data, parent_data, child_obj, parent_obj, new_local_left, new_local_top ): 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)) except Exception: pass return new_local_left, new_local_top def _sync_child_canvas_index_with_parent(self, child_data, parent_data): if not child_data.get('visual_parent_canvas'): return try: child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) except Exception: pass def _apply_child_absolute_position(self, child_obj, current_abs_left, current_abs_top, keep_world): if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top if keep_world: print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") else: print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") def _ensure_child_visibility_and_layer(self, child_data, parent_data, child_obj, parent_obj): 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 comp_type = child_data.get('type') if comp_type == 'Button': self._ensure_button_child_visibility(child_obj) elif comp_type == 'Frame': self._ensure_frame_child_visibility(child_obj) elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']: self._ensure_media_child_visibility(child_data) self._ensure_child_sprite_visibility_and_layer(child_data, parent_data, child_obj) if hasattr(child_obj, 'show'): child_obj.show() def _ensure_button_child_visibility(self, child_obj): 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 if lbl is not None and hasattr(lbl, 'show'): lbl.show() except Exception: pass def _ensure_frame_child_visibility(self, child_obj): try: if hasattr(child_obj, 'show'): child_obj.show() if hasattr(child_obj, 'visible'): child_obj.visible = True print("[_set_parent_child_relationship] Frame visibility ensured") except Exception as e: print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") def _ensure_media_child_visibility(self, child_data): try: spr = child_data.get('sprite') if spr is not None: if hasattr(spr, 'show'): spr.show() if hasattr(spr, 'visible'): spr.visible = True if child_data.get('type') == 'Plane' and hasattr(spr, 'color'): spr.color = child_data.get('color', (1, 1, 1, 1)) 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}") def _ensure_child_sprite_visibility_and_layer(self, child_data, parent_data, child_obj): child_sprite = child_data.get('sprite') if child_sprite is None: return try: if hasattr(child_sprite, 'show'): child_sprite.show() if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: child_sprite.parent = child_obj 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_z = 0.0 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) child_sprite.z_offset = max( parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0, ) except Exception: pass def _finalize_parent_child_link(self, child_index, parent_index, parent_data): 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}") before_snapshot = None if not getattr(self, "_restoring_lui_structure", False): before_snapshot = self._capture_lui_structure_snapshot() 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}") if before_snapshot is not None: self._record_lui_structure_snapshot_command( before_snapshot, self._capture_lui_structure_snapshot(), ) 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 hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): self.show_editor = bool(self.world.showLUIEditor) 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编辑器", self.show_editor) as window: self.show_editor = bool(window.opened) if hasattr(self, "world") and hasattr(self.world, "set_panel_visible"): self.world.set_panel_visible("lui_editor", window.opened) elif hasattr(self, "world") and hasattr(self.world, "showLUIEditor"): self.world.showLUIEditor = bool(window.opened) if not window.opened: return if not window.expanded: 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._create_canvas_with_history() 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: self._apply_lui_structure_change_with_history( lambda: ( canvas.__setitem__('visible', visible), panel.show() if visible 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: def apply_fill_mode(): canvas['fill_mode'] = fill_mode if fill_mode: self._update_canvas_geometry(canvas) self._apply_lui_structure_change_with_history(apply_fill_mode) 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 imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_margin_left") if changed: geometry_changed = True self._finish_lui_structure_snapshot_session("canvas_margin_left") changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_margin_right") if changed: geometry_changed = True self._finish_lui_structure_snapshot_session("canvas_margin_right") changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_margin_top") if changed: geometry_changed = True self._finish_lui_structure_snapshot_session("canvas_margin_top") changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_margin_bottom") if changed: geometry_changed = True self._finish_lui_structure_snapshot_session("canvas_margin_bottom") 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: self._apply_lui_structure_change_with_history( lambda: canvas.__setitem__('fixed', fixed) ) if not is_fixed: changed, pos = imgui.drag_float2("Canvas位置", curr_pos) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_position") if changed: panel.set_pos(pos[0], pos[1]) self._finish_lui_structure_snapshot_session("canvas_position") # Manual Size curr_size = (panel.get_width(), panel.get_height()) changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_size") 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] self._finish_lui_structure_snapshot_session("canvas_size") # 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 imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_bg_color") if changed: bg.color = (new_color[0], new_color[1], new_color[2], new_color[3]) self._finish_lui_structure_snapshot_session("canvas_bg_color") # 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: def apply_title_visible(): try: if title_visible: title.show() else: title.hide() except Exception: title.visible = title_visible self._apply_lui_structure_change_with_history(apply_title_visible) # Text content curr_text = title.text changed, new_text = imgui.input_text("标题文本", curr_text, 128) if imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_title_text") if changed: title.text = new_text self._finish_lui_structure_snapshot_session("canvas_title_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 imgui.is_item_activated(): self._begin_lui_structure_snapshot_session("canvas_title_pos") if changed: title.set_pos(new_pos[0], new_pos[1]) self._finish_lui_structure_snapshot_session("canvas_title_pos") 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.clearSelection() # 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._apply_lui_structure_change_with_history( lambda: 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: def reorder_root(): 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) self._apply_lui_structure_change_with_history(reorder_root) else: def reorder_children(): parent_data = self.components[src_parent] children = parent_data.get('children_indices', []) parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx) self._apply_lui_structure_change_with_history(reorder_children) else: self._apply_lui_structure_change_with_history( lambda: 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._apply_lui_structure_change_with_history( lambda target_idx=selected_idx, target_pos=pos: self._update_component_anchor_position( target_idx, target_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中心""" before_snapshot = None if not getattr(self, "_restoring_lui_structure", False): before_snapshot = self._capture_lui_structure_snapshot() # 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() if before_snapshot is not None: self._record_lui_structure_snapshot_command( before_snapshot, self._capture_lui_structure_snapshot(), ) except Exception as e: print(f"创建组件失败: {str(e)}") def set_component_anchor(self, comp_index, anchor_position): """设置组件锚点 - 保持向后兼容""" self._apply_lui_structure_change_with_history( lambda: 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 self._apply_lui_structure_change_with_history( lambda target_idx=selected_idx, target_pos=pos: self._update_component_anchor_position( target_idx, target_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()