1 #include "widget/wpixmapstore.h"
2
3 #include <QDir>
4 #include <QString>
5 #include <QtDebug>
6
7 #include "skin/legacy/imgloader.h"
8
9 #include "util/math.h"
10 #include "util/memory.h"
11 #include "util/painterscope.h"
12
13 // static
DrawModeFromString(const QString & str)14 Paintable::DrawMode Paintable::DrawModeFromString(const QString& str) {
15 if (str.compare("FIXED", Qt::CaseInsensitive) == 0) {
16 return FIXED;
17 } else if (str.compare("STRETCH", Qt::CaseInsensitive) == 0) {
18 return STRETCH;
19 } else if (str.compare("STRETCH_ASPECT", Qt::CaseInsensitive) == 0) {
20 return STRETCH_ASPECT;
21 } else if (str.compare("TILE", Qt::CaseInsensitive) == 0) {
22 return TILE;
23 }
24
25 // Fall back on the implicit default from before Mixxx supported draw modes.
26 qWarning() << "Unknown DrawMode string in DrawModeFromString:"
27 << str << "using FIXED";
28 return FIXED;
29 }
30
31 // static
DrawModeToString(DrawMode mode)32 QString Paintable::DrawModeToString(DrawMode mode) {
33 switch (mode) {
34 case FIXED:
35 return "FIXED";
36 case STRETCH:
37 return "STRETCH";
38 case STRETCH_ASPECT:
39 return "STRETCH_ASPECT";
40 case TILE:
41 return "TILE";
42 }
43 // Fall back on the implicit default from before Mixxx supported draw modes.
44 qWarning() << "Unknown DrawMode in DrawModeToString " << mode
45 << "using FIXED";
46 return "FIXED";
47 }
48
Paintable(const PixmapSource & source,DrawMode mode,double scaleFactor)49 Paintable::Paintable(const PixmapSource& source, DrawMode mode, double scaleFactor)
50 : m_drawMode(mode),
51 m_source(source) {
52 if (!source.isSVG()) {
53 m_pPixmap.reset(WPixmapStore::getPixmapNoCache(source.getPath(), scaleFactor));
54 } else {
55 auto pSvg = std::make_unique<QSvgRenderer>();
56 if (!source.getSvgSourceData().isEmpty()) {
57 // Call here the different overload for svg content
58 if (!pSvg->load(source.getSvgSourceData())) {
59 // The above line already logs a warning
60 return;
61 }
62 } else if (!source.getPath().isEmpty()) {
63 if (!pSvg->load(source.getPath())) {
64 // The above line already logs a warning
65 return;
66 }
67 } else {
68 return;
69 }
70 m_pSvg.reset(pSvg.release());
71 #ifdef __APPLE__
72 // Apple does Retina scaling behind the scenes, so we also pass a
73 // Paintable::FIXED image. On the other targets, it is better to
74 // cache the pixmap. We do not do this for TILE and color schemas.
75 // which can result in a correct but possibly blurry picture at a
76 // Retina display. This can be fixed when switching to QT5
77 if (mode == TILE || WPixmapStore::willCorrectColors()) {
78 #else
79 if (mode == TILE || mode == Paintable::FIXED || WPixmapStore::willCorrectColors()) {
80 #endif
81 // The SVG renderer doesn't directly support tiling, so we render
82 // it to a pixmap which will then get tiled.
83 QImage copy_buffer(m_pSvg->defaultSize() * scaleFactor, QImage::Format_ARGB32);
84 copy_buffer.fill(0x00000000); // Transparent black.
85 QPainter painter(©_buffer);
86 m_pSvg->render(&painter);
87 WPixmapStore::correctImageColors(©_buffer);
88
89 m_pPixmap.reset(new QPixmap(copy_buffer.size()));
90 m_pPixmap->convertFromImage(copy_buffer);
91 }
92 }
93 }
94
95 bool Paintable::isNull() const {
96 return m_source.isEmpty();
97 }
98
99 QSize Paintable::size() const {
100 if (!m_pPixmap.isNull()) {
101 return m_pPixmap->size();
102 } else if (!m_pSvg.isNull()) {
103 return m_pSvg->defaultSize();
104 }
105 return QSize();
106 }
107
108 int Paintable::width() const {
109 if (!m_pPixmap.isNull()) {
110 return m_pPixmap->width();
111 } else if (!m_pSvg.isNull()) {
112 QSize size = m_pSvg->defaultSize();
113 return size.width();
114 }
115 return 0;
116 }
117
118 int Paintable::height() const {
119 if (!m_pPixmap.isNull()) {
120 return m_pPixmap->height();
121 } else if (!m_pSvg.isNull()) {
122 QSize size = m_pSvg->defaultSize();
123 return size.height();
124 }
125 return 0;
126 }
127
128 QRectF Paintable::rect() const {
129 if (!m_pPixmap.isNull()) {
130 return m_pPixmap->rect();
131 } else if (!m_pSvg.isNull()) {
132 return QRectF(QPointF(0, 0), m_pSvg->defaultSize());
133 }
134 return QRectF();
135 }
136
137 void Paintable::draw(const QRectF& targetRect, QPainter* pPainter) {
138 // The sourceRect is implicitly the entire Paintable.
139 draw(targetRect, pPainter, rect());
140 }
141
142 void Paintable::draw(int x, int y, QPainter* pPainter) {
143 QRectF sourceRect(rect());
144 QRectF targetRect(QPointF(x, y), sourceRect.size());
145 draw(targetRect, pPainter, sourceRect);
146 }
147
148 void Paintable::draw(const QRectF& targetRect, QPainter* pPainter,
149 const QRectF& sourceRect) {
150 if (!targetRect.isValid() || !sourceRect.isValid() || isNull()) {
151 return;
152 }
153
154 switch (m_drawMode) {
155 case FIXED: {
156 // Only render the minimum overlapping rectangle between the source
157 // and target.
158 QSizeF fixedSize(math_min(sourceRect.width(), targetRect.width()),
159 math_min(sourceRect.height(), targetRect.height()));
160 QRectF adjustedTarget(targetRect.topLeft(), fixedSize);
161 QRectF adjustedSource(sourceRect.topLeft(), fixedSize);
162 drawInternal(adjustedTarget, pPainter, adjustedSource);
163 break;
164 }
165 case STRETCH_ASPECT: {
166 qreal sx = targetRect.width() / sourceRect.width();
167 qreal sy = targetRect.height() / sourceRect.height();
168
169 // Adjust the scale so that the scaling in both axes is equal.
170 if (sx != sy) {
171 qreal scale = math_min(sx, sy);
172 QRectF adjustedTarget(targetRect.x(),
173 targetRect.y(),
174 scale * sourceRect.width(),
175 scale * sourceRect.height());
176 drawInternal(adjustedTarget, pPainter, sourceRect);
177 } else {
178 drawInternal(targetRect, pPainter, sourceRect);
179 }
180 break;
181 }
182 case STRETCH:
183 drawInternal(targetRect, pPainter, sourceRect);
184 break;
185 case TILE:
186 drawInternal(targetRect, pPainter, sourceRect);
187 break;
188 }
189 }
190
191 void Paintable::drawCentered(const QRectF& targetRect, QPainter* pPainter,
192 const QRectF& sourceRect) {
193 switch (m_drawMode) {
194 case FIXED: {
195 // Only render the minimum overlapping rectangle between the source
196 // and target.
197 QSizeF fixedSize(math_min(sourceRect.width(), targetRect.width()),
198 math_min(sourceRect.height(), targetRect.height()));
199
200 QRectF adjustedSource(sourceRect.topLeft(), fixedSize);
201 QRectF adjustedTarget(QPointF(-adjustedSource.width() / 2.0,
202 -adjustedSource.height() / 2.0),
203 fixedSize);
204 drawInternal(adjustedTarget, pPainter, adjustedSource);
205 break;
206 }
207 case STRETCH_ASPECT: {
208 qreal sx = targetRect.width() / sourceRect.width();
209 qreal sy = targetRect.height() / sourceRect.height();
210
211 // Adjust the scale so that the scaling in both axes is equal.
212 if (sx != sy) {
213 qreal scale = math_min(sx, sy);
214 qreal scaledWidth = scale * sourceRect.width();
215 qreal scaledHeight = scale * sourceRect.height();
216 QRectF adjustedTarget(-scaledWidth / 2.0, -scaledHeight / 2.0,
217 scaledWidth, scaledHeight);
218 drawInternal(adjustedTarget, pPainter, sourceRect);
219 } else {
220 drawInternal(targetRect, pPainter, sourceRect);
221 }
222 break;
223 }
224 case STRETCH:
225 drawInternal(targetRect, pPainter, sourceRect);
226 break;
227 case TILE:
228 // TODO(XXX): What's the right behavior here? Draw the first tile at the
229 // center point and then tile all around it based on that?
230 drawInternal(targetRect, pPainter, sourceRect);
231 break;
232 }
233 }
234
235 void Paintable::drawInternal(const QRectF& targetRect, QPainter* pPainter,
236 const QRectF& sourceRect) {
237 // qDebug() << "Paintable::drawInternal" << DrawModeToString(m_draw_mode)
238 // << targetRect << sourceRect;
239 if (m_pPixmap) {
240 if (m_drawMode == TILE) {
241 // TODO(rryan): Using a source rectangle doesn't make much sense
242 // with tiling. Ignore the source rect and tile our natural size
243 // across the target rect. What's the right general behavior here?
244 // NOTE(rryan): We round our target/source rectangles to the nearest
245 // pixel for raster images.
246 pPainter->drawTiledPixmap(targetRect.toRect(), *m_pPixmap, QPoint(0,0));
247 } else {
248 // NOTE(rryan): We round our target/source rectangles to the nearest
249 // pixel for raster images.
250 pPainter->drawPixmap(targetRect.toRect(), *m_pPixmap,
251 sourceRect.toRect());
252 }
253 } else if (m_pSvg) {
254 if (m_drawMode == TILE) {
255 qWarning() << "Tiled SVG should have been rendered to pixmap!";
256 } else {
257 // NOTE(rryan): QSvgRenderer render does not clip for us -- it
258 // applies a world transformation using viewBox and renders the
259 // entire SVG to the painter. We save/restore the QPainter in case
260 // there is an existing clip region (I don't know of any Mixxx code
261 // that uses one but we may in the future).
262 PainterScope PainterScope(pPainter);
263 pPainter->setClipping(true);
264 pPainter->setClipRect(targetRect);
265 m_pSvg->setViewBox(sourceRect);
266 m_pSvg->render(pPainter, targetRect);
267 }
268 }
269 }
270
271 // static
272 QString Paintable::getAltFileName(const QString& fileName) {
273 // Detect if the alternate image file exists and, if it does,
274 // return its path instead
275 QStringList temp = fileName.split('.');
276 if (temp.length() != 2) {
277 return fileName;
278 }
279
280 QString newFileName = temp[0] + QLatin1String("@2x.") + temp[1];
281 QFile file(newFileName);
282 if (QFileInfo(file).exists()) {
283 return newFileName;
284 } else {
285 return fileName;
286 }
287 }
288