fix: Linux menu bar renders as horizontal tool buttons instead of collapsed extension popup

On Linux, QMenuBar inside a custom title bar widget (setMenuWidget) collapses
all items into the extension overflow popup. Replace with QToolButton widgets
on Linux that share the same QMenu objects. Includes hover-to-switch behavior
via event filter on open menus.

Windows and macOS paths are unchanged — guarded by #ifdef __linux__ and
runtime m_useToolButtons flag.
This commit is contained in:
Your Name
2026-03-14 15:51:31 -04:00
parent 97b6f55e1f
commit 4f2288048e
3 changed files with 83 additions and 1 deletions

View File

@@ -615,6 +615,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
createSymbolsDock();
createMenus();
if (m_titleBar) m_titleBar->finalizeMenuBar();
createStatusBar();
// Eliminate gap between central widget and status bar

View File

@@ -4,6 +4,7 @@
#include <QMouseEvent>
#include <QPainter>
#include <QStyle>
#include <QTimer>
#include <QWindow>
namespace rcx {
@@ -25,11 +26,23 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(m_appLabel);
// Menu bar
// Menu bar — hidden on Linux; visible on Windows.
// On Linux, QMenuBar inside a custom widget collapses all items into an
// extension popup. We keep it hidden and mirror its menus as QToolButtons
// via finalizeMenuBar() after createMenus() populates it.
m_menuBar = new QMenuBar(this);
m_menuBar->setNativeMenuBar(false);
#ifdef __linux__
m_useToolButtons = true;
m_menuBar->hide();
m_menuBtnLayout = new QHBoxLayout;
m_menuBtnLayout->setContentsMargins(0, 0, 0, 0);
m_menuBtnLayout->setSpacing(0);
layout->addLayout(m_menuBtnLayout);
#else
m_menuBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
layout->addWidget(m_menuBar);
#endif
layout->addStretch();
@@ -116,6 +129,17 @@ void TitleBarWidget::applyTheme(const Theme& theme) {
m_btnMin->setStyleSheet(btnStyle);
m_btnMax->setStyleSheet(btnStyle);
// Linux menu tool buttons
if (m_useToolButtons) {
QString menuBtnStyle = QStringLiteral(
"QToolButton { background: transparent; border: none; padding: 0 8px; color: %1; }"
"QToolButton:hover { background: %2; }"
"QToolButton::menu-indicator { image: none; }")
.arg(theme.text.name(), theme.hover.name());
for (auto* btn : m_menuButtons)
btn->setStyleSheet(menuBtnStyle);
}
// Close button: themed red hover
m_btnClose->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; }"
@@ -164,6 +188,58 @@ void TitleBarWidget::setMenuBarTitleCase(bool titleCase) {
action->setText("&" + result);
}
}
// Sync tool button labels on Linux
if (m_useToolButtons) {
auto actions = m_menuBar->actions();
for (int i = 0; i < m_menuButtons.size() && i < actions.size(); ++i)
m_menuButtons[i]->setText(actions[i]->text());
}
}
void TitleBarWidget::finalizeMenuBar() {
if (!m_useToolButtons) return;
// Create a QToolButton for each top-level menu in the hidden QMenuBar
for (auto* action : m_menuBar->actions()) {
if (!action->menu()) continue;
auto* btn = new QToolButton(this);
btn->setText(action->text());
btn->setMenu(action->menu());
btn->setPopupMode(QToolButton::InstantPopup);
btn->setAutoRaise(true);
btn->setFocusPolicy(Qt::NoFocus);
btn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
btn->setStyleSheet(QStringLiteral(
"QToolButton { background: transparent; border: none; padding: 0 8px; }"
"QToolButton:hover { background: %1; }"
"QToolButton::menu-indicator { image: none; }")
.arg(m_theme.hover.name()));
btn->installEventFilter(this);
btn->menu()->installEventFilter(this);
m_menuBtnLayout->addWidget(btn);
m_menuButtons.append(btn);
}
}
bool TitleBarWidget::eventFilter(QObject* obj, QEvent* event) {
if (!m_useToolButtons) return QWidget::eventFilter(obj, event);
// Watch for mouse movement inside an open QMenu — if the cursor moves
// over a sibling menu button, close this menu and open the other.
if (event->type() == QEvent::MouseMove) {
auto* menu = qobject_cast<QMenu*>(obj);
if (!menu || !menu->isVisible()) return false;
QPoint globalPos = QCursor::pos();
for (auto* btn : m_menuButtons) {
if (btn->menu() == menu) continue;
QRect btnRect(btn->mapToGlobal(QPoint(0, 0)), btn->size());
if (btnRect.contains(globalPos)) {
menu->close();
QTimer::singleShot(0, btn, [btn]() { btn->showMenu(); });
return true;
}
}
}
return QWidget::eventFilter(obj, event);
}
void TitleBarWidget::updateMaximizeIcon() {

View File

@@ -18,6 +18,7 @@ public:
void setShowIcon(bool show);
void setMenuBarTitleCase(bool titleCase);
bool menuBarTitleCase() const { return m_titleCase; }
void finalizeMenuBar();
void updateMaximizeIcon();
@@ -25,16 +26,20 @@ protected:
void mousePressEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void paintEvent(QPaintEvent* event) override;
bool eventFilter(QObject* obj, QEvent* event) override;
private:
QLabel* m_appLabel = nullptr;
QMenuBar* m_menuBar = nullptr;
QHBoxLayout* m_menuBtnLayout = nullptr;
QVector<QToolButton*> m_menuButtons;
QToolButton* m_btnMin = nullptr;
QToolButton* m_btnMax = nullptr;
QToolButton* m_btnClose = nullptr;
Theme m_theme;
bool m_titleCase = false;
bool m_useToolButtons = false;
QToolButton* makeChromeButton(const QString& iconPath);
void toggleMaximize();