Skip to content

Commit

Permalink
Add copy code blocks on header click.
Browse files Browse the repository at this point in the history
  • Loading branch information
john-preston committed Oct 19, 2023
1 parent 5ffbb90 commit 17d73a5
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 48 deletions.
6 changes: 6 additions & 0 deletions ui/integration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions ui/integration.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Integration {
[[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);
Expand Down
101 changes: 80 additions & 21 deletions ui/text/text.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<quintptr>(pointer);
};
const auto otherText = raw(data->spoiler->link->text().get());
data->spoiler->link->setText(
const auto raw = [](auto pointer) {
return reinterpret_cast<quintptr>(pointer);
};
const auto adjust = [&](auto &link) {
const auto otherText = raw(link->text().get());
link->setText(
reinterpret_cast<String*>(otherText + raw(this) - raw(other)));
};
if (data) {
if (data->spoiler) {
adjust(data->spoiler->link);
}
for (auto &quote : data->quotes) {
if (quote.copy) {
adjust(quote.copy);
}
}
}
}

Expand Down Expand Up @@ -1184,14 +1195,27 @@ int String::quoteMinWidth(QuoteDetails *quote) const {
}
const auto qpadding = quotePadding(quote);
const auto &qheader = quoteHeaderText(quote);
const auto qst = quote ? &quoteStyle(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 {
Expand Down Expand Up @@ -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<const NewlineBlock*>(i->get())->quoteIndex();
const auto blockLinkIndex = [&] {
if (IsMono(blockFlags) || (i == e)) {
return 0;
Expand All @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -1393,18 +1437,33 @@ TextForMimeData String::toText(
{ TextBlockFlag::Pre, EntityType::Pre },
{ TextBlockFlag::Blockquote, EntityType::Blockquote },
} : std::vector<MarkdownTagTracker>();
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();
}
}
Expand Down
37 changes: 37 additions & 0 deletions ui/text/text_extended_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "ui/text/text_extended_data.h"

#include "ui/text/text.h"
#include "ui/integration.h"

namespace Ui::Text {

Expand All @@ -32,5 +33,41 @@ void SpoilerClickHandler::onClick(ClickContext context) const {
_text->setSpoilerRevealed(true, anim::type::normal);
}

PreClickHandler::PreClickHandler(
not_null<String*> text,
uint16 offset,
uint16 length)
: _text(text)
, _offset(offset)
, _length(length) {
}

not_null<String*> PreClickHandler::text() const {
return _text;
}

void PreClickHandler::setText(not_null<String*> 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

18 changes: 17 additions & 1 deletion ui/text/text_extended_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ class SpoilerClickHandler final : public ClickHandler {

};

class PreClickHandler final : public ClickHandler {
public:
PreClickHandler(not_null<String*> text, uint16 offset, uint16 length);

[[nodiscard]] not_null<String*> text() const;
void setText(not_null<String*> text);

void onClick(ClickContext context) const override;

private:
not_null<String*> _text;
uint16 _offset = 0;
uint16 _length = 0;

};

struct SpoilerData {
explicit SpoilerData(Fn<void()> repaint)
: animation(std::move(repaint)) {
Expand All @@ -45,7 +61,7 @@ struct SpoilerData {

struct QuoteDetails {
QString language;
ClickHandlerPtr copy;
std::shared_ptr<PreClickHandler> copy;
int copyWidth = 0;
int maxWidth = 0;
int minHeight = 0;
Expand Down
13 changes: 13 additions & 0 deletions ui/text/text_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ void Parser::ensureAtNewline(QuoteDetails quote) {
createNewlineBlock(false);
_customEmojiData = base::take(saved);
}
_quoteStartPosition = _t->_text.size();
auto &quotes = _t->ensureExtended()->quotes;
quotes.push_back(std::move(quote));
const auto index = _quoteIndex = int(quotes.size());
Expand Down Expand Up @@ -274,6 +275,18 @@ void Parser::finishEntities() {
if ((*flags)
& (TextBlockFlag::Pre
| TextBlockFlag::Blockquote)) {
if (_quoteIndex) {
auto &quotes = _t->ensureExtended()->quotes;
auto &quote = quotes[_quoteIndex - 1];
const auto from = _quoteStartPosition;
const auto till = _t->_text.size();
if (quote.pre && till > from) {
quote.copy = std::make_shared<PreClickHandler>(
_t,
from,
till - from);
}
}
_quoteIndex = 0;
if (lastType != TextBlockType::Newline) {
_newlineAwaited = true;
Expand Down
1 change: 1 addition & 0 deletions ui/text/text_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class Parser {
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
Expand Down
67 changes: 41 additions & 26 deletions ui/text/text_renderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down

0 comments on commit 17d73a5

Please sign in to comment.