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