mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
QMenu + QMenuBar hover: amber indHoverSpan text via QProxyStyle
MenuBarStyle.drawControl overrides CE_MenuBarItem and CE_MenuItem: - Menu bar: strips hover state flags, swaps ButtonText to Link (amber), delegates to Fusion for identical layout (no text shift) - Popup menus: patches Highlight→Mid, HighlightedText→Link, delegates to Fusion for icons/shortcuts/checkmarks Test: real QEvent::Enter + MouseMove delivery, QScreen::grabWindow screenshot, pixel-scan for amber, always saves PNGs for inspection.
This commit is contained in:
126
src/main.cpp
126
src/main.cpp
@@ -155,8 +155,49 @@ public:
|
|||||||
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
|
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
|
||||||
if (type == CT_MenuBarItem)
|
if (type == CT_MenuBarItem)
|
||||||
s.setHeight(s.height() + qRound(s.height() * 0.5));
|
s.setHeight(s.height() + qRound(s.height() * 0.5));
|
||||||
|
if (type == CT_MenuItem)
|
||||||
|
s = QSize(s.width() + 24, s.height() + 4);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||||
|
QPainter* p, const QWidget* w) const override {
|
||||||
|
// Kill Fusion's 3D bevel on QMenu — the OS drop shadow is enough
|
||||||
|
if (elem == PE_FrameMenu)
|
||||||
|
return;
|
||||||
|
QProxyStyle::drawPrimitive(elem, opt, p, w);
|
||||||
|
}
|
||||||
|
void drawControl(ControlElement element, const QStyleOption* opt,
|
||||||
|
QPainter* p, const QWidget* w) const override {
|
||||||
|
// Menu bar items (File, Edit, View…) — direct paint, Fusion ignores palette
|
||||||
|
if (element == CE_MenuBarItem) {
|
||||||
|
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
|
||||||
|
if (mi->state & (State_Selected | State_Sunken)) {
|
||||||
|
QStyleOptionMenuItem patched = *mi;
|
||||||
|
patched.state &= ~(State_Selected | State_Sunken);
|
||||||
|
patched.palette.setColor(QPalette::ButtonText,
|
||||||
|
mi->palette.color(QPalette::Link)); // amber text only
|
||||||
|
QProxyStyle::drawControl(element, &patched, p, w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Popup menu items — palette patch then delegate to Fusion
|
||||||
|
if (element == CE_MenuItem) {
|
||||||
|
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
|
||||||
|
if ((mi->state & State_Selected)
|
||||||
|
&& mi->menuItemType != QStyleOptionMenuItem::Separator) {
|
||||||
|
QStyleOptionMenuItem patched = *mi;
|
||||||
|
patched.palette.setColor(QPalette::Highlight,
|
||||||
|
mi->palette.color(QPalette::Mid)); // theme.border
|
||||||
|
patched.palette.setColor(QPalette::HighlightedText,
|
||||||
|
mi->palette.color(QPalette::Link)); // theme.indHoverSpan
|
||||||
|
QProxyStyle::drawControl(element, &patched, p, w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QProxyStyle::drawControl(element, opt, p, w);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static void applyGlobalTheme(const rcx::Theme& theme) {
|
static void applyGlobalTheme(const rcx::Theme& theme) {
|
||||||
@@ -169,32 +210,24 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
|
|||||||
pal.setColor(QPalette::Button, theme.button);
|
pal.setColor(QPalette::Button, theme.button);
|
||||||
pal.setColor(QPalette::ButtonText, theme.text);
|
pal.setColor(QPalette::ButtonText, theme.text);
|
||||||
pal.setColor(QPalette::Highlight, theme.hover);
|
pal.setColor(QPalette::Highlight, theme.hover);
|
||||||
pal.setColor(QPalette::HighlightedText, theme.text);
|
pal.setColor(QPalette::HighlightedText, theme.indHoverSpan);
|
||||||
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
|
||||||
pal.setColor(QPalette::ToolTipText, theme.text);
|
pal.setColor(QPalette::ToolTipText, theme.text);
|
||||||
pal.setColor(QPalette::Mid, theme.border);
|
pal.setColor(QPalette::Mid, theme.border);
|
||||||
pal.setColor(QPalette::Dark, theme.background);
|
pal.setColor(QPalette::Dark, theme.background);
|
||||||
pal.setColor(QPalette::Light, theme.textFaint);
|
pal.setColor(QPalette::Light, theme.textFaint);
|
||||||
|
pal.setColor(QPalette::Link, theme.indHoverSpan);
|
||||||
|
|
||||||
|
// Disabled group: Fusion reads these for disabled menu items, buttons, etc.
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::WindowText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Text, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::ButtonText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::HighlightedText, theme.textMuted);
|
||||||
|
pal.setColor(QPalette::Disabled, QPalette::Light, theme.background);
|
||||||
|
|
||||||
qApp->setPalette(pal);
|
qApp->setPalette(pal);
|
||||||
|
|
||||||
qApp->setStyleSheet(QStringLiteral(
|
qApp->setStyleSheet(QString());
|
||||||
"QMenu {"
|
|
||||||
" background-color: %1;"
|
|
||||||
" color: %2;"
|
|
||||||
" border: 1px solid %3;"
|
|
||||||
" padding: 4px 6px;"
|
|
||||||
"}"
|
|
||||||
"QMenu::item { padding: 4px 24px; }"
|
|
||||||
"QMenu::item:selected { background-color: %4; }"
|
|
||||||
"QMenu::separator { height: 1px; background: %3; margin: 4px 8px; }"
|
|
||||||
"QMenu::item:disabled { color: %5; }"
|
|
||||||
"QToolTip {"
|
|
||||||
" background-color: %1;"
|
|
||||||
" color: %2;"
|
|
||||||
" border: 1px solid %3;"
|
|
||||||
"}")
|
|
||||||
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name(),
|
|
||||||
theme.hover.name(), theme.textMuted.name()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace rcx {
|
namespace rcx {
|
||||||
@@ -227,10 +260,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
|||||||
createStatusBar();
|
createStatusBar();
|
||||||
|
|
||||||
|
|
||||||
// Larger click targets on menu bar
|
// MenuBarStyle is set as app style in main() — covers both QMenuBar and QMenu
|
||||||
{
|
|
||||||
menuBar()->setStyle(new MenuBarStyle(menuBar()->style()));
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
connect(&ThemeManager::instance(), &ThemeManager::themeChanged,
|
||||||
this, &MainWindow::applyTheme);
|
this, &MainWindow::applyTheme);
|
||||||
@@ -355,9 +385,11 @@ void MainWindow::createStatusBar() {
|
|||||||
statusBar()->addWidget(m_statusLabel, 1);
|
statusBar()->addWidget(m_statusLabel, 1);
|
||||||
{
|
{
|
||||||
const auto& t = ThemeManager::instance().current();
|
const auto& t = ThemeManager::instance().current();
|
||||||
statusBar()->setStyleSheet(QStringLiteral(
|
QPalette sbPal = statusBar()->palette();
|
||||||
"QStatusBar { background: %1; color: %2; }")
|
sbPal.setColor(QPalette::Window, t.background);
|
||||||
.arg(t.backgroundAlt.name(), t.textDim.name()));
|
sbPal.setColor(QPalette::WindowText, t.textDim);
|
||||||
|
statusBar()->setPalette(sbPal);
|
||||||
|
statusBar()->setAutoFillBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSettings settings("ReclassX", "ReclassX");
|
QSettings settings("ReclassX", "ReclassX");
|
||||||
@@ -786,8 +818,12 @@ void MainWindow::about() {
|
|||||||
});
|
});
|
||||||
lay->addWidget(ghBtn, 0, Qt::AlignCenter);
|
lay->addWidget(ghBtn, 0, Qt::AlignCenter);
|
||||||
|
|
||||||
dlg.setStyleSheet(QStringLiteral("QDialog { background: %1; }")
|
{
|
||||||
.arg(ThemeManager::instance().current().background.name()));
|
QPalette dlgPal = dlg.palette();
|
||||||
|
dlgPal.setColor(QPalette::Window, ThemeManager::instance().current().background);
|
||||||
|
dlg.setPalette(dlgPal);
|
||||||
|
dlg.setAutoFillBackground(true);
|
||||||
|
}
|
||||||
dlg.exec();
|
dlg.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,9 +853,12 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
theme.backgroundAlt.name(), theme.hover.name()));
|
theme.backgroundAlt.name(), theme.hover.name()));
|
||||||
|
|
||||||
// Status bar
|
// Status bar
|
||||||
statusBar()->setStyleSheet(QStringLiteral(
|
{
|
||||||
"QStatusBar { background: %1; color: %2; }")
|
QPalette sbPal = statusBar()->palette();
|
||||||
.arg(theme.backgroundAlt.name(), theme.textDim.name()));
|
sbPal.setColor(QPalette::Window, theme.background);
|
||||||
|
sbPal.setColor(QPalette::WindowText, theme.textDim);
|
||||||
|
statusBar()->setPalette(sbPal);
|
||||||
|
}
|
||||||
|
|
||||||
// Split pane tab widgets
|
// Split pane tab widgets
|
||||||
for (auto& state : m_tabs) {
|
for (auto& state : m_tabs) {
|
||||||
@@ -831,14 +870,21 @@ void MainWindow::applyTheme(const Theme& theme) {
|
|||||||
|
|
||||||
void MainWindow::editTheme() {
|
void MainWindow::editTheme() {
|
||||||
auto& tm = ThemeManager::instance();
|
auto& tm = ThemeManager::instance();
|
||||||
Theme edited = tm.current();
|
|
||||||
ThemeEditor dlg(edited, this);
|
|
||||||
if (dlg.exec() == QDialog::Accepted) {
|
|
||||||
edited = dlg.result();
|
|
||||||
int idx = tm.currentIndex();
|
int idx = tm.currentIndex();
|
||||||
if (idx < tm.themes().size() && idx >= 0) {
|
ThemeEditor dlg(idx, this);
|
||||||
tm.updateTheme(idx, edited);
|
if (dlg.exec() == QDialog::Accepted) {
|
||||||
}
|
tm.revertPreview();
|
||||||
|
int selectedIdx = dlg.selectedIndex();
|
||||||
|
Theme edited = dlg.result();
|
||||||
|
// Switch to selected theme first (if changed)
|
||||||
|
if (selectedIdx != idx && selectedIdx >= 0 && selectedIdx < tm.themes().size())
|
||||||
|
tm.setCurrent(selectedIdx);
|
||||||
|
// Apply edits
|
||||||
|
int applyIdx = selectedIdx >= 0 ? selectedIdx : idx;
|
||||||
|
if (applyIdx >= 0 && applyIdx < tm.themes().size())
|
||||||
|
tm.updateTheme(applyIdx, edited);
|
||||||
|
} else {
|
||||||
|
tm.revertPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,6 +916,8 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
|||||||
m_workspaceTree->setFont(f);
|
m_workspaceTree->setFont(f);
|
||||||
// Sync status bar font
|
// Sync status bar font
|
||||||
statusBar()->setFont(f);
|
statusBar()->setFont(f);
|
||||||
|
// Sync menu bar / menu font via global stylesheet
|
||||||
|
applyGlobalTheme(ThemeManager::instance().current());
|
||||||
}
|
}
|
||||||
|
|
||||||
RcxController* MainWindow::activeController() const {
|
RcxController* MainWindow::activeController() const {
|
||||||
@@ -1396,7 +1444,7 @@ int main(int argc, char* argv[]) {
|
|||||||
DarkApp app(argc, argv);
|
DarkApp app(argc, argv);
|
||||||
app.setApplicationName("ReclassX");
|
app.setApplicationName("ReclassX");
|
||||||
app.setOrganizationName("ReclassX");
|
app.setOrganizationName("ReclassX");
|
||||||
app.setStyle("Fusion"); // Fusion style respects dark palette well
|
app.setStyle(new MenuBarStyle("Fusion")); // Fusion + generous menu sizing
|
||||||
|
|
||||||
// Load embedded fonts
|
// Load embedded fonts
|
||||||
int fontId = QFontDatabase::addApplicationFont(":/fonts/JetBrainsMono.ttf");
|
int fontId = QFontDatabase::addApplicationFont(":/fonts/JetBrainsMono.ttf");
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
#include <QFocusEvent>
|
#include <QFocusEvent>
|
||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QProxyStyle>
|
||||||
|
#include <QStyleOption>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QScreen>
|
||||||
#include <Qsci/qsciscintilla.h>
|
#include <Qsci/qsciscintilla.h>
|
||||||
#include <Qsci/qsciscintillabase.h>
|
#include <Qsci/qsciscintillabase.h>
|
||||||
#include "editor.h"
|
#include "editor.h"
|
||||||
@@ -473,27 +480,17 @@ private slots:
|
|||||||
QCOMPARE(cancelSpy.count(), 0);
|
QCOMPARE(cancelSpy.count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: type edit begins and can be cancelled ──
|
// ── Test: type edit emits typePickerRequested (popup-based, not inline edit) ──
|
||||||
void testTypeEditCancel() {
|
void testTypeEditCancel() {
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
// Begin type edit on a field line
|
QSignalSpy spy(m_editor, &RcxEditor::typePickerRequested);
|
||||||
|
|
||||||
|
// Begin type edit on a field line — now handled by TypeSelectorPopup
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
||||||
QVERIFY(ok);
|
QVERIFY(ok);
|
||||||
QVERIFY(m_editor->isEditing());
|
QCOMPARE(spy.count(), 1);
|
||||||
|
// Type editing uses popup, not inline edit state
|
||||||
// Process deferred events (showTypeAutocomplete is deferred via QTimer)
|
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// First Escape closes autocomplete popup (if active) or cancels edit
|
|
||||||
QKeyEvent esc1(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &esc1);
|
|
||||||
|
|
||||||
// If autocomplete was open, first Esc only closed popup; need second Esc
|
|
||||||
if (m_editor->isEditing()) {
|
|
||||||
QKeyEvent esc2(QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &esc2);
|
|
||||||
}
|
|
||||||
QVERIFY(!m_editor->isEditing());
|
QVERIFY(!m_editor->isEditing());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,11 +520,11 @@ private slots:
|
|||||||
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine);
|
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine);
|
||||||
QApplication::processEvents();
|
QApplication::processEvents();
|
||||||
|
|
||||||
// Type edit on header should succeed
|
// Type edit on header should succeed (emits popup signal, not inline edit)
|
||||||
|
QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested);
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, headerLine);
|
bool ok = m_editor->beginInlineEdit(EditTarget::Type, headerLine);
|
||||||
QVERIFY(ok);
|
QVERIFY(ok);
|
||||||
QVERIFY(m_editor->isEditing());
|
QCOMPARE(typeSpy.count(), 1);
|
||||||
m_editor->cancelInlineEdit();
|
|
||||||
|
|
||||||
// Name edit on header should succeed
|
// Name edit on header should succeed
|
||||||
ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine);
|
ok = m_editor->beginInlineEdit(EditTarget::Name, headerLine);
|
||||||
@@ -598,35 +595,19 @@ private slots:
|
|||||||
void testTypeAutocompleteTypingAndCommit() {
|
void testTypeAutocompleteTypingAndCommit() {
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
|
QSignalSpy spy(m_editor, &RcxEditor::typePickerRequested);
|
||||||
|
|
||||||
|
// Type edit now emits typePickerRequested for TypeSelectorPopup
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
||||||
QVERIFY(ok);
|
QVERIFY(ok);
|
||||||
|
|
||||||
// Autocomplete is deferred via QTimer::singleShot(0) — poll until active
|
|
||||||
QTRY_VERIFY2(m_editor->scintilla()->SendScintilla(
|
|
||||||
QsciScintillaBase::SCI_AUTOCACTIVE) != 0,
|
|
||||||
"Autocomplete should be active");
|
|
||||||
|
|
||||||
// Simulate typing 'i' — filters to typeName entries starting with 'i'
|
|
||||||
QKeyEvent keyI(QEvent::KeyPress, Qt::Key_I, Qt::NoModifier, "i");
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &keyI);
|
|
||||||
|
|
||||||
// Still editing
|
|
||||||
QVERIFY(m_editor->isEditing());
|
|
||||||
|
|
||||||
// Simulate Enter to select from autocomplete (handled synchronously)
|
|
||||||
QSignalSpy spy(m_editor, &RcxEditor::inlineEditCommitted);
|
|
||||||
QKeyEvent enter(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
|
|
||||||
QApplication::sendEvent(m_editor->scintilla(), &enter);
|
|
||||||
|
|
||||||
// Should have committed immediately (no deferred timer for type edits)
|
|
||||||
QCOMPARE(spy.count(), 1);
|
QCOMPARE(spy.count(), 1);
|
||||||
QVERIFY(!m_editor->isEditing());
|
|
||||||
|
|
||||||
// The committed text should be a valid typeName starting with 'i'
|
// Verify signal carries valid nodeIdx (second arg)
|
||||||
QList<QVariant> args = spy.first();
|
QList<QVariant> args = spy.first();
|
||||||
QString committedText = args.at(3).toString();
|
QVERIFY(args.at(1).toInt() >= 0);
|
||||||
QVERIFY2(committedText.startsWith('i'),
|
|
||||||
qPrintable("Expected typeName starting with 'i', got: " + committedText));
|
// No inline edit state — popup handles everything
|
||||||
|
QVERIFY(!m_editor->isEditing());
|
||||||
|
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
}
|
}
|
||||||
@@ -635,28 +616,15 @@ private slots:
|
|||||||
void testTypeEditClickAwayNoChange() {
|
void testTypeEditClickAwayNoChange() {
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
|
QSignalSpy spy(m_editor, &RcxEditor::typePickerRequested);
|
||||||
|
|
||||||
|
// Type edit emits typePickerRequested (popup handles click-away)
|
||||||
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
bool ok = m_editor->beginInlineEdit(EditTarget::Type, kFirstDataLine);
|
||||||
QVERIFY(ok);
|
QVERIFY(ok);
|
||||||
|
QCOMPARE(spy.count(), 1);
|
||||||
|
|
||||||
// Process deferred autocomplete
|
// No inline edit state — popup handles click-away behavior
|
||||||
QApplication::processEvents();
|
|
||||||
|
|
||||||
// Click away on viewport — should commit (not cancel)
|
|
||||||
QSignalSpy commitSpy(m_editor, &RcxEditor::inlineEditCommitted);
|
|
||||||
QMouseEvent click(QEvent::MouseButtonPress, QPointF(10, 10),
|
|
||||||
QPointF(10, 10), Qt::LeftButton,
|
|
||||||
Qt::LeftButton, Qt::NoModifier);
|
|
||||||
QApplication::sendEvent(m_editor->scintilla()->viewport(), &click);
|
|
||||||
|
|
||||||
QVERIFY(!m_editor->isEditing());
|
QVERIFY(!m_editor->isEditing());
|
||||||
QCOMPARE(commitSpy.count(), 1);
|
|
||||||
|
|
||||||
// The committed text should be the original typeName (no change)
|
|
||||||
// First field at kFirstDataLine is InheritedAddressSpace (UInt8 → "uint8_t")
|
|
||||||
QList<QVariant> args = commitSpy.first();
|
|
||||||
QString committedText = args.at(3).toString();
|
|
||||||
QVERIFY2(committedText == "uint8_t",
|
|
||||||
qPrintable("Expected 'uint8_t', got: " + committedText));
|
|
||||||
|
|
||||||
m_editor->applyDocument(m_result);
|
m_editor->applyDocument(m_result);
|
||||||
}
|
}
|
||||||
@@ -813,12 +781,11 @@ private slots:
|
|||||||
QVERIFY(m_editor->isEditing());
|
QVERIFY(m_editor->isEditing());
|
||||||
m_editor->cancelInlineEdit();
|
m_editor->cancelInlineEdit();
|
||||||
|
|
||||||
// Type edit on Padding SHOULD succeed
|
// Type edit on Padding SHOULD succeed (emits popup signal)
|
||||||
|
QSignalSpy typeSpy(m_editor, &RcxEditor::typePickerRequested);
|
||||||
ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine);
|
ok = m_editor->beginInlineEdit(EditTarget::Type, paddingLine);
|
||||||
QVERIFY2(ok, "Type edit should be allowed on Padding lines");
|
QVERIFY2(ok, "Type edit should be allowed on Padding lines");
|
||||||
QVERIFY(m_editor->isEditing());
|
QCOMPARE(typeSpy.count(), 1);
|
||||||
m_editor->cancelInlineEdit();
|
|
||||||
QApplication::processEvents(); // flush deferred autocomplete timer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ──
|
// ── Test: resolvedSpanFor rejects Value on Padding (defense-in-depth) ──
|
||||||
@@ -1096,6 +1063,247 @@ private slots:
|
|||||||
QVERIFY2(!foundRootHeader,
|
QVERIFY2(!foundRootHeader,
|
||||||
"Root header should be suppressed from compose output");
|
"Root header should be suppressed from compose output");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: MenuBarStyle gives QMenu items generous click targets ──
|
||||||
|
// ── Test: M_ACCENT marker appears on selected rows ──
|
||||||
|
void testAccentMarkerOnSelectedRows() {
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
|
||||||
|
// Find a data line with a valid nodeId
|
||||||
|
uint64_t targetId = 0;
|
||||||
|
int targetLine = -1;
|
||||||
|
for (int i = kFirstDataLine; i < m_result.meta.size(); i++) {
|
||||||
|
const auto& lm = m_result.meta[i];
|
||||||
|
if (lm.nodeId != 0 && lm.nodeId != kCommandRowId
|
||||||
|
&& lm.lineKind == LineKind::Field) {
|
||||||
|
targetId = lm.nodeId;
|
||||||
|
targetLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(targetLine >= 0, "No data line found for accent test");
|
||||||
|
|
||||||
|
// Apply selection overlay with that node
|
||||||
|
QSet<uint64_t> selIds;
|
||||||
|
selIds.insert(targetId);
|
||||||
|
m_editor->applySelectionOverlay(selIds);
|
||||||
|
|
||||||
|
auto* sci = m_editor->scintilla();
|
||||||
|
|
||||||
|
// Direct test: add M_ACCENT manually and read it back
|
||||||
|
int directHandle = sci->markerAdd(targetLine, M_ACCENT);
|
||||||
|
int directMarkers = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_MARKERGET, (unsigned long)targetLine);
|
||||||
|
QVERIFY2(directMarkers & (1 << M_ACCENT),
|
||||||
|
qPrintable(QString("Direct markerAdd(M_ACCENT=%1) failed on line %2 (handle=%3, mask=0x%4)")
|
||||||
|
.arg(M_ACCENT).arg(targetLine).arg(directHandle).arg(directMarkers, 0, 16)));
|
||||||
|
sci->markerDelete(targetLine, M_ACCENT);
|
||||||
|
|
||||||
|
// Now test via applySelectionOverlay
|
||||||
|
m_editor->applySelectionOverlay(selIds);
|
||||||
|
|
||||||
|
// Verify M_SELECTED is set on the target line
|
||||||
|
int markers = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_MARKERGET, (unsigned long)targetLine);
|
||||||
|
QVERIFY2(markers & (1 << M_SELECTED),
|
||||||
|
qPrintable(QString("M_SELECTED not set on line %1 (mask=0x%2)")
|
||||||
|
.arg(targetLine).arg(markers, 0, 16)));
|
||||||
|
|
||||||
|
// Verify M_ACCENT is set on the target line
|
||||||
|
QVERIFY2(markers & (1 << M_ACCENT),
|
||||||
|
qPrintable(QString("M_ACCENT not set on line %1 (mask=0x%2)")
|
||||||
|
.arg(targetLine).arg(markers, 0, 16)));
|
||||||
|
|
||||||
|
// Verify a non-selected line does NOT have M_ACCENT
|
||||||
|
int otherLine = -1;
|
||||||
|
for (int i = kFirstDataLine; i < m_result.meta.size(); i++) {
|
||||||
|
const auto& lm = m_result.meta[i];
|
||||||
|
if (lm.nodeId != targetId && lm.nodeId != 0
|
||||||
|
&& lm.nodeId != kCommandRowId && lm.lineKind == LineKind::Field) {
|
||||||
|
otherLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (otherLine >= 0) {
|
||||||
|
int otherMarkers = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_MARKERGET, (unsigned long)otherLine);
|
||||||
|
QVERIFY2(!(otherMarkers & (1 << M_ACCENT)),
|
||||||
|
qPrintable(QString("M_ACCENT should NOT be set on non-selected line %1 (mask=0x%2)")
|
||||||
|
.arg(otherLine).arg(otherMarkers, 0, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection and verify accent is removed
|
||||||
|
m_editor->applySelectionOverlay(QSet<uint64_t>());
|
||||||
|
markers = (int)sci->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_MARKERGET, (unsigned long)targetLine);
|
||||||
|
QVERIFY2(!(markers & (1 << M_ACCENT)),
|
||||||
|
qPrintable(QString("M_ACCENT should be cleared after deselection on line %1 (mask=0x%2)")
|
||||||
|
.arg(targetLine).arg(markers, 0, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMenuItemSizeIsAccessible() {
|
||||||
|
// Instantiate the same QProxyStyle used by the app (MenuBarStyle is
|
||||||
|
// defined in main.cpp — we replicate the logic here to test it)
|
||||||
|
class TestMenuStyle : public QProxyStyle {
|
||||||
|
public:
|
||||||
|
using QProxyStyle::QProxyStyle;
|
||||||
|
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
|
||||||
|
const QSize& sz, const QWidget* w) const override {
|
||||||
|
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
|
||||||
|
if (type == CT_MenuBarItem)
|
||||||
|
s.setHeight(s.height() + qRound(s.height() * 0.5));
|
||||||
|
if (type == CT_MenuItem)
|
||||||
|
s = QSize(s.width() + 24, s.height() + 4);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TestMenuStyle style;
|
||||||
|
QMenu menu;
|
||||||
|
auto* action = menu.addAction("Delete Node");
|
||||||
|
|
||||||
|
QStyleOptionMenuItem opt;
|
||||||
|
opt.initFrom(&menu);
|
||||||
|
opt.text = action->text();
|
||||||
|
|
||||||
|
QSize base = style.QProxyStyle::sizeFromContents(
|
||||||
|
QStyle::CT_MenuItem, &opt, QSize(80, 20), &menu);
|
||||||
|
QSize styled = style.sizeFromContents(
|
||||||
|
QStyle::CT_MenuItem, &opt, QSize(80, 20), &menu);
|
||||||
|
|
||||||
|
// Width must grow by at least 24px
|
||||||
|
QVERIFY2(styled.width() >= base.width() + 24,
|
||||||
|
qPrintable(QString("Menu item width %1 too narrow (base %2, need +24)")
|
||||||
|
.arg(styled.width()).arg(base.width())));
|
||||||
|
|
||||||
|
// Height must grow by at least 4px
|
||||||
|
QVERIFY2(styled.height() >= base.height() + 4,
|
||||||
|
qPrintable(QString("Menu item height %1 too short (base %2, need +4)")
|
||||||
|
.arg(styled.height()).arg(base.height())));
|
||||||
|
}
|
||||||
|
|
||||||
|
void testMenuHoverRendersAmberText() {
|
||||||
|
// Replicate MenuBarStyle with drawControl hover override
|
||||||
|
class TestMenuStyle : public QProxyStyle {
|
||||||
|
public:
|
||||||
|
using QProxyStyle::QProxyStyle;
|
||||||
|
QSize sizeFromContents(ContentsType type, const QStyleOption* opt,
|
||||||
|
const QSize& sz, const QWidget* w) const override {
|
||||||
|
QSize s = QProxyStyle::sizeFromContents(type, opt, sz, w);
|
||||||
|
if (type == CT_MenuBarItem)
|
||||||
|
s.setHeight(s.height() + qRound(s.height() * 0.5));
|
||||||
|
if (type == CT_MenuItem)
|
||||||
|
s = QSize(s.width() + 24, s.height() + 4);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
void drawPrimitive(PrimitiveElement elem, const QStyleOption* opt,
|
||||||
|
QPainter* p, const QWidget* w) const override {
|
||||||
|
if (elem == PE_FrameMenu) return;
|
||||||
|
QProxyStyle::drawPrimitive(elem, opt, p, w);
|
||||||
|
}
|
||||||
|
void drawControl(ControlElement element, const QStyleOption* opt,
|
||||||
|
QPainter* p, const QWidget* w) const override {
|
||||||
|
if (element == CE_MenuItem || element == CE_MenuBarItem) {
|
||||||
|
if (auto* mi = qstyleoption_cast<const QStyleOptionMenuItem*>(opt)) {
|
||||||
|
if ((mi->state & State_Selected)
|
||||||
|
&& mi->menuItemType != QStyleOptionMenuItem::Separator) {
|
||||||
|
QStyleOptionMenuItem patched = *mi;
|
||||||
|
patched.palette.setColor(QPalette::Highlight,
|
||||||
|
mi->palette.color(QPalette::Mid));
|
||||||
|
patched.palette.setColor(QPalette::HighlightedText,
|
||||||
|
mi->palette.color(QPalette::Link));
|
||||||
|
QProxyStyle::drawControl(element, &patched, p, w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QProxyStyle::drawControl(element, opt, p, w);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install our style as the app style (same as main.cpp does)
|
||||||
|
qApp->setStyle(new TestMenuStyle("Fusion"));
|
||||||
|
|
||||||
|
// Set app palette matching applyGlobalTheme for Reclass Dark
|
||||||
|
QPalette pal;
|
||||||
|
pal.setColor(QPalette::Window, QColor("#1e1e1e"));
|
||||||
|
pal.setColor(QPalette::WindowText, QColor("#d4d4d4"));
|
||||||
|
pal.setColor(QPalette::Base, QColor("#252526"));
|
||||||
|
pal.setColor(QPalette::AlternateBase, QColor("#2a2d2e"));
|
||||||
|
pal.setColor(QPalette::Text, QColor("#d4d4d4"));
|
||||||
|
pal.setColor(QPalette::Button, QColor("#333333"));
|
||||||
|
pal.setColor(QPalette::ButtonText, QColor("#d4d4d4"));
|
||||||
|
pal.setColor(QPalette::Highlight, QColor("#2b2b2b"));
|
||||||
|
pal.setColor(QPalette::HighlightedText, QColor("#E6B450"));
|
||||||
|
pal.setColor(QPalette::Mid, QColor("#3c3c3c"));
|
||||||
|
pal.setColor(QPalette::Dark, QColor("#1e1e1e"));
|
||||||
|
pal.setColor(QPalette::Light, QColor("#505050"));
|
||||||
|
pal.setColor(QPalette::Link, QColor("#E6B450"));
|
||||||
|
qApp->setPalette(pal);
|
||||||
|
|
||||||
|
// Build and show a real QMenu
|
||||||
|
QMenu menu;
|
||||||
|
menu.addAction("First Item");
|
||||||
|
menu.addAction("Second Item");
|
||||||
|
menu.addAction("Third Item");
|
||||||
|
menu.popup(QPoint(100, 100));
|
||||||
|
QVERIFY(QTest::qWaitForWindowExposed(&menu));
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// ── Deliver real mouse events to trigger hover on second item ──
|
||||||
|
QList<QAction*> actions = menu.actions();
|
||||||
|
QRect itemRect = menu.actionGeometry(actions[1]);
|
||||||
|
QPoint localCenter = itemRect.center();
|
||||||
|
|
||||||
|
// Enter event — tells QMenu the mouse is inside
|
||||||
|
QEvent enter(QEvent::Enter);
|
||||||
|
QApplication::sendEvent(&menu, &enter);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// MouseMove to the second item — triggers hover/select
|
||||||
|
QMouseEvent move(QEvent::MouseMove, QPointF(localCenter),
|
||||||
|
menu.mapToGlobal(localCenter),
|
||||||
|
Qt::NoButton, Qt::NoButton, Qt::NoModifier);
|
||||||
|
QApplication::sendEvent(&menu, &move);
|
||||||
|
QApplication::processEvents();
|
||||||
|
QTest::qWait(50); // let repaint settle
|
||||||
|
|
||||||
|
// Verify QMenu internally considers the action hovered
|
||||||
|
QVERIFY2(menu.activeAction() == actions[1],
|
||||||
|
"QMenu did not set activeAction after mouse move — "
|
||||||
|
"hover event delivery failed");
|
||||||
|
|
||||||
|
// ── Capture what's actually on screen ──
|
||||||
|
QScreen* screen = QGuiApplication::primaryScreen();
|
||||||
|
QVERIFY(screen);
|
||||||
|
QPixmap grab = screen->grabWindow(menu.winId());
|
||||||
|
QImage img = grab.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||||
|
|
||||||
|
// Crop to just the hovered item rect
|
||||||
|
QImage itemImg = img.copy(itemRect);
|
||||||
|
|
||||||
|
// Scan hovered item for amber pixels (E6B450 = R:230 G:180 B:80)
|
||||||
|
int amberPixels = 0;
|
||||||
|
int totalPixels = itemImg.width() * itemImg.height();
|
||||||
|
for (int y = 0; y < itemImg.height(); ++y) {
|
||||||
|
for (int x = 0; x < itemImg.width(); ++x) {
|
||||||
|
QColor c = itemImg.pixelColor(x, y);
|
||||||
|
if (c.red() > 180 && c.green() > 140 && c.blue() < 100)
|
||||||
|
++amberPixels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always save screenshots so we can visually inspect
|
||||||
|
img.save("menu_hover_full.png");
|
||||||
|
itemImg.save("menu_hover_item.png");
|
||||||
|
|
||||||
|
menu.close();
|
||||||
|
|
||||||
|
QVERIFY2(amberPixels > 10,
|
||||||
|
qPrintable(QString("Expected amber text pixels in hovered item, "
|
||||||
|
"found %1 / %2 total (see menu_hover_full.png, menu_hover_item.png)")
|
||||||
|
.arg(amberPixels).arg(totalPixels)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestEditor)
|
QTEST_MAIN(TestEditor)
|
||||||
|
|||||||
Reference in New Issue
Block a user