File indexing completed on 2024-12-22 04:40:11
0001 /* 0002 SPDX-FileCopyrightText: 2020-2022 Mladen Milinkovic <max@smoothware.net> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "richlineedit.h" 0008 0009 #include "appglobal.h" 0010 #include "application.h" 0011 #include "actions/useractionnames.h" 0012 #include "core/richtext/richdocumenteditor.h" 0013 #include "dialogs/subtitlecolordialog.h" 0014 0015 #include <QDrag> 0016 #include <QMimeData> 0017 #include <QPainter> 0018 #include <QStyleHints> 0019 0020 #include <KLocalizedString> 0021 0022 0023 using namespace SubtitleComposer; 0024 0025 RichLineEdit::RichLineEdit(const QStyleOptionViewItem &styleOption, QWidget *parent) 0026 : QWidget(parent), 0027 m_lineStyle(styleOption), 0028 m_control(new RichDocumentEditor()) 0029 { 0030 setupActions(); 0031 0032 setCursor(QCursor(Qt::IBeamCursor)); 0033 0034 m_control->setAccessibleObject(this); 0035 m_control->setCursorWidth(style()->pixelMetric(QStyle::PM_TextCursorWidth)); 0036 m_control->setLineSeparatorSize(QSizeF(.5 * styleOption.rect.height(), styleOption.rect.height())); 0037 0038 setFocusPolicy(Qt::StrongFocus); 0039 setAttribute(Qt::WA_InputMethodEnabled); 0040 setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed, QSizePolicy::LineEdit)); 0041 setBackgroundRole(QPalette::Base); 0042 setAttribute(Qt::WA_KeyCompression); 0043 setMouseTracking(true); 0044 setAcceptDrops(true); 0045 0046 setAttribute(Qt::WA_MacShowFocusRect); 0047 0048 #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) 0049 m_mouseYThreshold = 0; 0050 #else 0051 m_mouseYThreshold = QGuiApplication::styleHints()->mouseQuickSelectionThreshold(); 0052 #endif 0053 0054 connect(m_control, &RichDocumentEditor::cursorPositionChanged, this, QOverload<>::of(&RichLineEdit::update)); 0055 connect(m_control, &RichDocumentEditor::selectionChanged, this, QOverload<>::of(&RichLineEdit::update)); 0056 connect(m_control, &RichDocumentEditor::displayTextChanged, this, QOverload<>::of(&RichLineEdit::update)); 0057 connect(m_control, &RichDocumentEditor::updateNeeded, this, [this](QRect r){ r.setBottom(rect().bottom()); update(r); }); 0058 update(); 0059 } 0060 0061 RichLineEdit::~RichLineEdit() 0062 { 0063 delete m_control; 0064 } 0065 0066 static void 0067 setupActionCommon(QAction *act, const char *appActionId) 0068 { 0069 QAction *appAction = qobject_cast<QAction *>(app()->action(appActionId)); 0070 QObject::connect(appAction, &QAction::changed, act, [act, appAction](){ act->setShortcut(appAction->shortcut()); }); 0071 act->setShortcuts(appAction->shortcuts()); 0072 } 0073 0074 void 0075 RichLineEdit::setupActions() 0076 { 0077 m_actions.push_back(app()->action(ACT_UNDO)); 0078 m_actions.push_back(app()->action(ACT_REDO)); 0079 0080 QAction *act; 0081 #ifndef QT_NO_CLIPBOARD 0082 act = new QAction(this); 0083 act->setIcon(QIcon::fromTheme("edit-cut")); 0084 act->setText(i18n("Cut")); 0085 act->setShortcuts(KStandardShortcut::cut()); 0086 connect(act, &QAction::triggered, this, [this](){ m_control->cut(); }); 0087 m_actions.push_back(act); 0088 0089 act = new QAction(this); 0090 act->setIcon(QIcon::fromTheme("edit-copy")); 0091 act->setText(i18n("Copy")); 0092 act->setShortcuts(KStandardShortcut::copy()); 0093 connect(act, &QAction::triggered, this, [this](){ m_control->copy(); }); 0094 m_actions.push_back(act); 0095 0096 act = new QAction(this); 0097 act->setIcon(QIcon::fromTheme("edit-paste")); 0098 act->setText(i18n("Paste")); 0099 act->setShortcuts(KStandardShortcut::paste()); 0100 connect(act, &QAction::triggered, this, [this](){ m_control->paste(); }); 0101 m_actions.push_back(act); 0102 #endif 0103 0104 act = new QAction(this); 0105 act->setIcon(QIcon::fromTheme("edit-clear")); 0106 act->setText(i18nc("@action:inmenu Clear all text", "Clear")); 0107 connect(act, &QAction::triggered, this, [this](){ m_control->clear(); }); 0108 m_actions.push_back(act); 0109 0110 act = new QAction(this); 0111 act->setIcon(QIcon::fromTheme("edit-select-all")); 0112 act->setText(i18n("Select All")); 0113 setupActionCommon(act, ACT_SELECT_ALL_LINES); 0114 connect(act, &QAction::triggered, this, [this](){ m_control->selectAll(); }); 0115 m_actions.push_back(act); 0116 0117 act = new QAction(this); 0118 act->setIcon(QIcon::fromTheme("format-text-bold")); 0119 act->setText(i18nc("@action:inmenu Toggle bold style", "Bold")); 0120 act->setCheckable(true); 0121 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_BOLD); 0122 connect(act, &QAction::triggered, this, [this](){ m_control->toggleBold(); }); 0123 m_actions.push_back(act); 0124 0125 act = new QAction(this); 0126 act->setIcon(QIcon::fromTheme("format-text-italic")); 0127 act->setText(i18nc("@action:inmenu Toggle italic style", "Italic")); 0128 act->setCheckable(true); 0129 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_ITALIC); 0130 connect(act, &QAction::triggered, this, [this](){ m_control->toggleItalic(); }); 0131 m_actions.push_back(act); 0132 0133 act = new QAction(this); 0134 act->setIcon(QIcon::fromTheme("format-text-underline")); 0135 act->setText(i18nc("@action:inmenu Toggle underline style", "Underline")); 0136 act->setCheckable(true); 0137 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_UNDERLINE); 0138 connect(act, &QAction::triggered, this, [this](){ m_control->toggleUnderline(); }); 0139 m_actions.push_back(act); 0140 0141 act = new QAction(this); 0142 act->setIcon(QIcon::fromTheme("format-text-strikethrough")); 0143 act->setText(i18nc("@action:inmenu Toggle strike through style", "Strike Through")); 0144 act->setCheckable(true); 0145 setupActionCommon(act, ACT_TOGGLE_SELECTED_LINES_STRIKETHROUGH); 0146 connect(act, &QAction::triggered, this, [this](){ m_control->toggleStrikeOut(); }); 0147 m_actions.push_back(act); 0148 0149 act = new QAction(this); 0150 act->setIcon(QIcon::fromTheme("format-text-color")); 0151 act->setText(i18nc("@action:inmenu Change Text Color", "Text Color")); 0152 setupActionCommon(act, ACT_CHANGE_SELECTED_LINES_TEXT_COLOR); 0153 connect(act, &QAction::triggered, this, &RichLineEdit::changeTextColor); 0154 m_actions.push_back(act); 0155 } 0156 0157 void 0158 RichLineEdit::setDocument(RichDocument *document) 0159 { 0160 m_document = document; 0161 m_control->setDocument(m_document); 0162 m_control->setFont(m_lineStyle.font); 0163 m_control->setLayoutDirection(m_lineStyle.direction); 0164 } 0165 0166 void 0167 RichLineEdit::changeTextColor() 0168 { 0169 QColor color = SubtitleColorDialog::getColor(m_control->textColor(), this); 0170 if(!color.isValid()) 0171 return; 0172 m_control->setTextColor(color); 0173 } 0174 0175 bool 0176 RichLineEdit::event(QEvent *e) 0177 { 0178 if(e->type() == QEvent::ShortcutOverride) { 0179 QKeyEvent *ke = static_cast<QKeyEvent *>(e); 0180 const QKeySequence key(ke->modifiers() | ke->key()); 0181 0182 for(const QAction *act: m_actions) { 0183 if(act->shortcuts().contains(key)) { 0184 e->accept(); 0185 return true; 0186 } 0187 } 0188 0189 m_control->processShortcutOverrideEvent(ke); 0190 } 0191 0192 return QWidget::event(e); 0193 } 0194 0195 void 0196 RichLineEdit::mousePressEvent(QMouseEvent* e) 0197 { 0198 m_mousePressPos = e->pos(); 0199 0200 if(sendMouseEventToInputContext(e)) 0201 return; 0202 if(e->button() == Qt::RightButton) 0203 return; 0204 if(m_tripleClickTimer.isActive() && (e->pos() - m_tripleClickPos).manhattanLength() < QApplication::startDragDistance()) { 0205 m_control->selectAll(); 0206 return; 0207 } 0208 bool mark = e->modifiers() & Qt::ShiftModifier; 0209 int cursor = m_control->xToPos(e->pos().x()); 0210 #if QT_CONFIG(draganddrop) 0211 if(!mark && e->button() == Qt::LeftButton && m_control->inSelection(e->pos().x())) { 0212 if(!m_dndTimer.isActive()) 0213 m_dndTimer.start(QApplication::startDragTime(), this); 0214 } else 0215 #endif 0216 { 0217 m_control->cursorSetPosition(cursor, mark); 0218 } 0219 } 0220 0221 bool 0222 RichLineEdit::sendMouseEventToInputContext(QMouseEvent *e) 0223 { 0224 #if !defined QT_NO_IM 0225 if(m_control->composeMode()) { 0226 int tmp_cursor = m_control->xToPos(e->pos().x()); 0227 int mousePos = tmp_cursor - m_control->cursor(); 0228 if(mousePos < 0 || mousePos > m_control->preeditAreaText().length()) 0229 mousePos = -1; 0230 if(mousePos >= 0) { 0231 if(e->type() == QEvent::MouseButtonRelease) 0232 QGuiApplication::inputMethod()->invokeAction(QInputMethod::Click, mousePos); 0233 return true; 0234 } 0235 } 0236 #else 0237 Q_UNUSED(e); 0238 #endif 0239 0240 return false; 0241 } 0242 0243 void 0244 RichLineEdit::mouseMoveEvent(QMouseEvent * e) 0245 { 0246 if(e->buttons() & Qt::LeftButton) { 0247 #if QT_CONFIG(draganddrop) 0248 if(m_dndTimer.isActive()) { 0249 if((m_mousePressPos - e->pos()).manhattanLength() > QApplication::startDragDistance()) { 0250 m_dndTimer.stop(); 0251 QMimeData *mime = new QMimeData(); 0252 const QTextDocumentFragment &s = m_control->selection(); 0253 mime->setHtml(s.toHtml()); 0254 mime->setText(s.toPlainText()); 0255 QDrag *drag = new QDrag(this); 0256 drag->setMimeData(mime); 0257 Qt::DropAction action = drag->exec(Qt::MoveAction); 0258 if(action == Qt::MoveAction && !m_control->isReadOnly() && drag->target() != this) 0259 m_control->eraseSelectedText(); 0260 } 0261 } else 0262 #endif 0263 { 0264 const bool select = true; 0265 #ifndef QT_NO_IM 0266 if(m_mouseYThreshold > 0 && e->pos().y() > m_mousePressPos.y() + m_mouseYThreshold) { 0267 if(layoutDirection() == Qt::RightToLeft) 0268 m_control->home(select); 0269 else 0270 m_control->end(select); 0271 } else if(m_mouseYThreshold > 0 && e->pos().y() + m_mouseYThreshold < m_mousePressPos.y()) { 0272 if(layoutDirection() == Qt::RightToLeft) 0273 m_control->end(select); 0274 else 0275 m_control->home(select); 0276 } else if(m_control->composeMode() && select) { 0277 int startPos = m_control->xToPos(m_mousePressPos.x()); 0278 int currentPos = m_control->xToPos(e->pos().x()); 0279 if(startPos != currentPos) 0280 m_control->setSelection(startPos, currentPos - startPos); 0281 } else 0282 #endif 0283 { 0284 m_control->cursorSetPosition(m_control->xToPos(e->pos().x()), select); 0285 } 0286 } 0287 } 0288 0289 sendMouseEventToInputContext(e); 0290 } 0291 0292 void 0293 RichLineEdit::mouseReleaseEvent(QMouseEvent* e) 0294 { 0295 if(sendMouseEventToInputContext(e)) 0296 return; 0297 #if QT_CONFIG(draganddrop) 0298 if(e->button() == Qt::LeftButton) { 0299 if(m_dndTimer.isActive()) { 0300 m_dndTimer.stop(); 0301 m_control->deselect(); 0302 return; 0303 } 0304 } 0305 #endif 0306 #ifndef QT_NO_CLIPBOARD 0307 if(QApplication::clipboard()->supportsSelection()) { 0308 if(e->button() == Qt::LeftButton) { 0309 m_control->copy(QClipboard::Selection); 0310 } else if(!m_control->isReadOnly() && e->button() == Qt::MiddleButton) { 0311 m_control->deselect(); 0312 m_control->paste(QClipboard::Selection); 0313 } 0314 } 0315 #endif 0316 } 0317 0318 void 0319 RichLineEdit::mouseDoubleClickEvent(QMouseEvent* e) 0320 { 0321 if(e->button() == Qt::LeftButton) { 0322 int position = m_control->xToPos(e->pos().x()); 0323 0324 // exit composition mode 0325 #ifndef QT_NO_IM 0326 if(m_control->composeMode()) { 0327 int preeditPos = m_control->cursor(); 0328 int posInPreedit = position - m_control->cursor(); 0329 int preeditLength = m_control->preeditAreaText().length(); 0330 bool positionOnPreedit = false; 0331 0332 if(posInPreedit >= 0 && posInPreedit <= preeditLength) 0333 positionOnPreedit = true; 0334 0335 int textLength = m_control->end(); 0336 m_control->commitPreedit(); 0337 int sizeChange = m_control->end() - textLength; 0338 0339 if(positionOnPreedit) { 0340 if(sizeChange == 0) 0341 position = -1; // cancel selection, word disappeared 0342 else 0343 // ensure not selecting after preedit if event happened there 0344 position = qBound(preeditPos, position, preeditPos + sizeChange); 0345 } else if(position > preeditPos) { 0346 // adjust positions after former preedit by how much text changed 0347 position += (sizeChange - preeditLength); 0348 } 0349 } 0350 #endif 0351 0352 if(position >= 0) 0353 m_control->selectWordAtPos(position); 0354 0355 m_tripleClickTimer.start(QApplication::doubleClickInterval(), this); 0356 m_tripleClickPos = e->pos(); 0357 } else { 0358 sendMouseEventToInputContext(e); 0359 } 0360 } 0361 0362 void 0363 RichLineEdit::keyPressEvent(QKeyEvent *event) 0364 { 0365 const QKeySequence key(event->modifiers() | event->key()); 0366 0367 for(QAction *act: m_actions) { 0368 if(act->shortcuts().contains(key)) { 0369 act->trigger(); 0370 m_control->updateDisplayText(); 0371 return; 0372 } 0373 } 0374 0375 m_control->processKeyEvent(event); 0376 if(event->isAccepted()) { 0377 if(layoutDirection() != m_control->layoutDirection()) 0378 setLayoutDirection(m_control->layoutDirection()); 0379 m_control->updateCursorBlinking(); 0380 return; 0381 } 0382 0383 QWidget::keyPressEvent(event); 0384 } 0385 0386 void 0387 RichLineEdit::inputMethodEvent(QInputMethodEvent *e) 0388 { 0389 if(m_control->isReadOnly()) { 0390 e->ignore(); 0391 return; 0392 } 0393 0394 m_control->processInputMethodEvent(e); 0395 } 0396 0397 QVariant 0398 RichLineEdit::inputMethodQuery(Qt::InputMethodQuery property) const 0399 { 0400 switch(property) { 0401 case Qt::ImCursorRectangle: 0402 return m_control->cursorRect(); 0403 case Qt::ImAnchorRectangle: 0404 return m_control->anchorRect(); 0405 case Qt::ImFont: 0406 return font(); 0407 case Qt::ImCursorPosition: { 0408 return QVariant(m_control->cursor()); } 0409 case Qt::ImSurroundingText: 0410 return QVariant(m_control->text()); 0411 case Qt::ImCurrentSelection: 0412 return QVariant(m_control->selectedText()); 0413 case Qt::ImAnchorPosition: 0414 if(m_control->selectionStart() == m_control->selectionEnd()) 0415 return QVariant(m_control->cursor()); 0416 else if(m_control->selectionStart() == m_control->cursor()) 0417 return QVariant(m_control->selectionEnd()); 0418 else 0419 return QVariant(m_control->selectionStart()); 0420 default: 0421 return QWidget::inputMethodQuery(property); 0422 } 0423 } 0424 0425 void 0426 RichLineEdit::focusInEvent(QFocusEvent *e) 0427 { 0428 if(e->reason() == Qt::TabFocusReason || e->reason() == Qt::BacktabFocusReason || e->reason() == Qt::ShortcutFocusReason) { 0429 if(!m_control->hasSelection()) 0430 m_control->selectAll(); 0431 } else if(e->reason() == Qt::MouseFocusReason) { 0432 // no need to handle this yet 0433 } 0434 m_control->setBlinkingCursorEnabled(true); 0435 #if QT_CONFIG(completer) 0436 if(m_control->completer()) { 0437 m_control->completer()->setWidget(this); 0438 // FIXME: completion 0439 // QObject::connect(m_control->completer(), &QCompleter::activated, this, &RichLineEdit::setText); 0440 // QObject::connect(m_control->completer(), &QCompleter::highlighted, this, &RichLineEdit::_q_completionHighlighted); 0441 } 0442 #endif 0443 update(); 0444 } 0445 0446 void 0447 RichLineEdit::focusOutEvent(QFocusEvent *e) 0448 { 0449 Qt::FocusReason reason = e->reason(); 0450 if(reason != Qt::ActiveWindowFocusReason && reason != Qt::PopupFocusReason) 0451 m_control->deselect(); 0452 0453 m_control->setBlinkingCursorEnabled(false); 0454 if(reason != Qt::PopupFocusReason || !(QApplication::activePopupWidget() && QApplication::activePopupWidget()->parentWidget() == this)) { 0455 // if(hasAcceptableInput() || m_control->fixup()) 0456 // emit editingFinished(); 0457 } 0458 #if QT_CONFIG(completer) 0459 if(m_control->completer()) { 0460 QObject::disconnect(m_control->completer(), 0, this, 0); 0461 } 0462 #endif 0463 QWidget::focusOutEvent(e); 0464 } 0465 0466 void 0467 RichLineEdit::changeEvent(QEvent *e) 0468 { 0469 switch(e->type()) 0470 { 0471 case QEvent::ActivationChange: 0472 if(!palette().isEqual(QPalette::Active, QPalette::Inactive)) 0473 update(); 0474 break; 0475 case QEvent::FontChange: 0476 m_control->setFont(font()); 0477 break; 0478 case QEvent::StyleChange: 0479 update(); 0480 break; 0481 default: 0482 break; 0483 } 0484 QWidget::changeEvent(e); 0485 } 0486 0487 0488 #if QT_CONFIG(draganddrop) 0489 void 0490 RichLineEdit::dragMoveEvent(QDragMoveEvent *e) 0491 { 0492 if(!m_control->isReadOnly() && (e->mimeData()->hasText() || e->mimeData()->hasHtml())) { 0493 e->acceptProposedAction(); 0494 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0495 m_dndCursor = m_control->xToPos(e->pos().x()); 0496 #else 0497 m_dndCursor = m_control->xToPos(e->position().x()); 0498 #endif 0499 update(); 0500 } 0501 } 0502 0503 void 0504 RichLineEdit::dragEnterEvent(QDragEnterEvent *e) 0505 { 0506 dragMoveEvent(e); 0507 } 0508 0509 void 0510 RichLineEdit::dragLeaveEvent(QDragLeaveEvent *) 0511 { 0512 if(m_dndCursor >= 0) { 0513 m_dndCursor = -1; 0514 update(); 0515 } 0516 } 0517 0518 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0519 #define position pos 0520 #endif 0521 0522 void 0523 RichLineEdit::dropEvent(QDropEvent *e) 0524 { 0525 QString str = e->mimeData()->html(); 0526 RichDocumentEditor::TextType strType = RichDocumentEditor::HTML; 0527 if(str.isEmpty()) { 0528 str = e->mimeData()->text(); 0529 strType = RichDocumentEditor::Plain; 0530 } 0531 if(!str.isNull() && !m_control->isReadOnly()) { 0532 int dropPos = m_control->xToPos(e->position().x()); 0533 m_dndCursor = -1; 0534 if(e->source() == this && e->dropAction() == Qt::MoveAction && m_control->selectionContains(dropPos)) { 0535 e->ignore(); 0536 return; 0537 } 0538 if(e->source() == this && e->dropAction() == Qt::CopyAction) 0539 m_control->eraseSelectedText(); 0540 e->acceptProposedAction(); 0541 const int len = m_control->insert(str, dropPos, strType); 0542 if(e->source() == this) 0543 m_control->setSelection(m_control->cursor() - len, len); 0544 } else { 0545 e->ignore(); 0546 update(); 0547 } 0548 } 0549 #endif // QT_CONFIG(draganddrop) 0550 0551 void 0552 RichLineEdit::paintEvent(QPaintEvent *e) 0553 { 0554 QPainter p(this); 0555 p.setClipRect(e->rect()); 0556 0557 const QColor textColor = m_lineStyle.palette.color(QPalette::Normal, (m_lineStyle.state & QStyle::State_Selected) ? QPalette::HighlightedText : QPalette::Text); 0558 const QStyle *style = m_lineStyle.widget ? m_lineStyle.widget->style() : QApplication::style(); 0559 0560 const int hMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, m_lineStyle.widget); 0561 const int vMargin = style->pixelMetric(QStyle::PM_FocusFrameVMargin, nullptr, m_lineStyle.widget); 0562 0563 p.setPen(textColor); 0564 0565 QRect textRect = rect(); 0566 p.fillRect(textRect, m_lineStyle.palette.color(QPalette::Normal, QPalette::Highlight)); 0567 0568 textRect.adjust(hMargin, vMargin, -hMargin, -vMargin); 0569 p.fillRect(textRect, m_lineStyle.palette.color(QPalette::Normal, QPalette::Window)); 0570 0571 textRect.adjust(1, 1, -1, -1); 0572 0573 QPoint textPos(textRect.topLeft()); 0574 textPos.ry() += (qreal(textRect.height()) - m_control->height()) / 2.; 0575 0576 int flags = RichDocumentEditor::DrawText | RichDocumentEditor::DrawCursor; 0577 if(m_control->hasSelection()) 0578 flags |= RichDocumentEditor::DrawSelections; 0579 m_control->draw(&p, textPos, rect(), flags, m_dndCursor); 0580 }