svx/source/dialog/langbox.cxx |  106 ++++++++++++++++++++++++++++++++----------
 1 file changed, 81 insertions(+), 25 deletions(-)

New commits:
commit 6e540ab56e397d645b300d6cfbadc5c11a1e8151
Author:     Mike Kaganski <[email protected]>
AuthorDate: Sat Feb 3 16:08:54 2024 +0600
Commit:     Mike Kaganski <[email protected]>
CommitDate: Sun Feb 4 03:39:03 2024 +0100

    Make sure to use strict weak ordering when sorting
    
    See 
https://gerrit.libreoffice.org/c/core/+/152676/2#message-ea10fe33b46fee581d47c04d04291a59049d4ac3
    
    The complexity of intended sort requires to split the languages by
    language groups first, and use the "generic first" comparison only
    inside groups.
    
    As the result, e.g. Aranese and Occitan are grouped together.
    
    Change-Id: Ibf11e72c4dfb7876eecea7f3f302c313d153bcbc
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/162947
    Tested-by: Jenkins
    Reviewed-by: Mike Kaganski <[email protected]>

diff --git a/svx/source/dialog/langbox.cxx b/svx/source/dialog/langbox.cxx
index 713bf0d34b3f..f83cc956961e 100644
--- a/svx/source/dialog/langbox.cxx
+++ b/svx/source/dialog/langbox.cxx
@@ -17,6 +17,11 @@
  *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
  */
 
+#include <sal/config.h>
+
+#include <map>
+#include <unordered_map>
+
 #include <com/sun/star/linguistic2/XAvailableLocales.hpp>
 #include <com/sun/star/linguistic2/XLinguServiceManager2.hpp>
 #include <com/sun/star/linguistic2/XSpellChecker1.hpp>
@@ -28,12 +33,14 @@
 #include <svtools/langtab.hxx>
 #include <i18nlangtag/mslangid.hxx>
 #include <i18nlangtag/lang.h>
+#include <i18nlangtag/languagetagicu.hxx>
 #include <editeng/unolingu.hxx>
 #include <svl/languageoptions.hxx>
 #include <svx/langbox.hxx>
 #include <svx/dialmgr.hxx>
 #include <svx/strings.hrc>
 #include <bitmaps.hlst>
+#include <o3tl/sorted_vector.hxx>
 
 #include <comphelper/string.hxx>
 #include <comphelper/processfactory.hxx>
