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(&timesMenu);
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