File indexing completed on 2023-09-24 04:59:24

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 "subtitleedit.h"
0007 #include "bin/model/subtitlemodel.hpp"
0008 #include "monitor/monitor.h"
0009 
0010 #include "core.h"
0011 #include "kdenlivesettings.h"
0012 #include "widgets/timecodedisplay.h"
0013 
0014 #include "QTextEdit"
0015 #include "klocalizedstring.h"
0016 
0017 #include <QEvent>
0018 #include <QKeyEvent>
0019 #include <QMenu>
0020 #include <QToolButton>
0021 
0022 ShiftEnterFilter::ShiftEnterFilter(QObject *parent)
0023     : QObject(parent)
0024 {
0025 }
0026 
0027 bool ShiftEnterFilter::eventFilter(QObject *obj, QEvent *event)
0028 {
0029     if (event->type() == QEvent::KeyPress) {
0030         auto *keyEvent = static_cast<QKeyEvent *>(event);
0031         if ((keyEvent->modifiers() & Qt::ShiftModifier) && ((keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return))) {
0032             Q_EMIT triggerUpdate();
0033             return true;
0034         }
0035     }
0036     if (event->type() == QEvent::FocusOut) {
0037         Q_EMIT triggerUpdate();
0038         return true;
0039     }
0040     return QObject::eventFilter(obj, event);
0041 }
0042 
0043 SubtitleEdit::SubtitleEdit(QWidget *parent)
0044     : QWidget(parent)
0045     , m_model(nullptr)
0046 {
0047     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0048     setupUi(this);
0049     auto *keyFilter = new ShiftEnterFilter(this);
0050     subText->installEventFilter(keyFilter);
0051     connect(keyFilter, &ShiftEnterFilter::triggerUpdate, this, &SubtitleEdit::updateSubtitle);
0052     connect(subText, &KTextEdit::textChanged, this, [this]() {
0053         if (m_activeSub > -1) {
0054             buttonApply->setEnabled(true);
0055         }
0056         updateCharInfo();
0057     });
0058     connect(subText, &KTextEdit::cursorPositionChanged, this, [this]() {
0059         if (m_activeSub > -1) {
0060             buttonCut->setEnabled(true);
0061         }
0062         updateCharInfo();
0063     });
0064 
0065     connect(buttonStyle, &QToolButton::toggled, this, [this](bool toggle) { stackedWidget->setCurrentIndex(toggle ? 1 : 0); });
0066 
0067     frame_position->setEnabled(false);
0068     buttonDelete->setEnabled(false);
0069 
0070     connect(tc_position, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0071         updateSubtitle();
0072         if (buttonLock->isChecked()) {
0073             // Perform a move instead of a resize
0074             m_model->requestSubtitleMove(m_activeSub, GenTime(value, pCore->getCurrentFps()));
0075         } else {
0076             GenTime duration = m_endPos - GenTime(value, pCore->getCurrentFps());
0077             m_model->requestResize(m_activeSub, duration.frames(pCore->getCurrentFps()), false);
0078         }
0079     });
0080     connect(tc_end, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0081         updateSubtitle();
0082         if (buttonLock->isChecked()) {
0083             // Perform a move instead of a resize
0084             m_model->requestSubtitleMove(m_activeSub, GenTime(value, pCore->getCurrentFps()) - (m_endPos - m_startPos));
0085         } else {
0086             GenTime duration = GenTime(value, pCore->getCurrentFps()) - m_startPos;
0087             m_model->requestResize(m_activeSub, duration.frames(pCore->getCurrentFps()), true);
0088         }
0089     });
0090     connect(tc_duration, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0091         updateSubtitle();
0092         m_model->requestResize(m_activeSub, value, true);
0093     });
0094     connect(buttonAdd, &QToolButton::clicked, this, [this]() { Q_EMIT addSubtitle(subText->toPlainText()); });
0095     connect(buttonCut, &QToolButton::clicked, this, [this]() {
0096         if (m_activeSub > -1 && subText->hasFocus()) {
0097             int pos = subText->textCursor().position();
0098             updateSubtitle();
0099             Q_EMIT cutSubtitle(m_activeSub, pos);
0100         }
0101     });
0102     connect(buttonApply, &QToolButton::clicked, this, &SubtitleEdit::updateSubtitle);
0103     connect(buttonPrev, &QToolButton::clicked, this, &SubtitleEdit::goToPrevious);
0104     connect(buttonNext, &QToolButton::clicked, this, &SubtitleEdit::goToNext);
0105     connect(buttonIn, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("resize_timeline_clip_start")); });
0106     connect(buttonOut, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("resize_timeline_clip_end")); });
0107     connect(buttonDelete, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("delete_timeline_clip")); });
0108     buttonNext->setToolTip(i18n("Go to next subtitle"));
0109     buttonNext->setWhatsThis(xi18nc("@info:whatsthis", "Moves the playhead in the timeline to the beginning of the subtitle to the right."));
0110     buttonPrev->setToolTip(i18n("Go to previous subtitle"));
0111     buttonPrev->setWhatsThis(xi18nc("@info:whatsthis", "Moves the playhead in the timeline to the beginning of the subtitle to the left."));
0112     buttonAdd->setToolTip(i18n("Add subtitle"));
0113     buttonAdd->setWhatsThis(xi18nc("@info:whatsthis", "Creates a new subtitle with the default length (set in <interface>Settings->Configure "
0114                                                       "Kdenlive…->Misc</interface>) at the current playhead position/frame."));
0115     buttonCut->setToolTip(i18n("Split subtitle at cursor position"));
0116     buttonCut->setWhatsThis(
0117         xi18nc("@info:whatsthis", "Cuts the subtitle text at the cursor position and creates a new subtitle to the right (like cutting a clip)."));
0118     buttonApply->setToolTip(i18n("Update subtitle text"));
0119     buttonApply->setWhatsThis(xi18nc("@info:whatsthis", "Updates the subtitle display in the timeline."));
0120     buttonStyle->setToolTip(i18n("Show style options"));
0121     buttonStyle->setWhatsThis(xi18nc("@info:whatsthis", "Toggles a list to adjust font, size, color, outline style, shadow, position and background."));
0122     buttonDelete->setToolTip(i18n("Delete subtitle"));
0123     buttonDelete->setWhatsThis(xi18nc("@info:whatsthis", "Deletes the currently selected subtitle (doesn’t work on multiple subtitles)."));
0124 
0125     // Styling dialog
0126     connect(fontSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0127     connect(outlineSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0128     connect(shadowSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0129     connect(fontFamily, &QFontComboBox::currentFontChanged, this, &SubtitleEdit::updateStyle);
0130     connect(fontColor, &KColorButton::changed, this, &SubtitleEdit::updateStyle);
0131     connect(outlineColor, &KColorButton::changed, this, &SubtitleEdit::updateStyle);
0132     connect(checkFont, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0133     connect(checkFontSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0134     connect(checkFontColor, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0135     connect(checkOutlineColor, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0136     connect(checkOutlineSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0137     connect(checkPosition, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0138     connect(checkShadowSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0139     connect(checkOpaque, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0140     alignment->addItem(i18n("Bottom Center"), 2);
0141     alignment->addItem(i18n("Bottom Left"), 1);
0142     alignment->addItem(i18n("Bottom Right"), 3);
0143     alignment->addItem(i18n("Center Left"), 9);
0144     alignment->addItem(i18n("Center"), 10);
0145     alignment->addItem(i18n("Center Right"), 11);
0146     alignment->addItem(i18n("Top Left"), 4);
0147     alignment->addItem(i18n("Top Center"), 6);
0148     alignment->addItem(i18n("Top Right"), 7);
0149     connect(alignment, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SubtitleEdit::updateStyle);
0150     QAction *zoomIn = new QAction(QIcon::fromTheme(QStringLiteral("zoom-in")), i18n("Zoom In"), this);
0151     connect(zoomIn, &QAction::triggered, this, &SubtitleEdit::slotZoomIn);
0152     QAction *zoomOut = new QAction(QIcon::fromTheme(QStringLiteral("zoom-out")), i18n("Zoom Out"), this);
0153     connect(zoomOut, &QAction::triggered, this, &SubtitleEdit::slotZoomOut);
0154     QMenu *menu = new QMenu(this);
0155     menu->addAction(zoomIn);
0156     menu->addAction(zoomOut);
0157     subMenu->setMenu(menu);
0158     if (KdenliveSettings::subtitleEditFontSize() > 0) {
0159         QTextCursor cursor = subText->textCursor();
0160         subText->selectAll();
0161         subText->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0162         subText->setTextCursor(cursor);
0163     }
0164 }
0165 
0166 void SubtitleEdit::slotZoomIn()
0167 {
0168     QTextCursor cursor = subText->textCursor();
0169     subText->selectAll();
0170     qreal fontSize = QFontInfo(subText->currentFont()).pointSizeF() * 1.2;
0171     KdenliveSettings::setSubtitleEditFontSize(fontSize);
0172     subText->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0173     subText->setTextCursor(cursor);
0174 }
0175 
0176 void SubtitleEdit::slotZoomOut()
0177 {
0178     QTextCursor cursor = subText->textCursor();
0179     subText->selectAll();
0180     qreal fontSize = QFontInfo(subText->currentFont()).pointSizeF() / 1.2;
0181     fontSize = qMax(fontSize, QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pointSizeF());
0182     KdenliveSettings::setSubtitleEditFontSize(fontSize);
0183     subText->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0184     subText->setTextCursor(cursor);
0185 }
0186 
0187 void SubtitleEdit::applyFontSize()
0188 {
0189     if (KdenliveSettings::subtitleEditFontSize() > 0) {
0190         QTextCursor cursor = subText->textCursor();
0191         subText->selectAll();
0192         subText->setFontPointSize(KdenliveSettings::subtitleEditFontSize());
0193         subText->setTextCursor(cursor);
0194     }
0195 }
0196 
0197 void SubtitleEdit::updateStyle()
0198 {
0199     QStringList styleString;
0200     if (fontFamily->isEnabled()) {
0201         styleString << QStringLiteral("Fontname=%1").arg(fontFamily->currentFont().family());
0202     }
0203     if (fontSize->isEnabled()) {
0204         styleString << QStringLiteral("Fontsize=%1").arg(fontSize->value());
0205     }
0206     if (fontColor->isEnabled()) {
0207         QColor color = fontColor->color();
0208         QColor destColor(color.blue(), color.green(), color.red(), 255 - color.alpha());
0209         // Strip # character
0210         QString colorName = destColor.name(QColor::HexArgb);
0211         colorName.remove(0, 1);
0212         styleString << QStringLiteral("PrimaryColour=&H%1").arg(colorName);
0213     }
0214     if (outlineSize->isEnabled()) {
0215         styleString << QStringLiteral("Outline=%1").arg(outlineSize->value());
0216     }
0217     if (shadowSize->isEnabled()) {
0218         styleString << QStringLiteral("Shadow=%1").arg(shadowSize->value());
0219     }
0220     if (outlineColor->isEnabled()) {
0221         // Qt AARRGGBB must be converted to AABBGGRR where AA is 255-AA
0222         QColor color = outlineColor->color();
0223         QColor destColor(color.blue(), color.green(), color.red(), 255 - color.alpha());
0224         // Strip # character
0225         QString colorName = destColor.name(QColor::HexArgb);
0226         colorName.remove(0, 1);
0227         styleString << QStringLiteral("OutlineColour=&H%1").arg(colorName);
0228     }
0229     if (checkOpaque->isChecked()) {
0230         QColor color = outlineColor->color();
0231         if (color.alpha() < 255) {
0232             // To avoid alpha overlay with multi lines, draw only one box
0233             QColor destColor(color.blue(), color.green(), color.red(), 255 - color.alpha());
0234             // Strip # character
0235             QString colorName = destColor.name(QColor::HexArgb);
0236             colorName.remove(0, 1);
0237             styleString << QStringLiteral("BorderStyle=4") << QStringLiteral("BackColour=&H%1").arg(colorName);
0238         } else {
0239             styleString << QStringLiteral("BorderStyle=3");
0240         }
0241     }
0242     if (alignment->isEnabled()) {
0243         styleString << QStringLiteral("Alignment=%1").arg(alignment->currentData().toInt());
0244     }
0245     m_model->setStyle(styleString.join(QLatin1Char(',')));
0246 }
0247 
0248 void SubtitleEdit::setModel(std::shared_ptr<SubtitleModel> model)
0249 {
0250     m_model = model;
0251     m_activeSub = -1;
0252     buttonApply->setEnabled(false);
0253     buttonCut->setEnabled(false);
0254     if (m_model == nullptr) {
0255         QSignalBlocker bk(subText);
0256         subText->clear();
0257         loadStyle(QString());
0258         frame_position->setEnabled(false);
0259     } else {
0260         connect(m_model.get(), &SubtitleModel::updateSubtitleStyle, this, &SubtitleEdit::loadStyle);
0261         connect(m_model.get(), &SubtitleModel::dataChanged, this, [this](const QModelIndex &start, const QModelIndex &, const QVector<int> &roles) {
0262             if (m_activeSub > -1 && start.row() == m_model->getRowForId(m_activeSub)) {
0263                 if (roles.contains(SubtitleModel::SubtitleRole) || roles.contains(SubtitleModel::StartFrameRole) ||
0264                     roles.contains(SubtitleModel::EndFrameRole)) {
0265                     setActiveSubtitle(m_activeSub);
0266                 }
0267             }
0268         });
0269         frame_position->setEnabled(true);
0270         stackedWidget->widget(0)->setEnabled(false);
0271     }
0272 }
0273 
0274 void SubtitleEdit::loadStyle(const QString &style)
0275 {
0276     QStringList params = style.split(QLatin1Char(','));
0277     // Read style params
0278     QSignalBlocker bk1(checkFont);
0279     QSignalBlocker bk2(checkFontSize);
0280     QSignalBlocker bk3(checkFontColor);
0281     QSignalBlocker bk4(checkOutlineColor);
0282     QSignalBlocker bk5(checkOutlineSize);
0283     QSignalBlocker bk6(checkShadowSize);
0284     QSignalBlocker bk7(checkPosition);
0285     QSignalBlocker bk8(checkOpaque);
0286 
0287     checkFont->setChecked(false);
0288     checkFontSize->setChecked(false);
0289     checkFontColor->setChecked(false);
0290     checkOutlineColor->setChecked(false);
0291     checkOutlineSize->setChecked(false);
0292     checkShadowSize->setChecked(false);
0293     checkPosition->setChecked(false);
0294     checkOpaque->setChecked(false);
0295 
0296     fontFamily->setEnabled(false);
0297     fontSize->setEnabled(false);
0298     fontColor->setEnabled(false);
0299     outlineColor->setEnabled(false);
0300     outlineSize->setEnabled(false);
0301     shadowSize->setEnabled(false);
0302     alignment->setEnabled(false);
0303 
0304     for (const QString &p : params) {
0305         const QString pName = p.section(QLatin1Char('='), 0, 0);
0306         QString pValue = p.section(QLatin1Char('='), 1);
0307         if (pName == QLatin1String("Fontname")) {
0308             checkFont->setChecked(true);
0309             QFont font(pValue);
0310             QSignalBlocker bk(fontFamily);
0311             fontFamily->setEnabled(true);
0312             fontFamily->setCurrentFont(font);
0313         } else if (pName == QLatin1String("Fontsize")) {
0314             checkFontSize->setChecked(true);
0315             QSignalBlocker bk(fontSize);
0316             fontSize->setEnabled(true);
0317             fontSize->setValue(pValue.toInt());
0318         } else if (pName == QLatin1String("OutlineColour")) {
0319             checkOutlineColor->setChecked(true);
0320             pValue.replace(QLatin1String("&H"), QLatin1String("#"));
0321             QColor col(pValue);
0322             QColor result(col.blue(), col.green(), col.red(), 255 - col.alpha());
0323             QSignalBlocker bk(outlineColor);
0324             outlineColor->setEnabled(true);
0325             outlineColor->setColor(result);
0326         } else if (pName == QLatin1String("Outline")) {
0327             checkOutlineSize->setChecked(true);
0328             QSignalBlocker bk(outlineSize);
0329             outlineSize->setEnabled(true);
0330             outlineSize->setValue(pValue.toInt());
0331         } else if (pName == QLatin1String("Shadow")) {
0332             checkShadowSize->setChecked(true);
0333             QSignalBlocker bk(shadowSize);
0334             shadowSize->setEnabled(true);
0335             shadowSize->setValue(pValue.toInt());
0336         } else if (pName == QLatin1String("BorderStyle")) {
0337             checkOpaque->setChecked(true);
0338         } else if (pName == QLatin1String("Alignment")) {
0339             checkPosition->setChecked(true);
0340             QSignalBlocker bk(alignment);
0341             alignment->setEnabled(true);
0342             int ix = alignment->findData(pValue.toInt());
0343             if (ix > -1) {
0344                 alignment->setCurrentIndex(ix);
0345             }
0346         } else if (pName == QLatin1String("PrimaryColour")) {
0347             checkFontColor->setChecked(true);
0348             pValue.replace(QLatin1String("&H"), QLatin1String("#"));
0349             QColor col(pValue);
0350             QColor result(col.blue(), col.green(), col.red(), 255 - col.alpha());
0351             QSignalBlocker bk(fontColor);
0352             fontColor->setEnabled(true);
0353             fontColor->setColor(result);
0354         }
0355     }
0356 }
0357 
0358 void SubtitleEdit::updateSubtitle()
0359 {
0360     if (!buttonApply->isEnabled()) {
0361         return;
0362     }
0363     buttonApply->setEnabled(false);
0364     if (m_activeSub > -1 && m_model) {
0365         QString txt = subText->toPlainText().trimmed();
0366         txt.replace(QLatin1String("\n\n"), QStringLiteral("\n"));
0367         if (subText->document()->defaultTextOption().textDirection() == Qt::RightToLeft && !txt.startsWith(QChar(0x200E))) {
0368             txt.prepend(QChar(0x200E));
0369         }
0370         m_model->setText(m_activeSub, txt);
0371     }
0372 }
0373 
0374 void SubtitleEdit::setActiveSubtitle(int id)
0375 {
0376     m_activeSub = id;
0377     buttonApply->setEnabled(false);
0378     buttonCut->setEnabled(false);
0379     if (m_model && id > -1) {
0380         subText->setEnabled(true);
0381         QSignalBlocker bk(subText);
0382         stackedWidget->widget(0)->setEnabled(true);
0383         buttonDelete->setEnabled(true);
0384         QSignalBlocker bk2(tc_position);
0385         QSignalBlocker bk3(tc_end);
0386         QSignalBlocker bk4(tc_duration);
0387         subText->setPlainText(m_model->getText(id));
0388         m_startPos = m_model->getStartPosForId(id);
0389         GenTime duration = GenTime(m_model->getSubtitlePlaytime(id), pCore->getCurrentFps());
0390         m_endPos = m_startPos + duration;
0391         tc_position->setValue(m_startPos);
0392         tc_end->setValue(m_endPos);
0393         tc_duration->setValue(duration);
0394         tc_position->setEnabled(true);
0395         tc_end->setEnabled(true);
0396         tc_duration->setEnabled(true);
0397     } else {
0398         tc_position->setEnabled(false);
0399         tc_end->setEnabled(false);
0400         tc_duration->setEnabled(false);
0401         stackedWidget->widget(0)->setEnabled(false);
0402         buttonDelete->setEnabled(false);
0403         QSignalBlocker bk(subText);
0404         subText->clear();
0405     }
0406     updateCharInfo();
0407     applyFontSize();
0408 }
0409 
0410 void SubtitleEdit::goToPrevious()
0411 {
0412     if (m_model) {
0413         int id = -1;
0414         if (m_activeSub > -1) {
0415             id = m_model->getPreviousSub(m_activeSub);
0416         } else {
0417             // Start from timeline cursor position
0418             int cursorPos = pCore->getMonitorPosition();
0419             std::unordered_set<int> sids = m_model->getItemsInRange(cursorPos, cursorPos);
0420             if (sids.empty()) {
0421                 sids = m_model->getItemsInRange(0, cursorPos);
0422                 for (int s : sids) {
0423                     if (id == -1 || m_model->getSubtitleEnd(s) > m_model->getSubtitleEnd(id)) {
0424                         id = s;
0425                     }
0426                 }
0427             } else {
0428                 id = m_model->getPreviousSub(*sids.begin());
0429             }
0430         }
0431         if (id > -1) {
0432             if (buttonApply->isEnabled()) {
0433                 updateSubtitle();
0434             }
0435             GenTime prev = m_model->getStartPosForId(id);
0436             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(prev.frames(pCore->getCurrentFps()));
0437             pCore->selectTimelineItem(id);
0438         }
0439     }
0440     updateCharInfo();
0441 }
0442 
0443 void SubtitleEdit::goToNext()
0444 {
0445     if (m_model) {
0446         int id = -1;
0447         if (m_activeSub > -1) {
0448             id = m_model->getNextSub(m_activeSub);
0449         } else {
0450             // Start from timeline cursor position
0451             int cursorPos = pCore->getMonitorPosition();
0452             std::unordered_set<int> sids = m_model->getItemsInRange(cursorPos, cursorPos);
0453             if (sids.empty()) {
0454                 sids = m_model->getItemsInRange(cursorPos, -1);
0455                 for (int s : sids) {
0456                     if (id == -1 || m_model->getStartPosForId(s) < m_model->getStartPosForId(id)) {
0457                         id = s;
0458                     }
0459                 }
0460             } else {
0461                 id = m_model->getNextSub(*sids.begin());
0462             }
0463         }
0464         if (id > -1) {
0465             if (buttonApply->isEnabled()) {
0466                 updateSubtitle();
0467             }
0468             GenTime prev = m_model->getStartPosForId(id);
0469             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(prev.frames(pCore->getCurrentFps()));
0470             pCore->selectTimelineItem(id);
0471         }
0472     }
0473     updateCharInfo();
0474 }
0475 
0476 void SubtitleEdit::updateCharInfo()
0477 {
0478     char_count->setText(i18n("Character: %1, total: <b>%2</b>", subText->textCursor().position(), subText->document()->characterCount()));
0479 }