diff --git a/src/controller.cpp b/src/controller.cpp index 4aa62ec..f7c1b2d 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1321,6 +1321,10 @@ void RcxController::applyCommand(const Command& command, bool isUndo) { int idx = tree.indexOfId(c.nodeId); if (idx >= 0) tree.nodes[idx].isStatic = isUndo ? c.oldVal : c.newVal; + } else if constexpr (std::is_same_v) { + int idx = tree.indexOfId(c.nodeId); + if (idx >= 0) + tree.nodes[idx].isRelative = isUndo ? c.oldVal : c.newVal; } }, command); @@ -2006,6 +2010,19 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, if (addedQuickConvert) menu.addSeparator(); + // ── Hex byte / ASCII inline editing ── + if (isHexNode(node.kind) && m_doc->provider->isWritable()) { + menu.addAction(icon("edit.svg"), "Edit He&x Bytes", [editor, line]() { + editor->setHexEditPending(true); + editor->beginInlineEdit(EditTarget::Value, line); + }); + menu.addAction(icon("edit.svg"), "Edit &ASCII", [editor, line]() { + editor->setHexEditPending(true); + editor->beginInlineEdit(EditTarget::Name, line); + }); + menu.addSeparator(); + } + // ── Edit Value / Rename / Change Type ── bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array && !isHexNode(node.kind) @@ -2016,9 +2033,11 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, }); } - menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() { - editor->beginInlineEdit(EditTarget::Name, line); - }); + if (!isHexNode(node.kind)) { + menu.addAction(icon("rename.svg"), "Re&name\tF2", [editor, line]() { + editor->beginInlineEdit(EditTarget::Name, line); + }); + } menu.addAction("Change &Type\tT", [editor, line]() { editor->beginInlineEdit(EditTarget::Type, line); @@ -2088,6 +2107,21 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, hasConvert = true; } + // Toggle relative pointer (RVA: target = base + value) + if (node.kind == NodeKind::Pointer64 || node.kind == NodeKind::Pointer32) { + QString label = node.isRelative + ? QStringLiteral("Pointer is Absolute") + : QStringLiteral("Pointer is &Relative (base + value)"); + convertMenu->addAction(label, [this, nodeId]() { + int ni = m_doc->tree.indexOfId(nodeId); + if (ni < 0) return; + const Node& n = m_doc->tree.nodes[ni]; + m_doc->undoStack.push(new RcxCommand(this, + cmd::ToggleRelative{n.id, n.isRelative, !n.isRelative})); + }); + hasConvert = true; + } + // Split hex node into two half-sized hex nodes if (node.kind == NodeKind::Hex64) { convertMenu->addAction("Split to hex32+hex32", [this, nodeId]() { @@ -3462,6 +3496,10 @@ void RcxController::collectPointerRanges( : m_snapshotProv->readU64(ptrAddr); if (ptrVal == 0 || ptrVal == UINT64_MAX) continue; + // Relative pointer (RVA): target = base + value + if (child.isRelative) + ptrVal += memBase; + uint64_t pBase = ptrVal; collectPointerRanges(child.refId, pBase, depth + 1, maxDepth, visited, ranges); diff --git a/src/editor.cpp b/src/editor.cpp index 81c2177..b8e5561 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -515,7 +515,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { connect(m_sci, &QsciScintilla::textChanged, this, [this]() { if (!m_editState.active) return; if (m_updatingComment) return; // Skip queuing during comment update - if (m_editState.target == EditTarget::Value) + if (m_editState.target == EditTarget::Value && !m_editState.hexOverwrite) QTimer::singleShot(0, this, &RcxEditor::validateEditLive); // Autocomplete for static field expressions — show field names as user types @@ -1605,7 +1605,8 @@ RcxEditor::EndEditInfo RcxEditor::endInlineEdit() { // Dismiss any open user list / autocomplete popup m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCCANCEL); // Clear edit comment and error marker before deactivating - if (m_editState.target == EditTarget::Value) { + if (m_editState.target == EditTarget::Value + || (m_editState.hexOverwrite && m_editState.target == EditTarget::Name)) { setEditComment({}); // Clear to spaces m_sci->markerDelete(m_editState.line, M_ERR); } @@ -2341,6 +2342,10 @@ bool RcxEditor::handleNormalKey(QKeyEvent* ke) { // ── Edit mode key handling ── bool RcxEditor::handleEditKey(QKeyEvent* ke) { + // Hex/ASCII overwrite mode: fully custom key handling + if (m_editState.hexOverwrite) + return handleHexEditKey(ke); + // User list is handled via userListActivated signal, not here // SCI_AUTOCACTIVE is for autocomplete, not user lists @@ -2440,6 +2445,219 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { } } +// ── Hex/ASCII overwrite-mode key handling ── + +bool RcxEditor::handleHexEditKey(QKeyEvent* ke) { + const bool isHexMode = (m_editState.target == EditTarget::Value); + // isHexMode = true: editing "00 00 00 00 00 00 00 00" (hex bytes) + // isHexMode = false: editing "........" (ASCII preview) + + int line, col; + m_sci->getCursorPosition(&line, &col); + const int spanStart = m_editState.spanStart; + const int spanEnd = spanStart + m_editState.original.size(); + + // Helper: replace a single character and re-apply hex dimming indicator + // (SCI_REPLACETARGET can clear indicators at the replacement position) + auto replaceCharAt = [this](long pos, char ch) { + QByteArray buf(1, ch); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETSTART, pos); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETTARGETEND, pos + 1); + m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACETARGET, + (uintptr_t)1, buf.constData()); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM); + m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, 1); + }; + + switch (ke->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + commitInlineEdit(); + return true; + case Qt::Key_Escape: + cancelInlineEdit(); + return true; + case Qt::Key_Tab: + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + return true; // block + + case Qt::Key_Home: + m_sci->setCursorPosition(line, spanStart); + return true; + case Qt::Key_End: { + // Last data position (last char of span) + int endCol = spanEnd - 1; + if (endCol < spanStart) endCol = spanStart; + m_sci->setCursorPosition(line, endCol); + return true; + } + + case Qt::Key_Left: { + if (col <= spanStart) return true; + int newCol = col - 1; + // In hex mode, skip over space separators + if (isHexMode) { + QString lineText = getLineText(m_sci, line); + if (newCol >= spanStart && newCol < lineText.size() && lineText[newCol] == ' ') + newCol--; + } + if (newCol < spanStart) newCol = spanStart; + m_sci->setCursorPosition(line, newCol); + return true; + } + + case Qt::Key_Right: { + if (col >= spanEnd - 1) return true; + int newCol = col + 1; + if (isHexMode) { + QString lineText = getLineText(m_sci, line); + if (newCol < spanEnd && newCol < lineText.size() && lineText[newCol] == ' ') + newCol++; + } + if (newCol >= spanEnd) newCol = spanEnd - 1; + m_sci->setCursorPosition(line, newCol); + return true; + } + + case Qt::Key_Backspace: { + if (col <= spanStart) return true; + int prevCol = col - 1; + if (isHexMode) { + QString lineText = getLineText(m_sci, line); + if (prevCol >= spanStart && prevCol < lineText.size() && lineText[prevCol] == ' ') + prevCol--; + } + if (prevCol < spanStart) return true; + // Replace previous char with reset value + long pos = posFromCol(m_sci, line, prevCol); + replaceCharAt(pos, isHexMode ? '0' : '.'); + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos); + return true; + } + + case Qt::Key_Delete: { + if (col >= spanEnd) return true; + // Skip space separators in hex mode + if (isHexMode) { + QString lineText = getLineText(m_sci, line); + if (col < lineText.size() && lineText[col] == ' ') return true; + } + // Reset current char + long pos = posFromCol(m_sci, line, col); + replaceCharAt(pos, isHexMode ? '0' : '.'); + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, pos); + return true; + } + + case Qt::Key_Z: + if (ke->modifiers() & Qt::ControlModifier) + return true; // block Ctrl+Z during hex overwrite + break; + + case Qt::Key_V: + if (ke->modifiers() & Qt::ControlModifier) { + QString clip = QApplication::clipboard()->text(); + clip.remove('\n'); + clip.remove('\r'); + if (!clip.isEmpty()) { + QString lineText = getLineText(m_sci, line); + int writeCol = col; + for (int i = 0; i < clip.size() && writeCol < spanEnd; i++) { + QChar ch = clip[i]; + if (isHexMode) { + // Skip spaces in paste content + if (ch == ' ') continue; + // Skip over space separators in the target + if (writeCol < lineText.size() && lineText[writeCol] == ' ') + writeCol++; + if (writeCol >= spanEnd) break; + // Only accept hex digits + if (!ch.isDigit() && !(ch >= 'a' && ch <= 'f') && !(ch >= 'A' && ch <= 'F')) + continue; + ch = ch.toUpper(); + } else { + // Only accept printable ASCII + if (ch.unicode() < 0x20 || ch.unicode() > 0x7E) continue; + } + long pos = posFromCol(m_sci, line, writeCol); + replaceCharAt(pos, (char)ch.toLatin1()); + writeCol++; + // Re-read after each replace for hex space skip + if (isHexMode) lineText = getLineText(m_sci, line); + } + int finalCol = qMin(writeCol, spanEnd - 1); + // In hex mode, if we landed on a space, advance past it + if (isHexMode) { + lineText = getLineText(m_sci, line); + if (finalCol < spanEnd && finalCol < lineText.size() && lineText[finalCol] == ' ') + finalCol++; + if (finalCol >= spanEnd) finalCol = spanEnd - 1; + } + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, + posFromCol(m_sci, line, finalCol)); + } + return true; + } + break; + + default: + break; + } + + // Character input: overwrite current position and advance + QString text = ke->text(); + if (text.isEmpty() || text[0].unicode() < 0x20) + return true; // consume non-printable (block default Scintilla handling) + + QChar ch = text[0]; + + if (isHexMode) { + // Only accept hex digits + if (!ch.isDigit() && !(ch >= 'a' && ch <= 'f') && !(ch >= 'A' && ch <= 'F')) + return true; + ch = ch.toUpper(); + + // If cursor is on a space, skip to next byte + QString lineText = getLineText(m_sci, line); + int writeCol = col; + if (writeCol < lineText.size() && lineText[writeCol] == ' ') + writeCol++; + if (writeCol >= spanEnd) return true; + + // Overwrite current digit + long pos = posFromCol(m_sci, line, writeCol); + replaceCharAt(pos, (char)ch.toLatin1()); + + // Advance cursor, skip over spaces + int nextCol = writeCol + 1; + lineText = getLineText(m_sci, line); + if (nextCol < spanEnd && nextCol < lineText.size() && lineText[nextCol] == ' ') + nextCol++; + if (nextCol >= spanEnd) nextCol = spanEnd - 1; + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, + posFromCol(m_sci, line, nextCol)); + } else { + // ASCII mode: only printable ASCII + if (ch.unicode() < 0x20 || ch.unicode() > 0x7E) + return true; + if (col >= spanEnd) return true; + + // Overwrite current char + long pos = posFromCol(m_sci, line, col); + replaceCharAt(pos, (char)ch.toLatin1()); + + // Advance cursor + int nextCol = col + 1; + if (nextCol >= spanEnd) nextCol = spanEnd - 1; + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, + posFromCol(m_sci, line, nextCol)); + } + return true; +} + // ── Begin inline edit ── bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { @@ -2490,14 +2708,39 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { (target == EditTarget::BaseAddress || target == EditTarget::Source || target == EditTarget::RootClassType || target == EditTarget::RootClassName))) return false; - // Hex nodes: only Type is editable (ASCII preview + hex bytes are display-only) - // Exception: static field names are always editable (they're function names, not hex labels) - if ((target == EditTarget::Name || target == EditTarget::Value) && isHexNode(lm->nodeKind) && !lm->isStaticLine) + // Hex nodes: only Type is editable via normal flow (double-click, F2, Enter) + // Exception: context-menu-initiated hex/ASCII edits bypass this via m_hexEditPending + bool isHexEdit = m_hexEditPending && isHexNode(lm->nodeKind) && !lm->isStaticLine + && (target == EditTarget::Name || target == EditTarget::Value); + m_hexEditPending = false; + if ((target == EditTarget::Name || target == EditTarget::Value) + && isHexNode(lm->nodeKind) && !lm->isStaticLine && !isHexEdit) return false; QString lineText; NormalizedSpan norm; - if (!resolvedSpanFor(line, target, norm, &lineText)) return false; + + if (isHexEdit) { + // Compute hex spans directly (bypasses resolvedSpanFor which also blocks hex) + lineText = getLineText(m_sci, line); + int typeW = lm->effectiveTypeW; + int nameW = lm->effectiveNameW; + int byteCount = sizeForKind(lm->nodeKind); + if (target == EditTarget::Name) { + // ASCII preview: exactly byteCount chars (no trailing-space trim) + ColumnSpan s = nameSpanFor(*lm, typeW, nameW); + if (!s.valid) return false; + norm = {s.start, s.start + byteCount, true}; + } else { + // Hex bytes: "XX XX XX..." = byteCount*3-1 chars + ColumnSpan s = valueSpanFor(*lm, lineText.size(), typeW, nameW); + if (!s.valid) return false; + int hexWidth = byteCount * 3 - 1; + norm = {s.start, s.start + hexWidth, true}; + } + } else { + if (!resolvedSpanFor(line, target, norm, &lineText)) return false; + } QString trimmed = lineText.mid(norm.start, norm.end - norm.start); @@ -2558,6 +2801,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { m_editState.original = trimmed; m_editState.linelenAfterReplace = lineText.size(); m_editState.editKind = lm->nodeKind; + m_editState.hexOverwrite = isHexEdit; if (isVectorKind(lm->nodeKind)) { m_editState.subLine = vecComponent; m_editState.editKind = NodeKind::Float; @@ -2567,9 +2811,9 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { m_editState.editKind = NodeKind::Float; } - // Store fixed comment column position for value editing + // Store fixed comment column position for value editing (and hex ASCII edits) // Use large lineLength so commentCol is always computed (padding added dynamically) - if (target == EditTarget::Value) { + if (target == EditTarget::Value || (isHexEdit && target == EditTarget::Name)) { 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 @@ -2586,9 +2830,10 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { m_sci->SendScintilla(QsciScintillaBase::SCI_SETCARETWIDTH, 1); m_sci->setReadOnly(false); - // For value editing: extend line with trailing spaces for the edit comment area + // For value/hex 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 || target == EditTarget::BaseAddress) + if ((target == EditTarget::Value || target == EditTarget::BaseAddress + || (isHexEdit && target == EditTarget::Name)) && m_editState.commentCol >= 0) { int commentStart = m_editState.commentCol; int commentWidth = (target == EditTarget::BaseAddress) ? 60 : kColComment; @@ -2636,10 +2881,19 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { } m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd); + // Hex overwrite: place cursor at start, no selection + if (m_editState.hexOverwrite) + m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart); + // Show initial edit hint in comment column - if (target == EditTarget::Value) - setEditComment(QStringLiteral("Enter=Save Esc=Cancel")); - else if (target == EditTarget::BaseAddress) + if (target == EditTarget::Value) { + if (m_editState.hexOverwrite) + setEditComment(QStringLiteral("Hex edit: Enter=Save Esc=Cancel")); + else + setEditComment(QStringLiteral("Enter=Save Esc=Cancel")); + } else if (target == EditTarget::Name && m_editState.hexOverwrite) { + setEditComment(QStringLiteral("ASCII edit: Enter=Save Esc=Cancel")); + } else if (target == EditTarget::BaseAddress) setEditComment(QStringLiteral("e.g. + 0xFF | [0x1000 + 0x10] | 7ff6`1234ABCD")); // Note: Type, ArrayElementType, PointerTarget are handled by TypeSelectorPopup @@ -2665,6 +2919,19 @@ void RcxEditor::clampEditSelection() { if (m_clampingSelection) return; m_clampingSelection = true; + // Hex overwrite: collapse any selection to cursor (no selection allowed) + if (m_editState.hexOverwrite) { + int sL, sC, eL, eC; + m_sci->getSelection(&sL, &sC, &eL, &eC); + if (sL != eL || sC != eC) { + int curLine, curCol; + m_sci->getCursorPosition(&curLine, &curCol); + m_sci->setCursorPosition(m_editState.line, curCol); + } + m_clampingSelection = false; + return; + } + int selStartLine, selStartCol, selEndLine, selEndCol; m_sci->getSelection(&selStartLine, &selStartCol, &selEndLine, &selEndCol); @@ -2710,8 +2977,11 @@ void RcxEditor::commitInlineEdit() { int editedLen = m_editState.original.size() + delta; QString editedText; - if (editedLen > 0) - editedText = lineText.mid(m_editState.spanStart, editedLen).trimmed(); + if (editedLen > 0) { + editedText = lineText.mid(m_editState.spanStart, editedLen); + if (!m_editState.hexOverwrite) + editedText = editedText.trimmed(); + } // For Type edits: if nothing changed, commit original if (m_editState.target == EditTarget::Type && editedText.isEmpty()) diff --git a/src/editor.h b/src/editor.h index 129ec07..7db69b8 100644 --- a/src/editor.h +++ b/src/editor.h @@ -51,6 +51,7 @@ public: bool isEditing() const { return m_editState.active; } bool beginInlineEdit(EditTarget target, int line = -1, int col = -1); void cancelInlineEdit(); + void setHexEditPending(bool v) { m_hexEditPending = v; } void setStaticCompletions(const QStringList& words) { m_staticCompletions = words; } void applySelectionOverlay(const QSet& selIds); @@ -143,6 +144,7 @@ private: NodeKind editKind = NodeKind::Int32; int commentCol = -1; // fixed comment column (stored at edit start) bool lastValidationOk = true; // track state to avoid redundant updates + bool hexOverwrite = false; // true for hex-byte / ASCII-preview fixed-length editing }; InlineEditState m_editState; QStringList m_staticCompletions; // autocomplete words for StaticExpr editing @@ -171,6 +173,9 @@ private: long m_findPos = 0; void hideFindBar(); + // ── Hex inline edit ── + bool m_hexEditPending = false; // set by context menu before calling beginInlineEdit + // ── Reentrancy guards ── bool m_applyingDocument = false; bool m_clampingSelection = false; @@ -195,6 +200,7 @@ private: int editEndCol() const; bool handleNormalKey(QKeyEvent* ke); bool handleEditKey(QKeyEvent* ke); + bool handleHexEditKey(QKeyEvent* ke); void showTypeAutocomplete(); void showSourcePicker(); void showTypeListFiltered(const QString& filter);