Unity 8
CursorImageProvider.cpp
1 /*
2  * Copyright (C) 2015-2016 Canonical, Ltd.
3  *
4  * This program is free software: you can redistribute it and/or modify it under
5  * the terms of the GNU Lesser General Public License version 3, as published by
6  * the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful, but WITHOUT
9  * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10  * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11  * Lesser General Public License for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 #include "CursorImageProvider.h"
18 
19 #include <QCursor>
20 #include <QDebug>
21 #include <QFile>
22 #include <QPainter>
23 #include <QSvgRenderer>
24 
25 CursorImageProvider *CursorImageProvider::m_instance = nullptr;
26 
28 // BuiltInCursorImage
29 
30 BuiltInCursorImage::BuiltInCursorImage()
31 {
32  const char *svgString =
33  "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"
34  "<svg"
35  " xmlns:dc=\"http://purl.org/dc/elements/1.1/\""
36  " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
37  " xmlns:svg=\"http://www.w3.org/2000/svg\""
38  " xmlns=\"http://www.w3.org/2000/svg\""
39  " version=\"1.1\">"
40  " <path"
41  " style=\"fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:40;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\""
42  " d=\"M 20.504,50.94931 460.42533,518.14486 266.47603,515.61948 366.48114,719.16522 274.05218,770.68296 172.53185,559.56112 20.504,716.13476 Z\" />"
43  "</svg>";
44 
45  qimage = QImage(20, 32, QImage::Format_ARGB32);
46  qimage.fill(Qt::transparent);
47  QPainter imagePainter(&qimage);
48 
49  frameWidth = qimage.width();
50  frameHeight = qimage.height();
51 
52  QSvgRenderer *svgRenderer = new QSvgRenderer(QByteArray(svgString));
53  svgRenderer->render(&imagePainter);
54  delete svgRenderer;
55 }
56 
58 // BlankCursorImage
59 
60 
61 BlankCursorImage::BlankCursorImage()
62 {
63  qimage = QImage(1, 1, QImage::Format_ARGB32);
64  qimage.fill(Qt::transparent);
65  frameWidth = qimage.width();
66  frameHeight = qimage.height();
67 }
68 
70 // CustomCursorImage
71 
72 
73 CustomCursorImage::CustomCursorImage(const QCursor &cursor)
74 {
75  qimage = cursor.pixmap().toImage();
76  hotspot = cursor.hotSpot();
77  frameWidth = qimage.width();
78  frameHeight = qimage.height();
79 }
80 
82 // XCursorImage
83 
84 XCursorImage::XCursorImage(const QString &theme, const QString &file)
85 {
86  // TODO: Consider grid unit value
87  // Hardcoding to a medium size for now
88  int preferredCursorHeightPx = 32;
89 
90  XcursorImages *xcursorImages = XcursorLibraryLoadImages(QFile::encodeName(file), QFile::encodeName(theme),
91  preferredCursorHeightPx);
92  if (!xcursorImages || xcursorImages->nimage == 0) {
93  return;
94  }
95 
96  frameCount = xcursorImages->nimage;
97 
98  for (int i = 0; i < xcursorImages->nimage; ++i) {
99  XcursorImage *xcursorImage = xcursorImages->images[0];
100  if (frameWidth < (int)xcursorImage->width) {
101  frameWidth = xcursorImage->width;
102  }
103  if (frameHeight < (int)xcursorImage->height) {
104  frameHeight = xcursorImage->height;
105  }
106  if (i == 0) {
107  frameDuration = (int)xcursorImage->delay;
108  } else {
109  if (frameDuration != (int)xcursorImage->delay) {
110  qWarning().nospace() << "CursorImageProvider: XCursorImage("<<theme<<","<<file<<") has"
111  " varying delays in its animation. Animation won't look right.";
112  }
113  }
114  }
115 
116  {
117  // Assume that the hotspot position does not animate
118  XcursorImage *xcursorImage = xcursorImages->images[0];
119  hotspot.setX(xcursorImage->xhot);
120  hotspot.setY(xcursorImage->yhot);
121  }
122 
123  // Build the sprite as a single row of frames
124  qimage = QImage(frameWidth*frameCount, frameHeight, QImage::Format_ARGB32);
125  qimage.fill(Qt::transparent);
126 
127  {
128  QPainter painter(&qimage);
129 
130  for (int i = 0; i < xcursorImages->nimage; ++i) {
131  XcursorImage *xcursorImage = xcursorImages->images[i];
132 
133  auto frameImage = QImage((uchar*)xcursorImage->pixels,
134  xcursorImage->width, xcursorImage->height, QImage::Format_ARGB32);
135 
136  painter.drawImage(QPoint(i*frameWidth, 0), frameImage);
137  }
138  }
139 
140  XcursorImagesDestroy(xcursorImages);
141 }
142 
143 XCursorImage::~XCursorImage()
144 {
145 }
146 
148 // CursorImageProvider
149 
150 CursorImageProvider::CursorImageProvider()
151  : QQuickImageProvider(QQuickImageProvider::Image)
152 {
153  if (m_instance) {
154  qFatal("Cannot have multiple CursorImageProvider instances");
155  }
156  m_instance = this;
157 
158  m_fallbackNames[QStringLiteral("closedhand")].append(QStringLiteral("grabbing"));
159  m_fallbackNames[QStringLiteral("closedhand")].append(QStringLiteral("dnd-none"));
160 
161  m_fallbackNames[QStringLiteral("dnd-copy")].append(QStringLiteral("dnd-none"));
162  m_fallbackNames[QStringLiteral("dnd-copy")].append(QStringLiteral("grabbing"));
163  m_fallbackNames[QStringLiteral("dnd-copy")].append(QStringLiteral("closedhand"));
164 
165  m_fallbackNames[QStringLiteral("dnd-move")].append(QStringLiteral("dnd-none"));
166  m_fallbackNames[QStringLiteral("dnd-move")].append(QStringLiteral("grabbing"));
167  m_fallbackNames[QStringLiteral("dnd-move")].append(QStringLiteral("closedhand"));
168 
169  m_fallbackNames[QStringLiteral("dnd-link")].append(QStringLiteral("dnd-none"));
170  m_fallbackNames[QStringLiteral("dnd-link")].append(QStringLiteral("grabbing"));
171  m_fallbackNames[QStringLiteral("dnd-link")].append(QStringLiteral("closedhand"));
172 
173  m_fallbackNames[QStringLiteral("forbidden")].append(QStringLiteral("crossed_circle")); // DMZ-White and DMZ-Black themes
174  m_fallbackNames[QStringLiteral("forbidden")].append(QStringLiteral("not-allowed"));
175  m_fallbackNames[QStringLiteral("forbidden")].append(QStringLiteral("circle"));
176 
177  m_fallbackNames[QStringLiteral("hand")].append(QStringLiteral("pointing_hand"));
178  m_fallbackNames[QStringLiteral("hand")].append(QStringLiteral("pointer"));
179 
180  m_fallbackNames[QStringLiteral("ibeam")].append(QStringLiteral("xterm"));
181  m_fallbackNames[QStringLiteral("ibeam")].append(QStringLiteral("text"));
182 
183  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("default"));
184  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("top_left_arrow"));
185  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("left_arrow"));
186 
187  m_fallbackNames[QStringLiteral("left_ptr_watch")].append(QStringLiteral("half-busy"));
188  m_fallbackNames[QStringLiteral("left_ptr_watch")].append(QStringLiteral("progress"));
189 
190  m_fallbackNames[QStringLiteral("size_bdiag")].append(QStringLiteral("fd_double_arrow"));
191  m_fallbackNames[QStringLiteral("size_bdiag")].append(QStringLiteral("nesw-resize"));
192 
193  m_fallbackNames[QStringLiteral("size_fdiag")].append(QStringLiteral("bd_double_arrow")); // DMZ-White and DMZ-Black themes
194  m_fallbackNames[QStringLiteral("size_fdiag")].append(QStringLiteral("nwse-resize"));
195 
196  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("sb_h_double_arrow")); // DMZ-White and DMZ-Black themes
197  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("ew-resize"));
198  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("h_double_arrow"));
199 
200  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("sb_v_double_arrow")); // DMZ-White and DMZ-Black themes
201  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("ns-resize"));
202  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("v_double_arrow"));
203 
204  m_fallbackNames[QStringLiteral("split_h")].append(QStringLiteral("sb_h_double_arrow")); // DMZ-White and DMZ-Black themes
205  m_fallbackNames[QStringLiteral("split_h")].append(QStringLiteral("col-resize"));
206 
207  m_fallbackNames[QStringLiteral("split_v")].append(QStringLiteral("sb_v_double_arrow")); // DMZ-White and DMZ-Black themes
208  m_fallbackNames[QStringLiteral("split_v")].append(QStringLiteral("row-resize"));
209 
210  m_fallbackNames[QStringLiteral("up_arrow")].append(QStringLiteral("sb_up_arrow")); // DMZ-White and DMZ-Black themes
211 
212  m_fallbackNames[QStringLiteral("watch")].append(QStringLiteral("wait"));
213 
214  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("left_ptr_help"));
215  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("help"));
216  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("question_arrow"));
217 
218  m_fallbackNames[QStringLiteral("xterm")].append(QStringLiteral("ibeam"));
219 }
220 
221 CursorImageProvider::~CursorImageProvider()
222 {
223  {
224  QList< QMap<QString, CursorImage*> > cursorList = m_cursors.values();
225 
226  for (int i = 0; i < cursorList.count(); ++i) {
227  QList<CursorImage*> cursorImageList = cursorList[i].values();
228  for (int j = 0; j < cursorImageList.count(); ++j) {
229  delete cursorImageList[j];
230  }
231  }
232  }
233 
234  m_cursors.clear();
235  m_instance = nullptr;
236 }
237 
238 QImage CursorImageProvider::requestImage(const QString &cursorThemeAndName, QSize *size, const QSize & /*requestedSize*/)
239 {
240  CursorImage *cursorImage = fetchCursor(cursorThemeAndName);
241  size->setWidth(cursorImage->qimage.width());
242  size->setHeight(cursorImage->qimage.height());
243 
244  return cursorImage->qimage;
245 }
246 
247 CursorImage *CursorImageProvider::fetchCursor(const QString &cursorThemeAndName)
248 {
249  QString themeName;
250  QString cursorName;
251  {
252  QStringList themeAndNameList = cursorThemeAndName.split('/');
253  if (themeAndNameList.size() != 2) {
254  return nullptr;
255  }
256  themeName = themeAndNameList[0];
257  cursorName = themeAndNameList[1];
258  }
259 
260  return fetchCursor(themeName, cursorName);
261 }
262 
263 CursorImage *CursorImageProvider::fetchCursor(const QString &themeName, const QString &cursorName)
264 {
265  CursorImage *cursorImage = fetchCursorHelper(themeName, cursorName);
266 
267  // Try some fallbacks
268  if (cursorImage->qimage.isNull()) {
269  if (m_fallbackNames.contains(cursorName)) {
270  const QStringList &fallbackNames = m_fallbackNames[cursorName];
271  int i = 0;
272  while (cursorImage->qimage.isNull() && i < fallbackNames.count()) {
273  qDebug().nospace() << "CursorImageProvider: "<< cursorName <<" not found, trying " << fallbackNames.at(i);
274  cursorImage = fetchCursorHelper(themeName, fallbackNames.at(i));
275  ++i;
276  }
277  }
278  }
279 
280  // if it all fails, there must be at least a left_ptr
281  if (cursorImage->qimage.isNull() && cursorName != QLatin1String("left_ptr")) {
282  qDebug() << "CursorImageProvider:" << cursorName
283  << "not found (nor its fallbacks, if any). Going for \"left_ptr\" as a last resort.";
284  cursorImage = fetchCursorHelper(themeName, QStringLiteral("left_ptr"));
285  }
286 
287  if (cursorImage->qimage.isNull()) {
288  // finally, go for the built-in cursor
289  qWarning() << "CursorImageProvider: couldn't find any cursors. Using the built-in one";
290  if (!m_builtInCursorImage) {
291  m_builtInCursorImage.reset(new BuiltInCursorImage);
292  }
293  cursorImage = m_builtInCursorImage.data();
294  }
295 
296  return cursorImage;
297 }
298 
299 CursorImage *CursorImageProvider::fetchCursorHelper(const QString &themeName, const QString &cursorName)
300 {
301  if (cursorName == QLatin1String("blank")) {
302  return &m_blankCursorImage;
303  } else if (cursorName == QLatin1String("custom")) {
304  return m_customCursorImage.data();
305  } else {
306  QMap<QString, CursorImage*> &themeCursors = m_cursors[themeName];
307 
308  if (!themeCursors.contains(cursorName)) {
309  themeCursors[cursorName] = new XCursorImage(themeName, cursorName);
310  }
311 
312  return themeCursors[cursorName];
313  }
314 }
315 
316 void CursorImageProvider::setCustomCursor(const QCursor &customCursor)
317 {
318  if (customCursor.pixmap().isNull()) {
319  m_customCursorImage.reset();
320  } else {
321  m_customCursorImage.reset(new CustomCursorImage(customCursor));
322  }
323 }