File indexing completed on 2022-11-22 14:07:14

0001 /*
0002     SPDX-FileCopyrightText: 2020 Jean-Baptiste Mardelle
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "textbasededit.h"
0007 #include "bin/bin.h"
0008 #include "bin/projectclip.h"
0009 #include "bin/projectitemmodel.h"
0010 #include "bin/projectsubclip.h"
0011 #include "core.h"
0012 #include "kdenlivesettings.h"
0013 #include "mainwindow.h"
0014 #include "monitor/monitor.h"
0015 #include "timeline2/view/timelinecontroller.h"
0016 #include "timeline2/view/timelinewidget.h"
0017 #include "widgets/timecodedisplay.h"
0018 #include <profiles/profilemodel.hpp>
0019 
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 #include <KUrlRequesterDialog>
0023 #include <QAbstractTextDocumentLayout>
0024 #include <QEvent>
0025 #include <QFontDatabase>
0026 #include <QJsonArray>
0027 #include <QJsonObject>
0028 #include <QJsonParseError>
0029 #include <QKeyEvent>
0030 #include <QMenu>
0031 #include <QPainter>
0032 #include <QScrollBar>
0033 #include <QTextBlock>
0034 #include <QToolButton>
0035 
0036 #include <memory>
0037 
0038 VideoTextEdit::VideoTextEdit(QWidget *parent)
0039     : QTextEdit(parent)
0040 {
0041     setMouseTracking(true);
0042     setReadOnly(true);
0043     // setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
0044     lineNumberArea = new LineNumberArea(this);
0045     connect(this, &VideoTextEdit::cursorPositionChanged, [this]() { lineNumberArea->update(); });
0046     connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this]() { lineNumberArea->update(); });
0047     QRect rect = this->contentsRect();
0048     setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
0049     lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
0050 
0051     bookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add bookmark"), this);
0052     bookmarkAction->setEnabled(false);
0053     deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete selection"), this);
0054     deleteAction->setEnabled(false);
0055 }
0056 
0057 void VideoTextEdit::repaintLines()
0058 {
0059     lineNumberArea->update();
0060 }
0061 
0062 void VideoTextEdit::cleanup()
0063 {
0064     speechZones.clear();
0065     cutZones.clear();
0066     m_hoveredBlock = -1;
0067     clear();
0068     document()->setDefaultStyleSheet(QString("a {text-decoration:none;color:%1}").arg(palette().text().color().name()));
0069     setCurrentFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0070 }
0071 
0072 const QString VideoTextEdit::selectionStartAnchor(QTextCursor &cursor, int start, int max)
0073 {
0074     if (start == -1) {
0075         start = cursor.selectionStart();
0076     }
0077     if (max == -1) {
0078         max = cursor.selectionEnd();
0079     }
0080     cursor.setPosition(start);
0081     cursor.select(QTextCursor::WordUnderCursor);
0082     while (cursor.selectedText().isEmpty() && start < max) {
0083         start++;
0084         cursor.setPosition(start);
0085         cursor.select(QTextCursor::WordUnderCursor);
0086     }
0087     int selStart = cursor.selectionStart();
0088     int selEnd = cursor.selectionEnd();
0089     cursor.setPosition(selStart + (selEnd - selStart) / 2);
0090     return anchorAt(cursorRect(cursor).center());
0091 }
0092 
0093 const QString VideoTextEdit::selectionEndAnchor(QTextCursor &cursor, int end, int min)
0094 {
0095     qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , MIN: " << min;
0096     if (end == -1) {
0097         end = cursor.selectionEnd();
0098     }
0099     if (min == -1) {
0100         min = cursor.selectionStart();
0101     }
0102     cursor.setPosition(end);
0103     cursor.select(QTextCursor::WordUnderCursor);
0104     while (cursor.selectedText().isEmpty() && end > min) {
0105         end--;
0106         cursor.setPosition(end);
0107         cursor.select(QTextCursor::WordUnderCursor);
0108     }
0109     qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , WORD: " << cursor.selectedText();
0110     int selStart = cursor.selectionStart();
0111     int selEnd = cursor.selectionEnd();
0112     cursor.setPosition(selStart + (selEnd - selStart) / 2);
0113     qDebug() << "==== END POS SELECTION FOR: " << cursor.selectedText() << " = " << anchorAt(cursorRect(cursor).center());
0114     QString anch = anchorAt(cursorRect(cursor).center());
0115     double endMs = anch.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0116     qDebug() << "==== GOT LAST FRAME: " << GenTime(endMs).frames(25);
0117     return anchorAt(cursorRect(cursor).center());
0118 }
0119 
0120 void VideoTextEdit::processCutZones(const QList<QPoint> &loadZones)
0121 {
0122     // Remove all outside load zones
0123     qDebug() << "=== LOADING CUT ZONES: " << loadZones << "\n........................";
0124     QTextCursor curs = textCursor();
0125     curs.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
0126     qDebug() << "===== GOT DOCUMENT END: " << curs.position();
0127     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0128     double fps = pCore->getCurrentFps();
0129     while (!curs.atEnd()) {
0130         qDebug() << "=== CURSOR POS: " << curs.position();
0131         QString anchorStart = selectionStartAnchor(curs, curs.position(), document()->characterCount());
0132         int startPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble()).frames(fps);
0133         int endPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble()).frames(fps);
0134         bool isInZones = false;
0135         for (auto &p : loadZones) {
0136             if ((startPos >= p.x() && startPos <= p.y()) || (endPos >= p.x() && endPos <= p.y())) {
0137                 isInZones = true;
0138                 break;
0139             }
0140         }
0141         if (!isInZones) {
0142             // Delete current word
0143             qDebug() << "=== DELETING WORD: " << curs.selectedText();
0144             curs.select(QTextCursor::WordUnderCursor);
0145             curs.removeSelectedText();
0146             if (document()->characterAt(curs.position() - 1) == QLatin1Char(' ')) {
0147                 // Remove trailing space
0148                 curs.deleteChar();
0149             } else {
0150                 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
0151                     break;
0152                 }
0153             }
0154         } else {
0155             curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor);
0156             if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
0157                 break;
0158             }
0159             qDebug() << "=== WORD INSIDE, POS: " << curs.position();
0160         }
0161         qDebug() << "=== MOVED CURSOR POS: " << curs.position();
0162     }
0163 }
0164 
0165 void VideoTextEdit::rebuildZones()
0166 {
0167     speechZones.clear();
0168     m_selectedBlocks.clear();
0169     QTextCursor curs = textCursor();
0170     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0171     for (int i = 0; i < document()->blockCount(); ++i) {
0172         int start = curs.position() + 1;
0173         QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount());
0174         // qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position();
0175         curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
0176         int end = curs.position() - 1;
0177         QString anchorEnd = selectionEndAnchor(curs, end, start);
0178         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
0179             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0180             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0181             speechZones << QPair<double, double>(startMs, endMs);
0182         }
0183         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0184     }
0185     repaintLines();
0186 }
0187 
0188 int VideoTextEdit::lineNumberAreaWidth()
0189 {
0190     int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * 11;
0191     return space;
0192 }
0193 
0194 QVector<QPoint> VideoTextEdit::processedZones(const QVector<QPoint> &sourceZones)
0195 {
0196     QVector<QPoint> resultZones = sourceZones;
0197     for (auto &cut : cutZones) {
0198         QVector<QPoint> processingZones = resultZones;
0199         resultZones.clear();
0200         for (auto &zone : processingZones) {
0201             if (cut.x() > zone.x()) {
0202                 if (cut.x() > zone.y()) {
0203                     // Cut is outside zone, keep it as is
0204                     resultZones << zone;
0205                     continue;
0206                 }
0207                 // Cut is inside zone
0208                 if (cut.y() > zone.y()) {
0209                     // Only keep the start of this zone
0210                     resultZones << QPoint(zone.x(), cut.x());
0211                 } else {
0212                     // Cut is in the middle of this zone
0213                     resultZones << QPoint(zone.x(), cut.x());
0214                     resultZones << QPoint(cut.y(), zone.y());
0215                 }
0216             } else if (cut.y() < zone.y()) {
0217                 // Only keep the end of this zone
0218                 resultZones << QPoint(cut.y(), zone.y());
0219             }
0220         }
0221     }
0222     qDebug() << "=== FINAL CUTS: " << resultZones;
0223     return resultZones;
0224 }
0225 
0226 QVector<QPoint> VideoTextEdit::getInsertZones()
0227 {
0228     if (m_selectedBlocks.isEmpty()) {
0229         // return text selection, not blocks
0230         QTextCursor cursor = textCursor();
0231         QString anchorStart;
0232         QString anchorEnd;
0233         if (!cursor.selectedText().isEmpty()) {
0234             qDebug() << "=== EXPORTING SELECTION";
0235             int start = cursor.selectionStart();
0236             int end = cursor.selectionEnd() - 1;
0237             anchorStart = selectionStartAnchor(cursor, start, end);
0238             anchorEnd = selectionEndAnchor(cursor, end, start);
0239         } else {
0240             // Return full text
0241             cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
0242             int end = cursor.position() - 1;
0243             cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0244             int start = cursor.position();
0245             anchorStart = selectionStartAnchor(cursor, start, end);
0246             anchorEnd = selectionEndAnchor(cursor, end, start);
0247         }
0248         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
0249             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0250             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0251             qDebug() << "=== GOT EXPORT MAIN ZONE: " << GenTime(startMs).frames(pCore->getCurrentFps()) << " - "
0252                      << GenTime(endMs).frames(pCore->getCurrentFps());
0253             QPoint originalZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())));
0254             return processedZones({originalZone});
0255         }
0256         return {};
0257     }
0258     QVector<QPoint> zones;
0259     int zoneStart = -1;
0260     int zoneEnd = -1;
0261     int currentEnd = -1;
0262     int currentStart = -1;
0263     qDebug() << "=== FROM BLOCKS: " << m_selectedBlocks;
0264     for (auto &bk : m_selectedBlocks) {
0265         QPair<double, double> z = speechZones.at(bk);
0266         currentStart = GenTime(z.first).frames(pCore->getCurrentFps());
0267         currentEnd = GenTime(z.second).frames(pCore->getCurrentFps());
0268         if (zoneStart < 0) {
0269             zoneStart = currentStart;
0270         } else if (currentStart - zoneEnd > 1) {
0271             // Insert last zone
0272             zones << QPoint(zoneStart, zoneEnd);
0273             zoneStart = currentStart;
0274         }
0275         zoneEnd = currentEnd;
0276     }
0277     qDebug() << "=== INSERT LAST: " << currentStart << "-" << currentEnd;
0278     zones << QPoint(currentStart, currentEnd);
0279 
0280     qDebug() << "=== GOT RESULTING ZONES: " << zones;
0281     return processedZones(zones);
0282 }
0283 
0284 void VideoTextEdit::updateLineNumberArea(const QRect &rect, int dy)
0285 {
0286     if (dy)
0287         lineNumberArea->scroll(0, dy);
0288     else
0289         lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
0290 }
0291 
0292 void VideoTextEdit::resizeEvent(QResizeEvent *e)
0293 {
0294     QTextEdit::resizeEvent(e);
0295     QRect cr = contentsRect();
0296     lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
0297 }
0298 
0299 void VideoTextEdit::keyPressEvent(QKeyEvent *e)
0300 {
0301     QTextEdit::keyPressEvent(e);
0302 }
0303 
0304 void VideoTextEdit::checkHoverBlock(int yPos)
0305 {
0306     QTextCursor curs = QTextCursor(this->document());
0307     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0308 
0309     m_hoveredBlock = -1;
0310     for (int i = 0; i < this->document()->blockCount(); ++i) {
0311         QTextBlock block = curs.block();
0312         QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(0, 0 - (this->verticalScrollBar()->sliderPosition())).toRect();
0313         if (yPos < r2.x()) {
0314             break;
0315         }
0316         if (yPos > r2.x() && yPos < r2.bottom()) {
0317             m_hoveredBlock = i;
0318             break;
0319         }
0320         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0321     }
0322     setCursor(m_hoveredBlock == -1 ? Qt::ArrowCursor : Qt::PointingHandCursor);
0323     lineNumberArea->update();
0324 }
0325 
0326 void VideoTextEdit::blockClicked(Qt::KeyboardModifiers modifiers, bool play)
0327 {
0328     if (m_hoveredBlock > -1 && m_hoveredBlock < speechZones.count()) {
0329         if (m_selectedBlocks.contains(m_hoveredBlock)) {
0330             if (modifiers & Qt::ControlModifier) {
0331                 // remove from selection on ctrl+click an already selected block
0332                 m_selectedBlocks.removeAll(m_hoveredBlock);
0333             } else {
0334                 m_selectedBlocks = {m_hoveredBlock};
0335                 lineNumberArea->update();
0336             }
0337         } else {
0338             // Add to selection
0339             if (modifiers & Qt::ControlModifier) {
0340                 m_selectedBlocks << m_hoveredBlock;
0341             } else if (modifiers & Qt::ShiftModifier) {
0342                 if (m_lastClickedBlock > -1) {
0343                     for (int i = qMin(m_lastClickedBlock, m_hoveredBlock); i <= qMax(m_lastClickedBlock, m_hoveredBlock); i++) {
0344                         if (!m_selectedBlocks.contains(i)) {
0345                             m_selectedBlocks << i;
0346                         }
0347                     }
0348                 } else {
0349                     m_selectedBlocks = {m_hoveredBlock};
0350                 }
0351             } else {
0352                 m_selectedBlocks = {m_hoveredBlock};
0353             }
0354         }
0355         if (m_hoveredBlock >= 0) {
0356             m_lastClickedBlock = m_hoveredBlock;
0357         }
0358 
0359         // Find continuous block selection
0360         int startBlock = m_hoveredBlock;
0361         int endBlock = m_hoveredBlock;
0362         while (m_selectedBlocks.contains(startBlock)) {
0363             startBlock--;
0364         }
0365         if (!m_selectedBlocks.contains(startBlock)) {
0366             startBlock++;
0367         }
0368         while (m_selectedBlocks.contains(endBlock)) {
0369             endBlock++;
0370         }
0371         if (!m_selectedBlocks.contains(endBlock)) {
0372             endBlock--;
0373         }
0374         QPair<double, double> zone = {speechZones.at(startBlock).first, speechZones.at(endBlock).second};
0375         double startMs = zone.first;
0376         double endMs = zone.second;
0377         pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
0378         pCore->getMonitor(Kdenlive::ClipMonitor)
0379             ->slotLoadClipZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())));
0380         QTextCursor cursor = textCursor();
0381         cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0382         cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, m_hoveredBlock);
0383         cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
0384         setTextCursor(cursor);
0385         if (play) {
0386             pCore->getMonitor(Kdenlive::ClipMonitor)->slotPlayZone();
0387         }
0388     }
0389 }
0390 
0391 int VideoTextEdit::getFirstVisibleBlockId()
0392 {
0393     // Detect the first block for which bounding rect - once
0394     // translated in absolute coordinates - is contained
0395     // by the editor's text area
0396 
0397     // Costly way of doing but since
0398     // "blockBoundingGeometry(...)" doesn't exist
0399     // for "QTextEdit"...
0400 
0401     QTextCursor curs = QTextCursor(this->document());
0402     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0403     for (int i = 0; i < this->document()->blockCount(); ++i) {
0404         QTextBlock block = curs.block();
0405 
0406         QRect r1 = this->viewport()->geometry();
0407         QRect r2 =
0408             this->document()->documentLayout()->blockBoundingRect(block).translated(r1.x(), r1.y() - (this->verticalScrollBar()->sliderPosition())).toRect();
0409 
0410         if (r1.contains(r2, true)) {
0411             return i;
0412         }
0413 
0414         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0415     }
0416     return 0;
0417 }
0418 
0419 void VideoTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
0420 {
0421     this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition());
0422 
0423     QPainter painter(lineNumberArea);
0424     painter.fillRect(event->rect(), palette().alternateBase().color());
0425     int blockNumber = this->getFirstVisibleBlockId();
0426 
0427     QTextBlock block = this->document()->findBlockByNumber(blockNumber);
0428     QTextBlock prev_block = (blockNumber > 0) ? this->document()->findBlockByNumber(blockNumber - 1) : block;
0429     int translate_y = (blockNumber > 0) ? -this->verticalScrollBar()->sliderPosition() : 0;
0430 
0431     int top = this->viewport()->geometry().top();
0432 
0433     // Adjust text position according to the previous "non entirely visible" block
0434     // if applicable. Also takes in consideration the document's margin offset.
0435     int additional_margin;
0436     if (blockNumber == 0)
0437         // Simply adjust to document's margin
0438         additional_margin = int(this->document()->documentMargin()) - 1 - this->verticalScrollBar()->sliderPosition();
0439     else
0440         // Getting the height of the visible part of the previous "non entirely visible" block
0441         additional_margin = int(
0442             this->document()->documentLayout()->blockBoundingRect(prev_block).translated(0, translate_y).intersected(this->viewport()->geometry()).height());
0443 
0444     // Shift the starting point
0445     top += additional_margin;
0446 
0447     int bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
0448 
0449     QColor col_2 = palette().link().color();
0450     QColor col_1 = palette().highlightedText().color();
0451     QColor col_0 = palette().text().color();
0452 
0453     // Draw the numbers (displaying the current line number in green)
0454     while (block.isValid() && top <= event->rect().bottom()) {
0455         if (blockNumber >= speechZones.count()) {
0456             break;
0457         }
0458         if (block.isVisible() && bottom >= event->rect().top()) {
0459             if (m_selectedBlocks.contains(blockNumber)) {
0460                 painter.fillRect(QRect(0, top, lineNumberArea->width(), bottom - top), palette().highlight().color());
0461                 painter.setPen(col_1);
0462             } else {
0463                 painter.setPen((this->textCursor().blockNumber() == blockNumber) ? col_2 : col_0);
0464             }
0465             QString number = pCore->timecode().getDisplayTimecode(GenTime(speechZones[blockNumber].first), false);
0466             painter.drawText(-5, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number);
0467         }
0468         painter.setPen(palette().dark().color());
0469         painter.drawLine(0, bottom, width(), bottom);
0470         block = block.next();
0471         top = bottom;
0472         bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
0473         ++blockNumber;
0474     }
0475 }
0476 
0477 void VideoTextEdit::contextMenuEvent(QContextMenuEvent *event)
0478 {
0479     QMenu *menu = createStandardContextMenu();
0480     menu->addAction(bookmarkAction);
0481     menu->addAction(deleteAction);
0482     menu->exec(event->globalPos());
0483     delete menu;
0484 }
0485 
0486 void VideoTextEdit::mousePressEvent(QMouseEvent *e)
0487 {
0488     if (e->buttons() & Qt::LeftButton) {
0489         QTextCursor current = textCursor();
0490         QTextCursor cursor = cursorForPosition(e->pos());
0491         int pos = cursor.position();
0492         qDebug() << "=== CLICKED AT: " << pos << ", SEL: " << current.selectionStart() << "-" << current.selectionEnd();
0493         if (pos > current.selectionStart() && pos < current.selectionEnd()) {
0494             // Clicked in selection
0495             e->ignore();
0496             qDebug() << "=== IGNORING MOUSE CLICK";
0497             return;
0498         } else {
0499             QTextEdit::mousePressEvent(e);
0500             const QString link = anchorAt(e->pos());
0501             if (!link.isEmpty()) {
0502                 // Clicked on a word
0503                 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
0504                 double startMs = link.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0505                 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
0506             }
0507         }
0508         setTextCursor(cursor);
0509     } else {
0510         QTextEdit::mousePressEvent(e);
0511     }
0512 }
0513 
0514 void VideoTextEdit::mouseReleaseEvent(QMouseEvent *e)
0515 {
0516     QTextEdit::mouseReleaseEvent(e);
0517     if (e->button() == Qt::LeftButton) {
0518         QTextCursor cursor = textCursor();
0519         if (!cursor.selectedText().isEmpty()) {
0520             // We have a selection, ensure full word is selected
0521             int start = cursor.selectionStart();
0522             int end = cursor.selectionEnd();
0523             if (document()->characterAt(end - 1) == QLatin1Char(' ')) {
0524                 // Selection ends with a space
0525                 end--;
0526             }
0527             QTextBlock bk = cursor.block();
0528             if (bk.text().simplified() == i18n("No speech")) {
0529                 // This is a silence block, select all
0530                 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
0531                 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
0532             } else {
0533                 cursor.setPosition(start);
0534                 cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
0535                 cursor.setPosition(end, QTextCursor::KeepAnchor);
0536                 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0537             }
0538             setTextCursor(cursor);
0539         }
0540         if (!m_selectedBlocks.isEmpty()) {
0541             m_selectedBlocks.clear();
0542             repaintLines();
0543         }
0544     } else {
0545         qDebug() << "==== NO LEFT CLICK!";
0546     }
0547 }
0548 
0549 void VideoTextEdit::mouseMoveEvent(QMouseEvent *e)
0550 {
0551     QTextEdit::mouseMoveEvent(e);
0552     if (e->buttons() & Qt::LeftButton) {
0553         /*QTextCursor cursor = textCursor();
0554         cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0555         setTextCursor(cursor);*/
0556     } else {
0557         const QString link = anchorAt(e->pos());
0558         viewport()->setCursor(link.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
0559     }
0560 }
0561 
0562 TextBasedEdit::TextBasedEdit(QWidget *parent)
0563     : QWidget(parent)
0564 {
0565     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0566     setupUi(this);
0567     setFocusPolicy(Qt::StrongFocus);
0568     m_stt = new SpeechToText();
0569     m_voskConfig = new QAction(i18n("Configure"), this);
0570     connect(m_voskConfig, &QAction::triggered, []() { pCore->window()->slotPreferences(8); });
0571 
0572     // Visual text editor
0573     auto *l = new QVBoxLayout;
0574     l->setContentsMargins(0, 0, 0, 0);
0575     m_visualEditor = new VideoTextEdit(this);
0576     m_visualEditor->installEventFilter(this);
0577     l->addWidget(m_visualEditor);
0578     text_frame->setLayout(l);
0579     m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0580     // m_document = m_visualEditor->document();
0581     // m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0582     m_visualEditor->setDocument(&m_document);
0583     connect(&m_document, &QTextDocument::blockCountChanged, this, [this](int ct) {
0584         m_visualEditor->repaintLines();
0585         qDebug() << "++++++++++++++++++++\n\nGOT BLOCKS: " << ct << "\n\n+++++++++++++++++++++";
0586     });
0587 
0588     connect(m_visualEditor, &VideoTextEdit::selectionChanged, this, [this]() {
0589         bool hasSelection = m_visualEditor->textCursor().selectedText().simplified().isEmpty() == false;
0590         m_visualEditor->bookmarkAction->setEnabled(hasSelection);
0591         m_visualEditor->deleteAction->setEnabled(hasSelection);
0592         button_insert->setEnabled(hasSelection);
0593     });
0594 
0595     button_start->setEnabled(false);
0596     connect(button_start, &QPushButton::clicked, this, &TextBasedEdit::startRecognition);
0597     frame_progress->setVisible(false);
0598     connect(button_abort, &QToolButton::clicked, this, [this]() {
0599         if (m_speechJob && m_speechJob->state() == QProcess::Running) {
0600             m_speechJob->kill();
0601         } else if (m_tCodeJob && m_tCodeJob->state() == QProcess::Running) {
0602             m_tCodeJob->kill();
0603         }
0604     });
0605     connect(pCore.get(), &Core::voskModelUpdate, this, [&](const QStringList &models) {
0606         language_box->clear();
0607         language_box->addItems(models);
0608         if (models.isEmpty()) {
0609             showMessage(i18n("Please install speech recognition models"), KMessageWidget::Information, m_voskConfig);
0610         } else {
0611             if (!KdenliveSettings::vosk_text_model().isEmpty() && models.contains(KdenliveSettings::vosk_text_model())) {
0612                 int ix = language_box->findText(KdenliveSettings::vosk_text_model());
0613                 if (ix > -1) {
0614                     language_box->setCurrentIndex(ix);
0615                 }
0616             }
0617         }
0618     });
0619     connect(language_box, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this,
0620             [this]() { KdenliveSettings::setVosk_text_model(language_box->currentText()); });
0621     info_message->hide();
0622 
0623     m_logAction = new QAction(i18n("Show log"), this);
0624     connect(m_logAction, &QAction::triggered, this, [this]() { KMessageBox::error(this, m_errorString, i18n("Detailed log")); });
0625 
0626     speech_zone->setChecked(KdenliveSettings::speech_zone());
0627     connect(speech_zone, &QCheckBox::stateChanged, [](int state) { KdenliveSettings::setSpeech_zone(state == Qt::Checked); });
0628     button_delete->setDefaultAction(m_visualEditor->deleteAction);
0629     button_delete->setToolTip(i18n("Delete selected text"));
0630     connect(m_visualEditor->deleteAction, &QAction::triggered, this, &TextBasedEdit::deleteItem);
0631 
0632     connect(button_add, &QToolButton::clicked, this, [this]() { previewPlaylist(); });
0633 
0634     button_bookmark->setDefaultAction(m_visualEditor->bookmarkAction);
0635     button_bookmark->setToolTip(i18n("Add bookmark for current selection"));
0636     connect(m_visualEditor->bookmarkAction, &QAction::triggered, this, &TextBasedEdit::addBookmark);
0637 
0638     connect(button_insert, &QToolButton::clicked, this, &TextBasedEdit::insertToTimeline);
0639     button_insert->setEnabled(false);
0640 
0641     // Message Timer
0642     m_hideTimer.setSingleShot(true);
0643     m_hideTimer.setInterval(5000);
0644     connect(&m_hideTimer, &QTimer::timeout, info_message, &KMessageWidget::animatedHide);
0645 
0646     // Search stuff
0647     search_frame->setVisible(false);
0648     connect(button_search, &QToolButton::toggled, this, [&](bool toggled) {
0649         search_frame->setVisible(toggled);
0650         search_line->setFocus();
0651     });
0652     connect(search_line, &QLineEdit::textChanged, this, [this](const QString &searchText) {
0653         QPalette palette = this->palette();
0654         QColor col = palette.color(QPalette::Base);
0655         if (searchText.length() > 2) {
0656             bool found = m_visualEditor->find(searchText);
0657             if (found) {
0658                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0659                 palette.setColor(QPalette::Base, col);
0660                 QTextCursor cur = m_visualEditor->textCursor();
0661                 cur.select(QTextCursor::WordUnderCursor);
0662                 m_visualEditor->setTextCursor(cur);
0663             } else {
0664                 // Loop over, abort
0665                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0666                 palette.setColor(QPalette::Base, col);
0667             }
0668         }
0669         search_line->setPalette(palette);
0670     });
0671     connect(search_next, &QToolButton::clicked, this, [this]() {
0672         const QString searchText = search_line->text();
0673         QPalette palette = this->palette();
0674         QColor col = palette.color(QPalette::Base);
0675         if (searchText.length() > 2) {
0676             bool found = m_visualEditor->find(searchText);
0677             if (found) {
0678                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0679                 palette.setColor(QPalette::Base, col);
0680                 QTextCursor cur = m_visualEditor->textCursor();
0681                 cur.select(QTextCursor::WordUnderCursor);
0682                 m_visualEditor->setTextCursor(cur);
0683             } else {
0684                 // Loop over, abort
0685                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0686                 palette.setColor(QPalette::Base, col);
0687             }
0688         }
0689         search_line->setPalette(palette);
0690     });
0691     connect(search_prev, &QToolButton::clicked, this, [this]() {
0692         const QString searchText = search_line->text();
0693         QPalette palette = this->palette();
0694         QColor col = palette.color(QPalette::Base);
0695         if (searchText.length() > 2) {
0696             bool found = m_visualEditor->find(searchText, QTextDocument::FindBackward);
0697             if (found) {
0698                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0699                 palette.setColor(QPalette::Base, col);
0700                 QTextCursor cur = m_visualEditor->textCursor();
0701                 cur.select(QTextCursor::WordUnderCursor);
0702                 m_visualEditor->setTextCursor(cur);
0703             } else {
0704                 // Loop over, abort
0705                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0706                 palette.setColor(QPalette::Base, col);
0707             }
0708         }
0709         search_line->setPalette(palette);
0710     });
0711 
0712     m_stt->parseVoskDictionaries();
0713 }
0714 
0715 TextBasedEdit::~TextBasedEdit()
0716 {
0717     if (m_speechJob && m_speechJob->state() == QProcess::Running) {
0718         m_speechJob->kill();
0719         m_speechJob->waitForFinished();
0720     }
0721 }
0722 
0723 bool TextBasedEdit::eventFilter(QObject *obj, QEvent *event)
0724 {
0725     if (event->type() == QEvent::KeyPress) {
0726         qDebug() << "==== FOT TXTEDIT EVENT FILTER: " << static_cast<QKeyEvent *>(event)->key();
0727     }
0728     /*if(obj == m_visualEditor && event->type() == QEvent::KeyPress)
0729     {
0730         QKeyEvent *keyEvent = static_cast <QKeyEvent*> (event);
0731         if (keyEvent->key() != Qt::Key_Left && keyEvent->key() != Qt::Key_Up && keyEvent->key() != Qt::Key_Right && keyEvent->key() != Qt::Key_Down) {
0732             parentWidget()->setFocus();
0733             return true;
0734         }
0735     }*/
0736     return QObject::eventFilter(obj, event);
0737 }
0738 
0739 void TextBasedEdit::startRecognition()
0740 {
0741     if (m_speechJob && m_speechJob->state() != QProcess::NotRunning) {
0742         if (KMessageBox::questionYesNo(this, i18n("Another recognition job is running. Abort it ?")) != KMessageBox::Yes) {
0743             return;
0744         }
0745     }
0746     info_message->hide();
0747     m_errorString.clear();
0748     m_visualEditor->cleanup();
0749     // m_visualEditor->insertHtml(QStringLiteral("<body>"));
0750     m_stt->checkDependencies();
0751     if (!m_stt->checkSetup() || !m_stt->missingDependencies({QStringLiteral("vosk")}).isEmpty()) {
0752         showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig);
0753         return;
0754     }
0755     // Start python script
0756     QString language = language_box->currentText();
0757     if (language.isEmpty()) {
0758         showMessage(i18n("Please install a language model."), KMessageWidget::Warning, m_voskConfig);
0759         return;
0760     }
0761     m_binId = pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId();
0762     std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
0763     if (clip == nullptr) {
0764         showMessage(i18n("Select a clip with audio in Project Bin."), KMessageWidget::Information);
0765         return;
0766     }
0767 
0768     m_speechJob = std::make_unique<QProcess>(this);
0769     showMessage(i18n("Starting speech recognition"), KMessageWidget::Information);
0770     qApp->processEvents();
0771     QString modelDirectory = m_stt->voskModelPath();
0772     qDebug() << "==== ANALYSIS SPEECH: " << modelDirectory << " - " << language;
0773 
0774     m_sourceUrl.clear();
0775     QString clipName;
0776     m_clipOffset = 0;
0777     m_lastPosition = 0;
0778     double endPos = 0;
0779     bool hasAudio = false;
0780     if (clip->itemType() == AbstractProjectItem::ClipItem) {
0781         std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
0782         if (clipItem) {
0783             m_sourceUrl = clipItem->url();
0784             clipName = clipItem->clipName();
0785             hasAudio = clipItem->hasAudio();
0786             if (speech_zone->isChecked()) {
0787                 // Analyse clip zone only
0788                 QPoint zone = clipItem->zone();
0789                 m_lastPosition = zone.x();
0790                 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
0791                 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
0792                 endPos = m_clipDuration;
0793             } else {
0794                 m_clipDuration = clipItem->duration().seconds();
0795             }
0796         }
0797     } else if (clip->itemType() == AbstractProjectItem::SubClipItem) {
0798         std::shared_ptr<ProjectSubClip> clipItem = std::static_pointer_cast<ProjectSubClip>(clip);
0799         if (clipItem) {
0800             auto master = clipItem->getMasterClip();
0801             m_sourceUrl = master->url();
0802             hasAudio = master->hasAudio();
0803             clipName = master->clipName();
0804             QPoint zone = clipItem->zone();
0805             m_lastPosition = zone.x();
0806             m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
0807             m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
0808             endPos = m_clipDuration;
0809         }
0810     }
0811     if (m_sourceUrl.isEmpty() || !hasAudio) {
0812         showMessage(i18n("Select a clip with audio for speech recognition."), KMessageWidget::Information);
0813         return;
0814     }
0815     clipNameLabel->setText(clipName);
0816     if (clip->clipType() == ClipType::Playlist) {
0817         // We need to extract audio first
0818         m_playlistWav.remove();
0819         m_playlistWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav")));
0820         if (!m_playlistWav.open()) {
0821             showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning);
0822             return;
0823         }
0824         m_playlistWav.close();
0825 
0826         showMessage(i18n("Extracting audio for %1.", clipName), KMessageWidget::Information);
0827         qApp->processEvents();
0828         m_tCodeJob = std::make_unique<QProcess>(this);
0829         m_tCodeJob->setProcessChannelMode(QProcess::MergedChannels);
0830         connect(m_tCodeJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
0831                 [this, language, clipName, modelDirectory, endPos](int code, QProcess::ExitStatus status) {
0832                     Q_UNUSED(code)
0833                     qDebug() << "++++++++++++++++++++++ TCODE JOB FINISHED\n";
0834                     if (status == QProcess::CrashExit) {
0835                         showMessage(i18n("Audio extract failed."), KMessageWidget::Warning);
0836                         speech_progress->setValue(0);
0837                         frame_progress->setVisible(false);
0838                         m_playlistWav.remove();
0839                         return;
0840                     }
0841                     showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
0842                     qApp->processEvents();
0843                     connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
0844                     connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
0845                     connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
0846                             [this](int code, QProcess::ExitStatus status) {
0847                                 m_playlistWav.remove();
0848                                 slotProcessSpeechStatus(code, status);
0849                             });
0850                     m_speechJob->start(m_stt->pythonExec(), {m_stt->speechScript(), modelDirectory, language, m_playlistWav.fileName(),
0851                                                              QString::number(m_clipOffset), QString::number(endPos)});
0852                     speech_progress->setValue(0);
0853                     frame_progress->setVisible(true);
0854                 });
0855         connect(m_tCodeJob.get(), &QProcess::readyReadStandardOutput, this, [this]() {
0856             QString saveData = QString::fromUtf8(m_tCodeJob->readAllStandardOutput());
0857             qDebug() << "+GOT OUTPUT: " << saveData;
0858             saveData = saveData.section(QStringLiteral("percentage:"), 1).simplified();
0859             int percent = saveData.section(QLatin1Char(' '), 0, 0).toInt();
0860             speech_progress->setValue(percent);
0861         });
0862         m_tCodeJob->start(KdenliveSettings::rendererpath(),
0863                           {QStringLiteral("-progress"), m_sourceUrl, QStringLiteral("-consumer"), QString("avformat:%1").arg(m_playlistWav.fileName()),
0864                            QStringLiteral("vn=1"), QStringLiteral("ar=16000")});
0865         speech_progress->setValue(0);
0866         frame_progress->setVisible(true);
0867     } else {
0868         showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
0869         qApp->processEvents();
0870         connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
0871         connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
0872         connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
0873                 &TextBasedEdit::slotProcessSpeechStatus);
0874         qDebug() << "=== STARTING RECO: " << m_stt->speechScript() << " / " << modelDirectory << " / " << language << " / " << m_sourceUrl
0875                  << ", START: " << m_clipOffset << ", DUR: " << endPos;
0876         button_add->setEnabled(false);
0877         m_speechJob->start(m_stt->pythonExec(),
0878                            {m_stt->speechScript(), modelDirectory, language, m_sourceUrl, QString::number(m_clipOffset), QString::number(endPos)});
0879         speech_progress->setValue(0);
0880         frame_progress->setVisible(true);
0881     }
0882 }
0883 
0884 void TextBasedEdit::slotProcessSpeechStatus(int, QProcess::ExitStatus status)
0885 {
0886     if (status == QProcess::CrashExit) {
0887         showMessage(i18n("Speech recognition aborted."), KMessageWidget::Warning, m_errorString.isEmpty() ? nullptr : m_logAction);
0888     } else if (m_visualEditor->toPlainText().isEmpty()) {
0889         if (m_errorString.contains(QStringLiteral("ModuleNotFoundError"))) {
0890             showMessage(i18n("Error, please check the speech to text configuration."), KMessageWidget::Warning, m_voskConfig);
0891         } else {
0892             showMessage(i18n("No speech detected."), KMessageWidget::Information, m_errorString.isEmpty() ? nullptr : m_logAction);
0893         }
0894     } else {
0895         // Last empty object - no speech detected
0896         GenTime silenceStart(m_lastPosition + 1, pCore->getCurrentFps());
0897         if (silenceStart.seconds() < m_clipDuration + m_clipOffset) {
0898             m_visualEditor->moveCursor(QTextCursor::End);
0899             QTextCursor cursor = m_visualEditor->textCursor();
0900             QTextCharFormat fmt = cursor.charFormat();
0901             fmt.setAnchorHref(QString("%1#%2:%3").arg(m_binId).arg(silenceStart.seconds()).arg(GenTime(m_clipDuration + m_clipOffset).seconds()));
0902             fmt.setAnchor(true);
0903             cursor.insertText(i18n("No speech"), fmt);
0904             m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
0905             m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), GenTime(m_clipDuration + m_clipOffset).seconds());
0906             m_visualEditor->repaintLines();
0907         }
0908 
0909         button_add->setEnabled(true);
0910         showMessage(i18n("Speech recognition finished."), KMessageWidget::Positive);
0911         // Store speech analysis in clip properties
0912         std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
0913         if (clip) {
0914             std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
0915             QString oldSpeech;
0916             if (clipItem) {
0917                 oldSpeech = clipItem->getProducerProperty(QStringLiteral("kdenlive:speech"));
0918             }
0919             QMap<QString, QString> oldProperties;
0920             oldProperties.insert(QStringLiteral("kdenlive:speech"), oldSpeech);
0921             QMap<QString, QString> properties;
0922             properties.insert(QStringLiteral("kdenlive:speech"), m_visualEditor->toHtml());
0923             pCore->bin()->slotEditClipCommand(m_binId, oldProperties, properties);
0924         }
0925     }
0926     QTextCursor cur = m_visualEditor->textCursor();
0927     cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0928     m_visualEditor->setTextCursor(cur);
0929     frame_progress->setVisible(false);
0930 }
0931 
0932 void TextBasedEdit::slotProcessSpeechError()
0933 {
0934     m_errorString.append(QString::fromUtf8(m_speechJob->readAllStandardError()));
0935 }
0936 
0937 void TextBasedEdit::slotProcessSpeech()
0938 {
0939     QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput());
0940     qDebug() << "=== GOT DATA:\n" << saveData;
0941     QJsonParseError error;
0942     auto loadDoc = QJsonDocument::fromJson(saveData.toUtf8(), &error);
0943     qDebug() << "===JSON ERROR: " << error.errorString();
0944     QTextCursor cursor = m_visualEditor->textCursor();
0945     QTextCharFormat fmt = cursor.charFormat();
0946     // fmt.setForeground(palette().text().color());
0947     if (loadDoc.isObject()) {
0948         QJsonObject obj = loadDoc.object();
0949         if (!obj.isEmpty()) {
0950             // QString itemText = obj["text"].toString();
0951             bool textFound = false;
0952             QPair<double, double> sentenceZone;
0953             if (obj["result"].isArray()) {
0954                 QJsonArray obj2 = obj["result"].toArray();
0955 
0956                 // Get start time for first word
0957                 QJsonValue val = obj2.first();
0958                 if (val.isObject() && val.toObject().keys().contains("start")) {
0959                     double ms = val.toObject().value("start").toDouble() + m_clipOffset;
0960                     GenTime startPos(ms);
0961                     sentenceZone.first = ms;
0962                     if (startPos.frames(pCore->getCurrentFps()) > m_lastPosition + 1) {
0963                         // Insert space
0964                         GenTime silenceStart(m_lastPosition, pCore->getCurrentFps());
0965                         m_visualEditor->moveCursor(QTextCursor::End);
0966                         fmt.setAnchorHref(QString("%1#%2:%3")
0967                                               .arg(m_binId)
0968                                               .arg(silenceStart.seconds())
0969                                               .arg(GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()));
0970                         fmt.setAnchor(true);
0971                         cursor.insertText(i18n("No speech"), fmt);
0972                         m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
0973                         m_visualEditor->speechZones << QPair<double, double>(
0974                             silenceStart.seconds(), GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds());
0975                     }
0976                     val = obj2.last();
0977                     if (val.isObject() && val.toObject().keys().contains("end")) {
0978                         ms = val.toObject().value("end").toDouble() + m_clipOffset;
0979                         sentenceZone.second = ms;
0980                         m_lastPosition = GenTime(ms).frames(pCore->getCurrentFps());
0981                         if (m_clipDuration > 0.) {
0982                             speech_progress->setValue(static_cast<int>(100 * ms / (+m_clipOffset + m_clipDuration)));
0983                         }
0984                     }
0985                 }
0986                 // Store words with their start/end time
0987                 foreach (const QJsonValue &v, obj2) {
0988                     textFound = true;
0989                     fmt.setAnchor(true);
0990                     fmt.setAnchorHref(QString("%1#%2:%3")
0991                                           .arg(m_binId)
0992                                           .arg(v.toObject().value("start").toDouble() + m_clipOffset)
0993                                           .arg(v.toObject().value("end").toDouble() + m_clipOffset));
0994                     cursor.insertText(v.toObject().value("word").toString(), fmt);
0995                     fmt.setAnchor(false);
0996                     cursor.insertText(QStringLiteral(" "), fmt);
0997                 }
0998             } else {
0999                 // Last empty object - no speech detected
1000             }
1001             if (textFound) {
1002                 if (sentenceZone.second < m_clipOffset + m_clipDuration) {
1003                     m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1004                 }
1005                 m_visualEditor->speechZones << sentenceZone;
1006             }
1007         }
1008     } else if (loadDoc.isEmpty()) {
1009         qDebug() << "==== EMPTY OBJECT DOC";
1010     }
1011     qDebug() << "==== GOT BLOCKS: " << m_document.blockCount();
1012     qDebug() << "=== LINES: " << m_document.firstBlock().lineCount();
1013     m_visualEditor->repaintLines();
1014 }
1015 
1016 void TextBasedEdit::deleteItem()
1017 {
1018     QTextCursor cursor = m_visualEditor->textCursor();
1019     int start = cursor.selectionStart();
1020     int end = cursor.selectionEnd();
1021     qDebug() << "=== CUTTONG: " << start << " - " << end;
1022     if (end > start) {
1023         QString anchorStart = m_visualEditor->selectionStartAnchor(cursor, start, end);
1024         cursor.setPosition(end);
1025         bool blockEnd = cursor.atBlockEnd();
1026         cursor = m_visualEditor->textCursor();
1027         QString anchorEnd = m_visualEditor->selectionEndAnchor(cursor, end, start);
1028         qDebug() << "=== FINAL END CUT: " << end;
1029         qDebug() << "=== GOT END ANCHOR: " << cursor.selectedText() << " = " << anchorEnd;
1030         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
1031             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1032             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1033             if (startMs < endMs) {
1034                 qDebug() << "=== GOT CUT ZONE: " << GenTime(startMs).frames(pCore->getCurrentFps()) << " - " << GenTime(endMs).frames(pCore->getCurrentFps());
1035                 m_visualEditor->cutZones << QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()));
1036                 cursor = m_visualEditor->textCursor();
1037                 cursor.removeSelectedText();
1038                 if (blockEnd) {
1039                     cursor.deleteChar();
1040                 }
1041             }
1042         }
1043     } else {
1044         QTextCursor curs = m_visualEditor->textCursor();
1045         curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
1046         for (int i = 0; i < m_document.blockCount(); ++i) {
1047             int blockStart = curs.position();
1048             curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
1049             int blockEnd = curs.position();
1050             if (blockStart == blockEnd) {
1051                 // Empty block, delete
1052                 curs.select(QTextCursor::BlockUnderCursor);
1053                 curs.removeSelectedText();
1054                 curs.deleteChar();
1055             }
1056             curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
1057         }
1058     }
1059     // Reset selection and rebuild line numbers
1060     m_visualEditor->rebuildZones();
1061     previewPlaylist(false);
1062 }
1063 
1064 void TextBasedEdit::insertToTimeline()
1065 {
1066     QVector<QPoint> zones = m_visualEditor->getInsertZones();
1067     if (zones.isEmpty()) {
1068         return;
1069     }
1070     for (auto &zone : zones) {
1071         pCore->window()->getCurrentTimeline()->controller()->insertZone(m_binId, zone, false);
1072     }
1073 }
1074 
1075 void TextBasedEdit::previewPlaylist(bool createNew)
1076 {
1077     QVector<QPoint> zones = m_visualEditor->getInsertZones();
1078     if (zones.isEmpty()) {
1079         showMessage(i18n("No text to export"), KMessageWidget::Information);
1080         return;
1081     }
1082     std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
1083     std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
1084     QString sourcePath = clipItem->url();
1085     QMap<QString, QString> properties;
1086     properties.insert(QStringLiteral("kdenlive:baseid"), m_binId);
1087     QStringList playZones;
1088     for (const auto &p : qAsConst(zones)) {
1089         playZones << QString("%1:%2").arg(p.x()).arg(p.y());
1090     }
1091     properties.insert(QStringLiteral("kdenlive:cutzones"), playZones.join(QLatin1Char(';')));
1092     if (createNew) {
1093         int ix = 1;
1094         m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1095         while (QFile::exists(m_playlist)) {
1096             ix++;
1097             m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1098         }
1099         QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(m_playlist), this, i18n("Enter new playlist path"));
1100         if (url.isEmpty()) {
1101             return;
1102         }
1103         m_playlist = url.toLocalFile();
1104     }
1105     if (!m_playlist.isEmpty()) {
1106         pCore->bin()->savePlaylist(m_binId, m_playlist, zones, properties, createNew);
1107         clipNameLabel->setText(QFileInfo(m_playlist).fileName());
1108     }
1109 }
1110 
1111 void TextBasedEdit::showMessage(const QString &text, KMessageWidget::MessageType type, QAction *action)
1112 {
1113     if (m_currentMessageAction != nullptr && (action == nullptr || action != m_currentMessageAction)) {
1114         info_message->removeAction(m_currentMessageAction);
1115         m_currentMessageAction = action;
1116         if (m_currentMessageAction) {
1117             info_message->addAction(m_currentMessageAction);
1118         }
1119     } else if (action) {
1120         m_currentMessageAction = action;
1121         info_message->addAction(m_currentMessageAction);
1122     }
1123 
1124     if (info_message->isVisible()) {
1125         m_hideTimer.stop();
1126     }
1127     info_message->setMessageType(type);
1128     info_message->setText(text);
1129     info_message->animatedShow();
1130     if (type != KMessageWidget::Error && m_currentMessageAction == nullptr) {
1131         m_hideTimer.start();
1132     }
1133 }
1134 
1135 void TextBasedEdit::openClip(std::shared_ptr<ProjectClip> clip)
1136 {
1137     if (m_speechJob && m_speechJob->state() == QProcess::Running) {
1138         // TODO: ask for job cancelation
1139         return;
1140     }
1141     if (clip && clip->isValid() && clip->hasAudio()) {
1142         QString refId = clip->getProducerProperty(QStringLiteral("kdenlive:baseid"));
1143         if (!refId.isEmpty() && refId == m_refId) {
1144             // We opened a resulting playlist, do not clear text edit
1145             return;
1146         }
1147         if (!m_visualEditor->toPlainText().isEmpty()) {
1148             m_visualEditor->cleanup();
1149         }
1150         QString speech;
1151         QList<QPoint> cutZones;
1152         m_binId = refId.isEmpty() ? clip->binId() : refId;
1153         if (!refId.isEmpty()) {
1154             // this is a clip  playlist with a bin reference, fetch it
1155             m_refId = refId;
1156             std::shared_ptr<ProjectClip> refClip = pCore->bin()->getBinClip(refId);
1157             if (refClip) {
1158                 speech = refClip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1159                 clipNameLabel->setText(refClip->clipName());
1160             }
1161             QStringList zones = clip->getProducerProperty("kdenlive:cutzones").split(QLatin1Char(';'));
1162             for (const QString &z : qAsConst(zones)) {
1163                 cutZones << QPoint(z.section(QLatin1Char(':'), 0, 0).toInt(), z.section(QLatin1Char(':'), 1, 1).toInt());
1164             }
1165         } else {
1166             m_refId.clear();
1167             speech = clip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1168             clipNameLabel->setText(clip->clipName());
1169         }
1170         if (speech.isEmpty()) {
1171             // Nothing else to do
1172             button_add->setEnabled(false);
1173             button_start->setEnabled(true);
1174             return;
1175         }
1176         m_visualEditor->insertHtml(speech);
1177         if (!cutZones.isEmpty()) {
1178             m_visualEditor->processCutZones(cutZones);
1179         }
1180         m_visualEditor->rebuildZones();
1181         button_add->setEnabled(true);
1182         button_start->setEnabled(true);
1183     } else {
1184         button_start->setEnabled(false);
1185         clipNameLabel->clear();
1186         m_visualEditor->cleanup();
1187     }
1188 }
1189 
1190 void TextBasedEdit::addBookmark()
1191 {
1192     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(m_binId);
1193     if (clip) {
1194         QString txt = m_visualEditor->textCursor().selectedText();
1195         QTextCursor cursor = m_visualEditor->textCursor();
1196         QString startAnchor = m_visualEditor->selectionStartAnchor(cursor, -1, -1);
1197         cursor = m_visualEditor->textCursor();
1198         QString endAnchor = m_visualEditor->selectionEndAnchor(cursor, -1, -1);
1199         if (startAnchor.isEmpty()) {
1200             showMessage(i18n("No timecode found in selection"), KMessageWidget::Information);
1201             return;
1202         }
1203         double ms = startAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1204         int startPos = GenTime(ms).frames(pCore->getCurrentFps());
1205         ms = endAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1206         int endPos = GenTime(ms).frames(pCore->getCurrentFps());
1207         int monitorPos = pCore->getMonitor(Kdenlive::ClipMonitor)->position();
1208         qDebug() << "==== GOT MARKER: " << txt << ", FOR POS: " << startPos << "-" << endPos << ", MON: " << monitorPos;
1209         if (monitorPos > startPos && monitorPos < endPos) {
1210             // Monitor seek is on the selection, use the current frame
1211             pCore->bin()->addClipMarker(m_binId, {monitorPos}, {txt});
1212         } else {
1213             pCore->bin()->addClipMarker(m_binId, {startPos}, {txt});
1214         }
1215     } else {
1216         qDebug() << "==== NO CLIP FOR " << m_binId;
1217     }
1218 }