diff --git a/autotests/decorationbuttontest.cpp b/autotests/decorationbuttontest.cpp --- a/autotests/decorationbuttontest.cpp +++ b/autotests/decorationbuttontest.cpp @@ -59,6 +59,7 @@ void testMenu(); void testMenuDoubleClick(); void testMenuPressAndHold(); + void testApplicationMenu(); }; void DecorationButtonTest::testButton() @@ -1286,5 +1287,60 @@ QCOMPARE(closeRequestedSpy.count(), 0); } +void DecorationButtonTest::testApplicationMenu() +{ + MockBridge bridge; + auto decoSettings = QSharedPointer::create(&bridge); + MockDecoration mockDecoration(&bridge); + mockDecoration.setSettings(decoSettings); + MockClient *client = bridge.lastCreatedClient(); + MockButton button(KDecoration2::DecorationButtonType::ApplicationMenu, &mockDecoration); + button.setGeometry(QRect(0, 0, 10, 10)); + + QCOMPARE(button.isEnabled(), true); + QCOMPARE(button.isCheckable(), true); + QCOMPARE(button.isChecked(), false); + QCOMPARE(button.isVisible(), true); + QCOMPARE(button.acceptedButtons(), Qt::LeftButton); + + // clicking the button should trigger a request for application menu + QSignalSpy clickedSpy(&button, SIGNAL(clicked(Qt::MouseButton))); + QVERIFY(clickedSpy.isValid()); + QSignalSpy pressedSpy(&button, SIGNAL(pressed())); + QVERIFY(pressedSpy.isValid()); + QSignalSpy releasedSpy(&button, SIGNAL(released())); + QVERIFY(releasedSpy.isValid()); + QSignalSpy pressedChangedSpy(&button, SIGNAL(pressedChanged(bool))); + QVERIFY(pressedChangedSpy.isValid()); + QSignalSpy applicationMenuRequestedSpy(client, SIGNAL(applicationMenuRequested())); + QVERIFY(applicationMenuRequestedSpy.isValid()); + + QMouseEvent pressEvent(QEvent::MouseButtonPress, QPointF(5, 5), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + pressEvent.setAccepted(false); + button.event(&pressEvent); + QCOMPARE(pressEvent.isAccepted(), true); + QCOMPARE(button.isPressed(), true); + QCOMPARE(clickedSpy.count(), 0); + QCOMPARE(pressedSpy.count(), 1); + QCOMPARE(releasedSpy.count(), 0); + QCOMPARE(applicationMenuRequestedSpy.count(), 0); + QCOMPARE(pressedChangedSpy.count(), 1); + QCOMPARE(pressedChangedSpy.first().first().toBool(), true); + + QMouseEvent releaseEvent(QEvent::MouseButtonRelease, QPointF(5, 5), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + releaseEvent.setAccepted(false); + button.event(&releaseEvent); + QCOMPARE(releaseEvent.isAccepted(), true); + QCOMPARE(button.isPressed(), false); + QCOMPARE(clickedSpy.count(), 1); + QCOMPARE(clickedSpy.first().first().value(), Qt::LeftButton); + QCOMPARE(pressedSpy.count(), 1); + QCOMPARE(releasedSpy.count(), 1); + QVERIFY(applicationMenuRequestedSpy.wait()); + QCOMPARE(applicationMenuRequestedSpy.count(), 1); + QCOMPARE(pressedChangedSpy.count(), 2); + QCOMPARE(pressedChangedSpy.last().first().toBool(), false); +} + QTEST_MAIN(DecorationButtonTest) #include "decorationbuttontest.moc" diff --git a/autotests/mockclient.h b/autotests/mockclient.h --- a/autotests/mockclient.h +++ b/autotests/mockclient.h @@ -24,7 +24,7 @@ #include -class MockClient : public QObject, public KDecoration2::DecoratedClientPrivate +class MockClient : public QObject, public KDecoration2::ApplicationMenuEnabledDecoratedClientPrivate { Q_OBJECT public: @@ -52,19 +52,24 @@ bool isShadeable() const override; bool isShaded() const override; QPalette palette() const override; + bool hasApplicationMenu() const override; + bool isApplicationMenuActive() const override; bool providesContextHelp() const override; void requestClose() override; void requestContextHelp() override; void requestToggleMaximization(Qt::MouseButtons buttons) override; void requestMinimize() override; void requestShowWindowMenu() override; + void requestShowApplicationMenu(const QRect &rect, int actionId) override; void requestToggleKeepAbove() override; void requestToggleKeepBelow() override; void requestToggleOnAllDesktops() override; void requestToggleShade() override; int width() const override; WId windowId() const override; + void showApplicationMenu(int actionId) override; + void setCloseable(bool set); void setMinimizable(bool set); void setProvidesContextHelp(bool set); @@ -79,6 +84,7 @@ void minimizeRequested(); void quickHelpRequested(); void menuRequested(); + void applicationMenuRequested(); private: bool m_closeable = false; diff --git a/autotests/mockclient.cpp b/autotests/mockclient.cpp --- a/autotests/mockclient.cpp +++ b/autotests/mockclient.cpp @@ -24,7 +24,7 @@ MockClient::MockClient(KDecoration2::DecoratedClient *client, KDecoration2::Decoration *decoration) : QObject() - , DecoratedClientPrivate(client, decoration) + , ApplicationMenuEnabledDecoratedClientPrivate(client, decoration) { } @@ -138,6 +138,16 @@ return QPalette(); } +bool MockClient::hasApplicationMenu() const +{ + return true; +} + +bool MockClient::isApplicationMenuActive() const +{ + return false; +} + bool MockClient::providesContextHelp() const { return m_contextHelp; @@ -191,6 +201,13 @@ emit menuRequested(); } +void MockClient::requestShowApplicationMenu(const QRect &rect, int actionId) +{ + Q_UNUSED(rect); + Q_UNUSED(actionId); + emit applicationMenuRequested(); // FIXME TODO pass geometry +} + void MockClient::requestToggleKeepAbove() { m_keepAbove = !m_keepAbove; @@ -266,3 +283,8 @@ m_height = h; emit client()->heightChanged(h); } + +void MockClient::showApplicationMenu(int actionId) +{ + Q_UNUSED(actionId) +} diff --git a/src/decoratedclient.h b/src/decoratedclient.h --- a/src/decoratedclient.h +++ b/src/decoratedclient.h @@ -167,6 +167,17 @@ * will include all Edges. The Decoration can use this information to hide borders. **/ Q_PROPERTY(Qt::Edges adjacentScreenEdges READ adjacentScreenEdges NOTIFY adjacentScreenEdgesChanged) + /** + * Whether the DecoratedClient has an application menu + * @since 5.9 + */ + Q_PROPERTY(bool hasApplicationMenu READ hasApplicationMenu NOTIFY hasApplicationMenuChanged) + /** + * Whether the application menu for this DecoratedClient is currently shown to the user + * The Decoration can use this information to highlight the respective button. + * @since 5.9 + */ + Q_PROPERTY(bool applicationMenuActive READ isApplicationMenuActive NOTIFY applicationMenuActiveChanged) // TODO: properties for windowId and decorationId? @@ -221,6 +232,24 @@ **/ QColor color(ColorGroup group, ColorRole role) const; + /** + * Whether the DecoratedClient has an application menu + * @since 5.9 + */ + bool hasApplicationMenu() const; + /** + * Whether the application menu for this DecoratedClient is currently shown to the user + * The Decoration can use this information to highlight the respective button. + * @since 5.9 + */ + bool isApplicationMenuActive() const; + + /** + * Request the application menu to be shown to the user + * @param actionId The DBus menu ID of the action that should be highlighted, 0 for none. + */ + void showApplicationMenu(int actionId); + Q_SIGNALS: void activeChanged(bool); void captionChanged(QString); @@ -247,6 +276,9 @@ void paletteChanged(const QPalette &palette); void adjacentScreenEdgesChanged(Qt::Edges edges); + void hasApplicationMenuChanged(bool); + void applicationMenuActiveChanged(bool); + private: friend class Decoration; DecoratedClient(Decoration *parent, DecorationBridge *bridge); diff --git a/src/decoratedclient.cpp b/src/decoratedclient.cpp --- a/src/decoratedclient.cpp +++ b/src/decoratedclient.cpp @@ -69,6 +69,22 @@ #undef DELEGATE +bool DecoratedClient::hasApplicationMenu() const +{ + if (const auto *appMenuEnabledPrivate = dynamic_cast(d.get())) { + return appMenuEnabledPrivate->hasApplicationMenu(); + } + return false; +} + +bool DecoratedClient::isApplicationMenuActive() const +{ + if (const auto *appMenuEnabledPrivate = dynamic_cast(d.get())) { + return appMenuEnabledPrivate->isApplicationMenuActive(); + } + return false; +} + QPointer< Decoration > DecoratedClient::decoration() const { return QPointer(d->decoration()); @@ -84,4 +100,11 @@ return d->color(group, role); } +void DecoratedClient::showApplicationMenu(int actionId) +{ + if (auto *appMenuEnabledPrivate = dynamic_cast(d.get())) { + appMenuEnabledPrivate->showApplicationMenu(actionId); + } +} + } // namespace diff --git a/src/decoration.h b/src/decoration.h --- a/src/decoration.h +++ b/src/decoration.h @@ -177,6 +177,9 @@ void requestToggleKeepBelow(); void requestShowWindowMenu(); + void showApplicationMenu(int actionId); + void requestShowApplicationMenu(const QRect &rect, int actionId); + void update(const QRect &rect); void update(); diff --git a/src/decoration.cpp b/src/decoration.cpp --- a/src/decoration.cpp +++ b/src/decoration.cpp @@ -185,6 +185,24 @@ d->client->d->requestToggleMaximization(buttons); } +void Decoration::showApplicationMenu(int actionId) +{ + auto it = std::find_if(d->buttons.constBegin(), d->buttons.constEnd(), [](DecorationButton *button) { + return button->type() == DecorationButtonType::ApplicationMenu; + }); + + if (it != d->buttons.constEnd()) { + requestShowApplicationMenu((*it)->geometry().toRect(), actionId); + } +} + +void Decoration::requestShowApplicationMenu(const QRect &rect, int actionId) +{ + if (auto *appMenuEnabledPrivate = dynamic_cast(d->client->d.get())) { + appMenuEnabledPrivate->requestShowApplicationMenu(rect, actionId); + } +} + #define DELEGATE(name, variableName, type, emitValue) \ void Decoration::name(type a) \ { \ diff --git a/src/decorationbutton.cpp b/src/decorationbutton.cpp --- a/src/decorationbutton.cpp +++ b/src/decorationbutton.cpp @@ -77,6 +77,16 @@ setPressAndHold(settings->isCloseOnDoubleClickOnMenu()); setAcceptedButtons(Qt::LeftButton | Qt::RightButton); break; + case DecorationButtonType::ApplicationMenu: + setVisible(c->hasApplicationMenu()); + setCheckable(true); // will be "checked" whilst the menu is opened + // FIXME TODO connect directly and figure out the button geometry/offset stuff + QObject::connect(q, &DecorationButton::clicked, decoration.data(), [this] { + decoration->requestShowApplicationMenu(q->geometry().toRect(), 0 /* actionId */); + }, Qt::QueuedConnection); //&Decoration::requestShowApplicationMenu, Qt::QueuedConnection); + QObject::connect(c, &DecoratedClient::hasApplicationMenuChanged, q, &DecorationButton::setVisible); + QObject::connect(c, &DecoratedClient::applicationMenuActiveChanged, q, &DecorationButton::setChecked); + break; case DecorationButtonType::OnAllDesktops: setVisible(settings->isOnAllDesktopsAvailable()); setCheckable(true); diff --git a/src/private/decoratedclientprivate.h b/src/private/decoratedclientprivate.h --- a/src/private/decoratedclientprivate.h +++ b/src/private/decoratedclientprivate.h @@ -100,6 +100,21 @@ const QScopedPointer d; }; +class KDECORATIONS_PRIVATE_EXPORT ApplicationMenuEnabledDecoratedClientPrivate : public DecoratedClientPrivate +{ +public: + ~ApplicationMenuEnabledDecoratedClientPrivate() override; + + virtual bool hasApplicationMenu() const = 0; + virtual bool isApplicationMenuActive() const = 0; + + virtual void showApplicationMenu(int actionId) = 0; + virtual void requestShowApplicationMenu(const QRect &rect, int actionId) = 0; + +protected: + explicit ApplicationMenuEnabledDecoratedClientPrivate(DecoratedClient *client, Decoration *decoration); +}; + } // namespace #endif diff --git a/src/private/decoratedclientprivate.cpp b/src/private/decoratedclientprivate.cpp --- a/src/private/decoratedclientprivate.cpp +++ b/src/private/decoratedclientprivate.cpp @@ -68,4 +68,12 @@ return QColor(); } +ApplicationMenuEnabledDecoratedClientPrivate::ApplicationMenuEnabledDecoratedClientPrivate(DecoratedClient *client, Decoration *decoration) + : DecoratedClientPrivate(client, decoration) +{ + +} + +ApplicationMenuEnabledDecoratedClientPrivate::~ApplicationMenuEnabledDecoratedClientPrivate() = default; + }