]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiPainter.cpp
Fix aliasing logic for polylines
[lyx.git] / src / frontends / qt4 / 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 "Language.h"
25 #include "LyXRC.h"
26
27 #include "insets/Inset.h"
28
29 #include "support/lassert.h"
30 #include "support/debug.h"
31
32 #include <algorithm>
33
34 #include <QPixmapCache>
35 #include <QTextLayout>
36
37 // Set USE_PIXMAP_CACHE to 1 for enabling the use of a Pixmap cache when
38 // drawing text. This is especially useful for older PPC/Mac systems.
39 #if defined(Q_WS_X11) || defined(QPA_XCB)
40 #define USE_PIXMAP_CACHE 0
41 #else
42 #define USE_PIXMAP_CACHE 1
43 #endif
44
45 using namespace std;
46 using namespace lyx::support;
47
48 namespace lyx {
49 namespace frontend {
50
51 const int Painter::thin_line = 1;
52
53 GuiPainter::GuiPainter(QPaintDevice * device, double pixel_ratio)
54         : QPainter(device), Painter(pixel_ratio),
55           use_pixmap_cache_(lyxrc.use_pixmap_cache && USE_PIXMAP_CACHE)
56 {
57         // new QPainter has default QPen:
58         current_color_ = guiApp->colorCache().get(Color_black);
59         current_ls_ = line_solid;
60         current_lw_ = thin_line;
61 }
62
63
64 GuiPainter::~GuiPainter()
65 {
66         QPainter::end();
67         //lyxerr << "GuiPainter::end()" << endl;
68 }
69
70
71 void GuiPainter::setQPainterPen(QColor const & col,
72         Painter::line_style ls, int lw)
73 {
74         if (col == current_color_ && ls == current_ls_ && lw == current_lw_)
75                 return;
76
77         current_color_ = col;
78         current_ls_ = ls;
79         current_lw_ = lw;
80
81         QPen pen = QPainter::pen();
82         pen.setColor(col);
83
84         switch (ls) {
85         case line_solid:
86         case line_solid_aliased:
87                 pen.setStyle(Qt::SolidLine); break;
88         case line_onoffdash:
89                 pen.setStyle(Qt::DotLine); break;
90         }
91
92         pen.setWidth(lw);
93
94         setPen(pen);
95 }
96
97
98 QString GuiPainter::generateStringSignature(QString const & str,
99                                             FontInfo const & f,
100                                             double wordspacing)
101 {
102         QString sig = str;
103         sig.append(QChar(static_cast<short>(f.family())));
104         sig.append(QChar(static_cast<short>(f.series())));
105         sig.append(QChar(static_cast<short>(f.realShape())));
106         sig.append(QChar(static_cast<short>(f.size())));
107         Color const & color = f.realColor();
108         sig.append(QChar(static_cast<short>(color.baseColor)));
109         sig.append(QChar(static_cast<short>(color.mergeColor)));
110         sig.append(QString::number(wordspacing));
111         if (!monochrome_min_.empty()) {
112                 QColor const & min = monochrome_min_.top();
113                 QColor const & max = monochrome_max_.top();
114                 sig.append(QChar(static_cast<short>(min.red())));
115                 sig.append(QChar(static_cast<short>(min.green())));
116                 sig.append(QChar(static_cast<short>(min.blue())));
117                 sig.append(QChar(static_cast<short>(max.red())));
118                 sig.append(QChar(static_cast<short>(max.green())));
119                 sig.append(QChar(static_cast<short>(max.blue())));
120         }
121         return sig;
122 }
123
124
125 QColor GuiPainter::computeColor(Color col)
126 {
127         return filterColor(guiApp->colorCache().get(col));
128 }
129
130
131 QColor GuiPainter::filterColor(QColor const & col)
132 {
133         if (monochrome_min_.empty())
134                 return col;
135
136         // map into [min,max] interval
137         QColor const & min = monochrome_min_.top();
138         QColor const & max = monochrome_max_.top();
139                         
140         qreal v = col.valueF();
141         v *= v; // make it a bit steeper (i.e. darker)
142                 
143         qreal minr, ming, minb;
144         qreal maxr, maxg, maxb;
145         min.getRgbF(&minr, &ming, &minb);
146         max.getRgbF(&maxr, &maxg, &maxb);
147                         
148         QColor c;
149         c.setRgbF(
150                 v * (minr - maxr) + maxr,
151                 v * (ming - maxg) + maxg,
152                 v * (minb - maxb) + maxb);
153         return c;
154 }
155
156
157 void GuiPainter::enterMonochromeMode(Color const & min, Color const & max)
158 {
159         QColor qmin = filterColor(guiApp->colorCache().get(min));
160         QColor qmax = filterColor(guiApp->colorCache().get(max));
161         monochrome_min_.push(qmin);
162         monochrome_max_.push(qmax);
163 }
164
165
166 void GuiPainter::leaveMonochromeMode()
167 {
168         LASSERT(!monochrome_min_.empty(), return);
169         monochrome_min_.pop();
170         monochrome_max_.pop();
171 }
172
173
174 void GuiPainter::point(int x, int y, Color col)
175 {
176         if (!isDrawingEnabled())
177                 return;
178
179         setQPainterPen(computeColor(col));
180         drawPoint(x, y);
181 }
182
183
184 void GuiPainter::line(int x1, int y1, int x2, int y2,
185         Color col,
186         line_style ls,
187         int lw)
188 {
189         if (!isDrawingEnabled())
190                 return;
191
192         setQPainterPen(computeColor(col), ls, lw);
193         bool const do_antialiasing = renderHints() & TextAntialiasing
194                 && x1 != x2 && y1 != y2 && ls != line_solid_aliased;
195         setRenderHint(Antialiasing, do_antialiasing);
196         drawLine(x1, y1, x2, y2);
197         setRenderHint(Antialiasing, false);
198 }
199
200
201 void GuiPainter::lines(int const * xp, int const * yp, int np,
202         Color col,
203         fill_style fs,
204         line_style ls,
205         int lw)
206 {
207         if (!isDrawingEnabled())
208                 return;
209
210         // double the size if needed
211         // FIXME THREAD
212         static QVector<QPoint> points(32);
213         if (np > points.size())
214                 points.resize(2 * np);
215
216         bool antialias = false;
217         for (int i = 0; i < np; ++i) {
218                 points[i].setX(xp[i]);
219                 points[i].setY(yp[i]);
220                 if (i != 0)
221                         antialias |= xp[i-1] != xp[i] && yp[i-1] != yp[i];
222         }
223         QColor const color = computeColor(col);
224         setQPainterPen(color, ls, lw);
225         bool const text_is_antialiased = renderHints() & TextAntialiasing;
226         setRenderHint(Antialiasing,
227                       antialias && text_is_antialiased && ls != line_solid_aliased);
228         if (fs == fill_none) {
229                 drawPolyline(points.data(), np);
230         } else {
231                 QBrush const oldbrush = brush();
232                 setBrush(QBrush(color));
233                 drawPolygon(points.data(), np, fs == fill_oddeven ?
234                             Qt::OddEvenFill : Qt::WindingFill);
235                 setBrush(oldbrush);
236         }
237         setRenderHint(Antialiasing, false);
238 }
239
240
241 void GuiPainter::path(int const * xp, int const * yp,
242         int const * c1x, int const * c1y,
243         int const * c2x, int const * c2y,
244         int np,
245         Color col,
246         fill_style fs,
247         line_style ls,
248         int lw)
249 {
250         if (!isDrawingEnabled())
251                 return;
252
253         QPainterPath bpath;
254         // This is the starting point, so its control points are meaningless
255         bpath.moveTo(xp[0], yp[0]);
256
257         for (int i = 1; i < np; ++i) {
258                 bool line = c1x[i] == xp[i - 1] && c1y[i] == yp[i - 1] &&
259                             c2x[i] == xp[i] && c2y[i] == yp[i];
260                 if (line)
261                         bpath.lineTo(xp[i], yp[i]);
262                 else
263                         bpath.cubicTo(c1x[i], c1y[i],  c2x[i], c2y[i], xp[i], yp[i]);
264         }
265         QColor const color = computeColor(col);
266         setQPainterPen(color, ls, lw);
267         bool const text_is_antialiased = renderHints() & TextAntialiasing;
268         setRenderHint(Antialiasing, text_is_antialiased && ls != line_solid_aliased);
269         drawPath(bpath);
270         if (fs != fill_none)
271                 fillPath(bpath, QBrush(color));
272         setRenderHint(Antialiasing, false);
273 }
274
275
276 void GuiPainter::rectangle(int x, int y, int w, int h,
277         Color col,
278         line_style ls,
279         int lw)
280 {
281         if (!isDrawingEnabled())
282                 return;
283
284         setQPainterPen(computeColor(col), ls, lw);
285         drawRect(x, y, w, h);
286 }
287
288
289 void GuiPainter::fillRectangle(int x, int y, int w, int h, Color col)
290 {
291         if (!isDrawingEnabled())
292                 return;
293
294         fillRect(x, y, w, h, guiApp->colorCache().get(col));
295 }
296
297
298 void GuiPainter::arc(int x, int y, unsigned int w, unsigned int h,
299         int a1, int a2, Color col)
300 {
301         if (!isDrawingEnabled())
302                 return;
303
304         // LyX usings 1/64ths degree, Qt usings 1/16th
305         setQPainterPen(computeColor(col));
306         bool const do_antialiasing = renderHints() & TextAntialiasing;
307         setRenderHint(Antialiasing, do_antialiasing);
308         drawArc(x, y, w, h, a1 / 4, a2 / 4);
309         setRenderHint(Antialiasing, false);
310 }
311
312
313 void GuiPainter::image(int x, int y, int w, int h, graphics::Image const & i)
314 {
315         graphics::GuiImage const & qlimage =
316                 static_cast<graphics::GuiImage const &>(i);
317
318         fillRectangle(x, y, w, h, Color_graphicsbg);
319
320         if (!isDrawingEnabled())
321                 return;
322
323         QImage const image = qlimage.image();
324         QRectF const drect = QRectF(x, y, w, h);
325         QRectF const srect = QRectF(0, 0, image.width(), image.height());
326         // Bilinear filtering is needed on a rare occasion for instant previews when
327         // the user's configuration mixes low-dpi and high-dpi monitors (#10114).
328         // This filter is optimised by qt on pixel-aligned images, so this does not
329         // affect performances in other cases.
330         setRenderHint(SmoothPixmapTransform);
331         drawImage(drect, image, srect);
332         setRenderHint(SmoothPixmapTransform, false);
333 }
334
335
336 void GuiPainter::text(int x, int y, char_type c, FontInfo const & f)
337 {
338         text(x, y, docstring(1, c), f);
339 }
340
341
342 void GuiPainter::text(int x, int y, docstring const & s, FontInfo const & f)
343 {
344         text(x, y, s, f, Auto, 0.0, 0.0);
345 }
346
347
348 void GuiPainter::do_drawText(int x, int y, QString str,
349                              GuiPainter::Direction const dir,
350                              FontInfo const & f, QFont ff)
351 {
352         setQPainterPen(computeColor(f.realColor()));
353         if (font() != ff)
354                 setFont(ff);
355
356          /* In LyX, the character direction is forced by the language.
357           * Therefore, we have to signal that fact to Qt.
358           */
359 #if 1
360         /* Use unicode override characters to enforce drawing direction
361          * Source: http://www.iamcal.com/understanding-bidirectional-text/
362          */
363         if (dir == RtL)
364                 // Right-to-left override: forces to draw text right-to-left
365                 str = QChar(0x202E) + str;
366         else if (dir == LtR)
367                 // Left-to-right override: forces to draw text left-to-right
368                 str =  QChar(0x202D) + str;
369         drawText(x, y, str);
370 #else
371         /* This looks like a cleaner solution, but it has drawbacks
372          * - does not work reliably (Mac OS X, ...)
373          * - it is not really documented
374          * Keep it here for now, in case it can be helpful
375          */
376         //This is much stronger than setLayoutDirection.
377         int flag = 0;
378         if (dir == RtL)
379                 flag = Qt::TextForceRightToLeft;
380         else if (dir == LtR)
381                 flag = Qt::TextForceLeftToRight;
382         drawText(x + ((dir == RtL) ? textwidth : 0), y - fm.maxAscent(), 0, 0,
383                  flag | Qt::TextDontClip,
384                  str);
385 #endif
386 }
387
388
389 void GuiPainter::text(int x, int y, docstring const & s,
390                       FontInfo const & f, Direction const dir,
391                       double const wordspacing, double const tw)
392 {
393         //LYXERR0("text: x=" << x << ", s=" << s);
394         if (s.empty() || !isDrawingEnabled())
395                 return;
396
397         /* Caution: The following ucs4 to QString conversions work for symbol fonts
398         only because they are no real conversions but simple casts in reality.
399         When we want to draw a symbol or calculate the metrics we pass the position
400         of the symbol in the font (as given in lib/symbols) as a char_type to the
401         frontend. This is just wrong, because the symbol is no UCS4 character at
402         all. You can think of this number as the code point of the symbol in a
403         custom symbol encoding. It works because this char_type is later on again
404         interpreted as a position in the font.
405         The correct solution would be to have extra functions for symbols, but that
406         would require to duplicate a lot of frontend and mathed support code.
407         */
408         QString str = toqstr(s);
409
410 #if 0
411         // HACK: QT3 refuses to show single compose characters
412         //       Still needed with Qt4?
413         if (ls == 1 && str[0].unicode() >= 0x05b0 && str[0].unicode() <= 0x05c2)
414                 str = ' ' + str;
415 #endif
416
417         QFont ff = getFont(f);
418         ff.setWordSpacing(wordspacing);
419         GuiFontMetrics const & fm = getFontMetrics(f);
420
421         int textwidth = 0;
422         if (tw == 0.0)
423                 // Note that we have to take in account space stretching (word spacing)
424                 textwidth = fm.width(s) + count(s.begin(), s.end(), ' ') * wordspacing;
425         else
426                 textwidth = static_cast<int>(tw);
427
428         textDecoration(f, x, y, textwidth);
429
430         if (use_pixmap_cache_) {
431                 QPixmap pm;
432                 QString key = generateStringSignature(str, f, wordspacing);
433
434                 // Warning: Left bearing is in general negative! Only the case
435                 // where left bearing is negative is of interest WRT the
436                 // pixmap width and the text x-position.
437                 // Only the left bearing of the first character is important
438                 // as we always write from left to right, even for
439                 // right-to-left languages.
440                 // FIXME: this is probably broken for RTL now that we draw full strings.
441                 // Morover the first/last element is possibly not the right one since the glyph may have changed.
442                 int const lb = min(fm.lbearing(s[0]), 0);
443                 int const mA = fm.maxAscent();
444                 if (QPixmapCache::find(key, pm)) {
445                         // Draw the cached pixmap.
446                         drawPixmap(x + lb, y - mA, pm);
447                         return;
448                 }
449
450                 // Only the right bearing of the last character is
451                 // important as we always write from left to right,
452                 // even for right-to-left languages.
453                 int const rb = fm.rbearing(s[s.size()-1]);
454                 int const w = textwidth + rb - lb;
455                 int const mD = fm.maxDescent();
456                 int const h = mA + mD;
457                 if (w > 0 && h > 0) {
458                         pm = QPixmap(static_cast<int>(pixelRatio() * w),
459                                                  static_cast<int>(pixelRatio() * h));
460 #if QT_VERSION >= 0x050000
461                         pm.setDevicePixelRatio(pixelRatio());
462 #endif
463                         pm.fill(Qt::transparent);
464                         GuiPainter p(&pm, pixelRatio());
465                         p.do_drawText(-lb, mA, str, dir, f, ff);
466                         QPixmapCache::insert(key, pm);
467                         //LYXERR(Debug::PAINTING, "h=" << h << "  mA=" << mA << "  mD=" << mD
468                         //      << "  w=" << w << "  lb=" << lb << "  tw=" << textwidth
469                         //      << "  rb=" << rb);
470
471                         // Draw the new cached pixmap.
472                         drawPixmap(x + lb, y - mA, pm);
473                         //rectangle(x-lb, y-mA, w, h, Color_green);
474                 }
475                 return;
476         }
477
478         // don't use the pixmap cache,
479         do_drawText(x, y, str, dir, f, ff);
480         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
481         //      << " at " << x << "," << y);
482 }
483
484
485 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
486                       double const wordspacing, double const tw)
487 {
488         text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft() ? RtL : LtR,
489              wordspacing, tw);
490 }
491
492
493 void GuiPainter::text(int x, int y, docstring const & str, Font const & f,
494                       Color other, size_type const from, size_type const to,
495                       double const wordspacing, double const tw)
496 {
497         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
498         FontInfo fi = f.fontInfo();
499         Direction const dir = f.isVisibleRightToLeft() ? RtL : LtR;
500
501         // dimensions
502         int const ascent = fm.maxAscent();
503         int const height = fm.maxAscent() + fm.maxDescent();
504         int xmin = fm.pos2x(str, from, dir == RtL, wordspacing);
505         int xmax = fm.pos2x(str, to, dir == RtL, wordspacing);
506         if (xmin > xmax)
507                 swap(xmin, xmax);
508
509         // First the part in other color
510         Color const orig = fi.realColor();
511         fi.setPaintColor(other);
512         QRegion const clip(x + xmin, y - ascent, xmax - xmin, height);
513         setClipRegion(clip);
514         text(x, y, str, fi, dir, wordspacing, tw);
515
516         // Then the part in normal color
517         // Note that in Qt5, it is not possible to use Qt::UniteClip,
518         // therefore QRegion is used.
519         fi.setPaintColor(orig);
520         QRegion region(viewport());
521         setClipRegion(region - clip);
522         text(x, y, str, fi, dir, wordspacing, tw);
523         setClipping(false);
524 }
525
526
527 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
528 {
529         if (f.underbar() == FONT_ON)
530                 underline(f, x, y, width);
531         if (f.strikeout() == FONT_ON)
532                 strikeoutLine(f, x, y, width);
533         if (f.uuline() == FONT_ON)
534                 doubleUnderline(f, x, y, width);
535         if (f.uwave() == FONT_ON)
536                 // f.color() doesn't work on some circumstances
537                 wavyHorizontalLine(x, y, width,  f.realColor().baseColor);
538 }
539
540
541 static int max(int a, int b) { return a > b ? a : b; }
542
543
544 void GuiPainter::button(int x, int y, int w, int h, bool mouseHover)
545 {
546         if (mouseHover)
547                 fillRectangle(x, y, w, h, Color_buttonhoverbg);
548         else
549                 fillRectangle(x, y, w, h, Color_buttonbg);
550         buttonFrame(x, y, w, h);
551 }
552
553
554 void GuiPainter::buttonFrame(int x, int y, int w, int h)
555 {
556         line(x, y, x, y + h - 1, Color_buttonframe);
557         line(x - 1 + w, y, x - 1 + w, y + h - 1, Color_buttonframe);
558         line(x, y - 1, x - 1 + w, y - 1, Color_buttonframe);
559         line(x, y + h - 1, x - 1 + w, y + h - 1, Color_buttonframe);
560 }
561
562
563 void GuiPainter::rectText(int x, int y, docstring const & str,
564         FontInfo const & font, Color back, Color frame)
565 {
566         int width;
567         int ascent;
568         int descent;
569
570         FontMetrics const & fm = theFontMetrics(font);
571         fm.rectText(str, width, ascent, descent);
572
573         if (back != Color_none)
574                 fillRectangle(x + 1, y - ascent + 1, width - 1,
575                               ascent + descent - 1, back);
576
577         if (frame != Color_none)
578                 rectangle(x, y - ascent, width, ascent + descent, frame);
579
580         text(x + 3, y, str, font);
581 }
582
583
584 void GuiPainter::buttonText(int x, int y, docstring const & str,
585         FontInfo const & font, bool mouseHover)
586 {
587         int width;
588         int ascent;
589         int descent;
590
591         FontMetrics const & fm = theFontMetrics(font);
592         fm.buttonText(str, width, ascent, descent);
593
594         static int const d = Inset::TEXT_TO_INSET_OFFSET / 2;
595
596         button(x + d, y - ascent, width - Inset::TEXT_TO_INSET_OFFSET, descent + ascent, mouseHover);
597         text(x + Inset::TEXT_TO_INSET_OFFSET, y, str, font);
598 }
599
600
601 int GuiPainter::preeditText(int x, int y, char_type c,
602         FontInfo const & font, preedit_style style)
603 {
604         FontInfo temp_font = font;
605         FontMetrics const & fm = theFontMetrics(font);
606         int ascent = fm.maxAscent();
607         int descent = fm.maxDescent();
608         int height = ascent + descent;
609         int width = fm.width(c);
610
611         switch (style) {
612                 case preedit_default:
613                         // default unselecting mode.
614                         fillRectangle(x, y - height + 1, width, height, Color_background);
615                         dashedUnderline(font, x, y - descent + 1, width);
616                         break;
617                 case preedit_selecting:
618                         // We are in selecting mode: white text on black background.
619                         fillRectangle(x, y - height + 1, width, height, Color_black);
620                         temp_font.setColor(Color_white);
621                         break;
622                 case preedit_cursor:
623                         // The character comes with a cursor.
624                         fillRectangle(x, y - height + 1, width, height, Color_background);
625                         underline(font, x, y - descent + 1, width);
626                         break;
627         }
628         text(x, y - descent + 1, c, temp_font);
629
630         return width;
631 }
632
633
634 void GuiPainter::underline(FontInfo const & f, int x, int y, int width,
635                            line_style ls)
636 {
637         FontMetrics const & fm = theFontMetrics(f);
638         int const pos = fm.underlinePos();
639
640         line(x, y + pos, x + width, y + pos,
641              f.realColor(), ls, fm.lineWidth());
642 }
643
644
645 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
646 {
647         FontMetrics const & fm = theFontMetrics(f);
648         int const pos = fm.strikeoutPos();
649
650         line(x, y - pos, x + width, y - pos,
651              f.realColor(), line_solid, fm.lineWidth());
652 }
653
654
655 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
656 {
657         FontMetrics const & fm = theFontMetrics(f);
658         int const pos1 = fm.underlinePos() + fm.lineWidth();
659         int const pos2 = fm.underlinePos() - fm.lineWidth() + 1;
660
661         line(x, y + pos1, x + width, y + pos1,
662                  f.realColor(), line_solid, fm.lineWidth());
663         line(x, y + pos2, x + width, y + pos2,
664                  f.realColor(), line_solid, fm.lineWidth());
665 }
666
667
668 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
669 {
670         FontMetrics const & fm = theFontMetrics(f);
671
672         int const below = max(fm.maxDescent() / 2, 2);
673         int height = max((fm.maxDescent() / 4) - 1, 1);
674
675         if (height >= 2)
676                 height += below;
677
678         for (int n = 0; n != height; ++n)
679                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
680 }
681
682
683 void GuiPainter::wavyHorizontalLine(int x, int y, int width, ColorCode col)
684 {
685         setQPainterPen(computeColor(col));
686         int const step = 2;
687         int const xend = x + width;
688         int height = 1;
689         //FIXME: I am not sure if Antialiasing gives the best effect.
690         //setRenderHint(Antialiasing, true);
691         while (x < xend) {
692                 height = - height;
693                 drawLine(x, y - height, x + step, y + height);
694                 x += step;
695                 drawLine(x, y + height, x + step/2, y + height);
696                 x += step/2;
697         }
698         //setRenderHint(Antialiasing, false);
699 }
700
701 } // namespace frontend
702 } // namespace lyx