]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiFontMetrics.cpp
QDialogButtonBox for the remaining dialogs.
[lyx.git] / src / frontends / qt4 / GuiFontMetrics.cpp
1 /**
2  * \file GuiFontMetrics.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author unknown
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "GuiFontMetrics.h"
15
16 #include "qt_helpers.h"
17
18 #include "Dimension.h"
19
20 #include "support/convert.h"
21 #include "support/lassert.h"
22 #include "support/lyxlib.h"
23
24 #define DISABLE_PMPROF
25 #include "support/pmprof.h"
26
27 #include <QByteArray>
28
29 using namespace std;
30 using namespace lyx::support;
31
32 namespace std {
33
34 /*
35  * Argument-dependent lookup implies that this function shall be
36  * declared in the namespace of its argument. But this is std
37  * namespace, since lyx::docstring is just std::basic_string<wchar_t>.
38  */
39 uint qHash(lyx::docstring const & s)
40 {
41         return qHash(QByteArray(reinterpret_cast<char const *>(s.data()),
42                                 s.size() * sizeof(lyx::docstring::value_type)));
43 }
44
45 } // namespace std
46
47 namespace lyx {
48 namespace frontend {
49
50
51 /*
52  * Limit (strwidth|breakat)_cache_ size to 512kB of string data.
53  * Limit qtextlayout_cache_ size to 500 elements (we do not know the
54  * size of the QTextLayout objects anyway).
55  * Note that all these numbers are arbitrary.
56  * Also, setting size to 0 is tantamount to disabling the cache.
57  */
58 int cache_metrics_width_size = 1 << 19;
59 int cache_metrics_breakat_size = 1 << 19;
60 // Qt 5.x already has its own caching of QTextLayout objects
61 // but it does not seem to work well on MacOS X.
62 #if (QT_VERSION < 0x050000) || defined(Q_OS_MAC)
63 int cache_metrics_qtextlayout_size = 500;
64 #else
65 int cache_metrics_qtextlayout_size = 0;
66 #endif
67
68
69 namespace {
70 /**
71  * Convert a UCS4 character into a QChar.
72  * This is a hack (it does only make sense for the common part of the UCS4
73  * and UTF16 encodings) and should not be used.
74  * This does only exist because of performance reasons (a real conversion
75  * using iconv is too slow on windows).
76  *
77  * This is no real conversion but a simple cast in reality. This is the reason
78  * why this works well for symbol fonts used in mathed too, even though
79  * these are not real ucs4 characters. These are codepoints in the
80  * computer modern fonts used, nothing unicode related.
81  * See comment in GuiPainter::text() for more explanation.
82  **/
83 inline QChar const ucs4_to_qchar(char_type const ucs4)
84 {
85         LATTEST(is_utf16(ucs4));
86         return QChar(static_cast<unsigned short>(ucs4));
87 }
88 } // namespace
89
90
91 GuiFontMetrics::GuiFontMetrics(QFont const & font)
92         : font_(font), metrics_(font, 0),
93           strwidth_cache_(cache_metrics_width_size),
94           breakat_cache_(cache_metrics_breakat_size),
95           qtextlayout_cache_(cache_metrics_qtextlayout_size)
96 {
97 }
98
99
100 int GuiFontMetrics::maxAscent() const
101 {
102         return metrics_.ascent();
103 }
104
105
106 int GuiFontMetrics::maxDescent() const
107 {
108         // We add 1 as the value returned by QT is different than X
109         // See http://doc.trolltech.com/2.3/qfontmetrics.html#200b74
110         return metrics_.descent() + 1;
111 }
112
113
114 int GuiFontMetrics::em() const
115 {
116         return QFontInfo(font_).pixelSize();
117 }
118
119
120 int GuiFontMetrics::lineWidth() const
121 {
122         return metrics_.lineWidth();
123 }
124
125
126 int GuiFontMetrics::underlinePos() const
127 {
128         return metrics_.underlinePos();
129 }
130
131
132 int GuiFontMetrics::strikeoutPos() const
133 {
134         return metrics_.strikeOutPos();
135 }
136
137
138 int GuiFontMetrics::lbearing(char_type c) const
139 {
140         if (!is_utf16(c))
141                 // FIXME: QFontMetrics::leftBearing does not support the
142                 //        full unicode range. Once it does, we could use:
143                 //return metrics_.leftBearing(toqstr(docstring(1, c)));
144                 return 0;
145
146         return metrics_.leftBearing(ucs4_to_qchar(c));
147 }
148
149
150 namespace {
151 int const outOfLimitMetric = -10000;
152 }
153
154
155 int GuiFontMetrics::rbearing(char_type c) const
156 {
157         int value = rbearing_cache_.value(c, outOfLimitMetric);
158         if (value != outOfLimitMetric)
159                 return value;
160
161         // Qt rbearing is from the right edge of the char's width().
162         if (is_utf16(c)) {
163                 QChar sc = ucs4_to_qchar(c);
164                 value = width(c) - metrics_.rightBearing(sc);
165         } else {
166                 // FIXME: QFontMetrics::leftBearing does not support the
167                 //        full unicode range. Once it does, we could use:
168                 // metrics_.rightBearing(toqstr(docstring(1, c)));
169                 value = width(c);
170         }
171
172         rbearing_cache_.insert(c, value);
173
174         return value;
175 }
176
177
178 int GuiFontMetrics::width(docstring const & s) const
179 {
180         PROFILE_THIS_BLOCK(width);
181         if (strwidth_cache_.contains(s))
182                 return strwidth_cache_[s];
183         PROFILE_CACHE_MISS(width);
184         /* Several problems have to be taken into account:
185          * * QFontMetrics::width does not returns a wrong value with Qt5 with
186          *   some arabic text, since the glyph-shaping operations are not
187          *   done (documented in Qt5).
188          * * QTextLayout is broken for single characters with null width
189          *   (like \not in mathed).
190          * * While QTextLine::horizontalAdvance is the right thing to use
191      *   for text strings, it does not give a good result with some
192      *   characters like the \int (gyph 4) of esint.
193
194          * Also, as a safety measure, always use QFontMetrics::width with
195          * our math fonts.
196         */
197         int w = 0;
198         // is the string a single character from a math font ?
199 #if QT_VERSION >= 0x040800
200         bool const math_char = s.length() == 1 || font_.styleName() == "LyX";
201 #else
202         bool const math_char = s.length() == 1;
203 #endif
204         // keep value 0 for math chars with width 0
205         if (!math_char || metrics_.width(toqstr(s)) != 0) {
206                 QTextLayout tl;
207                 tl.setText(toqstr(s));
208                 tl.setFont(font_);
209                 tl.beginLayout();
210                 QTextLine line = tl.createLine();
211                 tl.endLayout();
212                 if (math_char)
213                         w = iround(line.naturalTextWidth());
214                 else
215                         w = iround(line.horizontalAdvance());
216         }
217         strwidth_cache_.insert(s, w, s.size() * sizeof(char_type));
218         return w;
219 }
220
221
222 int GuiFontMetrics::width(QString const & ucs2) const
223 {
224         return width(qstring_to_ucs4(ucs2));
225 }
226
227
228 int GuiFontMetrics::signedWidth(docstring const & s) const
229 {
230         if (s.empty())
231                 return 0;
232
233         if (s[0] == '-')
234                 return -width(s.substr(1, s.size() - 1));
235         else
236                 return width(s);
237 }
238
239
240 shared_ptr<QTextLayout const>
241 GuiFontMetrics::getTextLayout(docstring const & s, bool const rtl,
242                               double const wordspacing) const
243 {
244         PROFILE_THIS_BLOCK(getTextLayout);
245         docstring const s_cache =
246                 s + (rtl ? "r" : "l") + convert<docstring>(wordspacing);
247         if (auto ptl = qtextlayout_cache_[s_cache])
248                 return ptl;
249         PROFILE_CACHE_MISS(getTextLayout);
250         auto const ptl = make_shared<QTextLayout>();
251         ptl->setCacheEnabled(true);
252         ptl->setText(toqstr(s));
253         QFont copy = font_;
254         copy.setWordSpacing(wordspacing);
255         ptl->setFont(copy);
256         // Note that both setFlags and the enums are undocumented
257         ptl->setFlags(rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight);
258         ptl->beginLayout();
259         ptl->createLine();
260         ptl->endLayout();
261         qtextlayout_cache_.insert(s_cache, ptl);
262         return ptl;
263 }
264
265
266 int GuiFontMetrics::pos2x(docstring const & s, int pos, bool const rtl,
267                           double const wordspacing) const
268 {
269         if (pos <= 0)
270                 pos = 0;
271         shared_ptr<QTextLayout const> tl = getTextLayout(s, rtl, wordspacing);
272         /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
273          * not be the same when there are high-plan unicode characters
274          * (bug #10443).
275          */
276         int const qpos = toqstr(s.substr(0, pos)).length();
277         return static_cast<int>(tl->lineForTextPosition(qpos).cursorToX(qpos));
278 }
279
280
281 int GuiFontMetrics::x2pos(docstring const & s, int & x, bool const rtl,
282                           double const wordspacing) const
283 {
284         shared_ptr<QTextLayout const> tl = getTextLayout(s, rtl, wordspacing);
285         QTextLine const & tline = tl->lineForTextPosition(0);
286         int qpos = tline.xToCursor(x);
287         int newx = static_cast<int>(tline.cursorToX(qpos));
288         // The value of qpos may be wrong in rtl text (see ticket #10569).
289         // To work around this, let's have a look at adjacent positions to
290         // see whether we find closer matches.
291         if (rtl && newx < x) {
292                 while (qpos > 0) {
293                         int const xm = static_cast<int>(tline.cursorToX(qpos - 1));
294                         if (abs(xm - x) < abs(newx - x)) {
295                                 --qpos;
296                                 newx = xm;
297                         } else
298                                 break;
299                 }
300         } else if (rtl && newx > x) {
301                 while (qpos < tline.textLength()) {
302                         int const xp = static_cast<int>(tline.cursorToX(qpos + 1));
303                         if (abs(xp - x) < abs(newx - x)) {
304                                 ++qpos;
305                                 newx = xp;
306                         } else
307                                 break;
308                 }
309         }
310         // correct x value to the actual cursor position.
311         x = newx;
312
313         /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
314          * not be the same when there are high-plan unicode characters
315          * (bug #10443).
316          */
317 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
318         return qstring_to_ucs4(tl->text().left(qpos)).length();
319 #else
320         /* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
321          * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
322          * length. We work around the problem by trying all docstring
323          * positions until the right one is found. This is slow only if
324          * there are many high-plane Unicode characters. It might be
325          * worthwhile to implement a dichotomy search if this shows up
326          * under a profiler.
327          */
328         int pos = min(qpos, static_cast<int>(s.length()));
329         while (pos >= 0 && toqstr(s.substr(0, pos)).length() != qpos)
330                 --pos;
331         LASSERT(pos > 0 || qpos == 0, /**/);
332         return pos;
333 #endif
334 }
335
336
337 int GuiFontMetrics::countExpanders(docstring const & str) const
338 {
339         // Numbers of characters that are expanded by inter-word spacing.  These
340         // characters are spaces, except for characters 09-0D which are treated
341         // specially.  (From a combination of testing with the notepad found in qt's
342         // examples, and reading the source code.)  In addition, consecutive spaces
343         // only count as one expander.
344         bool wasspace = false;
345         int nexp = 0;
346         for (char_type c : str)
347                 if (c > 0x0d && QChar(c).isSpace()) {
348                         if (!wasspace) {
349                                 ++nexp;
350                                 wasspace = true;
351                         }
352                 } else
353                         wasspace = false;
354         return nexp;
355 }
356
357
358 pair<int, int>
359 GuiFontMetrics::breakAt_helper(docstring const & s, int const x,
360                                bool const rtl, bool const force) const
361 {
362         QTextLayout tl;
363         /* Qt will not break at a leading or trailing space, and we need
364          * that sometimes, see http://www.lyx.org/trac/ticket/9921.
365          *
366          * To work around the problem, we enclose the string between
367          * zero-width characters so that the QTextLayout algorithm will
368          * agree to break the text at these extremal spaces.
369          */
370         // Unicode character ZERO WIDTH NO-BREAK SPACE
371         QChar const zerow_nbsp(0xfeff);
372         QString qs = zerow_nbsp + toqstr(s) + zerow_nbsp;
373 #if 1
374         /* Use unicode override characters to enforce drawing direction
375          * Source: http://www.iamcal.com/understanding-bidirectional-text/
376          */
377         if (rtl)
378                 // Right-to-left override: forces to draw text right-to-left
379                 qs = QChar(0x202E) + qs;
380         else
381                 // Left-to-right override: forces to draw text left-to-right
382                 qs =  QChar(0x202D) + qs;
383         int const offset = 2;
384 #else
385         // Alternative version that breaks with Qt5 and arabic text (#10436)
386         // Note that both setFlags and the enums are undocumented
387         tl.setFlags(rtl ? Qt::TextForceRightToLeft : Qt::TextForceLeftToRight);
388         int const offset = 1;
389 #endif
390
391         tl.setText(qs);
392         tl.setFont(font_);
393         QTextOption to;
394         to.setWrapMode(force ? QTextOption::WrapAtWordBoundaryOrAnywhere
395                              : QTextOption::WordWrap);
396         tl.setTextOption(to);
397         tl.beginLayout();
398         QTextLine line = tl.createLine();
399         line.setLineWidth(x);
400         tl.createLine();
401         tl.endLayout();
402         int const line_wid = iround(line.horizontalAdvance());
403         if ((force && line.textLength() == offset) || line_wid > x)
404                 return {-1, -1};
405         /* Since QString is UTF-16 and docstring is UCS-4, the offsets may
406          * not be the same when there are high-plan unicode characters
407          * (bug #10443).
408          */
409         // The variable `offset' is here to account for the extra leading characters.
410         // The ending character zerow_nbsp has to be ignored if the line is complete.
411         int const qlen = line.textLength() - offset - (line.textLength() == qs.length());
412 #if QT_VERSION < 0x040801 || QT_VERSION >= 0x050100
413         int len = qstring_to_ucs4(qs.mid(offset, qlen)).length();
414 #else
415         /* Due to QTBUG-25536 in 4.8.1 <= Qt < 5.1.0, the string returned
416          * by QString::toUcs4 (used by qstring_to_ucs4) may have wrong
417          * length. We work around the problem by trying all docstring
418          * positions until the right one is found. This is slow only if
419          * there are many high-plane Unicode characters. It might be
420          * worthwhile to implement a dichotomy search if this shows up
421          * under a profiler.
422          */
423         int len = min(qlen, static_cast<int>(s.length()));
424         while (len >= 0 && toqstr(s.substr(0, len)).length() != qlen)
425                 --len;
426         LASSERT(len > 0 || qlen == 0, /**/);
427 #endif
428         return {len, line_wid};
429 }
430
431
432 bool GuiFontMetrics::breakAt(docstring & s, int & x, bool const rtl, bool const force) const
433 {
434         PROFILE_THIS_BLOCK(breakAt);
435         if (s.empty())
436                 return false;
437
438         docstring const s_cache =
439                 s + convert<docstring>(x) + (rtl ? "r" : "l") + (force ? "f" : "w");
440         pair<int, int> pp;
441
442         if (breakat_cache_.contains(s_cache))
443                 pp = breakat_cache_[s_cache];
444         else {
445                 PROFILE_CACHE_MISS(breakAt);
446                 pp = breakAt_helper(s, x, rtl, force);
447                 breakat_cache_.insert(s_cache, pp, s_cache.size() * sizeof(char_type));
448         }
449         if (pp.first == -1)
450                 return false;
451         s = s.substr(0, pp.first);
452         x = pp.second;
453         return true;
454 }
455
456
457 void GuiFontMetrics::rectText(docstring const & str,
458         int & w, int & ascent, int & descent) const
459 {
460         // FIXME: let offset depend on font (this is Inset::TEXT_TO_OFFSET)
461         int const offset = 4;
462
463         w = width(str) + offset;
464         ascent = metrics_.ascent() + offset / 2;
465         descent = metrics_.descent() + offset / 2;
466 }
467
468
469 void GuiFontMetrics::buttonText(docstring const & str, const int offset,
470         int & w, int & ascent, int & descent) const
471 {
472         rectText(str, w, ascent, descent);
473         w += offset;
474 }
475
476
477 Dimension const GuiFontMetrics::defaultDimension() const
478 {
479         return Dimension(0, maxAscent(), maxDescent());
480 }
481
482
483 Dimension const GuiFontMetrics::dimension(char_type c) const
484 {
485         return Dimension(width(c), ascent(c), descent(c));
486 }
487
488
489 GuiFontMetrics::AscendDescend const GuiFontMetrics::fillMetricsCache(
490                 char_type c) const
491 {
492         QRect r;
493         if (is_utf16(c))
494                 r = metrics_.boundingRect(ucs4_to_qchar(c));
495         else
496                 r = metrics_.boundingRect(toqstr(docstring(1, c)));
497
498         AscendDescend ad = { -r.top(), r.bottom() + 1};
499         // We could as well compute the width but this is not really
500         // needed for now as it is done directly in width() below.
501         metrics_cache_.insert(c, ad);
502
503         return ad;
504 }
505
506
507 int GuiFontMetrics::width(char_type c) const
508 {
509         int value = width_cache_.value(c, outOfLimitMetric);
510         if (value != outOfLimitMetric)
511                 return value;
512
513         if (is_utf16(c))
514                 value = metrics_.width(ucs4_to_qchar(c));
515         else
516                 value = metrics_.width(toqstr(docstring(1, c)));
517
518         width_cache_.insert(c, value);
519
520         return value;
521 }
522
523
524 int GuiFontMetrics::ascent(char_type c) const
525 {
526         static AscendDescend const outOfLimitAD =
527                 {outOfLimitMetric, outOfLimitMetric};
528         AscendDescend value = metrics_cache_.value(c, outOfLimitAD);
529         if (value.ascent != outOfLimitMetric)
530                 return value.ascent;
531
532         value = fillMetricsCache(c);
533         return value.ascent;
534 }
535
536
537 int GuiFontMetrics::descent(char_type c) const
538 {
539         static AscendDescend const outOfLimitAD =
540                 {outOfLimitMetric, outOfLimitMetric};
541         AscendDescend value = metrics_cache_.value(c, outOfLimitAD);
542         if (value.descent != outOfLimitMetric)
543                 return value.descent;
544
545         value = fillMetricsCache(c);
546         return value.descent;
547 }
548
549 } // namespace frontend
550 } // namespace lyx