mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Instead of hiding the sentinel tab (which leaked space on macOS), repurpose it as a visible "+" button that creates a new struct tab on click. Compact 32px icon-only tab with pixel-perfect cross drawn via fillRect. Skips context menu and middle-click. Always positioned as the last tab in the group.
229 lines
8.5 KiB
C++
229 lines
8.5 KiB
C++
#pragma once
|
|
#include "core.h"
|
|
#include "editor.h"
|
|
#include "providers/snapshot_provider.h"
|
|
#include <QObject>
|
|
#include <QUndoStack>
|
|
#include <QUndoCommand>
|
|
#include <QTimer>
|
|
#include <QFutureWatcher>
|
|
#include <QPointer>
|
|
#include <memory>
|
|
|
|
namespace rcx {
|
|
|
|
class RcxController;
|
|
class TypeSelectorPopup;
|
|
struct TypeEntry;
|
|
enum class TypePopupMode;
|
|
|
|
// ── Document ──
|
|
|
|
class RcxDocument : public QObject {
|
|
Q_OBJECT
|
|
public:
|
|
explicit RcxDocument(QObject* parent = nullptr);
|
|
|
|
NodeTree tree;
|
|
std::shared_ptr<Provider> provider;
|
|
QUndoStack undoStack;
|
|
QString filePath;
|
|
QString dataPath;
|
|
bool modified = false;
|
|
QHash<NodeKind, QString> typeAliases;
|
|
|
|
QString resolveTypeName(NodeKind kind) const {
|
|
auto it = typeAliases.find(kind);
|
|
if (it != typeAliases.end() && !it.value().isEmpty())
|
|
return it.value();
|
|
auto* m = kindMeta(kind);
|
|
return m ? QString::fromLatin1(m->typeName) : QStringLiteral("???");
|
|
}
|
|
|
|
ComposeResult compose(uint64_t viewRootId = 0, bool compactColumns = false,
|
|
bool treeLines = false, bool braceWrap = false,
|
|
bool typeHints = false,
|
|
SymbolLookupFn symbolLookup = {}) const;
|
|
bool save(const QString& path);
|
|
bool load(const QString& path);
|
|
void loadData(const QString& binaryPath);
|
|
void loadData(const QByteArray& data);
|
|
|
|
signals:
|
|
void documentChanged();
|
|
};
|
|
|
|
// ── Undo command ──
|
|
|
|
class RcxCommand : public QUndoCommand {
|
|
public:
|
|
RcxCommand(RcxController* ctrl, Command cmd);
|
|
void undo() override;
|
|
void redo() override;
|
|
private:
|
|
RcxController* m_ctrl;
|
|
Command m_cmd;
|
|
};
|
|
|
|
// ── Saved source entry ──
|
|
|
|
struct SavedSourceEntry {
|
|
QString kind; // "File" or provider identifier (e.g. "processmemory")
|
|
QString displayName; // filename or process name
|
|
QString filePath; // for File sources
|
|
QString providerTarget; // for plugin providers (e.g. "pid:name")
|
|
uint64_t baseAddress = 0;
|
|
QString baseAddressFormula;
|
|
};
|
|
|
|
// ── Controller ──
|
|
|
|
class RcxController : public QObject {
|
|
Q_OBJECT
|
|
public:
|
|
explicit RcxController(RcxDocument* doc, QWidget* parent = nullptr);
|
|
~RcxController() override;
|
|
|
|
RcxEditor* primaryEditor() const;
|
|
RcxEditor* addSplitEditor(QWidget* parent = nullptr);
|
|
void removeSplitEditor(RcxEditor* editor);
|
|
QList<RcxEditor*> editors() const { return m_editors; }
|
|
|
|
void convertRootKeyword(const QString& newKeyword);
|
|
void changeNodeKind(int nodeIdx, NodeKind newKind);
|
|
void renameNode(int nodeIdx, const QString& newName);
|
|
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
|
|
void insertNodeAbove(int beforeIdx, NodeKind kind, const QString& name);
|
|
void removeNode(int nodeIdx);
|
|
void toggleCollapse(int nodeIdx);
|
|
void materializeRefChildren(int nodeIdx);
|
|
void setNodeValue(int nodeIdx, int subLine, const QString& text,
|
|
bool isAscii = false, uint64_t resolvedAddr = 0);
|
|
void duplicateNode(int nodeIdx);
|
|
void convertToTypedPointer(uint64_t nodeId);
|
|
void splitHexNode(uint64_t nodeId);
|
|
void toggleBitfieldBit(uint64_t nodeId, int memberIdx);
|
|
void editBitfieldValue(uint64_t nodeId, int memberIdx);
|
|
void showContextMenu(RcxEditor* editor, int line, int nodeIdx, int subLine, const QPoint& globalPos);
|
|
void batchRemoveNodes(const QVector<int>& nodeIndices);
|
|
void batchChangeKind(const QVector<int>& nodeIndices, NodeKind newKind);
|
|
void deleteRootStruct(uint64_t structId);
|
|
void groupIntoUnion(const QSet<uint64_t>& nodeIds);
|
|
void dissolveUnion(uint64_t unionId);
|
|
|
|
void applyCommand(const Command& cmd, bool isUndo);
|
|
void refresh();
|
|
void applyTypePopupResult(TypePopupMode mode, int nodeIdx, const TypeEntry& entry, const QString& fullText);
|
|
uint64_t findOrCreateStructByName(const QString& typeName);
|
|
|
|
// Selection
|
|
void handleNodeClick(RcxEditor* source, int line, uint64_t nodeId,
|
|
Qt::KeyboardModifiers mods);
|
|
void clearSelection();
|
|
void applySelectionOverlays();
|
|
QSet<uint64_t> selectedIds() const { return m_selIds; }
|
|
|
|
void setViewRootId(uint64_t id);
|
|
uint64_t viewRootId() const { return m_viewRootId; }
|
|
void scrollToNodeId(uint64_t nodeId);
|
|
|
|
RcxDocument* document() const { return m_doc; }
|
|
void setEditorFont(const QString& fontName);
|
|
void setRefreshInterval(int ms);
|
|
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
|
|
void setSuppressRefresh(bool v) { m_suppressRefresh = v; }
|
|
void attachViaPlugin(const QString& providerIdentifier, const QString& target);
|
|
const QVector<SavedSourceEntry>& savedSources() const { return m_savedSources; }
|
|
int activeSourceIndex() const { return m_activeSourceIdx; }
|
|
void switchSource(int idx) { switchToSavedSource(idx); }
|
|
void clearSources();
|
|
void selectSource(const QString& text);
|
|
void copySavedSources(const QVector<SavedSourceEntry>& sources, int activeIdx);
|
|
|
|
// Value tracking toggle (per-tab, off by default)
|
|
bool trackValues() const { return m_trackValues; }
|
|
void setTrackValues(bool on);
|
|
void resetChangeTracking();
|
|
|
|
// Cross-tab type visibility: point at the project's full document list
|
|
void setProjectDocuments(QVector<RcxDocument*>* docs) { m_projectDocs = docs; }
|
|
|
|
// Test accessors
|
|
const QHash<uint64_t, ValueHistory>& valueHistory() const { return m_valueHistory; }
|
|
const ComposeResult& lastResult() const { return m_lastResult; }
|
|
int dataExtent() const { return computeDataExtent(); }
|
|
|
|
signals:
|
|
void nodeSelected(int nodeIdx);
|
|
void selectionChanged(int count);
|
|
void contextMenuAboutToShow(QMenu* menu, int line);
|
|
void requestOpenProviderTab(const QString& pluginId, const QString& target,
|
|
const QString& title);
|
|
|
|
private:
|
|
RcxDocument* m_doc;
|
|
QList<RcxEditor*> m_editors;
|
|
ComposeResult m_lastResult;
|
|
QSet<uint64_t> m_selIds;
|
|
int m_anchorLine = -1;
|
|
bool m_suppressRefresh = false;
|
|
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 ──
|
|
QVector<SavedSourceEntry> m_savedSources;
|
|
int m_activeSourceIdx = -1;
|
|
|
|
// ── Cached type selector popup (avoids ~350ms cold-start on first show) ──
|
|
QPointer<TypeSelectorPopup> m_cachedPopup;
|
|
int m_typePopupGen = 0; // generation counter for deferred content loading
|
|
|
|
// ── Auto-refresh state ──
|
|
using PageMap = QHash<uint64_t, QByteArray>;
|
|
QTimer* m_refreshTimer = nullptr;
|
|
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
|
|
std::unique_ptr<SnapshotProvider> m_snapshotProv;
|
|
PageMap m_prevPages;
|
|
QSet<int64_t> m_changedOffsets;
|
|
QHash<uint64_t, ValueHistory> m_valueHistory;
|
|
QHash<uint64_t, uint64_t> m_lastValueAddr; // nodeId → last offsetAddr used for value recording
|
|
bool m_trackValues = true;
|
|
int m_valueTrackCooldown = 0; // suppress value recording for N refresh cycles after clear
|
|
uint64_t m_refreshGen = 0;
|
|
uint64_t m_readGen = 0;
|
|
bool m_readInFlight = false;
|
|
|
|
QVector<RcxDocument*>* m_projectDocs = nullptr;
|
|
|
|
void connectEditor(RcxEditor* editor);
|
|
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
|
|
void updateCommandRow();
|
|
void switchToSavedSource(int idx);
|
|
void pushSavedSourcesToEditors();
|
|
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);
|
|
TypeSelectorPopup* ensurePopup(RcxEditor* editor);
|
|
|
|
// ── Auto-refresh methods ──
|
|
void setupAutoRefresh();
|
|
void onRefreshTick();
|
|
void onReadComplete();
|
|
int computeDataExtent() const;
|
|
void resetSnapshot();
|
|
void collectPointerRanges(uint64_t structId, uint64_t memBase,
|
|
int depth, int maxDepth,
|
|
QSet<QPair<uint64_t,uint64_t>>& visited,
|
|
QVector<QPair<uint64_t,int>>& ranges) const;
|
|
};
|
|
|
|
} // namespace rcx
|