// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "DockMenuBar.h" #include #include #include #include #include static constexpr int TAB_BAR_TOP_MARGIN = 2; static constexpr int RIGHT_MARGIN = 2; DockMenuBar::DockMenuBar(QWidget* original_menu_bar, QWidget* parent) : QWidget(parent) , m_original_menu_bar(original_menu_bar) { QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, RIGHT_MARGIN, 0); QWidget* menu_bar_wrapper = new QWidget; layout->addWidget(menu_bar_wrapper); QVBoxLayout* menu_bar_layout = new QVBoxLayout(menu_bar_wrapper); menu_bar_layout->setContentsMargins(0, 0, 0, 0); menu_bar_layout->addWidget(original_menu_bar, 0, Qt::AlignVCenter); QWidget* layout_switcher_wrapper = new QWidget; layout->addWidget(layout_switcher_wrapper); m_layout_switcher_layout = new QVBoxLayout(layout_switcher_wrapper); m_layout_switcher = new QTabBar; m_layout_switcher->setContextMenuPolicy(Qt::CustomContextMenu); m_layout_switcher->setDrawBase(false); m_layout_switcher->setExpanding(false); m_layout_switcher->setMovable(true); connect(m_layout_switcher, &QTabBar::tabMoved, this, [this](int from, int to) { DockLayout::Index from_index = static_cast(from); DockLayout::Index to_index = static_cast(to); emit layoutMoved(from_index, to_index); }); connect(m_layout_switcher, &QTabBar::customContextMenuRequested, this, [this](const QPoint& pos) { emit layoutSwitcherContextMenuRequested(pos, m_layout_switcher); }); m_blink_timer = new QTimer(this); connect(m_blink_timer, &QTimer::timeout, this, &DockMenuBar::updateBlink); m_layout_locked_toggle = new QPushButton; m_layout_locked_toggle->setCheckable(true); connect(m_layout_locked_toggle, &QPushButton::clicked, this, [this](bool checked) { if (m_ignore_lock_state_changed) return; emit lockButtonToggled(checked); }); layout->addWidget(m_layout_locked_toggle); layout->setStretchFactor(menu_bar_wrapper, 0); layout->setStretchFactor(layout_switcher_wrapper, 1); layout->setStretchFactor(m_layout_locked_toggle, 0); updateTheme(); } void DockMenuBar::updateTheme() { DockMenuBarStyle* style = new DockMenuBarStyle(m_layout_switcher); m_original_menu_bar->setStyle(style); m_layout_switcher->setStyle(style); m_layout_locked_toggle->setStyle(style); delete m_style; m_style = style; // Vertically centre the layout switcher tabs for the Windows 11 style // because I think it looks better. Do the same for macOS too. if (style->baseStyle()->name() == "windows11" || style->baseStyle()->name() == "macOS") { m_layout_switcher_layout->setContentsMargins(0, 0, 0, 0); m_layout_switcher_layout->addWidget(m_layout_switcher, 0, Qt::AlignVCenter); } else { m_layout_switcher_layout->setContentsMargins(0, TAB_BAR_TOP_MARGIN, 0, 0); m_layout_switcher_layout->addWidget(m_layout_switcher, 0, Qt::AlignBottom); } } void DockMenuBar::updateLayoutSwitcher(DockLayout::Index current_index, const std::vector& layouts) { disconnect(m_tab_connection); for (int i = m_layout_switcher->count(); i > 0; i--) m_layout_switcher->removeTab(i - 1); for (const DockLayout& layout : layouts) { const char* cpu_name = DebugInterface::cpuName(layout.cpu()); QString tab_name = tr("%1 (%2)").arg(layout.name()).arg(cpu_name); m_layout_switcher->addTab(tab_name); } m_plus_tab_index = m_layout_switcher->addTab("+"); m_current_tab_index = current_index; if (current_index != DockLayout::INVALID_INDEX) m_layout_switcher->setCurrentIndex(current_index); else m_layout_switcher->setCurrentIndex(m_plus_tab_index); // If we don't have any layouts, the currently selected tab will never be // changed, so we respond to all clicks instead. if (m_plus_tab_index > 0) m_tab_connection = connect(m_layout_switcher, &QTabBar::currentChanged, this, &DockMenuBar::tabChanged); else m_tab_connection = connect(m_layout_switcher, &QTabBar::tabBarClicked, this, &DockMenuBar::tabChanged); stopBlink(); } void DockMenuBar::onCurrentLayoutChanged(DockLayout::Index current_index) { m_ignore_current_tab_changed = true; if (current_index != DockLayout::INVALID_INDEX) m_layout_switcher->setCurrentIndex(current_index); else m_layout_switcher->setCurrentIndex(m_plus_tab_index); m_ignore_current_tab_changed = false; } void DockMenuBar::onLockStateChanged(bool layout_locked) { m_ignore_lock_state_changed = true; m_layout_locked_toggle->setChecked(layout_locked); if (layout_locked) { m_layout_locked_toggle->setText(tr("Layout Locked")); m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-lock"))); } else { m_layout_locked_toggle->setText(tr("Layout Unlocked")); m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-unlock"))); } m_ignore_lock_state_changed = false; } void DockMenuBar::startBlink(DockLayout::Index layout_index) { stopBlink(); if (layout_index == DockLayout::INVALID_INDEX) return; m_blink_tab = static_cast(layout_index); m_blink_stage = 0; m_blink_timer->start(500); updateBlink(); } void DockMenuBar::updateBlink() { if (m_blink_tab < m_layout_switcher->count()) { if (m_blink_stage % 2 == 0) m_layout_switcher->setTabTextColor(m_blink_tab, Qt::red); else m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color()); } m_blink_stage++; if (m_blink_stage > 7) m_blink_timer->stop(); } void DockMenuBar::stopBlink() { if (m_blink_timer->isActive()) { if (m_blink_tab < m_layout_switcher->count()) m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color()); m_blink_timer->stop(); } } void DockMenuBar::paintEvent(QPaintEvent* event) { QPainter painter(this); // This fixes the background colour of the menu bar when using the Windows // Vista style. QStyleOptionMenuItem menu_option; menu_option.palette = palette(); menu_option.state = QStyle::State_None; menu_option.menuItemType = QStyleOptionMenuItem::EmptyArea; menu_option.checkType = QStyleOptionMenuItem::NotCheckable; menu_option.rect = rect(); menu_option.menuRect = rect(); style()->drawControl(QStyle::CE_MenuBarEmptyArea, &menu_option, &painter, this); } void DockMenuBar::tabChanged(int index) { // Prevent recursion. if (m_ignore_current_tab_changed) return; if (index < m_plus_tab_index) { DockLayout::Index layout_index = static_cast(index); emit currentLayoutChanged(layout_index); } else if (index == m_plus_tab_index) { emit newButtonClicked(); } } // ***************************************************************************** DockMenuBarStyle::DockMenuBarStyle(QObject* parent) : QProxyStyle(QStyleFactory::create(qApp->style()->name())) { setParent(parent); } void DockMenuBarStyle::drawControl( ControlElement element, const QStyleOption* option, QPainter* painter, const QWidget* widget) const { switch (element) { case CE_TabBarTab: { QProxyStyle::drawControl(element, option, painter, widget); // Draw a slick-looking highlight under the currently selected tab. if (baseStyle()->name() == "fusion") { const QStyleOptionTab* tab = qstyleoption_cast(option); if (tab && (tab->state & State_Selected)) { painter->setPen(tab->palette.highlight().color()); painter->drawLine(tab->rect.bottomLeft(), tab->rect.bottomRight()); } } return; } case CE_MenuBarItem: { const QStyleOptionMenuItem* opt = qstyleoption_cast(option); if (!opt) break; if (baseStyle()->name() != "fusion") break; // This mirrors a check in QFusionStyle::drawControl. If act is // false, QFusionStyle will try to draw a border along the bottom. bool act = opt->state & State_Selected && opt->state & State_Sunken; if (act) break; QCommonStyle::drawControl(element, option, painter, widget); return; } case CE_MenuBarEmptyArea: { // Prevent it from drawing a border in the wrong position. return; } default: { break; } } QProxyStyle::drawControl(element, option, painter, widget); } QSize DockMenuBarStyle::sizeFromContents( QStyle::ContentsType type, const QStyleOption* option, const QSize& contents_size, const QWidget* widget) const { QSize size = QProxyStyle::sizeFromContents(type, option, contents_size, widget); // Adjust the sizes of the layout switcher tabs depending on the theme. if (type == CT_TabBarTab) { const QStyleOptionTab* opt = qstyleoption_cast(option); if (!opt) return size; if (baseStyle()->name() == "windows11") { // Make the tabs a bit taller, otherwise there's an awkward margin. size.setHeight(size.height() + 4); } } return size; }