Partially (italic+colored) support blockquotes.

This commit is contained in:
John Preston 2023-10-05 11:46:48 +04:00
parent 68b89a6ba9
commit c317f2a353
12 changed files with 134 additions and 58 deletions

View file

@ -148,6 +148,10 @@ QString Integration::phraseFormattingStrikeOut() {
return "Strike-through";
}
QString Integration::phraseFormattingBlockquote() {
return "Quote";
}
QString Integration::phraseFormattingMonospace() {
return "Monospace";
}

View file

@ -83,6 +83,7 @@ public:
[[nodiscard]] virtual QString phraseFormattingItalic();
[[nodiscard]] virtual QString phraseFormattingUnderline();
[[nodiscard]] virtual QString phraseFormattingStrikeOut();
[[nodiscard]] virtual QString phraseFormattingBlockquote();
[[nodiscard]] virtual QString phraseFormattingMonospace();
[[nodiscard]] virtual QString phraseFormattingSpoiler();
[[nodiscard]] virtual QString phraseButtonOk();

View file

@ -1006,6 +1006,7 @@ TextForMimeData String::toText(
{ TextBlockFlag::StrikeOut, EntityType::StrikeOut },
{ TextBlockFlag::Code, EntityType::Code }, // #TODO entities
{ TextBlockFlag::Pre, EntityType::Pre },
{ TextBlockFlag::Blockquote, EntityType::Blockquote },
} : std::vector<MarkdownTagTracker>();
const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) {
if (!composeEntities) {

View file

@ -392,7 +392,9 @@ style::font WithFlags(
|| (fontFlags & FontSemibold)) {
result = result->semibold();
}
if ((flags & TextBlockFlag::Italic) || (fontFlags & FontItalic)) {
if ((flags & TextBlockFlag::Italic)
|| (fontFlags & FontItalic)
|| (flags & TextBlockFlag::Blockquote)) {
result = result->italic();
}
if ((flags & TextBlockFlag::Underline) || (fontFlags & FontUnderline)) {

View file

@ -30,15 +30,16 @@ enum class TextBlockType : uint16 {
};
enum class TextBlockFlag : uint16 {
Bold = 0x001,
Italic = 0x002,
Underline = 0x004,
StrikeOut = 0x008,
Tilde = 0x010, // Tilde fix in OpenSans.
Semibold = 0x020,
Code = 0x040,
Pre = 0x080,
Spoiler = 0x100,
Bold = 0x001,
Italic = 0x002,
Underline = 0x004,
StrikeOut = 0x008,
Tilde = 0x010, // Tilde fix in OpenSans.
Semibold = 0x020,
Code = 0x040,
Pre = 0x080,
Spoiler = 0x100,
Blockquote = 0x200,
};
inline constexpr bool is_flag_type(TextBlockFlag) { return true; }
using TextBlockFlags = base::flags<TextBlockFlag>;

View file

@ -1438,7 +1438,9 @@ bool CutPart(TextWithEntities &sending, TextWithEntities &left, int32 limit) {
if (s > half) {
bool inEntity = (currentEntity < entityCount) && (ch > start + left.entities[currentEntity].offset()) && (ch < start + left.entities[currentEntity].offset() + left.entities[currentEntity].length());
EntityType entityType = (currentEntity < entityCount) ? left.entities[currentEntity].type() : EntityType::Invalid;
bool canBreakEntity = (entityType == EntityType::Pre || entityType == EntityType::Code); // #TODO entities
bool canBreakEntity = (entityType == EntityType::Pre)
|| (entityType == EntityType::Blockquote)
|| (entityType == EntityType::Code); // #TODO entities
int32 noEntityLevel = inEntity ? 0 : 1;
auto markGoodAsLevel = [&](int newLevel) {
@ -1464,9 +1466,15 @@ bool CutPart(TextWithEntities &sending, TextWithEntities &left, int32 limit) {
}
} else if (ch + 1 < end && IsNewline(*(ch + 1))) {
markGoodAsLevel(15);
} else if (currentEntity < entityCount && ch + 1 == start + left.entities[currentEntity].offset() && left.entities[currentEntity].type() == EntityType::Pre) {
} else if (currentEntity < entityCount
&& ch + 1 == start + left.entities[currentEntity].offset()
&& (left.entities[currentEntity].type() == EntityType::Pre
|| left.entities[currentEntity].type() == EntityType::Blockquote)) {
markGoodAsLevel(14);
} else if (currentEntity > 0 && ch == start + left.entities[currentEntity - 1].offset() + left.entities[currentEntity - 1].length() && left.entities[currentEntity - 1].type() == EntityType::Pre) {
} else if (currentEntity > 0
&& ch == start + left.entities[currentEntity - 1].offset() + left.entities[currentEntity - 1].length()
&& (left.entities[currentEntity - 1].type() == EntityType::Pre
|| left.entities[currentEntity - 1].type() == EntityType::Blockquote)) {
markGoodAsLevel(14);
} else {
markGoodAsLevel(13);
@ -2029,6 +2037,7 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
EntityType::Spoiler,
EntityType::Code,
EntityType::Pre,
EntityType::Blockquote,
};
struct State {
QString link;
@ -2146,6 +2155,8 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
&& single.startsWith(Tags::kTagPre)) {
result.set(EntityType::Pre);
result.language = single.mid(languageStart).toString();
} else if (single == Tags::kTagBlockquote) {
result.set(EntityType::Blockquote);
} else if (single == Tags::kTagSpoiler) {
result.set(EntityType::Spoiler);
} else {
@ -2253,6 +2264,9 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
}
push(Ui::InputField::kTagPre);
} break;
case EntityType::Blockquote:
push(Ui::InputField::kTagBlockquote);
break;
case EntityType::Spoiler: push(Ui::InputField::kTagSpoiler); break;
}
}

View file

@ -35,6 +35,7 @@ enum class EntityType : uchar {
StrikeOut,
Code, // inline
Pre, // block
Blockquote,
Spoiler,
};

View file

@ -53,7 +53,8 @@ constexpr auto kMaxDiacAfterSymbol = 2;
|| type == EntityType::Colorized
|| type == EntityType::Spoiler
|| type == EntityType::Code
|| type == EntityType::Pre))) {
|| type == EntityType::Pre
|| type == EntityType::Blockquote))) {
continue;
}
result.entities.push_back(preparsed.at(i));
@ -226,6 +227,17 @@ void Parser::createNewlineBlock() {
createBlock();
}
void Parser::ensureAtNewline() {
const auto lastType = _t->_blocks.empty()
? TextBlockType::Newline
: _t->_blocks.back()->type();
if (lastType != TextBlockType::Newline) {
auto saved = base::take(_customEmojiData);
createNewlineBlock();
_customEmojiData = base::take(saved);
}
}
void Parser::finishEntities() {
while (!_startedEntities.empty()
&& (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
@ -239,9 +251,13 @@ void Parser::finishEntities() {
if (_flags & (*flags)) {
createBlock();
_flags &= ~(*flags);
if (((*flags) & TextBlockFlag::Pre)
&& !_t->_blocks.empty()
&& _t->_blocks.back()->type() != TextBlockType::Newline) {
const auto lastType = _t->_blocks.empty()
? TextBlockType::Newline
: _t->_blocks.back()->type();
if ((lastType != TextBlockType::Newline)
&& ((*flags)
& (TextBlockFlag::Pre
| TextBlockFlag::Blockquote))) {
_newlineAwaited = true;
}
if (IsMono(*flags)) {
@ -320,11 +336,7 @@ bool Parser::checkEntities() {
} else {
flags = TextBlockFlag::Pre;
createBlock();
if (!_t->_blocks.empty()
&& _t->_blocks.back()->type() != TextBlockType::Newline
&& _customEmojiData.isEmpty()) {
createNewlineBlock();
}
ensureAtNewline();
}
const auto text = QString(entityBegin, entityLength);
@ -338,6 +350,10 @@ bool Parser::checkEntities() {
_monos.push_back({ .text = text, .type = entityType });
monoIndex = _monos.size();
}
} else if (entityType == EntityType::Blockquote) {
flags = TextBlockFlag::Blockquote;
createBlock();
ensureAtNewline();
} else if (entityType == EntityType::Url
|| entityType == EntityType::Email
|| entityType == EntityType::Mention

View file

@ -57,6 +57,7 @@ private:
void blockCreated();
void createBlock(int32 skipBack = 0);
void createNewlineBlock();
void ensureAtNewline();
// Returns true if at least one entity was parsed in the current position.
bool checkEntities();

View file

@ -2028,7 +2028,7 @@ void Renderer::applyBlockProperties(const AbstractBlock *block) {
_currentPen = &_originalPen;
_currentPenSelected = &_originalPenSelected;
}
} else if (isMono) {
} else if (isMono || (flags & TextBlockFlag::Blockquote)) {
_currentPen = &_palette->monoFg->p;
_currentPenSelected = &_palette->selectMonoFg->p;
} else if (block->linkIndex()) {

View file

@ -52,6 +52,7 @@ const auto &kTagUnderline = InputField::kTagUnderline;
const auto &kTagStrikeOut = InputField::kTagStrikeOut;
const auto &kTagCode = InputField::kTagCode;
const auto &kTagPre = InputField::kTagPre;
const auto &kTagBlockquote = InputField::kTagBlockquote;
const auto &kTagSpoiler = InputField::kTagSpoiler;
const auto &kCustomEmojiFormat = InputField::kCustomEmojiFormat;
const auto kTagCheckLinkMeta = u"^:/:/:^"_q;
@ -754,6 +755,9 @@ QTextCharFormat PrepareTagFormat(
font = font->underline();
} else if (tag == kTagStrikeOut) {
font = font->strikeout();
} else if (tag == kTagBlockquote) {
color = st::defaultTextPalette.monoFg;
font = font->italic();
} else if (tag == kTagCode || IsTagPre(tag)) {
color = st::defaultTextPalette.monoFg;
font = font->monospace();
@ -931,13 +935,14 @@ struct FormattingAction {
// kTagUnderline is not used for Markdown.
const QString InputField::kTagBold = QStringLiteral("**");
const QString InputField::kTagItalic = QStringLiteral("__");
const QString InputField::kTagUnderline = QStringLiteral("^^");
const QString InputField::kTagStrikeOut = QStringLiteral("~~");
const QString InputField::kTagCode = QStringLiteral("`");
const QString InputField::kTagPre = QStringLiteral("```");
const QString InputField::kTagSpoiler = QStringLiteral("||");
const QString InputField::kTagBold = u"**"_q;
const QString InputField::kTagItalic = u"__"_q;
const QString InputField::kTagUnderline = u"^^"_q;
const QString InputField::kTagStrikeOut = u"~~"_q;
const QString InputField::kTagCode = u"`"_q;
const QString InputField::kTagPre = u"```"_q;
const QString InputField::kTagSpoiler = u"||"_q;
const QString InputField::kTagBlockquote = u">"_q;
const QString InputField::kCustomEmojiTagStart = u"custom-emoji://"_q;
const int InputField::kCustomEmojiFormat
= QTextFormat::UserObject + 1;
@ -2903,6 +2908,14 @@ bool InputField::handleMarkdownKey(QKeyEvent *e) {
const auto events = QKeySequence(searchKey);
return sequence.matches(events) == QKeySequence::ExactMatch;
};
const auto matchesCtrlShiftDot = [&] {
// We can't match ctrl+shift+. with QKeySequence because
// shift+. gives us '>' and ctrl+shift+> is not the same.
// So we check by nativeVirtualKey instead.
return e->modifiers().testFlag(Qt::ControlModifier)
&& e->modifiers().testFlag(Qt::ShiftModifier)
&& (e->nativeVirtualKey() == 190);
};
if (e == QKeySequence::Bold) {
toggleSelectionMarkdown(kTagBold);
} else if (e == QKeySequence::Italic) {
@ -2913,6 +2926,8 @@ bool InputField::handleMarkdownKey(QKeyEvent *e) {
toggleSelectionMarkdown(kTagStrikeOut);
} else if (matches(kMonospaceSequence)) {
toggleSelectionMarkdown(kTagCode);
} else if (matches(kBlockquoteSequence) || matchesCtrlShiftDot()) {
toggleSelectionMarkdown(kTagBlockquote);
} else if (matches(kSpoilerSequence)) {
toggleSelectionMarkdown(kTagSpoiler);
} else if (matches(kClearFormatSequence)) {
@ -3518,9 +3533,9 @@ void InputField::commitMarkdownLinkEdit(
void InputField::toggleSelectionMarkdown(const QString &tag) {
_reverseMarkdownReplacement = false;
const auto cursor = textCursor();
const auto position = cursor.position();
const auto from = cursor.selectionStart();
const auto till = cursor.selectionEnd();
auto position = cursor.position();
auto from = cursor.selectionStart();
auto till = cursor.selectionEnd();
if (from == till) {
return;
}
@ -3529,33 +3544,50 @@ void InputField::toggleSelectionMarkdown(const QString &tag) {
} else if (HasFullTextTag(getTextWithTagsSelected(), tag)) {
removeMarkdownTag(from, till, tag);
} else {
const auto useTag = [&] {
if (tag != kTagCode) {
return tag;
const auto leftForBlock = [&] {
if (!from) {
return true;
}
const auto leftForBlock = [&] {
if (!from) {
return true;
}
const auto text = getTextWithTagsPart(
from - 1,
from + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
const auto rightForBlock = [&] {
const auto text = getTextWithTagsPart(
till - 1,
till + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
return (leftForBlock && rightForBlock) ? kTagPre : kTagCode;
const auto text = getTextWithTagsPart(
from - 1,
from + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
const auto rightForBlock = [&] {
const auto text = getTextWithTagsPart(
till - 1,
till + 1
).text;
return text.isEmpty()
|| IsNewline(text[0])
|| IsNewline(text[text.size() - 1]);
}();
const auto useTag = (tag != kTagCode)
? tag
: (leftForBlock && rightForBlock)
? kTagPre
: kTagCode;
if (tag == kTagBlockquote) {
QTextCursor(document()).beginEditBlock();
if (!leftForBlock) {
auto copy = textCursor();
copy.setPosition(from);
copy.insertText(u"\n"_q);
++position;
++from;
++till;
}
if (!rightForBlock) {
auto copy = textCursor();
copy.setPosition(till);
copy.insertText(u"\n"_q);
}
QTextCursor(document()).endEditBlock();
}
addMarkdownTag(from, till, useTag);
}
auto restorePosition = textCursor();
@ -3738,6 +3770,7 @@ void InputField::addMarkdownActions(
addtag(integration.phraseFormattingItalic(), QKeySequence::Italic, kTagItalic);
addtag(integration.phraseFormattingUnderline(), QKeySequence::Underline, kTagUnderline);
addtag(integration.phraseFormattingStrikeOut(), kStrikeOutSequence, kTagStrikeOut);
addtag(integration.phraseFormattingBlockquote(), kBlockquoteSequence, kTagBlockquote);
addtag(integration.phraseFormattingMonospace(), kMonospaceSequence, kTagCode);
addtag(integration.phraseFormattingSpoiler(), kSpoilerSequence, kTagSpoiler);

View file

@ -35,6 +35,7 @@ namespace Ui {
const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
const auto kStrikeOutSequence = QKeySequence("ctrl+shift+x");
const auto kBlockquoteSequence = QKeySequence("ctrl+shift+.");
const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
const auto kEditLinkSequence = QKeySequence("ctrl+k");
const auto kSpoilerSequence = QKeySequence("ctrl+shift+p");
@ -133,6 +134,7 @@ public:
static const QString kTagCode;
static const QString kTagPre;
static const QString kTagSpoiler;
static const QString kTagBlockquote;
static const QString kCustomEmojiTagStart;
static const int kCustomEmojiFormat;