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