1 /*
2     SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
3     SPDX-FileCopyrightText: 2010-2018 Mladen Milinkovic <max@smoothware.net>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "finder.h"
9 #include "core/richdocument.h"
10 #include "core/subtitleiterator.h"
11 
12 #include <QGroupBox>
13 #include <QRadioButton>
14 #include <QGridLayout>
15 #include <QDebug>
16 
17 #include <KMessageBox>
18 #include <KFind>
19 #include <KFindDialog>
20 #include <KLocalizedString>
21 
22 #include <ktextwidgets_version.h>
23 
24 
25 using namespace SubtitleComposer;
26 
Finder(QWidget * parent)27 Finder::Finder(QWidget *parent) :
28 	QObject(parent),
29 	m_subtitle(0),
30 	m_translationMode(false),
31 	m_feedingPrimary(false),
32 	m_find(0),
33 	m_iterator(0),
34 	m_dataLine(0)
35 {
36 	m_dialog = new KFindDialog(parent);
37 	m_dialog->setHasSelection(true);
38 	m_dialog->setHasCursor(true);
39 	m_dialog->setOptions(m_dialog->options() | KFind::FromCursor);
40 
41 	QWidget *mainWidget = m_dialog;//->mainWidget();
42 	QLayout *mainLayout = mainWidget->layout();
43 
44 	m_targetGroupBox = new QGroupBox(mainWidget);
45 	m_targetGroupBox->setTitle(i18n("Find In"));
46 
47 	QGridLayout *targetLayout = new QGridLayout(m_targetGroupBox);
48 	targetLayout->setAlignment(Qt::AlignTop);
49 	targetLayout->setSpacing(5);
50 
51 	m_targetRadioButtons[Both] = new QRadioButton(m_targetGroupBox);
52 	m_targetRadioButtons[Both]->setChecked(true);
53 	m_targetRadioButtons[Both]->setText(i18n("Both subtitles"));
54 	m_targetRadioButtons[Primary] = new QRadioButton(m_targetGroupBox);
55 	m_targetRadioButtons[Primary]->setText(i18n("Primary subtitle"));
56 	m_targetRadioButtons[Secondary] = new QRadioButton(m_targetGroupBox);
57 	m_targetRadioButtons[Secondary]->setText(i18n("Translation subtitle"));
58 
59 	targetLayout->addWidget(m_targetRadioButtons[Both], 0, 0);
60 	targetLayout->addWidget(m_targetRadioButtons[Primary], 1, 0);
61 	targetLayout->addWidget(m_targetRadioButtons[Secondary], 2, 0);
62 
63 	mainLayout->addWidget(m_targetGroupBox);
64 	m_targetGroupBox->hide();
65 }
66 
~Finder()67 Finder::~Finder()
68 {
69 	invalidate();
70 }
71 
72 void
invalidate()73 Finder::invalidate()
74 {
75 	if(m_find) {
76 		delete m_find;
77 		m_find = 0;
78 	}
79 
80 	if(m_iterator) {
81 		delete m_iterator;
82 		m_iterator = 0;
83 	}
84 
85 	m_feedingPrimary = false;
86 
87 	// NOTE a line shouldn't be deleted before being removed from the subtitle
88 	// so there's no risk of being left with an invalid reference as this
89 	// method is called when a line is removed from the subtitle and before
90 	// anyone has a chance to delete the line.
91 	if(m_dataLine) {
92 		disconnect(m_dataLine, &SubtitleLine::primaryTextChanged, this, &Finder::onLinePrimaryTextChanged);
93 		disconnect(m_dataLine, &SubtitleLine::secondaryTextChanged, this, &Finder::onLineSecondaryTextChanged);
94 		m_dataLine = 0;
95 	}
96 }
97 
98 QWidget *
parentWidget()99 Finder::parentWidget()
100 {
101 	return static_cast<QWidget *>(parent());
102 }
103 
104 void
setSubtitle(Subtitle * subtitle)105 Finder::setSubtitle(Subtitle *subtitle)
106 {
107 	m_subtitle = subtitle;
108 
109 	invalidate();
110 }
111 
112 void
setTranslationMode(bool enabled)113 Finder::setTranslationMode(bool enabled)
114 {
115 	if(m_translationMode != enabled) {
116 		m_translationMode = enabled;
117 
118 		if(m_translationMode)
119 			m_targetGroupBox->show();
120 		else
121 			m_targetGroupBox->hide();
122 
123 		invalidate();
124 	}
125 }
126 
127 void
find(const RangeList & selectionRanges,int currentIndex,const QString & text,bool findBackwards)128 Finder::find(const RangeList &selectionRanges, int currentIndex, const QString &text, bool findBackwards)
129 {
130 	if(!m_subtitle || !m_subtitle->linesCount())
131 		return;
132 
133 	invalidate();
134 
135 	m_dialog->setOptions(findBackwards ? m_dialog->options() | KFind::FindBackwards : m_dialog->options() & ~KFind::FindBackwards);
136 
137 	if(!text.isEmpty()) {
138 		QStringList history = m_dialog->findHistory();
139 		history.removeAll(text);
140 		history.prepend(text);
141 		m_dialog->setFindHistory(history);
142 	}
143 
144 	if(m_dialog->exec() != QDialog::Accepted)
145 		return;
146 
147 	m_find = new KFind(m_dialog->pattern(), m_dialog->options(), 0);
148 	m_find->closeFindNextDialog();
149 
150 #if KTEXTWIDGETS_VERSION < QT_VERSION_CHECK(5, 81, 0)
151 	connect(m_find, QOverload<const QString &, int, int>::of(&KFind::highlight), this, &Finder::onHighlight);
152 #else
153 	connect(m_find, &KFind::textFound, this, &Finder::onHighlight);
154 #endif
155 
156 	m_iterator = new SubtitleIterator(*m_subtitle, m_dialog->options() & KFind::SelectedText ? selectionRanges : Range::full());
157 	if(m_iterator->index() == SubtitleIterator::Invalid) {
158 		invalidate();
159 		return;
160 	}
161 
162 	if(m_dialog->options() & KFind::FromCursor)
163 		m_iterator->toIndex(currentIndex < 0 ? 0 : currentIndex);
164 
165 	m_allSearchedIndex = m_iterator->index();
166 
167 	m_find->setPattern(m_dialog->pattern());
168 	m_find->setOptions(m_dialog->options());
169 
170 	m_instancesFound = false;
171 
172 	advance();
173 }
174 
175 bool
findNext()176 Finder::findNext()
177 {
178 	if(!m_iterator)
179 		return false;
180 
181 	m_find->setOptions(m_find->options() & ~KFind::FindBackwards);
182 
183 	advance();
184 	return true;
185 }
186 
187 bool
findPrevious()188 Finder::findPrevious()
189 {
190 	if(!m_iterator)
191 		return false;
192 
193 	m_find->setOptions(m_find->options() | KFind::FindBackwards);
194 
195 	advance();
196 	return true;
197 }
198 
199 void
advance()200 Finder::advance()
201 {
202 	KFind::Result res = KFind::NoMatch;
203 
204 	bool selection = m_find->options() & KFind::SelectedText;
205 	bool backwards = m_find->options() & KFind::FindBackwards;
206 
207 	do {
208 		if(m_find->needData()) {
209 			if(m_dataLine) {
210 				disconnect(m_dataLine, &SubtitleLine::primaryTextChanged, this, &Finder::onLinePrimaryTextChanged);
211 				disconnect(m_dataLine, &SubtitleLine::secondaryTextChanged, this, &Finder::onLineSecondaryTextChanged);
212 			}
213 
214 			m_dataLine = m_iterator->current();
215 
216 			if(m_dataLine) {
217 				if(!m_translationMode || m_targetRadioButtons[Primary]->isChecked()) {
218 					m_feedingPrimary = true;
219 					m_find->setData(m_dataLine->primaryDoc()->toPlainText());
220 				} else if(m_targetRadioButtons[Secondary]->isChecked()) {
221 					m_feedingPrimary = false;
222 					m_find->setData(m_dataLine->secondaryDoc()->toPlainText());
223 				} else {                // m_translationMode && m_targetRadioButtons[SubtitleLine::Both]->isChecked()
224 					m_feedingPrimary = !m_feedingPrimary;   // we alternate the source of data
225 					m_find->setData((m_feedingPrimary ? m_dataLine->primaryDoc() : m_dataLine->secondaryDoc())->toPlainText());
226 				}
227 
228 				connect(m_dataLine, &SubtitleLine::primaryTextChanged, this, &Finder::onLinePrimaryTextChanged);
229 
230 				connect(m_dataLine, &SubtitleLine::secondaryTextChanged, this, &Finder::onLineSecondaryTextChanged);
231 			}
232 		}
233 
234 		res = m_find->find();
235 
236 		if(res == KFind::NoMatch && (!m_translationMode || !m_targetRadioButtons[Both]->isChecked() || !m_feedingPrimary)) {
237 			if(backwards)
238 				--(*m_iterator);
239 			else
240 				++(*m_iterator);
241 
242 			// special case: we searched all lines and didn't found any matches
243 			if(!m_instancesFound && (m_allSearchedIndex == m_iterator->index() || (backwards ? (m_allSearchedIndex == m_iterator->lastIndex() && m_iterator->index() == SubtitleIterator::BehindFirst) : (m_allSearchedIndex == m_iterator->firstIndex() && m_iterator->index() == SubtitleIterator::AfterLast))
244 									 ))
245 			{
246 				KMessageBox::sorry(parentWidget(), i18n("No instances of '%1' found!", m_find->pattern()), i18n("Find")
247 								   );
248 
249 				invalidate();
250 
251 				break;
252 			}
253 
254 			if(m_iterator->index() < 0) {
255 				if(backwards)
256 					m_iterator->toLast();
257 				else
258 					m_iterator->toFirst();
259 
260 				if(KMessageBox::warningContinueCancel(parentWidget(), backwards ? (selection ? i18n("Beginning of selection reached.\nContinue from the end?") : i18n("Beginning of subtitle reached.\nContinue from the end?")) : (selection ? i18n("End of selection reached.\nContinue from the beginning?") : i18n("End of subtitle reached.\nContinue from the beginning?")), i18n("Find")
261 													  ) != KMessageBox::Continue)
262 					break;
263 			}
264 		}
265 	} while(res == KFind::NoMatch);
266 }
267 
268 void
onHighlight(const QString &,int matchingIndex,int matchedLength)269 Finder::onHighlight(const QString &, int matchingIndex, int matchedLength)
270 {
271 	m_instancesFound = true;
272 
273 	emit found(m_iterator->current(), m_feedingPrimary, matchingIndex, matchingIndex + matchedLength - 1);
274 }
275 
276 void
onLinePrimaryTextChanged()277 Finder::onLinePrimaryTextChanged()
278 {
279 	if(m_feedingPrimary)
280 		m_find->setData(m_dataLine->primaryDoc()->toPlainText());
281 }
282 
283 void
onLineSecondaryTextChanged()284 Finder::onLineSecondaryTextChanged()
285 {
286 	if(!m_feedingPrimary)
287 		m_find->setData(m_dataLine->secondaryDoc()->toPlainText());
288 }
289 
290 void
onIteratorSynchronized(int firstIndex,int lastIndex,bool inserted)291 Finder::onIteratorSynchronized(int firstIndex, int lastIndex, bool inserted)
292 {
293 	if(m_iterator->index() == SubtitleIterator::Invalid) {
294 		invalidate();
295 		return;
296 	}
297 
298 	int linesCount = lastIndex - firstIndex + 1;
299 	if(inserted) {
300 		if(m_allSearchedIndex >= firstIndex)
301 			m_allSearchedIndex += linesCount;
302 	} else {
303 		if(m_dataLine->index() < 0) {   // m_dataLine was removed
304 			// work around missing "invalidateData" method in KFind
305 			long options = m_find->options();
306 			QString pattern = m_find->pattern();
307 			delete m_find;
308 			m_find = new KFind(pattern, options, 0);
309 			m_find->closeFindNextDialog();
310 #if KTEXTWIDGETS_VERSION < QT_VERSION_CHECK(5, 81, 0)
311 			connect(m_find, QOverload<const QString &, int, int>::of(&KFind::highlight), this, &Finder::onHighlight);
312 #else
313 			connect(m_find, &KFind::textFound, this, &Finder::onHighlight);
314 #endif
315 		}
316 
317 		if(m_allSearchedIndex > lastIndex)
318 			m_allSearchedIndex -= linesCount;
319 		else if(m_allSearchedIndex >= firstIndex && m_allSearchedIndex <= lastIndex) // was one of the removed lines
320 			m_allSearchedIndex = m_iterator->index();
321 	}
322 }
323 
324 
325