fix: 修正滑块填充残留、数值内联输入与信号/分组重命名,拖拽虚影跟随鼠标
- 滑块最小值时不再有超出圆钮的填充残留,三处滑块统一为共享实现 - 透明度/RGB/色号支持点击或双击直接输入,整段高亮、首次键入替换、回车提交、Ctrl+C 复制 - 双击信号行或分组名进入重命名编辑;分组竖线改为绘制而非字符 - 拖拽信号时曲线虚影跟随鼠标纵向位移 - 数据层新增展示名覆盖,面板改为持有可写数据集合
This commit is contained in:
1 parent
542aa51e40
commit
261b8ac07d
13 files changed
+1074
-274
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
@@ -30,7 +33,7 @@ namespace jdi
|
||||
const std::uint32_t kChannelColors[3] = {0xFFDC322Fu, 0xFF859900u, 0xFF268BD2u};
|
||||
|
||||
/**
|
||||
* @brief 绘制水平滑块(轨道 + 已填充部分 + 圆钮)。
|
||||
* @brief 绘制水平滑块(与样式区共用同一实现,保证行为一致)。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] track 轨道矩形。
|
||||
* @param [IN] ratio 当前值比例(0-1)。
|
||||
@@ -41,22 +44,7 @@ namespace jdi
|
||||
void DrawChannelSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||
bool hovered, float scale)
|
||||
{
|
||||
const float radius = track.Height * 0.5f;
|
||||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
||||
|
||||
ratio = std::max(0.0, std::min(1.0, ratio));
|
||||
const float fillWidth = std::max(track.Height, static_cast<float>(ratio) * track.Width);
|
||||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
||||
|
||||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
||||
const float knobRadius = radius + (hovered ? 2.5f * scale : 0.8f * scale);
|
||||
|
||||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
||||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
||||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||
knobRadius * 2.0f);
|
||||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||
knobRadius * 2.0f);
|
||||
UiDrawSlider(g, track, ratio, fillColor, hovered, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,11 +167,238 @@ namespace jdi
|
||||
|
||||
void ColorPickerPopup::Close()
|
||||
{
|
||||
// 收起前先提交正在编辑的取值,避免用户输入被静默丢弃。
|
||||
if (m_editField != EditField::NONE)
|
||||
{
|
||||
EndEdit(true);
|
||||
}
|
||||
m_open = false;
|
||||
m_draggingSlider = -1;
|
||||
m_hoverSlider = -1;
|
||||
}
|
||||
|
||||
gd::RectF ColorPickerPopup::EditFieldRect() const
|
||||
{
|
||||
switch (m_editField)
|
||||
{
|
||||
case EditField::R:
|
||||
return m_values[0];
|
||||
case EditField::G:
|
||||
return m_values[1];
|
||||
case EditField::B:
|
||||
return m_values[2];
|
||||
case EditField::HEX:
|
||||
return m_hexRect;
|
||||
case EditField::NONE:
|
||||
default:
|
||||
return gd::RectF();
|
||||
}
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::BeginEdit(EditField field)
|
||||
{
|
||||
if (!m_open || field == EditField::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_editField == field)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (field)
|
||||
{
|
||||
case EditField::R:
|
||||
case EditField::G:
|
||||
case EditField::B:
|
||||
{
|
||||
const int index = static_cast<int>(field) - 1;
|
||||
m_editText = std::to_wstring(ToByte(m_channels[static_cast<std::size_t>(index)]));
|
||||
break;
|
||||
}
|
||||
case EditField::HEX:
|
||||
{
|
||||
const std::uint32_t color = CurrentColor();
|
||||
wchar_t buffer[16];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||
m_editText = std::wstring(buffer);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
m_draggingSlider = -1;
|
||||
m_editField = field;
|
||||
m_editFresh = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::EndEdit(bool commit)
|
||||
{
|
||||
if (m_editField == EditField::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const EditField field = m_editField;
|
||||
const std::wstring text = m_editText;
|
||||
m_editField = EditField::NONE;
|
||||
m_editFresh = false;
|
||||
m_editText.clear();
|
||||
|
||||
if (!commit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::wstring trimmed = Trim(text);
|
||||
if (field == EditField::HEX)
|
||||
{
|
||||
// 接受带或不带 '#' 的 6 位十六进制色号。
|
||||
std::wstring digits = trimmed;
|
||||
if (!digits.empty() && digits.front() == L'#')
|
||||
{
|
||||
digits.erase(digits.begin());
|
||||
}
|
||||
if (digits.size() == 6)
|
||||
{
|
||||
unsigned int value = 0;
|
||||
bool valid = true;
|
||||
for (const wchar_t c : digits)
|
||||
{
|
||||
unsigned int digit = 0;
|
||||
if (c >= L'0' && c <= L'9')
|
||||
{
|
||||
digit = static_cast<unsigned int>(c - L'0');
|
||||
}
|
||||
else if (c >= L'a' && c <= L'f')
|
||||
{
|
||||
digit = static_cast<unsigned int>(c - L'a') + 10u;
|
||||
}
|
||||
else if (c >= L'A' && c <= L'F')
|
||||
{
|
||||
digit = static_cast<unsigned int>(c - L'A') + 10u;
|
||||
}
|
||||
else
|
||||
{
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
value = (value << 4) | digit;
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
|
||||
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
|
||||
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
|
||||
m_changed = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
wchar_t* stop = nullptr;
|
||||
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||
if (stop == trimmed.c_str() || parsed != parsed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
double value = parsed;
|
||||
if (value < 0.0)
|
||||
{
|
||||
value = 0.0;
|
||||
}
|
||||
if (value > 255.0)
|
||||
{
|
||||
value = 255.0;
|
||||
}
|
||||
|
||||
const int index = static_cast<int>(field) - 1;
|
||||
if (index >= 0 && index < 3)
|
||||
{
|
||||
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
|
||||
m_changed = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::OnChar(wchar_t character)
|
||||
{
|
||||
if (m_editField == EditField::NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ctrl+C:把当前文本复制到剪贴板。
|
||||
if (character == 0x03)
|
||||
{
|
||||
CopyTextToClipboard(nullptr, m_editText);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (character == L'\r' || character == L'\n')
|
||||
{
|
||||
return EndEdit(true);
|
||||
}
|
||||
if (character == L'\b')
|
||||
{
|
||||
m_editFresh = false;
|
||||
if (!m_editText.empty())
|
||||
{
|
||||
m_editText.pop_back();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (character < 0x20 || character == 0x7F)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_editField == EditField::HEX)
|
||||
{
|
||||
const bool hexDigit = (character >= L'0' && character <= L'9') ||
|
||||
(character >= L'a' && character <= L'f') ||
|
||||
(character >= L'A' && character <= L'F') || (character == L'#');
|
||||
if (!hexDigit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (m_editFresh)
|
||||
{
|
||||
m_editText.clear();
|
||||
m_editFresh = false;
|
||||
}
|
||||
if (m_editText.size() < 7)
|
||||
{
|
||||
m_editText.push_back(character);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool digit = (character >= L'0' && character <= L'9');
|
||||
if (!digit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (m_editFresh)
|
||||
{
|
||||
m_editText.clear();
|
||||
m_editFresh = false;
|
||||
}
|
||||
if (m_editText.size() < 3)
|
||||
{
|
||||
m_editText.push_back(character);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ColorPickerPopup::ContainsPoint(int x, int y) const
|
||||
{
|
||||
if (!m_open)
|
||||
@@ -283,6 +498,29 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 点击当前输入框以外的位置时提交输入。
|
||||
if (m_editField != EditField::NONE && !UiPointInRect(EditFieldRect(), static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
EndEdit(true);
|
||||
}
|
||||
|
||||
// 数值与色号文本优先于滑块:两者几何重叠时要能进入输入状态。
|
||||
for (int index = 0; index < 3; ++index)
|
||||
{
|
||||
if (UiPointInRect(m_values[static_cast<std::size_t>(index)], static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
BeginEdit(static_cast<EditField>(index + 1));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (UiPointInRect(m_hexRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
BeginEdit(EditField::HEX);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_draggingSlider = SliderAtPoint(x, y);
|
||||
if (m_draggingSlider >= 0)
|
||||
{
|
||||
@@ -308,6 +546,28 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_editField != EditField::NONE)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case VK_RETURN:
|
||||
EndEdit(true);
|
||||
return true;
|
||||
case VK_ESCAPE:
|
||||
EndEdit(false);
|
||||
return true;
|
||||
case VK_BACK:
|
||||
m_editFresh = false;
|
||||
if (!m_editText.empty())
|
||||
{
|
||||
m_editText.pop_back();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (key == VK_ESCAPE)
|
||||
{
|
||||
Close();
|
||||
@@ -347,8 +607,15 @@ namespace jdi
|
||||
const bool active = (m_draggingSlider == index) || (m_hoverSlider == index);
|
||||
DrawChannelSlider(g, track, m_channels[slot], kChannelColors[index], active, m_scale);
|
||||
|
||||
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
|
||||
m_theme->text, TextAlign::RIGHT);
|
||||
if (m_editField == static_cast<EditField>(index + 1))
|
||||
{
|
||||
UiDrawEditField(g, m_values[slot], m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiText(g, std::to_wstring(ToByte(m_channels[slot])), m_fonts->monoSmall, m_values[slot],
|
||||
m_theme->text, TextAlign::RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// 预览与十六进制值。
|
||||
@@ -356,10 +623,17 @@ namespace jdi
|
||||
UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color);
|
||||
UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
|
||||
wchar_t buffer[16];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
|
||||
if (m_editField == EditField::HEX)
|
||||
{
|
||||
UiDrawEditField(g, m_hexRect, m_editText, m_editFresh, m_fonts->monoSmall, m_scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t buffer[16];
|
||||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"#%02X%02X%02X", (color >> 16) & 0xFFu,
|
||||
(color >> 8) & 0xFFu, color & 0xFFu);
|
||||
UiText(g, std::wstring(buffer), m_fonts->monoSmall, m_hexRect, m_theme->text, TextAlign::LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
@@ -446,6 +446,47 @@ namespace jdi
|
||||
return &m_files[static_cast<std::size_t>(fileIndex)];
|
||||
}
|
||||
|
||||
bool DataSet::SetSignalDisplayName(int fileIndex, int signalIndex, const std::wstring& name)
|
||||
{
|
||||
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
|
||||
if (signalIndex < 0 || signalIndex >= static_cast<int>(file.signals.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 统一去除首尾空白;结果为空则视为恢复原始列名。
|
||||
std::wstring trimmed = Trim(name);
|
||||
SignalData& signal = file.signals[static_cast<std::size_t>(signalIndex)];
|
||||
if (trimmed == signal.displayName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
signal.displayName = std::move(trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::SetFileDisplayName(int fileIndex, const std::wstring& name)
|
||||
{
|
||||
if (fileIndex < 0 || fileIndex >= static_cast<int>(m_files.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring trimmed = Trim(name);
|
||||
DataFile& file = m_files[static_cast<std::size_t>(fileIndex)];
|
||||
if (trimmed == file.displayName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
file.displayName = std::move(trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const SignalData* DataSet::Resolve(const SignalRef& ref) const
|
||||
{
|
||||
const DataFile* file = FileAt(ref.fileIndex);
|
||||
|
||||
@@ -419,6 +419,83 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
void UiDrawSlider(gd::Graphics& g, const gd::RectF& track, double ratio, std::uint32_t fillColor,
|
||||
bool active, float scale)
|
||||
{
|
||||
if (track.Width <= 0.0f || track.Height <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ratio < 0.0)
|
||||
{
|
||||
ratio = 0.0;
|
||||
}
|
||||
if (ratio > 1.0)
|
||||
{
|
||||
ratio = 1.0;
|
||||
}
|
||||
|
||||
const float radius = track.Height * 0.5f;
|
||||
UiFillRoundRect(g, track, radius, 0x2A657B83u);
|
||||
|
||||
// 已填充部分严格止于圆钮中心,最小值时不会留下超出圆钮的填充残留。
|
||||
const float fillWidth = static_cast<float>(ratio) * track.Width;
|
||||
if (fillWidth > 0.5f)
|
||||
{
|
||||
UiFillRoundRect(g, gd::RectF(track.X, track.Y, fillWidth, track.Height), radius, fillColor);
|
||||
}
|
||||
|
||||
const float knobRadius = radius + (active ? 2.5f * scale : 0.8f * scale);
|
||||
const float knobX = track.X + static_cast<float>(ratio) * track.Width;
|
||||
|
||||
gd::SolidBrush knob(ToGdiColor(0xFFFFFFFFu));
|
||||
gd::Pen knobPen(ToGdiColor(0xFF586E75u), 1.4f * scale);
|
||||
g.FillEllipse(&knob, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||
knobRadius * 2.0f);
|
||||
g.DrawEllipse(&knobPen, knobX - knobRadius, track.Y + radius - knobRadius, knobRadius * 2.0f,
|
||||
knobRadius * 2.0f);
|
||||
}
|
||||
|
||||
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const std::wstring& text, bool fresh,
|
||||
HFONT font, float scale)
|
||||
{
|
||||
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const Theme& theme = SolarizedLight();
|
||||
const float corner = 3.0f * scale;
|
||||
|
||||
UiFillRoundRect(g, rect, corner, theme.cardBg);
|
||||
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
|
||||
|
||||
const std::wstring shown = text.empty() ? std::wstring(L" ") : text;
|
||||
|
||||
if (fresh)
|
||||
{
|
||||
// 整段内容处于“将被覆盖”状态:以强调色底衬表示已全选。
|
||||
const float textWidth = UiTextWidth(g, shown, font) + 8.0f * scale;
|
||||
const gd::RectF highlight(rect.X + (rect.Width - textWidth) * 0.5f, rect.Y + 2.0f * scale,
|
||||
textWidth, rect.Height - 4.0f * scale);
|
||||
UiFillRoundRect(g, highlight, 2.0f * scale, theme.accent);
|
||||
UiText(g, shown, font, highlight, theme.textOnAccent, TextAlign::CENTER);
|
||||
return;
|
||||
}
|
||||
|
||||
UiText(g, shown, font, rect, theme.textStrong, TextAlign::CENTER);
|
||||
|
||||
// 已开始键入:在文本末尾显示插入符。
|
||||
const float textWidth = UiTextWidth(g, shown, font);
|
||||
const float caretX = rect.X + (rect.Width + textWidth) * 0.5f + 1.0f * scale;
|
||||
if (caretX < rect.X + rect.Width - 2.0f * scale)
|
||||
{
|
||||
UiDrawLine(g, caretX, rect.Y + 4.0f * scale, caretX, rect.Y + rect.Height - 4.0f * scale,
|
||||
theme.accent, 1.4f * scale);
|
||||
}
|
||||
}
|
||||
|
||||
bool UiSwatchNeedsOutline(float opacity)
|
||||
{
|
||||
return !(opacity >= kSwatchOutlineOpacity);
|
||||
|
||||
@@ -367,4 +367,48 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OpenClipboard(owner) == FALSE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
if (EmptyClipboard() != FALSE)
|
||||
{
|
||||
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
|
||||
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (handle != nullptr)
|
||||
{
|
||||
void* target = GlobalLock(handle);
|
||||
if (target != nullptr)
|
||||
{
|
||||
std::memcpy(target, text.c_str(), bytes);
|
||||
GlobalUnlock(handle);
|
||||
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalFree(handle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalFree(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CloseClipboard();
|
||||
return success;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user