]> git.lyx.org Git - features.git/blob - src/frontends/qt4/GuiPainter.cpp
986ac4672aecc499126d535006166ae84bd9a447
[features.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)
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         line_style ls,
200         float lw)
201 {
202         if (!isDrawingEnabled())
203                 return;
204
205         // double the size if needed
206         // FIXME THREAD
207         static QVector<QPoint> points(32);
208         if (np > points.size())
209                 points.resize(2 * np);
210
211         bool antialias = false;
212         for (int i = 0; i < np; ++i) {
213                 points[i].setX(xp[i]);
214                 points[i].setY(yp[i]);
215                 if (i != 0)
216                         antialias |= xp[i-1] != xp[i] && yp[i-1] != yp[i];
217         }
218         setQPainterPen(computeColor(col), ls, lw);
219         bool const text_is_antialiased = renderHints() & TextAntialiasing;
220         setRenderHint(Antialiasing, antialias && text_is_antialiased);
221         drawPolyline(points.data(), np);
222         setRenderHint(Antialiasing, false);
223 }
224
225
226 void GuiPainter::rectangle(int x, int y, int w, int h,
227         Color col,
228         line_style ls,
229         float lw)
230 {
231         if (!isDrawingEnabled())
232                 return;
233
234         setQPainterPen(computeColor(col), ls, lw);
235         drawRect(x, y, w, h);
236 }
237
238
239 void GuiPainter::fillRectangle(int x, int y, int w, int h, Color col)
240 {
241         if (!isDrawingEnabled())
242                 return;
243
244         fillRect(x, y, w, h, guiApp->colorCache().get(col));
245 }
246
247
248 void GuiPainter::arc(int x, int y, unsigned int w, unsigned int h,
249         int a1, int a2, Color col)
250 {
251         if (!isDrawingEnabled())
252                 return;
253
254         // LyX usings 1/64ths degree, Qt usings 1/16th
255         setQPainterPen(computeColor(col));
256         bool const do_antialiasing = renderHints() & TextAntialiasing;
257         setRenderHint(Antialiasing, do_antialiasing);
258         drawArc(x, y, w, h, a1 / 4, a2 / 4);
259         setRenderHint(Antialiasing, false);
260 }
261
262
263 void GuiPainter::image(int x, int y, int w, int h, graphics::Image const & i)
264 {
265         graphics::GuiImage const & qlimage =
266                 static_cast<graphics::GuiImage const &>(i);
267
268         fillRectangle(x, y, w, h, Color_graphicsbg);
269
270         if (!isDrawingEnabled())
271                 return;
272
273         QImage const image = qlimage.image();
274         QRectF const drect = QRectF(x, y, w, h);
275         QRectF const srect = QRectF(0, 0, image.width(), image.height());
276         drawImage(drect, image, srect);
277 }
278
279
280 int GuiPainter::text(int x, int y, char_type c, FontInfo const & f)
281 {
282         return text(x, y, docstring(1, c), f);
283 }
284
285
286 int GuiPainter::text(int x, int y, docstring const & s,
287                      FontInfo const & f, bool const rtl)
288 {
289         //LYXERR0("text: x=" << x << ", s=" << s);
290         if (s.empty())
291                 return 0;
292
293         /* Caution: The following ucs4 to QString conversions work for symbol fonts
294         only because they are no real conversions but simple casts in reality.
295         When we want to draw a symbol or calculate the metrics we pass the position
296         of the symbol in the font (as given in lib/symbols) as a char_type to the
297         frontend. This is just wrong, because the symbol is no UCS4 character at
298         all. You can think of this number as the code point of the symbol in a
299         custom symbol encoding. It works because this char_type is lateron again
300         interpreted as a position in the font again.
301         The correct solution would be to have extra functions for symbols, but that
302         would require to duplicate a lot of frontend and mathed support code.
303         */
304         QString str = toqstr(s);
305
306 #if 0
307         // HACK: QT3 refuses to show single compose characters
308         //       Still needed with Qt4?
309         if (ls == 1 && str[0].unicode() >= 0x05b0 && str[0].unicode() <= 0x05c2)
310                 str = ' ' + str;
311 #endif
312
313         QFont const & ff = getFont(f);
314         GuiFontMetrics const & fm = getFontMetrics(f);
315
316         // Here we use the font width cache instead of
317         //   textwidth = fontMetrics().width(str);
318         // because the above is awfully expensive on MacOSX
319         int const textwidth = fm.width(s);
320
321         if (!isDrawingEnabled())
322                 return textwidth;
323
324         textDecoration(f, x, y, textwidth);
325
326         // Qt4 does not display a glyph whose codepoint is the
327         // same as that of a soft-hyphen (0x00ad), unless it
328         // occurs at a line-break. As a kludge, we force Qt to
329         // render this glyph using a one-column line.
330         // This is needed for some math glyphs.
331         // Should the soft hyphen char be displayed at all?
332         // I don't think so (i.e., Qt is correct as far as
333         // texted is concerned). /spitz
334         if (s.size() == 1 && str[0].unicode() == 0x00ad) {
335                 setQPainterPen(computeColor(f.realColor()));
336                 QTextLayout adsymbol(str);
337                 adsymbol.setFont(ff);
338                 adsymbol.beginLayout();
339                 QTextLine line = adsymbol.createLine();
340                 line.setNumColumns(1);
341                 line.setPosition(QPointF(0, -line.ascent()));
342                 adsymbol.endLayout();
343                 line.draw(this, QPointF(x, y));
344                 return textwidth;
345         }
346
347         if (use_pixmap_cache_) {
348                 QPixmap pm;
349                 QString key = generateStringSignature(str, f);
350
351                 // Warning: Left bearing is in general negative! Only the case
352                 // where left bearing is negative is of interest WRT the
353                 // pixmap width and the text x-position.
354                 // Only the left bearing of the first character is important
355                 // as we always write from left to right, even for
356                 // right-to-left languages.
357                 int const lb = min(fm.lbearing(s[0]), 0);
358                 int const mA = fm.maxAscent();
359                 if (QPixmapCache::find(key, pm)) {
360                         // Draw the cached pixmap.
361                         drawPixmap(x + lb, y - mA, pm);
362                         return textwidth;
363                 }
364
365                 // Only the right bearing of the last character is
366                 // important as we always write from left to right,
367                 // even for right-to-left languages.
368                 int const rb = fm.rbearing(s[s.size()-1]);
369                 int const w = textwidth + rb - lb;
370                 int const mD = fm.maxDescent();
371                 int const h = mA + mD;
372                 if (w > 0 && h > 0) {
373                         pm = QPixmap(pixelRatio() * w , pixelRatio() * h);
374 #if QT_VERSION > 0x050000
375                         pm.setDevicePixelRatio(pixelRatio());
376 #endif
377                         pm.fill(Qt::transparent);
378                         GuiPainter p(&pm, pixelRatio());
379                         p.setQPainterPen(computeColor(f.realColor()));
380                         if (p.font() != ff)
381                                 p.setFont(ff);
382                         // We need to draw the text as LTR as we use our own bidi code.
383                         p.setLayoutDirection(Qt::LeftToRight);
384                         p.drawText(-lb, mA, str);
385                         QPixmapCache::insert(key, pm);
386                         //LYXERR(Debug::PAINTING, "h=" << h << "  mA=" << mA << "  mD=" << mD
387                         //      << "  w=" << w << "  lb=" << lb << "  tw=" << textwidth
388                         //      << "  rb=" << rb);
389
390                         // Draw the new cached pixmap.
391                         drawPixmap(x + lb, y - mA, pm);
392
393                         return textwidth;
394                 }
395         }
396
397         // don't use the pixmap cache,
398         // draw directly onto the painting device
399         setQPainterPen(computeColor(f.realColor()));
400         if (font() != ff)
401                 setFont(ff);
402
403          /* In LyX, the character direction is forced by the language.
404           * Therefore, we have to signal that fact to Qt.
405           */
406 #ifdef USE_RTL_OVERRIDE
407         /* Use unicode override characters to enforce drawing direction
408          * Source: http://www.iamcal.com/understanding-bidirectional-text/
409          */
410         if (rtl)
411                 // Right-to-left override: forces to draw text right-to-left
412                 str = QChar(0x202E) + str;
413         else
414                 // Left-to-right override: forces to draw text left-to-right
415                 str =  QChar(0x202D) + str;
416         drawText(x, y, str);
417 #else
418         /* This is a cleanr solution, but it has two drawbacks
419          * - it seems that it does not work under Mac OS X
420          * - it is not really documented
421          */
422         //This is much stronger than setLayoutDirection.
423         int flag = rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight;
424         drawText(x + (rtl ? textwidth : 0), y - fm.maxAscent(), 0, 0,
425                  flag | Qt::TextDontClip,
426                  str);
427 #endif
428         //LYXERR(Debug::PAINTING, "draw " << string(str.toUtf8())
429         //      << " at " << x << "," << y);
430         return textwidth;
431 }
432
433
434 int GuiPainter::text(int x, int y, docstring const & str, Font const & f)
435 {
436         return text(x, y, str, f.fontInfo(), f.isVisibleRightToLeft());
437 }
438
439
440 int GuiPainter::text(int x, int y, docstring const & str, Font const & f,
441                      Color other, size_type from, size_type to)
442 {
443         GuiFontMetrics const & fm = getFontMetrics(f.fontInfo());
444         FontInfo fi = f.fontInfo();
445         bool const rtl = f.isVisibleRightToLeft();
446
447         // dimensions
448         int const ascent = fm.maxAscent();
449         int const height = fm.maxAscent() + fm.maxDescent();
450         int xmin = fm.pos2x(str, from, rtl);
451         int xmax = fm.pos2x(str, to, rtl);
452         if (xmin > xmax)
453                 swap(xmin, xmax);
454
455         // First the part in other color
456         Color const orig = fi.realColor();
457         fi.setPaintColor(other);
458         setClipRect(QRect(x + xmin, y - ascent, xmax - xmin, height));
459         int const textwidth = text(x, y, str, fi, rtl);
460
461         // Then the part in normal color
462         // Note that in Qt5, it is not possible to use Qt::UniteClip
463         fi.setPaintColor(orig);
464         setClipRect(QRect(x, y - ascent, xmin, height));
465         text(x, y, str, fi, rtl);
466         setClipRect(QRect(x + xmax, y - ascent, textwidth - xmax, height));
467         text(x, y, str, fi, rtl);
468         setClipping(false);
469
470         return textwidth;
471 }
472
473
474 void GuiPainter::textDecoration(FontInfo const & f, int x, int y, int width)
475 {
476         if (f.underbar() == FONT_ON)
477                 underline(f, x, y, width);
478         if (f.strikeout() == FONT_ON)
479                 strikeoutLine(f, x, y, width);
480         if (f.uuline() == FONT_ON)
481                 doubleUnderline(f, x, y, width);
482         if (f.uwave() == FONT_ON)
483                 // f.color() doesn't work on some circumstances
484                 wavyHorizontalLine(x, y, width,  f.realColor().baseColor);
485 }
486
487
488 static int max(int a, int b) { return a > b ? a : b; }
489
490
491 void GuiPainter::button(int x, int y, int w, int h, bool mouseHover)
492 {
493         if (mouseHover)
494                 fillRectangle(x, y, w, h, Color_buttonhoverbg);
495         else
496                 fillRectangle(x, y, w, h, Color_buttonbg);
497         buttonFrame(x, y, w, h);
498 }
499
500
501 void GuiPainter::buttonFrame(int x, int y, int w, int h)
502 {
503         line(x, y, x, y + h - 1, Color_buttonframe);
504         line(x - 1 + w, y, x - 1 + w, y + h - 1, Color_buttonframe);
505         line(x, y - 1, x - 1 + w, y - 1, Color_buttonframe);
506         line(x, y + h - 1, x - 1 + w, y + h - 1, Color_buttonframe);
507 }
508
509
510 void GuiPainter::rectText(int x, int y, docstring const & str,
511         FontInfo const & font, Color back, Color frame)
512 {
513         int width;
514         int ascent;
515         int descent;
516
517         FontMetrics const & fm = theFontMetrics(font);
518         fm.rectText(str, width, ascent, descent);
519
520         if (back != Color_none)
521                 fillRectangle(x + 1, y - ascent + 1, width - 1,
522                               ascent + descent - 1, back);
523
524         if (frame != Color_none)
525                 rectangle(x, y - ascent, width, ascent + descent, frame);
526
527         text(x + 3, y, str, font);
528 }
529
530
531 void GuiPainter::buttonText(int x, int y, docstring const & str,
532         FontInfo const & font, bool mouseHover)
533 {
534         int width;
535         int ascent;
536         int descent;
537
538         FontMetrics const & fm = theFontMetrics(font);
539         fm.buttonText(str, width, ascent, descent);
540
541         static int const d = Inset::TEXT_TO_INSET_OFFSET / 2;
542
543         button(x + d, y - ascent, width - d, descent + ascent, mouseHover);
544         text(x + Inset::TEXT_TO_INSET_OFFSET, y, str, font);
545 }
546
547
548 int GuiPainter::preeditText(int x, int y, char_type c,
549         FontInfo const & font, preedit_style style)
550 {
551         FontInfo temp_font = font;
552         FontMetrics const & fm = theFontMetrics(font);
553         int ascent = fm.maxAscent();
554         int descent = fm.maxDescent();
555         int height = ascent + descent;
556         int width = fm.width(c);
557
558         switch (style) {
559                 case preedit_default:
560                         // default unselecting mode.
561                         fillRectangle(x, y - height + 1, width, height, Color_background);
562                         dashedUnderline(font, x, y - descent + 1, width);
563                         break;
564                 case preedit_selecting:
565                         // We are in selecting mode: white text on black background.
566                         fillRectangle(x, y - height + 1, width, height, Color_black);
567                         temp_font.setColor(Color_white);
568                         break;
569                 case preedit_cursor:
570                         // The character comes with a cursor.
571                         fillRectangle(x, y - height + 1, width, height, Color_background);
572                         underline(font, x, y - descent + 1, width);
573                         break;
574         }
575         text(x, y - descent + 1, c, temp_font);
576
577         return width;
578 }
579
580
581 void GuiPainter::doubleUnderline(FontInfo const & f, int x, int y, int width)
582 {
583         FontMetrics const & fm = theFontMetrics(f);
584
585         int const below = max(fm.maxDescent() / 2, 2);
586
587         line(x, y + below, x + width, y + below, f.realColor());
588         line(x, y + below - 2, x + width, y + below - 2, f.realColor());
589 }
590
591
592 void GuiPainter::underline(FontInfo const & f, int x, int y, int width)
593 {
594         FontMetrics const & fm = theFontMetrics(f);
595
596         int const below = max(fm.maxDescent() / 2, 2);
597         int const height = max((fm.maxDescent() / 4) - 1, 1);
598
599         if (height < 2)
600                 line(x, y + below, x + width, y + below, f.realColor());
601         else
602                 fillRectangle(x, y + below, width, below + height, f.realColor());
603 }
604
605
606 void GuiPainter::strikeoutLine(FontInfo const & f, int x, int y, int width)
607 {
608         FontMetrics const & fm = theFontMetrics(f);
609
610         int const middle = max((fm.maxHeight() / 4), 1);
611         int const height =  middle/3;
612
613         if (height < 2)
614                 line(x, y - middle, x + width, y - middle, f.realColor());
615         else
616                 fillRectangle(x, y - middle, width, height, f.realColor());
617 }
618
619
620 void GuiPainter::dashedUnderline(FontInfo const & f, int x, int y, int width)
621 {
622         FontMetrics const & fm = theFontMetrics(f);
623
624         int const below = max(fm.maxDescent() / 2, 2);
625         int height = max((fm.maxDescent() / 4) - 1, 1);
626
627         if (height >= 2)
628                 height += below;
629
630         for (int n = 0; n != height; ++n)
631                 line(x, y + below + n, x + width, y + below + n, f.realColor(), line_onoffdash);
632 }
633
634
635 void GuiPainter::wavyHorizontalLine(int x, int y, int width, ColorCode col)
636 {
637         setQPainterPen(computeColor(col));
638         int const step = 2;
639         int const xend = x + width;
640         int height = 1;
641         //FIXME: I am not sure if Antialiasing gives the best effect.
642         //setRenderHint(Antialiasing, true);
643         while (x < xend) {
644                 height = - height;
645                 drawLine(x, y - height, x + step, y + height);
646                 x += step;
647                 drawLine(x, y + height, x + step/2, y + height);
648                 x += step/2;
649         }
650         //setRenderHint(Antialiasing, false);
651 }
652
653 } // namespace frontend
654 } // namespace lyx