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