"""LUIManager tree/editor/layout mixin.""" import struct from pathlib import Path import panda3d.core as p3d from imgui_bundle import imgui, imgui_ctx class LUIManagerEditorMixin: def _add_to_scene_tree(self, comp_data): """Add a virtual node to represent the LUI component in the scene tree""" from panda3d.core import NodePath node_name = f"UI_{comp_data['type']}_{len(self.components)}" ui_node = NodePath(node_name) # Reparent to current canvas node if available if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_node = self.canvases[self.current_canvas_index]['node'] ui_node.reparent_to(canvas_node) comp_data['canvas_index'] = self.current_canvas_index else: ui_node.reparent_to(self.world.render) comp_data['canvas_index'] = None comp_data['ui_node'] = ui_node comp_data['parent_index'] = None comp_data['children_indices'] = [] def _set_parent_child_relationship(self, child_index, parent_index, anchor_position=None, keep_world=False): # Set parent-child relationship if (child_index < 0 or child_index >= len(self.components) or parent_index < 0 or parent_index >= len(self.components)): print("Error: invalid component index") return child_data = self.components[child_index] parent_data = self.components[parent_index] if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_data['draggable'] = False if child_data.get('parent_index') == parent_index: print(f"Component {child_index} already under {parent_index}") return current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) print(f"[_set_parent_child_relationship] Child #{child_index} -> Parent #{parent_index}") print(f" Current absolute position: ({current_abs_left:.1f}, {current_abs_top:.1f})") old_parent_index = child_data.get('parent_index') if old_parent_index is not None and old_parent_index >= 0: old_parent_data = self.components[old_parent_index] if child_index in old_parent_data.get('children_indices', []): old_parent_data['children_indices'].remove(child_index) child_data['parent_index'] = parent_index if 'children_indices' not in parent_data: parent_data['children_indices'] = [] if child_index not in parent_data['children_indices']: parent_data['children_indices'].append(child_index) parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) new_local_left = current_abs_left - parent_abs_left new_local_top = current_abs_top - parent_abs_top print(f" Parent absolute position: ({parent_abs_left:.1f}, {parent_abs_top:.1f})") print(f" Calculated local position: ({new_local_left:.1f}, {new_local_top:.1f})") # 在数据结构中存储局部坐标(相对于父组件) child_data['left'] = new_local_left child_data['top'] = new_local_top child_obj = child_data['object'] parent_obj = parent_data['object'] parent_type = parent_data.get('type') visual_container_types = [ 'Frame', 'Plane', 'Video', 'HttpText', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout' ] if parent_data.get('type') == 'ScrollableRegion': parent_obj = parent_data['object'].content_node elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): if parent_data.get('layout_wrap', True): parent_obj = parent_data.get('object') else: parent_obj = parent_data.get('layout_obj').cell() try: if parent_type in visual_container_types: child_data['visual_parent_canvas'] = False # 关键修改:不要调用 reparent_to(),避免破坏内部结构 # 只更新数据结构,保持原有的父对象关系 print(f"[_set_parent_child_relationship] Setting parent relationship (data only, no reparenting)") else: # Keep visual parent on canvas to avoid clipping, but keep logical parent child_data['visual_parent_canvas'] = True # If not preserving world position, clamp into parent bounds for visibility if not keep_world: try: parent_w = parent_data.get('width') parent_h = parent_data.get('height') if parent_w is None and hasattr(parent_obj, 'width'): parent_w = parent_obj.width if parent_h is None and hasattr(parent_obj, 'height'): parent_h = parent_obj.height child_w = child_data.get('width') child_h = child_data.get('height') if child_w is None and hasattr(child_obj, 'width'): child_w = child_obj.width if child_h is None and hasattr(child_obj, 'height'): child_h = child_obj.height if parent_w and parent_h and child_w and child_h: max_x = max(0.0, float(parent_w) - float(child_w)) max_y = max(0.0, float(parent_h) - float(child_h)) new_local_left = max(0.0, min(float(new_local_left), max_x)) new_local_top = max(0.0, min(float(new_local_top), max_y)) child_data['left'] = new_local_left child_data['top'] = new_local_top except Exception: pass # 关键修改:由于我们不实际重新父化对象,所有组件都保持相对于Canvas的绝对位置 # 只在数据结构中记录相对于父组件的局部坐标 if child_data.get('visual_parent_canvas'): # Keep world position try: child_data['canvas_index'] = parent_data.get('canvas_index', self.current_canvas_index) except Exception: pass # 根据keep_world参数决定是否保持世界位置 if keep_world: # 保持世界位置:使用绝对坐标 if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_child_relationship] Child position kept at absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") else: # 不保持世界位置:使用局部坐标(但由于没有实际重新父化,这会导致位置跳变) # 为了避免跳变,我们仍然使用绝对坐标 if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_child_relationship] Child position set to absolute: ({current_abs_left:.1f}, {current_abs_top:.1f})") # Ensure child visuals are visible above parent if hasattr(child_obj, 'z_offset'): try: parent_z = getattr(parent_obj, 'z_offset', 0) child_obj.z_offset = float(parent_z) + 2.0 except Exception: pass # Special handling for Button: raise internal sprites/label above parent if child_data.get('type') == 'Button': try: layout = getattr(child_obj, '_layout', None) if layout is not None: for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): spr = getattr(layout, attr, None) if spr is not None and hasattr(spr, 'z_offset'): spr.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 1.0 lbl = getattr(child_obj, '_label', None) if lbl is not None and hasattr(lbl, 'z_offset'): lbl.z_offset = float(getattr(child_obj, 'z_offset', 0)) + 2.0 # Ensure label visible if lbl is not None and hasattr(lbl, 'show'): lbl.show() except Exception: pass # Special handling for Frame: ensure visibility elif child_data.get('type') == 'Frame': try: if hasattr(child_obj, 'show'): child_obj.show() if hasattr(child_obj, 'visible'): child_obj.visible = True print(f"[_set_parent_child_relationship] Frame visibility ensured") except Exception as e: print(f"[_set_parent_child_relationship] Error ensuring Frame visibility: {e}") # Special handling for Plane, Image, Video: ensure sprite visibility elif child_data.get('type') in ['Plane', 'Image', 'Video', 'HttpText']: try: spr = child_data.get('sprite') if spr is not None: if hasattr(spr, 'show'): spr.show() if hasattr(spr, 'visible'): spr.visible = True # Ensure sprite color is not transparent if child_data.get('type') == 'Plane': color = child_data.get('color', (1, 1, 1, 1)) if hasattr(spr, 'color'): spr.color = color print(f"[_set_parent_child_relationship] {child_data.get('type')} sprite visibility ensured") except Exception as e: print(f"[_set_parent_child_relationship] Error ensuring {child_data.get('type')} visibility: {e}") child_sprite = child_data.get('sprite') if child_sprite is not None: try: if hasattr(child_sprite, 'show'): child_sprite.show() # Ensure sprite is attached to the child object if hasattr(child_sprite, 'parent') and child_sprite.parent != child_obj: child_sprite.parent = child_obj # Restore sprite color if stored if hasattr(child_sprite, 'color') and 'color' in child_data: child_sprite.color = child_data.get('color', child_sprite.color) if hasattr(child_sprite, 'z_offset'): parent_sprite = parent_data.get('sprite') parent_sprite_z = 0.0 if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): parent_sprite_z = float(parent_sprite.z_offset) child_sprite.z_offset = max(parent_sprite_z + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) except Exception: pass # Keep child visible above parent visuals if hasattr(child_obj, 'z_offset'): parent_z = getattr(parent_obj, 'z_offset', 0) try: child_obj.z_offset = float(parent_z) + 2.0 except Exception: pass # Ensure child sprite renders above parent visuals child_sprite = child_data.get('sprite') if child_sprite is not None and hasattr(child_sprite, 'z_offset'): parent_sprite_z = 0 try: parent_sprite = parent_data.get('sprite') if parent_sprite is not None and hasattr(parent_sprite, 'z_offset'): parent_sprite_z = float(parent_sprite.z_offset) except Exception: parent_sprite_z = 0 try: child_sprite.z_offset = max(float(parent_sprite_z) + 1.0, float(getattr(child_obj, 'z_offset', 0)) + 1.0) except Exception: pass if hasattr(child_obj, 'show'): child_obj.show() except Exception as e: print(f"Reparenting error: {e}") # Ensure canvas_index follows parent canvas (visibility) parent_canvas = parent_data.get('canvas_index') if parent_canvas is not None: self._update_canvas_index_recursive(child_index, parent_canvas) if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): self._apply_wrap_layout(parent_index) print(f"Set parent: child #{child_index} -> parent #{parent_index}") def _set_parent_root(self, child_index, keep_world=False): """Detach a component from its parent and move it to root (canvas).""" if child_index < 0 or child_index >= len(self.components): print("Error: invalid component index") return child_data = self.components[child_index] # 保存当前的累积位置(相对于Canvas的绝对位置) current_abs_left, current_abs_top = self._get_component_accumulated_pos(child_index) print(f"[_set_parent_root] Component #{child_index} current accumulated pos: ({current_abs_left:.1f}, {current_abs_top:.1f})") old_parent_index = child_data.get('parent_index') if old_parent_index is not None and old_parent_index >= 0: old_parent_data = self.components[old_parent_index] if child_index in old_parent_data.get('children_indices', []): old_parent_data['children_indices'].remove(child_index) # 更新数据结构 child_data['parent_index'] = None child_data['visual_parent_canvas'] = False child_data['draggable'] = True # 获取Canvas panel parent_obj = None canvas_index = self.current_canvas_index if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): parent_obj = self.canvases[canvas_index]['panel'] child_data['canvas_index'] = canvas_index else: parent_obj = getattr(self, 'overlay_root', None) or getattr(self.world, 'render', None) child_obj = child_data.get('object') # 关键修改:对于 Button 等复杂组件,不要重新父化! # 只更新位置和数据结构 if child_obj is not None: # 更新位置数据 if keep_world: child_data['left'] = current_abs_left child_data['top'] = current_abs_top # 直接设置对象位置,不改变父对象 if hasattr(child_obj, 'set_pos'): child_obj.set_pos(current_abs_left, current_abs_top) else: if hasattr(child_obj, 'left'): child_obj.left = current_abs_left if hasattr(child_obj, 'top'): child_obj.top = current_abs_top print(f"[_set_parent_root] Set position to: ({current_abs_left:.1f}, {current_abs_top:.1f}) without reparenting") else: # 不保持世界位置:使用原有的局部坐标 if hasattr(child_obj, 'left'): child_obj.left = child_data.get('left', 0) if hasattr(child_obj, 'top'): child_obj.top = child_data.get('top', 0) # 确保可见性 if child_obj is not None: try: if hasattr(child_obj, 'show'): child_obj.show() elif hasattr(child_obj, 'visible'): child_obj.visible = True except Exception: pass # 对于 Button,确保内部组件可见 comp_type = child_data.get('type') if comp_type == 'Button' and child_obj is not None: try: layout = getattr(child_obj, '_layout', None) lbl = getattr(child_obj, '_label', None) if layout is not None: if hasattr(layout, 'show'): layout.show() if hasattr(layout, 'visible'): layout.visible = True if lbl is not None: if hasattr(lbl, 'show'): lbl.show() if hasattr(lbl, 'visible'): lbl.visible = True # 重新设置文本 current_text = child_data.get('text', 'Button') if hasattr(lbl, 'set_text'): lbl.set_text(current_text) elif hasattr(lbl, 'text'): lbl.text = current_text print(f"[_set_parent_root] Button components visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring button visibility: {e}") # 对于 Plane, Image, Video - 确保内部 sprite 可见 elif comp_type in ['Plane', 'Image', 'Video', 'HttpText'] and child_obj is not None: try: spr = child_data.get('sprite') if spr is not None: if hasattr(spr, 'show'): spr.show() if hasattr(spr, 'visible'): spr.visible = True # 确保 sprite 的颜色不是透明的 if comp_type == 'Plane': color = child_data.get('color', (1, 1, 1, 1)) if hasattr(spr, 'color'): spr.color = color print(f"[_set_parent_root] {comp_type} sprite visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring {comp_type} visibility: {e}") # 对于 Frame - 直接确保可见 elif comp_type == 'Frame' and child_obj is not None: try: if hasattr(child_obj, 'show'): child_obj.show() if hasattr(child_obj, 'visible'): child_obj.visible = True print(f"[_set_parent_root] Frame visibility ensured") except Exception as e: print(f"[_set_parent_root] Error ensuring Frame visibility: {e}") # Update canvas index recursively self._update_canvas_index_recursive(child_index, child_data.get('canvas_index')) # 调试输出:确认组件状态 print(f"[_set_parent_root] Component #{child_index} ({child_data.get('name', 'Unknown')}) moved to root (data only)") print(f" - Position: ({child_data.get('left', 0):.1f}, {child_data.get('top', 0):.1f})") print(f" - Size: ({child_data.get('width', 0):.1f}, {child_data.get('height', 0):.1f})") print(f" - Canvas index: {child_data.get('canvas_index')}") print(f" - Visible: {getattr(child_obj, 'visible', 'N/A') if child_obj else 'N/A'}") print(f" - Parent (unchanged): {getattr(child_obj, 'parent', 'N/A') if child_obj else 'N/A'}") def create_child_component(self, parent_index, comp_type, anchor_position=None): # Create child component under parent print("\n=== Create child component ===") print(f"Parent index: {parent_index}, Type: {comp_type}") if parent_index < 0 or parent_index >= len(self.components): print("Error: invalid parent index") return None parent_data = self.components[parent_index] parent_obj = parent_data['object'] if parent_data.get('type') == 'ScrollableRegion': parent_obj = parent_data['object'].content_node elif parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout'] and parent_data.get('layout_obj'): if parent_data.get('layout_wrap', True): parent_obj = parent_data.get('object') else: parent_obj = parent_data.get('layout_obj').cell() parent_width = parent_data.get('width', 100) parent_height = parent_data.get('height', 30) comp_sizes = { 'Button': (100, 30), 'Text': (80, 20), 'Label': (80, 20), 'CheckBox': (120, 20), 'InputField': (200, 24), 'Slider': (200, 16), 'Frame': (300, 200), 'Plane': (100, 100), 'Image': (100, 100), 'Video': (320, 240), 'HttpText': (320, 120), 'Progressbar': (200, 30), 'Selectbox': (200, 30), 'ScrollableRegion': (200, 200), 'TabbedFrame': (300, 200), 'VerticalLayout': (300, 200), 'HorizontalLayout': (300, 200), } child_w, child_h = comp_sizes.get(comp_type, (80, 20)) child_x = (parent_width - child_w) / 2 child_y = (parent_height - child_h) / 2 if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_x = 0 child_y = 0 child_obj = None try: if comp_type == 'Button': child_obj = self.luiFunction.create_button(self, text="ChildButton", x=child_x, y=child_y, parent=parent_obj) elif comp_type in ['Text', 'Label']: child_obj = self.luiFunction.create_label(self, text="ChildText", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'CheckBox': child_obj = self.luiFunction.create_checkbox(self, label="ChildCheckbox", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'InputField': child_obj = self.luiFunction.create_input_field(self, text="ChildInput", x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Slider': child_obj = self.luiFunction.create_slider(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Frame': child_obj = self.luiFunction.create_frame(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Plane': child_obj = self.luiFunction.create_plane(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Image': child_obj = self.luiFunction.create_image(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Video': child_obj = self.luiFunction.create_video(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'HttpText': child_obj = self.luiFunction.create_http_text(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Progressbar': child_obj = self.luiFunction.create_progressbar(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'Selectbox': child_obj = self.luiFunction.create_selectbox(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'ScrollableRegion': child_obj = self.luiFunction.create_scrollable_region(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'TabbedFrame': child_obj = self.luiFunction.create_tabbed_frame(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'VerticalLayout': child_obj = self.luiFunction.create_vertical_layout(self, x=child_x, y=child_y, parent=parent_obj) elif comp_type == 'HorizontalLayout': child_obj = self.luiFunction.create_horizontal_layout(self, x=child_x, y=child_y, parent=parent_obj) else: print(f"Error: unsupported component type {comp_type}") return None except Exception as e: print(f"Error: create component failed: {e}") import traceback traceback.print_exc() return None if child_obj: child_index = -1 for i, comp in enumerate(self.components): if comp.get('object') == child_obj: child_index = i break if child_index >= 0: child_data = self.components[child_index] if parent_data.get('type') in ['HorizontalLayout', 'VerticalLayout'] and parent_data.get('layout_wrap', True): self._apply_wrap_layout(parent_index) if parent_data.get('type') in ['VerticalLayout', 'HorizontalLayout']: child_data['draggable'] = False child_data['parent_index'] = parent_index if 'children_indices' not in parent_data: parent_data['children_indices'] = [] if child_index not in parent_data['children_indices']: parent_data['children_indices'].append(child_index) print(f"Linked child #{child_index} -> parent #{parent_index}") return child_index else: print("Child creation failed") return None def _get_descendants(self, idx): """Return set of all descendant indices for a component.""" desc = set() if idx < 0 or idx >= len(self.components): return desc comp = self.components[idx] for c_idx in comp.get('children_indices', []): if c_idx in desc: continue desc.add(c_idx) desc.update(self._get_descendants(c_idx)) return desc def _update_canvas_index_recursive(self, idx, canvas_index): """Update canvas_index for component and its descendants.""" if idx < 0 or idx >= len(self.components): return comp = self.components[idx] comp['canvas_index'] = canvas_index for c_idx in comp.get('children_indices', []): self._update_canvas_index_recursive(c_idx, canvas_index) def _reorder_list(self, items, src, target): """Move src before target within items list.""" if src == target: return items if src not in items or target not in items: return items items = list(items) items.remove(src) insert_at = items.index(target) items.insert(insert_at, src) return items def _sync_canvas_children(self, parent_index): """Update positions for children that keep visual parent on canvas.""" if parent_index < 0 or parent_index >= len(self.components): return # Get parent's absolute position parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) parent_data = self.components[parent_index] parent_obj = parent_data.get('object') for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') if child_obj is None: continue # Check actual parenting child_parent = getattr(child_obj, 'parent', None) is_scene_parented = (child_parent is not None and child_parent == parent_obj) if not is_scene_parented: # If child is NOT physically parented to the parent object (e.g. it is on Canvas), # we MUST manually update its absolute position to follow the parent. abs_left = parent_abs_left + float(child.get('left', 0)) abs_top = parent_abs_top + float(child.get('top', 0)) if hasattr(child_obj, 'left'): child_obj.left = abs_left if hasattr(child_obj, 'top'): child_obj.top = abs_top if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_left, abs_top) # Recurse for grandchildren self._sync_canvas_children(child_idx) def draw_editor(self): """Draw the LUI Editor Window""" if not self.lui_enabled or not self.show_editor: return imgui.set_next_window_size((350, 600), imgui.Cond_.first_use_ever) with imgui_ctx.begin("LUI编辑器", True) as (opened, show): self.show_editor = opened if not show: return # Play/Stop if imgui.button("Play" if not self.play_mode else "Stop", (120, 25)): self.play_mode = not self.play_mode if self.play_mode: # Clear editor-only state when entering play mode self.selected_index = -1 self.dragging_comp = None self.pending_drag_comp = None self.pending_drag_index = -1 self.pending_drag_start_mouse = None self.pending_drag_start_abs = None self.pending_drag_start_parent = None self._hide_resize_handles() # Keep LUI input enabled for runtime UI interaction self.set_input_enabled(True) imgui.same_line() imgui.text_colored((0.2, 1.0, 0.2, 1.0) if self.play_mode else (1.0, 0.8, 0.2, 1.0), "Running" if self.play_mode else "Editing") imgui.separator() # In play mode, show toolbar only if self.play_mode: return # Canvas Management imgui.text("Canvas 管理") imgui.separator() if imgui.button("创建新Canvas", (150, 25)): self.luiFunction.create_canvas(self) imgui.same_line() # 显示当前Canvas信息 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): current_canvas_name = self.canvases[self.current_canvas_index]['name'] imgui.text_colored((0.0, 1.0, 0.0, 1.0), f"当前: {current_canvas_name}") else: imgui.text_colored((1.0, 0.0, 0.0, 1.0), "无Canvas") if len(self.canvases) > 0: canvas_names = [c['name'] for c in self.canvases] if self.current_canvas_index >= len(canvas_names): self.current_canvas_index = len(canvas_names) - 1 changed, new_index = imgui.combo("选择Canvas", self.current_canvas_index, canvas_names) if changed: self.switch_canvas(new_index) # Canvas Properties if self.current_canvas_index >= 0: canvas = self.canvases[self.current_canvas_index] panel = canvas['panel'] imgui.indent() # Visibility is_visible = canvas.get('visible', True) changed, visible = imgui.checkbox("显示Canvas", is_visible) if changed: canvas['visible'] = visible if visible: panel.show() else: panel.hide() # Fill Mode & Margins is_fill_mode = canvas.get('fill_mode', False) imgui.separator() changed, fill_mode = imgui.checkbox("场景填充模式", is_fill_mode) if changed: canvas['fill_mode'] = fill_mode if fill_mode: # 刚切换到填充模式时,更新一次 self._update_canvas_geometry(canvas) if fill_mode: imgui.text("场景边距设置 (调整以匹配视口)") margins = canvas.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) geometry_changed = False changed, margins['left'] = imgui.drag_float("左边距", margins['left'], 1.0, 0, 1000) if changed: geometry_changed = True changed, margins['right'] = imgui.drag_float("右边距", margins['right'], 1.0, 0, 1000) if changed: geometry_changed = True changed, margins['top'] = imgui.drag_float("顶边距", margins['top'], 1.0, 0, 500) if changed: geometry_changed = True changed, margins['bottom'] = imgui.drag_float("底边距", margins['bottom'], 1.0, 0, 500) if changed: geometry_changed = True if geometry_changed: canvas['margins'] = margins self._update_canvas_geometry(canvas) else: # Manual Position curr_pos_val = panel.get_pos() curr_pos = (curr_pos_val.x, curr_pos_val.y) # 固定开关 is_fixed = canvas.get('fixed', False) changed, fixed = imgui.checkbox("锁定位置 (禁止拖动)", is_fixed) if changed: canvas['fixed'] = fixed if not is_fixed: changed, pos = imgui.drag_float2("Canvas位置", curr_pos) if changed: panel.set_pos(pos[0], pos[1]) # Manual Size curr_size = (panel.get_width(), panel.get_height()) changed, size = imgui.drag_float2("Canvas尺寸", curr_size, 1, 100, 4000) if changed: panel.width = size[0] panel.height = size[1] # 同时更新背景尺寸 if 'background' in canvas: bg = canvas['background'] bg.width = size[0] bg.height = size[1] # Canvas Color if 'background' in canvas: imgui.separator() bg = canvas['background'] # 获取当前颜色 (RGBA) current_color = bg.color # 转换为可编辑的格式 (list) edit_color = [current_color[0], current_color[1], current_color[2], current_color[3]] changed, new_color = imgui.color_edit4("背景颜色/透明度", edit_color) if changed: bg.color = (new_color[0], new_color[1], new_color[2], new_color[3]) # Title settings if 'title_label' in canvas: imgui.separator() imgui.text("标题设置") title = canvas['title_label'] # Visible is_title_visible = title.visible changed, title_visible = imgui.checkbox("显示标题", is_title_visible) if changed: try: if title_visible: title.show() else: title.hide() except: title.visible = title_visible # Text content curr_text = title.text changed, new_text = imgui.input_text("标题文本", curr_text, 128) if changed: title.text = new_text # Position curr_pos_val = title.get_pos() curr_pos = (curr_pos_val.x, curr_pos_val.y) changed, new_pos = imgui.drag_float2("标题位置", curr_pos) if changed: title.set_pos(new_pos[0], new_pos[1]) imgui.unindent() else: imgui.text_colored((1, 1, 0, 1), "请先创建一个Canvas") imgui.spacing() imgui.text("创建组件") imgui.separator() if len(self.canvases) == 0: imgui.begin_disabled() # Component Type Selection component_types = ["按钮 (Button)", "文本 (Text)", "复选框 (CheckBox)", "输入框 (InputField)", "滑块 (Slider)", "框架 (Frame)","面板 (Plane)","图片 (Image)","视频 (Video)", "HTTP文本 (HttpText)", "进度条 (Progressbar)", "下拉框 (Selectbox)", "滚动区域 (ScrollableRegion)", "标签页 (TabbedFrame)", "垂直布局组 (VerticalLayout)", "水平布局组 (HorizontalLayout)"] changed, self._selected_type_index = imgui.combo("组件类型", self._selected_type_index, component_types) if imgui.button("创建组件", (120, 25)): self._create_component_by_type(self._selected_type_index) if len(self.canvases) == 0: imgui.end_disabled() imgui.spacing() imgui.text(f"已创建组件 ({len(self.components)})") imgui.separator() # Component Tree Helper # self.draw_component_tree() imgui.spacing() imgui.separator() # Anchor Popup Handling self._handle_anchor_popup() def draw_component_tree(self): """Draw the component tree (can be used in other windows)""" # Track tree item rects for drop hit-testing (allows drop on earlier items) self._tree_item_rects = {} def _store_item_rect(item_key): try: rmin = imgui.get_item_rect_min() rmax = imgui.get_item_rect_max() self._tree_item_rects[item_key] = (rmin, rmax) except Exception: pass def _hit_test_tree_item(mx, my): for key, (rmin, rmax) in self._tree_item_rects.items(): try: min_x = rmin.x min_y = rmin.y max_x = rmax.x max_y = rmax.y except Exception: min_x = rmin[0] min_y = rmin[1] max_x = rmax[0] max_y = rmax[1] if min_x <= mx <= max_x and min_y <= my <= max_y: return key return None # Root drop target if imgui.selectable("ROOT", self.selected_index < 0): # ???????? pass _store_item_rect(-1) def render_component_tree(idx): if idx >= len(self.components): return True comp = self.components[idx] comp_type = comp.get('type', 'Unknown') label = f"{comp.get('name', 'N/A')}" # Determine flags flags = imgui.TreeNodeFlags_.open_on_arrow | imgui.TreeNodeFlags_.open_on_double_click | imgui.TreeNodeFlags_.default_open if self.selected_index == idx: flags |= imgui.TreeNodeFlags_.selected # Check for children children = list(comp.get('children_indices', [])) if not children: flags |= imgui.TreeNodeFlags_.leaf else: label += f" ({len(children)})" # Draw tree node is_open = imgui.tree_node_ex(f"##node_{idx}", flags, label) _store_item_rect(idx) # Custom drag/drop (avoid payload capsule issues) if imgui.is_item_active() and imgui.is_mouse_dragging(0): self._tree_drag_src = idx imgui.begin_tooltip() imgui.text(f"Move: {label}") imgui.end_tooltip() # Handle selection if imgui.is_item_clicked(): self.selected_index = idx # Clear 3D scene selection if hasattr(self.world, 'selection') and self.world.selection: self.world.selection.selectedNode = None self.world.selection.clearSelectionBox() self.world.selection.clearGizmo() # Right-click menu if imgui.begin_popup_context_item(f"##comp_ctx_{idx}"): if imgui.menu_item("删除", "", False)[0]: self.luiFunction.delete_component(self, idx) imgui.end_popup() if is_open: imgui.tree_pop() return False imgui.separator() if imgui.begin_menu("创建子组件"): child_types = ['Button', 'Text', 'CheckBox', 'InputField', 'Slider', 'Frame', 'Plane','Image', 'Video', 'HttpText', 'Progressbar','Selectbox','ScrollableRegion','TabbedFrame', 'VerticalLayout', 'HorizontalLayout'] cn_names = ['按钮', '标签', '复选框', '输入框', '滑块', '框架', '面板', '图片', '视频', 'HTTP文本', '进度条','下拉框','滚动区域','标签页', '垂直布局组', '水平布局组'] for ct_idx, ct in enumerate(child_types): if imgui.menu_item(f"{cn_names[ct_idx]}", "", False)[0]: self.create_child_component(idx, ct) imgui.end_menu() imgui.end_popup() # Draw children if open if is_open: for child_idx in children: # After a deletion, the list might have shifted, # so we check bounds again or just return False to stop this frame's recursion if child_idx < len(self.components): if not render_component_tree(child_idx): # If a child (or sub-child) was deleted, indices shifted. # Stopping current rendering frame's branch is safest. imgui.tree_pop() return False imgui.tree_pop() return True # Find root components roots = [] for i, comp in enumerate(self.components): p_idx = comp.get('parent_index') if p_idx is None or p_idx < 0: roots.append(i) # Maintain root order for sorting if not self.root_order: self.root_order = list(roots) else: self.root_order = [r for r in self.root_order if r in roots] for r in roots: if r not in self.root_order: self.root_order.append(r) # Render roots for root_idx in self.root_order: if not render_component_tree(root_idx): break # Handle tree drop after full render (allows drop on earlier items) if self._tree_drag_src is not None and imgui.is_mouse_released(0): src_idx = self._tree_drag_src self._tree_drag_src = None mx, my = imgui.get_mouse_pos() target_idx = _hit_test_tree_item(mx, my) if target_idx is None: target_idx = -1 if 0 <= src_idx < len(self.components): if target_idx == -1: self._set_parent_root(src_idx, keep_world=True) elif target_idx != src_idx: if target_idx in self._get_descendants(src_idx): print("Invalid drop: cannot parent to descendant") else: src_parent = self.components[src_idx].get('parent_index', -1) tgt_parent = self.components[target_idx].get('parent_index', -1) io = imgui.get_io() want_reorder = bool(getattr(io, 'key_shift', False)) if want_reorder and src_parent == tgt_parent: if src_parent is None or src_parent < 0: if not self.root_order: self.root_order = [i for i, c in enumerate(self.components) if c.get('parent_index') is None or c.get('parent_index') < 0] self.root_order = self._reorder_list(self.root_order, src_idx, target_idx) else: parent_data = self.components[src_parent] children = parent_data.get('children_indices', []) parent_data['children_indices'] = self._reorder_list(children, src_idx, target_idx) else: self._set_parent_child_relationship(src_idx, target_idx, keep_world=True) # Clear drag state if released outside any item if self._tree_drag_src is not None and imgui.is_mouse_released(0): self._tree_drag_src = None imgui.spacing() imgui.separator() # Property Editing if self.selected_index >= 0 and self.selected_index < len(self.components): imgui.text("属性编辑") imgui.separator() self.luiFunction._draw_component_properties(self,self.selected_index) else: imgui.text_disabled("未选中任何组件") # Anchor popup if self.anchor_popup_open: imgui.open_popup("选择锚点位置") if imgui.begin_popup("选择锚点位置"): imgui.text("选择锚点位置:") # 中文显示名称 anchor_names = { 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' } anchor_positions = [ ['top-left', 'top-center', 'top-right'], ['middle-left', 'center', 'middle-right'], ['bottom-left', 'bottom-center', 'bottom-right'] ] for row in anchor_positions: for i, pos in enumerate(row): button_text = anchor_names.get(pos, pos) if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): # 使用新的锚点更新方法 if hasattr(self, '_temp_selected_index_for_anchor'): selected_idx = self._temp_selected_index_for_anchor self._update_component_anchor_position(selected_idx, pos) self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() if i < len(row) - 1: imgui.same_line() imgui.spacing() if imgui.button("取消"): self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() imgui.end_popup() def _create_component_by_type(self, type_index): """Create LUI component by index - 默认生成在Canvas中心""" # 1. 确定生成位置 (默认 Canvas 中心) canvas_relative_x = 100 canvas_relative_y = 100 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_width = canvas_panel.get_width() canvas_height = canvas_panel.get_height() # 根据组件类型预测尺寸,以便进行中心点对齐 # 注意:这些是 lui_function.py 中定义的默认值 comp_sizes = { 0: (100, 30), # Button 1: (80, 20), # Text 2: (120, 20), # Checkbox 3: (200, 24), # InputField 4: (200, 16), # Slider 5: (300, 200), # Frame 6: (100, 100), # Plane 7: (100, 100), # Image 8: (320, 240), # Video 9: (320, 120), # HttpText 10: (200, 30), # Progressbar 11: (200, 30), # Selectbox 12: (200, 200), # ScrollableRegion 13: (300, 200), # TabbedFrame 14: (300, 200), # VerticalLayout 15: (300, 200) # HorizontalLayout } w, h = comp_sizes.get(type_index, (100, 100)) # 计算中心位置 canvas_relative_x = (canvas_width - w) / 2 canvas_relative_y = (canvas_height - h) / 2 # 确保不超出合理范围 (防止 Canvas 太小时位置为负) canvas_relative_x = max(10, canvas_relative_x) canvas_relative_y = max(10, canvas_relative_y) try: if type_index == 0: # Button self.luiFunction.create_button(self, text="New Button", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 1: # Text self.luiFunction.create_label(self, text="New Text", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 2: # Checkbox self.luiFunction.create_checkbox(self, label="Check box", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 3: # InputField self.luiFunction.create_input_field(self, text="", x=canvas_relative_x, y=canvas_relative_y) elif type_index == 4: # Slider self.luiFunction.create_slider(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 5: # Frame self.luiFunction.create_frame(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 6: # Plane self.luiFunction.create_plane(self, x=canvas_relative_x, y=canvas_relative_y, color=(0.8, 0.8, 0.8, 1)) elif type_index == 7: # Image self.luiFunction.create_image(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 8: # Video self.luiFunction.create_video(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 9: # HttpText self.luiFunction.create_http_text(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 10: # Progressbar self.luiFunction.create_progressbar(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 11: # Selectbox self.luiFunction.create_selectbox(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 12: # ScrollableRegion self.luiFunction.create_scrollable_region(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 13: # TabbedFrame self.luiFunction.create_tabbed_frame(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 14: # VerticalLayout self.luiFunction.create_vertical_layout(self, x=canvas_relative_x, y=canvas_relative_y) elif type_index == 15: # HorizontalLayout self.luiFunction.create_horizontal_layout(self, x=canvas_relative_x, y=canvas_relative_y) print(f"✓ 创建组件成功,已重定向至 Canvas 中心: ({canvas_relative_x:.1f}, {canvas_relative_y:.1f})") if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() except Exception as e: print(f"创建组件失败: {str(e)}") def set_component_anchor(self, comp_index, anchor_position): """设置组件锚点 - 保持向后兼容""" self._update_component_anchor_position(comp_index, anchor_position) def _update_anchored_children(self, parent_index): """Recursively update anchored/fill children.""" if parent_index < 0 or parent_index >= len(self.components): return print(f"[_update_anchored_children] Updating children of #{parent_index}") parent_data = self.components[parent_index] children = parent_data.get('children_indices', []) for child_idx in children: if child_idx < len(self.components): child_data = self.components[child_idx] # Fill layout has priority over anchored positioning if child_data.get('layout_mode') == 'fill': print(f" -> Child #{child_idx} is FILL mode") self._apply_fill_layout(child_idx) elif child_data.get('anchored_to_parent'): anchor_pos = child_data.get('anchor_position') manual_offset = ( child_data.get('anchor_manual_offset_x', 0.0), child_data.get('anchor_manual_offset_y', 0.0) ) if anchor_pos: self._update_component_anchor_position(child_idx, anchor_pos, manual_offset) # Recurse into children self._update_anchored_children(child_idx) def _apply_fill_layout(self, comp_index): """Apply fill layout to component: match parent/canvas size with margins.""" if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] comp_obj = comp_data.get('object') comp_type = comp_data.get('type', '') parent_width = None parent_height = None parent_index = comp_data.get('parent_index') if parent_index is not None and parent_index >= 0 and parent_index < len(self.components): parent_data = self.components[parent_index] parent_width = parent_data.get('width') parent_height = parent_data.get('height') parent_obj = parent_data.get('object') if parent_width is None and parent_obj is not None and hasattr(parent_obj, 'width'): parent_width = parent_obj.width if parent_height is None and parent_obj is not None and hasattr(parent_obj, 'height'): parent_height = parent_obj.height else: canvas_index = comp_data.get('canvas_index', self.current_canvas_index) if canvas_index is not None and canvas_index >= 0 and canvas_index < len(self.canvases): canvas_panel = self.canvases[canvas_index].get('panel') if canvas_panel is not None: if hasattr(canvas_panel, 'width'): parent_width = canvas_panel.width elif hasattr(canvas_panel, 'get_width'): parent_width = canvas_panel.get_width() if hasattr(canvas_panel, 'height'): parent_height = canvas_panel.height elif hasattr(canvas_panel, 'get_height'): parent_height = canvas_panel.get_height() # DEBUG: Trace fill layout calculations # print(f"[_apply_fill_layout] Comp #{comp_index} Parent #{parent_index}: PW={parent_width} PH={parent_height}") if parent_width is None or parent_height is None: # print(f"[_apply_fill_layout] Failed to get parent dimensions for Comp #{comp_index}") return margin_left = float(comp_data.get('fill_margin_left', 0.0)) margin_right = float(comp_data.get('fill_margin_right', 0.0)) margin_top = float(comp_data.get('fill_margin_top', 0.0)) margin_bottom = float(comp_data.get('fill_margin_bottom', 0.0)) new_left = margin_left new_top = margin_top new_width = float(parent_width) - (margin_left + margin_right) new_height = float(parent_height) - (margin_top + margin_bottom) if new_width < 1.0: new_width = 1.0 if new_height < 1.0: new_height = 1.0 comp_data['left'] = new_left comp_data['top'] = new_top comp_data['width'] = new_width comp_data['height'] = new_height if comp_obj is not None: # Handle visual_parent_canvas case: convert local to absolute abs_left = new_left abs_top = new_top if parent_index is not None and parent_index >= 0 and comp_data.get('visual_parent_canvas'): p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) abs_left = p_abs_x + new_left abs_top = p_abs_y + new_top if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(abs_left, abs_top) else: if hasattr(comp_obj, 'left'): comp_obj.left = abs_left if hasattr(comp_obj, 'top'): comp_obj.top = abs_top if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height if comp_type in ['Plane', 'Image', 'Video', 'HttpText'] and 'sprite' in comp_data: comp_data['sprite'].width = new_width comp_data['sprite'].height = new_height if comp_type == 'HttpText': self.luiFunction.sync_http_text_layout(self, comp_data) elif comp_type == 'Button': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) # Force update internal layout for Button to ensure fill works try: layout = getattr(comp_obj, '_layout', None) if layout: if hasattr(layout, 'width'): layout.width = new_width if hasattr(layout, 'height'): layout.height = new_height # Inner layout inner = getattr(layout, '_layout', None) if inner: if hasattr(inner, 'width'): inner.width = new_width if hasattr(inner, 'height'): inner.height = new_height # Mid sprite usually stretches spr_mid = getattr(layout, '_sprite_mid', None) if spr_mid: spr_mid.width = new_width spr_mid.height = new_height except Exception: pass elif comp_type == 'InputField': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) elif comp_type == 'Slider': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) def _apply_layout_spacing(self, layout_obj, spacing): try: if hasattr(layout_obj, 'set_spacing'): layout_obj.set_spacing(spacing) elif hasattr(layout_obj, 'setSpacing'): layout_obj.setSpacing(spacing) elif hasattr(layout_obj, 'spacing'): layout_obj.spacing = spacing elif hasattr(layout_obj, '_spacing'): layout_obj._spacing = spacing except Exception: pass def _apply_layout_alignment(self, layout_obj, align): try: if hasattr(layout_obj, 'set_alignment'): layout_obj.set_alignment(align) elif hasattr(layout_obj, 'alignment'): layout_obj.alignment = align elif hasattr(layout_obj, 'align'): layout_obj.align = align except Exception: pass def _update_layout_inner(self, comp_index): if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] layout_obj = comp_data.get('layout_obj') if layout_obj is None: return width = float(comp_data.get('width', 0.0)) height = float(comp_data.get('height', 0.0)) pad_left = float(comp_data.get('layout_padding_left', 0.0)) pad_right = float(comp_data.get('layout_padding_right', 0.0)) pad_top = float(comp_data.get('layout_padding_top', 0.0)) pad_bottom = float(comp_data.get('layout_padding_bottom', 0.0)) inner_w = max(1.0, width - (pad_left + pad_right)) inner_h = max(1.0, height - (pad_top + pad_bottom)) try: if hasattr(layout_obj, 'left'): layout_obj.left = pad_left if hasattr(layout_obj, 'top'): layout_obj.top = pad_top if hasattr(layout_obj, 'width'): layout_obj.width = inner_w if hasattr(layout_obj, 'height'): layout_obj.height = inner_h if hasattr(layout_obj, 'set_size'): layout_obj.set_size(inner_w, inner_h) except Exception: pass spacing = float(comp_data.get('layout_spacing', 0.0)) self._apply_layout_spacing(layout_obj, spacing) align = comp_data.get('layout_align', None) if align: self._apply_layout_alignment(layout_obj, align) if comp_data.get('type') in ['HorizontalLayout', 'VerticalLayout']: self._apply_wrap_layout(comp_index) try: if hasattr(layout_obj, 'update'): layout_obj.update() except Exception: pass def _reparent_scrollable_children(self, parent_index): if parent_index < 0 or parent_index >= len(self.components): return parent_data = self.components[parent_index] if parent_data.get('type') != 'ScrollableRegion': return parent_obj = parent_data['object'] try: content_node = parent_obj.content_node except Exception: return for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child_data = self.components[child_idx] child_obj = child_data.get('object') if child_obj is None: continue try: if hasattr(child_obj, 'reparent_to'): child_obj.reparent_to(content_node) elif hasattr(child_obj, 'parent'): child_obj.parent = content_node except Exception: pass def _apply_wrap_layout(self, parent_index): if parent_index < 0 or parent_index >= len(self.components): return parent_data = self.components[parent_index] layout_type = parent_data.get('type') if layout_type not in ['HorizontalLayout', 'VerticalLayout']: return if not parent_data.get('layout_wrap', True): return # Get parent absolute position for coordinate conversion parent_abs_left, parent_abs_top = self._get_component_accumulated_pos(parent_index) width = float(parent_data.get('width', 0.0)) height = float(parent_data.get('height', 0.0)) pad_left = float(parent_data.get('layout_padding_left', 0.0)) pad_right = float(parent_data.get('layout_padding_right', 0.0)) pad_top = float(parent_data.get('layout_padding_top', 0.0)) pad_bottom = float(parent_data.get('layout_padding_bottom', 0.0)) spacing = float(parent_data.get('layout_spacing', 0.0)) line_spacing = float(parent_data.get('layout_line_spacing', 0.0)) inner_w = max(1.0, width - (pad_left + pad_right)) inner_h = max(1.0, height - (pad_top + pad_bottom)) if layout_type == 'HorizontalLayout': x = pad_left y = pad_top row_h = 0.0 for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') cw = float(child.get('width', 0.0)) ch = float(child.get('height', 0.0)) # Allow layout even if size is 0, but usually skip # if cw <= 0 or ch <= 0: continue if x > pad_left and (x + cw) > (pad_left + inner_w): x = pad_left y += row_h + line_spacing row_h = 0.0 # Data stores relative position (to parent) child['left'] = x child['top'] = y # Update visual object try: child_parent = getattr(child_obj, 'parent', None) parent_obj = parent_data.get('object') if child_parent is not None and child_parent == parent_obj: # Child is parented to layout object: use relative coordinates if hasattr(child_obj, 'left'): child_obj.left = x if hasattr(child_obj, 'top'): child_obj.top = y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(x, y) else: # Child is NOT parented to layout object (e.g. at root): use absolute coordinates abs_x = parent_abs_left + x abs_y = parent_abs_top + y if hasattr(child_obj, 'left'): child_obj.left = abs_x if hasattr(child_obj, 'top'): child_obj.top = abs_y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_x, abs_y) except Exception: pass # Recursively update child layout/visuals if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): self._apply_wrap_layout(child_idx) elif child.get('children_indices'): self._sync_canvas_children(child_idx) row_h = max(row_h, ch) x += cw + spacing else: # Vertical wrap: flow top->bottom, then next column x = pad_left y = pad_top col_w = 0.0 for child_idx in parent_data.get('children_indices', []): if child_idx < 0 or child_idx >= len(self.components): continue child = self.components[child_idx] child_obj = child.get('object') cw = float(child.get('width', 0.0)) ch = float(child.get('height', 0.0)) if y > pad_top and (y + ch) > (pad_top + inner_h): y = pad_top x += col_w + line_spacing col_w = 0.0 child['left'] = x child['top'] = y try: child_parent = getattr(child_obj, 'parent', None) parent_obj = parent_data.get('object') if child_parent is not None and child_parent == parent_obj: # Child is parented to layout object: use relative coordinates if hasattr(child_obj, 'left'): child_obj.left = x if hasattr(child_obj, 'top'): child_obj.top = y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(x, y) else: # Child is NOT parented to layout object (e.g. at root): use absolute coordinates abs_x = parent_abs_left + x abs_y = parent_abs_top + y if hasattr(child_obj, 'left'): child_obj.left = abs_x if hasattr(child_obj, 'top'): child_obj.top = abs_y if hasattr(child_obj, 'set_pos'): child_obj.set_pos(abs_x, abs_y) except Exception: pass # If child is also a layout, recursively update it if child.get('type') in ['HorizontalLayout', 'VerticalLayout'] and child.get('layout_wrap', True): self._apply_wrap_layout(child_idx) # If child is NOT a layout but has children (like a Frame with children), update their visual text elif child.get('children_indices'): self._sync_canvas_children(child_idx) col_w = max(col_w, cw) y += ch + spacing def get_selected_component(self): """获取当前选中的组件""" if self.selected_index >= 0 and self.selected_index < len(self.components): return self.components[self.selected_index] return None def select_component(self, index): """选中指定索引的组件""" if 0 <= index < len(self.components): self.selected_index = index print(f"✓ 选中组件 #{index}: {self.components[index]['type']}") else: self.selected_index = -1 def deselect_all(self): """取消选中所有组件""" if getattr(self, '_force_keep_selection_frames', 0) > 0: return self.selected_index = -1 print("✓ 已取消选中所有组件") def _change_image_texture(self, title = "选择文件",filetypes=None,initialdir=None): """更改图像组件的纹理""" try: import tkinter as tk from tkinter import filedialog root = tk.Tk() root.withdraw() root.attributes('-topmost',True) if filetypes is None: filetypes = [("所有文件","*.*")] file_path = filedialog.askopenfilename( title = title, filetypes = filetypes, initialdir = initialdir ) root.destroy() if file_path: return file_path return None except Exception as e: print(f"更改纹理失败: {e}") def _update_component_anchor_position(self, comp_index, anchor_position, manual_offset=(0.0, 0.0)): """更新组件的锚点位置""" if comp_index < 0 or comp_index >= len(self.components): return comp_data = self.components[comp_index] comp_obj = comp_data['object'] # Update data comp_data['anchor_position'] = anchor_position comp_data['anchored_to_parent'] = True comp_data['anchor_manual_offset_x'] = manual_offset[0] comp_data['anchor_manual_offset_y'] = manual_offset[1] # Determine strict parent reference parent_index = comp_data.get('parent_index') parent_width = 0 parent_height = 0 if parent_index is not None and parent_index >= 0: parent_data = self.components[parent_index] parent_width = parent_data.get('width', 100) parent_height = parent_data.get('height', 30) # Try to get actual object size if available p_obj = parent_data.get('object') if p_obj: if hasattr(p_obj, 'width') and p_obj.width > 0: parent_width = p_obj.width if hasattr(p_obj, 'height') and p_obj.height > 0: parent_height = p_obj.height elif self.current_canvas_index >= 0: # Canvas anchor canvas = self.canvases[self.current_canvas_index]['panel'] if hasattr(canvas, 'get_width'): parent_width = canvas.get_width() parent_height = canvas.get_height() else: parent_width = canvas.width parent_height = canvas.height # Calculate anchor point in parent local space anchor_ratios = { 'top-left': (0, 0), 'top-center': (0.5, 0), 'top-right': (1, 0), 'middle-left': (0, 0.5), 'center': (0.5, 0.5), 'middle-right': (1, 0.5), 'bottom-left': (0, 1), 'bottom-center': (0.5, 1), 'bottom-right': (1, 1) } rx, ry = anchor_ratios.get(anchor_position, (0, 0)) anchor_x = parent_width * rx anchor_y = parent_height * ry child_width = comp_data.get('width', 0) child_height = comp_data.get('height', 0) # Try to get actual object size if hasattr(comp_obj, 'width') and comp_obj.width > 0: child_width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height > 0: child_height = comp_obj.height # Center child on anchor (pivot logic) child_left = anchor_x - (child_width * rx) + manual_offset[0] child_top = anchor_y - (child_height * ry) + manual_offset[1] # Update logical position data comp_data['left'] = child_left comp_data['top'] = child_top # Update visual position if parent_index is not None and parent_index >= 0: # Check if visually parented to canvas (absolute positioning needed) if comp_data.get('visual_parent_canvas'): p_abs_x, p_abs_y = self._get_component_accumulated_pos(parent_index) abs_left = p_abs_x + child_left abs_top = p_abs_y + child_top # Prefer left/top properties for LUI objects if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = abs_left comp_obj.top = abs_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(abs_left, abs_top) except TypeError: # Fallback for NodePath-like objects requiring 3 args comp_obj.set_pos(abs_left, abs_top, 0) else: # Standard local parenting if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = child_left comp_obj.top = child_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(child_left, child_top) except TypeError: comp_obj.set_pos(child_left, child_top, 0) else: # Canvas root if hasattr(comp_obj, 'left') and hasattr(comp_obj, 'top'): comp_obj.left = child_left comp_obj.top = child_top elif hasattr(comp_obj, 'set_pos'): try: comp_obj.set_pos(child_left, child_top) except TypeError: comp_obj.set_pos(child_left, child_top, 0) print(f"✓ 已更新锚点位置: {anchor_position} -> ({child_left:.1f}, {child_top:.1f})") # Update children positions (for visual_parent_canvas or anchored children) self._sync_canvas_children(comp_index) self._update_anchored_children(comp_index) def _handle_anchor_popup(self): """Handle component anchor selection popup""" if imgui.begin_popup("选择锚点位置"): imgui.text("选择锚点位置:") anchor_names = { 'top-left': '左上', 'top-center': '中上', 'top-right': '右上', 'middle-left': '左中', 'center': '中心', 'middle-right': '右中', 'bottom-left': '左下', 'bottom-center': '中下', 'bottom-right': '右下' } anchor_positions = [ ['top-left', 'top-center', 'top-right'], ['middle-left', 'center', 'middle-right'], ['bottom-left', 'bottom-center', 'bottom-right'] ] for row in anchor_positions: for i, pos in enumerate(row): button_text = anchor_names.get(pos, pos) if imgui.button(f"{button_text}##popup_{pos}", (50, 25)): if hasattr(self, '_temp_selected_index_for_anchor'): selected_idx = self._temp_selected_index_for_anchor # Reset manual offset by passing (0,0) explicitly to match original behavior logic self._update_component_anchor_position(selected_idx, pos, manual_offset=(0.0, 0.0)) # Clean up temp attribute delattr(self, '_temp_selected_index_for_anchor') # Reset flag if used elsewhere self.anchor_popup_open = False imgui.close_current_popup() if i < len(row) - 1: imgui.same_line() imgui.spacing() if imgui.button("取消", (160, 25)): self.anchor_popup_open = False if hasattr(self, '_temp_selected_index_for_anchor'): delattr(self, '_temp_selected_index_for_anchor') imgui.close_current_popup() imgui.end_popup()