File indexing completed on 2024-05-05 05:29:53

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     case DecorationButtonType::Spacer:
0150         setEnabled(false);
0151         break;
0152     default:
0153         // nothing
0154         break;
0155     }
0156 }
0157 
0158 void DecorationButton::Private::setHovered(bool set)
0159 {
0160     if (hovered == set) {
0161         return;
0162     }
0163     hovered = set;
0164     Q_EMIT q->hoveredChanged(hovered);
0165 }
0166 
0167 void DecorationButton::Private::setEnabled(bool set)
0168 {
0169     if (enabled == set) {
0170         return;
0171     }
0172     enabled = set;
0173     Q_EMIT q->enabledChanged(enabled);
0174     if (!enabled) {
0175         setHovered(false);
0176         if (isPressed()) {
0177             m_pressed = Qt::NoButton;
0178             Q_EMIT q->pressedChanged(false);
0179         }
0180     }
0181 }
0182 
0183 void DecorationButton::Private::setVisible(bool set)
0184 {
0185     if (visible == set) {
0186         return;
0187     }
0188     visible = set;
0189     Q_EMIT q->visibilityChanged(set);
0190     if (!visible) {
0191         setHovered(false);
0192         if (isPressed()) {
0193             m_pressed = Qt::NoButton;
0194             Q_EMIT q->pressedChanged(false);
0195         }
0196     }
0197 }
0198 
0199 void DecorationButton::Private::setChecked(bool set)
0200 {
0201     if (!checkable || checked == set) {
0202         return;
0203     }
0204     checked = set;
0205     Q_EMIT q->checkedChanged(checked);
0206 }
0207 
0208 void DecorationButton::Private::setCheckable(bool set)
0209 {
0210     if (checkable == set) {
0211         return;
0212     }
0213     if (!set) {
0214         setChecked(false);
0215     }
0216     checkable = set;
0217     Q_EMIT q->checkableChanged(checkable);
0218 }
0219 
0220 void DecorationButton::Private::setPressed(Qt::MouseButton button, bool pressed)
0221 {
0222     if (pressed) {
0223         m_pressed = m_pressed | button;
0224     } else {
0225         m_pressed = m_pressed & ~button;
0226     }
0227     Q_EMIT q->pressedChanged(isPressed());
0228 }
0229 
0230 void DecorationButton::Private::setAcceptedButtons(Qt::MouseButtons buttons)
0231 {
0232     if (acceptedButtons == buttons) {
0233         return;
0234     }
0235     acceptedButtons = buttons;
0236     Q_EMIT q->acceptedButtonsChanged(acceptedButtons);
0237 }
0238 
0239 void DecorationButton::Private::startDoubleClickTimer()
0240 {
0241     if (!doubleClickEnabled) {
0242         return;
0243     }
0244     if (!m_doubleClickTimer) {
0245         m_doubleClickTimer = std::make_unique<QElapsedTimer>();
0246     }
0247     m_doubleClickTimer->start();
0248 }
0249 
0250 void DecorationButton::Private::invalidateDoubleClickTimer()
0251 {
0252     if (!m_doubleClickTimer) {
0253         return;
0254     }
0255     m_doubleClickTimer->invalidate();
0256 }
0257 
0258 bool DecorationButton::Private::wasDoubleClick() const
0259 {
0260     if (!m_doubleClickTimer || !m_doubleClickTimer->isValid()) {
0261         return false;
0262     }
0263     return !m_doubleClickTimer->hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval());
0264 }
0265 
0266 void DecorationButton::Private::setPressAndHold(bool enable)
0267 {
0268     if (pressAndHold == enable) {
0269         return;
0270     }
0271     pressAndHold = enable;
0272     if (!pressAndHold) {
0273         m_pressAndHoldTimer.reset();
0274     }
0275 }
0276 
0277 void DecorationButton::Private::startPressAndHold()
0278 {
0279     if (!pressAndHold) {
0280         return;
0281     }
0282     if (!m_pressAndHoldTimer) {
0283         m_pressAndHoldTimer.reset(new QTimer());
0284         m_pressAndHoldTimer->setSingleShot(true);
0285         QObject::connect(m_pressAndHoldTimer.get(), &QTimer::timeout, q, [this]() {
0286             q->clicked(Qt::LeftButton);
0287         });
0288     }
0289     m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval());
0290 }
0291 
0292 void DecorationButton::Private::stopPressAndHold()
0293 {
0294     if (m_pressAndHoldTimer) {
0295         m_pressAndHoldTimer->stop();
0296     }
0297 }
0298 
0299 QString DecorationButton::Private::typeToString(DecorationButtonType type)
0300 {
0301     switch (type) {
0302     case DecorationButtonType::Menu:
0303         return i18n("More actions for this window");
0304     case DecorationButtonType::ApplicationMenu:
0305         return i18n("Application menu");
0306     case DecorationButtonType::OnAllDesktops:
0307         if (this->q->isChecked())
0308             return i18n("On one desktop");
0309         else
0310             return i18n("On all desktops");
0311     case DecorationButtonType::Minimize:
0312         return i18n("Minimize");
0313     case DecorationButtonType::Maximize:
0314         if (this->q->isChecked())
0315             return i18n("Restore");
0316         else
0317             return i18n("Maximize");
0318     case DecorationButtonType::Close:
0319         return i18n("Close");
0320     case DecorationButtonType::ContextHelp:
0321         return i18n("Context help");
0322     case DecorationButtonType::Shade:
0323         if (this->q->isChecked())
0324             return i18n("Unshade");
0325         else
0326             return i18n("Shade");
0327     case DecorationButtonType::KeepBelow:
0328         if (this->q->isChecked())
0329             return i18n("Don't keep below other windows");
0330         else
0331             return i18n("Keep below other windows");
0332     case DecorationButtonType::KeepAbove:
0333         if (this->q->isChecked())
0334             return i18n("Don't keep above other windows");
0335         else
0336             return i18n("Keep above other windows");
0337     default:
0338         return QString();
0339     }
0340 }
0341 
0342 DecorationButton::DecorationButton(DecorationButtonType type, Decoration *decoration, QObject *parent)
0343     : QObject(parent)
0344     , d(new Private(type, decoration, this))
0345 {
0346     decoration->d->addButton(this);
0347     connect(this, &DecorationButton::geometryChanged, this, static_cast<void (DecorationButton::*)(const QRectF &)>(&DecorationButton::update));
0348     auto updateSlot = static_cast<void (DecorationButton::*)()>(&DecorationButton::update);
0349     connect(this, &DecorationButton::hoveredChanged, this, updateSlot);
0350     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
0351         if (hovered) {
0352             // TODO: show tooltip if hovered and hide if not
0353             const QString type = this->d->typeToString(this->type());
0354             this->decoration()->requestShowToolTip(type);
0355         } else {
0356             this->decoration()->requestHideToolTip();
0357         }
0358     });
0359     connect(this, &DecorationButton::pressedChanged, this, updateSlot);
0360     connect(this, &DecorationButton::pressedChanged, this, [this](bool pressed) {
0361         if (pressed) {
0362             this->decoration()->requestHideToolTip();
0363         }
0364     });
0365     connect(this, &DecorationButton::checkedChanged, this, updateSlot);
0366     connect(this, &DecorationButton::enabledChanged, this, updateSlot);
0367     connect(this, &DecorationButton::visibilityChanged, this, updateSlot);
0368     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
0369         if (hovered) {
0370             Q_EMIT pointerEntered();
0371         } else {
0372             Q_EMIT pointerLeft();
0373         }
0374     });
0375     connect(this, &DecorationButton::pressedChanged, this, [this](bool p) {
0376         if (p) {
0377             Q_EMIT pressed();
0378         } else {
0379             Q_EMIT released();
0380         }
0381     });
0382 }
0383 
0384 DecorationButton::~DecorationButton() = default;
0385 
0386 void DecorationButton::update(const QRectF &rect)
0387 {
0388     decoration()->update(rect.isNull() ? geometry().toRect() : rect.toRect());
0389 }
0390 
0391 void DecorationButton::update()
0392 {
0393     update(QRectF());
0394 }
0395 
0396 QSizeF DecorationButton::size() const
0397 {
0398     return d->geometry.size();
0399 }
0400 
0401 bool DecorationButton::isPressed() const
0402 {
0403     return d->isPressed();
0404 }
0405 
0406 bool DecorationButton::isHovered() const
0407 {
0408     return d->hovered;
0409 }
0410 
0411 bool DecorationButton::isEnabled() const
0412 {
0413     return d->enabled;
0414 }
0415 
0416 bool DecorationButton::isChecked() const
0417 {
0418     return d->checked;
0419 }
0420 
0421 bool DecorationButton::isCheckable() const
0422 {
0423     return d->checkable;
0424 }
0425 
0426 bool DecorationButton::isVisible() const
0427 {
0428     return d->visible;
0429 }
0430 
0431 QRectF DecorationButton::geometry() const
0432 {
0433     return d->geometry;
0434 }
0435 
0436 Decoration *DecorationButton::decoration() const
0437 {
0438     return d->decoration;
0439 }
0440 
0441 Qt::MouseButtons DecorationButton::acceptedButtons() const
0442 {
0443     return d->acceptedButtons;
0444 }
0445 
0446 DecorationButtonType DecorationButton::type() const
0447 {
0448     return d->type;
0449 }
0450 
0451 void DecorationButton::setAcceptedButtons(Qt::MouseButtons value)
0452 {
0453     d->setAcceptedButtons(value);
0454 }
0455 
0456 void DecorationButton::setEnabled(bool value)
0457 {
0458     d->setEnabled(value);
0459 }
0460 
0461 void DecorationButton::setChecked(bool value)
0462 {
0463     d->setChecked(value);
0464 }
0465 
0466 void DecorationButton::setCheckable(bool value)
0467 {
0468     d->setCheckable(value);
0469 }
0470 
0471 void DecorationButton::setVisible(bool value)
0472 {
0473     d->setVisible(value);
0474 }
0475 
0476 void DecorationButton::setGeometry(const QRectF &geometry)
0477 {
0478     if (d->geometry == geometry) {
0479         return;
0480     }
0481     d->geometry = geometry;
0482     Q_EMIT geometryChanged(d->geometry);
0483 }
0484 
0485 bool DecorationButton::contains(const QPointF &pos) const
0486 {
0487     auto flooredPoint = QPoint(std::floor(pos.x()), std::floor(pos.y()));
0488     return d->geometry.toRect().contains(flooredPoint);
0489 }
0490 
0491 bool DecorationButton::event(QEvent *event)
0492 {
0493     switch (event->type()) {
0494     case QEvent::HoverEnter:
0495         hoverEnterEvent(static_cast<QHoverEvent *>(event));
0496         return true;
0497     case QEvent::HoverLeave:
0498         hoverLeaveEvent(static_cast<QHoverEvent *>(event));
0499         return true;
0500     case QEvent::HoverMove:
0501         hoverMoveEvent(static_cast<QHoverEvent *>(event));
0502         return true;
0503     case QEvent::MouseButtonPress:
0504         mousePressEvent(static_cast<QMouseEvent *>(event));
0505         return true;
0506     case QEvent::MouseButtonRelease:
0507         mouseReleaseEvent(static_cast<QMouseEvent *>(event));
0508         return true;
0509     case QEvent::MouseMove:
0510         mouseMoveEvent(static_cast<QMouseEvent *>(event));
0511         return true;
0512     case QEvent::Wheel:
0513         wheelEvent(static_cast<QWheelEvent *>(event));
0514         return true;
0515     default:
0516         return QObject::event(event);
0517     }
0518 }
0519 
0520 void DecorationButton::hoverEnterEvent(QHoverEvent *event)
0521 {
0522     if (!d->enabled || !d->visible || !contains(event->position())) {
0523         return;
0524     }
0525     d->setHovered(true);
0526     event->setAccepted(true);
0527 }
0528 
0529 void DecorationButton::hoverLeaveEvent(QHoverEvent *event)
0530 {
0531     if (!d->enabled || !d->visible || !d->hovered || contains(event->position())) {
0532         return;
0533     }
0534     d->setHovered(false);
0535     event->setAccepted(true);
0536 }
0537 
0538 void DecorationButton::hoverMoveEvent(QHoverEvent *event)
0539 {
0540     Q_UNUSED(event)
0541 }
0542 
0543 void DecorationButton::mouseMoveEvent(QMouseEvent *event)
0544 {
0545     if (!d->enabled || !d->visible || !d->hovered) {
0546         return;
0547     }
0548     if (!contains(event->position())) {
0549         d->setHovered(false);
0550         event->setAccepted(true);
0551     }
0552 }
0553 
0554 void DecorationButton::mousePressEvent(QMouseEvent *event)
0555 {
0556     if (!d->enabled || !d->visible || !contains(event->position()) || !d->acceptedButtons.testFlag(event->button())) {
0557         return;
0558     }
0559     d->setPressed(event->button(), true);
0560     event->setAccepted(true);
0561     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
0562         // check for double click
0563         if (d->wasDoubleClick()) {
0564             event->setAccepted(true);
0565             Q_EMIT doubleClicked();
0566         }
0567         d->invalidateDoubleClickTimer();
0568     }
0569     if (d->pressAndHold && event->button() == Qt::LeftButton) {
0570         d->startPressAndHold();
0571     }
0572 }
0573 
0574 void DecorationButton::mouseReleaseEvent(QMouseEvent *event)
0575 {
0576     if (!d->enabled || !d->visible || !d->isPressed(event->button())) {
0577         return;
0578     }
0579     if (contains(event->position())) {
0580         if (!d->pressAndHold || event->button() != Qt::LeftButton) {
0581             Q_EMIT clicked(event->button());
0582         } else {
0583             d->stopPressAndHold();
0584         }
0585     }
0586     d->setPressed(event->button(), false);
0587     event->setAccepted(true);
0588 
0589     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
0590         d->startDoubleClickTimer();
0591     }
0592 }
0593 
0594 void DecorationButton::wheelEvent(QWheelEvent *event)
0595 {
0596     Q_UNUSED(event)
0597 }
0598 
0599 }
0600 
0601 #include "moc_decorationbutton.cpp"