fix: 统一输入框编辑行为、外观跨视图同步并优化标尺与网格可读性

输入框(TextEdit,全组件共用):
- 修复退格一次删除两个字符:WM_CHAR 与 WM_KEYDOWN 重复处理了退格/删除,
  现统一由 OnEditKey 处理,WM_CHAR 忽略全部控制字符。
- 进入编辑(整段选中)后按右键:取消全选并把插入符移到文本最右侧。
- 存在选区时松开 Shift 再按左右方向键,插入符收拢到选区的左/右端。
- 修复整段选中后按 Home/End 不收拢选区的问题。

标签页:
- 按加粗字体测量宽度,避免激活页长名称被提前省略(预览过短)。
- 重命名过程中按正在输入的文本测量并加宽上限(320px),标签随输入增长,
  解决长内容绘制错误;输入溢出时整体等比收缩,保证「+」始终可见。

信号显示外观跨标签页同步:
- 外观(颜色/自定义色记忆/透明度/线宽/样式)从 TabConfig 上移到 DataSet
  的 SignalData,成为信号的全局属性;可见性仍按标签页独立保存。
- 因此任一视图中修改某信号的颜色/线宽/透明度/样式,其它视图自动一致。

工具栏:
- 移除「全选信号」按钮;「清空勾选」改为「清空网格」,
  「拆分信号网格」改为「拆分网格」。

自适应:
- 无可见信号(未勾选/全部取消/清空网格)时复位 ViewInitialized,
  下次勾选第一个信号会重新自适应,而不是停留在旧视图范围。

绘图区标尺与网格:
- 幅值刻度密度按网格高度自适应(3-8 档),各网格刻度数量一致;
  网格较矮时同步收紧标签行距,提高可读密度。
- 网格线与五等分估读线加深,与网格边界(蓝灰)保持明显区分。
- 时间轴列名补上单位,显示为 time (s)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 15:16:53 +08:00
1 parent 53768b8b25
commit e3744f3eff
16 files changed
+462 -340

No files matched your search

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