File indexing completed on 2024-05-19 05:38:25

0001 /*
0002     SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund <fredrik@kde.org>
0003     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@broulik.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only
0006 */
0007 
0008 #include "previewitem.h"
0009 #include "kcm_style_debug.h"
0010 
0011 #include <chrono>
0012 #include <cmath>
0013 
0014 #include <QHoverEvent>
0015 #include <QMouseEvent>
0016 #include <QPainter>
0017 #include <QPixmapCache>
0018 #include <QQuickWindow>
0019 #include <QStyleFactory>
0020 #include <QWidget>
0021 
0022 #include <KColorScheme>
0023 #include <KSharedConfig>
0024 
0025 using namespace std::chrono_literals;
0026 
0027 PreviewItem::PreviewItem(QQuickItem *parent)
0028     : QQuickPaintedItem(parent)
0029 {
0030     setAcceptHoverEvents(true);
0031 
0032     // HACK QtCurve deadlocks on application teardown when the Q_GLOBAL_STATIC QFactoryLoader
0033     // in QStyleFactory is destroyed which destroys all loaded styles prompting QtCurve
0034     // to disconnect from DBus stalling the application.
0035     // This also happens before any of the KCM objects are destroyed, so our only chance
0036     // is cleaning up in response to aboutToQuit
0037     connect(qApp, &QApplication::aboutToQuit, this, [this] {
0038         m_style.reset();
0039     });
0040 }
0041 
0042 PreviewItem::~PreviewItem() = default;
0043 
0044 void PreviewItem::componentComplete()
0045 {
0046     QQuickPaintedItem::componentComplete();
0047     reload();
0048 }
0049 
0050 bool PreviewItem::eventFilter(QObject *watched, QEvent *event)
0051 {
0052     if (watched == m_widget.get()) {
0053         switch (event->type()) {
0054         case QEvent::Show:
0055             update();
0056             break;
0057         case QEvent::UpdateRequest:
0058             // Some 3rd-party styles (e.g. QSvgStyle) frequently request updates which cause high CPU usage.
0059             // Suppress them to work around buggy styles.
0060             if (m_containsMouse) {
0061                 update();
0062             } else if (!m_timerId) {
0063                 m_timerId = startTimer(1s);
0064             }
0065             break;
0066         default:
0067             break;
0068         }
0069     }
0070 
0071     return QQuickPaintedItem::eventFilter(watched, event);
0072 }
0073 
0074 QString PreviewItem::styleName() const
0075 {
0076     return m_styleName;
0077 }
0078 
0079 void PreviewItem::setStyleName(const QString &styleName)
0080 {
0081     if (m_styleName == styleName) {
0082         return;
0083     }
0084 
0085     m_styleName = styleName;
0086     reload();
0087     Q_EMIT styleNameChanged();
0088 }
0089 
0090 bool PreviewItem::isValid() const
0091 {
0092     return m_style && m_widget;
0093 }
0094 
0095 void setStyleRecursively(QWidget *widget, QStyle *style, const QPalette &palette)
0096 {
0097     // Don't let styles kill the palette for other styles being previewed.
0098     widget->setPalette(QPalette());
0099     widget->setPalette(palette);
0100 
0101     widget->setStyle(style);
0102 
0103     const auto children = widget->children();
0104     for (QObject *child : children) {
0105         if (child->isWidgetType()) {
0106             setStyleRecursively(static_cast<QWidget *>(child), style, palette);
0107         }
0108     }
0109 }
0110 
0111 void PreviewItem::reload()
0112 {
0113     if (!isComponentComplete()) {
0114         return;
0115     }
0116 
0117     const bool oldValid = isValid();
0118 
0119     m_style.reset(QStyleFactory::create(m_styleName));
0120     if (!m_style) {
0121         qCWarning(KCM_STYLE_DEBUG) << "Failed to load style" << m_styleName;
0122         if (oldValid != isValid()) {
0123             Q_EMIT validChanged();
0124         }
0125         return;
0126     }
0127 
0128     m_widget.reset(new QWidget);
0129     // Don't actually show the widget as a separate window when calling show()
0130     m_widget->setAttribute(Qt::WA_DontShowOnScreen);
0131     // Do not wait for this widget to close before the app closes
0132     m_widget->setAttribute(Qt::WA_QuitOnClose, false);
0133 
0134     m_ui.setupUi(m_widget.get());
0135 
0136     // Prevent Qt from wrongly caching radio button images
0137     QPixmapCache::clear();
0138 
0139     QPalette palette(KColorScheme::createApplicationPalette(KSharedConfig::openConfig()));
0140     m_style->polish(palette);
0141 
0142     // HACK Needed so the previews look like their window is active
0143     // The previews don't have a parent (we're in QML, after all, there is no QWidget* to parent it to)
0144     // so QWidget::isActiveWindow() always returns false making the widget look dull
0145     // You still won't get hover effects in some themes (those that don't do that for inactive windows)
0146     // but at least at a glance it looks fine...
0147     for (int i = 0; i < QPalette::NColorRoles; ++i) {
0148         const auto role = static_cast<QPalette::ColorRole>(i);
0149         palette.setColor(QPalette::Inactive, role, palette.color(QPalette::Active, role));
0150     }
0151 
0152     setStyleRecursively(m_widget.get(), m_style.get(), palette);
0153 
0154     m_widget->ensurePolished();
0155 
0156     resizeWidget(size());
0157 
0158     m_widget->installEventFilter(this);
0159 
0160     m_widget->show();
0161 
0162     const auto sizeHint = m_widget->sizeHint();
0163     setImplicitSize(sizeHint.width(), sizeHint.height());
0164 
0165     if (oldValid != isValid()) {
0166         Q_EMIT validChanged();
0167     }
0168 }
0169 
0170 void PreviewItem::paint(QPainter *painter)
0171 {
0172     if (m_widget && m_widget->isVisible()) {
0173         painter->scale(width() / static_cast<qreal>(m_widget->width()), height() / static_cast<qreal>(m_widget->height()));
0174         m_widget->render(painter);
0175     }
0176 }
0177 
0178 void PreviewItem::hoverEnterEvent(QHoverEvent *event)
0179 {
0180     m_containsMouse = true;
0181     event->ignore(); // Propagate hover event to parent
0182 }
0183 
0184 void PreviewItem::hoverMoveEvent(QHoverEvent *event)
0185 {
0186     sendHoverEvent(event);
0187     event->ignore(); // Propagate hover event to parent
0188 }
0189 
0190 void PreviewItem::hoverLeaveEvent(QHoverEvent *event)
0191 {
0192     m_containsMouse = false;
0193 
0194     if (m_lastWidgetUnderMouse) {
0195         dispatchEnterLeave(nullptr, m_lastWidgetUnderMouse, mapToGlobal(event->position()));
0196         m_lastWidgetUnderMouse = nullptr;
0197     }
0198     event->ignore(); // Propagate hover event to parent
0199 }
0200 
0201 void PreviewItem::timerEvent(QTimerEvent *event)
0202 {
0203     if (event->timerId() != m_timerId) {
0204         return;
0205     }
0206     killTimer(m_timerId);
0207     m_timerId = 0;
0208     update();
0209 }
0210 
0211 void PreviewItem::sendHoverEvent(QHoverEvent *event)
0212 {
0213     if (!m_widget || !m_widget->isVisible()) {
0214         return;
0215     }
0216 
0217     QPointF pos = event->position();
0218 
0219     QWidget *child = m_widget->childAt(pos.toPoint());
0220     QWidget *receiver = child ? child : m_widget.get();
0221 
0222     dispatchEnterLeave(receiver, m_lastWidgetUnderMouse, mapToGlobal(event->position()));
0223 
0224     m_lastWidgetUnderMouse = receiver;
0225 
0226     pos = receiver->mapFrom(m_widget.get(), pos.toPoint());
0227 
0228     QMouseEvent mouseEvent(QEvent::MouseMove,
0229                            pos,
0230                            receiver->mapTo(receiver->topLevelWidget(), pos.toPoint()),
0231                            receiver->mapToGlobal(pos.toPoint()),
0232                            Qt::NoButton,
0233                            {} /*buttons*/,
0234                            event->modifiers());
0235 
0236     qApp->sendEvent(receiver, &mouseEvent);
0237 
0238     event->setAccepted(mouseEvent.isAccepted());
0239 }
0240 
0241 // Simplified copy of QApplicationPrivate::dispatchEnterLeave
0242 void PreviewItem::dispatchEnterLeave(QWidget *enter, QWidget *leave, const QPointF &globalPosF)
0243 {
0244     if ((!enter && !leave) || (enter == leave)) {
0245         return;
0246     }
0247 
0248     QWidgetList leaveList;
0249     QWidgetList enterList;
0250 
0251     bool sameWindow = leave && enter && leave->window() == enter->window();
0252     if (leave && !sameWindow) {
0253         auto *w = leave;
0254         do {
0255             leaveList.append(w);
0256         } while (!w->isWindow() && (w = w->parentWidget()));
0257     }
0258     if (enter && !sameWindow) {
0259         auto *w = enter;
0260         do {
0261             enterList.append(w);
0262         } while (!w->isWindow() && (w = w->parentWidget()));
0263     }
0264     if (sameWindow) {
0265         int enterDepth = 0;
0266         int leaveDepth = 0;
0267         auto *e = enter;
0268         while (!e->isWindow() && (e = e->parentWidget()))
0269             enterDepth++;
0270         auto *l = leave;
0271         while (!l->isWindow() && (l = l->parentWidget()))
0272             leaveDepth++;
0273         QWidget *wenter = enter;
0274         QWidget *wleave = leave;
0275         while (enterDepth > leaveDepth) {
0276             wenter = wenter->parentWidget();
0277             enterDepth--;
0278         }
0279         while (leaveDepth > enterDepth) {
0280             wleave = wleave->parentWidget();
0281             leaveDepth--;
0282         }
0283         while (!wenter->isWindow() && wenter != wleave) {
0284             wenter = wenter->parentWidget();
0285             wleave = wleave->parentWidget();
0286         }
0287 
0288         for (auto *w = leave; w != wleave; w = w->parentWidget())
0289             leaveList.append(w);
0290 
0291         for (auto *w = enter; w != wenter; w = w->parentWidget())
0292             enterList.append(w);
0293     }
0294 
0295     const QPoint globalPos = globalPosF.toPoint();
0296 
0297     QEvent leaveEvent(QEvent::Leave);
0298     for (int i = 0; i < leaveList.size(); ++i) {
0299         auto *w = leaveList.at(i);
0300         QApplication::sendEvent(w, &leaveEvent);
0301         if (w->testAttribute(Qt::WA_Hover)) {
0302             QHoverEvent he(QEvent::HoverLeave, QPoint(-1, -1), w->mapFromGlobal(globalPos), QApplication::keyboardModifiers());
0303             QApplication::sendEvent(w, &he);
0304         }
0305     }
0306     if (!enterList.isEmpty()) {
0307         const QPoint windowPos = std::as_const(enterList).back()->window()->mapFromGlobal(globalPos);
0308         for (auto it = enterList.crbegin(), end = enterList.crend(); it != end; ++it) {
0309             auto *w = *it;
0310             const QPointF localPos = w->mapFromGlobal(globalPos);
0311             QEnterEvent enterEvent(localPos, windowPos, globalPosF);
0312             QApplication::sendEvent(w, &enterEvent);
0313             if (w->testAttribute(Qt::WA_Hover)) {
0314                 QHoverEvent he(QEvent::HoverEnter, localPos, QPoint(-1, -1), QApplication::keyboardModifiers());
0315                 QApplication::sendEvent(w, &he);
0316             }
0317         }
0318     }
0319 }
0320 
0321 void PreviewItem::resizeWidget(const QSizeF &newSize)
0322 {
0323     if (!m_widget) {
0324         return;
0325     }
0326 
0327     QSizeF size = newSize;
0328     if (size.width() < implicitWidth() || size.height() < implicitHeight()) {
0329         size.scale(implicitWidth(), implicitHeight(), Qt::KeepAspectRatioByExpanding);
0330     }
0331 
0332     m_widget->resize(std::ceil(size.width()), std::ceil(size.height()));
0333 }
0334 
0335 void PreviewItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
0336 {
0337     if (newGeometry != oldGeometry) {
0338         resizeWidget(newGeometry.size());
0339     }
0340 
0341     QQuickPaintedItem::geometryChange(newGeometry, oldGeometry);
0342 }