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