/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define STRING_DELIM char(0x0A) #define GLOS_TIMEOUT 30000 // update every 30 seconds #define FIND_MAX_GLOS 20 namespace { struct TripleString { OUString sGroup; OUString sBlock; OUString sShort; }; class SwGlossDecideDlg : public weld::GenericDialogController { std::unique_ptr m_xOk; std::unique_ptr m_xListLB; DECL_LINK(DoubleClickHdl, weld::TreeView&, bool); DECL_LINK(SelectHdl, weld::TreeView&, void); public: explicit SwGlossDecideDlg(weld::Window* pParent); weld::TreeView& GetTreeView() {return *m_xListLB;} }; } SwGlossDecideDlg::SwGlossDecideDlg(weld::Window* pParent) : GenericDialogController(pParent, "modules/swriter/ui/selectautotextdialog.ui", "SelectAutoTextDialog") , m_xOk(m_xBuilder->weld_button("ok")) , m_xListLB(m_xBuilder->weld_tree_view("treeview")) { m_xListLB->set_size_request(m_xListLB->get_approximate_digit_width() * 32, m_xListLB->get_height_rows(8)); m_xListLB->connect_row_activated(LINK(this, SwGlossDecideDlg, DoubleClickHdl)); m_xListLB->connect_changed(LINK(this, SwGlossDecideDlg, SelectHdl)); } IMPL_LINK_NOARG(SwGlossDecideDlg, DoubleClickHdl, weld::TreeView&, bool) { m_xDialog->response(RET_OK); return true; } IMPL_LINK_NOARG(SwGlossDecideDlg, SelectHdl, weld::TreeView&, void) { m_xOk->set_sensitive(m_xListLB->get_selected_index() != -1); } SwGlossaryList::SwGlossaryList() : bFilled(false) { SvtPathOptions aPathOpt; sPath = aPathOpt.GetAutoTextPath(); SetTimeout(GLOS_TIMEOUT); } SwGlossaryList::~SwGlossaryList() { ClearGroups(); } // If the GroupName is already known, then only rShortName // will be filled. Otherwise also rGroupName will be set and // on demand asked for the right group. bool SwGlossaryList::GetShortName(std::u16string_view rLongName, OUString& rShortName, OUString& rGroupName ) { if(!bFilled) Update(); std::vector aTripleStrings; size_t nCount = aGroupArr.size(); for(size_t i = 0; i < nCount; i++ ) { AutoTextGroup* pGroup = aGroupArr[i].get(); if(!rGroupName.isEmpty() && rGroupName != pGroup->sName) continue; sal_Int32 nPosLong = 0; for(sal_uInt16 j = 0; j < pGroup->nCount; j++) { const OUString sLong = pGroup->sLongNames.getToken(0, STRING_DELIM, nPosLong); if(rLongName != sLong) continue; TripleString aTriple; aTriple.sGroup = pGroup->sName; aTriple.sBlock = sLong; aTriple.sShort = pGroup->sShortNames.getToken(j, STRING_DELIM); aTripleStrings.push_back(aTriple); } } bool bRet = false; nCount = aTripleStrings.size(); if(1 == nCount) { const TripleString& rTriple(aTripleStrings.front()); rShortName = rTriple.sShort; rGroupName = rTriple.sGroup; bRet = true; } else if(1 < nCount) { SwView *pView = ::GetActiveView(); SwGlossDecideDlg aDlg(pView ? pView->GetFrameWeld() : nullptr); OUString sTitle = aDlg.get_title() + " " + aTripleStrings.front().sBlock; aDlg.set_title(sTitle); weld::TreeView& rLB = aDlg.GetTreeView(); for (const auto& rTriple : aTripleStrings) rLB.append_text(rTriple.sGroup.getToken(0, GLOS_DELIM)); rLB.select(0); if (aDlg.run() == RET_OK && rLB.get_selected_index() != -1) { const TripleString& rTriple(aTripleStrings[rLB.get_selected_index()]); rShortName = rTriple.sShort; rGroupName = rTriple.sGroup; bRet = true; } else bRet = false; } return bRet; } size_t SwGlossaryList::GetGroupCount() { if(!bFilled) Update(); return aGroupArr.size(); } OUString SwGlossaryList::GetGroupName(size_t nPos) { OSL_ENSURE(aGroupArr.size() > nPos, "group not available"); if(nPos < aGroupArr.size()) { AutoTextGroup* pGroup = aGroupArr[nPos].get(); OUString sRet = pGroup->sName; return sRet; } return OUString(); } OUString SwGlossaryList::GetGroupTitle(size_t nPos) { OSL_ENSURE(aGroupArr.size() > nPos, "group not available"); if(nPos < aGroupArr.size()) { AutoTextGroup* pGroup = aGroupArr[nPos].get(); return pGroup->sTitle; } return OUString(); } sal_uInt16 SwGlossaryList::GetBlockCount(size_t nGroup) { OSL_ENSURE(aGroupArr.size() > nGroup, "group not available"); if(nGroup < aGroupArr.size()) { AutoTextGroup* pGroup = aGroupArr[nGroup].get(); return pGroup->nCount; } return 0; } OUString SwGlossaryList::GetBlockLongName(size_t nGroup, sal_uInt16 nBlock) { OSL_ENSURE(aGroupArr.size() > nGroup, "group not available"); if(nGroup < aGroupArr.size()) { AutoTextGroup* pGroup = aGroupArr[nGroup].get(); return pGroup->sLongNames.getToken(nBlock, STRING_DELIM); } return OUString(); } OUString SwGlossaryList::GetBlockShortName(size_t nGroup, sal_uInt16 nBlock) { OSL_ENSURE(aGroupArr.size() > nGroup, "group not available"); if(nGroup < aGroupArr.size()) { AutoTextGroup* pGroup = aGroupArr[nGroup].get(); return pGroup->sShortNames.getToken(nBlock, STRING_DELIM); } return OUString(); } void SwGlossaryList::Update() { if(!IsActive()) Start(); SvtPathOptions aPathOpt; const OUString& sTemp( aPathOpt.GetAutoTextPath() ); if(sTemp != sPath) { sPath = sTemp; bFilled = false; ClearGroups(); } SwGlossaries* pGlossaries = ::GetGlossaries(); const std::vector & rPathArr = pGlossaries->GetPathArray(); const OUString sExt( SwGlossaries::GetExtension() ); if(!bFilled) { const size_t nGroupCount = pGlossaries->GetGroupCnt(); for(size_t i = 0; i < nGroupCount; ++i) { OUString sGrpName = pGlossaries->GetGroupName(i); const size_t nPath = static_cast( sGrpName.getToken(1, GLOS_DELIM).toInt32()); if( nPath < rPathArr.size() ) { std::unique_ptr pGroup(new AutoTextGroup); pGroup->sName = sGrpName; FillGroup(pGroup.get(), pGlossaries); OUString sName = rPathArr[nPath] + "/" + pGroup->sName.getToken(0, GLOS_DELIM) + sExt; FStatHelper::GetModifiedDateTimeOfFile( sName, &pGroup->aDateModified, &pGroup->aDateModified ); aGroupArr.insert( aGroupArr.begin(), std::move(pGroup) ); } } bFilled = true; } else { for( size_t nPath = 0; nPath < rPathArr.size(); nPath++ ) { std::vector aFoundGroupNames; std::vector aFiles; std::vector aDateTimeArr; SWUnoHelper::UCB_GetFileListOfFolder( rPathArr[nPath], aFiles, &sExt, &aDateTimeArr ); for( size_t nFiles = 0; nFiles < aFiles.size(); ++nFiles ) { const OUString aTitle = aFiles[ nFiles ]; ::DateTime& rDT = aDateTimeArr[ nFiles ]; OUString sName( aTitle.copy( 0, aTitle.getLength() - sExt.getLength() )); aFoundGroupNames.push_back(sName); sName += OUStringChar(GLOS_DELIM) + OUString::number( o3tl::narrowing(nPath) ); AutoTextGroup* pFound = FindGroup( sName ); if( !pFound ) { pFound = new AutoTextGroup; pFound->sName = sName; FillGroup( pFound, pGlossaries ); pFound->aDateModified = rDT; aGroupArr.push_back(std::unique_ptr(pFound)); } else if( pFound->aDateModified < rDT ) { FillGroup(pFound, pGlossaries); pFound->aDateModified = rDT; } } for( size_t i = aGroupArr.size(); i>0; ) { --i; // maybe remove deleted groups AutoTextGroup* pGroup = aGroupArr[i].get(); const size_t nGroupPath = static_cast( pGroup->sName.getToken( 1, GLOS_DELIM).toInt32()); // Only the groups will be checked which are registered // for the current subpath. if( nGroupPath == nPath ) { OUString sCompareGroup = pGroup->sName.getToken(0, GLOS_DELIM); bool bFound = std::any_of(aFoundGroupNames.begin(), aFoundGroupNames.end(), [&sCompareGroup](const OUString& rGroupName) { return sCompareGroup == rGroupName; }); if(!bFound) { aGroupArr.erase(aGroupArr.begin() + i); } } } } } } void SwGlossaryList::Invoke() { // Only update automatically if a SwView has the focus. if(::GetActiveView()) Update(); } AutoTextGroup* SwGlossaryList::FindGroup(std::u16string_view rGroupName) { for(const auto & pRet : aGroupArr) { if(pRet->sName == rGroupName) return pRet.get(); } return nullptr; } void SwGlossaryList::FillGroup(AutoTextGroup* pGroup, SwGlossaries* pGlossaries) { std::unique_ptr pBlock = pGlossaries->GetGroupDoc(pGroup->sName); pGroup->nCount = pBlock ? pBlock->GetCount() : 0; pGroup->sLongNames.clear(); pGroup->sShortNames.clear(); if(pBlock) pGroup->sTitle = pBlock->GetName(); for(sal_uInt16 j = 0; j < pGroup->nCount; j++) { pGroup->sLongNames += pBlock->GetLongName(j) + OUStringChar(STRING_DELIM); pGroup->sShortNames += pBlock->GetShortName(j) + OUStringChar(STRING_DELIM); } } // Give back all (not exceeding FIND_MAX_GLOS) found modules // with matching beginning. void SwGlossaryList::HasLongName(const std::vector& rBeginCandidates, std::vector>& rLongNames) { if(!bFilled) Update(); const ::utl::TransliterationWrapper& rSCmp = GetAppCmpStrIgnore(); // We store results for all candidate words in separate lists, so that later // we can sort them according to the candidate position std::vector> aResults(rBeginCandidates.size()); // We can't break after FIND_MAX_GLOS items found, since first items may have ended up in // lower-priority lists, and those from higher-priority lists are yet to come. So process all. for (const auto& pGroup : aGroupArr) { sal_Int32 nIdx{ 0 }; for(sal_uInt16 j = 0; j < pGroup->nCount; j++) { OUString sBlock = pGroup->sLongNames.getToken(0, STRING_DELIM, nIdx); for (size_t i = 0; i < rBeginCandidates.size(); ++i) { const OUString& s = rBeginCandidates[i]; if (s.getLength() + 1 < sBlock.getLength() && rSCmp.isEqual(sBlock.copy(0, s.getLength()), s)) { aResults[i].push_back(sBlock); } } } } std::vector> aAllResults; // Sort and concatenate all result lists. See QuickHelpData::SortAndFilter for (size_t i = 0; i < rBeginCandidates.size(); ++i) { std::sort(aResults[i].begin(), aResults[i].end(), [origWord = rBeginCandidates[i]](const OUString& s1, const OUString& s2) { int nRet = s1.compareToIgnoreAsciiCase(s2); if (nRet == 0) { // fdo#61251 sort stuff that starts with the exact rOrigWord before // another ignore-case candidate int n1StartsWithOrig = s1.startsWith(origWord) ? 0 : 1; int n2StartsWithOrig = s2.startsWith(origWord) ? 0 : 1; return n1StartsWithOrig < n2StartsWithOrig; } return nRet < 0; }); // All suggestions must be accompanied with length of the text they would replace std::transform(aResults[i].begin(), aResults[i].end(), std::back_inserter(aAllResults), [nLen = sal_uInt16(rBeginCandidates[i].getLength())](const OUString& s) { return std::make_pair(s, nLen); }); } const auto& it = std::unique( aAllResults.begin(), aAllResults.end(), [](const std::pair& s1, const std::pair& s2) { return s1.first.equalsIgnoreAsciiCase(s2.first); }); if (const auto nCount = std::min(std::distance(aAllResults.begin(), it), FIND_MAX_GLOS)) { rLongNames.insert(rLongNames.end(), aAllResults.begin(), aAllResults.begin() + nCount); } } void SwGlossaryList::ClearGroups() { aGroupArr.clear(); bFilled = false; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */