From d05e0be5f4ab2bdb31d6cf14528a9a8ee5ac965c Mon Sep 17 00:00:00 2001 From: AhmedHamed Date: Fri, 5 Jul 2024 21:41:26 +0300 Subject: [PATCH] tdf#161543 Enhance the searching functionality in FD & FW Change-Id: I1a21595228f886c942ae46d90e41705443d31550 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/170073 Reviewed-by: Heiko Tietze Reviewed-by: Andreas Heinisch Tested-by: Jenkins --- formula/source/ui/dlg/funcpage.cxx | 64 +++++++++++++----- formula/source/ui/dlg/funcpage.hxx | 7 ++ formula/uiconfig/ui/functionpage.ui | 30 +++++++-- include/unotools/textsearch.hxx | 27 ++++++++ sc/source/ui/formdlg/dwfunctr.cxx | 70 ++++++++++++------- sc/source/ui/inc/dwfunctr.hxx | 5 ++ sc/uiconfig/scalc/ui/functionpanel.ui | 22 +++++- unotools/source/i18n/textsearch.cxx | 97 +++++++++++++++++++++++++++ 8 files changed, 276 insertions(+), 46 deletions(-) diff --git a/formula/source/ui/dlg/funcpage.cxx b/formula/source/ui/dlg/funcpage.cxx index c844a29f612e..9724793928a5 100644 --- a/formula/source/ui/dlg/funcpage.cxx +++ b/formula/source/ui/dlg/funcpage.cxx @@ -25,6 +25,7 @@ #include "funcpage.hxx" #include #include +#include namespace formula { @@ -48,10 +49,10 @@ FuncPage::FuncPage(weld::Container* pParent, const IFunctionManager* _pFunctionM , m_xLbFunction(m_xBuilder->weld_tree_view(u"function"_ustr)) , m_xScratchIter(m_xLbFunction->make_iterator()) , m_xLbFunctionSearchString(m_xBuilder->weld_entry(u"search"_ustr)) + , m_xSimilaritySearch(m_xBuilder->weld_check_button(u"similaritysearch"_ustr)) , m_xHelpButton(m_xBuilder->weld_button(u"help"_ustr)) , m_pFunctionManager(_pFunctionManager) { - m_xLbFunction->make_sorted(); m_aHelpId = m_xLbFunction->get_help_id(); m_pFunctionManager->fillLastRecentlyUsedFunctions(aLRUList); @@ -76,6 +77,7 @@ FuncPage::FuncPage(weld::Container* pParent, const IFunctionManager* _pFunctionM m_xLbFunction->connect_row_activated(LINK(this, FuncPage, DblClkHdl)); m_xLbFunction->connect_key_press(LINK(this, FuncPage, KeyInputHdl)); m_xLbFunctionSearchString->connect_changed(LINK(this, FuncPage, ModifyHdl)); + m_xSimilaritySearch->connect_toggled(LINK(this, FuncPage, SimilarityToggleHdl)); m_xHelpButton->connect_clicked(LINK(this, FuncPage, SelHelpClickHdl)); m_xHelpButton->set_sensitive(false); @@ -117,12 +119,26 @@ void FuncPage::impl_addFunctions(const IFunctionCategory* _pCategory, bool bFill } } +void FuncPage::SearchFunction(const OUString& rFuncName, const OUString& rSearchString, + TFunctionDesc pDesc, const bool bSimilaritySearch) +{ + std::pair score = std::make_pair(0, 0); + if (bSimilaritySearch && !utl::TextSearch::SimilaritySearch(rFuncName, rSearchString, score)) + return; + if (!bSimilaritySearch && rFuncName.indexOf(rSearchString) < 0 + && rSearchString.indexOf(rFuncName) < 0) + return; + + sFuncScores.insert(std::make_pair(score, std::make_pair(rFuncName, pDesc))); +} + //aStr is non-empty when user types in the search box to search some function void FuncPage::UpdateFunctionList(const OUString& aStr) { m_xLbFunction->clear(); m_xLbFunction->freeze(); mCategories.clear(); + sFuncScores.clear(); const sal_Int32 nSelPos = m_xLbCategory->get_active(); bool bCollapse = nSelPos == 1; @@ -195,21 +211,23 @@ void FuncPage::UpdateFunctionList(const OUString& aStr) for (sal_uInt32 j = 0; j < nFunctionCount; ++j) { TFunctionDesc pDesc(pCategory->getFunction(j)); - // tdf#146781 - search for the desired function also in the description - if (rCharClass.uppercase(pDesc->getFunctionName()).indexOf(aSearchStr) >= 0 - || rCharClass.uppercase(pDesc->getDescription()).indexOf(aSearchStr) >= 0) - { - if (!pDesc->isHidden()) - { - OUString aFunction(pDesc->getFunctionName()); - OUString sId(weld::toId(pDesc)); + const OUString aFunction(rCharClass.uppercase(pDesc->getFunctionName())); + SearchFunction(aFunction, aSearchStr, pDesc, m_xSimilaritySearch->get_active()); + } + } - weld::TreeIter* pCategoryIter - = FillCategoriesMap(pCategory->getName(), bCollapse); - m_xLbFunction->insert(pCategoryIter, -1, &aFunction, &sId, nullptr, nullptr, - false, m_xScratchIter.get()); - } - } + for (const auto& func : sFuncScores) + { + TFunctionDesc pDesc(func.second.second); + if (!pDesc->isHidden()) + { + const OUString aCategory(pDesc->getCategory()->getName()); + const OUString aFunction(func.second.first); + const OUString aFuncDescId(weld::toId(pDesc)); + weld::TreeIter* pCategory = FillCategoriesMap(aCategory, bCollapse); + + m_xLbFunction->insert(pCategory, -1, &aFunction, &aFuncDescId, nullptr, nullptr, + false, m_xScratchIter.get()); } } } @@ -231,11 +249,12 @@ void FuncPage::UpdateFunctionList(const OUString& aStr) IMPL_LINK_NOARG(FuncPage, SelComboBoxHdl, weld::ComboBox&, void) { - m_xLbFunctionSearchString->set_sensitive(m_xLbCategory->get_active() > 0); + if (m_xLbCategory->get_active() == 0) + m_xLbFunctionSearchString->set_text(u""_ustr); + m_xHelpButton->set_sensitive(false); OUString searchStr = m_xLbFunctionSearchString->get_text(); m_xLbFunction->set_help_id(m_aHelpId); UpdateFunctionList(searchStr); - m_xHelpButton->set_sensitive(false); } IMPL_LINK_NOARG(FuncPage, SelTreeViewHdl, weld::TreeView&, void) @@ -270,6 +289,17 @@ IMPL_LINK_NOARG(FuncPage, DblClkHdl, weld::TreeView&, bool) } IMPL_LINK_NOARG(FuncPage, ModifyHdl, weld::Entry&, void) +{ + if (m_xLbCategory->get_active() == 0) + { + m_xLbCategory->set_active(1); + m_xHelpButton->set_sensitive(false); + } + OUString searchStr = m_xLbFunctionSearchString->get_text(); + UpdateFunctionList(searchStr); +} + +IMPL_LINK_NOARG(FuncPage, SimilarityToggleHdl, weld::Toggleable&, void) { OUString searchStr = m_xLbFunctionSearchString->get_text(); UpdateFunctionList(searchStr); diff --git a/formula/source/ui/dlg/funcpage.hxx b/formula/source/ui/dlg/funcpage.hxx index 0df77e1b156b..010bca872832 100644 --- a/formula/source/ui/dlg/funcpage.hxx +++ b/formula/source/ui/dlg/funcpage.hxx @@ -21,6 +21,7 @@ #include #include +#include #include namespace formula @@ -42,6 +43,7 @@ private: std::unique_ptr m_xLbFunction; std::unique_ptr m_xScratchIter; std::unique_ptr m_xLbFunctionSearchString; + std::unique_ptr m_xSimilaritySearch; std::unique_ptr m_xHelpButton; Link aDoubleClickLink; @@ -50,6 +52,8 @@ private: ::std::vector< TFunctionDesc > aLRUList; ::std::unordered_map> mCategories; + ::std::set, std::pair>> + sFuncScores; OUString m_aHelpId; // tdf#104487 - remember last used function category @@ -64,6 +68,7 @@ private: DECL_LINK(KeyInputHdl, const KeyEvent&, bool); DECL_LINK(ModifyHdl, weld::Entry&, void); DECL_LINK(SelHelpClickHdl, weld::Button&, void); + DECL_LINK(SimilarityToggleHdl, weld::Toggleable&, void); void UpdateFunctionList(const OUString&); @@ -92,6 +97,8 @@ public: void SetSelectHdl( const Link& rLink ) { aSelectionLink = rLink; } bool IsVisible() const { return m_xContainer->get_visible(); } + + void SearchFunction(const OUString&, const OUString&, TFunctionDesc, const bool); }; } // formula diff --git a/formula/uiconfig/ui/functionpage.ui b/formula/uiconfig/ui/functionpage.ui index 4ed38281cd1b..c48beb6918ac 100644 --- a/formula/uiconfig/ui/functionpage.ui +++ b/formula/uiconfig/ui/functionpage.ui @@ -52,6 +52,26 @@ 1 + + + Similar + True + True + False + True + True + + + Search and Sort functions by similarity + + + + + False + True + 2 + + True @@ -64,7 +84,7 @@ False True - 2 + 3 @@ -84,7 +104,7 @@ False True - 3 + 4 @@ -99,7 +119,7 @@ False True - 4 + 5 @@ -147,7 +167,7 @@ True True - 5 + 6 @@ -163,7 +183,7 @@ False True - 6 + 7 diff --git a/include/unotools/textsearch.hxx b/include/unotools/textsearch.hxx index 753534166098..3b06c93549d5 100644 --- a/include/unotools/textsearch.hxx +++ b/include/unotools/textsearch.hxx @@ -27,6 +27,9 @@ #include +#define WLD_THRESHOLD 3 +#define SMALL_STRING_THRESHOLD 4 + class CharClass; namespace com::sun::star::lang { struct Locale; } @@ -209,6 +212,30 @@ public: /* replace back references in the replace string by the sub expressions from the search result */ static void ReplaceBackReferences( OUString& rReplaceStr, std::u16string_view rStr, const css::util::SearchResult& rResult ); + + /** + * @brief Search for a string in a another one based on similarity + * @param rString The string we compare with + * @param rSearchString The search term + * @param rSimilarityScore The similarity score (sent by reference to be filled) + * @return True if the search term is found, false otherwise + */ + static bool SimilaritySearch(const OUString& rString, const OUString& rSearchString, + ::std::pair& rSimilarityScore); + /** + * @brief Get similarity score between two strings + * according to the length of the common substring and its position + * @param rString The string we compare with + * @param rSearchString The search term + * @param nInitialScore The initial score + * @param bFromStart True if the search is from the start + * @return Score if the search term is found in the text, -1 otherwise + */ + static sal_Int32 GetSubstringSimilarity(std::u16string_view rString, + std::u16string_view rSearchString, + sal_Int32& nInitialScore, const bool bFromStart); + static sal_Int32 GetWeightedLevenshteinDistance(const OUString& rString, + const OUString& rSearchString); }; } // namespace utl diff --git a/sc/source/ui/formdlg/dwfunctr.cxx b/sc/source/ui/formdlg/dwfunctr.cxx index c4cf19cabf94..3ca34e1cc118 100644 --- a/sc/source/ui/formdlg/dwfunctr.cxx +++ b/sc/source/ui/formdlg/dwfunctr.cxx @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -55,6 +56,7 @@ ScFunctionWin::ScFunctionWin(weld::Widget* pParent) , xScratchIter(xFuncList->make_iterator()) , xInsertButton(m_xBuilder->weld_button(u"insert"_ustr)) , xHelpButton(m_xBuilder->weld_button(u"help"_ustr)) + , xSimilaritySearch(m_xBuilder->weld_check_button(u"similaritysearch"_ustr)) , xFiFuncDesc(m_xBuilder->weld_text_view(u"funcdesc"_ustr)) , m_xSearchString(m_xBuilder->weld_entry(u"search"_ustr)) , xConfigListener(new comphelper::ConfigurationListener(u"/org.openoffice.Office.Calc/Formula/Syntax"_ustr)) @@ -79,6 +81,7 @@ ScFunctionWin::ScFunctionWin(weld::Widget* pParent) xFuncList->connect_row_activated(LINK( this, ScFunctionWin, SetRowActivatedHdl)); xInsertButton->connect_clicked(LINK( this, ScFunctionWin, SetSelectionClickHdl)); xHelpButton->connect_clicked(LINK( this, ScFunctionWin, SetHelpClickHdl)); + xSimilaritySearch->connect_toggled(LINK(this, ScFunctionWin, SetSimilarityToggleHdl)); xCatBox->set_active(0); @@ -109,6 +112,7 @@ ScFunctionWin::~ScFunctionWin() xFuncList.reset(); xInsertButton.reset(); xHelpButton.reset(); + xSimilaritySearch.reset(); xFiFuncDesc.reset(); } @@ -189,6 +193,19 @@ void ScFunctionWin::UpdateLRUList() } } +void ScFunctionWin::SearchFunction(const OUString& rFuncName, const OUString& rSearchString, + const ScFuncDesc* pDesc, const bool bSimilaritySearch) +{ + std::pair score = std::make_pair(0, 0); + if (bSimilaritySearch && !utl::TextSearch::SimilaritySearch(rFuncName, rSearchString, score)) + return; + if (!bSimilaritySearch && rFuncName.indexOf(rSearchString) < 0 + && rSearchString.indexOf(rFuncName) < 0) + return; + + sFuncScores.insert(std::make_pair(score, std::make_pair(rFuncName, pDesc))); +} + /************************************************************************* #* Member: SetDescription #*------------------------------------------------------------------------ @@ -252,6 +269,7 @@ void ScFunctionWin::UpdateFunctionList(const OUString& rSearchString) xFuncList->clear(); xFuncList->freeze(); mCategories.clear(); + sFuncScores.clear(); bool bCollapse = nCategory == 0; bool bFilter = !rSearchString.isEmpty(); @@ -270,38 +288,31 @@ void ScFunctionWin::UpdateFunctionList(const OUString& rSearchString) const ScFuncDesc* pDesc = pFuncMgr->First(nCategory); while (pDesc) { - OUString aCategory = pDesc->getCategory()->getName(); - OUString aFunction = pDesc->getFunctionName(); - OUString aFuncDescId = weld::toId(pDesc); + const OUString aCategory(pDesc->getCategory()->getName()); + const OUString aFunction(pCharClass->uppercase(pDesc->getFunctionName())); + const OUString aFuncDescId(weld::toId(pDesc)); - if (!bFilter || (pCharClass->uppercase(aFunction).startsWith(aSearchStr))) + if (bFilter) + SearchFunction(aFunction, aSearchStr, pDesc, xSimilaritySearch->get_active()); + else { weld::TreeIter* pCategory = FillCategoriesMap(aCategory, bCollapse); xFuncList->insert(pCategory, -1, &aFunction, &aFuncDescId, nullptr, nullptr, - false, xScratchIter.get()); + false, xScratchIter.get()); } pDesc = pFuncMgr->Next(); } - // Now add the functions that have the search string in the middle of the function name - // Note that this will only be necessary if the search string is not empty - if (bFilter) + for (const auto& func : sFuncScores) { - pDesc = pFuncMgr->First( nCategory ); - while ( pDesc ) - { - OUString aCategory = pDesc->getCategory()->getName(); - OUString aFunction = pDesc->getFunctionName(); - OUString aFuncDescId = weld::toId(pDesc); + pDesc = func.second.second; + const OUString aCategory(pDesc->getCategory()->getName()); + const OUString aFunction(func.second.first); + const OUString aFuncDescId(weld::toId(pDesc)); + weld::TreeIter* pCategory = FillCategoriesMap(aCategory, bCollapse); - if (pCharClass->uppercase(aFunction).indexOf(aSearchStr) > 0) - { - weld::TreeIter* pCategory = FillCategoriesMap(aCategory, bCollapse); - xFuncList->insert(pCategory, -1, &aFunction, &aFuncDescId, nullptr, nullptr, - false, xScratchIter.get()); - } - pDesc = pFuncMgr->Next(); - } + xFuncList->insert(pCategory, -1, &aFunction, &aFuncDescId, nullptr, nullptr, false, + xScratchIter.get()); } } else // LRU list @@ -477,6 +488,11 @@ void ScFunctionWin::DoEnter(bool bDoubleOrEnter) IMPL_LINK_NOARG(ScFunctionWin, ModifyHdl, weld::Entry&, void) { + if (xCatBox->get_active() == 0) + { + xCatBox->set_active(1); + xHelpButton->set_sensitive(false); + } OUString searchStr = m_xSearchString->get_text(); UpdateFunctionList(searchStr); SetDescription(); @@ -576,8 +592,9 @@ IMPL_LINK(ScFunctionWin, KeyInputHdl, const KeyEvent&, rEvent, bool) IMPL_LINK_NOARG(ScFunctionWin, SelComboHdl, weld::ComboBox&, void) { + if (xCatBox->get_active() == 0) + m_xSearchString->set_text(u""_ustr); xHelpButton->set_sensitive(xCatBox->get_active() != 1); - m_xSearchString->set_sensitive(xCatBox->get_active() > 0); OUString searchStr = m_xSearchString->get_text(); UpdateFunctionList(searchStr); SetDescription(); @@ -638,6 +655,13 @@ IMPL_LINK_NOARG( ScFunctionWin, SetHelpClickHdl, weld::Button&, void ) } } +IMPL_LINK_NOARG(ScFunctionWin, SetSimilarityToggleHdl, weld::Toggleable&, void) +{ + OUString searchStr = m_xSearchString->get_text(); + UpdateFunctionList(searchStr); + SetDescription(); +} + IMPL_LINK_NOARG( ScFunctionWin, SetRowActivatedHdl, weld::TreeView&, bool ) { DoEnter(true); // saves the input diff --git a/sc/source/ui/inc/dwfunctr.hxx b/sc/source/ui/inc/dwfunctr.hxx index e393f2a69fcb..9c05491dac2d 100644 --- a/sc/source/ui/inc/dwfunctr.hxx +++ b/sc/source/ui/inc/dwfunctr.hxx @@ -49,6 +49,7 @@ private: std::unique_ptr xScratchIter; std::unique_ptr xInsertButton; std::unique_ptr xHelpButton; + std::unique_ptr xSimilaritySearch; std::unique_ptr xFiFuncDesc; std::unique_ptr m_xSearchString; @@ -59,6 +60,8 @@ private: OUString m_aListHelpId; OUString m_aSearchHelpId; + ::std::set, std::pair>> + sFuncScores; ::std::vector< const formula::IFunctionDescription*> aLRUList; ::std::unordered_map> mCategories; @@ -70,6 +73,7 @@ private: DECL_LINK( SetRowActivatedHdl, weld::TreeView&, bool ); DECL_LINK( SetSelectionClickHdl, weld::Button&, void ); DECL_LINK( SetHelpClickHdl, weld::Button&, void ); + DECL_LINK( SetSimilarityToggleHdl, weld::Toggleable&, void ); DECL_LINK( SelComboHdl, weld::ComboBox&, void ); DECL_LINK( SelTreeHdl, weld::TreeView&, void ); DECL_LINK( ModifyHdl, weld::Entry&, void ); @@ -82,6 +86,7 @@ public: void InitLRUList(); void UpdateFunctionList(const OUString&); + void SearchFunction(const OUString&, const OUString&, const ScFuncDesc*, const bool); }; /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sc/uiconfig/scalc/ui/functionpanel.ui b/sc/uiconfig/scalc/ui/functionpanel.ui index 671aa7149270..f2bade14020e 100644 --- a/sc/uiconfig/scalc/ui/functionpanel.ui +++ b/sc/uiconfig/scalc/ui/functionpanel.ui @@ -145,7 +145,7 @@ True False - vertical + horizontal 6 @@ -166,6 +166,26 @@ 1 + + + Similar + True + True + False + True + True + + + Search and Sort functions by similarity + + + + + False + True + 2 + + 0 diff --git a/unotools/source/i18n/textsearch.cxx b/unotools/source/i18n/textsearch.cxx index 9c4573c38538..5958240cda44 100644 --- a/unotools/source/i18n/textsearch.cxx +++ b/unotools/source/i18n/textsearch.cxx @@ -353,6 +353,103 @@ void TextSearch::ReplaceBackReferences( OUString& rReplaceStr, std::u16string_vi rReplaceStr = sBuff.makeStringAndClear(); } +bool TextSearch::SimilaritySearch(const OUString& rString, const OUString& rSearchString, + ::std::pair& rSimilarityScore) +{ + sal_Int32 nScore = 0; + sal_Int32 nFirstScore = GetSubstringSimilarity(rString, rSearchString, nScore, true); + if (nFirstScore == -1) + nFirstScore = GetSubstringSimilarity(rString, rSearchString, nScore, false); + if (nFirstScore == -1) + { + if (rSearchString.getLength() == 1) + { + if (rString.startsWith(rSearchString)) + nFirstScore = nScore; + else if (rString.endsWith(rSearchString)) + nFirstScore = nScore + 1; + nScore += 2; + } + else if (rString.getLength() == 1 && rSearchString.getLength() < SMALL_STRING_THRESHOLD) + { + if (rSearchString.startsWith(rString)) + nFirstScore = nScore; + else if (rSearchString.endsWith(rString)) + nFirstScore = nScore + 1; + nScore += 2; + } + } + sal_Int32 nSecondScore = GetWeightedLevenshteinDistance(rString, rSearchString); + + if (nFirstScore == -1 && nSecondScore >= WLD_THRESHOLD) + return false; + + rSimilarityScore.first = (nFirstScore == -1) ? nScore : nFirstScore; + rSimilarityScore.second = nSecondScore; + return true; +} + +sal_Int32 TextSearch::GetSubstringSimilarity(std::u16string_view rString, + std::u16string_view rSearchString, + sal_Int32& nInitialScore, const bool bFromStart) +{ + sal_Int32 nScore = -1; + for (sal_Int32 length = rSearchString.length(); length > 1; length--) + { + sal_Int32 nStartPos = bFromStart ? 0 : rSearchString.length() - length; + std::u16string_view rSearchSubString = rSearchString.substr(nStartPos, length); + if (rString.starts_with(rSearchSubString)) + { + nScore = nInitialScore; + break; + } + else if (rString.ends_with(rSearchSubString)) + { + nScore = nInitialScore + 1; + break; + } + else if (rString.find(rSearchSubString) != std::u16string_view::npos) + { + nScore = nInitialScore + 2; + break; + } + nInitialScore += 3; + } + return nScore; +} + +sal_Int32 TextSearch::GetWeightedLevenshteinDistance(const OUString& rString, + const OUString& rSearchString) +{ + sal_Int32 n = rString.getLength(); + sal_Int32 m = rSearchString.getLength(); + std::vector> ScoreDP(n + 1, std::vector(m + 1)); + + for (sal_Int32 i = 0; i <= n; i++) + { + ScoreDP[i][0] = i; + } + for (sal_Int32 j = 0; j <= m; j++) + { + ScoreDP[0][j] = j; + } + + for (sal_Int32 i = 1; i <= n; i++) + { + for (sal_Int32 j = 1; j <= m; j++) + { + sal_Int32& minE = ScoreDP[i][j]; + minE = ScoreDP[i - 1][j] + 1; + minE = std::min(minE, ScoreDP[i][j - 1] + 1); + if (rString[i - 1] != rSearchString[j - 1]) + minE = std::min(minE, ScoreDP[i - 1][j - 1] + 2); + else + minE = std::min(minE, ScoreDP[i - 1][j - 1]); + } + } + return ScoreDP[n][m]; +} + } // namespace utl /* vim:set shiftwidth=4 softtabstop=4 expandtab: */