Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
305 lines
8.3 KiB
C++
305 lines
8.3 KiB
C++
// This file is part of Desktop App Toolkit,
|
|
// a set of libraries for developing nice desktop applications.
|
|
//
|
|
// For license and copyright information please follow this link:
|
|
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
|
//
|
|
#include "ui/widgets/fields/custom_field_object.h"
|
|
|
|
#include "ui/effects/spoiler_mess.h"
|
|
#include "ui/text/text.h"
|
|
#include "ui/text/text_renderer.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "ui/widgets/fields/input_field.h"
|
|
#include "ui/integration.h"
|
|
#include "styles/style_basic.h"
|
|
#include "styles/style_widgets.h"
|
|
|
|
#include <QtWidgets/QTextEdit>
|
|
#include <QtWidgets/QScrollBar>
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kSpoilerHiddenOpacity = 0.5;
|
|
|
|
using SpoilerRect = InputFieldSpoilerRect;
|
|
|
|
} // namespace
|
|
|
|
class FieldSpoilerOverlay final : public RpWidget {
|
|
public:
|
|
FieldSpoilerOverlay(
|
|
not_null<InputField*> field,
|
|
Fn<float64()> shown,
|
|
Fn<bool()> paused);
|
|
|
|
private:
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
const not_null<InputField*> _field;
|
|
const Fn<float64()> _shown;
|
|
const Fn<bool()> _paused;
|
|
SpoilerAnimation _animation;
|
|
|
|
};
|
|
|
|
FieldSpoilerOverlay::FieldSpoilerOverlay(
|
|
not_null<InputField*> field,
|
|
Fn<float64()> shown,
|
|
Fn<bool()> paused)
|
|
: RpWidget(field->rawTextEdit())
|
|
, _field(field)
|
|
, _shown(std::move(shown))
|
|
, _paused(std::move(paused))
|
|
, _animation([=] { update(); }) {
|
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
|
show();
|
|
}
|
|
|
|
void FieldSpoilerOverlay::paintEvent(QPaintEvent *e) {
|
|
auto p = std::optional<QPainter>();
|
|
auto topShift = std::optional<int>();
|
|
auto frame = std::optional<SpoilerMessFrame>();
|
|
auto blockquoteBg = std::optional<QColor>();
|
|
const auto clip = e->rect();
|
|
|
|
const auto shown = _shown();
|
|
const auto bgOpacity = shown;
|
|
const auto fgOpacity = 1. * shown + kSpoilerHiddenOpacity * (1. - shown);
|
|
for (const auto &rect : _field->_spoilerRects) {
|
|
const auto fill = rect.geometry.intersected(clip);
|
|
if (fill.isEmpty()) {
|
|
continue;
|
|
} else if (!p) {
|
|
p.emplace(this);
|
|
const auto paused = _paused && _paused();
|
|
frame.emplace(
|
|
Text::DefaultSpoilerCache()->lookup(
|
|
st::defaultTextPalette.spoilerFg->c)->frame(
|
|
_animation.index(crl::now(), paused)));
|
|
topShift = -_field->rawTextEdit()->verticalScrollBar()->value();
|
|
}
|
|
if (bgOpacity > 0.) {
|
|
p->setOpacity(bgOpacity);
|
|
if (rect.blockquote && !blockquoteBg) {
|
|
const auto bg = _field->_blockquoteBg;
|
|
blockquoteBg = (bg.alphaF() < 1.)
|
|
? anim::color(
|
|
_field->_st.textBg->c,
|
|
QColor(bg.red(), bg.green(), bg.blue()),
|
|
bg.alphaF())
|
|
: bg;
|
|
}
|
|
p->fillRect(
|
|
fill,
|
|
rect.blockquote ? *blockquoteBg : _field->_st.textBg->c);
|
|
}
|
|
p->setOpacity(fgOpacity);
|
|
const auto shift = QPoint(0, *topShift) - rect.geometry.topLeft();
|
|
FillSpoilerRect(*p, rect.geometry, *frame, shift);
|
|
}
|
|
}
|
|
|
|
CustomFieldObject::CustomFieldObject(
|
|
not_null<InputField*> field,
|
|
Text::MarkedContext context,
|
|
Fn<bool()> pausedEmoji,
|
|
Fn<bool()> pausedSpoiler)
|
|
: _field(field)
|
|
, _context(std::move(context))
|
|
, _pausedEmoji(std::move(pausedEmoji))
|
|
, _pausedSpoiler(std::move(pausedSpoiler))
|
|
, _factory(makeFactory())
|
|
, _now(crl::now()) {
|
|
}
|
|
|
|
CustomFieldObject::~CustomFieldObject() = default;
|
|
|
|
void *CustomFieldObject::qt_metacast(const char *iid) {
|
|
if (QLatin1String(iid) == qobject_interface_iid<QTextObjectInterface*>()) {
|
|
return static_cast<QTextObjectInterface*>(this);
|
|
}
|
|
return QObject::qt_metacast(iid);
|
|
}
|
|
|
|
QSizeF CustomFieldObject::intrinsicSize(
|
|
QTextDocument *doc,
|
|
int posInDocument,
|
|
const QTextFormat &format) {
|
|
const auto line = _field->_st.style.font->height;
|
|
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
|
|
const auto &padding = _field->_st.style.blockquote.padding;
|
|
const auto paddings = padding.left() + padding.right();
|
|
const auto skip = 2 * doc->documentMargin();
|
|
const auto height = Text::kQuoteCollapsedLines * line;
|
|
return QSizeF(doc->pageSize().width() - paddings - skip, height);
|
|
}
|
|
const auto size = st::emojiSize * 1.;
|
|
const auto width = size + st::emojiPadding * 2.;
|
|
const auto height = std::max(line * 1., size);
|
|
if (!_skip) {
|
|
const auto emoji = Text::AdjustCustomEmojiSize(st::emojiSize);
|
|
_skip = (st::emojiSize - emoji) / 2;
|
|
}
|
|
return { width, height };
|
|
}
|
|
|
|
void CustomFieldObject::drawObject(
|
|
QPainter *painter,
|
|
const QRectF &rect,
|
|
QTextDocument *doc,
|
|
int posInDocument,
|
|
const QTextFormat &format) {
|
|
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
|
|
const auto left = 0;
|
|
const auto top = 0;
|
|
const auto id = format.property(InputField::kQuoteId).toInt();
|
|
if (const auto i = _quotes.find(id); i != end(_quotes)) {
|
|
i->second.string.draw(*painter, {
|
|
.position = QPoint(left + rect.x(), top + rect.y()),
|
|
.outerWidth = int(base::SafeRound(doc->pageSize().width())),
|
|
.availableWidth = int(std::floor(rect.width())),
|
|
|
|
.palette = nullptr,
|
|
.spoiler = Text::DefaultSpoilerCache(),
|
|
.now = _now,
|
|
.pausedEmoji = _pausedEmoji(),
|
|
.pausedSpoiler = _pausedSpoiler(),
|
|
|
|
.elisionLines = Text::kQuoteCollapsedLines,
|
|
.useFullWidth = true,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
const auto id = format.property(InputField::kCustomEmojiId).toULongLong();
|
|
if (!id) {
|
|
return;
|
|
}
|
|
auto i = _emoji.find(id);
|
|
if (i == end(_emoji)) {
|
|
const auto link = format.property(InputField::kCustomEmojiLink);
|
|
const auto data = InputField::CustomEmojiEntityData(link.toString());
|
|
if (auto emoji = _factory(data)) {
|
|
i = _emoji.emplace(id, std::move(emoji)).first;
|
|
}
|
|
}
|
|
if (i == end(_emoji)) {
|
|
return;
|
|
}
|
|
i->second->paint(*painter, {
|
|
.textColor = format.foreground().color(),
|
|
.now = _now,
|
|
.position = QPoint(
|
|
int(base::SafeRound(rect.x())) + st::emojiPadding + _skip,
|
|
int(base::SafeRound(rect.y())) + _skip),
|
|
.paused = _pausedEmoji && _pausedEmoji(),
|
|
});
|
|
}
|
|
|
|
void CustomFieldObject::clearEmoji() {
|
|
_emoji.clear();
|
|
}
|
|
|
|
void CustomFieldObject::clearQuotes() {
|
|
_quotes.clear();
|
|
}
|
|
|
|
std::unique_ptr<RpWidget> CustomFieldObject::createSpoilerOverlay() {
|
|
return std::make_unique<FieldSpoilerOverlay>(
|
|
_field,
|
|
[=] { return _spoilerOpacity.value(_spoilerHidden ? 0. : 1.); },
|
|
_pausedSpoiler);
|
|
}
|
|
|
|
void CustomFieldObject::refreshSpoilerShown(InputFieldTextRange range) {
|
|
auto hidden = false;
|
|
using Range = InputFieldTextRange;
|
|
const auto intersects = [](Range a, Range b) {
|
|
return (a.from < b.till) && (b.from < a.till);
|
|
};
|
|
if (range.till > range.from) {
|
|
const auto check = [&](const std::vector<Range> &list) {
|
|
if (!hidden) {
|
|
for (const auto &spoiler : list) {
|
|
if (intersects(spoiler, range)) {
|
|
hidden = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
check(_field->_spoilerRangesText);
|
|
check(_field->_spoilerRangesEmoji);
|
|
} else {
|
|
auto touchesLeft = false;
|
|
auto touchesRight = false;
|
|
const auto cursor = range.from;
|
|
const auto check = [&](const std::vector<Range> &list) {
|
|
if (!touchesLeft || !touchesRight) {
|
|
for (const auto &spoiler : list) {
|
|
if (spoiler.from <= cursor && spoiler.till >= cursor) {
|
|
if (spoiler.from < cursor) {
|
|
touchesLeft = true;
|
|
}
|
|
if (spoiler.till >= cursor) {
|
|
touchesRight = true;
|
|
}
|
|
if (touchesLeft && touchesRight) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
check(_field->_spoilerRangesText);
|
|
check(_field->_spoilerRangesEmoji);
|
|
hidden = touchesLeft && touchesRight;
|
|
}
|
|
if (_spoilerHidden != hidden) {
|
|
_spoilerHidden = hidden;
|
|
_spoilerOpacity.start(
|
|
[=] { _field->update(); },
|
|
hidden ? 1. : 0.,
|
|
hidden ? 0. : 1.,
|
|
st::fadeWrapDuration);
|
|
}
|
|
}
|
|
|
|
void CustomFieldObject::setCollapsedText(int quoteId, TextWithTags text) {
|
|
auto "e = _quotes[quoteId];
|
|
quote.string = Text::String(_field->_st.widthMin);
|
|
quote.string.setMarkedText(_field->_st.style, {
|
|
text.text,
|
|
TextUtilities::ConvertTextTagsToEntities(text.tags),
|
|
}, kMarkupTextOptions, makeFieldContext());
|
|
quote.text = std::move(text);
|
|
}
|
|
|
|
const TextWithTags &CustomFieldObject::collapsedText(int quoteId) const {
|
|
if (const auto i = _quotes.find(quoteId); i != end(_quotes)) {
|
|
return i->second.text;
|
|
}
|
|
static const auto kEmpty = TextWithTags();
|
|
return kEmpty;
|
|
}
|
|
|
|
Text::MarkedContext CustomFieldObject::makeFieldContext() {
|
|
auto context = _context;
|
|
context.repaint = [field = _field] { field->update(); };
|
|
return context;
|
|
}
|
|
|
|
CustomFieldObject::Factory CustomFieldObject::makeFactory() {
|
|
return [context = makeFieldContext()](QStringView data) {
|
|
return Text::MakeCustomEmoji(data, context);
|
|
};
|
|
}
|
|
|
|
void CustomFieldObject::setNow(crl::time now) {
|
|
_now = now;
|
|
}
|
|
|
|
} // namespace Ui
|