diff --git a/ui/integration.cpp b/ui/integration.cpp index 620b9d0..5d23a95 100644 --- a/ui/integration.cpp +++ b/ui/integration.cpp @@ -9,6 +9,7 @@ #include "ui/gl/gl_detection.h" #include "ui/text/text_entity.h" #include "ui/text/text_block.h" +#include "ui/toast/toast.h" #include "ui/basic_click_handlers.h" #include "base/platform/base_platform_info.h" @@ -88,6 +89,11 @@ bool Integration::handleUrlClick( return false; } +bool Integration::copyPreOnClick(const QVariant &context) { + Toast::Show(u"Code copied to clipboard."_q); + return true; +} + QString Integration::convertTagToMimeTag(const QString &tagId) { return tagId; } diff --git a/ui/integration.h b/ui/integration.h index e6847f6..ae169b4 100644 --- a/ui/integration.h +++ b/ui/integration.h @@ -57,6 +57,7 @@ public: [[nodiscard]] virtual bool handleUrlClick( const QString &url, const QVariant &context); + [[nodiscard]] virtual bool copyPreOnClick(const QVariant &context); [[nodiscard]] virtual QString convertTagToMimeTag(const QString &tagId); [[nodiscard]] virtual const Emoji::One *defaultEmojiVariant( const Emoji::One *emoji); diff --git a/ui/text/text.cpp b/ui/text/text.cpp index af66be4..9360676 100644 --- a/ui/text/text.cpp +++ b/ui/text/text.cpp @@ -239,6 +239,7 @@ void ValidateQuotePaintCache( p.setClipRect(outline, header, side - outline, side - header); p.drawRoundedRect(0, 0, side, side, radius, radius); if (icon) { + p.setClipping(false); const auto left = side - icon->width() - st.iconPosition.x(); const auto top = st.iconPosition.y(); icon->paint(p, left, top, side, cache.icon); @@ -364,13 +365,23 @@ String::ExtendedWrap::~ExtendedWrap() = default; void String::ExtendedWrap::adjustFrom(const ExtendedWrap *other) { const auto data = get(); - if (data && data->spoiler) { - const auto raw = [](auto pointer) { - return reinterpret_cast(pointer); - }; - const auto otherText = raw(data->spoiler->link->text().get()); - data->spoiler->link->setText( + const auto raw = [](auto pointer) { + return reinterpret_cast(pointer); + }; + const auto adjust = [&](auto &link) { + const auto otherText = raw(link->text().get()); + link->setText( reinterpret_cast(otherText + raw(this) - raw(other))); + }; + if (data) { + if (data->spoiler) { + adjust(data->spoiler->link); + } + for (auto "e : data->quotes) { + if (quote.copy) { + adjust(quote.copy); + } + } } } @@ -1184,14 +1195,27 @@ int String::quoteMinWidth(QuoteDetails *quote) const { } const auto qpadding = quotePadding(quote); const auto &qheader = quoteHeaderText(quote); - const auto qst = quote ? "eStyle(quote) : nullptr; - return qpadding.left() - + (qheader.isEmpty() ? 0 : _st->font->monospace()->width(qheader)) + const auto &qst = quoteStyle(quote); + const auto radius = qst.radius; + const auto header = qst.header; + const auto outline = qst.outline; + const auto iconsize = (!qst.icon.empty()) + ? std::max( + qst.icon.width() + qst.iconPosition.x(), + qst.icon.height() + qst.iconPosition.y()) + : 0; + const auto corner = std::max({ header, radius, outline, iconsize }); + const auto top = qpadding.left() + + (qheader.isEmpty() + ? 0 + : (_st->font->monospace()->width(qheader) + + _st->pre.headerPosition.x())) + std::max( qpadding.right(), - ((qst && !qst->icon.empty()) - ? (qst->iconPosition.x() + qst->icon.width()) + (!qst.icon.empty() + ? (qst.iconPosition.x() + qst.icon.width()) : 0)); + return std::max(top, 2 * corner); } const QString &String::quoteHeaderText(QuoteDetails *quote) const { @@ -1221,11 +1245,19 @@ void String::enumerateText( int linkIndex = 0; uint16 linkPosition = 0; + int quoteIndex = _startQuoteIndex; - int32 flags = 0; + TextBlockFlags flags = {}; for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - const auto blockPosition = (i == e) ? uint16(_text.size()) : (*i)->position(); + const auto blockPosition = (i == e) + ? uint16(_text.size()) + : (*i)->position(); const auto blockFlags = (i == e) ? TextBlockFlags() : (*i)->flags(); + const auto blockQuoteIndex = (i == e) + ? 0 + : ((*i)->type() != TextBlockType::Newline) + ? quoteIndex + : static_cast(i->get())->quoteIndex(); const auto blockLinkIndex = [&] { if (IsMono(blockFlags) || (i == e)) { return 0; @@ -1240,7 +1272,10 @@ void String::enumerateText( auto rangeFrom = qMax(selection.from, linkPosition); auto rangeTo = qMin(selection.to, blockPosition); if (rangeTo > rangeFrom) { // handle click handler - const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom); + const auto r = base::StringViewMid( + _text, + rangeFrom, + rangeTo - rangeFrom); // Ignore links that are partially copied. const auto handler = (linkPosition != rangeFrom || blockPosition != rangeTo @@ -1267,11 +1302,20 @@ void String::enumerateText( const auto checkBlockFlags = (blockPosition >= selection.from) && (blockPosition <= selection.to); - if (checkBlockFlags && blockFlags != flags) { - flagsChangeCallback(flags, blockFlags); + if (checkBlockFlags + && (blockFlags != flags + || ((flags & TextBlockFlag::Pre) + && blockQuoteIndex != quoteIndex))) { + flagsChangeCallback( + flags, + quoteIndex, + blockFlags, + blockQuoteIndex); flags = blockFlags; } - if (i == e || (linkIndex ? linkPosition : blockPosition) >= selection.to) { + quoteIndex = blockQuoteIndex; + if (i == e + || (linkIndex ? linkPosition : blockPosition) >= selection.to) { break; } @@ -1393,18 +1437,33 @@ TextForMimeData String::toText( { TextBlockFlag::Pre, EntityType::Pre }, { TextBlockFlag::Blockquote, EntityType::Blockquote }, } : std::vector(); - const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) { + const auto flagsChangeCallback = [&]( + TextBlockFlags oldFlags, + int oldQuoteIndex, + TextBlockFlags newFlags, + int newQuoteIndex) { if (!composeEntities) { return; } for (auto &tracker : markdownTrackers) { const auto flag = tracker.flag; - if ((oldFlags & flag) && !(newFlags & flag)) { + const auto quoteWithLanguage = (flag == TextBlockFlag::Pre); + const auto quoteWithLanguageChanged = quoteWithLanguage + && (oldQuoteIndex != newQuoteIndex); + const auto data = (quoteWithLanguage && oldQuoteIndex) + ? _extended->quotes[oldQuoteIndex - 1].language + : QString(); + if (((oldFlags & flag) && !(newFlags & flag)) + || quoteWithLanguageChanged) { insertEntity({ tracker.type, tracker.start, - int(result.rich.text.size()) - tracker.start }); - } else if ((newFlags & flag) && !(oldFlags & flag)) { + int(result.rich.text.size()) - tracker.start, + data, + }); + } + if (((newFlags & flag) && !(oldFlags & flag)) + || quoteWithLanguageChanged) { tracker.start = result.rich.text.size(); } } diff --git a/ui/text/text_extended_data.cpp b/ui/text/text_extended_data.cpp index 38cb0e7..dbae483 100644 --- a/ui/text/text_extended_data.cpp +++ b/ui/text/text_extended_data.cpp @@ -7,6 +7,7 @@ #include "ui/text/text_extended_data.h" #include "ui/text/text.h" +#include "ui/integration.h" namespace Ui::Text { @@ -32,5 +33,41 @@ void SpoilerClickHandler::onClick(ClickContext context) const { _text->setSpoilerRevealed(true, anim::type::normal); } +PreClickHandler::PreClickHandler( + not_null text, + uint16 offset, + uint16 length) +: _text(text) +, _offset(offset) +, _length(length) { +} + +not_null PreClickHandler::text() const { + return _text; +} + +void PreClickHandler::setText(not_null text) { + _text = text; +} + +void PreClickHandler::onClick(ClickContext context) const { + if (context.button != Qt::LeftButton) { + return; + } + const auto till = uint16(_offset + _length); + auto text = _text->toTextForMimeData({ _offset, till }); + if (text.empty()) { + return; + } else if (!text.rich.text.endsWith('\n')) { + text.rich.text.append('\n'); + } + if (!text.expanded.endsWith('\n')) { + text.expanded.append('\n'); + } + if (Integration::Instance().copyPreOnClick(context.other)) { + TextUtilities::SetClipboardText(std::move(text)); + } +} + } // namespace Ui::Text diff --git a/ui/text/text_extended_data.h b/ui/text/text_extended_data.h index f86177f..a0b25fa 100644 --- a/ui/text/text_extended_data.h +++ b/ui/text/text_extended_data.h @@ -32,6 +32,22 @@ private: }; +class PreClickHandler final : public ClickHandler { +public: + PreClickHandler(not_null text, uint16 offset, uint16 length); + + [[nodiscard]] not_null text() const; + void setText(not_null text); + + void onClick(ClickContext context) const override; + +private: + not_null _text; + uint16 _offset = 0; + uint16 _length = 0; + +}; + struct SpoilerData { explicit SpoilerData(Fn repaint) : animation(std::move(repaint)) { @@ -45,7 +61,7 @@ struct SpoilerData { struct QuoteDetails { QString language; - ClickHandlerPtr copy; + std::shared_ptr copy; int copyWidth = 0; int maxWidth = 0; int minHeight = 0; diff --git a/ui/text/text_parser.cpp b/ui/text/text_parser.cpp index 27de57a..816eab6 100644 --- a/ui/text/text_parser.cpp +++ b/ui/text/text_parser.cpp @@ -243,6 +243,7 @@ void Parser::ensureAtNewline(QuoteDetails quote) { createNewlineBlock(false); _customEmojiData = base::take(saved); } + _quoteStartPosition = _t->_text.size(); auto "es = _t->ensureExtended()->quotes; quotes.push_back(std::move(quote)); const auto index = _quoteIndex = int(quotes.size()); @@ -274,6 +275,18 @@ void Parser::finishEntities() { if ((*flags) & (TextBlockFlag::Pre | TextBlockFlag::Blockquote)) { + if (_quoteIndex) { + auto "es = _t->ensureExtended()->quotes; + auto "e = quotes[_quoteIndex - 1]; + const auto from = _quoteStartPosition; + const auto till = _t->_text.size(); + if (quote.pre && till > from) { + quote.copy = std::make_shared( + _t, + from, + till - from); + } + } _quoteIndex = 0; if (lastType != TextBlockType::Newline) { _newlineAwaited = true; diff --git a/ui/text/text_parser.h b/ui/text/text_parser.h index c245e8f..847aca9 100644 --- a/ui/text/text_parser.h +++ b/ui/text/text_parser.h @@ -116,6 +116,7 @@ private: uint16 _colorIndex = 0; uint16 _monoIndex = 0; uint16 _quoteIndex = 0; + int _quoteStartPosition = 0; EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero int32 _blockStart = 0; // offset in result, from which current parsed block is started int32 _diacritics = 0; // diacritic chars skipped without good char diff --git a/ui/text/text_renderer.cpp b/ui/text/text_renderer.cpp index 7e75bc0..70eae12 100644 --- a/ui/text/text_renderer.cpp +++ b/ui/text/text_renderer.cpp @@ -404,22 +404,8 @@ void Renderer::enumerate() { } void Renderer::fillParagraphBg(int paddingBottom) { - const auto cache = (!_p || !_quote) - ? nullptr - : _quote->pre - ? _quotePreCache - : _quote->blockquote - ? _quoteBlockquoteCache - : nullptr; - if (cache) { + if (_quote) { const auto &st = _t->quoteStyle(_quote); - auto &valid = _quote->pre - ? _quotePreValid - : _quoteBlockquoteValid; - if (!valid) { - valid = true; - ValidateQuotePaintCache(*cache, st); - } const auto skip = st.verticalSkip; const auto isTop = (_y != _quoteLineTop); const auto isBottom = (paddingBottom != 0); @@ -428,19 +414,48 @@ void Renderer::fillParagraphBg(int paddingBottom) { const auto fill = _y + _lineHeight + paddingBottom - top - (isBottom ? skip : 0); const auto rect = QRect(left, top, _startLineWidth, fill); - FillQuotePaint(*_p, rect, *cache, st, { - .skipTop = !isTop, - .skipBottom = !isBottom, - }); + const auto cache = (!_p || !_quote) + ? nullptr + : _quote->pre + ? _quotePreCache + : _quote->blockquote + ? _quoteBlockquoteCache + : nullptr; + if (cache) { + auto &valid = _quote->pre + ? _quotePreValid + : _quoteBlockquoteValid; + if (!valid) { + valid = true; + ValidateQuotePaintCache(*cache, st); + } + FillQuotePaint(*_p, rect, *cache, st, { + .skipTop = !isTop, + .skipBottom = !isBottom, + }); + } if (isTop && st.header > 0) { - const auto font = _t->_st->font->monospace(); - const auto topleft = rect.topLeft(); - const auto position = topleft + st.headerPosition; - const auto baseline = position + QPoint(0, font->ascent); - _p->setFont(font); - _p->setPen(_palette->monoFg->p); - _p->drawText(baseline, _t->quoteHeaderText(_quote)); + if (_p) { + const auto font = _t->_st->font->monospace(); + const auto topleft = rect.topLeft(); + const auto position = topleft + st.headerPosition; + const auto lbaseline = position + QPoint(0, font->ascent); + _p->setFont(font); + _p->setPen(_palette->monoFg->p); + _p->drawText(lbaseline, _t->quoteHeaderText(_quote)); + } else if (_lookupX >= left + && _lookupX < left + _startLineWidth + && _lookupY >= top + && _lookupY < top + st.header) { + if (_lookupLink) { + _lookupResult.link = _quote->copy; + } + if (_lookupSymbol) { + _lookupResult.symbol = _lineStart; + _lookupResult.afterSymbol = false; + } + } } } _quoteLineTop = _y + _lineHeight + paddingBottom;