File indexing completed on 2024-05-26 16:53:42

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