mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Compare commits
6 Commits
snapshot-0
...
snapshot-0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efae193520 | ||
|
|
ba1c2f8e5a | ||
|
|
5a0a4d1802 | ||
|
|
030eb34510 | ||
|
|
2939b25895 | ||
|
|
d38cb02fa2 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -2,7 +2,8 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
|
||||
@@ -36,10 +36,35 @@ file(GLOB RAW_PDB_SRCS third_party/raw_pdb/src/*.cpp)
|
||||
add_library(raw_pdb STATIC ${RAW_PDB_SRCS})
|
||||
target_include_directories(raw_pdb PUBLIC third_party/raw_pdb/src)
|
||||
target_compile_features(raw_pdb PRIVATE cxx_std_11)
|
||||
# PDB_CRT.h forward-declares printf/memcmp/etc with __cdecl which conflicts
|
||||
# with non-MSVC compilers (GCC, Clang, MinGW). Force-include a prefix header
|
||||
# that pulls in the real CRT headers and strips __cdecl.
|
||||
if(NOT MSVC)
|
||||
target_compile_options(raw_pdb PUBLIC
|
||||
-include "${CMAKE_CURRENT_SOURCE_DIR}/cmake/raw_pdb_prefix.h")
|
||||
endif()
|
||||
if(WIN32)
|
||||
target_link_libraries(raw_pdb PRIVATE rpcrt4)
|
||||
endif()
|
||||
|
||||
# Fadec — generate decode tables (.inc files) from instrs.txt at configure time
|
||||
find_package(Python3 3.9 REQUIRED)
|
||||
set(FADEC_DIR "${CMAKE_SOURCE_DIR}/third_party/fadec")
|
||||
if(NOT EXISTS "${FADEC_DIR}/fadec-decode-public.inc")
|
||||
message(STATUS "Generating fadec decode tables...")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE} "${FADEC_DIR}/parseinstrs.py" decode
|
||||
"${FADEC_DIR}/instrs.txt"
|
||||
"${FADEC_DIR}/fadec-decode-public.inc"
|
||||
"${FADEC_DIR}/fadec-decode-private.inc"
|
||||
--32 --64
|
||||
RESULT_VARIABLE _fadec_result
|
||||
)
|
||||
if(NOT _fadec_result EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to generate fadec decode tables")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(Reclass
|
||||
src/main.cpp
|
||||
src/editor.h
|
||||
|
||||
29
cmake/raw_pdb_prefix.h
Normal file
29
cmake/raw_pdb_prefix.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// Force-included before every raw_pdb translation unit (and consumers).
|
||||
// PDB_CRT.h forward-declares printf/memcmp/etc with extern "C" __cdecl,
|
||||
// which conflicts with MinGW's CRT headers (C++ linkage, no __cdecl).
|
||||
//
|
||||
// Fix: include the real CRT headers, then include PDB_CRT.h with function
|
||||
// names macro-renamed to harmless dummies. This triggers #pragma once so
|
||||
// no raw_pdb source file ever processes PDB_CRT.h's conflicting declarations.
|
||||
//
|
||||
// Guarded with __cplusplus because PUBLIC propagation applies this to C
|
||||
// sources (fadec) where PDB_CRT.h is irrelevant and <cstdio> doesn't exist.
|
||||
#ifdef __cplusplus
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#undef __cdecl
|
||||
#define __cdecl
|
||||
|
||||
#define printf _pdb_crt_unused_printf
|
||||
#define memcmp _pdb_crt_unused_memcmp
|
||||
#define memcpy _pdb_crt_unused_memcpy
|
||||
#define strlen _pdb_crt_unused_strlen
|
||||
#define strcmp _pdb_crt_unused_strcmp
|
||||
#include "Foundation/PDB_CRT.h"
|
||||
#undef printf
|
||||
#undef memcmp
|
||||
#undef memcpy
|
||||
#undef strlen
|
||||
#undef strcmp
|
||||
#endif
|
||||
@@ -1581,6 +1581,8 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
} else if (commonKind == NodeKind::Hex32) {
|
||||
menu.addAction("Change to uint32_t", [this, collectIndices]() {
|
||||
batchChangeKind(collectIndices(), NodeKind::UInt32); });
|
||||
menu.addAction("Change to float", [this, collectIndices]() {
|
||||
batchChangeKind(collectIndices(), NodeKind::Float); });
|
||||
addedQuickConvert = true;
|
||||
} else if (commonKind == NodeKind::Hex16) {
|
||||
menu.addAction("Change to int16_t", [this, collectIndices]() {
|
||||
@@ -1629,6 +1631,18 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
act->setChecked(m_trackValues);
|
||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||
}
|
||||
{
|
||||
auto* act = menu.addAction("Clear Value History");
|
||||
act->setToolTip(QStringLiteral("Reset change tracking for selected nodes"));
|
||||
connect(act, &QAction::triggered, this, [this, ids]() {
|
||||
for (uint64_t id : ids) {
|
||||
m_valueHistory[id].clear();
|
||||
for (auto& lm : m_lastResult.meta)
|
||||
if (lm.nodeId == id) lm.heatLevel = 0;
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// Check if all selected nodes share the same parent (required for grouping)
|
||||
@@ -1723,6 +1737,10 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::UInt32);
|
||||
});
|
||||
menu.addAction("Change to float", [this, nodeId]() {
|
||||
int ni = m_doc->tree.indexOfId(nodeId);
|
||||
if (ni >= 0) changeNodeKind(ni, NodeKind::Float);
|
||||
});
|
||||
addedQuickConvert = true;
|
||||
} else if (node.kind == NodeKind::Hex16) {
|
||||
menu.addAction("Change to int16_t", [this, nodeId]() {
|
||||
@@ -1812,6 +1830,17 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
|
||||
act->setChecked(m_trackValues);
|
||||
connect(act, &QAction::toggled, this, &RcxController::setTrackValues);
|
||||
}
|
||||
{
|
||||
auto* act = menu.addAction("Clear Value History");
|
||||
act->setToolTip(QStringLiteral("Reset change tracking for this node"));
|
||||
act->setEnabled(m_valueHistory.contains(nodeId) && m_valueHistory[nodeId].uniqueCount() > 0);
|
||||
connect(act, &QAction::triggered, this, [this, nodeId]() {
|
||||
m_valueHistory[nodeId].clear();
|
||||
for (auto& lm : m_lastResult.meta)
|
||||
if (lm.nodeId == nodeId) lm.heatLevel = 0;
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
menu.addSeparator();
|
||||
|
||||
// Convert to Hex nodes (decompose non-hex types into Hex64/32/16/8)
|
||||
@@ -2934,7 +2963,32 @@ void RcxController::selectSource(const QString& text) {
|
||||
m_doc->undoStack.clear();
|
||||
m_doc->provider = std::move(provider);
|
||||
m_doc->dataPath.clear();
|
||||
m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress;
|
||||
m_doc->tree.pointerSize = m_doc->provider->pointerSize();
|
||||
|
||||
// Re-evaluate formula if present (mirrors attachViaPlugin)
|
||||
if (!m_doc->tree.baseAddressFormula.isEmpty()) {
|
||||
AddressParserCallbacks cbs;
|
||||
auto* prov = m_doc->provider.get();
|
||||
cbs.resolveModule = [prov](const QString& name, bool* ok) -> uint64_t {
|
||||
uint64_t base = prov->symbolToAddress(name);
|
||||
*ok = (base != 0);
|
||||
return base;
|
||||
};
|
||||
int ptrSz = m_doc->tree.pointerSize;
|
||||
cbs.readPointer = [prov, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
||||
uint64_t val = 0;
|
||||
*ok = prov->read(addr, &val, ptrSz);
|
||||
return val;
|
||||
};
|
||||
auto result = AddressParser::evaluate(
|
||||
m_doc->tree.baseAddressFormula, ptrSz, &cbs);
|
||||
if (result.ok)
|
||||
m_doc->tree.baseAddress = result.value;
|
||||
} else if (newBase != 0 && m_doc->tree.baseAddress == 0x00400000) {
|
||||
// Only apply provider base for fresh/default projects.
|
||||
// If user loaded an .rcx with a custom base, preserve it.
|
||||
m_doc->tree.baseAddress = newBase;
|
||||
}
|
||||
resetSnapshot();
|
||||
emit m_doc->documentChanged();
|
||||
|
||||
|
||||
19
src/core.h
19
src/core.h
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "providers/buffer_provider.h"
|
||||
@@ -500,6 +501,7 @@ struct NodeTree {
|
||||
struct ValueHistory {
|
||||
static constexpr int kCapacity = 10;
|
||||
std::array<QString, kCapacity> values;
|
||||
std::array<qint64, kCapacity> timestamps{}; // msec since epoch
|
||||
int count = 0; // total unique values recorded
|
||||
int head = 0; // next write position in ring
|
||||
|
||||
@@ -509,10 +511,16 @@ struct ValueHistory {
|
||||
if (values[last] == v) return; // no change
|
||||
}
|
||||
values[head] = v;
|
||||
timestamps[head] = QDateTime::currentMSecsSinceEpoch();
|
||||
head = (head + 1) % kCapacity;
|
||||
if (count < INT_MAX) count++;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
count = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
int uniqueCount() const { return qMin(count, kCapacity); }
|
||||
|
||||
// 0=static, 1=cold(2 unique), 2=warm(3-4), 3=hot(5+)
|
||||
@@ -536,6 +544,17 @@ struct ValueHistory {
|
||||
for (int i = 0; i < n; i++)
|
||||
fn(values[(start + i) % kCapacity]);
|
||||
}
|
||||
|
||||
// Iterate with timestamps from oldest to newest
|
||||
template<typename Fn>
|
||||
void forEachWithTime(Fn&& fn) const {
|
||||
int n = uniqueCount();
|
||||
int start = (head + kCapacity - n) % kCapacity;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int idx = (start + i) % kCapacity;
|
||||
fn(values[idx], timestamps[idx]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ── LineMeta ──
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
#include <QClipboard>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QLineEdit>
|
||||
#include <QScreen>
|
||||
#include <QScrollBar>
|
||||
#include <QDateTime>
|
||||
#include <functional>
|
||||
#include "themes/thememanager.h"
|
||||
|
||||
@@ -102,7 +104,8 @@ public:
|
||||
sep->setPalette(sp);
|
||||
vbox->addWidget(sep);
|
||||
|
||||
for (const QString& v : vals) {
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
hist.forEachWithTime([&](const QString& v, qint64 msec) {
|
||||
auto* row = new QHBoxLayout;
|
||||
row->setContentsMargins(0, 1, 0, 1);
|
||||
row->setSpacing(8);
|
||||
@@ -113,6 +116,24 @@ public:
|
||||
row->addWidget(label, 1);
|
||||
m_labels.append(label);
|
||||
|
||||
if (msec > 0) {
|
||||
qint64 elapsed = now - msec;
|
||||
QString timeStr;
|
||||
if (elapsed < 1000)
|
||||
timeStr = QStringLiteral("now");
|
||||
else if (elapsed < 60000)
|
||||
timeStr = QStringLiteral("%1s ago").arg(elapsed / 1000);
|
||||
else if (elapsed < 3600000)
|
||||
timeStr = QStringLiteral("%1m ago").arg(elapsed / 60000);
|
||||
else
|
||||
timeStr = QStringLiteral("%1h ago").arg(elapsed / 3600000);
|
||||
|
||||
auto* timeLabel = new QLabel(timeStr);
|
||||
timeLabel->setFont(font);
|
||||
timeLabel->setStyleSheet(QStringLiteral("color: %1;").arg(theme.textDim.name()));
|
||||
row->addWidget(timeLabel);
|
||||
}
|
||||
|
||||
if (showButtons) {
|
||||
auto* setBtn = new QToolButton;
|
||||
setBtn->setText(QStringLiteral("Set"));
|
||||
@@ -130,7 +151,7 @@ public:
|
||||
row->addWidget(setBtn);
|
||||
}
|
||||
vbox->addLayout(row);
|
||||
}
|
||||
});
|
||||
|
||||
adjustSize();
|
||||
}
|
||||
@@ -380,6 +401,12 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci = new QsciScintilla(this);
|
||||
layout->addWidget(m_sci);
|
||||
|
||||
// Find bar (hidden by default, shown with Ctrl+F)
|
||||
m_findBar = new QLineEdit(this);
|
||||
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
||||
m_findBar->setVisible(false);
|
||||
layout->addWidget(m_findBar);
|
||||
|
||||
setupScintilla();
|
||||
setupLexer();
|
||||
setupMargins();
|
||||
@@ -395,6 +422,27 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
||||
m_sci->viewport()->installEventFilter(this);
|
||||
m_sci->viewport()->setMouseTracking(true);
|
||||
|
||||
// Find bar: live search on text change
|
||||
connect(m_findBar, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||
if (text.isEmpty()) return;
|
||||
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
|
||||
});
|
||||
// Find bar: Enter jumps to next match (wraps at end)
|
||||
connect(m_findBar, &QLineEdit::returnPressed, this, [this]() {
|
||||
QString text = m_findBar->text();
|
||||
if (text.isEmpty()) return;
|
||||
if (!m_sci->findNext())
|
||||
m_sci->findFirst(text, false, false, false, true, true, 0, 0);
|
||||
});
|
||||
// Escape hides find bar
|
||||
{
|
||||
auto* escAction = new QAction(m_findBar);
|
||||
escAction->setShortcut(QKeySequence(Qt::Key_Escape));
|
||||
escAction->setShortcutContext(Qt::WidgetShortcut);
|
||||
m_findBar->addAction(escAction);
|
||||
connect(escAction, &QAction::triggered, this, [this]() { hideFindBar(); });
|
||||
}
|
||||
|
||||
// Recalculate hover when the viewport scrolls (scrollbar drag, wheel
|
||||
// deceleration, etc.) so the highlight tracks whatever is under the cursor.
|
||||
connect(m_sci->verticalScrollBar(), &QScrollBar::valueChanged,
|
||||
@@ -782,6 +830,14 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
||||
abs, theme.background);
|
||||
}
|
||||
}
|
||||
|
||||
// Find bar
|
||||
if (m_findBar) {
|
||||
m_findBar->setStyleSheet(
|
||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
||||
" padding: 4px 8px; font-size: 13px; }")
|
||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
||||
}
|
||||
}
|
||||
|
||||
void RcxEditor::applyDocument(const ComposeResult& result) {
|
||||
@@ -1243,6 +1299,17 @@ int RcxEditor::currentNodeIndex() const {
|
||||
return lm ? lm->nodeIdx : -1;
|
||||
}
|
||||
|
||||
void RcxEditor::showFindBar() {
|
||||
m_findBar->setVisible(true);
|
||||
m_findBar->setFocus();
|
||||
m_findBar->selectAll();
|
||||
}
|
||||
|
||||
void RcxEditor::hideFindBar() {
|
||||
m_findBar->setVisible(false);
|
||||
m_sci->setFocus();
|
||||
}
|
||||
|
||||
void RcxEditor::scrollToNodeId(uint64_t nodeId) {
|
||||
for (int i = 0; i < m_meta.size(); i++) {
|
||||
if (m_meta[i].nodeId == nodeId && m_meta[i].lineKind != LineKind::Footer) {
|
||||
@@ -1810,6 +1877,10 @@ static bool hitTestTarget(QsciScintilla* sci,
|
||||
bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
|
||||
if (obj == m_sci && event->type() == QEvent::KeyPress) {
|
||||
auto* ke = static_cast<QKeyEvent*>(event);
|
||||
if (ke->matches(QKeySequence::Find)) {
|
||||
showFindBar();
|
||||
return true;
|
||||
}
|
||||
bool handled = m_editState.active ? handleEditKey(ke) : handleNormalKey(ke);
|
||||
if (!handled && !m_editState.active) {
|
||||
// Clear hover on keyboard navigation (stale after scroll)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QPoint>
|
||||
#include <QHash>
|
||||
|
||||
class QLineEdit;
|
||||
class QsciScintilla;
|
||||
class QsciLexerCPP;
|
||||
|
||||
@@ -154,6 +155,11 @@ private:
|
||||
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
|
||||
const NodeTree* m_disasmTree = nullptr;
|
||||
|
||||
// ── Find bar ──
|
||||
QLineEdit* m_findBar = nullptr;
|
||||
void showFindBar();
|
||||
void hideFindBar();
|
||||
|
||||
// ── Reentrancy guards ──
|
||||
bool m_applyingDocument = false;
|
||||
bool m_clampingSelection = false;
|
||||
|
||||
11329
src/examples/t6zm.rcx
Normal file
11329
src/examples/t6zm.rcx
Normal file
File diff suppressed because it is too large
Load Diff
162
src/main.cpp
162
src/main.cpp
@@ -514,6 +514,8 @@ void MainWindow::createMenus() {
|
||||
Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct);
|
||||
Qt5Qt6AddAction(file, "New &Enum", QKeySequence(Qt::CTRL | Qt::Key_E), QIcon(), this, &MainWindow::newEnum);
|
||||
Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
|
||||
m_recentFilesMenu = file->addMenu("Recent &Files");
|
||||
updateRecentFilesMenu();
|
||||
file->addSeparator();
|
||||
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
|
||||
Qt5Qt6AddAction(file, "Save &As...", QKeySequence::SaveAs, makeIcon(":/vsicons/save-as.svg"), this, &MainWindow::saveFileAs);
|
||||
@@ -550,8 +552,13 @@ void MainWindow::createMenus() {
|
||||
// View
|
||||
auto* view = m_titleBar->menuBar()->addMenu("&View");
|
||||
Qt5Qt6AddAction(view, "Split &Horizontal", QKeySequence::UnknownKey, makeIcon(":/vsicons/split-horizontal.svg"), this, &MainWindow::splitView);
|
||||
Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
m_removeSplitAction = Qt5Qt6AddAction(view, "&Remove Split", QKeySequence::UnknownKey, makeIcon(":/vsicons/chrome-close.svg"), this, &MainWindow::unsplitView);
|
||||
m_removeSplitAction->setVisible(false);
|
||||
view->addSeparator();
|
||||
connect(view, &QMenu::aboutToShow, this, [this]() {
|
||||
auto* tab = activeTab();
|
||||
m_removeSplitAction->setVisible(tab && tab->panes.size() > 1);
|
||||
});
|
||||
m_sourceMenu = view->addMenu("&Data Source");
|
||||
connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu);
|
||||
view->addSeparator();
|
||||
@@ -1342,52 +1349,6 @@ static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QStri
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,57 +1477,11 @@ static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
|
||||
tree.addNode(e);
|
||||
}
|
||||
|
||||
// ── Example class with a union: _SAMPLE_OBJECT ──
|
||||
{
|
||||
Node cls;
|
||||
cls.kind = NodeKind::Struct;
|
||||
cls.name = QStringLiteral("sample");
|
||||
cls.structTypeName = QStringLiteral("_SAMPLE_OBJECT");
|
||||
cls.classKeyword = QStringLiteral("class");
|
||||
cls.parentId = 0;
|
||||
cls.offset = 0;
|
||||
int ci = tree.addNode(cls);
|
||||
uint64_t clsId = tree.nodes[ci].id;
|
||||
|
||||
// Field: uint32_t Type at offset 0
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Type");
|
||||
n.parentId = clsId; n.offset = 0; tree.addNode(n); }
|
||||
// Field: uint32_t Size at offset 4
|
||||
{ Node n; n.kind = NodeKind::UInt32; n.name = QStringLiteral("Size");
|
||||
n.parentId = clsId; n.offset = 4; tree.addNode(n); }
|
||||
|
||||
// Union at offset 8
|
||||
{
|
||||
Node u;
|
||||
u.kind = NodeKind::Struct;
|
||||
u.name = QStringLiteral("Data");
|
||||
u.structTypeName = QStringLiteral("Data");
|
||||
u.classKeyword = QStringLiteral("union");
|
||||
u.parentId = clsId;
|
||||
u.offset = 8;
|
||||
int ui = tree.addNode(u);
|
||||
uint64_t uId = tree.nodes[ui].id;
|
||||
|
||||
// Union member: uint64_t AsLong
|
||||
{ Node n; n.kind = NodeKind::UInt64; n.name = QStringLiteral("AsLong");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
// Union member: void* AsPointer
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("AsPointer");
|
||||
n.parentId = uId; n.offset = 0; n.collapsed = true; tree.addNode(n); }
|
||||
// Union member: float[2] AsFloat2
|
||||
{ Node n; n.kind = NodeKind::Vec2; n.name = QStringLiteral("AsFloat2");
|
||||
n.parentId = uId; n.offset = 0; tree.addNode(n); }
|
||||
}
|
||||
|
||||
// Field: void* Next at offset 16
|
||||
{ Node n; n.kind = NodeKind::Pointer64; n.name = QStringLiteral("Next");
|
||||
n.parentId = clsId; n.offset = 16; n.collapsed = true; tree.addNode(n); }
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::selfTest() {
|
||||
#ifdef Q_OS_WIN
|
||||
// Create a new project, then point it at the live editor object
|
||||
// Tab 2: Editor demo with live process memory (created first)
|
||||
project_new();
|
||||
|
||||
auto* ctrl = activeController();
|
||||
@@ -1583,8 +1498,14 @@ void MainWindow::selfTest() {
|
||||
QString target = QString("%1:Reclass.exe").arg(pid);
|
||||
|
||||
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
|
||||
|
||||
// Tab 1: Empty class for user work (created second, becomes active)
|
||||
auto* userTab = project_new(QStringLiteral("class"));
|
||||
m_mdiArea->setActiveSubWindow(userTab);
|
||||
#else
|
||||
project_new();
|
||||
auto* userTab = project_new(QStringLiteral("class"));
|
||||
m_mdiArea->setActiveSubWindow(userTab);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2566,6 +2487,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||
setAppStatus(QStringLiteral("Imported %1 classes from %2")
|
||||
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||
addRecentFile(filePath);
|
||||
return sub;
|
||||
}
|
||||
|
||||
@@ -2582,6 +2504,7 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
||||
auto* sub = createTab(doc);
|
||||
rebuildWorkspaceModel();
|
||||
m_workspaceDock->show();
|
||||
addRecentFile(filePath);
|
||||
return sub;
|
||||
}
|
||||
|
||||
@@ -2595,8 +2518,10 @@ bool MainWindow::project_save(QMdiSubWindow* sub, bool saveAs) {
|
||||
"Save Definition", {}, "Reclass (*.rcx);;JSON (*.json)");
|
||||
if (path.isEmpty()) return false;
|
||||
tab.doc->save(path);
|
||||
addRecentFile(path);
|
||||
} else {
|
||||
tab.doc->save(tab.doc->filePath);
|
||||
addRecentFile(tab.doc->filePath);
|
||||
}
|
||||
updateWindowTitle();
|
||||
return true;
|
||||
@@ -2812,6 +2737,18 @@ void MainWindow::createWorkspaceDock() {
|
||||
}
|
||||
});
|
||||
|
||||
// Ctrl+F focuses the workspace search field
|
||||
{
|
||||
auto* findAction = new QAction(dockContainer);
|
||||
findAction->setShortcut(QKeySequence::Find);
|
||||
findAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
|
||||
dockContainer->addAction(findAction);
|
||||
connect(findAction, &QAction::triggered, this, [this]() {
|
||||
m_workspaceSearch->setFocus();
|
||||
m_workspaceSearch->selectAll();
|
||||
});
|
||||
}
|
||||
|
||||
m_workspaceDock->setWidget(dockContainer);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_workspaceDock);
|
||||
m_workspaceDock->hide();
|
||||
@@ -2967,6 +2904,43 @@ void MainWindow::rebuildWorkspaceModel() {
|
||||
m_workspaceTree->expandToDepth(0);
|
||||
}
|
||||
|
||||
void MainWindow::addRecentFile(const QString& path) {
|
||||
if (path.isEmpty()) return;
|
||||
QString absPath = QFileInfo(path).absoluteFilePath();
|
||||
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QStringList recent = s.value("recentFiles").toStringList();
|
||||
recent.removeAll(absPath);
|
||||
recent.prepend(absPath);
|
||||
while (recent.size() > 10)
|
||||
recent.removeLast();
|
||||
s.setValue("recentFiles", recent);
|
||||
|
||||
updateRecentFilesMenu();
|
||||
}
|
||||
|
||||
void MainWindow::updateRecentFilesMenu() {
|
||||
if (!m_recentFilesMenu) return;
|
||||
m_recentFilesMenu->clear();
|
||||
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QStringList recent = s.value("recentFiles").toStringList();
|
||||
|
||||
int added = 0;
|
||||
for (const QString& path : recent) {
|
||||
if (!QFile::exists(path)) continue;
|
||||
QString label = QStringLiteral("&%1 %2").arg(added + 1).arg(QFileInfo(path).fileName());
|
||||
m_recentFilesMenu->addAction(label, this, [this, path]() {
|
||||
project_open(path);
|
||||
})->setToolTip(path);
|
||||
if (++added >= 10) break;
|
||||
}
|
||||
if (added == 0) {
|
||||
auto* empty = m_recentFilesMenu->addAction(QStringLiteral("(empty)"));
|
||||
empty->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::populateSourceMenu() {
|
||||
m_sourceMenu->clear();
|
||||
auto* ctrl = activeController();
|
||||
|
||||
@@ -92,7 +92,9 @@ private:
|
||||
PluginManager m_pluginManager;
|
||||
McpBridge* m_mcp = nullptr;
|
||||
QAction* m_mcpAction = nullptr;
|
||||
QAction* m_removeSplitAction = nullptr;
|
||||
QMenu* m_sourceMenu = nullptr;
|
||||
QMenu* m_recentFilesMenu = nullptr;
|
||||
|
||||
struct SplitPane {
|
||||
QTabWidget* tabWidget = nullptr;
|
||||
@@ -119,6 +121,8 @@ private:
|
||||
void createStatusBar();
|
||||
void showPluginsDialog();
|
||||
void populateSourceMenu();
|
||||
void addRecentFile(const QString& path);
|
||||
void updateRecentFilesMenu();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
|
||||
RcxController* activeController() const;
|
||||
|
||||
@@ -256,7 +256,7 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
"insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. "
|
||||
"change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
|
||||
"change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000'}. "
|
||||
"change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. "
|
||||
"change_struct_type: {op:'change_struct_type', nodeId:'ID', structTypeName:'Name'}. "
|
||||
"change_class_keyword: {op:'change_class_keyword', nodeId:'ID', classKeyword:'class'}. "
|
||||
"change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
|
||||
@@ -396,6 +396,24 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
||||
}}
|
||||
});
|
||||
|
||||
// 9. node.history
|
||||
tools.append(QJsonObject{
|
||||
{"name", "node.history"},
|
||||
{"description", "Returns timestamped value change history (up to 10 entries) "
|
||||
"for specified nodes. Requires live provider with value tracking enabled."},
|
||||
{"inputSchema", QJsonObject{
|
||||
{"type", "object"},
|
||||
{"properties", QJsonObject{
|
||||
{"nodeIds", QJsonObject{{"type", "array"},
|
||||
{"items", QJsonObject{{"type", "string"}}},
|
||||
{"description", "Array of node IDs to get history for."}}},
|
||||
{"tabIndex", QJsonObject{{"type", "integer"},
|
||||
{"description", "MDI tab index. Omit for active tab."}}}
|
||||
}},
|
||||
{"required", QJsonArray{"nodeIds"}}
|
||||
}}
|
||||
});
|
||||
|
||||
return okReply(id, QJsonObject{{"tools", tools}});
|
||||
}
|
||||
|
||||
@@ -420,6 +438,7 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
|
||||
else if (toolName == "status.set") result = toolStatusSet(args);
|
||||
else if (toolName == "ui.action") result = toolUiAction(args);
|
||||
else if (toolName == "tree.search") result = toolTreeSearch(args);
|
||||
else if (toolName == "node.history") result = toolNodeHistory(args);
|
||||
else return errReply(id, -32601, "Unknown tool: " + toolName);
|
||||
|
||||
m_mainWindow->clearMcpStatus();
|
||||
@@ -751,8 +770,10 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
|
||||
}
|
||||
else if (opType == "change_base") {
|
||||
uint64_t newBase = op.value("baseAddress").toString().toULongLong(nullptr, 16);
|
||||
QString oldFormula = tree.baseAddressFormula;
|
||||
QString newFormula = op.value("formula").toString();
|
||||
doc->undoStack.push(new RcxCommand(ctrl,
|
||||
cmd::ChangeBase{tree.baseAddress, newBase}));
|
||||
cmd::ChangeBase{tree.baseAddress, newBase, oldFormula, newFormula}));
|
||||
applied++;
|
||||
}
|
||||
else if (opType == "change_struct_type") {
|
||||
@@ -1226,6 +1247,43 @@ QJsonObject McpBridge::toolTreeSearch(const QJsonObject& args) {
|
||||
QJsonDocument(out).toJson(QJsonDocument::Indented)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Tool: node.history — return timestamped value history for nodes
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
QJsonObject McpBridge::toolNodeHistory(const QJsonObject& args) {
|
||||
auto* tab = resolveTab(args);
|
||||
if (!tab) return makeTextResult("No active tab.", true);
|
||||
|
||||
const auto& histMap = tab->ctrl->valueHistory();
|
||||
QJsonArray requestedIds = args.value("nodeIds").toArray();
|
||||
if (requestedIds.isEmpty())
|
||||
return makeTextResult("nodeIds array is required.", true);
|
||||
|
||||
QJsonObject result;
|
||||
for (const auto& idVal : requestedIds) {
|
||||
QString idStr = idVal.toString();
|
||||
uint64_t nodeId = idStr.toULongLong();
|
||||
auto it = histMap.find(nodeId);
|
||||
QJsonArray entries;
|
||||
if (it != histMap.end()) {
|
||||
it->forEachWithTime([&](const QString& val, qint64 msec) {
|
||||
QJsonObject entry;
|
||||
entry.insert(QStringLiteral("value"), val);
|
||||
entry.insert(QStringLiteral("timestamp"), msec);
|
||||
entries.append(entry);
|
||||
});
|
||||
}
|
||||
QJsonObject nodeResult;
|
||||
nodeResult.insert(QStringLiteral("entries"), entries);
|
||||
nodeResult.insert(QStringLiteral("heatLevel"), it != histMap.end() ? it->heatLevel() : 0);
|
||||
nodeResult.insert(QStringLiteral("uniqueCount"), it != histMap.end() ? it->uniqueCount() : 0);
|
||||
result.insert(idStr, nodeResult);
|
||||
}
|
||||
return makeTextResult(QString::fromUtf8(
|
||||
QJsonDocument(result).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Notifications (call from MainWindow/Controller hooks)
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -59,6 +59,7 @@ private:
|
||||
QJsonObject toolStatusSet(const QJsonObject& args);
|
||||
QJsonObject toolUiAction(const QJsonObject& args);
|
||||
QJsonObject toolTreeSearch(const QJsonObject& args);
|
||||
QJsonObject toolNodeHistory(const QJsonObject& args);
|
||||
|
||||
// Helpers
|
||||
QJsonObject makeTextResult(const QString& text, bool isError = false);
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QMenu>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -27,22 +31,9 @@ ProcessPicker::ProcessPicker(QWidget *parent)
|
||||
, m_useCustomList(false)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column - fixed width
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column - fixed width
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true); // Path column - fills remaining space
|
||||
ui->processTable->setWordWrap(false); // Disable word wrap for single-line display
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft); // Elide from left (show end of path)
|
||||
|
||||
// Connect signals
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Initial process enumeration
|
||||
initUi();
|
||||
refreshProcessList();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget *parent)
|
||||
@@ -51,23 +42,102 @@ ProcessPicker::ProcessPicker(const QList<ProcessInfo>& customProcesses, QWidget
|
||||
, m_useCustomList(true)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Configure table
|
||||
ui->processTable->setColumnWidth(0, 80);
|
||||
ui->processTable->setColumnWidth(1, 200);
|
||||
initUi();
|
||||
ui->refreshButton->setVisible(false);
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
selectPreferredProcess();
|
||||
}
|
||||
|
||||
void ProcessPicker::initUi()
|
||||
{
|
||||
// Table configuration
|
||||
ui->processTable->setColumnWidth(0, 80); // PID column
|
||||
ui->processTable->setColumnWidth(1, 200); // Name column
|
||||
ui->processTable->horizontalHeader()->setStretchLastSection(true);
|
||||
ui->processTable->setWordWrap(false);
|
||||
ui->processTable->setTextElideMode(Qt::ElideLeft);
|
||||
|
||||
// Connect signals (no refresh button for custom lists)
|
||||
ui->refreshButton->setVisible(false);
|
||||
ui->processTable->setShowGrid(false);
|
||||
ui->processTable->verticalHeader()->setDefaultSectionSize(fontMetrics().height() + 6);
|
||||
|
||||
// Signal connections
|
||||
connect(ui->refreshButton, &QPushButton::clicked, this, &ProcessPicker::refreshProcessList);
|
||||
connect(ui->processTable, &QTableWidget::itemDoubleClicked, this, &ProcessPicker::onProcessSelected);
|
||||
connect(ui->filterEdit, &QLineEdit::textChanged, this, &ProcessPicker::filterProcesses);
|
||||
connect(ui->attachButton, &QPushButton::clicked, this, &ProcessPicker::onProcessSelected);
|
||||
|
||||
// Use custom process list
|
||||
m_allProcesses = customProcesses;
|
||||
applyFilter();
|
||||
|
||||
// Derive theme colors from the global palette (set by applyGlobalTheme)
|
||||
QPalette pal = qApp->palette();
|
||||
QString bg = pal.color(QPalette::Base).name();
|
||||
QString text = pal.color(QPalette::Text).name();
|
||||
QString hover = pal.color(QPalette::Mid).name();
|
||||
QString surface = pal.color(QPalette::AlternateBase).name();
|
||||
QString button = pal.color(QPalette::Button).name();
|
||||
QString highlight= pal.color(QPalette::Highlight).name();
|
||||
QString border = pal.color(QPalette::Mid).darker(120).name();
|
||||
QString mutedText= pal.color(QPalette::Disabled, QPalette::WindowText).name();
|
||||
QString hoverDk = pal.color(QPalette::Mid).darker(130).name();
|
||||
|
||||
ui->processTable->setStyleSheet(QStringLiteral(
|
||||
"QTableWidget { background: %1; color: %2; border: none; }"
|
||||
"QTableWidget::item { padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:hover { background: %3; padding: 2px 6px; border: none; }"
|
||||
"QTableWidget::item:selected { background: %3; color: %2; padding: 2px 6px; border: none; }")
|
||||
.arg(bg, text, hover));
|
||||
|
||||
ui->processTable->horizontalHeader()->setStyleSheet(QStringLiteral(
|
||||
"QHeaderView::section { background: %1; color: %2; border: none;"
|
||||
" padding: 4px 6px; text-align: left; }")
|
||||
.arg(surface, text));
|
||||
ui->processTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
ui->filterEdit->setStyleSheet(QStringLiteral(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; padding: 2px 4px; }"
|
||||
"QLineEdit:focus { border-color: %4; }")
|
||||
.arg(bg, text, border, highlight));
|
||||
|
||||
QString btnStyle = QStringLiteral(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; padding: 4px 12px; }"
|
||||
"QPushButton:hover { background: %4; }"
|
||||
"QPushButton:pressed { background: %5; }"
|
||||
"QPushButton:disabled { color: %6; }")
|
||||
.arg(button, text, border, hover, hoverDk, mutedText);
|
||||
ui->refreshButton->setStyleSheet(btnStyle);
|
||||
ui->attachButton->setStyleSheet(btnStyle);
|
||||
ui->cancelButton->setStyleSheet(btnStyle);
|
||||
|
||||
// Right-click context menu
|
||||
ui->processTable->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(ui->processTable, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
int row = ui->processTable->rowAt(pos.y());
|
||||
if (row < 0) return;
|
||||
auto* pidItem = ui->processTable->item(row, 0);
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
auto* pathItem = ui->processTable->item(row, 2);
|
||||
if (!pidItem || !nameItem) return;
|
||||
|
||||
QString pid = QString::number(pidItem->data(Qt::EditRole).toUInt());
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
QString path = pathItem ? pathItem->text() : QString();
|
||||
|
||||
QMenu menu;
|
||||
auto* copyPid = menu.addAction(QStringLiteral("Copy PID"));
|
||||
auto* copyName = menu.addAction(QStringLiteral("Copy Name"));
|
||||
QAction* copyPath = nullptr;
|
||||
if (!path.isEmpty())
|
||||
copyPath = menu.addAction(QStringLiteral("Copy Path"));
|
||||
|
||||
auto* chosen = menu.exec(ui->processTable->viewport()->mapToGlobal(pos));
|
||||
if (chosen == copyPid)
|
||||
QApplication::clipboard()->setText(pid);
|
||||
else if (chosen == copyName)
|
||||
QApplication::clipboard()->setText(name);
|
||||
else if (copyPath && chosen == copyPath)
|
||||
QApplication::clipboard()->setText(path);
|
||||
});
|
||||
|
||||
// Auto-focus filter for immediate typing
|
||||
ui->filterEdit->setFocus();
|
||||
}
|
||||
|
||||
ProcessPicker::~ProcessPicker()
|
||||
@@ -97,31 +167,31 @@ void ProcessPicker::onProcessSelected()
|
||||
{
|
||||
auto* item = ui->processTable->currentItem();
|
||||
if (!item) return;
|
||||
|
||||
|
||||
int row = item->row();
|
||||
m_selectedPid = ui->processTable->item(row, 0)->data(Qt::EditRole).toUInt();
|
||||
// Use original name stored in UserRole (without architecture suffix)
|
||||
QVariant origName = ui->processTable->item(row, 1)->data(Qt::UserRole);
|
||||
m_selectedName = origName.isValid() ? origName.toString()
|
||||
: ui->processTable->item(row, 1)->text();
|
||||
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
void ProcessPicker::enumerateProcesses()
|
||||
{
|
||||
QList<ProcessInfo> processes;
|
||||
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) {
|
||||
QMessageBox::warning(this, "Error", "Failed to enumerate processes.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
PROCESSENTRY32W pe32;
|
||||
pe32.dwSize = sizeof(PROCESSENTRY32W);
|
||||
|
||||
|
||||
if (Process32FirstW(snapshot, &pe32))
|
||||
{
|
||||
do
|
||||
@@ -129,10 +199,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
ProcessInfo info;
|
||||
info.pid = pe32.th32ProcessID;
|
||||
info.name = QString::fromWCharArray(pe32.szExeFile);
|
||||
|
||||
// Try to get full path and extract icon
|
||||
// If we can't open a process with PROCESS_QUERY_LIMITED_INFORMATION then
|
||||
// we for sure can't access their memory. - Skip in this case
|
||||
|
||||
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
|
||||
if (hProcess)
|
||||
{
|
||||
@@ -143,7 +210,7 @@ void ProcessPicker::enumerateProcesses()
|
||||
GetModuleFileNameExW(hProcess, nullptr, path, pathLen))
|
||||
{
|
||||
info.path = QString::fromWCharArray(path);
|
||||
|
||||
|
||||
// Extract icon from executable
|
||||
SHFILEINFOW sfi = {};
|
||||
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
|
||||
@@ -292,3 +359,22 @@ void ProcessPicker::applyFilter()
|
||||
|
||||
populateTable(filtered);
|
||||
}
|
||||
|
||||
void ProcessPicker::selectPreferredProcess()
|
||||
{
|
||||
// Try to select the last-attached process if it's in the list
|
||||
QSettings s("Reclass", "Reclass");
|
||||
QString lastProc = s.value("lastAttachedProcess").toString();
|
||||
if (lastProc.isEmpty()) return;
|
||||
|
||||
for (int row = 0; row < ui->processTable->rowCount(); ++row) {
|
||||
auto* nameItem = ui->processTable->item(row, 1);
|
||||
if (!nameItem) continue;
|
||||
QString name = nameItem->data(Qt::UserRole).toString();
|
||||
if (name.compare(lastProc, Qt::CaseInsensitive) == 0) {
|
||||
ui->processTable->selectRow(row);
|
||||
ui->processTable->scrollToItem(nameItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ private slots:
|
||||
void filterProcesses(const QString& text);
|
||||
|
||||
private:
|
||||
void initUi();
|
||||
void enumerateProcesses();
|
||||
void populateTable(const QList<ProcessInfo>& processes);
|
||||
void applyFilter();
|
||||
void selectPreferredProcess();
|
||||
|
||||
Ui::ProcessPicker *ui;
|
||||
uint32_t m_selectedPid = 0;
|
||||
|
||||
@@ -127,22 +127,6 @@
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>attachButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ProcessPicker</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>600</x>
|
||||
<y>470</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>350</x>
|
||||
<y>250</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>cancelButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
|
||||
@@ -46,10 +46,17 @@ inline void buildProjectExplorer(QStandardItemModel* model,
|
||||
auto nameOf = [](const Node* n) {
|
||||
return n->structTypeName.isEmpty() ? n->name : n->structTypeName;
|
||||
};
|
||||
// Sort structs by children count descending (most fields first)
|
||||
auto cmpChildren = [&](const Entry& a, const Entry& b) {
|
||||
int ca = a.tree->childrenOf(a.node->id).size();
|
||||
int cb = b.tree->childrenOf(b.node->id).size();
|
||||
if (ca != cb) return ca > cb;
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpChildren);
|
||||
auto cmpName = [&](const Entry& a, const Entry& b) {
|
||||
return nameOf(a.node).compare(nameOf(b.node), Qt::CaseInsensitive) < 0;
|
||||
};
|
||||
std::sort(types.begin(), types.end(), cmpName);
|
||||
std::sort(enums.begin(), enums.end(), cmpName);
|
||||
|
||||
// Helper: type display string for a member node
|
||||
|
||||
Reference in New Issue
Block a user