1// Copyright (C) 2023 Jan Grulich <jgrulich@redhat.com>
2// Copyright (C) 2023 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5#include "qwaylandadwaitadecoration_p.h"
6
7// QtCore
8#include <QtCore/QLoggingCategory>
9#include <QScopeGuard>
10
11// QtDBus
12#include <QtDBus/QDBusArgument>
13#include <QtDBus/QDBusConnection>
14#include <QtDBus/QDBusMessage>
15#include <QtDBus/QDBusPendingCall>
16#include <QtDBus/QDBusPendingCallWatcher>
17#include <QtDBus/QDBusPendingReply>
18#include <QtDBus/QDBusVariant>
19#include <QtDBus/QtDBus>
20
21// QtGui
22#include <QtGui/QColor>
23#include <QtGui/QPainter>
24#include <QtGui/QPainterPath>
25
26#include <QtGui/private/qguiapplication_p.h>
27#include <QtGui/qpa/qplatformtheme.h>
28
29// QtSvg
30#include <QtSvg/QSvgRenderer>
31
32// QtWayland
33#include <QtWaylandClient/private/qwaylandshmbackingstore_p.h>
34#include <QtWaylandClient/private/qwaylandwindow_p.h>
35
36
37QT_BEGIN_NAMESPACE
38
39using namespace Qt::StringLiterals;
40
41namespace QtWaylandClient {
42
43static constexpr int ceButtonSpacing = 12;
44static constexpr int ceButtonWidth = 24;
45static constexpr int ceCornerRadius = 12;
46static constexpr int ceShadowsWidth = 10;
47static constexpr int ceTitlebarHeight = 38;
48static constexpr int ceWindowBorderWidth = 1;
49static constexpr qreal ceTitlebarSeperatorWidth = 0.5;
50
51static QMap<QWaylandAdwaitaDecoration::ButtonIcon, QString> buttonMap = {
52 { QWaylandAdwaitaDecoration::CloseIcon, "window-close-symbolic"_L1 },
53 { QWaylandAdwaitaDecoration::MinimizeIcon, "window-minimize-symbolic"_L1 },
54 { QWaylandAdwaitaDecoration::MaximizeIcon, "window-maximize-symbolic"_L1 },
55 { QWaylandAdwaitaDecoration::RestoreIcon, "window-restore-symbolic"_L1 }
56};
57
58const QDBusArgument &operator>>(const QDBusArgument &argument, QMap<QString, QVariantMap> &map)
59{
60 argument.beginMap();
61 map.clear();
62
63 while (!argument.atEnd()) {
64 QString key;
65 QVariantMap value;
66 argument.beginMapEntry();
67 argument >> key >> value;
68 argument.endMapEntry();
69 map.insert(key, value);
70 }
71
72 argument.endMap();
73 return argument;
74}
75
76Q_LOGGING_CATEGORY(lcQWaylandAdwaitaDecorationLog, "qt.qpa.qwaylandadwaitadecoration", QtWarningMsg)
77
78QWaylandAdwaitaDecoration::QWaylandAdwaitaDecoration()
79 : QWaylandAbstractDecoration()
80{
81 m_lastButtonClick = QDateTime::currentDateTime();
82
83 QTextOption option(Qt::AlignHCenter | Qt::AlignVCenter);
84 option.setWrapMode(QTextOption::NoWrap);
85 m_windowTitle.setTextOption(option);
86 m_windowTitle.setTextFormat(Qt::PlainText);
87
88 const QPlatformTheme *theme = QGuiApplicationPrivate::platformTheme();
89 if (const QFont *font = theme->font(type: QPlatformTheme::TitleBarFont))
90 m_font = std::make_unique<QFont>(args: *font);
91 if (!m_font) // Fallback to GNOME's default font
92 m_font = std::make_unique<QFont>(args: "Cantarell"_L1, args: 10);
93
94 QTimer::singleShot(interval: 0, receiver: this, slot: &QWaylandAdwaitaDecoration::loadConfiguration);
95}
96
97QMargins QWaylandAdwaitaDecoration::margins(QWaylandAbstractDecoration::MarginsType marginsType) const
98{
99 const bool onlyShadows = marginsType == QWaylandAbstractDecoration::ShadowsOnly;
100 const bool shadowsExcluded = marginsType == ShadowsExcluded;
101
102 if (waylandWindow()->windowStates() & Qt::WindowMaximized) {
103 // Maximized windows don't have anything around, no shadows, border,
104 // etc. Only report titlebar height in case we are not asking for shadow
105 // margins.
106 return QMargins(0, onlyShadows ? 0 : ceTitlebarHeight, 0, 0);
107 }
108
109 const QWaylandWindow::ToplevelWindowTilingStates tilingStates = waylandWindow()->toplevelWindowTilingStates();
110
111 // Since all sides (left, right, bottom) are going to be same
112 const int marginsBase = shadowsExcluded ? ceWindowBorderWidth : ceShadowsWidth + ceWindowBorderWidth;
113 const int sideMargins = onlyShadows ? ceShadowsWidth : marginsBase;
114 const int topMargins = onlyShadows ? ceShadowsWidth : ceTitlebarHeight + marginsBase;
115
116 return QMargins(tilingStates & QWaylandWindow::WindowTiledLeft ? 0 : sideMargins,
117 tilingStates & QWaylandWindow::WindowTiledTop ? onlyShadows ? 0 : ceTitlebarHeight : topMargins,
118 tilingStates & QWaylandWindow::WindowTiledRight ? 0 : sideMargins,
119 tilingStates & QWaylandWindow::WindowTiledBottom ? 0 : sideMargins);
120}
121
122void QWaylandAdwaitaDecoration::paint(QPaintDevice *device)
123{
124 const QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(marginsType: ShadowsOnly);
125
126 QPainter p(device);
127 p.setRenderHint(hint: QPainter::Antialiasing);
128
129 /*
130 * Titlebar and window border
131 */
132 const int titleBarWidth = surfaceRect.width() - margins().left() - margins().right();
133 QPainterPath path;
134
135 // Maximized or tiled won't have rounded corners
136 if (waylandWindow()->windowStates() & Qt::WindowMaximized
137 || waylandWindow()->toplevelWindowTilingStates() != QWaylandWindow::WindowNoState)
138 path.addRect(x: margins().left(), y: margins().bottom(), w: titleBarWidth, h: margins().top());
139 else
140 path.addRoundedRect(x: margins().left(), y: margins().bottom(), w: titleBarWidth,
141 h: margins().top() + ceCornerRadius, xRadius: ceCornerRadius, yRadius: ceCornerRadius);
142
143 p.save();
144 p.setPen(color(type: Border));
145 p.fillPath(path: path.simplified(), brush: color(type: Background));
146 p.drawPath(path);
147 p.drawRect(x: margins().left(), y: margins().top(), w: titleBarWidth, h: surfaceRect.height() - margins().top() - margins().bottom());
148 p.restore();
149
150
151 /*
152 * Titlebar separator
153 */
154 p.save();
155 p.setPen(color(type: Border));
156 p.drawLine(l: QLineF(margins().left(), margins().top() - ceTitlebarSeperatorWidth,
157 surfaceRect.width() - margins().right(),
158 margins().top() - ceTitlebarSeperatorWidth));
159 p.restore();
160
161
162 /*
163 * Window title
164 */
165 const QRect top = QRect(margins().left(), margins().bottom(), surfaceRect.width(),
166 margins().top() - margins().bottom());
167 const QString windowTitleText = waylandWindow()->windowTitle();
168 if (!windowTitleText.isEmpty()) {
169 if (m_windowTitle.text() != windowTitleText) {
170 m_windowTitle.setText(windowTitleText);
171 m_windowTitle.prepare();
172 }
173
174 QRect titleBar = top;
175 if (m_placement == Right) {
176 titleBar.setLeft(margins().left());
177 titleBar.setRight(static_cast<int>(buttonRect(button: Minimize).left()) - 8);
178 } else {
179 titleBar.setLeft(static_cast<int>(buttonRect(button: Minimize).right()) + 8);
180 titleBar.setRight(surfaceRect.width() - margins().right());
181 }
182
183 p.save();
184 p.setClipRect(titleBar);
185 p.setPen(color(type: Foreground));
186 QSize size = m_windowTitle.size().toSize();
187 int dx = (top.width() - size.width()) / 2;
188 int dy = (top.height() - size.height()) / 2;
189 p.setFont(*m_font);
190 QPoint windowTitlePoint(top.topLeft().x() + dx, top.topLeft().y() + dy);
191 p.drawStaticText(p: windowTitlePoint, staticText: m_windowTitle);
192 p.restore();
193 }
194
195
196 /*
197 * Buttons
198 */
199 if (m_buttons.contains(key: Close))
200 drawButton(button: Close, painter: &p);
201
202 if (m_buttons.contains(key: Maximize))
203 drawButton(button: Maximize, painter: &p);
204
205 if (m_buttons.contains(key: Minimize))
206 drawButton(button: Minimize, painter: &p);
207}
208
209bool QWaylandAdwaitaDecoration::handleMouse(QWaylandInputDevice *inputDevice, const QPointF &local,
210 const QPointF &global, Qt::MouseButtons b,
211 Qt::KeyboardModifiers mods)
212{
213 Q_UNUSED(global)
214
215 if (local.y() > margins().top())
216 updateButtonHoverState(hoveredButton: Button::None);
217
218 // Figure out what area mouse is in
219 QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(marginsType: ShadowsOnly);
220 if (local.y() <= surfaceRect.top() + margins().top())
221 processMouseTop(inputDevice, local, b, mods);
222 else if (local.y() > surfaceRect.bottom() - margins().bottom())
223 processMouseBottom(inputDevice, local, b, mods);
224 else if (local.x() <= surfaceRect.left() + margins().left())
225 processMouseLeft(inputDevice, local, b, mods);
226 else if (local.x() > surfaceRect.right() - margins().right())
227 processMouseRight(inputDevice, local, b, mods);
228 else {
229#if QT_CONFIG(cursor)
230 waylandWindow()->restoreMouseCursor(device: inputDevice);
231#endif
232 }
233
234 // Reset clicking state in case a button press is released outside
235 // the button area
236 if (isLeftReleased(newMouseButtonState: b)) {
237 m_clicking = None;
238 requestRepaint();
239 }
240
241 setMouseButtons(b);
242 return false;
243}
244
245bool QWaylandAdwaitaDecoration::handleTouch(QWaylandInputDevice *inputDevice, const QPointF &local,
246 const QPointF &global, QEventPoint::State state,
247 Qt::KeyboardModifiers mods)
248{
249 Q_UNUSED(inputDevice)
250 Q_UNUSED(global)
251 Q_UNUSED(mods)
252
253 bool handled = state == QEventPoint::Pressed;
254
255 if (handled) {
256 if (buttonRect(button: Close).contains(p: local))
257 QWindowSystemInterface::handleCloseEvent(window: window());
258 else if (m_buttons.contains(key: Maximize) && buttonRect(button: Maximize).contains(p: local))
259 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
260 else if (m_buttons.contains(key: Minimize) && buttonRect(button: Minimize).contains(p: local))
261 window()->setWindowState(Qt::WindowMinimized);
262 else if (local.y() <= margins().top())
263 waylandWindow()->shellSurface()->move(inputDevice);
264 else
265 handled = false;
266 }
267
268 return handled;
269}
270
271QString getIconSvg(const QString &iconName)
272{
273 const QStringList themeNames = { QIcon::themeName(), QIcon::fallbackThemeName(), "Adwaita"_L1 };
274
275 qCDebug(lcQWaylandAdwaitaDecorationLog) << "Searched icon themes: " << themeNames;
276
277 for (const QString &themeName : themeNames) {
278 if (themeName.isEmpty())
279 continue;
280
281 for (const QString &path : QIcon::themeSearchPaths()) {
282 if (path.startsWith(c: QLatin1Char(':')))
283 continue;
284
285 const QString fullPath = QString("%1/%2").arg(a: path).arg(a: themeName);
286 QDirIterator dirIt(fullPath, {"*.svg"}, QDir::Files, QDirIterator::Subdirectories);
287 while (dirIt.hasNext()) {
288 const QString fileName = dirIt.next();
289 const QFileInfo fileInfo(fileName);
290
291 if (fileInfo.fileName() == iconName) {
292 qCDebug(lcQWaylandAdwaitaDecorationLog) << "Using " << iconName << " from " << themeName << " theme";
293 QFile readFile(fileInfo.filePath());
294 readFile.open(flags: QFile::ReadOnly);
295 return readFile.readAll();
296 }
297 }
298 }
299 }
300
301 qCWarning(lcQWaylandAdwaitaDecorationLog) << "Failed to find an svg icon for " << iconName;
302
303 return QString();
304}
305
306void QWaylandAdwaitaDecoration::loadConfiguration()
307{
308 qRegisterMetaType<QDBusVariant>();
309 qDBusRegisterMetaType<QMap<QString, QVariantMap>>();
310
311 QDBusConnection connection = QDBusConnection::sessionBus();
312
313 QDBusMessage message = QDBusMessage::createMethodCall(destination: "org.freedesktop.portal.Desktop"_L1,
314 path: "/org/freedesktop/portal/desktop"_L1,
315 interface: "org.freedesktop.portal.Settings"_L1,
316 method: "ReadAll"_L1);
317 message << QStringList{ { "org.gnome.desktop.wm.preferences"_L1 },
318 { "org.freedesktop.appearance"_L1 } };
319
320 QDBusPendingCall pendingCall = connection.asyncCall(message);
321 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this);
322 QObject::connect(sender: watcher, signal: &QDBusPendingCallWatcher::finished, context: this, slot: [this](QDBusPendingCallWatcher *watcher) {
323 QDBusPendingReply<QMap<QString, QVariantMap>> reply = *watcher;
324 if (reply.isValid()) {
325 QMap<QString, QVariantMap> settings = reply.value();
326 if (!settings.isEmpty()) {
327 const uint colorScheme = settings.value(key: "org.freedesktop.appearance"_L1).value(key: "color-scheme"_L1).toUInt();
328 updateColors(isDark: colorScheme == 1); // 1 == Prefer Dark
329 const QString buttonLayout = settings.value(key: "org.gnome.desktop.wm.preferences"_L1).value(key: "button-layout"_L1).toString();
330 if (!buttonLayout.isEmpty())
331 updateTitlebarLayout(layout: buttonLayout);
332 // Workaround for QGtkStyle not having correct titlebar font
333 const QString titlebarFont =
334 settings.value(key: "org.gnome.desktop.wm.preferences"_L1).value(key: "titlebar-font"_L1).toString();
335 if (titlebarFont.contains(s: "bold"_L1, cs: Qt::CaseInsensitive)) {
336 m_font->setBold(true);
337 }
338 }
339 }
340 watcher->deleteLater();
341 });
342
343 QDBusConnection::sessionBus().connect(service: QString(), path: "/org/freedesktop/portal/desktop"_L1,
344 interface: "org.freedesktop.portal.Settings"_L1, name: "SettingChanged"_L1, receiver: this,
345 SLOT(settingChanged(QString, QString, QDBusVariant)));
346
347 // Load SVG icons
348 for (auto mapIt = buttonMap.constBegin(); mapIt != buttonMap.constEnd(); mapIt++) {
349 const QString fullName = mapIt.value() + QStringLiteral(".svg");
350 m_icons[mapIt.key()] = getIconSvg(iconName: fullName);
351 }
352
353 updateColors(isDark: false);
354}
355
356void QWaylandAdwaitaDecoration::updateColors(bool isDark)
357{
358 qCDebug(lcQWaylandAdwaitaDecorationLog) << "Color scheme changed to: " << (isDark ? "dark" : "light");
359
360 m_colors = { { Background, isDark ? QColor(0x303030) : QColor(0xffffff) },
361 { BackgroundInactive, isDark ? QColor(0x242424) : QColor(0xfafafa) },
362 { Foreground, isDark ? QColor(0xffffff) : QColor(0x2e2e2e) },
363 { ForegroundInactive, isDark ? QColor(0x919191) : QColor(0x949494) },
364 { Border, isDark ? QColor(0x3b3b3b) : QColor(0xdbdbdb) },
365 { BorderInactive, isDark ? QColor(0x303030) : QColor(0xdbdbdb) },
366 { ButtonBackground, isDark ? QColor(0x444444) : QColor(0xebebeb) },
367 { ButtonBackgroundInactive, isDark ? QColor(0x2e2e2e) : QColor(0xf0f0f0) },
368 { HoveredButtonBackground, isDark ? QColor(0x4f4f4f) : QColor(0xe0e0e0) },
369 { PressedButtonBackground, isDark ? QColor(0x6e6e6e) : QColor(0xc2c2c2) } };
370 requestRepaint();
371}
372
373void QWaylandAdwaitaDecoration::updateTitlebarLayout(const QString &layout)
374{
375 const QStringList layouts = layout.split(sep: QLatin1Char(':'));
376 if (layouts.count() != 2)
377 return;
378
379 // Remove previous configuration
380 m_buttons.clear();
381
382 const QString &leftLayout = layouts.at(i: 0);
383 const QString &rightLayout = layouts.at(i: 1);
384 m_placement = leftLayout.contains(s: "close"_L1) ? Left : Right;
385
386 int pos = 1;
387 const QString &buttonLayout = m_placement == Right ? rightLayout : leftLayout;
388
389 QStringList buttonList = buttonLayout.split(sep: QLatin1Char(','));
390 if (m_placement == Right)
391 std::reverse(first: buttonList.begin(), last: buttonList.end());
392
393 for (const QString &button : buttonList) {
394 if (button == "close"_L1)
395 m_buttons.insert(key: Close, value: pos);
396 else if (button == "maximize"_L1)
397 m_buttons.insert(key: Maximize, value: pos);
398 else if (button == "minimize"_L1)
399 m_buttons.insert(key: Minimize, value: pos);
400
401 pos++;
402 }
403
404 qCDebug(lcQWaylandAdwaitaDecorationLog) << "Button layout changed to: " << layout;
405
406 requestRepaint();
407}
408
409void QWaylandAdwaitaDecoration::settingChanged(const QString &group, const QString &key,
410 const QDBusVariant &value)
411{
412 if (group == "org.gnome.desktop.wm.preferences"_L1 && key == "button-layout"_L1) {
413 const QString layout = value.variant().toString();
414 updateTitlebarLayout(layout);
415 } else if (group == "org.freedesktop.appearance"_L1 && key == "color-scheme"_L1) {
416 const uint colorScheme = value.variant().toUInt();
417 updateColors(isDark: colorScheme == 1); // 1 == Prefer Dark
418 }
419}
420
421QRectF QWaylandAdwaitaDecoration::buttonRect(Button button) const
422{
423 int xPos;
424 int yPos;
425 const int btnPos = m_buttons.value(key: button);
426
427 const QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(marginsType: QWaylandAbstractDecoration::ShadowsOnly);
428 if (m_placement == Right) {
429 xPos = surfaceRect.width();
430 xPos -= ceButtonWidth * btnPos;
431 xPos -= ceButtonSpacing * btnPos;
432 xPos -= margins(marginsType: ShadowsOnly).right();
433 } else {
434 xPos = 0;
435 xPos += ceButtonWidth * btnPos;
436 xPos += ceButtonSpacing * btnPos;
437 xPos += margins(marginsType: ShadowsOnly).left();
438 // We are painting from the left to the right so the real
439 // position doesn't need to by moved by the size of the button.
440 xPos -= ceButtonWidth;
441 }
442
443 yPos = margins().top();
444 yPos += margins().bottom();
445 yPos -= ceButtonWidth;
446 yPos /= 2;
447
448 return QRectF(xPos, yPos, ceButtonWidth, ceButtonWidth);
449}
450
451static void renderFlatRoundedButtonFrame(QPainter *painter, const QRect &rect, const QColor &color)
452{
453 painter->save();
454 painter->setRenderHint(hint: QPainter::Antialiasing, on: true);
455 painter->setPen(Qt::NoPen);
456 painter->setBrush(color);
457 painter->drawEllipse(r: rect);
458 painter->restore();
459}
460
461static void renderButtonIcon(const QString &svgIcon, QPainter *painter, const QRect &rect, const QColor &color)
462{
463 painter->save();
464 painter->setRenderHints(hints: QPainter::Antialiasing, on: true);
465
466 QString icon = svgIcon;
467 QRegularExpression regexp("fill=[\"']#[0-9A-F]{6}[\"']", QRegularExpression::CaseInsensitiveOption);
468 QRegularExpression regexpAlt("fill:#[0-9A-F]{6}", QRegularExpression::CaseInsensitiveOption);
469 QRegularExpression regexpCurrentColor("fill=[\"']currentColor[\"']");
470 icon.replace(re: regexp, after: QString("fill=\"%1\"").arg(a: color.name()));
471 icon.replace(re: regexpAlt, after: QString("fill:%1").arg(a: color.name()));
472 icon.replace(re: regexpCurrentColor, after: QString("fill=\"%1\"").arg(a: color.name()));
473 QSvgRenderer svgRenderer(icon.toLocal8Bit());
474 svgRenderer.render(p: painter, bounds: rect);
475
476 painter->restore();
477}
478
479static void renderButtonIcon(QWaylandAdwaitaDecoration::ButtonIcon buttonIcon, QPainter *painter, const QRect &rect)
480{
481 QString iconName = buttonMap[buttonIcon];
482
483 painter->save();
484 painter->setRenderHints(hints: QPainter::Antialiasing, on: true);
485 painter->drawPixmap(r: rect, pm: QIcon::fromTheme(name: iconName).pixmap(w: ceButtonWidth, h: ceButtonWidth));
486 painter->restore();
487}
488
489static QWaylandAdwaitaDecoration::ButtonIcon iconFromButtonAndState(QWaylandAdwaitaDecoration::Button button, bool maximized)
490{
491 if (button == QWaylandAdwaitaDecoration::Close)
492 return QWaylandAdwaitaDecoration::CloseIcon;
493 else if (button == QWaylandAdwaitaDecoration::Minimize)
494 return QWaylandAdwaitaDecoration::MinimizeIcon;
495 else if (button == QWaylandAdwaitaDecoration::Maximize && maximized)
496 return QWaylandAdwaitaDecoration::RestoreIcon;
497 else
498 return QWaylandAdwaitaDecoration::MaximizeIcon;
499}
500
501void QWaylandAdwaitaDecoration::drawButton(Button button, QPainter *painter)
502{
503 const Qt::WindowStates windowStates = waylandWindow()->windowStates();
504 const bool maximized = windowStates & Qt::WindowMaximized;
505
506 const QRect btnRect = buttonRect(button).toRect();
507 renderFlatRoundedButtonFrame(painter, rect: btnRect, color: color(type: ButtonBackground, button));
508
509 QRect adjustedBtnRect = btnRect;
510 adjustedBtnRect.setSize(QSize(16, 16));
511 adjustedBtnRect.translate(dx: 4, dy: 4);
512 const QString svgIcon = m_icons[iconFromButtonAndState(button, maximized)];
513 if (!svgIcon.isEmpty())
514 renderButtonIcon(svgIcon, painter, rect: adjustedBtnRect, color: color(type: Foreground));
515 else // Fallback to use QIcon
516 renderButtonIcon(buttonIcon: iconFromButtonAndState(button, maximized), painter, rect: adjustedBtnRect);
517}
518
519QColor QWaylandAdwaitaDecoration::color(ColorType type, Button button)
520{
521 const bool active = waylandWindow()->windowStates() & Qt::WindowActive;
522
523 switch (type) {
524 case Background:
525 case BackgroundInactive:
526 return active ? m_colors[Background] : m_colors[BackgroundInactive];
527 case Foreground:
528 case ForegroundInactive:
529 return active ? m_colors[Foreground] : m_colors[ForegroundInactive];
530 case Border:
531 case BorderInactive:
532 return active ? m_colors[Border] : m_colors[BorderInactive];
533 case ButtonBackground:
534 case ButtonBackgroundInactive:
535 case HoveredButtonBackground: {
536 if (m_clicking == button) {
537 return m_colors[PressedButtonBackground];
538 } else if (m_hoveredButtons.testFlag(flag: button)) {
539 return m_colors[HoveredButtonBackground];
540 }
541 return active ? m_colors[ButtonBackground] : m_colors[ButtonBackgroundInactive];
542 }
543 default:
544 return m_colors[Background];
545 }
546}
547
548bool QWaylandAdwaitaDecoration::clickButton(Qt::MouseButtons b, Button btn)
549{
550 auto repaint = qScopeGuard(f: [this] { requestRepaint(); });
551
552 if (isLeftClicked(newMouseButtonState: b)) {
553 m_clicking = btn;
554 return false;
555 } else if (isLeftReleased(newMouseButtonState: b)) {
556 if (m_clicking == btn) {
557 m_clicking = None;
558 return true;
559 } else {
560 m_clicking = None;
561 }
562 }
563 return false;
564}
565
566bool QWaylandAdwaitaDecoration::doubleClickButton(Qt::MouseButtons b, const QPointF &local,
567 const QDateTime &currentTime)
568{
569 if (isLeftClicked(newMouseButtonState: b)) {
570 const qint64 clickInterval = m_lastButtonClick.msecsTo(currentTime);
571 m_lastButtonClick = currentTime;
572 const int doubleClickDistance = 5;
573 const QPointF posDiff = m_lastButtonClickPosition - local;
574 if ((clickInterval <= 500)
575 && ((posDiff.x() <= doubleClickDistance && posDiff.x() >= -doubleClickDistance)
576 && ((posDiff.y() <= doubleClickDistance && posDiff.y() >= -doubleClickDistance)))) {
577 return true;
578 }
579
580 m_lastButtonClickPosition = local;
581 }
582
583 return false;
584}
585
586void QWaylandAdwaitaDecoration::updateButtonHoverState(Button hoveredButton)
587{
588 bool currentCloseButtonState = m_hoveredButtons.testFlag(flag: Close);
589 bool currentMaximizeButtonState = m_hoveredButtons.testFlag(flag: Maximize);
590 bool currentMinimizeButtonState = m_hoveredButtons.testFlag(flag: Minimize);
591
592 m_hoveredButtons.setFlag(flag: Close, on: hoveredButton == Button::Close);
593 m_hoveredButtons.setFlag(flag: Maximize, on: hoveredButton == Button::Maximize);
594 m_hoveredButtons.setFlag(flag: Minimize, on: hoveredButton == Button::Minimize);
595
596 if (m_hoveredButtons.testFlag(flag: Close) != currentCloseButtonState
597 || m_hoveredButtons.testFlag(flag: Maximize) != currentMaximizeButtonState
598 || m_hoveredButtons.testFlag(flag: Minimize) != currentMinimizeButtonState) {
599 requestRepaint();
600 }
601}
602
603void QWaylandAdwaitaDecoration::processMouseTop(QWaylandInputDevice *inputDevice, const QPointF &local,
604 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
605{
606 Q_UNUSED(mods)
607
608 QDateTime currentDateTime = QDateTime::currentDateTime();
609 QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(marginsType: ShadowsOnly);
610
611 if (!buttonRect(button: Close).contains(p: local) && !buttonRect(button: Maximize).contains(p: local)
612 && !buttonRect(button: Minimize).contains(p: local))
613 updateButtonHoverState(hoveredButton: Button::None);
614
615 if (local.y() <= surfaceRect.top() + margins().bottom()) {
616 if (local.x() <= margins().left()) {
617 // top left bit
618#if QT_CONFIG(cursor)
619 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeFDiagCursor);
620#endif
621 startResize(inputDevice, edges: Qt::TopEdge | Qt::LeftEdge, buttons: b);
622 } else if (local.x() > surfaceRect.right() - margins().left()) {
623 // top right bit
624#if QT_CONFIG(cursor)
625 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeBDiagCursor);
626#endif
627 startResize(inputDevice, edges: Qt::TopEdge | Qt::RightEdge, buttons: b);
628 } else {
629 // top resize bit
630#if QT_CONFIG(cursor)
631 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeVerCursor);
632#endif
633 startResize(inputDevice, edges: Qt::TopEdge, buttons: b);
634 }
635 } else if (local.x() <= surfaceRect.left() + margins().left()) {
636 processMouseLeft(inputDevice, local, b, mods);
637 } else if (local.x() > surfaceRect.right() - margins().right()) {
638 processMouseRight(inputDevice, local, b, mods);
639 } else if (buttonRect(button: Close).contains(p: local)) {
640 if (clickButton(b, btn: Close)) {
641 QWindowSystemInterface::handleCloseEvent(window: window());
642 m_hoveredButtons.setFlag(flag: Close, on: false);
643 }
644 updateButtonHoverState(hoveredButton: Close);
645 } else if (m_buttons.contains(key: Maximize) && buttonRect(button: Maximize).contains(p: local)) {
646 updateButtonHoverState(hoveredButton: Maximize);
647 if (clickButton(b, btn: Maximize)) {
648 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
649 m_hoveredButtons.setFlag(flag: Maximize, on: false);
650 }
651 } else if (m_buttons.contains(key: Minimize) && buttonRect(button: Minimize).contains(p: local)) {
652 updateButtonHoverState(hoveredButton: Minimize);
653 if (clickButton(b, btn: Minimize)) {
654 window()->setWindowState(Qt::WindowMinimized);
655 m_hoveredButtons.setFlag(flag: Minimize, on: false);
656 }
657 } else if (doubleClickButton(b, local, currentTime: currentDateTime)) {
658 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
659 } else {
660 // Show window menu
661 if (b == Qt::MouseButton::RightButton)
662 waylandWindow()->shellSurface()->showWindowMenu(seat: inputDevice);
663#if QT_CONFIG(cursor)
664 waylandWindow()->restoreMouseCursor(device: inputDevice);
665#endif
666 startMove(inputDevice, buttons: b);
667 }
668}
669
670void QWaylandAdwaitaDecoration::processMouseBottom(QWaylandInputDevice *inputDevice, const QPointF &local,
671 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
672{
673 Q_UNUSED(mods)
674 if (local.x() <= margins().left()) {
675 // bottom left bit
676#if QT_CONFIG(cursor)
677 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeBDiagCursor);
678#endif
679 startResize(inputDevice, edges: Qt::BottomEdge | Qt::LeftEdge, buttons: b);
680 } else if (local.x() > window()->width() + margins().right()) {
681 // bottom right bit
682#if QT_CONFIG(cursor)
683 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeFDiagCursor);
684#endif
685 startResize(inputDevice, edges: Qt::BottomEdge | Qt::RightEdge, buttons: b);
686 } else {
687 // bottom bit
688#if QT_CONFIG(cursor)
689 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeVerCursor);
690#endif
691 startResize(inputDevice, edges: Qt::BottomEdge, buttons: b);
692 }
693}
694
695void QWaylandAdwaitaDecoration::processMouseLeft(QWaylandInputDevice *inputDevice, const QPointF &local,
696 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
697{
698 Q_UNUSED(local)
699 Q_UNUSED(mods)
700#if QT_CONFIG(cursor)
701 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeHorCursor);
702#endif
703 startResize(inputDevice, edges: Qt::LeftEdge, buttons: b);
704}
705
706void QWaylandAdwaitaDecoration::processMouseRight(QWaylandInputDevice *inputDevice, const QPointF &local,
707 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
708{
709 Q_UNUSED(local)
710 Q_UNUSED(mods)
711#if QT_CONFIG(cursor)
712 waylandWindow()->setMouseCursor(device: inputDevice, cursor: Qt::SizeHorCursor);
713#endif
714 startResize(inputDevice, edges: Qt::RightEdge, buttons: b);
715}
716
717void QWaylandAdwaitaDecoration::requestRepaint() const
718{
719 // Set dirty flag
720 if (waylandWindow()->decoration())
721 waylandWindow()->decoration()->update();
722
723 // Request re-paint
724 waylandWindow()->window()->requestUpdate();
725}
726
727} // namespace QtWaylandClient
728
729QT_END_NAMESPACE
730
731#include "moc_qwaylandadwaitadecoration_p.cpp"
732

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtwayland/src/plugins/decorations/adwaita/qwaylandadwaitadecoration.cpp