]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiPainter.cpp
Use QFontMetrics information for underlines (and friends) width and position
[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 int GuiPainter::text(int x, int y, docstring const & s,
293                      FontInfo const & f, bool const rtl,
294                      double const wordspacing)
295 {
296         //LYXERR0("text: x=" << x << ", s=" << s);
297         if (s.empty())
298                 return 0;
299
300         /* Caution: The following ucs4 to QString conversions work for symbol fonts
301         only because they are no real conversions but simple casts in reality.
302         When we want to draw a symbol or calculate the metrics we pass the position
303         of the symbol in the font (as given in lib/symbols) as a char_type to the
304         frontend. This is just wrong, because the symbol is no UCS4 character at
305         all. You can think of this number as the code point of the symbol in a
306         custom symbol encoding. It works because this char_type is lateron again
307         interpreted as a position in the font again.
308         The correct solution would be to have extra functions for symbols, but that
309         would require to duplicate a lot of frontend and mathed support code.
310         */
311         QString str = toqstr(s);
312
313 #if 0
314         // HACK: QT3 refuses to show single compose characters
315         //       Still needed with Qt4?
316         if (ls == 1 && str[0].unicode() >= 0x05b0 && str[0].unicode() <= 0x05c2)
317                 str = ' ' + str;
318 #endif
319
320         QFont ff = getFont(f);
321         ff.setWordSpacing(wordspacing);
322         GuiFontMetrics const & fm = getFontMetrics(f);
323
324         // Here we use the font width cache instead of
325         //   textwidth = fontMetrics().width(str);
326         // because the above is awfully expensive on MacOSX
327         int const textwidth = fm.width(s);
328
329         if (!isDrawingEnabled())
330                 return textwidth;
331
332         textDecoration(f, x, y, textwidth);
333
334         if (use_pixmap_cache_) {
335                 QPixmap pm;
336                 QString key = generateStringSignature(str, f);
337
338                 // Warning: Left bearing is in general negative! Only the case
339                 // where left bearing is negative is of interest WRT the
340                 // pixmap width and the text x-position.
341                 // Only the left bearing of the first character is important
342                 // as we always write from left to right, even for
343                 // right-to-left languages.
344                 int const lb = min(fm.lbearing(s[0]), 0);
345                 int const mA = fm.maxAscent();
346                 if (QPixmapCache::find(key, pm)) {
347                         // Draw the cached pixmap.
348                         drawPixmap(x + lb, y - mA, pm);
349                         return textwidth;
350                 }
351
352                 // Only the right bearing of the last character is
353                 // important as we always write from left to right,
354                 // even for right-to-left languages.
355                 int const rb = fm.rbearing(s[s.size()-1]);
356                 int const w = textwidth + rb - lb;
357                 int const mD = fm.maxDescent();
358                 int const h = mA + mD;
359                 if (w > 0 && h > 0) {
360                         pm = QPixmap(static_cast<int>(pixelRatio() * w),
361                                                  static_cast<int>(pixelRatio() * h));
362 #if QT_VERSION >= 0x050000
363                         pm.setDevicePixelRatio(pixelRatio());
364 #endif
365                         pm.fill(Qt::transparent);
366                         GuiPainter p(&pm, pixelRatio());
367                         p.setQPainterPen(computeColor(f.realColor()));
368                         if (p.font() != ff)
369                                 p.setFont(ff);
370                         // We need to draw the text as LTR as we use our own bidi code.
371                         p.setLayoutDirection(Qt::LeftToRight);
372                         p.drawText(-lb, mA, str);
373                         QPixmapCache::insert(key, pm);
374                         //LYXERR(Debug::PAINTING, "h=" << h << "  mA=" << mA << "  mD=" << mD
375                         //      << "  w=" << w << "  lb=" << lb << "  tw=" << textwidth
376                         //      << "  rb=" << rb);
377
378                         // Draw the new cached pixmap.
379                         drawPixmap(x + lb, y - mA, pm);
380
381                         return textwidth;
382                 }
383         }
384
385         // don't use the pixmap cache,
386         // draw directly onto the painting device
387         setQPainterPen(computeColor(f.realColor()));
388         if (font() != ff)
389                 setFont(ff);
390
391          /* In LyX, the character direction is forced by the language.
392           * Therefore, we have to signal that fact to Qt.
393           */
394 #if 1
395         /* Use unicode override characters to enforce drawing direction
396          * Source: http://www.iamcal.com/understanding-bidirectional-text/
397          */
398         if (rtl)
399                 // Right-to-left override: forces to draw text right-to-left
400                 str = QChar(0x202E) + str;
401         else
402                 // Left-to-right override: forces to draw text left-to-right
403                 str =  QChar(0x202D) + str;
404         drawText(x, y, str);
405 #else
406         /* This looks like a cleaner solution, but it has drawbacks
407          * - does not work reliably (Mac OS X, ...)
408          * - it is not really documented
409          * Keep it here for now, in case it can be helpful
410          */
411         //This is much stronger than setLayoutDirection.
412         int flag = rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight;
413         drawText(x + (rtl ? textwidth : 0), y - fm.maxAscent(), 0, 0,
414                  flag | Qt::TextDontClip,
415                  str);
416 #endif
417         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
418         //      << " at " << x << "," << y);
419         return textwidth;
420 }
421
422
423 int GuiPainter::text(int x, int y, docstring const & str, Font const & f,
424                      double const wordspacing)
425 {
426         return text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft(), wordspacing);
427 }
428
429
430 int GuiPainter::text(int x, int y, docstring const & str, Font const & f,
431                      Color other, size_type const from, size_type const to,
432                      double const wordspacing)
433 {
434         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
435         FontInfo fi = f.fontInfo();
436         bool const rtl = f.isVisibleRightToLeft();
437
438         // dimensions
439         int const ascent = fm.maxAscent();
440         int const height = fm.maxAscent() + fm.maxDescent();
441         int xmin = fm.pos2x(str, from, rtl, wordspacing);
442         int xmax = fm.pos2x(str, to, rtl, wordspacing);
443         if (xmin > xmax)
444                 swap(xmin, xmax);
445
446         // First the part in other color
447         Color const orig = fi.realColor();
448         fi.setPaintColor(other);
449         QRegion const clip(x + xmin, y - ascent, xmax - xmin, height);
450         setClipRegion(clip);
451         int const textwidth = text(x, y, str, fi, rtl, wordspacing);
452
453         // Then the part in normal color
454         // Note that in Qt5, it is not possible to use Qt::UniteClip,
455         // therefore QRegion is used.
456         fi.setPaintColor(orig);
457         QRegion region(viewport());
458         setClipRegion(region - clip);
459         text(x, y, str, fi, rtl, wordspacing);
460         setClipping(false);
461
462         return textwidth;
463 }
464
465
466 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
467 {
468         if (f.underbar() == FONT_ON)
469                 underline(f, x, y, width);
470         if (f.strikeout() == FONT_ON)
471                 strikeoutLine(f, x, y, width);
472         if (f.uuline() == FONT_ON)
473                 doubleUnderline(f, x, y, width);
474         if (f.uwave() == FONT_ON)
475                 // f.color() doesn't work on some circumstances
476                 wavyHorizontalLine(x, y, width,  f.realColor().baseColor);
477 }
478
479
480 static int max(int a, int b) { return a > b ? a : b; }
481
482
483 void GuiPainter::button(int x, int y, int w, int h, bool mouseHover)
484 {
485         if (mouseHover)
486                 fillRectangle(x, y, w, h, Color_buttonhoverbg);
487         else
488                 fillRectangle(x, y, w, h, Color_buttonbg);
489         buttonFrame(x, y, w, h);
490 }
491
492
493 void GuiPainter::buttonFrame(int x, int y, int w, int h)
494 {
495         line(x, y, x, y + h - 1, Color_buttonframe);
496         line(x - 1 + w, y, x - 1 + w, y + h - 1, Color_buttonframe);
497         line(x, y - 1, x - 1 + w, y - 1, Color_buttonframe);
498         line(x, y + h - 1, x - 1 + w, y + h - 1, Color_buttonframe);
499 }
500
501
502 void GuiPainter::rectText(int x, int y, docstring const & str,
503         FontInfo const & font, Color back, Color frame)
504 {
505         int width;
506         int ascent;
507         int descent;
508
509         FontMetrics const & fm = theFontMetrics(font);
510         fm.rectText(str, width, ascent, descent);
511
512         if (back != Color_none)
513                 fillRectangle(x + 1, y - ascent + 1, width - 1,
514                               ascent + descent - 1, back);
515
516         if (frame != Color_none)
517                 rectangle(x, y - ascent, width, ascent + descent, frame);
518
519         text(x + 3, y, str, font);
520 }
521
522
523 void GuiPainter::buttonText(int x, int y, docstring const & str,
524         FontInfo const & font, bool mouseHover)
525 {
526         int width;
527         int ascent;
528         int descent;
529
530         FontMetrics const & fm = theFontMetrics(font);
531         fm.buttonText(str, width, ascent, descent);
532
533         static int const d = Inset::TEXT_TO_INSET_OFFSET / 2;
534
535         button(x + d, y - ascent, width - d, descent + ascent, mouseHover);
536         text(x + Inset::TEXT_TO_INSET_OFFSET, y, str, font);
537 }
538
539
540 int GuiPainter::preeditText(int x, int y, char_type c,
541         FontInfo const & font, preedit_style style)
542 {
543         FontInfo temp_font = font;
544         FontMetrics const & fm = theFontMetrics(font);
545         int ascent = fm.maxAscent();
546         int descent = fm.maxDescent();
547         int height = ascent + descent;
548         int width = fm.width(c);
549
550         switch (style) {
551                 case preedit_default:
552                         // default unselecting mode.
553                         fillRectangle(x, y - height + 1, width, height, Color_background);
554                         dashedUnderline(font, x, y - descent + 1, width);
555                         break;
556                 case preedit_selecting:
557                         // We are in selecting mode: white text on black background.
558                         fillRectangle(x, y - height + 1, width, height, Color_black);
559                         temp_font.setColor(Color_white);
560                         break;
561                 case preedit_cursor:
562                         // The character comes with a cursor.
563                         fillRectangle(x, y - height + 1, width, height, Color_background);
564                         underline(font, x, y - descent + 1, width);
565                         break;
566         }
567         text(x, y - descent + 1, c, temp_font);
568
569         return width;
570 }
571
572
573 void GuiPainter::underline(FontInfo const & f, int x, int y, int width,
574                            line_style ls)
575 {
576         FontMetrics const & fm = theFontMetrics(f);
577         int const pos = fm.underlinePos();
578
579         line(x, y + pos, x + width, y + pos,
580              f.realColor(), ls, fm.lineWidth());
581 }
582
583
584 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
585 {
586         FontMetrics const & fm = theFontMetrics(f);
587         int const pos = fm.strikeoutPos();
588
589         line(x, y - pos, x + width, y - pos,
590              f.realColor(), line_solid, fm.lineWidth());
591 }
592
593
594 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
595 {
596         FontMetrics const & fm = theFontMetrics(f);
597         int const pos1 = fm.underlinePos() + fm.lineWidth();
598         int const pos2 = fm.underlinePos() - fm.lineWidth() + 1;
599
600         line(x, y + pos1, x + width, y + pos1,
601                  f.realColor(), line_solid, fm.lineWidth());
602         line(x, y + pos2, x + width, y + pos2,
603                  f.realColor(), line_solid, fm.lineWidth());
604 }
605
606
607 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
608 {
609         FontMetrics const & fm = theFontMetrics(f);
610
611         int const below = max(fm.maxDescent() / 2, 2);
612         int height = max((fm.maxDescent() / 4) - 1, 1);
613
614         if (height >= 2)
615                 height += below;
616
617         for (int n = 0; n != height; ++n)
618                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
619 }
620
621
622 void GuiPainter::wavyHorizontalLine(int x, int y, int width, ColorCode col)
623 {
624         setQPainterPen(computeColor(col));
625         int const step = 2;
626         int const xend = x + width;
627         int height = 1;
628         //FIXME: I am not sure if Antialiasing gives the best effect.
629         //setRenderHint(Antialiasing, true);
630         while (x < xend) {
631                 height = - height;
632                 drawLine(x, y - height, x + step, y + height);
633                 x += step;
634                 drawLine(x, y + height, x + step/2, y + height);
635                 x += step/2;
636         }
637         //setRenderHint(Antialiasing, false);
638 }
639
640 } // namespace frontend
641 } // namespace lyx