Add copy code blocks on header click.
This commit is contained in:
parent
5ffbb90fd6
commit
17d73a5c0c
8 changed files with 196 additions and 48 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<quintptr>(pointer);
|
||||
};
|
||||
const auto otherText = raw(data->spoiler->link->text().get());
|
||||
data->spoiler->link->setText(
|
||||
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 "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<const NewlineBlock*>(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<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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,22 @@ private:
|
|||
|
||||
};
|
||||
|
||||
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)) {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<PreClickHandler>(
|
||||
_t,
|
||||
from,
|
||||
till - from);
|
||||
}
|
||||
}
|
||||
_quoteIndex = 0;
|
||||
if (lastType != TextBlockType::Newline) {
|
||||
_newlineAwaited = true;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
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) {
|
||||
if (_p) {
|
||||
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);
|
||||
const auto lbaseline = position + QPoint(0, font->ascent);
|
||||
_p->setFont(font);
|
||||
_p->setPen(_palette->monoFg->p);
|
||||
_p->drawText(baseline, _t->quoteHeaderText(_quote));
|
||||
_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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue