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 }