/src/libreoffice/vcl/source/text/textlayout.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <osl/file.h> |
21 | | #include <rtl/ustrbuf.hxx> |
22 | | #include <sal/log.hxx> |
23 | | #include <comphelper/processfactory.hxx> |
24 | | #include <i18nlangtag/languagetag.hxx> |
25 | | |
26 | | #include <vcl/ctrl.hxx> |
27 | | #include <vcl/svapp.hxx> |
28 | | #include <vcl/unohelp.hxx> |
29 | | |
30 | | #include <textlayout.hxx> |
31 | | #include <textlineinfo.hxx> |
32 | | |
33 | | static bool lcl_IsCharIn(sal_Unicode c, const char* pStr) |
34 | 456k | { |
35 | 913k | while ( *pStr ) |
36 | 456k | { |
37 | 456k | if ( *pStr == c ) |
38 | 190 | return true; |
39 | 456k | pStr++; |
40 | 456k | } |
41 | | |
42 | 456k | return false; |
43 | 456k | } |
44 | | |
45 | | ImplMultiTextLineInfo::ImplMultiTextLineInfo() |
46 | 0 | { |
47 | 0 | } |
48 | | |
49 | | ImplMultiTextLineInfo::~ImplMultiTextLineInfo() |
50 | 0 | { |
51 | 0 | } |
52 | | |
53 | | void ImplMultiTextLineInfo::AddLine( const ImplTextLineInfo& rLine ) |
54 | 0 | { |
55 | 0 | mvLines.push_back(rLine); |
56 | 0 | } |
57 | | |
58 | | void ImplMultiTextLineInfo::Clear() |
59 | 0 | { |
60 | 0 | mvLines.clear(); |
61 | 0 | } |
62 | | |
63 | | namespace vcl |
64 | | { |
65 | | TextLayoutCommon::~TextLayoutCommon() |
66 | 2.97k | {} |
67 | | |
68 | | OUString TextLayoutCommon::GetCenterEllipsisString(std::u16string_view rOrigStr, sal_Int32 nIndex, tools::Long nMaxWidth) |
69 | 0 | { |
70 | 0 | OUStringBuffer aTmpStr(rOrigStr); |
71 | | |
72 | | // speed it up by removing all but 1.33x as many as the break pos. |
73 | 0 | sal_Int32 nEraseChars = std::max<sal_Int32>(4, rOrigStr.size() - (nIndex*4)/3); |
74 | 0 | while(nEraseChars < static_cast<sal_Int32>(rOrigStr.size()) && GetTextWidth(aTmpStr.toString(), 0, aTmpStr.getLength()) > nMaxWidth) |
75 | 0 | { |
76 | 0 | aTmpStr = rOrigStr; |
77 | 0 | sal_Int32 i = (aTmpStr.getLength() - nEraseChars)/2; |
78 | 0 | aTmpStr.remove(i, nEraseChars++); |
79 | 0 | aTmpStr.insert(i, "..."); |
80 | 0 | } |
81 | |
|
82 | 0 | return aTmpStr.makeStringAndClear(); |
83 | 0 | } |
84 | | |
85 | | OUString TextLayoutCommon::GetEndEllipsisString(OUString const& rOrigStr, sal_Int32 nIndex, tools::Long nMaxWidth, bool bClipText) |
86 | 328 | { |
87 | 328 | OUString aStr = rOrigStr; |
88 | 328 | aStr = aStr.copy(0, nIndex); |
89 | | |
90 | 328 | if (nIndex > 1) |
91 | 274 | { |
92 | 274 | aStr += "..."; |
93 | 3.00k | while (!aStr.isEmpty() && (GetTextWidth(aStr, 0, aStr.getLength()) > nMaxWidth)) |
94 | 2.72k | { |
95 | 2.72k | if ((nIndex > 1) || (nIndex == aStr.getLength())) |
96 | 2.67k | nIndex--; |
97 | | |
98 | 2.72k | aStr = aStr.replaceAt(nIndex, 1, u""); |
99 | 2.72k | } |
100 | 274 | } |
101 | | |
102 | 328 | if (aStr.isEmpty() && bClipText) |
103 | 13 | aStr += OUStringChar(rOrigStr[0]); |
104 | | |
105 | 328 | return aStr; |
106 | 328 | } |
107 | | |
108 | | namespace |
109 | | { |
110 | | OUString lcl_GetPathEllipsisString(OUString const& rOrigStr, sal_Int32 nIndex) |
111 | 42 | { |
112 | 42 | OUString aPath(rOrigStr); |
113 | 42 | OUString aAbbreviatedPath; |
114 | 42 | osl_abbreviateSystemPath(aPath.pData, &aAbbreviatedPath.pData, nIndex, nullptr); |
115 | 42 | return aAbbreviatedPath; |
116 | 42 | } |
117 | | } |
118 | | |
119 | | OUString TextLayoutCommon::GetNewsEllipsisString(OUString const& rOrigStr, tools::Long nMaxWidth, DrawTextFlags nStyle) |
120 | 164 | { |
121 | 164 | OUString aStr = rOrigStr; |
122 | 164 | static char const pSepChars[] = "."; |
123 | | |
124 | | // Determine last section |
125 | 164 | sal_Int32 nLastContent = aStr.getLength(); |
126 | 390k | while (nLastContent) |
127 | 390k | { |
128 | 390k | nLastContent--; |
129 | | |
130 | 390k | if (lcl_IsCharIn(aStr[nLastContent], pSepChars)) |
131 | 53 | break; |
132 | 390k | } |
133 | | |
134 | 185 | while (nLastContent && lcl_IsCharIn(aStr[nLastContent-1], pSepChars)) |
135 | 21 | { |
136 | 21 | nLastContent--; |
137 | 21 | } |
138 | | |
139 | 164 | OUString aLastStr = aStr.copy(nLastContent); |
140 | 164 | OUString aTempLastStr1 = "..." + aLastStr; |
141 | | |
142 | 164 | if (GetTextWidth(aTempLastStr1, 0, aTempLastStr1.getLength()) > nMaxWidth) |
143 | 140 | return GetEllipsisString(aStr, nMaxWidth, DrawTextFlags::EndEllipsis); |
144 | | |
145 | 24 | sal_Int32 nFirstContent = 0; |
146 | 54.3k | while (nFirstContent < nLastContent) |
147 | 54.3k | { |
148 | 54.3k | nFirstContent++; |
149 | 54.3k | if (lcl_IsCharIn(aStr[nFirstContent], pSepChars)) |
150 | 24 | break; |
151 | 54.3k | } |
152 | | |
153 | 55 | while ((nFirstContent < nLastContent) && lcl_IsCharIn(aStr[nFirstContent], pSepChars)) |
154 | 31 | { |
155 | 31 | nFirstContent++; |
156 | 31 | } |
157 | | |
158 | 24 | if (nFirstContent >= nLastContent) |
159 | 11 | return GetEllipsisString(aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis); |
160 | | |
161 | 13 | if (nFirstContent > 4) |
162 | 13 | nFirstContent = 4; |
163 | | |
164 | 13 | OUString aFirstStr = OUString::Concat(aStr.subView(0, nFirstContent)) + "..."; |
165 | 13 | OUString aTempStr = aFirstStr + aLastStr; |
166 | | |
167 | 13 | if (GetTextWidth(aTempStr, 0, aTempStr.getLength() ) > nMaxWidth) |
168 | 1 | return GetEllipsisString(aStr, nMaxWidth, nStyle | DrawTextFlags::EndEllipsis); |
169 | | |
170 | 12 | do |
171 | 12 | { |
172 | 12 | aStr = aTempStr; |
173 | | |
174 | 12 | if (nLastContent > aStr.getLength()) |
175 | 11 | nLastContent = aStr.getLength(); |
176 | | |
177 | 11.7k | while (nFirstContent < nLastContent) |
178 | 11.7k | { |
179 | 11.7k | nLastContent--; |
180 | 11.7k | if (lcl_IsCharIn(aStr[nLastContent], pSepChars)) |
181 | 12 | break; |
182 | | |
183 | 11.7k | } |
184 | | |
185 | 61 | while ((nFirstContent < nLastContent) && lcl_IsCharIn(aStr[nLastContent-1], pSepChars)) |
186 | 49 | { |
187 | 49 | nLastContent--; |
188 | 49 | } |
189 | | |
190 | 12 | if (nFirstContent < nLastContent) |
191 | 0 | { |
192 | 0 | std::u16string_view aTempLastStr = aStr.subView(nLastContent); |
193 | 0 | aTempStr = aFirstStr + aTempLastStr; |
194 | |
|
195 | 0 | if (GetTextWidth(aTempStr, 0, aTempStr.getLength()) > nMaxWidth) |
196 | 0 | break; |
197 | 0 | } |
198 | 12 | } |
199 | 12 | while (nFirstContent < nLastContent); |
200 | | |
201 | 12 | return aStr; |
202 | 13 | } |
203 | | |
204 | | OUString TextLayoutCommon::GetEllipsisString(OUString const& rOrigStr, tools::Long nMaxWidth, DrawTextFlags nStyle) |
205 | 534 | { |
206 | 534 | OUString aStr = rOrigStr; |
207 | 534 | sal_Int32 nIndex = GetTextBreak( aStr, nMaxWidth, 0, aStr.getLength() ); |
208 | | |
209 | 534 | if (nIndex == -1) |
210 | 0 | return aStr; |
211 | | |
212 | 534 | if ((nStyle & DrawTextFlags::CenterEllipsis) == DrawTextFlags::CenterEllipsis) |
213 | 0 | aStr = GetCenterEllipsisString(rOrigStr, nIndex, nMaxWidth); |
214 | 534 | else if (nStyle & DrawTextFlags::EndEllipsis) |
215 | 328 | aStr = GetEndEllipsisString(rOrigStr, nIndex, nMaxWidth, (nStyle & DrawTextFlags::Clip) == DrawTextFlags::Clip); |
216 | 206 | else if (nStyle & DrawTextFlags::PathEllipsis) |
217 | 42 | aStr = lcl_GetPathEllipsisString(rOrigStr, nIndex); |
218 | 164 | else if ( nStyle & DrawTextFlags::NewsEllipsis ) |
219 | 164 | aStr = GetNewsEllipsisString(rOrigStr, nMaxWidth, nStyle); |
220 | | |
221 | 534 | return aStr; |
222 | 534 | } |
223 | | |
224 | | std::tuple<sal_Int32, sal_Int32> TextLayoutCommon::BreakLine(const tools::Long nWidth, OUString const& rStr, |
225 | | css::uno::Reference< css::linguistic2::XHyphenator > const& xHyph, |
226 | | css::uno::Reference<css::i18n::XBreakIterator>& xBI, |
227 | | const bool bHyphenate, const tools::Long nOrigLineWidth, |
228 | | const sal_Int32 nPos, const sal_Int32 nLen) |
229 | 0 | { |
230 | 0 | if (!xBI.is()) |
231 | 0 | xBI = vcl::unohelper::CreateBreakIterator(); |
232 | |
|
233 | 0 | if (!xBI.is()) |
234 | 0 | return BreakLineSimple(nWidth, rStr, nPos, nLen, nOrigLineWidth); |
235 | | |
236 | 0 | const css::lang::Locale& rDefLocale(Application::GetSettings().GetUILanguageTag().getLocale()); |
237 | |
|
238 | 0 | sal_Int32 nSoftBreak = GetTextBreak(rStr, nWidth, nPos, nLen - nPos); |
239 | 0 | if (nSoftBreak == -1) |
240 | 0 | nSoftBreak = nPos; |
241 | |
|
242 | 0 | SAL_WARN_IF( nSoftBreak >= nLen, "vcl", "Break?!" ); |
243 | | |
244 | 0 | css::i18n::LineBreakHyphenationOptions aHyphOptions( xHyph, css::uno::Sequence <css::beans::PropertyValue>(), 1 ); |
245 | 0 | css::i18n::LineBreakUserOptions aUserOptions; |
246 | 0 | css::i18n::LineBreakResults aLBR = xBI->getLineBreak( rStr, nSoftBreak, rDefLocale, nPos, aHyphOptions, aUserOptions ); |
247 | |
|
248 | 0 | sal_Int32 nBreakPos = aLBR.breakIndex; |
249 | |
|
250 | 0 | if (nBreakPos <= nPos) |
251 | 0 | nBreakPos = nSoftBreak; |
252 | |
|
253 | 0 | if (!bHyphenate || !xHyph.is()) |
254 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
255 | | |
256 | | // Whether hyphen or not: Put the word after the hyphen through |
257 | | // the word boundary. |
258 | | |
259 | | // We run into a problem if the doc is so narrow, that a word |
260 | | // is broken into more than two lines ... |
261 | | |
262 | 0 | css::i18n::Boundary aBoundary = xBI->getWordBoundary( rStr, nBreakPos, rDefLocale, css::i18n::WordType::DICTIONARY_WORD, true ); |
263 | 0 | sal_Int32 nWordStart = nPos; |
264 | 0 | sal_Int32 nWordEnd = aBoundary.endPos; |
265 | 0 | SAL_WARN_IF(nWordEnd <= nWordStart, "vcl", "Start >= End?"); |
266 | | |
267 | 0 | sal_Int32 nWordLen = nWordEnd - nWordStart; |
268 | 0 | if ((nWordEnd < nSoftBreak) || (nWordLen <= 3)) |
269 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
270 | | |
271 | 0 | OUString aWord = rStr.copy( nWordStart, nWordLen ); |
272 | 0 | sal_Int32 nMinTrail = nWordEnd-nSoftBreak+1; //+1: Before the "broken off" char |
273 | 0 | css::uno::Reference< css::linguistic2::XHyphenatedWord > xHyphWord; |
274 | 0 | if (xHyph.is()) |
275 | 0 | xHyphWord = xHyph->hyphenate( aWord, rDefLocale, aWord.getLength() - nMinTrail, css::uno::Sequence< css::beans::PropertyValue >() ); |
276 | |
|
277 | 0 | if (!xHyphWord.is()) |
278 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
279 | | |
280 | 0 | bool bAlternate = xHyphWord->isAlternativeSpelling(); |
281 | 0 | sal_Int32 _nWordLen = 1 + xHyphWord->getHyphenPos(); |
282 | |
|
283 | 0 | if ((_nWordLen < 2 ) || ( (nWordStart + _nWordLen) < 2)) |
284 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
285 | | |
286 | 0 | if (bAlternate) |
287 | 0 | { |
288 | 0 | nBreakPos = nWordStart + _nWordLen; |
289 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
290 | 0 | } |
291 | | |
292 | 0 | OUString aAlt( xHyphWord->getHyphenatedWord() ); |
293 | | |
294 | | // We can have two cases: |
295 | | // 1) "packen" turns into "pak-ken" |
296 | | // 2) "Schiffahrt" turns into "Schiff-fahrt" |
297 | | |
298 | | // In case 1 we need to replace a char |
299 | | // In case 2 we add a char |
300 | | |
301 | | // Correct recognition is made harder by words such as |
302 | | // "Schiffahrtsbrennesseln", as the Hyphenator splits all |
303 | | // positions of the word and comes up with "Schifffahrtsbrennnesseln" |
304 | | // Thus, we cannot infer the aWord from the AlternativeWord's |
305 | | // index. |
306 | | // TODO: The whole junk will be made easier by a function in |
307 | | // the Hyphenator, as soon as AMA adds it. |
308 | 0 | sal_Int32 nAltStart = _nWordLen - 1; |
309 | 0 | sal_Int32 nTxtStart = nAltStart - (aAlt.getLength() - aWord.getLength()); |
310 | 0 | sal_Int32 nTxtEnd = nTxtStart; |
311 | 0 | sal_Int32 nAltEnd = nAltStart; |
312 | | |
313 | | // The area between nStart and nEnd is the difference |
314 | | // between AlternativeString and OriginalString |
315 | 0 | while( nTxtEnd < aWord.getLength() && nAltEnd < aAlt.getLength() && |
316 | 0 | aWord[nTxtEnd] != aAlt[nAltEnd] ) |
317 | 0 | { |
318 | 0 | ++nTxtEnd; |
319 | 0 | ++nAltEnd; |
320 | 0 | } |
321 | | |
322 | | // If a char was added, we notice it now: |
323 | 0 | if( nAltEnd > nTxtEnd && nAltStart == nAltEnd && |
324 | 0 | aWord[ nTxtEnd ] == aAlt[nAltEnd] ) |
325 | 0 | { |
326 | 0 | ++nAltEnd; |
327 | 0 | ++nTxtStart; |
328 | 0 | ++nTxtEnd; |
329 | 0 | } |
330 | |
|
331 | 0 | SAL_INFO_IF( ( nAltEnd - nAltStart ) != 1, "vcl", "Alternate: Wrong assumption!" ); |
332 | | |
333 | 0 | sal_Unicode cAlternateReplChar = 0; |
334 | 0 | if ( nTxtEnd > nTxtStart ) |
335 | 0 | cAlternateReplChar = aAlt[ nAltStart ]; |
336 | |
|
337 | 0 | nBreakPos = nWordStart + nTxtStart; |
338 | 0 | if ( cAlternateReplChar ) |
339 | 0 | nBreakPos++; |
340 | |
|
341 | 0 | return { nBreakPos, GetTextWidth(rStr, nPos, nBreakPos - nPos) }; |
342 | 0 | } |
343 | | |
344 | | std::tuple<sal_Int32, sal_Int32> TextLayoutCommon::BreakLineSimple(const tools::Long nWidth, OUString const& rStr, |
345 | | const sal_Int32 nPos, const sal_Int32 nLen, const tools::Long nOrigLineWidth) |
346 | 0 | { |
347 | 0 | sal_Int32 nBreakPos = nLen; |
348 | 0 | tools::Long nLineWidth = nOrigLineWidth; |
349 | 0 | sal_Int32 nSpacePos = rStr.getLength(); |
350 | 0 | tools::Long nW = 0; |
351 | |
|
352 | 0 | do |
353 | 0 | { |
354 | 0 | nSpacePos = rStr.lastIndexOf( ' ', nSpacePos ); |
355 | 0 | if( nSpacePos != -1 ) |
356 | 0 | { |
357 | 0 | if( nSpacePos > nPos ) |
358 | 0 | nSpacePos--; |
359 | 0 | nW = GetTextWidth( rStr, nPos, nSpacePos-nPos ); |
360 | 0 | } |
361 | 0 | } while( nW > nWidth ); |
362 | |
|
363 | 0 | if( nSpacePos != -1 ) |
364 | 0 | { |
365 | 0 | nBreakPos = nSpacePos; |
366 | 0 | nLineWidth = GetTextWidth( rStr, nPos, nBreakPos-nPos ); |
367 | 0 | if( nBreakPos < rStr.getLength()-1 ) |
368 | 0 | nBreakPos++; |
369 | 0 | } |
370 | |
|
371 | 0 | return { nBreakPos, nLineWidth }; |
372 | 0 | } |
373 | | |
374 | | namespace |
375 | | { |
376 | | bool lcl_ShouldBreakWord(const sal_Int32 nLineWidth, const sal_Int32 nWidth, const DrawTextFlags nStyle) |
377 | 0 | { |
378 | 0 | return ((nLineWidth > nWidth) && (nStyle & DrawTextFlags::WordBreak)); |
379 | 0 | } |
380 | | |
381 | | sal_Int32 lcl_GetEndOfLine(std::u16string_view rStr, const sal_Int32 nPos, const sal_Int32 nLen) |
382 | 0 | { |
383 | 0 | sal_Int32 nBreakPos = nPos; |
384 | |
|
385 | 0 | while ((nBreakPos < nLen) && (rStr[nBreakPos] != '\r') && (rStr[nBreakPos] != '\n')) |
386 | 0 | { |
387 | 0 | nBreakPos++; |
388 | 0 | } |
389 | |
|
390 | 0 | return nBreakPos; |
391 | 0 | } |
392 | | } |
393 | | |
394 | | tools::Long TextLayoutCommon::GetTextLines(tools::Rectangle const& rRect, const tools::Long nTextHeight, |
395 | | ImplMultiTextLineInfo& rLineInfo, |
396 | | tools::Long nWidth, OUString const& rStr, |
397 | | DrawTextFlags nStyle) |
398 | 0 | { |
399 | 0 | SAL_WARN_IF( nWidth <= 0, "vcl", "ImplGetTextLines: nWidth <= 0!" ); |
400 | | |
401 | 0 | if ( nWidth <= 0 ) |
402 | 0 | nWidth = 1; |
403 | |
|
404 | 0 | rLineInfo.Clear(); |
405 | 0 | if (rStr.isEmpty()) |
406 | 0 | return 0; |
407 | | |
408 | 0 | tools::Long nMaxLineWidth = 0; |
409 | 0 | const bool bHyphenate = (nStyle & DrawTextFlags::WordBreakHyphenation) == DrawTextFlags::WordBreakHyphenation; |
410 | 0 | css::uno::Reference< css::linguistic2::XHyphenator > xHyph; |
411 | 0 | if (bHyphenate) |
412 | 0 | { |
413 | | // get service provider |
414 | 0 | const css::uno::Reference<css::uno::XComponentContext>& xContext(comphelper::getProcessComponentContext()); |
415 | 0 | css::uno::Reference<css::linguistic2::XLinguServiceManager2> xLinguMgr = css::linguistic2::LinguServiceManager::create(xContext); |
416 | 0 | xHyph = xLinguMgr->getHyphenator(); |
417 | 0 | } |
418 | |
|
419 | 0 | css::uno::Reference<css::i18n::XBreakIterator> xBI; |
420 | 0 | sal_Int32 nPos = 0; |
421 | 0 | sal_Int32 nLen = rStr.getLength(); |
422 | 0 | sal_Int32 nCurrentTextY = 0; |
423 | |
|
424 | 0 | while ( nPos < nLen ) |
425 | 0 | { |
426 | 0 | sal_Int32 nBreakPos = lcl_GetEndOfLine(rStr, nPos, nLen); |
427 | 0 | tools::Long nLineWidth = GetTextWidth(rStr, nPos, nBreakPos-nPos); |
428 | | |
429 | |
|
430 | 0 | if (lcl_ShouldBreakWord(nLineWidth, nWidth, nStyle)) |
431 | 0 | std::tie(nBreakPos, nLineWidth) = BreakLine(nWidth, rStr, xHyph, xBI, bHyphenate, nLineWidth, nPos, nBreakPos); |
432 | |
|
433 | 0 | if ( nLineWidth > nMaxLineWidth ) |
434 | 0 | nMaxLineWidth = nLineWidth; |
435 | |
|
436 | 0 | rLineInfo.AddLine( ImplTextLineInfo( nLineWidth, nPos, nBreakPos-nPos ) ); |
437 | |
|
438 | 0 | if ( nBreakPos == nPos ) |
439 | 0 | nBreakPos++; |
440 | |
|
441 | 0 | nPos = nBreakPos; |
442 | |
|
443 | 0 | if ( nPos < nLen && ( ( rStr[ nPos ] == '\r' ) || ( rStr[ nPos ] == '\n' ) ) ) |
444 | 0 | { |
445 | 0 | nPos++; |
446 | | // CR/LF? |
447 | 0 | if ( ( nPos < nLen ) && ( rStr[ nPos ] == '\n' ) && ( rStr[ nPos-1 ] == '\r' ) ) |
448 | 0 | nPos++; |
449 | 0 | } |
450 | 0 | nCurrentTextY += nTextHeight; |
451 | |
|
452 | 0 | const bool bClipping = (nStyle & DrawTextFlags::Clip) && !(nStyle & DrawTextFlags::EndEllipsis); |
453 | |
|
454 | 0 | if (bClipping && nCurrentTextY > rRect.GetHeight()) |
455 | 0 | break; |
456 | 0 | } |
457 | |
|
458 | | #ifdef DBG_UTIL |
459 | | for ( sal_Int32 nL = 0; nL < rLineInfo.Count(); nL++ ) |
460 | | { |
461 | | ImplTextLineInfo& rLine = rLineInfo.GetLine( nL ); |
462 | | OUString aLine = rStr.copy( rLine.GetIndex(), rLine.GetLen() ); |
463 | | SAL_WARN_IF( aLine.indexOf( '\r' ) != -1, "vcl", "ImplGetTextLines - Found CR!" ); |
464 | | SAL_WARN_IF( aLine.indexOf( '\n' ) != -1, "vcl", "ImplGetTextLines - Found LF!" ); |
465 | | } |
466 | | #endif |
467 | |
|
468 | 0 | return nMaxLineWidth; |
469 | 0 | } |
470 | | |
471 | | DefaultTextLayout::~DefaultTextLayout() |
472 | | { |
473 | | } |
474 | | |
475 | | tools::Long DefaultTextLayout::GetTextWidth( const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const |
476 | 5.46k | { |
477 | 5.46k | return m_rTargetDevice.GetTextWidth( _rText, _nStartIndex, _nLength ); |
478 | 5.46k | } |
479 | | |
480 | | void DefaultTextLayout::DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, |
481 | | sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) |
482 | 1.90k | { |
483 | 1.90k | m_rTargetDevice.DrawText( _rStartPoint, _rText, _nStartIndex, _nLength, _pVector, _pDisplayText ); |
484 | 1.90k | } |
485 | | |
486 | | tools::Long DefaultTextLayout::GetTextArray( const OUString& _rText, KernArray* _pDXArray, |
487 | | sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret ) const |
488 | 530 | { |
489 | 530 | return basegfx::fround<tools::Long>( |
490 | 530 | m_rTargetDevice.GetTextArray(_rText, _pDXArray, _nStartIndex, _nLength, bCaret)); |
491 | 530 | } |
492 | | |
493 | | sal_Int32 DefaultTextLayout::GetTextBreak( const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const |
494 | 534 | { |
495 | 534 | return m_rTargetDevice.GetTextBreak( _rText, _nMaxTextWidth, _nStartIndex, _nLength ); |
496 | 534 | } |
497 | | |
498 | | bool DefaultTextLayout::DecomposeTextRectAction() const |
499 | 0 | { |
500 | 0 | return false; |
501 | 0 | } |
502 | | |
503 | | class ReferenceDeviceTextLayout : public TextLayoutCommon |
504 | | { |
505 | | public: |
506 | | ReferenceDeviceTextLayout( const Control& _rControl, OutputDevice& _rTargetDevice, OutputDevice& _rReferenceDevice ); |
507 | | virtual ~ReferenceDeviceTextLayout(); |
508 | | |
509 | | // TextLayoutCommon |
510 | | virtual tools::Long GetTextWidth( const OUString& rStr, sal_Int32 nIndex, sal_Int32 nLen ) const override; |
511 | | virtual void DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) override; |
512 | | virtual tools::Long GetTextArray( const OUString& _rText, KernArray* _pDXAry, sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret = false ) const override; |
513 | | virtual sal_Int32 GetTextBreak(const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength) const override; |
514 | | virtual bool DecomposeTextRectAction() const override; |
515 | | |
516 | | public: |
517 | | // equivalents to the respective OutputDevice methods, which take the reference device into account |
518 | | tools::Rectangle DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ); |
519 | | tools::Rectangle GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize ); |
520 | | |
521 | | private: |
522 | | |
523 | | void ImplDestroy(); |
524 | | |
525 | | OutputDevice& m_rTargetDevice; |
526 | | OutputDevice& m_rReferenceDevice; |
527 | | const bool m_bRTLEnabled; |
528 | | |
529 | | tools::Rectangle m_aCompleteTextRect; |
530 | | }; |
531 | | |
532 | | ReferenceDeviceTextLayout::ReferenceDeviceTextLayout( const Control& _rControl, OutputDevice& _rTargetDevice, |
533 | | OutputDevice& _rReferenceDevice ) |
534 | 0 | :m_rTargetDevice( _rTargetDevice ) |
535 | 0 | ,m_rReferenceDevice( _rReferenceDevice ) |
536 | 0 | ,m_bRTLEnabled( _rControl.IsRTLEnabled() ) |
537 | 0 | { |
538 | 0 | Font const aUnzoomedPointFont( _rControl.GetUnzoomedControlPointFont() ); |
539 | 0 | const Fraction& aZoom( _rControl.GetZoom() ); |
540 | 0 | m_rTargetDevice.Push( PushFlags::MAPMODE | PushFlags::FONT | PushFlags::TEXTLAYOUTMODE ); |
541 | |
|
542 | 0 | MapMode aTargetMapMode( m_rTargetDevice.GetMapMode() ); |
543 | 0 | SAL_WARN_IF(aTargetMapMode.GetOrigin() != Point(), "vcl", "uhm, the code below won't work here ..."); |
544 | | |
545 | | // normally, controls simulate "zoom" by "zooming" the font. This is responsible for (part of) the discrepancies |
546 | | // between text in Writer and text in controls in Writer, though both have the same font. |
547 | | // So, if we have a zoom set at the control, then we do not scale the font, but instead modify the map mode |
548 | | // to accommodate for the zoom. |
549 | 0 | aTargetMapMode.SetScaleX( aZoom ); // TODO: shouldn't this be "current_scale * zoom"? |
550 | 0 | aTargetMapMode.SetScaleY( aZoom ); |
551 | | |
552 | | // also, use a higher-resolution map unit than "pixels", which should save us some rounding errors when |
553 | | // translating coordinates between the reference device and the target device. |
554 | 0 | SAL_WARN_IF(aTargetMapMode.GetMapUnit() != MapUnit::MapPixel, "vcl", "this class is not expected to work with such target devices!"); |
555 | | // we *could* adjust all the code in this class to handle this case, but at the moment, it's not necessary |
556 | 0 | const MapUnit eTargetMapUnit = m_rReferenceDevice.GetMapMode().GetMapUnit(); |
557 | 0 | aTargetMapMode.SetMapUnit( eTargetMapUnit ); |
558 | 0 | SAL_WARN_IF(aTargetMapMode.GetMapUnit() == MapUnit::MapPixel, "vcl", "a reference device which has map mode PIXEL?!"); |
559 | | |
560 | 0 | m_rTargetDevice.SetMapMode( aTargetMapMode ); |
561 | | |
562 | | // now that the Zoom is part of the map mode, reset the target device's font to the "unzoomed" version |
563 | 0 | Font aDrawFont( aUnzoomedPointFont ); |
564 | 0 | aDrawFont.SetFontSize( OutputDevice::LogicToLogic(aDrawFont.GetFontSize(), MapMode(MapUnit::MapPoint), MapMode(eTargetMapUnit)) ); |
565 | 0 | _rTargetDevice.SetFont( aDrawFont ); |
566 | | |
567 | | // transfer font to the reference device |
568 | 0 | m_rReferenceDevice.Push( PushFlags::FONT | PushFlags::TEXTLAYOUTMODE ); |
569 | 0 | Font aRefFont( aUnzoomedPointFont ); |
570 | 0 | aRefFont.SetFontSize( OutputDevice::LogicToLogic( |
571 | 0 | aRefFont.GetFontSize(), MapMode(MapUnit::MapPoint), m_rReferenceDevice.GetMapMode()) ); |
572 | 0 | m_rReferenceDevice.SetFont( aRefFont ); |
573 | 0 | } |
574 | | |
575 | | void ReferenceDeviceTextLayout::ImplDestroy() |
576 | 0 | { |
577 | 0 | m_rReferenceDevice.Pop(); |
578 | 0 | m_rTargetDevice.Pop(); |
579 | 0 | } |
580 | | |
581 | | ReferenceDeviceTextLayout::~ReferenceDeviceTextLayout() |
582 | 0 | { |
583 | 0 | suppress_fun_call_w_exception(ImplDestroy()); |
584 | 0 | } |
585 | | |
586 | | namespace |
587 | | { |
588 | | bool lcl_normalizeLength( std::u16string_view _rText, const sal_Int32 _nStartIndex, sal_Int32& _io_nLength ) |
589 | 0 | { |
590 | 0 | sal_Int32 nTextLength = _rText.size(); |
591 | 0 | if ( _nStartIndex > nTextLength ) |
592 | 0 | return false; |
593 | 0 | if ( _nStartIndex + _io_nLength > nTextLength ) |
594 | 0 | _io_nLength = nTextLength - _nStartIndex; |
595 | 0 | return true; |
596 | 0 | } |
597 | | } |
598 | | |
599 | | tools::Long ReferenceDeviceTextLayout::GetTextArray( const OUString& _rText, KernArray* _pDXAry, sal_Int32 _nStartIndex, sal_Int32 _nLength, bool bCaret ) const |
600 | 0 | { |
601 | 0 | if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) |
602 | 0 | return 0; |
603 | | |
604 | | // retrieve the character widths from the reference device |
605 | 0 | tools::Long nTextWidth = basegfx::fround<tools::Long>( |
606 | 0 | m_rReferenceDevice.GetTextArray(_rText, _pDXAry, _nStartIndex, _nLength, bCaret)); |
607 | | #if OSL_DEBUG_LEVEL > 1 |
608 | | if ( _pDXAry ) |
609 | | { |
610 | | OStringBuffer aTrace; |
611 | | aTrace.append( "ReferenceDeviceTextLayout::GetTextArray( " ); |
612 | | aTrace.append( OUStringToOString( _rText, RTL_TEXTENCODING_UTF8 ) ); |
613 | | aTrace.append( " ): " ); |
614 | | aTrace.append( nTextWidth ); |
615 | | aTrace.append( " = ( " ); |
616 | | for ( sal_Int32 i=0; i<_nLength; ) |
617 | | { |
618 | | aTrace.append( _pDXAry->get(i) ); |
619 | | if ( ++i < _nLength ) |
620 | | aTrace.append( ", " ); |
621 | | } |
622 | | aTrace.append( ")" ); |
623 | | SAL_INFO( "vcl", aTrace.makeStringAndClear() ); |
624 | | } |
625 | | #endif |
626 | 0 | return nTextWidth; |
627 | 0 | } |
628 | | |
629 | | tools::Long ReferenceDeviceTextLayout::GetTextWidth( const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const |
630 | 0 | { |
631 | 0 | return GetTextArray( _rText, nullptr, _nStartIndex, _nLength ); |
632 | 0 | } |
633 | | |
634 | | void ReferenceDeviceTextLayout::DrawText( const Point& _rStartPoint, const OUString& _rText, sal_Int32 _nStartIndex, sal_Int32 _nLength, std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText ) |
635 | 0 | { |
636 | 0 | if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) |
637 | 0 | return; |
638 | | |
639 | 0 | if ( _pVector && _pDisplayText ) |
640 | 0 | { |
641 | 0 | std::vector< tools::Rectangle > aGlyphBounds; |
642 | 0 | m_rReferenceDevice.GetGlyphBoundRects( _rStartPoint, _rText, _nStartIndex, _nLength, aGlyphBounds ); |
643 | 0 | _pVector->insert( _pVector->end(), aGlyphBounds.begin(), aGlyphBounds.end() ); |
644 | 0 | *_pDisplayText += _rText.subView( _nStartIndex, _nLength ); |
645 | 0 | return; |
646 | 0 | } |
647 | | |
648 | 0 | KernArray aCharWidths; |
649 | 0 | tools::Long nTextWidth = GetTextArray( _rText, &aCharWidths, _nStartIndex, _nLength ); |
650 | 0 | m_rTargetDevice.DrawTextArray( _rStartPoint, _rText, aCharWidths, {}, _nStartIndex, _nLength ); |
651 | |
|
652 | 0 | m_aCompleteTextRect.Union( tools::Rectangle( _rStartPoint, Size( nTextWidth, m_rTargetDevice.GetTextHeight() ) ) ); |
653 | 0 | } |
654 | | |
655 | | sal_Int32 ReferenceDeviceTextLayout::GetTextBreak( const OUString& _rText, tools::Long _nMaxTextWidth, sal_Int32 _nStartIndex, sal_Int32 _nLength ) const |
656 | 0 | { |
657 | 0 | if ( !lcl_normalizeLength( _rText, _nStartIndex, _nLength ) ) |
658 | 0 | return 0; |
659 | | |
660 | 0 | return m_rReferenceDevice.GetTextBreak( _rText, _nMaxTextWidth, _nStartIndex, _nLength ); |
661 | 0 | } |
662 | | |
663 | | bool ReferenceDeviceTextLayout::DecomposeTextRectAction() const |
664 | 0 | { |
665 | 0 | return true; |
666 | 0 | } |
667 | | |
668 | | tools::Rectangle ReferenceDeviceTextLayout::DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, |
669 | | std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ) |
670 | 0 | { |
671 | 0 | if ( _rText.isEmpty() ) |
672 | 0 | return tools::Rectangle(); |
673 | | |
674 | | // determine text layout mode from the RTL-ness of the control whose text we render |
675 | 0 | text::ComplexTextLayoutFlags nTextLayoutMode = m_bRTLEnabled ? text::ComplexTextLayoutFlags::BiDiRtl : text::ComplexTextLayoutFlags::Default; |
676 | 0 | m_rReferenceDevice.SetLayoutMode( nTextLayoutMode ); |
677 | 0 | m_rTargetDevice.SetLayoutMode( nTextLayoutMode | text::ComplexTextLayoutFlags::TextOriginLeft ); |
678 | | |
679 | | // ComplexTextLayoutFlags::TextOriginLeft is because when we do actually draw the text (in DrawText( Point, ... )), then |
680 | | // our caller gives us the left border of the draw position, regardless of script type, text layout, |
681 | | // and the like in our ctor, we set the map mode of the target device from pixel to twip, but our caller doesn't know this, |
682 | | // but passed pixel coordinates. So, adjust the rect. |
683 | 0 | tools::Rectangle aRect( m_rTargetDevice.PixelToLogic( _rRect ) ); |
684 | 0 | if (i_pDeviceSize) |
685 | 0 | { |
686 | | //if i_pDeviceSize is passed in here, it was the original pre logic-to-pixel size of _rRect |
687 | 0 | SAL_WARN_IF(std::abs(_rRect.GetSize().Width() - m_rTargetDevice.LogicToPixel(*i_pDeviceSize).Width()) > 1, "vcl", "DeviceSize width was expected to match Pixel width"); |
688 | 0 | SAL_WARN_IF(std::abs(_rRect.GetSize().Height() - m_rTargetDevice.LogicToPixel(*i_pDeviceSize).Height()) > 1, "vcl", "DeviceSize height was expected to match Pixel height"); |
689 | 0 | aRect.SetSize(*i_pDeviceSize); |
690 | 0 | } |
691 | | |
692 | 0 | m_aCompleteTextRect.SetEmpty(); |
693 | 0 | m_rTargetDevice.DrawText( aRect, _rText, _nStyle, _pVector, _pDisplayText, this ); |
694 | 0 | tools::Rectangle aTextRect = m_aCompleteTextRect; |
695 | |
|
696 | 0 | if ( aTextRect.IsEmpty() && !aRect.IsEmpty() ) |
697 | 0 | { |
698 | | // this happens for instance if we're in a PaintToDevice call, where only a MetaFile is recorded, |
699 | | // but no actual painting happens, so our "DrawText( Point, ... )" is never called |
700 | | // In this case, calculate the rect from what OutputDevice::GetTextRect would give us. This has |
701 | | // the disadvantage of less accuracy, compared with the approach to calculate the rect from the |
702 | | // single "DrawText( Point, ... )" calls, since more intermediate arithmetic will translate |
703 | | // from ref- to target-units. |
704 | 0 | aTextRect = m_rTargetDevice.GetTextRect( aRect, _rText, _nStyle, nullptr, this ); |
705 | 0 | } |
706 | | |
707 | | // similar to above, the text rect now contains TWIPs (or whatever unit the ref device has), but the caller |
708 | | // expects pixel coordinates |
709 | 0 | aTextRect = m_rTargetDevice.LogicToPixel( aTextRect ); |
710 | | |
711 | | // convert the metric vector |
712 | 0 | if ( _pVector ) |
713 | 0 | { |
714 | 0 | for ( auto& rCharRect : *_pVector ) |
715 | 0 | { |
716 | 0 | rCharRect = m_rTargetDevice.LogicToPixel( rCharRect ); |
717 | 0 | } |
718 | 0 | } |
719 | |
|
720 | 0 | return aTextRect; |
721 | 0 | } |
722 | | |
723 | | tools::Rectangle ReferenceDeviceTextLayout::GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize ) |
724 | 0 | { |
725 | 0 | if ( _rText.isEmpty() ) |
726 | 0 | return tools::Rectangle(); |
727 | | |
728 | | // determine text layout mode from the RTL-ness of the control whose text we render |
729 | 0 | text::ComplexTextLayoutFlags nTextLayoutMode = m_bRTLEnabled ? text::ComplexTextLayoutFlags::BiDiRtl : text::ComplexTextLayoutFlags::Default; |
730 | 0 | m_rReferenceDevice.SetLayoutMode( nTextLayoutMode ); |
731 | 0 | m_rTargetDevice.SetLayoutMode( nTextLayoutMode | text::ComplexTextLayoutFlags::TextOriginLeft ); |
732 | | |
733 | | // ComplexTextLayoutFlags::TextOriginLeft is because when we do actually draw the text (in DrawText( Point, ... )), then |
734 | | // our caller gives us the left border of the draw position, regardless of script type, text layout, |
735 | | // and the like in our ctor, we set the map mode of the target device from pixel to twip, but our caller doesn't know this, |
736 | | // but passed pixel coordinates. So, adjust the rect. |
737 | 0 | tools::Rectangle aRect( m_rTargetDevice.PixelToLogic( _rRect ) ); |
738 | |
|
739 | 0 | tools::Rectangle aTextRect = m_rTargetDevice.GetTextRect( aRect, _rText, _nStyle, nullptr, this ); |
740 | | |
741 | | //if o_pDeviceSize is available, stash the pre logic-to-pixel size in it |
742 | 0 | if (o_pDeviceSize) |
743 | 0 | { |
744 | 0 | *o_pDeviceSize = aTextRect.GetSize(); |
745 | 0 | } |
746 | | |
747 | | // similar to above, the text rect now contains TWIPs (or whatever unit the ref device has), but the caller |
748 | | // expects pixel coordinates |
749 | 0 | aTextRect = m_rTargetDevice.LogicToPixel( aTextRect ); |
750 | |
|
751 | 0 | return aTextRect; |
752 | 0 | } |
753 | | |
754 | | ControlTextRenderer::ControlTextRenderer( const Control& _rControl, OutputDevice& _rTargetDevice, OutputDevice& _rReferenceDevice ) |
755 | 0 | :m_pImpl( new ReferenceDeviceTextLayout( _rControl, _rTargetDevice, _rReferenceDevice ) ) |
756 | 0 | { |
757 | 0 | } |
758 | | |
759 | | ControlTextRenderer::~ControlTextRenderer() |
760 | 0 | { |
761 | 0 | } |
762 | | |
763 | | tools::Rectangle ControlTextRenderer::DrawText( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, |
764 | | std::vector< tools::Rectangle >* _pVector, OUString* _pDisplayText, const Size* i_pDeviceSize ) |
765 | 0 | { |
766 | 0 | return m_pImpl->DrawText( _rRect, _rText, _nStyle, _pVector, _pDisplayText, i_pDeviceSize ); |
767 | 0 | } |
768 | | |
769 | | tools::Rectangle ControlTextRenderer::GetTextRect( const tools::Rectangle& _rRect, const OUString& _rText, DrawTextFlags _nStyle, Size* o_pDeviceSize = nullptr ) |
770 | 0 | { |
771 | 0 | return m_pImpl->GetTextRect( _rRect, _rText, _nStyle, o_pDeviceSize ); |
772 | 0 | } |
773 | | |
774 | | } // namespace vcl |
775 | | |
776 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |