Single-click type chooser, popup warmup fix, rename ProcessMemory plugin

- Type chooser popup now opens on single click (no need to pre-select node)
- Fix ~170ms first-open delay by pre-initializing Qt popup subsystem at startup
- Rename ProcessMemoryWindows -> ProcessMemory (already supports Linux)
This commit is contained in:
IChooseYOu
2026-02-14 16:08:44 -07:00
parent c856ba2697
commit 0a8244dad4
8 changed files with 207 additions and 44 deletions

View File

@@ -288,5 +288,5 @@ if(BUILD_TESTING)
)
endif()
endif()
add_subdirectory(plugins/ProcessMemoryWindows)
add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/WinDbgMemory)

View File

@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(ProcessMemoryWindowsPlugin LANGUAGES CXX)
project(ProcessMemoryPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,36 +12,36 @@ set(CMAKE_AUTOUIC ON)
# Plugin sources
set(PLUGIN_SOURCES
ProcessMemoryWindowsPlugin.h
ProcessMemoryWindowsPlugin.cpp
ProcessMemoryPlugin.h
ProcessMemoryPlugin.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui
)
# Create shared library (DLL)
add_library(ProcessMemoryWindowsPlugin SHARED ${PLUGIN_SOURCES})
add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
target_link_libraries(ProcessMemoryPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS})
# Platform-specific linking
if(WIN32)
target_link_libraries(ProcessMemoryWindowsPlugin PRIVATE psapi shell32)
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryWindowsPlugin PRIVATE -fvisibility=hidden)
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
# Include directories
target_include_directories(ProcessMemoryWindowsPlugin PRIVATE
target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src
)
# Output to Plugins folder
set_target_properties(ProcessMemoryWindowsPlugin PROPERTIES
set_target_properties(ProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
)

View File

@@ -1,4 +1,4 @@
#include "ProcessMemoryWindowsPlugin.h"
#include "ProcessMemoryPlugin.h"
#include "../../src/processpicker.h"
@@ -32,12 +32,12 @@
#endif
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryWindowsProvider implementation
// ProcessMemoryProvider implementation
// ──────────────────────────────────────────────────────────────────────────
#ifdef _WIN32
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_handle(nullptr)
, m_pid(pid)
, m_processName(processName)
@@ -60,7 +60,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
cacheModules();
}
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (!m_handle || len <= 0) return false;
@@ -71,7 +71,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
return bytesRead > 0;
}
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (!m_handle || !m_writable || len <= 0) return false;
@@ -81,7 +81,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
return false;
}
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
@@ -96,7 +96,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {};
}
void ProcessMemoryWindowsProvider::cacheModules()
void ProcessMemoryProvider::cacheModules()
{
HMODULE mods[1024];
DWORD needed = 0;
@@ -126,7 +126,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
#elif defined(__linux__)
ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_fd(-1)
, m_pid(pid)
, m_processName(processName)
@@ -152,7 +152,7 @@ ProcessMemoryWindowsProvider::ProcessMemoryWindowsProvider(uint32_t pid, const Q
}
bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
@@ -176,7 +176,7 @@ bool ProcessMemoryWindowsProvider::read(uint64_t addr, void* buf, int len) const
return nread == static_cast<ssize_t>(len);
}
bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len)
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_fd < 0 || !m_writable || len <= 0) return false;
@@ -200,7 +200,7 @@ bool ProcessMemoryWindowsProvider::write(uint64_t addr, const void* buf, int len
return nwritten == static_cast<ssize_t>(len);
}
QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
@@ -215,7 +215,7 @@ QString ProcessMemoryWindowsProvider::getSymbol(uint64_t addr) const
return {};
}
void ProcessMemoryWindowsProvider::cacheModules()
void ProcessMemoryProvider::cacheModules()
{
// Parse /proc/<pid>/maps to discover loaded modules
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
@@ -288,7 +288,7 @@ void ProcessMemoryWindowsProvider::cacheModules()
#endif // platform
ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32
if (m_handle)
@@ -299,7 +299,7 @@ ProcessMemoryWindowsProvider::~ProcessMemoryWindowsProvider()
#endif
}
int ProcessMemoryWindowsProvider::size() const
int ProcessMemoryProvider::size() const
{
#ifdef _WIN32
return m_handle ? 0x10000 : 0;
@@ -309,22 +309,22 @@ int ProcessMemoryWindowsProvider::size() const
}
// ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryWindowsPlugin implementation
// ProcessMemoryPlugin implementation
// ──────────────────────────────────────────────────────────────────────────
QIcon ProcessMemoryWindowsPlugin::Icon() const
QIcon ProcessMemoryPlugin::Icon() const
{
return qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
}
bool ProcessMemoryWindowsPlugin::canHandle(const QString& target) const
bool ProcessMemoryPlugin::canHandle(const QString& target) const
{
// Target format: "pid:name" or just "pid"
QRegularExpression re("^\\d+");
return re.match(target).hasMatch();
}
std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const QString& target, QString* errorMsg)
std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
{
// Parse target: "pid:name" or just "pid"
QStringList parts = target.split(':');
@@ -339,7 +339,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
auto provider = std::make_unique<ProcessMemoryWindowsProvider>(pid, name);
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
if (!provider->isValid())
{
if (errorMsg)
@@ -352,7 +352,7 @@ std::unique_ptr<rcx::Provider> ProcessMemoryWindowsPlugin::createProvider(const
return provider;
}
uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target) const
uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
{
#ifdef _WIN32
// Parse PID from target
@@ -409,7 +409,7 @@ uint64_t ProcessMemoryWindowsPlugin::getInitialBaseAddress(const QString& target
#endif
}
bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{
// Use custom process enumeration from plugin
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
@@ -440,7 +440,7 @@ bool ProcessMemoryWindowsPlugin::selectTarget(QWidget* parent, QString* target)
return false;
}
QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
{
QVector<PluginProcessInfo> processes;
@@ -543,5 +543,5 @@ QVector<PluginProcessInfo> ProcessMemoryWindowsPlugin::enumerateProcesses()
extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{
return new ProcessMemoryWindowsPlugin();
return new ProcessMemoryPlugin();
}

View File

@@ -5,14 +5,14 @@
#include <cstdint>
/**
* Process memory provider (Windows)
* Reads/writes memory from a live process using Windows platform APIs
* Process memory provider
* Reads/writes memory from a live process using platform APIs
*/
class ProcessMemoryWindowsProvider : public rcx::Provider
class ProcessMemoryProvider : public rcx::Provider
{
public:
ProcessMemoryWindowsProvider(uint32_t pid, const QString& processName);
~ProcessMemoryWindowsProvider() override;
ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryProvider() override;
// Required overrides
bool read(uint64_t addr, void* buf, int len) const override;
@@ -57,15 +57,15 @@ private:
};
/**
* Plugin that provides ProcessMemoryWindowsProvider
* Plugin that provides ProcessMemoryProvider
*/
class ProcessMemoryWindowsPlugin : public IProviderPlugin
class ProcessMemoryPlugin : public IProviderPlugin
{
public:
std::string Name() const override { return "Process Memory Windows"; }
std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "Reclass"; }
std::string Description() const override { return "Read and write memory from local running processes (Windows)"; }
std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override;

View File

@@ -178,6 +178,14 @@ RcxEditor* RcxController::addSplitEditor(QWidget* parent) {
editor->applyDocument(m_lastResult);
}
updateCommandRow();
// Eagerly pre-warm the type popup so first click isn't slow (~350ms cold start).
if (!m_cachedPopup) {
QTimer::singleShot(0, this, [this, editor]() {
if (!m_cachedPopup && !m_editors.isEmpty())
ensurePopup(editor);
});
}
return editor;
}

View File

@@ -1330,7 +1330,15 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) {
// Single-click on editable token of already-selected node → edit
int tLine, tCol; EditTarget t;
if (hitTestTarget(m_sci, m_meta, me->pos(), tLine, tCol, t)) {
if (alreadySelected && plain) {
// Type/ArrayElementType/PointerTarget open a dismissible popup
// (not inline text edit), so allow on first click without
// requiring the node to be pre-selected.
bool isPopupTarget = (t == EditTarget::Type
|| t == EditTarget::ArrayElementType
|| t == EditTarget::PointerTarget);
if ((alreadySelected || isPopupTarget) && plain) {
if (!alreadySelected)
emit nodeClicked(h.line, h.nodeId, me->modifiers());
m_pendingClickNodeId = 0;
return beginInlineEdit(t, tLine, tCol);
}

View File

@@ -16,6 +16,7 @@
#include <QApplication>
#include <QScreen>
#include <QIntValidator>
#include <QElapsedTimer>
#include "themes/thememanager.h"
namespace rcx {
@@ -384,10 +385,33 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
}
void TypeSelectorPopup::warmUp() {
// One-time per-process cost (~170ms): Qt lazily initializes the style/font/DLL
// subsystem the first time a popup with complex children is shown. Pre-pay it
// by briefly showing a throwaway dummy popup with a QListView, then show+hide
// ourselves.
{
auto* primer = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
primer->resize(300, 400);
auto* lay = new QVBoxLayout(primer);
lay->addWidget(new QLabel(QStringLiteral("x")));
lay->addWidget(new QLineEdit);
auto* model = new QStringListModel(primer);
QStringList items; for (int i = 0; i < 10; i++) items << QStringLiteral("x");
model->setStringList(items);
auto* lv = new QListView;
lv->setModel(model);
lay->addWidget(lv);
primer->show();
QApplication::processEvents();
primer->hide();
QApplication::processEvents();
delete primer;
}
TypeEntry dummy;
dummy.entryKind = TypeEntry::Primitive;
dummy.primitiveKind = NodeKind::Hex8;
dummy.displayName = "warmup";
dummy.displayName = QStringLiteral("warmup");
setTypes({dummy});
popup(QPoint(-9999, -9999));
hide();

View File

@@ -8,6 +8,8 @@
#include <QLineEdit>
#include <QListView>
#include <QStringListModel>
#include <QLabel>
#include <QFrame>
#include <Qsci/qsciscintilla.h>
#include "controller.h"
#include "typeselectorpopup.h"
@@ -198,6 +200,127 @@ private slots:
}
}
// ── Isolate first-show cost with different window flags ──
void benchmarkFirstShow() {
auto ms = [](qint64 ns) { return QString::number(ns / 1000000.0, 'f', 2); };
struct FlagTest {
const char* name;
Qt::WindowFlags flags;
};
FlagTest tests[] = {
{"Qt::Popup|Frameless", Qt::Popup | Qt::FramelessWindowHint},
{"Qt::Tool|Frameless", Qt::Tool | Qt::FramelessWindowHint},
{"Qt::ToolTip", Qt::ToolTip},
{"Qt::Window|Frameless", Qt::Window | Qt::FramelessWindowHint},
{"Qt::Popup|Frameless (2nd)", Qt::Popup | Qt::FramelessWindowHint},
};
for (const auto& test : tests) {
auto* f = new QFrame(nullptr, test.flags);
f->resize(300, 400);
QElapsedTimer t; t.start();
f->show();
qint64 t1 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t2 = t.nsecsElapsed();
f->hide();
QApplication::processEvents();
t.restart();
f->show();
qint64 t3 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t4 = t.nsecsElapsed();
f->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== %1 ===").arg(test.name);
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
delete f;
}
// TypeSelectorPopup: cold vs after warmUp
{
auto* popup = new TypeSelectorPopup();
TypeEntry dummy;
dummy.entryKind = TypeEntry::Primitive;
dummy.primitiveKind = NodeKind::Hex8;
dummy.displayName = "test";
popup->setTypes({dummy});
QElapsedTimer t; t.start();
popup->show();
qint64 t1 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t2 = t.nsecsElapsed();
popup->hide();
QApplication::processEvents();
t.restart();
popup->show();
qint64 t3 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t4 = t.nsecsElapsed();
popup->hide();
QApplication::processEvents();
qDebug() << "";
qDebug().noquote() << QString("=== TypeSelectorPopup (cold, Qt::Popup) ===");
qDebug().noquote() << QString(" 1st: show=%1ms events=%2ms | 2nd: show=%3ms events=%4ms")
.arg(ms(t1)).arg(ms(t2)).arg(ms(t3)).arg(ms(t4));
delete popup;
}
// Clean order test: dummy popup with children FIRST, then TypeSelectorPopup
qDebug() << "";
qDebug() << "=== CLEAN: dummy popup first, then TypeSelectorPopup ===";
{
auto* dummy = new QFrame(nullptr, Qt::Popup | Qt::FramelessWindowHint);
dummy->resize(300, 400);
auto* dLay = new QVBoxLayout(dummy);
dLay->addWidget(new QLabel("dummy"));
dLay->addWidget(new QLineEdit);
auto* dModel = new QStringListModel(dummy);
QStringList dItems; for (int i = 0; i < 10; i++) dItems << "x";
dModel->setStringList(dItems);
auto* dLv = new QListView; dLv->setModel(dModel);
dLay->addWidget(dLv);
QElapsedTimer t; t.start();
dummy->show();
qint64 t1 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t2 = t.nsecsElapsed();
dummy->hide();
QApplication::processEvents();
qDebug().noquote() << QString(" Dummy popup: show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
delete dummy;
}
{
auto* popup = new TypeSelectorPopup();
TypeEntry e;
e.entryKind = TypeEntry::Primitive;
e.primitiveKind = NodeKind::Hex8;
e.displayName = "test";
popup->setTypes({e});
popup->resize(300, 400);
QElapsedTimer t; t.start();
popup->show();
qint64 t1 = t.nsecsElapsed(); t.restart();
QApplication::processEvents();
qint64 t2 = t.nsecsElapsed();
popup->hide();
QApplication::processEvents();
qDebug().noquote() << QString(" TypeSelectorPopup (after dummy): show=%1ms events=%2ms").arg(ms(t1)).arg(ms(t2));
delete popup;
}
}
// ── Popup data model ──
void testPopupListsRootStructs() {