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