diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index e740f23..f19d573 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -389,7 +389,13 @@ namespace jdi m_cachedLaneCount = laneCount; // 标签页首次出现可见信号时,把时间轴对齐到可见数据的范围。 - if (anyVisible && !tab.ViewInitialized()) + // 没有任何可见信号时(未勾选 / 全部取消勾选 / 清空网格)主动复位自适应标记, + // 这样用户下一次勾选信号时会重新做一次自适应,而不是停留在上次的视图范围。 + if (!anyVisible) + { + tab.ViewInitialized() = false; + } + else if (!tab.ViewInitialized()) { m_plot.FitAll(); } @@ -804,20 +810,12 @@ namespace jdi SetStatusMessage(L"当前标签页已切换为「每信号一格」。", false); } break; - case ToolbarAction::SELECT_ALL: - if (!m_dataSet.Empty()) - { - ActiveTab().SetAllVisible(m_dataSet, true); - m_panel.InvalidateRows(); - SetStatusMessage(L"已勾选当前标签页全部信号。", false); - } - break; case ToolbarAction::CLEAR_ALL: if (!m_dataSet.Empty()) { ActiveTab().SetAllVisible(m_dataSet, false); m_panel.InvalidateRows(); - SetStatusMessage(L"已清空当前标签页的信号勾选。", false); + SetStatusMessage(L"已清空当前标签页的全部网格。", false); } break; case ToolbarAction::NONE: @@ -1604,6 +1602,11 @@ namespace jdi { SetFocus(hwnd); changed = m_tabBar.OnDoubleClick(x, y); + if (m_tabBar.IsEditing()) + { + // 进入重命名后该标签会加宽,需要重新排布标签栏。 + Relayout(); + } } else if (m_panel.HitTest(x, y)) { @@ -1624,6 +1627,21 @@ namespace jdi const int x = GET_X_LPARAM(lParam); const int y = GET_Y_LPARAM(lParam); + // 编辑态下右键 = 取消选区并把插入符移到文本末尾(常规输入框行为), + // 其它操作(弹菜单、拖拽)一并被吞掉。 + if (m_tabBar.IsEditing() || m_panel.IsEditing() || m_plot.IsEditing() || m_colorPicker.IsEditing()) + { + bool changed = m_tabBar.CollapseEditSelectionToEnd(); + changed = m_panel.CollapseEditSelectionToEnd() || changed; + changed = m_plot.CollapseEditSelectionToEnd() || changed; + changed = m_colorPicker.CollapseEditSelectionToEnd() || changed; + if (changed) + { + InvalidateRect(hwnd, nullptr, FALSE); + } + return 0; + } + POINT cursor = {x, y}; ClientToScreen(hwnd, &cursor); @@ -1727,6 +1745,11 @@ namespace jdi TabBarResult tabResult; m_tabBar.OnEditKey(wParam, tabResult); OnTabBarResult(tabResult); + if (!m_tabBar.IsEditing()) + { + // 退出重命名后标签宽度恢复,需要重新排布。 + Relayout(); + } InvalidateRect(hwnd, nullptr, FALSE); return 0; } @@ -1758,6 +1781,11 @@ namespace jdi { if (m_tabBar.OnEditChar(static_cast(wParam))) { + // 标签宽度随输入内容增长(重命名时上限更宽),需重新排布才能看到完整名称。 + if (m_tabBar.IsEditing()) + { + Relayout(); + } InvalidateRect(hwnd, nullptr, FALSE); } return 0; diff --git a/Components/PlotView/Inc/PlotView.hpp b/Components/PlotView/Inc/PlotView.hpp index 32bcfa2..682c5bf 100644 --- a/Components/PlotView/Inc/PlotView.hpp +++ b/Components/PlotView/Inc/PlotView.hpp @@ -318,6 +318,12 @@ namespace jdi */ bool OnEditKey(WPARAM key); + /** + * @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。 + * @return 发生改变返回 true;不在编辑态返回 false。 + */ + bool CollapseEditSelectionToEnd(); + /** * @brief 获取当前视图左边界时间。 * @return 视图左边界时间。 @@ -351,9 +357,10 @@ namespace jdi */ struct ResolvedSignal { - const SignalData* data = nullptr; /**< 信号数据。 */ - const SignalStyle* style = nullptr; /**< 显示配置。 */ + const SignalData* data = nullptr; /**< 信号数据(含全局共享的显示外观)。 */ + const SignalStyle* style = nullptr; /**< 显示外观。 */ const DataFile* file = nullptr; /**< 所属文件。 */ + bool visible = false; /**< 在当前标签页中是否可见。 */ }; /** diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 1f60399..2d76acc 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -12,8 +12,13 @@ namespace jdi namespace { const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */ - const int kYTickTarget = 6; /**< 幅值轴期望刻度数。 */ + const int kYTickTarget = 6; /**< 幅值轴期望刻度数(网格较高时)。 */ + const int kYTickTargetMin = 3; /**< 幅值轴最少刻度数(网格很矮时也要保证可读数)。 */ + const int kYTickTargetMax = 8; /**< 幅值轴最多刻度数。 */ + const float kYTickSpacing = 28.0f; /**< 幅值刻度之间的理想间距(逻辑像素)。 */ + const float kYLabelMinGap = 15.0f; /**< 幅值标签的最小行距(逻辑像素)。 */ const int kMinorDivisions = 5; /**< 相邻主刻度之间的等分数(用于估读)。 */ + const unsigned int kMinorLineAlpha = 0x9Au; /**< 辅助等分线的透明度(越高越清晰)。 */ const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */ const float kLanePadding = 8.0f; /**< 网格内边距。 */ const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */ @@ -162,10 +167,8 @@ namespace jdi } result.data = m_dataSet->Resolve(ref); result.file = m_dataSet->FileAt(ref.fileIndex); - if (m_tab != nullptr) - { - result.style = m_tab->Style(ref); - } + result.style = m_dataSet->Style(ref); + result.visible = (m_tab != nullptr) && m_tab->IsVisible(ref); return result; } @@ -229,8 +232,8 @@ namespace jdi int firstFile = -1; for (const SignalRef& ref : m_tab->Lanes()[static_cast(laneIndex)].signals) { - const SignalStyle* style = m_tab->Style(ref); - if (style == nullptr || !style->visible) + const SignalStyle* style = m_dataSet->Style(ref); + if (style == nullptr || !m_tab->IsVisible(ref)) { continue; } @@ -888,7 +891,7 @@ namespace jdi for (const SignalRef& ref : m_tab->Lanes()[static_cast(laneIndex)].signals) { const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) { continue; } @@ -1157,21 +1160,23 @@ namespace jdi { return EndEdit(true); } - if (character == L'\b') - { - return m_edit.Backspace(); - } - if (character == 0x7F) - { - return m_edit.DeleteForward(); - } - if (character < 0x20) + // 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理,此处忽略控制字符避免重复删除。 + if (character < 0x20 || character == 0x7F) { return false; } return m_edit.Insert(character); } + bool PlotView::CollapseEditSelectionToEnd() + { + if (m_editTarget == PlotEditTarget::NONE) + { + return false; + } + return m_edit.DeselectToEnd(); + } + bool PlotView::OnEditKey(WPARAM key) { if (m_editTarget == PlotEditTarget::NONE) @@ -1893,7 +1898,11 @@ namespace jdi } // 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。 - const double yStep = NiceStep(lane.yMax - lane.yMin, kYTickTarget); + // 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致, + // 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。 + int yTickTarget = static_cast(std::lround(lane.rect.Height / (kYTickSpacing * m_scale))); + yTickTarget = std::max(kYTickTargetMin, std::min(yTickTarget, kYTickTargetMax)); + const double yStep = NiceStep(lane.yMax - lane.yMin, yTickTarget); std::vector yTicks; BuildTicks(lane.yMin, lane.yMax, yStep, yTicks); @@ -1916,7 +1925,7 @@ namespace jdi continue; } UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y, - WithAlpha(m_theme->gridWeak, 0x70), 1.0f); + WithAlpha(m_theme->gridWeak, kMinorLineAlpha), 1.0f); } for (const double tick : yTicks) @@ -1952,7 +1961,7 @@ namespace jdi continue; } UiDrawLine(g, x, lane.rect.Y, x, lane.rect.Y + lane.rect.Height, - WithAlpha(m_theme->gridWeak, 0x70), 1.0f); + WithAlpha(m_theme->gridWeak, kMinorLineAlpha), 1.0f); } for (const double tick : timeTicks) @@ -1983,7 +1992,7 @@ namespace jdi for (const SignalRef& ref : laneSignals) { const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) { continue; } @@ -2074,6 +2083,11 @@ namespace jdi const bool showLabels = (lane.rect.Height >= 34.0f * m_scale); float lastLabelY = -1.0e9f; + // 网格较矮时同步收紧标签行距,让密度随可用高度提升而不是整片被过滤掉。 + const float labelMinGap = + std::min(kYLabelMinGap * m_scale, + lane.rect.Height / static_cast(yTickTarget + 1)); + for (const double tick : yTicks) { if (!showLabels) @@ -2086,7 +2100,7 @@ namespace jdi { continue; } - if (std::fabs(y - lastLabelY) < 15.0f * m_scale) + if (std::fabs(y - lastLabelY) < labelMinGap) { continue; } @@ -2150,7 +2164,7 @@ namespace jdi for (const SignalRef& ref : m_tab->Lanes()[static_cast(laneIndex)].signals) { const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) { continue; } @@ -2223,7 +2237,7 @@ namespace jdi for (const SignalRef& ref : laneSignals) { const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) { continue; } @@ -2266,7 +2280,7 @@ namespace jdi for (const SignalRef& ref : laneSignals) { const ResolvedSignal resolved = ResolveSignal(ref); - if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible) + if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible) { continue; } @@ -2370,7 +2384,7 @@ namespace jdi { const ResolvedSignal resolved = ResolveSignal(ref); if (resolved.data == nullptr || resolved.style == nullptr || resolved.file == nullptr || - !resolved.style->visible) + !resolved.visible) { continue; } @@ -2455,6 +2469,12 @@ namespace jdi if (file != nullptr && !file->timeColumnName.empty()) { axisName = file->timeColumnName; + // CSV 中该列通常直接叫 "time",这里补上时间单位秒便于阅读; + // 若列名自带单位描述(以右括号结尾)则不重复追加。 + if (axisName.back() != L')') + { + axisName += L" (s)"; + } } break; } @@ -2532,7 +2552,7 @@ namespace jdi { const ResolvedSignal resolved = ResolveSignal(ref); if (resolved.data == nullptr || resolved.style == nullptr || resolved.file == nullptr || - !resolved.style->visible) + !resolved.visible) { continue; } diff --git a/Components/SignalPanel/Inc/SignalPanel.hpp b/Components/SignalPanel/Inc/SignalPanel.hpp index 64f5bc1..834e0b0 100644 --- a/Components/SignalPanel/Inc/SignalPanel.hpp +++ b/Components/SignalPanel/Inc/SignalPanel.hpp @@ -182,6 +182,26 @@ namespace jdi */ bool OnEditKey(WPARAM key); + /** + * @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。 + * @return 发生改变返回 true;不在编辑态返回 false。 + */ + bool CollapseEditSelectionToEnd(); + + /** + * @brief 获取指定信号的显示外观(只读,全局共享)。 + * @param [IN] ref 信号引用。 + * @return 外观指针,引用无效时返回 nullptr。 + */ + const SignalStyle* StyleOf(const SignalRef& ref) const; + + /** + * @brief 获取指定信号的显示外观(可写,全局共享)。 + * @param [IN] ref 信号引用。 + * @return 外观指针,引用无效时返回 nullptr。 + */ + SignalStyle* StyleOf(const SignalRef& ref); + /** * @brief 将浮动拾色器的取色结果应用到当前选中信号。 * @param [IN] color 0xAARRGGBB 颜色值。 diff --git a/Components/SignalPanel/Src/SignalPanel.cpp b/Components/SignalPanel/Src/SignalPanel.cpp index 4616075..66c2cc5 100644 --- a/Components/SignalPanel/Src/SignalPanel.cpp +++ b/Components/SignalPanel/Src/SignalPanel.cpp @@ -708,7 +708,7 @@ namespace jdi const ListRow& item = m_rows[static_cast(row)]; const SignalRef ref{item.fileIndex, item.signalIndex}; const SignalData* signal = m_dataSet->Resolve(ref); - const SignalStyle* style = m_tab->Style(ref); + const SignalStyle* style = StyleOf(ref); if (signal == nullptr || style == nullptr) { return; @@ -734,18 +734,20 @@ namespace jdi // 复选框。 const gd::RectF checkRect(baseX + kDisclosureWidth * m_scale, centerY - kCheckSize * m_scale * 0.5f, kCheckSize * m_scale, kCheckSize * m_scale); - UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.0f * m_scale); + // 可见性属于当前标签页,与全局共享的显示外观分开读取。 + const bool visible = m_tab->IsVisible(ref); + UiDrawCheckBox(g, checkRect, visible ? 1 : 0, 3.0f * m_scale); // 颜色块:叠加不透明度,透明度过低时自动补描边(与样式区色块规则一致)。 const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale, centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale, kSwatchSize * m_scale); - UiDrawSwatch(g, swatchRect, style->color, style->visible ? style->opacity : 0.3f, 2.5f * m_scale, + UiDrawSwatch(g, swatchRect, style->color, visible ? style->opacity : 0.3f, 2.5f * m_scale, m_scale); // 网格徽标(可点击,用于调整所属网格)。 const int laneIndex = m_tab->LaneOfSignal(ref); - const bool badgeVisible = (laneIndex >= 0) && style->visible; + const bool badgeVisible = (laneIndex >= 0) && visible; const gd::RectF badgeRect = SignalBadgeRect(row); float nameRightLimit = rect.X + rect.Width - kPadding * m_scale; @@ -763,7 +765,7 @@ namespace jdi else { const float nameWidth = std::max(16.0f, nameRightLimit - nameLeft); - const std::uint32_t nameColor = style->visible ? m_theme->text : m_theme->textMuted; + const std::uint32_t nameColor = visible ? m_theme->text : m_theme->textMuted; UiText(g, UiEllipsize(g, signal->Title(), m_fonts->ui, nameWidth), m_fonts->ui, gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), nameColor, TextAlign::LEFT); } @@ -816,7 +818,7 @@ namespace jdi UiFillRect(g, m_styleRect, m_theme->panelBg); - const SignalStyle* style = (m_tab == nullptr) ? nullptr : m_tab->Style(m_tab->Selected()); + const SignalStyle* style = (m_tab == nullptr) ? nullptr : StyleOf(m_tab->Selected()); if (style == nullptr || m_tab->Selected().IsValid() == false) { UiText(g, L"请选择信号以编辑样式", m_fonts->uiSmall, m_styleRect, m_theme->textMuted, @@ -962,7 +964,7 @@ namespace jdi { return false; } - SignalStyle* style = m_tab->Style(m_tab->Selected()); + SignalStyle* style = StyleOf(m_tab->Selected()); if (style == nullptr) { return false; @@ -1041,7 +1043,7 @@ namespace jdi { case StyleEditField::WIDTH: { - const SignalStyle* style = m_tab->Style(m_tab->Selected()); + const SignalStyle* style = StyleOf(m_tab->Selected()); if (style == nullptr) { return false; @@ -1051,7 +1053,7 @@ namespace jdi } case StyleEditField::OPACITY: { - const SignalStyle* style = m_tab->Style(m_tab->Selected()); + const SignalStyle* style = StyleOf(m_tab->Selected()); if (style == nullptr) { return false; @@ -1138,7 +1140,7 @@ namespace jdi { break; } - if (SignalStyle* style = m_tab->Style(m_tab->Selected())) + if (SignalStyle* style = StyleOf(m_tab->Selected())) { const float value = ClampFloat(static_cast(parsed), kWidthMin, kWidthMax); style->lineWidth = std::round(value * 10.0f) / 10.0f; @@ -1157,7 +1159,7 @@ namespace jdi { break; } - if (SignalStyle* style = m_tab->Style(m_tab->Selected())) + if (SignalStyle* style = StyleOf(m_tab->Selected())) { style->opacity = ClampFloat(static_cast(parsed) / 100.0f, 0.0f, 1.0f); } @@ -1233,20 +1235,14 @@ namespace jdi { return EndEdit(true); } - if (character == L'\b') - { - return m_edit.Backspace(); - } - if (character == 0x7F) - { - return m_edit.DeleteForward(); - } - // 百分比字段允许直接输入 % 但会被忽略,其余控制字符一律丢弃。 - if (m_editField == StyleEditField::OPACITY && character == L'%') + // 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理:这里必须忽略控制字符, + // 否则同一次按键会被处理两次(一次删除两个字符)。 + if (character < 0x20 || character == 0x7F) { return false; } - if (character < 0x20) + // 百分比字段允许直接输入 % 但会被忽略,其余控制字符一律丢弃。 + if (m_editField == StyleEditField::OPACITY && character == L'%') { return false; } @@ -1294,13 +1290,32 @@ namespace jdi return true; } + bool SignalPanel::CollapseEditSelectionToEnd() + { + if (m_editField == StyleEditField::NONE) + { + return false; + } + return m_edit.DeselectToEnd(); + } + + const SignalStyle* SignalPanel::StyleOf(const SignalRef& ref) const + { + return (m_dataSet != nullptr) ? m_dataSet->Style(ref) : nullptr; + } + + SignalStyle* SignalPanel::StyleOf(const SignalRef& ref) + { + return (m_dataSetWritable != nullptr) ? m_dataSetWritable->Style(ref) : nullptr; + } + bool SignalPanel::ApplyColor(std::uint32_t color) { if (m_tab == nullptr) { return false; } - SignalStyle* style = m_tab->Style(m_tab->Selected()); + SignalStyle* style = StyleOf(m_tab->Selected()); if (style == nullptr) { return false; @@ -1319,7 +1334,7 @@ namespace jdi { return false; } - SignalStyle* style = m_tab->Style(m_tab->Selected()); + SignalStyle* style = StyleOf(m_tab->Selected()); if (style == nullptr) { return false; @@ -1400,7 +1415,7 @@ namespace jdi { hoverStyle = StyleHit::TOGGLE_ALL; } - else if (m_tab != nullptr && m_tab->Style(m_tab->Selected()) != nullptr) + else if (m_tab != nullptr && StyleOf(m_tab->Selected()) != nullptr) { for (int index = 0; index < m_colorCount; ++index) { @@ -1513,7 +1528,7 @@ namespace jdi } // 样式编辑区。 - if (SignalStyle* style = m_tab->Style(m_tab->Selected())) + if (SignalStyle* style = StyleOf(m_tab->Selected())) { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); @@ -1641,7 +1656,7 @@ namespace jdi const float fy = static_cast(y); if (UiPointInRect(m_customChipRect, fx, fy) && m_tab != nullptr) { - if (const SignalStyle* style = m_tab->Style(m_tab->Selected())) + if (const SignalStyle* style = StyleOf(m_tab->Selected())) { changed = OpenColorPickerPopup(*style) || changed; } @@ -1789,7 +1804,7 @@ namespace jdi return; } - const SignalStyle* style = m_tab->Style(ref); + const SignalStyle* style = StyleOf(ref); const SignalData* signal = m_dataSet->Resolve(ref); if (style == nullptr || signal == nullptr) { @@ -1807,7 +1822,7 @@ namespace jdi items.push_back(header); MenuItem toggle; - toggle.label = style->visible ? L"隐藏该信号" : L"显示该信号"; + toggle.label = m_tab->IsVisible(ref) ? L"隐藏该信号" : L"显示该信号"; toggle.id = CMD_SIGNAL_TOGGLE; items.push_back(toggle); @@ -2038,7 +2053,7 @@ namespace jdi } else if (result.id == CMD_COLOR_BASE && result.swatchIndex >= 0) { - if (SignalStyle* style = m_tab->Style(m_menuTargetSignal)) + if (SignalStyle* style = StyleOf(m_menuTargetSignal)) { int paletteCount = 0; const std::uint32_t* palette = SignalPalette(paletteCount); @@ -2047,7 +2062,7 @@ namespace jdi } else if (result.id >= CMD_MODE_BASE && result.id < CMD_MODE_BASE + kModeOptionCount) { - if (SignalStyle* style = m_tab->Style(m_menuTargetSignal)) + if (SignalStyle* style = StyleOf(m_menuTargetSignal)) { style->mode = kModeOptions[result.id - CMD_MODE_BASE]; } diff --git a/Components/TabBar/Inc/TabBar.hpp b/Components/TabBar/Inc/TabBar.hpp index cf9be66..be63f6b 100644 --- a/Components/TabBar/Inc/TabBar.hpp +++ b/Components/TabBar/Inc/TabBar.hpp @@ -157,6 +157,12 @@ namespace jdi */ bool OnEditKey(WPARAM key, TabBarResult& result); + /** + * @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。 + * @return 发生改变返回 true;不在编辑态返回 false。 + */ + bool CollapseEditSelectionToEnd(); + /** * @brief 查询拖拽排序时应当插入的位置。 * @param [IN] x 客户区横坐标。 diff --git a/Components/TabBar/Src/TabBar.cpp b/Components/TabBar/Src/TabBar.cpp index 1452044..559ee7d 100644 --- a/Components/TabBar/Src/TabBar.cpp +++ b/Components/TabBar/Src/TabBar.cpp @@ -20,6 +20,8 @@ namespace jdi const float kAddGap = 6.0f; /**< 新建按钮与末个标签的间距。 */ const float kCorner = 5.0f; /**< 标签圆角半径。 */ const float kMaxTabWidth = 200.0f; /**< 单个标签最大宽度(逻辑像素)。 */ + const float kEditingTabWidth = 320.0f; /**< 重命名输入时标签的最大宽度(便于看清长名称)。 */ + const float kMinTabWidth = 70.0f; /**< 单个标签最小宽度(逻辑像素)。 */ } TabBar::TabBar() = default; @@ -72,16 +74,50 @@ namespace jdi const float tabHeight = kTabHeight * m_scale; const float top = area.Y + kTopMargin * m_scale; const float closeSize = kCloseSize * m_scale; - const float maxWidth = kMaxTabWidth * m_scale; - const float minWidth = 70.0f * m_scale; + const float minWidth = kMinTabWidth * m_scale; + const float addSize = kAddSize * m_scale; - float cursorX = area.X + kLeftPadding * m_scale; - - for (TabItem& tab : m_tabs) + // 标签宽度按「加粗字体」测量:激活页用 uiBold 绘制, + // 若按常规字体测量,加粗后的长名称会在激活时被省略号截断(预览过短)。 + std::vector desired; + desired.reserve(m_tabs.size()); + float total = kLeftPadding * m_scale; + for (std::size_t index = 0; index < m_tabs.size(); ++index) { - const float textWidth = UiMeasureTextWidth(m_fonts->ui, tab.name); + const bool editing = (static_cast(index) == m_editIndex); + + // 重命名过程中按「正在输入的文本」测量,标签才能随输入逐步变宽; + // 否则始终按已提交的旧名称算宽度,长名称会被挤成很短的一截(预览过短)。 + const std::wstring& measureText = editing ? m_edit.Text() : m_tabs[index].name; + const float boldWidth = UiMeasureTextWidth(m_fonts->uiBold, measureText); + const float regularWidth = UiMeasureTextWidth(m_fonts->ui, measureText); + const float textWidth = std::max(boldWidth, regularWidth); + + const float maxWidth = (editing ? kEditingTabWidth : kMaxTabWidth) * m_scale; + float width = kTabPadding * 2.0f * m_scale + textWidth + kCloseGap * m_scale + closeSize; width = std::max(minWidth, std::min(width, maxWidth)); + desired.push_back(width); + total += width + kAddGap * m_scale; + } + total += addSize; + + // 标签总宽超出可用宽度时整体等比收缩,保证「+」按钮始终可见。 + const float available = area.Width - kLeftPadding * m_scale; + float shrink = 1.0f; + if (total > available && total > 0.0f) + { + shrink = available / total; + } + + float cursorX = area.X + kLeftPadding * m_scale; + for (std::size_t index = 0; index < m_tabs.size(); ++index) + { + TabItem& tab = m_tabs[index]; + float width = std::max(minWidth * shrink, desired[index] * shrink); + + // 收缩后仍要保证「文本 + 关闭按钮」有最小可用空间。 + width = std::max(width, closeSize + kTabPadding * m_scale + 8.0f * m_scale); tab.rect = gd::RectF(cursorX, top, width, tabHeight); tab.closeRect = gd::RectF(cursorX + width - kTabPadding * m_scale - closeSize, @@ -90,8 +126,8 @@ namespace jdi cursorX += width + kAddGap * m_scale; } - const float addSize = kAddSize * m_scale; - m_addRect = gd::RectF(cursorX, top + (tabHeight - addSize) * 0.5f, addSize, addSize); + m_addRect = gd::RectF(std::min(cursorX, area.X + area.Width - addSize - 2.0f * m_scale), + top + (tabHeight - addSize) * 0.5f, addSize, addSize); } void TabBar::Paint(gd::Graphics& g) @@ -421,15 +457,8 @@ namespace jdi OnEditKey(VK_RETURN, result); return true; } - if (character == L'\b') - { - return m_edit.Backspace(); - } - if (character == 0x7F) - { - return m_edit.DeleteForward(); - } - if (character < L' ') + // 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理,此处忽略全部控制字符。 + if (character < L' ' || character == 0x7F) { return false; } @@ -491,6 +520,15 @@ namespace jdi return true; } + bool TabBar::CollapseEditSelectionToEnd() + { + if (m_editIndex < 0) + { + return false; + } + return m_edit.DeselectToEnd(); + } + bool TabBar::OnRButtonDown(int x, int y, int screenX, int screenY, TabBarResult& result) { result = TabBarResult(); diff --git a/Components/Toolbar/Inc/Toolbar.hpp b/Components/Toolbar/Inc/Toolbar.hpp index 763c1fb..7103bd3 100644 --- a/Components/Toolbar/Inc/Toolbar.hpp +++ b/Components/Toolbar/Inc/Toolbar.hpp @@ -27,8 +27,7 @@ namespace jdi CURSOR, /**< 切换时间光标。 */ MERGE_LANES, /**< 合并为单一网格。 */ SPLIT_LANES, /**< 按信号拆分为多网格。 */ - SELECT_ALL, /**< 勾选当前标签页全部信号。 */ - CLEAR_ALL /**< 取消当前标签页全部信号勾选。 */ + CLEAR_ALL /**< 清空当前标签页的全部网格与信号勾选。 */ }; /** diff --git a/Components/Toolbar/Src/Toolbar.cpp b/Components/Toolbar/Src/Toolbar.cpp index eee1dfb..1cf0189 100644 --- a/Components/Toolbar/Src/Toolbar.cpp +++ b/Components/Toolbar/Src/Toolbar.cpp @@ -72,9 +72,8 @@ namespace jdi {ToolbarAction::ZOOM_VALUE, L"Y 轴缩放", false, true, true}, {ToolbarAction::CURSOR, L"光标", false, true, true}, {ToolbarAction::MERGE_LANES, L"合并网格", false, true, false}, - {ToolbarAction::SPLIT_LANES, L"拆分信号网格", false, true, false}, - {ToolbarAction::SELECT_ALL, L"全选信号", false, true, false}, - {ToolbarAction::CLEAR_ALL, L"清空勾选", false, true, false}, + {ToolbarAction::SPLIT_LANES, L"拆分网格", false, true, false}, + {ToolbarAction::CLEAR_ALL, L"清空网格", false, true, false}, }; const std::size_t specCount = sizeof(specs) / sizeof(specs[0]); diff --git a/Utilities/Inc/ColorPickerPopup.hpp b/Utilities/Inc/ColorPickerPopup.hpp index 8dd7be7..91764e3 100644 --- a/Utilities/Inc/ColorPickerPopup.hpp +++ b/Utilities/Inc/ColorPickerPopup.hpp @@ -118,6 +118,12 @@ namespace jdi */ bool IsEditing() const { return m_editField != EditField::NONE; } + /** + * @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。 + * @return 发生改变返回 true;不在编辑态返回 false。 + */ + bool CollapseEditSelectionToEnd(); + /** * @brief 设置剪贴板所有者窗口句柄(用于输入框的复制/粘贴)。 * @param [IN] owner 宿主窗口句柄,允许为空。 diff --git a/Utilities/Inc/DataModel.hpp b/Utilities/Inc/DataModel.hpp index 5522825..11affdb 100644 --- a/Utilities/Inc/DataModel.hpp +++ b/Utilities/Inc/DataModel.hpp @@ -81,7 +81,23 @@ namespace jdi }; /** - * @brief 单条信号的元数据与采样值(纯数据,不含显示配置)。 + * @brief 单个信号的显示外观(颜色/透明度/线宽/绘制方式)。 + * + * 外观属于信号自身而非某个标签页——同一信号在所有标签页中必须显示效果一致, + * 因此在任一视图里修改颜色/线宽/透明度/样式,其它视图会自动同步。 + * 信号是否可见则属于各标签页自己的配置(见 `TabConfig`)。 + */ + struct SignalStyle + { + std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */ + std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */ + float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */ + float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */ + DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */ + }; + + /** + * @brief 单条信号的元数据与采样值(纯数据,不含可见性配置)。 */ struct SignalData { @@ -93,6 +109,7 @@ namespace jdi double minValue = 0.0; /**< 有限采样值最小值。 */ double maxValue = 0.0; /**< 有限采样值最大值。 */ std::size_t finiteCount = 0; /**< 有限采样点数量。 */ + SignalStyle style; /**< 显示外观(全局共享,跨标签页同步)。 */ /** * @brief 获取用于界面展示的名称。 @@ -206,6 +223,20 @@ namespace jdi */ const SignalData* Resolve(const SignalRef& ref) const; + /** + * @brief 获取指定信号的显示外观(只读)。 + * @param [IN] ref 信号引用。 + * @return 外观指针,引用无效时返回 nullptr。 + */ + const SignalStyle* Style(const SignalRef& ref) const; + + /** + * @brief 获取指定信号的显示外观(可写)。 + * @param [IN] ref 信号引用。 + * @return 外观指针,引用无效时返回 nullptr。 + */ + SignalStyle* Style(const SignalRef& ref); + /** * @brief 计算全部文件时间范围的并集下限。 * @return 时间下限。 @@ -222,19 +253,6 @@ namespace jdi std::vector m_files; /**< 已加载文件集合。 */ }; - /** - * @brief 单个信号的显示配置(属于某个标签页)。 - */ - struct SignalStyle - { - bool visible = false; /**< 是否参与绘制。默认关闭,由用户自行勾选。 */ - std::uint32_t color = 0xFF268BD2; /**< 波形颜色,格式 0xAARRGGBB。 */ - std::uint32_t customColor = 0xFF6C71C4; /**< 自定义颜色记忆值(切换预设色后仍保留)。 */ - float opacity = 1.0f; /**< 不透明度,取值 0.0-1.0,默认 1.0。 */ - float lineWidth = 1.6f; /**< 线宽(逻辑像素,未乘 DPI 缩放)。 */ - DrawMode mode = DrawMode::LINE; /**< 绘制方式。 */ - }; - /** * @brief 一个波形网格(纵向泳道),可容纳多条信号叠加显示。 */ @@ -267,7 +285,10 @@ namespace jdi int FindNearestSample(const DataFile& file, double time); /** - * @brief 单个标签页的独立视图配置(信号可见性、样式、网格划分与视图范围)。 + * @brief 单个标签页的独立视图配置(信号可见性、网格划分与视图范围)。 + * + * 仅保存「本页看到哪些信号」与视图状态;信号的显示外观(颜色/线宽/透明度/样式) + * 保存在 `DataSet` 中并被所有标签页共享,因此同一个信号在各视图中的显示效果一致。 */ class TabConfig { @@ -299,20 +320,6 @@ namespace jdi */ void RemapAfterFileRemoval(int fileIndex); - /** - * @brief 获取指定信号的显示配置(只读)。 - * @param [IN] ref 信号引用。 - * @return 配置指针,引用无效时返回 nullptr。 - */ - const SignalStyle* Style(const SignalRef& ref) const; - - /** - * @brief 获取指定信号的显示配置(可写)。 - * @param [IN] ref 信号引用。 - * @return 配置指针,引用无效时返回 nullptr。 - */ - SignalStyle* Style(const SignalRef& ref); - /** * @brief 判断指定信号是否可见。 * @param [IN] ref 信号引用。 @@ -501,16 +508,8 @@ namespace jdi PlotZoomMode ZoomMode() const { return m_zoomMode; } private: - /** - * @brief 为新增信号补默认样式(含调色板取色)。 - * @param [IN] dataSet 数据集合。 - * @param [IN] fileIndex 文件下标。 - * @param [IN] signalIndex 信号下标。 - */ - void EnsureStyle(const DataSet& dataSet, int fileIndex, int signalIndex); - std::wstring m_name; /**< 标签页名称。 */ - std::vector> m_styles; /**< 显示配置,按 [文件][信号] 索引。 */ + std::vector> m_visible; /**< 信号可见性,按 [文件][信号] 索引(本页独立)。 */ std::vector m_collapsed; /**< 文件分组折叠状态,按文件索引。 */ std::vector m_lanes; /**< 网格划分。 */ LaneMode m_laneMode = LaneMode::PER_SIGNAL; /**< 网格划分策略。 */ diff --git a/Utilities/Inc/TextEdit.hpp b/Utilities/Inc/TextEdit.hpp index 07027ef..034b813 100644 --- a/Utilities/Inc/TextEdit.hpp +++ b/Utilities/Inc/TextEdit.hpp @@ -138,6 +138,12 @@ namespace jdi */ void SelectAll(); + /** + * @brief 取消选区并把插入符移动到文本末尾(模拟在输入框右侧按下鼠标)。 + * @return 光标或选区发生变化返回 true。 + */ + bool DeselectToEnd(); + /** * @brief 复制当前内容到剪贴板(存在选区时只复制选区)。 * @param [IN] owner 剪贴板所有者窗口句柄,允许为空。 diff --git a/Utilities/Src/ColorPickerPopup.cpp b/Utilities/Src/ColorPickerPopup.cpp index 88e3fef..0f4f75e 100644 --- a/Utilities/Src/ColorPickerPopup.cpp +++ b/Utilities/Src/ColorPickerPopup.cpp @@ -356,21 +356,23 @@ namespace jdi { return EndEdit(true); } - if (character == L'\b') - { - return m_edit.Backspace(); - } - if (character == 0x7F) - { - return m_edit.DeleteForward(); - } - if (character < 0x20) + // 退格/删除已由 WM_KEYDOWN 的 OnKeyDown 处理,此处忽略控制字符避免重复删除。 + if (character < 0x20 || character == 0x7F) { return false; } return m_edit.Insert(character); } + bool ColorPickerPopup::CollapseEditSelectionToEnd() + { + if (m_editField == EditField::NONE) + { + return false; + } + return m_edit.DeselectToEnd(); + } + bool ColorPickerPopup::ContainsPoint(int x, int y) const { if (!m_open) diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index 89ca2b7..4c9879c 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -74,22 +74,6 @@ namespace jdi return inner; } - /** - * @brief 依据全局信号序号从调色板取色,保证跨标签页配色一致。 - * @param [IN] globalIndex 全局信号序号。 - * @return 0xAARRGGBB 颜色值。 - */ - std::uint32_t PaletteColor(std::size_t globalIndex) - { - int paletteCount = 0; - const std::uint32_t* palette = SignalPalette(paletteCount); - if (palette == nullptr || paletteCount <= 0) - { - return 0xFF268BD2; - } - return palette[globalIndex % static_cast(paletteCount)]; - } - /** * @brief 为网格范围补充留白,避免波形紧贴边界。 * @param [IN, OUT] yMin 范围下限。 @@ -120,35 +104,50 @@ namespace jdi /** * @brief 依据全部可见信号重新生成网格划分。 - * @param [IN] styles 显示配置。 + * @param [IN] visible 各信号的可见性(按 [文件][信号] 索引)。 * @param [IN] lanes 网格集合(原地更新)。 * @param [IN] mode 网格划分策略。 */ - void RebuildLanes(const std::vector>& styles, std::vector& lanes, + void RebuildLanes(const std::vector>& visible, std::vector& lanes, LaneMode mode) { // 收集全部可见信号引用。 - std::vector visible; - for (std::size_t fileIndex = 0; fileIndex < styles.size(); ++fileIndex) + std::vector visibleRefs; + for (std::size_t fileIndex = 0; fileIndex < visible.size(); ++fileIndex) { - for (std::size_t signalIndex = 0; signalIndex < styles[fileIndex].size(); ++signalIndex) + for (std::size_t signalIndex = 0; signalIndex < visible[fileIndex].size(); ++signalIndex) { - if (styles[fileIndex][signalIndex].visible) + if (visible[fileIndex][signalIndex]) { SignalRef ref; ref.fileIndex = static_cast(fileIndex); ref.signalIndex = static_cast(signalIndex); - visible.push_back(ref); + visibleRefs.push_back(ref); } } } + // 判断某引用对应的信号是否可见(越界视为不可见)。 + const auto isVisibleRef = [&visible](const SignalRef& ref) + { + if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= visible.size()) + { + return false; + } + const std::vector& fileVisible = visible[static_cast(ref.fileIndex)]; + if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) + { + return false; + } + return fileVisible[static_cast(ref.signalIndex)]; + }; + if (mode == LaneMode::PER_SIGNAL) { // 每条可见信号独占一个网格:网格数量严格跟随可见信号数量。 lanes.clear(); - lanes.reserve(visible.size()); - for (const SignalRef& ref : visible) + lanes.reserve(visibleRefs.size()); + for (const SignalRef& ref : visibleRefs) { Lane lane; lane.autoRange = true; @@ -170,13 +169,13 @@ namespace jdi } lanes.clear(); - if (!visible.empty()) + if (!visibleRefs.empty()) { Lane lane; lane.autoRange = previousAutoRange; lane.yMin = previousYMin; lane.yMax = previousYMax; - lane.signals = visible; + lane.signals = visibleRefs; lanes.push_back(std::move(lane)); } } @@ -187,22 +186,8 @@ namespace jdi { std::vector& refs = lane.signals; refs.erase(std::remove_if(refs.begin(), refs.end(), - [&styles](const SignalRef& ref) - { - if (ref.fileIndex < 0 || - static_cast(ref.fileIndex) >= styles.size()) - { - return true; - } - const std::vector& fileStyles = - styles[static_cast(ref.fileIndex)]; - if (ref.signalIndex < 0 || - static_cast(ref.signalIndex) >= fileStyles.size()) - { - return true; - } - return !fileStyles[static_cast(ref.signalIndex)].visible; - }), + [&isVisibleRef](const SignalRef& ref) + { return !isVisibleRef(ref); }), refs.end()); } @@ -213,7 +198,7 @@ namespace jdi // 未分配网格的可见信号各自独占一个新网格。 // 注意:不能把它们塞进已有的第一个网格——那会导致“拖拽合并后新勾选的信号 // 被悄悄并入合并网格”,用户看不到新增的独立泳道。 - for (const SignalRef& ref : visible) + for (const SignalRef& ref : visibleRefs) { bool placed = false; for (const Lane& lane : lanes) @@ -379,6 +364,22 @@ namespace jdi } signal.finiteCount = finiteCount; + // 显示外观随信号一起初始化:调色板按「全局信号序号」取色, + // 使同一信号在任意标签页中都保持同一默认颜色。 + { + int paletteCount = 0; + const std::uint32_t* palette = SignalPalette(paletteCount); + const std::size_t globalIndex = SignalCount() + file.signals.size(); + signal.style.color = + (palette != nullptr && paletteCount > 0) + ? palette[globalIndex % static_cast(paletteCount)] + : 0xFF268BD2u; + signal.style.customColor = signal.style.color; + signal.style.opacity = 1.0f; + signal.style.lineWidth = 1.6f; + signal.style.mode = DrawMode::LINE; + } + file.signals.push_back(std::move(signal)); } @@ -500,6 +501,29 @@ namespace jdi return &file->signals[static_cast(ref.signalIndex)]; } + const SignalStyle* DataSet::Style(const SignalRef& ref) const + { + const SignalData* signal = Resolve(ref); + return (signal != nullptr) ? &signal->style : nullptr; + } + + SignalStyle* DataSet::Style(const SignalRef& ref) + { + const DataFile* file = FileAt(ref.fileIndex); + if (file == nullptr) + { + return nullptr; + } + if (ref.signalIndex < 0 || ref.signalIndex >= static_cast(file->signals.size())) + { + return nullptr; + } + + // 这里需要写入,因此借助非 const 路径重新定位信号。 + DataFile& mutableFile = m_files[static_cast(ref.fileIndex)]; + return &mutableFile.signals[static_cast(ref.signalIndex)].style; + } + double DataSet::MinTime() const { double value = std::numeric_limits::infinity(); @@ -643,46 +667,10 @@ namespace jdi { } - void TabConfig::EnsureStyle(const DataSet& dataSet, int fileIndex, int signalIndex) - { - (void)dataSet; - if (fileIndex < 0 || signalIndex < 0) - { - return; - } - - const std::size_t fileSlot = static_cast(fileIndex); - if (fileSlot >= m_styles.size()) - { - return; - } - std::vector& fileStyles = m_styles[fileSlot]; - if (static_cast(signalIndex) >= fileStyles.size()) - { - return; - } - - // 若样式仍为默认状态且颜色为默认色,则按全局序号重新取色。 - SignalStyle& style = fileStyles[static_cast(signalIndex)]; - - std::size_t globalIndex = 0; - for (std::size_t index = 0; index < fileSlot; ++index) - { - if (index < m_styles.size()) - { - globalIndex += m_styles[index].size(); - } - } - globalIndex += static_cast(signalIndex); - - style.color = PaletteColor(globalIndex); - style.lineWidth = static_cast(kDefaultLineWidth); - } - void TabConfig::SyncToDataSet(const DataSet& dataSet) { const std::size_t fileCount = dataSet.FileCount(); - m_styles.resize(fileCount); + m_visible.resize(fileCount); m_collapsed.resize(fileCount, false); for (std::size_t fileIndex = 0; fileIndex < fileCount; ++fileIndex) @@ -693,15 +681,9 @@ namespace jdi continue; } - std::vector& fileStyles = m_styles[fileIndex]; - const std::size_t previousSize = fileStyles.size(); - fileStyles.resize(file->signals.size()); - - // 新增信号补默认配置(默认不可见,颜色取自调色板)。 - for (std::size_t signalIndex = previousSize; signalIndex < file->signals.size(); ++signalIndex) - { - EnsureStyle(dataSet, static_cast(fileIndex), static_cast(signalIndex)); - } + // 新增信号默认为未勾选(由用户自行决定查看哪些信号), + // 其显示外观已在 DataSet 中初始化完毕。 + m_visible[fileIndex].resize(file->signals.size(), false); } // 清理已失效的选中状态与网格引用。 @@ -732,9 +714,9 @@ namespace jdi } const std::size_t slot = static_cast(fileIndex); - if (slot < m_styles.size()) + if (slot < m_visible.size()) { - m_styles.erase(m_styles.begin() + fileIndex); + m_visible.erase(m_visible.begin() + fileIndex); } if (slot < m_collapsed.size()) { @@ -770,43 +752,36 @@ namespace jdi } } - const SignalStyle* TabConfig::Style(const SignalRef& ref) const - { - if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= m_styles.size()) - { - return nullptr; - } - const std::vector& fileStyles = m_styles[static_cast(ref.fileIndex)]; - if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileStyles.size()) - { - return nullptr; - } - return &fileStyles[static_cast(ref.signalIndex)]; - } - - SignalStyle* TabConfig::Style(const SignalRef& ref) - { - return const_cast(static_cast(this)->Style(ref)); - } - bool TabConfig::IsVisible(const SignalRef& ref) const { - const SignalStyle* style = Style(ref); - return (style != nullptr) && style->visible; + if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= m_visible.size()) + { + return false; + } + const std::vector& fileVisible = m_visible[static_cast(ref.fileIndex)]; + if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) + { + return false; + } + return fileVisible[static_cast(ref.signalIndex)]; } void TabConfig::SetVisible(const DataSet& dataSet, const SignalRef& ref, bool visible) { - SignalStyle* style = Style(ref); - if (style == nullptr) + if (ref.fileIndex < 0 || static_cast(ref.fileIndex) >= m_visible.size()) { return; } - if (style->visible == visible) + std::vector& fileVisible = m_visible[static_cast(ref.fileIndex)]; + if (ref.signalIndex < 0 || static_cast(ref.signalIndex) >= fileVisible.size()) { return; } - style->visible = visible; + if (fileVisible[static_cast(ref.signalIndex)] == visible) + { + return; + } + fileVisible[static_cast(ref.signalIndex)] = visible; // 合并模式下新勾选的信号独占一个新网格:切到自定义划分后, // RebuildLanes 会把尚未分配网格的可见信号各自放入新网格。 @@ -820,104 +795,71 @@ namespace jdi void TabConfig::ToggleVisible(const DataSet& dataSet, const SignalRef& ref) { - const SignalStyle* style = Style(ref); - if (style == nullptr) - { - return; - } - SetVisible(dataSet, ref, !style->visible); + SetVisible(dataSet, ref, !IsVisible(ref)); } void TabConfig::SetFileVisible(const DataSet& dataSet, int fileIndex, bool visible) { - if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) + if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return; } - for (SignalStyle& style : m_styles[static_cast(fileIndex)]) - { - style.visible = visible; - } + std::vector& fileVisible = m_visible[static_cast(fileIndex)]; + std::fill(fileVisible.begin(), fileVisible.end(), visible); SyncLanes(dataSet); } void TabConfig::SetAllVisible(const DataSet& dataSet, bool visible) { - for (std::vector& fileStyles : m_styles) + for (std::vector& fileVisible : m_visible) { - for (SignalStyle& style : fileStyles) - { - style.visible = visible; - } + std::fill(fileVisible.begin(), fileVisible.end(), visible); } SyncLanes(dataSet); } void TabConfig::SoloSignal(const DataSet& dataSet, const SignalRef& ref) { - for (std::vector& fileStyles : m_styles) + for (std::vector& fileVisible : m_visible) { - for (SignalStyle& style : fileStyles) - { - style.visible = false; - } + std::fill(fileVisible.begin(), fileVisible.end(), false); } - if (SignalStyle* style = Style(ref); style != nullptr) - { - style->visible = true; - } - SyncLanes(dataSet); + SetVisible(dataSet, ref, true); } bool TabConfig::FileHasVisible(const DataSet& dataSet, int fileIndex) const { (void)dataSet; - if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) + if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return false; } - for (const SignalStyle& style : m_styles[static_cast(fileIndex)]) - { - if (style.visible) - { - return true; - } - } - return false; + const std::vector& fileVisible = m_visible[static_cast(fileIndex)]; + return std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end(); } bool TabConfig::FileAllVisible(const DataSet& dataSet, int fileIndex) const { (void)dataSet; - if (fileIndex < 0 || static_cast(fileIndex) >= m_styles.size()) + if (fileIndex < 0 || static_cast(fileIndex) >= m_visible.size()) { return false; } - const std::vector& fileStyles = m_styles[static_cast(fileIndex)]; - if (fileStyles.empty()) + const std::vector& fileVisible = m_visible[static_cast(fileIndex)]; + if (fileVisible.empty()) { return false; } - for (const SignalStyle& style : fileStyles) - { - if (!style.visible) - { - return false; - } - } - return true; + return std::find(fileVisible.begin(), fileVisible.end(), false) == fileVisible.end(); } bool TabConfig::AnyVisible() const { - for (const std::vector& fileStyles : m_styles) + for (const std::vector& fileVisible : m_visible) { - for (const SignalStyle& style : fileStyles) + if (std::find(fileVisible.begin(), fileVisible.end(), true) != fileVisible.end()) { - if (style.visible) - { - return true; - } + return true; } } return false; @@ -926,15 +868,9 @@ namespace jdi int TabConfig::VisibleCount() const { int count = 0; - for (const std::vector& fileStyles : m_styles) + for (const std::vector& fileVisible : m_visible) { - for (const SignalStyle& style : fileStyles) - { - if (style.visible) - { - ++count; - } - } + count += static_cast(std::count(fileVisible.begin(), fileVisible.end(), true)); } return count; } @@ -948,7 +884,7 @@ namespace jdi void TabConfig::SyncLanes(const DataSet& dataSet) { (void)dataSet; - RebuildLanes(m_styles, m_lanes, m_laneMode); + RebuildLanes(m_visible, m_lanes, m_laneMode); } int TabConfig::LaneOfSignal(const SignalRef& ref) const @@ -1117,18 +1053,10 @@ namespace jdi double low = std::numeric_limits::infinity(); double high = -std::numeric_limits::infinity(); - for (std::size_t fileIndex = 0; fileIndex < m_styles.size(); ++fileIndex) + for (std::size_t fileIndex = 0; fileIndex < m_visible.size(); ++fileIndex) { - bool fileUsed = false; - for (const SignalStyle& style : m_styles[fileIndex]) - { - if (style.visible) - { - fileUsed = true; - break; - } - } - if (!fileUsed) + const std::vector& fileVisible = m_visible[fileIndex]; + if (std::find(fileVisible.begin(), fileVisible.end(), true) == fileVisible.end()) { continue; } diff --git a/Utilities/Src/TextEdit.cpp b/Utilities/Src/TextEdit.cpp index a4c4062..6612d1f 100644 --- a/Utilities/Src/TextEdit.cpp +++ b/Utilities/Src/TextEdit.cpp @@ -211,6 +211,17 @@ namespace jdi return false; } + // 未按 Shift 且存在选区时,左右键先「收拢」到选区的对应边缘(常规输入框行为): + // 左键跳到选区左端、右键跳到选区右端,而不是在光标处再移动一格。 + if (!extendSelection && HasSelection()) + { + const std::size_t target = (delta < 0) ? SelectionBegin() : SelectionEnd(); + m_caret = target; + m_anchor = target; + m_fresh = false; + return true; + } + std::size_t offset = m_caret; const int steps = (delta > 0) ? delta : -delta; for (int index = 0; index < steps; ++index) @@ -239,30 +250,52 @@ namespace jdi bool TextEdit::MoveHome(bool extendSelection) { - if (!m_active || m_caret == 0) + if (!m_active) + { + return false; + } + + // 未按 Shift 时 Home 必须收拢已有选区(例如整段选中后按 Home 应取消全选)。 + if (!extendSelection) + { + const bool changed = HasSelection() || (m_caret != 0); + m_caret = 0; + m_anchor = 0; + m_fresh = false; + return changed; + } + + if (m_caret == 0) { return false; } m_caret = 0; - if (!extendSelection) - { - m_anchor = 0; - } m_fresh = false; return true; } bool TextEdit::MoveEnd(bool extendSelection) { - if (!m_active || m_caret == m_text.size()) + if (!m_active) + { + return false; + } + + // 同上:End 在未按 Shift 时收拢到行尾。 + if (!extendSelection) + { + const bool changed = HasSelection() || (m_caret != m_text.size()); + m_caret = m_text.size(); + m_anchor = m_caret; + m_fresh = false; + return changed; + } + + if (m_caret == m_text.size()) { return false; } m_caret = m_text.size(); - if (!extendSelection) - { - m_anchor = m_caret; - } m_fresh = false; return true; } @@ -274,6 +307,20 @@ namespace jdi m_fresh = !m_text.empty(); } + bool TextEdit::DeselectToEnd() + { + if (!m_active) + { + return false; + } + + const bool changed = HasSelection() || (m_caret != m_text.size()); + m_caret = m_text.size(); + m_anchor = m_caret; + m_fresh = false; + return changed; + } + bool TextEdit::CopyToClipboard(HWND owner) const { if (m_text.empty()) diff --git a/Utilities/Src/Theme.cpp b/Utilities/Src/Theme.cpp index 722231a..afc57d3 100644 --- a/Utilities/Src/Theme.cpp +++ b/Utilities/Src/Theme.cpp @@ -56,8 +56,10 @@ namespace jdi theme.pressed = MakeArgb(0xD8CFB6u); theme.selection = MakeArgb(0xDCEAF6u); - theme.gridWeak = MakeArgb(0xECE5D1u); - theme.gridStrong = MakeArgb(0xDCD4BEu); + // 网格线需要在绘图区底色(kBase3)上清晰可辨,同时又要与网格边界 + // (axisLine/borderStrong 的蓝灰)明显区分——因此采用偏暖的加深米色。 + theme.gridWeak = MakeArgb(0xDDD3B8u); + theme.gridStrong = MakeArgb(0xC3B896u); theme.axisLine = MakeArgb(kBase1); theme.cursor = MakeArgb(kOrange); theme.readoutBg = MakeArgb(0xF6EFDCu);