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