"""LUIManager interaction and resize mixin.""" import struct from pathlib import Path import panda3d.core as p3d from imgui_bundle import imgui, imgui_ctx class LUIManagerInteractionMixin: def _make_canvas_draggable(self, canvas_panel): """使Canvas可拖拽""" def on_canvas_drag_start(event): # 检查是否有组件或手柄在当前帧被点击 current_frame = 0 if hasattr(p3d, 'ClockObject'): current_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: current_frame = self.world.taskMgr.globalClock.getFrameCount() if self._last_interaction_frame == current_frame: # 当前帧已经处理了组件或手柄的点击,忽略Canvas点击事件 return # 点击空白处:不自动取消选中,避免事件穿透导致闪退 # self.deselect_all() # 如果启用填充模式,则自动禁用,以便用户可以自由拖动 canvas_index = -1 for i, c in enumerate(self.canvases): if c['panel'] == canvas_panel: canvas_index = i break if canvas_index >= 0: canvas_data = self.canvases[canvas_index] # 如果是固定模式,则禁止拖动初始化 if canvas_data.get('fixed', False): return if canvas_data.get('fill_mode', False): canvas_data['fill_mode'] = False print("✓ 自动禁用填充模式以允许自由拖动") # 记录Canvas拖拽状态 if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 pos = canvas_panel.get_pos() self.canvas_drag_offset = (pixel_x - pos.x, pixel_y - pos.y) self.dragging_canvas = canvas_panel canvas_panel.bind("mousedown", on_canvas_drag_start) def switch_canvas(self, index): """切换到指定Canvas并显示/隐藏相应的UI组件""" if index < 0 or index >= len(self.canvases): return self.current_canvas_index = index # 显示选中的Canvas,隐藏其他Canvas for i, canvas in enumerate(self.canvases): if i == index: canvas['panel'].show() canvas['visible'] = True else: canvas['panel'].hide() canvas['visible'] = False # 显示/隐藏属于不同Canvas的组件(包括子组件) visible_count = 0 hidden_count = 0 for comp in self.components: comp_canvas_index = comp.get('canvas_index') comp_obj = comp['object'] comp_name = comp.get('name', 'Unknown') should_be_visible = (comp_canvas_index == index) if should_be_visible: if hasattr(comp_obj, 'show'): comp_obj.show() elif hasattr(comp_obj, 'visible'): comp_obj.visible = True visible_count += 1 else: if hasattr(comp_obj, 'hide'): comp_obj.hide() elif hasattr(comp_obj, 'visible'): comp_obj.visible = False hidden_count += 1 # 更新当前根节点 if index >= 0: self.root = self.canvases[index]['panel'] # 如果选中的组件不属于当前Canvas,取消选中 if self.selected_index >= 0 and self.selected_index < len(self.components): selected_comp = self.components[self.selected_index] if selected_comp.get('canvas_index') != index: if getattr(self, '_force_keep_selection_frames', 0) > 0: pass else: self.selected_index = -1 self._hide_selection() print(f"✓ Switched to Canvas: {self.canvases[index]['name']}") print(f" 显示组件: {visible_count}, 隐藏组件: {hidden_count}") def _update_canvas_geometry(self, canvas_data): """根据填充模式和边距更新Canvas几何信息""" if not canvas_data.get('fill_mode', False): return panel = canvas_data['panel'] margins = canvas_data.get('margins', {'left':0,'right':0,'top':0,'bottom':0}) win_width = 800 win_height = 600 if hasattr(self.world, 'win'): win_width = self.world.win.getXSize() win_height = self.world.win.getYSize() new_x = margins['left'] new_y = margins['top'] new_width = max(10, win_width - margins['left'] - margins['right']) new_height = max(10, win_height - margins['top'] - margins['bottom']) panel.set_pos(new_x, new_y) panel.width = new_width panel.height = new_height if 'background' in canvas_data: bg = canvas_data['background'] bg.width = new_width bg.height = new_height def _setup_component_drag(self, comp_data, comp_index): """为组件设置拖动功能 - 支持Canvas相对坐标系""" comp_obj = comp_data['object'] def on_mouse_down(event): comp_type = comp_data.get('type') if getattr(self, 'play_mode', False): # Let runtime widgets handle their own events (especially Slider) if comp_type == 'Slider': return try: if hasattr(comp_obj, 'on_mousedown'): comp_obj.on_mousedown(event) except Exception: pass return # Prevent re-entrant drag if self.dragging_comp is not None: return # Find current index current_index = -1 for i, c in enumerate(self.components): if c is comp_data: current_index = i break if current_index == -1: return # Mark interaction frame if hasattr(p3d, 'ClockObject'): self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() # Select component self.selected_index = current_index self._last_selected_index = current_index self._force_keep_selection_frames = 3 # Force component to current canvas to avoid immediate deselect try: comp_data['canvas_index'] = self.current_canvas_index except Exception: pass try: if comp_data.get('canvas_index') is None: comp_data['canvas_index'] = self.current_canvas_index except Exception: pass try: self._show_and_update_handles(comp_data) except Exception: pass # Slider: allow knob/bar drag in edit mode without hijacking if comp_type == 'Slider': sender = getattr(event, 'sender', None) knob = getattr(comp_obj, '_knob', None) bg = getattr(comp_obj, '_slider_bg', None) if sender is not None and (sender == knob or sender == bg): # Let slider handle its own drag logic try: if hasattr(comp_obj, '_start_drag'): comp_obj._start_drag(event) except Exception: pass return parent_idx = comp_data.get('parent_index') if parent_idx is not None and parent_idx >= 0: parent_type = self.components[parent_idx].get('type') if parent_type in ['VerticalLayout', 'HorizontalLayout']: return if not comp_data.get('draggable', True): return self.pending_drag_comp = comp_data self.pending_drag_index = current_index self.pending_drag_start_abs = self._get_component_accumulated_pos(current_index) self.pending_drag_start_parent = comp_obj.parent self._is_drag_reparented = False if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 self.pending_drag_start_mouse = (pixel_x, pixel_y) else: self.pending_drag_start_mouse = None # Bind mouse event comp_obj.bind("mousedown", on_mouse_down) # Slider selection bindings: allow clicking knob/bar to select in edit mode try: if comp_data.get('type') == 'Slider': knob = getattr(comp_obj, '_knob', None) bg = getattr(comp_obj, '_slider_bg', None) except Exception: pass def on_mouse_up(event): if getattr(self, 'play_mode', False): try: if hasattr(comp_obj, 'on_mouseup'): comp_obj.on_mouseup(event) except Exception: pass return if comp_data.get('type') == 'Slider': sender = getattr(event, 'sender', None) knob = getattr(comp_obj, '_knob', None) if sender is not None and sender == knob: try: if hasattr(comp_obj, '_stop_drag'): comp_obj._stop_drag(event) except Exception: pass comp_obj.bind("mouseup", on_mouse_up) def _make_draggable(self, lui_obj): """启用 LUI 对象的鼠标拖拽功能 - 兼容旧接口""" # 查找对应的组件索引 comp_index = -1 for i, comp in enumerate(self.components): if comp.get('object') == lui_obj: comp_index = i break if comp_index >= 0: self._setup_component_drag(self.components[comp_index], comp_index) def _get_component_accumulated_pos(self, comp_index): """递归计算组件相对于Canvas的累积位置""" if comp_index < 0 or comp_index >= len(self.components): return 0, 0 comp_data = self.components[comp_index] comp_obj = comp_data['object'] # 优先使用递归计算(更可靠) left = comp_data.get('left', 0) top = comp_data.get('top', 0) parent_index = comp_data.get('parent_index') if parent_index is not None and parent_index >= 0: parent_left, parent_top = self._get_component_accumulated_pos(parent_index) result_x = parent_left + left result_y = parent_top + top print(f"[_get_component_accumulated_pos] Component #{comp_index} (has parent #{parent_index}): local=({left:.1f}, {top:.1f}), parent_offset=({parent_left:.1f}, {parent_top:.1f}), result=({result_x:.1f}, {result_y:.1f})") return result_x, result_y # 没有父组件,直接返回局部坐标(相对于Canvas) print(f"[_get_component_accumulated_pos] Component #{comp_index} (root): local=({left:.1f}, {top:.1f})") return left, top def _clear_pending_drag_state(self): """清空 pending 拖拽状态。""" 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 def _has_mouse_for_drag(self): """检查是否存在可用鼠标输入。""" return hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse() def _is_left_mouse_down(self): """检查鼠标左键是否按下。""" import panda3d.core as p3d return self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()) def _get_mouse_pixel_data(self): """获取鼠标像素坐标及窗口尺寸。""" mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 return pixel_x, pixel_y, win_x, win_y def _find_canvas_data_by_panel(self, canvas_panel): """按 panel 对象查找 canvas 配置。""" for c in self.canvases: if c['panel'] == canvas_panel: return c return None def _process_canvas_drag(self): """处理 Canvas 拖动,返回是否已处理该帧。""" if not (hasattr(self, 'dragging_canvas') and self.dragging_canvas): return False canvas_data = self._find_canvas_data_by_panel(self.dragging_canvas) if canvas_data and canvas_data.get('fixed', False): return True if not self._has_mouse_for_drag(): return True if not self._is_left_mouse_down(): self.dragging_canvas = None return True pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data() new_x = pixel_x - self.canvas_drag_offset[0] new_y = pixel_y - self.canvas_drag_offset[1] self.dragging_canvas.set_pos(new_x, new_y) return True def _try_start_pending_component_drag(self): """处理 pending 拖拽转正,返回是否需要本帧提前结束。""" if self.dragging_comp is not None: return False if self.pending_drag_comp is None: return True if not self._has_mouse_for_drag(): return True if not self._is_left_mouse_down(): # click without drag self._clear_pending_drag_state() return True pixel_x, pixel_y, _, _ = self._get_mouse_pixel_data() if self.pending_drag_start_mouse is None: self.pending_drag_start_mouse = (pixel_x, pixel_y) return True dx = pixel_x - self.pending_drag_start_mouse[0] dy = pixel_y - self.pending_drag_start_mouse[1] if (dx * dx + dy * dy) < (self.drag_start_threshold * self.drag_start_threshold): return True # Start drag now comp_data = self.pending_drag_comp self.dragging_comp = comp_data self.dragging_index = self.pending_drag_index self._original_parent_obj = self.pending_drag_start_parent self._is_drag_reparented = False acc_left, acc_top = self.pending_drag_start_abs if acc_left is None or acc_top is None: acc_left, acc_top = self._get_component_accumulated_pos(self.pending_drag_index) # If child, reparent to canvas for free drag p_idx = comp_data.get('parent_index') if p_idx is not None and p_idx >= 0 and self.current_canvas_index >= 0: canvas_panel = self.canvases[self.current_canvas_index]['panel'] comp_obj = comp_data['object'] if not comp_data.get('visual_parent_canvas'): try: if getattr(comp_obj, 'parent', None) == canvas_panel: pass elif hasattr(comp_obj, 'reparent_to'): comp_obj.reparent_to(canvas_panel) if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(acc_left, acc_top) else: comp_obj.left = acc_left comp_obj.top = acc_top self._is_drag_reparented = True except Exception: pass # Compute drag offset canvas_abs_left = 0 canvas_abs_top = 0 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except Exception: canvas_abs_left = 300 canvas_abs_top = 100 comp_abs_left = canvas_abs_left + acc_left comp_abs_top = canvas_abs_top + acc_top self.drag_offset = (pixel_x - comp_abs_left, pixel_y - comp_abs_top) self._clear_pending_drag_state() return False def _finish_component_drag_if_released(self): """在鼠标释放时完成拖拽收尾,返回是否已处理该帧。""" if not self._is_left_mouse_down(): if self.dragging_comp: comp_data = self.dragging_comp print(f"组件移动到: left={comp_data.get('left', 0):.1f}, top={comp_data.get('top', 0):.1f} (局部坐标)") # 拖动结束:检查是否需要接触父子关系 (Auto-unparent) should_unparent = False if getattr(self, '_is_drag_reparented', False): comp_obj = comp_data['object'] # Check if center of component is outside parent bounds p_idx = comp_data.get('parent_index') if p_idx is not None and p_idx >= 0: parent_data = self.components[p_idx] p_w = parent_data.get('width', 100) p_h = parent_data.get('height', 100) c_w = comp_data.get('width', 0) c_h = comp_data.get('height', 0) # Current comp_data['left'] is LOCAL to parent local_l = comp_data.get('left', 0) local_t = comp_data.get('top', 0) center_x = local_l + c_w / 2 center_y = local_t + c_h / 2 # Check bounds: center must be in 0..p_w, 0..p_h # Allow some leniency or strict? Let's say if center is outside, unparent. if center_x < 0 or center_x > p_w or center_y < 0 or center_y > p_h: should_unparent = True if should_unparent: # Unparent! # The component is currently visually attached to Canvas (from drag start) # We just need to update the data to reflect this permanent change. # Calculate the new global (canvas-relative) position # current local + parent offset p_off_x, p_off_y = self._get_component_accumulated_pos(p_idx) new_canvas_left = local_l + p_off_x new_canvas_top = local_t + p_off_y comp_data['left'] = new_canvas_left comp_data['top'] = new_canvas_top # Remove parent linkage if 'parent_index' in comp_data: del comp_data['parent_index'] if 'anchored_to_parent' in comp_data: del comp_data['anchored_to_parent'] print(f"✓ Auto-unparented component from #{p_idx} (Dragged out)") elif hasattr(comp_obj, 'reparent_to') and hasattr(self, '_original_parent_obj'): comp_obj.reparent_to(self._original_parent_obj) # 重新应用计算出的局部坐标 comp_obj.left = comp_data.get('left', 0) comp_obj.top = comp_data.get('top', 0) print(f"✓ 组件归位到逻辑父级") self.dragging_comp = None return True return False def _resolve_canvas_metrics_for_drag(self, win_x, win_y): """获取当前 Canvas 的绝对位置与尺寸。""" canvas_abs_left = 0 canvas_abs_top = 0 canvas_width = 800 canvas_height = 600 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_data = self.canvases[self.current_canvas_index] canvas_panel = canvas_data['panel'] # 获取Canvas的绝对位置 canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: pos = canvas_panel.get_pos() canvas_abs_left = pos.x canvas_abs_top = pos.y except Exception: # 如果获取失败,使用默认值 canvas_abs_left = canvas_data.get('margins', {}).get('left', 240) canvas_abs_top = canvas_data.get('margins', {}).get('top', 89) # 获取Canvas的实际尺寸 try: if hasattr(canvas_panel, 'width') and canvas_panel.width is not None: canvas_width = float(canvas_panel.width) elif hasattr(canvas_panel, 'get_width'): canvas_width = float(canvas_panel.get_width()) if hasattr(canvas_panel, 'height') and canvas_panel.height is not None: canvas_height = float(canvas_panel.height) elif hasattr(canvas_panel, 'get_height'): canvas_height = float(canvas_panel.get_height()) except Exception as e: print(f"Warning: Failed to get canvas size: {e}") # 使用窗口尺寸减去边距作为后备 margins = canvas_data.get('margins', {'left': 240, 'right': 480, 'top': 89, 'bottom': 220}) canvas_width = win_x - margins.get('left', 0) - margins.get('right', 0) canvas_height = win_y - margins.get('top', 0) - margins.get('bottom', 0) return canvas_abs_left, canvas_abs_top, canvas_width, canvas_height def _update_active_component_drag(self, pixel_x, pixel_y, win_x, win_y): """更新已开始拖拽的组件位置。""" # 计算新的绝对位置(减去偏移量) new_abs_left = pixel_x - self.drag_offset[0] new_abs_top = pixel_y - self.drag_offset[1] # 获取Canvas信息 canvas_abs_left, canvas_abs_top, canvas_width, canvas_height = self._resolve_canvas_metrics_for_drag(win_x, win_y) # 转换为Canvas相对坐标 (Global Canvas Pos) canvas_relative_left = new_abs_left - canvas_abs_left canvas_relative_top = new_abs_top - canvas_abs_top # 获取当前拖拽的组件数据 comp_data = self.dragging_comp comp_obj = comp_data['object'] # 获取组件宽高 (用于边界限制) comp_width = comp_data.get('width', 0) comp_height = comp_data.get('height', 0) # 尝试从对象获取更准确的宽高 if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: comp_width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: comp_height = comp_obj.height # 限制在Canvas范围内 # 确保左边界不小于0 if canvas_relative_left < 0: canvas_relative_left = 0 # 确保上边界不小于0 if canvas_relative_top < 0: canvas_relative_top = 0 # 确保右边界不超出Canvas宽度 if canvas_relative_left + comp_width > canvas_width: canvas_relative_left = max(0, canvas_width - comp_width) # 确保下边界不超出Canvas高度 if canvas_relative_top + comp_height > canvas_height: canvas_relative_top = max(0, canvas_height - comp_height) # 如果有父组件,需要转换为相对于父组件的局部坐标 parent_index = comp_data.get('parent_index') parent_offset_x = 0 parent_offset_y = 0 if parent_index is not None and parent_index >= 0: # 获取父组件相对于Canvas的累积位置 parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) # 计算新的局部坐标 new_local_left = canvas_relative_left - parent_offset_x new_local_top = canvas_relative_top - parent_offset_y # 如果物理上已脱离父级(挂在Canvas上),则物理坐标就是Canvas相对坐标 if getattr(self, '_is_drag_reparented', False): # 子组件被重定向到Canvas,使其可以移动到负数局部坐标(即父组件外侧) if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(canvas_relative_left, canvas_relative_top) else: comp_obj.left = canvas_relative_left comp_obj.top = canvas_relative_top # print(f"Dragging Child (Canvas Rel): {canvas_relative_left:.1f}, {canvas_relative_top:.1f} | Local: {new_local_left:.1f}, {new_local_top:.1f}") else: # If visual parent is canvas, keep world position while storing local if comp_data.get('visual_parent_canvas'): abs_left = parent_offset_x + new_local_left abs_top = parent_offset_y + new_local_top if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(abs_left, abs_top) else: comp_obj.left = abs_left comp_obj.top = abs_top else: if hasattr(comp_obj, 'set_pos'): comp_obj.set_pos(new_local_left, new_local_top) else: comp_obj.left = new_local_left comp_obj.top = new_local_top # 更新存储的逻辑坐标数据 (保持始终相对于逻辑父级) comp_data['left'] = new_local_left comp_data['top'] = new_local_top # Sync visually-canvas children for non-container parents if self.dragging_index is not None and self.dragging_index >= 0: self._sync_canvas_children(self.dragging_index) def _update_drag(self, task): """每帧更新拖动状态 - 支持Canvas边界约束和局部坐标""" if getattr(self, 'play_mode', False): # Disable editor drag logic while in play mode return task.cont if self._process_canvas_drag(): return task.cont if self._try_start_pending_component_drag(): return task.cont if not self._has_mouse_for_drag(): return task.cont if self._finish_component_drag_if_released(): return task.cont pixel_x, pixel_y, win_x, win_y = self._get_mouse_pixel_data() self._update_active_component_drag(pixel_x, pixel_y, win_x, win_y) return task.cont def _create_resize_handles(self): """创建选择框和8个resize手柄""" from Builtin.LUISprite import LUISprite from Builtin.LUIObject import LUIObject print("开始创建resize handles...") # 先创建4条选择框边线(蓝色,2px宽) for i in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 border.visible = False # 设置选择框层级,在组件之上但在LUI内部 if hasattr(border, 'z_offset'): border.z_offset = 40 # 选中框在普通边框(25)之上 elif hasattr(border, 'set_sort'): border.set_sort(40) # 确保边框在最后创建,这样在渲染顺序上会在前面 self.selection_box.append(border) # 最后创建8个resize手柄(白色圆形,蓝色边框) handle_positions = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left' ] for i, pos in enumerate(handle_positions): # 创建容器对象用于接收鼠标事件 handle_container = LUIObject(parent=self.overlay_root) handle_container.width = 10 handle_container.height = 10 handle_container.solid = True handle_container.visible = False # 设置手柄层级,在选择框之上但在LUI内部 if hasattr(handle_container, 'z_offset'): handle_container.z_offset = 50 # 在选择框之上 elif hasattr(handle_container, 'set_sort'): handle_container.set_sort(50) # 在选择框之上 # 创建蓝色边框(外圈) handle_border = LUISprite(handle_container, "blank", "skin") handle_border.width = 10 handle_border.height = 10 handle_border.left = 0 handle_border.top = 0 handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 # 创建白色圆形背景(内圈) handle_bg = LUISprite(handle_container, "blank", "skin") handle_bg.width = 8 handle_bg.height = 8 handle_bg.left = 1 handle_bg.top = 1 handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 # 存储手柄位置信息 handle_container._resize_position = pos handle_container._resize_index = i handle_container._bg = handle_bg handle_container._border = handle_border # 绑定鼠标事件 handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) self.resize_handles.append(handle_container) print(f"✓ 创建了{len(self.selection_box)}条边框和{len(self.resize_handles)}个手柄") # 强制将手柄移到最前面(在LUI中,后创建的元素通常在前面) for handle in self.resize_handles: # 尝试重新设置父节点来强制更新渲染顺序 if hasattr(handle, 'reparent_to'): handle.reparent_to(self.overlay_root) for border in self.selection_box: if hasattr(border, 'reparent_to'): border.reparent_to(self.overlay_root) def _delayed_create_resize_handles(self, task): """延迟创建resize handles,确保在Canvas之后创建""" self._create_resize_handles() return task.done def _on_handle_mousedown(self, event, handle): """手柄鼠标按下事件""" # 标记交互帧,防止Canvas点击事件触发取消选中 if hasattr(p3d, 'ClockObject'): self._last_interaction_frame = p3d.ClockObject.getGlobalClock().getFrameCount() else: # Fallback if specific import not avail in this scope (though p3d is imported) self._last_interaction_frame = self.world.taskMgr.globalClock.getFrameCount() if self.selected_index < 0: return comp_data = self.components[self.selected_index] print(f"✓ 开始resize: 手柄位置={handle._resize_position}") # 记录开始调整大小的状态 self.resizing_handle = handle # 获取鼠标位置 if hasattr(self.world, 'mouseWatcherNode') and self.world.mouseWatcherNode.hasMouse(): mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 self.resize_start_pos = (pixel_x, pixel_y) # 记录组件的初始边界 self.resize_start_bounds = { 'left': comp_data.get('left', 0), 'top': comp_data.get('top', 0), 'width': comp_data.get('width', 100), 'height': comp_data.get('height', 30), } def _update_resize_handles(self, task): # Restore selection if it was cleared immediately if self.selected_index < 0 and getattr(self, '_force_keep_selection_frames', 0) > 0: if getattr(self, '_last_selected_index', -1) >= 0: self.selected_index = self._last_selected_index if getattr(self, '_force_keep_selection_frames', 0) > 0: self._force_keep_selection_frames -= 1 """更新resize handles的位置和可见性""" # 检查当前Canvas是否可见 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas = self.canvases[self.current_canvas_index] if not canvas.get('visible', True): self._hide_resize_handles() return task.cont if self.selected_index < 0 or self.selected_index >= len(self.components): # 隐藏所有handles self._hide_resize_handles() return task.cont comp_data = self.components[self.selected_index] comp_type = comp_data['type'] # 只对Frame、Button、Slider、InputField、Plane、Image显示resize handles if comp_type not in ['Frame', 'Button', 'Slider', 'InputField', 'Plane', 'Image', 'Checkbox', 'Text', 'Label', 'Video', 'Progressbar', 'Selectbox', 'ScrollableRegion', 'TabbedFrame', 'VerticalLayout', 'HorizontalLayout', 'HttpText']: self._hide_resize_handles() return task.cont # 显示并更新handles位置 self._show_and_update_handles(comp_data) # 处理resize拖拽 if self.resizing_handle is not None: self._handle_resize_drag() return task.cont def _hide_resize_handles(self): """隐藏所有resize handles和selection box""" for border in self.selection_box: border.visible = False for handle in self.resize_handles: handle.visible = False def _ensure_component_outline(self, comp_index): 'Create a simple 4-edge outline for a component if not exists.' if comp_index in self.debug_outlines: return self.debug_outlines[comp_index] from Builtin.LUISprite import LUISprite borders = [] for _ in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.7, 1.0, 1.0) border.visible = False # Ensure outline is above components if hasattr(border, 'z_offset'): border.z_offset = 25 elif hasattr(border, 'set_sort'): border.set_sort(25) borders.append(border) self.debug_outlines[comp_index] = borders return borders def _update_component_outlines(self, task): 'Show component outlines in edit mode, hide in play mode.' if getattr(self, 'play_mode', False) or not self.lui_enabled or not self.show_editor: for borders in self.debug_outlines.values(): for b in borders: b.visible = False return task.cont for idx, comp_data in enumerate(self.components): # If selected, let the resize handles/selection box handle the outline (Bright Blue) # We hide the "debug" outline for the selected item to avoid double-drawing or z-fighting if idx == self.selected_index: borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue comp_obj = comp_data.get('object') if comp_obj is None: continue canvas_index = comp_data.get('canvas_index', self.current_canvas_index) if canvas_index is None or canvas_index < 0 or canvas_index >= len(self.canvases): # Component not assigned to a valid canvas borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue canvas = self.canvases[canvas_index] if not canvas.get('visible', True): # Hide outlines for components on hidden canvases borders = self.debug_outlines.get(idx) if borders: for b in borders: b.visible = False continue # Determine size width = comp_data.get('width', 0) height = comp_data.get('height', 0) try: if hasattr(comp_obj, 'width') and comp_obj.width is not None and comp_obj.width > 0: width = comp_obj.width if hasattr(comp_obj, 'height') and comp_obj.height is not None and comp_obj.height > 0: height = comp_obj.height except Exception: pass if width <= 0 or height <= 0: continue # Determine Position (Absolute) canvas_abs_left = 0 canvas_abs_top = 0 canvas_panel = self.canvases[canvas_index]['panel'] canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except Exception: canvas_abs_left = 300 canvas_abs_top = 100 if hasattr(comp_obj, 'get_abs_pos'): try: abs_pos = comp_obj.get_abs_pos() abs_left = abs_pos.x abs_top = abs_pos.y except Exception: acc_left, acc_top = self._get_component_accumulated_pos(idx) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top else: acc_left, acc_top = self._get_component_accumulated_pos(idx) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top # Draw Outline borders = self._ensure_component_outline(idx) # Style for unselected components: Gray, thinner or same thickness for b in borders: b.color = (0.7, 0.7, 0.7, 0.5) # Light gray, semi-transparent if hasattr(b, 'z_offset'): b.z_offset = 25 elif hasattr(b, 'set_sort'): b.set_sort(25) b.visible = True t = 1.0 # Thickness borders[0].left = abs_left borders[0].top = abs_top - t borders[0].width = width borders[0].height = t borders[1].left = abs_left borders[1].top = abs_top + height borders[1].width = width borders[1].height = t borders[2].left = abs_left - t borders[2].top = abs_top borders[2].width = t borders[2].height = height borders[3].left = abs_left + width borders[3].top = abs_top borders[3].width = t borders[3].height = height return task.cont def _update_http_components_task(self, task): """Drive async updates for HttpText components.""" if not self.lui_enabled: return task.cont try: self.luiFunction.update_http_components(self) except Exception: pass return task.cont def _show_and_update_handles(self, comp_data): """显示并更新handles位置 - 修复坐标系不一致问题""" # 每次显示时重新创建手柄,确保它们在最前面 if not hasattr(self, '_handles_recreated'): self._recreate_handles_on_top() self._handles_recreated = True # 获取组件的Canvas相对位置和尺寸 comp_obj = comp_data['object'] # 获取Canvas的绝对位置用于坐标转换 canvas_abs_left = 0 canvas_abs_top = 0 if self.current_canvas_index >= 0 and self.current_canvas_index < len(self.canvases): canvas_panel = self.canvases[self.current_canvas_index]['panel'] # 使用LUI对象的left和top属性,而不是get_pos()方法 canvas_abs_left = getattr(canvas_panel, 'left', 0) canvas_abs_top = getattr(canvas_panel, 'top', 0) # 如果left/top属性不存在,尝试使用get_pos() if canvas_abs_left == 0 and canvas_abs_top == 0: try: canvas_pos = canvas_panel.get_pos() canvas_abs_left = canvas_pos.x canvas_abs_top = canvas_pos.y except: # 如果get_pos()也失败,使用默认值 canvas_abs_left = 300 # 默认Canvas位置 canvas_abs_top = 100 # 尝试直接获取组件的绝对屏幕坐标 if hasattr(comp_obj, 'get_abs_pos'): try: abs_pos = comp_obj.get_abs_pos() abs_left = abs_pos.x abs_top = abs_pos.y # print(f"使用 get_abs_pos(): {abs_left}, {abs_top}") except Exception as e: print(f"get_abs_pos() 失败: {e}") # Fallback to manual calculation acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top else: # 递归计算组件的 Canvas 相对位置(累加所有父组件的相对位置) # 使用 self.selected_index 似乎最安全,或者反查 comp_data if self.selected_index >= 0 and self.components[self.selected_index] == comp_data: acc_left, acc_top = self._get_component_accumulated_pos(self.selected_index) else: acc_left = comp_data.get('left', 0) acc_top = comp_data.get('top', 0) # 转换为绝对屏幕坐标(用于手柄和边框显示) abs_left = canvas_abs_left + acc_left abs_top = canvas_abs_top + acc_top # 获取组件尺寸 - 优先从LUI对象获取实际尺寸 comp_type = comp_data['type'] width = comp_data.get('width', 100) height = comp_data.get('height', 30) # 尝试从LUI对象获取实际尺寸 try: if hasattr(comp_obj, 'width') and comp_obj.width is not None: actual_width = comp_obj.width if actual_width > 0: width = actual_width if hasattr(comp_obj, 'height') and comp_obj.height is not None: actual_height = comp_obj.height if actual_height > 0: height = actual_height except: pass # 确保宽高是数值类型 try: width = float(width) height = float(height) except (TypeError, ValueError): width = 100.0 height = 30.0 # 更新comp_data中的尺寸 comp_data['width'] = width comp_data['height'] = height # 更新4条边框(使用绝对位置,因为边框在overlay_root下) # 确保 selection_box 存在 if not self.selection_box: self._create_resize_handles() if not self.selection_box: # Still empty? return # 上边框 self.selection_box[0].left = abs_left self.selection_box[0].top = abs_top - 2 self.selection_box[0].width = width self.selection_box[0].height = 2 self.selection_box[0].visible = True # 下边框 self.selection_box[1].left = abs_left self.selection_box[1].top = abs_top + height self.selection_box[1].width = width self.selection_box[1].height = 2 self.selection_box[1].visible = True # 左边框 self.selection_box[2].left = abs_left - 2 self.selection_box[2].top = abs_top self.selection_box[2].width = 2 self.selection_box[2].height = height self.selection_box[2].visible = True # 右边框 self.selection_box[3].left = abs_left + width self.selection_box[3].top = abs_top self.selection_box[3].width = 2 self.selection_box[3].height = height self.selection_box[3].visible = True # 更新8个手柄位置(使用绝对位置,因为手柄在overlay_root下) handle_positions = [ (abs_left - 5, abs_top - 5), # 左上 (abs_left + width/2 - 5, abs_top - 5), # 上 (abs_left + width - 5, abs_top - 5), # 右上 (abs_left + width - 5, abs_top + height/2 - 5), # 右 (abs_left + width - 5, abs_top + height - 5), # 右下 (abs_left + width/2 - 5, abs_top + height - 5), # 下 (abs_left - 5, abs_top + height - 5), # 左下 (abs_left - 5, abs_top + height/2 - 5), # 左 ] for i, (h_left, h_top) in enumerate(handle_positions): if i < len(self.resize_handles): handle = self.resize_handles[i] handle.left = h_left handle.top = h_top handle.visible = True def _recreate_handles_on_top(self): """重新创建手柄,确保它们在最前面""" from Builtin.LUISprite import LUISprite from Builtin.LUIObject import LUIObject # 清除旧的手柄 for handle in self.resize_handles: if hasattr(handle, 'remove'): handle.remove() for border in self.selection_box: if hasattr(border, 'remove'): border.remove() self.resize_handles.clear() self.selection_box.clear() # 重新创建边框 for i in range(4): border = LUISprite(self.overlay_root, "blank", "skin") border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色 border.visible = False # 设置选择框层级,在组件之上但在LUI内部 if hasattr(border, 'z_offset'): border.z_offset = 40 elif hasattr(border, 'set_sort'): border.set_sort(40) self.selection_box.append(border) # 重新创建手柄 handle_positions = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left' ] for i, pos in enumerate(handle_positions): handle_container = LUIObject(parent=self.overlay_root) handle_container.width = 10 handle_container.height = 10 handle_container.solid = True handle_container.visible = False # 设置手柄层级,在选择框之上但在LUI内部 if hasattr(handle_container, 'z_offset'): handle_container.z_offset = 50 elif hasattr(handle_container, 'set_sort'): handle_container.set_sort(50) # 创建蓝色边框(外圈) handle_border = LUISprite(handle_container, "blank", "skin") handle_border.width = 10 handle_border.height = 10 handle_border.left = 0 handle_border.top = 0 handle_border.color = (0.2, 0.5, 1.0, 1.0) # 蓝色边框 # 创建白色圆形背景(内圈) handle_bg = LUISprite(handle_container, "blank", "skin") handle_bg.width = 8 handle_bg.height = 8 handle_bg.left = 1 handle_bg.top = 1 handle_bg.color = (1.0, 1.0, 1.0, 1.0) # 白色 # 存储手柄位置信息 handle_container._resize_position = pos handle_container._resize_index = i handle_container._bg = handle_bg handle_container._border = handle_border # 绑定鼠标事件 handle_container.bind("mousedown", lambda event, h=handle_container: self._on_handle_mousedown(event, h)) self.resize_handles.append(handle_container) print("✓ 重新创建了手柄,确保在最前面") def _handle_resize_drag(self): """处理 resize 拖拽(编排入口)。""" self._handle_resize_drag_core() def _handle_resize_drag_core(self): """处理resize拖拽 - 完整功能实现,支持Shift和Alt键操作""" if not hasattr(self.world, 'mouseWatcherNode') or not self.world.mouseWatcherNode.hasMouse(): return import panda3d.core as p3d if self._finish_resize_drag_if_released(p3d): return # 获取当前鼠标位置 mouse_x = self.world.mouseWatcherNode.getMouseX() mouse_y = self.world.mouseWatcherNode.getMouseY() win_x = self.world.win.getXSize() win_y = self.world.win.getYSize() pixel_x = (mouse_x + 1) * win_x / 2 pixel_y = (1 - mouse_y) * win_y / 2 # 计算鼠标移动距离 delta_x = pixel_x - self.resize_start_pos[0] delta_y = pixel_y - self.resize_start_pos[1] # 检查修饰键 shift_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.shift()) alt_held = self.world.mouseWatcherNode.is_button_down(p3d.KeyboardButton.alt()) # 获取组件数据 comp_data = self.components[self.selected_index] comp_obj = comp_data['object'] comp_type = comp_data['type'] new_left, new_top, new_width, new_height = self._compute_resize_drag_bounds( delta_x, delta_y, shift_held, alt_held ) if self.selected_index < 0: return comp_data = self.components[self.selected_index] self._apply_resize_drag_updates( comp_data, comp_obj, comp_type, new_left, new_top, new_width, new_height, shift_held, alt_held, ) def _finish_resize_drag_if_released(self, p3d): """鼠标释放时结束缩放并执行收尾同步。""" if self.world.mouseWatcherNode.is_button_down(p3d.MouseButton.one()): return False if self.resizing_handle and self.selected_index >= 0 and self.selected_index < len(self.components): comp_data = self.components[self.selected_index] width_val = comp_data.get('width', 0) height_val = comp_data.get('height', 0) print(f"✓ 组件调整完成: width={width_val:.1f}, height={height_val:.1f}") # 更新锚点位置(如果有锚点) if comp_data.get('anchored_to_parent'): anchor_pos = comp_data.get('anchor_position') if anchor_pos: self._update_component_anchor_position(self.selected_index, anchor_pos) self.resizing_handle = None return True def _compute_resize_drag_bounds(self, delta_x, delta_y, shift_held, alt_held): """根据当前手柄/修饰键计算新的 left/top/width/height。""" start_left, start_top, start_width, start_height, aspect_ratio = self._get_resize_start_values() handle_pos = self.resizing_handle._resize_position new_left, new_top, new_width, new_height = self._apply_resize_handle_delta( handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y ) new_left, new_top, new_width, new_height = self._apply_resize_min_size_clamp( handle_pos, start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, ) if shift_held: new_left, new_top, new_width, new_height = self._apply_shift_keep_ratio_resize( handle_pos, start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, aspect_ratio, ) if alt_held: new_left, new_top, new_width, new_height = self._apply_alt_center_resize( start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, ) self._cache_resize_canvas_bounds() return self._sanitize_resize_dimensions(new_left, new_top, new_width, new_height) def _apply_resize_drag_updates( self, comp_data, comp_obj, comp_type, new_left, new_top, new_width, new_height, shift_held, alt_held, ): """将缩放结果写回组件数据与渲染对象。""" parent_index, parent_offset_x, parent_offset_y, is_scene_parented = self._resolve_resize_parent_context( comp_data, comp_obj ) _ = parent_index self._write_resize_component_data(comp_data, new_left, new_top, new_width, new_height) self._apply_resize_component_position( comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented ) try: self._apply_resize_component_size_by_type(comp_data, comp_obj, comp_type, new_width, new_height) self._post_resize_component_sync(comp_type, shift_held, alt_held) except Exception as e: print(f"⚠ 设置组件尺寸失败 ({comp_type}): {e}") def _get_resize_start_values(self): start_left = self.resize_start_bounds['left'] start_top = self.resize_start_bounds['top'] start_width = self.resize_start_bounds['width'] start_height = self.resize_start_bounds['height'] aspect_ratio = start_width / start_height if start_height > 0 else 1 return start_left, start_top, start_width, start_height, aspect_ratio def _apply_resize_handle_delta( self, handle_pos, start_left, start_top, start_width, start_height, delta_x, delta_y ): new_left = start_left new_top = start_top new_width = start_width new_height = start_height if handle_pos == 'top-left': new_left = start_left + delta_x new_top = start_top + delta_y new_width = start_width - delta_x new_height = start_height - delta_y elif handle_pos == 'top': new_top = start_top + delta_y new_height = start_height - delta_y elif handle_pos == 'top-right': new_top = start_top + delta_y new_width = start_width + delta_x new_height = start_height - delta_y elif handle_pos == 'right': new_width = start_width + delta_x elif handle_pos == 'bottom-right': new_width = start_width + delta_x new_height = start_height + delta_y elif handle_pos == 'bottom': new_height = start_height + delta_y elif handle_pos == 'bottom-left': new_left = start_left + delta_x new_width = start_width - delta_x new_height = start_height + delta_y elif handle_pos == 'left': new_left = start_left + delta_x new_width = start_width - delta_x return new_left, new_top, new_width, new_height def _apply_resize_min_size_clamp( self, handle_pos, start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, ): if new_width < self.min_size: if handle_pos in ['top-left', 'left', 'bottom-left']: new_left = start_left + start_width - self.min_size new_width = self.min_size if new_height < self.min_size: if handle_pos in ['top-left', 'top', 'top-right']: new_top = start_top + start_height - self.min_size new_height = self.min_size return new_left, new_top, new_width, new_height def _apply_shift_keep_ratio_resize( self, handle_pos, start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, aspect_ratio, ): width_change = abs(new_width - start_width) height_change = abs(new_height - start_height) if width_change > height_change: new_height = new_width / aspect_ratio if handle_pos in ['top-left', 'top', 'top-right']: new_top = start_top + start_height - new_height else: new_width = new_height * aspect_ratio if handle_pos in ['top-left', 'left', 'bottom-left']: new_left = start_left + start_width - new_width return new_left, new_top, new_width, new_height def _apply_alt_center_resize( self, start_left, start_top, start_width, start_height, new_left, new_top, new_width, new_height, ): center_x = start_left + start_width / 2 center_y = start_top + start_height / 2 width_diff = new_width - start_width height_diff = new_height - start_height new_width = start_width + width_diff * 2 new_height = start_height + height_diff * 2 new_left = center_x - new_width / 2 new_top = center_y - new_height / 2 if new_width < self.min_size: new_width = self.min_size new_left = center_x - new_width / 2 if new_height < self.min_size: new_height = self.min_size new_top = center_y - new_height / 2 return new_left, new_top, new_width, new_height def _cache_resize_canvas_bounds(self): canvas_width = 800 canvas_height = 600 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() _ = (canvas_width, canvas_height) def _sanitize_resize_dimensions(self, new_left, new_top, new_width, new_height): if new_width < 1.0: new_width = 1.0 if new_height < 1.0: new_height = 1.0 return new_left, new_top, new_width, new_height def _resolve_resize_parent_context(self, comp_data, comp_obj): parent_index = comp_data.get('parent_index') parent_offset_x = 0 parent_offset_y = 0 if parent_index is not None and parent_index >= 0: parent_offset_x, parent_offset_y = self._get_component_accumulated_pos(parent_index) child_parent = getattr(comp_obj, 'parent', None) parent_obj = None if parent_index is not None and parent_index >= 0: parent_obj = self.components[parent_index].get('object') is_scene_parented = (child_parent is not None and child_parent == parent_obj) return parent_index, parent_offset_x, parent_offset_y, is_scene_parented def _write_resize_component_data(self, comp_data, new_left, new_top, new_width, new_height): comp_data['left'] = new_left comp_data['top'] = new_top comp_data['width'] = new_width comp_data['height'] = new_height def _apply_resize_component_position( self, comp_obj, new_left, new_top, parent_offset_x, parent_offset_y, is_scene_parented ): if is_scene_parented: comp_obj.left = new_left comp_obj.top = new_top else: comp_obj.left = parent_offset_x + new_left comp_obj.top = parent_offset_y + new_top def _apply_resize_component_size_by_type(self, comp_data, comp_obj, comp_type, new_width, new_height): if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height if comp_type == 'Frame': pass elif comp_type in ['Plane', 'Image', 'Video', 'HttpText']: sprite = comp_data.get('sprite') if sprite is not None: sprite.width = new_width sprite.height = new_height if comp_type == 'HttpText': self.luiFunction.sync_http_text_layout(self, comp_data) elif comp_type == 'Button': self._apply_button_resize_size(comp_obj, new_width, new_height) elif comp_type == 'InputField': self._apply_input_field_resize_size(comp_obj, new_width, new_height) elif comp_type == 'Slider': if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) def _apply_button_resize_size(self, comp_obj, new_width, new_height): if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) if hasattr(comp_obj, '_apply_stretch_sizes'): comp_obj._apply_stretch_sizes() def _apply_input_field_resize_size(self, comp_obj, new_width, new_height): if hasattr(comp_obj, 'set_width'): comp_obj.set_width(new_width) if hasattr(comp_obj, 'set_height'): comp_obj.set_height(new_height) if hasattr(comp_obj, 'width'): comp_obj.width = new_width if hasattr(comp_obj, 'height'): comp_obj.height = new_height self._sync_input_field_layout_stretch(comp_obj) def _sync_input_field_layout_stretch(self, comp_obj): try: layout = getattr(comp_obj, '_layout', None) if layout is not None: if hasattr(layout, 'width'): layout.width = '100%' if hasattr(layout, 'height'): layout.height = '100%' inner = getattr(layout, '_layout', None) if inner is not None: if hasattr(inner, 'width'): inner.width = '100%' if hasattr(inner, 'height'): inner.height = '100%' for attr in ('_sprite_left', '_sprite_mid', '_sprite_right'): spr = getattr(layout, attr, None) if spr is not None and hasattr(spr, 'height'): spr.height = '100%' if spr is not None and attr == '_sprite_mid' and hasattr(spr, 'width'): spr.width = '100%' except Exception: pass def _post_resize_component_sync(self, comp_type, shift_held, alt_held): if comp_type in ['VerticalLayout', 'HorizontalLayout']: self._update_layout_inner(self.selected_index) self._update_anchored_children(self.selected_index) modifier_str = self._get_resize_modifier_info(shift_held, alt_held) _ = modifier_str def _get_resize_modifier_info(self, shift_held, alt_held): modifier_info = [] if shift_held: modifier_info.append('Shift(keep ratio)') if alt_held: modifier_info.append('Alt(center scale)') return ' + '.join(modifier_info) if modifier_info else 'Normal'