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 <tools/urlobj.hxx>
21 #include <vcl/weld.hxx>
22 #include <svl/fstathelper.hxx>
23 #include <unotools/pathoptions.hxx>
24 #include <unotools/transliterationwrapper.hxx>
25 #include <swtypes.hxx>
26 #include <swmodule.hxx>
27 #include <shellio.hxx>
28 #include <initui.hxx>
29 #include <glosdoc.hxx>
30 #include <gloslst.hxx>
31 #include <swunohelper.hxx>
32 #include <view.hxx>
33 
34 #include <vector>
35 
36 #define STRING_DELIM char(0x0A)
37 #define GLOS_TIMEOUT 30000   // update every 30 seconds
38 #define FIND_MAX_GLOS 20
39 
40 struct TripleString
41 {
42     OUString sGroup;
43     OUString sBlock;
44     OUString sShort;
45 };
46 
47 class SwGlossDecideDlg : public weld::GenericDialogController
48 {
49     std::unique_ptr<weld::Button> m_xOk;
50     std::unique_ptr<weld::TreeView> m_xListLB;
51 
52     DECL_LINK(DoubleClickHdl, weld::TreeView&, bool);
53     DECL_LINK(SelectHdl, weld::TreeView&, void);
54 
55 public:
56     explicit SwGlossDecideDlg(weld::Window* pParent);
57 
GetTreeView()58     weld::TreeView& GetTreeView() {return *m_xListLB;}
59 };
60 
SwGlossDecideDlg(weld::Window * pParent)61 SwGlossDecideDlg::SwGlossDecideDlg(weld::Window* pParent)
62     : GenericDialogController(pParent, "modules/swriter/ui/selectautotextdialog.ui", "SelectAutoTextDialog")
63     , m_xOk(m_xBuilder->weld_button("ok"))
64     , m_xListLB(m_xBuilder->weld_tree_view("treeview"))
65 {
66     m_xListLB->set_size_request(m_xListLB->get_approximate_digit_width() * 32,
67                                 m_xListLB->get_height_rows(8));
68     m_xListLB->connect_row_activated(LINK(this, SwGlossDecideDlg, DoubleClickHdl));
69     m_xListLB->connect_changed(LINK(this, SwGlossDecideDlg, SelectHdl));
70 }
71 
IMPL_LINK_NOARG(SwGlossDecideDlg,DoubleClickHdl,weld::TreeView &,bool)72 IMPL_LINK_NOARG(SwGlossDecideDlg, DoubleClickHdl, weld::TreeView&, bool)
73 {
74     m_xDialog->response(RET_OK);
75     return true;
76 }
77 
IMPL_LINK_NOARG(SwGlossDecideDlg,SelectHdl,weld::TreeView &,void)78 IMPL_LINK_NOARG(SwGlossDecideDlg, SelectHdl, weld::TreeView&, void)
79 {
80     m_xOk->set_sensitive(m_xListLB->get_selected_index() != -1);
81 }
82 
SwGlossaryList()83 SwGlossaryList::SwGlossaryList() :
84     bFilled(false)
85 {
86     SvtPathOptions aPathOpt;
87     sPath = aPathOpt.GetAutoTextPath();
88     SetTimeout(GLOS_TIMEOUT);
89 }
90 
~SwGlossaryList()91 SwGlossaryList::~SwGlossaryList()
92 {
93     ClearGroups();
94 }
95 
96 // If the GroupName is already known, then only rShortName
97 // will be filled. Otherwise also rGroupName will be set and
98 // on demand asked for the right group.
99 
GetShortName(const OUString & rLongName,OUString & rShortName,OUString & rGroupName)100 bool SwGlossaryList::GetShortName(const OUString& rLongName,
101                                   OUString& rShortName, OUString& rGroupName )
102 {
103     if(!bFilled)
104         Update();
105 
106     std::vector<TripleString> aTripleStrings;
107 
108     size_t nCount = aGroupArr.size();
109     for(size_t i = 0; i < nCount; i++ )
110     {
111         AutoTextGroup* pGroup = aGroupArr[i].get();
112         if(!rGroupName.isEmpty() && rGroupName != pGroup->sName)
113             continue;
114 
115         sal_Int32 nPosLong = 0;
116         for(sal_uInt16 j = 0; j < pGroup->nCount; j++)
117         {
118             const OUString sLong = pGroup->sLongNames.getToken(0, STRING_DELIM, nPosLong);
119             if(rLongName != sLong)
120                 continue;
121 
122             TripleString aTriple;
123             aTriple.sGroup = pGroup->sName;
124             aTriple.sBlock = sLong;
125             aTriple.sShort = pGroup->sShortNames.getToken(j, STRING_DELIM);
126             aTripleStrings.push_back(aTriple);
127         }
128     }
129 
130     bool bRet = false;
131     nCount = aTripleStrings.size();
132     if(1 == nCount)
133     {
134         const TripleString& rTriple(aTripleStrings.front());
135         rShortName = rTriple.sShort;
136         rGroupName = rTriple.sGroup;
137         bRet = true;
138     }
139     else if(1 < nCount)
140     {
141         SwView *pView  = ::GetActiveView();
142         SwGlossDecideDlg aDlg(pView ? pView->GetFrameWeld() : nullptr);
143         OUString sTitle = aDlg.get_title() + " " + aTripleStrings.front().sBlock;
144         aDlg.set_title(sTitle);
145 
146         weld::TreeView& rLB = aDlg.GetTreeView();
147         for (const auto& rTriple : aTripleStrings)
148             rLB.append_text(rTriple.sGroup.getToken(0, GLOS_DELIM));
149 
150         rLB.select(0);
151         if (aDlg.run() == RET_OK && rLB.get_selected_index() != -1)
152         {
153             const TripleString& rTriple(aTripleStrings[rLB.get_selected_index()]);
154             rShortName = rTriple.sShort;
155             rGroupName = rTriple.sGroup;
156             bRet = true;
157         }
158         else
159             bRet = false;
160     }
161     return bRet;
162 }
163 
GetGroupCount()164 size_t SwGlossaryList::GetGroupCount()
165 {
166     if(!bFilled)
167         Update();
168     return aGroupArr.size();
169 }
170 
GetGroupName(size_t nPos)171 OUString SwGlossaryList::GetGroupName(size_t nPos)
172 {
173     OSL_ENSURE(aGroupArr.size() > nPos, "group not available");
174     if(nPos < aGroupArr.size())
175     {
176         AutoTextGroup* pGroup = aGroupArr[nPos].get();
177         OUString sRet = pGroup->sName;
178         return sRet;
179     }
180     return OUString();
181 }
182 
GetGroupTitle(size_t nPos)183 OUString SwGlossaryList::GetGroupTitle(size_t nPos)
184 {
185     OSL_ENSURE(aGroupArr.size() > nPos, "group not available");
186     if(nPos < aGroupArr.size())
187     {
188         AutoTextGroup* pGroup = aGroupArr[nPos].get();
189         return pGroup->sTitle;
190     }
191     return OUString();
192 }
193 
GetBlockCount(size_t nGroup)194 sal_uInt16 SwGlossaryList::GetBlockCount(size_t nGroup)
195 {
196     OSL_ENSURE(aGroupArr.size() > nGroup, "group not available");
197     if(nGroup < aGroupArr.size())
198     {
199         AutoTextGroup* pGroup = aGroupArr[nGroup].get();
200         return pGroup->nCount;
201     }
202     return 0;
203 }
204 
GetBlockLongName(size_t nGroup,sal_uInt16 nBlock)205 OUString SwGlossaryList::GetBlockLongName(size_t nGroup, sal_uInt16 nBlock)
206 {
207     OSL_ENSURE(aGroupArr.size() > nGroup, "group not available");
208     if(nGroup < aGroupArr.size())
209     {
210         AutoTextGroup* pGroup = aGroupArr[nGroup].get();
211         return pGroup->sLongNames.getToken(nBlock, STRING_DELIM);
212     }
213     return OUString();
214 }
215 
GetBlockShortName(size_t nGroup,sal_uInt16 nBlock)216 OUString SwGlossaryList::GetBlockShortName(size_t nGroup, sal_uInt16 nBlock)
217 {
218     OSL_ENSURE(aGroupArr.size() > nGroup, "group not available");
219     if(nGroup < aGroupArr.size())
220     {
221         AutoTextGroup* pGroup = aGroupArr[nGroup].get();
222         return pGroup->sShortNames.getToken(nBlock, STRING_DELIM);
223     }
224     return OUString();
225 }
226 
Update()227 void SwGlossaryList::Update()
228 {
229     if(!IsActive())
230         Start();
231 
232     SvtPathOptions aPathOpt;
233     const OUString& sTemp( aPathOpt.GetAutoTextPath() );
234     if(sTemp != sPath)
235     {
236         sPath = sTemp;
237         bFilled = false;
238         ClearGroups();
239     }
240     SwGlossaries* pGlossaries = ::GetGlossaries();
241     const std::vector<OUString> & rPathArr = pGlossaries->GetPathArray();
242     const OUString sExt( SwGlossaries::GetExtension() );
243     if(!bFilled)
244     {
245         const size_t nGroupCount = pGlossaries->GetGroupCnt();
246         for(size_t i = 0; i < nGroupCount; ++i)
247         {
248             OUString sGrpName = pGlossaries->GetGroupName(i);
249             const size_t nPath = static_cast<size_t>(
250                 sGrpName.getToken(1, GLOS_DELIM).toInt32());
251             if( nPath < rPathArr.size() )
252             {
253                 std::unique_ptr<AutoTextGroup> pGroup(new AutoTextGroup);
254                 pGroup->sName = sGrpName;
255 
256                 FillGroup(pGroup.get(), pGlossaries);
257                 OUString sName = rPathArr[nPath] + "/" +
258                     pGroup->sName.getToken(0, GLOS_DELIM) + sExt;
259                 FStatHelper::GetModifiedDateTimeOfFile( sName,
260                                                 &pGroup->aDateModified,
261                                                 &pGroup->aDateModified );
262 
263                 aGroupArr.insert( aGroupArr.begin(), std::move(pGroup) );
264             }
265         }
266         bFilled = true;
267     }
268     else
269     {
270         for( size_t nPath = 0; nPath < rPathArr.size(); nPath++ )
271         {
272             std::vector<OUString> aFoundGroupNames;
273             std::vector<OUString> aFiles;
274             std::vector<DateTime> aDateTimeArr;
275 
276             SWUnoHelper::UCB_GetFileListOfFolder( rPathArr[nPath], aFiles,
277                                                     &sExt, &aDateTimeArr );
278             for( size_t nFiles = 0; nFiles < aFiles.size(); ++nFiles )
279             {
280                 const OUString aTitle = aFiles[ nFiles ];
281                 ::DateTime& rDT = aDateTimeArr[ nFiles ];
282 
283                 OUString sName( aTitle.copy( 0, aTitle.getLength() - sExt.getLength() ));
284 
285                 aFoundGroupNames.push_back(sName);
286                 sName += OUStringChar(GLOS_DELIM) + OUString::number( static_cast<sal_uInt16>(nPath) );
287                 AutoTextGroup* pFound = FindGroup( sName );
288                 if( !pFound )
289                 {
290                     pFound = new AutoTextGroup;
291                     pFound->sName = sName;
292                     FillGroup( pFound, pGlossaries );
293                     pFound->aDateModified = rDT;
294 
295                     aGroupArr.push_back(std::unique_ptr<AutoTextGroup>(pFound));
296                 }
297                 else if( pFound->aDateModified < rDT )
298                 {
299                     FillGroup(pFound, pGlossaries);
300                     pFound->aDateModified = rDT;
301                 }
302             }
303 
304             for( size_t i = aGroupArr.size(); i>0; )
305             {
306                 --i;
307                 // maybe remove deleted groups
308                 AutoTextGroup* pGroup = aGroupArr[i].get();
309                 const size_t nGroupPath = static_cast<size_t>(
310                     pGroup->sName.getToken( 1, GLOS_DELIM).toInt32());
311                 // Only the groups will be checked which are registered
312                 // for the current subpath.
313                 if( nGroupPath == nPath )
314                 {
315                     OUString sCompareGroup = pGroup->sName.getToken(0, GLOS_DELIM);
316                     bool bFound = std::any_of(aFoundGroupNames.begin(), aFoundGroupNames.end(),
317                         [&sCompareGroup](const OUString& rGroupName) { return sCompareGroup == rGroupName; });
318 
319                     if(!bFound)
320                     {
321                         aGroupArr.erase(aGroupArr.begin() + i);
322                     }
323                 }
324             }
325         }
326     }
327 }
328 
Invoke()329 void SwGlossaryList::Invoke()
330 {
331     // Only update automatically if a SwView has the focus.
332     if(::GetActiveView())
333         Update();
334 }
335 
FindGroup(const OUString & rGroupName)336 AutoTextGroup* SwGlossaryList::FindGroup(const OUString& rGroupName)
337 {
338     for(const auto & pRet : aGroupArr)
339     {
340         if(pRet->sName == rGroupName)
341             return pRet.get();
342     }
343     return nullptr;
344 }
345 
FillGroup(AutoTextGroup * pGroup,SwGlossaries * pGlossaries)346 void SwGlossaryList::FillGroup(AutoTextGroup* pGroup, SwGlossaries* pGlossaries)
347 {
348     std::unique_ptr<SwTextBlocks> pBlock = pGlossaries->GetGroupDoc(pGroup->sName);
349     pGroup->nCount = pBlock ? pBlock->GetCount() : 0;
350     pGroup->sLongNames.clear();
351     pGroup->sShortNames.clear();
352     if(pBlock)
353         pGroup->sTitle = pBlock->GetName();
354 
355     for(sal_uInt16 j = 0; j < pGroup->nCount; j++)
356     {
357         pGroup->sLongNames += pBlock->GetLongName(j)
358             + OUStringChar(STRING_DELIM);
359         pGroup->sShortNames += pBlock->GetShortName(j)
360             + OUStringChar(STRING_DELIM);
361     }
362 }
363 
364 // Give back all (not exceeding FIND_MAX_GLOS) found modules
365 // with matching beginning.
366 
HasLongName(const std::vector<OUString> & rBeginCandidates,std::vector<std::pair<OUString,sal_uInt16>> & rLongNames)367 void SwGlossaryList::HasLongName(const std::vector<OUString>& rBeginCandidates,
368                                  std::vector<std::pair<OUString, sal_uInt16>>& rLongNames)
369 {
370     if(!bFilled)
371         Update();
372     const ::utl::TransliterationWrapper& rSCmp = GetAppCmpStrIgnore();
373     // We store results for all candidate words in separate lists, so that later
374     // we can sort them according to the candidate position
375     std::vector<std::vector<OUString>> aResults(rBeginCandidates.size());
376 
377     // We can't break after FIND_MAX_GLOS items found, since first items may have ended up in
378     // lower-priority lists, and those from higher-priority lists are yet to come. So process all.
379     for (const auto& pGroup : aGroupArr)
380     {
381         sal_Int32 nIdx{ 0 };
382         for(sal_uInt16 j = 0; j < pGroup->nCount; j++)
383         {
384             OUString sBlock = pGroup->sLongNames.getToken(0, STRING_DELIM, nIdx);
385             for (size_t i = 0; i < rBeginCandidates.size(); ++i)
386             {
387                 const OUString& s = rBeginCandidates[i];
388                 if (s.getLength() + 1 < sBlock.getLength()
389                     && rSCmp.isEqual(sBlock.copy(0, s.getLength()), s))
390                 {
391                     aResults[i].push_back(sBlock);
392                 }
393             }
394         }
395     }
396 
397     std::vector<std::pair<OUString, sal_uInt16>> aAllResults;
398     // Sort and concatenate all result lists. See QuickHelpData::SortAndFilter
399     for (size_t i = 0; i < rBeginCandidates.size(); ++i)
400     {
401         std::sort(aResults[i].begin(), aResults[i].end(),
402                   [origWord = rBeginCandidates[i]](const OUString& s1, const OUString& s2) {
403                       int nRet = s1.compareToIgnoreAsciiCase(s2);
404                       if (nRet == 0)
405                       {
406                           // fdo#61251 sort stuff that starts with the exact rOrigWord before
407                           // another ignore-case candidate
408                           int n1StartsWithOrig = s1.startsWith(origWord) ? 0 : 1;
409                           int n2StartsWithOrig = s2.startsWith(origWord) ? 0 : 1;
410                           return n1StartsWithOrig < n2StartsWithOrig;
411                       }
412                       return nRet < 0;
413                   });
414         // All suggestions must be accompanied with length of the text they would replace
415         std::transform(aResults[i].begin(), aResults[i].end(), std::back_inserter(aAllResults),
416                        [nLen = sal_uInt16(rBeginCandidates[i].getLength())](const OUString& s) {
417                            return std::make_pair(s, nLen);
418                        });
419     }
420 
421     const auto& it = std::unique(
422         aAllResults.begin(), aAllResults.end(),
423         [](const std::pair<OUString, sal_uInt16>& s1, const std::pair<OUString, sal_uInt16>& s2) {
424             return s1.first.equalsIgnoreAsciiCase(s2.first);
425         });
426     if (const auto nCount = std::min<size_t>(std::distance(aAllResults.begin(), it), FIND_MAX_GLOS))
427     {
428         rLongNames.insert(rLongNames.end(), aAllResults.begin(), aAllResults.begin() + nCount);
429     }
430 }
431 
ClearGroups()432 void    SwGlossaryList::ClearGroups()
433 {
434     aGroupArr.clear();
435     bFilled = false;
436 }
437 
438 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
439