8#include <QtCore/QLoggingCategory>
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>
22#include <QtGui/QColor>
23#include <QtGui/QPainter>
24#include <QtGui/QPainterPath>
26#include <QtGui/private/qguiapplication_p.h>
27#include <QtGui/qpa/qplatformtheme.h>
30#include <QtSvg/QSvgRenderer>
33#include <QtWaylandClient/private/qwaylandshmbackingstore_p.h>
34#include <QtWaylandClient/private/qwaylandwindow_p.h>
38using namespace Qt::StringLiterals;
61 while (!argument.atEnd()) {
64 argument.beginMapEntry();
65 argument >> key >> value;
66 argument.endMapEntry();
67 map.insert(key, value);
97 const bool onlyShadows = marginsType == QWaylandAbstractDecoration::ShadowsOnly;
98 const bool shadowsExcluded = marginsType == ShadowsExcluded;
100 if (waylandWindow()->windowStates() & Qt::WindowMaximized) {
107 const QWaylandWindow::ToplevelWindowTilingStates tilingStates = waylandWindow()->toplevelWindowTilingStates();
111 const int sideMargins = onlyShadows ?
ceShadowsWidth : marginsBase;
114 return QMargins(tilingStates & QWaylandWindow::WindowTiledLeft ? 0 : sideMargins,
115 tilingStates & QWaylandWindow::WindowTiledTop ? onlyShadows ? 0 : ceTitlebarHeight : topMargins,
116 tilingStates & QWaylandWindow::WindowTiledRight ? 0 : sideMargins,
117 tilingStates & QWaylandWindow::WindowTiledBottom ? 0 : sideMargins);
122 const QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(ShadowsOnly);
125 p.setRenderHint(QPainter::Antialiasing);
128
129
131 const QPointF topLeft = { margins(ShadowsOnly).left() + 0.5,
132 margins(ShadowsOnly).top() - 0.5 };
133 const int titleBarWidth = surfaceRect.width() - margins(ShadowsOnly).left()
134 - margins(ShadowsOnly).right() - 0.5;
135 const int borderRectHeight =
136 surfaceRect.height() - margins().top() - margins().bottom() + 0.5;
139 if (waylandWindow()->windowStates() & Qt::WindowMaximized
140 || waylandWindow()->toplevelWindowTilingStates() != QWaylandWindow::WindowNoState) {
141 path.addRect(QRectF(topLeft, QSizeF(titleBarWidth, margins().top())));
143 path.addRoundedRect(QRectF(topLeft, QSizeF(titleBarWidth, margins().top() + ceCornerRadius)),
144 ceCornerRadius, ceCornerRadius);
148 p.setPen(color(Border));
149 p.fillPath(path.simplified(), color(Background));
151 p.drawRect(QRectF(topLeft.x(), margins().top(), titleBarWidth, borderRectHeight));
155
156
157 const QRect top = QRect(margins().left(), margins().bottom(), surfaceRect.width(),
158 margins().top() - margins().bottom());
159 const QString windowTitleText = waylandWindow()->windowTitle();
160 if (!windowTitleText.isEmpty()) {
161 if (m_windowTitle.text() != windowTitleText) {
162 m_windowTitle.setText(windowTitleText);
163 m_windowTitle.prepare();
166 QRect titleBar = top;
167 if (m_placement ==
Right) {
168 titleBar.setLeft(margins().left());
169 titleBar.setRight(
static_cast<
int>(buttonRect(Minimize).left()) - 8);
171 titleBar.setLeft(
static_cast<
int>(buttonRect(Minimize).right()) + 8);
172 titleBar.setRight(surfaceRect.width() - margins().right());
176 p.setClipRect(titleBar);
177 p.setPen(color(Foreground));
178 QSize size = m_windowTitle.size().toSize();
179 int dx = (top.width() - size.width()) / 2;
180 int dy = (top.height() - size.height()) / 2;
182 QPoint windowTitlePoint(top.topLeft().x() + dx, top.topLeft().y() + dy);
183 p.drawStaticText(windowTitlePoint, m_windowTitle);
189
190
191 if (m_buttons.contains(Close))
192 drawButton(
Close, &p);
194 if (m_buttons.contains(Maximize))
197 if (m_buttons.contains(Minimize))
202 const QPointF &global, Qt::MouseButtons b,
203 Qt::KeyboardModifiers mods)
207 if (local.y() > margins().top())
211 QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(ShadowsOnly);
212 if (local.y() <= surfaceRect.top() + margins().top())
213 processMouseTop(inputDevice, local, b, mods);
214 else if (local.y() > surfaceRect.bottom() - margins().bottom())
215 processMouseBottom(inputDevice, local, b, mods);
216 else if (local.x() <= surfaceRect.left() + margins().left())
217 processMouseLeft(inputDevice, local, b, mods);
218 else if (local.x() > surfaceRect.right() - margins().right())
219 processMouseRight(inputDevice, local, b, mods);
222 waylandWindow()->restoreMouseCursor(inputDevice);
228 if (isLeftReleased(b)) {
238 const QPointF &global, QEventPoint::State state,
239 Qt::KeyboardModifiers mods)
241 Q_UNUSED(inputDevice)
245 bool handled = state == QEventPoint::Pressed;
248 if (buttonRect(Close).contains(local))
249 QWindowSystemInterface::handleCloseEvent(window());
250 else if (m_buttons.contains(Maximize) && buttonRect(Maximize).contains(local))
251 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
252 else if (m_buttons.contains(Minimize) && buttonRect(Minimize).contains(local))
253 window()->setWindowState(Qt::WindowMinimized);
254 else if (local.y() <= margins().top())
255 waylandWindow()->shellSurface()->move(inputDevice);
265 const QStringList themeNames = { QIcon::themeName(), QIcon::fallbackThemeName(),
"Adwaita"_L1 };
267 qCDebug(lcQWaylandAdwaitaDecorationLog) <<
"Searched icon themes: " << themeNames;
269 for (
const QString &themeName : themeNames) {
270 if (themeName.isEmpty())
273 for (
const QString &path : QIcon::themeSearchPaths()) {
274 if (path.startsWith(QLatin1Char(
':')))
277 const QString fullPath = QString(
"%1/%2").arg(path).arg(themeName);
278 QDirIterator dirIt(fullPath, {
"*.svg"}, QDir::Files, QDirIterator::Subdirectories);
279 while (dirIt.hasNext()) {
280 const QString fileName = dirIt.next();
281 const QFileInfo fileInfo(fileName);
283 if (fileInfo.fileName() == iconName) {
284 qCDebug(lcQWaylandAdwaitaDecorationLog) <<
"Using " << iconName <<
" from " << themeName <<
" theme";
285 QFile readFile(fileInfo.filePath());
286 if (!readFile.open(QFile::ReadOnly)) {
287 qCWarning(lcQWaylandAdwaitaDecorationLog,
"Failed to open %ls: %ls",
288 qUtf16Printable(readFile.fileName()),
289 qUtf16Printable(readFile.errorString()));
291 return readFile.readAll();
297 qCWarning(lcQWaylandAdwaitaDecorationLog) <<
"Failed to find an svg icon for " << iconName;
305 qDBusRegisterMetaType<QMap<QString, QVariantMap>>();
307 QDBusConnection connection = QDBusConnection::sessionBus();
309 QDBusMessage message = QDBusMessage::createMethodCall(
"org.freedesktop.portal.Desktop"_L1,
310 "/org/freedesktop/portal/desktop"_L1,
311 "org.freedesktop.portal.Settings"_L1,
313 message << QStringList{ {
"org.gnome.desktop.wm.preferences"_L1 },
314 {
"org.freedesktop.appearance"_L1 } };
316 QDBusPendingCall pendingCall = connection.asyncCall(message);
317 QDBusPendingCallWatcher *watcher =
new QDBusPendingCallWatcher(pendingCall,
this);
318 QObject::connect(watcher, &QDBusPendingCallWatcher::finished,
this, [
this](QDBusPendingCallWatcher *watcher) {
319 QDBusPendingReply<QMap<QString, QVariantMap>> reply = *watcher;
320 if (reply.isValid()) {
321 QMap<QString, QVariantMap> settings = reply.value();
322 if (!settings.isEmpty()) {
323 const uint colorScheme = settings.value(
"org.freedesktop.appearance"_L1).value(
"color-scheme"_L1).toUInt();
324 updateColors(colorScheme == 1);
325 const QString buttonLayout = settings.value(
"org.gnome.desktop.wm.preferences"_L1).value(
"button-layout"_L1).toString();
326 if (!buttonLayout.isEmpty())
327 updateTitlebarLayout(buttonLayout);
330 const bool titlebarUseDesktopFont = settings.value(
"org.gnome.desktop.wm.preferences"_L1).value(
"titlebar-uses-desktop-font"_L1).toBool();
331 if (!titlebarUseDesktopFont) {
333 const QString titlebarFont =
334 settings.value(
"org.gnome.desktop.wm.preferences"_L1).value(
"titlebar-font"_L1).toString();
335 if (titlebarFont.contains(
"bold"_L1, Qt::CaseInsensitive)) {
336 m_font->setBold(
true);
341 watcher->deleteLater();
344 QDBusConnection::sessionBus().connect(QString(),
"/org/freedesktop/portal/desktop"_L1,
345 "org.freedesktop.portal.Settings"_L1,
"SettingChanged"_L1,
this,
346 SLOT(settingChanged(QString,QString,QDBusVariant)));
349 for (
auto mapIt = buttonMap.constBegin(); mapIt != buttonMap.constEnd(); mapIt++) {
350 const QString fullName = mapIt.value() + QStringLiteral(
".svg");
351 m_icons[mapIt.key()] = getIconSvg(fullName);
359 qCDebug(lcQWaylandAdwaitaDecorationLog) <<
"Color scheme changed to: " << (isDark ?
"dark" :
"light");
361 m_colors = { { Background, isDark ? QColor(0x2e2e32) : QColor(0xffffff) },
362 { BackgroundInactive, isDark ? QColor(0x222226) : QColor(0xfafafb) },
363 { Foreground, isDark ? QColor(0xffffff) : QColor(0x333338) },
364 { ForegroundInactive, isDark ? QColor(0x919193) : QColor(0x969699) },
365 { Border, isDark ? QColor(0x2e2e32) : QColor(0xffffff) },
366 { BorderInactive, isDark ? QColor(0x2e2e32) : QColor(0xffffff) },
367 { ButtonBackground, isDark ? QColor(0x434347) : QColor(0xebebeb) },
368 { ButtonBackgroundInactive, isDark ? QColor(0x2d2d31) : QColor(0xf0f0f1) },
369 { HoveredButtonBackground, isDark ? QColor(0x4d4d51) : QColor(0xe0e0e1) },
370 { PressedButtonBackground, isDark ? QColor(0x6c6c6f) : QColor(0xc2c2c3) } };
376 const QStringList layouts = layout.split(QLatin1Char(
':'));
377 if (layouts.count() != 2)
383 const QString &leftLayout = layouts.at(0);
384 const QString &rightLayout = layouts.at(1);
385 m_placement = leftLayout.contains(
"close"_L1) ? Left : Right;
388 const QString &buttonLayout = m_placement ==
Right ? rightLayout : leftLayout;
390 QStringList buttonList = buttonLayout.split(QLatin1Char(
','));
391 if (m_placement == Right)
392 std::reverse(buttonList.begin(), buttonList.end());
394 for (
const QString &button : buttonList) {
395 if (button ==
"close"_L1)
396 m_buttons.insert(Close, pos);
397 else if (button ==
"maximize"_L1)
398 m_buttons.insert(Maximize, pos);
399 else if (button ==
"minimize"_L1)
400 m_buttons.insert(Minimize, pos);
405 qCDebug(lcQWaylandAdwaitaDecorationLog) <<
"Button layout changed to: " << layout;
413 if (group ==
"org.gnome.desktop.wm.preferences"_L1 && key ==
"button-layout"_L1) {
414 const QString layout = value.variant().toString();
415 updateTitlebarLayout(layout);
416 }
else if (group ==
"org.freedesktop.appearance"_L1 && key ==
"color-scheme"_L1) {
417 const uint colorScheme = value.variant().toUInt();
418 updateColors(colorScheme == 1);
426 const int btnPos = m_buttons.value(button);
428 const QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(QWaylandAbstractDecoration::ShadowsOnly);
429 if (m_placement ==
Right) {
430 xPos = surfaceRect.width();
433 xPos -= margins(ShadowsOnly).right();
438 xPos += margins(ShadowsOnly).left();
444 yPos = margins().top();
445 yPos += margins().bottom();
455 painter->setRenderHint(QPainter::Antialiasing,
true);
456 painter->setPen(Qt::NoPen);
457 painter->setBrush(color);
458 painter->drawEllipse(rect);
465 painter->setRenderHints(QPainter::Antialiasing,
true);
467 QString icon = svgIcon;
468 QRegularExpression regexp(
"fill=[\"']#[0-9A-F]{6}[\"']", QRegularExpression::CaseInsensitiveOption);
469 QRegularExpression regexpAlt(
"fill:#[0-9A-F]{6}", QRegularExpression::CaseInsensitiveOption);
470 QRegularExpression regexpCurrentColor(
"fill=[\"']currentColor[\"']");
471 icon.replace(regexp, QString(
"fill=\"%1\"").arg(color.name()));
472 icon.replace(regexpAlt, QString(
"fill:%1").arg(color.name()));
473 icon.replace(regexpCurrentColor, QString(
"fill=\"%1\"").arg(color.name()));
474 QSvgRenderer svgRenderer(icon.toLocal8Bit());
475 svgRenderer.render(painter, rect);
482 QString iconName = buttonMap[buttonIcon];
485 painter->setRenderHints(QPainter::Antialiasing,
true);
486 painter->drawPixmap(rect, QIcon::fromTheme(iconName).pixmap(ceButtonWidth, ceButtonWidth));
504 const Qt::WindowStates windowStates = waylandWindow()->windowStates();
505 const bool maximized = windowStates & Qt::WindowMaximized;
507 const QRect btnRect = buttonRect(button).toRect();
508 renderFlatRoundedButtonFrame(painter, btnRect, color(ButtonBackground, button));
510 QRect adjustedBtnRect = btnRect;
511 adjustedBtnRect.setSize(QSize(16, 16));
512 adjustedBtnRect.translate(4, 4);
513 const QString svgIcon = m_icons[iconFromButtonAndState(button, maximized)];
514 if (!svgIcon.isEmpty())
515 renderButtonIcon(svgIcon, painter, adjustedBtnRect, color(Foreground));
522 QColor transparentColor = color;
523 transparentColor.setAlphaF(level);
524 return transparentColor;
529 const bool active = waylandWindow()->windowStates() & Qt::WindowActive;
533 case BackgroundInactive:
534 return active ? m_colors[Background] : m_colors[BackgroundInactive];
536 case ForegroundInactive:
537 return active ? m_colors[Foreground] : m_colors[ForegroundInactive];
540 return active ? makeTransparent(m_colors[Border], 0.5) : makeTransparent(m_colors[BorderInactive], 0.5);
541 case ButtonBackground:
542 case ButtonBackgroundInactive:
543 case HoveredButtonBackground: {
544 if (m_clicking == button) {
545 return m_colors[PressedButtonBackground];
546 }
else if (m_hoveredButtons.testFlag(button)) {
547 return m_colors[HoveredButtonBackground];
549 return active ? m_colors[ButtonBackground] : m_colors[ButtonBackgroundInactive];
552 return m_colors[Background];
558 auto repaint = qScopeGuard([
this] { requestRepaint(); });
560 if (isLeftClicked(b)) {
563 }
else if (isLeftReleased(b)) {
564 if (m_clicking == btn) {
575 const QDateTime ¤tTime)
577 if (isLeftClicked(b)) {
578 const qint64 clickInterval = m_lastButtonClick.msecsTo(currentTime);
579 m_lastButtonClick = currentTime;
580 const int doubleClickDistance = 5;
581 const QPointF posDiff = m_lastButtonClickPosition - local;
582 if ((clickInterval <= 500)
583 && ((posDiff.x() <= doubleClickDistance && posDiff.x() >= -doubleClickDistance)
584 && ((posDiff.y() <= doubleClickDistance && posDiff.y() >= -doubleClickDistance)))) {
588 m_lastButtonClickPosition = local;
596 bool currentCloseButtonState = m_hoveredButtons.testFlag(Close);
597 bool currentMaximizeButtonState = m_hoveredButtons.testFlag(Maximize);
598 bool currentMinimizeButtonState = m_hoveredButtons.testFlag(Minimize);
600 m_hoveredButtons.setFlag(Close, hoveredButton == Button::Close);
601 m_hoveredButtons.setFlag(Maximize, hoveredButton == Button::Maximize);
602 m_hoveredButtons.setFlag(Minimize, hoveredButton == Button::Minimize);
604 if (m_hoveredButtons.testFlag(Close) != currentCloseButtonState
605 || m_hoveredButtons.testFlag(Maximize) != currentMaximizeButtonState
606 || m_hoveredButtons.testFlag(Minimize) != currentMinimizeButtonState) {
612 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
616 QDateTime currentDateTime = QDateTime::currentDateTime();
617 QRect surfaceRect = waylandWindow()->windowContentGeometry() + margins(ShadowsOnly);
619 if (!buttonRect(Close).contains(local) && !buttonRect(Maximize).contains(local)
620 && !buttonRect(Minimize).contains(local))
623 if (local.y() <= surfaceRect.top() + margins().bottom()) {
624 if (local.x() <= margins().left()) {
627 waylandWindow()->applyCursor(inputDevice, Qt::SizeFDiagCursor);
629 startResize(inputDevice, Qt::TopEdge | Qt::LeftEdge, b);
630 }
else if (local.x() > surfaceRect.right() - margins().left()) {
633 waylandWindow()->applyCursor(inputDevice, Qt::SizeBDiagCursor);
635 startResize(inputDevice, Qt::TopEdge | Qt::RightEdge, b);
639 waylandWindow()->applyCursor(inputDevice, Qt::SizeVerCursor);
641 startResize(inputDevice, Qt::TopEdge, b);
643 }
else if (local.x() <= surfaceRect.left() + margins().left()) {
644 processMouseLeft(inputDevice, local, b, mods);
645 }
else if (local.x() > surfaceRect.right() - margins().right()) {
646 processMouseRight(inputDevice, local, b, mods);
647 }
else if (buttonRect(Close).contains(local)) {
648 if (clickButton(b, Close)) {
649 QWindowSystemInterface::handleCloseEvent(window());
650 m_hoveredButtons.setFlag(Close,
false);
652 updateButtonHoverState(
Close);
653 }
else if (m_buttons.contains(Maximize) && buttonRect(Maximize).contains(local)) {
655 if (clickButton(b, Maximize)) {
656 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
657 m_hoveredButtons.setFlag(Maximize,
false);
659 }
else if (m_buttons.contains(Minimize) && buttonRect(Minimize).contains(local)) {
661 if (clickButton(b, Minimize)) {
662 window()->setWindowState(Qt::WindowMinimized);
663 m_hoveredButtons.setFlag(Minimize,
false);
665 }
else if (doubleClickButton(b, local, currentDateTime)) {
666 window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized);
669 if (b == Qt::MouseButton::RightButton)
670 waylandWindow()->shellSurface()->showWindowMenu(inputDevice);
672 waylandWindow()->restoreMouseCursor(inputDevice);
674 startMove(inputDevice, b);
679 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
682 if (local.x() <= margins().left()) {
685 waylandWindow()->applyCursor(inputDevice, Qt::SizeBDiagCursor);
687 startResize(inputDevice, Qt::BottomEdge | Qt::LeftEdge, b);
688 }
else if (local.x() > window()->width() + margins().right()) {
691 waylandWindow()->applyCursor(inputDevice, Qt::SizeFDiagCursor);
693 startResize(inputDevice, Qt::BottomEdge | Qt::RightEdge, b);
697 waylandWindow()->applyCursor(inputDevice, Qt::SizeVerCursor);
699 startResize(inputDevice, Qt::BottomEdge, b);
704 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
709 waylandWindow()->applyCursor(inputDevice, Qt::SizeHorCursor);
711 startResize(inputDevice, Qt::LeftEdge, b);
715 Qt::MouseButtons b, Qt::KeyboardModifiers mods)
720 waylandWindow()->applyCursor(inputDevice, Qt::SizeHorCursor);
722 startResize(inputDevice, Qt::RightEdge, b);
728 if (waylandWindow()->decoration())
729 waylandWindow()->decoration()->update();
732 waylandWindow()->window()->requestUpdate();
739#include "moc_qwaylandadwaitadecoration_p.cpp"
bool handleMouse(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, Qt::MouseButtons b, Qt::KeyboardModifiers mods) override
bool handleTouch(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, QEventPoint::State state, Qt::KeyboardModifiers mods) override
void paint(QPaintDevice *device) override
QMargins margins(MarginsType marginsType=Full) const override
Q_LOGGING_CATEGORY(lcQpaWayland, "qt.qpa.wayland")
static QWaylandAdwaitaDecoration::ButtonIcon iconFromButtonAndState(QWaylandAdwaitaDecoration::Button button, bool maximized)
static constexpr int ceTitlebarHeight
static constexpr int ceButtonWidth
static QMap< QWaylandAdwaitaDecoration::ButtonIcon, QString > buttonMap
static constexpr int ceButtonSpacing
static constexpr int ceWindowBorderWidth
static constexpr int ceShadowsWidth
static QColor makeTransparent(const QColor &color, qreal level)
const QDBusArgument & operator>>(const QDBusArgument &argument, QMap< QString, QVariantMap > &map)
static void renderButtonIcon(const QString &svgIcon, QPainter *painter, const QRect &rect, const QColor &color)
QString getIconSvg(const QString &iconName)
static constexpr int ceCornerRadius
static void renderButtonIcon(QWaylandAdwaitaDecoration::ButtonIcon buttonIcon, QPainter *painter, const QRect &rect)
static void renderFlatRoundedButtonFrame(QPainter *painter, const QRect &rect, const QColor &color)