js8call/JS8_Main/TransmitTextEdit.cpp
Chris-AC9KH 356bdc0f50 Code cleanup - move loose source and header files in source tree to group folders
Rename all header files with .h extension
Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override
Remove unused variable in Modulator.cpp
Remove unused files from source tree
Remove duplicate LazyFillComboBox files
Reformat codebase to LLVM C++ standard
2026-01-02 16:46:38 -06:00

514 lines
16 KiB
C++

#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)