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