@@ -190,44 +197,93 @@ void SvxLanguageBox::AddLanguages(const std::vector< 
LanguageType >& rLanguageTy
 
 static void SortLanguages(std::vector<weld::ComboBoxEntry>& rEntries)
 {
-    auto langLess = [](const weld::ComboBoxEntry& e1, const 
weld::ComboBoxEntry& e2)
+    struct NaturalStringSorterCompare
+    {
+        bool operator()(const OUString& rLHS, const OUString& rRHS) const
+        {
+            static const auto aSorter = 
comphelper::string::NaturalStringSorter(
+                comphelper::getProcessComponentContext(),
+                Application::GetSettings().GetUILanguageTag().getLocale());
+            return aSorter.compare(rLHS, rRHS) < 0;
+        }
+    };
+
+    struct EntryData
+    {
+        LanguageTag tag;
+        weld::ComboBoxEntry entry;
+    };
+
+    struct GenericFirst
     {
-        if (e1.sId == e2.sId)
-            return false; // shortcut
-        // Make sure that e.g. generic 'Spanish {es}' goes before 'Spanish 
(Argentina)'.
-        // We can't depend on MsLangId::getPrimaryLanguage/getSubLanguage, 
because e.g.
-        // for generic Bosnian {bs}, the MS-LCID is 0x781A, and getSubLanguage 
is not 0.
-        // So we have to do the expensive LanguageTag construction.
-        LanguageTag lt1(LanguageType(e1.sId.toInt32())), 
lt2(LanguageType(e2.sId.toInt32()));
-        if (lt1.getLanguage() == lt2.getLanguage())
+        bool operator()(const EntryData& e1, const EntryData& e2) const
         {
-            const bool isLangOnly1 = lt1.isIsoLocale() && 
lt1.getCountry().isEmpty();
-            const bool isLangOnly2 = lt2.isIsoLocale() && 
lt2.getCountry().isEmpty();
+            assert(e1.tag.getLanguage() == e2.tag.getLanguage());
+            if (e1.entry.sId == e2.entry.sId)
+                return false; // shortcut
+
+            // Make sure that e.g. generic 'Spanish {es}' goes before 'Spanish 
(Argentina)'.
+            // We can't depend on MsLangId::getPrimaryLanguage/getSubLanguage, 
because e.g.
+            // for generic Bosnian {bs}, the MS-LCID is 0x781A, and 
getSubLanguage is not 0.
+            // So we have to do the expensive LanguageTag construction in 
EntryData.
+
+            const bool isLangOnly1 = e1.tag.isIsoLocale() && 
e1.tag.getCountry().isEmpty();
+            const bool isLangOnly2 = e2.tag.isIsoLocale() && 
e2.tag.getCountry().isEmpty();
+            assert(!(isLangOnly1 && isLangOnly2));
 
             if (isLangOnly1)
             {
-                // lt1 is a generic language-only tag
-                if (!isLangOnly2)
-                    return true; // lt2 is not
+                // e1.tag is a generic language-only tag, e2.tag is not
+                return true;
             }
             else if (isLangOnly2)
             {
-                // lt2 is a generic language-only tag, lt1 is not
+                // e2.tag is a generic language-only tag, e1.tag is not
                 return false;
             }
+
+            // Do a normal string comparison for other cases
+            return NaturalStringSorterCompare()(e1.entry.sString, 
e2.entry.sString);
         }
-        // Do a normal string comparison for other cases
-        static const auto aSorter = comphelper::string::NaturalStringSorter(
-            comphelper::getProcessComponentContext(),
-            Application::GetSettings().GetUILanguageTag().getLocale());
-        return aSorter.compare(e1.sString, e2.sString) < 0;
     };
+    using SortedLangEntries = o3tl::sorted_vector<EntryData, GenericFirst>;
+
+    // It is impossible to sort using only GenericFirst comparison: it would 
fail the strict weak
+    // ordering requirement, where the following simplified example would fail 
the last assertion:
+    //
+    //  weld::ComboBoxEntry nn{ u"노르웨이어(니노르스크) {nn}"_ustr, "30740", "" }
+    //  weld::ComboBoxEntry nn_NO{ u"노르웨이어 뉘노르스크>"_ustr, "2068", "" };
+    //  weld::ComboBoxEntry nb_NO{ u"노르웨이어 부크몰"_ustr, "1044", "" };
+    //
+    //  assert(GenericFirst(nn, nn_NO));
+    //  assert(GenericFirst(nn_NO, nb_NO));
+    //  assert(GenericFirst(nn, nb_NO));
+    //
+    // So only sort this way inside language groups, where the data set itself 
guarantees the
+    // comparison's strict weak ordering.
+
+    // 1. Create lang-to-set-of-ComboBoxEntry map
+    std::unordered_map<OUString, SortedLangEntries> langToEntriesMap;
+
+    for (const auto& entry : rEntries)
+    {
+        LanguageTag tag(LanguageType(entry.sId.toInt32()));
+        langToEntriesMap[tag.getLanguage()].insert({ tag, entry }); // also 
makes unique
+    }
+
+    // 2. Sort using generic language's translated name, plus ISO language tag 
appended just in case
+    std::map<OUString, const SortedLangEntries&, NaturalStringSorterCompare> 
finalSort;
+    const LanguageTag& uiLang = Application::GetSettings().GetUILanguageTag();
+    for (const auto& [lang, lang_entries] : langToEntriesMap)
+    {
+        OUString translatedLangName = 
LanguageTagIcu::getDisplayName(LanguageTag(lang), uiLang);
+        finalSort.emplace(translatedLangName + "_" + lang, lang_entries);
+    }
 
-    std::sort(rEntries.begin(), rEntries.end(), langLess);
-    rEntries.erase(std::unique(rEntries.begin(), rEntries.end(),
-                               [](const weld::ComboBoxEntry& e1, const 
weld::ComboBoxEntry& e2)
-                               { return e1.sId == e2.sId; }),
-                   rEntries.end());
+    rEntries.clear();
+    for ([[maybe_unused]] const auto& [lang, lang_entries] : finalSort)
+        for (auto& entryData : lang_entries)
+            rEntries.push_back(entryData.entry);
 }
 
 void SvxLanguageBox::SetLanguageList(SvxLanguageListFlags nLangList, bool 
bHasLangNone,

Reply via email to