]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiPainter.cpp
Allow compiling with Qt6
[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
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::ellipse(double x, double y, double rx, double ry,
237         Color col, fill_style fs, line_style ls, int lw)
238 {
239         QColor const color = computeColor(col);
240         setQPainterPen(color, ls, lw);
241         bool const do_antialiasing = renderHints() & TextAntialiasing;
242         setRenderHint(Antialiasing, do_antialiasing);
243         if (fs == fill_none) {
244                 drawEllipse(QPointF(x, y), rx, ry);
245         } else {
246                 QBrush const oldbrush = brush();
247                 setBrush(QBrush(color));
248                 drawEllipse(QPointF(x, y), rx, ry);
249                 setBrush(oldbrush);
250         }
251         setRenderHint(Antialiasing, false);
252 }
253
254
255 void GuiPainter::image(int x, int y, int w, int h, graphics::Image const & i,
256                        bool const revert_in_darkmode)
257 {
258         graphics::GuiImage const & qlimage =
259                 static_cast<graphics::GuiImage const &>(i);
260
261         fillRectangle(x, y, w, h, Color_graphicsbg);
262
263         QImage image = qlimage.image();
264
265         if (revert_in_darkmode && guiApp && guiApp->colorCache().isDarkMode())
266                 // FIXME this is only a cheap approximation
267                 // Ideally, replace colors as in GuiApplication::prepareForDarkmode()
268                 image.invertPixels();
269
270         QRectF const drect = QRectF(x, y, w, h);
271         QRectF const srect = QRectF(0, 0, image.width(), image.height());
272         // Bilinear filtering is needed on a rare occasion for instant previews when
273         // the user's configuration mixes low-dpi and high-dpi monitors (#10114).
274         // This filter is optimised by qt on pixel-aligned images, so this does not
275         // affect performances in other cases.
276         setRenderHint(SmoothPixmapTransform);
277         drawImage(drect, image, srect);
278         setRenderHint(SmoothPixmapTransform, false);
279 }
280
281
282 void GuiPainter::text(int x, int y, char_type c, FontInfo const & f)
283 {
284         text(x, y, docstring(1, c), f);
285 }
286
287
288 void GuiPainter::text(int x, int y, docstring const & s, FontInfo const & f)
289 {
290         text(x, y, s, f, Auto, 0.0, 0.0);
291 }
292
293
294 void GuiPainter::text(int x, int y, docstring const & s,
295                       FontInfo const & f, Direction const dir,
296                       double const wordspacing, double const tw)
297 {
298         //LYXERR0("text: x=" << x << ", s=" << s);
299         if (s.empty())
300                 return;
301
302         /* Caution: The following ucs4 to QString conversions work for symbol fonts
303         only because they are no real conversions but simple casts in reality.
304         When we want to draw a symbol or calculate the metrics we pass the position
305         of the symbol in the font (as given in lib/symbols) as a char_type to the
306         frontend. This is just wrong, because the symbol is no UCS4 character at
307         all. You can think of this number as the code point of the symbol in a
308         custom symbol encoding. It works because this char_type is later on again
309         interpreted as a position in the font.
310         The correct solution would be to have extra functions for symbols, but that
311         would require to duplicate a lot of frontend and mathed support code.
312         */
313         QString str = toqstr(s);
314
315 #if 0
316         // HACK: QT3 refuses to show single compose characters
317         //       Still needed with Qt4?
318         if (ls == 1 && str[0].unicode() >= 0x05b0 && str[0].unicode() <= 0x05c2)
319                 str = ' ' + str;
320 #endif
321
322         QFont ff = getFont(f);
323         ff.setWordSpacing(wordspacing);
324         GuiFontMetrics const & fm = getFontMetrics(f);
325
326         int textwidth = 0;
327         if (tw == 0.0)
328                 // Take into account space stretching (word spacing)
329                 textwidth = fm.width(s) +
330                         static_cast<int>(fm.countExpanders(s) * wordspacing);
331         else
332                 textwidth = static_cast<int>(tw);
333
334         textDecoration(f, x, y, textwidth);
335
336         setQPainterPen(computeColor(f.realColor()));
337         if (dir != Auto) {
338                 auto ptl = fm.getTextLayout(s, dir == RtL, wordspacing);
339                 QTextLine const & tline = ptl->lineForTextPosition(0);
340                 ptl->draw(this, QPointF(x, y - tline.ascent()));
341         } else {
342                 if (font() != ff)
343                         setFont(ff);
344                 drawText(x, y, str);
345         }
346         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
347         //      << " at " << x << "," << y);
348 }
349
350
351 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
352                       double const wordspacing, double const tw)
353 {
354         text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft() ? RtL : LtR,
355              wordspacing, tw);
356 }
357
358
359 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
360                       Color other, size_type const from, size_type const to,
361                       double const wordspacing, double const tw)
362 {
363         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
364         FontInfo fi = f.fontInfo();
365         Direction const dir = f.isVisibleRightToLeft() ? RtL : LtR;
366
367         // dimensions
368         int const ascent = fm.maxAscent();
369         int const height = fm.maxAscent() + fm.maxDescent();
370         int xmin = fm.pos2x(str, from, dir == RtL, wordspacing);
371         int xmax = fm.pos2x(str, to, dir == RtL, wordspacing);
372         // Avoid this case, since it would make the `other' text spill in some cases
373         if (xmin == xmax) {
374                 text(x, y, str, fi, dir, wordspacing, tw);
375                 return;
376         } else if (xmin > xmax)
377                 swap(xmin, xmax);
378
379         // First the part in other color
380         Color const orig = fi.realColor();
381         fi.setPaintColor(other);
382         QRegion const clip(x + xmin, y - ascent, xmax - xmin, height);
383         setClipRegion(clip);
384         text(x, y, str, fi, dir, wordspacing, tw);
385
386         // Then the part in normal color
387         // Note that in Qt5, it is not possible to use Qt::UniteClip,
388         // therefore QRegion is used.
389         fi.setPaintColor(orig);
390         QRegion region(viewport());
391         setClipRegion(region - clip);
392         text(x, y, str, fi, dir, wordspacing, tw);
393         setClipping(false);
394 }
395
396
397 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
398 {
399         if (f.underbar() == FONT_ON)
400                 underline(f, x, y, width);
401         if (f.strikeout() == FONT_ON)
402                 strikeoutLine(f, x, y, width);
403         if (f.xout() == FONT_ON)
404                 crossoutLines(f, x, y, width);
405         if (f.uuline() == FONT_ON)
406                 doubleUnderline(f, x, y, width);
407         if (f.uwave() == FONT_ON)
408                 // f.color() doesn't work on some circumstances
409                 wavyHorizontalLine(f, x, y, width,  f.realColor().baseColor);
410 }
411
412
413 static int max(int a, int b) { return a > b ? a : b; }
414
415
416 void GuiPainter::rectText(int x, int y, docstring const & str,
417         FontInfo const & font, Color back, Color frame)
418 {
419         int width, ascent, descent;
420
421         FontMetrics const & fm = theFontMetrics(font);
422         fm.rectText(str, width, ascent, descent);
423
424         if (back != Color_none)
425                 fillRectangle(x + 1, y - ascent + 1, width - 1,
426                               ascent + descent - 1, back);
427
428         if (frame != Color_none)
429                 rectangle(x, y - ascent, width, ascent + descent, frame);
430
431         // FIXME: let offset depend on font
432         text(x + 3, y, str, font);
433 }
434
435
436 void GuiPainter::buttonText(int x, int baseline, docstring const & s,
437         FontInfo const & font, Color back, Color frame, int offset)
438 {
439         int width, ascent, descent;
440
441         FontMetrics const & fm = theFontMetrics(font);
442         fm.buttonText(s, offset, width, ascent, descent);
443
444         int const d = offset / 2;
445
446         fillRectangle(x + d, baseline - ascent, width - offset,
447                               ascent + descent, back);
448         rectangle(x + d, baseline - ascent, width - offset, ascent + descent, frame);
449         text(x + offset, baseline, s, font);
450 }
451
452
453 int GuiPainter::preeditText(int x, int y, char_type c,
454         FontInfo const & font, preedit_style style)
455 {
456         FontInfo temp_font = font;
457         FontMetrics const & fm = theFontMetrics(font);
458         int ascent = fm.maxAscent();
459         int descent = fm.maxDescent();
460         int height = ascent + descent;
461         int width = fm.width(c);
462
463         switch (style) {
464                 case preedit_default:
465                         // default unselecting mode.
466                         fillRectangle(x, y - height + 1, width, height, Color_background);
467                         dashedUnderline(font, x, y - descent + 1, width);
468                         break;
469                 case preedit_selecting:
470                         // We are in selecting mode: white text on black background.
471                         fillRectangle(x, y - height + 1, width, height, Color_black);
472                         temp_font.setColor(Color_white);
473                         break;
474                 case preedit_cursor:
475                         // The character comes with a cursor.
476                         fillRectangle(x, y - height + 1, width, height, Color_background);
477                         underline(font, x, y - descent + 1, width);
478                         break;
479         }
480         text(x, y - descent + 1, c, temp_font);
481
482         return width;
483 }
484
485
486 void GuiPainter::underline(FontInfo const & f, int x, int y, int width,
487                            line_style ls)
488 {
489         FontMetrics const & fm = theFontMetrics(f);
490         int const pos = fm.underlinePos();
491
492         line(x, y + pos, x + width, y + pos,
493              f.realColor(), ls, fm.lineWidth());
494 }
495
496
497 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
498 {
499         FontMetrics const & fm = theFontMetrics(f);
500         int const pos = fm.strikeoutPos();
501
502         line(x, y - pos, x + width, y - pos,
503              f.realColor(), line_solid, fm.lineWidth());
504 }
505
506
507 void GuiPainter::crossoutLines(FontInfo const & f, int x, int y, int width)
508 {
509         FontInfo tmpf = f;
510         tmpf.setXout(FONT_OFF);
511
512         // the definition of \xout in ulem.sty is
513     //  \def\xout{\bgroup \markoverwith{\hbox to.35em{\hss/\hss}}\ULon}
514         // Let's mimic it somewhat.
515         double offset = max(0.35 * theFontMetrics(tmpf).em(), 1);
516         for (int i = 0 ; i < iround(width / offset) ; ++i)
517                 text(x + iround(i * offset), y, '/', tmpf);
518 }
519
520
521 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
522 {
523         FontMetrics const & fm = theFontMetrics(f);
524         int const pos1 = fm.underlinePos() + fm.lineWidth();
525         int const pos2 = fm.underlinePos() - fm.lineWidth() + 1;
526
527         line(x, y + pos1, x + width, y + pos1,
528                  f.realColor(), line_solid, fm.lineWidth());
529         line(x, y + pos2, x + width, y + pos2,
530                  f.realColor(), line_solid, fm.lineWidth());
531 }
532
533
534 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
535 {
536         FontMetrics const & fm = theFontMetrics(f);
537
538         int const below = max(fm.maxDescent() / 2, 2);
539         int height = max((fm.maxDescent() / 4) - 1, 1);
540
541         if (height >= 2)
542                 height += below;
543
544         for (int n = 0; n != height; ++n)
545                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
546 }
547
548
549 void GuiPainter::wavyHorizontalLine(FontInfo const & f, int x, int y, int width, ColorCode col)
550 {
551         FontMetrics const & fm = theFontMetrics(f);
552         int const pos = fm.underlinePos();
553
554         setQPainterPen(computeColor(col), line_solid, fm.lineWidth());
555         int const step = 2 * fm.lineWidth();
556         int const xend = x + width;
557         int height = 1 * fm.lineWidth();
558         //FIXME: I am not sure if Antialiasing gives the best effect.
559         //setRenderHint(Antialiasing, true);
560         QVector<QPoint> points;
561         while (x < xend) {
562                 height = - height;
563                 points.append(QPoint(x, y + pos - height));
564                 points.append(QPoint(x + step, y + pos + height));
565                 x += step;
566                 points.append(QPoint(x, (qreal)y + pos + height));
567                 points.append(QPoint(x + step/2, y + pos + height));
568                 x += step/2;
569         }
570         drawPolyline(points);
571         //setRenderHint(Antialiasing, false);
572 }
573
574 } // namespace frontend
575 } // namespace lyx