/src/libreoffice/vcl/source/window/mnemonic.cxx
Line | Count | Source (jump to first uncovered line) |
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 <string.h> |
21 | | #include <vcl/svapp.hxx> |
22 | | #include <vcl/settings.hxx> |
23 | | #include <vcl/mnemonic.hxx> |
24 | | |
25 | | #include <vcl/unohelp.hxx> |
26 | | #include <com/sun/star/i18n/XCharacterClassification.hpp> |
27 | | #include <i18nlangtag/languagetag.hxx> |
28 | | #include <i18nlangtag/mslangid.hxx> |
29 | | #include <rtl/character.hxx> |
30 | | #include <sal/log.hxx> |
31 | | |
32 | | using namespace ::com::sun::star; |
33 | | |
34 | | MnemonicGenerator::MnemonicGenerator(sal_Unicode cMnemonic) |
35 | 0 | : m_cMnemonic(cMnemonic) |
36 | 0 | { |
37 | 0 | memset( maMnemonics, 1, sizeof( maMnemonics ) ); |
38 | 0 | } |
39 | | |
40 | 0 | MnemonicGenerator& MnemonicGenerator::operator=(MnemonicGenerator const &) = default; //MSVC2022 workaround |
41 | 0 | MnemonicGenerator::MnemonicGenerator(MnemonicGenerator const&) = default; //MSVC2022 workaround |
42 | | |
43 | | sal_uInt16 MnemonicGenerator::ImplGetMnemonicIndex( sal_Unicode c ) |
44 | 0 | { |
45 | 0 | static sal_uInt16 const aImplMnemonicRangeTab[MNEMONIC_RANGES*2] = |
46 | 0 | { |
47 | 0 | MNEMONIC_RANGE_1_START, MNEMONIC_RANGE_1_END, |
48 | 0 | MNEMONIC_RANGE_2_START, MNEMONIC_RANGE_2_END, |
49 | 0 | MNEMONIC_RANGE_3_START, MNEMONIC_RANGE_3_END, |
50 | 0 | MNEMONIC_RANGE_4_START, MNEMONIC_RANGE_4_END |
51 | 0 | }; |
52 | |
|
53 | 0 | sal_uInt16 nMnemonicIndex = 0; |
54 | 0 | for ( sal_uInt16 i = 0; i < MNEMONIC_RANGES; i++ ) |
55 | 0 | { |
56 | 0 | if ( (c >= aImplMnemonicRangeTab[i*2]) && |
57 | 0 | (c <= aImplMnemonicRangeTab[i*2+1]) ) |
58 | 0 | return nMnemonicIndex+c-aImplMnemonicRangeTab[i*2]; |
59 | | |
60 | 0 | nMnemonicIndex += aImplMnemonicRangeTab[i*2+1]-aImplMnemonicRangeTab[i*2]; |
61 | 0 | } |
62 | | |
63 | 0 | return MNEMONIC_INDEX_NOTFOUND; |
64 | 0 | } |
65 | | |
66 | | sal_Unicode MnemonicGenerator::ImplFindMnemonic( const OUString& rKey ) |
67 | 0 | { |
68 | 0 | sal_Int32 nIndex = 0; |
69 | 0 | while ( (nIndex = rKey.indexOf( m_cMnemonic, nIndex )) != -1 ) |
70 | 0 | { |
71 | 0 | if (nIndex == rKey.getLength() - 1) { |
72 | 0 | SAL_WARN("vcl", "key \"" << rKey << "\" ends in lone mnemonic prefix"); |
73 | 0 | break; |
74 | 0 | } |
75 | 0 | sal_Unicode cMnemonic = rKey[ nIndex+1 ]; |
76 | 0 | if ( cMnemonic != m_cMnemonic ) |
77 | 0 | return cMnemonic; |
78 | 0 | nIndex += 2; |
79 | 0 | } |
80 | | |
81 | 0 | return 0; |
82 | 0 | } |
83 | | |
84 | | void MnemonicGenerator::RegisterMnemonic( const OUString& rKey ) |
85 | 0 | { |
86 | 0 | uno::Reference < i18n::XCharacterClassification > xCharClass = GetCharClass(); |
87 | | |
88 | | // Don't crash even when we don't have access to i18n service |
89 | 0 | if ( !xCharClass.is() ) |
90 | 0 | return; |
91 | | |
92 | 0 | OUString aKey = xCharClass->toLower(rKey, 0, rKey.getLength(), css::lang::Locale()); |
93 | | |
94 | | // If we find a Mnemonic, set the flag. In other case count the |
95 | | // characters, because we need this to set most as possible |
96 | | // Mnemonics |
97 | 0 | sal_Unicode cMnemonic = ImplFindMnemonic( aKey ); |
98 | 0 | if ( cMnemonic ) |
99 | 0 | { |
100 | 0 | sal_uInt16 nMnemonicIndex = ImplGetMnemonicIndex( cMnemonic ); |
101 | 0 | if ( nMnemonicIndex != MNEMONIC_INDEX_NOTFOUND ) |
102 | 0 | maMnemonics[nMnemonicIndex] = 0; |
103 | 0 | } |
104 | 0 | else |
105 | 0 | { |
106 | 0 | sal_Int32 nIndex = 0; |
107 | 0 | sal_Int32 nLen = aKey.getLength(); |
108 | 0 | while ( nIndex < nLen ) |
109 | 0 | { |
110 | 0 | sal_Unicode c = aKey[ nIndex ]; |
111 | |
|
112 | 0 | sal_uInt16 nMnemonicIndex = ImplGetMnemonicIndex( c ); |
113 | 0 | if ( nMnemonicIndex != MNEMONIC_INDEX_NOTFOUND ) |
114 | 0 | { |
115 | 0 | if ( maMnemonics[nMnemonicIndex] && (maMnemonics[nMnemonicIndex] < 0xFF) ) |
116 | 0 | maMnemonics[nMnemonicIndex]++; |
117 | 0 | } |
118 | |
|
119 | 0 | nIndex++; |
120 | 0 | } |
121 | 0 | } |
122 | 0 | } |
123 | | |
124 | | OUString MnemonicGenerator::CreateMnemonic( const OUString& _rKey ) |
125 | 0 | { |
126 | 0 | if ( _rKey.isEmpty() || ImplFindMnemonic( _rKey ) ) |
127 | 0 | return _rKey; |
128 | | |
129 | 0 | uno::Reference < i18n::XCharacterClassification > xCharClass = GetCharClass(); |
130 | | |
131 | | // Don't crash even when we don't have access to i18n service |
132 | 0 | if ( !xCharClass.is() ) |
133 | 0 | return _rKey; |
134 | | |
135 | 0 | OUString aKey = xCharClass->toLower(_rKey, 0, _rKey.getLength(), css::lang::Locale()); |
136 | |
|
137 | 0 | bool bChanged = false; |
138 | 0 | sal_Int32 nLen = aKey.getLength(); |
139 | |
|
140 | 0 | bool bCJK = MsLangId::isCJK(Application::GetSettings().GetUILanguageTag().getLanguageType()); |
141 | | |
142 | | // #107889# in CJK versions ALL strings (even those that contain latin characters) |
143 | | // will get mnemonics in the form: xyz (M) |
144 | | // thus steps 1) and 2) are skipped for CJK locales |
145 | | |
146 | | // #110720#, avoid CJK-style mnemonics for latin-only strings that do not contain useful mnemonic chars |
147 | 0 | if( bCJK ) |
148 | 0 | { |
149 | 0 | bool bLatinOnly = true; |
150 | 0 | bool bMnemonicIndexFound = false; |
151 | 0 | sal_Unicode c; |
152 | 0 | sal_Int32 nIndex; |
153 | |
|
154 | 0 | for( nIndex=0; nIndex < nLen; nIndex++ ) |
155 | 0 | { |
156 | 0 | c = aKey[ nIndex ]; |
157 | 0 | if ( ((c >= 0x3000) && (c <= 0xD7FF)) || // cjk |
158 | 0 | ((c >= 0xFF61) && (c <= 0xFFDC)) ) // halfwidth forms |
159 | 0 | { |
160 | 0 | bLatinOnly = false; |
161 | 0 | break; |
162 | 0 | } |
163 | 0 | if( ImplGetMnemonicIndex( c ) != MNEMONIC_INDEX_NOTFOUND ) |
164 | 0 | bMnemonicIndexFound = true; |
165 | 0 | } |
166 | 0 | if( bLatinOnly && !bMnemonicIndexFound ) |
167 | 0 | return _rKey; |
168 | 0 | } |
169 | | |
170 | 0 | OUString rKey(_rKey); |
171 | 0 | int nCJK = 0; |
172 | 0 | sal_uInt16 nMnemonicIndex; |
173 | 0 | sal_Unicode c; |
174 | 0 | sal_Int32 nIndex = 0; |
175 | 0 | if( !bCJK ) |
176 | 0 | { |
177 | | // 1) first try the first character of a word |
178 | 0 | do |
179 | 0 | { |
180 | 0 | c = aKey[ nIndex ]; |
181 | |
|
182 | 0 | if ( nCJK != 2 ) |
183 | 0 | { |
184 | 0 | if ( ((c >= 0x3000) && (c <= 0xD7FF)) || // cjk |
185 | 0 | ((c >= 0xFF61) && (c <= 0xFFDC)) ) // halfwidth forms |
186 | 0 | nCJK = 1; |
187 | 0 | else if ( ((c >= 0x0030) && (c <= 0x0039)) || // digits |
188 | 0 | ((c >= 0x0041) && (c <= 0x005A)) || // latin capitals |
189 | 0 | ((c >= 0x0061) && (c <= 0x007A)) || // latin small |
190 | 0 | ((c >= 0x0370) && (c <= 0x037F)) || // greek numeral signs |
191 | 0 | ((c >= 0x0400) && (c <= 0x04FF)) ) // cyrillic |
192 | 0 | nCJK = 2; |
193 | 0 | } |
194 | |
|
195 | 0 | nMnemonicIndex = ImplGetMnemonicIndex( c ); |
196 | 0 | if ( nMnemonicIndex != MNEMONIC_INDEX_NOTFOUND ) |
197 | 0 | { |
198 | 0 | if ( maMnemonics[nMnemonicIndex] ) |
199 | 0 | { |
200 | 0 | maMnemonics[nMnemonicIndex] = 0; |
201 | 0 | rKey = rKey.replaceAt( nIndex, 0, rtl::OUStringChar(m_cMnemonic) ); |
202 | 0 | bChanged = true; |
203 | 0 | break; |
204 | 0 | } |
205 | 0 | } |
206 | | |
207 | | // Search for next word |
208 | 0 | nIndex++; |
209 | 0 | while ( nIndex < nLen ) |
210 | 0 | { |
211 | 0 | c = aKey[ nIndex ]; |
212 | 0 | if ( c == ' ' ) |
213 | 0 | break; |
214 | 0 | nIndex++; |
215 | 0 | } |
216 | 0 | nIndex++; |
217 | 0 | } |
218 | 0 | while ( nIndex < nLen ); |
219 | | |
220 | | // 2) search for a unique/uncommon character |
221 | 0 | if ( !bChanged ) |
222 | 0 | { |
223 | 0 | sal_uInt16 nBestCount = 0xFFFF; |
224 | 0 | sal_uInt16 nBestMnemonicIndex = 0; |
225 | 0 | sal_Int32 nBestIndex = 0; |
226 | 0 | nIndex = 0; |
227 | 0 | do |
228 | 0 | { |
229 | 0 | c = aKey[ nIndex ]; |
230 | 0 | nMnemonicIndex = ImplGetMnemonicIndex( c ); |
231 | 0 | if ( nMnemonicIndex != MNEMONIC_INDEX_NOTFOUND ) |
232 | 0 | { |
233 | 0 | if ( maMnemonics[nMnemonicIndex] ) |
234 | 0 | { |
235 | 0 | if ( maMnemonics[nMnemonicIndex] < nBestCount ) |
236 | 0 | { |
237 | 0 | nBestCount = maMnemonics[nMnemonicIndex]; |
238 | 0 | nBestIndex = nIndex; |
239 | 0 | nBestMnemonicIndex = nMnemonicIndex; |
240 | 0 | if ( nBestCount == 2 ) |
241 | 0 | break; |
242 | 0 | } |
243 | 0 | } |
244 | 0 | } |
245 | | |
246 | 0 | nIndex++; |
247 | 0 | } |
248 | 0 | while ( nIndex < nLen ); |
249 | | |
250 | 0 | if ( nBestCount != 0xFFFF ) |
251 | 0 | { |
252 | 0 | maMnemonics[nBestMnemonicIndex] = 0; |
253 | 0 | rKey = rKey.replaceAt( nBestIndex, 0, rtl::OUStringChar(m_cMnemonic) ); |
254 | 0 | bChanged = true; |
255 | 0 | } |
256 | 0 | } |
257 | 0 | } |
258 | 0 | else |
259 | 0 | nCJK = 1; |
260 | | |
261 | | // 3) Add English Mnemonic for CJK Text |
262 | 0 | if ( !bChanged && (nCJK == 1) && !rKey.isEmpty() ) |
263 | 0 | { |
264 | | // Append Ascii Mnemonic |
265 | 0 | for ( c = MNEMONIC_RANGE_2_START; c <= MNEMONIC_RANGE_2_END; c++ ) |
266 | 0 | { |
267 | 0 | nMnemonicIndex = ImplGetMnemonicIndex(c); |
268 | 0 | if ( nMnemonicIndex != MNEMONIC_INDEX_NOTFOUND ) |
269 | 0 | { |
270 | 0 | if ( maMnemonics[nMnemonicIndex] ) |
271 | 0 | { |
272 | 0 | maMnemonics[nMnemonicIndex] = 0; |
273 | 0 | OUString aStr = OUString::Concat("(") + OUStringChar(m_cMnemonic) + |
274 | 0 | OUStringChar(sal_Unicode(rtl::toAsciiUpperCase(c))) + |
275 | 0 | ")"; |
276 | 0 | nIndex = rKey.getLength(); |
277 | 0 | if( nIndex >= 2 ) |
278 | 0 | { |
279 | 0 | if ( ( rKey[nIndex-2] == '>' && rKey[nIndex-1] == '>' ) || |
280 | 0 | ( rKey[nIndex-2] == 0xFF1E && rKey[nIndex-1] == 0xFF1E ) ) |
281 | 0 | nIndex -= 2; |
282 | 0 | } |
283 | 0 | if( nIndex >= 3 ) |
284 | 0 | { |
285 | 0 | if ( ( rKey[nIndex-3] == '.' && rKey[nIndex-2] == '.' && rKey[nIndex-1] == '.' ) || |
286 | 0 | ( rKey[nIndex-3] == 0xFF0E && rKey[nIndex-2] == 0xFF0E && rKey[nIndex-1] == 0xFF0E ) ) |
287 | 0 | nIndex -= 3; |
288 | 0 | } |
289 | 0 | if( nIndex >= 1) |
290 | 0 | { |
291 | 0 | sal_Unicode cLastChar = rKey[ nIndex-1 ]; |
292 | 0 | if ( (cLastChar == ':') || (cLastChar == 0xFF1A) || |
293 | 0 | (cLastChar == '.') || (cLastChar == 0xFF0E) || |
294 | 0 | (cLastChar == '?') || (cLastChar == 0xFF1F) || |
295 | 0 | (cLastChar == ' ') ) |
296 | 0 | nIndex--; |
297 | 0 | } |
298 | 0 | rKey = rKey.replaceAt( nIndex, 0, aStr ); |
299 | 0 | break; |
300 | 0 | } |
301 | 0 | } |
302 | 0 | } |
303 | 0 | } |
304 | |
|
305 | 0 | return rKey; |
306 | 0 | } |
307 | | |
308 | | uno::Reference< i18n::XCharacterClassification > const & MnemonicGenerator::GetCharClass() |
309 | 0 | { |
310 | 0 | if ( !mxCharClass.is() ) |
311 | 0 | mxCharClass = vcl::unohelper::CreateCharacterClassification(); |
312 | 0 | return mxCharClass; |
313 | 0 | } |
314 | | |
315 | | OUString MnemonicGenerator::EraseAllMnemonicChars( const OUString& rStr ) |
316 | 45 | { |
317 | 45 | OUString aStr = rStr; |
318 | 45 | sal_Int32 nLen = aStr.getLength(); |
319 | 45 | sal_Int32 i = 0; |
320 | | |
321 | 567 | while ( i < nLen ) |
322 | 522 | { |
323 | 522 | if ( aStr[ i ] == '~' ) |
324 | 1 | { |
325 | | // check for CJK-style mnemonic |
326 | 1 | if( i > 0 && (i+2) < nLen ) |
327 | 0 | { |
328 | 0 | sal_Unicode c = sal_Unicode(rtl::toAsciiLowerCase(aStr[i+1])); |
329 | 0 | if( aStr[ i-1 ] == '(' && |
330 | 0 | aStr[ i+2 ] == ')' && |
331 | 0 | c >= MNEMONIC_RANGE_2_START && c <= MNEMONIC_RANGE_2_END ) |
332 | 0 | { |
333 | 0 | aStr = aStr.replaceAt( i-1, 4, u"" ); |
334 | 0 | nLen -= 4; |
335 | 0 | i--; |
336 | 0 | continue; |
337 | 0 | } |
338 | 0 | } |
339 | | |
340 | | // remove standard mnemonics |
341 | 1 | aStr = aStr.replaceAt( i, 1, u"" ); |
342 | 1 | nLen--; |
343 | 1 | } |
344 | 521 | else |
345 | 521 | i++; |
346 | 522 | } |
347 | | |
348 | 45 | return aStr; |
349 | 45 | } |
350 | | |
351 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |