File indexing completed on 2024-04-21 04:51:25

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 "utils/KMessageBox_KdenliveCompat.h"
0021 #include <KLocalizedString>
0022 #include <KMessageBox>
0023 #include <KUrlRequesterDialog>
0024 #include <QAbstractTextDocumentLayout>
0025 #include <QEvent>
0026 #include <QFontDatabase>
0027 #include <QJsonArray>
0028 #include <QJsonObject>
0029 #include <QJsonParseError>
0030 #include <QKeyEvent>
0031 #include <QMenu>
0032 #include <QPainter>
0033 #include <QScrollBar>
0034 #include <QTextBlock>
0035 #include <QTextDocumentFragment>
0036 #include <QToolButton>
0037 
0038 #include <memory>
0039 
0040 VideoTextEdit::VideoTextEdit(QWidget *parent)
0041     : QTextEdit(parent)
0042 {
0043     setMouseTracking(true);
0044     setReadOnly(true);
0045     // setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
0046     lineNumberArea = new LineNumberArea(this);
0047     connect(this, &VideoTextEdit::cursorPositionChanged, [this]() { lineNumberArea->update(); });
0048     connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this]() { lineNumberArea->update(); });
0049     QRect rect = this->contentsRect();
0050     setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
0051     lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
0052 
0053     bookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add marker"), this);
0054     bookmarkAction->setEnabled(false);
0055     deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete selection"), this);
0056     deleteAction->setEnabled(false);
0057 }
0058 
0059 void VideoTextEdit::repaintLines()
0060 {
0061     lineNumberArea->update();
0062 }
0063 
0064 void VideoTextEdit::cleanup()
0065 {
0066     speechZones.clear();
0067     cutZones.clear();
0068     m_hoveredBlock = -1;
0069     clear();
0070     document()->setDefaultStyleSheet(QString("a {text-decoration:none;color:%1}").arg(palette().text().color().name()));
0071     setCurrentFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0072 }
0073 
0074 const QString VideoTextEdit::selectionStartAnchor(QTextCursor &cursor, int start, int max)
0075 {
0076     if (start == -1) {
0077         start = cursor.selectionStart();
0078     }
0079     if (max == -1) {
0080         max = cursor.selectionEnd();
0081     }
0082     cursor.setPosition(start);
0083     cursor.select(QTextCursor::WordUnderCursor);
0084     while (cursor.selectedText().isEmpty() && start < max) {
0085         start++;
0086         cursor.setPosition(start);
0087         cursor.select(QTextCursor::WordUnderCursor);
0088     }
0089     int selStart = cursor.selectionStart();
0090     int selEnd = cursor.selectionEnd();
0091     cursor.setPosition(selStart + (selEnd - selStart) / 2);
0092     return anchorAt(cursorRect(cursor).center());
0093 }
0094 
0095 const QString VideoTextEdit::selectionEndAnchor(QTextCursor &cursor, int end, int min)
0096 {
0097     qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , MIN: " << min;
0098     if (end == -1) {
0099         end = cursor.selectionEnd();
0100     }
0101     if (min == -1) {
0102         min = cursor.selectionStart();
0103     }
0104     cursor.setPosition(end);
0105     cursor.select(QTextCursor::WordUnderCursor);
0106     while (cursor.selectedText().isEmpty() && end > min) {
0107         end--;
0108         cursor.setPosition(end);
0109         cursor.select(QTextCursor::WordUnderCursor);
0110     }
0111     qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , WORD: " << cursor.selectedText();
0112     int selStart = cursor.selectionStart();
0113     int selEnd = cursor.selectionEnd();
0114     cursor.setPosition(selStart + (selEnd - selStart) / 2);
0115     qDebug() << "==== END POS SELECTION FOR: " << cursor.selectedText() << " = " << anchorAt(cursorRect(cursor).center());
0116     QString anch = anchorAt(cursorRect(cursor).center());
0117     double endMs = anch.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0118     qDebug() << "==== GOT LAST FRAME: " << GenTime(endMs).frames(25);
0119     return anchorAt(cursorRect(cursor).center());
0120 }
0121 
0122 void VideoTextEdit::processCutZones(const QList<QPoint> &loadZones)
0123 {
0124     // Remove all outside load zones
0125     qDebug() << "=== LOADING CUT ZONES: " << loadZones << "\n........................";
0126     QTextCursor curs = textCursor();
0127     curs.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
0128     qDebug() << "===== GOT DOCUMENT END: " << curs.position();
0129     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0130     double fps = pCore->getCurrentFps();
0131     while (!curs.atEnd()) {
0132         qDebug() << "=== CURSOR POS: " << curs.position();
0133         QString anchorStart = selectionStartAnchor(curs, curs.position(), document()->characterCount());
0134         int startPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble()).frames(fps);
0135         int endPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble()).frames(fps);
0136         bool isInZones = false;
0137         for (auto &p : loadZones) {
0138             if ((startPos >= p.x() && startPos <= p.y()) || (endPos >= p.x() && endPos <= p.y())) {
0139                 isInZones = true;
0140                 break;
0141             }
0142         }
0143         if (!isInZones) {
0144             // Delete current word
0145             qDebug() << "=== DELETING WORD: " << curs.selectedText();
0146             curs.select(QTextCursor::WordUnderCursor);
0147             curs.removeSelectedText();
0148             if (document()->characterAt(curs.position() - 1) == QLatin1Char(' ')) {
0149                 // Remove trailing space
0150                 curs.deleteChar();
0151             } else {
0152                 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
0153                     break;
0154                 }
0155             }
0156         } else {
0157             curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor);
0158             if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) {
0159                 break;
0160             }
0161             qDebug() << "=== WORD INSIDE, POS: " << curs.position();
0162         }
0163         qDebug() << "=== MOVED CURSOR POS: " << curs.position();
0164     }
0165 }
0166 
0167 void VideoTextEdit::rebuildZones()
0168 {
0169     speechZones.clear();
0170     m_selectedBlocks.clear();
0171     QTextCursor curs = textCursor();
0172     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0173     for (int i = 0; i < document()->blockCount(); ++i) {
0174         int start = curs.position() + 1;
0175         QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount());
0176         // qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position();
0177         curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
0178         int end = curs.position() - 1;
0179         QString anchorEnd = selectionEndAnchor(curs, end, start);
0180         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
0181             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0182             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0183             speechZones << QPair<double, double>(startMs, endMs);
0184         }
0185         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0186     }
0187     repaintLines();
0188 }
0189 
0190 int VideoTextEdit::lineNumberAreaWidth()
0191 {
0192     int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * 11;
0193     return space;
0194 }
0195 
0196 QVector<QPoint> VideoTextEdit::processedZones(const QVector<QPoint> &sourceZones)
0197 {
0198     if (cutZones.isEmpty()) {
0199         return sourceZones;
0200     }
0201     QVector<QPoint> resultZones;
0202     QVector<QPoint> processingZones = sourceZones;
0203     for (auto &cut : cutZones) {
0204         for (auto &zone : processingZones) {
0205             if (cut.x() > zone.y() || cut.y() < zone.x()) {
0206                 // Cut is outside zone, keep it as is
0207                 resultZones << zone;
0208             } else if (cut.y() >= zone.y()) {
0209                 // Only keep the start of this zone
0210                 resultZones << QPoint(zone.x(), cut.x());
0211             } else if (cut.x() <= zone.x()) {
0212                 // Only keep the end of this zone
0213                 resultZones << QPoint(cut.y(), zone.y());
0214             } else {
0215                 // Cut is in the middle of this zone
0216                 resultZones << QPoint(zone.x(), cut.x());
0217                 resultZones << QPoint(cut.y(), zone.y());
0218             }
0219         }
0220         processingZones = resultZones;
0221         resultZones.clear();
0222     }
0223     return processingZones;
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 QVector<QPoint> VideoTextEdit::fullExport()
0285 {
0286     // Loop through all blocks
0287     QVector<QPoint> zones;
0288     int currentEnd = -1;
0289     int currentStart = -1;
0290     for (int i = 0; i < document()->blockCount(); ++i) {
0291         QTextBlock block = document()->findBlockByNumber(i);
0292         QTextCursor curs(block);
0293         int start = curs.position() + 1;
0294         QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount());
0295         // qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position();
0296         curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
0297         int end = curs.position() - 1;
0298         QString anchorEnd = selectionEndAnchor(curs, end, start);
0299         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
0300             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0301             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
0302             currentStart = GenTime(startMs).frames(pCore->getCurrentFps());
0303             currentEnd = GenTime(endMs).frames(pCore->getCurrentFps());
0304             zones << QPoint(currentStart, currentEnd);
0305         }
0306     }
0307     return processedZones(zones);
0308 }
0309 
0310 void VideoTextEdit::slotRemoveSilence()
0311 {
0312     for (int i = 0; i < document()->blockCount(); ++i) {
0313         QTextBlock block = document()->findBlockByNumber(i);
0314         if (block.text() == i18n("No speech")) {
0315             QTextCursor curs(block);
0316             curs.select(QTextCursor::BlockUnderCursor);
0317             curs.removeSelectedText();
0318             curs.deleteChar();
0319             i--;
0320             continue;
0321         }
0322     }
0323     rebuildZones();
0324 }
0325 
0326 void VideoTextEdit::updateLineNumberArea(const QRect &rect, int dy)
0327 {
0328     if (dy)
0329         lineNumberArea->scroll(0, dy);
0330     else
0331         lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
0332 }
0333 
0334 void VideoTextEdit::resizeEvent(QResizeEvent *e)
0335 {
0336     QTextEdit::resizeEvent(e);
0337     QRect cr = contentsRect();
0338     lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
0339 }
0340 
0341 void VideoTextEdit::keyPressEvent(QKeyEvent *e)
0342 {
0343     QTextEdit::keyPressEvent(e);
0344 }
0345 
0346 void VideoTextEdit::checkHoverBlock(int yPos)
0347 {
0348     QTextCursor curs = QTextCursor(this->document());
0349     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0350 
0351     m_hoveredBlock = -1;
0352     for (int i = 0; i < this->document()->blockCount(); ++i) {
0353         QTextBlock block = curs.block();
0354         QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(0, 0 - (this->verticalScrollBar()->sliderPosition())).toRect();
0355         if (yPos < r2.x()) {
0356             break;
0357         }
0358         if (yPos > r2.x() && yPos < r2.bottom()) {
0359             m_hoveredBlock = i;
0360             break;
0361         }
0362         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0363     }
0364     setCursor(m_hoveredBlock == -1 ? Qt::ArrowCursor : Qt::PointingHandCursor);
0365     lineNumberArea->update();
0366 }
0367 
0368 void VideoTextEdit::blockClicked(Qt::KeyboardModifiers modifiers, bool play)
0369 {
0370     if (m_hoveredBlock > -1 && m_hoveredBlock < speechZones.count()) {
0371         if (m_selectedBlocks.contains(m_hoveredBlock)) {
0372             if (modifiers & Qt::ControlModifier) {
0373                 // remove from selection on ctrl+click an already selected block
0374                 m_selectedBlocks.removeAll(m_hoveredBlock);
0375             } else {
0376                 m_selectedBlocks = {m_hoveredBlock};
0377                 lineNumberArea->update();
0378             }
0379         } else {
0380             // Add to selection
0381             if (modifiers & Qt::ControlModifier) {
0382                 m_selectedBlocks << m_hoveredBlock;
0383             } else if (modifiers & Qt::ShiftModifier) {
0384                 if (m_lastClickedBlock > -1) {
0385                     for (int i = qMin(m_lastClickedBlock, m_hoveredBlock); i <= qMax(m_lastClickedBlock, m_hoveredBlock); i++) {
0386                         if (!m_selectedBlocks.contains(i)) {
0387                             m_selectedBlocks << i;
0388                         }
0389                     }
0390                 } else {
0391                     m_selectedBlocks = {m_hoveredBlock};
0392                 }
0393             } else {
0394                 m_selectedBlocks = {m_hoveredBlock};
0395             }
0396         }
0397         if (m_hoveredBlock >= 0) {
0398             m_lastClickedBlock = m_hoveredBlock;
0399         }
0400 
0401         // Find continuous block selection
0402         int startBlock = m_hoveredBlock;
0403         int endBlock = m_hoveredBlock;
0404         while (m_selectedBlocks.contains(startBlock)) {
0405             startBlock--;
0406         }
0407         if (!m_selectedBlocks.contains(startBlock)) {
0408             startBlock++;
0409         }
0410         while (m_selectedBlocks.contains(endBlock)) {
0411             endBlock++;
0412         }
0413         if (!m_selectedBlocks.contains(endBlock)) {
0414             endBlock--;
0415         }
0416         QPair<double, double> zone = {speechZones.at(startBlock).first, speechZones.at(endBlock).second};
0417         double startMs = zone.first;
0418         double endMs = zone.second;
0419         pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
0420         pCore->getMonitor(Kdenlive::ClipMonitor)
0421             ->slotLoadClipZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())));
0422         QTextCursor cursor = textCursor();
0423         cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0424         cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, m_hoveredBlock);
0425         cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
0426         setTextCursor(cursor);
0427         if (play) {
0428             pCore->getMonitor(Kdenlive::ClipMonitor)->slotPlayZone();
0429         }
0430     }
0431 }
0432 
0433 int VideoTextEdit::getFirstVisibleBlockId()
0434 {
0435     // Detect the first block for which bounding rect - once
0436     // translated in absolute coordinates - is contained
0437     // by the editor's text area
0438 
0439     // Costly way of doing but since
0440     // "blockBoundingGeometry(...)" doesn't exist
0441     // for "QTextEdit"...
0442 
0443     QTextCursor curs = QTextCursor(this->document());
0444     curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
0445     for (int i = 0; i < this->document()->blockCount(); ++i) {
0446         QTextBlock block = curs.block();
0447 
0448         QRect r1 = this->viewport()->geometry();
0449         QRect r2 =
0450             this->document()->documentLayout()->blockBoundingRect(block).translated(r1.x(), r1.y() - (this->verticalScrollBar()->sliderPosition())).toRect();
0451 
0452         if (r1.contains(r2, true)) {
0453             return i;
0454         }
0455 
0456         curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
0457     }
0458     return 0;
0459 }
0460 
0461 void VideoTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
0462 {
0463     this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition());
0464 
0465     QPainter painter(lineNumberArea);
0466     painter.fillRect(event->rect(), palette().alternateBase().color());
0467     int blockNumber = this->getFirstVisibleBlockId();
0468 
0469     QTextBlock block = this->document()->findBlockByNumber(blockNumber);
0470     QTextBlock prev_block = (blockNumber > 0) ? this->document()->findBlockByNumber(blockNumber - 1) : block;
0471     int translate_y = (blockNumber > 0) ? -this->verticalScrollBar()->sliderPosition() : 0;
0472 
0473     int top = this->viewport()->geometry().top();
0474 
0475     // Adjust text position according to the previous "non entirely visible" block
0476     // if applicable. Also takes in consideration the document's margin offset.
0477     int additional_margin;
0478     if (blockNumber == 0)
0479         // Simply adjust to document's margin
0480         additional_margin = int(this->document()->documentMargin()) - 1 - this->verticalScrollBar()->sliderPosition();
0481     else
0482         // Getting the height of the visible part of the previous "non entirely visible" block
0483         additional_margin = int(
0484             this->document()->documentLayout()->blockBoundingRect(prev_block).translated(0, translate_y).intersected(this->viewport()->geometry()).height());
0485 
0486     // Shift the starting point
0487     top += additional_margin;
0488 
0489     int bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
0490 
0491     QColor col_2 = palette().link().color();
0492     QColor col_1 = palette().highlightedText().color();
0493     QColor col_0 = palette().text().color();
0494 
0495     // Draw the numbers (displaying the current line number in green)
0496     while (block.isValid() && top <= event->rect().bottom()) {
0497         if (blockNumber >= speechZones.count()) {
0498             break;
0499         }
0500         if (block.isVisible() && bottom >= event->rect().top()) {
0501             if (m_selectedBlocks.contains(blockNumber)) {
0502                 painter.fillRect(QRect(0, top, lineNumberArea->width(), bottom - top), palette().highlight().color());
0503                 painter.setPen(col_1);
0504             } else {
0505                 painter.setPen((this->textCursor().blockNumber() == blockNumber) ? col_2 : col_0);
0506             }
0507             QString number = pCore->timecode().getDisplayTimecode(GenTime(speechZones[blockNumber].first), false);
0508             painter.drawText(-5, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number);
0509         }
0510         painter.setPen(palette().dark().color());
0511         painter.drawLine(0, bottom, width(), bottom);
0512         block = block.next();
0513         top = bottom;
0514         bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height());
0515         ++blockNumber;
0516     }
0517 }
0518 
0519 void VideoTextEdit::contextMenuEvent(QContextMenuEvent *event)
0520 {
0521     QMenu *menu = createStandardContextMenu();
0522     menu->addAction(bookmarkAction);
0523     menu->addAction(deleteAction);
0524     menu->exec(event->globalPos());
0525     delete menu;
0526 }
0527 
0528 void VideoTextEdit::mousePressEvent(QMouseEvent *e)
0529 {
0530     if (e->buttons() & Qt::LeftButton) {
0531         QTextCursor current = textCursor();
0532         QTextCursor cursor = cursorForPosition(e->pos());
0533         int pos = cursor.position();
0534         qDebug() << "=== CLICKED AT: " << pos << ", SEL: " << current.selectionStart() << "-" << current.selectionEnd();
0535         if (pos > current.selectionStart() && pos < current.selectionEnd()) {
0536             // Clicked in selection
0537             e->ignore();
0538             qDebug() << "=== IGNORING MOUSE CLICK";
0539             return;
0540         } else {
0541             QTextEdit::mousePressEvent(e);
0542             const QString link = anchorAt(e->pos());
0543             if (!link.isEmpty()) {
0544                 // Clicked on a word
0545                 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
0546                 double startMs = link.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
0547                 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps()));
0548             }
0549         }
0550         setTextCursor(cursor);
0551     } else {
0552         QTextEdit::mousePressEvent(e);
0553     }
0554 }
0555 
0556 void VideoTextEdit::mouseReleaseEvent(QMouseEvent *e)
0557 {
0558     QTextEdit::mouseReleaseEvent(e);
0559     if (e->button() == Qt::LeftButton) {
0560         QTextCursor cursor = textCursor();
0561         if (!cursor.selectedText().isEmpty()) {
0562             // We have a selection, ensure full word is selected
0563             int start = cursor.selectionStart();
0564             int end = cursor.selectionEnd();
0565             if (document()->characterAt(end - 1) == QLatin1Char(' ')) {
0566                 // Selection ends with a space
0567                 end--;
0568             }
0569             QTextBlock bk = cursor.block();
0570             if (bk.text().simplified() == i18n("No speech")) {
0571                 // This is a silence block, select all
0572                 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
0573                 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
0574             } else {
0575                 cursor.setPosition(start);
0576                 cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
0577                 cursor.setPosition(end, QTextCursor::KeepAnchor);
0578                 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0579             }
0580             setTextCursor(cursor);
0581         }
0582         if (!m_selectedBlocks.isEmpty()) {
0583             m_selectedBlocks.clear();
0584             repaintLines();
0585         }
0586     } else {
0587         qDebug() << "==== NO LEFT CLICK!";
0588     }
0589 }
0590 
0591 void VideoTextEdit::mouseMoveEvent(QMouseEvent *e)
0592 {
0593     QTextEdit::mouseMoveEvent(e);
0594     if (e->buttons() & Qt::LeftButton) {
0595         /*QTextCursor cursor = textCursor();
0596         cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
0597         setTextCursor(cursor);*/
0598     } else {
0599         const QString link = anchorAt(e->pos());
0600         viewport()->setCursor(link.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor);
0601     }
0602 }
0603 
0604 void VideoTextEdit::wheelEvent(QWheelEvent *e)
0605 {
0606     QTextEdit::wheelEvent(e);
0607 }
0608 
0609 TextBasedEdit::TextBasedEdit(QWidget *parent)
0610     : QWidget(parent)
0611     , m_stt(nullptr)
0612 {
0613     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0614     setupUi(this);
0615     setFocusPolicy(Qt::StrongFocus);
0616     connect(pCore.get(), &Core::speechEngineChanged, this, &TextBasedEdit::updateEngine);
0617 
0618     // Settings menu
0619     QMenu *menu = new QMenu(this);
0620     m_translateAction = new QAction(i18n("Translate to English"), this);
0621     m_translateAction->setCheckable(true);
0622     menu->addAction(m_translateAction);
0623     QAction *configAction = new QAction(i18n("Configure Speech Recognition"), this);
0624     menu->addAction(configAction);
0625     button_config->setMenu(menu);
0626     button_config->setIcon(QIcon::fromTheme(QStringLiteral("application-menu")));
0627     connect(m_translateAction, &QAction::triggered, [](bool enabled) { KdenliveSettings::setWhisperTranslate(enabled); });
0628     connect(configAction, &QAction::triggered, []() { pCore->window()->slotShowPreferencePage(Kdenlive::PageSpeech); });
0629     connect(menu, &QMenu::aboutToShow, [this]() {
0630         m_translateAction->setChecked(KdenliveSettings::whisperTranslate());
0631         m_translateAction->setEnabled(KdenliveSettings::speechEngine() == QLatin1String("whisper"));
0632     });
0633 
0634     m_voskConfig = new QAction(i18n("Configure"), this);
0635     connect(m_voskConfig, &QAction::triggered, []() { pCore->window()->slotShowPreferencePage(Kdenlive::PageSpeech); });
0636 
0637     // Visual text editor
0638     auto *l = new QVBoxLayout;
0639     l->setContentsMargins(0, 0, 0, 0);
0640     m_visualEditor = new VideoTextEdit(this);
0641     m_visualEditor->installEventFilter(this);
0642     l->addWidget(m_visualEditor);
0643     text_frame->setLayout(l);
0644     m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0645     // m_document = m_visualEditor->document();
0646     // m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0647     m_visualEditor->setDocument(&m_document);
0648     connect(&m_document, &QTextDocument::blockCountChanged, this, [this](int ct) {
0649         m_visualEditor->repaintLines();
0650         qDebug() << "++++++++++++++++++++\n\nGOT BLOCKS: " << ct << "\n\n+++++++++++++++++++++";
0651     });
0652 
0653     QMenu *insertMenu = new QMenu(this);
0654     QAction *createSequence = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Create new sequence with edit"), this);
0655     QAction *insertSelection = new QAction(QIcon::fromTheme(QStringLiteral("timeline-insert")), i18n("Insert selection in timeline"), this);
0656     QAction *saveAsPlaylist = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save edited text in a playlist file"), this);
0657     insertMenu->addAction(createSequence);
0658     insertMenu->addAction(insertSelection);
0659     insertMenu->addSeparator();
0660     insertMenu->addAction(saveAsPlaylist);
0661     button_insert->setMenu(insertMenu);
0662     button_insert->setDefaultAction(createSequence);
0663     button_insert->setToolTip(i18n("Create new sequence with text edit "));
0664 
0665     connect(createSequence, &QAction::triggered, this, &TextBasedEdit::createSequence);
0666     connect(insertSelection, &QAction::triggered, this, &TextBasedEdit::insertToTimeline);
0667     connect(saveAsPlaylist, &QAction::triggered, this, [&]() { previewPlaylist(true); });
0668     insertSelection->setEnabled(false);
0669 
0670     connect(m_visualEditor, &VideoTextEdit::selectionChanged, this, [this, insertSelection]() {
0671         bool hasSelection = m_visualEditor->textCursor().selectedText().simplified().isEmpty() == false;
0672         m_visualEditor->bookmarkAction->setEnabled(hasSelection);
0673         m_visualEditor->deleteAction->setEnabled(hasSelection);
0674         insertSelection->setEnabled(hasSelection);
0675     });
0676 
0677     button_start->setEnabled(false);
0678     connect(button_start, &QPushButton::clicked, this, &TextBasedEdit::startRecognition);
0679     frame_progress->setVisible(false);
0680     connect(button_abort, &QToolButton::clicked, this, [this]() {
0681         if (m_speechJob && m_speechJob->state() == QProcess::Running) {
0682             m_speechJob->kill();
0683         } else if (m_tCodeJob && m_tCodeJob->state() == QProcess::Running) {
0684             m_tCodeJob->kill();
0685         }
0686     });
0687     language_box->setToolTip(i18n("Speech model"));
0688     speech_language->setToolTip(i18n("Speech language"));
0689     connect(pCore.get(), &Core::voskModelUpdate, this, [&](const QStringList &models) {
0690         if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
0691             return;
0692         }
0693         language_box->clear();
0694         language_box->addItems(models);
0695 
0696         if (models.isEmpty()) {
0697             showMessage(i18n("Please install speech recognition models"), KMessageWidget::Information, m_voskConfig);
0698         } else {
0699             if (!KdenliveSettings::vosk_text_model().isEmpty() && models.contains(KdenliveSettings::vosk_text_model())) {
0700                 int ix = language_box->findText(KdenliveSettings::vosk_text_model());
0701                 if (ix > -1) {
0702                     language_box->setCurrentIndex(ix);
0703                 }
0704             }
0705         }
0706     });
0707     connect(language_box, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [this]() {
0708         if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
0709             const QString modelName = language_box->currentData().toString();
0710             speech_language->setEnabled(!modelName.endsWith(QLatin1String(".en")));
0711             KdenliveSettings::setWhisperModel(modelName);
0712         } else {
0713             KdenliveSettings::setVosk_text_model(language_box->currentText());
0714         }
0715     });
0716     info_message->hide();
0717     updateEngine();
0718 
0719     m_logAction = new QAction(i18n("Show log"), this);
0720     connect(m_logAction, &QAction::triggered, this, [this]() { KMessageBox::error(this, m_errorString, i18n("Detailed log")); });
0721 
0722     speech_zone->setChecked(KdenliveSettings::speech_zone());
0723     connect(speech_zone, &QCheckBox::stateChanged, [](int state) { KdenliveSettings::setSpeech_zone(state == Qt::Checked); });
0724     button_delete->setDefaultAction(m_visualEditor->deleteAction);
0725     button_delete->setToolTip(i18n("Delete selected text"));
0726     connect(m_visualEditor->deleteAction, &QAction::triggered, this, &TextBasedEdit::deleteItem);
0727 
0728     button_bookmark->setDefaultAction(m_visualEditor->bookmarkAction);
0729     button_bookmark->setToolTip(i18n("Add marker for current selection"));
0730     connect(m_visualEditor->bookmarkAction, &QAction::triggered, this, &TextBasedEdit::addBookmark);
0731 
0732     // Zoom
0733     QAction *zoomIn = new QAction(QIcon::fromTheme(QStringLiteral("zoom-in")), i18n("Zoom In"), this);
0734     connect(zoomIn, &QAction::triggered, this, &TextBasedEdit::slotZoomIn);
0735     QAction *zoomOut = new QAction(QIcon::fromTheme(QStringLiteral("zoom-out")), i18n("Zoom Out"), this);
0736     connect(zoomOut, &QAction::triggered, this, &TextBasedEdit::slotZoomOut);
0737     QAction *removeSilence = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove non speech zones"), this);
0738     connect(removeSilence, &QAction::triggered, m_visualEditor, &VideoTextEdit::slotRemoveSilence);
0739     // Build menu
0740     QMenu *extraMenu = new QMenu(this);
0741     extraMenu->addAction(zoomIn);
0742     extraMenu->addAction(zoomOut);
0743     extraMenu->addSeparator();
0744     extraMenu->addAction(removeSilence);
0745     subMenu->setMenu(extraMenu);
0746 
0747     // Message Timer
0748     m_hideTimer.setSingleShot(true);
0749     m_hideTimer.setInterval(5000);
0750     connect(&m_hideTimer, &QTimer::timeout, info_message, &KMessageWidget::animatedHide);
0751 
0752     // Search stuff
0753     search_frame->setVisible(false);
0754     connect(button_search, &QToolButton::toggled, this, [&](bool toggled) {
0755         search_frame->setVisible(toggled);
0756         search_line->setFocus();
0757     });
0758     connect(search_line, &QLineEdit::textChanged, this, [this](const QString &searchText) {
0759         QPalette palette = this->palette();
0760         QColor col = palette.color(QPalette::Base);
0761         if (searchText.length() > 2) {
0762             bool found = m_visualEditor->find(searchText);
0763             if (found) {
0764                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0765                 palette.setColor(QPalette::Base, col);
0766                 QTextCursor cur = m_visualEditor->textCursor();
0767                 cur.select(QTextCursor::WordUnderCursor);
0768                 m_visualEditor->setTextCursor(cur);
0769             } else {
0770                 // Loop over, abort
0771                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0772                 palette.setColor(QPalette::Base, col);
0773             }
0774         }
0775         search_line->setPalette(palette);
0776     });
0777     connect(search_next, &QToolButton::clicked, this, [this]() {
0778         const QString searchText = search_line->text();
0779         QPalette palette = this->palette();
0780         QColor col = palette.color(QPalette::Base);
0781         if (searchText.length() > 2) {
0782             bool found = m_visualEditor->find(searchText);
0783             if (found) {
0784                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0785                 palette.setColor(QPalette::Base, col);
0786                 QTextCursor cur = m_visualEditor->textCursor();
0787                 cur.select(QTextCursor::WordUnderCursor);
0788                 m_visualEditor->setTextCursor(cur);
0789             } else {
0790                 // Loop over, abort
0791                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0792                 palette.setColor(QPalette::Base, col);
0793             }
0794         }
0795         search_line->setPalette(palette);
0796     });
0797     connect(search_prev, &QToolButton::clicked, this, [this]() {
0798         const QString searchText = search_line->text();
0799         QPalette palette = this->palette();
0800         QColor col = palette.color(QPalette::Base);
0801         if (searchText.length() > 2) {
0802             bool found = m_visualEditor->find(searchText, QTextDocument::FindBackward);
0803             if (found) {
0804                 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5)));
0805                 palette.setColor(QPalette::Base, col);
0806                 QTextCursor cur = m_visualEditor->textCursor();
0807                 cur.select(QTextCursor::WordUnderCursor);
0808                 m_visualEditor->setTextCursor(cur);
0809             } else {
0810                 // Loop over, abort
0811                 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5)));
0812                 palette.setColor(QPalette::Base, col);
0813             }
0814         }
0815         search_line->setPalette(palette);
0816     });
0817 }
0818 
0819 void TextBasedEdit::slotZoomIn()
0820 {
0821     QTextCursor cursor = m_visualEditor->textCursor();
0822     m_visualEditor->selectAll();
0823     qreal fontSize = QFontInfo(m_visualEditor->currentFont()).pointSizeF() * 1.2;
0824     KdenliveSettings::setSubtitleEditFontSize(fontSize);
0825     m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0826     m_visualEditor->setTextCursor(cursor);
0827 }
0828 
0829 void TextBasedEdit::slotZoomOut()
0830 {
0831     QTextCursor cursor = m_visualEditor->textCursor();
0832     m_visualEditor->selectAll();
0833     qreal fontSize = QFontInfo(m_visualEditor->currentFont()).pointSizeF() / 1.2;
0834     fontSize = qMax(fontSize, QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pointSizeF());
0835     KdenliveSettings::setSubtitleEditFontSize(fontSize);
0836     m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0837     m_visualEditor->setTextCursor(cursor);
0838 }
0839 
0840 void TextBasedEdit::applyFontSize()
0841 {
0842     if (KdenliveSettings::subtitleEditFontSize() > 0) {
0843         QTextCursor cursor = m_visualEditor->textCursor();
0844         m_visualEditor->selectAll();
0845         m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0846         m_visualEditor->setTextCursor(cursor);
0847     }
0848 }
0849 
0850 void TextBasedEdit::updateEngine()
0851 {
0852     delete m_stt;
0853     if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
0854         m_stt = new SpeechToText(SpeechToText::EngineType::EngineWhisper, this);
0855         language_box->clear();
0856         QList<std::pair<QString, QString>> whisperModels = m_stt->whisperModels();
0857         for (auto &w : whisperModels) {
0858             language_box->addItem(w.first, w.second);
0859         }
0860         int ix = language_box->findData(KdenliveSettings::whisperModel());
0861         if (ix > -1) {
0862             language_box->setCurrentIndex(ix);
0863         }
0864         if (speech_language->count() == 0) {
0865             // Fill whisper languages
0866             QMap<QString, QString> languages = m_stt->whisperLanguages();
0867             QMapIterator<QString, QString> j(languages);
0868             while (j.hasNext()) {
0869                 j.next();
0870                 speech_language->addItem(j.key(), j.value());
0871             }
0872             int ix = speech_language->findData(KdenliveSettings::whisperLanguage());
0873             if (ix > -1) {
0874                 speech_language->setCurrentIndex(ix);
0875             }
0876         }
0877         speech_language->setEnabled(!KdenliveSettings::whisperModel().endsWith(QLatin1String(".en")));
0878         speech_language->setVisible(true);
0879     } else {
0880         // VOSK
0881         speech_language->setVisible(false);
0882         m_stt = new SpeechToText(SpeechToText::EngineType::EngineVosk, this);
0883         language_box->clear();
0884         m_stt->parseVoskDictionaries();
0885     }
0886 }
0887 
0888 TextBasedEdit::~TextBasedEdit()
0889 {
0890     if (m_speechJob && m_speechJob->state() == QProcess::Running) {
0891         m_speechJob->kill();
0892         m_speechJob->waitForFinished();
0893     }
0894 }
0895 
0896 bool TextBasedEdit::eventFilter(QObject *obj, QEvent *event)
0897 {
0898     if (event->type() == QEvent::KeyPress) {
0899         qDebug() << "==== FOT TXTEDIT EVENT FILTER: " << static_cast<QKeyEvent *>(event)->key();
0900     }
0901     /*if(obj == m_visualEditor && event->type() == QEvent::KeyPress)
0902     {
0903         QKeyEvent *keyEvent = static_cast <QKeyEvent*> (event);
0904         if (keyEvent->key() != Qt::Key_Left && keyEvent->key() != Qt::Key_Up && keyEvent->key() != Qt::Key_Right && keyEvent->key() != Qt::Key_Down) {
0905             parentWidget()->setFocus();
0906             return true;
0907         }
0908     }*/
0909     return QObject::eventFilter(obj, event);
0910 }
0911 
0912 void TextBasedEdit::startRecognition()
0913 {
0914     if (m_speechJob && m_speechJob->state() != QProcess::NotRunning) {
0915         if (KMessageBox::questionTwoActions(
0916                 this, i18n("Another recognition job is already running. It will be aborted in favor of the new job. Do you want to proceed?"), {},
0917                 KStandardGuiItem::cont(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
0918             return;
0919         }
0920     }
0921     info_message->hide();
0922     m_errorString.clear();
0923     m_visualEditor->cleanup();
0924     // m_visualEditor->insertHtml(QStringLiteral("<body>"));
0925     m_stt->checkDependencies(false);
0926     QString modelDirectory;
0927     QString language;
0928     QString modelName;
0929     if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
0930         // Whisper engine
0931         if (!m_stt->checkSetup() || !m_stt->missingDependencies({QStringLiteral("openai-whisper")}).isEmpty()) {
0932             showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig);
0933             return;
0934         }
0935         modelName = language_box->currentData().toString();
0936         language = speech_language->isEnabled() && !speech_language->currentData().isNull()
0937                        ? QStringLiteral("language=%1").arg(speech_language->currentData().toString())
0938                        : QString();
0939         if (KdenliveSettings::whisperDisableFP16()) {
0940             language.append(QStringLiteral(" fp16=False"));
0941         }
0942     } else {
0943         // VOSK engine
0944         if (!m_stt->checkSetup() || !m_stt->missingDependencies({QStringLiteral("vosk")}).isEmpty()) {
0945             showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig);
0946             return;
0947         }
0948         // Start python script
0949         modelName = language_box->currentText();
0950         if (modelName.isEmpty()) {
0951             showMessage(i18n("Please install a language model."), KMessageWidget::Warning, m_voskConfig);
0952             return;
0953         }
0954         modelDirectory = m_stt->voskModelPath();
0955     }
0956     m_binId = pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId();
0957     std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
0958     if (clip == nullptr) {
0959         showMessage(i18n("Select a clip with audio in Project Bin."), KMessageWidget::Information);
0960         return;
0961     }
0962 
0963     m_speechJob = std::make_unique<QProcess>(this);
0964     showMessage(i18n("Starting speech recognition"), KMessageWidget::Information);
0965     qApp->processEvents();
0966 
0967     m_sourceUrl.clear();
0968     QString clipName;
0969     m_clipOffset = 0;
0970     m_lastPosition = 0;
0971     double endPos = 0;
0972     bool hasAudio = false;
0973     if (clip->itemType() == AbstractProjectItem::ClipItem) {
0974         std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
0975         if (clipItem) {
0976             m_sourceUrl = clipItem->url();
0977             clipName = clipItem->clipName();
0978             hasAudio = clipItem->hasAudio();
0979             if (speech_zone->isChecked()) {
0980                 // Analyse clip zone only
0981                 QPoint zone = clipItem->zone();
0982                 m_lastPosition = zone.x();
0983                 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
0984                 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
0985                 endPos = m_clipDuration;
0986             } else {
0987                 m_clipDuration = clipItem->duration().seconds();
0988             }
0989         }
0990     } else if (clip->itemType() == AbstractProjectItem::SubClipItem) {
0991         std::shared_ptr<ProjectSubClip> clipItem = std::static_pointer_cast<ProjectSubClip>(clip);
0992         if (clipItem) {
0993             auto master = clipItem->getMasterClip();
0994             m_sourceUrl = master->url();
0995             hasAudio = master->hasAudio();
0996             clipName = master->clipName();
0997             QPoint zone = clipItem->zone();
0998             m_lastPosition = zone.x();
0999             m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds();
1000             m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds();
1001             endPos = m_clipDuration;
1002         }
1003     }
1004     if (m_sourceUrl.isEmpty() || !hasAudio) {
1005         showMessage(i18n("Select a clip with audio for speech recognition."), KMessageWidget::Information);
1006         return;
1007     }
1008     clipNameLabel->setText(clipName);
1009     if (clip->clipType() == ClipType::Playlist) {
1010         // We need to extract audio first
1011         m_playlistWav.remove();
1012         m_playlistWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav")));
1013         if (!m_playlistWav.open()) {
1014             showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning);
1015             return;
1016         }
1017         m_playlistWav.close();
1018 
1019         showMessage(i18n("Extracting audio for %1.", clipName), KMessageWidget::Information);
1020         qApp->processEvents();
1021         m_tCodeJob = std::make_unique<QProcess>(this);
1022         connect(m_tCodeJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
1023                 [this, modelName, language, clipName, modelDirectory, endPos](int code, QProcess::ExitStatus status) {
1024                     Q_UNUSED(code)
1025                     qDebug() << "++++++++++++++++++++++ TCODE JOB FINISHED\n";
1026                     if (status == QProcess::CrashExit) {
1027                         showMessage(i18n("Audio extract failed."), KMessageWidget::Warning);
1028                         speech_progress->setValue(0);
1029                         frame_progress->setVisible(false);
1030                         m_playlistWav.remove();
1031                         return;
1032                     }
1033                     showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
1034                     qApp->processEvents();
1035                     connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
1036                     connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
1037                     connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
1038                             [this](int code, QProcess::ExitStatus status) {
1039                                 m_playlistWav.remove();
1040                                 slotProcessSpeechStatus(code, status);
1041                             });
1042                     qDebug() << "::: STARTING SPEECH: " << modelDirectory << " / " << modelName << " / " << language;
1043                     if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
1044                         // Whisper
1045                         connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessWhisperSpeech);
1046                         if (speech_zone->isChecked()) {
1047                             m_tmpCutWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav")));
1048                             if (!m_tmpCutWav.open()) {
1049                                 showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning);
1050                                 return;
1051                             }
1052                             m_tmpCutWav.close();
1053                             m_speechJob->start(m_stt->pythonExec(),
1054                                                {m_stt->speechScript(), m_playlistWav.fileName(), modelName, KdenliveSettings::whisperDevice(),
1055                                                 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language,
1056                                                 QString::number(m_clipOffset), QString::number(endPos), m_tmpCutWav.fileName()});
1057                         } else {
1058                             m_speechJob->start(m_stt->pythonExec(),
1059                                                {m_stt->speechScript(), m_playlistWav.fileName(), modelName, KdenliveSettings::whisperDevice(),
1060                                                 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language});
1061                         }
1062                     } else {
1063                         m_speechJob->start(m_stt->pythonExec(), {m_stt->speechScript(), modelDirectory, modelName, m_playlistWav.fileName(),
1064                                                                  QString::number(m_clipOffset), QString::number(endPos)});
1065                     }
1066                     speech_progress->setValue(0);
1067                     frame_progress->setVisible(true);
1068                 });
1069         connect(m_tCodeJob.get(), &QProcess::readyReadStandardOutput, this, [this]() {
1070             QString saveData = QString::fromUtf8(m_tCodeJob->readAllStandardOutput());
1071             qDebug() << "+GOT OUTPUT: " << saveData;
1072             saveData = saveData.section(QStringLiteral("percentage:"), 1).simplified();
1073             int percent = saveData.section(QLatin1Char(' '), 0, 0).toInt();
1074             speech_progress->setValue(percent);
1075         });
1076         m_tCodeJob->start(KdenliveSettings::meltpath(),
1077                           {QStringLiteral("-progress"), m_sourceUrl, QStringLiteral("-consumer"), QString("avformat:%1").arg(m_playlistWav.fileName()),
1078                            QStringLiteral("vn=1"), QStringLiteral("ar=16000")});
1079         speech_progress->setValue(0);
1080         frame_progress->setVisible(true);
1081     } else {
1082         showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information);
1083         qApp->processEvents();
1084         connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError);
1085         connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
1086                 &TextBasedEdit::slotProcessSpeechStatus);
1087         button_insert->setEnabled(false);
1088         if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
1089             // Whisper
1090             qDebug() << "=== STARTING Whisper reco: " << m_stt->speechScript() << " / " << language_box->currentData() << " / "
1091                      << KdenliveSettings::whisperDevice() << " / "
1092                      << (KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe")) << " / " << m_sourceUrl
1093                      << ", START: " << m_clipOffset << ", DUR: " << endPos << " / " << language;
1094             connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessWhisperSpeech);
1095             if (speech_zone->isChecked()) {
1096                 m_tmpCutWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav")));
1097                 if (!m_tmpCutWav.open()) {
1098                     showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning);
1099                     return;
1100                 }
1101                 m_tmpCutWav.close();
1102                 m_speechJob->start(m_stt->pythonExec(), {m_stt->speechScript(), m_sourceUrl, modelName, KdenliveSettings::whisperDevice(),
1103                                                          KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"),
1104                                                          language, QString::number(m_clipOffset), QString::number(endPos), m_tmpCutWav.fileName()});
1105             } else {
1106                 m_speechJob->start(m_stt->pythonExec(),
1107                                    {m_stt->speechScript(), m_sourceUrl, modelName, KdenliveSettings::whisperDevice(),
1108                                     KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language});
1109             }
1110         } else {
1111             // VOSK
1112             qDebug() << "=== STARTING RECO: " << m_stt->speechScript() << " / " << modelDirectory << " / " << modelName << " / " << m_sourceUrl
1113                      << ", START: " << m_clipOffset << ", DUR: " << endPos;
1114             connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech);
1115             m_speechJob->start(m_stt->pythonExec(),
1116                                {m_stt->speechScript(), modelDirectory, modelName, m_sourceUrl, QString::number(m_clipOffset), QString::number(endPos)});
1117         }
1118         speech_progress->setValue(0);
1119         frame_progress->setVisible(true);
1120     }
1121 }
1122 
1123 void TextBasedEdit::slotProcessSpeechStatus(int, QProcess::ExitStatus status)
1124 {
1125     m_tmpCutWav.remove();
1126     if (status == QProcess::CrashExit) {
1127         showMessage(i18n("Speech recognition aborted."), KMessageWidget::Warning, m_errorString.isEmpty() ? nullptr : m_logAction);
1128     } else if (m_visualEditor->toPlainText().isEmpty()) {
1129         if (m_errorString.contains(QStringLiteral("ModuleNotFoundError"))) {
1130             showMessage(i18n("Error, please check the speech to text configuration."), KMessageWidget::Warning, m_voskConfig);
1131         } else {
1132             showMessage(i18n("No speech detected."), KMessageWidget::Information, m_errorString.isEmpty() ? nullptr : m_logAction);
1133         }
1134     } else {
1135         // Last empty object - no speech detected
1136         if (KdenliveSettings::speechEngine() != QLatin1String("whisper")) {
1137             // VOSK
1138             GenTime silenceStart(m_lastPosition + 1, pCore->getCurrentFps());
1139             if (silenceStart.seconds() < m_clipDuration + m_clipOffset) {
1140                 m_visualEditor->moveCursor(QTextCursor::End);
1141                 QTextCursor cursor = m_visualEditor->textCursor();
1142                 QTextCharFormat fmt = cursor.charFormat();
1143                 fmt.setAnchorHref(QString("%1#%2:%3").arg(m_binId).arg(silenceStart.seconds()).arg(GenTime(m_clipDuration + m_clipOffset).seconds()));
1144                 fmt.setAnchor(true);
1145                 cursor.insertText(i18n("No speech"), fmt);
1146                 m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1147                 m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), GenTime(m_clipDuration + m_clipOffset).seconds());
1148                 m_visualEditor->repaintLines();
1149             }
1150         }
1151 
1152         button_insert->setEnabled(true);
1153         showMessage(i18n("Speech recognition finished."), KMessageWidget::Positive);
1154         // Store speech analysis in clip properties
1155         std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
1156         if (clip) {
1157             std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
1158             QString oldSpeech;
1159             if (clipItem) {
1160                 oldSpeech = clipItem->getProducerProperty(QStringLiteral("kdenlive:speech"));
1161             }
1162             QMap<QString, QString> oldProperties;
1163             oldProperties.insert(QStringLiteral("kdenlive:speech"), oldSpeech);
1164             QMap<QString, QString> properties;
1165             properties.insert(QStringLiteral("kdenlive:speech"), m_visualEditor->toHtml());
1166             pCore->bin()->slotEditClipCommand(m_binId, oldProperties, properties);
1167         }
1168     }
1169     QTextCursor cur = m_visualEditor->textCursor();
1170     cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
1171     m_visualEditor->setTextCursor(cur);
1172     frame_progress->setVisible(false);
1173     applyFontSize();
1174 }
1175 
1176 void TextBasedEdit::slotProcessSpeechError()
1177 {
1178     const QString log = QString::fromUtf8(m_speechJob->readAllStandardError());
1179     if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) {
1180         if (log.contains(QStringLiteral("%|"))) {
1181             int prog = log.section(QLatin1Char('%'), 0, 0).toInt();
1182             speech_progress->setValue(prog);
1183         }
1184     }
1185     m_errorString.append(log);
1186 }
1187 
1188 void TextBasedEdit::slotProcessWhisperSpeech()
1189 {
1190     const QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput());
1191     QStringList sentences = saveData.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
1192     QString sentenceTimings = sentences.takeFirst();
1193     if (!sentenceTimings.startsWith(QLatin1Char('['))) {
1194         // This is not a timing output
1195         if (sentenceTimings.startsWith(QStringLiteral("Detected "))) {
1196             showMessage(sentenceTimings, KMessageWidget::Information);
1197         }
1198         return;
1199     }
1200     QPair<double, double> sentenceZone;
1201     sentenceZone.first = sentenceTimings.section(QLatin1Char('['), 1).section(QLatin1Char('>'), 0, 0).toDouble() + m_clipOffset;
1202     sentenceZone.second = sentenceTimings.section(QLatin1Char('>'), 1).section(QLatin1Char(']'), 0, 0).toDouble() + m_clipOffset;
1203     QTextCursor cursor = m_visualEditor->textCursor();
1204     QTextCharFormat fmt = cursor.charFormat();
1205     QPair<double, double> wordZone;
1206     GenTime sentenceStart(sentenceZone.first);
1207     if (sentenceStart.frames(pCore->getCurrentFps()) > m_lastPosition + 1) {
1208         // Insert space
1209         GenTime silenceStart(m_lastPosition, pCore->getCurrentFps());
1210         m_visualEditor->moveCursor(QTextCursor::End);
1211         fmt.setAnchorHref(QString("%1#%2:%3")
1212                               .arg(m_binId)
1213                               .arg(silenceStart.seconds())
1214                               .arg(GenTime(sentenceStart.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()));
1215         fmt.setAnchor(true);
1216         cursor.insertText(i18n("No speech"), fmt);
1217         fmt.setAnchor(false);
1218         m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1219         m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(),
1220                                                              GenTime(sentenceStart.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds());
1221     }
1222     for (auto &s : sentences) {
1223         wordZone.first = s.section(QLatin1Char('['), 1).section(QLatin1Char('>'), 0, 0).toDouble() + m_clipOffset;
1224         wordZone.second = s.section(QLatin1Char('>'), 1).section(QLatin1Char(']'), 0, 0).toDouble() + m_clipOffset;
1225         const QString text = s.section(QLatin1Char(']'), 1);
1226         if (text.isEmpty()) {
1227             // new section, insert
1228             m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1229             m_visualEditor->speechZones << sentenceZone;
1230             GenTime lastSentence(sentenceZone.second);
1231             GenTime nextSentence(wordZone.first);
1232             if (nextSentence.frames(pCore->getCurrentFps()) > lastSentence.frames(pCore->getCurrentFps()) + 1) {
1233                 // Insert space
1234                 m_visualEditor->moveCursor(QTextCursor::End);
1235                 fmt.setAnchorHref(QString("%1#%2:%3")
1236                                       .arg(m_binId)
1237                                       .arg(lastSentence.seconds())
1238                                       .arg(GenTime(nextSentence.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()));
1239                 fmt.setAnchor(true);
1240                 cursor.insertText(i18n("No speech"), fmt);
1241                 fmt.setAnchor(false);
1242                 m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1243                 m_visualEditor->speechZones << QPair<double, double>(
1244                     lastSentence.seconds(), GenTime(nextSentence.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds());
1245             }
1246             sentenceZone = wordZone;
1247             continue;
1248         }
1249         fmt.setAnchor(true);
1250         fmt.setAnchorHref(QString("%1#%2:%3").arg(m_binId).arg(wordZone.first).arg(wordZone.second));
1251         cursor.insertText(text, fmt);
1252         fmt.setAnchor(false);
1253         cursor.insertText(QStringLiteral(" "), fmt);
1254     }
1255     m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1256     m_visualEditor->speechZones << sentenceZone;
1257     if (sentenceZone.second < m_clipOffset + m_clipDuration) {
1258     }
1259     m_visualEditor->repaintLines();
1260     qDebug() << ":::  " << saveData;
1261 }
1262 
1263 void TextBasedEdit::slotProcessSpeech()
1264 {
1265     QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput());
1266     qDebug() << "=== GOT DATA:\n" << saveData;
1267     QJsonParseError error;
1268     auto loadDoc = QJsonDocument::fromJson(saveData.toUtf8(), &error);
1269     qDebug() << "===JSON ERROR: " << error.errorString();
1270     QTextCursor cursor = m_visualEditor->textCursor();
1271     QTextCharFormat fmt = cursor.charFormat();
1272     // fmt.setForeground(palette().text().color());
1273     if (loadDoc.isObject()) {
1274         QJsonObject obj = loadDoc.object();
1275         if (!obj.isEmpty()) {
1276             // QString itemText = obj["text"].toString();
1277             bool textFound = false;
1278             QPair<double, double> sentenceZone;
1279             if (obj["result"].isArray()) {
1280                 QJsonArray obj2 = obj["result"].toArray();
1281 
1282                 // Get start time for first word
1283                 QJsonValue val = obj2.first();
1284                 if (val.isObject() && val.toObject().keys().contains("start")) {
1285                     double ms = val.toObject().value("start").toDouble() + m_clipOffset;
1286                     GenTime startPos(ms);
1287                     sentenceZone.first = ms;
1288                     if (startPos.frames(pCore->getCurrentFps()) > m_lastPosition + 1) {
1289                         // Insert space
1290                         GenTime silenceStart(m_lastPosition, pCore->getCurrentFps());
1291                         m_visualEditor->moveCursor(QTextCursor::End);
1292                         fmt.setAnchorHref(QString("%1#%2:%3")
1293                                               .arg(m_binId)
1294                                               .arg(silenceStart.seconds())
1295                                               .arg(GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()));
1296                         fmt.setAnchor(true);
1297                         cursor.insertText(i18n("No speech"), fmt);
1298                         m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1299                         m_visualEditor->speechZones << QPair<double, double>(
1300                             silenceStart.seconds(), GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds());
1301                     }
1302                     val = obj2.last();
1303                     if (val.isObject() && val.toObject().keys().contains("end")) {
1304                         ms = val.toObject().value("end").toDouble() + m_clipOffset;
1305                         sentenceZone.second = ms;
1306                         m_lastPosition = GenTime(ms).frames(pCore->getCurrentFps());
1307                         if (m_clipDuration > 0.) {
1308                             speech_progress->setValue(static_cast<int>(100 * ms / (+m_clipOffset + m_clipDuration)));
1309                         }
1310                     }
1311                 }
1312                 // Store words with their start/end time
1313                 for (const QJsonValue &v : obj2) {
1314                     textFound = true;
1315                     fmt.setAnchor(true);
1316                     fmt.setAnchorHref(QString("%1#%2:%3")
1317                                           .arg(m_binId)
1318                                           .arg(v.toObject().value("start").toDouble() + m_clipOffset)
1319                                           .arg(v.toObject().value("end").toDouble() + m_clipOffset));
1320                     cursor.insertText(v.toObject().value("word").toString(), fmt);
1321                     fmt.setAnchor(false);
1322                     cursor.insertText(QStringLiteral(" "), fmt);
1323                 }
1324             } else {
1325                 // Last empty object - no speech detected
1326             }
1327             if (textFound) {
1328                 if (sentenceZone.second < m_clipOffset + m_clipDuration) {
1329                     m_visualEditor->textCursor().insertBlock(cursor.blockFormat());
1330                 }
1331                 m_visualEditor->speechZones << sentenceZone;
1332             }
1333         }
1334     } else if (loadDoc.isEmpty()) {
1335         qDebug() << "==== EMPTY OBJECT DOC";
1336     }
1337     qDebug() << "==== GOT BLOCKS: " << m_document.blockCount();
1338     qDebug() << "=== LINES: " << m_document.firstBlock().lineCount();
1339     m_visualEditor->repaintLines();
1340 }
1341 
1342 void TextBasedEdit::deleteItem()
1343 {
1344     QTextCursor cursor = m_visualEditor->textCursor();
1345     int start = cursor.selectionStart();
1346     int end = cursor.selectionEnd();
1347     if (end > start) {
1348         QTextDocumentFragment fragment = cursor.selection();
1349         QString anchorStart = m_visualEditor->selectionStartAnchor(cursor, start, end);
1350         cursor.setPosition(end);
1351         bool blockEnd = cursor.atBlockEnd();
1352         cursor = m_visualEditor->textCursor();
1353         QString anchorEnd = m_visualEditor->selectionEndAnchor(cursor, end, start);
1354         qDebug() << "=== FINAL END CUT: " << end;
1355         qDebug() << "=== GOT END ANCHOR: " << cursor.selectedText() << " = " << anchorEnd;
1356         if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) {
1357             double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1358             double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1359             if (startMs < endMs) {
1360                 Fun redo = [this, start, end, startMs, endMs, blockEnd]() {
1361                     QTextCursor tCursor = m_visualEditor->textCursor();
1362                     tCursor.setPosition(start);
1363                     tCursor.setPosition(end, QTextCursor::KeepAnchor);
1364                     m_visualEditor->cutZones << QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()));
1365                     tCursor.removeSelectedText();
1366                     if (blockEnd) {
1367                         tCursor.deleteChar();
1368                     }
1369                     // Reset selection and rebuild line numbers
1370                     m_visualEditor->rebuildZones();
1371                     previewPlaylist(false);
1372                     return true;
1373                 };
1374                 Fun undo = [this, start, fr = fragment, startMs, endMs]() {
1375                     qDebug() << "::: PASTING FRAGMENT: " << fr.toPlainText();
1376                     QTextCursor tCursor = m_visualEditor->textCursor();
1377                     QPoint p(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()));
1378                     int ix = m_visualEditor->cutZones.indexOf(p);
1379                     if (ix > -1) {
1380                         m_visualEditor->cutZones.remove(ix);
1381                     }
1382                     tCursor.setPosition(start);
1383                     tCursor.insertFragment(fr);
1384                     // Reset selection and rebuild line numbers
1385                     m_visualEditor->rebuildZones();
1386                     previewPlaylist(false);
1387                     return true;
1388                 };
1389                 redo();
1390                 pCore->pushUndo(undo, redo, i18nc("@action", "Edit clip text"));
1391             }
1392         }
1393     } else {
1394         QTextCursor curs = m_visualEditor->textCursor();
1395         curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
1396         for (int i = 0; i < m_document.blockCount(); ++i) {
1397             int blockStart = curs.position();
1398             curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor);
1399             int blockEnd = curs.position();
1400             if (blockStart == blockEnd) {
1401                 // Empty block, delete
1402                 curs.select(QTextCursor::BlockUnderCursor);
1403                 curs.removeSelectedText();
1404                 curs.deleteChar();
1405             }
1406             curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor);
1407         }
1408     }
1409 }
1410 
1411 void TextBasedEdit::insertToTimeline()
1412 {
1413     QVector<QPoint> zones = m_visualEditor->getInsertZones();
1414     if (zones.isEmpty()) {
1415         return;
1416     }
1417     Fun undo = []() { return true; };
1418     Fun redo = []() { return true; };
1419     for (auto &zone : zones) {
1420         pCore->window()->getCurrentTimeline()->controller()->insertZone(m_binId, zone, false, undo, redo);
1421     }
1422     pCore->pushUndo(undo, redo, i18nc("@action", "Create sequence clip"));
1423 }
1424 
1425 void TextBasedEdit::previewPlaylist(bool createNew)
1426 {
1427     QVector<QPoint> zones = m_visualEditor->getInsertZones();
1428     if (zones.isEmpty()) {
1429         showMessage(i18n("No text to export"), KMessageWidget::Information);
1430         return;
1431     }
1432     std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
1433     std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
1434     QString sourcePath = clipItem->url();
1435     QMap<QString, QString> properties;
1436     properties.insert(QStringLiteral("kdenlive:baseid"), m_binId);
1437     QStringList playZones;
1438     for (const auto &p : qAsConst(zones)) {
1439         playZones << QString("%1:%2").arg(p.x()).arg(p.y());
1440     }
1441     properties.insert(QStringLiteral("kdenlive:cutzones"), playZones.join(QLatin1Char(';')));
1442     if (createNew) {
1443         int ix = 1;
1444         m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1445         while (QFile::exists(m_playlist)) {
1446             ix++;
1447             m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix);
1448         }
1449         QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(m_playlist), this, i18n("Enter new playlist path"));
1450         if (url.isEmpty()) {
1451             return;
1452         }
1453         m_playlist = url.toLocalFile();
1454     }
1455     if (!m_playlist.isEmpty()) {
1456         pCore->bin()->savePlaylist(m_binId, m_playlist, zones, properties, createNew);
1457         clipNameLabel->setText(QFileInfo(m_playlist).fileName());
1458     }
1459 }
1460 
1461 void TextBasedEdit::createSequence()
1462 {
1463     QVector<QPoint> zones = m_visualEditor->fullExport();
1464     if (zones.isEmpty()) {
1465         showMessage(i18n("No text to export"), KMessageWidget::Information);
1466         return;
1467     }
1468     QVector<QPoint> mergedZones;
1469     int max = zones.count();
1470     int current = 0;
1471     int referenceStart = zones.at(current).x();
1472     int currentEnd = zones.at(current).y();
1473     int nextStart = 0;
1474     current++;
1475     while (current < max) {
1476         nextStart = zones.at(current).x();
1477         if (nextStart == currentEnd || nextStart == currentEnd + 1) {
1478             // Contiguous zones
1479             currentEnd = zones.at(current).y();
1480             current++;
1481             continue;
1482         }
1483         // Insert zone
1484         mergedZones << QPoint(referenceStart, currentEnd);
1485         referenceStart = zones.at(current).x();
1486         currentEnd = zones.at(current).y();
1487         current++;
1488     }
1489     Fun undo = []() { return true; };
1490     Fun redo = []() { return true; };
1491     // Create new timeline sequence
1492     std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId);
1493     std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip);
1494     const QString sequenceId = pCore->bin()->buildSequenceClipWithUndo(undo, redo, -1, -1, clipItem->clipName());
1495     if (sequenceId == QLatin1String("-1")) {
1496         // Aborting
1497         return;
1498     }
1499     for (const auto &p : qAsConst(zones)) {
1500         if (p.y() > p.x()) {
1501             pCore->window()->getCurrentTimeline()->controller()->insertZone(m_binId, p, false, undo, redo);
1502         }
1503     }
1504     pCore->pushUndo(undo, redo, i18nc("@action", "Create sequence clip"));
1505 }
1506 
1507 void TextBasedEdit::showMessage(const QString &text, KMessageWidget::MessageType type, QAction *action)
1508 {
1509     if (m_currentMessageAction != nullptr && (action == nullptr || action != m_currentMessageAction)) {
1510         info_message->removeAction(m_currentMessageAction);
1511         m_currentMessageAction = action;
1512         if (m_currentMessageAction) {
1513             info_message->addAction(m_currentMessageAction);
1514         }
1515     } else if (action) {
1516         m_currentMessageAction = action;
1517         info_message->addAction(m_currentMessageAction);
1518     }
1519 
1520     if (info_message->isVisible()) {
1521         m_hideTimer.stop();
1522     }
1523     info_message->setMessageType(type);
1524     info_message->setText(text);
1525     info_message->animatedShow();
1526     if (type != KMessageWidget::Error && m_currentMessageAction == nullptr) {
1527         m_hideTimer.start();
1528     }
1529 }
1530 
1531 void TextBasedEdit::openClip(std::shared_ptr<ProjectClip> clip)
1532 {
1533     if (m_speechJob && m_speechJob->state() == QProcess::Running) {
1534         // TODO: ask for job cancellation
1535         return;
1536     }
1537     if (clip && clip->isValid() && clip->hasAudio()) {
1538         qDebug() << "====== OPENING CLIP: " << clip->clipName();
1539         QString refId = clip->getProducerProperty(QStringLiteral("kdenlive:baseid"));
1540         if (!refId.isEmpty() && refId == m_refId) {
1541             // We opened a resulting playlist, do not clear text edit
1542             // TODO: this is broken. We should try reading the kdenlive:speech data from the sequence xml
1543             return;
1544         }
1545         if (!m_visualEditor->toPlainText().isEmpty()) {
1546             m_visualEditor->cleanup();
1547         }
1548         QString speech;
1549         QList<QPoint> cutZones;
1550         m_binId = refId.isEmpty() ? clip->binId() : refId;
1551         if (!refId.isEmpty()) {
1552             // this is a clip  playlist with a bin reference, fetch it
1553             m_refId = refId;
1554             std::shared_ptr<ProjectClip> refClip = pCore->bin()->getBinClip(refId);
1555             if (refClip) {
1556                 speech = refClip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1557                 clipNameLabel->setText(refClip->clipName());
1558             }
1559             QStringList zones = clip->getProducerProperty("kdenlive:cutzones").split(QLatin1Char(';'));
1560             for (const QString &z : qAsConst(zones)) {
1561                 cutZones << QPoint(z.section(QLatin1Char(':'), 0, 0).toInt(), z.section(QLatin1Char(':'), 1, 1).toInt());
1562             }
1563         } else {
1564             m_refId.clear();
1565             speech = clip->getProducerProperty(QStringLiteral("kdenlive:speech"));
1566             clipNameLabel->setText(clip->clipName());
1567         }
1568         if (speech.isEmpty()) {
1569             // Nothing else to do
1570             button_insert->setEnabled(false);
1571             button_start->setEnabled(true);
1572             return;
1573         }
1574         m_visualEditor->insertHtml(speech);
1575         if (!cutZones.isEmpty()) {
1576             m_visualEditor->processCutZones(cutZones);
1577         }
1578         m_visualEditor->rebuildZones();
1579         button_insert->setEnabled(true);
1580         button_start->setEnabled(true);
1581     } else {
1582         button_start->setEnabled(false);
1583         clipNameLabel->clear();
1584         m_visualEditor->cleanup();
1585     }
1586     applyFontSize();
1587 }
1588 
1589 void TextBasedEdit::addBookmark()
1590 {
1591     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(m_binId);
1592     if (clip) {
1593         QString txt = m_visualEditor->textCursor().selectedText();
1594         QTextCursor cursor = m_visualEditor->textCursor();
1595         QString startAnchor = m_visualEditor->selectionStartAnchor(cursor, -1, -1);
1596         cursor = m_visualEditor->textCursor();
1597         QString endAnchor = m_visualEditor->selectionEndAnchor(cursor, -1, -1);
1598         if (startAnchor.isEmpty()) {
1599             showMessage(i18n("No timecode found in selection"), KMessageWidget::Information);
1600             return;
1601         }
1602         double ms = startAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble();
1603         int startPos = GenTime(ms).frames(pCore->getCurrentFps());
1604         ms = endAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble();
1605         int endPos = GenTime(ms).frames(pCore->getCurrentFps());
1606         int monitorPos = pCore->getMonitor(Kdenlive::ClipMonitor)->position();
1607         qDebug() << "==== GOT MARKER: " << txt << ", FOR POS: " << startPos << "-" << endPos << ", MON: " << monitorPos;
1608         if (monitorPos > startPos && monitorPos < endPos) {
1609             // Monitor seek is on the selection, use the current frame
1610             pCore->bin()->addClipMarker(m_binId, {monitorPos}, {txt});
1611         } else {
1612             pCore->bin()->addClipMarker(m_binId, {startPos}, {txt});
1613         }
1614     } else {
1615         qDebug() << "==== NO CLIP FOR " << m_binId;
1616     }
1617 }