1 /*
2     SPDX-FileCopyrightText: 2020 Mladen Milinkovic <max@smoothware.net>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "richlineedit.h"
8 
9 #include "application.h"
10 #include "actions/useractionnames.h"
11 #include "core/richdocumenteditor.h"
12 #include "dialogs/subtitlecolordialog.h"
13 
14 #include <QDrag>
15 #include <QMimeData>
16 #include <QPainter>
17 #include <QStyleHints>
18 
19 #include <KLocalizedString>
20 
21 
22 using namespace SubtitleComposer;
23 
RichLineEdit(QWidget * parent)24 RichLineEdit::RichLineEdit(QWidget *parent)
25 	: QWidget(parent),
26 	  m_control(new RichDocumentEditor())
27 {
28 	setupActions();
29 
30 	setCursor(QCursor(Qt::IBeamCursor));
31 
32 	m_control->setAccessibleObject(this);
33 	m_control->setCursorWidth(style()->pixelMetric(QStyle::PM_TextCursorWidth));
34 
35 	setFocusPolicy(Qt::StrongFocus);
36 	setAttribute(Qt::WA_InputMethodEnabled);
37 	setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed, QSizePolicy::LineEdit));
38 	setBackgroundRole(QPalette::Base);
39 	setAttribute(Qt::WA_KeyCompression);
40 	setMouseTracking(true);
41 	setAcceptDrops(true);
42 
43 	setAttribute(Qt::WA_MacShowFocusRect);
44 
45 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
46 	m_mouseYThreshold = 0;
47 #else
48 	m_mouseYThreshold = QGuiApplication::styleHints()->mouseQuickSelectionThreshold();
49 #endif
50 
51 	connect(m_control, &RichDocumentEditor::cursorPositionChanged, this, QOverload<>::of(&RichLineEdit::update));
52 	connect(m_control, &RichDocumentEditor::selectionChanged, this, QOverload<>::of(&RichLineEdit::update));
53 	connect(m_control, &RichDocumentEditor::displayTextChanged, this, QOverload<>::of(&RichLineEdit::update));
54 	connect(m_control, &RichDocumentEditor::updateNeeded, this, [this](QRect r){ r.setBottom(rect().bottom()); update(r); });
55 	update();
56 }
57 
~RichLineEdit()58 RichLineEdit::~RichLineEdit()
59 {
60 	delete m_control;
61 }
62 
63 static void
setupActionCommon(QAction * act,const char * appActionId)64 setupActionCommon(QAction *act, const char *appActionId)
65 {
66 	QAction *appAction = qobject_cast<QAction *>(app()->action(appActionId));
67 	QObject::connect(appAction, &QAction::changed, act, [act, appAction](){ act->setShortcut(appAction->shortcut()); });
68 	act->setShortcuts(appAction->shortcuts());
69 }
70 
71 void
setupActions()72 RichLineEdit::setupActions()
73 {
74 	m_actions.push_back(app()->action(ACT_UNDO));
75 	m_actions.push_back(app()->action(ACT_REDO));
76 
77 	QAction *act;
78 #ifndef QT_NO_CLIPBOARD
79 	act = new QAction(this);
80 	act->setIcon(QIcon::fromTheme("edit-cut"));
81 	act->setText(i18n("Cut"));
82 	act->setShortcuts(KStandardShortcut::cut());
83 	connect(act, &QAction::triggered, [this](){ m_control->cut(); });
84 	m_actions.push_back(act);
85 
86 	act = new QAction(this);
87 	act->setIcon(QIcon::fromTheme("edit-copy"));
88 	act->setText(i18n("Copy"));
89 	act->setShortcuts(KStandardShortcut::copy());
90 	connect(act, &QAction::triggered, [this](){ m_control->copy(); });
91 	m_actions.push_back(act);
92 
93 	act = new QAction(this);
94 	act->setIcon(QIcon::fromTheme("edit-paste"));
95 	act->setText(i18n("Paste"));
96 	act->setShortcuts(KStandardShortcut::paste());
97 	connect(act, &QAction::triggered, [this](){ m_control->paste(); });
98 	m_actions.push_back(act);
99 #endif
100 
101 	act = new QAction(this);
102 	act->setIcon(QIcon::fromTheme("edit-clear"));
103 	act->setText(i18nc("@action:inmenu Clear all text", "Clear"));
104 	connect(act, &QAction::triggered, [this](){ m_control->clear(); });
105 	m_actions.push_back(act);
106 
107 	act = new QAction(this);
108 	act->setIcon(QIcon::fromTheme("edit-select-all"));
109 	act->setText(i18n("Select All"));
110 	setupActionCommon(act, ACT_SELECT_ALL_LINES);
111 	connect(act, &QAction::triggered, [this](){ m_control->selectAll(); });
112 	m_actions.push_back(act);
113 
114 	act = new QAction(this);
115 	act->setIcon(QIcon::fromTheme("format-text-bold"));
116 	act->setText(i18nc("@action:inmenu Toggle bold style", "Bold"));
117 	act->setCheckable(true);
118 	setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_BOLD);
119 	connect(act, &QAction::triggered, [this](){ m_control->toggleBold(); });
120 	m_actions.push_back(act);
121 
122 	act = new QAction(this);
123 	act->setIcon(QIcon::fromTheme("format-text-italic"));
124 	act->setText(i18nc("@action:inmenu Toggle italic style", "Italic"));
125 	act->setCheckable(true);
126 	setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_ITALIC);
127 	connect(act, &QAction::triggered, [this](){ m_control->toggleItalic(); });
128 	m_actions.push_back(act);
129 
130 	act = new QAction(this);
131 	act->setIcon(QIcon::fromTheme("format-text-underline"));
132 	act->setText(i18nc("@action:inmenu Toggle underline style", "Underline"));
133 	act->setCheckable(true);
134 	setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_UNDERLINE);
135 	connect(act, &QAction::triggered, [this](){ m_control->toggleUnderline(); });
136 	m_actions.push_back(act);
137 
138 	act = new QAction(this);
139 	act->setIcon(QIcon::fromTheme("format-text-strikethrough"));
140 	act->setText(i18nc("@action:inmenu Toggle strike through style", "Strike Through"));
141 	act->setCheckable(true);
142 	setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH);
143 	connect(act, &QAction::triggered, [this](){ m_control->toggleStrikeOut(); });
144 	m_actions.push_back(act);
145 
146 	act = new QAction(this);
147 	act->setIcon(QIcon::fromTheme("format-text-color"));
148 	act->setText(i18nc("@action:inmenu Change Text Color", "Text Color"));
149 	setupActionCommon(act, ACT_CHANGE_SELECTED_LINES_TEXT_COLOR);
150 	connect(act, &QAction::triggered, this, &RichLineEdit::changeTextColor);
151 	m_actions.push_back(act);
152 }
153 
154 void
setDocument(RichDocument * document)155 RichLineEdit::setDocument(RichDocument *document)
156 {
157 	m_document = document;
158 	m_control->setDocument(m_document);
159 	m_control->setFont(m_lineStyle.font);
160 	m_control->setLayoutDirection(m_lineStyle.direction);
161 }
162 
163 void
changeTextColor()164 RichLineEdit::changeTextColor()
165 {
166 	QColor color = SubtitleColorDialog::getColor(m_control->textColor(), this);
167 	if(!color.isValid())
168 		return;
169 	m_control->setTextColor(color);
170 }
171 
172 bool
event(QEvent * e)173 RichLineEdit::event(QEvent *e)
174 {
175 	if(e->type() == QEvent::ShortcutOverride) {
176 		QKeyEvent *ke = static_cast<QKeyEvent *>(e);
177 		const QKeySequence key(ke->modifiers() + ke->key());
178 
179 		for(const QAction *act: m_actions) {
180 			if(act->shortcuts().contains(key)) {
181 				e->accept();
182 				return true;
183 			}
184 		}
185 
186 		m_control->processShortcutOverrideEvent(ke);
187 	}
188 
189 	return QWidget::event(e);
190 }
191 
192 void
mousePressEvent(QMouseEvent * e)193 RichLineEdit::mousePressEvent(QMouseEvent* e)
194 {
195 	m_mousePressPos = e->pos();
196 
197 	if(sendMouseEventToInputContext(e))
198 		return;
199 	if(e->button() == Qt::RightButton)
200 		return;
201 	if(m_tripleClickTimer.isActive() && (e->pos() - m_tripleClickPos).manhattanLength() < QApplication::startDragDistance()) {
202 		m_control->selectAll();
203 		return;
204 	}
205 	bool mark = e->modifiers() & Qt::ShiftModifier;
206 	int cursor = m_control->xToPos(e->pos().x());
207 #if QT_CONFIG(draganddrop)
208 	if(!mark && e->button() == Qt::LeftButton && m_control->inSelection(e->pos().x())) {
209 		if(!m_dndTimer.isActive())
210 			m_dndTimer.start(QApplication::startDragTime(), this);
211 	} else
212 #endif
213 	{
214 		m_control->cursorSetPosition(cursor, mark);
215 	}
216 }
217 
218 bool
sendMouseEventToInputContext(QMouseEvent * e)219 RichLineEdit::sendMouseEventToInputContext(QMouseEvent *e)
220 {
221 #if !defined QT_NO_IM
222 	if(m_control->composeMode()) {
223 		int tmp_cursor = m_control->xToPos(e->pos().x());
224 		int mousePos = tmp_cursor - m_control->cursor();
225 		if(mousePos < 0 || mousePos > m_control->preeditAreaText().length())
226 			mousePos = -1;
227 		if(mousePos >= 0) {
228 			if(e->type() == QEvent::MouseButtonRelease)
229 				QGuiApplication::inputMethod()->invokeAction(QInputMethod::Click, mousePos);
230 			return true;
231 		}
232 	}
233 #else
234 	Q_UNUSED(e);
235 #endif
236 
237 	return false;
238 }
239 
240 void
mouseMoveEvent(QMouseEvent * e)241 RichLineEdit::mouseMoveEvent(QMouseEvent * e)
242 {
243 	if(e->buttons() & Qt::LeftButton) {
244 #if QT_CONFIG(draganddrop)
245 		if(m_dndTimer.isActive()) {
246 			if((m_mousePressPos - e->pos()).manhattanLength() > QApplication::startDragDistance()) {
247 				m_dndTimer.stop();
248 				QMimeData *mime = new QMimeData();
249 				const QTextDocumentFragment &s = m_control->selection();
250 				mime->setHtml(s.toHtml());
251 				mime->setText(s.toPlainText());
252 				QDrag *drag = new QDrag(this);
253 				drag->setMimeData(mime);
254 				Qt::DropAction action = drag->exec(Qt::MoveAction);
255 				if(action == Qt::MoveAction && !m_control->isReadOnly() && drag->target() != this)
256 					m_control->eraseSelectedText();
257 			}
258 		} else
259 #endif
260 		{
261 			const bool select = true;
262 #ifndef QT_NO_IM
263 			if(m_mouseYThreshold > 0 && e->pos().y() > m_mousePressPos.y() + m_mouseYThreshold) {
264 				if(layoutDirection() == Qt::RightToLeft)
265 					m_control->home(select);
266 				else
267 					m_control->end(select);
268 			} else if(m_mouseYThreshold > 0 && e->pos().y() + m_mouseYThreshold < m_mousePressPos.y()) {
269 				if(layoutDirection() == Qt::RightToLeft)
270 					m_control->end(select);
271 				else
272 					m_control->home(select);
273 			} else if(m_control->composeMode() && select) {
274 				int startPos = m_control->xToPos(m_mousePressPos.x());
275 				int currentPos = m_control->xToPos(e->pos().x());
276 				if(startPos != currentPos)
277 					m_control->setSelection(startPos, currentPos - startPos);
278 			} else
279 #endif
280 			{
281 				m_control->cursorSetPosition(m_control->xToPos(e->pos().x()), select);
282 			}
283 		}
284 	}
285 
286 	sendMouseEventToInputContext(e);
287 }
288 
289 void
mouseReleaseEvent(QMouseEvent * e)290 RichLineEdit::mouseReleaseEvent(QMouseEvent* e)
291 {
292 	if(sendMouseEventToInputContext(e))
293 		return;
294 #if QT_CONFIG(draganddrop)
295 	if(e->button() == Qt::LeftButton) {
296 		if(m_dndTimer.isActive()) {
297 			m_dndTimer.stop();
298 			m_control->deselect();
299 			return;
300 		}
301 	}
302 #endif
303 #ifndef QT_NO_CLIPBOARD
304 	if(QApplication::clipboard()->supportsSelection()) {
305 		if(e->button() == Qt::LeftButton) {
306 			m_control->copy(QClipboard::Selection);
307 		} else if(!m_control->isReadOnly() && e->button() == Qt::MidButton) {
308 			m_control->deselect();
309 			m_control->paste(QClipboard::Selection);
310 		}
311 	}
312 #endif
313 }
314 
315 void
mouseDoubleClickEvent(QMouseEvent * e)316 RichLineEdit::mouseDoubleClickEvent(QMouseEvent* e)
317 {
318 	if(e->button() == Qt::LeftButton) {
319 		int position =
320 //				d->xToPos(e->pos().x())
321 				m_control->xToPos(e->pos().x())
322 				;
323 
324 		// exit composition mode
325 #ifndef QT_NO_IM
326 		if(m_control->composeMode()) {
327 			int preeditPos = m_control->cursor();
328 			int posInPreedit = position - m_control->cursor();
329 			int preeditLength = m_control->preeditAreaText().length();
330 			bool positionOnPreedit = false;
331 
332 			if(posInPreedit >= 0 && posInPreedit <= preeditLength)
333 				positionOnPreedit = true;
334 
335 			int textLength = m_control->end();
336 			m_control->commitPreedit();
337 			int sizeChange = m_control->end() - textLength;
338 
339 			if(positionOnPreedit) {
340 				if(sizeChange == 0)
341 					position = -1; // cancel selection, word disappeared
342 				else
343 					// ensure not selecting after preedit if event happened there
344 					position = qBound(preeditPos, position, preeditPos + sizeChange);
345 			} else if(position > preeditPos) {
346 				// adjust positions after former preedit by how much text changed
347 				position += (sizeChange - preeditLength);
348 			}
349 		}
350 #endif
351 
352 		if(position >= 0)
353 			m_control->selectWordAtPos(position);
354 
355 		m_tripleClickTimer.start(QApplication::doubleClickInterval(), this);
356 		m_tripleClickPos = e->pos();
357 	} else {
358 		sendMouseEventToInputContext(e);
359 	}
360 }
361 
362 void
keyPressEvent(QKeyEvent * event)363 RichLineEdit::keyPressEvent(QKeyEvent *event)
364 {
365 	const QKeySequence key(event->modifiers() + event->key());
366 
367 	for(QAction *act: m_actions) {
368 		if(act->shortcuts().contains(key)) {
369 			act->trigger();
370 			m_control->updateDisplayText();
371 			return;
372 		}
373 	}
374 
375 	m_control->processKeyEvent(event);
376 	if(event->isAccepted()) {
377 		if(layoutDirection() != m_control->layoutDirection())
378 			setLayoutDirection(m_control->layoutDirection());
379 		m_control->updateCursorBlinking();
380 		return;
381 	}
382 
383 	QWidget::keyPressEvent(event);
384 }
385 
386 void
inputMethodEvent(QInputMethodEvent * e)387 RichLineEdit::inputMethodEvent(QInputMethodEvent *e)
388 {
389 	if(m_control->isReadOnly()) {
390 		e->ignore();
391 		return;
392 	}
393 
394 	m_control->processInputMethodEvent(e);
395 }
396 
397 QVariant
inputMethodQuery(Qt::InputMethodQuery property) const398 RichLineEdit::inputMethodQuery(Qt::InputMethodQuery property) const
399 {
400 	switch(property) {
401 	case Qt::ImCursorRectangle:
402 		return m_control->cursorRect();
403 	case Qt::ImAnchorRectangle:
404 		return m_control->anchorRect();
405 	case Qt::ImFont:
406 		return font();
407 	case Qt::ImCursorPosition: {
408 		return QVariant(m_control->cursor()); }
409 	case Qt::ImSurroundingText:
410 		return QVariant(m_control->surroundingText());
411 	case Qt::ImCurrentSelection:
412 		return QVariant(m_control->selectedText());
413 	case Qt::ImAnchorPosition:
414 		if(m_control->selectionStart() == m_control->selectionEnd())
415 			return QVariant(m_control->cursor());
416 		else if(m_control->selectionStart() == m_control->cursor())
417 			return QVariant(m_control->selectionEnd());
418 		else
419 			return QVariant(m_control->selectionStart());
420 	default:
421 		return QWidget::inputMethodQuery(property);
422 	}
423 }
424 
425 void
focusInEvent(QFocusEvent * e)426 RichLineEdit::focusInEvent(QFocusEvent *e)
427 {
428 	if(e->reason() == Qt::TabFocusReason || e->reason() == Qt::BacktabFocusReason || e->reason() == Qt::ShortcutFocusReason) {
429 		if(!m_control->hasSelection())
430 			m_control->selectAll();
431 	} else if(e->reason() == Qt::MouseFocusReason) {
432 		// no need to handle this yet
433 	}
434 	m_control->setBlinkingCursorEnabled(true);
435 #if QT_CONFIG(completer)
436 	if(m_control->completer()) {
437 		m_control->completer()->setWidget(this);
438 		// FIXME: completion
439 //		QObject::connect(m_control->completer(), &QCompleter::activated, this, &RichLineEdit::setText);
440 //		QObject::connect(m_control->completer(), &QCompleter::highlighted, this, &RichLineEdit::_q_completionHighlighted);
441 	}
442 #endif
443 	update();
444 }
445 
446 void
focusOutEvent(QFocusEvent * e)447 RichLineEdit::focusOutEvent(QFocusEvent *e)
448 {
449 	Qt::FocusReason reason = e->reason();
450 	if(reason != Qt::ActiveWindowFocusReason && reason != Qt::PopupFocusReason)
451 		m_control->deselect();
452 
453 	m_control->setBlinkingCursorEnabled(false);
454 	if(reason != Qt::PopupFocusReason || !(QApplication::activePopupWidget() && QApplication::activePopupWidget()->parentWidget() == this)) {
455 //		if(hasAcceptableInput() || m_control->fixup())
456 //			emit editingFinished();
457 	}
458 #if QT_CONFIG(completer)
459 	if(m_control->completer()) {
460 		QObject::disconnect(m_control->completer(), 0, this, 0);
461 	}
462 #endif
463 	QWidget::focusOutEvent(e);
464 }
465 
466 void
changeEvent(QEvent * e)467 RichLineEdit::changeEvent(QEvent *e)
468 {
469 	switch(e->type())
470 	{
471 	case QEvent::ActivationChange:
472 		if(!palette().isEqual(QPalette::Active, QPalette::Inactive))
473 			update();
474 		break;
475 	case QEvent::FontChange:
476 		m_control->setFont(font());
477 		break;
478 	case QEvent::StyleChange:
479 		update();
480 		break;
481 	default:
482 		break;
483 	}
484 	QWidget::changeEvent(e);
485 }
486 
487 
488 #if QT_CONFIG(draganddrop)
489 void
dragMoveEvent(QDragMoveEvent * e)490 RichLineEdit::dragMoveEvent(QDragMoveEvent *e)
491 {
492 	if(!m_control->isReadOnly() && (e->mimeData()->hasText() || e->mimeData()->hasHtml())) {
493 		e->acceptProposedAction();
494 		m_dndCursor = m_control->xToPos(e->pos().x());
495 		update();
496 	}
497 }
498 
499 void
dragEnterEvent(QDragEnterEvent * e)500 RichLineEdit::dragEnterEvent(QDragEnterEvent *e)
501 {
502 	dragMoveEvent(e);
503 }
504 
505 void
dragLeaveEvent(QDragLeaveEvent *)506 RichLineEdit::dragLeaveEvent(QDragLeaveEvent *)
507 {
508 	if(m_dndCursor >= 0) {
509 		m_dndCursor = -1;
510 		update();
511 	}
512 }
513 
514 void
dropEvent(QDropEvent * e)515 RichLineEdit::dropEvent(QDropEvent *e)
516 {
517 	QString str = e->mimeData()->html();
518 	RichDocumentEditor::TextType strType = RichDocumentEditor::HTML;
519 	if(str.isEmpty()) {
520 		str = e->mimeData()->text();
521 		strType = RichDocumentEditor::Plain;
522 	}
523 	if(!str.isNull() && !m_control->isReadOnly()) {
524 		int dropPos = m_control->xToPos(e->pos().x());
525 		m_dndCursor = -1;
526 		if(e->source() == this && e->dropAction() == Qt::MoveAction && m_control->selectionContains(dropPos)) {
527 			e->ignore();
528 			return;
529 		}
530 		if(e->source() == this && e->dropAction() == Qt::CopyAction)
531 			m_control->eraseSelectedText();
532 		e->acceptProposedAction();
533 		const int len = m_control->insert(str, dropPos, strType);
534 		if(e->source() == this)
535 			m_control->setSelection(m_control->cursor() - len, len);
536 	} else {
537 		e->ignore();
538 		update();
539 	}
540 }
541 #endif // QT_CONFIG(draganddrop)
542 
543 void
paintEvent(QPaintEvent * e)544 RichLineEdit::paintEvent(QPaintEvent *e)
545 {
546 	QPainter p(this);
547 	p.setClipRect(e->rect());
548 
549 	const QColor textColor = m_lineStyle.palette.color(QPalette::Normal, (m_lineStyle.state & QStyle::State_Selected) ? QPalette::HighlightedText : QPalette::Text);
550 	const QStyle *style = m_lineStyle.widget ? m_lineStyle.widget->style() : QApplication::style();
551 
552 	const int hMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_lineStyle.widget);
553 	const int vMargin = style->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, m_lineStyle.widget);
554 
555 	p.setPen(textColor);
556 
557 	QRect textRect = rect();
558 	p.fillRect(textRect, m_lineStyle.palette.color(QPalette::Normal, QPalette::Highlight));
559 
560 	textRect.adjust(hMargin, vMargin, -hMargin, -vMargin);
561 	p.fillRect(textRect, m_lineStyle.palette.color(QPalette::Normal, QPalette::Window));
562 
563 	textRect.adjust(1, 1, -1, -1);
564 
565 	QPoint textPos(textRect.topLeft());
566 	textPos.ry() += (qreal(textRect.height()) - m_control->textLayout()->lineAt(0).height()) / 2.;
567 
568 	int flags = RichDocumentEditor::DrawText | RichDocumentEditor::DrawCursor;
569 	if(m_control->hasSelection())
570 		flags |= RichDocumentEditor::DrawSelections;
571 	m_control->draw(&p, textPos, rect(), flags, m_dndCursor);
572 }
573