]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiPainter.cpp
Do not unnecessarily reset the outliner to TOC on internal model changes
[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 #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>(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(f, 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, baseline - ascent, width - offset,
450                               ascent + descent, 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(FontInfo const & f, int x, int y, int width, ColorCode col)
553 {
554         FontMetrics const & fm = theFontMetrics(f);
555         int const pos = fm.underlinePos();
556
557         setQPainterPen(computeColor(col), line_solid, fm.lineWidth());
558         int const step = 2 * fm.lineWidth();
559         int const xend = x + width;
560         int height = 1 * fm.lineWidth();
561         //FIXME: I am not sure if Antialiasing gives the best effect.
562         //setRenderHint(Antialiasing, true);
563         QVector<QPoint> points;
564         while (x < xend) {
565                 height = - height;
566                 points.append(QPoint(x, y + pos - height));
567                 points.append(QPoint(x + step, y + pos + height));
568                 x += step;
569                 points.append(QPoint(x, (qreal)y + pos + height));
570                 points.append(QPoint(x + step/2, y + pos + height));
571                 x += step/2;
572         }
573         drawPolyline(points);
574         //setRenderHint(Antialiasing, false);
575 }
576
577 } // namespace frontend
578 } // namespace lyx