]> git.lyx.org Git - features.git/blob - src/frontends/qt/GuiPainter.cpp
Try to automatically handle transparent pictures in darkmode (#12076)
[features.git] / src / frontends / qt / GuiPainter.cpp
1 /**
2  * \file GuiPainter.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author John Levon
7  * \author Abdelrazak Younes
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "GuiPainter.h"
15
16 #include "ColorCache.h"
17 #include "GuiApplication.h"
18 #include "GuiFontLoader.h"
19 #include "GuiFontMetrics.h"
20 #include "GuiImage.h"
21 #include "qt_helpers.h"
22
23 #include "Font.h"
24 #include "LyXRC.h"
25
26 #include "support/debug.h"
27 #include "support/lassert.h"
28 #include "support/lyxlib.h"
29
30 #include <algorithm>
31
32 #include <QTextLayout>
33
34 using namespace std;
35 using namespace lyx::support;
36
37 namespace lyx {
38 namespace frontend {
39
40 const int Painter::thin_line = 1;
41
42 GuiPainter::GuiPainter(QPaintDevice * device, double pixel_ratio, bool devel_mode)
43         : QPainter(device), Painter(pixel_ratio, devel_mode)
44 {
45         // set cache correctly
46         current_color_ = pen().color();
47         current_ls_ = pen().style() == Qt::DotLine ? line_onoffdash : line_solid;
48         current_lw_ = pen().width();
49 }
50
51
52 GuiPainter::~GuiPainter()
53 {
54         QPainter::end();
55         //lyxerr << "GuiPainter::end()" << endl;
56 }
57
58
59 void GuiPainter::setQPainterPen(QColor const & col,
60         Painter::line_style ls, int lw)
61 {
62         if (col == current_color_ && ls == current_ls_ && lw == current_lw_)
63                 return;
64
65         current_color_ = col;
66         current_ls_ = ls;
67         current_lw_ = lw;
68
69         QPen pen = QPainter::pen();
70         pen.setColor(col);
71
72         switch (ls) {
73         case line_solid:
74         case line_solid_aliased:
75                 pen.setStyle(Qt::SolidLine); break;
76         case line_onoffdash:
77                 pen.setStyle(Qt::DotLine); break;
78         }
79
80         pen.setWidth(lw);
81
82         setPen(pen);
83 }
84
85
86 QColor GuiPainter::computeColor(Color col)
87 {
88         return filterColor(guiApp->colorCache().get(col));
89 }
90
91
92 QColor GuiPainter::filterColor(QColor const & col)
93 {
94         if (monochrome_blend_.empty())
95                 return col;
96
97         QColor const blend = monochrome_blend_.top();
98         return QColor::fromHsv(blend.hue(), blend.saturation(), qGray(col.rgb()));
99 }
100
101
102 void GuiPainter::enterMonochromeMode(Color const & blend)
103 {
104         QColor qblend = filterColor(guiApp->colorCache().get(blend));
105         monochrome_blend_.push(qblend);
106 }
107
108
109 void GuiPainter::leaveMonochromeMode()
110 {
111         LASSERT(!monochrome_blend_.empty(), return);
112         monochrome_blend_.pop();
113 }
114
115
116 void GuiPainter::point(int x, int y, Color col)
117 {
118         setQPainterPen(computeColor(col));
119         drawPoint(x, y);
120 }
121
122
123 void GuiPainter::line(int x1, int y1, int x2, int y2,
124         Color col,
125         line_style ls,
126         int lw)
127 {
128         setQPainterPen(computeColor(col), ls, lw);
129         bool const do_antialiasing = renderHints() & TextAntialiasing
130                 && x1 != x2 && y1 != y2 && ls != line_solid_aliased;
131         setRenderHint(Antialiasing, do_antialiasing);
132         drawLine(x1, y1, x2, y2);
133         setRenderHint(Antialiasing, false);
134 }
135
136
137 void GuiPainter::lines(int const * xp, int const * yp, int np,
138         Color col,
139         fill_style fs,
140         line_style ls,
141         int lw)
142 {
143         // double the size if needed
144         // FIXME THREAD
145         static QVector<QPoint> points(32);
146         if (np > points.size())
147                 points.resize(2 * np);
148
149         // Note: the proper way to not get blurry vertical and horizontal lines is
150         // to add 0.5 to all coordinates.
151         bool antialias = false;
152         for (int i = 0; i < np; ++i) {
153                 points[i].setX(xp[i]);
154                 points[i].setY(yp[i]);
155                 if (i != 0)
156                         antialias |= xp[i-1] != xp[i] && yp[i-1] != yp[i];
157         }
158         QColor const color = computeColor(col);
159         setQPainterPen(color, ls, lw);
160         bool const text_is_antialiased = renderHints() & TextAntialiasing;
161         setRenderHint(Antialiasing,
162                       antialias && text_is_antialiased && ls != line_solid_aliased);
163         if (fs == fill_none) {
164                 drawPolyline(points.data(), np);
165         } else {
166                 QBrush const oldbrush = brush();
167                 setBrush(QBrush(color));
168                 drawPolygon(points.data(), np, fs == fill_oddeven ?
169                             Qt::OddEvenFill : Qt::WindingFill);
170                 setBrush(oldbrush);
171         }
172         setRenderHint(Antialiasing, false);
173 }
174
175
176 void GuiPainter::path(int const * xp, int const * yp,
177         int const * c1x, int const * c1y,
178         int const * c2x, int const * c2y,
179         int np,
180         Color col,
181         fill_style fs,
182         line_style ls,
183         int lw)
184 {
185         QPainterPath bpath;
186         // This is the starting point, so its control points are meaningless
187         bpath.moveTo(xp[0], yp[0]);
188
189         for (int i = 1; i < np; ++i) {
190                 bool line = c1x[i] == xp[i - 1] && c1y[i] == yp[i - 1] &&
191                             c2x[i] == xp[i] && c2y[i] == yp[i];
192                 if (line)
193                         bpath.lineTo(xp[i], yp[i]);
194                 else
195                         bpath.cubicTo(c1x[i], c1y[i],  c2x[i], c2y[i], xp[i], yp[i]);
196         }
197         QColor const color = computeColor(col);
198         setQPainterPen(color, ls, lw);
199         bool const text_is_antialiased = renderHints() & TextAntialiasing;
200         setRenderHint(Antialiasing, text_is_antialiased && ls != line_solid_aliased);
201         drawPath(bpath);
202         if (fs != fill_none)
203                 fillPath(bpath, QBrush(color));
204         setRenderHint(Antialiasing, false);
205 }
206
207
208 void GuiPainter::rectangle(int x, int y, int w, int h,
209         Color col,
210         line_style ls,
211         int lw)
212 {
213         setQPainterPen(computeColor(col), ls, lw);
214         drawRect(x, y, w, h);
215 }
216
217
218 void GuiPainter::fillRectangle(int x, int y, int w, int h, Color col)
219 {
220         fillRect(x, y, w, h, guiApp->colorCache().get(col));
221 }
222
223
224 void GuiPainter::arc(int x, int y, unsigned int w, unsigned int h,
225         int a1, int a2, Color col)
226 {
227         // LyX usings 1/64ths degree, Qt usings 1/16th
228         setQPainterPen(computeColor(col));
229         bool const do_antialiasing = renderHints() & TextAntialiasing;
230         setRenderHint(Antialiasing, do_antialiasing);
231         drawArc(x, y, w, h, a1 / 4, a2 / 4);
232         setRenderHint(Antialiasing, false);
233 }
234
235
236 void GuiPainter::image(int x, int y, int w, int h, graphics::Image const & i,
237                        bool revert_in_darkmode)
238 {
239         graphics::GuiImage const & qlimage =
240                 static_cast<graphics::GuiImage const &>(i);
241
242         fillRectangle(x, y, w, h, Color_graphicsbg);
243
244         QImage image = qlimage.image();
245         
246         QPalette palette = QPalette();
247         QColor text_color = palette.color(QPalette::Active, QPalette::WindowText);
248         QColor bg_color = palette.color(QPalette::Active, QPalette::Window);
249         // guess whether we are in dark mode
250         bool const in_dark_mode = text_color.black() < bg_color.black();
251         // if we are in dark mode, check whether we have transparent pixels
252         if (in_dark_mode && !revert_in_darkmode) {
253                 QImage img = image.convertToFormat(QImage::Format_ARGB32);
254                 for (int x = 0 ; x < img.width() ; x++) {
255                         if (revert_in_darkmode)
256                                 break;
257                         for (int y = 0 ; y < img.height() ; y++) {
258                                 QRgb currentPixel = (img.pixel(x, y));
259                                 if (qAlpha(currentPixel) == 0) {
260                                         // we have transparent pixels, revert
261                                         // this image in dark mode (#12076)
262                                         revert_in_darkmode = true;
263                                         break;
264                                 }
265                         }
266                 }
267         }
268         if (in_dark_mode && revert_in_darkmode)
269                 // FIXME this is only a cheap approximation
270                 // Ideally, replace colors as in GuiApplication::prepareForDarkmode()
271                 image.invertPixels();
272
273         QRectF const drect = QRectF(x, y, w, h);
274         QRectF const srect = QRectF(0, 0, image.width(), image.height());
275         // Bilinear filtering is needed on a rare occasion for instant previews when
276         // the user's configuration mixes low-dpi and high-dpi monitors (#10114).
277         // This filter is optimised by qt on pixel-aligned images, so this does not
278         // affect performances in other cases.
279         setRenderHint(SmoothPixmapTransform);
280         drawImage(drect, image, srect);
281         setRenderHint(SmoothPixmapTransform, false);
282 }
283
284
285 void GuiPainter::text(int x, int y, char_type c, FontInfo const & f)
286 {
287         text(x, y, docstring(1, c), f);
288 }
289
290
291 void GuiPainter::text(int x, int y, docstring const & s, FontInfo const & f)
292 {
293         text(x, y, s, f, Auto, 0.0, 0.0);
294 }
295
296
297 void GuiPainter::text(int x, int y, docstring const & s,
298                       FontInfo const & f, Direction const dir,
299                       double const wordspacing, double const tw)
300 {
301         //LYXERR0("text: x=" << x << ", s=" << s);
302         if (s.empty())
303                 return;
304
305         /* Caution: The following ucs4 to QString conversions work for symbol fonts
306         only because they are no real conversions but simple casts in reality.
307         When we want to draw a symbol or calculate the metrics we pass the position
308         of the symbol in the font (as given in lib/symbols) as a char_type to the
309         frontend. This is just wrong, because the symbol is no UCS4 character at
310         all. You can think of this number as the code point of the symbol in a
311         custom symbol encoding. It works because this char_type is later on again
312         interpreted as a position in the font.
313         The correct solution would be to have extra functions for symbols, but that
314         would require to duplicate a lot of frontend and mathed support code.
315         */
316         QString str = toqstr(s);
317
318 #if 0
319         // HACK: QT3 refuses to show single compose characters
320         //       Still needed with Qt4?
321         if (ls == 1 && str[0].unicode() >= 0x05b0 && str[0].unicode() <= 0x05c2)
322                 str = ' ' + str;
323 #endif
324
325         QFont ff = getFont(f);
326         ff.setWordSpacing(wordspacing);
327         GuiFontMetrics const & fm = getFontMetrics(f);
328
329         int textwidth = 0;
330         if (tw == 0.0)
331                 // Take into account space stretching (word spacing)
332                 textwidth = fm.width(s) +
333                         static_cast<int>(fm.countExpanders(s) * wordspacing);
334         else
335                 textwidth = static_cast<int>(tw);
336
337         textDecoration(f, x, y, textwidth);
338
339         setQPainterPen(computeColor(f.realColor()));
340         if (dir != Auto) {
341                 auto ptl = fm.getTextLayout(s, dir == RtL, wordspacing);
342                 QTextLine const & tline = ptl->lineForTextPosition(0);
343                 ptl->draw(this, QPointF(x, y - tline.ascent()));
344         } else {
345                 if (font() != ff)
346                         setFont(ff);
347                 drawText(x, y, str);
348         }
349         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
350         //      << " at " << x << "," << y);
351 }
352
353
354 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
355                       double const wordspacing, double const tw)
356 {
357         text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft() ? RtL : LtR,
358              wordspacing, tw);
359 }
360
361
362 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
363                       Color other, size_type const from, size_type const to,
364                       double const wordspacing, double const tw)
365 {
366         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
367         FontInfo fi = f.fontInfo();
368         Direction const dir = f.isVisibleRightToLeft() ? RtL : LtR;
369
370         // dimensions
371         int const ascent = fm.maxAscent();
372         int const height = fm.maxAscent() + fm.maxDescent();
373         int xmin = fm.pos2x(str, from, dir == RtL, wordspacing);
374         int xmax = fm.pos2x(str, to, dir == RtL, wordspacing);
375         // Avoid this case, since it would make the `other' text spill in some cases
376         if (xmin == xmax) {
377                 text(x, y, str, fi, dir, wordspacing, tw);
378                 return;
379         } else if (xmin > xmax)
380                 swap(xmin, xmax);
381
382         // First the part in other color
383         Color const orig = fi.realColor();
384         fi.setPaintColor(other);
385         QRegion const clip(x + xmin, y - ascent, xmax - xmin, height);
386         setClipRegion(clip);
387         text(x, y, str, fi, dir, wordspacing, tw);
388
389         // Then the part in normal color
390         // Note that in Qt5, it is not possible to use Qt::UniteClip,
391         // therefore QRegion is used.
392         fi.setPaintColor(orig);
393         QRegion region(viewport());
394         setClipRegion(region - clip);
395         text(x, y, str, fi, dir, wordspacing, tw);
396         setClipping(false);
397 }
398
399
400 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
401 {
402         if (f.underbar() == FONT_ON)
403                 underline(f, x, y, width);
404         if (f.strikeout() == FONT_ON)
405                 strikeoutLine(f, x, y, width);
406         if (f.xout() == FONT_ON)
407                 crossoutLines(f, x, y, width);
408         if (f.uuline() == FONT_ON)
409                 doubleUnderline(f, x, y, width);
410         if (f.uwave() == FONT_ON)
411                 // f.color() doesn't work on some circumstances
412                 wavyHorizontalLine(x, y, width,  f.realColor().baseColor);
413 }
414
415
416 static int max(int a, int b) { return a > b ? a : b; }
417
418
419 void GuiPainter::rectText(int x, int y, docstring const & str,
420         FontInfo const & font, Color back, Color frame)
421 {
422         int width, ascent, descent;
423
424         FontMetrics const & fm = theFontMetrics(font);
425         fm.rectText(str, width, ascent, descent);
426
427         if (back != Color_none)
428                 fillRectangle(x + 1, y - ascent + 1, width - 1,
429                               ascent + descent - 1, back);
430
431         if (frame != Color_none)
432                 rectangle(x, y - ascent, width, ascent + descent, frame);
433
434         // FIXME: let offset depend on font
435         text(x + 3, y, str, font);
436 }
437
438
439 void GuiPainter::buttonText(int x, int baseline, docstring const & s,
440         FontInfo const & font, Color back, Color frame, int offset)
441 {
442         int width, ascent, descent;
443
444         FontMetrics const & fm = theFontMetrics(font);
445         fm.buttonText(s, offset, width, ascent, descent);
446
447         int const d = offset / 2;
448
449         fillRectangle(x + d + 1, baseline - ascent + 1, width - offset - 1,
450                               ascent + descent - 1, back);
451         rectangle(x + d, baseline - ascent, width - offset, ascent + descent, frame);
452         text(x + offset, baseline, s, font);
453 }
454
455
456 int GuiPainter::preeditText(int x, int y, char_type c,
457         FontInfo const & font, preedit_style style)
458 {
459         FontInfo temp_font = font;
460         FontMetrics const & fm = theFontMetrics(font);
461         int ascent = fm.maxAscent();
462         int descent = fm.maxDescent();
463         int height = ascent + descent;
464         int width = fm.width(c);
465
466         switch (style) {
467                 case preedit_default:
468                         // default unselecting mode.
469                         fillRectangle(x, y - height + 1, width, height, Color_background);
470                         dashedUnderline(font, x, y - descent + 1, width);
471                         break;
472                 case preedit_selecting:
473                         // We are in selecting mode: white text on black background.
474                         fillRectangle(x, y - height + 1, width, height, Color_black);
475                         temp_font.setColor(Color_white);
476                         break;
477                 case preedit_cursor:
478                         // The character comes with a cursor.
479                         fillRectangle(x, y - height + 1, width, height, Color_background);
480                         underline(font, x, y - descent + 1, width);
481                         break;
482         }
483         text(x, y - descent + 1, c, temp_font);
484
485         return width;
486 }
487
488
489 void GuiPainter::underline(FontInfo const & f, int x, int y, int width,
490                            line_style ls)
491 {
492         FontMetrics const & fm = theFontMetrics(f);
493         int const pos = fm.underlinePos();
494
495         line(x, y + pos, x + width, y + pos,
496              f.realColor(), ls, fm.lineWidth());
497 }
498
499
500 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
501 {
502         FontMetrics const & fm = theFontMetrics(f);
503         int const pos = fm.strikeoutPos();
504
505         line(x, y - pos, x + width, y - pos,
506              f.realColor(), line_solid, fm.lineWidth());
507 }
508
509
510 void GuiPainter::crossoutLines(FontInfo const & f, int x, int y, int width)
511 {
512         FontInfo tmpf = f;
513         tmpf.setXout(FONT_OFF);
514
515         // the definition of \xout in ulem.sty is
516     //  \def\xout{\bgroup \markoverwith{\hbox to.35em{\hss/\hss}}\ULon}
517         // Let's mimic it somewhat.
518         double offset = max(0.35 * theFontMetrics(tmpf).em(), 1);
519         for (int i = 0 ; i < iround(width / offset) ; ++i)
520                 text(x + iround(i * offset), y, '/', tmpf);
521 }
522
523
524 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
525 {
526         FontMetrics const & fm = theFontMetrics(f);
527         int const pos1 = fm.underlinePos() + fm.lineWidth();
528         int const pos2 = fm.underlinePos() - fm.lineWidth() + 1;
529
530         line(x, y + pos1, x + width, y + pos1,
531                  f.realColor(), line_solid, fm.lineWidth());
532         line(x, y + pos2, x + width, y + pos2,
533                  f.realColor(), line_solid, fm.lineWidth());
534 }
535
536
537 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
538 {
539         FontMetrics const & fm = theFontMetrics(f);
540
541         int const below = max(fm.maxDescent() / 2, 2);
542         int height = max((fm.maxDescent() / 4) - 1, 1);
543
544         if (height >= 2)
545                 height += below;
546
547         for (int n = 0; n != height; ++n)
548                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
549 }
550
551
552 void GuiPainter::wavyHorizontalLine(int x, int y, int width, ColorCode col)
553 {
554         setQPainterPen(computeColor(col));
555         int const step = 2;
556         int const xend = x + width;
557         int height = 1;
558         //FIXME: I am not sure if Antialiasing gives the best effect.
559         //setRenderHint(Antialiasing, true);
560         while (x < xend) {
561                 height = - height;
562                 drawLine(x, y - height, x + step, y + height);
563                 x += step;
564                 drawLine(x, y + height, x + step/2, y + height);
565                 x += step/2;
566         }
567         //setRenderHint(Antialiasing, false);
568 }
569
570 } // namespace frontend
571 } // namespace lyx