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