// PopupMenu 实现分片 —— 几何与排版(BuildLayout / 子菜单 / 滚动区 / 内联输入框位置)。 // // 本文件只包含 PopupMenu 的成员函数定义(从 PopupMenu.cpp 机械搬移,行为不变)。 // // ⚠️ 这里是菜单**唯一**的几何来源:`Paint` 与所有命中测试都读它产出的矩形, // 不得在别处再反推一遍(否则会出现"看得见点不到")。 // ⚠️ `NaturalEditFieldWidth` / `EditSuffixWidth` 同时参与排版与绘制,改公式必须两处一致。 #include "PopupMenu.hpp" #include "PopupMenuInternal.hpp" namespace jdi { void PopupMenu::Setup(const UiFonts* fonts, const Theme* theme) { m_fonts = fonts; m_theme = theme; } void PopupMenu::SetScale(float scale) { m_scale = (scale > 0.1f) ? scale : 1.0f; } float PopupMenu::ItemHeight(const MenuItem& item) const { switch (item.kind) { case MenuItemKind::SEPARATOR: return kSeparatorHeight * m_scale; case MenuItemKind::HEADER: case MenuItemKind::HEADER_ACTION: return kHeaderHeight * m_scale; case MenuItemKind::SWATCHES: return kSwatchRowHeight * m_scale; case MenuItemKind::EDIT: // 编辑项比普通项略高,给输入框留出上下呼吸空间。 return (kRowHeight + 4.0f) * m_scale; case MenuItemKind::NORMAL: default: return kRowHeight * m_scale; } } void PopupMenu::BuildLayout(const gd::RectF& bounds, int x, int y, bool expandUp) { m_itemRects.assign(m_items.size(), gd::RectF()); m_swatchRects.assign(m_items.size(), std::vector()); if (m_fonts == nullptr) { m_rect = gd::RectF(); return; } float contentWidth = kMinWidth * m_scale; float totalHeight = kPadV * 2.0f * m_scale; std::vector heights(m_items.size(), 0.0f); for (std::size_t index = 0; index < m_items.size(); ++index) { const MenuItem& item = m_items[index]; heights[index] = ItemHeight(item); totalHeight += heights[index]; if (item.kind == MenuItemKind::SWATCHES) { const int count = std::max(0, std::min(item.swatchCount, 16)); const float width = (kPadH + kItemPad) * 2.0f * m_scale + static_cast(count) * kSwatchSize * m_scale + static_cast(std::max(0, count - 1)) * kSwatchGap * m_scale; contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::HEADER || item.kind == MenuItemKind::HEADER_ACTION) { // 标题与普通项共用同一文字列与右侧标记列,保证整个菜单的左右边距一致。 const float iconExtra = (item.kind == MenuItemKind::HEADER_ACTION) ? (kHeaderIconSize + kHeaderIconGap) * m_scale : 0.0f; const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale + kMarkerGap * m_scale + iconExtra + UiMeasureTextWidth(m_fonts->uiSmallBold, item.label); contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::NORMAL) { const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale + kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label); contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::EDIT) { // 说明文字 + 输入框;输入框宽度按数值文本自适应(与 EditFieldRect 保持一致)。 const float fieldWidth = NaturalEditFieldWidth(item); const float width = (kPadH + kItemPad) * 2.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale + fieldWidth; contentWidth = std::max(contentWidth, width); } } contentWidth = std::min(contentWidth, kMaxWidth * m_scale); const float width = contentWidth; // ------------------------------------------------------------------ // 超长菜单:限制高度 + 条目区可滚动 + 顶部/尾部固定项(见 `MenuLayoutConstraint`)。 // * 顶部固定 = **前导的 `HEADER` / `HEADER_ACTION`**(典型:「所属网格」标题)—— // 用户 2026-10-16 要求:滚动时标题必须留在最上方不动; // * 尾部固定 = **最后一个分隔线及其之后的条目**(典型:「新建网格」)钉在底部; // * 中间是滚动区(鼠标滚轮 / 键盘 / 滚动条),右侧画一根滚动条。 // ------------------------------------------------------------------ int pinnedFrom = -1; int scrollFirst = 0; if (m_constraint.maxHeight > 0.0f || m_constraint.maxScrollRows > 0) { for (int index = static_cast(m_items.size()) - 1; index > 0; --index) { if (m_items[static_cast(index)].kind == MenuItemKind::SEPARATOR) { pinnedFrom = index; break; } } while (scrollFirst < pinnedFrom && (m_items[static_cast(scrollFirst)].kind == MenuItemKind::HEADER || m_items[static_cast(scrollFirst)].kind == MenuItemKind::HEADER_ACTION)) { ++scrollFirst; // 前导标题固定在顶部,不参与滚动 } } float headHeight = 0.0f; float scrollRegionHeight = 0.0f; float tailHeight = 0.0f; if (pinnedFrom > 0) { for (int index = 0; index < static_cast(m_items.size()); ++index) { if (index >= pinnedFrom) { tailHeight += heights[static_cast(index)]; } else if (index < scrollFirst) { headHeight += heights[static_cast(index)]; } else { scrollRegionHeight += heights[static_cast(index)]; } } } float height = totalHeight; m_scrollEnabled = false; m_scrollRegionHeight = 0.0f; m_scrollFirst = 0; m_pinnedIndex = -1; m_scrollViewport = gd::RectF(); m_scrollBarRect = gd::RectF(); // ⚠️ **不要在这里复位 `m_scrollBarDragging`**:拖拽滚动条时每一帧都会 `RebuildLayout()`, // 在这里清零会把拖拽态当场吃掉(拖一下断一下)。该状态只在 `Open` / `Close` 里复位。 // ⚠️ **不要在这里无条件清零 `m_scrollOffset`**:`RebuildLayout()`(滚轮 / 数值回写 / 拖滚动条) // 会重新走一遍本函数,清零就等于把滚动位置吃掉(滚轮“滚不动”)。只有在菜单**不再需要** // 滚动时才复位(`Open` 里已经为每次打开重置过)。 if (pinnedFrom > 0) { // 滚动视口高度上限:`maxHeight`(客户区像素 → 换算成视口可用高)与 // `maxScrollRows`(标准行数)**取更严格的那个**;0 = 该约束不生效。 float viewportLimit = 0.0f; if (m_constraint.maxHeight > 0.0f) { // 夹到宿主矩形以内(不盖住窗口边框);小到放不下时的上限直接视作“不限制”, // 交给下方的边界收敛处理,避免算出一个高度为 0 的菜单。 const float menuLimit = std::min(m_constraint.maxHeight, bounds.Height - 4.0f * m_scale); if (menuLimit >= 40.0f * m_scale) { viewportLimit = std::max( 0.0f, menuLimit - kPadV * 2.0f * m_scale - headHeight - tailHeight); } } if (m_constraint.maxScrollRows > 0) { const float rows = static_cast(m_constraint.maxScrollRows) * kRowHeight * m_scale; viewportLimit = (viewportLimit > 0.0f) ? std::min(viewportLimit, rows) : rows; } if (viewportLimit >= kRowHeight * m_scale && scrollRegionHeight > viewportLimit + 0.5f) { const float viewportHeight = std::min(scrollRegionHeight, viewportLimit); height = kPadV * 2.0f * m_scale + headHeight + viewportHeight + tailHeight; m_scrollEnabled = true; m_scrollFirst = scrollFirst; m_pinnedIndex = pinnedFrom; m_scrollRegionHeight = scrollRegionHeight; m_scrollOffset = std::max( 0.0f, std::min(m_scrollOffset, std::max(0.0f, scrollRegionHeight - viewportHeight))); } else { m_scrollOffset = 0.0f; } } else { m_scrollOffset = 0.0f; } // 带说明文字的 EDIT 项共用**同一个输入框宽度**:取其中最大的自然宽度, // 再按“给最宽的说明文字留位 / 不超过内容区 3/4”夹取。 // ⚠️ 若每项各自按自己的文本长度算,同一面板里就会出现一个宽一个窄 // (时间轴行的文本更长、还带单位后缀,于是它的输入框比 Y 轴行宽一截)。 float labeledFieldWidth = 0.0f; float widestLabel = 0.0f; for (const MenuItem& item : m_items) { if (item.kind != MenuItemKind::EDIT || item.label.empty()) { continue; } labeledFieldWidth = std::max(labeledFieldWidth, NaturalEditFieldWidth(item)); widestLabel = std::max(widestLabel, UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale); } if (labeledFieldWidth > 0.0f) { const float rowContent = std::max(40.0f * m_scale, (width - 2.0f * (kPadH + kItemPad) * m_scale)); const float limit = std::max(40.0f * m_scale, std::min(rowContent * 0.75f, rowContent - widestLabel)); labeledFieldWidth = std::min(labeledFieldWidth, limit); } m_editFieldWidth = labeledFieldWidth; float left = static_cast(x); // `expandUp`:锚点是菜单的**左下角**(向上展开);否则锚点是左上角(向右下展开)。 float top = expandUp ? (static_cast(y) - height) : static_cast(y); // 优先向右展开(向左下/右下);越界时向左或向上翻转,最后做边界收敛。 if (left + width > bounds.X + bounds.Width) { left = static_cast(x) - width; } if (!expandUp && top + height > bounds.Y + bounds.Height) { top = static_cast(y) - height; } left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f)); top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f)); m_rect = gd::RectF(left, top, width, height); // 条目排布:头部固定区从菜单顶往下排;滚动区条目整体上移 `m_scrollOffset`; // 尾部固定区从菜单底部往上对齐。 const float bodyTop = top + kPadV * m_scale; const float tailTop = top + height - kPadV * m_scale - tailHeight; const float scrollBarReserve = m_scrollEnabled ? (kScrollBarWidth + kScrollBarGap) * m_scale : 0.0f; m_scrollViewport = m_scrollEnabled ? gd::RectF(left + kPadH * m_scale, bodyTop + headHeight, std::max(0.0f, width - kPadH * 2.0f * m_scale), std::max(0.0f, height - kPadV * 2.0f * m_scale - headHeight - tailHeight)) : gd::RectF(); float cursorY = bodyTop; float headCursorY = bodyTop; float scrollCursorY = bodyTop + headHeight - m_scrollOffset; float tailCursorY = (m_scrollEnabled) ? tailTop : bodyTop; for (std::size_t index = 0; index < m_items.size(); ++index) { const MenuItem& item = m_items[index]; const float itemHeight = heights[index]; float rectY = cursorY; float rectWidth = std::max(0.0f, width - kPadH * 2.0f * m_scale); if (m_scrollEnabled) { const int itemIndex = static_cast(index); if (itemIndex < m_scrollFirst) { rectY = headCursorY; headCursorY += itemHeight; } else if (itemIndex < m_pinnedIndex) { rectY = scrollCursorY; scrollCursorY += itemHeight; // 滚动区条目让出右侧滚动条的位置(对勾/文本不会压在滑块上)。 rectWidth = std::max(0.0f, rectWidth - scrollBarReserve); } else { rectY = tailCursorY; tailCursorY += itemHeight; } } m_itemRects[index] = gd::RectF(left + kPadH * m_scale, rectY, rectWidth, itemHeight); if (item.kind == MenuItemKind::SWATCHES) { const int count = std::max(0, std::min(item.swatchCount, 16)); std::vector& rects = m_swatchRects[index]; rects.reserve(static_cast(count)); const float swatchY = rectY + (itemHeight - kSwatchSize * m_scale) * 0.5f; const float startX = left + (kPadH + kItemPad) * m_scale; for (int swatch = 0; swatch < count; ++swatch) { const float swatchX = startX + static_cast(swatch) * (kSwatchSize + kSwatchGap) * m_scale; rects.push_back(gd::RectF(swatchX, swatchY, kSwatchSize * m_scale, kSwatchSize * m_scale)); } } cursorY += itemHeight; } // 滚动条轨道:贴在可滚动条目区的右内侧(只在条目区旁边,不会盖住固定区)。 if (m_scrollEnabled) { const float thickness = kScrollBarWidth * m_scale; m_scrollBarRect = gd::RectF(left + width - kPadH * m_scale - thickness, m_scrollViewport.Y, thickness, m_scrollViewport.Height); } } bool PopupMenu::ScrollRegion(int& first, int& last) const { if (!m_scrollEnabled || m_pinnedIndex < 0) { first = 0; last = static_cast(m_items.size()); return false; } first = m_scrollFirst; last = m_pinnedIndex; return true; } gd::RectF PopupMenu::ScrollBarThumbRect() const { if (!m_scrollEnabled || m_scrollBarRect.Height <= 0.0f || m_scrollRegionHeight <= 0.0f) { return gd::RectF(); } const float viewport = m_scrollViewport.Height; if (viewport <= 0.0f) { return gd::RectF(); } const float ratio = std::min(1.0f, viewport / m_scrollRegionHeight); const float thumbHeight = std::max(kScrollBarMinThumb * m_scale, m_scrollBarRect.Height * ratio); const float maxScroll = std::max(0.0f, m_scrollRegionHeight - viewport); const float progress = (maxScroll > 0.0f) ? (m_scrollOffset / maxScroll) : 0.0f; const float thumbY = m_scrollBarRect.Y + std::max(0.0f, std::min(1.0f, progress)) * std::max(0.0f, m_scrollBarRect.Height - thumbHeight); return gd::RectF(m_scrollBarRect.X, thumbY, m_scrollBarRect.Width, thumbHeight); } float PopupMenu::ScrollOffsetForThumbTop(float thumbTop) const { if (!m_scrollEnabled || m_scrollBarRect.Height <= 0.0f) { return m_scrollOffset; } const float viewport = m_scrollViewport.Height; const float maxScroll = std::max(0.0f, m_scrollRegionHeight - viewport); if (maxScroll <= 0.0f) { return 0.0f; } const float track = m_scrollBarRect.Height; const float thumbHeight = std::max(kScrollBarMinThumb * m_scale, track * std::min(1.0f, viewport / std::max(1.0f, m_scrollRegionHeight))); const float travel = std::max(0.0f, track - thumbHeight); if (travel <= 0.0f) { return 0.0f; } const float relative = thumbTop - m_scrollBarRect.Y; const float progress = std::max(0.0f, std::min(1.0f, relative / travel)); return progress * maxScroll; } bool PopupMenu::EnsureItemVisible(int index) { if (!m_scrollEnabled || index < m_scrollFirst || index >= m_pinnedIndex || m_scrollViewport.Height <= 0.0f) { return false; } const gd::RectF& rect = m_itemRects[static_cast(index)]; const float relative = rect.Y - m_scrollViewport.Y; float delta = 0.0f; if (relative < 0.0f) { delta = relative; } else if (relative + rect.Height > m_scrollViewport.Height) { delta = relative + rect.Height - m_scrollViewport.Height; } if (std::fabs(delta) < 0.5f) { return false; } m_scrollOffset += delta; // 重新排布(内部会重新夹取 `m_scrollOffset`)。 RebuildLayout(); return true; } bool PopupMenu::OnMouseWheel(int delta) { if (!m_open || !m_scrollEnabled || delta == 0) { return false; } const float previous = m_scrollOffset; // 向上滚(`delta > 0`)= 看更靠前的条目 ⇒ 偏移减小。 m_scrollOffset -= static_cast(delta) / 120.0f * 54.0f * m_scale; // 重新排布:条目矩形依赖滚动偏移,同时会夹取到合法范围。 RebuildLayout(); return std::fabs(m_scrollOffset - previous) > 0.01f; } float PopupMenu::NaturalEditFieldWidth(const MenuItem& item) const { const std::wstring& widest = (item.editValue.size() > item.value.size()) ? item.editValue : item.value; const float natural = std::max(kEditFieldWidth * m_scale, UiMeasureTextWidth(m_fonts->uiSmall, widest) + 20.0f * m_scale); const float withSuffix = natural + EditSuffixWidth(item); // 无说明文字 = 整行输入框(如「系数调整」面板):给一个更宽的默认值,视觉上独占整行。 return item.label.empty() ? std::max(withSuffix, kRowFieldWidth * m_scale) : withSuffix; } void PopupMenu::CloseSubmenu() { m_subOpen = false; m_subParentIndex = -1; m_subRect = gd::RectF(); m_subItems.clear(); m_subItemRects.clear(); m_subHoverIndex = -1; m_subPressedIndex = -1; } void PopupMenu::BuildSubLayout(int parentIndex) { CloseSubmenu(); if (parentIndex < 0 || parentIndex >= static_cast(m_items.size())) { return; } const MenuItem& parent = m_items[static_cast(parentIndex)]; if (parent.kind != MenuItemKind::SUBMENU || parent.submenu.empty()) { return; } m_subItems = parent.submenu; // 与主菜单一致:宽度取条目中的最大者并夹在上下限之间。 float contentWidth = kMinWidth * m_scale; float totalHeight = kPadV * 2.0f * m_scale; for (const MenuItem& item : m_subItems) { totalHeight += ItemHeight(item); if (item.kind == MenuItemKind::SWATCHES) { const int count = std::max(0, std::min(item.swatchCount, 16)); const float width = (kPadH + kItemPad) * 2.0f * m_scale + static_cast(count) * kSwatchSize * m_scale + static_cast(std::max(0, count - 1)) * kSwatchGap * m_scale; contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::HEADER) { const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale + kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmallBold, item.label); contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::EDIT) { const float fieldWidth = NaturalEditFieldWidth(item); const float width = (kPadH + kItemPad) * 2.0f * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale + fieldWidth; contentWidth = std::max(contentWidth, width); } else if (item.kind == MenuItemKind::NORMAL || item.kind == MenuItemKind::SUBMENU) { const float width = (kPadH + kItemPad) * 2.0f * m_scale + kMarkerColumn * m_scale + kMarkerGap * m_scale + UiMeasureTextWidth(m_fonts->uiSmall, item.label); contentWidth = std::max(contentWidth, width); } } contentWidth = std::min(contentWidth, kMaxWidth * m_scale); const float width = contentWidth; const float height = totalHeight; const gd::RectF& parentRect = m_itemRects[static_cast(parentIndex)]; const gd::RectF& bounds = m_buildBounds; // 默认贴主菜单右缘并向上与父项顶端对齐;右侧空间不足时翻到左侧。 float left = m_rect.X + m_rect.Width - kSubOverlap * m_scale; if (left + width > bounds.X + bounds.Width - 2.0f) { left = m_rect.X - width + kSubOverlap * m_scale; } left = std::max(bounds.X + 2.0f, std::min(left, bounds.X + bounds.Width - width - 2.0f)); float top = parentRect.Y - kPadV * m_scale; top = std::max(bounds.Y + 2.0f, std::min(top, bounds.Y + bounds.Height - height - 2.0f)); m_subRect = gd::RectF(left, top, width, height); m_subItemRects.assign(m_subItems.size(), gd::RectF()); float cursorY = top + kPadV * m_scale; for (std::size_t index = 0; index < m_subItems.size(); ++index) { const float itemHeight = ItemHeight(m_subItems[index]); m_subItemRects[index] = gd::RectF(left + kPadH * m_scale, cursorY, std::max(0.0f, width - kPadH * 2.0f * m_scale), itemHeight); cursorY += itemHeight; } m_subParentIndex = parentIndex; m_subOpen = true; m_subHoverIndex = -1; m_subPressedIndex = -1; } int PopupMenu::SubItemAtPoint(int x, int y) const { if (!m_subOpen) { return -1; } const float fx = static_cast(x); const float fy = static_cast(y); for (std::size_t index = 0; index < m_subItems.size(); ++index) { const MenuItem& item = m_subItems[index]; if (item.kind == MenuItemKind::SEPARATOR || item.kind == MenuItemKind::HEADER) { continue; } if (!item.enabled) { continue; } if (UiPointInRect(m_subItemRects[index], fx, fy)) { return static_cast(index); } } return -1; } float PopupMenu::EditSuffixWidth(const MenuItem& item) const { if (item.kind != MenuItemKind::EDIT || item.editSuffix.empty()) { return 0.0f; } return UiMeasureTextWidth(m_fonts->uiSmall, item.editSuffix) + 8.0f * m_scale; } gd::RectF PopupMenu::EditFieldRect(int index) const { if (index < 0 || index >= static_cast(m_items.size())) { return gd::RectF(); } const MenuItem& item = m_items[static_cast(index)]; if (item.kind != MenuItemKind::EDIT) { return gd::RectF(); } // 输入框靠右对齐到条目内容区右边界;宽度取自然宽度,但必须给说明文字留出自然宽度, // 避免菜单被最大宽度夹取后输入框把左侧标签挤成省略号。 // ⚠️ 说明文字为空(例如「系数调整」面板的整行输入框)时不设 3/4 上限, // 让输入框独占整行。 // ⚠️ 带说明文字的项统一使用排布阶段算好的 `m_editFieldWidth`: // 同一面板里所有这类输入框必须一样宽、右边界对齐。 const gd::RectF& rect = m_itemRects[static_cast(index)]; const float contentWidth = std::max(40.0f * m_scale, rect.Width - 2.0f * kItemPad * m_scale); const float labelWidth = UiMeasureTextWidth(m_fonts->uiSmall, item.label) + kEditFieldGap * m_scale; float width = NaturalEditFieldWidth(item); width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth)); if (item.label.empty()) { // 整行输入框(无说明文字):占满内容区,视觉上就是一个独立的一行输入框。 width = contentWidth; } else { if (m_editFieldWidth > 0.0f) { width = m_editFieldWidth; } width = std::min(width, std::max(40.0f * m_scale, contentWidth - labelWidth)); width = std::min(width, contentWidth * 0.75f); } const float height = rect.Height - kEditFieldPadV * 2.0f * m_scale; return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - width, rect.Y + (rect.Height - height) * 0.5f, width, height); } gd::RectF PopupMenu::HeaderIconRect(int index) const { if (index < 0 || index >= static_cast(m_items.size())) { return gd::RectF(); } const MenuItem& item = m_items[static_cast(index)]; if (item.kind != MenuItemKind::HEADER_ACTION) { return gd::RectF(); } const gd::RectF& rect = m_itemRects[static_cast(index)]; const float size = std::min(kHeaderIconSize * m_scale, std::max(6.0f, rect.Height - 4.0f * m_scale)); return gd::RectF(rect.X + rect.Width - kItemPad * m_scale - size, rect.Y + (rect.Height - size) * 0.5f, size, size); } bool PopupMenu::IsOnEditField(int x, int y) const { if (m_editIndex < 0) { return false; } const gd::RectF rect = EditFieldRect(m_editIndex); return UiPointInRect(rect, static_cast(x), static_cast(y)); } bool PopupMenu::BeginItemEdit(int index) { if (!m_open || index < 0 || index >= static_cast(m_items.size())) { return false; } const MenuItem& item = m_items[static_cast(index)]; if (item.kind != MenuItemKind::EDIT || !item.enabled) { return false; } // 输入框初值取「编辑初值」(未设置时用显示值),整段选中,便于直接键入新数值。 m_edit.Begin(item.editValue.empty() ? item.value : item.editValue, true, item.filter, 0u); m_editIndex = index; m_hoverIndex = index; return true; } bool PopupMenu::EndItemEdit(bool commit, MenuResult& result) { if (m_editIndex < 0) { return false; } const int index = m_editIndex; const std::wstring text = m_edit.Text(); m_editIndex = -1; m_edit.End(); if (!commit || index >= static_cast(m_items.size())) { return true; } MenuItem& item = m_items[static_cast(index)]; // 立即回写显示值,使菜单上的数值与数据层一致(宿主会在派发后再纠正为夹取后的真实值); // 键入文本同时记为**编辑初值**(`value` 与 `editValue` 不一致的行也能正确回显上次输入)。 item.value = text; item.editValue = text; result.valid = true; result.scope = m_scope; result.id = item.id; result.editCommit = true; result.editText = text; return true; } void PopupMenu::RebuildLayout() { if (!m_open) { return; } const int subParent = m_subParentIndex; BuildLayout(m_buildBounds, m_buildX, m_buildY, m_buildExpandUp); // 主菜单重新排布后子菜单位置会失效:若之前是展开的则按新几何重建。 if (subParent >= 0) { BuildSubLayout(subParent); } } bool PopupMenu::UpdateItemValue(int id, const std::wstring& value, const std::wstring& editValue) { for (MenuItem& item : m_items) { if (item.id == id && item.kind == MenuItemKind::EDIT) { const bool valueSame = (item.value == value); const bool editSame = editValue.empty() || (item.editValue == editValue); if (valueSame && editSame) { return true; } item.value = value; if (!editValue.empty()) { item.editValue = editValue; } RebuildLayout(); return true; } } return false; } bool PopupMenu::UpdateItemLabel(int id, const std::wstring& label, bool checked, bool enabled) { for (MenuItem& item : m_items) { if (item.id == id && item.kind == MenuItemKind::NORMAL) { if (item.label == label && item.checked == checked && item.enabled == enabled) { return true; } item.label = label; item.checked = checked; item.enabled = enabled; RebuildLayout(); return true; } } return false; } bool PopupMenu::UpdateSubmenuItemLabel(int id, const std::wstring& label, bool checked, bool enabled) { for (std::size_t parentIndex = 0; parentIndex < m_items.size(); ++parentIndex) { MenuItem& parent = m_items[parentIndex]; if (parent.kind != MenuItemKind::SUBMENU) { continue; } for (MenuItem& option : parent.submenu) { if (option.id != id || option.kind != MenuItemKind::NORMAL) { continue; } if (option.label == label && option.checked == checked && option.enabled == enabled) { return true; } option.label = label; option.checked = checked; option.enabled = enabled; // 子菜单是 `parent.submenu` 的**快照**(`m_subItems`),必须重建才生效。 if (m_subOpen && m_subParentIndex == static_cast(parentIndex)) { BuildSubLayout(static_cast(parentIndex)); } return true; } } return false; } } // namespace jdi