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