#include "platformmenu.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../window/proxywindow.hpp" #include "../window/windowinterface.hpp" #include "iconprovider.hpp" #include "platformmenu_p.hpp" #include "popupanchor.hpp" #include "qsmenu.hpp" namespace qs::menu::platform { namespace { QVector> CREATION_HOOKS; // NOLINT PlatformMenuQMenu* ACTIVE_MENU = nullptr; // NOLINT } // namespace PlatformMenuQMenu::~PlatformMenuQMenu() { if (this == ACTIVE_MENU) { ACTIVE_MENU = nullptr; } } void PlatformMenuQMenu::setVisible(bool visible) { if (visible) { for (auto& hook: CREATION_HOOKS) { hook(this); } } else { if (this == ACTIVE_MENU) { ACTIVE_MENU = nullptr; } } this->QMenu::setVisible(visible); } PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(menu) { this->relayout(); // clang-format off QObject::connect(menu, &QsMenuEntry::enabledChanged, this, &PlatformMenuEntry::onEnabledChanged); QObject::connect(menu, &QsMenuEntry::textChanged, this, &PlatformMenuEntry::onTextChanged); QObject::connect(menu, &QsMenuEntry::iconChanged, this, &PlatformMenuEntry::onIconChanged); QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged); QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged); QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent); // clang-format on } PlatformMenuEntry::~PlatformMenuEntry() { this->clearChildren(); delete this->qaction; delete this->qmenu; } void PlatformMenuEntry::registerCreationHook(std::function hook) { CREATION_HOOKS.push_back(std::move(hook)); } bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) { QWindow* window = nullptr; if (qobject_cast(QCoreApplication::instance()) == nullptr) { qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " "QApplication mode."; qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " "root QML file and restart quickshell."; return false; } else if (this->qmenu == nullptr) { qCritical() << "Cannot display PlatformMenuEntry as it is not a menu."; return false; } else if (parentWindow == nullptr) { qCritical() << "Cannot display PlatformMenuEntry with null parent window."; return false; } else if (auto* proxy = qobject_cast(parentWindow)) { window = proxy->backingWindow(); } else if (auto* interface = qobject_cast(parentWindow)) { window = interface->proxyWindow()->backingWindow(); } else { qCritical() << "PlatformMenuEntry.display() must be called with a window."; return false; } if (window == nullptr) { qCritical() << "Cannot display PlatformMenuEntry from a parent window that is not visible."; return false; } if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { ACTIVE_MENU->close(); } ACTIVE_MENU = this->qmenu; auto point = window->mapToGlobal(QPoint(relativeX, relativeY)); this->qmenu->createWinId(); this->qmenu->windowHandle()->setTransientParent(window); // Skips screen edge repositioning so it can be left to the compositor on wayland. this->qmenu->targetPosition = point; this->qmenu->popup(point); return true; } bool PlatformMenuEntry::display(PopupAnchor* anchor) { if (qobject_cast(QCoreApplication::instance()) == nullptr) { qCritical() << "Cannot display PlatformMenuEntry as quickshell was not started in " "QApplication mode."; qCritical() << "To use platform menus, add `//@ pragma UseQApplication` to the top of your " "root QML file and restart quickshell."; return false; } else if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window."; return false; } if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { ACTIVE_MENU->close(); } ACTIVE_MENU = this->qmenu; this->qmenu->createWinId(); this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow()); // Update the window geometry to the menu's actual dimensions so reposition // can accurately adjust it if applicable for the current platform. this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()}); PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false); // Open the menu at the position determined by the popup positioner. this->qmenu->popup(this->qmenu->windowHandle()->position()); return true; } void PlatformMenuEntry::relayout() { if (qobject_cast(QCoreApplication::instance()) == nullptr) { return; } if (this->menu->hasChildren()) { delete this->qaction; this->qaction = nullptr; if (this->qmenu == nullptr) { this->qmenu = new PlatformMenuQMenu(); QObject::connect(this->qmenu, &QMenu::aboutToShow, this, &PlatformMenuEntry::onAboutToShow); QObject::connect(this->qmenu, &QMenu::aboutToHide, this, &PlatformMenuEntry::onAboutToHide); } else { this->clearChildren(); } this->qmenu->setTitle(this->menu->text()); auto icon = this->menu->icon(); if (!icon.isEmpty()) { this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon)); } auto children = this->menu->children(); auto len = children.count(&children); for (auto i = 0; i < len; i++) { auto* child = children.at(&children, i); auto* instance = new PlatformMenuEntry(child); QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed); QObject::connect( instance, &PlatformMenuEntry::relayoutParent, this, &PlatformMenuEntry::relayout ); this->childEntries.push_back(instance); instance->addToQMenu(this->qmenu); } } else if (!this->menu->isSeparator()) { this->clearChildren(); delete this->qmenu; this->qmenu = nullptr; if (this->qaction == nullptr) { this->qaction = new QAction(this); QObject::connect( this->qaction, &QAction::triggered, this, &PlatformMenuEntry::onActionTriggered ); } this->qaction->setText(this->menu->text()); auto icon = this->menu->icon(); if (!icon.isEmpty()) { this->qaction->setIcon(getCurrentEngineImageAsIcon(icon)); } this->qaction->setEnabled(this->menu->enabled()); this->qaction->setCheckable(this->menu->buttonType() != QsMenuButtonType::None); if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); this->qaction->setActionGroup(this->qactiongroup); } this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); } else { delete this->qmenu; delete this->qaction; this->qmenu = nullptr; this->qaction = nullptr; } } void PlatformMenuEntry::onAboutToShow() { this->menu->ref(); } void PlatformMenuEntry::onAboutToHide() { this->menu->unref(); emit this->closed(); } void PlatformMenuEntry::onActionTriggered() { auto* action = qobject_cast(this->sender()->parent()); emit action->menu->triggered(); } void PlatformMenuEntry::onChildDestroyed() { this->childEntries.removeOne(this->sender()); } void PlatformMenuEntry::onEnabledChanged() { if (this->qaction != nullptr) { this->qaction->setEnabled(this->menu->enabled()); } } void PlatformMenuEntry::onTextChanged() { if (this->qmenu != nullptr) { this->qmenu->setTitle(this->menu->text()); } else if (this->qaction != nullptr) { this->qaction->setText(this->menu->text()); } } void PlatformMenuEntry::onIconChanged() { if (this->qmenu == nullptr && this->qaction == nullptr) return; auto iconName = this->menu->icon(); QIcon icon; if (!iconName.isEmpty()) { icon = getCurrentEngineImageAsIcon(iconName); } if (this->qmenu != nullptr) { this->qmenu->setIcon(icon); } else if (this->qaction != nullptr) { this->qaction->setIcon(icon); } } void PlatformMenuEntry::onButtonTypeChanged() { if (this->qaction != nullptr) { QActionGroup* group = nullptr; if (this->menu->buttonType() == QsMenuButtonType::RadioButton) { if (!this->qactiongroup) this->qactiongroup = new QActionGroup(this); group = this->qactiongroup; } this->qaction->setActionGroup(group); } } void PlatformMenuEntry::onCheckStateChanged() { if (this->qaction != nullptr) { this->qaction->setChecked(this->menu->checkState() != Qt::Unchecked); } } void PlatformMenuEntry::clearChildren() { for (auto* child: this->childEntries) { delete child; } this->childEntries.clear(); } void PlatformMenuEntry::addToQMenu(PlatformMenuQMenu* menu) { if (this->qmenu != nullptr) { menu->addMenu(this->qmenu); this->qmenu->containingMenu = menu; } else if (this->qaction != nullptr) { menu->addAction(this->qaction); } else { menu->addSeparator(); } } } // namespace qs::menu::platform