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