Warning, file /multimedia/kdenlive/src/dialogs/subtitleedit.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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 <QToolButton>
0020 
0021 ShiftEnterFilter::ShiftEnterFilter(QObject *parent)
0022     : QObject(parent)
0023 {
0024 }
0025 
0026 bool ShiftEnterFilter::eventFilter(QObject *obj, QEvent *event)
0027 {
0028     if (event->type() == QEvent::KeyPress) {
0029         auto *keyEvent = static_cast<QKeyEvent *>(event);
0030         if ((keyEvent->modifiers() & Qt::ShiftModifier) && ((keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return))) {
0031             emit triggerUpdate();
0032             return true;
0033         }
0034     }
0035     if (event->type() == QEvent::FocusOut) {
0036         emit triggerUpdate();
0037         return true;
0038     }
0039     return QObject::eventFilter(obj, event);
0040 }
0041 
0042 SubtitleEdit::SubtitleEdit(QWidget *parent)
0043     : QWidget(parent)
0044     , m_model(nullptr)
0045 {
0046     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0047     setupUi(this);
0048     auto *keyFilter = new ShiftEnterFilter(this);
0049     subText->installEventFilter(keyFilter);
0050     connect(keyFilter, &ShiftEnterFilter::triggerUpdate, this, &SubtitleEdit::updateSubtitle);
0051     connect(subText, &KTextEdit::textChanged, this, [this]() {
0052         if (m_activeSub > -1) {
0053             buttonApply->setEnabled(true);
0054         }
0055     });
0056     connect(subText, &KTextEdit::cursorPositionChanged, this, [this]() {
0057         if (m_activeSub > -1) {
0058             buttonCut->setEnabled(true);
0059         }
0060     });
0061 
0062     connect(buttonStyle, &QToolButton::toggled, this, [this](bool toggle) { stackedWidget->setCurrentIndex(toggle ? 1 : 0); });
0063 
0064     frame_position->setEnabled(false);
0065     buttonDelete->setEnabled(false);
0066 
0067     connect(tc_position, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0068         updateSubtitle();
0069         if (buttonLock->isChecked()) {
0070             // Perform a move instead of a resize
0071             m_model->requestSubtitleMove(m_activeSub, GenTime(value, pCore->getCurrentFps()));
0072         } else {
0073             GenTime duration = m_endPos - GenTime(value, pCore->getCurrentFps());
0074             m_model->requestResize(m_activeSub, duration.frames(pCore->getCurrentFps()), false);
0075         }
0076     });
0077     connect(tc_end, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0078         updateSubtitle();
0079         if (buttonLock->isChecked()) {
0080             // Perform a move instead of a resize
0081             m_model->requestSubtitleMove(m_activeSub, GenTime(value, pCore->getCurrentFps()) - (m_endPos - m_startPos));
0082         } else {
0083             GenTime duration = GenTime(value, pCore->getCurrentFps()) - m_startPos;
0084             m_model->requestResize(m_activeSub, duration.frames(pCore->getCurrentFps()), true);
0085         }
0086     });
0087     connect(tc_duration, &TimecodeDisplay::timeCodeEditingFinished, this, [this](int value) {
0088         updateSubtitle();
0089         m_model->requestResize(m_activeSub, value, true);
0090     });
0091     connect(buttonAdd, &QToolButton::clicked, this, [this]() { emit addSubtitle(subText->toPlainText()); });
0092     connect(buttonCut, &QToolButton::clicked, this, [this]() {
0093         if (m_activeSub > -1 && subText->hasFocus()) {
0094             int pos = subText->textCursor().position();
0095             updateSubtitle();
0096             emit cutSubtitle(m_activeSub, pos);
0097         }
0098     });
0099     connect(buttonApply, &QToolButton::clicked, this, &SubtitleEdit::updateSubtitle);
0100     connect(buttonPrev, &QToolButton::clicked, this, &SubtitleEdit::goToPrevious);
0101     connect(buttonNext, &QToolButton::clicked, this, &SubtitleEdit::goToNext);
0102     connect(buttonIn, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("resize_timeline_clip_start")); });
0103     connect(buttonOut, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("resize_timeline_clip_end")); });
0104     connect(buttonDelete, &QToolButton::clicked, []() { pCore->triggerAction(QStringLiteral("delete_timeline_clip")); });
0105     buttonNext->setToolTip(i18n("Go to next subtitle"));
0106     buttonPrev->setToolTip(i18n("Go to previous subtitle"));
0107     buttonAdd->setToolTip(i18n("Add subtitle"));
0108     buttonCut->setToolTip(i18n("Split subtitle at cursor position"));
0109     buttonApply->setToolTip(i18n("Update subtitle text"));
0110     buttonStyle->setToolTip(i18n("Show style options"));
0111     buttonDelete->setToolTip(i18n("Delete subtitle"));
0112 
0113     // Styling dialog
0114     connect(fontSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0115     connect(outlineSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0116     connect(shadowSize, QOverload<int>::of(&QSpinBox::valueChanged), this, &SubtitleEdit::updateStyle);
0117     connect(fontFamily, &QFontComboBox::currentFontChanged, this, &SubtitleEdit::updateStyle);
0118     connect(fontColor, &KColorButton::changed, this, &SubtitleEdit::updateStyle);
0119     connect(outlineColor, &KColorButton::changed, this, &SubtitleEdit::updateStyle);
0120     connect(checkFont, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0121     connect(checkFontSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0122     connect(checkFontColor, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0123     connect(checkOutlineColor, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0124     connect(checkOutlineSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0125     connect(checkPosition, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0126     connect(checkShadowSize, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0127     connect(checkOpaque, &QCheckBox::toggled, this, &SubtitleEdit::updateStyle);
0128     alignment->addItem(i18n("Bottom Center"), 2);
0129     alignment->addItem(i18n("Bottom Left"), 1);
0130     alignment->addItem(i18n("Bottom Right"), 3);
0131     alignment->addItem(i18n("Center Left"), 9);
0132     alignment->addItem(i18n("Center"), 10);
0133     alignment->addItem(i18n("Center Right"), 11);
0134     alignment->addItem(i18n("Top Left"), 4);
0135     alignment->addItem(i18n("Top Center"), 6);
0136     alignment->addItem(i18n("Top Right"), 7);
0137     connect(alignment, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SubtitleEdit::updateStyle);
0138 }
0139 
0140 void SubtitleEdit::updateStyle()
0141 {
0142     QString styleString;
0143     if (fontFamily->isEnabled()) {
0144         styleString.append(QStringLiteral("Fontname=%1,").arg(fontFamily->currentFont().family()));
0145     }
0146     if (fontSize->isEnabled()) {
0147         styleString.append(QStringLiteral("Fontsize=%1,").arg(fontSize->value()));
0148     }
0149     if (fontColor->isEnabled()) {
0150         QColor color = fontColor->color();
0151         QColor destColor(color.blue(), color.green(), color.red(), 255 - color.alpha());
0152         // Strip # character
0153         QString colorName = destColor.name(QColor::HexArgb);
0154         colorName.remove(0, 1);
0155         styleString.append(QStringLiteral("PrimaryColour=&H%1,").arg(colorName));
0156     }
0157     if (outlineSize->isEnabled()) {
0158         styleString.append(QStringLiteral("Outline=%1,").arg(outlineSize->value()));
0159     }
0160     if (shadowSize->isEnabled()) {
0161         styleString.append(QStringLiteral("Shadow=%1,").arg(shadowSize->value()));
0162     }
0163     if (outlineColor->isEnabled()) {
0164         // Qt AARRGGBB must be converted to AABBGGRR where AA is 255-AA
0165         QColor color = outlineColor->color();
0166         QColor destColor(color.blue(), color.green(), color.red(), 255 - color.alpha());
0167         // Strip # character
0168         QString colorName = destColor.name(QColor::HexArgb);
0169         colorName.remove(0, 1);
0170         styleString.append(QStringLiteral("OutlineColour=&H%1,").arg(colorName));
0171     }
0172     if (checkOpaque->isChecked()) {
0173         styleString.append(QStringLiteral("BorderStyle=3,"));
0174     }
0175     if (alignment->isEnabled()) {
0176         styleString.append(QStringLiteral("Alignment=%1,").arg(alignment->currentData().toInt()));
0177     }
0178     m_model->setStyle(styleString);
0179 }
0180 
0181 void SubtitleEdit::setModel(std::shared_ptr<SubtitleModel> model)
0182 {
0183     m_model = model;
0184     m_activeSub = -1;
0185     buttonApply->setEnabled(false);
0186     buttonCut->setEnabled(false);
0187     if (m_model == nullptr) {
0188         QSignalBlocker bk(subText);
0189         subText->clear();
0190         loadStyle(QString());
0191         frame_position->setEnabled(false);
0192     } else {
0193         connect(m_model.get(), &SubtitleModel::updateSubtitleStyle, this, &SubtitleEdit::loadStyle);
0194         connect(m_model.get(), &SubtitleModel::dataChanged, this, [this](const QModelIndex &start, const QModelIndex &, const QVector<int> &roles) {
0195             if (m_activeSub > -1 && start.row() == m_model->getRowForId(m_activeSub)) {
0196                 if (roles.contains(SubtitleModel::SubtitleRole) || roles.contains(SubtitleModel::StartFrameRole) ||
0197                     roles.contains(SubtitleModel::EndFrameRole)) {
0198                     setActiveSubtitle(m_activeSub);
0199                 }
0200             }
0201         });
0202         frame_position->setEnabled(true);
0203         stackedWidget->widget(0)->setEnabled(false);
0204     }
0205 }
0206 
0207 void SubtitleEdit::loadStyle(const QString &style)
0208 {
0209     QStringList params = style.split(QLatin1Char(','));
0210     // Read style params
0211     QSignalBlocker bk1(checkFont);
0212     QSignalBlocker bk2(checkFontSize);
0213     QSignalBlocker bk3(checkFontColor);
0214     QSignalBlocker bk4(checkOutlineColor);
0215     QSignalBlocker bk5(checkOutlineSize);
0216     QSignalBlocker bk6(checkShadowSize);
0217     QSignalBlocker bk7(checkPosition);
0218     QSignalBlocker bk8(checkOpaque);
0219 
0220     checkFont->setChecked(false);
0221     checkFontSize->setChecked(false);
0222     checkFontColor->setChecked(false);
0223     checkOutlineColor->setChecked(false);
0224     checkOutlineSize->setChecked(false);
0225     checkShadowSize->setChecked(false);
0226     checkPosition->setChecked(false);
0227     checkOpaque->setChecked(false);
0228 
0229     fontFamily->setEnabled(false);
0230     fontSize->setEnabled(false);
0231     fontColor->setEnabled(false);
0232     outlineColor->setEnabled(false);
0233     outlineSize->setEnabled(false);
0234     shadowSize->setEnabled(false);
0235     alignment->setEnabled(false);
0236 
0237     for (const QString &p : params) {
0238         const QString pName = p.section(QLatin1Char('='), 0, 0);
0239         QString pValue = p.section(QLatin1Char('='), 1);
0240         if (pName == QLatin1String("Fontname")) {
0241             checkFont->setChecked(true);
0242             QFont font(pValue);
0243             QSignalBlocker bk(fontFamily);
0244             fontFamily->setEnabled(true);
0245             fontFamily->setCurrentFont(font);
0246         } else if (pName == QLatin1String("Fontsize")) {
0247             checkFontSize->setChecked(true);
0248             QSignalBlocker bk(fontSize);
0249             fontSize->setEnabled(true);
0250             fontSize->setValue(pValue.toInt());
0251         } else if (pName == QLatin1String("OutlineColour")) {
0252             checkOutlineColor->setChecked(true);
0253             pValue.replace(QLatin1String("&H"), QLatin1String("#"));
0254             QColor col(pValue);
0255             QColor result(col.blue(), col.green(), col.red(), 255 - col.alpha());
0256             QSignalBlocker bk(outlineColor);
0257             outlineColor->setEnabled(true);
0258             outlineColor->setColor(result);
0259         } else if (pName == QLatin1String("Outline")) {
0260             checkOutlineSize->setChecked(true);
0261             QSignalBlocker bk(outlineSize);
0262             outlineSize->setEnabled(true);
0263             outlineSize->setValue(pValue.toInt());
0264         } else if (pName == QLatin1String("Shadow")) {
0265             checkShadowSize->setChecked(true);
0266             QSignalBlocker bk(shadowSize);
0267             shadowSize->setEnabled(true);
0268             shadowSize->setValue(pValue.toInt());
0269         } else if (pName == QLatin1String("BorderStyle")) {
0270             checkOpaque->setChecked(true);
0271         } else if (pName == QLatin1String("Alignment")) {
0272             checkPosition->setChecked(true);
0273             QSignalBlocker bk(alignment);
0274             alignment->setEnabled(true);
0275             int ix = alignment->findData(pValue.toInt());
0276             if (ix > -1) {
0277                 alignment->setCurrentIndex(ix);
0278             }
0279         } else if (pName == QLatin1String("PrimaryColour")) {
0280             checkFontColor->setChecked(true);
0281             pValue.replace(QLatin1String("&H"), QLatin1String("#"));
0282             QColor col(pValue);
0283             QColor result(col.blue(), col.green(), col.red(), 255 - col.alpha());
0284             QSignalBlocker bk(fontColor);
0285             fontColor->setEnabled(true);
0286             fontColor->setColor(result);
0287         }
0288     }
0289 }
0290 
0291 void SubtitleEdit::updateSubtitle()
0292 {
0293     if (!buttonApply->isEnabled()) {
0294         return;
0295     }
0296     buttonApply->setEnabled(false);
0297     if (m_activeSub > -1 && m_model) {
0298         QString txt = subText->toPlainText().trimmed();
0299         txt.replace(QLatin1String("\n\n"), QStringLiteral("\n"));
0300         if (subText->document()->defaultTextOption().textDirection() == Qt::RightToLeft && !txt.startsWith(QChar(0x200E))) {
0301             txt.prepend(QChar(0x200E));
0302         }
0303         m_model->setText(m_activeSub, txt);
0304     }
0305 }
0306 
0307 void SubtitleEdit::setActiveSubtitle(int id)
0308 {
0309     m_activeSub = id;
0310     buttonApply->setEnabled(false);
0311     buttonCut->setEnabled(false);
0312     if (m_model && id > -1) {
0313         subText->setEnabled(true);
0314         QSignalBlocker bk(subText);
0315         stackedWidget->widget(0)->setEnabled(true);
0316         buttonDelete->setEnabled(true);
0317         QSignalBlocker bk2(tc_position);
0318         QSignalBlocker bk3(tc_end);
0319         QSignalBlocker bk4(tc_duration);
0320         subText->setPlainText(m_model->getText(id));
0321         m_startPos = m_model->getStartPosForId(id);
0322         GenTime duration = GenTime(m_model->getSubtitlePlaytime(id), pCore->getCurrentFps());
0323         m_endPos = m_startPos + duration;
0324         tc_position->setValue(m_startPos);
0325         tc_end->setValue(m_endPos);
0326         tc_duration->setValue(duration);
0327         tc_position->setEnabled(true);
0328         tc_end->setEnabled(true);
0329         tc_duration->setEnabled(true);
0330     } else {
0331         tc_position->setEnabled(false);
0332         tc_end->setEnabled(false);
0333         tc_duration->setEnabled(false);
0334         stackedWidget->widget(0)->setEnabled(false);
0335         buttonDelete->setEnabled(false);
0336         QSignalBlocker bk(subText);
0337         subText->clear();
0338     }
0339 }
0340 
0341 void SubtitleEdit::goToPrevious()
0342 {
0343     if (m_model) {
0344         int id = -1;
0345         if (m_activeSub > -1) {
0346             id = m_model->getPreviousSub(m_activeSub);
0347         } else {
0348             // Start from timeline cursor position
0349             int cursorPos = pCore->getMonitorPosition();
0350             std::unordered_set<int> sids = m_model->getItemsInRange(cursorPos, cursorPos);
0351             if (sids.empty()) {
0352                 sids = m_model->getItemsInRange(0, cursorPos);
0353                 for (int s : sids) {
0354                     if (id == -1 || m_model->getSubtitleEnd(s) > m_model->getSubtitleEnd(id)) {
0355                         id = s;
0356                     }
0357                 }
0358             } else {
0359                 id = m_model->getPreviousSub(*sids.begin());
0360             }
0361         }
0362         if (id > -1) {
0363             if (buttonApply->isEnabled()) {
0364                 updateSubtitle();
0365             }
0366             GenTime prev = m_model->getStartPosForId(id);
0367             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(prev.frames(pCore->getCurrentFps()));
0368             pCore->selectTimelineItem(id);
0369         }
0370     }
0371 }
0372 
0373 void SubtitleEdit::goToNext()
0374 {
0375     if (m_model) {
0376         int id = -1;
0377         if (m_activeSub > -1) {
0378             id = m_model->getNextSub(m_activeSub);
0379         } else {
0380             // Start from timeline cursor position
0381             int cursorPos = pCore->getMonitorPosition();
0382             std::unordered_set<int> sids = m_model->getItemsInRange(cursorPos, cursorPos);
0383             if (sids.empty()) {
0384                 sids = m_model->getItemsInRange(cursorPos, -1);
0385                 for (int s : sids) {
0386                     if (id == -1 || m_model->getStartPosForId(s) < m_model->getStartPosForId(id)) {
0387                         id = s;
0388                     }
0389                 }
0390             } else {
0391                 id = m_model->getNextSub(*sids.begin());
0392             }
0393         }
0394         if (id > -1) {
0395             if (buttonApply->isEnabled()) {
0396                 updateSubtitle();
0397             }
0398             GenTime prev = m_model->getStartPosForId(id);
0399             pCore->getMonitor(Kdenlive::ProjectMonitor)->requestSeek(prev.frames(pCore->getCurrentFps()));
0400             pCore->selectTimelineItem(id);
0401         }
0402     }
0403 }