File indexing completed on 2024-12-22 04:40:10
0001 /* 0002 SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar> 0003 SPDX-FileCopyrightText: 2010-2022 Mladen Milinkovic <max@smoothware.net> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "core/richtext/richcss.h" 0009 #include "core/richtext/richdocument.h" 0010 #include "core/subtitle.h" 0011 #include "gui/treeview/linesmodel.h" 0012 #include "gui/treeview/lineswidget.h" 0013 #include "gui/treeview/richdocumentptr.h" 0014 #include "helpers/common.h" 0015 0016 #include "scconfig.h" 0017 0018 #include <QFont> 0019 #include <QFontMetrics> 0020 #include <QItemSelectionModel> 0021 #include <QTimer> 0022 0023 #include <KLocalizedString> 0024 0025 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) 0026 #define horizontalAdvance width 0027 #endif 0028 0029 using namespace SubtitleComposer; 0030 0031 LinesModel::LinesModel(QObject *parent) 0032 : QAbstractListModel(parent), 0033 m_subtitle(nullptr), 0034 m_playingLine(nullptr), 0035 m_dataChangedTimer(new QTimer(this)), 0036 m_minChangedLineIndex(-1), 0037 m_maxChangedLineIndex(-1), 0038 m_resetModelTimer(new QTimer(this)), 0039 m_resetModelSelection(nullptr, nullptr) 0040 { 0041 m_dataChangedTimer->setInterval(0); 0042 m_dataChangedTimer->setSingleShot(true); 0043 connect(m_dataChangedTimer, &QTimer::timeout, this, &LinesModel::emitDataChanged); 0044 0045 m_resetModelTimer->setInterval(0); 0046 m_resetModelTimer->setSingleShot(true); 0047 connect(m_resetModelTimer, &QTimer::timeout, this, &LinesModel::onModelReset); 0048 } 0049 0050 void 0051 LinesModel::setSubtitle(Subtitle *subtitle) 0052 { 0053 if(m_subtitle != subtitle) { 0054 m_playingLine = nullptr; 0055 0056 if(m_subtitle) { 0057 disconnect(m_subtitle.constData(), &Subtitle::linesInserted, this, &LinesModel::onLinesInserted); 0058 disconnect(m_subtitle.constData(), &Subtitle::linesAboutToBeRemoved, this, &LinesModel::onLinesAboutToRemove); 0059 disconnect(m_subtitle.constData(), &Subtitle::linesRemoved, this, &LinesModel::onLinesRemoved); 0060 0061 disconnect(m_subtitle.constData(), &Subtitle::lineAnchorChanged, this, &LinesModel::onLineChanged); 0062 disconnect(m_subtitle.constData(), &Subtitle::lineErrorFlagsChanged, this, &LinesModel::onLineChanged); 0063 disconnect(m_subtitle.constData(), &Subtitle::linePrimaryTextChanged, this, &LinesModel::onLineChanged); 0064 disconnect(m_subtitle.constData(), &Subtitle::lineSecondaryTextChanged, this, &LinesModel::onLineChanged); 0065 disconnect(m_subtitle.constData(), &Subtitle::lineShowTimeChanged, this, &LinesModel::onLineChanged); 0066 disconnect(m_subtitle.constData(), &Subtitle::lineHideTimeChanged, this, &LinesModel::onLineChanged); 0067 0068 disconnect(m_subtitle->stylesheet(), &RichCSS::changed, this, &LinesModel::onLinesChanged); 0069 0070 if(m_subtitle->linesCount()) { 0071 onLinesAboutToRemove(0, m_subtitle->linesCount() - 1); 0072 onLinesRemoved(0, m_subtitle->linesCount() - 1); 0073 } 0074 } 0075 0076 m_subtitle = subtitle; 0077 0078 if(m_subtitle) { 0079 if(m_subtitle->linesCount()) { 0080 onLinesInserted(0, m_subtitle->linesCount() - 1); 0081 } 0082 0083 connect(m_subtitle.constData(), &Subtitle::linesInserted, this, &LinesModel::onLinesInserted); 0084 connect(m_subtitle.constData(), &Subtitle::linesAboutToBeRemoved, this, &LinesModel::onLinesAboutToRemove); 0085 connect(m_subtitle.constData(), &Subtitle::linesRemoved, this, &LinesModel::onLinesRemoved); 0086 0087 connect(m_subtitle.constData(), &Subtitle::lineAnchorChanged, this, &LinesModel::onLineChanged); 0088 connect(m_subtitle.constData(), &Subtitle::lineErrorFlagsChanged, this, &LinesModel::onLineChanged); 0089 connect(m_subtitle.constData(), &Subtitle::linePrimaryTextChanged, this, &LinesModel::onLineChanged); 0090 connect(m_subtitle.constData(), &Subtitle::lineSecondaryTextChanged, this, &LinesModel::onLineChanged); 0091 connect(m_subtitle.constData(), &Subtitle::lineShowTimeChanged, this, &LinesModel::onLineChanged); 0092 connect(m_subtitle.constData(), &Subtitle::lineHideTimeChanged, this, &LinesModel::onLineChanged); 0093 0094 connect(m_subtitle->stylesheet(), &RichCSS::changed, this, &LinesModel::onLinesChanged); 0095 } 0096 } 0097 } 0098 0099 void 0100 LinesModel::setPlayingLine(SubtitleLine *line) 0101 { 0102 if(m_playingLine != line) { 0103 if(m_playingLine) { 0104 int row = m_playingLine->index(); 0105 m_playingLine = nullptr; 0106 emit dataChanged(index(row, 0), index(row, ColumnCount)); 0107 } 0108 0109 m_playingLine = line; 0110 0111 if(line) { 0112 int row = m_playingLine->index(); 0113 emit dataChanged(index(row, 0), index(row, ColumnCount)); 0114 } 0115 } 0116 } 0117 0118 int 0119 LinesModel::rowCount(const QModelIndex &parent) const 0120 { 0121 Q_UNUSED(parent); 0122 return m_subtitle ? m_subtitle->linesCount() : 0; 0123 } 0124 0125 int 0126 LinesModel::columnCount(const QModelIndex &parent) const 0127 { 0128 Q_UNUSED(parent); 0129 return ColumnCount; 0130 } 0131 0132 Qt::ItemFlags 0133 LinesModel::flags(const QModelIndex &index) const 0134 { 0135 if(!index.isValid() || index.column() < Text) 0136 return QAbstractItemModel::flags(index); 0137 0138 return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; 0139 } 0140 0141 QString 0142 LinesModel::buildToolTip(SubtitleLine *line, bool primary) 0143 { 0144 int errorFlags = line->errorFlags(); 0145 if(primary) 0146 errorFlags &= ~SubtitleLine::SecondaryOnlyErrors; 0147 else 0148 errorFlags &= ~SubtitleLine::PrimaryOnlyErrors; 0149 0150 const RichDocument *text = primary ? line->primaryDoc() : line->secondaryDoc(); 0151 0152 if(errorFlags) { 0153 QString toolTip = "<p style='white-space:pre;margin-bottom:6px;'>" + text->toHtml() + "</p><p style='white-space:pre;margin-top:0px;'>"; 0154 0155 if(errorFlags) { 0156 toolTip += i18n("<b>Observations:</b>"); 0157 0158 for(int id = 0; id < SubtitleLine::ErrorSIZE; ++id) { 0159 if(!((0x1 << id) & errorFlags)) 0160 continue; 0161 0162 QString errorText = line->fullErrorText((SubtitleLine::ErrorID)id); 0163 if(!errorText.isEmpty()) 0164 toolTip += "\n - " + errorText; 0165 } 0166 } 0167 0168 toolTip += "</p>"; 0169 0170 return toolTip; 0171 } 0172 0173 return text->toHtml(); 0174 } 0175 0176 QVariant 0177 LinesModel::headerData(int section, Qt::Orientation orientation, int role) const 0178 { 0179 if(role != Qt::DisplayRole || orientation == Qt::Vertical) 0180 return QVariant(); 0181 0182 switch(section) { 0183 case Number: return i18nc("@title:column Subtitle line number", "Line"); 0184 case PauseTime: return i18nc("@title:column", "Pause"); 0185 case ShowTime: return i18nc("@title:column", "Show Time"); 0186 case HideTime: return i18nc("@title:column", "Hide Time"); 0187 case Duration: return i18nc("@title:column", "Duration"); 0188 case Text: return i18nc("@title:column Subtitle line (primary) text", "Text"); 0189 case Translation: return i18nc("@title:column Subtitle line translation text", "Translation"); 0190 default: return QVariant(); 0191 } 0192 } 0193 0194 QVariant 0195 LinesModel::data(const QModelIndex &index, int role) const 0196 { 0197 if(!m_subtitle || m_subtitle->count() <= index.row()) 0198 return QVariant(); 0199 0200 SubtitleLine *line = m_subtitle->at(index.row()); 0201 0202 if(role == PlayingLineRole) 0203 return line == m_playingLine; 0204 0205 if(role == AnchoredRole) 0206 return !m_subtitle->hasAnchors() ? 0 : (m_subtitle->isLineAnchored(line) ? 1 : -1); 0207 0208 switch(index.column()) { 0209 case Number: 0210 if(role == Qt::DisplayRole) 0211 return index.row() + 1; 0212 if(role == Qt::SizeHintRole) 0213 return QSize(QFontMetrics(QFont()).horizontalAdvance(QString::number(index.row() + 1)) + 28, 0); 0214 break; 0215 0216 case PauseTime: 0217 if(role == Qt::DisplayRole) 0218 return line->pauseTime().toString(true, false); 0219 if(role == Qt::TextAlignmentRole) 0220 return Qt::AlignCenter; 0221 break; 0222 0223 case ShowTime: 0224 if(role == Qt::DisplayRole) 0225 return line->showTime().toString(); 0226 if(role == Qt::TextAlignmentRole) 0227 return Qt::AlignCenter; 0228 break; 0229 0230 case HideTime: 0231 if(role == Qt::DisplayRole) 0232 return line->hideTime().toString(); 0233 if(role == Qt::TextAlignmentRole) 0234 return Qt::AlignCenter; 0235 break; 0236 0237 case Duration: 0238 if(role == Qt::DisplayRole) 0239 return line->durationTime().toString(true, false); 0240 if(role == Qt::TextAlignmentRole) 0241 return Qt::AlignCenter; 0242 if(role == Qt::ForegroundRole) { 0243 const QPalette &pal = static_cast<LinesWidget *>(parent())->palette(); 0244 QColor fg = pal.color(QPalette::WindowText); 0245 return line->durationColor(fg); 0246 } 0247 break; 0248 0249 case Text: 0250 if(role == Qt::DisplayRole || role == Qt::EditRole) 0251 return QVariant::fromValue(RichDocumentPtr(line->primaryDoc())); 0252 if(role == MarkedRole) 0253 return line->errorFlags() & SubtitleLine::UserMark; 0254 if(role == ErrorRole) 0255 return line->errorFlags() & ((SubtitleLine::SharedErrors | SubtitleLine::PrimaryOnlyErrors) & ~SubtitleLine::UserMark); 0256 if(role == Qt::ToolTipRole) 0257 return buildToolTip(line, true); 0258 break; 0259 0260 case Translation: 0261 if(role == Qt::DisplayRole || role == Qt::EditRole) 0262 return QVariant::fromValue(RichDocumentPtr(line->secondaryDoc())); 0263 if(role == MarkedRole) 0264 return line->errorFlags() & SubtitleLine::UserMark; 0265 if(role == ErrorRole) 0266 return line->errorFlags() & ((SubtitleLine::SharedErrors | SubtitleLine::SecondaryOnlyErrors) & ~SubtitleLine::UserMark); 0267 if(role == Qt::ToolTipRole) 0268 return buildToolTip(line, false); 0269 break; 0270 0271 default: 0272 break; 0273 } 0274 return QVariant(); 0275 } 0276 0277 bool 0278 LinesModel::setData(const QModelIndex &index, const QVariant &value, int role) 0279 { 0280 Q_UNUSED(value); 0281 0282 if(!m_subtitle || !index.isValid()) 0283 return false; 0284 0285 if(role == Qt::EditRole && (index.column() == Text || index.column() == Translation)) { 0286 emit dataChanged(index, index); 0287 return true; 0288 } 0289 0290 return false; 0291 } 0292 0293 void 0294 LinesModel::onLinesInserted(int firstIndex, int lastIndex) 0295 { 0296 m_resetModelSelection.first = m_subtitle->at(firstIndex); 0297 m_resetModelSelection.second = m_subtitle->at(lastIndex); 0298 LinesWidget *lw = static_cast<LinesWidget *>(parent()); 0299 m_resetModelResumeEditing = lw->isEditing(); 0300 { 0301 QItemSelectionModel *sm = lw->selectionModel(); 0302 QSignalBlocker s(sm); 0303 sm->clear(); 0304 } 0305 m_resetModelTimer->start(); 0306 } 0307 0308 void 0309 LinesModel::onLinesAboutToRemove(int firstIndex, int lastIndex) 0310 { 0311 const int index = lastIndex == rowCount() - 1 ? firstIndex - 1 : lastIndex + 1; 0312 m_resetModelSelection.first = m_resetModelSelection.second = m_subtitle->line(index); 0313 } 0314 0315 void 0316 LinesModel::onLinesRemoved(int firstIndex, int lastIndex) 0317 { 0318 Q_UNUSED(firstIndex); 0319 Q_UNUSED(lastIndex); 0320 m_resetModelTimer->start(); 0321 } 0322 0323 void 0324 LinesModel::onModelReset() 0325 { 0326 LinesWidget *w = static_cast<LinesWidget *>(parent()); 0327 QItemSelectionModel *sm = w->selectionModel(); 0328 0329 const QModelIndex prevIndex = sm->currentIndex();; 0330 0331 beginResetModel(); 0332 endResetModel(); 0333 0334 if(sm->hasSelection()) { 0335 if(!sm->currentIndex().isValid()) { 0336 const QModelIndex idx = index(sm->selection().first().top()); 0337 sm->setCurrentIndex(idx, QItemSelectionModel::Rows); 0338 } 0339 } else if(m_resetModelSelection.first) { 0340 const QModelIndex first = index(m_resetModelSelection.first->index(), 0, QModelIndex()); 0341 if(m_resetModelSelection.first != m_subtitle->firstLine() && m_resetModelSelection.second != m_subtitle->lastLine()) { 0342 const QModelIndex last = index(m_resetModelSelection.second->index(), columnCount() - 1, QModelIndex()); 0343 sm->select(QItemSelection(first, last), QItemSelectionModel::ClearAndSelect); 0344 } 0345 sm->setCurrentIndex(first, QItemSelectionModel::Rows); 0346 } else { 0347 if(prevIndex.isValid() && !sm->currentIndex().isValid()) { 0348 // model reset should invalidate current index and prevent signals 0349 QSignalBlocker s(sm); // make sure nothing fires anyway 0350 sm->setCurrentIndex(prevIndex, QItemSelectionModel::Rows); 0351 } 0352 sm->clear(); 0353 } 0354 0355 if(w->scrollFollowsModel()) 0356 w->scrollTo(sm->currentIndex(), QAbstractItemView::EnsureVisible); 0357 0358 if(m_resetModelResumeEditing) 0359 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) 0360 QMetaObject::invokeMethod(w, "editCurrentLineInPlace", Qt::QueuedConnection); 0361 #else 0362 QMetaObject::invokeMethod(w, [w](){ if(!w->isEditing()) w->editCurrentLineInPlace(); }, Qt::QueuedConnection); 0363 #endif 0364 } 0365 0366 void 0367 LinesModel::onLineChanged(const SubtitleLine *line) 0368 { 0369 const int lineIndex = line->index(); 0370 0371 if(m_minChangedLineIndex < 0) { 0372 m_minChangedLineIndex = lineIndex; 0373 m_maxChangedLineIndex = lineIndex; 0374 m_dataChangedTimer->start(); 0375 } else if(lineIndex < m_minChangedLineIndex) { 0376 m_minChangedLineIndex = lineIndex; 0377 } else if(lineIndex > m_maxChangedLineIndex) { 0378 m_maxChangedLineIndex = lineIndex; 0379 } 0380 } 0381 0382 void 0383 LinesModel::onLinesChanged() 0384 { 0385 if(m_minChangedLineIndex < 0) 0386 m_dataChangedTimer->start(); 0387 m_minChangedLineIndex = 0; 0388 m_maxChangedLineIndex = m_subtitle->lastIndex(); 0389 } 0390 0391 void 0392 LinesModel::emitDataChanged() 0393 { 0394 if(m_minChangedLineIndex < 0) 0395 m_minChangedLineIndex = m_maxChangedLineIndex; 0396 else if(m_maxChangedLineIndex < 0) 0397 m_maxChangedLineIndex = m_minChangedLineIndex; 0398 0399 emit dataChanged(index(m_minChangedLineIndex, 0), index(m_maxChangedLineIndex, ColumnCount - 1)); 0400 0401 m_minChangedLineIndex = -1; 0402 m_maxChangedLineIndex = -1; 0403 }