mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: kill Fusion outline on QScintilla, type inference hints, workspace styling
- Suppress PE_Frame on QsciScintilla in MenuBarStyle to eliminate the 1px dark (#171717) Fusion outline around the editor area - Add --screenshot flag for automated pixel regression testing - Add type inference engine (typeinfer.h) with hex pattern analysis - Show inferred type hints on hex nodes in compose output - Style workspace tree corner/header widgets to match theme - Fix integer overflow in compose.cpp array element addressing - Fix integer overflow in core.h structSpan calculation - Add bounds check on activePaneIdx in controller - Use QPointer for deferred dock lambda safety - Workspace delegate uses icon Normal/Disabled for viewed state
This commit is contained in:
@@ -314,6 +314,11 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_core COMMAND test_core)
|
||||
|
||||
add_executable(test_typeinfer tests/test_typeinfer.cpp)
|
||||
target_include_directories(test_typeinfer PRIVATE src)
|
||||
target_link_libraries(test_typeinfer PRIVATE ${QT}::Core ${QT}::Test)
|
||||
add_test(NAME test_typeinfer COMMAND test_typeinfer)
|
||||
|
||||
add_executable(test_format tests/test_format.cpp src/format.cpp src/addressparser.cpp)
|
||||
target_include_directories(test_format PRIVATE src)
|
||||
target_link_libraries(test_format PRIVATE ${QT}::Core ${QT}::Test)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "core.h"
|
||||
#include "typeinfer.h"
|
||||
#include "addressparser.h"
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -26,6 +27,7 @@ struct ComposeState {
|
||||
bool compactColumns = false; // compact column mode: cap type width, overflow long types
|
||||
bool treeLines = false; // draw Unicode tree connectors in indentation
|
||||
bool braceWrap = false; // opening brace on its own line
|
||||
bool typeHints = false; // show type inference hints on hex nodes
|
||||
QVector<bool> siblingStack; // per-depth: true = more siblings follow at this level
|
||||
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
|
||||
|
||||
@@ -208,6 +210,21 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
|
||||
QString lineText = fmt::fmtNodeLine(node, prov, absAddr, depth, sub,
|
||||
/*comment=*/{}, typeW, nameW, ptrTypeOverride,
|
||||
state.compactColumns);
|
||||
|
||||
// Type inference hint for hex nodes (when enabled)
|
||||
if (state.typeHints && isHexNode(node.kind) && sub == 0) {
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(absAddr, sz)
|
||||
? prov.readBytes(absAddr, sz) : QByteArray(sz, '\0');
|
||||
auto suggestions = inferTypes(
|
||||
reinterpret_cast<const uint8_t*>(b.constData()), sz);
|
||||
if (!suggestions.isEmpty()) {
|
||||
lm.typeHintStart = lineText.size() + 2; // after " " gap
|
||||
lm.typeHint = formatHint(suggestions[0]);
|
||||
lineText += QStringLiteral(" ") + lm.typeHint;
|
||||
}
|
||||
}
|
||||
|
||||
state.emitLine(lineText, std::move(lm));
|
||||
}
|
||||
}
|
||||
@@ -469,7 +486,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
int eNW = state.effectiveNameW(node.id);
|
||||
for (int i = 0; i < node.arrayLen; i++) {
|
||||
state.setTreeSibling(childDepth, i < node.arrayLen - 1);
|
||||
uint64_t elemAddr = absAddr + i * elemSize;
|
||||
uint64_t elemAddr = absAddr + (uint64_t)i * elemSize;
|
||||
|
||||
// Type override: "float[0]", "uint32_t[1]", etc.
|
||||
QString elemTypeStr = fmt::typeNameRaw(node.elementKind)
|
||||
@@ -478,7 +495,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
|
||||
Node elem;
|
||||
elem.kind = node.elementKind;
|
||||
elem.name = QString(); // no name for array elements
|
||||
elem.offset = node.offset + i * elemSize;
|
||||
elem.offset = node.offset + (int)((uint64_t)i * elemSize);
|
||||
elem.parentId = node.id;
|
||||
elem.id = 0;
|
||||
|
||||
@@ -971,11 +988,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
|
||||
} // anonymous namespace
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId,
|
||||
bool compactColumns, bool treeLines, bool braceWrap) {
|
||||
bool compactColumns, bool treeLines, bool braceWrap,
|
||||
bool typeHints) {
|
||||
ComposeState state;
|
||||
state.compactColumns = compactColumns;
|
||||
state.treeLines = treeLines;
|
||||
state.braceWrap = braceWrap;
|
||||
state.typeHints = typeHints;
|
||||
|
||||
// Precompute parent→children map
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
|
||||
@@ -73,8 +73,8 @@ RcxDocument::RcxDocument(QObject* parent)
|
||||
}
|
||||
|
||||
ComposeResult RcxDocument::compose(uint64_t viewRootId, bool compactColumns,
|
||||
bool treeLines, bool braceWrap) const {
|
||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap);
|
||||
bool treeLines, bool braceWrap, bool typeHints) const {
|
||||
return rcx::compose(tree, *provider, viewRootId, compactColumns, treeLines, braceWrap, typeHints);
|
||||
}
|
||||
|
||||
bool RcxDocument::save(const QString& path) {
|
||||
@@ -548,9 +548,9 @@ void RcxController::refresh() {
|
||||
|
||||
// Compose against snapshot provider if active, otherwise real provider
|
||||
if (m_snapshotProv)
|
||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
|
||||
m_lastResult = rcx::compose(m_doc->tree, *m_snapshotProv, m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
|
||||
else
|
||||
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap);
|
||||
m_lastResult = m_doc->compose(m_viewRootId, m_compactColumns, m_treeLines, m_braceWrap, m_typeHints);
|
||||
|
||||
s_composeDoc = nullptr;
|
||||
|
||||
@@ -3313,6 +3313,11 @@ void RcxController::setBraceWrap(bool v) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::setTypeHints(bool v) {
|
||||
m_typeHints = v;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void RcxController::setupAutoRefresh() {
|
||||
int ms = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||
m_refreshTimer = new QTimer(this);
|
||||
|
||||
@@ -41,7 +41,8 @@ public:
|
||||
}
|
||||
|
||||
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
||||
bool treeLines = false, bool braceWrap = false) const;
|
||||
bool treeLines = false, bool braceWrap = false,
|
||||
bool typeHints = false) const;
|
||||
bool save(const QString& path);
|
||||
bool load(const QString& path);
|
||||
void loadData(const QString& binaryPath);
|
||||
@@ -131,6 +132,8 @@ public:
|
||||
void setCompactColumns(bool v);
|
||||
void setTreeLines(bool v);
|
||||
void setBraceWrap(bool v);
|
||||
void setTypeHints(bool v);
|
||||
bool typeHints() const { return m_typeHints; }
|
||||
void resetProvider();
|
||||
|
||||
// MCP bridge accessors
|
||||
@@ -171,6 +174,7 @@ private:
|
||||
bool m_compactColumns = false;
|
||||
bool m_treeLines = false;
|
||||
bool m_braceWrap = false;
|
||||
bool m_typeHints = false;
|
||||
uint64_t m_viewRootId = 0;
|
||||
|
||||
// ── Saved sources for quick-switch ──
|
||||
|
||||
@@ -450,8 +450,8 @@ struct NodeTree {
|
||||
if (c.isStatic) continue; // static fields don't affect struct size
|
||||
int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array)
|
||||
? structSpan(c.id, childMap, visited) : c.byteSize();
|
||||
int end = c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
int64_t end = (int64_t)c.offset + sz;
|
||||
if (end > maxEnd) maxEnd = (int)qMin(end, (int64_t)INT_MAX);
|
||||
}
|
||||
|
||||
// Embedded struct reference: no own children but refId points to a struct definition
|
||||
@@ -625,6 +625,8 @@ struct LineMeta {
|
||||
bool isArrayElement = false; // true for synthesized primitive array element lines
|
||||
bool isMemberLine = false; // true for enum member / bitfield member lines
|
||||
bool isStaticLine = false; // true for static field node lines
|
||||
QString typeHint; // Type inference hint text (e.g. "Float×2") — only set for hex nodes when hints enabled
|
||||
int typeHintStart = -1; // Character offset where hint text starts in line text (-1 = none)
|
||||
};
|
||||
|
||||
inline bool isSyntheticLine(const LineMeta& lm) {
|
||||
@@ -1037,6 +1039,6 @@ namespace fmt {
|
||||
|
||||
ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewRootId = 0,
|
||||
bool compactColumns = false, bool treeLines = false,
|
||||
bool braceWrap = false);
|
||||
bool braceWrap = false, bool typeHints = false);
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
439
src/editor.cpp
439
src/editor.cpp
@@ -32,17 +32,14 @@ namespace rcx {
|
||||
// Forward declaration (defined below, after RcxEditor constructor)
|
||||
static QString getLineText(QsciScintilla* sci, int line);
|
||||
|
||||
// ── Value history popup (styled like TypeSelectorPopup) ──
|
||||
// ── Base class for all hover popups ──
|
||||
|
||||
class ValueHistoryPopup : public QFrame {
|
||||
class HoverPopup : public QFrame {
|
||||
protected:
|
||||
uint64_t m_nodeId = 0;
|
||||
bool m_hasButtons = false;
|
||||
QStringList m_values;
|
||||
QVector<QLabel*> m_labels;
|
||||
std::function<void(const QString&)> m_onSet;
|
||||
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||
public:
|
||||
explicit ValueHistoryPopup(QWidget* parent)
|
||||
explicit HoverPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
@@ -53,9 +50,129 @@ public:
|
||||
}
|
||||
|
||||
uint64_t nodeId() const { return m_nodeId; }
|
||||
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
||||
|
||||
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - lineHeight - 4;
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
virtual void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (m_onMouseMove) m_onMouseMove(e);
|
||||
else QFrame::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
void applyThemePalette(const Theme& t) {
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, t.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, t.text);
|
||||
setPalette(pal);
|
||||
}
|
||||
|
||||
void styleSeparator(const Theme& t) {
|
||||
for (auto* child : findChildren<QFrame*>()) {
|
||||
if (child->frameShape() == QFrame::HLine) {
|
||||
QPalette sp;
|
||||
sp.setColor(QPalette::WindowText, t.border);
|
||||
child->setPalette(sp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── Title + body popup (used for disasm/hex-dump and struct preview) ──
|
||||
|
||||
class TitleBodyPopup : public HoverPopup {
|
||||
QString m_body;
|
||||
QLabel* m_titleLabel = nullptr;
|
||||
QLabel* m_bodyLabel = nullptr;
|
||||
public:
|
||||
explicit TitleBodyPopup(QWidget* parent) : HoverPopup(parent) {
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
vbox->setSpacing(2);
|
||||
|
||||
m_titleLabel = new QLabel;
|
||||
QFont bold = m_titleLabel->font();
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
vbox->addWidget(m_titleLabel);
|
||||
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
sep->setFixedHeight(1);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
m_bodyLabel = new QLabel;
|
||||
m_bodyLabel->setTextFormat(Qt::PlainText);
|
||||
m_bodyLabel->setWordWrap(false);
|
||||
vbox->addWidget(m_bodyLabel);
|
||||
}
|
||||
|
||||
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||
const QFont& font, const QColor& bodyColor) {
|
||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||
return;
|
||||
|
||||
m_nodeId = nodeId;
|
||||
m_body = body;
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
applyThemePalette(theme);
|
||||
|
||||
QFont bold = font;
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
m_titleLabel->setText(title);
|
||||
m_titleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
|
||||
styleSeparator(theme);
|
||||
|
||||
m_bodyLabel->setFont(font);
|
||||
m_bodyLabel->setText(body);
|
||||
m_bodyLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(bodyColor.name()));
|
||||
|
||||
setMaximumWidth(600);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void dismiss() override {
|
||||
HoverPopup::dismiss();
|
||||
m_body.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// ── Value history popup ──
|
||||
|
||||
class ValueHistoryPopup : public HoverPopup {
|
||||
bool m_hasButtons = false;
|
||||
QStringList m_values;
|
||||
QVector<QLabel*> m_labels;
|
||||
std::function<void(const QString&)> m_onSet;
|
||||
public:
|
||||
explicit ValueHistoryPopup(QWidget* parent) : HoverPopup(parent) {}
|
||||
|
||||
bool hasButtons() const { return m_hasButtons; }
|
||||
void setOnSet(std::function<void(const QString&)> fn) { m_onSet = std::move(fn); }
|
||||
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
||||
|
||||
protected:
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (!m_hasButtons && m_onMouseMove)
|
||||
@@ -63,8 +180,8 @@ protected:
|
||||
else
|
||||
QFrame::mouseMoveEvent(e);
|
||||
}
|
||||
public:
|
||||
|
||||
public:
|
||||
void populate(uint64_t nodeId, const ValueHistory& hist, const QFont& font,
|
||||
bool showButtons = false) {
|
||||
QStringList vals;
|
||||
@@ -93,10 +210,7 @@ public:
|
||||
qDeleteAll(findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
applyThemePalette(theme);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
@@ -169,240 +283,13 @@ public:
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - lineHeight - 4;
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
void dismiss() override {
|
||||
HoverPopup::dismiss();
|
||||
m_values.clear();
|
||||
m_labels.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// ── Disassembly / hex-dump hover popup ──
|
||||
|
||||
class DisasmPopup : public QFrame {
|
||||
uint64_t m_nodeId = 0;
|
||||
QString m_body;
|
||||
QLabel* m_titleLabel = nullptr;
|
||||
QLabel* m_bodyLabel = nullptr;
|
||||
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||
public:
|
||||
explicit DisasmPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||
setMouseTracking(true);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
vbox->setSpacing(2);
|
||||
|
||||
m_titleLabel = new QLabel;
|
||||
QFont bold = m_titleLabel->font();
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
vbox->addWidget(m_titleLabel);
|
||||
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
sep->setFixedHeight(1);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
m_bodyLabel = new QLabel;
|
||||
m_bodyLabel->setTextFormat(Qt::PlainText);
|
||||
m_bodyLabel->setWordWrap(false);
|
||||
vbox->addWidget(m_bodyLabel);
|
||||
}
|
||||
|
||||
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
||||
uint64_t nodeId() const { return m_nodeId; }
|
||||
protected:
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (m_onMouseMove) m_onMouseMove(e);
|
||||
else QFrame::mouseMoveEvent(e);
|
||||
}
|
||||
public:
|
||||
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||
const QFont& font) {
|
||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||
return;
|
||||
|
||||
m_nodeId = nodeId;
|
||||
m_body = body;
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
QFont bold = font;
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
m_titleLabel->setText(title);
|
||||
m_titleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
|
||||
// Find and style the separator
|
||||
for (auto* child : findChildren<QFrame*>()) {
|
||||
if (child->frameShape() == QFrame::HLine) {
|
||||
QPalette sp;
|
||||
sp.setColor(QPalette::WindowText, theme.border);
|
||||
child->setPalette(sp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_bodyLabel->setFont(font);
|
||||
m_bodyLabel->setText(body);
|
||||
m_bodyLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
|
||||
|
||||
setMaximumWidth(600);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - lineHeight - 4;
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
m_body.clear();
|
||||
}
|
||||
};
|
||||
|
||||
class StructPreviewPopup : public QFrame {
|
||||
uint64_t m_nodeId = 0;
|
||||
QString m_body;
|
||||
QLabel* m_titleLabel = nullptr;
|
||||
QLabel* m_bodyLabel = nullptr;
|
||||
std::function<void(QMouseEvent*)> m_onMouseMove;
|
||||
public:
|
||||
explicit StructPreviewPopup(QWidget* parent)
|
||||
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
|
||||
{
|
||||
setAttribute(Qt::WA_DeleteOnClose, false);
|
||||
setAttribute(Qt::WA_ShowWithoutActivating, true);
|
||||
setMouseTracking(true);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setAutoFillBackground(true);
|
||||
|
||||
auto* vbox = new QVBoxLayout(this);
|
||||
vbox->setContentsMargins(8, 6, 8, 6);
|
||||
vbox->setSpacing(2);
|
||||
|
||||
m_titleLabel = new QLabel;
|
||||
QFont bold = m_titleLabel->font();
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
vbox->addWidget(m_titleLabel);
|
||||
|
||||
auto* sep = new QFrame;
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setFrameShadow(QFrame::Plain);
|
||||
sep->setFixedHeight(1);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
m_bodyLabel = new QLabel;
|
||||
m_bodyLabel->setTextFormat(Qt::PlainText);
|
||||
m_bodyLabel->setWordWrap(false);
|
||||
vbox->addWidget(m_bodyLabel);
|
||||
}
|
||||
|
||||
uint64_t nodeId() const { return m_nodeId; }
|
||||
void setOnMouseMove(std::function<void(QMouseEvent*)> fn) { m_onMouseMove = std::move(fn); }
|
||||
protected:
|
||||
void mouseMoveEvent(QMouseEvent* e) override {
|
||||
if (m_onMouseMove) m_onMouseMove(e);
|
||||
else QFrame::mouseMoveEvent(e);
|
||||
}
|
||||
public:
|
||||
void populate(uint64_t nodeId, const QString& title, const QString& body,
|
||||
const QFont& font) {
|
||||
if (nodeId == m_nodeId && body == m_body && isVisible())
|
||||
return;
|
||||
|
||||
m_nodeId = nodeId;
|
||||
m_body = body;
|
||||
|
||||
const auto& theme = ThemeManager::instance().current();
|
||||
QPalette pal;
|
||||
pal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
pal.setColor(QPalette::WindowText, theme.text);
|
||||
setPalette(pal);
|
||||
|
||||
QFont bold = font;
|
||||
bold.setBold(true);
|
||||
m_titleLabel->setFont(bold);
|
||||
m_titleLabel->setText(title);
|
||||
m_titleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
|
||||
for (auto* child : findChildren<QFrame*>()) {
|
||||
if (child->frameShape() == QFrame::HLine) {
|
||||
QPalette sp;
|
||||
sp.setColor(QPalette::WindowText, theme.border);
|
||||
child->setPalette(sp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_bodyLabel->setFont(font);
|
||||
m_bodyLabel->setText(body);
|
||||
m_bodyLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.text.name()));
|
||||
|
||||
setMaximumWidth(600);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void showAt(const QPoint& globalPos, int lineHeight = 0) {
|
||||
QSize sz = sizeHint();
|
||||
QRect screen = QApplication::screenAt(globalPos)
|
||||
? QApplication::screenAt(globalPos)->availableGeometry()
|
||||
: QRect(0, 0, 1920, 1080);
|
||||
int x = qMin(globalPos.x(), screen.right() - sz.width());
|
||||
int y = globalPos.y();
|
||||
if (y + sz.height() > screen.bottom())
|
||||
y = globalPos.y() - sz.height() - lineHeight - 4;
|
||||
move(x, y);
|
||||
if (!isVisible()) show();
|
||||
}
|
||||
|
||||
void dismiss() {
|
||||
if (isVisible()) hide();
|
||||
m_nodeId = 0;
|
||||
m_body.clear();
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr int IND_EDITABLE = 8;
|
||||
static constexpr int IND_HEX_DIM = 9;
|
||||
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
|
||||
@@ -415,6 +302,7 @@ static constexpr int IND_LOCAL_OFF = 16; // Dim text for inline local offset
|
||||
static constexpr int IND_HEAT_WARM = 17; // Heatmap level 2 (moderate changes)
|
||||
static constexpr int IND_HEAT_HOT = 18; // Heatmap level 3 (frequent changes)
|
||||
static constexpr int IND_FIND = 19; // Search match highlight
|
||||
static constexpr int IND_TYPE_HINT = 20; // Dimmed type inference hint text on hex nodes
|
||||
|
||||
static QString g_fontName = "JetBrains Mono";
|
||||
|
||||
@@ -724,6 +612,10 @@ void RcxEditor::setupScintilla() {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_LOCAL_OFF, 17 /*INDIC_TEXTFORE*/);
|
||||
|
||||
// Type inference hint — dimmed text appended to hex lines
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_TYPE_HINT, 17 /*INDIC_TEXTFORE*/);
|
||||
|
||||
// Find match highlight — thick underline (avoids box rendering artifacts)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETSTYLE,
|
||||
IND_FIND, 14 /*INDIC_COMPOSITIONTHICK*/);
|
||||
@@ -869,6 +761,8 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
IND_HINT_GREEN, theme.indHintGreen);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_LOCAL_OFF, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_TYPE_HINT, theme.textFaint);
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICSETFORE,
|
||||
IND_FIND, theme.borderFocused);
|
||||
|
||||
@@ -1030,9 +924,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
if (m_hoveredNodeId != 0 && !m_nodeLineIndex.contains(m_hoveredNodeId)) {
|
||||
m_hoveredNodeId = 0;
|
||||
m_hoveredLine = -1;
|
||||
dismissHistoryPopup();
|
||||
if (m_disasmPopup) m_disasmPopup->hide();
|
||||
if (m_structPreviewPopup) m_structPreviewPopup->hide();
|
||||
dismissAllPopups();
|
||||
}
|
||||
|
||||
// Re-apply hover markers (setText() clears all Scintilla markers).
|
||||
@@ -1463,7 +1355,13 @@ void RcxEditor::showFindBar() {
|
||||
|
||||
void RcxEditor::dismissHistoryPopup() {
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
void RcxEditor::dismissAllPopups() {
|
||||
if (m_historyPopup) static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||
}
|
||||
|
||||
void RcxEditor::hideFindBar() {
|
||||
@@ -2503,10 +2401,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
|
||||
m_hoveredLine = -1;
|
||||
applyHoverHighlight();
|
||||
// Dismiss hover popups so they get recreated with Set buttons once edit starts
|
||||
if (m_historyPopup)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_structPreviewPopup)
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
dismissAllPopups();
|
||||
// Clear editable-token color hints (de-emphasize non-active tokens)
|
||||
clearIndicatorLine(IND_EDITABLE, m_hintLine);
|
||||
m_hintLine = -1;
|
||||
@@ -3109,25 +3004,19 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
}
|
||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
// Always dismiss disasm/preview popups during inline editing
|
||||
if (m_disasmPopup && m_disasmPopup->isVisible())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
if (m_disasmPopup) static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup) static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// Mouse left viewport - set Arrow, dismiss popups
|
||||
// (but not during applyDocument — the Leave is synthetic from setText)
|
||||
if (!m_hoverInside) {
|
||||
if (m_historyPopup && !m_applyingDocument)
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
if (m_disasmPopup && !m_applyingDocument)
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
if (m_structPreviewPopup && !m_applyingDocument)
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
if (!m_applyingDocument)
|
||||
dismissAllPopups();
|
||||
m_sci->viewport()->setCursor(Qt::ArrowCursor);
|
||||
return;
|
||||
}
|
||||
@@ -3283,7 +3172,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
}
|
||||
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
|
||||
@@ -3348,8 +3237,8 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
if (!body.isEmpty()) {
|
||||
if (!m_disasmPopup) {
|
||||
m_disasmPopup = new DisasmPopup(this);
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
m_disasmPopup = new TitleBodyPopup(this);
|
||||
static_cast<TitleBodyPopup*>(m_disasmPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
QPoint gp = e->globalPosition().toPoint();
|
||||
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
||||
m_lastHoverPos = vp;
|
||||
@@ -3367,10 +3256,11 @@ void RcxEditor::applyHoverCursor() {
|
||||
applyHoverCursor();
|
||||
});
|
||||
}
|
||||
auto* popup = static_cast<DisasmPopup*>(
|
||||
auto* popup = static_cast<TitleBodyPopup*>(
|
||||
m_disasmPopup);
|
||||
popup->populate(lm.nodeId, title, body,
|
||||
editorFont());
|
||||
editorFont(),
|
||||
ThemeManager::instance().current().syntaxNumber);
|
||||
long linePos = m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
@@ -3391,7 +3281,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
showDisasm = true;
|
||||
// Dismiss value history popup to avoid fighting
|
||||
if (m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3400,7 +3290,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
}
|
||||
if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible())
|
||||
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_disasmPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Struct preview popup for collapsed typed pointers
|
||||
@@ -3435,8 +3325,8 @@ void RcxEditor::applyHoverCursor() {
|
||||
}
|
||||
if (!body.isEmpty()) {
|
||||
if (!m_structPreviewPopup) {
|
||||
m_structPreviewPopup = new StructPreviewPopup(this);
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
m_structPreviewPopup = new TitleBodyPopup(this);
|
||||
static_cast<TitleBodyPopup*>(m_structPreviewPopup)->setOnMouseMove([this](QMouseEvent* e) {
|
||||
QPoint gp = e->globalPosition().toPoint();
|
||||
QPoint vp = m_sci->viewport()->mapFromGlobal(gp);
|
||||
m_lastHoverPos = vp;
|
||||
@@ -3454,9 +3344,10 @@ void RcxEditor::applyHoverCursor() {
|
||||
applyHoverCursor();
|
||||
});
|
||||
}
|
||||
auto* popup = static_cast<StructPreviewPopup*>(m_structPreviewPopup);
|
||||
auto* popup = static_cast<TitleBodyPopup*>(m_structPreviewPopup);
|
||||
popup->populate(lm.nodeId,
|
||||
lm.pointerTargetName, body, editorFont());
|
||||
lm.pointerTargetName, body, editorFont(),
|
||||
ThemeManager::instance().current().text);
|
||||
long linePos = m_sci->SendScintilla(
|
||||
QsciScintillaBase::SCI_POSITIONFROMLINE,
|
||||
(unsigned long)h.line);
|
||||
@@ -3475,14 +3366,14 @@ void RcxEditor::applyHoverCursor() {
|
||||
popup->showAt(anchor, lh);
|
||||
showPreview = true;
|
||||
if (m_historyPopup && m_historyPopup->isVisible())
|
||||
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_historyPopup)->dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showPreview && m_structPreviewPopup && m_structPreviewPopup->isVisible())
|
||||
static_cast<StructPreviewPopup*>(m_structPreviewPopup)->dismiss();
|
||||
static_cast<HoverPopup*>(m_structPreviewPopup)->dismiss();
|
||||
}
|
||||
|
||||
// Determine cursor shape based on interaction type
|
||||
|
||||
@@ -37,6 +37,7 @@ public:
|
||||
void scrollToNodeId(uint64_t nodeId);
|
||||
void showFindBar();
|
||||
void dismissHistoryPopup();
|
||||
void dismissAllPopups();
|
||||
|
||||
// ── Column span computation ──
|
||||
static ColumnSpan typeSpan(const LineMeta& lm, int typeW = kColType);
|
||||
@@ -155,8 +156,8 @@ private:
|
||||
// ── Value history ref (owned by controller) ──
|
||||
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
|
||||
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // StructPreviewPopup (file-local class in editor.cpp)
|
||||
QWidget* m_disasmPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
QWidget* m_structPreviewPopup = nullptr; // TitleBodyPopup (file-local class in editor.cpp)
|
||||
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
439
src/main.cpp
439
src/main.cpp
@@ -299,6 +299,9 @@ public:
|
||||
// Kill the status bar item frame and panel border
|
||||
if (elem == PE_FrameStatusBarItem || elem == PE_PanelStatusBar)
|
||||
return;
|
||||
// Kill Fusion's frame outline on QScintilla (window.darker(140) = ~#171717)
|
||||
if (elem == PE_Frame && w && w->inherits("QsciScintilla"))
|
||||
return;
|
||||
// Transparent menu bar background (no CSS needed)
|
||||
if (elem == PE_PanelMenuBar)
|
||||
return;
|
||||
@@ -835,6 +838,15 @@ void MainWindow::createMenus() {
|
||||
pane.editor->setRelativeOffsets(checked);
|
||||
});
|
||||
|
||||
auto* actTypeHints = view->addAction("Type &Hints");
|
||||
actTypeHints->setCheckable(true);
|
||||
actTypeHints->setChecked(settings.value("typeHints", false).toBool());
|
||||
connect(actTypeHints, &QAction::triggered, this, [this](bool checked) {
|
||||
QSettings("Reclass", "Reclass").setValue("typeHints", checked);
|
||||
for (auto& tab : m_tabs)
|
||||
tab.ctrl->setTypeHints(checked);
|
||||
});
|
||||
|
||||
view->addSeparator();
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
{
|
||||
@@ -1233,6 +1245,7 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString editorFont = s.value("font", "JetBrains Mono").toString();
|
||||
pane.tabWidget->setStyleSheet(QStringLiteral(
|
||||
"QTabWidget::pane { border: none; }"
|
||||
"QTabBar { border: none; }"
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;"
|
||||
@@ -1378,7 +1391,8 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
||||
|
||||
// Sync status bar buttons if this is the active pane
|
||||
auto* tab = activeTab();
|
||||
if (tab && &tab->panes[tab->activePaneIdx] == p)
|
||||
if (tab && tab->activePaneIdx >= 0 && tab->activePaneIdx < tab->panes.size()
|
||||
&& &tab->panes[tab->activePaneIdx] == p)
|
||||
syncViewButtons(p->viewMode);
|
||||
|
||||
if (index == 1) {
|
||||
@@ -1599,6 +1613,7 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) {
|
||||
ctrl->setCompactColumns(QSettings("Reclass", "Reclass").value("compactColumns", true).toBool());
|
||||
ctrl->setTreeLines(QSettings("Reclass", "Reclass").value("treeLines", true).toBool());
|
||||
ctrl->setBraceWrap(QSettings("Reclass", "Reclass").value("braceWrap", false).toBool());
|
||||
ctrl->setTypeHints(QSettings("Reclass", "Reclass").value("typeHints", false).toBool());
|
||||
|
||||
// Give every controller the shared document list for cross-tab type visibility
|
||||
ctrl->setProjectDocuments(&m_allDocs);
|
||||
@@ -2338,9 +2353,9 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
// QWidget default colors are required because having ANY stylesheet on QMainWindow
|
||||
// switches children from palette-based to CSS-based rendering.
|
||||
setStyleSheet(QStringLiteral(
|
||||
"QMainWindow::separator { width: 1px; height: 1px; background: transparent; }"
|
||||
"QMainWindow::separator { width: 1px; height: 1px; background: %1; }"
|
||||
"QDockWidget { border: none; }"
|
||||
"QDockWidget > QWidget { border: none; }"));
|
||||
"QDockWidget > QWidget { border: none; }").arg(theme.background.name()));
|
||||
|
||||
// Custom title bar — applied AFTER setStyleSheet() because the MainWindow
|
||||
// stylesheet re-resolves descendant palettes and would reset the QMenuBar palette.
|
||||
@@ -2384,6 +2399,7 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
{
|
||||
QString editorFont = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
|
||||
QString paneTabStyle = QStringLiteral(
|
||||
"QTabWidget::pane { border: none; }"
|
||||
"QTabBar { border: none; }"
|
||||
"QTabBar::tab {"
|
||||
" background: %1; color: %2; padding: 0px 16px; border: none; border-radius: 0px; height: 24px;"
|
||||
@@ -2432,7 +2448,10 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||
"QTreeView { background: %1; border: none; }"
|
||||
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }")
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }"
|
||||
"QAbstractScrollArea::corner { background: %1; border: none; }"
|
||||
"QHeaderView { background: %1; border: none; }"
|
||||
"QHeaderView::section { background: %1; border: none; }")
|
||||
.arg(theme.background.name()));
|
||||
m_workspaceTree->viewport()->update();
|
||||
}
|
||||
@@ -2446,12 +2465,10 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
theme.hover.name()));
|
||||
}
|
||||
|
||||
// Dock titlebar: restyle via palette + close button
|
||||
if (m_dockTitleLabel) {
|
||||
QPalette lp = m_dockTitleLabel->palette();
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_dockTitleLabel->setPalette(lp);
|
||||
}
|
||||
// Dock titlebar: restyle via stylesheet + close button
|
||||
if (m_dockTitleLabel)
|
||||
m_dockTitleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
if (auto* titleBar = m_workspaceDock ? m_workspaceDock->titleBarWidget() : nullptr) {
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
@@ -2466,16 +2483,14 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
m_dockGrip->setGripColor(theme.textFaint);
|
||||
if (m_workspaceDock)
|
||||
m_workspaceDock->setStyleSheet(QStringLiteral(
|
||||
"QDockWidget { border: 1px solid %1; }").arg(theme.border.name()));
|
||||
"QDockWidget { border: 1px solid %1; border-right: none; }").arg(theme.border.name()));
|
||||
|
||||
// Scanner dock
|
||||
if (m_scannerPanel)
|
||||
m_scannerPanel->applyTheme(theme);
|
||||
if (m_scanDockTitle) {
|
||||
QPalette lp = m_scanDockTitle->palette();
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_scanDockTitle->setPalette(lp);
|
||||
}
|
||||
if (m_scanDockTitle)
|
||||
m_scanDockTitle->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
if (auto* titleBar = m_scannerDock ? m_scannerDock->titleBarWidget() : nullptr) {
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
@@ -3368,9 +3383,8 @@ void MainWindow::createWorkspaceDock() {
|
||||
|
||||
m_dockTitleLabel = new QLabel("Project", titleBar);
|
||||
{
|
||||
QPalette lp = m_dockTitleLabel->palette();
|
||||
lp.setColor(QPalette::WindowText, t.textDim);
|
||||
m_dockTitleLabel->setPalette(lp);
|
||||
m_dockTitleLabel->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(t.textDim.name()));
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||
f.setFixedPitch(true);
|
||||
@@ -3381,13 +3395,14 @@ void MainWindow::createWorkspaceDock() {
|
||||
layout->addStretch();
|
||||
|
||||
m_dockCloseBtn = new QToolButton(titleBar);
|
||||
m_dockCloseBtn->setText(QStringLiteral("\u2715"));
|
||||
m_dockCloseBtn->setIcon(QIcon(QStringLiteral(":/vsicons/close.svg")));
|
||||
m_dockCloseBtn->setIconSize(QSize(14, 14));
|
||||
m_dockCloseBtn->setAutoRaise(true);
|
||||
m_dockCloseBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_dockCloseBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(t.textDim.name(), t.indHoverSpan.name()));
|
||||
"QToolButton { border: none; padding: 0px 4px; }"
|
||||
"QToolButton:hover { background: %1; }")
|
||||
.arg(t.hover.name()));
|
||||
connect(m_dockCloseBtn, &QToolButton::clicked, m_workspaceDock, &QDockWidget::close);
|
||||
layout->addWidget(m_dockCloseBtn);
|
||||
|
||||
@@ -3395,10 +3410,11 @@ void MainWindow::createWorkspaceDock() {
|
||||
}
|
||||
|
||||
// Outer border around entire dock (header + search + tree)
|
||||
// background + ::title needed to suppress Fusion outline frame (renders ~#171717)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
m_workspaceDock->setStyleSheet(QStringLiteral(
|
||||
"QDockWidget { border: 1px solid %1; }").arg(t.border.name()));
|
||||
"QDockWidget { border: 1px solid %1; border-right: none; }").arg(t.border.name()));
|
||||
}
|
||||
|
||||
// Container widget: search box + tree view
|
||||
@@ -3472,6 +3488,7 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_workspaceTree->setExpandsOnDoubleClick(false);
|
||||
m_workspaceTree->setMouseTracking(true);
|
||||
m_workspaceTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
{
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QFont f(s.value("font", "JetBrains Mono").toString(), 10);
|
||||
@@ -3501,7 +3518,10 @@ void MainWindow::createWorkspaceDock() {
|
||||
m_workspaceTree->setStyleSheet(QStringLiteral(
|
||||
"QTreeView { background: %1; border: none; }"
|
||||
"QTreeView::branch:has-children:closed { image: url(:/chevron-right.svg); }"
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }")
|
||||
"QTreeView::branch:has-children:open { image: url(:/chevron-down.svg); }"
|
||||
"QAbstractScrollArea::corner { background: %1; border: none; }"
|
||||
"QHeaderView { background: %1; border: none; }"
|
||||
"QHeaderView::section { background: %1; border: none; }")
|
||||
.arg(t.background.name()));
|
||||
}
|
||||
|
||||
@@ -3509,10 +3529,10 @@ void MainWindow::createWorkspaceDock() {
|
||||
|
||||
m_workspaceTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_workspaceTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
QModelIndex index = m_workspaceTree->indexAt(pos);
|
||||
QModelIndex clickedIndex = m_workspaceTree->indexAt(pos);
|
||||
|
||||
// Right-click on empty area → New Class / New Struct / New Enum
|
||||
if (!index.isValid()) {
|
||||
if (!clickedIndex.isValid()) {
|
||||
QMenu menu;
|
||||
auto* actClass = menu.addAction("New Class");
|
||||
auto* actStruct = menu.addAction("New Struct");
|
||||
@@ -3524,85 +3544,245 @@ void MainWindow::createWorkspaceDock() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
if (structId == 0) return;
|
||||
// If right-clicked item is not in current selection, select only it
|
||||
auto* sel = m_workspaceTree->selectionModel();
|
||||
if (!sel->isSelected(clickedIndex))
|
||||
sel->select(clickedIndex,
|
||||
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) return;
|
||||
auto* dock = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||
if (!dock || !m_tabs.contains(dock)) return;
|
||||
// Gather all selected ROOT items (children are not independently actionable)
|
||||
struct SelItem {
|
||||
uint64_t structId;
|
||||
QDockWidget* dock;
|
||||
int nodeIdx;
|
||||
QString keyword;
|
||||
QString typeName;
|
||||
};
|
||||
QVector<SelItem> items;
|
||||
|
||||
auto& tab = m_tabs[dock];
|
||||
int ni = tab.doc->tree.indexOfId(structId);
|
||||
if (ni < 0) return;
|
||||
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
||||
for (const auto& idx : sel->selectedIndexes()) {
|
||||
if (idx.parent().isValid()) continue; // skip children
|
||||
auto idVar = idx.data(Qt::UserRole + 1);
|
||||
uint64_t sid = idVar.isValid() ? idVar.toULongLong() : 0;
|
||||
if (sid == 0) continue;
|
||||
auto subVar = idx.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) continue;
|
||||
auto* dk = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||
if (!dk || !m_tabs.contains(dk)) continue;
|
||||
int ni = m_tabs[dk].doc->tree.indexOfId(sid);
|
||||
if (ni < 0) continue;
|
||||
const auto& nd = m_tabs[dk].doc->tree.nodes[ni];
|
||||
QString tn = nd.structTypeName.isEmpty() ? nd.name : nd.structTypeName;
|
||||
if (tn.isEmpty()) tn = QStringLiteral("(unnamed)");
|
||||
items.append({sid, dk, ni, nd.resolvedClassKeyword(), tn});
|
||||
}
|
||||
if (items.isEmpty()) return;
|
||||
|
||||
QMenu menu;
|
||||
|
||||
// Navigation actions (single selection only)
|
||||
QAction* actOpenCurrent = nullptr;
|
||||
QAction* actOpenNew = nullptr;
|
||||
QAction* actDuplicate = nullptr;
|
||||
if (items.size() == 1) {
|
||||
actOpenCurrent = menu.addAction("Open in Current Tab");
|
||||
actOpenNew = menu.addAction("Open in New Tab");
|
||||
actDuplicate = menu.addAction("Duplicate");
|
||||
menu.addSeparator();
|
||||
}
|
||||
|
||||
// Convert: only for single selection, class↔struct (not enum)
|
||||
QAction* actConvert = nullptr;
|
||||
// class↔struct conversion only (no enum conversion)
|
||||
if (kw == QStringLiteral("class"))
|
||||
actConvert = menu.addAction("Convert to Struct");
|
||||
else if (kw == QStringLiteral("struct"))
|
||||
actConvert = menu.addAction("Convert to Class");
|
||||
auto* actDelete = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete");
|
||||
if (items.size() == 1) {
|
||||
if (items[0].keyword == QStringLiteral("class"))
|
||||
actConvert = menu.addAction("Convert to Struct");
|
||||
else if (items[0].keyword == QStringLiteral("struct"))
|
||||
actConvert = menu.addAction("Convert to Class");
|
||||
}
|
||||
|
||||
// Delete: works for single or multi
|
||||
QString delLabel = items.size() == 1
|
||||
? QStringLiteral("Delete")
|
||||
: QStringLiteral("Delete %1 items").arg(items.size());
|
||||
auto* actDelete = menu.addAction(QIcon(":/vsicons/remove.svg"), delLabel);
|
||||
|
||||
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
|
||||
if (chosen == actDelete) {
|
||||
QString typeName = tab.doc->tree.nodes[ni].structTypeName.isEmpty()
|
||||
? tab.doc->tree.nodes[ni].name
|
||||
: tab.doc->tree.nodes[ni].structTypeName;
|
||||
if (typeName.isEmpty()) typeName = QStringLiteral("(unnamed)");
|
||||
|
||||
// Collect detailed reference info
|
||||
if (chosen == actDelete) {
|
||||
// Collect reference info across all selected items
|
||||
QStringList refDetails;
|
||||
for (const auto& n : tab.doc->tree.nodes) {
|
||||
if (n.refId == structId) {
|
||||
QString ownerName;
|
||||
uint64_t pid = n.parentId;
|
||||
while (pid != 0) {
|
||||
int pi = tab.doc->tree.indexOfId(pid);
|
||||
if (pi < 0) break;
|
||||
if (tab.doc->tree.nodes[pi].parentId == 0) {
|
||||
ownerName = tab.doc->tree.nodes[pi].structTypeName.isEmpty()
|
||||
? tab.doc->tree.nodes[pi].name
|
||||
: tab.doc->tree.nodes[pi].structTypeName;
|
||||
break;
|
||||
QStringList typeNames;
|
||||
for (const auto& item : items) {
|
||||
typeNames << item.typeName;
|
||||
if (!m_tabs.contains(item.dock)) continue;
|
||||
for (const auto& n : m_tabs[item.dock].doc->tree.nodes) {
|
||||
if (n.refId == item.structId) {
|
||||
QString ownerName;
|
||||
uint64_t pid = n.parentId;
|
||||
while (pid != 0) {
|
||||
int pi = m_tabs[item.dock].doc->tree.indexOfId(pid);
|
||||
if (pi < 0) break;
|
||||
if (m_tabs[item.dock].doc->tree.nodes[pi].parentId == 0) {
|
||||
const auto& pn = m_tabs[item.dock].doc->tree.nodes[pi];
|
||||
ownerName = pn.structTypeName.isEmpty()
|
||||
? pn.name : pn.structTypeName;
|
||||
break;
|
||||
}
|
||||
pid = m_tabs[item.dock].doc->tree.nodes[pi].parentId;
|
||||
}
|
||||
pid = tab.doc->tree.nodes[pi].parentId;
|
||||
QString fieldDesc = ownerName.isEmpty()
|
||||
? n.name
|
||||
: QStringLiteral("%1::%2").arg(ownerName, n.name);
|
||||
refDetails << QStringLiteral(" \u2022 %1 (%2)")
|
||||
.arg(fieldDesc, kindToString(n.kind));
|
||||
}
|
||||
QString fieldDesc = ownerName.isEmpty()
|
||||
? n.name
|
||||
: QStringLiteral("%1::%2").arg(ownerName, n.name);
|
||||
refDetails << QStringLiteral(" \u2022 %1 (%2)")
|
||||
.arg(fieldDesc, kindToString(n.kind));
|
||||
}
|
||||
}
|
||||
|
||||
QString msg;
|
||||
if (refDetails.isEmpty()) {
|
||||
msg = QString("Delete '%1'?").arg(typeName);
|
||||
if (items.size() == 1) {
|
||||
msg = refDetails.isEmpty()
|
||||
? QStringLiteral("Delete '%1'?").arg(typeNames[0])
|
||||
: QStringLiteral("Delete '%1'?\n\n"
|
||||
"The following %2 field(s) reference this type "
|
||||
"and will become untyped (void):\n\n%3")
|
||||
.arg(typeNames[0])
|
||||
.arg(refDetails.size())
|
||||
.arg(refDetails.join('\n'));
|
||||
} else {
|
||||
msg = QString("Delete '%1'?\n\n"
|
||||
"The following %2 field(s) reference this type "
|
||||
"and will become untyped (void):\n\n%3")
|
||||
.arg(typeName)
|
||||
.arg(refDetails.size())
|
||||
.arg(refDetails.join('\n'));
|
||||
msg = QStringLiteral("Delete %1 types?\n\n%2")
|
||||
.arg(items.size())
|
||||
.arg(typeNames.join(QStringLiteral(", ")));
|
||||
if (!refDetails.isEmpty())
|
||||
msg += QStringLiteral("\n\n%1 field(s) reference these types "
|
||||
"and will become untyped (void):\n\n%2")
|
||||
.arg(refDetails.size())
|
||||
.arg(refDetails.join('\n'));
|
||||
}
|
||||
|
||||
auto answer = QMessageBox::question(this, "Delete Type", msg,
|
||||
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
|
||||
if (answer != QMessageBox::Yes) return;
|
||||
|
||||
tab.ctrl->deleteRootStruct(structId);
|
||||
// Group deletes by controller for single undo macro per document
|
||||
QHash<RcxController*, QVector<uint64_t>> byCtrl;
|
||||
for (const auto& item : items) {
|
||||
if (!m_tabs.contains(item.dock)) continue;
|
||||
byCtrl[m_tabs[item.dock].ctrl].append(item.structId);
|
||||
}
|
||||
for (auto it = byCtrl.begin(); it != byCtrl.end(); ++it) {
|
||||
auto* ctrl = it.key();
|
||||
const auto& ids = it.value();
|
||||
if (ids.size() == 1) {
|
||||
ctrl->deleteRootStruct(ids[0]);
|
||||
} else {
|
||||
// Wrap multiple deletes in a single undo macro
|
||||
ctrl->document()->undoStack.beginMacro(
|
||||
QStringLiteral("Delete %1 types").arg(ids.size()));
|
||||
for (uint64_t sid : ids)
|
||||
ctrl->deleteRootStruct(sid);
|
||||
ctrl->document()->undoStack.endMacro();
|
||||
}
|
||||
}
|
||||
rebuildWorkspaceModel();
|
||||
} else if (chosen && chosen == actConvert) {
|
||||
QString newKw = kw == QStringLiteral("class")
|
||||
? QStringLiteral("struct") : QStringLiteral("class");
|
||||
QString oldKw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
|
||||
|
||||
} else if (chosen && chosen == actOpenCurrent && items.size() == 1) {
|
||||
// Open in current (active) tab — set viewRootId on active editor
|
||||
const auto& item = items[0];
|
||||
if (!m_tabs.contains(item.dock)) return;
|
||||
RcxDocument* doc = m_tabs[item.dock].doc;
|
||||
doc->tree.nodes[item.nodeIdx].collapsed = false;
|
||||
|
||||
// Use the active tab if it shares the same document, else use owner
|
||||
QDockWidget* targetDock = item.dock;
|
||||
if (m_activeDocDock && m_tabs.contains(m_activeDocDock)
|
||||
&& m_tabs[m_activeDocDock].doc == doc)
|
||||
targetDock = m_activeDocDock;
|
||||
|
||||
auto& tab = m_tabs[targetDock];
|
||||
tab.ctrl->setViewRootId(item.structId);
|
||||
tab.ctrl->refresh();
|
||||
targetDock->raise();
|
||||
targetDock->show();
|
||||
m_activeDocDock = targetDock;
|
||||
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty()
|
||||
? doc->tree.nodes[item.nodeIdx].name
|
||||
: doc->tree.nodes[item.nodeIdx].structTypeName;
|
||||
if (!structName.isEmpty())
|
||||
targetDock->setWindowTitle(structName);
|
||||
|
||||
} else if (chosen && chosen == actOpenNew && items.size() == 1) {
|
||||
// Open in a brand new tab (sharing the same document)
|
||||
const auto& item = items[0];
|
||||
if (!m_tabs.contains(item.dock)) return;
|
||||
RcxDocument* doc = m_tabs[item.dock].doc;
|
||||
doc->tree.nodes[item.nodeIdx].collapsed = false;
|
||||
auto* newDock = createTab(doc);
|
||||
m_tabs[newDock].ctrl->setViewRootId(item.structId);
|
||||
m_tabs[newDock].ctrl->refresh();
|
||||
QString structName = doc->tree.nodes[item.nodeIdx].structTypeName.isEmpty()
|
||||
? doc->tree.nodes[item.nodeIdx].name
|
||||
: doc->tree.nodes[item.nodeIdx].structTypeName;
|
||||
if (!structName.isEmpty())
|
||||
newDock->setWindowTitle(structName);
|
||||
rebuildWorkspaceModel();
|
||||
|
||||
} else if (chosen && chosen == actDuplicate && items.size() == 1) {
|
||||
// Duplicate: deep-copy the struct as a new root with a unique name
|
||||
const auto& item = items[0];
|
||||
if (!m_tabs.contains(item.dock)) return;
|
||||
auto& tab = m_tabs[item.dock];
|
||||
auto& tree = tab.doc->tree;
|
||||
|
||||
// Generate unique name
|
||||
QString baseName = item.typeName + QStringLiteral("_copy");
|
||||
QString newName = baseName;
|
||||
int counter = 1;
|
||||
QSet<QString> existing;
|
||||
for (const auto& n : tree.nodes)
|
||||
if (n.kind == rcx::NodeKind::Struct && !n.structTypeName.isEmpty())
|
||||
existing.insert(n.structTypeName);
|
||||
while (existing.contains(newName))
|
||||
newName = baseName + QString::number(counter++);
|
||||
|
||||
tab.ctrl->setSuppressRefresh(true);
|
||||
tab.doc->undoStack.beginMacro(QStringLiteral("Duplicate ") + item.typeName);
|
||||
|
||||
// Clone root node
|
||||
rcx::Node root = tree.nodes[item.nodeIdx];
|
||||
root.id = tree.reserveId();
|
||||
root.structTypeName = newName;
|
||||
root.name = newName;
|
||||
root.parentId = 0;
|
||||
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
||||
rcx::cmd::ChangeClassKeyword{structId, oldKw, newKw}));
|
||||
rcx::cmd::Insert{root}));
|
||||
|
||||
// Clone children (re-lookup after insert since indices may shift)
|
||||
QVector<int> children = tree.childrenOf(item.structId);
|
||||
for (int ci : children) {
|
||||
rcx::Node child = tree.nodes[ci];
|
||||
child.id = tree.reserveId();
|
||||
child.parentId = root.id;
|
||||
child.refId = 0; // don't copy pointer refs
|
||||
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
||||
rcx::cmd::Insert{child}));
|
||||
}
|
||||
|
||||
tab.doc->undoStack.endMacro();
|
||||
tab.ctrl->setSuppressRefresh(false);
|
||||
tab.ctrl->refresh();
|
||||
rebuildWorkspaceModel();
|
||||
|
||||
} else if (chosen && chosen == actConvert && items.size() == 1) {
|
||||
const auto& item = items[0];
|
||||
if (!m_tabs.contains(item.dock)) return;
|
||||
auto& tab = m_tabs[item.dock];
|
||||
int ni = tab.doc->tree.indexOfId(item.structId);
|
||||
if (ni < 0) return;
|
||||
QString newKw = item.keyword == QStringLiteral("class")
|
||||
? QStringLiteral("struct") : QStringLiteral("class");
|
||||
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
|
||||
rcx::cmd::ChangeClassKeyword{item.structId, item.keyword, newKw}));
|
||||
rebuildWorkspaceModel();
|
||||
}
|
||||
});
|
||||
@@ -3651,9 +3831,10 @@ void MainWindow::createWorkspaceDock() {
|
||||
if (pi >= 0) tree.nodes[pi].collapsed = false;
|
||||
tab.ctrl->setViewRootId(parentId);
|
||||
tab.ctrl->scrollToNodeId(structId);
|
||||
QTimer::singleShot(0, this, [this, ownerDock]() {
|
||||
if (!m_tabs.contains(ownerDock)) return;
|
||||
auto& t = m_tabs[ownerDock];
|
||||
QPointer<QDockWidget> dockRef = ownerDock;
|
||||
QTimer::singleShot(0, this, [this, dockRef]() {
|
||||
if (!dockRef || !m_tabs.contains(dockRef)) return;
|
||||
auto& t = m_tabs[dockRef];
|
||||
if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) {
|
||||
auto& p = t.panes[t.activePaneIdx];
|
||||
if (p.viewMode == VM_Rendered) updateRenderedView(t, p);
|
||||
@@ -3684,6 +3865,59 @@ void MainWindow::createWorkspaceDock() {
|
||||
newDock->setWindowTitle(structName);
|
||||
rebuildWorkspaceModel();
|
||||
});
|
||||
|
||||
// Single-click: peek (raise existing tab / scroll to member) — no new tab creation
|
||||
connect(m_workspaceTree, &QTreeView::clicked, this, [this](const QModelIndex& index) {
|
||||
// Modifier held → user is multi-selecting, don't navigate
|
||||
if (QApplication::keyboardModifiers() & (Qt::ControlModifier | Qt::ShiftModifier))
|
||||
return;
|
||||
|
||||
auto structIdVar = index.data(Qt::UserRole + 1);
|
||||
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
|
||||
if (structId == 0) return;
|
||||
|
||||
auto subVar = index.data(Qt::UserRole);
|
||||
if (!subVar.isValid()) return;
|
||||
auto* ownerDock = static_cast<QDockWidget*>(subVar.value<void*>());
|
||||
if (!ownerDock || !m_tabs.contains(ownerDock)) return;
|
||||
|
||||
RcxDocument* doc = m_tabs[ownerDock].doc;
|
||||
auto& tree = doc->tree;
|
||||
int ni = tree.indexOfId(structId);
|
||||
if (ni < 0) return;
|
||||
|
||||
uint64_t parentId = tree.nodes[ni].parentId;
|
||||
if (parentId != 0) {
|
||||
// Child member: navigate within owner tab, scroll to member
|
||||
ownerDock->raise();
|
||||
ownerDock->show();
|
||||
m_activeDocDock = ownerDock;
|
||||
auto& tab = m_tabs[ownerDock];
|
||||
int pi = tree.indexOfId(parentId);
|
||||
if (pi >= 0) tree.nodes[pi].collapsed = false;
|
||||
tab.ctrl->setViewRootId(parentId);
|
||||
tab.ctrl->scrollToNodeId(structId);
|
||||
QPointer<QDockWidget> dockRef = ownerDock;
|
||||
QTimer::singleShot(0, this, [this, dockRef]() {
|
||||
if (!dockRef || !m_tabs.contains(dockRef)) return;
|
||||
auto& t = m_tabs[dockRef];
|
||||
if (t.activePaneIdx >= 0 && t.activePaneIdx < t.panes.size()) {
|
||||
auto& p = t.panes[t.activePaneIdx];
|
||||
if (p.viewMode == VM_Rendered) updateRenderedView(t, p);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Root item: raise existing tab if one views this struct (peek only)
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) {
|
||||
if (it->doc == doc && it->ctrl->viewRootId() == structId) {
|
||||
it.key()->raise();
|
||||
it.key()->show();
|
||||
m_activeDocDock = it.key();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scanner Dock ──
|
||||
@@ -3717,11 +3951,8 @@ void MainWindow::createScannerDock() {
|
||||
layout->addWidget(m_scanDockGrip);
|
||||
|
||||
m_scanDockTitle = new QLabel("Scanner", titleBar);
|
||||
{
|
||||
QPalette lp = m_scanDockTitle->palette();
|
||||
lp.setColor(QPalette::WindowText, t.textDim);
|
||||
m_scanDockTitle->setPalette(lp);
|
||||
}
|
||||
m_scanDockTitle->setStyleSheet(
|
||||
QStringLiteral("color: %1;").arg(t.textDim.name()));
|
||||
layout->addWidget(m_scanDockTitle);
|
||||
|
||||
layout->addStretch();
|
||||
@@ -3852,6 +4083,16 @@ void MainWindow::rebuildWorkspaceModelNow() {
|
||||
}
|
||||
rcx::syncProjectExplorer(m_workspaceModel, tabs);
|
||||
|
||||
// Mark items that are currently viewed in a tab
|
||||
QSet<uint64_t> viewedIds;
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
|
||||
viewedIds.insert(it->ctrl->viewRootId());
|
||||
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
|
||||
auto* item = m_workspaceModel->item(i);
|
||||
uint64_t id = item->data(Qt::UserRole + 1).toULongLong();
|
||||
item->setData(viewedIds.contains(id), Qt::UserRole + 3);
|
||||
}
|
||||
|
||||
if (m_dockTitleLabel) {
|
||||
int structs = 0, enums = 0;
|
||||
for (int i = 0; i < m_workspaceModel->rowCount(); ++i) {
|
||||
@@ -4202,6 +4443,24 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
window.show();
|
||||
|
||||
// --screenshot <path>: open default project, grab window, save, exit
|
||||
{
|
||||
QStringList args = app.arguments();
|
||||
int ssIdx = args.indexOf("--screenshot");
|
||||
if (ssIdx >= 0 && ssIdx + 1 < args.size()) {
|
||||
QString ssPath = args[ssIdx + 1];
|
||||
QMetaObject::invokeMethod(&window, [&window, ssPath]() {
|
||||
window.project_new();
|
||||
QTimer::singleShot(500, &window, [&window, ssPath]() {
|
||||
QPixmap px = window.grab();
|
||||
px.save(ssPath);
|
||||
qApp->quit();
|
||||
});
|
||||
}, Qt::QueuedConnection);
|
||||
return app.exec();
|
||||
}
|
||||
}
|
||||
|
||||
// Show VS2022-style start page instead of jumping straight to demo
|
||||
QMetaObject::invokeMethod(&window, "showStartPage", Qt::QueuedConnection);
|
||||
|
||||
|
||||
499
src/typeinfer.h
Normal file
499
src/typeinfer.h
Normal file
@@ -0,0 +1,499 @@
|
||||
#pragma once
|
||||
#include <QVector>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "core.h"
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// ── Hints from value history (optional, improves accuracy) ──
|
||||
|
||||
struct InferHints {
|
||||
const uint8_t* minObserved = nullptr; // raw bytes, same len as data
|
||||
const uint8_t* maxObserved = nullptr;
|
||||
bool monotonic = false; // value only increases or only decreases
|
||||
bool neverChanged = false; // identical across all samples
|
||||
int sampleCount = 0; // 0 = no history
|
||||
int ptrSize = 8;
|
||||
};
|
||||
|
||||
// ── Suggestion result ──
|
||||
|
||||
struct TypeSuggestion {
|
||||
QVector<NodeKind> kinds; // size==1: convert, size>1: uniform split
|
||||
int score = 0; // 0-100 feature ratio (passed / checked × 100)
|
||||
int strength = 0; // 0=hidden, 1=weak, 2=moderate, 3=strong
|
||||
};
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
QVector<TypeSuggestion> inferTypes(
|
||||
const uint8_t* data, int len,
|
||||
const InferHints& hints = {},
|
||||
int maxResults = 3);
|
||||
|
||||
// Format top suggestion as short display string (e.g. "Float×2", "Int32", "UTF8")
|
||||
inline QString formatHint(const TypeSuggestion& s) {
|
||||
if (s.kinds.isEmpty()) return {};
|
||||
const char* name = kindMeta(s.kinds[0])->typeName;
|
||||
QString base = (s.kinds.size() == 1)
|
||||
? QString::fromLatin1(name)
|
||||
: QStringLiteral("%1\u00D7%2").arg(QString::fromLatin1(name)).arg(s.kinds.size());
|
||||
if (s.strength <= 2) base += QLatin1Char('?'); // moderate gets ?
|
||||
return base;
|
||||
}
|
||||
|
||||
// ── Implementation (header-only) ──
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline uint32_t loadU32(const uint8_t* p) {
|
||||
uint32_t v; std::memcpy(&v, p, 4); return v;
|
||||
}
|
||||
inline uint64_t loadU64(const uint8_t* p) {
|
||||
uint64_t v; std::memcpy(&v, p, 8); return v;
|
||||
}
|
||||
inline uint16_t loadU16(const uint8_t* p) {
|
||||
uint16_t v; std::memcpy(&v, p, 2); return v;
|
||||
}
|
||||
inline float loadF32(const uint8_t* p) {
|
||||
float v; std::memcpy(&v, p, 4); return v;
|
||||
}
|
||||
inline double loadF64(const uint8_t* p) {
|
||||
double v; std::memcpy(&v, p, 8); return v;
|
||||
}
|
||||
|
||||
inline bool allZero(const uint8_t* p, int n) {
|
||||
for (int i = 0; i < n; ++i) if (p[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline int popcount32(uint32_t v) {
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
return __builtin_popcount(v);
|
||||
#else
|
||||
int c = 0; while (v) { v &= v - 1; ++c; } return c;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool isPrintable(uint8_t c) {
|
||||
return c >= 0x20 && c <= 0x7E;
|
||||
}
|
||||
|
||||
// ── Float feature checker ──
|
||||
// Returns features passed out of features checked (as pair)
|
||||
|
||||
struct FeatureResult { int passed; int checked; };
|
||||
|
||||
inline bool isGoodFloat(uint32_t bits) {
|
||||
uint32_t exp = (bits >> 23) & 0xFF;
|
||||
if (exp == 0xFF) return false; // inf/nan
|
||||
if (exp == 0 && (bits & 0x7FFFFF)) return false; // denormal
|
||||
float f; std::memcpy(&f, &bits, 4);
|
||||
double af = std::fabs((double)f);
|
||||
return f == 0.0f || (af >= 1e-6 && af <= 1e7);
|
||||
}
|
||||
|
||||
inline FeatureResult countFloatFeatures(uint32_t cur,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 4;
|
||||
float f; std::memcpy(&f, &cur, 4);
|
||||
|
||||
// Feature 1: finite
|
||||
passed += std::isfinite((double)f) ? 1 : 0;
|
||||
// Feature 2: non-denormal (exponent > 0 or value is ±0)
|
||||
uint32_t exp = (cur >> 23) & 0xFF;
|
||||
passed += (exp > 0 || (cur & 0x7FFFFFFF) == 0) ? 1 : 0;
|
||||
// Feature 3: reasonable range
|
||||
double af = std::fabs((double)f);
|
||||
passed += (f == 0.0f || (af >= 1e-6 && af <= 1e7)) ? 1 : 0;
|
||||
// Feature 4: has fractional part (not just a reinterpreted integer)
|
||||
float ip; double frac = std::fabs((double)std::modf(f, &ip));
|
||||
passed += (frac > 0.0001) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 4;
|
||||
uint32_t minBits = loadU32(minP), maxBits = loadU32(maxP);
|
||||
// Feature 5-6: min/max are also valid floats
|
||||
passed += isGoodFloat(minBits) ? 1 : 0;
|
||||
passed += isGoodFloat(maxBits) ? 1 : 0;
|
||||
// Feature 7: field changes
|
||||
passed += (minBits != maxBits) ? 1 : 0;
|
||||
// Feature 8: range is game-plausible
|
||||
float fmin, fmax;
|
||||
std::memcpy(&fmin, &minBits, 4);
|
||||
std::memcpy(&fmax, &maxBits, 4);
|
||||
double range = std::fabs((double)fmax - (double)fmin);
|
||||
passed += (range < 1e6) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Integer feature checker ──
|
||||
|
||||
inline FeatureResult countIntFeatures(uint32_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 3;
|
||||
int32_t sv = (int32_t)val;
|
||||
|
||||
// Feature 1: non-zero
|
||||
passed += (val != 0) ? 1 : 0;
|
||||
// Feature 2: small absolute value
|
||||
passed += (val <= 1000000u || (uint32_t)(sv + 1000000) <= 2000000u) ? 1 : 0;
|
||||
// Feature 3: fits int16 range
|
||||
passed += (sv >= -32768 && sv <= 32767) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 3;
|
||||
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
|
||||
// Feature 4: min/max in reasonable range
|
||||
passed += (minV <= 1000000u && maxV <= 1000000u) ? 1 : 0;
|
||||
// Feature 5: monotonic (counter/timer)
|
||||
passed += h.monotonic ? 1 : 0;
|
||||
// Feature 6: field varies
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Flags feature checker ──
|
||||
|
||||
inline FeatureResult countFlagFeatures(uint32_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 2;
|
||||
int pc = popcount32(val);
|
||||
|
||||
// Feature 1: sparse bits (1-3 set)
|
||||
passed += (pc >= 1 && pc <= 3) ? 1 : 0;
|
||||
// Feature 2: not a small sequential integer (flags are usually not 1,2,3...)
|
||||
passed += (val > 256 || (val & (val - 1)) != 0) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 3;
|
||||
uint32_t minV = loadU32(minP), maxV = loadU32(maxP);
|
||||
// Feature 3: XOR of min/max has low popcount (specific bits toggle)
|
||||
passed += (popcount32(minV ^ maxV) <= 4) ? 1 : 0;
|
||||
// Feature 4: field varies
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
// Feature 5: max is superset of min bits
|
||||
passed += ((minV & maxV) == minV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Pointer feature checker ──
|
||||
|
||||
inline FeatureResult countPtrFeatures64(uint64_t val) {
|
||||
int passed = 0, checked = 5;
|
||||
// Feature 1: non-zero and not common sentinel values
|
||||
passed += (val != 0 && val != 0xFFFFFFFFFFFFFFFFULL
|
||||
&& val != 0x00000000FFFFFFFFULL) ? 1 : 0;
|
||||
// Feature 2: canonical 48-bit address (sign-extended from bit 47)
|
||||
passed += (val <= 0x00007FFFFFFFFFFFULL
|
||||
|| val >= 0xFFFF800000000000ULL) ? 1 : 0;
|
||||
// Feature 3: aligned to 8 (heap/vtable allocations)
|
||||
passed += ((val & 7) == 0) ? 1 : 0;
|
||||
// Feature 4: above null guard pages (real addresses >= 64KB)
|
||||
passed += (val >= 0x10000) ? 1 : 0;
|
||||
// Feature 5: has upper 32 bits (real 64-bit address, not a small constant)
|
||||
passed += ((val >> 32) != 0) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
inline FeatureResult countPtrFeatures32(uint32_t val) {
|
||||
int passed = 0, checked = 3;
|
||||
// Feature 1: non-zero and not sentinel
|
||||
passed += (val != 0 && val != 0xFFFFFFFF) ? 1 : 0;
|
||||
// Feature 2: aligned to 4
|
||||
passed += ((val & 3) == 0) ? 1 : 0;
|
||||
// Feature 3: above null guard pages (>= 64KB)
|
||||
passed += (val >= 0x10000) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── String feature checker ──
|
||||
|
||||
inline FeatureResult countStringFeatures(const uint8_t* data, int len) {
|
||||
if (len < 2) return {0, 4};
|
||||
int printable = 0, letters = 0, consecutive = 0, maxConsec = 0;
|
||||
for (int i = 0; i < len; ++i) {
|
||||
if (isPrintable(data[i])) {
|
||||
printable++;
|
||||
consecutive++;
|
||||
maxConsec = std::max(maxConsec, consecutive);
|
||||
if ((data[i] >= 'A' && data[i] <= 'Z') || (data[i] >= 'a' && data[i] <= 'z'))
|
||||
letters++;
|
||||
} else {
|
||||
consecutive = 0;
|
||||
}
|
||||
}
|
||||
double ratio = (double)printable / len;
|
||||
int passed = 0, checked = 4;
|
||||
passed += (maxConsec >= 4) ? 1 : 0;
|
||||
passed += (ratio > 0.75) ? 1 : 0;
|
||||
passed += (letters >= 1) ? 1 : 0;
|
||||
passed += (ratio > 0.90) ? 1 : 0;
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Int16 feature checker ──
|
||||
|
||||
inline FeatureResult countInt16Features(uint16_t val,
|
||||
const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h) {
|
||||
int passed = 0, checked = 2;
|
||||
int16_t sv = (int16_t)val;
|
||||
passed += (val != 0) ? 1 : 0;
|
||||
passed += (sv >= -4096 && sv <= 4096) ? 1 : 0;
|
||||
|
||||
if (h.sampleCount > 0 && minP && maxP) {
|
||||
checked += 2;
|
||||
uint16_t minV = loadU16(minP), maxV = loadU16(maxP);
|
||||
passed += (minV <= 4096 && maxV <= 4096) ? 1 : 0;
|
||||
passed += (minV != maxV) ? 1 : 0;
|
||||
}
|
||||
return {passed, checked};
|
||||
}
|
||||
|
||||
// ── Score from feature result ──
|
||||
|
||||
inline int featureScore(FeatureResult r) {
|
||||
if (r.checked == 0) return 0;
|
||||
return (r.passed * 100) / r.checked;
|
||||
}
|
||||
|
||||
inline int strengthFromScore(int score) {
|
||||
if (score >= 75) return 3;
|
||||
if (score >= 50) return 2;
|
||||
if (score >= 25) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Candidate accumulator ──
|
||||
|
||||
struct Candidate {
|
||||
QVector<NodeKind> kinds;
|
||||
int score;
|
||||
};
|
||||
|
||||
inline void addCandidate(QVector<Candidate>& out, NodeKind k, int score) {
|
||||
if (score >= 25) out.append({{k}, score});
|
||||
}
|
||||
|
||||
inline void addSplitCandidate(QVector<Candidate>& out, NodeKind k, int count, int score) {
|
||||
if (score >= 25) {
|
||||
QVector<NodeKind> kinds(count, k);
|
||||
out.append({std::move(kinds), score});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Try whole-width interpretations ──
|
||||
|
||||
inline void tryWhole8(const uint8_t* data, const InferHints& h, QVector<Candidate>& out) {
|
||||
uint64_t u64 = loadU64(data);
|
||||
|
||||
// Pointer64
|
||||
if (h.ptrSize == 8)
|
||||
addCandidate(out, NodeKind::Pointer64, featureScore(countPtrFeatures64(u64)));
|
||||
|
||||
// Double
|
||||
{
|
||||
double d; std::memcpy(&d, data, 8);
|
||||
uint64_t exp = (u64 >> 52) & 0x7FF;
|
||||
int passed = 0, checked = 3;
|
||||
passed += std::isfinite(d) ? 1 : 0;
|
||||
passed += (exp > 0 || (u64 & 0x7FFFFFFFFFFFFFFFull) == 0) ? 1 : 0;
|
||||
double ad = std::fabs(d);
|
||||
passed += (d == 0.0 || (ad >= 1e-6 && ad <= 1e12)) ? 1 : 0;
|
||||
addCandidate(out, NodeKind::Double, featureScore({passed, checked}));
|
||||
}
|
||||
|
||||
// UTF8
|
||||
addCandidate(out, NodeKind::UTF8, featureScore(countStringFeatures(data, 8)));
|
||||
|
||||
// UInt64 / Int64
|
||||
{
|
||||
int passed = 0, checked = 4;
|
||||
// Feature 1: fits in 32 bits (small constant, not an address)
|
||||
passed += (u64 <= 0xFFFFFFFFull) ? 1 : 0;
|
||||
// Feature 2: upper 32 bits are zero (confirms it's a small value, not a pointer)
|
||||
passed += ((u64 >> 32) == 0) ? 1 : 0;
|
||||
// Feature 3: non-zero
|
||||
passed += (u64 != 0) ? 1 : 0;
|
||||
// Feature 4: monotonic or very small (< 0x10000)
|
||||
passed += (h.monotonic || u64 < 0x10000) ? 1 : 0;
|
||||
addCandidate(out, NodeKind::UInt64, featureScore({passed, checked}));
|
||||
}
|
||||
}
|
||||
|
||||
inline void tryWhole4(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h, QVector<Candidate>& out) {
|
||||
uint32_t u32 = loadU32(data);
|
||||
|
||||
// Float
|
||||
addCandidate(out, NodeKind::Float, featureScore(countFloatFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Int32
|
||||
addCandidate(out, NodeKind::Int32, featureScore(countIntFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// UInt32
|
||||
addCandidate(out, NodeKind::UInt32, featureScore(countIntFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Flags (only if sparse bits)
|
||||
addCandidate(out, NodeKind::UInt32, featureScore(countFlagFeatures(u32, minP, maxP, h)));
|
||||
|
||||
// Pointer32
|
||||
if (h.ptrSize == 4)
|
||||
addCandidate(out, NodeKind::Pointer32, featureScore(countPtrFeatures32(u32)));
|
||||
}
|
||||
|
||||
inline void tryWhole2(const uint8_t* data, const uint8_t* minP, const uint8_t* maxP,
|
||||
const InferHints& h, QVector<Candidate>& out) {
|
||||
uint16_t u16 = loadU16(data);
|
||||
int scoreI = featureScore(countInt16Features(u16, minP, maxP, h));
|
||||
addCandidate(out, NodeKind::Int16, scoreI);
|
||||
addCandidate(out, NodeKind::UInt16, scoreI);
|
||||
}
|
||||
|
||||
inline void tryWhole1(const uint8_t* data, QVector<Candidate>& out) {
|
||||
uint8_t v = data[0];
|
||||
int score = (v == 0 || v == 1) ? 50 : 25;
|
||||
addCandidate(out, NodeKind::UInt8, score);
|
||||
if (v <= 1) addCandidate(out, NodeKind::Bool, 60);
|
||||
}
|
||||
|
||||
// ── Try uniform splits ──
|
||||
|
||||
inline void trySplitUniform(const uint8_t* data, int len,
|
||||
const InferHints& h,
|
||||
QVector<Candidate>& out) {
|
||||
|
||||
// 8 → 2×4
|
||||
if (len == 8) {
|
||||
const uint8_t* minA = h.minObserved;
|
||||
const uint8_t* minB = h.minObserved ? h.minObserved + 4 : nullptr;
|
||||
const uint8_t* maxA = h.maxObserved;
|
||||
const uint8_t* maxB = h.maxObserved ? h.maxObserved + 4 : nullptr;
|
||||
bool zA = allZero(data, 4), zB = allZero(data + 4, 4);
|
||||
|
||||
// Float×2: both halves must be good floats and at least one non-zero
|
||||
if (!zA || !zB) {
|
||||
uint32_t bitsA = loadU32(data), bitsB = loadU32(data + 4);
|
||||
bool fA = zA || isGoodFloat(bitsA);
|
||||
bool fB = zB || isGoodFloat(bitsB);
|
||||
if (fA && fB) {
|
||||
auto rA = zA ? FeatureResult{2, 4} : countFloatFeatures(bitsA, minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{2, 4} : countFloatFeatures(bitsB, minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::Float, 2, score);
|
||||
}
|
||||
}
|
||||
|
||||
// Int32×2: both halves, at least one non-zero
|
||||
if (!zA || !zB) {
|
||||
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::Int32, 2, score);
|
||||
}
|
||||
|
||||
// UInt32×2
|
||||
if (!zA || !zB) {
|
||||
auto rA = zA ? FeatureResult{1, 3} : countIntFeatures(loadU32(data), minA, maxA, h);
|
||||
auto rB = zB ? FeatureResult{1, 3} : countIntFeatures(loadU32(data + 4), minB, maxB, h);
|
||||
int score = std::min(featureScore(rA), featureScore(rB));
|
||||
addSplitCandidate(out, NodeKind::UInt32, 2, score);
|
||||
}
|
||||
}
|
||||
|
||||
// 8 → 4×2 or 4 → 2×2
|
||||
int halfLen = len / 2;
|
||||
if (halfLen == 2) {
|
||||
int minScore = 100;
|
||||
int count = len / 2;
|
||||
bool anyNonZero = false;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t* part = data + i * 2;
|
||||
if (!allZero(part, 2)) anyNonZero = true;
|
||||
const uint8_t* mp = h.minObserved ? h.minObserved + i * 2 : nullptr;
|
||||
const uint8_t* xp = h.maxObserved ? h.maxObserved + i * 2 : nullptr;
|
||||
int s = featureScore(countInt16Features(loadU16(part), mp, xp, h));
|
||||
minScore = std::min(minScore, s);
|
||||
}
|
||||
if (anyNonZero) {
|
||||
addSplitCandidate(out, NodeKind::Int16, count, minScore);
|
||||
addSplitCandidate(out, NodeKind::UInt16, count, minScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Prune and rank ──
|
||||
|
||||
inline QVector<TypeSuggestion> pruneAndRank(QVector<Candidate>& cands, int maxResults) {
|
||||
// Sort descending by score
|
||||
std::sort(cands.begin(), cands.end(), [](const Candidate& a, const Candidate& b) {
|
||||
return a.score > b.score;
|
||||
});
|
||||
|
||||
// Dedup: keep highest-scoring per unique kinds vector
|
||||
QVector<Candidate> deduped;
|
||||
for (const auto& c : cands) {
|
||||
bool dup = false;
|
||||
for (const auto& d : deduped) {
|
||||
if (d.kinds == c.kinds) { dup = true; break; }
|
||||
}
|
||||
if (!dup) deduped.append(c);
|
||||
}
|
||||
|
||||
// Dominance: if top >= 1.5× second, keep only top
|
||||
if (deduped.size() >= 2 && deduped[0].score >= deduped[1].score * 3 / 2)
|
||||
deduped.resize(1);
|
||||
else if (deduped.size() > maxResults)
|
||||
deduped.resize(maxResults);
|
||||
|
||||
QVector<TypeSuggestion> result;
|
||||
result.reserve(deduped.size());
|
||||
for (const auto& c : deduped) {
|
||||
int str = strengthFromScore(c.score);
|
||||
if (str > 0)
|
||||
result.append({c.kinds, c.score, str});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
inline QVector<TypeSuggestion> inferTypes(
|
||||
const uint8_t* data, int len,
|
||||
const InferHints& hints,
|
||||
int maxResults)
|
||||
{
|
||||
using namespace detail;
|
||||
|
||||
if (!data || len <= 0) return {};
|
||||
if (allZero(data, len)) return {}; // NULL → skip entirely
|
||||
|
||||
QVector<Candidate> cands;
|
||||
cands.reserve(12);
|
||||
|
||||
// Whole-width candidates
|
||||
if (len >= 8) tryWhole8(data, hints, cands);
|
||||
if (len == 4) tryWhole4(data, hints.minObserved, hints.maxObserved, hints, cands);
|
||||
if (len == 2) tryWhole2(data, hints.minObserved, hints.maxObserved, hints, cands);
|
||||
if (len == 1) tryWhole1(data, cands);
|
||||
|
||||
// Uniform splits (compete directly with whole-width candidates)
|
||||
if (len >= 4)
|
||||
trySplitUniform(data, len, hints, cands);
|
||||
|
||||
return pruneAndRank(cands, maxResults);
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
@@ -236,12 +236,14 @@ public:
|
||||
|
||||
// Draw icon for top-level items
|
||||
if (!isChild) {
|
||||
bool viewed = index.data(Qt::UserRole + 3).toBool();
|
||||
QVariant iconVar = index.data(Qt::DecorationRole);
|
||||
if (iconVar.isValid()) {
|
||||
QIcon icon = iconVar.value<QIcon>();
|
||||
int iconSz = opt.fontMetrics.height();
|
||||
int iconY = textRect.y() + (textRect.height() - iconSz) / 2;
|
||||
icon.paint(painter, textRect.x(), iconY, iconSz, iconSz);
|
||||
icon.paint(painter, QRect(textRect.x(), iconY, iconSz, iconSz),
|
||||
Qt::AlignCenter, viewed ? QIcon::Normal : QIcon::Disabled);
|
||||
textRect.setLeft(textRect.left() + iconSz + 4);
|
||||
}
|
||||
}
|
||||
|
||||
92
tests/test_pixels.py
Normal file
92
tests/test_pixels.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Pixel boundary test: validates no Fusion outline leak at the workspace→editor seam.
|
||||
|
||||
Usage:
|
||||
python tests/test_pixels.py [screenshot.png]
|
||||
|
||||
If no screenshot given, launches Reclass.exe --screenshot to grab one.
|
||||
Scans for the specific Fusion outline artifact: color (23,23,23) which is
|
||||
window.darker(140) for the VS2022 Dark theme background #1e1e1e.
|
||||
"""
|
||||
import sys, os, subprocess
|
||||
from PIL import Image
|
||||
from collections import defaultdict
|
||||
|
||||
GRAB_PATH = os.path.join("build", "test_grab.png")
|
||||
|
||||
def get_screenshot(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Launching Reclass.exe --screenshot {path}")
|
||||
subprocess.run(["./build/Reclass.exe", "--screenshot", path],
|
||||
timeout=15, check=True)
|
||||
return Image.open(path)
|
||||
|
||||
def scan_for_artifact(img):
|
||||
"""Scan entire image for the Fusion outline color (23,23,23).
|
||||
Also find all near-black pixels (< 28,28,28) that aren't the
|
||||
theme background (30,30,30)."""
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
|
||||
target = (23, 23, 23)
|
||||
bg = (30, 30, 30)
|
||||
|
||||
target_hits = []
|
||||
dark_hits = defaultdict(list) # color → [(x,y), ...]
|
||||
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b = px[x, y][:3]
|
||||
if r == target[0] and g == target[1] and b == target[2]:
|
||||
target_hits.append((x, y))
|
||||
elif r < 28 and g < 28 and b < 28 and (r, g, b) != (0, 0, 0):
|
||||
# Near-black but not pure black (text anti-aliasing) and not bg
|
||||
dark_hits[(r, g, b)].append((x, y))
|
||||
|
||||
return target_hits, dark_hits
|
||||
|
||||
def summarize_region(hits):
|
||||
"""Summarize a list of (x,y) hits."""
|
||||
if not hits:
|
||||
return "none"
|
||||
xs = [p[0] for p in hits]
|
||||
ys = [p[1] for p in hits]
|
||||
return (f"{len(hits)}px x=[{min(xs)}..{max(xs)}] y=[{min(ys)}..{max(ys)}] "
|
||||
f"size={max(xs)-min(xs)+1}x{max(ys)-min(ys)+1}")
|
||||
|
||||
def main():
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else GRAB_PATH
|
||||
img = get_screenshot(path)
|
||||
w, h = img.size
|
||||
print(f"Image: {w}x{h}")
|
||||
|
||||
target_hits, dark_hits = scan_for_artifact(img)
|
||||
|
||||
print(f"\n(23,23,23) Fusion outline pixels: {summarize_region(target_hits)}")
|
||||
|
||||
if dark_hits:
|
||||
print(f"\nOther near-black pixels (< 28,28,28):")
|
||||
for c, positions in sorted(dark_hits.items(), key=lambda t: -len(t[1])):
|
||||
print(f" ({c[0]:3},{c[1]:3},{c[2]:3}): {summarize_region(positions)}")
|
||||
|
||||
if target_hits:
|
||||
# Show row distribution (condensed)
|
||||
rows = defaultdict(list)
|
||||
for x, y in target_hits:
|
||||
rows[y].append(x)
|
||||
print(f"\n(23,23,23) row detail:")
|
||||
for y in sorted(rows.keys()):
|
||||
xs = sorted(rows[y])
|
||||
if len(xs) > 5:
|
||||
print(f" y={y}: {len(xs)}px x=[{xs[0]}..{xs[-1]}]")
|
||||
else:
|
||||
print(f" y={y}: {len(xs)}px x={xs}")
|
||||
|
||||
print(f"\nFAIL: Found {len(target_hits)} Fusion outline pixels (23,23,23)")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("\nPASS: No Fusion outline artifact found")
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
189
tests/test_typeinfer.cpp
Normal file
189
tests/test_typeinfer.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <cstring>
|
||||
#include "typeinfer.h"
|
||||
|
||||
using namespace rcx;
|
||||
|
||||
class TestTypeInfer : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
|
||||
// ── NULL / zero → empty ──
|
||||
|
||||
void nullPtr() {
|
||||
QVERIFY(inferTypes(nullptr, 8).isEmpty());
|
||||
}
|
||||
void zeroLen() {
|
||||
uint8_t d[4] = {};
|
||||
QVERIFY(inferTypes(d, 0).isEmpty());
|
||||
}
|
||||
void allZeros8() {
|
||||
uint8_t d[8] = {};
|
||||
QVERIFY(inferTypes(d, 8).isEmpty());
|
||||
}
|
||||
void allZeros4() {
|
||||
uint8_t d[4] = {};
|
||||
QVERIFY(inferTypes(d, 4).isEmpty());
|
||||
}
|
||||
void allZeros2() {
|
||||
uint8_t d[2] = {};
|
||||
QVERIFY(inferTypes(d, 2).isEmpty());
|
||||
}
|
||||
|
||||
// ── Hex64: float pair ──
|
||||
// {21.0488f, 547.3f} — two clear floats with fractional parts;
|
||||
// whole-width Double/Ptr64 score poorly → Float×2 dominates
|
||||
void hex64_floatPair() {
|
||||
float a = 21.0488f, b = 547.3f;
|
||||
uint8_t d[8];
|
||||
std::memcpy(d, &a, 4);
|
||||
std::memcpy(d + 4, &b, 4);
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
auto& top = r[0];
|
||||
QCOMPARE(top.kinds.size(), 2);
|
||||
QCOMPARE(top.kinds[0], NodeKind::Float);
|
||||
QVERIFY(top.strength >= 3); // strong
|
||||
}
|
||||
|
||||
// ── Hex64: int32 pair ──
|
||||
// {42, 99} — two small integers
|
||||
void hex64_intPair() {
|
||||
int32_t a = 42, b = 99;
|
||||
uint8_t d[8];
|
||||
std::memcpy(d, &a, 4);
|
||||
std::memcpy(d + 4, &b, 4);
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
auto& top = r[0];
|
||||
QVERIFY(top.kinds.size() == 2);
|
||||
QVERIFY(top.kinds[0] == NodeKind::Int32 || top.kinds[0] == NodeKind::UInt32);
|
||||
}
|
||||
|
||||
// ── Hex64: UTF-8 string ──
|
||||
void hex64_utf8() {
|
||||
uint8_t d[8] = {'I', 'C', 'h', 'o', 'o', 's', 'e', 'Y'};
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
// Top should be UTF8 (strong)
|
||||
bool foundUtf8 = false;
|
||||
for (const auto& s : r) {
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::UTF8) {
|
||||
foundUtf8 = true;
|
||||
QVERIFY(s.strength >= 3); // strong
|
||||
}
|
||||
}
|
||||
QVERIFY(foundUtf8);
|
||||
}
|
||||
|
||||
// ── Hex64: pointer-like value ──
|
||||
void hex64_pointer() {
|
||||
// 0x00007FF6A0B01000 — typical Windows user-mode address
|
||||
uint8_t d[8] = {0x00, 0x10, 0xB0, 0xA0, 0xF6, 0x7F, 0x00, 0x00};
|
||||
auto r = inferTypes(d, 8);
|
||||
QVERIFY(!r.isEmpty());
|
||||
bool foundPtr = false;
|
||||
for (const auto& s : r)
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Pointer64)
|
||||
foundPtr = true;
|
||||
QVERIFY(foundPtr);
|
||||
}
|
||||
|
||||
// ── Hex32: clear float ──
|
||||
void hex32_float() {
|
||||
// 21.0488f = 0x41A86600
|
||||
uint8_t d[4] = {0x00, 0x66, 0xA8, 0x41};
|
||||
auto r = inferTypes(d, 4);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QCOMPARE(r[0].kinds.size(), 1);
|
||||
QCOMPARE(r[0].kinds[0], NodeKind::Float);
|
||||
QVERIFY(r[0].strength >= 2);
|
||||
}
|
||||
|
||||
// ── Hex32: small integer with monotonic history ──
|
||||
void hex32_int_monotonic() {
|
||||
// Value: 0x0000BFFC = 49148 (signed: 49148)
|
||||
uint8_t d[4] = {0xFC, 0xBF, 0x00, 0x00};
|
||||
InferHints h;
|
||||
h.monotonic = true;
|
||||
h.sampleCount = 10;
|
||||
uint8_t minB[4] = {0x10, 0x00, 0x00, 0x00}; // 16
|
||||
uint8_t maxB[4] = {0xFC, 0xBF, 0x00, 0x00}; // 49148
|
||||
h.minObserved = minB;
|
||||
h.maxObserved = maxB;
|
||||
auto r = inferTypes(d, 4, h);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QVERIFY(r[0].kinds[0] == NodeKind::Int32 || r[0].kinds[0] == NodeKind::UInt32);
|
||||
QVERIFY(r[0].strength >= 2);
|
||||
}
|
||||
|
||||
// ── Hex16: small unsigned ──
|
||||
void hex16_uint() {
|
||||
uint8_t d[2] = {0x5F, 0x00}; // 95
|
||||
auto r = inferTypes(d, 2);
|
||||
QVERIFY(!r.isEmpty());
|
||||
QVERIFY(r[0].kinds[0] == NodeKind::Int16 || r[0].kinds[0] == NodeKind::UInt16);
|
||||
}
|
||||
|
||||
// ── Hex8: bool-like ──
|
||||
void hex8_bool() {
|
||||
uint8_t d[1] = {1};
|
||||
auto r = inferTypes(d, 1);
|
||||
QVERIFY(!r.isEmpty());
|
||||
bool foundBool = false;
|
||||
for (const auto& s : r)
|
||||
if (s.kinds.size() == 1 && s.kinds[0] == NodeKind::Bool)
|
||||
foundBool = true;
|
||||
QVERIFY(foundBool);
|
||||
}
|
||||
|
||||
// ── formatHint ──
|
||||
void formatHint_single() {
|
||||
TypeSuggestion s;
|
||||
s.kinds = {NodeKind::Float};
|
||||
QCOMPARE(formatHint(s), QStringLiteral("float"));
|
||||
}
|
||||
void formatHint_split() {
|
||||
TypeSuggestion s;
|
||||
s.kinds = {NodeKind::Float, NodeKind::Float};
|
||||
QString h = formatHint(s);
|
||||
QVERIFY(h.contains("float"));
|
||||
QVERIFY(h.contains("2"));
|
||||
}
|
||||
|
||||
// ── Denormal rejection ──
|
||||
void denormalRejected() {
|
||||
// Denormal float: exp=0, mantissa non-zero → 0x00000001
|
||||
uint8_t d[4] = {0x01, 0x00, 0x00, 0x00};
|
||||
auto r = inferTypes(d, 4);
|
||||
// Should NOT suggest Float as top pick
|
||||
if (!r.isEmpty() && r[0].kinds.size() == 1)
|
||||
QVERIFY(r[0].kinds[0] != NodeKind::Float);
|
||||
}
|
||||
|
||||
// ── Benchmark: single call ──
|
||||
void bench_singleCall() {
|
||||
uint8_t d[8] = {0x00, 0x00, 0x80, 0x3F, 0xCD, 0xCC, 0x4C, 0x3E};
|
||||
QBENCHMARK {
|
||||
inferTypes(d, 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Benchmark: 200-node batch (simulates one refresh) ──
|
||||
void bench_batchRefresh() {
|
||||
// Prepare 200 varied byte patterns
|
||||
QVector<std::array<uint8_t, 8>> data(200);
|
||||
for (int i = 0; i < 200; ++i) {
|
||||
uint32_t seed = (uint32_t)(i * 7919 + 1);
|
||||
for (int j = 0; j < 8; ++j)
|
||||
data[i][j] = (uint8_t)((seed >> (j * 3)) ^ (i + j));
|
||||
}
|
||||
QBENCHMARK {
|
||||
for (int i = 0; i < 200; ++i)
|
||||
inferTypes(data[i].data(), 8);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestTypeInfer)
|
||||
#include "test_typeinfer.moc"
|
||||
Reference in New Issue
Block a user