1 /*
2     SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
3     SPDX-FileCopyrightText: 2010-2019 Mladen Milinkovic <max@smoothware.net>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "core/richdocument.h"
9 #include "core/subtitle.h"
10 #include "core/subtitleline.h"
11 #include "core/subtitleiterator.h"
12 #include "core/undo/subtitleactions.h"
13 #include "core/undo/subtitlelineactions.h"
14 #include "core/undo/undostack.h"
15 #include "helpers/objectref.h"
16 #include "scconfig.h"
17 #include "application.h"
18 #include "gui/treeview/lineswidget.h"
19 
20 #include <QTextDocumentFragment>
21 
22 #include <KLocalizedString>
23 
24 using namespace SubtitleComposer;
25 
26 double Subtitle::s_defaultFramesPerSecond(23.976);
27 
28 double
defaultFramesPerSecond()29 Subtitle::defaultFramesPerSecond()
30 {
31 	return s_defaultFramesPerSecond;
32 }
33 
34 void
setDefaultFramesPerSecond(double framesPerSecond)35 Subtitle::setDefaultFramesPerSecond(double framesPerSecond)
36 {
37 	s_defaultFramesPerSecond = framesPerSecond;
38 }
39 
Subtitle(double framesPerSecond)40 Subtitle::Subtitle(double framesPerSecond)
41 	: m_primaryDirtyState(false),
42 	  m_primaryCleanIndex(0),
43 	  m_secondaryDirtyState(false),
44 	  m_secondaryCleanIndex(0),
45 	  m_framesPerSecond(framesPerSecond),
46 	  m_formatData(nullptr)
47 {}
48 
~Subtitle()49 Subtitle::~Subtitle()
50 {
51 	qDeleteAll(m_lines);
52 
53 	delete m_formatData;
54 }
55 
56 void
setPrimaryData(const Subtitle & from,bool usePrimaryData)57 Subtitle::setPrimaryData(const Subtitle &from, bool usePrimaryData)
58 {
59 	beginCompositeAction(i18n("Set Primary Data"));
60 
61 	setFormatData(from.m_formatData);
62 
63 	setFramesPerSecond(from.framesPerSecond());
64 
65 	SubtitleIterator fromIt(from, Range::full());
66 	SubtitleIterator thisIt(*this, Range::full());
67 
68 	// the errors that we are going to take from 'from':
69 	const int fromErrors = (usePrimaryData ? SubtitleLine::PrimaryOnlyErrors : SubtitleLine::SecondaryOnlyErrors) | SubtitleLine::SharedErrors;
70 	// the errors that we are going to keep:
71 	const int thisErrors = SubtitleLine::SecondaryOnlyErrors;
72 
73 	for(SubtitleLine *fromLine = fromIt.current(), *thisLine = thisIt.current(); fromLine && thisLine; ++fromIt, ++thisIt, fromLine = fromIt.current(), thisLine = thisIt.current()) {
74 		thisLine->setPrimaryDoc(usePrimaryData ? fromLine->primaryDoc() : fromLine->secondaryDoc());
75 		thisLine->setTimes(fromLine->showTime(), fromLine->hideTime());
76 		thisLine->setErrorFlags((fromLine->errorFlags() & fromErrors) | (thisLine->errorFlags() & thisErrors));
77 		thisLine->setFormatData(fromLine->formatData());
78 	}
79 
80 	if(fromIt.current()) { // from has more lines
81 		QList<SubtitleLine *> lines;
82 		for(; fromIt.current(); ++fromIt) {
83 			const SubtitleLine *cur = fromIt.current();
84 			SubtitleLine *thisLine = new SubtitleLine(cur->showTime(), cur->hideTime());
85 			thisLine->setPrimaryDoc(usePrimaryData ? cur->primaryDoc() : cur->secondaryDoc());
86 			thisLine->setErrorFlags(SubtitleLine::SecondaryOnlyErrors, false);
87 			thisLine->setFormatData(cur->formatData());
88 			lines.append(thisLine);
89 		}
90 		processAction(new InsertLinesAction(this, lines));
91 	} else if(thisIt.current()) { // this has more lines
92 		for(SubtitleLine *thisLine = thisIt.current(); thisLine; ++thisIt, thisLine = thisIt.current()) {
93 			thisLine->primaryDoc()->clear();
94 			thisLine->setErrorFlags(SubtitleLine::PrimaryOnlyErrors, false);
95 			thisLine->setFormatData(0);
96 		}
97 	}
98 
99 	endCompositeAction();
100 }
101 
102 void
clearPrimaryTextData()103 Subtitle::clearPrimaryTextData()
104 {
105 	beginCompositeAction(i18n("Clear Primary Text Data"));
106 
107 	for(SubtitleIterator it(*this); it.current(); ++it) {
108 		it.current()->primaryDoc()->clear();
109 		it.current()->setErrorFlags(SubtitleLine::PrimaryOnlyErrors, false);
110 	}
111 
112 	endCompositeAction();
113 }
114 
115 void
setSecondaryData(const Subtitle & from,bool usePrimaryData)116 Subtitle::setSecondaryData(const Subtitle &from, bool usePrimaryData)
117 {
118 	beginCompositeAction(i18n("Set Secondary Data"));
119 
120 	const int srcErrors = usePrimaryData ? SubtitleLine::PrimaryOnlyErrors : SubtitleLine::SecondaryOnlyErrors;
121 	const int dstErrors = SubtitleLine::PrimaryOnlyErrors | SubtitleLine::SharedErrors;
122 
123 	for(int i = 0, n = qMin(m_lines.size(), from.m_lines.size()); i < n; i++) {
124 		const SubtitleLine *srcLine = from.m_lines.at(i).obj();
125 		SubtitleLine *dstLine = m_lines.at(i).obj();
126 		dstLine->setSecondaryDoc(usePrimaryData ? srcLine->primaryDoc() : srcLine->secondaryDoc());
127 		dstLine->setErrorFlags((dstLine->errorFlags() & dstErrors) | (srcLine->errorFlags() & srcErrors));
128 	}
129 
130 	// clear remaining local translations
131 	for(int i = from.m_lines.size(), n = m_lines.size(); i < n; i++) {
132 		SubtitleLine *dstLine = m_lines.at(i).obj();
133 		dstLine->secondaryDoc()->clear();
134 		dstLine->setErrorFlags(SubtitleLine::SecondaryOnlyErrors, false);
135 	}
136 
137 	// insert remaining source translations
138 	QList<SubtitleLine *> newLines;
139 	for(int i = m_lines.size(), n = from.m_lines.size(); i < n; i++) {
140 		const SubtitleLine *srcLine = from.m_lines.at(i).obj();
141 		SubtitleLine *dstLine = new SubtitleLine(srcLine->showTime(), srcLine->hideTime());
142 		dstLine->setSecondaryDoc(usePrimaryData ? srcLine->primaryDoc() : srcLine->secondaryDoc());
143 		dstLine->setErrorFlags(SubtitleLine::PrimaryOnlyErrors, false);
144 		newLines.append(dstLine);
145 	}
146 	if(!newLines.isEmpty())
147 		processAction(new InsertLinesAction(this, newLines));
148 
149 	endCompositeAction(UndoStack::Secondary);
150 }
151 
152 void
clearSecondaryTextData()153 Subtitle::clearSecondaryTextData()
154 {
155 	beginCompositeAction(i18n("Clear Secondary Text Data"));
156 
157 	for(SubtitleIterator it(*this); it.current(); ++it) {
158 		it.current()->secondaryDoc()->clear();
159 		it.current()->setErrorFlags(SubtitleLine::SecondaryOnlyErrors, false);
160 	}
161 
162 	endCompositeAction();
163 }
164 
165 void
clearPrimaryDirty()166 Subtitle::clearPrimaryDirty()
167 {
168 	if(!m_primaryDirtyState)
169 		return;
170 
171 	m_primaryDirtyState = false;
172 	m_primaryCleanIndex = app()->undoStack()->index();
173 	emit primaryDirtyStateChanged(false);
174 }
175 
176 void
clearSecondaryDirty()177 Subtitle::clearSecondaryDirty()
178 {
179 	if(!m_secondaryDirtyState)
180 		return;
181 
182 	m_secondaryDirtyState = false;
183 	m_secondaryCleanIndex = app()->undoStack()->index();
184 	emit secondaryDirtyStateChanged(false);
185 }
186 
187 FormatData *
formatData() const188 Subtitle::formatData() const
189 {
190 	return m_formatData;
191 }
192 
193 void
setFormatData(const FormatData * formatData)194 Subtitle::setFormatData(const FormatData *formatData)
195 {
196 	delete m_formatData;
197 
198 	m_formatData = formatData ? new FormatData(*formatData) : NULL;
199 }
200 
201 double
framesPerSecond() const202 Subtitle::framesPerSecond() const
203 {
204 	return m_framesPerSecond;
205 }
206 
207 void
setFramesPerSecond(double framesPerSecond)208 Subtitle::setFramesPerSecond(double framesPerSecond)
209 {
210 	if(qAbs(m_framesPerSecond - framesPerSecond) > 1e-6)
211 		processAction(new SetFramesPerSecondAction(this, framesPerSecond));
212 }
213 
214 void
changeFramesPerSecond(double toFramesPerSecond,double fromFramesPerSecond)215 Subtitle::changeFramesPerSecond(double toFramesPerSecond, double fromFramesPerSecond)
216 {
217 	if(toFramesPerSecond <= 0)
218 		return;
219 
220 	if(fromFramesPerSecond <= 0)
221 		fromFramesPerSecond = m_framesPerSecond;
222 
223 	beginCompositeAction(i18n("Change Frame Rate"));
224 
225 	setFramesPerSecond(toFramesPerSecond);
226 
227 	double scaleFactor = fromFramesPerSecond / toFramesPerSecond;
228 
229 	if(scaleFactor != 1.0) {
230 		for(SubtitleIterator it(*this, Range::full()); it.current(); ++it) {
231 			Time showTime = it.current()->showTime();
232 			showTime *= scaleFactor;
233 
234 			Time hideTime = it.current()->hideTime();
235 			hideTime *= scaleFactor;
236 
237 			processAction(new SetLineTimesAction(it, showTime, hideTime));
238 		}
239 	}
240 
241 	endCompositeAction();
242 }
243 
244 SubtitleLine *
line(int index)245 Subtitle::line(int index)
246 {
247 	return index < 0 || index >= m_lines.count() ? nullptr : m_lines.at(index).obj();
248 }
249 
250 const SubtitleLine *
line(int index) const251 Subtitle::line(int index) const
252 {
253 	return index < 0 || index >= m_lines.count() ? nullptr : m_lines.at(index).obj();
254 }
255 bool
hasAnchors() const256 Subtitle::hasAnchors() const
257 {
258 	return !m_anchoredLines.empty();
259 }
260 
261 bool
isLineAnchored(int index) const262 Subtitle::isLineAnchored(int index) const
263 {
264 	if(index < 0 || index >= m_lines.count())
265 		return false;
266 
267 	return isLineAnchored(m_lines[index]);
268 }
269 
270 bool
isLineAnchored(const SubtitleLine * line) const271 Subtitle::isLineAnchored(const SubtitleLine *line) const
272 {
273 	if(!line)
274 		return false;
275 
276 	return m_anchoredLines.indexOf(line) != -1;
277 }
278 
279 void
toggleLineAnchor(int index)280 Subtitle::toggleLineAnchor(int index)
281 {
282 	if(index < 0 || index >= m_lines.count())
283 		return;
284 
285 	toggleLineAnchor(m_lines[index]);
286 }
287 
288 void
toggleLineAnchor(const SubtitleLine * line)289 Subtitle::toggleLineAnchor(const SubtitleLine *line)
290 {
291 	if(!line)
292 		return;
293 
294 	int anchorIndex = m_anchoredLines.indexOf(line);
295 
296 	if(anchorIndex == -1)
297 		m_anchoredLines.append(line);
298 	else
299 		m_anchoredLines.removeAt(anchorIndex);
300 
301 	emit lineAnchorChanged(line, anchorIndex == -1);
302 }
303 
304 void
removeAllAnchors()305 Subtitle::removeAllAnchors()
306 {
307 	QList<const SubtitleLine *> anchoredLines;
308 
309 	m_anchoredLines.swap(anchoredLines);
310 
311 	foreach(auto line, anchoredLines)
312 		emit lineAnchorChanged(line, false);
313 }
314 
315 int
insertIndex(const Time & showTime,int start,int end) const316 Subtitle::insertIndex(const Time &showTime, int start, int end) const
317 {
318 	while(end - start > 1) {
319 		const int mid = (start + end) / 2;
320 		if(showTime < m_lines.at(mid)->showTime())
321 			end = mid - 1;
322 		else
323 			start = mid;
324 	}
325 	if(m_lines.empty() || showTime < m_lines.at(start)->showTime())
326 		return start;
327 	return showTime < m_lines.at(end)->showTime() ? end : end + 1;
328 }
329 
330 void
insertLine(SubtitleLine * line)331 Subtitle::insertLine(SubtitleLine *line)
332 {
333 	QList<SubtitleLine *> lines;
334 	lines.append(line);
335 	processAction(new InsertLinesAction(this, lines, insertIndex(line->showTime())));
336 }
337 
338 void
insertLine(SubtitleLine * line,int index)339 Subtitle::insertLine(SubtitleLine *line, int index)
340 {
341 	Q_ASSERT(index >= 0 && index <= m_lines.count());
342 	QList<SubtitleLine *> lines;
343 	lines.append(line);
344 	processAction(new InsertLinesAction(this, lines, index));
345 }
346 
347 SubtitleLine *
insertNewLine(int index,bool insertAfter,SubtitleTarget target)348 Subtitle::insertNewLine(int index, bool insertAfter, SubtitleTarget target)
349 {
350 	Q_ASSERT(index <= count());
351 
352 	if(index < 0)
353 		index = count();
354 
355 	SubtitleLine *newLine = new SubtitleLine();
356 	const int newLineIndex = (target == Secondary) ? m_lines.count() : index;
357 
358 	const double linePause = (double)SCConfig::linePause();
359 	const double lineDuration = (double)SCConfig::lineDuration();
360 	const double lineDurationAndPause = lineDuration + linePause;
361 
362 	if(insertAfter) {
363 		if(newLineIndex) {              // there is a previous line
364 			const SubtitleLine *prevLine = at(newLineIndex - 1);
365 			newLine->setTimes(prevLine->hideTime() + linePause, prevLine->hideTime() + lineDurationAndPause);
366 		} else if(newLineIndex < count()) {     // there is a next line
367 			const SubtitleLine *nextLine = at(newLineIndex);
368 			newLine->setTimes(nextLine->showTime() - lineDurationAndPause, nextLine->showTime() - linePause);
369 		} else
370 			newLine->setHideTime(lineDuration);
371 	} else {
372 		if(newLineIndex < count()) {    // there is a next line
373 			const SubtitleLine *nextLine = at(newLineIndex);
374 			newLine->setTimes(nextLine->showTime() - lineDurationAndPause, nextLine->showTime() - linePause);
375 		} else if(newLineIndex) {       // there is a previous line
376 			const SubtitleLine *prevLine = at(newLineIndex - 1);
377 			newLine->setTimes(prevLine->hideTime() + linePause, prevLine->hideTime() + lineDurationAndPause);
378 		} else
379 			newLine->setHideTime(lineDuration);
380 	}
381 
382 	if(target == Both || index == count()) {
383 		insertLine(newLine, newLineIndex);
384 	} else if(target == Primary) {
385 		beginCompositeAction(i18n("Insert Line"));
386 
387 		insertLine(newLine, newLineIndex);
388 
389 		SubtitleLine *line = newLine;
390 		SubtitleIterator it(*this, Range::full(), false);
391 		for(it.toIndex(newLineIndex + 1); it.current(); ++it) {
392 			line->setSecondaryDoc(it.current()->secondaryDoc());
393 			line = it.current();
394 		}
395 		line->secondaryDoc()->clear();
396 
397 		endCompositeAction();
398 	} else if(target == Secondary) {
399 		beginCompositeAction(i18n("Insert Line"));
400 
401 		insertLine(newLine, newLineIndex);
402 
403 		SubtitleIterator it(*this, Range::full(), true);
404 		SubtitleLine *line = it.current();
405 		for(--it; it.index() >= index; --it) {
406 			line->setSecondaryDoc(it.current()->secondaryDoc());
407 			line = it.current();
408 		}
409 		line->secondaryDoc()->clear();
410 
411 		newLine = line;
412 
413 		endCompositeAction();
414 	}
415 
416 	return newLine;
417 }
418 
419 void
removeLines(const RangeList & r,SubtitleTarget target)420 Subtitle::removeLines(const RangeList &r, SubtitleTarget target)
421 {
422 	if(m_lines.isEmpty())
423 		return;
424 
425 	RangeList ranges = r;
426 	ranges.trimToIndex(m_lines.count() - 1);
427 
428 	if(ranges.isEmpty())
429 		return;
430 
431 	if(target == Both) {
432 		beginCompositeAction(i18n("Remove Lines"));
433 
434 		RangeList::ConstIterator rangesIt = ranges.end(), begin = ranges.begin();
435 		do {
436 			rangesIt--;
437 			processAction(new RemoveLinesAction(this, rangesIt->start(), rangesIt->end()));
438 		} while(rangesIt != begin);
439 
440 		endCompositeAction();
441 	} else if(target == Secondary) {
442 		beginCompositeAction(i18n("Remove Lines"));
443 
444 		RangeList rangesComplement = ranges.complement();
445 		rangesComplement.trimToRange(Range(ranges.firstIndex(), m_lines.count() - 1));
446 
447 		// we have to move the secondary texts up (we do it in chunks)
448 		SubtitleIterator srcIt(*this, rangesComplement);
449 		SubtitleIterator dstIt(*this, Range::upper(ranges.firstIndex()));
450 		for(; srcIt.current() && dstIt.current(); ++srcIt, ++dstIt)
451 			dstIt.current()->setSecondaryDoc(srcIt.current()->secondaryDoc());
452 
453 		// the remaining lines secondary text must be cleared
454 		for(; dstIt.current(); ++dstIt)
455 			dstIt.current()->secondaryDoc()->clear();
456 
457 		endCompositeAction();
458 	} else { // target == Primary
459 		beginCompositeAction(i18n("Remove Lines"));
460 
461 		RangeList mutableRanges(ranges);
462 		mutableRanges.trimToIndex(m_lines.count() - 1);
463 
464 		// first, we need to append as many empty lines as we're to remove
465 		// we insert them with a greater time than the one of the last (non deleted) line
466 
467 		int linesCount = m_lines.count();
468 
469 		Range lastRange = mutableRanges.last();
470 		int lastIndex = lastRange.end() == linesCount - 1 ? lastRange.start() - 1 : linesCount - 1;
471 		SubtitleLine *lastLine = lastIndex < linesCount ? at(lastIndex) : 0;
472 		Time showTime(lastLine ? lastLine->hideTime() + 100. : Time());
473 		Time hideTime(showTime + 1000.);
474 
475 		QList<SubtitleLine *> lines;
476 		for(int index = 0, size = ranges.indexesCount(); index < size; ++index) {
477 			lines.append(new SubtitleLine(showTime, hideTime));
478 			showTime.shift(1100.);
479 			hideTime.shift(1100.);
480 		}
481 
482 		processAction(new InsertLinesAction(this, lines));
483 
484 		// then, we move the secondary texts down (we need to iterate from bottom to top for that)
485 		RangeList rangesComplement = mutableRanges.complement();
486 
487 		SubtitleIterator srcIt(*this, Range(ranges.firstIndex(), m_lines.count() - lines.count() - 1), true);
488 		SubtitleIterator dstIt(*this, rangesComplement, true);
489 		for(; srcIt.current() && dstIt.current(); --srcIt, --dstIt)
490 			dstIt.current()->setSecondaryDoc(srcIt.current()->secondaryDoc());
491 
492 		// finally, we can remove the specified lines
493 		RangeList::ConstIterator rangesIt = ranges.end(), begin = ranges.begin();
494 		do {
495 			rangesIt--;
496 			processAction(new RemoveLinesAction(this, rangesIt->start(), rangesIt->end()));
497 		} while(rangesIt != begin);
498 
499 		endCompositeAction();
500 	}
501 }
502 
503 void
swapTexts(const RangeList & ranges)504 Subtitle::swapTexts(const RangeList &ranges)
505 {
506 	processAction(new SwapLinesTextsAction(this, ranges));
507 }
508 
509 void
splitLines(const RangeList & ranges)510 Subtitle::splitLines(const RangeList &ranges)
511 {
512 	auto splitOnSpace = [&](RichDocument *doc)->bool{
513 		if(doc->isEmpty())
514 			return false;
515 		const QString &text = doc->toPlainText();
516 		QTextCursor *c = doc->undoableCursor();
517 		int len = text.length();
518 		int i = len / 2;
519 		int j = i + len % 2;
520 		for(; ; i--, j++) {
521 			if(text.at(i) == QChar::Space) {
522 				c->movePosition(QTextCursor::Start);
523 				c->movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, i);
524 				c->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
525 				c->insertText(QString(QChar::LineFeed));
526 				return true;
527 			}
528 			if(text.at(j) == QChar::Space) {
529 				c->movePosition(QTextCursor::Start);
530 				c->movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, j);
531 				c->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
532 				c->insertText(QString(QChar::LineFeed));
533 				return true;
534 			}
535 			if(i == 0) {
536 				c->movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
537 				c->insertText(QString(QChar::LineFeed));
538 				return true;
539 			}
540 		}
541 		return false;
542 	};
543 
544 	beginCompositeAction(i18n("Split Lines"));
545 
546 	bool hasMultipleLines = false;
547 
548 	for(SubtitleIterator it(*this, ranges, true); it.current(); --it) {
549 		SubtitleLine *ln = it.current();
550 		ln->simplifyTextWhiteSpace(Both);
551 		if(!hasMultipleLines && (ln->primaryDoc()->lineCount() > 1 || ln->secondaryDoc()->lineCount() > 1))
552 			hasMultipleLines = true;
553 	}
554 
555 	for(SubtitleIterator it(*this, ranges, true); it.current(); --it) {
556 		SubtitleLine *line = it.current();
557 
558 		if(line->primaryDoc()->isEmpty())
559 			continue;
560 
561 		if(!hasMultipleLines) {
562 			if(splitOnSpace(line->primaryDoc()))
563 				splitOnSpace(line->secondaryDoc());
564 			else
565 				continue;
566 		}
567 
568 		QTextCursor c1(line->primaryDoc());
569 		QTextCursor c2(line->secondaryDoc());
570 
571 		c1.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
572 		QVector<quint32> dur;
573 		dur.push_back(c1.selectedText().size());
574 		quint32 totalDuration = dur.back();
575 
576 		QVector<SubtitleLine *> newLines;
577 		for(;;) {
578 			if(!c1.movePosition(QTextCursor::NextBlock))
579 				c1.movePosition(QTextCursor::EndOfBlock);
580 			if(!c2.movePosition(QTextCursor::NextBlock))
581 				c2.movePosition(QTextCursor::EndOfBlock);
582 			if(c1.atEnd() && c2.atEnd())
583 				break;
584 
585 			c1.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
586 			c2.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
587 
588 			dur.push_back(c1.selectedText().size());
589 			totalDuration += dur.back();
590 
591 			SubtitleLine *nl = new SubtitleLine();
592 			QTextCursor(nl->primaryDoc()).insertFragment(c1.selection());
593 			QTextCursor(nl->secondaryDoc()).insertFragment(c2.selection());
594 			newLines.push_back(nl);
595 		}
596 
597 		c1.movePosition(QTextCursor::Start);
598 		c1.movePosition(QTextCursor::EndOfBlock);
599 		c1.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
600 		c1.removeSelectedText();
601 
602 		c2.movePosition(QTextCursor::Start);
603 		c2.movePosition(QTextCursor::EndOfBlock);
604 		c2.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
605 		c2.removeSelectedText();
606 
607 		const double lineDur = line->durationTime().toMillis();
608 		SubtitleLine *pl = line;
609 		auto duri = dur.begin();
610 		pl->setDurationTime(lineDur * *duri / totalDuration);
611 		for(SubtitleLine *nl: newLines) {
612 			++duri;
613 			const Time st = pl->hideTime() + 1.;
614 			nl->setTimes(st, qMax(st, pl->hideTime() + lineDur * *duri / totalDuration));
615 			insertLine(nl, pl->index() + 1);
616 			pl = nl;
617 		}
618 	}
619 
620 	endCompositeAction();
621 }
622 
623 void
joinLines(const RangeList & ranges)624 Subtitle::joinLines(const RangeList &ranges)
625 {
626 	beginCompositeAction(i18n("Join Lines"));
627 
628 	RangeList deleteRanges;
629 
630 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
631 		int rangeStart = rangesIt->start();
632 		int rangeEnd = normalizeRangeIndex(rangesIt->end());
633 
634 		if(rangeStart >= rangeEnd)
635 			continue;
636 
637 		SubtitleLine *line = at(rangeStart);
638 		line->setHideTime(at(rangeEnd)->hideTime());
639 		Range postLines(rangeStart + 1, rangeEnd);
640 		for(SubtitleIterator it(*this, postLines); it.current(); ++it) {
641 			SubtitleLine *ln = it.current();
642 			if(!ln->primaryDoc()->isEmpty()) {
643 				QTextCursor *c = line->primaryDoc()->undoableCursor();
644 				c->movePosition(QTextCursor::End);
645 				c->insertBlock();
646 				c->insertFragment(QTextDocumentFragment(ln->primaryDoc()));
647 			}
648 			if(!ln->secondaryDoc()->isEmpty()) {
649 				QTextCursor *c = line->secondaryDoc()->undoableCursor();
650 				c->movePosition(QTextCursor::End);
651 				c->insertBlock();
652 				c->insertFragment(QTextDocumentFragment(ln->secondaryDoc()));
653 			}
654 		}
655 		deleteRanges << postLines;
656 	}
657 
658 	removeLines(deleteRanges, Both);
659 
660 	endCompositeAction();
661 }
662 
663 void
shiftAnchoredLine(SubtitleLine * anchoredLine,const Time & newShowTime)664 Subtitle::shiftAnchoredLine(SubtitleLine *anchoredLine, const Time &newShowTime)
665 {
666 	if(m_anchoredLines.indexOf(anchoredLine) == -1 || m_lines.isEmpty())
667 		return;
668 
669 	const SubtitleLine *prevAnchor = nullptr;
670 	const SubtitleLine *nextAnchor = nullptr;
671 	foreach(auto anchor, m_anchoredLines) {
672 		if((prevAnchor == nullptr || prevAnchor->m_showTime < anchor->m_showTime) && anchor->m_showTime < anchoredLine->m_showTime)
673 			prevAnchor = anchor;
674 		if((nextAnchor == nullptr || nextAnchor->m_showTime > anchor->m_showTime) && anchor->m_showTime > anchoredLine->m_showTime)
675 			nextAnchor = anchor;
676 	}
677 	if((prevAnchor && prevAnchor->m_showTime > newShowTime) || (nextAnchor && nextAnchor->m_showTime < newShowTime))
678 		return;
679 
680 	if(!prevAnchor && !nextAnchor) {
681 		double shift = newShowTime.toMillis() - anchoredLine->m_showTime.toMillis();
682 		for(int i = 0, n = count(); i < n; i++)
683 			at(i)->shiftTimes(shift);
684 	} else {
685 		// save times as adjustLines() will modify them, and processing nextAnchor will modify them again
686 		Time savedShowTime(anchoredLine->m_showTime);
687 		Time savedHideTime(anchoredLine->m_hideTime);
688 		if(prevAnchor) {
689 			adjustLines(Range(prevAnchor->index(), anchoredLine->index()), prevAnchor->m_showTime.toMillis(), newShowTime.toMillis());
690 		} else if(nextAnchor->m_showTime != anchoredLine->m_showTime) {
691 			const SubtitleLine *first = firstLine();
692 			double scaleFactor = (nextAnchor->m_showTime.toMillis() - newShowTime.toMillis()) / (nextAnchor->m_showTime.toMillis() - anchoredLine->m_showTime.toMillis());
693 			Time firstShowTime(scaleFactor * (first->m_showTime.toMillis() - nextAnchor->m_showTime.toMillis()) + nextAnchor->m_showTime.toMillis());
694 			adjustLines(Range(first->index(), anchoredLine->index()), firstShowTime.toMillis(), newShowTime.toMillis());
695 		}
696 
697 		double lastShowTime;
698 		const SubtitleLine *last;
699 		if(nextAnchor) {
700 			last = nextAnchor;
701 			lastShowTime = nextAnchor->m_showTime.toMillis();
702 		} else if(anchoredLine->m_showTime != prevAnchor->m_showTime) {
703 			last = lastLine();
704 			double scaleFactor = (newShowTime.toMillis() - prevAnchor->m_showTime.toMillis()) / (savedShowTime.toMillis() - prevAnchor->m_showTime.toMillis());
705 			lastShowTime = scaleFactor * (last->m_showTime.toMillis() - prevAnchor->m_showTime.toMillis()) + prevAnchor->m_showTime.toMillis();
706 		} else {
707 			last = nullptr;
708 			lastShowTime = 0;
709 		}
710 		if(newShowTime.toMillis() < lastShowTime && anchoredLine != last) {
711 			anchoredLine->m_showTime = savedShowTime;
712 			anchoredLine->m_hideTime = savedHideTime;
713 			adjustLines(Range(anchoredLine->index(), last->index()), newShowTime.toMillis(), lastShowTime);
714 		}
715 	}
716 }
717 
718 void
shiftLines(const RangeList & ranges,long msecs)719 Subtitle::shiftLines(const RangeList &ranges, long msecs)
720 {
721 	if(msecs == 0)
722 		return;
723 
724 	beginCompositeAction(i18n("Shift Lines"));
725 
726 	if(!m_anchoredLines.empty()) {
727 		for(SubtitleIterator it(*this, ranges); it.current(); ++it) {
728 			SubtitleLine *line = it.current();
729 			if(m_anchoredLines.indexOf(line) != -1) {
730 				shiftAnchoredLine(line, line->showTime().shifted(msecs));
731 				break;
732 			}
733 		}
734 	} else {
735 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
736 			it.current()->shiftTimes(msecs);
737 	}
738 
739 	endCompositeAction();
740 }
741 
742 void
adjustLines(const Range & range,long newFirstTime,long newLastTime)743 Subtitle::adjustLines(const Range &range, long newFirstTime, long newLastTime)
744 {
745 	if(m_lines.isEmpty() || newFirstTime >= newLastTime)
746 		return;
747 
748 	int firstIndex = range.start();
749 	int lastIndex = normalizeRangeIndex(range.end());
750 
751 	if(firstIndex >= lastIndex)
752 		return;
753 
754 	double oldFirstTime = at(firstIndex)->showTime().toMillis();
755 	double oldLastTime = at(lastIndex)->showTime().toMillis();
756 	double oldDeltaTime = oldLastTime - oldFirstTime;
757 
758 	double newDeltaTime = newLastTime - newFirstTime;
759 
760 	// special case in which we can't proceed as there's no way to
761 	// linearly transform the same time into two different ones...
762 	if(!oldDeltaTime && newDeltaTime)
763 		return;
764 
765 	double shiftMseconds;
766 	double scaleFactor;
767 
768 	if(oldDeltaTime) {
769 		shiftMseconds = newFirstTime - (newDeltaTime / oldDeltaTime) * oldFirstTime;
770 		scaleFactor = newDeltaTime / oldDeltaTime;
771 	} else {                                        // oldDeltaTime == 0 && newDeltaTime == 0
772 		// in this particular case we can make the adjust transformation act as a plain shift
773 		shiftMseconds = newFirstTime - oldFirstTime;
774 		scaleFactor = 1.0;
775 	}
776 
777 	if(shiftMseconds == 0 && scaleFactor == 1.0)
778 		return;
779 
780 	beginCompositeAction(i18n("Adjust Lines"));
781 
782 	for(SubtitleIterator it(*this, range); it.current(); ++it)
783 		it.current()->adjustTimes(shiftMseconds, scaleFactor);
784 
785 	endCompositeAction();
786 }
787 
788 void
sortLines(const Range & range)789 Subtitle::sortLines(const Range &range)
790 {
791 	beginCompositeAction(i18n("Sort"));
792 
793 	SubtitleIterator it(*this, range);
794 	SubtitleLine *line = it.current();
795 	SubtitleLine *nextLine = (++it).current();
796 	for(; nextLine; ++it, line = nextLine, nextLine = it.current()) {
797 		if(line->showTime() <= nextLine->showTime()) // already sorted
798 			continue;
799 
800 		// TODO: could be improved by using insertIndex()
801 		SubtitleIterator tmp(it);
802 		int fromIndex = tmp.index();
803 		int toIndex = -1;
804 		while((--tmp).current() && tmp.current()->showTime() > nextLine->showTime())
805 			toIndex = tmp.index();
806 
807 		Q_ASSERT(toIndex != -1);
808 
809 		processAction(new MoveLineAction(this, fromIndex, toIndex));
810 
811 		--it;
812 	}
813 
814 	endCompositeAction();
815 }
816 
817 void
applyDurationLimits(const RangeList & ranges,const Time & minDuration,const Time & maxDuration,bool canOverlap)818 Subtitle::applyDurationLimits(const RangeList &ranges, const Time &minDuration, const Time &maxDuration, bool canOverlap)
819 {
820 	if(m_lines.isEmpty() || minDuration > maxDuration)
821 		return;
822 
823 	beginCompositeAction(i18n("Enforce Duration Limits"));
824 
825 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
826 		Time lineDuration;
827 		SubtitleIterator it(*this, *rangesIt);
828 		SubtitleLine *line = it.current();
829 		++it;
830 		SubtitleLine *nextLine = it.current();
831 
832 		for(; line; ++it, line = nextLine, nextLine = it.current()) {
833 			lineDuration = line->durationTime();
834 
835 			if(lineDuration > maxDuration)
836 				line->setDurationTime(maxDuration);
837 			else if(lineDuration < minDuration) {
838 				if(!nextLine) // the last line doesn't have risk of overlapping
839 					line->setDurationTime(minDuration);
840 				else {
841 					if(canOverlap || line->showTime() + minDuration < nextLine->showTime())
842 						line->setDurationTime(minDuration);
843 					else {          // setting the duration to minDuration will cause an unwanted overlap
844 						if(line->hideTime() < nextLine->showTime()) // make duration as big as possible without overlap
845 							line->setHideTime(nextLine->showTime() - 1);
846 						// else line is already at the maximum duration without overlap (or overlapping) so we don't change it
847 					}
848 				}
849 			}
850 		}
851 	}
852 
853 	endCompositeAction();
854 }
855 
856 void
setMaximumDurations(const RangeList & ranges)857 Subtitle::setMaximumDurations(const RangeList &ranges)
858 {
859 	if(m_lines.isEmpty())
860 		return;
861 
862 	beginCompositeAction(i18n("Maximize Durations"));
863 
864 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
865 		SubtitleIterator it(*this, *rangesIt);
866 		SubtitleLine *line = it.current();
867 		++it;
868 		SubtitleLine *nextLine = it.current();
869 
870 		for(; line && nextLine; ++it, line = nextLine, nextLine = it.current()) {
871 			if(line->hideTime() < nextLine->showTime())
872 				line->setHideTime(nextLine->showTime() - 1);
873 		}
874 	}
875 
876 	endCompositeAction();
877 }
878 
879 void
setAutoDurations(const RangeList & ranges,int msecsPerChar,int msecsPerWord,int msecsPerLine,bool canOverlap,SubtitleTarget calculationTarget)880 Subtitle::setAutoDurations(const RangeList &ranges, int msecsPerChar, int msecsPerWord, int msecsPerLine, bool canOverlap, SubtitleTarget calculationTarget)
881 {
882 	if(m_lines.isEmpty())
883 		return;
884 
885 	beginCompositeAction(i18n("Set Automatic Durations"));
886 
887 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
888 		Time autoDuration;
889 
890 		SubtitleIterator it(*this, *rangesIt);
891 		SubtitleLine *line = it.current();
892 		++it;
893 		SubtitleLine *nextLine = it.current();
894 
895 		for(; line; ++it, line = nextLine, nextLine = it.current()) {
896 			autoDuration = line->autoDuration(msecsPerChar, msecsPerWord, msecsPerLine, calculationTarget);
897 
898 			if(!nextLine) // the last line doesn't have risk of overlapping
899 				line->setDurationTime(autoDuration);
900 			else {
901 				if(canOverlap || line->showTime() + autoDuration < nextLine->showTime())
902 					line->setDurationTime(autoDuration);
903 				else // setting the duration to autoDuration will cause an unwanted overlap
904 					line->setHideTime(nextLine->showTime() - 1);
905 			}
906 		}
907 	}
908 
909 	endCompositeAction();
910 }
911 
912 void
fixOverlappingLines(const RangeList & ranges,const Time & minInterval)913 Subtitle::fixOverlappingLines(const RangeList &ranges, const Time &minInterval)
914 {
915 	if(m_lines.isEmpty())
916 		return;
917 
918 	beginCompositeAction(i18n("Fix Overlapping Times"));
919 
920 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
921 		int rangeStart = rangesIt->start();
922 		int rangeEnd = normalizeRangeIndex(rangesIt->end() + 1);
923 
924 		if(rangeStart >= rangeEnd)
925 			break;
926 
927 		SubtitleIterator it(*this, Range(rangeStart, rangeEnd));
928 		SubtitleLine *line = it.current();
929 		++it;
930 		SubtitleLine *nextLine = it.current();
931 
932 		for(; nextLine; ++it, line = nextLine, nextLine = it.current()) {
933 			if(line->hideTime() + minInterval >= nextLine->showTime()) {
934 				Time newHideTime = nextLine->showTime() - minInterval;
935 				line->setHideTime(newHideTime >= line->showTime() ? newHideTime : line->showTime());
936 			}
937 		}
938 	}
939 
940 	endCompositeAction();
941 }
942 
943 void
fixPunctuation(const RangeList & ranges,bool spaces,bool quotes,bool engI,bool ellipsis,SubtitleTarget target)944 Subtitle::fixPunctuation(const RangeList &ranges, bool spaces, bool quotes, bool engI, bool ellipsis, SubtitleTarget target)
945 {
946 	if(m_lines.isEmpty() || (!spaces && !quotes && !engI && !ellipsis) || target >= SubtitleTargetSize)
947 		return;
948 
949 	beginCompositeAction(i18n("Fix Lines Punctuation"));
950 
951 	for(RangeList::ConstIterator rangesIt = ranges.begin(), end = ranges.end(); rangesIt != end; ++rangesIt) {
952 		SubtitleIterator it(*this, *rangesIt);
953 
954 		bool primaryContinues = false;
955 		bool secondaryContinues = false;
956 
957 		if(it.index() > 0) {
958 			if(target == Primary || target == Both) // init primaryContinues
959 				at(it.index() - 1)->primaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &primaryContinues, true);
960 			if(target == Secondary || target == Both) // init secondaryContinues
961 				at(it.index() - 1)->secondaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &secondaryContinues, true);
962 		}
963 
964 		for(; it.current(); ++it) {
965 			switch(target) {
966 			case Primary:
967 				it.current()->primaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &primaryContinues);
968 				break;
969 			case Secondary:
970 				it.current()->secondaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &secondaryContinues);
971 				break;
972 			case Both:
973 				it.current()->primaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &primaryContinues);
974 				it.current()->secondaryDoc()->fixPunctuation(spaces, quotes, engI, ellipsis, &secondaryContinues);
975 				break;
976 			default:
977 				break;
978 			}
979 		}
980 	}
981 
982 	endCompositeAction();
983 }
984 
985 void
lowerCase(const RangeList & ranges,SubtitleTarget target)986 Subtitle::lowerCase(const RangeList &ranges, SubtitleTarget target)
987 {
988 	if(m_lines.isEmpty() || target >= SubtitleTargetSize)
989 		return;
990 
991 	beginCompositeAction(i18n("Lower Case"));
992 
993 	switch(target) {
994 	case Primary:
995 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
996 			it.current()->primaryDoc()->toLower();
997 		break;
998 	case Secondary:
999 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1000 			it.current()->secondaryDoc()->toLower();
1001 		break;
1002 	case Both:
1003 		for(SubtitleIterator it(*this, ranges); it.current(); ++it) {
1004 			it.current()->primaryDoc()->toLower();
1005 			it.current()->secondaryDoc()->toLower();
1006 		}
1007 		break;
1008 	default:
1009 		break;
1010 	}
1011 
1012 	endCompositeAction();
1013 }
1014 
1015 void
upperCase(const RangeList & ranges,SubtitleTarget target)1016 Subtitle::upperCase(const RangeList &ranges, SubtitleTarget target)
1017 {
1018 	if(m_lines.isEmpty() || target >= SubtitleTargetSize)
1019 		return;
1020 
1021 	beginCompositeAction(i18n("Upper Case"));
1022 
1023 	switch(target) {
1024 	case Primary:
1025 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1026 			it.current()->primaryDoc()->toUpper();
1027 		break;
1028 	case Secondary:
1029 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1030 			it.current()->secondaryDoc()->toUpper();
1031 		break;
1032 	case Both:
1033 		for(SubtitleIterator it(*this, ranges); it.current(); ++it) {
1034 			it.current()->primaryDoc()->toUpper();
1035 			it.current()->secondaryDoc()->toUpper();
1036 		}
1037 		break;
1038 	default:
1039 		break;
1040 	}
1041 
1042 	endCompositeAction();
1043 }
1044 
1045 void
titleCase(const RangeList & ranges,bool lowerFirst,SubtitleTarget target)1046 Subtitle::titleCase(const RangeList &ranges, bool lowerFirst, SubtitleTarget target)
1047 {
1048 	if(m_lines.isEmpty() || target >= SubtitleTargetSize)
1049 		return;
1050 
1051 	beginCompositeAction(i18n("Title Case"));
1052 
1053 	switch(target) {
1054 	case Primary: {
1055 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1056 			it.current()->primaryDoc()->toSentenceCase(nullptr, lowerFirst, true);
1057 		break;
1058 	}
1059 	case Secondary: {
1060 		for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1061 			it.current()->secondaryDoc()->toSentenceCase(nullptr, lowerFirst, true);
1062 		break;
1063 	}
1064 	case Both: {
1065 		for(SubtitleIterator it(*this, ranges); it.current(); ++it) {
1066 			it.current()->primaryDoc()->toSentenceCase(nullptr, lowerFirst, true);
1067 			it.current()->secondaryDoc()->toSentenceCase(nullptr, lowerFirst, true);
1068 		}
1069 		break;
1070 	}
1071 	default:
1072 		break;
1073 	}
1074 
1075 	endCompositeAction();
1076 }
1077 
1078 void
sentenceCase(const RangeList & ranges,bool lowerFirst,SubtitleTarget target)1079 Subtitle::sentenceCase(const RangeList &ranges, bool lowerFirst, SubtitleTarget target)
1080 {
1081 	if(m_lines.isEmpty() || target >= SubtitleTargetSize)
1082 		return;
1083 
1084 	beginCompositeAction(i18n("Sentence Case"));
1085 
1086 	for(RangeList::ConstIterator rangesIt = ranges.begin(), rangesEnd = ranges.end(); rangesIt != rangesEnd; ++rangesIt) {
1087 		SubtitleIterator it(*this, *rangesIt);
1088 
1089 		bool pCont = false;
1090 		bool sCont = false;
1091 
1092 		if(it.index() > 0) {
1093 			if(target == Primary || target == Both)
1094 				at(it.index() - 1)->primaryDoc()->toSentenceCase(&pCont, lowerFirst, false, true);
1095 			if(target == Secondary || target == Both)
1096 				at(it.index() - 1)->secondaryDoc()->toSentenceCase(&sCont, lowerFirst, false, true);
1097 		}
1098 
1099 		switch(target) {
1100 		case Primary: {
1101 			for(; it.current(); ++it)
1102 				it.current()->primaryDoc()->toSentenceCase(&pCont, lowerFirst);
1103 			break;
1104 		}
1105 		case Secondary: {
1106 			for(; it.current(); ++it)
1107 				it.current()->secondaryDoc()->toSentenceCase(&sCont, lowerFirst);
1108 			break;
1109 		}
1110 		case Both: {
1111 			for(; it.current(); ++it) {
1112 				it.current()->primaryDoc()->toSentenceCase(&pCont, lowerFirst);
1113 				it.current()->secondaryDoc()->toSentenceCase(&sCont, lowerFirst);
1114 			}
1115 			break;
1116 		}
1117 		default:
1118 			break;
1119 		}
1120 	}
1121 
1122 	endCompositeAction();
1123 }
1124 
1125 void
breakLines(const RangeList & ranges,unsigned minLengthForLineBreak,SubtitleTarget target)1126 Subtitle::breakLines(const RangeList &ranges, unsigned minLengthForLineBreak, SubtitleTarget target)
1127 {
1128 	SubtitleCompositeActionExecutor executor(this, i18n("Break Lines"));
1129 
1130 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1131 		it.current()->breakText(minLengthForLineBreak, target);
1132 }
1133 
1134 void
unbreakTexts(const RangeList & ranges,SubtitleTarget target)1135 Subtitle::unbreakTexts(const RangeList &ranges, SubtitleTarget target)
1136 {
1137 	SubtitleCompositeActionExecutor executor(this, i18n("Unbreak Lines"));
1138 
1139 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1140 		it.current()->unbreakText(target);
1141 }
1142 
1143 void
simplifyTextWhiteSpace(const RangeList & ranges,SubtitleTarget target)1144 Subtitle::simplifyTextWhiteSpace(const RangeList &ranges, SubtitleTarget target)
1145 {
1146 	SubtitleCompositeActionExecutor executor(this, i18n("Simplify Spaces"));
1147 
1148 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1149 		it.current()->simplifyTextWhiteSpace(target);
1150 }
1151 
1152 void
syncWithSubtitle(const Subtitle & refSubtitle)1153 Subtitle::syncWithSubtitle(const Subtitle &refSubtitle)
1154 {
1155 	beginCompositeAction(i18n("Synchronize Subtitles"));
1156 
1157 	for(SubtitleIterator it(*this, Range::full()), refIt(refSubtitle, Range::full()); it.current() && refIt.current(); ++it, ++refIt)
1158 		processAction(new SetLineTimesAction(it.current(), refIt.current()->showTime(), refIt.current()->hideTime()));
1159 
1160 	sortLines(Range::full());
1161 
1162 	endCompositeAction();
1163 }
1164 
1165 void
appendSubtitle(const Subtitle & srcSubtitle,double shiftMsecsBeforeAppend)1166 Subtitle::appendSubtitle(const Subtitle &srcSubtitle, double shiftMsecsBeforeAppend)
1167 {
1168 	if(!srcSubtitle.count())
1169 		return;
1170 
1171 	QList<SubtitleLine *> lines;
1172 	for(SubtitleIterator it(srcSubtitle); it.current(); ++it) {
1173 		SubtitleLine *ln = it.current();
1174 		SubtitleLine *newLine = new SubtitleLine(ln->showTime() + shiftMsecsBeforeAppend, ln->hideTime() + shiftMsecsBeforeAppend);
1175 		newLine->primaryDoc()->setDocument(ln->primaryDoc());
1176 		newLine->secondaryDoc()->setDocument(ln->secondaryDoc());
1177 		lines.append(newLine);
1178 	}
1179 
1180 	beginCompositeAction(i18n("Join Subtitles"));
1181 
1182 	processAction(new InsertLinesAction(this, lines));
1183 
1184 	endCompositeAction();
1185 }
1186 
1187 void
splitSubtitle(Subtitle & dstSubtitle,const Time & splitTime,bool shiftSplitLines)1188 Subtitle::splitSubtitle(Subtitle &dstSubtitle, const Time &splitTime, bool shiftSplitLines)
1189 {
1190 	if(!m_lines.count())
1191 		return;
1192 
1193 	int splitIndex = -1; // the index of the first line to move (or copy) to dstSub
1194 	bool splitsLine = false; // splitTime falls in within a line's time
1195 	const double shiftTime = shiftSplitLines ? -splitTime.toMillis() : 0.;
1196 	const double dstSplitTime = splitTime.toMillis() + shiftTime;
1197 
1198 	QList<SubtitleLine *> lines;
1199 	for(SubtitleIterator it(*this, Range::full()); it.current(); ++it) {
1200 		if(splitTime <= it.current()->hideTime()) {
1201 			SubtitleLine *ln = it.current();
1202 			double newShowTime = ln->showTime().toMillis() + shiftTime;
1203 
1204 			if(splitIndex < 0) { // first line of the new subtitle
1205 				splitIndex = it.index();
1206 				splitsLine = dstSplitTime > newShowTime;
1207 				if(splitsLine)
1208 					newShowTime = dstSplitTime;
1209 			}
1210 
1211 			SubtitleLine *newLine = new SubtitleLine(newShowTime, ln->hideTime() + shiftTime);
1212 			newLine->primaryDoc()->setDocument(ln->primaryDoc());
1213 			newLine->secondaryDoc()->setDocument(ln->secondaryDoc());
1214 			if(ln->m_formatData)
1215 				newLine->m_formatData = new FormatData(*ln->m_formatData);
1216 
1217 			lines.append(newLine);
1218 		}
1219 	}
1220 
1221 	if(splitIndex > 0 || (splitIndex == 0 && splitsLine)) {
1222 		dstSubtitle.m_formatData = m_formatData ? new FormatData(*m_formatData) : 0;
1223 
1224 		dstSubtitle.beginCompositeAction(i18n("Split Subtitles"));
1225 		if(dstSubtitle.count())
1226 			dstSubtitle.processAction(new RemoveLinesAction(&dstSubtitle, 0, -1));
1227 		dstSubtitle.processAction(new InsertLinesAction(&dstSubtitle, lines, 0));
1228 		dstSubtitle.endCompositeAction();
1229 
1230 		beginCompositeAction(i18n("Split Subtitles"));
1231 		if(splitsLine) {
1232 			at(splitIndex)->setHideTime(splitTime);
1233 			splitIndex++;
1234 			if(splitIndex < count())
1235 				processAction(new RemoveLinesAction(this, splitIndex));
1236 		} else
1237 			processAction(new RemoveLinesAction(this, splitIndex));
1238 		endCompositeAction();
1239 	}
1240 }
1241 
1242 void
toggleStyleFlag(const RangeList & ranges,SString::StyleFlag styleFlag)1243 Subtitle::toggleStyleFlag(const RangeList &ranges, SString::StyleFlag styleFlag)
1244 {
1245 	SubtitleIterator it(*this, ranges);
1246 	if(!it.current())
1247 		return;
1248 
1249 	beginCompositeAction(i18n("Toggle Lines Style"));
1250 
1251 	QTextCharFormat fmtPri, fmtSec;
1252 	switch(styleFlag) {
1253 	case SString::Bold:
1254 		fmtPri.setFontWeight(QTextCursor(it.current()->primaryDoc()).charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
1255 		fmtSec.setFontWeight(QTextCursor(it.current()->secondaryDoc()).charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
1256 		break;
1257 	case SString::Italic:
1258 		fmtPri.setFontItalic(!QTextCursor(it.current()->primaryDoc()).charFormat().fontItalic());
1259 		fmtSec.setFontItalic(!QTextCursor(it.current()->secondaryDoc()).charFormat().fontItalic());
1260 		break;
1261 	case SString::Underline:
1262 		fmtPri.setFontUnderline(!QTextCursor(it.current()->primaryDoc()).charFormat().fontUnderline());
1263 		fmtSec.setFontUnderline(!QTextCursor(it.current()->secondaryDoc()).charFormat().fontUnderline());
1264 		break;
1265 	case SString::StrikeThrough:
1266 		fmtPri.setFontStrikeOut(!QTextCursor(it.current()->primaryDoc()).charFormat().fontStrikeOut());
1267 		fmtSec.setFontStrikeOut(!QTextCursor(it.current()->secondaryDoc()).charFormat().fontStrikeOut());
1268 		break;
1269 	default:
1270 		Q_ASSERT_X(false, "Subtitle::toggleStyleFlag", "Unsupported format");
1271 		break;
1272 	}
1273 
1274 	for(; it.current(); ++it) {
1275 		QTextCursor cp(it.current()->primaryDoc());
1276 		cp.select(QTextCursor::Document);
1277 		cp.mergeCharFormat(fmtPri);
1278 		QTextCursor cs(it.current()->secondaryDoc());
1279 		cs.select(QTextCursor::Document);
1280 		cs.mergeCharFormat(fmtPri);
1281 	}
1282 
1283 	endCompositeAction();
1284 }
1285 
1286 void
changeTextColor(const RangeList & ranges,QRgb color)1287 Subtitle::changeTextColor(const RangeList &ranges, QRgb color)
1288 {
1289 	SubtitleIterator it(*this, ranges);
1290 	if(!it.current())
1291 		return;
1292 
1293 	beginCompositeAction(i18n("Change Lines Text Color"));
1294 
1295 	QTextCharFormat fmt;
1296 	fmt.setForeground(color ? QBrush(QColor(color)) : QBrush());
1297 
1298 	for(; it.current(); ++it) {
1299 		QTextCursor cp(it.current()->primaryDoc());
1300 		cp.select(QTextCursor::Document);
1301 		cp.mergeCharFormat(fmt);
1302 		QTextCursor cs(it.current()->secondaryDoc());
1303 		cs.select(QTextCursor::Document);
1304 		cs.mergeCharFormat(fmt);
1305 	}
1306 
1307 	endCompositeAction();
1308 }
1309 
1310 void
setMarked(const RangeList & ranges,bool value)1311 Subtitle::setMarked(const RangeList &ranges, bool value)
1312 {
1313 	beginCompositeAction(value ? i18n("Set Lines Mark") : i18n("Clear Lines Mark"));
1314 
1315 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1316 		it.current()->setErrorFlags(SubtitleLine::UserMark, value);
1317 
1318 	endCompositeAction();
1319 }
1320 
1321 void
toggleMarked(const RangeList & ranges)1322 Subtitle::toggleMarked(const RangeList &ranges)
1323 {
1324 	SubtitleIterator it(*this, ranges);
1325 	if(!it.current())
1326 		return;
1327 
1328 	beginCompositeAction(i18n("Toggle Lines Mark"));
1329 
1330 	setMarked(ranges, !(it.current()->errorFlags() & SubtitleLine::UserMark));
1331 
1332 	endCompositeAction();
1333 }
1334 
1335 void
clearErrors(const RangeList & ranges,int errorFlags)1336 Subtitle::clearErrors(const RangeList &ranges, int errorFlags)
1337 {
1338 	beginCompositeAction(i18n("Clear Line Errors"));
1339 
1340 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1341 		it.current()->setErrorFlags(errorFlags, false);
1342 
1343 	endCompositeAction();
1344 }
1345 
1346 void
checkErrors(const RangeList & ranges,int errorFlags)1347 Subtitle::checkErrors(const RangeList &ranges, int errorFlags)
1348 {
1349 	beginCompositeAction(i18n("Check Lines Errors"));
1350 
1351 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1352 		it.current()->check(errorFlags);
1353 
1354 	endCompositeAction();
1355 }
1356 
1357 void
recheckErrors(const RangeList & ranges)1358 Subtitle::recheckErrors(const RangeList &ranges)
1359 {
1360 	beginCompositeAction(i18n("Check Lines Errors"));
1361 
1362 	for(SubtitleIterator it(*this, ranges); it.current(); ++it)
1363 		it.current()->check(it.current()->errorFlags());
1364 
1365 	endCompositeAction();
1366 }
1367 
1368 void
processAction(UndoAction * action) const1369 Subtitle::processAction(UndoAction *action) const
1370 {
1371 	if(app()->subtitle() == this)
1372 		app()->undoStack()->push(action);
1373 	else
1374 		action->redo();
1375 }
1376 
1377 void
beginCompositeAction(const QString & title) const1378 Subtitle::beginCompositeAction(const QString &title) const
1379 {
1380 	if(app()->subtitle() == this)
1381 		app()->undoStack()->beginMacro(title);
1382 }
1383 
1384 void
endCompositeAction(UndoStack::DirtyMode dirtyOverride) const1385 Subtitle::endCompositeAction(UndoStack::DirtyMode dirtyOverride) const
1386 {
1387 	if(app()->subtitle() == this)
1388 		app()->undoStack()->endMacro(dirtyOverride);
1389 }
1390 
1391 bool
isPrimaryDirty(int index) const1392 Subtitle::isPrimaryDirty(int index) const
1393 {
1394 	const UndoStack *undoStack = app()->undoStack();
1395 
1396 	int i = m_primaryCleanIndex;
1397 	const int d = i > index ? -1 : 1;
1398 	for(;;) {
1399 		if(i < 0)
1400 			return m_primaryCleanIndex >= 0;
1401 		const UndoStack::DirtyMode dirtyMode = i > 0 ? undoStack->dirtyMode(i - 1) : UndoStack::None;
1402 		if(i != m_primaryCleanIndex && (dirtyMode & UndoStack::Primary))
1403 			return true;
1404 		if(i == index)
1405 			return false;
1406 		i += d;
1407 	}
1408 }
1409 
1410 bool
isSecondaryDirty(int index) const1411 Subtitle::isSecondaryDirty(int index) const
1412 {
1413 	const UndoStack *undoStack = app()->undoStack();
1414 
1415 	int i = m_secondaryCleanIndex;
1416 	const int d = i > index ? -1 : 1;
1417 	for(;;) {
1418 		if(i < 0)
1419 			return m_secondaryCleanIndex >= 0;
1420 		const UndoStack::DirtyMode dirtyMode = i > 0 ? undoStack->dirtyMode(i - 1) : UndoStack::None;
1421 		if(i != m_secondaryCleanIndex && (dirtyMode & UndoStack::Secondary))
1422 			return true;
1423 		if(i == index)
1424 			return false;
1425 		i += d;
1426 	}
1427 }
1428 
1429 void
updateState()1430 Subtitle::updateState()
1431 {
1432 	const UndoStack *undoStack = app()->undoStack();
1433 	const int index = undoStack->index();
1434 	const UndoStack::DirtyMode dirtyMode = index > 0 ? undoStack->dirtyMode(index - 1) : UndoStack::Both;
1435 
1436 	if(m_primaryDirtyState != isPrimaryDirty(index)) {
1437 		m_primaryDirtyState = !m_primaryDirtyState;
1438 		emit primaryDirtyStateChanged(m_primaryDirtyState);
1439 	}
1440 	if(dirtyMode & UndoStack::Primary)
1441 		emit primaryChanged();
1442 
1443 	if(m_secondaryDirtyState != isSecondaryDirty(index)) {
1444 		m_secondaryDirtyState = !m_secondaryDirtyState;
1445 		emit secondaryDirtyStateChanged(m_secondaryDirtyState);
1446 	}
1447 	if(dirtyMode & UndoStack::Secondary)
1448 		emit secondaryChanged();
1449 }
1450 
1451 /// SUBTITLECOMPOSITEACTIONEXECUTOR
1452 
SubtitleCompositeActionExecutor(const Subtitle * subtitle,const QString & title)1453 SubtitleCompositeActionExecutor::SubtitleCompositeActionExecutor(const Subtitle *subtitle, const QString &title)
1454 	: m_subtitle(subtitle)
1455 {
1456 	m_subtitle->beginCompositeAction(title);
1457 }
1458 
~SubtitleCompositeActionExecutor()1459 SubtitleCompositeActionExecutor::~SubtitleCompositeActionExecutor()
1460 {
1461 	m_subtitle->endCompositeAction();
1462 }
1463