tdesktop/Telegram/SourceFiles/ui/text.h
John Preston 8e89486fbc Error handling changed, 'auto' keyword used for MTP types.
All errors that lead to MTP request resending by default
error handler now can be handled differently. For example
inline bot requests are not being resent on 5XX error codes.
+ extensive use of auto keyword in MTP types handling.
2016-04-08 14:44:35 +04:00

1060 lines
31 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
*/
#pragma once
enum EntityInTextType {
EntityInTextUrl,
EntityInTextCustomUrl,
EntityInTextEmail,
EntityInTextHashtag,
EntityInTextMention,
EntityInTextBotCommand,
EntityInTextBold,
EntityInTextItalic,
EntityInTextCode, // inline
EntityInTextPre, // block
};
struct EntityInText {
EntityInText(EntityInTextType type, int32 offset, int32 length, const QString &text = QString()) : type(type), offset(offset), length(length), text(text) {
}
EntityInTextType type;
int32 offset, length;
QString text;
};
typedef QList<EntityInText> EntitiesInText;
// text preprocess
QString textClean(const QString &text);
QString textRichPrepare(const QString &text);
QString textOneLine(const QString &text, bool trim = true, bool rich = false);
QString textAccentFold(const QString &text);
QString textSearchKey(const QString &text);
bool textSplit(QString &sendingText, EntitiesInText &sendingEntities, QString &leftText, EntitiesInText &leftEntities, int32 limit);
enum {
TextParseMultiline = 0x001,
TextParseLinks = 0x002,
TextParseRichText = 0x004,
TextParseMentions = 0x008,
TextParseHashtags = 0x010,
TextParseBotCommands = 0x020,
TextParseMono = 0x040,
TextTwitterMentions = 0x100,
TextTwitterHashtags = 0x200,
TextInstagramMentions = 0x400,
TextInstagramHashtags = 0x800,
};
inline EntitiesInText entitiesFromMTP(const QVector<MTPMessageEntity> &entities) {
EntitiesInText result;
if (!entities.isEmpty()) {
result.reserve(entities.size());
for (int32 i = 0, l = entities.size(); i != l; ++i) {
const auto &e(entities.at(i));
switch (e.type()) {
case mtpc_messageEntityUrl: { const auto &d(e.c_messageEntityUrl()); result.push_back(EntityInText(EntityInTextUrl, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityTextUrl: { const auto &d(e.c_messageEntityTextUrl()); result.push_back(EntityInText(EntityInTextCustomUrl, d.voffset.v, d.vlength.v, textClean(qs(d.vurl)))); } break;
case mtpc_messageEntityEmail: { const auto &d(e.c_messageEntityEmail()); result.push_back(EntityInText(EntityInTextEmail, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityHashtag: { const auto &d(e.c_messageEntityHashtag()); result.push_back(EntityInText(EntityInTextHashtag, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityMention: { const auto &d(e.c_messageEntityMention()); result.push_back(EntityInText(EntityInTextMention, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityBotCommand: { const auto &d(e.c_messageEntityBotCommand()); result.push_back(EntityInText(EntityInTextBotCommand, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityBold: { const auto &d(e.c_messageEntityBold()); result.push_back(EntityInText(EntityInTextBold, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityItalic: { const auto &d(e.c_messageEntityItalic()); result.push_back(EntityInText(EntityInTextItalic, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityCode: { const auto &d(e.c_messageEntityCode()); result.push_back(EntityInText(EntityInTextCode, d.voffset.v, d.vlength.v)); } break;
case mtpc_messageEntityPre: { const auto &d(e.c_messageEntityPre()); result.push_back(EntityInText(EntityInTextPre, d.voffset.v, d.vlength.v, textClean(qs(d.vlanguage)))); } break;
}
}
}
return result;
}
inline MTPVector<MTPMessageEntity> linksToMTP(const EntitiesInText &links, bool sending = false) {
MTPVector<MTPMessageEntity> result(MTP_vector<MTPMessageEntity>(0));
QVector<MTPMessageEntity> &v(result._vector().v);
for (int32 i = 0, s = links.size(); i != s; ++i) {
const EntityInText &l(links.at(i));
if (l.length <= 0 || (sending && l.type != EntityInTextCode && l.type != EntityInTextPre)) continue;
switch (l.type) {
case EntityInTextUrl: v.push_back(MTP_messageEntityUrl(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextCustomUrl: v.push_back(MTP_messageEntityTextUrl(MTP_int(l.offset), MTP_int(l.length), MTP_string(l.text))); break;
case EntityInTextEmail: v.push_back(MTP_messageEntityEmail(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextHashtag: v.push_back(MTP_messageEntityHashtag(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextMention: v.push_back(MTP_messageEntityMention(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextBotCommand: v.push_back(MTP_messageEntityBotCommand(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextBold: v.push_back(MTP_messageEntityBold(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextItalic: v.push_back(MTP_messageEntityItalic(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextCode: v.push_back(MTP_messageEntityCode(MTP_int(l.offset), MTP_int(l.length))); break;
case EntityInTextPre: v.push_back(MTP_messageEntityPre(MTP_int(l.offset), MTP_int(l.length), MTP_string(l.text))); break;
}
}
return result;
}
EntitiesInText textParseEntities(QString &text, int32 flags, bool rich = false); // changes text if (flags & TextParseMono)
QString textApplyEntities(const QString &text, const EntitiesInText &entities);
#include "ui/emoji_config.h"
void emojiDraw(QPainter &p, EmojiPtr e, int x, int y);
#include "../../../QtStatic/qtbase/src/gui/text/qfontengine_p.h"
enum TextBlockType {
TextBlockTNewline = 0x01,
TextBlockTText = 0x02,
TextBlockTEmoji = 0x03,
TextBlockTSkip = 0x04,
};
enum TextBlockFlags {
TextBlockFBold = 0x01,
TextBlockFItalic = 0x02,
TextBlockFUnderline = 0x04,
TextBlockFTilde = 0x08, // tilde fix in OpenSans
TextBlockFSemibold = 0x10,
TextBlockFCode = 0x20,
TextBlockFPre = 0x40,
};
class ITextBlock {
public:
ITextBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, const style::color &color, uint16 lnkIndex) : _from(from), _flags((flags & 0xFF) | ((lnkIndex & 0xFFFF) << 12))/*, _color(color)*/, _lpadding(0) {
if (length) {
if (str.at(_from + length - 1).unicode() == QChar::Space) {
_rpadding = font->spacew;
}
if (length > 1 && str.at(0).unicode() == QChar::Space) {
_lpadding = font->spacew;
}
}
}
uint16 from() const {
return _from;
}
int32 width() const {
return _width.toInt();
}
int32 lpadding() const {
return _lpadding.toInt();
}
int32 rpadding() const {
return _rpadding.toInt();
}
QFixed f_width() const {
return _width;
}
QFixed f_lpadding() const {
return _lpadding;
}
QFixed f_rpadding() const {
return _rpadding;
}
uint16 lnkIndex() const {
return (_flags >> 12) & 0xFFFF;
}
void setLnkIndex(uint16 lnkIndex) {
_flags = (_flags & ~(0xFFFF << 12)) | (lnkIndex << 12);
}
TextBlockType type() const {
return TextBlockType((_flags >> 8) & 0x0F);
}
int32 flags() const {
return (_flags & 0xFF);
}
const style::color &color() const {
static style::color tmp;
return tmp;//_color;
}
virtual ITextBlock *clone() const = 0;
virtual ~ITextBlock() {
}
protected:
uint16 _from;
uint32 _flags; // 4 bits empty, 16 bits lnkIndex, 4 bits type, 8 bits flags
QFixed _width, _lpadding, _rpadding;
};
class NewlineBlock : public ITextBlock {
public:
Qt::LayoutDirection nextDirection() const {
return _nextDir;
}
ITextBlock *clone() const {
return new NewlineBlock(*this);
}
private:
NewlineBlock(const style::font &font, const QString &str, uint16 from, uint16 length) : ITextBlock(font, str, from, length, 0, st::transparent, 0), _nextDir(Qt::LayoutDirectionAuto) {
_flags |= ((TextBlockTNewline & 0x0F) << 8);
}
Qt::LayoutDirection _nextDir;
friend class Text;
friend class TextParser;
friend class TextPainter;
};
struct TextWord {
TextWord() {
}
TextWord(uint16 from, QFixed width, QFixed rbearing, QFixed rpadding = 0) : from(from),
_rbearing(rbearing.value() > 0x7FFF ? 0x7FFF : (rbearing.value() < -0x7FFF ? -0x7FFF : rbearing.value())), width(width), rpadding(rpadding) {
}
QFixed f_rbearing() const {
return QFixed::fromFixed(_rbearing);
}
uint16 from;
int16 _rbearing;
QFixed width, rpadding;
};
class TextBlock : public ITextBlock {
public:
QFixed f_rbearing() const {
return _words.isEmpty() ? 0 : _words.back().f_rbearing();
}
ITextBlock *clone() const {
return new TextBlock(*this);
}
private:
TextBlock(const style::font &font, const QString &str, QFixed minResizeWidth, uint16 from, uint16 length, uchar flags, const style::color &color, uint16 lnkIndex);
typedef QVector<TextWord> TextWords;
TextWords _words;
friend class Text;
friend class TextParser;
friend class BlockParser;
friend class TextPainter;
};
class EmojiBlock : public ITextBlock {
public:
ITextBlock *clone() const {
return new EmojiBlock(*this);
}
private:
EmojiBlock(const style::font &font, const QString &str, uint16 from, uint16 length, uchar flags, const style::color &color, uint16 lnkIndex, const EmojiData *emoji);
const EmojiData *emoji;
friend class Text;
friend class TextParser;
friend class TextPainter;
};
class SkipBlock : public ITextBlock {
public:
int32 height() const {
return _height;
}
ITextBlock *clone() const {
return new SkipBlock(*this);
}
private:
SkipBlock(const style::font &font, const QString &str, uint16 from, int32 w, int32 h, uint16 lnkIndex);
int32 _height;
friend class Text;
friend class TextParser;
friend class TextPainter;
};
class ClickHandler;
using ClickHandlerPtr = QSharedPointer<ClickHandler>;
class ClickHandlerHost {
protected:
virtual void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
}
virtual void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) {
}
virtual ~ClickHandlerHost() = 0;
friend class ClickHandler;
};
class ClickHandler {
public:
virtual void onClick(Qt::MouseButton) const = 0;
virtual QString tooltip() const {
return QString();
}
virtual void copyToClipboard() const {
}
virtual QString copyToClipboardContextItem() const {
return QString();
}
virtual QString text() const {
return QString();
}
virtual QString dragText() const {
return text();
}
virtual ~ClickHandler() {
}
// this method should be called on mouse over a click handler
// it returns true if something was changed or false otherwise
static bool setActive(const ClickHandlerPtr &p, ClickHandlerHost *host = nullptr);
// this method should be called when mouse leaves the host
// it returns true if something was changed or false otherwise
static bool clearActive(ClickHandlerHost *host = nullptr) {
if (host && _activeHost != host) {
return false;
}
return setActive(ClickHandlerPtr(), host);
}
// this method should be called on mouse pressed
static void pressed() {
unpressed();
if (!_active || !*_active) {
return;
}
_pressed.makeIfNull();
*_pressed = *_active;
if ((_pressedHost = _activeHost)) {
_pressedHost->clickHandlerPressedChanged(*_pressed, true);
}
}
// this method should be called on mouse released
// the activated click handler is returned
static ClickHandlerPtr unpressed() {
if (_pressed && *_pressed) {
bool activated = (_active && *_active == *_pressed);
ClickHandlerPtr waspressed = *_pressed;
(*_pressed).clear();
if (_pressedHost) {
_pressedHost->clickHandlerPressedChanged(waspressed, false);
_pressedHost = nullptr;
}
if (activated) {
return *_active;
} else if (_active && *_active && _activeHost) {
// emit clickHandlerActiveChanged for current active
// click handler, which we didn't emit while we has
// a pressed click handler
_activeHost->clickHandlerActiveChanged(*_active, true);
}
}
return ClickHandlerPtr();
}
static ClickHandlerPtr getActive() {
return _active ? *_active : ClickHandlerPtr();
}
static ClickHandlerPtr getPressed() {
return _pressed ? *_pressed : ClickHandlerPtr();
}
static bool showAsActive(const ClickHandlerPtr &p) {
if (!p || !_active || p != *_active) {
return false;
}
return !_pressed || !*_pressed || (p == *_pressed);
}
static bool showAsPressed(const ClickHandlerPtr &p) {
if (!p || !_active || p != *_active) {
return false;
}
return _pressed && (p == *_pressed);
}
static void hostDestroyed(ClickHandlerHost *host) {
if (_activeHost == host) {
_activeHost = nullptr;
} else if (_pressedHost == host) {
_pressedHost = nullptr;
}
}
private:
static NeverFreedPointer<ClickHandlerPtr> _active;
static NeverFreedPointer<ClickHandlerPtr> _pressed;
static ClickHandlerHost *_activeHost;
static ClickHandlerHost *_pressedHost;
};
class LeftButtonClickHandler : public ClickHandler {
public:
void onClick(Qt::MouseButton button) const override final {
if (button != Qt::LeftButton) return;
onClickImpl();
}
protected:
virtual void onClickImpl() const = 0;
};
class TextClickHandler : public ClickHandler {
public:
TextClickHandler(bool fullDisplayed = true) : _fullDisplayed(fullDisplayed) {
}
void copyToClipboard() const override {
QString u = url();
if (!u.isEmpty()) {
QApplication::clipboard()->setText(u);
}
}
QString tooltip() const override {
return _fullDisplayed ? QString() : readable();
}
void setFullDisplayed(bool full) {
_fullDisplayed = full;
}
protected:
virtual QString url() const = 0;
virtual QString readable() const {
return url();
}
bool _fullDisplayed;
};
class UrlClickHandler : public TextClickHandler {
public:
UrlClickHandler(const QString &url, bool fullDisplayed = true) : TextClickHandler(fullDisplayed), _url(url) {
if (isEmail()) {
_readable = _url;
} else {
QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString());
_readable = good.isValid() ? good.toDisplayString() : _url;
}
}
QString copyToClipboardContextItem() const override;
QString text() const override {
return _url;
}
QString dragText() const override {
return url();
}
static void doOpen(QString url);
void onClick(Qt::MouseButton button) const override {
if (button == Qt::LeftButton || button == Qt::MiddleButton) {
doOpen(url());
}
}
protected:
QString url() const override {
if (isEmail()) {
return _url;
}
QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString());
QString result(good.isValid() ? QString::fromUtf8(good.toEncoded()) : _url);
if (!QRegularExpression(qsl("^[a-zA-Z]+:")).match(result).hasMatch()) { // no protocol
return qsl("http://") + result;
}
return result;
}
QString readable() const override {
return _readable;
}
private:
static bool isEmail(const QString &url) {
int at = url.indexOf('@'), slash = url.indexOf('/');
return ((at > 0) && (slash < 0 || slash > at));
}
bool isEmail() const {
return isEmail(_url);
}
QString _url, _readable;
};
typedef QSharedPointer<TextClickHandler> TextClickHandlerPtr;
class HiddenUrlClickHandler : public UrlClickHandler {
public:
HiddenUrlClickHandler(QString url) : UrlClickHandler(url, false) {
}
void onClick(Qt::MouseButton button) const override;
};
struct LocationCoords {
LocationCoords() : lat(0), lon(0) {
}
LocationCoords(float64 lat, float64 lon) : lat(lat), lon(lon) {
}
LocationCoords(const MTPDgeoPoint &point) : lat(point.vlat.v), lon(point.vlong.v) {
}
float64 lat, lon;
};
inline bool operator==(const LocationCoords &a, const LocationCoords &b) {
return (a.lat == b.lat) && (a.lon == b.lon);
}
inline bool operator<(const LocationCoords &a, const LocationCoords &b) {
return (a.lat < b.lat) || ((a.lat == b.lat) && (a.lon < b.lon));
}
inline uint qHash(const LocationCoords &t, uint seed = 0) {
return qHash(QtPrivate::QHashCombine().operator()(qHash(t.lat), t.lon), seed);
}
class LocationClickHandler : public TextClickHandler {
public:
LocationClickHandler(const LocationCoords &coords) : _coords(coords) {
setup();
}
QString copyToClipboardContextItem() const override;
QString text() const override {
return _text;
}
void onClick(Qt::MouseButton button) const override;
protected:
QString url() const override {
return _text;
}
private:
void setup();
LocationCoords _coords;
QString _text;
};
class MentionClickHandler : public TextClickHandler {
public:
MentionClickHandler(const QString &tag) : _tag(tag) {
}
QString copyToClipboardContextItem() const override;
QString text() const override {
return _tag;
}
void onClick(Qt::MouseButton button) const override;
protected:
QString url() const override {
return _tag;
}
private:
QString _tag;
};
class HashtagClickHandler : public TextClickHandler {
public:
HashtagClickHandler(const QString &tag) : _tag(tag) {
}
QString copyToClipboardContextItem() const override;
QString text() const override {
return _tag;
}
void onClick(Qt::MouseButton button) const override;
protected:
QString url() const override {
return _tag;
}
private:
QString _tag;
};
class BotCommandClickHandler : public TextClickHandler {
public:
BotCommandClickHandler(const QString &cmd) : _cmd(cmd) {
}
QString text() const override {
return _cmd;
}
void onClick(Qt::MouseButton button) const override;
protected:
QString url() const override {
return _cmd;
}
private:
QString _cmd;
};
static const QChar TextCommand(0x0010);
enum TextCommands {
TextCommandBold = 0x01,
TextCommandNoBold = 0x02,
TextCommandItalic = 0x03,
TextCommandNoItalic = 0x04,
TextCommandUnderline = 0x05,
TextCommandNoUnderline = 0x06,
TextCommandSemibold = 0x07,
TextCommandNoSemibold = 0x08,
TextCommandLinkIndex = 0x09, // 0 - NoLink
TextCommandLinkText = 0x0A,
TextCommandColor = 0x0B,
TextCommandNoColor = 0x0C,
TextCommandSkipBlock = 0x0D,
TextCommandLangTag = 0x20,
};
struct TextParseOptions {
int32 flags;
int32 maxw;
int32 maxh;
Qt::LayoutDirection dir;
};
extern const TextParseOptions _defaultOptions, _textPlainOptions;
enum TextSelectType {
TextSelectLetters = 0x01,
TextSelectWords = 0x02,
TextSelectParagraphs = 0x03,
};
typedef QPair<QString, QString> TextCustomTag; // open str and close str
typedef QMap<QChar, TextCustomTag> TextCustomTagsMap;
class Text {
public:
Text(int32 minResizeWidth = QFIXED_MAX);
Text(style::font font, const QString &text, const TextParseOptions &options = _defaultOptions, int32 minResizeWidth = QFIXED_MAX, bool richText = false);
Text(const Text &other);
Text(Text &&other);
Text &operator=(const Text &other);
Text &operator=(Text &&other);
int32 countWidth(int32 width) const;
int32 countHeight(int32 width) const;
void setText(style::font font, const QString &text, const TextParseOptions &options = _defaultOptions);
void setRichText(style::font font, const QString &text, TextParseOptions options = _defaultOptions, const TextCustomTagsMap &custom = TextCustomTagsMap());
void setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options = _defaultOptions);
void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk);
bool hasLinks() const;
bool hasSkipBlock() const {
return _blocks.isEmpty() ? false : _blocks.back()->type() == TextBlockTSkip;
}
void setSkipBlock(int32 width, int32 height);
void removeSkipBlock();
int32 maxWidth() const {
return _maxWidth.ceil().toInt();
}
int32 minHeight() const {
return _minHeight;
}
void replaceFont(style::font f); // does not recount anything, use at your own risk!
void draw(QPainter &p, int32 left, int32 top, int32 width, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, uint16 selectedFrom = 0, uint16 selectedTo = 0) const;
void drawElided(QPainter &p, int32 left, int32 top, int32 width, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false) const;
void drawLeft(QPainter &p, int32 left, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, uint16 selectedFrom = 0, uint16 selectedTo = 0) const {
draw(p, rtl() ? (outerw - left - width) : left, top, width, align, yFrom, yTo, selectedFrom, selectedTo);
}
void drawLeftElided(QPainter &p, int32 left, int32 top, int32 width, int32 outerw, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false) const {
drawElided(p, rtl() ? (outerw - left - width) : left, top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere);
}
void drawRight(QPainter &p, int32 right, int32 top, int32 width, int32 outerw, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, uint16 selectedFrom = 0, uint16 selectedTo = 0) const {
draw(p, rtl() ? right : (outerw - right - width), top, width, align, yFrom, yTo, selectedFrom, selectedTo);
}
void drawRightElided(QPainter &p, int32 right, int32 top, int32 width, int32 outerw, int32 lines = 1, style::align align = style::al_left, int32 yFrom = 0, int32 yTo = -1, int32 removeFromEnd = 0, bool breakEverywhere = false) const {
drawElided(p, rtl() ? right : (outerw - right - width), top, width, lines, align, yFrom, yTo, removeFromEnd, breakEverywhere);
}
const ClickHandlerPtr &link(int32 x, int32 y, int32 width, style::align align = style::al_left) const;
const ClickHandlerPtr &linkLeft(int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left) const {
return link(rtl() ? (outerw - x - width) : x, y, width, align);
}
void getState(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 width, style::align align = style::al_left, bool breakEverywhere = false) const;
void getStateLeft(ClickHandlerPtr &lnk, bool &inText, int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left, bool breakEverywhere = false) const {
return getState(lnk, inText, rtl() ? (outerw - x - width) : x, y, width, align, breakEverywhere);
}
void getSymbol(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y, int32 width, style::align align = style::al_left) const;
void getSymbolLeft(uint16 &symbol, bool &after, bool &upon, int32 x, int32 y, int32 width, int32 outerw, style::align align = style::al_left) const {
return getSymbol(symbol, after, upon, rtl() ? (outerw - x - width) : x, y, width, align);
}
uint32 adjustSelection(uint16 from, uint16 to, TextSelectType selectType) const;
bool isEmpty() const {
return _text.isEmpty();
}
bool isNull() const {
return !_font;
}
enum ExpandLinksMode {
ExpandLinksNone,
ExpandLinksShortened,
ExpandLinksAll,
};
QString original(uint16 selectedFrom = 0, uint16 selectedTo = 0xFFFF, ExpandLinksMode mode = ExpandLinksShortened) const;
EntitiesInText originalEntities() const;
bool lastDots(int32 dots, int32 maxdots = 3) { // hack for typing animation
if (_text.size() < maxdots) return false;
int32 nowDots = 0, from = _text.size() - maxdots, to = _text.size();
for (int32 i = from; i < to; ++i) {
if (_text.at(i) == QChar('.')) {
++nowDots;
}
}
if (nowDots == dots) return false;
for (int32 j = from; j < from + dots; ++j) {
_text[j] = QChar('.');
}
for (int32 j = from + dots; j < to; ++j) {
_text[j] = QChar(' ');
}
return true;
}
void clear();
~Text() {
clear();
}
private:
void recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto);
// clear() deletes all blocks and calls this method
// it is also called from move constructor / assignment operator
void clearFields();
QFixed _minResizeWidth, _maxWidth;
int32 _minHeight;
QString _text;
style::font _font;
typedef QVector<ITextBlock*> TextBlocks;
TextBlocks _blocks;
typedef QVector<ClickHandlerPtr> TextLinks;
TextLinks _links;
Qt::LayoutDirection _startDir;
friend class TextParser;
friend class TextPainter;
};
void initLinkSets();
const QSet<int32> &validProtocols();
const QSet<int32> &validTopDomains();
const QRegularExpression &reDomain();
const QRegularExpression &reMailName();
const QRegularExpression &reMailStart();
const QRegularExpression &reHashtag();
const QRegularExpression &reBotCommand();
// text style
const style::textStyle *textstyleCurrent();
void textstyleSet(const style::textStyle *style);
inline void textstyleRestore() {
textstyleSet(0);
}
// textcmd
QString textcmdSkipBlock(ushort w, ushort h);
QString textcmdStartLink(ushort lnkIndex);
QString textcmdStartLink(const QString &url);
QString textcmdStopLink();
QString textcmdLink(ushort lnkIndex, const QString &text);
QString textcmdLink(const QString &url, const QString &text);
QString textcmdStartColor(const style::color &color);
QString textcmdStopColor();
QString textcmdStartSemibold();
QString textcmdStopSemibold();
const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink = true);
inline bool chIsSpace(QChar ch, bool rich = false) {
return ch.isSpace() || (ch < 32 && !(rich && ch == TextCommand)) || (ch == QChar::ParagraphSeparator) || (ch == QChar::LineSeparator) || (ch == QChar::ObjectReplacementCharacter) || (ch == QChar::SoftHyphen) || (ch == QChar::CarriageReturn) || (ch == QChar::Tabulation);
}
inline bool chIsDiac(QChar ch) { // diac and variation selectors
return (ch.category() == QChar::Mark_NonSpacing) || (ch.unicode() == 1652);
}
inline bool chIsBad(QChar ch) {
return (ch == 0) || (ch >= 8232 && ch < 8237) || (ch >= 65024 && ch < 65040 && ch != 65039) || (ch >= 127 && ch < 160 && ch != 156) || (cPlatform() == dbipMac && ch >= 0x0B00 && ch <= 0x0B7F && chIsDiac(ch) && cIsElCapitan()); // tmp hack see https://bugreports.qt.io/browse/QTBUG-48910
}
inline bool chIsTrimmed(QChar ch, bool rich = false) {
return (!rich || ch != TextCommand) && (chIsSpace(ch) || chIsBad(ch));
}
inline bool chReplacedBySpace(QChar ch) {
// \xe2\x80[\xa8 - \xac\xad] // 8232 - 8237
// QString from1 = QString::fromUtf8("\xe2\x80\xa8"), to1 = QString::fromUtf8("\xe2\x80\xad");
// \xcc[\xb3\xbf\x8a] // 819, 831, 778
// QString bad1 = QString::fromUtf8("\xcc\xb3"), bad2 = QString::fromUtf8("\xcc\xbf"), bad3 = QString::fromUtf8("\xcc\x8a");
// [\x00\x01\x02\x07\x08\x0b-\x1f] // '\t' = 0x09
return (/*code >= 0x00 && */ch <= 0x02) || (ch >= 0x07 && ch <= 0x09) || (ch >= 0x0b && ch <= 0x1f) ||
(ch == 819) || (ch == 831) || (ch == 778) || (ch >= 8232 && ch <= 8237);
}
inline int32 chMaxDiacAfterSymbol() {
return 2;
}
inline bool chIsNewline(QChar ch) {
return (ch == QChar::LineFeed || ch == 156);
}
inline bool chIsLinkEnd(QChar ch) {
return ch == TextCommand || chIsBad(ch) || chIsSpace(ch) || chIsNewline(ch) || ch.isLowSurrogate() || ch.isHighSurrogate();
}
inline bool chIsAlmostLinkEnd(QChar ch) {
switch (ch.unicode()) {
case '?':
case ',':
case '.':
case '"':
case ':':
case '!':
case '\'':
return true;
default:
break;
}
return false;
}
inline bool chIsWordSeparator(QChar ch) {
switch (ch.unicode()) {
case QChar::Space:
case QChar::LineFeed:
case '.':
case ',':
case '?':
case '!':
case '@':
case '#':
case '$':
case ':':
case ';':
case '-':
case '<':
case '>':
case '[':
case ']':
case '(':
case ')':
case '{':
case '}':
case '=':
case '/':
case '+':
case '%':
case '&':
case '^':
case '*':
case '\'':
case '"':
case '`':
case '~':
case '|':
return true;
default:
break;
}
return false;
}
inline bool chIsSentenceEnd(QChar ch) {
switch (ch.unicode()) {
case '.':
case '?':
case '!':
return true;
default:
break;
}
return false;
}
inline bool chIsSentencePartEnd(QChar ch) {
switch (ch.unicode()) {
case ',':
case ':':
case ';':
return true;
default:
break;
}
return false;
}
inline bool chIsParagraphSeparator(QChar ch) {
switch (ch.unicode()) {
case QChar::LineFeed:
return true;
default:
break;
}
return false;
}
inline QString myUrlEncode(const QString &str) {
return QString::fromLatin1(QUrl::toPercentEncoding(str));
}
inline QString myUrlDecode(const QString &enc) {
return QUrl::fromPercentEncoding(enc.toUtf8());
}
QString prepareTextWithEntities(QString result, EntitiesInText &entities, int32 flags);
inline QString prepareText(QString result, bool checkLinks = false) {
EntitiesInText entities;
return prepareTextWithEntities(result, entities, checkLinks ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0);
}
inline void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText &entities) {
if (count > 0) {
if (to < from) {
memmove(start + to, start + from, count * sizeof(QChar));
for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) {
if (i->offset >= from + count) break;
if (i->offset + i->length < from) continue;
if (i->offset >= from) {
i->offset -= (from - to);
i->length += (from - to);
}
if (i->offset + i->length < from + count) {
i->length -= (from - to);
}
}
}
to += count;
from += count;
}
}
// replace bad symbols with space and remove \r
inline void cleanTextWithEntities(QString &result, EntitiesInText &entities) {
result = result.replace('\t', qstr(" "));
int32 len = result.size(), to = 0, from = 0;
QChar *start = result.data();
for (QChar *ch = start, *end = start + len; ch < end; ++ch) {
if (ch->unicode() == '\r') {
moveStringPart(start, to, from, (ch - start) - from, entities);
++from;
} else if (chReplacedBySpace(*ch)) {
*ch = ' ';
}
}
moveStringPart(start, to, from, len - from, entities);
if (to < len) result.resize(to);
}
inline void trimTextWithEntities(QString &result, EntitiesInText &entities) {
bool foundNotTrimmed = false;
for (QChar *s = result.data(), *e = s + result.size(), *ch = e; ch != s;) { // rtrim
--ch;
if (!chIsTrimmed(*ch)) {
if (ch + 1 < e) {
int32 l = ch + 1 - s;
for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) {
if (i->offset > l) {
i->offset = l;
i->length = 0;
} else if (i->offset + i->length > l) {
i->length = l - i->offset;
}
}
result.resize(l);
}
foundNotTrimmed = true;
break;
}
}
if (!foundNotTrimmed) {
result.clear();
entities.clear();
return;
}
for (QChar *s = result.data(), *ch = s, *e = s + result.size(); ch != e; ++ch) { // ltrim
if (!chIsTrimmed(*ch)) {
if (ch > s) {
int32 l = ch - s;
for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) {
if (i->offset + i->length <= l) {
i->length = 0;
i->offset = 0;
} else if (i->offset < l) {
i->length = i->offset + i->length - l;
i->offset = 0;
} else {
i->offset -= l;
}
}
result = result.mid(l);
}
break;
}
}
}