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