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 }