File indexing completed on 2025-03-02 05:11:57
0001 /* 0002 SPDX-FileCopyrightText: 2003-2007 Fredrik Höglund <fredrik@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-only 0005 */ 0006 0007 #include <QMouseEvent> 0008 #include <QPainter> 0009 #include <QQuickRenderControl> 0010 #include <QQuickWindow> 0011 0012 #include <KWindowSystem> 0013 0014 #include "previewwidget.h" 0015 0016 #include "config-X11.h" 0017 #include "cursortheme.h" 0018 0019 namespace 0020 { 0021 // Preview cursors 0022 const char *const cursor_names[] = { 0023 "left_ptr", 0024 "left_ptr_watch", 0025 "wait", 0026 "pointer", 0027 "help", 0028 "ibeam", 0029 "size_all", 0030 "size_fdiag", 0031 "cross", 0032 "split_h", 0033 "size_ver", 0034 "size_hor", 0035 "size_bdiag", 0036 "split_v", 0037 }; 0038 0039 const int numCursors = 9; // The number of cursors from the above list to be previewed 0040 constexpr int cursorSpacing = 14 * 2; // Spacing between preview cursors 0041 const qreal widgetMinWidth = 10; // The minimum width of the preview widget 0042 const qreal widgetMinHeight = 48; // The minimum height of the preview widget 0043 } 0044 0045 class PreviewCursor 0046 { 0047 public: 0048 PreviewCursor(const CursorTheme *theme, const QString &name, int size); 0049 0050 const QPixmap &pixmap() const 0051 { 0052 return m_pixmap; 0053 } 0054 int width() const 0055 { 0056 return m_pixmap.width(); 0057 } 0058 int height() const 0059 { 0060 return m_pixmap.height(); 0061 } 0062 int boundingSize() const 0063 { 0064 return m_boundingSize; 0065 } 0066 inline QRectF rect() const; 0067 void setPosition(const QPoint &p) 0068 { 0069 m_pos = p; 0070 } 0071 void setPosition(int x, int y) 0072 { 0073 m_pos = QPoint(x, y); 0074 } 0075 QPoint position() const 0076 { 0077 return m_pos; 0078 } 0079 operator const QPixmap &() const 0080 { 0081 return pixmap(); 0082 } 0083 const std::vector<CursorTheme::CursorImage> &images() const 0084 { 0085 return m_images; 0086 } 0087 0088 private: 0089 int m_boundingSize; 0090 QPixmap m_pixmap; 0091 std::vector<CursorTheme::CursorImage> m_images; 0092 QPoint m_pos; 0093 }; 0094 0095 PreviewCursor::PreviewCursor(const CursorTheme *theme, const QString &name, int size) 0096 : m_boundingSize(size > 0 ? size : theme->defaultCursorSize()) 0097 , m_images(theme->loadImages(name, size)) 0098 { 0099 if (m_images.empty()) 0100 return; 0101 0102 m_pixmap = QPixmap::fromImage(m_images.front().image); 0103 } 0104 0105 QRectF PreviewCursor::rect() const 0106 { 0107 return QRectF(m_pos, m_pixmap.size()).adjusted(-(cursorSpacing / 2), -(cursorSpacing / 2), cursorSpacing / 2, cursorSpacing / 2); 0108 } 0109 0110 // ------------------------------------------------------------------------------ 0111 0112 PreviewWidget::PreviewWidget(QQuickItem *parent) 0113 : QQuickPaintedItem(parent) 0114 , m_currentIndex(-1) 0115 , m_currentSize(0) 0116 { 0117 setAcceptHoverEvents(true); 0118 current = nullptr; 0119 connect(&m_animationTimer, &QTimer::timeout, this, [this] { 0120 Q_ASSERT(current); 0121 setCursor(QCursor(QPixmap::fromImage(current->images().at(nextAnimationFrame).image))); 0122 m_animationTimer.setInterval(current->images().at(nextAnimationFrame).delay); 0123 nextAnimationFrame = (nextAnimationFrame + 1) % current->images().size(); 0124 }); 0125 } 0126 0127 PreviewWidget::~PreviewWidget() 0128 { 0129 qDeleteAll(list); 0130 list.clear(); 0131 } 0132 0133 void PreviewWidget::setThemeModel(SortProxyModel *themeModel) 0134 { 0135 if (m_themeModel == themeModel) { 0136 return; 0137 } 0138 0139 m_themeModel = themeModel; 0140 Q_EMIT themeModelChanged(); 0141 } 0142 0143 SortProxyModel *PreviewWidget::themeModel() 0144 { 0145 return m_themeModel; 0146 } 0147 0148 void PreviewWidget::setCurrentIndex(int idx) 0149 { 0150 if (m_currentIndex == idx) { 0151 return; 0152 } 0153 0154 m_currentIndex = idx; 0155 Q_EMIT currentIndexChanged(); 0156 0157 if (!m_themeModel) { 0158 return; 0159 } 0160 const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(idx, 0)); 0161 setTheme(theme, m_currentSize); 0162 } 0163 0164 int PreviewWidget::currentIndex() const 0165 { 0166 return m_currentIndex; 0167 } 0168 0169 void PreviewWidget::setCurrentSize(int size) 0170 { 0171 if (m_currentSize == size) { 0172 return; 0173 } 0174 0175 m_currentSize = size; 0176 Q_EMIT currentSizeChanged(); 0177 0178 if (!m_themeModel) { 0179 return; 0180 } 0181 const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); 0182 setTheme(theme, size); 0183 } 0184 0185 int PreviewWidget::currentSize() const 0186 { 0187 return m_currentSize; 0188 } 0189 0190 void PreviewWidget::refresh() 0191 { 0192 if (!m_themeModel) { 0193 return; 0194 } 0195 0196 const CursorTheme *theme = m_themeModel->theme(m_themeModel->index(m_currentIndex, 0)); 0197 setTheme(theme, m_currentSize); 0198 } 0199 0200 void PreviewWidget::updateImplicitSize() 0201 { 0202 qreal totalWidth = 0; 0203 qreal maxHeight = 0; 0204 0205 for (const auto *c : std::as_const(list)) { 0206 totalWidth += c->width(); 0207 maxHeight = qMax(c->height(), (int)maxHeight); 0208 } 0209 0210 totalWidth += (list.count() - 1) * cursorSpacing; 0211 maxHeight = qMax(maxHeight, widgetMinHeight); 0212 0213 setImplicitWidth(qMax(totalWidth, widgetMinWidth)); 0214 setImplicitHeight(qMax(height(), maxHeight)); 0215 } 0216 0217 void PreviewWidget::layoutItems() 0218 { 0219 if (!list.isEmpty()) { 0220 double deviceCoordinateWidth = width(); 0221 #if HAVE_X11 0222 if (KWindowSystem::isPlatformX11()) { 0223 deviceCoordinateWidth *= window()->devicePixelRatio(); 0224 } 0225 #endif 0226 const int spacing = cursorSpacing / 2; 0227 int nextX = spacing; 0228 int nextY = spacing; 0229 0230 for (auto *c : std::as_const(list)) { 0231 c->setPosition(nextX, nextY); 0232 const int boundingSize = c->boundingSize(); 0233 nextX += boundingSize + spacing; 0234 if (nextX + boundingSize > deviceCoordinateWidth) { 0235 nextX = spacing; 0236 nextY += boundingSize + spacing; 0237 } 0238 } 0239 } 0240 0241 needLayout = false; 0242 } 0243 0244 void PreviewWidget::setTheme(const CursorTheme *theme, const int size) 0245 { 0246 qDeleteAll(list); 0247 list.clear(); 0248 0249 if (theme) { 0250 for (int i = 0; i < numCursors; i++) 0251 list << new PreviewCursor(theme, cursor_names[i], size); 0252 0253 needLayout = true; 0254 updateImplicitSize(); 0255 } 0256 0257 current = nullptr; 0258 m_animationTimer.stop(); 0259 update(); 0260 } 0261 0262 void PreviewWidget::paint(QPainter *painter) 0263 { 0264 if (needLayout) 0265 layoutItems(); 0266 0267 painter->setRenderHint(QPainter::SmoothPixmapTransform, true); 0268 0269 // for cursor themes we must ignore the native scaling, 0270 // as they will be rendered by X11/KWin, ignoring whatever Qt 0271 // scaling 0272 double devicePixelRatio = 1; 0273 #if HAVE_X11 0274 if (KWindowSystem::isPlatformX11()) { 0275 devicePixelRatio = window()->devicePixelRatio(); 0276 } 0277 painter->scale(1 / devicePixelRatio, 1 / devicePixelRatio); 0278 #endif 0279 for (const auto *c : std::as_const(list)) { 0280 if (c->pixmap().isNull()) 0281 continue; 0282 0283 painter->drawPixmap(c->position(), *c); 0284 } 0285 } 0286 0287 void PreviewWidget::hoverMoveEvent(QHoverEvent *e) 0288 { 0289 e->ignore(); // Propagate hover event to parent 0290 0291 if (needLayout) 0292 layoutItems(); 0293 0294 double devicePixelRatio = 1.0; 0295 #if HAVE_X11 0296 if (KWindowSystem::isPlatformX11()) { 0297 devicePixelRatio = window()->devicePixelRatio(); 0298 } 0299 #endif 0300 auto it = std::find_if(list.cbegin(), list.cend(), [e, devicePixelRatio](const PreviewCursor *c) { 0301 return c->rect().contains(e->position() * devicePixelRatio); 0302 }); 0303 const PreviewCursor *cursor = it != list.cend() ? *it : nullptr; 0304 0305 if (cursor == std::exchange(current, cursor)) { 0306 return; 0307 } 0308 m_animationTimer.stop(); 0309 0310 if (current == nullptr) { 0311 setCursor(Qt::ArrowCursor); 0312 return; 0313 } 0314 0315 if (current->images().size() <= 1) { 0316 setCursor(QCursor(current->pixmap())); 0317 return; 0318 } 0319 0320 nextAnimationFrame = 0; 0321 m_animationTimer.setInterval(0); 0322 m_animationTimer.start(); 0323 } 0324 0325 void PreviewWidget::hoverLeaveEvent(QHoverEvent *e) 0326 { 0327 m_animationTimer.stop(); 0328 unsetCursor(); 0329 0330 e->ignore(); // Propagate hover event to parent 0331 } 0332 0333 void PreviewWidget::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) 0334 { 0335 Q_UNUSED(newGeometry) 0336 Q_UNUSED(oldGeometry) 0337 if (!list.isEmpty()) { 0338 needLayout = true; 0339 } 0340 }