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