File indexing completed on 2024-04-28 08:43:45

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