1 /*
2 SPDX-FileCopyrightText: 2020 Jean-Baptiste Mardelle
3 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5
6 #include "textbasededit.h"
7 #include "bin/bin.h"
8 #include "bin/projectclip.h"
9 #include "bin/projectitemmodel.h"
10 #include "bin/projectsubclip.h"
11 #include "core.h"
12 #include "kdenlivesettings.h"
13 #include "mainwindow.h"
14 #include "monitor/monitor.h"
15 #include "timecodedisplay.h"
16 #include "timeline2/view/timelinecontroller.h"
17 #include "timeline2/view/timelinewidget.h"
18 #include <memory>
19 #include <profiles/profilemodel.hpp>
20
21 #include "klocalizedstring.h"
22
23 #include <QEvent>
24 #include <QKeyEvent>
25 #include <QToolButton>
26 #include <KMessageBox>
27 #include <KUrlRequesterDialog>
28
VideoTextEdit(QWidget * parent)29 VideoTextEdit::VideoTextEdit(QWidget *parent)
30 : QTextEdit(parent)
31 {
32 setMouseTracking(true);
33 setReadOnly(true);
34 //setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
35 lineNumberArea = new LineNumberArea(this);
36 connect(this, &VideoTextEdit::cursorPositionChanged, [this]() {
37 lineNumberArea->update();
38 });
39 connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this]() {
40 lineNumberArea->update();
41 });
42 QRect rect = this->contentsRect();
43 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
44 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
45
46 bookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add bookmark"), this);
47 bookmarkAction->setEnabled(false);
48 deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete selection"), this);
49 deleteAction->setEnabled(false);
50 }
51
repaintLines()52 void VideoTextEdit::repaintLines()
53 {
54 lineNumberArea->update();
55 }
56
cleanup()57 void VideoTextEdit::cleanup()
58 {
59 speechZones.clear();
60 cutZones.clear();
61 m_hoveredBlock = -1;
62 clear();
63 document()->setDefaultStyleSheet(QString("body {font-size:%2px;}\na { text-decoration:none;color:%1;font-size:%2px;}").arg(palette().text().color().name()).arg(QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pixelSize()));
64 }
65
selectionStartAnchor(QTextCursor & cursor,int start,int max)66 const QString VideoTextEdit::selectionStartAnchor(QTextCursor &cursor, int start, int max)
67 {
68 if (start == -1) {
69 start = cursor.selectionStart();
70 }
71 if (max == -1) {
72 max = cursor.selectionEnd();
73 }
74 cursor.setPosition(start);
75 cursor.select(QTextCursor::WordUnderCursor);
76 while (cursor.selectedText().isEmpty() && start < max) {
77 start++;
78 cursor.setPosition(start);
79 cursor.select(QTextCursor::WordUnderCursor);
80 }
81 int selStart = cursor.selectionStart();
82 int selEnd = cursor.selectionEnd();
83 cursor.setPosition(selStart + (selEnd - selStart) / 2);
84 return anchorAt(cursorRect(cursor).center());
85 }
86
selectionEndAnchor(QTextCursor & cursor,int end,int min)87 const QString VideoTextEdit::selectionEndAnchor(QTextCursor &cursor, int end, int min)
88 {
89 qDebug()<<"==== TESTING SELECTION END ANCHOR FROM: "<<end<<" , MIN: "<<min;
90 if (end == -1) {
91 end = cursor.selectionEnd();
92 }
93 if (min == -1) {
94 min = cursor.selectionStart();
95 }
96 cursor.setPosition(end);
97 cursor.select(QTextCursor::WordUnderCursor);
98 while (cursor.selectedText().isEmpty() && end > min) {
99 end--;
100 cursor.setPosition(end);
101 cursor.select(QTextCursor::WordUnderCursor);
102 }
103 qDebug()<<"==== TESTING SELECTION END ANCHOR FROM: "<<end<<" , WORD: "<<cursor.selectedText();
104 int selStart = cursor.selectionStart();
105 int selEnd = cursor.selectionEnd();
106 cursor.setPosition(selStart + (selEnd - selStart) / 2);
107 qDebug()<<"==== END POS SELECTION FOR: "<<cursor.selectedText()<<" = "<<anchorAt(cursorRect(cursor).center());
108 QString anch = anchorAt(cursorRect(cursor).center());
109 double endMs = anch.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
110 qDebug()<<"==== GOT LAST FRAME: "<<GenTime(endMs).frames(25);
111 return anchorAt(cursorRect(cursor).center());
112 }
113
processCutZones(QList<QPoint> loadZones)114 void VideoTextEdit::processCutZones(QList <QPoint> loadZones)
115 {
116 // Remove all outside load zones
117 qDebug()<<"=== LOADING CUT ZONES: "<<loadZones<<"\n........................";
118 QTextCursor curs = textCursor();
119 curs.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
120 qDebug()<<"===== GOT DOCUMENT END: "<<curs.position();
121 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
122 double fps = pCore->getCurrentFps();
123 while (!curs.atEnd()) {
124 qDebug()<<"=== CURSOR POS: "<<curs.position();
125 QString anchorStart = selectionStartAnchor(curs, curs.position(), document()->characterCount());
126 int startPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble()).frames(fps);
127 int endPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble()).frames(fps);
128 bool isInZones = false;
129 for (auto &p : loadZones) {
130 if ((startPos >= p.x() && startPos <= p.y()) || (endPos >= p.x() && endPos <= p.y())) {
131 isInZones = true;
132 break;
133 }
134 }
135 if (!isInZones) {
136 // Delete current word
137 qDebug()<<"=== DELETING WORD: "<<curs.selectedText();
138 curs.select(QTextCursor::WordUnderCursor);
139 curs.removeSelectedText();
140 if (document()->characterAt(curs.position() - 1) == QLatin1Char(' ')) {
141 // Remove trailing space
142 curs.deleteChar();
143 } else {
144 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
145 break;
146 }
147 }
148 } else {
149 curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor);
150 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
151 break;
152 }
153 qDebug()<<"=== WORD INSIDE, POS: "<<curs.position();
154 }
155 qDebug()<<"=== MOVED CURSOR POS: "<<curs.position();
156 }
157 }
158
rebuildZones()159 void VideoTextEdit::rebuildZones()
160 {
161 speechZones.clear();
162 m_selectedBlocks.clear();
163 QTextCursor curs = textCursor();
164 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
165 for (int i = 0; i < document()->blockCount(); ++i) {
166 int start = curs.position() + 1;
167 QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount());
168 //qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position();
169 curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
170 int end = curs.position() - 1;
171 QString anchorEnd = selectionEndAnchor(curs, end, start);
172 qDebug()<<"=== ANCHORAs FOR : "<<i<<", "<<anchorStart<<"-"<<anchorEnd<<" AT POS: "<<curs.position();
173 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
174 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
175 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
176 speechZones << QPair<double, double>(startMs, endMs);
177 }
178 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
179 }
180 repaintLines();
181 }
182
lineNumberAreaWidth()183 int VideoTextEdit::lineNumberAreaWidth()
184 {
185 int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * 11;
186 return space;
187 }
188
processedZones(QVector<QPoint> sourceZones)189 QVector<QPoint> VideoTextEdit::processedZones(QVector<QPoint> sourceZones)
190 {
191 QVector<QPoint> resultZones = sourceZones;
192 for (auto &cut : cutZones) {
193 QVector<QPoint> processingZones = resultZones;
194 resultZones.clear();
195 for (auto &zone : processingZones) {
196 if (cut.x() > zone.x()) {
197 if (cut.x() > zone.y()) {
198 // Cut is outside zone, keep it as is
199 resultZones << zone;
200 continue;
201 }
202 // Cut is inside zone
203 if (cut.y() > zone.y()) {
204 // Only keep the start of this zone
205 resultZones << QPoint(zone.x(), cut.x());
206 } else {
207 // Cut is in the middle of this zone
208 resultZones << QPoint(zone.x(), cut.x());
209 resultZones << QPoint(cut.y(), zone.y());
210 }
211 } else if (cut.y() < zone.y()) {
212 // Only keep the end of this zone
213 resultZones << QPoint(cut.y(), zone.y());
214 }
215 }
216 }
217 qDebug()<<"=== FINAL CUTS: "<<resultZones;
218 return resultZones;
219 }
220
getInsertZones()221 QVector<QPoint> VideoTextEdit::getInsertZones()
222 {
223 if (m_selectedBlocks.isEmpty()) {
224 // return text selection, not blocks
225 QTextCursor cursor = textCursor();
226 QString anchorStart;
227 QString anchorEnd;
228 if (!cursor.selectedText().isEmpty()) {
229 qDebug()<<"=== EXPORTING SELECTION";
230 int start = cursor.selectionStart();
231 int end = cursor.selectionEnd() - 1;
232 anchorStart = selectionStartAnchor(cursor, start, end);
233 anchorEnd = selectionEndAnchor(cursor, end, start);
234 } else {
235 // Return full text
236 cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
237 int end = cursor.position() - 1;
238 cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
239 int start = cursor.position();
240 anchorStart = selectionStartAnchor(cursor, start, end);
241 anchorEnd = selectionEndAnchor(cursor, end, start);
242 }
243 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
244 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
245 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
246 qDebug()<<"=== GOT EXPORT MAIN ZONE: "<<GenTime(startMs).frames(pCore->getCurrentFps())<<" - "<<GenTime(endMs).frames(pCore->getCurrentFps());
247 QPoint originalZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())));
248 return processedZones({originalZone});
249 }
250 return {};
251 }
252 QVector<QPoint> zones;
253 int zoneStart = -1;
254 int zoneEnd = -1;
255 int currentEnd = -1;
256 int currentStart = -1;
257 qDebug()<<"=== FROM BLOCKS: "<<m_selectedBlocks;
258 for (auto &bk : m_selectedBlocks) {
259 QPair<double, double> z = speechZones.at(bk);
260 currentStart = GenTime(z.first).frames(pCore->getCurrentFps());
261 currentEnd = GenTime(z.second).frames(pCore->getCurrentFps());
262 if (zoneStart < 0) {
263 zoneStart = currentStart;
264 } else if (currentStart - zoneEnd > 1) {
265 // Insert last zone
266 zones << QPoint(zoneStart, zoneEnd);
267 zoneStart = currentStart;
268 }
269 zoneEnd = currentEnd;
270 }
271 qDebug()<<"=== INSERT LAST: "<<currentStart<<"-"<<currentEnd;
272 zones << QPoint(currentStart, currentEnd);
273
274 qDebug()<<"=== GOT RESULTING ZONES: "<<zones;
275 return processedZones(zones);
276 }
277
updateLineNumberArea(const QRect & rect,int dy)278 void VideoTextEdit::updateLineNumberArea(const QRect &rect, int dy)
279 {
280 if (dy)
281 lineNumberArea->scroll(0, dy);
282 else
283 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
284 }
285
resizeEvent(QResizeEvent * e)286 void VideoTextEdit::resizeEvent(QResizeEvent *e)
287 {
288 QTextEdit::resizeEvent(e);
289 QRect cr = contentsRect();
290 lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
291 }
292
keyPressEvent(QKeyEvent * e)293 void VideoTextEdit::keyPressEvent(QKeyEvent *e)
294 {
295 QTextEdit::keyPressEvent(e);
296 }
297
checkHoverBlock(int yPos)298 void VideoTextEdit::checkHoverBlock(int yPos)
299 {
300 QTextCursor curs = QTextCursor(this->document());
301 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
302
303 m_hoveredBlock = -1;
304 for (int i = 0; i < this->document()->blockCount(); ++i) {
305 QTextBlock block = curs.block();
306 QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(
307 0, 0 - (
308 this->verticalScrollBar()->sliderPosition()
309 ) ).toRect();
310 if (yPos < r2.x()) {
311 break;
312 }
313 if (yPos > r2.x() && yPos < r2.bottom()) {
314 m_hoveredBlock = i;
315 break;
316 }
317 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
318 }
319 setCursor(m_hoveredBlock == -1 ? Qt::ArrowCursor : Qt::PointingHandCursor);
320 lineNumberArea->update();
321 }
322
blockClicked(Qt::KeyboardModifiers modifiers,bool play)323 void VideoTextEdit::blockClicked(Qt::KeyboardModifiers modifiers, bool play)
324 {
325 if (m_hoveredBlock > -1 && m_hoveredBlock < speechZones.count()) {
326 if (m_selectedBlocks.contains(m_hoveredBlock)) {
327 if (modifiers & Qt::ControlModifier) {
328 // remove from selection on ctrl+click an already selected block
329 m_selectedBlocks.removeAll(m_hoveredBlock);
330 } else {
331 m_selectedBlocks = {m_hoveredBlock};
332 lineNumberArea->update();
333 }
334 } else {
335 // Add to selection
336 if (modifiers & Qt::ControlModifier) {
337 m_selectedBlocks << m_hoveredBlock;
338 } else if (modifiers & Qt::ShiftModifier) {
339 if (m_lastClickedBlock > -1) {
340 for (int i = qMin(m_lastClickedBlock, m_hoveredBlock); i <= qMax(m_lastClickedBlock, m_hoveredBlock); i++) {
341 if (!m_selectedBlocks.contains(i)) {
342 m_selectedBlocks << i;
343 }
344 }
345 } else {
346 m_selectedBlocks = {m_hoveredBlock};
347 }
348 } else {
349 m_selectedBlocks = {m_hoveredBlock};
350 }
351 }
352 if (m_hoveredBlock >= 0) {
353 m_lastClickedBlock = m_hoveredBlock;
354 }
355 QPair<double, double> zone = speechZones.at(m_hoveredBlock);
356 double startMs = zone.first;
357 double endMs = zone.second;
358 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
359 pCore->getMonitor(Kdenlive::ClipMonitor)->slotLoadClipZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())));
360 QTextCursor cursor = textCursor();
361 cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
362 cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, m_hoveredBlock);
363 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
364 setTextCursor(cursor);
365 if (play) {
366 pCore->getMonitor(Kdenlive::ClipMonitor)->slotPlayZone();
367 }
368 }
369 }
370
getFirstVisibleBlockId()371 int VideoTextEdit::getFirstVisibleBlockId()
372 {
373 // Detect the first block for which bounding rect - once
374 // translated in absolute coordinates - is contained
375 // by the editor's text area
376
377 // Costly way of doing but since
378 // "blockBoundingGeometry(...)" doesn't exist
379 // for "QTextEdit"...
380
381 QTextCursor curs = QTextCursor(this->document());
382 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
383 for(int i=0; i < this->document()->blockCount(); ++i)
384 {
385 QTextBlock block = curs.block();
386
387 QRect r1 = this->viewport()->geometry();
388 QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(
389 r1.x(), r1.y() - (
390 this->verticalScrollBar()->sliderPosition()
391 ) ).toRect();
392
393 if (r1.contains(r2, true)) { return i; }
394
395 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
396 }
397 return 0;
398 }
399
lineNumberAreaPaintEvent(QPaintEvent * event)400 void VideoTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
401 {
402 this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition());
403
404 QPainter painter(lineNumberArea);
405 painter.fillRect(event->rect(), palette().alternateBase().color());
406 int blockNumber = this->getFirstVisibleBlockId();
407
408 QTextBlock block = this->document()->findBlockByNumber(blockNumber);
409 QTextBlock prev_block = (blockNumber > 0) ? this->document()->findBlockByNumber(blockNumber-1) : block;
410 int translate_y = (blockNumber > 0) ? -this->verticalScrollBar()->sliderPosition() : 0;
411
412 int top = this->viewport()->geometry().top();
413
414 // Adjust text position according to the previous "non entirely visible" block
415 // if applicable. Also takes in consideration the document's margin offset.
416 int additional_margin;
417 if (blockNumber == 0)
418 // Simply adjust to document's margin
419 additional_margin = int(this->document()->documentMargin()) -1 - this->verticalScrollBar()->sliderPosition();
420 else
421 // Getting the height of the visible part of the previous "non entirely visible" block
422 additional_margin = int(this->document()->documentLayout()->blockBoundingRect(prev_block)
423 .translated(0, translate_y).intersected(this->viewport()->geometry()).height());
424
425 // Shift the starting point
426 top += additional_margin;
427
428 int bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
429
430 QColor col_2 = palette().link().color();
431 QColor col_1 = palette().highlightedText().color();
432 QColor col_0 = palette().text().color();
433
434 // Draw the numbers (displaying the current line number in green)
435 while (block.isValid() && top <= event->rect().bottom()) {
436 if (blockNumber >= speechZones.count()) {
437 break;
438 }
439 if (block.isVisible() && bottom >= event->rect().top()) {
440 if (m_selectedBlocks.contains(blockNumber)) {
441 painter.fillRect(QRect(0, top, lineNumberArea->width(), bottom - top), palette().highlight().color());
442 }
443 QString number = pCore->timecode().getDisplayTimecode(GenTime(speechZones[blockNumber].first), false);
444 painter.setPen(QColor(120, 120, 120));
445 painter.setPen((this->textCursor().blockNumber() == blockNumber) ? col_2 : m_selectedBlocks.contains(blockNumber) ? col_1 : col_0);
446 painter.drawText(-5, top,
447 lineNumberArea->width(), fontMetrics().height(),
448 Qt::AlignRight, number);
449 }
450
451 block = block.next();
452 top = bottom;
453 bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
454 ++blockNumber;
455 }
456
457 }
458
contextMenuEvent(QContextMenuEvent * event)459 void VideoTextEdit::contextMenuEvent(QContextMenuEvent *event)
460 {
461 QMenu *menu = createStandardContextMenu();
462 menu->addAction(bookmarkAction);
463 menu->addAction(deleteAction);
464 menu->exec(event->globalPos());
465 delete menu;
466 }
467
mousePressEvent(QMouseEvent * e)468 void VideoTextEdit::mousePressEvent(QMouseEvent *e)
469 {
470 if (e->buttons() & Qt::LeftButton) {
471 QTextCursor current = textCursor();
472 QTextCursor cursor = cursorForPosition(e->pos());
473 int pos = cursor.position();
474 qDebug()<<"=== CLICKED AT: "<<pos<<", SEL: "<<current.selectionStart()<<"-"<<current.selectionEnd();
475 if (pos > current.selectionStart() && pos < current.selectionEnd()) {
476 // Clicked in selection
477 e->ignore();
478 qDebug()<<"=== IGNORING MOUSE CLICK";
479 return;
480 } else {
481 QTextEdit::mousePressEvent(e);
482 const QString link = anchorAt(e->pos());
483 if (!link.isEmpty()) {
484 // Clicked on a word
485 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
486 double startMs = link.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
487 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
488 }
489 }
490 setTextCursor(cursor);
491 } else {
492 QTextEdit::mousePressEvent(e);
493 }
494 }
495
mouseReleaseEvent(QMouseEvent * e)496 void VideoTextEdit::mouseReleaseEvent(QMouseEvent *e)
497 {
498 QTextEdit::mouseReleaseEvent(e);
499 if (e->button() == Qt::LeftButton) {
500 QTextCursor cursor = textCursor();
501 if (!cursor.selectedText().isEmpty()) {
502 // We have a selection, ensure full word is selected
503 int pos = cursor.position();
504 int start = cursor.selectionStart();
505 int end = cursor.selectionEnd();
506 if (document()->characterAt(end - 1) == QLatin1Char(' ')) {
507 // Selection already ends with a space
508 return;
509 }
510 QTextBlock bk = cursor.block();
511 if (bk.text().simplified() == i18n("No speech")) {
512 // This is a silence block, select all
513 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
514 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
515 } else {
516 cursor.setPosition(start);
517 cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
518 cursor.setPosition(end, QTextCursor::KeepAnchor);
519 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
520 }
521 pos = cursor.position();
522 if (!cursor.atBlockEnd() && document()->characterAt(pos - 1) != QLatin1Char(' ')) {
523 // Remove trailing space
524 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
525 }
526 setTextCursor(cursor);
527 }
528 if (!m_selectedBlocks.isEmpty()) {
529 m_selectedBlocks.clear();
530 repaintLines();
531 }
532 } else {
533 qDebug()<<"==== NO LEFT CLICK!";
534 }
535 }
536
mouseMoveEvent(QMouseEvent * e)537 void VideoTextEdit::mouseMoveEvent(QMouseEvent *e)
538 {
539 qDebug()<<"==== MOUSE MOVE EVENT!!!";
540 QTextEdit::mouseMoveEvent(e);
541 if (e->buttons() & Qt::LeftButton) {
542 /*QTextCursor cursor = textCursor();
543 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
544 setTextCursor(cursor);*/
545 } else {
546 const QString link = anchorAt(e->pos());
547 viewport()->setCursor(link.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
548 }
549 }
550
TextBasedEdit(QWidget * parent)551 TextBasedEdit::TextBasedEdit(QWidget *parent)
552 : QWidget(parent)
553 {
554 setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
555 setupUi(this);
556 setFocusPolicy(Qt::StrongFocus);
557 m_voskConfig = new QAction(i18n("Configure"), this);
558 connect(m_voskConfig, &QAction::triggered, []() {
559 pCore->window()->slotPreferences(8);
560 });
561
562 // Visual text editor
563 auto *l = new QVBoxLayout;
564 l->setContentsMargins(0, 0, 0, 0);
565 m_visualEditor = new VideoTextEdit(this);
566 m_visualEditor->installEventFilter(this);
567 l->addWidget(m_visualEditor);
568 text_frame->setLayout(l);
569 m_visualEditor->setDocument(&m_document);
570 connect(&m_document, &QTextDocument::blockCountChanged, this, [this](int ct) {
571 m_visualEditor->repaintLines();
572 qDebug()<<"++++++++++++++++++++\n\nGOT BLOCKS: "<<ct<<"\n\n+++++++++++++++++++++";
573 });
574
575 connect(m_visualEditor, &VideoTextEdit::selectionChanged, this, [this]() {
576 bool hasSelection = m_visualEditor->textCursor().selectedText().simplified().isEmpty() == false;
577 m_visualEditor->bookmarkAction->setEnabled(hasSelection);
578 m_visualEditor->deleteAction->setEnabled(hasSelection);
579 button_insert->setEnabled(hasSelection);
580 });
581
582 button_start->setEnabled(false);
583 connect(button_start, &QPushButton::clicked, this, &TextBasedEdit::startRecognition);
584 frame_progress->setVisible(false);
585 button_abort->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
586 connect(button_abort, &QToolButton::clicked, this, [this]() {
587 if (m_speechJob && m_speechJob->state() == QProcess::Running) {
588 m_speechJob->kill();
589 } else if (m_tCodeJob && m_tCodeJob->state() == QProcess::Running) {
590 m_tCodeJob->kill();
591 }
592 });
593 connect(pCore.get(), &Core::voskModelUpdate, this, [&](QStringList models) {
594 language_box->clear();
595 language_box->addItems(models);
596 if (models.isEmpty()) {
597 showMessage(i18n("Please install speech recognition models"), KMessageWidget::Information, m_voskConfig);
598 } else {
599 if (!KdenliveSettings::vosk_text_model().isEmpty() && models.contains(KdenliveSettings::vosk_text_model())) {
600 int ix = language_box->findText(KdenliveSettings::vosk_text_model());
601 if (ix > -1) {
602 language_box->setCurrentIndex(ix);
603 }
604 }
605 }
606 });
607 connect(language_box, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [this]() {
608 KdenliveSettings::setVosk_text_model(language_box->currentText());
609 });
610 info_message->hide();
611
612 m_logAction = new QAction(i18n("Show log"), this);
613 connect(m_logAction, &QAction::triggered, this, [this]() {
614 KMessageBox::sorry(this, m_errorString, i18n("Detailed log"));
615 });
616
617 speech_zone->setChecked(KdenliveSettings::speech_zone());
618 connect(speech_zone, &QCheckBox::stateChanged, [](int state) {
619 KdenliveSettings::setSpeech_zone(state == Qt::Checked);
620 });
621 button_delete->setDefaultAction(m_visualEditor->deleteAction);
622 button_delete->setToolTip(i18n("Delete selected text"));
623 connect(m_visualEditor->deleteAction, &QAction::triggered, this, &TextBasedEdit::deleteItem);
624
625 button_add->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as")));
626 button_add->setToolTip(i18n("Save edited text in a new playlist"));
627 button_add->setEnabled(false);
628 connect(button_add, &QToolButton::clicked, this, [this]() {
629 previewPlaylist();
630 });
631
632 button_bookmark->setDefaultAction(m_visualEditor->bookmarkAction);
633 button_bookmark->setToolTip(i18n("Add bookmark for current selection"));
634 connect(m_visualEditor->bookmarkAction, &QAction::triggered, this, &TextBasedEdit::addBookmark);
635
636 button_insert->setIcon(QIcon::fromTheme(QStringLiteral("timeline-insert")));
637 button_insert->setToolTip(i18n("Insert selected blocks in timeline"));
638 connect(button_insert, &QToolButton::clicked, this, &TextBasedEdit::insertToTimeline);
639 button_insert->setEnabled(false);
640
641 // Message Timer
642 m_hideTimer.setSingleShot(true);
643 m_hideTimer.setInterval(5000);
644 connect(&m_hideTimer, &QTimer::timeout, info_message, &KMessageWidget::animatedHide);
645
646 // Search stuff
647 search_frame->setVisible(false);
648 button_search->setIcon(QIcon::fromTheme(QStringLiteral("edit-find")));
649 search_prev->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
650 search_next->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
651 connect(button_search, &QToolButton::toggled, this, [&](bool toggled) {
652 search_frame->setVisible(toggled);
653 search_line->setFocus();
654 });
655 connect(search_line, &QLineEdit::textChanged, this, [this](const QString &searchText) {
656 QPalette palette = this->palette();
657 QColor col = palette.color(QPalette::Base);
658 if (searchText.length() > 2) {
659 bool found = m_visualEditor->find(searchText);
660 if (found) {
661 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
662 palette.setColor(QPalette::Base,col);
663 QTextCursor cur = m_visualEditor->textCursor();
664 cur.select(QTextCursor::WordUnderCursor);
665 m_visualEditor->setTextCursor(cur);
666 } else {
667 // Loop over, abort
668 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
669 palette.setColor(QPalette::Base,col);
670 }
671 }
672 search_line->setPalette(palette);
673 });
674 connect(search_next, &QToolButton::clicked, this, [this]() {
675 const QString searchText = search_line->text();
676 QPalette palette = this->palette();
677 QColor col = palette.color(QPalette::Base);
678 if (searchText.length() > 2) {
679 bool found = m_visualEditor->find(searchText);
680 if (found) {
681 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
682 palette.setColor(QPalette::Base,col);
683 QTextCursor cur = m_visualEditor->textCursor();
684 cur.select(QTextCursor::WordUnderCursor);
685 m_visualEditor->setTextCursor(cur);
686 } else {
687 // Loop over, abort
688 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
689 palette.setColor(QPalette::Base,col);
690 }
691 }
692 search_line->setPalette(palette);
693 });
694 connect(search_prev, &QToolButton::clicked, this, [this]() {
695 const QString searchText = search_line->text();
696 QPalette palette = this->palette();
697 QColor col = palette.color(QPalette::Base);
698 if (searchText.length() > 2) {
699 bool found = m_visualEditor->find(searchText, QTextDocument::FindBackward);
700 if (found) {
701 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
702 palette.setColor(QPalette::Base,col);
703 QTextCursor cur = m_visualEditor->textCursor();
704 cur.select(QTextCursor::WordUnderCursor);
705 m_visualEditor->setTextCursor(cur);
706 } else {
707 // Loop over, abort
708 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
709 palette.setColor(QPalette::Base,col);
710 }
711 }
712 search_line->setPalette(palette);
713 });
714 parseVoskDictionaries();
715 }
716
~TextBasedEdit()717 TextBasedEdit::~TextBasedEdit()
718 {
719 if (m_speechJob && m_speechJob->state() == QProcess::Running) {
720 m_speechJob->kill();
721 m_speechJob->waitForFinished();
722 }
723 }
724
eventFilter(QObject * obj,QEvent * event)725 bool TextBasedEdit::eventFilter(QObject *obj, QEvent *event)
726 {
727 if (event->type() == QEvent::KeyPress) {
728 qDebug()<<"==== FOT TXTEDIT EVENT FILTER: "<<static_cast <QKeyEvent*> (event)->key();
729 }
730 /*if(obj == m_visualEditor && event->type() == QEvent::KeyPress)
731 {
732 QKeyEvent *keyEvent = static_cast <QKeyEvent*> (event);
733 if (keyEvent->key() != Qt::Key_Left && keyEvent->key() != Qt::Key_Up && keyEvent->key() != Qt::Key_Right && keyEvent->key() != Qt::Key_Down) {
734 parentWidget()->setFocus();
735 return true;
736 }
737 }*/
738 return QObject::eventFilter(obj, event);
739 }
740
startRecognition()741 void TextBasedEdit::startRecognition()
742 {
743 if (m_speechJob && m_speechJob->state() != QProcess::NotRunning) {
744 if (KMessageBox::questionYesNo(this, i18n("Another recognition job is running. Abort it ?")) != KMessageBox::Yes) {
745 return;
746 }
747 }
748 info_message->hide();
749 m_errorString.clear();
750 m_visualEditor->cleanup();
751 //m_visualEditor->insertHtml(QStringLiteral("<body>"));
752 #ifdef Q_OS_WIN
753 QString pyExec = QStandardPaths::findExecutable(QStringLiteral("python"));
754 #else
755 QString pyExec = QStandardPaths::findExecutable(QStringLiteral("python3"));
756 #endif
757 if (pyExec.isEmpty()) {
758 showMessage(i18n("Cannot find python3, please install it on your system."), KMessageWidget::Warning);
759 return;
760 }
761
762 if (!KdenliveSettings::vosk_found()) {
763 showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig);
764 return;
765 }
766 // Start python script
767 QString language = language_box->currentText();
768 if (language.isEmpty()) {
769 showMessage(i18n("Please install a language model."), KMessageWidget::Warning, m_voskConfig);
770 return;
771 }
772 QString speechScript = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("scripts/speechtotext.py"));
773 if (speechScript.isEmpty()) {
774 showMessage(i18n("The speech script was not found, check your install."), KMessageWidget::Warning);
775 return;
776 }
777 m_binId = pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId();
778 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
779 if (clip == nullptr) {
780 showMessage(i18n("Select a clip with audio in Project Bin."), KMessageWidget::Information);
781 return;
782 }
783
784 m_speechJob = std::make_unique<QProcess>(this);
785 showMessage(i18n("Starting speech recognition"), KMessageWidget::Information);
786 qApp->processEvents();
787 QString modelDirectory = KdenliveSettings::vosk_folder_path();
788 if (modelDirectory.isEmpty()) {
789 modelDirectory = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("speechmodels"), QStandardPaths::LocateDirectory);
790 }
791 qDebug()<<"==== ANALYSIS SPEECH: "<<modelDirectory<<" - "<<language;
792
793 m_sourceUrl.clear();
794 QString clipName;
795 m_clipOffset = 0;
796 m_lastPosition = 0;
797 double endPos = 0;
798 bool hasAudio = false;
799 if (clip->itemType() == AbstractProjectItem::ClipItem) {
800 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
801 if (clipItem) {
802 m_sourceUrl = clipItem->url();
803 clipName = clipItem->clipName();
804 hasAudio = clipItem->hasAudio();
805 if (speech_zone->isChecked()) {
806 // Analyse clip zone only
807 QPoint zone = clipItem->zone();
808 m_lastPosition = zone.x();
809 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
810 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
811 endPos = m_clipDuration;
812 } else {
813 m_clipDuration = clipItem->duration().seconds();
814 }
815 }
816 } else if (clip->itemType() == AbstractProjectItem::SubClipItem) {
817 std::shared_ptr<ProjectSubClip> clipItem = std::static_pointer_cast<ProjectSubClip>(clip);
818 if (clipItem) {
819 auto master = clipItem->getMasterClip();
820 m_sourceUrl = master->url();
821 hasAudio = master->hasAudio();
822 clipName = master->clipName();
823 QPoint zone = clipItem->zone();
824 m_lastPosition = zone.x();
825 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
826 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
827 endPos = m_clipDuration;
828 }
829 }
830 if (m_sourceUrl.isEmpty() || !hasAudio) {
831 showMessage(i18n("Select a clip with audio for speech recognition."), KMessageWidget::Information);
832 return;
833 }
834 clipNameLabel->setText(clipName);
835 if (clip->clipType() == ClipType::Playlist) {
836 // We need to extract audio first
837 m_playlistWav.remove();
838 m_playlistWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav")));
839 if (!m_playlistWav.open()) {
840 showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning);
841 return;
842 }
843 m_playlistWav.close();
844
845 showMessage(i18n("Extracting audio for %1.", clipName), KMessageWidget::Information);
846 qApp->processEvents();
847 m_tCodeJob = std::make_unique<QProcess>(this);
848 m_tCodeJob->setProcessChannelMode(QProcess::MergedChannels);
849 connect(m_tCodeJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
850 this, [this, language, pyExec, speechScript, clipName, modelDirectory, endPos](int code, QProcess::ExitStatus status) {
851 Q_UNUSED(code)
852 qDebug()<<"++++++++++++++++++++++ TCODE JOB FINISHED\n";
853 if (status == QProcess::CrashExit) {
854 showMessage(i18n("Audio extract failed."), KMessageWidget::Warning);
855 speech_progress->setValue(0);
856 frame_progress->setVisible(false);
857 m_playlistWav.remove();
858 return;
859 }
860 showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
861 qApp->processEvents();
862 connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
863 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
864 connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, [this](int code, QProcess::ExitStatus status) {
865 m_playlistWav.remove();
866 slotProcessSpeechStatus(code, status);
867 });
868 m_speechJob->start(pyExec, {speechScript, modelDirectory, language, m_playlistWav.fileName(), QString::number(m_clipOffset), QString::number(endPos)});
869 speech_progress->setValue(0);
870 frame_progress->setVisible(true);
871 });
872 connect(m_tCodeJob.get(), &QProcess::readyReadStandardOutput, this, [this]() {
873 QString saveData = QString::fromUtf8(m_tCodeJob->readAllStandardOutput());
874 qDebug()<<"+GOT OUTPUT: "<<saveData;
875 saveData = saveData.section(QStringLiteral("percentage:"), 1).simplified();
876 int percent = saveData.section(QLatin1Char(' '), 0, 0).toInt();
877 speech_progress->setValue(percent);
878 });
879 m_tCodeJob->start(KdenliveSettings::rendererpath(), {QStringLiteral("-progress"), m_sourceUrl, QStringLiteral("-consumer"), QString("avformat:%1").arg(m_playlistWav.fileName()), QStringLiteral("vn=1"), QStringLiteral("ar=16000")});
880 speech_progress->setValue(0);
881 frame_progress->setVisible(true);
882 } else {
883 showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
884 qApp->processEvents();
885 connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
886 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
887 connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &TextBasedEdit::slotProcessSpeechStatus);
888 qDebug()<<"=== STARTING RECO: "<<speechScript<<" / "<<modelDirectory<<" / "<<language<<" / "<<m_sourceUrl<<", START: "<<m_clipOffset<<", DUR: "<<endPos;
889 button_add->setEnabled(false);
890 m_speechJob->start(pyExec, {speechScript, modelDirectory, language, m_sourceUrl, QString::number(m_clipOffset), QString::number(endPos)});
891 speech_progress->setValue(0);
892 frame_progress->setVisible(true);
893 }
894 }
895
slotProcessSpeechStatus(int,QProcess::ExitStatus status)896 void TextBasedEdit::slotProcessSpeechStatus(int, QProcess::ExitStatus status)
897 {
898 if (status == QProcess::CrashExit) {
899 showMessage(i18n("Speech recognition aborted."), KMessageWidget::Warning, m_errorString.isEmpty() ? nullptr : m_logAction);
900 } else if (m_visualEditor->toPlainText().isEmpty()) {
901 if (m_errorString.contains(QStringLiteral("ModuleNotFoundError"))) {
902 showMessage(i18n("Error, please check the speech to text configuration."), KMessageWidget::Warning, m_voskConfig);
903 } else {
904 showMessage(i18n("No speech detected."), KMessageWidget::Information, m_errorString.isEmpty() ? nullptr : m_logAction);
905 }
906 } else {
907 button_add->setEnabled(true);
908 showMessage(i18n("Speech recognition finished."), KMessageWidget::Positive);
909 // Store speech analysis in clip properties
910 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
911 if (clip) {
912 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
913 QString oldSpeech;
914 if (clipItem) {
915 oldSpeech = clipItem->getProducerProperty(QStringLiteral("kdenlive:speech"));
916 }
917 QMap<QString, QString> oldProperties;
918 oldProperties.insert(QStringLiteral("kdenlive:speech"), oldSpeech);
919 QMap<QString, QString> properties;
920 properties.insert(QStringLiteral("kdenlive:speech"), m_visualEditor->toHtml());
921 pCore->bin()->slotEditClipCommand(m_binId, oldProperties, properties);
922 }
923 }
924 QTextCursor cur = m_visualEditor->textCursor();
925 cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
926 m_visualEditor->setTextCursor(cur);
927 frame_progress->setVisible(false);
928 }
929
slotProcessSpeechError()930 void TextBasedEdit::slotProcessSpeechError()
931 {
932 m_errorString.append(QString::fromUtf8(m_speechJob->readAllStandardError()));
933 }
934
slotProcessSpeech()935 void TextBasedEdit::slotProcessSpeech()
936 {
937 QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput());
938 qDebug()<<"=== GOT DATA:\n"<<saveData;
939 QJsonParseError error;
940 auto loadDoc = QJsonDocument::fromJson(saveData.toUtf8(), &error);
941 qDebug()<<"===JSON ERROR: "<<error.errorString();
942 QTextCursor cursor = m_visualEditor->textCursor();
943 if (loadDoc.isObject()) {
944 QJsonObject obj = loadDoc.object();
945 if (!obj.isEmpty()) {
946 //QString itemText = obj["text"].toString();
947 QString htmlLine;
948 QPair <double, double>sentenceZone;
949 if (obj["result"].isArray()) {
950 QJsonArray obj2 = obj["result"].toArray();
951 // Store words with their start/end time
952 foreach (const QJsonValue & v, obj2) {
953 htmlLine.append(QString("<a href=\"%1#%2:%3\">%4</a> ").arg(m_binId).arg(v.toObject().value("start").toDouble() + m_clipOffset).arg(v.toObject().value("end").toDouble() + m_clipOffset).arg(v.toObject().value("word").toString()));
954 }
955 // Get start time for first word
956 QJsonValue val = obj2.first();
957 if (val.isObject() && val.toObject().keys().contains("start")) {
958 double ms = val.toObject().value("start").toDouble() + m_clipOffset;
959 GenTime startPos(ms);
960 sentenceZone.first = ms;
961 if (startPos.frames(pCore->getCurrentFps()) > m_lastPosition + 1) {
962 // Insert space
963 GenTime silenceStart(m_lastPosition, pCore->getCurrentFps());
964 m_visualEditor->moveCursor(QTextCursor::End);
965 QString htmlSpace = QString("<a href=\"#%1:%2\">%3</a>").arg(silenceStart.seconds()).arg(GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()).arg(i18n("No speech"));
966 m_visualEditor->insertHtml(htmlSpace);
967 m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
968 m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds());
969 }
970 val = obj2.last();
971 if (val.isObject() && val.toObject().keys().contains("end")) {
972 double ms = val.toObject().value("end").toDouble() + m_clipOffset;
973 sentenceZone.second = ms;
974 m_lastPosition = GenTime(ms).frames(pCore->getCurrentFps());
975 if (m_clipDuration > 0.) {
976 speech_progress->setValue(static_cast<int>(100 * ms / ( + m_clipOffset + m_clipDuration)));
977 }
978 }
979 }
980 } else {
981 // Last empty object - no speech detected
982 GenTime silenceStart(m_lastPosition + 1, pCore->getCurrentFps());
983 m_visualEditor->moveCursor(QTextCursor::End);
984 QString htmlSpace = QString("<a href=\"#%1:%2\">%3</a>").arg(silenceStart.seconds()).arg(GenTime(m_clipDuration + m_clipOffset).seconds()).arg(i18n("No speech"));
985 m_visualEditor->insertHtml(htmlSpace);
986 m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), GenTime(m_clipDuration + m_clipOffset).seconds());
987 }
988 if (!htmlLine.isEmpty()) {
989 m_visualEditor->insertHtml(htmlLine.simplified());
990 if (sentenceZone.second < m_clipOffset + m_clipDuration) {
991 m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
992 }
993 m_visualEditor->speechZones << sentenceZone;
994 }
995 }
996 } else if (loadDoc.isEmpty()) {
997 qDebug()<<"==== EMPTY OBJECT DOC";
998 }
999 qDebug()<<"==== GOT BLOCKS: "<<m_document.blockCount();
1000 qDebug()<<"=== LINES: "<<m_document.firstBlock().lineCount();
1001 m_visualEditor->repaintLines();
1002 }
1003
parseVoskDictionaries()1004 void TextBasedEdit::parseVoskDictionaries()
1005 {
1006 QString modelDirectory = KdenliveSettings::vosk_folder_path();
1007 QDir dir;
1008 if (modelDirectory.isEmpty()) {
1009 modelDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
1010 dir = QDir(modelDirectory);
1011 if (!dir.cd(QStringLiteral("speechmodels"))) {
1012 qDebug()<<"=== /// CANNOT ACCESS SPEECH DICTIONARIES FOLDER";
1013 emit pCore->voskModelUpdate({});
1014 return;
1015 }
1016 } else {
1017 dir = QDir(modelDirectory);
1018 }
1019 QStringList dicts = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
1020 QStringList final;
1021 for (auto &d : dicts) {
1022 QDir sub(dir.absoluteFilePath(d));
1023 if (sub.exists(QStringLiteral("mfcc.conf")) || (sub.exists(QStringLiteral("conf/mfcc.conf")))) {
1024 final << d;
1025 }
1026 }
1027 emit pCore->voskModelUpdate(final);
1028 }
1029
deleteItem()1030 void TextBasedEdit::deleteItem()
1031 {
1032 QTextCursor cursor = m_visualEditor->textCursor();
1033 int start = cursor.selectionStart();
1034 int end = cursor.selectionEnd();
1035 qDebug()<<"=== CUTTONG: "<<start<<" - "<<end;
1036 if (end > start) {
1037 QString anchorStart = m_visualEditor->selectionStartAnchor(cursor, start, end);
1038 cursor.setPosition(end);
1039 bool blockEnd = cursor.atBlockEnd();
1040 cursor = m_visualEditor->textCursor();
1041 QString anchorEnd = m_visualEditor->selectionEndAnchor(cursor, end, start);
1042 qDebug()<<"=== FINAL END CUT: "<<end;
1043 qDebug()<<"=== GOT END ANCHOR: "<<cursor.selectedText()<<" = "<<anchorEnd;
1044 if (!anchorEnd.isEmpty() && !anchorEnd.isEmpty()) {
1045 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1046 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1047 if (startMs < endMs) {
1048 qDebug()<<"=== GOT CUT ZONE: "<<GenTime(startMs).frames(pCore->getCurrentFps())<<" - "<<GenTime(endMs).frames(pCore->getCurrentFps());
1049 m_visualEditor->cutZones << QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()));
1050 cursor = m_visualEditor->textCursor();
1051 cursor.removeSelectedText();
1052 if (blockEnd) {
1053 cursor.deleteChar();
1054 }
1055 }
1056 }
1057 } else {
1058 QTextCursor curs = m_visualEditor->textCursor();
1059 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
1060 for (int i = 0; i < m_document.blockCount(); ++i) {
1061 int blockStart = curs.position();
1062 curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
1063 int blockEnd = curs.position();
1064 if (blockStart == blockEnd) {
1065 // Empty block, delete
1066 curs.select(QTextCursor::BlockUnderCursor);
1067 curs.removeSelectedText();
1068 curs.deleteChar();
1069 }
1070 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
1071 }
1072 }
1073 // Reset selection and rebuild line numbers
1074 m_visualEditor->rebuildZones();
1075 previewPlaylist(false);
1076 }
1077
insertToTimeline()1078 void TextBasedEdit::insertToTimeline()
1079 {
1080 QVector<QPoint> zones = m_visualEditor->getInsertZones();
1081 if (zones.isEmpty()) {
1082 return;
1083 }
1084 for (auto &zone : zones) {
1085 pCore->window()->getMainTimeline()->controller()->insertZone(m_binId, zone, false);
1086 }
1087 }
1088
previewPlaylist(bool createNew)1089 void TextBasedEdit::previewPlaylist(bool createNew)
1090 {
1091 QVector<QPoint> zones = m_visualEditor->getInsertZones();
1092 if (zones.isEmpty()) {
1093 showMessage(i18n("No text to export"), KMessageWidget::Information);
1094 return;
1095 }
1096 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
1097 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
1098 QString sourcePath = clipItem->url();
1099 QMap<QString, QString> properties;
1100 properties.insert(QStringLiteral("kdenlive:baseid"), m_binId);
1101 QStringList playZones;
1102 for (const auto&p : qAsConst(zones)) {
1103 playZones << QString("%1:%2").arg(p.x()).arg(p.y());
1104 }
1105 properties.insert(QStringLiteral("kdenlive:cutzones"), playZones.join(QLatin1Char(';')));
1106 int ix = 1;
1107 if (createNew) {
1108 m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1109 while (QFile::exists(m_playlist)) {
1110 ix++;
1111 m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1112 }
1113 QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(m_playlist), this, i18n("Enter new playlist path"));
1114 if (url.isEmpty()) {
1115 return;
1116 }
1117 m_playlist = url.toLocalFile();
1118 }
1119 if (!m_playlist.isEmpty()) {
1120 pCore->bin()->savePlaylist(m_binId, m_playlist, zones, properties, createNew);
1121 clipNameLabel->setText(QFileInfo(m_playlist).fileName());
1122 }
1123 }
1124
showMessage(const QString & text,KMessageWidget::MessageType type,QAction * action)1125 void TextBasedEdit::showMessage(const QString &text, KMessageWidget::MessageType type, QAction *action)
1126 {
1127 if (m_currentMessageAction != nullptr && (action == nullptr || action != m_currentMessageAction)) {
1128 info_message->removeAction(m_currentMessageAction);
1129 m_currentMessageAction = action;
1130 if (m_currentMessageAction) {
1131 info_message->addAction(m_currentMessageAction);
1132 }
1133 } else if (action) {
1134 m_currentMessageAction = action;
1135 info_message->addAction(m_currentMessageAction);
1136 }
1137
1138 if (info_message->isVisible()) {
1139 m_hideTimer.stop();
1140 }
1141 info_message->setMessageType(type);
1142 info_message->setText(text);
1143 info_message->animatedShow();
1144 if (type != KMessageWidget::Error && m_currentMessageAction == nullptr) {
1145 m_hideTimer.start();
1146 }
1147 }
1148
openClip(std::shared_ptr<ProjectClip> clip)1149 void TextBasedEdit::openClip(std::shared_ptr<ProjectClip> clip)
1150 {
1151 if (m_speechJob && m_speechJob->state() == QProcess::Running) {
1152 // TODO: ask for job cancelation
1153 return;
1154 }
1155 if (clip && clip->isValid() && clip->hasAudio()) {
1156 QString refId = clip->getProducerProperty(QStringLiteral("kdenlive:baseid"));
1157 if (!refId.isEmpty() && refId == m_refId) {
1158 // We opened a resulting playlist, do not clear text edit
1159 return;
1160 }
1161 m_visualEditor->cleanup();
1162 QString speech;
1163 QList<QPoint> cutZones;
1164 m_binId = refId.isEmpty() ? clip->binId() : refId;
1165 if (!refId.isEmpty()) {
1166 // this is a clip playlist with a bin reference, fetch it
1167 m_refId = refId;
1168 std::shared_ptr<ProjectClip> refClip = pCore->bin()->getBinClip(refId);
1169 if (refClip) {
1170 speech = refClip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1171 clipNameLabel->setText(refClip->clipName());
1172 }
1173 QStringList zones = clip->getProducerProperty("kdenlive:cutzones").split(QLatin1Char(';'));
1174 for (const QString &z : qAsConst(zones)) {
1175 cutZones << QPoint(z.section(QLatin1Char(':'), 0, 0).toInt(), z.section(QLatin1Char(':'), 1, 1).toInt());
1176 }
1177 } else {
1178 m_refId.clear();
1179 speech = clip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1180 clipNameLabel->setText(clip->clipName());
1181 }
1182 m_visualEditor->insertHtml(speech);
1183 if (!cutZones.isEmpty()) {
1184 m_visualEditor->processCutZones(cutZones);
1185 }
1186 m_visualEditor->rebuildZones();
1187 button_add->setEnabled(!speech.isEmpty());
1188 button_start->setEnabled(true);
1189 } else {
1190 button_start->setEnabled(false);
1191 clipNameLabel->clear();
1192 }
1193 }
1194
addBookmark()1195 void TextBasedEdit::addBookmark()
1196 {
1197 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(m_binId);
1198 if (clip) {
1199 QString txt = m_visualEditor->textCursor().selectedText();
1200 QTextCursor cursor = m_visualEditor->textCursor();
1201 QString startAnchor = m_visualEditor->selectionStartAnchor(cursor, -1, -1);
1202 cursor = m_visualEditor->textCursor();
1203 QString endAnchor = m_visualEditor->selectionEndAnchor(cursor, -1, -1);
1204 if (startAnchor.isEmpty()) {
1205 showMessage(i18n("No timecode found in selection"), KMessageWidget::Information);
1206 return;
1207 }
1208 double ms = startAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1209 int startPos = GenTime(ms).frames(pCore->getCurrentFps());
1210 ms = endAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1211 int endPos = GenTime(ms).frames(pCore->getCurrentFps());
1212 int monitorPos = pCore->getMonitor(Kdenlive::ClipMonitor)->position();
1213 qDebug()<<"==== GOT MARKER: "<<txt<<", FOR POS: "<<startPos<<"-"<<endPos<<", MON: "<<monitorPos;
1214 if (monitorPos > startPos && monitorPos < endPos) {
1215 // Monitor seek is on the selection, use the current frame
1216 pCore->bin()->addClipMarker(m_binId, {monitorPos}, {txt});
1217 } else {
1218 pCore->bin()->addClipMarker(m_binId, {startPos}, {txt});
1219 }
1220 } else {
1221 qDebug()<<"==== NO CLIP FOR "<<m_binId;
1222 }
1223 }
1224