File indexing completed on 2024-12-22 04:40:11

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 "lineswidget.h"
0009 
0010 #include "appglobal.h"
0011 #include "application.h"
0012 #include "core/richtext/richdocument.h"
0013 #include "actions/useractionnames.h"
0014 #include "dialogs/actionwithtargetdialog.h"
0015 #include "gui/treeview/linesitemdelegate.h"
0016 #include "gui/treeview/linesselectionmodel.h"
0017 
0018 #include <QHeaderView>
0019 #include <QMenu>
0020 #include <QMimeData>
0021 #include <QMouseEvent>
0022 #include <QPainter>
0023 
0024 #include <KConfigGroup>
0025 #include <KSharedConfig>
0026 
0027 
0028 #define RESTORE_STATE false
0029 
0030 using namespace SubtitleComposer;
0031 
0032 LinesWidget::LinesWidget(QWidget *parent)
0033     : TreeView(parent),
0034       m_scrollFollowsModel(true),
0035       m_translationMode(false),
0036       m_showingContextMenu(false),
0037       m_itemsDelegate(new LinesItemDelegate(this)),
0038       m_inlineEditor(nullptr)
0039 {
0040     setModel(new LinesModel(this));
0041     selectionModel()->deleteLater();
0042     setSelectionModel(new LinesSelectionModel(model()));
0043 
0044     for(int column = 0, columnCount = model()->columnCount(); column < columnCount; ++column)
0045         setItemDelegateForColumn(column, m_itemsDelegate);
0046 
0047     QHeaderView *hdr = header();
0048     hdr->setSectionsClickable(false);
0049     hdr->setSectionsMovable(false);
0050     updateHeader();
0051 
0052     setUniformRowHeights(true);
0053     setItemsExpandable(false);
0054     setAutoExpandDelay(-1);
0055     setExpandsOnDoubleClick(false);
0056     setRootIsDecorated(false);
0057     setAllColumnsShowFocus(true);
0058     setSortingEnabled(false);
0059     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
0060 
0061     setSelectionMode(QAbstractItemView::ExtendedSelection);
0062     setSelectionBehavior(QAbstractItemView::SelectRows);
0063 
0064     m_gridPen.setColor(palette().mid().color().lighter(120));
0065 
0066     setAcceptDrops(true);
0067     viewport()->installEventFilter(this);
0068 
0069     connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LinesWidget::onCurrentRowChanged);
0070 }
0071 
0072 LinesWidget::~LinesWidget()
0073 {
0074 }
0075 
0076 void
0077 LinesWidget::updateHeader()
0078 {
0079     QHeaderView *hdr = header();
0080     for(int i = 0; i < LinesModel::ColumnCount; i++) {
0081         switch(i) {
0082         case LinesModel::Text:
0083             hdr->setSectionResizeMode(i, QHeaderView::Stretch);
0084             hdr->setSectionHidden(i, false);
0085             break;
0086         case LinesModel::Translation:
0087             hdr->setSectionResizeMode(i, QHeaderView::Stretch);
0088             hdr->setSectionHidden(i, !m_translationMode);
0089             break;
0090         default:
0091             hdr->setSectionResizeMode(i, QHeaderView::ResizeToContents);
0092             hdr->setSectionHidden(i, false);
0093             break;
0094         }
0095     }
0096     // do this last so columns get autosized
0097     hdr->setSectionResizeMode(LinesModel::Text, QHeaderView::Interactive);
0098     hdr->setSectionResizeMode(LinesModel::Translation, QHeaderView::Interactive);
0099     // give some small size to last column, it will stretch anyways
0100     hdr->resizeSection(LinesModel::Translation, 100);
0101 }
0102 
0103 void
0104 LinesWidget::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint)
0105 {
0106     auto ehint = LinesItemDelegate::ExtendedEditHint(hint);
0107     if(ehint == LinesItemDelegate::NoHint || ehint == LinesItemDelegate::SubmitModelCache || ehint == LinesItemDelegate::RevertModelCache) {
0108         TreeView::closeEditor(editor, hint);
0109     } else {
0110         QModelIndex index = currentIndex();
0111 
0112         switch(ehint) {
0113         case LinesItemDelegate::EditPreviousItem:
0114             if(m_translationMode) {
0115                 if(index.column() == LinesModel::Translation)
0116                     index = index.sibling(index.row(), LinesModel::Text);
0117                 else if(index.row())
0118 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
0119                     index = index.sibling(moveCursor(MoveUp, Qt::NoModifier).row(), LinesModel::Translation);
0120 #else
0121                     index = moveCursor(MoveUp, Qt::NoModifier).siblingAtColumn(LinesModel::Translation);
0122 #endif
0123                 break;
0124             }
0125             // fallthrough
0126         case LinesItemDelegate::EditUpperItem:
0127             index = moveCursor(MoveUp, Qt::NoModifier);
0128             break;
0129         case LinesItemDelegate::EditNextItem:
0130             if(m_translationMode) {
0131                 if(index.column() == LinesModel::Text)
0132                     index = index.sibling(index.row(), LinesModel::Translation);
0133                 else if(index.row() < model()->rowCount() - 1)
0134 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
0135                     index = index.sibling(moveCursor(MoveDown, Qt::NoModifier).row(), LinesModel::Text);
0136 #else
0137                     index = moveCursor(MoveDown, Qt::NoModifier).siblingAtColumn(LinesModel::Text);
0138 #endif
0139                 break;
0140             }
0141             // fallthrough
0142         case LinesItemDelegate::EditLowerItem:
0143             index = moveCursor(MoveDown, Qt::NoModifier);
0144             break;
0145         default: break;
0146         }
0147 
0148         QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::NoUpdate;
0149         if(selectionMode() != NoSelection) {
0150             flags = QItemSelectionModel::ClearAndSelect;
0151             switch(selectionBehavior()) {
0152             case SelectItems: break;
0153             case SelectRows: flags |= QItemSelectionModel::Rows; break;
0154             case SelectColumns: flags |= QItemSelectionModel::Columns; break;
0155             }
0156         }
0157 
0158         TreeView::closeEditor(editor, QAbstractItemDelegate::NoHint);
0159 
0160         QPersistentModelIndex persistent(index);
0161         selectionModel()->setCurrentIndex(persistent, flags);
0162 
0163         // currentChanged signal would have already started editing
0164         if((index.flags() & Qt::ItemIsEditable) && !(editTriggers() & QAbstractItemView::CurrentChanged))
0165             edit(persistent);
0166     }
0167 }
0168 
0169 void
0170 LinesWidget::editCurrentLineInPlace(bool primaryText)
0171 {
0172     QModelIndex curIdx = currentIndex();
0173     if(curIdx.isValid()) {
0174         const int col = primaryText || !m_translationMode ? LinesModel::Text : LinesModel::Translation;
0175         if(col != curIdx.column()) {
0176             curIdx = model()->index(curIdx.row(), col);
0177             setCurrentIndex(curIdx);
0178         }
0179         edit(curIdx);
0180     }
0181 }
0182 
0183 bool
0184 LinesWidget::showingContextMenu()
0185 {
0186     return m_showingContextMenu;
0187 }
0188 
0189 SubtitleLine *
0190 LinesWidget::currentLine() const
0191 {
0192     return qobject_cast<LinesSelectionModel *>(selectionModel())->currentLine();
0193 }
0194 
0195 int
0196 LinesWidget::currentLineIndex() const
0197 {
0198     const QModelIndex idx = currentIndex();
0199     return idx.isValid() ? idx.row() : -1;
0200 }
0201 
0202 int
0203 LinesWidget::firstSelectedIndex() const
0204 {
0205     int row = -1;
0206     const QItemSelection &selection = selectionModel()->selection();
0207     for(const QItemSelectionRange &r : selection) {
0208         if(row == -1 || r.top() < row)
0209             row = r.top();
0210     }
0211     return row;
0212 }
0213 
0214 int
0215 LinesWidget::lastSelectedIndex() const
0216 {
0217     int row = -1;
0218     const QItemSelection &selection = selectionModel()->selection();
0219     for(const QItemSelectionRange &r : selection) {
0220         if(r.bottom() > row)
0221             row = r.bottom();
0222     }
0223     return row;
0224 }
0225 
0226 bool
0227 LinesWidget::selectionHasMultipleRanges() const
0228 {
0229     const QItemSelection &selection = selectionModel()->selection();
0230     return selection.size() > 1;
0231 }
0232 
0233 RangeList
0234 LinesWidget::selectionRanges() const
0235 {
0236     RangeList ranges;
0237 
0238     const QItemSelection &selection = selectionModel()->selection();
0239     for(const QItemSelectionRange &r : selection) {
0240         ranges << Range(r.top(), r.bottom());
0241     }
0242 
0243     return ranges;
0244 }
0245 
0246 RangeList
0247 LinesWidget::targetRanges(int target) const
0248 {
0249     switch(target) {
0250     case ActionWithTargetDialog::AllLines:
0251         return Range::full();
0252     case ActionWithTargetDialog::Selection: return selectionRanges();
0253     case ActionWithTargetDialog::FromSelected: {
0254         int index = firstSelectedIndex();
0255         return index < 0 ? RangeList() : Range::upper(index);
0256     }
0257     case ActionWithTargetDialog::UpToSelected: {
0258         int index = lastSelectedIndex();
0259         return index < 0 ? RangeList() : Range::lower(index);
0260     }
0261     default:
0262         return RangeList();
0263     }
0264 }
0265 
0266 void
0267 LinesWidget::loadConfig()
0268 {
0269 #if RESTORE_STATE
0270     KConfigGroup group(KSharedConfig::openConfig()->group("LinesWidget Settings"));
0271 
0272     QByteArray state;
0273     QStringList strState = group.readXdgListEntry("Columns State", QStringList() << QString());
0274     for(QStringList::ConstIterator it = strState.constBegin(), end = strState.constEnd(); it != end; ++it)
0275         state.append(char(it->toInt()));
0276     header()->restoreState(state);
0277 #endif
0278 }
0279 
0280 void
0281 LinesWidget::saveConfig()
0282 {
0283     KConfigGroup group(KSharedConfig::openConfig()->group("LinesWidget Settings"));
0284 
0285     QStringList strState;
0286     QByteArray state = header()->saveState();
0287     for(int index = 0, size = state.size(); index < size; ++index)
0288         strState.append(QString::number(state[index]));
0289     group.writeXdgListEntry("Columns State", strState);
0290 }
0291 
0292 void
0293 LinesWidget::setSubtitle(Subtitle *subtitle)
0294 {
0295     model()->setSubtitle(subtitle);
0296 }
0297 
0298 void
0299 LinesWidget::setTranslationMode(bool enabled)
0300 {
0301     if(m_translationMode == enabled)
0302         return;
0303 
0304     m_translationMode = enabled;
0305     updateHeader();
0306 }
0307 
0308 void
0309 LinesWidget::setCurrentLine(SubtitleLine *line, bool clearSelection)
0310 {
0311     if(!line)
0312         return;
0313 
0314     selectionModel()->setCurrentIndex(model()->index(line->index(), 0),
0315                                       QItemSelectionModel::Select | QItemSelectionModel::Rows
0316                                       | (clearSelection ? QItemSelectionModel::Clear : QItemSelectionModel::NoUpdate));
0317 }
0318 
0319 void
0320 LinesWidget::setPlayingLine(SubtitleLine *line)
0321 {
0322     model()->setPlayingLine(line);
0323 }
0324 
0325 void
0326 LinesWidget::mouseDoubleClickEvent(QMouseEvent *e)
0327 {
0328 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0329     QModelIndex index = indexAt(viewport()->mapFromGlobal(e->globalPos()));
0330 #else
0331     QModelIndex index = indexAt(viewport()->mapFromGlobal(e->globalPosition()).toPoint());
0332 #endif
0333     if(index.isValid())
0334         emit lineDoubleClicked(model()->subtitle()->line(index.row()));
0335 }
0336 
0337 bool
0338 LinesWidget::eventFilter(QObject *object, QEvent *event)
0339 {
0340     if(object == viewport()) {
0341         switch(event->type()) {
0342         case QEvent::DragEnter:
0343         case QEvent::Drop:
0344             foreach(const QUrl &url, static_cast<QDropEvent *>(event)->mimeData()->urls()) {
0345                 if(url.scheme() == QLatin1String("file")) {
0346                     event->accept();
0347                     if(event->type() == QEvent::Drop) {
0348                         app()->openSubtitle(url);
0349                     }
0350                     return true; // eat event
0351                 }
0352             }
0353             event->ignore();
0354             return true;
0355 
0356         case QEvent::DragMove:
0357             return true; // eat event
0358 
0359         default:
0360             ;
0361         }
0362     }
0363     // standard event processing
0364     return TreeView::eventFilter(object, event);
0365 }
0366 
0367 inline static void
0368 addAppAction(QMenu *menu, const char *actionName, bool checkable=false, bool checked=false)
0369 {
0370     QAction *action = app()->action(actionName);
0371     if(checkable) {
0372         action->setCheckable(true);
0373         action->setChecked(checked);
0374     }
0375     menu->addAction(action);
0376     if(checkable)
0377         action->setCheckable(false);
0378 }
0379 
0380 void
0381 LinesWidget::contextMenuEvent(QContextMenuEvent *e)
0382 {
0383     SubtitleLine *subLine = static_cast<LinesSelectionModel *>(selectionModel())->currentLine();
0384 
0385     QMenu menu;
0386     addAppAction(&menu, ACT_SELECT_ALL_LINES);
0387     addAppAction(&menu, ACT_EDIT_CURRENT_LINE_IN_PLACE);
0388     menu.addSeparator();
0389     addAppAction(&menu, ACT_INSERT_BEFORE_CURRENT_LINE);
0390     addAppAction(&menu, ACT_INSERT_AFTER_CURRENT_LINE);
0391     addAppAction(&menu, ACT_REMOVE_SELECTED_LINES);
0392     menu.addSeparator();
0393     addAppAction(&menu, ACT_JOIN_SELECTED_LINES);
0394     addAppAction(&menu, ACT_SPLIT_SELECTED_LINES);
0395     menu.addSeparator();
0396     addAppAction(&menu, ACT_ANCHOR_TOGGLE);
0397     addAppAction(&menu, ACT_ANCHOR_REMOVE_ALL);
0398     menu.addSeparator();
0399 
0400     QMenu textsMenu(i18n("Texts"));
0401     addAppAction(&textsMenu, ACT_ADJUST_TEXTS);
0402     addAppAction(&textsMenu, ACT_UNBREAK_TEXTS);
0403     addAppAction(&textsMenu, ACT_SIMPLIFY_SPACES);
0404     addAppAction(&textsMenu, ACT_CHANGE_CASE);
0405     addAppAction(&textsMenu, ACT_FIX_PUNCTUATION);
0406     addAppAction(&textsMenu, ACT_TRANSLATE);
0407     textsMenu.addSeparator();
0408     addAppAction(&textsMenu, ACT_SPELL_CHECK);
0409     menu.addMenu(&textsMenu);
0410 
0411     QMenu stylesMenu(i18n("Styles"));
0412     const int styleFlags = subLine ? subLine->primaryDoc()->cummulativeStyleFlags() | subLine->secondaryDoc()->cummulativeStyleFlags() : 0;
0413     addAppAction(&stylesMenu, ACT_TOGGLE_SELECTED_LINES_BOLD, true, styleFlags & RichString::Bold);
0414     addAppAction(&stylesMenu, ACT_TOGGLE_SELECTED_LINES_ITALIC, true, styleFlags & RichString::Italic);
0415     addAppAction(&stylesMenu, ACT_TOGGLE_SELECTED_LINES_UNDERLINE, true, styleFlags & RichString::Underline);
0416     addAppAction(&stylesMenu, ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH, true, styleFlags & RichString::StrikeThrough);
0417     addAppAction(&stylesMenu, ACT_CHANGE_SELECTED_LINES_TEXT_COLOR);
0418     menu.addMenu(&stylesMenu);
0419 
0420     QMenu timesMenu(i18n("Times"));
0421     addAppAction(&timesMenu, ACT_SHIFT_SELECTED_LINES_FORWARDS);
0422     addAppAction(&timesMenu, ACT_SHIFT_SELECTED_LINES_BACKWARDS);
0423     timesMenu.addSeparator();
0424     addAppAction(&timesMenu, ACT_SHIFT);
0425     addAppAction(&timesMenu, ACT_DURATION_LIMITS);
0426     addAppAction(&timesMenu, ACT_AUTOMATIC_DURATIONS);
0427     addAppAction(&timesMenu, ACT_MAXIMIZE_DURATIONS);
0428     addAppAction(&timesMenu, ACT_FIX_OVERLAPPING_LINES);
0429     menu.addMenu(&timesMenu);
0430 
0431     QMenu errorsMenu(i18n("Errors"));
0432     addAppAction(&errorsMenu, ACT_TOGGLE_SELECTED_LINES_MARK, true, subLine ? subLine->errorFlags() & SubtitleLine::UserMark : 0);
0433     errorsMenu.addSeparator();
0434     addAppAction(&errorsMenu, ACT_DETECT_ERRORS);
0435     addAppAction(&errorsMenu, ACT_CLEAR_ERRORS);
0436     menu.addMenu(&errorsMenu);
0437 
0438     m_showingContextMenu = true;
0439     menu.exec(e->globalPos());
0440     m_showingContextMenu = false;
0441 
0442     e->ignore();
0443 
0444     TreeView::contextMenuEvent(e);
0445 }
0446 
0447 void
0448 LinesWidget::onCurrentRowChanged()
0449 {
0450     auto sm = qobject_cast<LinesSelectionModel *>(selectionModel());
0451     emit currentLineChanged(sm->currentLine());
0452 }
0453 
0454 void
0455 LinesWidget::drawHorizontalDotLine(QPainter *painter, int x1, int x2, int y)
0456 {
0457     static int aux;
0458 
0459     if(x1 > x2) {
0460         aux = x1;
0461         x1 = x2;
0462         x2 = aux;
0463     }
0464 
0465     for(int x = x1 + 1; x <= x2; x += 2)
0466         painter->drawPoint(x, y);
0467 }
0468 
0469 void
0470 LinesWidget::drawVerticalDotLine(QPainter *painter, int x, int y1, int y2)
0471 {
0472     static int aux;
0473 
0474     if(y1 > y2) {
0475         aux = y1;
0476         y1 = y2;
0477         y2 = aux;
0478     }
0479 
0480     for(int y = y1 + 1; y <= y2; y += 2)
0481         painter->drawPoint(x, y);
0482 }
0483 
0484 void
0485 LinesWidget::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0486 {
0487     TreeView::drawRow(painter, option, index);
0488 
0489     const int visibleColumns = m_translationMode ? LinesModel::ColumnCount : LinesModel::ColumnCount - 1;
0490     const int row = index.row();
0491     const bool rowSelected = selectionModel()->isSelected(index);
0492     const QPalette palette = this->palette();
0493     const QRect rowRect = QRect(visualRect(model()->index(row, 0)).topLeft(),
0494                                 visualRect(model()->index(row, visibleColumns - 1)).bottomRight());
0495 
0496     // draw row grid
0497     painter->setPen(m_gridPen);
0498     if(!rowSelected)
0499         drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.bottom());
0500     for(int column = 0; column < visibleColumns - 1; ++column) {
0501         const QRect cellRect = visualRect(model()->index(row, column));
0502         drawVerticalDotLine(painter, cellRect.right(), rowRect.top(), rowRect.bottom());
0503     }
0504     if(index.row() == currentIndex().row()) {
0505         painter->setPen(palette.windowText().color());
0506 
0507         drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.top());
0508         drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.bottom());
0509 
0510         drawVerticalDotLine(painter, rowRect.left(), rowRect.top(), rowRect.bottom());
0511         drawVerticalDotLine(painter, rowRect.right(), rowRect.top(), rowRect.bottom());
0512     }
0513 }