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 }