1 /**
2 * \file tagsearcher.cpp
3 * Search for strings in tags.
4 *
5 * \b Project: Kid3
6 * \author Urs Fleisch
7 * \date 08 Feb 2014
8 *
9 * Copyright (C) 2014-2018 Urs Fleisch
10 *
11 * This file is part of Kid3.
12 *
13 * Kid3 is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * Kid3 is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25 */
26
27 #include "tagsearcher.h"
28 #include "trackdatamodel.h"
29 #include "fileproxymodel.h"
30 #include "bidirfileproxymodeliterator.h"
31
32 /**
33 * Constructor.
34 */
Position()35 TagSearcher::Position::Position()
36 : m_part(FileName), m_frameIndex(-1), m_matchedPos(-1), m_matchedLength(-1)
37 {
38 }
39
40 /**
41 * Clear to invalid position.
42 */
clear()43 void TagSearcher::Position::clear()
44 {
45 m_fileIndex = QPersistentModelIndex();
46 m_frameName.clear();
47 m_frameIndex = -1;
48 m_matchedPos = -1;
49 m_matchedLength = -1;
50 }
51
52 /**
53 * Check if position is valid.
54 * @return true if valid, false if not found.
55 */
isValid() const56 bool TagSearcher::Position::isValid() const
57 {
58 return m_fileIndex.isValid() && m_matchedPos != -1;
59 }
60
61
62 /**
63 * Constructor.
64 * @param parent parent object
65 */
TagSearcher(QObject * parent)66 TagSearcher::TagSearcher(QObject* parent) : QObject(parent),
67 m_fileProxyModel(nullptr), m_iterator(nullptr), m_aborted(false), m_started(false)
68 {
69 }
70
71 /**
72 * Clear abort flag.
73 */
clearAborted()74 void TagSearcher::clearAborted()
75 {
76 m_aborted = false;
77 }
78
79 /**
80 * Check if dialog was aborted.
81 * @return true if aborted.
82 */
isAborted() const83 bool TagSearcher::isAborted() const
84 {
85 return m_aborted;
86 }
87
88 /**
89 * Set model of files to be searched.
90 * @param model file proxy model
91 */
setModel(FileProxyModel * model)92 void TagSearcher::setModel(FileProxyModel* model)
93 {
94 if (m_iterator && m_fileProxyModel != model) {
95 delete m_iterator;
96 m_iterator = nullptr;
97 }
98 m_fileProxyModel = model;
99 if (m_fileProxyModel && !m_iterator) {
100 m_iterator = new BiDirFileProxyModelIterator(m_fileProxyModel, this);
101 connect(m_iterator, &BiDirFileProxyModelIterator::nextReady,
102 this, &TagSearcher::searchNextFile);
103 }
104 }
105
106 /**
107 * Set root index of directory to search.
108 * @param index root index of directory
109 */
setRootIndex(const QPersistentModelIndex & index)110 void TagSearcher::setRootIndex(const QPersistentModelIndex& index)
111 {
112 m_iterator->setRootIndex(index);
113 }
114
115 /**
116 * Set index of file to start search.
117 * @param index index of file where search is started
118 */
setStartIndex(const QPersistentModelIndex & index)119 void TagSearcher::setStartIndex(const QPersistentModelIndex& index)
120 {
121 m_startIndex = index;
122 }
123
124 /**
125 * Set abort flag.
126 */
abort()127 void TagSearcher::abort()
128 {
129 m_aborted = true;
130 m_started = false;
131 if (m_iterator) {
132 m_iterator->abort();
133 }
134 }
135
136 /**
137 * Find next occurrence of string.
138 * @param params search parameters
139 */
find(const Parameters & params)140 void TagSearcher::find(const Parameters ¶ms)
141 {
142 setParameters(params);
143 findNext(1);
144 }
145
146 /**
147 * Find next occurrence of same string.
148 */
findNext(int advanceChars)149 void TagSearcher::findNext(int advanceChars)
150 {
151 m_aborted = false;
152 if (m_iterator) {
153 if (m_started) {
154 continueSearch(advanceChars);
155 } else {
156 bool continueFromCurrentPosition = false;
157 if (m_startIndex.isValid()) {
158 continueFromCurrentPosition = m_currentPosition.isValid() &&
159 m_currentPosition.getFileIndex() == m_startIndex;
160 m_iterator->setCurrentIndex(m_startIndex);
161 m_startIndex = QPersistentModelIndex();
162 }
163 m_started = true;
164 if (continueFromCurrentPosition) {
165 continueSearch(advanceChars);
166 } else {
167 m_iterator->start();
168 }
169 }
170 }
171 }
172
173 /**
174 * Search next file.
175 * @param index index of file in file proxy model
176 */
searchNextFile(const QPersistentModelIndex & index)177 void TagSearcher::searchNextFile(const QPersistentModelIndex& index)
178 {
179 if (index.isValid()) {
180 if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
181 emit progress(taggedFile->getFilename());
182 taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
183
184 Position pos;
185 if (searchInFile(taggedFile, &pos, 1)) {
186 pos.m_fileIndex = index;
187 m_currentPosition = pos;
188 if (m_iterator) {
189 m_iterator->suspend();
190 }
191 emit progress(getLocationString(taggedFile));
192 emit textFound();
193 }
194 }
195 } else {
196 m_started = false;
197 m_currentPosition.clear();
198 emit progress(tr("Search finished"));
199 emit textFound();
200 }
201 }
202
203 /**
204 * Continue search in current file, if no other match is found, resume
205 * file iteration.
206 * @param advanceChars number of characters to advance from current position
207 */
continueSearch(int advanceChars)208 void TagSearcher::continueSearch(int advanceChars)
209 {
210 if (m_currentPosition.isValid()) {
211 if (TaggedFile* taggedFile =
212 FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) {
213 if (searchInFile(taggedFile, &m_currentPosition, advanceChars)) {
214 emit progress(getLocationString(taggedFile));
215 emit textFound();
216 return;
217 }
218 }
219 }
220 if (m_iterator) {
221 m_iterator->resume();
222 }
223 }
224
225 /**
226 * Search for next occurrence in a file.
227 * @param taggedFile tagged file
228 * @param pos position of last match in @a taggedFile, will be updated
229 * with new position
230 * @param advanceChars number of characters to advance from current position
231 * @return true if found.
232 */
searchInFile(TaggedFile * taggedFile,Position * pos,int advanceChars) const233 bool TagSearcher::searchInFile(TaggedFile* taggedFile, Position* pos,
234 int advanceChars) const
235 {
236 if (pos->getPart() <= Position::FileName &&
237 ((m_params.getFlags() & AllFrames) ||
238 (m_params.getFrameMask() & (1ULL << TrackDataModel::FT_FileName)))) {
239 int idx = 0;
240 if (pos->getPart() == Position::FileName) {
241 idx = pos->m_matchedPos + advanceChars;
242 }
243 int len = findInString(taggedFile->getFilename(), idx);
244 if (len != -1) {
245 pos->m_part = Position::FileName;
246 pos->m_matchedPos = idx;
247 pos->m_matchedLength = len;
248 return true;
249 }
250 }
251 FOR_ALL_TAGS(tagNr) {
252 Position::Part part = Position::tagNumberToPart(tagNr);
253 if (pos->getPart() <= part) {
254 FrameCollection frames;
255 taggedFile->getAllFrames(tagNr, frames);
256 if (searchInFrames(frames, part, pos, advanceChars)) {
257 return true;
258 }
259 }
260 }
261 return false;
262 }
263
264 /**
265 * Search for next occurrence in frames.
266 * @param frames frames of tag
267 * @param part tag 1 or tag 2
268 * @param pos position of last match, will be updated with new position
269 * @param advanceChars number of characters to advance from current position
270 * @return true if found.
271 */
searchInFrames(const FrameCollection & frames,Position::Part part,Position * pos,int advanceChars) const272 bool TagSearcher::searchInFrames(const FrameCollection& frames,
273 Position::Part part, Position* pos,
274 int advanceChars) const
275 {
276 int idx = 0;
277 int frameNr = 0;
278 auto begin = frames.cbegin();
279 auto end = frames.cend();
280 if (pos->getPart() == part) {
281 idx = pos->m_matchedPos + advanceChars;
282 for (frameNr = 0;
283 frameNr < pos->getFrameIndex() && begin != end; ++frameNr) {
284 ++begin;
285 }
286 }
287 int len = -1;
288 QString frameName;
289 for (auto it = begin; it != end; ++it, ++frameNr) {
290 if ((m_params.getFlags() & AllFrames) ||
291 (m_params.getFrameMask() & (1ULL << it->getType()))) {
292 len = findInString(it->getValue(), idx);
293 if (len != -1) {
294 frameName = it->getExtendedType().getTranslatedName();
295 break;
296 }
297 }
298 idx = 0;
299 }
300 if (len != -1) {
301 pos->m_part = part;
302 pos->m_frameName = frameName;
303 pos->m_frameIndex = frameNr;
304 pos->m_matchedPos = idx;
305 pos->m_matchedLength = len;
306 return true;
307 }
308 return false;
309 }
310
311 /**
312 * Replace found text.
313 * @param params search parameters
314 */
replace(const TagSearcher::Parameters & params)315 void TagSearcher::replace(const TagSearcher::Parameters& params)
316 {
317 setParameters(params);
318 replaceNext();
319 }
320
321 /**
322 * Replace found text.
323 */
replaceNext()324 void TagSearcher::replaceNext()
325 {
326 QString replaced;
327 if (m_currentPosition.isValid()) {
328 if (TaggedFile* taggedFile =
329 FileProxyModel::getTaggedFileOfIndex(m_currentPosition.getFileIndex())) {
330 if (m_currentPosition.getPart() == Position::FileName) {
331 QString str = taggedFile->getFilename();
332 replaced = str.mid(m_currentPosition.getMatchedPos(),
333 m_currentPosition.getMatchedLength());
334 replaceString(replaced);
335 str.replace(m_currentPosition.getMatchedPos(),
336 m_currentPosition.getMatchedLength(), replaced);
337 taggedFile->setFilename(str);
338 } else {
339 FrameCollection frames;
340 taggedFile->getAllFrames(
341 Position::partToTagNumber(m_currentPosition.getPart()), frames);
342 auto it = frames.begin();
343 auto end = frames.end();
344 for (int frameNr = 0;
345 frameNr < m_currentPosition.getFrameIndex() && it != end;
346 ++frameNr) {
347 ++it;
348 }
349 if (it != end) {
350 auto& frame = const_cast<Frame&>(*it);
351 QString str = frame.getValue();
352 replaced = str.mid(m_currentPosition.getMatchedPos(),
353 m_currentPosition.getMatchedLength());
354 replaceString(replaced);
355 str.replace(m_currentPosition.getMatchedPos(),
356 m_currentPosition.getMatchedLength(), replaced);
357 frame.setValueIfChanged(str);
358 taggedFile->setFrames(
359 Position::partToTagNumber(m_currentPosition.getPart()), frames);
360 }
361 }
362 }
363 }
364 if (!replaced.isNull()) {
365 emit textReplaced();
366 findNext(replaced.length());
367 } else {
368 findNext(1);
369 }
370 }
371
372 /**
373 * Replace all occurrences.
374 * @param params search parameters
375 */
replaceAll(const TagSearcher::Parameters & params)376 void TagSearcher::replaceAll(const TagSearcher::Parameters& params)
377 {
378 setParameters(params);
379 disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext);
380 connect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext,
381 Qt::QueuedConnection);
382 replaceNext();
383 }
384
385 /**
386 * If a text is found replace it and then search the next occurrence.
387 */
replaceThenFindNext()388 void TagSearcher::replaceThenFindNext()
389 {
390 if (!m_aborted && m_currentPosition.isValid()) {
391 replaceNext();
392 } else {
393 disconnect(this, &TagSearcher::textFound, this, &TagSearcher::replaceThenFindNext);
394 }
395 }
396
397 /**
398 * Search string for text.
399 * @param str string to be searched
400 * @param idx start index of search, will be updated with index of found text
401 * @return length of match if found, else -1.
402 */
findInString(const QString & str,int & idx) const403 int TagSearcher::findInString(const QString& str, int& idx) const
404 {
405 if (m_regExp.pattern().isEmpty()) {
406 idx = str.indexOf(m_params.getSearchText(), idx,
407 m_params.getFlags() & CaseSensitive
408 ? Qt::CaseSensitive : Qt::CaseInsensitive);
409 return idx != -1 ? m_params.getSearchText().length() : -1;
410 } else {
411 auto match = m_regExp.match(str, idx);
412 idx = match.capturedStart();
413 return match.hasMatch() ? match.capturedLength() : -1;
414 }
415 }
416
417 /**
418 * Replace string.
419 * @param str string which will be replaced
420 */
replaceString(QString & str) const421 void TagSearcher::replaceString(QString& str) const
422 {
423 if (m_regExp.pattern().isEmpty()) {
424 str.replace(m_params.getSearchText(), m_params.getReplaceText(),
425 m_params.getFlags() & CaseSensitive
426 ? Qt::CaseSensitive : Qt::CaseInsensitive);
427 } else {
428 str.replace(m_regExp, m_params.getReplaceText());
429 }
430 }
431
432 /**
433 * Set and preprocess search parameters.
434 * @param params search parameters
435 */
setParameters(const Parameters & params)436 void TagSearcher::setParameters(const Parameters& params)
437 {
438 m_params = params;
439 SearchFlags flags = m_params.getFlags();
440 if (m_iterator) {
441 m_iterator->setDirectionBackwards(flags & Backwards);
442 }
443 if (flags & RegExp) {
444 m_regExp.setPattern(m_params.getSearchText());
445 m_regExp.setPatternOptions(flags & CaseSensitive
446 ? QRegularExpression::NoPatternOption
447 : QRegularExpression::CaseInsensitiveOption);
448 } else {
449 m_regExp.setPattern(QString());
450 m_regExp.setPatternOptions(QRegularExpression::NoPatternOption);
451 }
452 }
453
454 /**
455 * Get a string describing where the text was found.
456 * @param taggedFile tagged file
457 * @return description of location.
458 */
getLocationString(TaggedFile * taggedFile) const459 QString TagSearcher::getLocationString(TaggedFile* taggedFile) const
460 {
461 QString location = taggedFile->getFilename();
462 location += QLatin1String(": ");
463 if (m_currentPosition.getPart() == Position::FileName) {
464 location += tr("Filename");
465 } else {
466 location += tr("Tag %1").arg(Frame::tagNumberToString(
467 Position::partToTagNumber(m_currentPosition.getPart())));
468 location += QLatin1String(": ");
469 location += m_currentPosition.getFrameName();
470 }
471 return location;
472 }
473
474 /**
475 * Get parameters as variant list.
476 * @return variant list containing search text, replace text, flags,
477 * frameMask.
478 */
toVariantList() const479 QVariantList TagSearcher::Parameters::toVariantList() const
480 {
481 QVariantList lst;
482 lst << m_searchText << m_replaceText << static_cast<int>(m_flags)
483 << m_frameMask;
484 return lst;
485 }
486
487 /**
488 * Set parameters from variant list.
489 * @param lst variant list containing search text, replace text, flags,
490 * frameMask
491 */
fromVariantList(const QVariantList & lst)492 void TagSearcher::Parameters::fromVariantList(const QVariantList& lst)
493 {
494 if (lst.size() >= 4) {
495 m_searchText = lst.at(0).toString();
496 m_replaceText = lst.at(1).toString();
497 m_flags = SearchFlags(lst.at(2).toInt());
498 m_frameMask = lst.at(3).toULongLong();
499 }
500 }
501