tdesktop/Telegram/SourceFiles/ui/flattextarea.cpp
John Preston 0110a62776 Inline bot cancel button added.
It replaces Send button when the user is editing an inline bot
query. First cancel clears the inline bot request, second one
clears the field entirely. Escape key acts the same as cancel.
2016-04-09 15:02:50 +04:00

1026 lines
30 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
*/
#include "stdafx.h"
#include "flattextarea.h"
#include "ui/style.h"
#include "window.h"
FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent)
, _oldtext(v)
, _phVisible(!v.length())
, a_phLeft(_phVisible ? 0 : st.phShift)
, a_phAlpha(_phVisible ? 1 : 0)
, a_phColor(st.phColor->c)
, _a_appearance(animation(this, &FlatTextarea::step_appearance))
, _st(st) {
setAcceptRichText(false);
resize(_st.width, _st.font->height);
setFont(_st.font->f);
setAlignment(_st.align);
setPlaceholder(pholder);
QPalette p(palette());
p.setColor(QPalette::Text, _st.textColor->c);
setPalette(p);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setFrameStyle(QFrame::NoFrame | QFrame::Plain);
viewport()->setAutoFillBackground(false);
setContentsMargins(0, 0, 0, 0);
switch (cScale()) {
case dbisOneAndQuarter: _fakeMargin = 1; break;
case dbisOneAndHalf: _fakeMargin = 2; break;
case dbisTwo: _fakeMargin = 4; break;
}
setStyleSheet(qsl("QTextEdit { margin: %1px; }").arg(_fakeMargin));
viewport()->setAttribute(Qt::WA_AcceptTouchEvents);
_touchTimer.setSingleShot(true);
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(onDocumentContentsChange(int, int, int)));
connect(document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged()));
connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool)));
connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
if (!v.isEmpty()) {
setTextFast(v);
}
}
void FlatTextarea::setTextFast(const QString &text, bool clearUndoHistory) {
if (clearUndoHistory) {
setPlainText(text);
} else {
QTextCursor c(document()->docHandle(), 0);
c.joinPreviousEditBlock();
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
c.insertText(text);
c.movePosition(QTextCursor::End);
c.endEditBlock();
}
finishPlaceholder();
}
void FlatTextarea::finishPlaceholder() {
if (_a_appearance.animating()) {
a_phLeft.finish();
a_phAlpha.finish();
_a_appearance.stop();
update();
}
}
void FlatTextarea::setMaxLength(int32 maxLength) {
_maxLength = maxLength;
}
void FlatTextarea::setMinHeight(int32 minHeight) {
_minHeight = minHeight;
heightAutoupdated();
}
void FlatTextarea::setMaxHeight(int32 maxHeight) {
_maxHeight = maxHeight;
heightAutoupdated();
}
bool FlatTextarea::heightAutoupdated() {
if (_minHeight < 0 || _maxHeight < 0 || _inHeightCheck) return false;
_inHeightCheck = true;
myEnsureResized(this);
int newh = ceil(document()->size().height()) + 2 * fakeMargin();
if (newh > _maxHeight) {
newh = _maxHeight;
} else if (newh < _minHeight) {
newh = _minHeight;
}
if (height() != newh) {
resize(width(), newh);
_inHeightCheck = false;
return true;
}
_inHeightCheck = false;
return false;
}
void FlatTextarea::onTouchTimer() {
_touchRightButton = true;
}
bool FlatTextarea::viewportEvent(QEvent *e) {
if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
QTouchEvent *ev = static_cast<QTouchEvent*>(e);
if (ev->device()->type() == QTouchDevice::TouchScreen) {
touchEvent(ev);
return QTextEdit::viewportEvent(e);
}
}
return QTextEdit::viewportEvent(e);
}
void FlatTextarea::touchEvent(QTouchEvent *e) {
switch (e->type()) {
case QEvent::TouchBegin:
if (_touchPress || e->touchPoints().isEmpty()) return;
_touchTimer.start(QApplication::startDragTime());
_touchPress = true;
_touchMove = _touchRightButton = false;
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
break;
case QEvent::TouchUpdate:
if (!_touchPress || e->touchPoints().isEmpty()) return;
if (!_touchMove && (e->touchPoints().cbegin()->screenPos().toPoint() - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
_touchMove = true;
}
break;
case QEvent::TouchEnd:
if (!_touchPress) return;
if (!_touchMove && window()) {
Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
QPoint mapped(mapFromGlobal(_touchStart)), winMapped(window()->mapFromGlobal(_touchStart));
if (_touchRightButton) {
QContextMenuEvent contextEvent(QContextMenuEvent::Mouse, mapped, _touchStart);
contextMenuEvent(&contextEvent);
}
}
_touchTimer.stop();
_touchPress = _touchMove = _touchRightButton = false;
break;
case QEvent::TouchCancel:
_touchPress = false;
_touchTimer.stop();
break;
}
}
QRect FlatTextarea::getTextRect() const {
return rect().marginsRemoved(_st.textMrg + st::textRectMargins);
}
int32 FlatTextarea::fakeMargin() const {
return _fakeMargin;
}
void FlatTextarea::paintEvent(QPaintEvent *e) {
QPainter p(viewport());
QRect r(rect().intersected(e->rect()));
p.fillRect(r, _st.bgColor->b);
bool phDraw = _phVisible;
if (_a_appearance.animating()) {
p.setOpacity(a_phAlpha.current());
phDraw = true;
}
if (phDraw) {
p.save();
p.setClipRect(r);
p.setFont(_st.font);
p.setPen(a_phColor.current());
if (_st.phAlign == style::al_topleft && _phAfter > 0) {
p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + _st.font->width(getLastText().mid(0, _phAfter)), _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph);
} else {
QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + a_phLeft.current(), _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom());
p.drawText(phRect, _ph, QTextOption(_st.phAlign));
}
p.restore();
p.setOpacity(1);
}
QTextEdit::paintEvent(e);
}
void FlatTextarea::focusInEvent(QFocusEvent *e) {
a_phColor.start(_st.phFocusColor->c);
_a_appearance.start();
QTextEdit::focusInEvent(e);
}
void FlatTextarea::focusOutEvent(QFocusEvent *e) {
a_phColor.start(_st.phColor->c);
_a_appearance.start();
QTextEdit::focusOutEvent(e);
}
QSize FlatTextarea::sizeHint() const {
return geometry().size();
}
QSize FlatTextarea::minimumSizeHint() const {
return geometry().size();
}
EmojiPtr FlatTextarea::getSingleEmoji() const {
QString text;
QTextFragment fragment;
getSingleEmojiFragment(text, fragment);
if (!text.isEmpty()) {
QTextCharFormat format = fragment.charFormat();
QString imageName = static_cast<QTextImageFormat*>(&format)->name();
if (imageName.startsWith(qstr("emoji://e."))) {
return emojiFromUrl(imageName);
}
}
return 0;
}
QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const {
t_assert(outInlineBot != nullptr);
t_assert(outInlineBotUsername != nullptr);
const QString &text(getLastText());
int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size();
if (size > 2 && text.at(0) == '@' && text.at(1).isLetter()) {
inlineUsernameLength = 1;
for (int32 i = inlineUsernameStart + 1, l = text.size(); i < l; ++i) {
if (text.at(i).isLetterOrNumber() || text.at(i).unicode() == '_') {
++inlineUsernameLength;
continue;
}
if (!text.at(i).isSpace()) {
inlineUsernameLength = 0;
}
break;
}
if (inlineUsernameLength && inlineUsernameStart + inlineUsernameLength < text.size() && text.at(inlineUsernameStart + inlineUsernameLength).isSpace()) {
QStringRef username = text.midRef(inlineUsernameStart, inlineUsernameLength);
if (username != *outInlineBotUsername) {
*outInlineBotUsername = username.toString();
PeerData *peer = App::peerByName(*outInlineBotUsername);
if (peer) {
if (peer->isUser()) {
*outInlineBot = peer->asUser();
} else {
*outInlineBot = nullptr;
}
} else {
*outInlineBot = LookingUpInlineBot;
}
}
if (*outInlineBot == LookingUpInlineBot) return QString();
if (*outInlineBot && (!(*outInlineBot)->botInfo || (*outInlineBot)->botInfo->inlinePlaceholder.isEmpty())) {
*outInlineBot = nullptr;
} else {
return text.mid(inlineUsernameStart + inlineUsernameLength + 1);
}
} else {
inlineUsernameLength = 0;
}
}
if (inlineUsernameLength < 3) {
*outInlineBot = nullptr;
*outInlineBotUsername = QString();
}
return QString();
}
QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const {
start = false;
int32 pos = textCursor().position();
if (textCursor().anchor() != pos) return QString();
// check mention / hashtag / bot command
QTextDocument *doc(document());
QTextBlock block = doc->findBlock(pos);
for (QTextBlock::Iterator iter = block.begin(); !iter.atEnd(); ++iter) {
QTextFragment fr(iter.fragment());
if (!fr.isValid()) continue;
int32 p = fr.position(), e = (p + fr.length());
if (p >= pos || e < pos) continue;
QTextCharFormat f = fr.charFormat();
if (f.isImageFormat()) continue;
bool mentionInCommand = false;
QString t(fr.text());
for (int i = pos - p; i > 0; --i) {
if (t.at(i - 1) == '@') {
if ((pos - p - i < 1 || t.at(i).isLetter()) && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) {
start = (i == 1) && (p == 0);
return t.mid(i - 1, pos - p - i + 1);
} else if ((pos - p - i < 1 || t.at(i).isLetter()) && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) {
mentionInCommand = true;
--i;
continue;
}
return QString();
} else if (t.at(i - 1) == '#') {
if (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_')) {
start = (i == 1) && (p == 0);
return t.mid(i - 1, pos - p - i + 1);
}
return QString();
} else if (t.at(i - 1) == '/') {
if (i < 2) {
start = (i == 1) && (p == 0);
return t.mid(i - 1, pos - p - i + 1);
}
return QString();
}
if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break;
if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break;
}
break;
}
return QString();
}
void FlatTextarea::onMentionHashtagOrBotCommandInsert(QString str) {
QTextCursor c(textCursor());
int32 pos = c.position();
QTextDocument *doc(document());
QTextBlock block = doc->findBlock(pos);
for (QTextBlock::Iterator iter = block.begin(); !iter.atEnd(); ++iter) {
QTextFragment fr(iter.fragment());
if (!fr.isValid()) continue;
int32 p = fr.position(), e = (p + fr.length());
if (p >= pos || e < pos) continue;
QTextCharFormat f = fr.charFormat();
if (f.isImageFormat()) continue;
bool mentionInCommand = false;
QString t(fr.text());
for (int i = pos - p; i > 0; --i) {
if (t.at(i - 1) == '@' || t.at(i - 1) == '#' || t.at(i - 1) == '/') {
if ((i == pos - p || (t.at(i - 1) == '/' ? t.at(i).isLetterOrNumber() : t.at(i).isLetter()) || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) {
c.setPosition(p + i - 1, QTextCursor::MoveAnchor);
int till = p + i;
for (; (till < e) && (till - p - i + 1 < str.size()); ++till) {
if (t.at(till - p).toLower() != str.at(till - p - i + 1).toLower()) {
break;
}
}
if (till - p - i + 1 == str.size() && till < e && t.at(till - p) == ' ') {
++till;
}
c.setPosition(till, QTextCursor::KeepAnchor);
c.insertText(str + ' ');
return;
} else if ((i == pos - p || t.at(i).isLetter()) && t.at(i - 1) == '@' && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) {
mentionInCommand = true;
--i;
continue;
}
break;
}
if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break;
if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break;
}
break;
}
c.insertText(str + ' ');
}
void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const {
int32 end = textCursor().position(), start = end - 1;
if (textCursor().anchor() != end) return;
if (start < 0) start = 0;
QTextDocument *doc(document());
QTextBlock from = doc->findBlock(start), till = doc->findBlock(end);
if (till.isValid()) till = till.next();
for (QTextBlock b = from; b != till; b = b.next()) {
for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) {
QTextFragment fr(iter.fragment());
if (!fr.isValid()) continue;
int32 p = fr.position(), e = (p + fr.length());
if (p >= end || e <= start) {
continue;
}
QTextCharFormat f = fr.charFormat();
QString t(fr.text());
if (p < start) {
t = t.mid(start - p, end - start);
} else if (e > end) {
t = t.mid(0, end - p);
}
if (f.isImageFormat() && !t.isEmpty() && t.at(0).unicode() == QChar::ObjectReplacementCharacter) {
QString imageName = static_cast<QTextImageFormat*>(&f)->name();
if (imageName.startsWith(qstr("emoji://e."))) {
fragment = fr;
text = t;
return;
}
}
return;
}
}
return;
}
void FlatTextarea::removeSingleEmoji() {
QString text;
QTextFragment fragment;
getSingleEmojiFragment(text, fragment);
if (!text.isEmpty()) {
QTextCursor t(textCursor());
t.setPosition(fragment.position());
t.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor);
t.removeSelectedText();
setTextCursor(t);
}
}
QString FlatTextarea::getText(int32 start, int32 end) const {
if (end >= 0 && end <= start) return QString();
if (start < 0) start = 0;
bool full = (start == 0) && (end < 0);
QTextDocument *doc(document());
QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end);
if (till.isValid()) till = till.next();
int32 possibleLen = 0;
for (QTextBlock b = from; b != till; b = b.next()) {
possibleLen += b.length();
}
QString result;
result.reserve(possibleLen + 1);
if (!full && end < 0) {
end = possibleLen;
}
for (QTextBlock b = from; b != till; b = b.next()) {
for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) {
QTextFragment fragment(iter.fragment());
if (!fragment.isValid()) continue;
int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length());
if (!full) {
if (p >= end || e <= start) {
continue;
}
}
QTextCharFormat f = fragment.charFormat();
QString emojiText;
QString t(fragment.text());
if (!full) {
if (p < start) {
t = t.mid(start - p, end - start);
} else if (e > end) {
t = t.mid(0, end - p);
}
}
QChar *ub = t.data(), *uc = ub, *ue = uc + t.size();
for (; uc != ue; ++uc) {
switch (uc->unicode()) {
case 0xfdd0: // QTextBeginningOfFrame
case 0xfdd1: // QTextEndOfFrame
case QChar::ParagraphSeparator:
case QChar::LineSeparator:
*uc = QLatin1Char('\n');
break;
case QChar::Nbsp:
*uc = QLatin1Char(' ');
break;
case QChar::ObjectReplacementCharacter:
if (emojiText.isEmpty() && f.isImageFormat()) {
QString imageName = static_cast<QTextImageFormat*>(&f)->name();
if (imageName.startsWith(qstr("emoji://e."))) {
if (EmojiPtr emoji = emojiFromUrl(imageName)) {
emojiText = emojiString(emoji);
}
}
}
if (uc > ub) result.append(ub, uc - ub);
if (!emojiText.isEmpty()) result.append(emojiText);
ub = uc + 1;
break;
}
}
if (uc > ub) result.append(ub, uc - ub);
}
result.append('\n');
}
result.chop(1);
return result;
}
bool FlatTextarea::hasText() const {
QTextDocument *doc(document());
QTextBlock from = doc->begin(), till = doc->end();
if (from == till) return false;
for (QTextBlock::Iterator iter = from.begin(); !iter.atEnd(); ++iter) {
QTextFragment fragment(iter.fragment());
if (!fragment.isValid()) continue;
if (!fragment.text().isEmpty()) return true;
}
return (from.next() != till);
}
bool FlatTextarea::isUndoAvailable() const {
return _undoAvailable;
}
bool FlatTextarea::isRedoAvailable() const {
return _redoAvailable;
}
void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp!
LinkRanges newLinks;
QString text(toPlainText());
if (text.isEmpty()) {
if (!_links.isEmpty()) {
_links.clear();
emit linksChanged();
}
return;
}
initLinkSets();
int32 len = text.size();
const QChar *start = text.unicode(), *end = start + text.size();
for (int32 offset = 0, matchOffset = offset; offset < len;) {
QRegularExpressionMatch m = reDomain().match(text, matchOffset);
if (!m.hasMatch()) break;
int32 domainOffset = m.capturedStart();
QString protocol = m.captured(1).toLower();
QString topDomain = m.captured(3).toLower();
bool isProtocolValid = protocol.isEmpty() || validProtocols().contains(hashCrc32(protocol.constData(), protocol.size() * sizeof(QChar)));
bool isTopDomainValid = !protocol.isEmpty() || validTopDomains().contains(hashCrc32(topDomain.constData(), topDomain.size() * sizeof(QChar)));
if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
QString forMailName = text.mid(offset, domainOffset - offset - 1);
QRegularExpressionMatch mMailName = reMailName().match(forMailName);
if (mMailName.hasMatch()) {
offset = matchOffset = m.capturedEnd();
continue;
}
}
if (!isProtocolValid || !isTopDomainValid) {
offset = matchOffset = m.capturedEnd();
continue;
}
QStack<const QChar*> parenth;
const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
for (; p < end; ++p) {
QChar ch(*p);
if (chIsLinkEnd(ch)) break; // link finished
if (chIsAlmostLinkEnd(ch)) {
const QChar *endTest = p + 1;
while (endTest < end && chIsAlmostLinkEnd(*endTest)) {
++endTest;
}
if (endTest >= end || chIsLinkEnd(*endTest)) {
break; // link finished at p
}
p = endTest;
ch = *p;
}
if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
parenth.push(p);
} else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
if (parenth.isEmpty()) break;
const QChar *q = parenth.pop(), open(*q);
if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
p = q;
break;
}
}
}
if (p > domainEnd) { // check, that domain ended
if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
matchOffset = domainEnd - start;
continue;
}
}
newLinks.push_back(qMakePair(domainOffset - 1, p - start - domainOffset + 2));
offset = matchOffset = p - start;
}
if (newLinks != _links) {
_links = newLinks;
emit linksChanged();
}
}
QStringList FlatTextarea::linksList() const {
QStringList result;
if (!_links.isEmpty()) {
QString text(toPlainText());
for (LinkRanges::const_iterator i = _links.cbegin(), e = _links.cend(); i != e; ++i) {
result.push_back(text.mid(i->first + 1, i->second - 2));
}
}
return result;
}
void FlatTextarea::insertFromMimeData(const QMimeData *source) {
QTextEdit::insertFromMimeData(source);
if (!_inDrop) emit spacedReturnedPasted();
}
void FlatTextarea::correctValue(const QString &was, QString &now) {
}
void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) {
QTextImageFormat imageFormat;
int32 ew = ESize + st::emojiPadding * cIntRetinaFactor() * 2, eh = _st.font->height * cIntRetinaFactor();
imageFormat.setWidth(ew / cIntRetinaFactor());
imageFormat.setHeight(eh / cIntRetinaFactor());
imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16));
imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline);
static QString objectReplacement(QChar::ObjectReplacementCharacter);
c.insertText(objectReplacement, imageFormat);
}
QVariant FlatTextarea::loadResource(int type, const QUrl &name) {
QString imageName = name.toDisplayString();
if (imageName.startsWith(qstr("emoji://e."))) {
if (EmojiPtr emoji = emojiFromUrl(imageName)) {
return QVariant(App::emojiSingle(emoji, _st.font->height));
}
}
return QVariant();
}
void FlatTextarea::checkContentHeight() {
if (heightAutoupdated()) {
emit resized();
}
}
void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) {
int32 replacePosition = -1, replaceLen = 0;
const EmojiData *emoji = 0;
static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold");
bool checkTilde = !cRetina() && (font().pixelSize() == 13) && (font().family() == regular), wasTildeFragment = false;
QTextDocument *doc(document());
while (true) {
int32 start = position, end = position + charsAdded;
QTextBlock from = doc->findBlock(start), till = doc->findBlock(end);
if (till.isValid()) till = till.next();
for (QTextBlock b = from; b != till; b = b.next()) {
for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) {
QTextFragment fragment(iter.fragment());
if (!fragment.isValid()) continue;
int32 fp = fragment.position(), fe = fp + fragment.length();
if (fp >= end || fe <= start) {
continue;
}
if (checkTilde) {
wasTildeFragment = (fragment.charFormat().fontFamily() == semibold);
}
QString t(fragment.text());
const QChar *ch = t.constData(), *e = ch + t.size();
for (; ch != e; ++ch, ++fp) {
int32 emojiLen = 0;
emoji = emojiFromText(ch, e, &emojiLen);
if (emoji) {
if (replacePosition >= 0) {
emoji = 0; // replace tilde char format first
} else {
replacePosition = fp;
replaceLen = emojiLen;
}
break;
}
if (checkTilde && fp >= position) { // tilde fix in OpenSans
bool tilde = (ch->unicode() == '~');
if ((tilde && !wasTildeFragment) || (!tilde && wasTildeFragment)) {
if (replacePosition < 0) {
replacePosition = fp;
replaceLen = 1;
} else {
++replaceLen;
}
} else if (replacePosition >= 0) {
break;
}
}
if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) {
++ch;
++fp;
}
}
if (replacePosition >= 0) break;
}
if (replacePosition >= 0) break;
}
if (replacePosition >= 0) {
if (!document()->pageSize().isNull()) {
document()->setPageSize(QSizeF(0, 0));
}
QTextCursor c(doc->docHandle(), replacePosition);
c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor);
if (emoji) {
insertEmoji(emoji, c);
} else {
QTextCharFormat format;
format.setFontFamily(wasTildeFragment ? regular : semibold);
c.mergeCharFormat(format);
}
charsAdded -= replacePosition + replaceLen - position;
position = replacePosition + (emoji ? 1 : replaceLen);
emoji = 0;
replacePosition = -1;
} else {
break;
}
}
}
void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) {
if (_correcting) return;
QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock();
_correcting = true;
if (_maxLength >= 0) {
QTextCursor c(document()->docHandle(), 0);
c.movePosition(QTextCursor::End);
int32 fullSize = c.position(), toRemove = fullSize - _maxLength;
if (toRemove > 0) {
if (toRemove > charsAdded) {
if (charsAdded) {
c.setPosition(position);
c.setPosition((position + charsAdded), QTextCursor::KeepAnchor);
c.removeSelectedText();
}
c.setPosition(fullSize - (toRemove - charsAdded));
c.setPosition(fullSize, QTextCursor::KeepAnchor);
c.removeSelectedText();
} else {
c.setPosition(position + (charsAdded - toRemove));
c.setPosition(position + charsAdded, QTextCursor::KeepAnchor);
c.removeSelectedText();
}
}
}
_correcting = false;
if (!_links.isEmpty()) {
bool changed = false;
for (LinkRanges::iterator i = _links.begin(); i != _links.end();) {
if (i->first + i->second <= position) {
++i;
} else if (i->first >= position + charsRemoved) {
i->first += charsAdded - charsRemoved;
++i;
} else {
i = _links.erase(i);
changed = true;
}
}
if (changed) emit linksChanged();
}
if (document()->availableRedoSteps() > 0) {
QTextCursor(document()->docHandle(), 0).endEditBlock();
return;
}
const int takeBack = 3;
position -= takeBack;
charsAdded += takeBack;
if (position < 0) {
charsAdded += position;
position = 0;
}
if (charsAdded <= 0) {
QTextCursor(document()->docHandle(), 0).endEditBlock();
return;
}
_correcting = true;
QSizeF s = document()->pageSize();
processDocumentContentsChange(position, charsAdded);
if (document()->pageSize() != s) {
document()->setPageSize(s);
}
_correcting = false;
QTextCursor(document()->docHandle(), 0).endEditBlock();
}
void FlatTextarea::onDocumentContentsChanged() {
if (_correcting) return;
QString curText(getText());
_correcting = true;
correctValue(_oldtext, curText);
_correcting = false;
if (_oldtext != curText) {
_oldtext = curText;
emit changed();
checkContentHeight();
}
updatePlaceholder();
if (App::wnd()) App::wnd()->updateGlobalMenu();
}
void FlatTextarea::onUndoAvailable(bool avail) {
_undoAvailable = avail;
if (App::wnd()) App::wnd()->updateGlobalMenu();
}
void FlatTextarea::onRedoAvailable(bool avail) {
_redoAvailable = avail;
if (App::wnd()) App::wnd()->updateGlobalMenu();
}
void FlatTextarea::step_appearance(float64 ms, bool timer) {
float dt = ms / _st.phDuration;
if (dt >= 1) {
_a_appearance.stop();
a_phLeft.finish();
a_phAlpha.finish();
a_phColor.finish();
a_phLeft = anim::ivalue(a_phLeft.current());
a_phAlpha = anim::fvalue(a_phAlpha.current());
a_phColor = anim::cvalue(a_phColor.current());
} else {
a_phLeft.update(dt, _st.phLeftFunc);
a_phAlpha.update(dt, _st.phAlphaFunc);
a_phColor.update(dt, _st.phColorFunc);
}
if (timer) update();
}
void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) {
_ph = ph;
if (_phAfter != afterSymbols) {
_phAfter = afterSymbols;
updatePlaceholder();
}
_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - (_phAfter ? _st.font->width(getLastText().mid(0, _phAfter)) : 0));
if (_phVisible) update();
}
void FlatTextarea::updatePlaceholder() {
bool vis = (getLastText().size() <= _phAfter);
if (vis == _phVisible) return;
a_phLeft.start(vis ? 0 : _st.phShift);
a_phAlpha.start(vis ? 1 : 0);
_a_appearance.start();
_phVisible = vis;
}
QMimeData *FlatTextarea::createMimeDataFromSelection() const {
QMimeData *result = new QMimeData();
QTextCursor c(textCursor());
int32 start = c.selectionStart(), end = c.selectionEnd();
if (end > start) {
result->setText(getText(start, end));
}
return result;
}
void FlatTextarea::setSubmitSettings(SubmitSettings settings) {
_submitSettings = settings;
}
void FlatTextarea::keyPressEvent(QKeyEvent *e) {
bool shift = e->modifiers().testFlag(Qt::ShiftModifier);
bool macmeta = (cPlatform() == dbipMac || cPlatform() == dbipMacOld) && e->modifiers().testFlag(Qt::ControlModifier) && !e->modifiers().testFlag(Qt::MetaModifier) && !e->modifiers().testFlag(Qt::AltModifier);
bool ctrl = e->modifiers().testFlag(Qt::ControlModifier) || e->modifiers().testFlag(Qt::MetaModifier);
bool enterSubmit = (ctrl && shift);
if (ctrl && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::Enter) {
enterSubmit = true;
}
if (!ctrl && _submitSettings != SubmitSettings::None && _submitSettings != SubmitSettings::CtrlEnter) {
enterSubmit = true;
}
bool enter = (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return);
if (macmeta && e->key() == Qt::Key_Backspace) {
QTextCursor tc(textCursor()), start(tc);
start.movePosition(QTextCursor::StartOfLine);
tc.setPosition(start.position(), QTextCursor::KeepAnchor);
tc.removeSelectedText();
} else if (enter && enterSubmit) {
emit submitted(ctrl && shift);
} else if (e->key() == Qt::Key_Escape) {
emit cancelled();
} else if (e->key() == Qt::Key_Tab || (ctrl && e->key() == Qt::Key_Backtab)) {
if (ctrl) {
e->ignore();
} else {
emit tabbed();
}
} else if (e->key() == Qt::Key_Search || e == QKeySequence::Find) {
e->ignore();
} else {
QTextCursor tc(textCursor());
if (enter && ctrl) {
e->setModifiers(e->modifiers() & ~Qt::ControlModifier);
}
bool spaceOrReturn = false;
QString t(e->text());
if (!t.isEmpty() && t.size() < 3) {
if (t.at(0) == '\n' || t.at(0) == '\r' || t.at(0).isSpace() || t.at(0) == QChar::LineSeparator) {
spaceOrReturn = true;
}
}
QTextEdit::keyPressEvent(e);
if (tc == textCursor()) {
bool check = false;
if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Up) {
tc.movePosition(QTextCursor::Start, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
check = true;
} else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Down) {
tc.movePosition(QTextCursor::End, e->modifiers().testFlag(Qt::ShiftModifier) ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor);
check = true;
}
if (check) {
if (tc == textCursor()) {
e->ignore();
} else {
setTextCursor(tc);
}
}
}
if (spaceOrReturn) emit spacedReturnedPasted();
}
}
void FlatTextarea::resizeEvent(QResizeEvent *e) {
_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1);
QTextEdit::resizeEvent(e);
checkContentHeight();
}
void FlatTextarea::mousePressEvent(QMouseEvent *e) {
QTextEdit::mousePressEvent(e);
}
void FlatTextarea::dropEvent(QDropEvent *e) {
_inDrop = true;
QTextEdit::dropEvent(e);
_inDrop = false;
emit spacedReturnedPasted();
}
void FlatTextarea::contextMenuEvent(QContextMenuEvent *e) {
if (QMenu *menu = createStandardContextMenu()) {
(new PopupMenu(menu))->popup(e->globalPos());
}
}