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