#include "TransmitTextEdit.h" #include "JS8_Include/commons.h" #include "varicode.h" #include #include Q_DECLARE_LOGGING_CATEGORY(transmittextedit_js8) void setTextEditFont(QTextEdit *edit, QFont font) { // all uppercase font.setCapitalization(QFont::AllUppercase); edit->setFont(font); edit->setFontFamily(font.family()); edit->setFontItalic(font.italic()); edit->setFontPointSize(font.pointSize()); edit->setFontUnderline(font.underline()); edit->setFontWeight(font.weight()); auto d = edit->document(); d->setDefaultFont(font); edit->setDocument(d); auto c = edit->textCursor(); c.select(QTextCursor::Document); auto cf = c.blockCharFormat(); cf.setFont(font); cf.setFontCapitalization(QFont::AllUppercase); c.mergeBlockCharFormat(cf); edit->updateGeometry(); } void setTextEditStyle(QTextEdit *edit, QColor fg, QColor bg, QFont font) { edit->setStyleSheet(QString("QTextEdit { color:%1; background: %2; %3; }") .arg(fg.name()) .arg(bg.name()) .arg(font_as_stylesheet(font))); // QTimer::singleShot(10, nullptr, [edit, fg, bg](){ QPalette p = edit->palette(); p.setColor(QPalette::Base, bg); p.setColor(QPalette::Active, QPalette::Base, bg); p.setColor(QPalette::Disabled, QPalette::Base, bg); p.setColor(QPalette::Inactive, QPalette::Base, bg); p.setColor(QPalette::Text, fg); p.setColor(QPalette::Active, QPalette::Text, fg); p.setColor(QPalette::Disabled, QPalette::Text, fg); p.setColor(QPalette::Inactive, QPalette::Text, fg); edit->setBackgroundRole(QPalette::Base); edit->setForegroundRole(QPalette::Text); edit->setPalette(p); edit->updateGeometry(); edit->update(); //}); } void highlightBlock(QTextBlock block, QFont font, QColor foreground, QColor background) { QTextCursor cursor(block); // Set background color QTextBlockFormat blockFormat = cursor.blockFormat(); blockFormat.setBackground(background); cursor.setBlockFormat(blockFormat); // Set font /* for (QTextBlock::iterator it = cursor.block().begin(); !(it.atEnd()); ++it) { QTextCharFormat charFormat = it.fragment().charFormat(); charFormat.setFont(font); charFormat.setFontCapitalization(QFont::AllUppercase); charFormat.setForeground(QBrush(foreground)); QTextCursor tempCursor = cursor; tempCursor.setPosition(it.fragment().position()); tempCursor.setPosition(it.fragment().position() + it.fragment().length(), QTextCursor::KeepAnchor); tempCursor.setCharFormat(charFormat); } */ cursor.select(QTextCursor::BlockUnderCursor); auto charFormat = cursor.charFormat(); charFormat.setFont(font); charFormat.setFontCapitalization(QFont::AllUppercase); charFormat.setForeground(QBrush(foreground)); cursor.setCharFormat(charFormat); } TransmitTextEdit::TransmitTextEdit(QWidget *parent) : QTextEdit(parent), m_sent{0}, m_protected{false} { connect(this, &QTextEdit::selectionChanged, this, &TransmitTextEdit::on_selectionChanged); connect(this, &QTextEdit::cursorPositionChanged, this, &TransmitTextEdit::on_selectionChanged); connect(this->document(), &QTextDocument::contentsChange, this, &TransmitTextEdit::on_textContentsChanged); installEventFilter(this); } void TransmitTextEdit::setCharsSent(int n) { // Never can send more than the document length. // From the QTextDocument::characterCount() documentation: // As a QTextDocument always contains at least one // QChar::ParagraphSeparator, this method will return at least 1. We do not // send that paragraph separator. n = qMin(n, document()->characterCount() - 1); // update sent display auto c = textCursor(); c.movePosition(QTextCursor::Start); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, n); // keep track of sent text m_textSent = c.selectedText().toUpper(); m_sent = n; // highlight the sent text highlight(); } // override QString TransmitTextEdit::toPlainText() const { return QTextEdit::toPlainText().toUpper(); } // override void TransmitTextEdit::setPlainText(const QString &text) { m_textSent.clear(); m_sent = 0; // Do this last, as it may trigger events that, during processing, like to // see m_textSent and m_sent reset. QTextEdit::setPlainText(text); } // void TransmitTextEdit::replaceUnsentText(const QString &text, bool keepCursor) { auto rel = relativeTextCursorPosition(textCursor()); auto c = textCursor(); c.movePosition(QTextCursor::Start); c.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_sent); c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); c.removeSelectedText(); c.insertText(text); // keep cursor if (keepCursor) { c.movePosition(QTextCursor::End); c.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, rel.first); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, rel.second - rel.first); setTextCursor(c); } } // void TransmitTextEdit::replacePlainText(const QString &text, bool keepCursor) { auto rel = relativeTextCursorPosition(textCursor()); auto c = textCursor(); c.movePosition(QTextCursor::Start); c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); c.removeSelectedText(); c.insertText(text); // keep cursor if (keepCursor) { c.movePosition(QTextCursor::End); c.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, rel.first); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, rel.second - rel.first); setTextCursor(c); } } // void TransmitTextEdit::setFont(QFont f) { m_font = f; // then rehighlight highlight(); } // void TransmitTextEdit::setFont(QFont f, QColor fg, QColor bg) { m_font = f; m_fg = fg; m_bg = bg; // then rehighlight highlight(); } // override void TransmitTextEdit::clear() { m_textSent.clear(); m_sent = 0; // Do this last, as it may trigger events that, during processing, like to // see m_textSent and m_sent reset. QTextEdit::clear(); } void TransmitTextEdit::setProtected(bool protect) { m_protected = protect; } bool TransmitTextEdit::cursorShouldBeProtected(QTextCursor c) { int start = c.selectionStart(); int end = c.selectionEnd(); if (end < start) { int x = end; end = start; start = x; } // qCDebug(transmittextedit_js8) << "selection" << start << end << m_sent; if (m_sent && start <= m_sent) { qCDebug(transmittextedit_js8) << "cursor in protected zone" << start << "<=" << m_sent; return true; } else { return false; } } // slot void TransmitTextEdit::on_selectionChanged() { auto c = textCursor(); auto shouldProtect = cursorShouldBeProtected(c); if (shouldProtect) { blockSignals(true); { int end = c.selectionEnd(); c.movePosition(QTextCursor::Start); c.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, m_sent); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, qMax(0, end - m_sent)); setTextCursor(c); } blockSignals(false); } setProtected(shouldProtect); } // slot void TransmitTextEdit::on_textContentsChanged(int /*pos*/, int rem, int add) { if (rem == 0 && add == 0) { return; } QString text = toPlainText(); if (text == m_lastText) { return; } #if JS8_ALLOW_EXTENDED QString normalized = text; #else QString normalized = text.normalized(QString::NormalizationForm_KD); #endif QString result; std::copy_if(normalized.begin(), normalized.end(), std::back_inserter(result), [](auto const c) { auto const lc = c.toLatin1(); return (lc && (lc == 0x10 || lc == 0x1A || ((lc >= 32) && (lc <= 127)))) #if JS8_ALLOW_UNICODE || c.isPrint(); #elif JS8_ALLOW_EXTENDED || Varicode::extendedChars().contains(c.toUpper()) #endif ; }); if (result != text) { bool blocked = signalsBlocked(); blockSignals(true); { replacePlainText(result, true); text = result; } blockSignals(blocked); } highlight(); m_dirty = true; m_lastText = text; } void TransmitTextEdit::highlightBase() { auto d = document(); if (!d) { return; } auto block = d->firstBlock(); while (block.isValid()) { highlightBlock(block, m_font, m_fg, m_bg); block = block.next(); } } void TransmitTextEdit::highlightCharsSent() { if (!m_sent) { return; } auto d = document(); if (!d) { return; } // highlight sent text auto c = textCursor(); if (c.isNull()) { return; } c.movePosition(QTextCursor::Start); c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_sent); auto ch = c.charFormat(); ch.setFontStrikeOut(true); c.mergeCharFormat(ch); } void TransmitTextEdit::highlight() { highlightBase(); highlightCharsSent(); } QTextCursor::MoveOperation deleteKeyEventToMoveOperation(QKeyEvent *e) { QTextCursor::MoveOperation op = QTextCursor::NoMove; #if 0 if (e == QKeySequence::Delete) { op = QTextCursor::PreviousCharacter; } else #endif if (e == QKeySequence::DeleteStartOfWord) { op = QTextCursor::StartOfWord; } else if (e == QKeySequence::DeleteEndOfWord) { op = QTextCursor::EndOfWord; } else if (e == QKeySequence::DeleteCompleteLine) { op = QTextCursor::StartOfLine; } else if (e == QKeySequence::DeleteEndOfLine) { op = QTextCursor::EndOfLine; } return op; } bool isDeleteKeyEvent(QKeyEvent *e) { return deleteKeyEventToMoveOperation(e) != QTextCursor::NoMove; } QTextCursor::MoveOperation movementKeyEventToMoveOperation(QKeyEvent *e) { QTextCursor::MoveOperation op = QTextCursor::NoMove; if (e == QKeySequence::Delete) { op = QTextCursor::PreviousCharacter; } else if (e == QKeySequence::DeleteStartOfWord) { op = QTextCursor::StartOfWord; } else if (e == QKeySequence::DeleteEndOfWord) { op = QTextCursor::EndOfWord; } else if (e == QKeySequence::DeleteCompleteLine) { op = QTextCursor::StartOfLine; } else if (e == QKeySequence::DeleteEndOfLine) { op = QTextCursor::EndOfLine; } else if (e == QKeySequence::MoveToNextChar) { op = QTextCursor::Right; } else if (e == QKeySequence::MoveToPreviousChar) { op = QTextCursor::Left; } else if (e == QKeySequence::SelectNextChar) { op = QTextCursor::Right; } else if (e == QKeySequence::SelectPreviousChar) { op = QTextCursor::Left; } else if (e == QKeySequence::SelectNextWord) { op = QTextCursor::WordRight; } else if (e == QKeySequence::SelectPreviousWord) { op = QTextCursor::WordLeft; } else if (e == QKeySequence::SelectStartOfLine) { op = QTextCursor::StartOfLine; } else if (e == QKeySequence::SelectEndOfLine) { op = QTextCursor::EndOfLine; } else if (e == QKeySequence::SelectStartOfBlock) { op = QTextCursor::StartOfBlock; } else if (e == QKeySequence::SelectEndOfBlock) { op = QTextCursor::EndOfBlock; } else if (e == QKeySequence::SelectStartOfDocument) { op = QTextCursor::Start; } else if (e == QKeySequence::SelectEndOfDocument) { op = QTextCursor::End; } else if (e == QKeySequence::SelectPreviousLine) { op = QTextCursor::Up; } else if (e == QKeySequence::SelectNextLine) { op = QTextCursor::Down; } else if (e == QKeySequence::MoveToNextWord) { op = QTextCursor::WordRight; } else if (e == QKeySequence::MoveToPreviousWord) { op = QTextCursor::WordLeft; } else if (e == QKeySequence::MoveToEndOfBlock) { op = QTextCursor::EndOfBlock; } else if (e == QKeySequence::MoveToStartOfBlock) { op = QTextCursor::StartOfBlock; } else if (e == QKeySequence::MoveToNextLine) { op = QTextCursor::Down; } else if (e == QKeySequence::MoveToPreviousLine) { op = QTextCursor::Up; } else if (e == QKeySequence::MoveToPreviousLine) { op = QTextCursor::Up; } else if (e == QKeySequence::MoveToStartOfLine) { op = QTextCursor::StartOfLine; } else if (e == QKeySequence::MoveToEndOfLine) { op = QTextCursor::EndOfLine; } else if (e == QKeySequence::MoveToStartOfDocument) { op = QTextCursor::Start; } else if (e == QKeySequence::MoveToEndOfDocument) { op = QTextCursor::End; } return op; } bool isMovementKeyEvent(QKeyEvent *k) { return movementKeyEventToMoveOperation(k) != QTextCursor::NoMove; } bool TransmitTextEdit::eventFilter(QObject * /*o*/, QEvent *e) { if (e->type() != QEvent::KeyPress) { return false; } // -1. don't filter the escape key, return key, or enter key here QKeyEvent *k = static_cast(e); if (k->key() == Qt::Key_Escape) { return false; } if (k->key() == Qt::Key_Return || k->key() == Qt::Key_Enter) { return false; } auto c = textCursor(); auto c2 = QTextCursor(c); auto op = movementKeyEventToMoveOperation(k); c2.movePosition(op); bool shouldBeProtected = cursorShouldBeProtected(c2); // -1. if this is a delete event (like Ctrl+Backspace) and it should be // protected, force protection if (isDeleteKeyEvent(k)) { // if we technically shouldn't be protected...check to see if the // previous character is a space if it is, let's try the move operation // from before the space, since the space can cause word coallescing in // the qtextdocument making the operation not actually move the cursor if (!shouldBeProtected) { auto c3 = QTextCursor(c); c3.movePosition(QTextCursor::PreviousCharacter); if (c.document() ->characterAt(qMin(c3.selectionStart(), c3.selectionEnd())) .isSpace()) { c3.movePosition(op); shouldBeProtected = cursorShouldBeProtected(c3); } } if (shouldBeProtected) { k->ignore(); return true; } } // 0. only filter when in a protected range // 0b. but only if we're not moving/deleting _into_ the protected range :/ if (!isProtected() && !shouldBeProtected) { return false; } // 1. do not filter movement sequences if (isMovementKeyEvent(k)) { return false; } // 2. if on the edge, do not filter if not a backspace int start = qMin(c.selectionStart(), c.selectionEnd()); if (start == m_sent && k->key() != Qt::Key_Backspace) { return false; } // 3. if on the edge, do not filter if a backspace and there is text // selected int end = qMax(c.selectionStart(), c.selectionEnd()); if (start == m_sent && start != end && k->key() == Qt::Key_Backspace) { return false; } return true; } Q_LOGGING_CATEGORY(transmittextedit_js8, "transmittextedit.js8", QtWarningMsg)