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 }