feat: value history timestamps, Ctrl+F search, base address fixes

- Add timestamps to ValueHistory ring buffer, expose via new MCP tool
  node.history, show relative time in popup ("26s ago", "2m ago")
- Add "Clear Value History" right-click menu for single and multi-select
- Add Ctrl+F find bar to RcxEditor with live search, Enter-to-next, wrap
- Fix Ctrl+F in workspace dock to auto-focus search field
- Add "Change to float" quick-convert for Hex32 right-click menu
- Sort workspace explorer by children count descending (most fields first)
- Fix provider->base() overwriting saved base address from .rcx files
- Add formula support to MCP change_base operation
- Re-evaluate baseAddressFormula on provider attach in selectSource()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IChooseYou
2026-03-02 10:00:17 -07:00
parent ba1c2f8e5a
commit efae193520
9 changed files with 11563 additions and 6 deletions

View File

@@ -19,8 +19,10 @@
#include <QClipboard>
#include <QLabel>
#include <QToolButton>
#include <QLineEdit>
#include <QScreen>
#include <QScrollBar>
#include <QDateTime>
#include <functional>
#include "themes/thememanager.h"
@@ -102,7 +104,8 @@ public:
sep->setPalette(sp);
vbox->addWidget(sep);
for (const QString& v : vals) {
qint64 now = QDateTime::currentMSecsSinceEpoch();
hist.forEachWithTime([&](const QString& v, qint64 msec) {
auto* row = new QHBoxLayout;
row->setContentsMargins(0, 1, 0, 1);
row->setSpacing(8);
@@ -113,6 +116,24 @@ public:
row->addWidget(label, 1);
m_labels.append(label);
if (msec > 0) {
qint64 elapsed = now - msec;
QString timeStr;
if (elapsed < 1000)
timeStr = QStringLiteral("now");
else if (elapsed < 60000)
timeStr = QStringLiteral("%1s ago").arg(elapsed / 1000);
else if (elapsed < 3600000)
timeStr = QStringLiteral("%1m ago").arg(elapsed / 60000);
else
timeStr = QStringLiteral("%1h ago").arg(elapsed / 3600000);
auto* timeLabel = new QLabel(timeStr);
timeLabel->setFont(font);
timeLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
row->addWidget(timeLabel);
}
if (showButtons) {
auto* setBtn = new QToolButton;
setBtn->setText(QStringLiteral("Set"));
@@ -130,7 +151,7 @@ public:
row->addWidget(setBtn);
}
vbox->addLayout(row);
}
});
adjustSize();
}
@@ -380,6 +401,12 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
m_sci = new QsciScintilla(this);
layout->addWidget(m_sci);
// Find bar (hidden by default, shown with Ctrl+F)
m_findBar = new QLineEdit(this);
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
m_findBar->setVisible(false);
layout->addWidget(m_findBar);
setupScintilla();
setupLexer();
setupMargins();
@@ -395,6 +422,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
m_sci->viewport()->installEventFilter(this);
m_sci->viewport()->setMouseTracking(true);
// Find bar: live search on text change
connect(m_findBar, &QLineEdit::textChanged, this, [this](const QString& text) {
if (text.isEmpty()) return;
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
});
// Find bar: Enter jumps to next match (wraps at end)
connect(m_findBar, &QLineEdit::returnPressed, this, [this]() {
QString text = m_findBar->text();
if (text.isEmpty()) return;
if (!m_sci->findNext())
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
});
// Escape hides find bar
{
auto* escAction = new QAction(m_findBar);
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
escAction->setShortcutContext(Qt::WidgetShortcut);
m_findBar->addAction(escAction);
connect(escAction, &QAction::triggered, this, [this]() { hideFindBar(); });
}
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
@@ -782,6 +830,14 @@ void RcxEditor::applyTheme(const Theme& theme) {
abs, theme.background);
}
}
// Find bar
if (m_findBar) {
m_findBar->setStyleSheet(
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
" padding: 4px 8px; font-size: 13px; }")
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
}
}
void RcxEditor::applyDocument(const ComposeResult& result) {
@@ -1243,6 +1299,17 @@ int RcxEditor::currentNodeIndex() const {
return lm ? lm->nodeIdx : -1;
}
void RcxEditor::showFindBar() {
m_findBar->setVisible(true);
m_findBar->setFocus();
m_findBar->selectAll();
}
void RcxEditor::hideFindBar() {
m_findBar->setVisible(false);
m_sci->setFocus();
}
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
for (int i = 0; i < m_meta.size(); i++) {
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
@@ -1810,6 +1877,10 @@ static bool hitTestTarget(QsciScintilla* sci,
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
if (obj == m_sci && event->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(event);
if (ke->matches(QKeySequence::Find)) {
showFindBar();
return true;
}
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
if (!handled && !m_editState.active) {
// Clear hover on keyboard navigation (stale after scroll)