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(int cursorHeight)
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  // NB: Original image dimension is 20x32. Ensure aspect ratio is kept
46  qimage = QImage((20./32.)*cursorHeight, cursorHeight, QImage::Format_ARGB32);
47  qimage.fill(Qt::transparent);
48  QPainter imagePainter(&qimage);
49 
50  frameWidth = qimage.width();
51  frameHeight = qimage.height();
52  requestedHeight = cursorHeight;
53 
54  QSvgRenderer *svgRenderer = new QSvgRenderer(QByteArray(svgString));
55  svgRenderer->render(&imagePainter);
56  delete svgRenderer;
57 }
58 
60 // BlankCursorImage
61 
62 
63 BlankCursorImage::BlankCursorImage()
64 {
65  qimage = QImage(1, 1, QImage::Format_ARGB32);
66  qimage.fill(Qt::transparent);
67  frameWidth = qimage.width();
68  frameHeight = qimage.height();
69 }
70 
72 // CustomCursorImage
73 
74 
75 CustomCursorImage::CustomCursorImage(const QCursor &cursor)
76 {
77  qimage = cursor.pixmap().toImage();
78  hotspot = cursor.hotSpot();
79  frameWidth = qimage.width();
80  frameHeight = qimage.height();
81 }
82 
84 // XCursorImage
85 
86 XCursorImage::XCursorImage(const QString &theme, const QString &file, int preferredCursorHeightPx)
87 {
88  requestedHeight = preferredCursorHeightPx;
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[i];
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("grabbing")].append(QStringLiteral("closedhand")); // Breeze
178 
179  m_fallbackNames[QStringLiteral("hand")].append(QStringLiteral("pointing_hand"));
180  m_fallbackNames[QStringLiteral("hand")].append(QStringLiteral("pointer"));
181 
182  m_fallbackNames[QStringLiteral("ibeam")].append(QStringLiteral("xterm"));
183  m_fallbackNames[QStringLiteral("ibeam")].append(QStringLiteral("text"));
184 
185  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("default"));
186  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("top_left_arrow"));
187  m_fallbackNames[QStringLiteral("left_ptr")].append(QStringLiteral("left_arrow"));
188 
189  m_fallbackNames[QStringLiteral("left_ptr_watch")].append(QStringLiteral("half-busy"));
190  m_fallbackNames[QStringLiteral("left_ptr_watch")].append(QStringLiteral("progress"));
191 
192  m_fallbackNames[QStringLiteral("size_bdiag")].append(QStringLiteral("fd_double_arrow"));
193  m_fallbackNames[QStringLiteral("size_bdiag")].append(QStringLiteral("nesw-resize"));
194 
195  m_fallbackNames[QStringLiteral("size_fdiag")].append(QStringLiteral("bd_double_arrow")); // DMZ-White and DMZ-Black themes
196  m_fallbackNames[QStringLiteral("size_fdiag")].append(QStringLiteral("nwse-resize"));
197 
198  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("sb_h_double_arrow")); // DMZ-White and DMZ-Black themes
199  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("ew-resize"));
200  m_fallbackNames[QStringLiteral("size_hor")].append(QStringLiteral("h_double_arrow"));
201 
202  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("sb_v_double_arrow")); // DMZ-White and DMZ-Black themes
203  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("ns-resize"));
204  m_fallbackNames[QStringLiteral("size_ver")].append(QStringLiteral("v_double_arrow"));
205 
206  m_fallbackNames[QStringLiteral("split_h")].append(QStringLiteral("sb_h_double_arrow")); // DMZ-White and DMZ-Black themes
207  m_fallbackNames[QStringLiteral("split_h")].append(QStringLiteral("col-resize"));
208 
209  m_fallbackNames[QStringLiteral("split_v")].append(QStringLiteral("sb_v_double_arrow")); // DMZ-White and DMZ-Black themes
210  m_fallbackNames[QStringLiteral("split_v")].append(QStringLiteral("row-resize"));
211 
212  m_fallbackNames[QStringLiteral("up_arrow")].append(QStringLiteral("sb_up_arrow")); // DMZ-White and DMZ-Black themes
213 
214  m_fallbackNames[QStringLiteral("watch")].append(QStringLiteral("wait"));
215 
216  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("left_ptr_help"));
217  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("help"));
218  m_fallbackNames[QStringLiteral("whats_this")].append(QStringLiteral("question_arrow"));
219 
220  m_fallbackNames[QStringLiteral("xterm")].append(QStringLiteral("ibeam"));
221 }
222 
223 CursorImageProvider::~CursorImageProvider()
224 {
225  {
226  QList< QMap<QString, CursorImage*> > cursorList = m_cursors.values();
227 
228  for (int i = 0; i < cursorList.count(); ++i) {
229  QList<CursorImage*> cursorImageList = cursorList[i].values();
230  for (int j = 0; j < cursorImageList.count(); ++j) {
231  delete cursorImageList[j];
232  }
233  }
234  }
235 
236  m_cursors.clear();
237  m_instance = nullptr;
238 }
239 
240 QImage CursorImageProvider::requestImage(const QString &cursorThemeAndNameAndHeight, QSize *size, const QSize & /*requestedSize*/)
241 {
242  CursorImage *cursorImage = fetchCursor(cursorThemeAndNameAndHeight);
243  size->setWidth(cursorImage->qimage.width());
244  size->setHeight(cursorImage->qimage.height());
245 
246  return cursorImage->qimage;
247 }
248 
249 CursorImage *CursorImageProvider::fetchCursor(const QString &cursorThemeAndNameAndHeight)
250 {
251  QString themeName;
252  QString cursorName;
253  int cursorHeight;
254  {
255  QStringList themeAndNameList = cursorThemeAndNameAndHeight.split('/');
256  if (themeAndNameList.size() != 3) {
257  return nullptr;
258  }
259  themeName = themeAndNameList[0];
260  cursorName = themeAndNameList[1];
261 
262  bool ok;
263  cursorHeight = themeAndNameList[2].toInt(&ok);
264  if (!ok) {
265  cursorHeight = 32;
266  qWarning().nospace() << "CursorImageProvider: invalid cursor height ("<<themeAndNameList[2]<<")."
267  " Falling back to "<<cursorHeight<<" pixels";
268  }
269  }
270 
271  return fetchCursor(themeName, cursorName, cursorHeight);
272 }
273 
274 CursorImage *CursorImageProvider::fetchCursor(const QString &themeName, const QString &cursorName, int cursorHeight)
275 {
276  CursorImage *cursorImage = fetchCursorHelper(themeName, cursorName, cursorHeight);
277 
278  // Try some fallbacks
279  if (cursorImage->qimage.isNull()) {
280  if (m_fallbackNames.contains(cursorName)) {
281  const QStringList &fallbackNames = m_fallbackNames[cursorName];
282  int i = 0;
283  while (cursorImage->qimage.isNull() && i < fallbackNames.count()) {
284  qDebug().nospace() << "CursorImageProvider: "<< cursorName <<" not found, trying " << fallbackNames.at(i);
285  cursorImage = fetchCursorHelper(themeName, fallbackNames.at(i), cursorHeight);
286  ++i;
287  }
288  }
289  }
290 
291  // if it all fails, there must be at least a left_ptr
292  if (cursorImage->qimage.isNull() && cursorName != QLatin1String("left_ptr")) {
293  qDebug() << "CursorImageProvider:" << cursorName
294  << "not found (nor its fallbacks, if any). Going for \"left_ptr\" as a last resort.";
295  cursorImage = fetchCursorHelper(themeName, QStringLiteral("left_ptr"), cursorHeight);
296  }
297 
298  if (cursorImage->qimage.isNull()) {
299  // finally, go for the built-in cursor
300  qWarning() << "CursorImageProvider: couldn't find any cursors. Using the built-in one";
301  if (!m_builtInCursorImage || m_builtInCursorImage->requestedHeight != cursorHeight) {
302  m_builtInCursorImage.reset(new BuiltInCursorImage(cursorHeight));
303  }
304  cursorImage = m_builtInCursorImage.data();
305  }
306 
307  return cursorImage;
308 }
309 
310 CursorImage *CursorImageProvider::fetchCursorHelper(const QString &themeName, const QString &cursorName, int cursorHeight)
311 {
312  if (cursorName == QLatin1String("blank")) {
313  return &m_blankCursorImage;
314  } else if (cursorName.startsWith(QLatin1String("custom"))) {
315  return m_customCursorImage.data();
316  } else {
317  QMap<QString, CursorImage*> &themeCursors = m_cursors[themeName];
318 
319  if (!themeCursors.contains(cursorName)) {
320  themeCursors[cursorName] = new XCursorImage(themeName, cursorName, cursorHeight);
321  } else if (themeCursors[cursorName]->requestedHeight != cursorHeight) {
322  delete themeCursors.take(cursorName);
323  themeCursors[cursorName] = new XCursorImage(themeName, cursorName, cursorHeight);
324  }
325 
326  return themeCursors[cursorName];
327  }
328 }
329 
330 void CursorImageProvider::setCustomCursor(const QCursor &customCursor)
331 {
332  if (customCursor.pixmap().isNull()) {
333  m_customCursorImage.reset();
334  } else {
335  m_customCursorImage.reset(new CustomCursorImage(customCursor));
336  }
337 }