1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <sal/config.h>
23 #include <unordered_map>
25 #include <com/sun/star/linguistic2/XAvailableLocales.hpp>
26 #include <com/sun/star/linguistic2/XLinguServiceManager2.hpp>
27 #include <com/sun/star/linguistic2/XSpellChecker1.hpp>
28 #include <linguistic/misc.hxx>
29 #include <rtl/ustring.hxx>
30 #include <sal/log.hxx>
31 #include <unotools/localedatawrapper.hxx>
32 #include <tools/urlobj.hxx>
33 #include <svtools/langtab.hxx>
34 #include <i18nlangtag/mslangid.hxx>
35 #include <i18nlangtag/lang.h>
36 #include <i18nlangtag/languagetagicu.hxx>
37 #include <editeng/unolingu.hxx>
38 #include <svl/languageoptions.hxx>
39 #include <svx/langbox.hxx>
40 #include <svx/dialmgr.hxx>
41 #include <svx/strings.hrc>
42 #include <bitmaps.hlst>
43 #include <o3tl/sorted_vector.hxx>
45 #include <comphelper/string.hxx>
46 #include <comphelper/processfactory.hxx>
47 #include <comphelper/scopeguard.hxx>
48 #include <vcl/svapp.hxx>
49 #include <vcl/settings.hxx>
51 using namespace ::com::sun::star::util
;
52 using namespace ::com::sun::star::linguistic2
;
53 using namespace ::com::sun::star::uno
;
55 OUString
GetDicInfoStr( std::u16string_view rName
, const LanguageType nLang
, bool bNeg
)
57 INetURLObject aURLObj
;
58 aURLObj
.SetSmartProtocol( INetProtocol::File
);
59 aURLObj
.SetSmartURL( rName
, INetURLObject::EncodeMechanism::All
);
60 OUString
aTmp( aURLObj
.GetBase() + " " );
67 if ( LANGUAGE_NONE
== nLang
)
68 aTmp
+= SvxResId(RID_SVXSTR_LANGUAGE_ALL
);
71 aTmp
+= "[" + SvtLanguageTable::GetLanguageString( nLang
) + "]";
77 // misc local helper functions
78 static void appendLocaleSeqToLangs(Sequence
<css::lang::Locale
> const& rSeq
,
79 std::vector
<LanguageType
>& aLangs
)
81 sal_Int32 nCount
= rSeq
.getLength();
83 aLangs
.reserve(aLangs
.size() + nCount
);
85 std::transform(rSeq
.begin(), rSeq
.end(), std::back_inserter(aLangs
),
86 [](const css::lang::Locale
& rLocale
) -> LanguageType
{
87 return LanguageTag::convertToLanguageType(rLocale
); });
90 static bool lcl_SeqHasLang( const Sequence
< sal_Int16
> & rLangSeq
, sal_Int16 nLang
)
92 return rLangSeq
.hasElements()
93 && std::find(rLangSeq
.begin(), rLangSeq
.end(), nLang
) != rLangSeq
.end();
98 bool lcl_isPrerequisite(LanguageType nLangType
, bool requireSublang
)
101 nLangType
!= LANGUAGE_DONTKNOW
&&
102 nLangType
!= LANGUAGE_SYSTEM
&&
103 nLangType
!= LANGUAGE_NONE
&&
104 nLangType
!= LANGUAGE_MULTIPLE
&&
105 nLangType
!= LANGUAGE_UNDETERMINED
&&
106 nLangType
!= LANGUAGE_USER_KEYID
&&
107 !MsLangId::isLegacy( nLangType
) &&
108 (!requireSublang
|| MsLangId::getSubLanguage( nLangType
));
111 bool lcl_isScriptTypeRequested( LanguageType nLangType
, SvxLanguageListFlags nLangList
)
114 bool(nLangList
& SvxLanguageListFlags::ALL
) ||
115 (bool(nLangList
& SvxLanguageListFlags::WESTERN
) &&
116 (SvtLanguageOptions::GetScriptTypeOfLanguage(nLangType
) == SvtScriptType::LATIN
)) ||
117 (bool(nLangList
& SvxLanguageListFlags::CTL
) &&
118 (SvtLanguageOptions::GetScriptTypeOfLanguage(nLangType
) == SvtScriptType::COMPLEX
)) ||
119 (bool(nLangList
& SvxLanguageListFlags::CJK
) &&
120 (SvtLanguageOptions::GetScriptTypeOfLanguage(nLangType
) == SvtScriptType::ASIAN
));
126 LanguageType
SvxLanguageBox::get_active_id() const
128 OUString sLang
= m_xControl
->get_active_id();
129 if (!sLang
.isEmpty())
130 return LanguageType(sLang
.toInt32());
132 return LANGUAGE_DONTKNOW
;
135 int SvxLanguageBox::find_id(const LanguageType eLangType
) const
137 return m_xControl
->find_id(OUString::number(static_cast<sal_uInt16
>(eLangType
)));
140 void SvxLanguageBox::set_id(int pos
, const LanguageType eLangType
)
142 m_xControl
->set_id(pos
, OUString::number(static_cast<sal_uInt16
>(eLangType
)));
145 LanguageType
SvxLanguageBox::get_id(int pos
) const
147 return LanguageType(m_xControl
->get_id(pos
).toInt32());
150 void SvxLanguageBox::remove_id(const LanguageType eLangType
)
152 m_xControl
->remove_id(OUString::number(static_cast<sal_uInt16
>(eLangType
)));
155 void SvxLanguageBox::append(const LanguageType eLangType
, const OUString
& rStr
)
157 m_xControl
->append(OUString::number(static_cast<sal_uInt16
>(eLangType
)), rStr
);
160 void SvxLanguageBox::set_active_id(const LanguageType eLangType
)
162 // If the core uses a LangID of an imported MS document and wants to select
163 // a language that is replaced, we need to select the replacement instead.
164 LanguageType nLang
= MsLangId::getReplacementForObsoleteLanguage( eLangType
);
166 sal_Int32 nAt
= find_id( nLang
);
170 InsertLanguage( nLang
); // on-the-fly-ID
171 nAt
= find_id( nLang
);
175 m_xControl
->set_active(nAt
);
178 void SvxLanguageBox::AddLanguages(const std::vector
< LanguageType
>& rLanguageTypes
,
179 SvxLanguageListFlags nLangList
, std::vector
<weld::ComboBoxEntry
>& rEntries
, bool requireSublang
)
181 for ( auto const & nLangType
: rLanguageTypes
)
183 if (lcl_isPrerequisite(nLangType
, requireSublang
))
185 LanguageType nLang
= MsLangId::getReplacementForObsoleteLanguage( nLangType
);
186 if (lcl_isScriptTypeRequested( nLang
, nLangList
))
188 int nAt
= find_id(nLang
);
191 weld::ComboBoxEntry
aNewEntry(BuildEntry(nLang
));
192 if (aNewEntry
.sString
.isEmpty())
194 rEntries
.push_back(aNewEntry
);
200 static void SortLanguages(std::vector
<weld::ComboBoxEntry
>& rEntries
)
202 struct NaturalStringSorterCompare
204 bool operator()(const OUString
& rLHS
, const OUString
& rRHS
) const
206 static const auto aSorter
= comphelper::string::NaturalStringSorter(
207 comphelper::getProcessComponentContext(),
208 Application::GetSettings().GetUILanguageTag().getLocale());
209 return aSorter
.compare(rLHS
, rRHS
) < 0;
216 weld::ComboBoxEntry entry
;
221 bool operator()(const EntryData
& e1
, const EntryData
& e2
) const
223 assert(e1
.tag
.getLanguage() == e2
.tag
.getLanguage());
224 if (e1
.entry
.sId
== e2
.entry
.sId
)
225 return false; // shortcut
227 // Make sure that e.g. generic 'Spanish {es}' goes before 'Spanish (Argentina)'.
228 // We can't depend on MsLangId::getPrimaryLanguage/getSubLanguage, because e.g.
229 // for generic Bosnian {bs}, the MS-LCID is 0x781A, and getSubLanguage is not 0.
230 // So we have to do the expensive LanguageTag construction in EntryData.
232 const bool isLangOnly1
= e1
.tag
.isIsoLocale() && e1
.tag
.getCountry().isEmpty();
233 const bool isLangOnly2
= e2
.tag
.isIsoLocale() && e2
.tag
.getCountry().isEmpty();
234 assert(!(isLangOnly1
&& isLangOnly2
));
238 // e1.tag is a generic language-only tag, e2.tag is not
241 else if (isLangOnly2
)
243 // e2.tag is a generic language-only tag, e1.tag is not
247 // Do a normal string comparison for other cases
248 return NaturalStringSorterCompare()(e1
.entry
.sString
, e2
.entry
.sString
);
251 using SortedLangEntries
= o3tl::sorted_vector
<EntryData
, GenericFirst
>;
253 // It is impossible to sort using only GenericFirst comparison: it would fail the strict weak
254 // ordering requirement, where the following simplified example would fail the last assertion:
256 // weld::ComboBoxEntry nn{ u"노르웨이어(니노르스크) {nn}"_ustr, "30740", "" }
257 // weld::ComboBoxEntry nn_NO{ u"노르웨이어 뉘노르스크>"_ustr, "2068", "" };
258 // weld::ComboBoxEntry nb_NO{ u"노르웨이어 부크몰"_ustr, "1044", "" };
260 // assert(GenericFirst(nn, nn_NO));
261 // assert(GenericFirst(nn_NO, nb_NO));
262 // assert(GenericFirst(nn, nb_NO));
264 // So only sort this way inside language groups, where the data set itself guarantees the
265 // comparison's strict weak ordering.
267 // 1. Create lang-to-set-of-ComboBoxEntry map
268 std::unordered_map
<OUString
, SortedLangEntries
> langToEntriesMap
;
270 for (const auto& entry
: rEntries
)
272 LanguageType
languageType(entry
.sId
.toInt32());
273 // Remove LANGUAGE_USER_SYSTEM_CONFIG special entry and friends from the list
274 if (languageType
>= LanguageType(0xFFE0))
276 LanguageTag
tag(languageType
);
277 langToEntriesMap
[tag
.getLanguage()].insert({ tag
, entry
}); // also makes unique
280 // 2. Sort using generic language's translated name, plus ISO language tag appended just in case
281 std::map
<OUString
, const SortedLangEntries
&, NaturalStringSorterCompare
> finalSort
;
282 const LanguageTag
& uiLang
= Application::GetSettings().GetUILanguageTag();
283 for (const auto& [lang
, lang_entries
] : langToEntriesMap
)
285 OUString translatedLangName
= LanguageTagIcu::getDisplayName(LanguageTag(lang
), uiLang
);
286 finalSort
.emplace(translatedLangName
+ "_" + lang
, lang_entries
);
290 for ([[maybe_unused
]] const auto& [lang
, lang_entries
] : finalSort
)
291 for (auto& entryData
: lang_entries
)
292 rEntries
.push_back(entryData
.entry
);
295 void SvxLanguageBox::SetLanguageList(SvxLanguageListFlags nLangList
, bool bHasLangNone
,
296 bool bLangNoneIsLangAll
, bool bCheckSpellAvail
,
297 bool bDefaultLangExist
, LanguageType eDefaultLangType
,
298 sal_Int16 nDefaultType
)
300 m_bHasLangNone
= bHasLangNone
;
301 m_bLangNoneIsLangAll
= bLangNoneIsLangAll
;
302 m_bWithCheckmark
= bCheckSpellAvail
;
304 m_xControl
->freeze();
305 comphelper::ScopeGuard
aThawGuard([this]() { m_xControl
->thaw(); });
308 if (SvxLanguageListFlags::EMPTY
== nLangList
)
311 bool bAddSeparator
= false;
315 m_xControl
->append(BuildEntry(LANGUAGE_NONE
));
316 m_xControl
->append(BuildEntry(LANGUAGE_MULTIPLE
));
317 m_xControl
->append(BuildEntry(LANGUAGE_UNDETERMINED
));
318 bAddSeparator
= true;
321 if (bDefaultLangExist
)
323 m_xControl
->append(BuildEntry(eDefaultLangType
, nDefaultType
));
324 bAddSeparator
= true;
328 m_xControl
->append_separator(u
""_ustr
);
330 bool bAddAvailable
= (!(nLangList
& SvxLanguageListFlags::ONLY_KNOWN
) &&
331 ((nLangList
& SvxLanguageListFlags::ALL
) ||
332 (nLangList
& SvxLanguageListFlags::WESTERN
) ||
333 (nLangList
& SvxLanguageListFlags::CTL
) ||
334 (nLangList
& SvxLanguageListFlags::CJK
)));
335 std::vector
< LanguageType
> aAvailLang
;
336 Sequence
< sal_Int16
> aSpellUsedLang
;
339 if (auto xAvail
= LinguMgr::GetLngSvcMgr())
341 appendLocaleSeqToLangs(xAvail
->getAvailableLocales(SN_SPELLCHECKER
), aAvailLang
);
342 appendLocaleSeqToLangs(xAvail
->getAvailableLocales(SN_HYPHENATOR
), aAvailLang
);
343 appendLocaleSeqToLangs(xAvail
->getAvailableLocales(SN_THESAURUS
), aAvailLang
);
346 if (SvxLanguageListFlags::SPELL_USED
& nLangList
)
348 Reference
< XSpellChecker1
> xTmp1
= LinguMgr::GetSpellChecker();
350 aSpellUsedLang
= xTmp1
->getLanguages();
353 std::vector
<LanguageType
> aKnown
;
355 if ( nLangList
& SvxLanguageListFlags::ONLY_KNOWN
)
357 aKnown
= LocaleDataWrapper::getInstalledLanguageTypes();
358 nCount
= aKnown
.size();
362 nCount
= SvtLanguageTable::GetLanguageEntryCount();
365 std::vector
<weld::ComboBoxEntry
> aEntries
;
366 for ( sal_uInt32 i
= 0; i
< nCount
; i
++ )
368 LanguageType nLangType
;
369 if ( nLangList
& SvxLanguageListFlags::ONLY_KNOWN
)
370 nLangType
= aKnown
[i
];
372 nLangType
= SvtLanguageTable::GetLanguageTypeAtIndex( i
);
373 if ( lcl_isPrerequisite( nLangType
, true ) &&
374 (lcl_isScriptTypeRequested( nLangType
, nLangList
) ||
375 (bool(nLangList
& SvxLanguageListFlags::FBD_CHARS
) &&
376 MsLangId::hasForbiddenCharacters(nLangType
)) ||
377 (bool(nLangList
& SvxLanguageListFlags::SPELL_USED
) &&
378 lcl_SeqHasLang(aSpellUsedLang
, static_cast<sal_uInt16
>(nLangType
)))
381 aEntries
.push_back(BuildEntry(nLangType
));
382 if (aEntries
.back().sString
.isEmpty())
389 // Spell checkers, hyphenators and thesauri may add language tags
391 AddLanguages(aAvailLang
, nLangList
, aEntries
, true);
394 SortLanguages(aEntries
);
395 m_xControl
->insert_vector(aEntries
, true);
398 void SvxLanguageBox::InsertLanguage(const LanguageType nLangType
)
400 if (find_id(nLangType
) != -1)
402 weld::ComboBoxEntry aEntry
= BuildEntry(nLangType
);
403 if (aEntry
.sString
.isEmpty())
405 m_xControl
->append(aEntry
);
408 void SvxLanguageBox::InsertLanguages(const std::vector
<LanguageType
>& rLanguageTypes
)
410 std::vector
<weld::ComboBoxEntry
> entries
;
411 AddLanguages(rLanguageTypes
, SvxLanguageListFlags::ALL
, entries
, false);
412 SortLanguages(entries
);
413 m_xControl
->insert_vector(entries
, true);
416 weld::ComboBoxEntry
SvxLanguageBox::BuildEntry(const LanguageType nLangType
, sal_Int16 nType
)
418 LanguageType nLang
= MsLangId::getReplacementForObsoleteLanguage(nLangType
);
419 // For obsolete and to be replaced languages check whether an entry of the
420 // replacement already exists and if so don't add an entry with identical
421 // string as would be returned by SvtLanguageTable::GetString().
422 if (nLang
!= nLangType
)
424 int nAt
= find_id( nLang
);
426 return weld::ComboBoxEntry(u
""_ustr
);
429 OUString aStrEntry
= (LANGUAGE_NONE
== nLang
&& m_bHasLangNone
&& m_bLangNoneIsLangAll
)
430 ? SvxResId(RID_SVXSTR_LANGUAGE_ALL
)
431 : SvtLanguageTable::GetLanguageString(nLang
);
433 LanguageType nRealLang
= nLang
;
434 if (nRealLang
== LANGUAGE_SYSTEM
)
436 nRealLang
= MsLangId::resolveSystemLanguageByScriptType(nRealLang
, nType
);
437 aStrEntry
+= " - " + SvtLanguageTable::GetLanguageString( nRealLang
);
439 else if (nRealLang
== LANGUAGE_USER_SYSTEM_CONFIG
)
441 nRealLang
= MsLangId::getSystemLanguage();
442 // Whatever we obtained, ensure a known supported locale.
443 nRealLang
= LanguageTag(nRealLang
).makeFallback().getLanguageType();
444 aStrEntry
+= " - " + SvtLanguageTable::GetLanguageString( nRealLang
);
447 if (m_bWithCheckmark
)
449 if (!m_xSpellUsedLang
)
451 Reference
<XSpellChecker1
> xSpell
= LinguMgr::GetSpellChecker();
453 m_xSpellUsedLang
.reset(new Sequence
<sal_Int16
>(xSpell
->getLanguages()));
456 bool bFound
= m_xSpellUsedLang
&& lcl_SeqHasLang(*m_xSpellUsedLang
, static_cast<sal_uInt16
>(nRealLang
));
458 return weld::ComboBoxEntry(aStrEntry
, OUString::number(static_cast<sal_uInt16
>(nLang
)), bFound
? RID_SVXBMP_CHECKED
: RID_SVXBMP_NOTCHECKED
);
461 return weld::ComboBoxEntry(aStrEntry
, OUString::number(static_cast<sal_uInt16
>(nLang
)));
464 IMPL_LINK(SvxLanguageBox
, ChangeHdl
, weld::ComboBox
&, rControl
, void)
466 if (rControl
.has_entry())
468 EditedAndValid eOldState
= m_eEditedAndValid
;
469 OUString
aStr(rControl
.get_active_text());
471 m_eEditedAndValid
= EditedAndValid::Invalid
;
474 const int nPos
= rControl
.find_text(aStr
);
477 int nStartSelectPos
, nEndSelectPos
;
478 rControl
.get_entry_selection_bounds(nStartSelectPos
, nEndSelectPos
);
480 // Select the corresponding listbox entry if not current. This
481 // invalidates the Edit Selection thus has to happen between
482 // obtaining the Selection and setting the new Selection.
483 int nSelPos
= m_xControl
->get_active();
484 bool bSetEditSelection
;
486 bSetEditSelection
= false;
489 m_xControl
->set_active(nPos
);
490 bSetEditSelection
= true;
493 // If typing into the Edit control led us here, advance start of a
494 // full selection by one so the next character will already
495 // continue the string instead of having to type the same character
496 // again to start a new string. The selection is in reverse
497 // when obtained from the Edit control.
498 if (nEndSelectPos
== 0)
500 OUString
aText(m_xControl
->get_active_text());
501 if (nStartSelectPos
== aText
.getLength())
504 bSetEditSelection
= true;
508 if (bSetEditSelection
)
509 rControl
.select_entry_region(nStartSelectPos
, nEndSelectPos
);
511 m_eEditedAndValid
= EditedAndValid::No
;
515 OUString aCanonicalized
;
516 bool bValid
= LanguageTag::isValidBcp47( aStr
, &aCanonicalized
, LanguageTag::PrivateUse::ALLOW_ART_X
);
517 m_eEditedAndValid
= (bValid
? EditedAndValid::Valid
: EditedAndValid::Invalid
);
518 if (bValid
&& aCanonicalized
!= aStr
)
520 m_xControl
->set_entry_text(aCanonicalized
);
521 const auto nCursorPos
= aCanonicalized
.getLength();
522 m_xControl
->select_entry_region(nCursorPos
, nCursorPos
);
526 if (eOldState
!= m_eEditedAndValid
)
528 if (m_eEditedAndValid
== EditedAndValid::Invalid
)
529 rControl
.set_entry_message_type(weld::EntryMessageType::Error
);
531 rControl
.set_entry_message_type(weld::EntryMessageType::Normal
);
534 m_aChangeHdl
.Call(rControl
);
537 SvxLanguageBox::SvxLanguageBox(std::unique_ptr
<weld::ComboBox
> pControl
)
538 : m_xControl(std::move(pControl
))
539 , m_eSavedLanguage(LANGUAGE_DONTKNOW
)
540 , m_eEditedAndValid(EditedAndValid::No
)
541 , m_bHasLangNone(false)
542 , m_bLangNoneIsLangAll(false)
543 , m_bWithCheckmark(false)
545 m_xControl
->connect_changed(LINK(this, SvxLanguageBox
, ChangeHdl
));
548 SvxLanguageBox
* SvxLanguageBox::SaveEditedAsEntry(SvxLanguageBox
* ppBoxes
[3])
550 if (m_eEditedAndValid
!= EditedAndValid::Valid
)
553 LanguageTag
aLanguageTag(m_xControl
->get_active_text());
554 LanguageType nLang
= aLanguageTag
.getLanguageType();
555 if (nLang
== LANGUAGE_DONTKNOW
)
557 SAL_WARN( "svx.dialog", "SvxLanguageBox::SaveEditedAsEntry: unknown tag");
561 for (size_t i
= 0; i
< 3; ++i
)
563 SvxLanguageBox
* pBox
= ppBoxes
[i
];
567 const int nPos
= pBox
->find_id( nLang
);
570 // Already present but with a different string or in another list.
571 pBox
->m_xControl
->set_active(nPos
);
576 if (SvtLanguageTable::HasLanguageType( nLang
))
578 // In SvtLanguageTable but not in SvxLanguageBox. On purpose? This
579 // may be an entry with different settings.
580 SAL_WARN( "svx.dialog", "SvxLanguageBox::SaveEditedAsEntry: already in SvtLanguageTable: " <<
581 SvtLanguageTable::GetLanguageString( nLang
) << ", " << nLang
);
585 // Add to SvtLanguageTable first. This at an on-the-fly LanguageTag
586 // also sets the ScriptType needed below.
587 SvtLanguageTable::AddLanguageTag( aLanguageTag
);
590 // Add to the proper list.
591 SvxLanguageBox
* pBox
= nullptr;
592 switch (MsLangId::getScriptType(nLang
))
595 case css::i18n::ScriptType::LATIN
:
598 case css::i18n::ScriptType::ASIAN
:
601 case css::i18n::ScriptType::COMPLEX
:
607 pBox
->InsertLanguage(nLang
);
610 const int nPos
= pBox
->find_id(nLang
);
612 pBox
->m_xControl
->set_active(nPos
);
617 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */