mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
520 lines
16 KiB
C++
520 lines
16 KiB
C++
/**
|
|
* @file TransmitTextEdit.cpp
|
|
* @brief Implementation of TransmitTextEdit class for the text entry box
|
|
*/
|
|
|
|
#include "TransmitTextEdit.h"
|
|
#include "JS8_Include/commons.h"
|
|
#include "Varicode.h"
|
|
|
|
#include <QLoggingCategory>
|
|
|
|
#include <iterator>
|
|
|
|
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<QKeyEvent *>(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)
|