File indexing completed on 2024-05-12 09:31:35

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005  */
0006 #include "decorationbutton.h"
0007 #include "decoratedclient.h"
0008 #include "decoration.h"
0009 #include "decoration_p.h"
0010 #include "decorationbutton_p.h"
0011 #include "decorationsettings.h"
0012 
0013 #include <KLocalizedString>
0014 
0015 #include <QDebug>
0016 #include <QElapsedTimer>
0017 #include <QGuiApplication>
0018 #include <QHoverEvent>
0019 #include <QStyleHints>
0020 #include <QTimer>
0021 
0022 #include <cmath>
0023 
0024 #ifndef K_DOXYGEN
0025 size_t qHash(const KDecoration2::DecorationButtonType &type, size_t seed)
0026 {
0027     return qHash(uint(type), seed);
0028 }
0029 #endif
0030 
0031 namespace KDecoration2
0032 {
0033 
0034 DecorationButton::Private::Private(DecorationButtonType type, const QPointer<Decoration> &decoration, DecorationButton *parent)
0035     : decoration(decoration)
0036     , type(type)
0037     , hovered(false)
0038     , enabled(true)
0039     , checkable(false)
0040     , checked(false)
0041     , visible(true)
0042     , acceptedButtons(Qt::LeftButton)
0043     , doubleClickEnabled(false)
0044     , pressAndHold(false)
0045     , q(parent)
0046     , m_pressed(Qt::NoButton)
0047 {
0048     init();
0049 }
0050 
0051 DecorationButton::Private::~Private() = default;
0052 
0053 void DecorationButton::Private::init()
0054 {
0055     auto c = decoration->client();
0056     auto settings = decoration->settings();
0057     switch (type) {
0058     case DecorationButtonType::Menu:
0059         QObject::connect(
0060             q,
0061             &DecorationButton::clicked,
0062             decoration.data(),
0063             [this](Qt::MouseButton button) {
0064                 Q_UNUSED(button)
0065                 decoration->requestShowWindowMenu(q->geometry().toRect());
0066             },
0067             Qt::QueuedConnection);
0068         QObject::connect(q, &DecorationButton::doubleClicked, decoration.data(), &Decoration::requestClose, Qt::QueuedConnection);
0069         QObject::connect(
0070             settings.get(),
0071             &DecorationSettings::closeOnDoubleClickOnMenuChanged,
0072             q,
0073             [this](bool enabled) {
0074                 doubleClickEnabled = enabled;
0075                 setPressAndHold(enabled);
0076             },
0077             Qt::QueuedConnection);
0078         doubleClickEnabled = settings->isCloseOnDoubleClickOnMenu();
0079         setPressAndHold(settings->isCloseOnDoubleClickOnMenu());
0080         setAcceptedButtons(Qt::LeftButton | Qt::RightButton);
0081         break;
0082     case DecorationButtonType::ApplicationMenu:
0083         setVisible(c->hasApplicationMenu());
0084         setCheckable(true); // will be "checked" whilst the menu is opened
0085         // FIXME TODO connect directly and figure out the button geometry/offset stuff
0086         QObject::connect(
0087             q,
0088             &DecorationButton::clicked,
0089             decoration.data(),
0090             [this] {
0091                 decoration->requestShowApplicationMenu(q->geometry().toRect(), 0 /* actionId */);
0092             },
0093             Qt::QueuedConnection); //&Decoration::requestShowApplicationMenu, Qt::QueuedConnection);
0094         QObject::connect(c, &DecoratedClient::hasApplicationMenuChanged, q, &DecorationButton::setVisible);
0095         QObject::connect(c, &DecoratedClient::applicationMenuActiveChanged, q, &DecorationButton::setChecked);
0096         break;
0097     case DecorationButtonType::OnAllDesktops:
0098         setVisible(settings->isOnAllDesktopsAvailable());
0099         setCheckable(true);
0100         setChecked(c->isOnAllDesktops());
0101         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleOnAllDesktops, Qt::QueuedConnection);
0102         QObject::connect(settings.get(), &DecorationSettings::onAllDesktopsAvailableChanged, q, &DecorationButton::setVisible);
0103         QObject::connect(c, &DecoratedClient::onAllDesktopsChanged, q, &DecorationButton::setChecked);
0104         break;
0105     case DecorationButtonType::Minimize:
0106         setEnabled(c->isMinimizeable());
0107         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestMinimize, Qt::QueuedConnection);
0108         QObject::connect(c, &DecoratedClient::minimizeableChanged, q, &DecorationButton::setEnabled);
0109         break;
0110     case DecorationButtonType::Maximize:
0111         setEnabled(c->isMaximizeable());
0112         setCheckable(true);
0113         setChecked(c->isMaximized());
0114         setAcceptedButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton);
0115         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleMaximization, Qt::QueuedConnection);
0116         QObject::connect(c, &DecoratedClient::maximizeableChanged, q, &DecorationButton::setEnabled);
0117         QObject::connect(c, &DecoratedClient::maximizedChanged, q, &DecorationButton::setChecked);
0118         break;
0119     case DecorationButtonType::Close:
0120         setEnabled(c->isCloseable());
0121         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestClose, Qt::QueuedConnection);
0122         QObject::connect(c, &DecoratedClient::closeableChanged, q, &DecorationButton::setEnabled);
0123         break;
0124     case DecorationButtonType::ContextHelp:
0125         setVisible(c->providesContextHelp());
0126         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestContextHelp, Qt::QueuedConnection);
0127         QObject::connect(c, &DecoratedClient::providesContextHelpChanged, q, &DecorationButton::setVisible);
0128         break;
0129     case DecorationButtonType::KeepAbove:
0130         setCheckable(true);
0131         setChecked(c->isKeepAbove());
0132         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleKeepAbove, Qt::QueuedConnection);
0133         QObject::connect(c, &DecoratedClient::keepAboveChanged, q, &DecorationButton::setChecked);
0134         break;
0135     case DecorationButtonType::KeepBelow:
0136         setCheckable(true);
0137         setChecked(c->isKeepBelow());
0138         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleKeepBelow, Qt::QueuedConnection);
0139         QObject::connect(c, &DecoratedClient::keepBelowChanged, q, &DecorationButton::setChecked);
0140         break;
0141     case DecorationButtonType::Shade:
0142         setEnabled(c->isShadeable());
0143         setCheckable(true);
0144         setChecked(c->isShaded());
0145         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleShade, Qt::QueuedConnection);
0146         QObject::connect(c, &DecoratedClient::shadedChanged, q, &DecorationButton::setChecked);
0147         QObject::connect(c, &DecoratedClient::shadeableChanged, q, &DecorationButton::setEnabled);
0148         break;
0149     default:
0150         // nothing
0151         break;
0152     }
0153 }
0154 
0155 void DecorationButton::Private::setHovered(bool set)
0156 {
0157     if (hovered == set) {
0158         return;
0159     }
0160     hovered = set;
0161     Q_EMIT q->hoveredChanged(hovered);
0162 }
0163 
0164 void DecorationButton::Private::setEnabled(bool set)
0165 {
0166     if (enabled == set) {
0167         return;
0168     }
0169     enabled = set;
0170     Q_EMIT q->enabledChanged(enabled);
0171     if (!enabled) {
0172         setHovered(false);
0173         if (isPressed()) {
0174             m_pressed = Qt::NoButton;
0175             Q_EMIT q->pressedChanged(false);
0176         }
0177     }
0178 }
0179 
0180 void DecorationButton::Private::setVisible(bool set)
0181 {
0182     if (visible == set) {
0183         return;
0184     }
0185     visible = set;
0186     Q_EMIT q->visibilityChanged(set);
0187     if (!visible) {
0188         setHovered(false);
0189         if (isPressed()) {
0190             m_pressed = Qt::NoButton;
0191             Q_EMIT q->pressedChanged(false);
0192         }
0193     }
0194 }
0195 
0196 void DecorationButton::Private::setChecked(bool set)
0197 {
0198     if (!checkable || checked == set) {
0199         return;
0200     }
0201     checked = set;
0202     Q_EMIT q->checkedChanged(checked);
0203 }
0204 
0205 void DecorationButton::Private::setCheckable(bool set)
0206 {
0207     if (checkable == set) {
0208         return;
0209     }
0210     if (!set) {
0211         setChecked(false);
0212     }
0213     checkable = set;
0214     Q_EMIT q->checkableChanged(checkable);
0215 }
0216 
0217 void DecorationButton::Private::setPressed(Qt::MouseButton button, bool pressed)
0218 {
0219     if (pressed) {
0220         m_pressed = m_pressed | button;
0221     } else {
0222         m_pressed = m_pressed & ~button;
0223     }
0224     Q_EMIT q->pressedChanged(isPressed());
0225 }
0226 
0227 void DecorationButton::Private::setAcceptedButtons(Qt::MouseButtons buttons)
0228 {
0229     if (acceptedButtons == buttons) {
0230         return;
0231     }
0232     acceptedButtons = buttons;
0233     Q_EMIT q->acceptedButtonsChanged(acceptedButtons);
0234 }
0235 
0236 void DecorationButton::Private::startDoubleClickTimer()
0237 {
0238     if (!doubleClickEnabled) {
0239         return;
0240     }
0241     if (!m_doubleClickTimer) {
0242         m_doubleClickTimer = std::make_unique<QElapsedTimer>();
0243     }
0244     m_doubleClickTimer->start();
0245 }
0246 
0247 void DecorationButton::Private::invalidateDoubleClickTimer()
0248 {
0249     if (!m_doubleClickTimer) {
0250         return;
0251     }
0252     m_doubleClickTimer->invalidate();
0253 }
0254 
0255 bool DecorationButton::Private::wasDoubleClick() const
0256 {
0257     if (!m_doubleClickTimer || !m_doubleClickTimer->isValid()) {
0258         return false;
0259     }
0260     return !m_doubleClickTimer->hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval());
0261 }
0262 
0263 void DecorationButton::Private::setPressAndHold(bool enable)
0264 {
0265     if (pressAndHold == enable) {
0266         return;
0267     }
0268     pressAndHold = enable;
0269     if (!pressAndHold) {
0270         m_pressAndHoldTimer.reset();
0271     }
0272 }
0273 
0274 void DecorationButton::Private::startPressAndHold()
0275 {
0276     if (!pressAndHold) {
0277         return;
0278     }
0279     if (!m_pressAndHoldTimer) {
0280         m_pressAndHoldTimer.reset(new QTimer());
0281         m_pressAndHoldTimer->setSingleShot(true);
0282         QObject::connect(m_pressAndHoldTimer.get(), &QTimer::timeout, q, [this]() {
0283             q->clicked(Qt::LeftButton);
0284         });
0285     }
0286     m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval());
0287 }
0288 
0289 void DecorationButton::Private::stopPressAndHold()
0290 {
0291     if (m_pressAndHoldTimer) {
0292         m_pressAndHoldTimer->stop();
0293     }
0294 }
0295 
0296 QString DecorationButton::Private::typeToString(DecorationButtonType type)
0297 {
0298     switch (type) {
0299     case DecorationButtonType::Menu:
0300         return i18n("More actions for this window");
0301     case DecorationButtonType::ApplicationMenu:
0302         return i18n("Application menu");
0303     case DecorationButtonType::OnAllDesktops:
0304         if (this->q->isChecked())
0305             return i18n("On one desktop");
0306         else
0307             return i18n("On all desktops");
0308     case DecorationButtonType::Minimize:
0309         return i18n("Minimize");
0310     case DecorationButtonType::Maximize:
0311         if (this->q->isChecked())
0312             return i18n("Restore");
0313         else
0314             return i18n("Maximize");
0315     case DecorationButtonType::Close:
0316         return i18n("Close");
0317     case DecorationButtonType::ContextHelp:
0318         return i18n("Context help");
0319     case DecorationButtonType::Shade:
0320         if (this->q->isChecked())
0321             return i18n("Unshade");
0322         else
0323             return i18n("Shade");
0324     case DecorationButtonType::KeepBelow:
0325         if (this->q->isChecked())
0326             return i18n("Don't keep below other windows");
0327         else
0328             return i18n("Keep below other windows");
0329     case DecorationButtonType::KeepAbove:
0330         if (this->q->isChecked())
0331             return i18n("Don't keep above other windows");
0332         else
0333             return i18n("Keep above other windows");
0334     default:
0335         return QString();
0336     }
0337 }
0338 
0339 DecorationButton::DecorationButton(DecorationButtonType type, Decoration *decoration, QObject *parent)
0340     : QObject(parent)
0341     , d(new Private(type, decoration, this))
0342 {
0343     decoration->d->addButton(this);
0344     connect(this, &DecorationButton::geometryChanged, this, static_cast<void (DecorationButton::*)(const QRectF &)>(&DecorationButton::update));
0345     auto updateSlot = static_cast<void (DecorationButton::*)()>(&DecorationButton::update);
0346     connect(this, &DecorationButton::hoveredChanged, this, updateSlot);
0347     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
0348         if (hovered) {
0349             // TODO: show tooltip if hovered and hide if not
0350             const QString type = this->d->typeToString(this->type());
0351             this->decoration()->requestShowToolTip(type);
0352         } else {
0353             this->decoration()->requestHideToolTip();
0354         }
0355     });
0356     connect(this, &DecorationButton::pressedChanged, this, updateSlot);
0357     connect(this, &DecorationButton::pressedChanged, this, [this](bool pressed) {
0358         if (pressed) {
0359             this->decoration()->requestHideToolTip();
0360         }
0361     });
0362     connect(this, &DecorationButton::checkedChanged, this, updateSlot);
0363     connect(this, &DecorationButton::enabledChanged, this, updateSlot);
0364     connect(this, &DecorationButton::visibilityChanged, this, updateSlot);
0365     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
0366         if (hovered) {
0367             Q_EMIT pointerEntered();
0368         } else {
0369             Q_EMIT pointerLeft();
0370         }
0371     });
0372     connect(this, &DecorationButton::pressedChanged, this, [this](bool p) {
0373         if (p) {
0374             Q_EMIT pressed();
0375         } else {
0376             Q_EMIT released();
0377         }
0378     });
0379 }
0380 
0381 DecorationButton::~DecorationButton() = default;
0382 
0383 void DecorationButton::update(const QRectF &rect)
0384 {
0385     decoration()->update(rect.isNull() ? geometry().toRect() : rect.toRect());
0386 }
0387 
0388 void DecorationButton::update()
0389 {
0390     update(QRectF());
0391 }
0392 
0393 QSizeF DecorationButton::size() const
0394 {
0395     return d->geometry.size();
0396 }
0397 
0398 bool DecorationButton::isPressed() const
0399 {
0400     return d->isPressed();
0401 }
0402 
0403 #define DELEGATE(name, variableName, type)                                                                                                                     \
0404     type DecorationButton::name() const                                                                                                                        \
0405     {                                                                                                                                                          \
0406         return d->variableName;                                                                                                                                \
0407     }
0408 
0409 DELEGATE(isHovered, hovered, bool)
0410 DELEGATE(isEnabled, enabled, bool)
0411 DELEGATE(isChecked, checked, bool)
0412 DELEGATE(isCheckable, checkable, bool)
0413 DELEGATE(isVisible, visible, bool)
0414 
0415 #define DELEGATE2(name, type) DELEGATE(name, name, type)
0416 DELEGATE2(geometry, QRectF)
0417 DELEGATE2(decoration, Decoration *)
0418 DELEGATE2(acceptedButtons, Qt::MouseButtons)
0419 DELEGATE2(type, DecorationButtonType)
0420 
0421 #undef DELEGATE2
0422 #undef DELEGATE
0423 
0424 #define DELEGATE(name, type)                                                                                                                                   \
0425     void DecorationButton::name(type a)                                                                                                                        \
0426     {                                                                                                                                                          \
0427         d->name(a);                                                                                                                                            \
0428     }
0429 
0430 DELEGATE(setAcceptedButtons, Qt::MouseButtons)
0431 DELEGATE(setEnabled, bool)
0432 DELEGATE(setChecked, bool)
0433 DELEGATE(setCheckable, bool)
0434 DELEGATE(setVisible, bool)
0435 
0436 #undef DELEGATE
0437 
0438 #define DELEGATE(name, variableName, type)                                                                                                                     \
0439     void DecorationButton::name(type a)                                                                                                                        \
0440     {                                                                                                                                                          \
0441         if (d->variableName == a) {                                                                                                                            \
0442             return;                                                                                                                                            \
0443         }                                                                                                                                                      \
0444         d->variableName = a;                                                                                                                                   \
0445         Q_EMIT variableName##Changed(d->variableName);                                                                                                         \
0446     }
0447 
0448 DELEGATE(setGeometry, geometry, const QRectF &)
0449 
0450 #undef DELEGATE
0451 
0452 bool DecorationButton::contains(const QPointF &pos) const
0453 {
0454     auto flooredPoint = QPoint(std::floor(pos.x()), std::floor(pos.y()));
0455     return d->geometry.toRect().contains(flooredPoint);
0456 }
0457 
0458 bool DecorationButton::event(QEvent *event)
0459 {
0460     switch (event->type()) {
0461     case QEvent::HoverEnter:
0462         hoverEnterEvent(static_cast<QHoverEvent *>(event));
0463         return true;
0464     case QEvent::HoverLeave:
0465         hoverLeaveEvent(static_cast<QHoverEvent *>(event));
0466         return true;
0467     case QEvent::HoverMove:
0468         hoverMoveEvent(static_cast<QHoverEvent *>(event));
0469         return true;
0470     case QEvent::MouseButtonPress:
0471         mousePressEvent(static_cast<QMouseEvent *>(event));
0472         return true;
0473     case QEvent::MouseButtonRelease:
0474         mouseReleaseEvent(static_cast<QMouseEvent *>(event));
0475         return true;
0476     case QEvent::MouseMove:
0477         mouseMoveEvent(static_cast<QMouseEvent *>(event));
0478         return true;
0479     case QEvent::Wheel:
0480         wheelEvent(static_cast<QWheelEvent *>(event));
0481         return true;
0482     default:
0483         return QObject::event(event);
0484     }
0485 }
0486 
0487 void DecorationButton::hoverEnterEvent(QHoverEvent *event)
0488 {
0489     if (!d->enabled || !d->visible || !contains(event->posF())) {
0490         return;
0491     }
0492     d->setHovered(true);
0493     event->setAccepted(true);
0494 }
0495 
0496 void DecorationButton::hoverLeaveEvent(QHoverEvent *event)
0497 {
0498     if (!d->enabled || !d->visible || !d->hovered || contains(event->posF())) {
0499         return;
0500     }
0501     d->setHovered(false);
0502     event->setAccepted(true);
0503 }
0504 
0505 void DecorationButton::hoverMoveEvent(QHoverEvent *event)
0506 {
0507     Q_UNUSED(event)
0508 }
0509 
0510 void DecorationButton::mouseMoveEvent(QMouseEvent *event)
0511 {
0512     if (!d->enabled || !d->visible || !d->hovered) {
0513         return;
0514     }
0515     if (!contains(event->localPos())) {
0516         d->setHovered(false);
0517         event->setAccepted(true);
0518     }
0519 }
0520 
0521 void DecorationButton::mousePressEvent(QMouseEvent *event)
0522 {
0523     if (!d->enabled || !d->visible || !contains(event->localPos()) || !d->acceptedButtons.testFlag(event->button())) {
0524         return;
0525     }
0526     d->setPressed(event->button(), true);
0527     event->setAccepted(true);
0528     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
0529         // check for double click
0530         if (d->wasDoubleClick()) {
0531             event->setAccepted(true);
0532             Q_EMIT doubleClicked();
0533         }
0534         d->invalidateDoubleClickTimer();
0535     }
0536     if (d->pressAndHold && event->button() == Qt::LeftButton) {
0537         d->startPressAndHold();
0538     }
0539 }
0540 
0541 void DecorationButton::mouseReleaseEvent(QMouseEvent *event)
0542 {
0543     if (!d->enabled || !d->visible || !d->isPressed(event->button())) {
0544         return;
0545     }
0546     if (contains(event->localPos())) {
0547         if (!d->pressAndHold || event->button() != Qt::LeftButton) {
0548             Q_EMIT clicked(event->button());
0549         } else {
0550             d->stopPressAndHold();
0551         }
0552     }
0553     d->setPressed(event->button(), false);
0554     event->setAccepted(true);
0555 
0556     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
0557         d->startDoubleClickTimer();
0558     }
0559 }
0560 
0561 void DecorationButton::wheelEvent(QWheelEvent *event)
0562 {
0563     Q_UNUSED(event)
0564 }
0565 
0566 }
0567 
0568 #include "moc_decorationbutton.cpp"