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