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 }