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