521 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			521 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "TransmitTextEdit.h"
 | |
| 
 | |
| #include "commons.h"
 | |
| #include "varicode.h"
 | |
| 
 | |
| #include <iterator>
 | |
| #include <QDebug>
 | |
| 
 | |
| 
 | |
| 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
 | |
|     n = qMin(n, document()->characterCount());
 | |
| 
 | |
|     // 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){
 | |
|     QTextEdit::setPlainText(text);
 | |
|     m_textSent.clear();
 | |
|     m_sent = 0;
 | |
| }
 | |
| 
 | |
| //
 | |
| 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(){
 | |
|     QTextEdit::clear();
 | |
|     m_textSent.clear();
 | |
|     m_sent = 0;
 | |
| }
 | |
| 
 | |
| 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;
 | |
|     }
 | |
| 
 | |
|     //qDebug() << "selection" << start << end << m_sent;
 | |
| 
 | |
|     if(m_sent && start <= m_sent){
 | |
|         qDebug() << "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), [](QChar& c) {
 | |
| #if JS8_ALLOW_UNICODE
 | |
|         return (c == 10 || c == 0x1A || (c > 31 && c < 128)) || c.isPrint());
 | |
| #elif JS8_ALLOW_EXTENDED
 | |
|         return c.toLatin1() != 0 && (c == 10 || c == 0x1A || (c > 31 && c < 128) || Varicode::extendedChars().contains(c.toUpper()));
 | |
| #else
 | |
|         return c.toLatin1() != 0 && (c == 10 || c == 0x1A || (c > 31 && c < 128));
 | |
| #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;
 | |
| }
 | 
