]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiPainter.cpp
Make code a bit easier to read
[lyx.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 #include "support/lstrings.h"
30
31 #include <algorithm>
32
33 #include <QTextLayout>
34
35 using namespace std;
36 using namespace lyx::support;
37
38 namespace lyx {
39 namespace frontend {
40
41 const int Painter::thin_line = 1;
42
43 GuiPainter::GuiPainter(QPaintDevice * device, double pixel_ratio, bool devel_mode)
44         : QPainter(device), Painter(pixel_ratio, devel_mode)
45 {
46         // set cache correctly
47         current_color_ = pen().color();
48         current_ls_ = pen().style() == Qt::DotLine ? line_onoffdash : line_solid;
49         current_lw_ = pen().width();
50 }
51
52
53 GuiPainter::~GuiPainter()
54 {
55         QPainter::end();
56         //lyxerr << "GuiPainter::end()" << endl;
57 }
58
59
60 void GuiPainter::setQPainterPen(QColor const & col,
61         Painter::line_style ls, int lw, Qt::PenJoinStyle js)
62 {
63         if (col == current_color_ && ls == current_ls_ && lw == current_lw_)
64                 return;
65
66         current_color_ = col;
67         current_ls_ = ls;
68         current_lw_ = lw;
69
70         QPen pen = QPainter::pen();
71         pen.setColor(col);
72
73         switch (ls) {
74         case line_solid:
75         case line_solid_aliased:
76                 pen.setStyle(Qt::SolidLine); break;
77         case line_onoffdash:
78                 pen.setStyle(Qt::DotLine); break;
79         }
80
81         pen.setWidth(lw);
82
83         pen.setJoinStyle(js);
84
85         setPen(pen);
86 }
87
88
89 QColor GuiPainter::computeColor(Color col)
90 {
91         return filterColor(guiApp->colorCache().get(col));
92 }
93
94
95 QColor GuiPainter::filterColor(QColor const & col)
96 {
97         if (monochrome_blend_.empty())
98                 return col;
99
100         QColor const blend = monochrome_blend_.top();
101         return QColor::fromHsv(blend.hue(), blend.saturation(), qGray(col.rgb()));
102 }
103
104
105 void GuiPainter::enterMonochromeMode(Color const & blend)
106 {
107         QColor qblend = filterColor(guiApp->colorCache().get(blend));
108         monochrome_blend_.push(qblend);
109 }
110
111
112 void GuiPainter::leaveMonochromeMode()
113 {
114         LASSERT(!monochrome_blend_.empty(), return);
115         monochrome_blend_.pop();
116 }
117
118
119 void GuiPainter::point(int x, int y, Color col)
120 {
121         setQPainterPen(computeColor(col));
122         drawPoint(x, y);
123 }
124
125
126 void GuiPainter::line(int x1, int y1, int x2, int y2,
127         Color col,
128         line_style ls,
129         int lw)
130 {
131         setQPainterPen(computeColor(col), ls, lw);
132         bool const do_antialiasing = renderHints() & TextAntialiasing
133                 && x1 != x2 && y1 != y2 && ls != line_solid_aliased;
134         setRenderHint(Antialiasing, do_antialiasing);
135         drawLine(x1, y1, x2, y2);
136         setRenderHint(Antialiasing, false);
137 }
138
139
140 void GuiPainter::lines(int const * xp, int const * yp, int np,
141         Color col,
142         fill_style fs,
143         line_style ls,
144         int lw)
145 {
146         // double the size if needed
147         // FIXME THREAD
148         static QVector<QPoint> points(32);
149         if (np > points.size())
150                 points.resize(2 * np);
151
152         // Note: the proper way to not get blurry vertical and horizontal lines is
153         // to add 0.5 to all coordinates.
154         bool antialias = false;
155         for (int i = 0; i < np; ++i) {
156                 points[i].setX(xp[i]);
157                 points[i].setY(yp[i]);
158                 if (i != 0)
159                         antialias |= xp[i-1] != xp[i] && yp[i-1] != yp[i];
160         }
161         QColor const color = computeColor(col);
162         setQPainterPen(color, ls, lw);
163         bool const text_is_antialiased = renderHints() & TextAntialiasing;
164         setRenderHint(Antialiasing,
165                       antialias && text_is_antialiased && ls != line_solid_aliased);
166         if (fs == fill_none) {
167                 drawPolyline(points.data(), np);
168         } else {
169                 QBrush const oldbrush = brush();
170                 setBrush(QBrush(color));
171                 drawPolygon(points.data(), np, fs == fill_oddeven ?
172                             Qt::OddEvenFill : Qt::WindingFill);
173                 setBrush(oldbrush);
174         }
175         setRenderHint(Antialiasing, false);
176 }
177
178
179 void GuiPainter::path(int const * xp, int const * yp,
180         int const * c1x, int const * c1y,
181         int const * c2x, int const * c2y,
182         int np,
183         Color col,
184         fill_style fs,
185         line_style ls,
186         int lw)
187 {
188         QPainterPath bpath;
189         // This is the starting point, so its control points are meaningless
190         bpath.moveTo(xp[0], yp[0]);
191
192         for (int i = 1; i < np; ++i) {
193                 bool line = c1x[i] == xp[i - 1] && c1y[i] == yp[i - 1] &&
194                             c2x[i] == xp[i] && c2y[i] == yp[i];
195                 if (line)
196                         bpath.lineTo(xp[i], yp[i]);
197                 else
198                         bpath.cubicTo(c1x[i], c1y[i],  c2x[i], c2y[i], xp[i], yp[i]);
199         }
200         QColor const color = computeColor(col);
201         setQPainterPen(color, ls, lw);
202         bool const text_is_antialiased = renderHints() & TextAntialiasing;
203         setRenderHint(Antialiasing, text_is_antialiased && ls != line_solid_aliased);
204         drawPath(bpath);
205         if (fs != fill_none)
206                 fillPath(bpath, QBrush(color));
207         setRenderHint(Antialiasing, false);
208 }
209
210
211 void GuiPainter::rectangle(int x, int y, int w, int h,
212         Color col,
213         line_style ls,
214         int lw)
215 {
216         setQPainterPen(computeColor(col), ls, lw, Qt::MiterJoin);
217         drawRect(x, y, w, h);
218 }
219
220
221 void GuiPainter::fillRectangle(int x, int y, int w, int h, Color col)
222 {
223         fillRect(x, y, w, h, guiApp->colorCache().get(col));
224 }
225
226
227 void GuiPainter::arc(int x, int y, unsigned int w, unsigned int h,
228         int a1, int a2, Color col)
229 {
230         // LyX usings 1/64ths degree, Qt usings 1/16th
231         setQPainterPen(computeColor(col));
232         bool const do_antialiasing = renderHints() & TextAntialiasing;
233         setRenderHint(Antialiasing, do_antialiasing);
234         drawArc(x, y, w, h, a1 / 4, a2 / 4);
235         setRenderHint(Antialiasing, false);
236 }
237
238
239 void GuiPainter::ellipse(double x, double y, double rx, double ry,
240         Color col, fill_style fs, line_style ls, int lw)
241 {
242         QColor const color = computeColor(col);
243         setQPainterPen(color, ls, lw);
244         bool const do_antialiasing = renderHints() & TextAntialiasing;
245         setRenderHint(Antialiasing, do_antialiasing);
246         if (fs == fill_none) {
247                 drawEllipse(QPointF(x, y), rx, ry);
248         } else {
249                 QBrush const oldbrush = brush();
250                 setBrush(QBrush(color));
251                 drawEllipse(QPointF(x, y), rx, ry);
252                 setBrush(oldbrush);
253         }
254         setRenderHint(Antialiasing, false);
255 }
256
257
258 void GuiPainter::image(int x, int y, int w, int h, graphics::Image const & i,
259                        bool const revert_in_darkmode)
260 {
261         graphics::GuiImage const & qlimage =
262                 static_cast<graphics::GuiImage const &>(i);
263
264         fillRectangle(x, y, w, h, Color_graphicsbg);
265
266         QImage image = qlimage.image();
267
268         if (revert_in_darkmode && guiApp && guiApp->colorCache().isDarkMode())
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         QFont ff = getFont(f);
319         ff.setWordSpacing(wordspacing);
320         GuiFontMetrics const & fm = getFontMetrics(f);
321
322         int textwidth = 0;
323         if (tw == 0.0)
324                 // Take into account space stretching (word spacing)
325                 textwidth = fm.width(s) +
326                         static_cast<int>(countExpanders(s) * wordspacing);
327         else
328                 textwidth = static_cast<int>(tw);
329
330         textDecoration(f, x, y, textwidth);
331
332         setQPainterPen(computeColor(f.realColor()));
333         if (dir != Auto) {
334                 auto ptl = fm.getTextLayout(s, dir == RtL, wordspacing);
335                 QTextLine const & tline = ptl->lineForTextPosition(0);
336                 ptl->draw(this, QPointF(x, y - tline.ascent()));
337         } else {
338                 if (font() != ff)
339                         setFont(ff);
340                 drawText(x, y, str);
341         }
342         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
343         //      << " at " << x << "," << y);
344 }
345
346
347 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
348                       double const wordspacing, double const tw)
349 {
350         text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft() ? RtL : LtR,
351              wordspacing, tw);
352 }
353
354
355 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
356                       Color other, size_type const from, size_type const to,
357                       double const wordspacing, double const tw)
358 {
359         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
360         FontInfo fi = f.fontInfo();
361         Direction const dir = f.isVisibleRightToLeft() ? RtL : LtR;
362
363         // dimensions
364         int const ascent = fm.maxAscent();
365         int const height = fm.maxAscent() + fm.maxDescent();
366         int xmin = fm.pos2x(str, from, dir == RtL, wordspacing);
367         int xmax = fm.pos2x(str, to, dir == RtL, wordspacing);
368         // Avoid this case, since it would make the `other' text spill in some cases
369         if (xmin == xmax) {
370                 text(x, y, str, fi, dir, wordspacing, tw);
371                 return;
372         } else if (xmin > xmax)
373                 swap(xmin, xmax);
374
375         // First the part in other color
376         Color const orig = fi.realColor();
377         fi.setPaintColor(other);
378         QRegion const clip(x + xmin, y - ascent, xmax - xmin, height);
379         setClipRegion(clip);
380         text(x, y, str, fi, dir, wordspacing, tw);
381
382         // Then the part in normal color
383         // Note that in Qt5, it is not possible to use Qt::UniteClip,
384         // therefore QRegion is used.
385         fi.setPaintColor(orig);
386         QRegion region(viewport());
387         setClipRegion(region - clip);
388         text(x, y, str, fi, dir, wordspacing, tw);
389         setClipping(false);
390 }
391
392
393 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
394 {
395         if (f.underbar() == FONT_ON)
396                 underline(f, x, y, width);
397         if (f.strikeout() == FONT_ON)
398                 strikeoutLine(f, x, y, width);
399         if (f.xout() == FONT_ON)
400                 crossoutLines(f, x, y, width);
401         if (f.uuline() == FONT_ON)
402                 doubleUnderline(f, x, y, width);
403         if (f.uwave() == FONT_ON)
404                 // f.color() doesn't work on some circumstances
405                 wavyHorizontalLine(f, x, y, width,  f.realColor().baseColor);
406 }
407
408
409 static int max(int a, int b) { return a > b ? a : b; }
410
411
412 void GuiPainter::rectText(int x, int y, docstring const & str,
413         FontInfo const & font, Color back, Color frame)
414 {
415         int width, ascent, descent;
416
417         FontMetrics const & fm = theFontMetrics(font);
418         fm.rectText(str, width, ascent, descent);
419
420         if (back != Color_none)
421                 fillRectangle(x + 1, y - ascent + 1, width - 1,
422                               ascent + descent - 1, back);
423
424         if (frame != Color_none)
425                 rectangle(x, y - ascent, width, ascent + descent, frame);
426
427         // FIXME: let offset depend on font
428         text(x + 3, y, str, font);
429 }
430
431
432 void GuiPainter::buttonText(int x, int baseline, docstring const & s,
433         FontInfo const & font, Color back, Color frame, int offset)
434 {
435         int width, ascent, descent;
436
437         FontMetrics const & fm = theFontMetrics(font);
438         fm.buttonText(s, offset, width, ascent, descent);
439
440         int const d = offset / 2;
441
442         fillRectangle(x + d, baseline - ascent, width - offset,
443                               ascent + descent, back);
444         rectangle(x + d, baseline - ascent, width - offset, ascent + descent, frame);
445         text(x + offset, baseline, s, font);
446 }
447
448
449 int GuiPainter::preeditText(int x, int y, char_type c,
450         FontInfo const & font, preedit_style style)
451 {
452         FontInfo temp_font = font;
453         FontMetrics const & fm = theFontMetrics(font);
454         int ascent = fm.maxAscent();
455         int descent = fm.maxDescent();
456         int height = ascent + descent;
457         int width = fm.width(c);
458
459         switch (style) {
460                 case preedit_default:
461                         // default unselecting mode.
462                         fillRectangle(x, y - height + 1, width, height, Color_background);
463                         dashedUnderline(font, x, y - descent + 1, width);
464                         break;
465                 case preedit_selecting:
466                         // We are in selecting mode: white text on black background.
467                         fillRectangle(x, y - height + 1, width, height, Color_black);
468                         temp_font.setColor(Color_white);
469                         break;
470                 case preedit_cursor:
471                         // The character comes with a cursor.
472                         fillRectangle(x, y - height + 1, width, height, Color_background);
473                         underline(font, x, y - descent + 1, width);
474                         break;
475         }
476         text(x, y - descent + 1, c, temp_font);
477
478         return width;
479 }
480
481
482 void GuiPainter::underline(FontInfo const & f, int x, int y, int width,
483                            line_style ls)
484 {
485         FontMetrics const & fm = theFontMetrics(f);
486         int const pos = fm.underlinePos();
487
488         line(x, y + pos, x + width, y + pos,
489              f.realColor(), ls, fm.lineWidth());
490 }
491
492
493 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
494 {
495         FontMetrics const & fm = theFontMetrics(f);
496         int const pos = fm.strikeoutPos();
497
498         line(x, y - pos, x + width, y - pos,
499              f.realColor(), line_solid, fm.lineWidth());
500 }
501
502
503 void GuiPainter::crossoutLines(FontInfo const & f, int x, int y, int width)
504 {
505         FontInfo tmpf = f;
506         tmpf.setXout(FONT_OFF);
507
508         // the definition of \xout in ulem.sty is
509     //  \def\xout{\bgroup \markoverwith{\hbox to.35em{\hss/\hss}}\ULon}
510         // Let's mimic it somewhat.
511         double offset = max(0.35 * theFontMetrics(tmpf).em(), 1);
512         for (int i = 0 ; i < iround(width / offset) ; ++i)
513                 text(x + iround(i * offset), y, '/', tmpf);
514 }
515
516
517 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
518 {
519         FontMetrics const & fm = theFontMetrics(f);
520         int const pos1 = fm.underlinePos() + fm.lineWidth();
521         int const pos2 = fm.underlinePos() - fm.lineWidth() + 1;
522
523         line(x, y + pos1, x + width, y + pos1,
524                  f.realColor(), line_solid, fm.lineWidth());
525         line(x, y + pos2, x + width, y + pos2,
526                  f.realColor(), line_solid, fm.lineWidth());
527 }
528
529
530 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
531 {
532         FontMetrics const & fm = theFontMetrics(f);
533
534         int const below = max(fm.maxDescent() / 2, 2);
535         int height = max((fm.maxDescent() / 4) - 1, 1);
536
537         if (height >= 2)
538                 height += below;
539
540         for (int n = 0; n != height; ++n)
541                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
542 }
543
544
545 void GuiPainter::wavyHorizontalLine(FontInfo const & f, int x, int y, int width, ColorCode col)
546 {
547         FontMetrics const & fm = theFontMetrics(f);
548         int const pos = fm.underlinePos();
549         int const lw = max(1, fm.lineWidth());
550
551         setQPainterPen(computeColor(col), line_solid, lw);
552         int const step = 2 * lw;
553         int const xend = x + width;
554         int height = lw;
555         //FIXME: I am not sure if Antialiasing gives the best effect.
556         //setRenderHint(Antialiasing, true);
557         QVector<QPoint> points;
558         while (x < xend) {
559                 height = - height;
560                 points.append(QPoint(x, y + pos - height));
561                 points.append(QPoint(x + step, y + pos + height));
562                 x += step;
563                 points.append(QPoint(x, (qreal)y + pos + height));
564                 points.append(QPoint(x + lw, y + pos + height));
565                 x += lw;
566         }
567         drawPolyline(points);
568         //setRenderHint(Antialiasing, false);
569 }
570
571 } // namespace frontend
572 } // namespace lyx