diff --git a/src/compose.cpp b/src/compose.cpp index 5f59b7c..313d0fa 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -18,6 +18,7 @@ struct ComposeState { int currentLine = 0; int typeW = kColType; // global type column width (fallback) int nameW = kColName; // global name column width (fallback) + int offsetHexDigits = 8; // hex digit tier for offset margin bool baseEmitted = false; // only first root struct shows base address // Precomputed for O(1) lookups @@ -144,7 +145,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree, lm.isContinuation = isCont; lm.lineKind = isCont ? LineKind::Continuation : LineKind::Field; lm.nodeKind = node.kind; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits); lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth); lm.foldLevel = computeFoldLevel(depth, false); lm.effectiveTypeW = typeW; @@ -191,7 +192,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::Field; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.nodeKind = node.kind; lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR); lm.foldLevel = computeFoldLevel(depth, false); @@ -208,7 +209,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::ArrayElementSeparator; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.nodeKind = node.kind; lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; @@ -234,7 +235,7 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = LineKind::Header; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.nodeKind = node.kind; lm.isRootHeader = false; lm.foldHead = true; @@ -288,10 +289,10 @@ void composeParent(ComposeState& state, const NodeTree& tree, lm.lineKind = LineKind::Footer; lm.nodeKind = node.kind; lm.isRootHeader = isRootHeader; // root footer: flush left (no fold prefix) - lm.offsetText.clear(); lm.foldLevel = computeFoldLevel(depth, false); lm.markerMask = 0; int sz = tree.structSpan(node.id, &state.childMap); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits); state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm); } @@ -322,7 +323,7 @@ void composeNode(ComposeState& state, const NodeTree& tree, lm.nodeId = node.id; lm.depth = depth; lm.lineKind = node.collapsed ? LineKind::Field : LineKind::Header; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits); lm.nodeKind = node.kind; lm.foldHead = true; lm.foldCollapsed = node.collapsed; @@ -411,6 +412,19 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR for (int i = 0; i < tree.nodes.size(); i++) state.absOffsets[i] = tree.computeOffset(i); + // Compute hex digit tier from max absolute address + { + uint64_t maxAddr = tree.baseAddress; + for (int i = 0; i < tree.nodes.size(); i++) { + uint64_t addr = tree.baseAddress + (uint64_t)state.absOffsets[i]; + if (addr > maxAddr) maxAddr = addr; + } + if (maxAddr <= 0xFFFFULL) state.offsetHexDigits = 4; + else if (maxAddr <= 0xFFFFFFFFULL) state.offsetHexDigits = 8; + else if (maxAddr <= 0xFFFFFFFFFFFFULL) state.offsetHexDigits = 12; + else state.offsetHexDigits = 16; + } + // Helper: compute the display type string for a node (for width calculation) auto nodeTypeName = [&](const Node& n) -> QString { if (n.kind == NodeKind::Array) @@ -491,7 +505,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR lm.lineKind = LineKind::CommandRow; lm.foldLevel = SC_FOLDLEVELBASE; lm.foldHead = false; - lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false); + lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits); lm.markerMask = 0; lm.effectiveTypeW = state.typeW; lm.effectiveNameW = state.nameW; @@ -510,7 +524,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR composeNode(state, tree, prov, idx, 0); } - return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW} }; + return { state.text, state.meta, LayoutInfo{state.typeW, state.nameW, state.offsetHexDigits} }; } QSet NodeTree::normalizePreferAncestors(const QSet& ids) const { diff --git a/src/controller.cpp b/src/controller.cpp index 9188eb7..a7af92e 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1496,7 +1496,7 @@ void RcxController::showTypeSelectorPopup(RcxEditor* editor) { // Get font with zoom QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "Consolas").toString(); + QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont font(fontName, 12); font.setFixedPitch(true); auto* sci = editor->scintilla(); diff --git a/src/core.h b/src/core.h index b471a6e..b1cc40b 100644 --- a/src/core.h +++ b/src/core.h @@ -439,6 +439,7 @@ inline bool isSyntheticLine(const LineMeta& lm) { struct LayoutInfo { int typeW = 14; // Effective type column width (default = kColType) int nameW = 22; // Effective name column width (default = kColName) + int offsetHexDigits = 8; // Hex digits for offset margin (4/8/12/16) }; // ── ComposeResult ── @@ -750,7 +751,7 @@ namespace fmt { uint64_t addr, int depth, int subLine = 0, const QString& comment = {}, int colType = kColType, int colName = kColName, const QString& typeOverride = {}); - QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation); + QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits = 8); QString fmtStructHeader(const Node& node, int depth, bool collapsed, int colType = kColType, int colName = kColName); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); QString fmtArrayHeader(const Node& node, int depth, int viewIdx, bool collapsed, int colType = kColType, int colName = kColName); diff --git a/src/editor.cpp b/src/editor.cpp index 717dd40..1f3821c 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -32,7 +32,7 @@ static constexpr int IND_DATA_CHANGED = 13; // Amber text for changed data value static constexpr int IND_CLASS_NAME = 14; // Teal text for root class name static constexpr int IND_HINT_GREEN = 15; // Green text for hint/comment text -static QString g_fontName = "Consolas"; +static QString g_fontName = "JetBrains Mono"; static QFont editorFont() { QFont f(g_fontName, 12); @@ -139,6 +139,10 @@ void RcxEditor::setupScintilla() { m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)0, (long)0); + // Auto-size horizontal scrollbar to actual content width (default is fixed 2000px) + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTHTRACKING, 1); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, 1); + // Editable-field indicator - HIDDEN (no visual) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE, IND_EDITABLE, 5 /*INDIC_HIDDEN*/); @@ -240,7 +244,7 @@ void RcxEditor::setupMargins() { // Margin 0: Offset text m_sci->setMarginType(0, QsciScintilla::TextMarginRightJustified); - m_sci->setMarginWidth(0, " 0x00000000 "); + m_sci->setMarginWidth(0, " 00000000 "); // default 8-digit; resized dynamically in applyDocument() m_sci->setMarginsBackgroundColor(kBgMargin); m_sci->setMarginsForegroundColor(kFgMarginDim); m_sci->setMarginSensitivity(0, true); @@ -346,10 +350,18 @@ void RcxEditor::applyDocument(const ComposeResult& result) { m_meta = result.meta; m_layout = result.layout; + // Dynamically resize margin to fit the current hex digit tier + QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); + m_sci->setMarginWidth(0, marginSizer); + m_sci->setReadOnly(false); m_sci->setText(result.text); m_sci->setReadOnly(true); + // Reset scroll width so tracking re-measures from current content + // (tracking never shrinks automatically — only grows) + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSCROLLWIDTH, 1); + // Force full re-lex to fix stale syntax coloring after edits m_sci->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (uintptr_t)0, (long)-1); @@ -370,7 +382,6 @@ void RcxEditor::applyDocument(const ComposeResult& result) { } void RcxEditor::applyMarginText(const QVector& meta) { - // Clear all margin text m_sci->clearMarginText(-1); for (int i = 0; i < meta.size(); i++) { @@ -450,6 +461,21 @@ void RcxEditor::applyHexDimming(const QVector& meta) { if (len > 0) m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len); } + // Dim struct/array braces: entire footer line, trailing "{" on headers + if (meta[i].lineKind == LineKind::Footer) { + long pos, len; lineRangeNoEol(m_sci, i, pos, len); + if (len > 0) + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len); + } else if (meta[i].lineKind == LineKind::Header) { + long endPos = m_sci->SendScintilla(QsciScintillaBase::SCI_GETLINEENDPOSITION, (unsigned long)i); + for (long p = endPos - 1; p >= 0; --p) { + int ch = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETCHARAT, (unsigned long)p); + if (ch == ' ' || ch == '\t') continue; + if (ch == '{') + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, p, 1); + break; + } + } } } @@ -1351,12 +1377,30 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) minCol = m_editState.spanStart + 2; } + // If there's an active selection, collapse it to the left end (Left only, not Backspace) + if (ke->key() == Qt::Key_Left) { + int sL, sC, eL, eC; + m_sci->getSelection(&sL, &sC, &eL, &eC); + if (sL >= 0 && (sL != eL || sC != eC)) { + int leftEnd = qMax(qMin(sC, eC), minCol); + m_sci->setCursorPosition(m_editState.line, leftEnd); + return true; + } + } if (col <= minCol) return true; return false; } case Qt::Key_Right: { int line, col; m_sci->getCursorPosition(&line, &col); + // If there's an active selection, collapse it to the right end first + int sL, sC, eL, eC; + m_sci->getSelection(&sL, &sC, &eL, &eC); + if (sL >= 0 && (sL != eL || sC != eC)) { + int rightEnd = qMin(qMax(sC, eC), editEndCol()); + m_sci->setCursorPosition(m_editState.line, rightEnd); + return true; + } if (col >= editEndCol()) return true; // block past end return false; } @@ -1468,8 +1512,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_editState.editKind = NodeKind::Float; // Store fixed comment column position for value editing + // Use large lineLength so commentCol is always computed (padding added dynamically) if (target == EditTarget::Value) { - ColumnSpan cs = commentSpanFor(*lm, lineText.size(), lm->effectiveTypeW, lm->effectiveNameW); + ColumnSpan cs = commentSpanFor(*lm, 9999, lm->effectiveTypeW, lm->effectiveNameW); m_editState.commentCol = cs.valid ? cs.start : -1; m_editState.lastValidationOk = true; // original value is always valid } else { @@ -1480,6 +1525,26 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETUNDOCOLLECTION, (long)0); m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1); m_sci->setReadOnly(false); + + // For value editing: extend line with trailing spaces for the edit comment area + // (comment padding is no longer baked into every line to avoid unnecessary scroll width) + if (target == EditTarget::Value && m_editState.commentCol >= 0) { + int commentStart = norm.end + 2; + int neededLen = commentStart + kColComment; + int currentLen = (int)lineText.size(); + if (currentLen < neededLen) { + int extend = neededLen - currentLen; + long lineEndPos = posFromCol(m_sci, line, currentLen); + QString pad(extend, ' '); + QByteArray padUtf8 = pad.toUtf8(); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, lineEndPos); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, lineEndPos); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, + (uintptr_t)padUtf8.size(), padUtf8.constData()); + m_editState.linelenAfterReplace += extend; + } + } + // Switch to I-beam for editing (skip for picker-based targets) if (target != EditTarget::Type && target != EditTarget::Source && target != EditTarget::ArrayElementType && target != EditTarget::PointerTarget @@ -2079,8 +2144,10 @@ void RcxEditor::setEditorFont(const QString& fontName) { m_lexer->setFont(f, i); m_sci->setMarginsFont(f); - // Re-apply margin styles with new font + // Re-apply margin styles and width with new font metrics allocateMarginStyles(); + QString marginSizer = QString(" %1 ").arg(QString(m_layout.offsetHexDigits, '0')); + m_sci->setMarginWidth(0, marginSizer); } void RcxEditor::setGlobalFontName(const QString& fontName) { diff --git a/src/fonts/Iosevka-Regular.ttf b/src/fonts/Iosevka-Regular.ttf deleted file mode 100644 index 43947d1..0000000 Binary files a/src/fonts/Iosevka-Regular.ttf and /dev/null differ diff --git a/src/fonts/JetBrainsMono.ttf b/src/fonts/JetBrainsMono.ttf new file mode 100644 index 0000000..711830e Binary files /dev/null and b/src/fonts/JetBrainsMono.ttf differ diff --git a/src/format.cpp b/src/format.cpp index e137f35..69045da 100644 --- a/src/format.cpp +++ b/src/format.cpp @@ -111,9 +111,10 @@ QString indent(int depth) { // ── Offset margin ── -QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation) { +QString fmtOffsetMargin(uint64_t absoluteOffset, bool isContinuation, int hexDigits) { if (isContinuation) return QStringLiteral(" \u00B7 "); - return QString::number(absoluteOffset, 16).toUpper() + QChar(' '); + return QString::number(absoluteOffset, 16).toUpper() + .rightJustified(hexDigits, '0') + QChar(' '); } // ── Struct type name (for width calculation) ── @@ -313,8 +314,8 @@ QString fmtNodeLine(const Node& node, const Provider& prov, // Blank prefix for continuation lines (same width as type+sep+name+sep) const int prefixW = colType + colName + 2 * kSepWidth; - // Comment suffix (padded or empty) - QString cmtSuffix = comment.isEmpty() ? QString(COL_COMMENT, ' ') + // Comment suffix (only present when a comment is provided; no trailing padding) + QString cmtSuffix = comment.isEmpty() ? QString() : fit(comment, COL_COMMENT); // Mat4x4: subLine 0..3 = rows diff --git a/src/main.cpp b/src/main.cpp index 3ac6d4d..64854b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -315,16 +315,16 @@ void MainWindow::createMenus() { auto* actConsolas = fontMenu->addAction("Consolas"); actConsolas->setCheckable(true); actConsolas->setActionGroup(fontGroup); - auto* actIosevka = fontMenu->addAction("Iosevka"); - actIosevka->setCheckable(true); - actIosevka->setActionGroup(fontGroup); + auto* actJetBrains = fontMenu->addAction("JetBrains Mono"); + actJetBrains->setCheckable(true); + actJetBrains->setActionGroup(fontGroup); // Load saved preference QSettings settings("ReclassX", "ReclassX"); - QString savedFont = settings.value("font", "Consolas").toString(); - if (savedFont == "Iosevka") actIosevka->setChecked(true); + QString savedFont = settings.value("font", "JetBrains Mono").toString(); + if (savedFont == "JetBrains Mono") actJetBrains->setChecked(true); else actConsolas->setChecked(true); connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); }); - connect(actIosevka, &QAction::triggered, this, [this]() { setEditorFont("Iosevka"); }); + connect(actJetBrains, &QAction::triggered, this, [this]() { setEditorFont("JetBrains Mono"); }); view->addSeparator(); view->addAction(m_workspaceDock->toggleViewAction()); @@ -353,7 +353,7 @@ void MainWindow::createStatusBar() { statusBar()->setStyleSheet("QStatusBar { background: #252526; color: #858585; }"); QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "Consolas").toString(); + QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont f(fontName, 12); f.setFixedPitch(true); statusBar()->setFont(f); @@ -361,7 +361,7 @@ void MainWindow::createStatusBar() { void MainWindow::applyTabWidgetStyle(QTabWidget* tw) { QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "Consolas").toString(); + QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont tabFont(fontName, 12); tabFont.setFixedPitch(true); tw->tabBar()->setFont(tabFont); @@ -813,7 +813,7 @@ void MainWindow::updateWindowTitle() { void MainWindow::setupRenderedSci(QsciScintilla* sci) { QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "Consolas").toString(); + QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont f(fontName, 12); f.setFixedPitch(true); @@ -1112,7 +1112,7 @@ void MainWindow::createWorkspaceDock() { // Match editor font { QSettings settings("ReclassX", "ReclassX"); - QString fontName = settings.value("font", "Consolas").toString(); + QString fontName = settings.value("font", "JetBrains Mono").toString(); QFont f(fontName, 12); f.setFixedPitch(true); m_workspaceTree->setFont(f); @@ -1299,13 +1299,13 @@ int main(int argc, char* argv[]) { app.setStyle("Fusion"); // Fusion style respects dark palette well // Load embedded fonts - int fontId = QFontDatabase::addApplicationFont(":/fonts/Iosevka-Regular.ttf"); + int fontId = QFontDatabase::addApplicationFont(":/fonts/JetBrainsMono.ttf"); if (fontId == -1) - qWarning("Failed to load embedded Iosevka font"); + qWarning("Failed to load embedded JetBrains Mono font"); // Apply saved font preference before creating any editors { QSettings settings("ReclassX", "ReclassX"); - QString savedFont = settings.value("font", "Consolas").toString(); + QString savedFont = settings.value("font", "JetBrains Mono").toString(); rcx::RcxEditor::setGlobalFontName(savedFont); } diff --git a/src/resources.qrc b/src/resources.qrc index 21d7922..cfa974c 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -4,7 +4,7 @@ icons/chevron-down.png - fonts/Iosevka-Regular.ttf + fonts/JetBrainsMono.ttf vsicons/file.svg diff --git a/tests/test_compose.cpp b/tests/test_compose.cpp index e14b64b..b862917 100644 --- a/tests/test_compose.cpp +++ b/tests/test_compose.cpp @@ -48,8 +48,8 @@ private slots: QCOMPARE(result.meta[2].depth, 1); // Offset text - QCOMPARE(result.meta[1].offsetText, QString("0")); - QCOMPARE(result.meta[2].offsetText, QString("4")); + QCOMPARE(result.meta[1].offsetText, QString("0000 ")); + QCOMPARE(result.meta[2].offsetText, QString("0004 ")); // Line 3 is root footer QCOMPARE(result.meta[3].lineKind, LineKind::Footer); @@ -81,7 +81,7 @@ private slots: // Line 1: single Vec3 line, not continuation, depth 1 QVERIFY(!result.meta[1].isContinuation); - QCOMPARE(result.meta[1].offsetText, QString("0")); + QCOMPARE(result.meta[1].offsetText, QString("0000 ")); QCOMPARE(result.meta[1].depth, 1); QCOMPARE(result.meta[1].nodeKind, NodeKind::Vec3); diff --git a/tests/test_format.cpp b/tests/test_format.cpp index ad870b6..8690f56 100644 --- a/tests/test_format.cpp +++ b/tests/test_format.cpp @@ -39,12 +39,21 @@ private slots: } void testFmtOffsetMargin_primary() { - QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("10")); - QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("0")); + QCOMPARE(fmt::fmtOffsetMargin(0x10, false), QString("00000010 ")); + QCOMPARE(fmt::fmtOffsetMargin(0, false), QString("00000000 ")); } void testFmtOffsetMargin_continuation() { - QCOMPARE(fmt::fmtOffsetMargin(0x10, true), QString(" \u00B7")); + QCOMPARE(fmt::fmtOffsetMargin(0x10, true), QString(" \u00B7 ")); + } + + void testFmtOffsetMargin_kernelAddr() { + QCOMPARE(fmt::fmtOffsetMargin(0xFFFFF80012345678ULL, false, 16), + QString("FFFFF80012345678 ")); + QCOMPARE(fmt::fmtOffsetMargin(0x10, false, 16), + QString("0000000000000010 ")); + QCOMPARE(fmt::fmtOffsetMargin(0x10, false, 4), + QString("0010 ")); } void testFmtStructHeader() { diff --git a/video.mp4 b/video.mp4 deleted file mode 100644 index fc5b2a7..0000000 Binary files a/video.mp4 and /dev/null differ