1 /*
2 SPDX-FileCopyrightText: 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
3 SPDX-FileCopyrightText: 2010-2020 Mladen Milinkovic <max@smoothware.net>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "lineswidget.h"
9 #include "application.h"
10 #include "core/richdocument.h"
11 #include "actions/useractionnames.h"
12 #include "dialogs/actionwithtargetdialog.h"
13 #include "gui/treeview/linesitemdelegate.h"
14 #include "gui/treeview/linesselectionmodel.h"
15
16 #include <QHeaderView>
17 #include <QMenu>
18 #include <QMimeData>
19 #include <QMouseEvent>
20 #include <QPainter>
21
22 #include <KConfigGroup>
23 #include <KSharedConfig>
24
25
26 #define RESTORE_STATE false
27
28 using namespace SubtitleComposer;
29
LinesWidget(QWidget * parent)30 LinesWidget::LinesWidget(QWidget *parent)
31 : TreeView(parent),
32 m_scrollFollowsModel(true),
33 m_translationMode(false),
34 m_showingContextMenu(false),
35 m_itemsDelegate(new LinesItemDelegate(this)),
36 m_inlineEditor(nullptr)
37 {
38 setModel(new LinesModel(this));
39 selectionModel()->deleteLater();
40 setSelectionModel(new LinesSelectionModel(model()));
41
42 for(int column = 0, columnCount = model()->columnCount(); column < columnCount; ++column)
43 setItemDelegateForColumn(column, m_itemsDelegate);
44
45 QHeaderView *hdr = header();
46 hdr->setSectionsClickable(false);
47 hdr->setSectionsMovable(false);
48 updateHeader();
49
50 setUniformRowHeights(true);
51 setItemsExpandable(false);
52 setAutoExpandDelay(-1);
53 setExpandsOnDoubleClick(false);
54 setRootIsDecorated(false);
55 setAllColumnsShowFocus(true);
56 setSortingEnabled(false);
57 setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
58
59 setSelectionMode(QAbstractItemView::ExtendedSelection);
60 setSelectionBehavior(QAbstractItemView::SelectRows);
61
62 m_gridPen.setColor(palette().mid().color().lighter(120));
63
64 setAcceptDrops(true);
65 viewport()->installEventFilter(this);
66
67 connect(selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LinesWidget::onCurrentRowChanged);
68 }
69
~LinesWidget()70 LinesWidget::~LinesWidget()
71 {
72 }
73
74 void
updateHeader()75 LinesWidget::updateHeader()
76 {
77 QHeaderView *hdr = header();
78 for(int i = 0; i < LinesModel::ColumnCount; i++) {
79 switch(i) {
80 case LinesModel::Text:
81 hdr->setSectionResizeMode(i, QHeaderView::Stretch);
82 hdr->setSectionHidden(i, false);
83 break;
84 case LinesModel::Translation:
85 hdr->setSectionResizeMode(i, QHeaderView::Stretch);
86 hdr->setSectionHidden(i, !m_translationMode);
87 break;
88 default:
89 hdr->setSectionResizeMode(i, QHeaderView::ResizeToContents);
90 hdr->setSectionHidden(i, false);
91 break;
92 }
93 }
94 // do this last so columns get autosized
95 hdr->setSectionResizeMode(LinesModel::Text, QHeaderView::Interactive);
96 hdr->setSectionResizeMode(LinesModel::Translation, QHeaderView::Interactive);
97 // give some small size to last column, it will stretch anyways
98 hdr->resizeSection(LinesModel::Translation, 100);
99 }
100
101 void
closeEditor(QWidget * editor,QAbstractItemDelegate::EndEditHint hint)102 LinesWidget::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint)
103 {
104 auto ehint = LinesItemDelegate::ExtendedEditHint(hint);
105 if(ehint == LinesItemDelegate::NoHint || ehint == LinesItemDelegate::SubmitModelCache || ehint == LinesItemDelegate::RevertModelCache) {
106 TreeView::closeEditor(editor, hint);
107 } else {
108 QModelIndex index = currentIndex();
109
110 switch(ehint) {
111 case LinesItemDelegate::EditPreviousItem:
112 if(m_translationMode) {
113 if(index.column() == LinesModel::Translation)
114 index = index.sibling(index.row(), LinesModel::Text);
115 else if(index.row())
116 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
117 index = index.sibling(moveCursor(MoveUp, Qt::NoModifier).row(), LinesModel::Translation);
118 #else
119 index = moveCursor(MoveUp, Qt::NoModifier).siblingAtColumn(LinesModel::Translation);
120 #endif
121 break;
122 }
123 // fallthrough
124 case LinesItemDelegate::EditUpperItem:
125 index = moveCursor(MoveUp, Qt::NoModifier);
126 break;
127 case LinesItemDelegate::EditNextItem:
128 if(m_translationMode) {
129 if(index.column() == LinesModel::Text)
130 index = index.sibling(index.row(), LinesModel::Translation);
131 else if(index.row() < model()->rowCount() - 1)
132 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
133 index = index.sibling(moveCursor(MoveDown, Qt::NoModifier).row(), LinesModel::Text);
134 #else
135 index = moveCursor(MoveDown, Qt::NoModifier).siblingAtColumn(LinesModel::Text);
136 #endif
137 break;
138 }
139 // fallthrough
140 case LinesItemDelegate::EditLowerItem:
141 index = moveCursor(MoveDown, Qt::NoModifier);
142 break;
143 default: break;
144 }
145
146 QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::NoUpdate;
147 if(selectionMode() != NoSelection) {
148 flags = QItemSelectionModel::ClearAndSelect;
149 switch(selectionBehavior()) {
150 case SelectItems: break;
151 case SelectRows: flags |= QItemSelectionModel::Rows; break;
152 case SelectColumns: flags |= QItemSelectionModel::Columns; break;
153 }
154 }
155
156 TreeView::closeEditor(editor, QAbstractItemDelegate::NoHint);
157
158 QPersistentModelIndex persistent(index);
159 selectionModel()->setCurrentIndex(persistent, flags);
160
161 // currentChanged signal would have already started editing
162 if((index.flags() & Qt::ItemIsEditable) && !(editTriggers() & QAbstractItemView::CurrentChanged))
163 edit(persistent);
164 }
165 }
166
167 void
editCurrentLineInPlace(bool primaryText)168 LinesWidget::editCurrentLineInPlace(bool primaryText)
169 {
170 QModelIndex curIdx = currentIndex();
171 if(curIdx.isValid()) {
172 const int col = primaryText || !m_translationMode ? LinesModel::Text : LinesModel::Translation;
173 if(col != curIdx.column()) {
174 curIdx = model()->index(curIdx.row(), col);
175 setCurrentIndex(curIdx);
176 }
177 edit(curIdx);
178 }
179 }
180
181 bool
showingContextMenu()182 LinesWidget::showingContextMenu()
183 {
184 return m_showingContextMenu;
185 }
186
187 SubtitleLine *
currentLine() const188 LinesWidget::currentLine() const
189 {
190 return qobject_cast<LinesSelectionModel *>(selectionModel())->currentLine();
191 }
192
193 int
currentLineIndex() const194 LinesWidget::currentLineIndex() const
195 {
196 const QModelIndex idx = currentIndex();
197 return idx.isValid() ? idx.row() : -1;
198 }
199
200 int
firstSelectedIndex() const201 LinesWidget::firstSelectedIndex() const
202 {
203 int row = -1;
204 const QItemSelection &selection = selectionModel()->selection();
205 for(const QItemSelectionRange &r : selection) {
206 if(row == -1 || r.top() < row)
207 row = r.top();
208 }
209 return row;
210 }
211
212 int
lastSelectedIndex() const213 LinesWidget::lastSelectedIndex() const
214 {
215 int row = -1;
216 const QItemSelection &selection = selectionModel()->selection();
217 for(const QItemSelectionRange &r : selection) {
218 if(r.bottom() > row)
219 row = r.bottom();
220 }
221 return row;
222 }
223
224 bool
selectionHasMultipleRanges() const225 LinesWidget::selectionHasMultipleRanges() const
226 {
227 const QItemSelection &selection = selectionModel()->selection();
228 return selection.size() > 1;
229 }
230
231 RangeList
selectionRanges() const232 LinesWidget::selectionRanges() const
233 {
234 RangeList ranges;
235
236 const QItemSelection &selection = selectionModel()->selection();
237 for(const QItemSelectionRange &r : selection) {
238 ranges << Range(r.top(), r.bottom());
239 }
240
241 return ranges;
242 }
243
244 RangeList
targetRanges(int target) const245 LinesWidget::targetRanges(int target) const
246 {
247 switch(target) {
248 case ActionWithTargetDialog::AllLines:
249 return Range::full();
250 case ActionWithTargetDialog::Selection: return selectionRanges();
251 case ActionWithTargetDialog::FromSelected: {
252 int index = firstSelectedIndex();
253 return index < 0 ? RangeList() : Range::upper(index);
254 }
255 case ActionWithTargetDialog::UpToSelected: {
256 int index = lastSelectedIndex();
257 return index < 0 ? RangeList() : Range::lower(index);
258 }
259 default:
260 return RangeList();
261 }
262 }
263
264 void
loadConfig()265 LinesWidget::loadConfig()
266 {
267 #if RESTORE_STATE
268 KConfigGroup group(KSharedConfig::openConfig()->group("LinesWidget Settings"));
269
270 QByteArray state;
271 QStringList strState = group.readXdgListEntry("Columns State", QStringList() << QString());
272 for(QStringList::ConstIterator it = strState.constBegin(), end = strState.constEnd(); it != end; ++it)
273 state.append(char(it->toInt()));
274 header()->restoreState(state);
275 #endif
276 }
277
278 void
saveConfig()279 LinesWidget::saveConfig()
280 {
281 KConfigGroup group(KSharedConfig::openConfig()->group("LinesWidget Settings"));
282
283 QStringList strState;
284 QByteArray state = header()->saveState();
285 for(int index = 0, size = state.size(); index < size; ++index)
286 strState.append(QString::number(state[index]));
287 group.writeXdgListEntry("Columns State", strState);
288 }
289
290 void
setSubtitle(Subtitle * subtitle)291 LinesWidget::setSubtitle(Subtitle *subtitle)
292 {
293 model()->setSubtitle(subtitle);
294 }
295
296 void
setTranslationMode(bool enabled)297 LinesWidget::setTranslationMode(bool enabled)
298 {
299 if(m_translationMode == enabled)
300 return;
301
302 m_translationMode = enabled;
303 updateHeader();
304 }
305
306 void
setCurrentLine(SubtitleLine * line,bool clearSelection)307 LinesWidget::setCurrentLine(SubtitleLine *line, bool clearSelection)
308 {
309 if(!line)
310 return;
311
312 selectionModel()->setCurrentIndex(model()->index(line->index(), 0),
313 QItemSelectionModel::Select | QItemSelectionModel::Rows
314 | (clearSelection ? QItemSelectionModel::Clear : QItemSelectionModel::NoUpdate));
315 }
316
317 void
setPlayingLine(SubtitleLine * line)318 LinesWidget::setPlayingLine(SubtitleLine *line)
319 {
320 model()->setPlayingLine(line);
321 }
322
323 void
mouseDoubleClickEvent(QMouseEvent * e)324 LinesWidget::mouseDoubleClickEvent(QMouseEvent *e)
325 {
326 QModelIndex index = indexAt(viewport()->mapFromGlobal(e->globalPos()));
327 if(index.isValid())
328 emit lineDoubleClicked(model()->subtitle()->line(index.row()));
329 }
330
331 bool
eventFilter(QObject * object,QEvent * event)332 LinesWidget::eventFilter(QObject *object, QEvent *event)
333 {
334 if(object == viewport()) {
335 switch(event->type()) {
336 case QEvent::DragEnter:
337 case QEvent::Drop:
338 foreach(const QUrl &url, static_cast<QDropEvent *>(event)->mimeData()->urls()) {
339 if(url.scheme() == QLatin1String("file")) {
340 event->accept();
341 if(event->type() == QEvent::Drop) {
342 app()->openSubtitle(url);
343 }
344 return true; // eat event
345 }
346 }
347 event->ignore();
348 return true;
349
350 case QEvent::DragMove:
351 return true; // eat event
352
353 default:
354 ;
355 }
356 }
357 // standard event processing
358 return TreeView::eventFilter(object, event);
359 }
360
361 void
contextMenuEvent(QContextMenuEvent * e)362 LinesWidget::contextMenuEvent(QContextMenuEvent *e)
363 {
364 SubtitleLine *referenceLine = nullptr;
365 QItemSelectionModel *selection = selectionModel();
366 for(int row = 0, rowCount = model()->rowCount(); row < rowCount; ++row) {
367 if(selection->isSelected(model()->index(row, 0))) {
368 referenceLine = model()->subtitle()->line(row);
369 break;
370 }
371 }
372 if(!referenceLine)
373 return;
374
375 QList<QAction *> checkableActions;
376 auto appAction = [&](const char *actionName, bool checkable=false, bool checked=false) -> QAction * {
377 QAction *action = app()->action(actionName);
378 if(checkable) {
379 checkableActions.append(action);
380 action->setCheckable(true);
381 action->setChecked(checked);
382 }
383 return action;
384 };
385
386 QMenu menu;
387 menu.addAction(appAction(ACT_SELECT_ALL_LINES));
388 menu.addAction(appAction(ACT_EDIT_CURRENT_LINE_IN_PLACE));
389 menu.addSeparator();
390 menu.addAction(appAction(ACT_INSERT_BEFORE_CURRENT_LINE));
391 menu.addAction(appAction(ACT_INSERT_AFTER_CURRENT_LINE));
392 menu.addAction(appAction(ACT_REMOVE_SELECTED_LINES));
393 menu.addSeparator();
394 menu.addAction(appAction(ACT_JOIN_SELECTED_LINES));
395 menu.addAction(appAction(ACT_SPLIT_SELECTED_LINES));
396 menu.addSeparator();
397 menu.addAction(appAction(ACT_ANCHOR_TOGGLE));
398 menu.addAction(appAction(ACT_ANCHOR_REMOVE_ALL));
399 menu.addSeparator();
400
401 QMenu textsMenu(i18n("Texts"));
402 textsMenu.addAction(appAction(ACT_ADJUST_TEXTS));
403 textsMenu.addAction(appAction(ACT_UNBREAK_TEXTS));
404 textsMenu.addAction(appAction(ACT_SIMPLIFY_SPACES));
405 textsMenu.addAction(appAction(ACT_CHANGE_CASE));
406 textsMenu.addAction(appAction(ACT_FIX_PUNCTUATION));
407 textsMenu.addAction(appAction(ACT_TRANSLATE));
408 textsMenu.addSeparator();
409 textsMenu.addAction(appAction(ACT_SPELL_CHECK));
410 menu.addMenu(&textsMenu);
411
412 QMenu stylesMenu(i18n("Styles"));
413 const int styleFlags = referenceLine->primaryDoc()->cummulativeStyleFlags() | referenceLine->secondaryDoc()->cummulativeStyleFlags();
414 stylesMenu.addAction(appAction(ACT_TOGGLE_SELECTED_LINES_BOLD, true, styleFlags & SString::Bold));
415 stylesMenu.addAction(appAction(ACT_TOGGLE_SELECTED_LINES_ITALIC, true, styleFlags & SString::Italic));
416 stylesMenu.addAction(appAction(ACT_TOGGLE_SELECTED_LINES_UNDERLINE, true, styleFlags & SString::Underline));
417 stylesMenu.addAction(appAction(ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH, true, styleFlags & SString::StrikeThrough));
418 stylesMenu.addAction(appAction(ACT_CHANGE_SELECTED_LINES_TEXT_COLOR));
419 menu.addMenu(&stylesMenu);
420
421 QMenu timesMenu(i18n("Times"));
422 timesMenu.addAction(appAction(ACT_SHIFT_SELECTED_LINES_FORWARDS));
423 timesMenu.addAction(appAction(ACT_SHIFT_SELECTED_LINES_BACKWARDS));
424 timesMenu.addSeparator();
425 timesMenu.addAction(appAction(ACT_SHIFT));
426 timesMenu.addAction(appAction(ACT_DURATION_LIMITS));
427 timesMenu.addAction(appAction(ACT_AUTOMATIC_DURATIONS));
428 timesMenu.addAction(appAction(ACT_MAXIMIZE_DURATIONS));
429 timesMenu.addAction(appAction(ACT_FIX_OVERLAPPING_LINES));
430 menu.addMenu(×Menu);
431
432 QMenu errorsMenu(i18n("Errors"));
433 errorsMenu.addAction(appAction(ACT_TOGGLE_SELECTED_LINES_MARK, true, referenceLine->errorFlags() & SubtitleLine::UserMark));
434 errorsMenu.addSeparator();
435 errorsMenu.addAction(appAction(ACT_DETECT_ERRORS));
436 errorsMenu.addAction(appAction(ACT_CLEAR_ERRORS));
437 errorsMenu.addSeparator();
438 errorsMenu.addAction(appAction(ACT_SHOW_ERRORS));
439 menu.addMenu(&errorsMenu);
440
441 m_showingContextMenu = true;
442 menu.exec(e->globalPos());
443 m_showingContextMenu = false;
444
445 foreach(QAction *action, checkableActions)
446 action->setCheckable(false);
447
448 e->ignore();
449
450 TreeView::contextMenuEvent(e);
451 }
452
453 void
onCurrentRowChanged()454 LinesWidget::onCurrentRowChanged()
455 {
456 auto sm = qobject_cast<LinesSelectionModel *>(selectionModel());
457 emit currentLineChanged(sm->currentLine());
458 }
459
460 void
drawHorizontalDotLine(QPainter * painter,int x1,int x2,int y)461 LinesWidget::drawHorizontalDotLine(QPainter *painter, int x1, int x2, int y)
462 {
463 static int aux;
464
465 if(x1 > x2) {
466 aux = x1;
467 x1 = x2;
468 x2 = aux;
469 }
470
471 for(int x = x1 + 1; x <= x2; x += 2)
472 painter->drawPoint(x, y);
473 }
474
475 void
drawVerticalDotLine(QPainter * painter,int x,int y1,int y2)476 LinesWidget::drawVerticalDotLine(QPainter *painter, int x, int y1, int y2)
477 {
478 static int aux;
479
480 if(y1 > y2) {
481 aux = y1;
482 y1 = y2;
483 y2 = aux;
484 }
485
486 for(int y = y1 + 1; y <= y2; y += 2)
487 painter->drawPoint(x, y);
488 }
489
490 void
drawRow(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const491 LinesWidget::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
492 {
493 TreeView::drawRow(painter, option, index);
494
495 const int visibleColumns = m_translationMode ? LinesModel::ColumnCount : LinesModel::ColumnCount - 1;
496 const int row = index.row();
497 const bool rowSelected = selectionModel()->isSelected(index);
498 const QPalette palette = this->palette();
499 const QRect rowRect = QRect(visualRect(model()->index(row, 0)).topLeft(),
500 visualRect(model()->index(row, visibleColumns - 1)).bottomRight());
501
502 // draw row grid
503 painter->setPen(m_gridPen);
504 if(!rowSelected)
505 drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.bottom());
506 for(int column = 0; column < visibleColumns - 1; ++column) {
507 const QRect cellRect = visualRect(model()->index(row, column));
508 drawVerticalDotLine(painter, cellRect.right(), rowRect.top(), rowRect.bottom());
509 }
510 if(index.row() == currentIndex().row()) {
511 painter->setPen(palette.windowText().color());
512
513 drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.top());
514 drawHorizontalDotLine(painter, rowRect.left(), rowRect.right(), rowRect.bottom());
515
516 drawVerticalDotLine(painter, rowRect.left(), rowRect.top(), rowRect.bottom());
517 drawVerticalDotLine(painter, rowRect.right(), rowRect.top(), rowRect.bottom());
518 }
519 }
520