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