From a88b56cbe5ab0723aef3a19adcf8981eac729bc0 Mon Sep 17 00:00:00 2001 From: Jan Grulich Date: Aug 15 2019 07:09:31 +0000 Subject: Add Gnome-like wayland decorations --- diff --git a/qgnomeplatform-gnome-decorations.patch b/qgnomeplatform-gnome-decorations.patch new file mode 100644 index 0000000..6fc5d52 --- /dev/null +++ b/qgnomeplatform-gnome-decorations.patch @@ -0,0 +1,1108 @@ +diff --git a/common/common.pro b/common/common.pro +new file mode 100644 +index 0000000..99bb9e3 +--- /dev/null ++++ b/common/common.pro +@@ -0,0 +1,20 @@ ++ ++TEMPLATE = lib ++ ++CONFIG += c++11 \ ++ link_pkgconfig \ ++ staticlib ++ ++QT += core \ ++ dbus \ ++ theme_support-private \ ++ widgets ++ ++PKGCONFIG += gtk+-3.0 \ ++ gtk+-x11-3.0 ++ ++SOURCES += gnomehintssettings.cpp \ ++ qgtk3dialoghelpers.cpp ++ ++HEADERS += gnomehintssettings.h \ ++ qgtk3dialoghelpers.h +diff --git a/src/gnomehintssettings.cpp b/common/gnomehintssettings.cpp +similarity index 92% +rename from src/gnomehintssettings.cpp +rename to common/gnomehintssettings.cpp +index 409bc48..4dfe990 100644 +--- a/src/gnomehintssettings.cpp ++++ b/common/gnomehintssettings.cpp +@@ -1,5 +1,5 @@ + /* +- * Copyright (C) 2016 Jan Grulich ++ * Copyright (C) 2016-2019 Jan Grulich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -78,6 +78,7 @@ void gtkMessageHandler(const gchar *log_domain, + GnomeHintsSettings::GnomeHintsSettings() + : QObject(0) + , m_usePortal(checkUsePortalSupport()) ++ , m_gnomeDesktopSettings(g_settings_new("org.gnome.desktop.wm.preferences")) + , m_settings(g_settings_new("org.gnome.desktop.interface")) + { + gtk_init(nullptr, nullptr); +@@ -108,6 +109,8 @@ GnomeHintsSettings::GnomeHintsSettings() + // Get current theme and variant + loadTheme(); + ++ loadTitlebar(); ++ + loadStaticHints(); + + m_hints[QPlatformTheme::DialogButtonBoxLayout] = QDialogButtonBox::GnomeLayout; +@@ -260,6 +263,39 @@ void GnomeHintsSettings::themeChanged() + } + } + ++void GnomeHintsSettings::loadTitlebar() ++{ ++ const QString buttonLayout = getSettingsProperty(m_gnomeDesktopSettings, "button-layout"); ++ ++ if (buttonLayout.isEmpty()) { ++ return; ++ } ++ ++ const QStringList btnList = buttonLayout.split(QLatin1Char(':')); ++ if (btnList.count() == 2) { ++ const QString leftButtons = btnList.first(); ++ const QString rightButtons = btnList.last(); ++ ++ m_titlebarButtonPlacement = leftButtons.contains(QStringLiteral("close")) ? GnomeHintsSettings::LeftPlacement : GnomeHintsSettings::RightPlacement; ++ ++ // TODO support button order ++ TitlebarButtons buttons; ++ if (leftButtons.contains(QStringLiteral("close")) || rightButtons.contains("close")) { ++ buttons = buttons | GnomeHintsSettings::CloseButton; ++ } ++ ++ if (leftButtons.contains(QStringLiteral("maximize")) || rightButtons.contains("maximize")) { ++ buttons = buttons | GnomeHintsSettings::MaximizeButton; ++ } ++ ++ if (leftButtons.contains(QStringLiteral("minimize")) || rightButtons.contains("minimize")) { ++ buttons = buttons | GnomeHintsSettings::MinimizeButton; ++ } ++ ++ m_titlebarButtons = buttons; ++ } ++} ++ + void GnomeHintsSettings::loadTheme() + { + // g_object_get(gtk_settings_get_default(), "gtk-theme-name", &m_gtkTheme, NULL); +@@ -270,6 +306,11 @@ void GnomeHintsSettings::loadTheme() + qCWarning(QGnomePlatform) << "Couldn't get current gtk theme!"; + } else { + qCDebug(QGnomePlatform) << "Theme name: " << m_gtkTheme; ++ ++ if (m_gtkTheme.toLower() == QStringLiteral("adwaita-dark")) { ++ m_gtkThemeDarkVariant = true; ++ } ++ + qCDebug(QGnomePlatform) << "Dark version: " << (m_gtkThemeDarkVariant ? "yes" : "no"); + } + +diff --git a/src/gnomehintssettings.h b/common/gnomehintssettings.h +similarity index 84% +rename from src/gnomehintssettings.h +rename to common/gnomehintssettings.h +index 8c8d56c..a1ce4a1 100644 +--- a/src/gnomehintssettings.h ++++ b/common/gnomehintssettings.h +@@ -1,5 +1,5 @@ + /* +- * Copyright (C) 2016 Jan Grulich ++ * Copyright (C) 2016-2019 Jan Grulich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -22,6 +22,7 @@ + + #include + #include ++#include + #include + #include + +@@ -41,6 +42,19 @@ class GnomeHintsSettings : public QObject + { + Q_OBJECT + public: ++ enum TitlebarButtonsPlacement { ++ LeftPlacement = 0, ++ RightPlacement = 1 ++ }; ++ ++ enum TitlebarButton { ++ CloseButton = 0x1, ++ MinimizeButton = 0x02, ++ MaximizeButton = 0x04, ++ AllButtons = 0x8 ++ }; ++ Q_DECLARE_FLAGS(TitlebarButtons, TitlebarButton); ++ + explicit GnomeHintsSettings(); + virtual ~GnomeHintsSettings(); + +@@ -76,6 +90,16 @@ public: + return m_palette; + } + ++ inline TitlebarButtons titlebarButtons() const ++ { ++ return m_titlebarButtons; ++ } ++ ++ inline TitlebarButtonsPlacement titlebarButtonPlacement() const ++ { ++ return m_titlebarButtonPlacement; ++ } ++ + public Q_SLOTS: + void cursorBlinkTimeChanged(); + void fontChanged(); +@@ -83,8 +107,9 @@ public Q_SLOTS: + void themeChanged(); + + private Q_SLOTS: +- void loadTheme(); + void loadFonts(); ++ void loadTheme(); ++ void loadTitlebar(); + void loadPalette(); + void loadStaticHints(); + void portalSettingChanged(const QString &group, const QString &key, const QDBusVariant &value); +@@ -127,9 +152,12 @@ private: + + bool m_usePortal; + bool m_gtkThemeDarkVariant = false; ++ TitlebarButtons m_titlebarButtons = TitlebarButton::CloseButton; ++ TitlebarButtonsPlacement m_titlebarButtonPlacement = TitlebarButtonsPlacement::RightPlacement; + QString m_gtkTheme = nullptr; + QPalette *m_palette = nullptr; + GSettings *m_cinnamonSettings = nullptr; ++ GSettings *m_gnomeDesktopSettings = nullptr; + GSettings *m_settings = nullptr; + QHash m_fonts; + QHash m_hints; +@@ -156,4 +184,6 @@ template <> inline qreal GnomeHintsSettings::getSettingsProperty(GSettings *sett + return g_settings_get_double(settings, property.toStdString().c_str()); + } + ++Q_DECLARE_OPERATORS_FOR_FLAGS(GnomeHintsSettings::TitlebarButtons) ++ + #endif // GNOME_HINTS_SETTINGS_H +diff --git a/src/qgtk3dialoghelpers.cpp b/common/qgtk3dialoghelpers.cpp +similarity index 100% +rename from src/qgtk3dialoghelpers.cpp +rename to common/qgtk3dialoghelpers.cpp +diff --git a/src/qgtk3dialoghelpers.h b/common/qgtk3dialoghelpers.h +similarity index 100% +rename from src/qgtk3dialoghelpers.h +rename to common/qgtk3dialoghelpers.h +diff --git a/decoration/decoration.pro b/decoration/decoration.pro +new file mode 100644 +index 0000000..6c3bb05 +--- /dev/null ++++ b/decoration/decoration.pro +@@ -0,0 +1,32 @@ ++lessThan(QT_MINOR_VERSION, 9): error("Qt 5.9 and newer is required.") ++ ++TEMPLATE = lib ++ ++QMAKE_LIBDIR += ../common ++INCLUDEPATH += ../common ++ ++CONFIG += plugin \ ++ c++11 \ ++ link_pkgconfig ++ ++QT += core \ ++ gui \ ++ waylandclient-private \ ++ widgets ++ ++LIBS += -lcommon ++ ++QMAKE_USE += wayland-client ++ ++PKGCONFIG += gtk+-3.0 ++ ++TARGET = qgnomeplatformdecoration ++target.path += $$[QT_INSTALL_PLUGINS]/wayland-decoration-client ++INSTALLS += target ++ ++SOURCES += decorationplugin.cpp \ ++ qgnomeplatformdecoration.cpp ++ ++HEADERS += decorationplugin.h \ ++ qgnomeplatformdecoration.h ++ +diff --git a/decoration/decorationplugin.cpp b/decoration/decorationplugin.cpp +new file mode 100644 +index 0000000..7066fef +--- /dev/null ++++ b/decoration/decorationplugin.cpp +@@ -0,0 +1,30 @@ ++/* ++ * Copyright (C) 2019 Jan Grulich ++ * ++ * This library is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library; if not, write to the Free Software ++ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++ * ++ */ ++ ++#include "decorationplugin.h" ++#include "qgnomeplatformdecoration.h" ++ ++QWaylandAbstractDecoration *QGnomePlatformDecorationPlugin::create(const QString &key, const QStringList ¶mList) ++{ ++ Q_UNUSED(paramList); ++ if (key == "gnome" || key == "gtk3" || key == "qgnomeplatform") ++ return new QGnomePlatformDecoration(); ++ return nullptr; ++} ++ +diff --git a/decoration/decorationplugin.h b/decoration/decorationplugin.h +new file mode 100644 +index 0000000..edc4c85 +--- /dev/null ++++ b/decoration/decorationplugin.h +@@ -0,0 +1,36 @@ ++/* ++ * Copyright (C) 2019 Jan Grulich ++ * ++ * This library is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library; if not, write to the Free Software ++ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++ * ++ */ ++ ++#ifndef DECORATIONPLUGIN_H ++#define DECORATIONPLUGIN_H ++ ++#include ++ ++using namespace QtWaylandClient; ++ ++class QGnomePlatformDecorationPlugin : public QWaylandDecorationPlugin ++{ ++ Q_OBJECT ++ Q_PLUGIN_METADATA(IID QWaylandDecorationFactoryInterface_iid FILE "qgnomeplatformdecoration.json") ++public: ++ QWaylandAbstractDecoration *create(const QString &key, const QStringList ¶mList) override; ++}; ++ ++#endif // DECORATIONPLUGIN_H ++ +diff --git a/decoration/qgnomeplatformdecoration.cpp b/decoration/qgnomeplatformdecoration.cpp +new file mode 100644 +index 0000000..107d2c3 +--- /dev/null ++++ b/decoration/qgnomeplatformdecoration.cpp +@@ -0,0 +1,525 @@ ++/**************************************************************************** ++** ++** Copyright (C) 2016 Robin Burchell ++** Copyright (C) 2016 The Qt Company Ltd. ++** Contact: https://www.qt.io/licensing/ ++** ++** This file is part of the plugins of the Qt Toolkit. ++** ++** $QT_BEGIN_LICENSE:LGPL$ ++** Commercial License Usage ++** Licensees holding valid commercial Qt licenses may use this file in ++** accordance with the commercial license agreement provided with the ++** Software or, alternatively, in accordance with the terms contained in ++** a written agreement between you and The Qt Company. For licensing terms ++** and conditions see https://www.qt.io/terms-conditions. For further ++** information use the contact form at https://www.qt.io/contact-us. ++** ++** GNU Lesser General Public License Usage ++** Alternatively, this file may be used under the terms of the GNU Lesser ++** General Public License version 3 as published by the Free Software ++** Foundation and appearing in the file LICENSE.LGPL3 included in the ++** packaging of this file. Please review the following information to ++** ensure the GNU Lesser General Public License version 3 requirements ++** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ++** ++** GNU General Public License Usage ++** Alternatively, this file may be used under the terms of the GNU ++** General Public License version 2.0 or (at your option) the GNU General ++** Public license version 3 or any later version approved by the KDE Free ++** Qt Foundation. The licenses are as published by the Free Software ++** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ++** included in the packaging of this file. Please review the following ++** information to ensure the GNU General Public License requirements will ++** be met: https://www.gnu.org/licenses/gpl-2.0.html and ++** https://www.gnu.org/licenses/gpl-3.0.html. ++** ++** $QT_END_LICENSE$ ++** ++****************************************************************************/ ++ ++#include "qgnomeplatformdecoration.h" ++ ++#include "gnomehintssettings.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include ++ ++#include ++#include ++ ++#define BUTTON_SPACING 8 ++#define BUTTON_WIDTH 26 ++#define BUTTONS_RIGHT_MARGIN 6 ++ ++// Copied from adwaita-qt ++static QColor transparentize(const QColor &color, qreal amount = 0.1) ++{ ++ qreal h, s, l, a; ++ color.getHslF(&h, &s, &l, &a); ++ ++ qreal alpha = a - amount; ++ if (alpha < 0) ++ alpha = 0; ++ return QColor::fromHslF(h, s, l, alpha); ++} ++ ++static QColor darken(const QColor &color, qreal amount = 0.1) ++{ ++ qreal h, s, l, a; ++ color.getHslF(&h, &s, &l, &a); ++ ++ qreal lightness = l - amount; ++ if (lightness < 0) ++ lightness = 0; ++ ++ return QColor::fromHslF(h, s, lightness, a); ++} ++ ++static QColor desaturate(const QColor &color, qreal amount = 0.1) ++{ ++ qreal h, s, l, a; ++ color.getHslF(&h, &s, &l, &a); ++ ++ qreal saturation = s - amount; ++ if (saturation < 0) ++ saturation = 0; ++ return QColor::fromHslF(h, saturation, l, a); ++} ++ ++QGnomePlatformDecoration::QGnomePlatformDecoration() ++ : m_closeButtonHovered(false) ++ , m_maximizeButtonHovered(false) ++ , m_minimizeButtonHovered(false) ++ , m_hints(new GnomeHintsSettings) ++{ ++ initializeButtonPixmaps(); ++ initializeColors(); ++ ++ QTextOption option(Qt::AlignHCenter | Qt::AlignVCenter); ++ option.setWrapMode(QTextOption::NoWrap); ++ m_windowTitle.setTextOption(option); ++} ++ ++QGnomePlatformDecoration::~QGnomePlatformDecoration() ++{ ++ delete m_hints; ++} ++ ++void QGnomePlatformDecoration::initializeButtonPixmaps() ++{ ++ const QString iconTheme = m_hints->hint(QPlatformTheme::SystemIconThemeName).toString(); ++ const bool isAdwaitaIconTheme = iconTheme.toLower() == QStringLiteral("adwaita"); ++ const bool isDarkVariant = m_hints->gtkThemeDarkVariant(); ++ ++ QIcon::setThemeName(m_hints->hint(QPlatformTheme::SystemIconThemeName).toString()); ++ ++ QPixmap closeIcon = QIcon::fromTheme(QStringLiteral("window-close-symbolic"), QIcon::fromTheme(QStringLiteral("window-close"))).pixmap(QSize(16, 16)); ++ QPixmap maximizeIcon = QIcon::fromTheme(QStringLiteral("window-maximize-symbolic"), QIcon::fromTheme(QStringLiteral("window-maximize"))).pixmap(QSize(16, 16)); ++ QPixmap minimizeIcon = QIcon::fromTheme(QStringLiteral("window-minimize-symbolic"), QIcon::fromTheme(QStringLiteral("window-minimize"))).pixmap(QSize(16, 16)); ++ QPixmap restoreIcon = QIcon::fromTheme(QStringLiteral("window-restore-symbolic"), QIcon::fromTheme(QStringLiteral("window-restore"))).pixmap(QSize(16, 16)); ++ ++ m_buttonPixmaps.insert(Button::Close, isAdwaitaIconTheme && isDarkVariant ? pixmapDarkVariant(closeIcon) : closeIcon); ++ m_buttonPixmaps.insert(Button::Maximize, isAdwaitaIconTheme && isDarkVariant ? pixmapDarkVariant(maximizeIcon) : maximizeIcon); ++ m_buttonPixmaps.insert(Button::Minimize, isAdwaitaIconTheme && isDarkVariant ? pixmapDarkVariant(minimizeIcon) : minimizeIcon); ++ m_buttonPixmaps.insert(Button::Restore, isAdwaitaIconTheme && isDarkVariant ? pixmapDarkVariant(restoreIcon) : restoreIcon); ++} ++ ++void QGnomePlatformDecoration::initializeColors() ++{ ++ const bool darkVariant = m_hints->gtkThemeDarkVariant(); ++ m_foregroundColor = darkVariant ? QColor("#eeeeec") : QColor("#2e3436"); // Adwaita fg_color ++ m_backgroundColorStart = darkVariant ? QColor("#262626") : QColor("#dad6d2"); // Adwaita GtkHeaderBar color ++ m_backgroundColorEnd = darkVariant ? QColor("#2b2b2b") : QColor("#e1dedb"); // Adwaita GtkHeaderBar color ++ m_foregroundInactiveColor = darkVariant ? QColor("#919190") : QColor("#929595"); ++ m_backgroundInactiveColor = darkVariant ? QColor("#353535") : QColor("#f6f5f4"); ++ m_borderColor = darkVariant ? transparentize(QColor("#1b1b1b"), 0.1) : transparentize(QColor("black"), 0.77); ++ m_borderInactiveColor = darkVariant ? transparentize(QColor("#1b1b1b"), 0.1) : transparentize(QColor("black"), 0.82); ++} ++ ++QPixmap QGnomePlatformDecoration::pixmapDarkVariant(const QPixmap &pixmap) ++{ ++ // FIXME: dark variant colors are probably not 1:1, but this is the easiest and most reliable approach for now ++ QImage image = pixmap.toImage(); ++ image.invertPixels(); ++ return QPixmap::fromImage(image); ++} ++ ++QRectF QGnomePlatformDecoration::closeButtonRect() const ++{ ++ if (m_hints->titlebarButtonPlacement() == GnomeHintsSettings::RightPlacement) { ++ return QRectF(window()->frameGeometry().width() - BUTTON_WIDTH - BUTTON_SPACING * 0 - BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } else { ++ return QRectF(BUTTON_SPACING * 0 + BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } ++} ++ ++QRectF QGnomePlatformDecoration::maximizeButtonRect() const ++{ ++ if (m_hints->titlebarButtonPlacement() == GnomeHintsSettings::RightPlacement) { ++ return QRectF(window()->frameGeometry().width() - BUTTON_WIDTH * 2 - BUTTON_SPACING * 1 - BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } else { ++ return QRectF(BUTTON_WIDTH * 1 + BUTTON_SPACING * 1 + BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } ++} ++ ++QRectF QGnomePlatformDecoration::minimizeButtonRect() const ++{ ++ const bool maximizeEnabled = m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MaximizeButton); ++ ++ if (m_hints->titlebarButtonPlacement() == GnomeHintsSettings::RightPlacement) { ++ return QRectF(window()->frameGeometry().width() - BUTTON_WIDTH * (maximizeEnabled ? 3 : 2) - BUTTON_SPACING * (maximizeEnabled ? 2 : 1) - BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } else { ++ return QRectF(BUTTON_WIDTH * (maximizeEnabled ? 2 : 1) + BUTTON_SPACING * (maximizeEnabled ? 2 : 1) + BUTTONS_RIGHT_MARGIN, ++ (margins().top() - BUTTON_WIDTH) / 2, BUTTON_WIDTH, BUTTON_WIDTH); ++ } ++} ++ ++QMargins QGnomePlatformDecoration::margins() const ++{ ++ return QMargins(1, 38, 1, 1); ++} ++ ++void QGnomePlatformDecoration::paint(QPaintDevice *device) ++{ ++ bool active = window()->handle()->isActive(); ++ QRect surfaceRect(QPoint(), window()->frameGeometry().size()); ++ ++ QPainter p(device); ++ p.setRenderHint(QPainter::Antialiasing); ++ ++ // Title bar (border) ++ QPainterPath borderRect; ++ if ((window()->windowStates() & Qt::WindowMaximized)) ++ borderRect.addRect(0, 0, surfaceRect.width(), margins().top() + 8); ++ else ++ borderRect.addRoundedRect(0, 0, surfaceRect.width(), margins().top() + 8, 10, 10); ++ ++ p.fillPath(borderRect.simplified(), active ? m_borderColor : m_borderInactiveColor); ++ ++ // Title bar ++ QPainterPath roundedRect; ++ if ((window()->windowStates() & Qt::WindowMaximized)) ++ roundedRect.addRect(1, 1, surfaceRect.width() - margins().left() - margins().right(), margins().top() + 8); ++ else ++ roundedRect.addRoundedRect(1, 1, surfaceRect.width() - margins().left() - margins().right(), margins().top() + 8, 8, 8); ++ ++ QLinearGradient gradient(margins().left(), margins().top() + 6, margins().left(), 1); ++ gradient.setColorAt(0, active ? m_backgroundColorStart : m_backgroundInactiveColor); ++ gradient.setColorAt(1, active ? m_backgroundColorEnd : m_backgroundInactiveColor); ++ p.fillPath(roundedRect.simplified(), gradient); ++ ++ QPainterPath borderPath; ++ borderPath.addRect(0, margins().top(), margins().left(), surfaceRect.height() - margins().top()); ++ borderPath.addRect(0, surfaceRect.height() - margins().bottom(), surfaceRect.width(), margins().bottom()); ++ borderPath.addRect(surfaceRect.width() - margins().right(), margins().top(), margins().right(), surfaceRect.height() - margins().bottom()); ++ p.fillPath(borderPath, active ? m_borderColor : m_borderInactiveColor); ++ ++ QRect top = QRect(0, 0, surfaceRect.width(), margins().top()); ++ ++ // Window title ++ QString windowTitleText = window()->title(); ++ if (!windowTitleText.isEmpty()) { ++ if (m_windowTitle.text() != windowTitleText) { ++ m_windowTitle.setText(windowTitleText); ++ m_windowTitle.prepare(); ++ } ++ ++ QRect titleBar = top; ++ if (m_hints->titlebarButtonPlacement() == GnomeHintsSettings::RightPlacement) { ++ titleBar.setLeft(margins().left()); ++ titleBar.setRight(minimizeButtonRect().left() - 8); ++ } else { ++ titleBar.setLeft(minimizeButtonRect().right() + 8); ++ titleBar.setRight(surfaceRect.width() - margins().right()); ++ } ++ ++ p.save(); ++ p.setClipRect(titleBar); ++ p.setPen(active ? m_foregroundColor : m_foregroundInactiveColor); ++ QSizeF size = m_windowTitle.size(); ++ int dx = (top.width() - size.width()) /2; ++ int dy = (top.height()- size.height()) /2; ++ QFont font; ++ const QFont *themeFont = m_hints->font(QPlatformTheme::SystemFont); ++ font.setPointSizeF(themeFont->pointSizeF()); ++ font.setFamily(themeFont->family()); ++ font.setBold(true); ++ p.setFont(font); ++ QPoint windowTitlePoint(top.topLeft().x() + dx, ++ top.topLeft().y() + dy); ++ p.drawStaticText(windowTitlePoint, m_windowTitle); ++ p.restore(); ++ } ++ ++ QRectF rect; ++ ++ // From adwaita-qt ++ QColor windowColor; ++ QColor buttonHoverBorderColor; ++ // QColor buttonHoverFrameColor; ++ if (m_hints->gtkThemeDarkVariant()) { ++ windowColor = darken(desaturate(QColor("#3d3846"), 1.0), 0.04); ++ buttonHoverBorderColor = darken(windowColor, 0.1); ++ // buttonHoverFrameColor = darken(windowColor, 0.01); ++ } else { ++ windowColor = QColor("#f6f5f4"); ++ buttonHoverBorderColor = darken(windowColor, 0.18); ++ // buttonHoverFrameColor = darken(windowColor, 0.04); ++ } ++ ++ // Close button ++ p.save(); ++ rect = closeButtonRect(); ++ if (m_closeButtonHovered) { ++ QRectF buttonRect(rect.x() - 0.5, rect.y() - 0.5, 28, 28); ++ // QLinearGradient buttonGradient(buttonRect.bottomLeft(), buttonRect.topLeft()); ++ // buttonGradient.setColorAt(0, buttonHoverFrameColor); ++ // buttonGradient.setColorAt(1, windowColor); ++ QPainterPath path; ++ path.addRoundedRect(buttonRect, 4, 4); ++ p.setPen(QPen(buttonHoverBorderColor, 1.0)); ++ p.fillPath(path, windowColor); ++ p.drawPath(path); ++ } ++ p.drawPixmap(QPoint(rect.x() + 6, rect.y() + 6), m_buttonPixmaps[Button::Close]); ++ ++ p.restore(); ++ ++ // Maximize button ++ if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MaximizeButton)) { ++ p.save(); ++ rect = maximizeButtonRect(); ++ if (m_maximizeButtonHovered) { ++ QRectF buttonRect(rect.x() - 0.5, rect.y() - 0.5, 28, 28); ++ // QLinearGradient buttonGradient(buttonRect.bottomLeft(), buttonRect.topLeft()); ++ // buttonGradient.setColorAt(0, buttonHoverFrameColor); ++ // buttonGradient.setColorAt(1, windowColor); ++ QPainterPath path; ++ path.addRoundedRect(buttonRect, 4, 4); ++ p.setPen(QPen(buttonHoverBorderColor, 1.0)); ++ p.fillPath(path, windowColor); ++ p.drawPath(path); ++ } ++ if ((window()->windowStates() & Qt::WindowMaximized)) { ++ p.drawPixmap(QPoint(rect.x() + 5, rect.y() + 5), m_buttonPixmaps[Button::Restore]); ++ } else { ++ p.drawPixmap(QPoint(rect.x() + 5, rect.y() + 5), m_buttonPixmaps[Button::Maximize]); ++ } ++ p.restore(); ++ } ++ ++ // Minimize button ++ if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MinimizeButton)) { ++ p.save(); ++ rect = minimizeButtonRect(); ++ if (m_minimizeButtonHovered) { ++ QRectF buttonRect(rect.x() - 0.5, rect.y() - 0.5, 28, 28); ++ // QLinearGradient buttonGradient(buttonRect.bottomLeft(), buttonRect.topLeft()); ++ // buttonGradient.setColorAt(0, buttonHoverFrameColor); ++ // buttonGradient.setColorAt(1, windowColor); ++ QPainterPath path; ++ path.addRoundedRect(buttonRect, 4, 4); ++ p.setPen(QPen(buttonHoverBorderColor, 1.0)); ++ p.fillPath(path, windowColor); ++ p.drawPath(path); ++ } ++ p.drawPixmap(QPoint(rect.x() + 5, rect.y() + 5), m_buttonPixmaps[Button::Minimize]); ++ p.restore(); ++ } ++} ++ ++bool QGnomePlatformDecoration::clickButton(Qt::MouseButtons b, Button btn) ++{ ++ if (isLeftClicked(b)) { ++ m_clicking = btn; ++ return false; ++ } else if (isLeftReleased(b)) { ++ if (m_clicking == btn) { ++ m_clicking = None; ++ return true; ++ } else { ++ m_clicking = None; ++ } ++ } ++ return false; ++} ++ ++bool QGnomePlatformDecoration::handleMouse(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, Qt::MouseButtons b, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(global); ++ ++ if (local.y() > margins().top()) { ++ updateButtonHoverState(Button::None); ++ } ++ ++ // Figure out what area mouse is in ++ if (local.y() <= margins().top()) { ++ processMouseTop(inputDevice,local,b,mods); ++ } else if (local.y() > window()->height() + margins().top()) { ++ processMouseBottom(inputDevice,local,b,mods); ++ } else if (local.x() <= margins().left()) { ++ processMouseLeft(inputDevice,local,b,mods); ++ } else if (local.x() > window()->width() + margins().left()) { ++ processMouseRight(inputDevice,local,b,mods); ++ } else { ++#if QT_CONFIG(cursor) ++ waylandWindow()->restoreMouseCursor(inputDevice); ++#endif ++ setMouseButtons(b); ++ return false; ++ } ++ ++ setMouseButtons(b); ++ return true; ++} ++ ++bool QGnomePlatformDecoration::handleTouch(QWaylandInputDevice *inputDevice, const QPointF &local, const QPointF &global, Qt::TouchPointState state, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(inputDevice); ++ Q_UNUSED(global); ++ Q_UNUSED(mods); ++ bool handled = state == Qt::TouchPointPressed; ++ if (handled) { ++ if (closeButtonRect().contains(local)) ++ QWindowSystemInterface::handleCloseEvent(window()); ++ else if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MaximizeButton) && maximizeButtonRect().contains(local)) ++ window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized); ++ else if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MinimizeButton) && minimizeButtonRect().contains(local)) ++ window()->setWindowState(Qt::WindowMinimized); ++ else if (local.y() <= margins().top()) ++ waylandWindow()->shellSurface()->move(inputDevice); ++ else ++ handled = false; ++ } ++ ++ return handled; ++} ++ ++void QGnomePlatformDecoration::processMouseTop(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(mods); ++ ++ if (!closeButtonRect().contains(local) && !maximizeButtonRect().contains(local) && !minimizeButtonRect().contains(local)) { ++ updateButtonHoverState(Button::None); ++ } ++ ++ if (local.y() <= margins().bottom()) { ++ if (local.x() <= margins().left()) { ++ //top left bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SizeFDiagCursor); ++#endif ++ startResize(inputDevice,WL_SHELL_SURFACE_RESIZE_TOP_LEFT,b); ++ } else if (local.x() > window()->width() + margins().left()) { ++ //top right bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SizeBDiagCursor); ++#endif ++ startResize(inputDevice,WL_SHELL_SURFACE_RESIZE_TOP_RIGHT,b); ++ } else { ++ //top resize bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SplitVCursor); ++#endif ++ startResize(inputDevice,WL_SHELL_SURFACE_RESIZE_TOP,b); ++ } ++ } else if (local.x() <= margins().left()) { ++ processMouseLeft(inputDevice, local, b, mods); ++ } else if (local.x() > window()->width() + margins().left()) { ++ processMouseRight(inputDevice, local, b, mods); ++ } else if (closeButtonRect().contains(local)) { ++ updateButtonHoverState(Button::Close); ++ if (clickButton(b, Close)) ++ QWindowSystemInterface::handleCloseEvent(window()); ++ } else if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MaximizeButton) && maximizeButtonRect().contains(local)) { ++ updateButtonHoverState(Button::Maximize); ++ if (clickButton(b, Maximize)) ++ window()->setWindowStates(window()->windowStates() ^ Qt::WindowMaximized); ++ } else if (m_hints->titlebarButtons().testFlag(GnomeHintsSettings::MinimizeButton) && minimizeButtonRect().contains(local)) { ++ updateButtonHoverState(Button::Minimize); ++ if (clickButton(b, Minimize)) ++ window()->setWindowState(Qt::WindowMinimized); ++ } else { ++#if QT_CONFIG(cursor) ++ waylandWindow()->restoreMouseCursor(inputDevice); ++#endif ++ startMove(inputDevice,b); ++ } ++} ++ ++void QGnomePlatformDecoration::processMouseBottom(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(mods); ++ if (local.x() <= margins().left()) { ++ //bottom left bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SizeBDiagCursor); ++#endif ++ startResize(inputDevice, WL_SHELL_SURFACE_RESIZE_BOTTOM_LEFT,b); ++ } else if (local.x() > window()->width() + margins().left()) { ++ //bottom right bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SizeFDiagCursor); ++#endif ++ startResize(inputDevice, WL_SHELL_SURFACE_RESIZE_BOTTOM_RIGHT,b); ++ } else { ++ //bottom bit ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SplitVCursor); ++#endif ++ startResize(inputDevice,WL_SHELL_SURFACE_RESIZE_BOTTOM,b); ++ } ++} ++ ++void QGnomePlatformDecoration::processMouseLeft(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(local); ++ Q_UNUSED(mods); ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SplitHCursor); ++#endif ++ startResize(inputDevice,WL_SHELL_SURFACE_RESIZE_LEFT,b); ++} ++ ++void QGnomePlatformDecoration::processMouseRight(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b, Qt::KeyboardModifiers mods) ++{ ++ Q_UNUSED(local); ++ Q_UNUSED(mods); ++#if QT_CONFIG(cursor) ++ waylandWindow()->setMouseCursor(inputDevice, Qt::SplitHCursor); ++#endif ++ startResize(inputDevice, WL_SHELL_SURFACE_RESIZE_RIGHT,b); ++} ++ ++bool QGnomePlatformDecoration::updateButtonHoverState(Button hoveredButton) ++{ ++#if 0 ++ bool currentCloseButtonState = m_closeButtonHovered; ++ bool currentMaximizeButtonState = m_maximizeButtonHovered; ++ bool currentMinimizeButtonState = m_maximizeButtonHovered; ++ ++ m_closeButtonHovered = hoveredButton == Button::Close; ++ m_maximizeButtonHovered = hoveredButton == Button::Maximize; ++ m_minimizeButtonHovered = hoveredButton == Button::Minimize; ++ ++ if (m_closeButtonHovered != currentCloseButtonState ++ || m_maximizeButtonHovered != currentMaximizeButtonState ++ || m_minimizeButtonHovered != currentMinimizeButtonState) { ++ waylandWindow()->requestUpdate(); ++ return true; ++ } ++#endif ++ return false; ++} +diff --git a/decoration/qgnomeplatformdecoration.h b/decoration/qgnomeplatformdecoration.h +new file mode 100644 +index 0000000..600e082 +--- /dev/null ++++ b/decoration/qgnomeplatformdecoration.h +@@ -0,0 +1,88 @@ ++/* ++ * Copyright (C) 2019 Jan Grulich ++ * ++ * This library is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * This library is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ++ * Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with this library; if not, write to the Free Software ++ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ++ * ++ */ ++ ++#ifndef QGNOMEPLATFORMDECORATION_H ++#define QGNOMEPLATFORMDECORATION_H ++ ++#include ++ ++class GnomeHintsSettings; ++class QPixmap; ++ ++using namespace QtWaylandClient; ++ ++enum Button ++{ ++ None, ++ Close, ++ Maximize, ++ Minimize, ++ Restore ++}; ++ ++class QGnomePlatformDecoration : public QWaylandAbstractDecoration ++{ ++public: ++ QGnomePlatformDecoration(); ++ ~QGnomePlatformDecoration(); ++protected: ++ QMargins margins() const override; ++ void paint(QPaintDevice *device) override; ++ 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, Qt::TouchPointState state, Qt::KeyboardModifiers mods) override; ++private: ++ void initializeButtonPixmaps(); ++ void initializeColors(); ++ QPixmap pixmapDarkVariant(const QPixmap &pixmap); ++ ++ void processMouseTop(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b,Qt::KeyboardModifiers mods); ++ void processMouseBottom(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b,Qt::KeyboardModifiers mods); ++ void processMouseLeft(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b,Qt::KeyboardModifiers mods); ++ void processMouseRight(QWaylandInputDevice *inputDevice, const QPointF &local, Qt::MouseButtons b,Qt::KeyboardModifiers mods); ++ bool clickButton(Qt::MouseButtons b, Button btn); ++ bool updateButtonHoverState(Button hoveredButton); ++ ++ QRectF closeButtonRect() const; ++ QRectF maximizeButtonRect() const; ++ QRectF minimizeButtonRect() const; ++ ++ // Colors ++ QColor m_backgroundColorStart; ++ QColor m_backgroundColorEnd; ++ QColor m_backgroundInactiveColor; ++ QColor m_borderColor; ++ QColor m_borderInactiveColor; ++ QColor m_foregroundColor; ++ QColor m_foregroundInactiveColor; ++ ++ // Buttons ++ QHash m_buttonPixmaps; ++ bool m_closeButtonHovered; ++ bool m_maximizeButtonHovered; ++ bool m_minimizeButtonHovered; ++ ++ ++ QStaticText m_windowTitle; ++ Button m_clicking = None; ++ ++ GnomeHintsSettings *m_hints; ++}; ++ ++ ++#endif // QGNOMEPLATFORMDECORATION_H +diff --git a/src/qgnomeplatform.json b/decoration/qgnomeplatformdecoration.json +similarity index 100% +rename from src/qgnomeplatform.json +rename to decoration/qgnomeplatformdecoration.json +diff --git a/qgnomeplatform.pro b/qgnomeplatform.pro +index a67ec65..60e4d49 100644 +--- a/qgnomeplatform.pro ++++ b/qgnomeplatform.pro +@@ -1,32 +1,6 @@ +-lessThan(QT_MINOR_VERSION, 6): error("Qt 5.6 and newer is required.") ++TEMPLATE = subdirs + +-TEMPLATE = lib ++SUBDIRS += common decoration theme + +-CONFIG += plugin \ +- c++11 \ +- link_pkgconfig +- +-QT += core-private \ +- dbus \ +- gui-private \ +- widgets +- +-equals(QT_MAJOR_VERSION, 5): greaterThan(QT_MINOR_VERSION, 7): QT += theme_support-private +-equals(QT_MAJOR_VERSION, 5): lessThan(QT_MINOR_VERSION, 8): QT += platformsupport-private +- +-PKGCONFIG += gtk+-3.0 \ +- gtk+-x11-3.0 +- +-TARGET = qgnomeplatform +-target.path += $$[QT_INSTALL_PLUGINS]/platformthemes +-INSTALLS += target +- +-SOURCES += src/platformplugin.cpp \ +- src/qgnomeplatformtheme.cpp \ +- src/gnomehintssettings.cpp \ +- src/qgtk3dialoghelpers.cpp +- +-HEADERS += src/platformplugin.h \ +- src/qgnomeplatformtheme.h \ +- src/gnomehintssettings.h \ +- src/qgtk3dialoghelpers.h ++decoration.depends = common ++theme.depends = common +diff --git a/src/platformplugin.cpp b/theme/platformplugin.cpp +similarity index 100% +rename from src/platformplugin.cpp +rename to theme/platformplugin.cpp +diff --git a/src/platformplugin.h b/theme/platformplugin.h +similarity index 95% +rename from src/platformplugin.h +rename to theme/platformplugin.h +index c45dc4b..0f6387e 100644 +--- a/src/platformplugin.h ++++ b/theme/platformplugin.h +@@ -27,7 +27,7 @@ + + class QGnomePlatformThemePlugin : public QPlatformThemePlugin { + Q_OBJECT +- Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QPA.QPlatformThemeFactoryInterface.5.1" FILE "qgnomeplatform.json") ++ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QPA.QPlatformThemeFactoryInterface.5.1" FILE "qgnomeplatformtheme.json") + public: + QGnomePlatformThemePlugin(QObject *parent = 0); + +diff --git a/src/qgnomeplatformtheme.cpp b/theme/qgnomeplatformtheme.cpp +similarity index 93% +rename from src/qgnomeplatformtheme.cpp +rename to theme/qgnomeplatformtheme.cpp +index 65c29a0..78f2b5b 100644 +--- a/src/qgnomeplatformtheme.cpp ++++ b/theme/qgnomeplatformtheme.cpp +@@ -1,5 +1,6 @@ + /* + * Copyright (C) 2015 Martin Bříza ++ * Copyright (C) 2017-2019 Jan Grulich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +@@ -23,6 +24,7 @@ + + #include + #include ++#include + + #if !defined(QT_NO_DBUS) && !defined(QT_NO_SYSTEMTRAYICON) + #include +@@ -30,6 +32,11 @@ + + QGnomePlatformTheme::QGnomePlatformTheme() + { ++ if (!QX11Info::isPlatformX11()) { ++ if (!qEnvironmentVariableIsSet("QT_WAYLAND_DECORATION")) ++ qputenv("QT_WAYLAND_DECORATION", "gnome"); ++ } ++ + loadSettings(); + + /* Initialize some types here so that Gtk+ does not crash when reading +diff --git a/src/qgnomeplatformtheme.h b/theme/qgnomeplatformtheme.h +similarity index 96% +rename from src/qgnomeplatformtheme.h +rename to theme/qgnomeplatformtheme.h +index 240df25..50012a7 100644 +--- a/src/qgnomeplatformtheme.h ++++ b/theme/qgnomeplatformtheme.h +@@ -1,5 +1,6 @@ + /* + * Copyright (C) 2015 Martin Bříza ++ * Copyright (C) 2017-2019 Jan Grulich + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public +diff --git a/theme/qgnomeplatformtheme.json b/theme/qgnomeplatformtheme.json +new file mode 100644 +index 0000000..891c3e4 +--- /dev/null ++++ b/theme/qgnomeplatformtheme.json +@@ -0,0 +1,3 @@ ++{ ++ "Keys": [ "gnome", "gtk3", "qgnomeplatform" ] ++} +diff --git a/theme/theme.pro b/theme/theme.pro +new file mode 100644 +index 0000000..a33d3af +--- /dev/null ++++ b/theme/theme.pro +@@ -0,0 +1,32 @@ ++lessThan(QT_MINOR_VERSION, 9): error("Qt 5.9 and newer is required.") ++ ++TEMPLATE = lib ++ ++QMAKE_LIBDIR += ../common ++INCLUDEPATH += ../common ++ ++CONFIG += plugin \ ++ c++11 \ ++ link_pkgconfig ++ ++QT += core-private \ ++ dbus \ ++ gui-private \ ++ theme_support-private \ ++ x11extras \ ++ widgets ++ ++LIBS += -lcommon ++ ++PKGCONFIG += gtk+-3.0 \ ++ gtk+-x11-3.0 ++ ++TARGET = qgnomeplatform ++target.path += $$[QT_INSTALL_PLUGINS]/platformthemes ++INSTALLS += target ++ ++SOURCES += platformplugin.cpp \ ++ qgnomeplatformtheme.cpp ++ ++HEADERS += platformplugin.h \ ++ qgnomeplatformtheme.h diff --git a/qgnomeplatform.spec b/qgnomeplatform.spec index 0a4569d..52656aa 100644 --- a/qgnomeplatform.spec +++ b/qgnomeplatform.spec @@ -1,7 +1,7 @@ Name: qgnomeplatform Version: 0.5 -Release: 11%{?dist} +Release: 12%{?dist} Summary: Qt Platform Theme aimed to accommodate Gnome settings License: LGPLv2+ @@ -9,6 +9,7 @@ URL: https://github.com/MartinBriza/QGnomePlatform Source0: https://github.com/MartinBriza/QGnomePlatform/archive/%{version}/QGnomePlatform-%{version}.tar.gz # Upstream patches +Patch0: qgnomeplatform-gnome-decorations.patch BuildRequires: pkgconfig(gio-2.0) BuildRequires: pkgconfig(udev) @@ -55,6 +56,10 @@ make install INSTALL_ROOT=%{buildroot} -C %{_target_platform} %changelog +* Thu Aug 15 2019 Jan Grulich - 0.5-12 +- Add Gnome-like wayland decorations + BUG: bz#1732129 + * Fri Jul 26 2019 Fedora Release Engineering - 0.5-11 - Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild