1 /*
2  * SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
3  *
4  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5  */
6 #include "decorationbutton.h"
7 #include "decoratedclient.h"
8 #include "decoration.h"
9 #include "decoration_p.h"
10 #include "decorationbutton_p.h"
11 #include "decorationsettings.h"
12 
13 #include <KLocalizedString>
14 
15 #include <QDebug>
16 #include <QElapsedTimer>
17 #include <QGuiApplication>
18 #include <QHoverEvent>
19 #include <QStyleHints>
20 #include <QTimer>
21 
22 namespace KDecoration2
23 {
24 #ifndef K_DOXYGEN
qHash(const DecorationButtonType & type)25 uint qHash(const DecorationButtonType &type)
26 {
27     return static_cast<uint>(type);
28 }
29 #endif
30 
Private(DecorationButtonType type,const QPointer<Decoration> & decoration,DecorationButton * parent)31 DecorationButton::Private::Private(DecorationButtonType type, const QPointer<Decoration> &decoration, DecorationButton *parent)
32     : decoration(decoration)
33     , type(type)
34     , hovered(false)
35     , enabled(true)
36     , checkable(false)
37     , checked(false)
38     , visible(true)
39     , acceptedButtons(Qt::LeftButton)
40     , doubleClickEnabled(false)
41     , pressAndHold(false)
42     , q(parent)
43     , m_pressed(Qt::NoButton)
44 {
45     init();
46 }
47 
48 DecorationButton::Private::~Private() = default;
49 
init()50 void DecorationButton::Private::init()
51 {
52     auto clientPtr = decoration->client().toStrongRef();
53     Q_ASSERT(clientPtr);
54     auto c = clientPtr.data();
55     auto settings = decoration->settings();
56     switch (type) {
57     case DecorationButtonType::Menu:
58         QObject::connect(
59             q,
60             &DecorationButton::clicked,
61             decoration.data(),
62             [this](Qt::MouseButton button) {
63                 Q_UNUSED(button)
64                 decoration->requestShowWindowMenu(q->geometry().toRect());
65             },
66             Qt::QueuedConnection);
67         QObject::connect(q, &DecorationButton::doubleClicked, decoration.data(), &Decoration::requestClose, Qt::QueuedConnection);
68         QObject::connect(
69             settings.data(),
70             &DecorationSettings::closeOnDoubleClickOnMenuChanged,
71             q,
72             [this](bool enabled) {
73                 doubleClickEnabled = enabled;
74                 setPressAndHold(enabled);
75             },
76             Qt::QueuedConnection);
77         doubleClickEnabled = settings->isCloseOnDoubleClickOnMenu();
78         setPressAndHold(settings->isCloseOnDoubleClickOnMenu());
79         setAcceptedButtons(Qt::LeftButton | Qt::RightButton);
80         break;
81     case DecorationButtonType::ApplicationMenu:
82         setVisible(c->hasApplicationMenu());
83         setCheckable(true); // will be "checked" whilst the menu is opened
84         // FIXME TODO connect directly and figure out the button geometry/offset stuff
85         QObject::connect(
86             q,
87             &DecorationButton::clicked,
88             decoration.data(),
89             [this] {
90                 decoration->requestShowApplicationMenu(q->geometry().toRect(), 0 /* actionId */);
91             },
92             Qt::QueuedConnection); //&Decoration::requestShowApplicationMenu, Qt::QueuedConnection);
93         QObject::connect(c, &DecoratedClient::hasApplicationMenuChanged, q, &DecorationButton::setVisible);
94         QObject::connect(c, &DecoratedClient::applicationMenuActiveChanged, q, &DecorationButton::setChecked);
95         break;
96     case DecorationButtonType::OnAllDesktops:
97         setVisible(settings->isOnAllDesktopsAvailable());
98         setCheckable(true);
99         setChecked(c->isOnAllDesktops());
100         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleOnAllDesktops, Qt::QueuedConnection);
101         QObject::connect(settings.data(), &DecorationSettings::onAllDesktopsAvailableChanged, q, &DecorationButton::setVisible);
102         QObject::connect(c, &DecoratedClient::onAllDesktopsChanged, q, &DecorationButton::setChecked);
103         break;
104     case DecorationButtonType::Minimize:
105         setEnabled(c->isMinimizeable());
106         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestMinimize, Qt::QueuedConnection);
107         QObject::connect(c, &DecoratedClient::minimizeableChanged, q, &DecorationButton::setEnabled);
108         break;
109     case DecorationButtonType::Maximize:
110         setEnabled(c->isMaximizeable());
111         setCheckable(true);
112         setChecked(c->isMaximized());
113         setAcceptedButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton);
114         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleMaximization, Qt::QueuedConnection);
115         QObject::connect(c, &DecoratedClient::maximizeableChanged, q, &DecorationButton::setEnabled);
116         QObject::connect(c, &DecoratedClient::maximizedChanged, q, &DecorationButton::setChecked);
117         break;
118     case DecorationButtonType::Close:
119         setEnabled(c->isCloseable());
120         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestClose, Qt::QueuedConnection);
121         QObject::connect(c, &DecoratedClient::closeableChanged, q, &DecorationButton::setEnabled);
122         break;
123     case DecorationButtonType::ContextHelp:
124         setVisible(c->providesContextHelp());
125         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestContextHelp, Qt::QueuedConnection);
126         QObject::connect(c, &DecoratedClient::providesContextHelpChanged, q, &DecorationButton::setVisible);
127         break;
128     case DecorationButtonType::KeepAbove:
129         setCheckable(true);
130         setChecked(c->isKeepAbove());
131         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleKeepAbove, Qt::QueuedConnection);
132         QObject::connect(c, &DecoratedClient::keepAboveChanged, q, &DecorationButton::setChecked);
133         break;
134     case DecorationButtonType::KeepBelow:
135         setCheckable(true);
136         setChecked(c->isKeepBelow());
137         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleKeepBelow, Qt::QueuedConnection);
138         QObject::connect(c, &DecoratedClient::keepBelowChanged, q, &DecorationButton::setChecked);
139         break;
140     case DecorationButtonType::Shade:
141         setEnabled(c->isShadeable());
142         setCheckable(true);
143         setChecked(c->isShaded());
144         QObject::connect(q, &DecorationButton::clicked, decoration.data(), &Decoration::requestToggleShade, Qt::QueuedConnection);
145         QObject::connect(c, &DecoratedClient::shadedChanged, q, &DecorationButton::setChecked);
146         QObject::connect(c, &DecoratedClient::shadeableChanged, q, &DecorationButton::setEnabled);
147         break;
148     default:
149         // nothing
150         break;
151     }
152 }
153 
setHovered(bool set)154 void DecorationButton::Private::setHovered(bool set)
155 {
156     if (hovered == set) {
157         return;
158     }
159     hovered = set;
160     Q_EMIT q->hoveredChanged(hovered);
161 }
162 
setEnabled(bool set)163 void DecorationButton::Private::setEnabled(bool set)
164 {
165     if (enabled == set) {
166         return;
167     }
168     enabled = set;
169     Q_EMIT q->enabledChanged(enabled);
170     if (!enabled) {
171         setHovered(false);
172         if (isPressed()) {
173             m_pressed = Qt::NoButton;
174             Q_EMIT q->pressedChanged(false);
175         }
176     }
177 }
178 
setVisible(bool set)179 void DecorationButton::Private::setVisible(bool set)
180 {
181     if (visible == set) {
182         return;
183     }
184     visible = set;
185     Q_EMIT q->visibilityChanged(set);
186     if (!visible) {
187         setHovered(false);
188         if (isPressed()) {
189             m_pressed = Qt::NoButton;
190             Q_EMIT q->pressedChanged(false);
191         }
192     }
193 }
194 
setChecked(bool set)195 void DecorationButton::Private::setChecked(bool set)
196 {
197     if (!checkable || checked == set) {
198         return;
199     }
200     checked = set;
201     Q_EMIT q->checkedChanged(checked);
202 }
203 
setCheckable(bool set)204 void DecorationButton::Private::setCheckable(bool set)
205 {
206     if (checkable == set) {
207         return;
208     }
209     if (!set) {
210         setChecked(false);
211     }
212     checkable = set;
213     Q_EMIT q->checkableChanged(checkable);
214 }
215 
setPressed(Qt::MouseButton button,bool pressed)216 void DecorationButton::Private::setPressed(Qt::MouseButton button, bool pressed)
217 {
218     if (pressed) {
219         m_pressed = m_pressed | button;
220     } else {
221         m_pressed = m_pressed & ~button;
222     }
223     Q_EMIT q->pressedChanged(isPressed());
224 }
225 
setAcceptedButtons(Qt::MouseButtons buttons)226 void DecorationButton::Private::setAcceptedButtons(Qt::MouseButtons buttons)
227 {
228     if (acceptedButtons == buttons) {
229         return;
230     }
231     acceptedButtons = buttons;
232     Q_EMIT q->acceptedButtonsChanged(acceptedButtons);
233 }
234 
startDoubleClickTimer()235 void DecorationButton::Private::startDoubleClickTimer()
236 {
237     if (!doubleClickEnabled) {
238         return;
239     }
240     if (m_doubleClickTimer.isNull()) {
241         m_doubleClickTimer.reset(new QElapsedTimer());
242     }
243     m_doubleClickTimer->start();
244 }
245 
invalidateDoubleClickTimer()246 void DecorationButton::Private::invalidateDoubleClickTimer()
247 {
248     if (m_doubleClickTimer.isNull()) {
249         return;
250     }
251     m_doubleClickTimer->invalidate();
252 }
253 
wasDoubleClick() const254 bool DecorationButton::Private::wasDoubleClick() const
255 {
256     if (m_doubleClickTimer.isNull() || !m_doubleClickTimer->isValid()) {
257         return false;
258     }
259     return !m_doubleClickTimer->hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval());
260 }
261 
setPressAndHold(bool enable)262 void DecorationButton::Private::setPressAndHold(bool enable)
263 {
264     if (pressAndHold == enable) {
265         return;
266     }
267     pressAndHold = enable;
268     if (!pressAndHold) {
269         m_pressAndHoldTimer.reset();
270     }
271 }
272 
startPressAndHold()273 void DecorationButton::Private::startPressAndHold()
274 {
275     if (!pressAndHold) {
276         return;
277     }
278     if (m_pressAndHoldTimer.isNull()) {
279         m_pressAndHoldTimer.reset(new QTimer());
280         m_pressAndHoldTimer->setSingleShot(true);
281         QObject::connect(m_pressAndHoldTimer.data(), &QTimer::timeout, q, [this]() {
282             q->clicked(Qt::LeftButton);
283         });
284     }
285     m_pressAndHoldTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval());
286 }
287 
stopPressAndHold()288 void DecorationButton::Private::stopPressAndHold()
289 {
290     if (!m_pressAndHoldTimer.isNull()) {
291         m_pressAndHoldTimer->stop();
292     }
293 }
294 
typeToString(DecorationButtonType type)295 QString DecorationButton::Private::typeToString(DecorationButtonType type)
296 {
297     switch (type) {
298     case DecorationButtonType::Menu:
299         return i18n("More actions for this window");
300     case DecorationButtonType::ApplicationMenu:
301         return i18n("Application menu");
302     case DecorationButtonType::OnAllDesktops:
303         if (this->q->isChecked())
304             return i18n("On one desktop");
305         else
306             return i18n("On all desktops");
307     case DecorationButtonType::Minimize:
308         return i18n("Minimize");
309     case DecorationButtonType::Maximize:
310         if (this->q->isChecked())
311             return i18n("Restore");
312         else
313             return i18n("Maximize");
314     case DecorationButtonType::Close:
315         return i18n("Close");
316     case DecorationButtonType::ContextHelp:
317         return i18n("Context help");
318     case DecorationButtonType::Shade:
319         if (this->q->isChecked())
320             return i18n("Unshade");
321         else
322             return i18n("Shade");
323     case DecorationButtonType::KeepBelow:
324         if (this->q->isChecked())
325             return i18n("Don't keep below other windows");
326         else
327             return i18n("Keep below other windows");
328     case DecorationButtonType::KeepAbove:
329         if (this->q->isChecked())
330             return i18n("Don't keep above other windows");
331         else
332             return i18n("Keep above other windows");
333     default:
334         return QString();
335     }
336 }
337 
DecorationButton(DecorationButtonType type,const QPointer<Decoration> & decoration,QObject * parent)338 DecorationButton::DecorationButton(DecorationButtonType type, const QPointer<Decoration> &decoration, QObject *parent)
339     : QObject(parent)
340     , d(new Private(type, decoration, this))
341 {
342     decoration->d->addButton(this);
343     connect(this, &DecorationButton::geometryChanged, this, static_cast<void (DecorationButton::*)(const QRectF &)>(&DecorationButton::update));
344     auto updateSlot = static_cast<void (DecorationButton::*)()>(&DecorationButton::update);
345     connect(this, &DecorationButton::hoveredChanged, this, updateSlot);
346     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
347         if (hovered) {
348             // TODO: show tooltip if hovered and hide if not
349             const QString type = this->d->typeToString(this->type());
350             this->decoration()->requestShowToolTip(type);
351         } else {
352             this->decoration()->requestHideToolTip();
353         }
354     });
355     connect(this, &DecorationButton::pressedChanged, this, updateSlot);
356     connect(this, &DecorationButton::checkedChanged, this, updateSlot);
357     connect(this, &DecorationButton::enabledChanged, this, updateSlot);
358     connect(this, &DecorationButton::visibilityChanged, this, updateSlot);
359     connect(this, &DecorationButton::hoveredChanged, this, [this](bool hovered) {
360         if (hovered) {
361             Q_EMIT pointerEntered();
362         } else {
363             Q_EMIT pointerLeft();
364         }
365     });
366     connect(this, &DecorationButton::pressedChanged, this, [this](bool p) {
367         if (p) {
368             Q_EMIT pressed();
369         } else {
370             Q_EMIT released();
371         }
372     });
373 }
374 
375 DecorationButton::~DecorationButton() = default;
376 
update(const QRectF & rect)377 void DecorationButton::update(const QRectF &rect)
378 {
379     decoration()->update(rect.isNull() ? geometry().toRect() : rect.toRect());
380 }
381 
update()382 void DecorationButton::update()
383 {
384     update(QRectF());
385 }
386 
size() const387 QSizeF DecorationButton::size() const
388 {
389     return d->geometry.size();
390 }
391 
isPressed() const392 bool DecorationButton::isPressed() const
393 {
394     return d->isPressed();
395 }
396 
397 #define DELEGATE(name, variableName, type)                                                                                                                     \
398     type DecorationButton::name() const                                                                                                                        \
399     {                                                                                                                                                          \
400         return d->variableName;                                                                                                                                \
401     }
402 
DELEGATE(isHovered,hovered,bool)403 DELEGATE(isHovered, hovered, bool)
404 DELEGATE(isEnabled, enabled, bool)
405 DELEGATE(isChecked, checked, bool)
406 DELEGATE(isCheckable, checkable, bool)
407 DELEGATE(isVisible, visible, bool)
408 
409 #define DELEGATE2(name, type) DELEGATE(name, name, type)
410 DELEGATE2(geometry, QRectF)
411 DELEGATE2(decoration, QPointer<Decoration>)
412 DELEGATE2(acceptedButtons, Qt::MouseButtons)
413 DELEGATE2(type, DecorationButtonType)
414 
415 #undef DELEGATE2
416 #undef DELEGATE
417 
418 #define DELEGATE(name, type)                                                                                                                                   \
419     void DecorationButton::name(type a)                                                                                                                        \
420     {                                                                                                                                                          \
421         d->name(a);                                                                                                                                            \
422     }
423 
424 DELEGATE(setAcceptedButtons, Qt::MouseButtons)
425 DELEGATE(setEnabled, bool)
426 DELEGATE(setChecked, bool)
427 DELEGATE(setCheckable, bool)
428 DELEGATE(setVisible, bool)
429 
430 #undef DELEGATE
431 
432 #define DELEGATE(name, variableName, type)                                                                                                                     \
433     void DecorationButton::name(type a)                                                                                                                        \
434     {                                                                                                                                                          \
435         if (d->variableName == a) {                                                                                                                            \
436             return;                                                                                                                                            \
437         }                                                                                                                                                      \
438         d->variableName = a;                                                                                                                                   \
439         Q_EMIT variableName##Changed(d->variableName);                                                                                                         \
440     }
441 
442 DELEGATE(setGeometry, geometry, const QRectF &)
443 
444 #undef DELEGATE
445 
446 bool DecorationButton::contains(const QPointF &pos) const
447 {
448     return d->geometry.toRect().contains(pos.toPoint());
449 }
450 
event(QEvent * event)451 bool DecorationButton::event(QEvent *event)
452 {
453     switch (event->type()) {
454     case QEvent::HoverEnter:
455         hoverEnterEvent(static_cast<QHoverEvent *>(event));
456         return true;
457     case QEvent::HoverLeave:
458         hoverLeaveEvent(static_cast<QHoverEvent *>(event));
459         return true;
460     case QEvent::HoverMove:
461         hoverMoveEvent(static_cast<QHoverEvent *>(event));
462         return true;
463     case QEvent::MouseButtonPress:
464         mousePressEvent(static_cast<QMouseEvent *>(event));
465         return true;
466     case QEvent::MouseButtonRelease:
467         mouseReleaseEvent(static_cast<QMouseEvent *>(event));
468         return true;
469     case QEvent::MouseMove:
470         mouseMoveEvent(static_cast<QMouseEvent *>(event));
471         return true;
472     case QEvent::Wheel:
473         wheelEvent(static_cast<QWheelEvent *>(event));
474         return true;
475     default:
476         return QObject::event(event);
477     }
478 }
479 
hoverEnterEvent(QHoverEvent * event)480 void DecorationButton::hoverEnterEvent(QHoverEvent *event)
481 {
482     if (!d->enabled || !d->visible || !contains(event->posF())) {
483         return;
484     }
485     d->setHovered(true);
486     event->setAccepted(true);
487 }
488 
hoverLeaveEvent(QHoverEvent * event)489 void DecorationButton::hoverLeaveEvent(QHoverEvent *event)
490 {
491     if (!d->enabled || !d->visible || !d->hovered || contains(event->posF())) {
492         return;
493     }
494     d->setHovered(false);
495     event->setAccepted(true);
496 }
497 
hoverMoveEvent(QHoverEvent * event)498 void DecorationButton::hoverMoveEvent(QHoverEvent *event)
499 {
500     Q_UNUSED(event)
501 }
502 
mouseMoveEvent(QMouseEvent * event)503 void DecorationButton::mouseMoveEvent(QMouseEvent *event)
504 {
505     if (!d->enabled || !d->visible || !d->hovered) {
506         return;
507     }
508     if (!contains(event->localPos())) {
509         d->setHovered(false);
510         event->setAccepted(true);
511     }
512 }
513 
mousePressEvent(QMouseEvent * event)514 void DecorationButton::mousePressEvent(QMouseEvent *event)
515 {
516     if (!d->enabled || !d->visible || !contains(event->localPos()) || !d->acceptedButtons.testFlag(event->button())) {
517         return;
518     }
519     d->setPressed(event->button(), true);
520     event->setAccepted(true);
521     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
522         // check for double click
523         if (d->wasDoubleClick()) {
524             event->setAccepted(true);
525             Q_EMIT doubleClicked();
526         }
527         d->invalidateDoubleClickTimer();
528     }
529     if (d->pressAndHold && event->button() == Qt::LeftButton) {
530         d->startPressAndHold();
531     }
532 }
533 
mouseReleaseEvent(QMouseEvent * event)534 void DecorationButton::mouseReleaseEvent(QMouseEvent *event)
535 {
536     if (!d->enabled || !d->visible || !d->isPressed(event->button())) {
537         return;
538     }
539     if (contains(event->localPos())) {
540         if (!d->pressAndHold || event->button() != Qt::LeftButton) {
541             Q_EMIT clicked(event->button());
542         } else {
543             d->stopPressAndHold();
544         }
545     }
546     d->setPressed(event->button(), false);
547     event->setAccepted(true);
548 
549     if (d->doubleClickEnabled && event->button() == Qt::LeftButton) {
550         d->startDoubleClickTimer();
551     }
552 }
553 
wheelEvent(QWheelEvent * event)554 void DecorationButton::wheelEvent(QWheelEvent *event)
555 {
556     Q_UNUSED(event)
557 }
558 
559 }
560