mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Inline edit UX improvements: selection clamping, auto-select, validation fixes
- Constrain selection/cursor to edit span boundaries during inline edit - Auto-select entire text when entering edit mode (Name, Value, Type) - Double-click during edit selects entire editable text - Fix vector component validation (subLine >= 0 for x component) - Accept EU decimal separator (comma) for float parsing - Darker selection highlight (35,35,35) vs hover (43,43,43) - Remove blue text indicator, use hidden style - Fix validation error message display Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -474,7 +474,7 @@ enum class EditTarget { Name, Type, Value };
|
|||||||
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line
|
||||||
inline constexpr int kColType = 10;
|
inline constexpr int kColType = 10;
|
||||||
inline constexpr int kColName = 22;
|
inline constexpr int kColName = 22;
|
||||||
inline constexpr int kColValue = 8;
|
inline constexpr int kColValue = 32;
|
||||||
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits
|
||||||
inline constexpr int kSepWidth = 2;
|
inline constexpr int kSepWidth = 2;
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
if (m_editState.target == EditTarget::Value)
|
if (m_editState.target == EditTarget::Value)
|
||||||
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
QTimer::singleShot(0, this, &RcxEditor::validateEditLive);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_sci, &QsciScintilla::selectionChanged,
|
||||||
|
this, &RcxEditor::clampEditSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::setupScintilla() {
|
void RcxEditor::setupScintilla() {
|
||||||
@@ -116,11 +119,9 @@ void RcxEditor::setupScintilla() {
|
|||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0);
|
||||||
|
|
||||||
// Editable-field link-style indicator (colored text)
|
// Editable-field indicator - set to HIDDEN (no visual, avoids INDIC_PLAIN underline)
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
IND_EDITABLE, 17 /*INDIC_TEXTFORE*/);
|
IND_EDITABLE, 5 /*INDIC_HIDDEN*/);
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
|
||||||
IND_EDITABLE, QColor("#569cd6"));
|
|
||||||
|
|
||||||
// Hex/Padding node dim indicator — overrides text color to gray
|
// Hex/Padding node dim indicator — overrides text color to gray
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||||
@@ -232,7 +233,7 @@ void RcxEditor::setupMarkers() {
|
|||||||
|
|
||||||
// M_SELECTED (7): full-row selection highlight (higher = wins over hover)
|
// M_SELECTED (7): full-row selection highlight (higher = wins over hover)
|
||||||
m_sci->markerDefine(QsciScintilla::Background, M_SELECTED);
|
m_sci->markerDefine(QsciScintilla::Background, M_SELECTED);
|
||||||
m_sci->setMarkerBackgroundColor(QColor(53, 53, 53), M_SELECTED);
|
m_sci->setMarkerBackgroundColor(QColor(35, 35, 35), M_SELECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RcxEditor::allocateMarginStyles() {
|
void RcxEditor::allocateMarginStyles() {
|
||||||
@@ -753,9 +754,11 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
|||||||
m_pendingClickNodeId = 0;
|
m_pendingClickNodeId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Block double/triple-click during edit mode (prevents word/line selection)
|
// Double-click during edit mode: select entire editable text
|
||||||
if (obj == m_sci->viewport() && m_editState.active
|
if (obj == m_sci->viewport() && m_editState.active
|
||||||
&& event->type() == QEvent::MouseButtonDblClick) {
|
&& event->type() == QEvent::MouseButtonDblClick) {
|
||||||
|
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
||||||
|
m_editState.line, editEndCol());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (obj == m_sci->viewport() && !m_editState.active
|
if (obj == m_sci->viewport() && !m_editState.active
|
||||||
@@ -931,7 +934,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
m_editState.linelenAfterReplace = lineText.size();
|
m_editState.linelenAfterReplace = lineText.size();
|
||||||
m_editState.editKind = lm->nodeKind;
|
m_editState.editKind = lm->nodeKind;
|
||||||
if ((lm->nodeKind == NodeKind::Vec2 || lm->nodeKind == NodeKind::Vec3 ||
|
if ((lm->nodeKind == NodeKind::Vec2 || lm->nodeKind == NodeKind::Vec3 ||
|
||||||
lm->nodeKind == NodeKind::Vec4) && lm->subLine > 0)
|
lm->nodeKind == NodeKind::Vec4) && lm->subLine >= 0)
|
||||||
m_editState.editKind = NodeKind::Float;
|
m_editState.editKind = NodeKind::Float;
|
||||||
|
|
||||||
// Store fixed comment column position for value editing
|
// Store fixed comment column position for value editing
|
||||||
@@ -964,7 +967,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) {
|
|||||||
(unsigned long)line);
|
(unsigned long)line);
|
||||||
long posStart = lineStart + m_editState.spanStart;
|
long posStart = lineStart + m_editState.spanStart;
|
||||||
long posEnd = posStart + trimmed.toUtf8().size();
|
long posEnd = posStart + trimmed.toUtf8().size();
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posEnd, posEnd);
|
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, posStart, posEnd);
|
||||||
|
|
||||||
// Show initial edit hint in comment column
|
// Show initial edit hint in comment column
|
||||||
if (target == EditTarget::Value)
|
if (target == EditTarget::Value)
|
||||||
@@ -982,6 +985,50 @@ int RcxEditor::editEndCol() const {
|
|||||||
return m_editState.spanStart + m_editState.original.size() + delta;
|
return m_editState.spanStart + m_editState.original.size() + delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RcxEditor::clampEditSelection() {
|
||||||
|
if (!m_editState.active) return;
|
||||||
|
|
||||||
|
static bool s_clamping = false;
|
||||||
|
if (s_clamping) return;
|
||||||
|
s_clamping = true;
|
||||||
|
|
||||||
|
int selStartLine, selStartCol, selEndLine, selEndCol;
|
||||||
|
m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol);
|
||||||
|
|
||||||
|
int editEnd = editEndCol();
|
||||||
|
bool isCursor = (selStartLine == selEndLine && selStartCol == selEndCol);
|
||||||
|
|
||||||
|
if (isCursor) {
|
||||||
|
// Cursor positioning (no selection) - only clamp if outside bounds
|
||||||
|
if (selStartLine != m_editState.line ||
|
||||||
|
selStartCol < m_editState.spanStart || selStartCol > editEnd) {
|
||||||
|
int clampedCol = qBound(m_editState.spanStart, selStartCol, editEnd);
|
||||||
|
m_sci->setCursorPosition(m_editState.line, clampedCol);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Actual selection - clamp both ends to edit span
|
||||||
|
bool clamped = false;
|
||||||
|
|
||||||
|
// Force to edit line
|
||||||
|
if (selStartLine != m_editState.line || selEndLine != m_editState.line) {
|
||||||
|
m_sci->setSelection(m_editState.line, m_editState.spanStart,
|
||||||
|
m_editState.line, editEnd);
|
||||||
|
s_clamping = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selStartCol < m_editState.spanStart) { selStartCol = m_editState.spanStart; clamped = true; }
|
||||||
|
if (selEndCol < m_editState.spanStart) { selEndCol = m_editState.spanStart; clamped = true; }
|
||||||
|
if (selStartCol > editEnd) { selStartCol = editEnd; clamped = true; }
|
||||||
|
if (selEndCol > editEnd) { selEndCol = editEnd; clamped = true; }
|
||||||
|
|
||||||
|
if (clamped)
|
||||||
|
m_sci->setSelection(selStartLine, selStartCol, selEndLine, selEndCol);
|
||||||
|
}
|
||||||
|
|
||||||
|
s_clamping = false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Commit inline edit ──
|
// ── Commit inline edit ──
|
||||||
|
|
||||||
void RcxEditor::commitInlineEdit() {
|
void RcxEditor::commitInlineEdit() {
|
||||||
@@ -1015,11 +1062,7 @@ void RcxEditor::showTypeAutocomplete() {
|
|||||||
if (!m_editState.active || m_editState.target != EditTarget::Type)
|
if (!m_editState.active || m_editState.target != EditTarget::Type)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Collapse selection to start — old type text stays visible
|
// Selection stays intact - typing/autocomplete will replace selected text
|
||||||
long lineStart = m_sci->SendScintilla(QsciScintillaBase::SCI_POSITIONFROMLINE,
|
|
||||||
(unsigned long)m_editState.line);
|
|
||||||
long posStart = lineStart + m_editState.spanStart;
|
|
||||||
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, posStart);
|
|
||||||
|
|
||||||
// Build list from typeName (matches what the editor displays)
|
// Build list from typeName (matches what the editor displays)
|
||||||
QByteArray list = allTypeNamesForUI().join(' ').toUtf8();
|
QByteArray list = allTypeNamesForUI().join(' ').toUtf8();
|
||||||
@@ -1179,7 +1222,7 @@ void RcxEditor::validateEditLive() {
|
|||||||
} else {
|
} else {
|
||||||
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
|
if (isSelected) m_sci->markerDelete(m_editState.line, M_SELECTED);
|
||||||
m_sci->markerAdd(m_editState.line, M_ERR);
|
m_sci->markerAdd(m_editState.line, M_ERR);
|
||||||
if (stateChanged) setEditComment("! " + text);
|
if (stateChanged) setEditComment("! " + errorMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ private:
|
|||||||
void applyHoverHighlight();
|
void applyHoverHighlight();
|
||||||
void validateEditLive();
|
void validateEditLive();
|
||||||
void setEditComment(const QString& comment);
|
void setEditComment(const QString& comment);
|
||||||
|
void clampEditSelection();
|
||||||
|
|
||||||
// ── Refactored helpers ──
|
// ── Refactored helpers ──
|
||||||
struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; };
|
struct HitInfo { int line = -1; int col = -1; uint64_t nodeId = 0; bool inFoldCol = false; };
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ QString fmtUInt16(uint16_t v) { return hexVal(v); }
|
|||||||
QString fmtUInt32(uint32_t v) { return hexVal(v); }
|
QString fmtUInt32(uint32_t v) { return hexVal(v); }
|
||||||
QString fmtUInt64(uint64_t v) { return hexVal(v); }
|
QString fmtUInt64(uint64_t v) { return hexVal(v); }
|
||||||
|
|
||||||
QString fmtFloat(float v) { return QString::number(v, 'f', 3); }
|
QString fmtFloat(float v) { return QString::number(v, 'g', 4); } // 4 sig figs keeps it short
|
||||||
QString fmtDouble(double v) { return QString::number(v, 'f', 6); }
|
QString fmtDouble(double v) { return QString::number(v, 'g', 6); }
|
||||||
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
QString fmtBool(uint8_t v) { return v ? QStringLiteral("true") : QStringLiteral("false"); }
|
||||||
|
|
||||||
QString fmtPointer32(uint32_t v) {
|
QString fmtPointer32(uint32_t v) {
|
||||||
@@ -351,11 +351,13 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
|
|||||||
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
|
case NodeKind::UInt32: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; uint val = stripHex(s).toUInt(ok,b); return *ok ? toBytes<uint32_t>(val) : QByteArray{}; }
|
||||||
case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
|
case NodeKind::UInt64: { int b = s.startsWith("0x",Qt::CaseInsensitive)?16:10; qulonglong val = stripHex(s).toULongLong(ok,b); return *ok ? toBytes<uint64_t>(val) : QByteArray{}; }
|
||||||
case NodeKind::Float: {
|
case NodeKind::Float: {
|
||||||
float val = s.toFloat(ok);
|
QString n = s; n.replace(',', '.'); // Accept EU decimal separator
|
||||||
|
float val = n.toFloat(ok);
|
||||||
return *ok ? toBytes<float>(val) : QByteArray{};
|
return *ok ? toBytes<float>(val) : QByteArray{};
|
||||||
}
|
}
|
||||||
case NodeKind::Double: {
|
case NodeKind::Double: {
|
||||||
double val = s.toDouble(ok);
|
QString n = s; n.replace(',', '.'); // Accept EU decimal separator
|
||||||
|
double val = n.toDouble(ok);
|
||||||
return *ok ? toBytes<double>(val) : QByteArray{};
|
return *ok ? toBytes<double>(val) : QByteArray{};
|
||||||
}
|
}
|
||||||
case NodeKind::Bool: {
|
case NodeKind::Bool: {
|
||||||
@@ -433,6 +435,11 @@ QString validateValue(NodeKind kind, const QString& text) {
|
|||||||
parseValue(kind, text, &ok);
|
parseValue(kind, text, &ok);
|
||||||
if (ok) return {};
|
if (ok) return {};
|
||||||
|
|
||||||
|
// Type-appropriate error messages
|
||||||
|
bool isFloatKind = (kind == NodeKind::Float || kind == NodeKind::Double);
|
||||||
|
if (isFloatKind)
|
||||||
|
return QStringLiteral("invalid number");
|
||||||
|
|
||||||
// Return byte-capacity max based on type size
|
// Return byte-capacity max based on type size
|
||||||
const auto* m = kindMeta(kind);
|
const auto* m = kindMeta(kind);
|
||||||
if (m && m->size > 0 && m->size <= 8) {
|
if (m && m->size > 0 && m->size <= 8) {
|
||||||
|
|||||||
@@ -435,12 +435,11 @@ void MainWindow::newFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 0x100 bytes of Hex64 padding (32 nodes) ──
|
// ── Fill with Hex64 until 0x6000 for stress testing ──
|
||||||
int padStart = oh + 0xF0; // end of optional header
|
int padStart = oh + 0xF0; // end of optional header
|
||||||
for (int i = 0; i < 32; i++) {
|
for (int off = padStart; off < 0x6000; off += 8) {
|
||||||
int off = padStart + i * 8;
|
|
||||||
add(NodeKind::Hex64,
|
add(NodeKind::Hex64,
|
||||||
QString("pad_%1").arg(off, 4, 16, QChar('0')),
|
QString("data_%1").arg(off, 4, 16, QChar('0')),
|
||||||
off);
|
off);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,29 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QFocusEvent>
|
#include <QFocusEvent>
|
||||||
|
#include <QFile>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
#include "core.h"
|
#include "core.h"
|
||||||
|
|
||||||
using namespace rcx;
|
using namespace rcx;
|
||||||
|
|
||||||
// Minimal provider for testing
|
// Load first 0x6000 bytes of the test exe for realistic data
|
||||||
static FileProvider makeTestProvider() {
|
static FileProvider makeTestProvider() {
|
||||||
QByteArray data(256, '\0');
|
QFile exe(QCoreApplication::applicationFilePath());
|
||||||
// Write known values: uint16_t=23117 at offset 0, Hex64 at offset 8
|
if (exe.open(QIODevice::ReadOnly)) {
|
||||||
uint16_t u16 = 23117;
|
QByteArray data = exe.read(0x6000);
|
||||||
memcpy(data.data(), &u16, 2);
|
exe.close();
|
||||||
uint64_t h64 = 0x4D5A900000000000ULL;
|
if (data.size() >= 0x6000)
|
||||||
memcpy(data.data() + 8, &h64, 8);
|
return FileProvider(data);
|
||||||
|
}
|
||||||
|
// Fallback: minimal PE header stub
|
||||||
|
QByteArray data(0x6000, '\0');
|
||||||
|
data[0] = 'M'; data[1] = 'Z'; // DOS signature
|
||||||
return FileProvider(data);
|
return FileProvider(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a simple tree with a struct containing a few fields
|
// Build a tree covering 0x6000 bytes with Hex64 fields
|
||||||
static NodeTree makeTestTree() {
|
static NodeTree makeTestTree() {
|
||||||
NodeTree tree;
|
NodeTree tree;
|
||||||
tree.baseAddress = 0;
|
tree.baseAddress = 0;
|
||||||
@@ -33,6 +38,7 @@ static NodeTree makeTestTree() {
|
|||||||
int ri = tree.addNode(root);
|
int ri = tree.addNode(root);
|
||||||
uint64_t rootId = tree.nodes[ri].id;
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// First two fields for existing tests
|
||||||
Node f1;
|
Node f1;
|
||||||
f1.kind = NodeKind::UInt16;
|
f1.kind = NodeKind::UInt16;
|
||||||
f1.name = "field_u16";
|
f1.name = "field_u16";
|
||||||
@@ -47,6 +53,17 @@ static NodeTree makeTestTree() {
|
|||||||
f2.offset = 8;
|
f2.offset = 8;
|
||||||
tree.addNode(f2);
|
tree.addNode(f2);
|
||||||
|
|
||||||
|
// Fill remaining 0x6000 bytes with Hex64 fields (8 bytes each)
|
||||||
|
// Start at offset 16 (0x10), go to 0x6000
|
||||||
|
for (int off = 0x10; off < 0x6000; off += 8) {
|
||||||
|
Node f;
|
||||||
|
f.kind = NodeKind::Hex64;
|
||||||
|
f.name = QString("data_%1").arg(off, 4, 16, QChar('0'));
|
||||||
|
f.parentId = rootId;
|
||||||
|
f.offset = off;
|
||||||
|
tree.addNode(f);
|
||||||
|
}
|
||||||
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user