diff --git a/ui/integration.cpp b/ui/integration.cpp index 7de629b..620b9d0 100644 --- a/ui/integration.cpp +++ b/ui/integration.cpp @@ -148,6 +148,10 @@ QString Integration::phraseFormattingStrikeOut() { return "Strike-through"; } +QString Integration::phraseFormattingBlockquote() { + return "Quote"; +} + QString Integration::phraseFormattingMonospace() { return "Monospace"; } diff --git a/ui/integration.h b/ui/integration.h index 77f5f80..e6847f6 100644 --- a/ui/integration.h +++ b/ui/integration.h @@ -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(); diff --git a/ui/text/text.cpp b/ui/text/text.cpp index 33774e1..1327251 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -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(); const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) { if (!composeEntities) { diff --git a/ui/text/text_block.cpp b/ui/text/text_block.cpp index f7ad545..c5ce3d9 100644 --- a/ui/text/text_block.cpp +++ b/ui/text/text_block.cpp @@ -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)) { diff --git a/ui/text/text_block.h b/ui/text/text_block.h index dfb7ba5..b0e7168 100644 --- a/ui/text/text_block.h +++ b/ui/text/text_block.h @@ -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; diff --git a/ui/text/text_entity.cpp b/ui/text/text_entity.cpp index 4ca1999..c2d48ef 100644 --- a/ui/text/text_entity.cpp +++ b/ui/text/text_entity.cpp @@ -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; } } diff --git a/ui/text/text_entity.h b/ui/text/text_entity.h index 336d136..15236c2 100644 --- a/ui/text/text_entity.h +++ b/ui/text/text_entity.h @@ -35,6 +35,7 @@ enum class EntityType : uchar { StrikeOut, Code, // inline Pre, // block + Blockquote, Spoiler, }; diff --git a/ui/text/text_parser.cpp b/ui/text/text_parser.cpp index 8eb925a..79a0e45 100644 --- a/ui/text/text_parser.cpp +++ b/ui/text/text_parser.cpp @@ -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 diff --git a/ui/text/text_parser.h b/ui/text/text_parser.h index eb4f3a7..f50e452 100644 --- a/ui/text/text_parser.h +++ b/ui/text/text_parser.h @@ -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(); diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp index db95239..121d611 100644 --- a/ui/text/text_renderer.cpp +++ b/ui/text/text_renderer.cpp @@ -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()) { diff --git a/ui/widgets/fields/input_field.cpp b/ui/widgets/fields/input_field.cpp index 660ca56..d88b93e 100644 --- a/ui/widgets/fields/input_field.cpp +++ b/ui/widgets/fields/input_field.cpp @@ -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); diff --git a/ui/widgets/fields/input_field.h b/ui/widgets/fields/input_field.h index 13a5603..afb4f6f 100644 --- a/ui/widgets/fields/input_field.h +++ b/ui/widgets/fields/input_field.h @@ -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;