Updated lib_ui sources to TDesktop version 3.4.3

This commit is contained in:
Eric Kotato 2022-01-09 03:04:05 +03:00
commit 039cbd656d
24 changed files with 867 additions and 178 deletions

View file

@ -230,6 +230,8 @@ PRIVATE
ui/rect_part.h
ui/round_rect.cpp
ui/round_rect.h
ui/spoiler_click_handler.cpp
ui/spoiler_click_handler.h
ui/rp_widget.cpp
ui/rp_widget.h
ui/ui_utility.cpp

View file

@ -15,6 +15,9 @@ TextPalette {
selectMonoFg: color;
selectOverlay: color;
linkAlwaysActive: int;
spoilerBg: color;
spoilerActiveBg: color;
spoilerActiveFg: color;
}
TextStyle {
@ -44,6 +47,9 @@ defaultTextPalette: TextPalette {
selectLinkFg: historyLinkInFgSelected;
selectMonoFg: msgInMonoFgSelected;
selectOverlay: msgSelectOverlay;
spoilerBg: msgInDateFg;
spoilerActiveBg: msgInDateFg;
spoilerActiveFg: msgInBg;
}
defaultTextStyle: TextStyle {
font: normalFont;
@ -139,6 +145,9 @@ inTextPalette: TextPalette(defaultTextPalette) {
selectLinkFg: historyLinkInFgSelected;
selectMonoFg: msgInMonoFgSelected;
selectOverlay: msgSelectOverlay;
spoilerBg: msgInDateFg;
spoilerActiveBg: msgInDateFg;
spoilerActiveFg: msgInBg;
}
inTextPaletteSelected: TextPalette(inTextPalette) {
linkFg: historyLinkInFgSelected;
@ -152,6 +161,9 @@ outTextPalette: TextPalette(defaultTextPalette) {
selectLinkFg: historyLinkOutFgSelected;
selectMonoFg: msgOutMonoFgSelected;
selectOverlay: msgSelectOverlay;
spoilerBg: msgOutDateFg;
spoilerActiveBg: msgOutDateFg;
spoilerActiveFg: msgOutBg;
}
outTextPaletteSelected: TextPalette(outTextPalette) {
linkFg: historyLinkOutFgSelected;

View file

@ -55,7 +55,7 @@ menuBgOver: windowBgOver; // default popup menu item background with mouse over
menuBgRipple: windowBgRipple; // default popup menu item ripple effect
menuIconFg: #a8a8a8; // default popup menu item icon (like main menu)
menuIconFgOver: #999999; // default popup menu item icon with mouse over
menuSubmenuArrowFg: #373737; // default popup menu submenu arrow icon (like in message field context menu in case of RTL system language)
menuSubmenuArrowFg: #666B72; // default popup menu submenu arrow icon (like in message field context menu in case of RTL system language)
menuFgDisabled: #cccccc; // default popup menu item disabled text (like unavailable items in message field context menu)
menuSeparatorFg: #f1f1f1; // default popup menu separator (like in message field context menu)

View file

@ -144,4 +144,8 @@ QString Integration::phraseFormattingMonospace() {
return "Monospace";
}
QString Integration::phraseFormattingSpoiler() {
return "Spoiler";
}
} // namespace Ui

View file

@ -76,6 +76,7 @@ public:
[[nodiscard]] virtual QString phraseFormattingUnderline();
[[nodiscard]] virtual QString phraseFormattingStrikeOut();
[[nodiscard]] virtual QString phraseFormattingMonospace();
[[nodiscard]] virtual QString phraseFormattingSpoiler();
};

View file

@ -7,10 +7,8 @@
#include "ui/platform/linux/ui_utility_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/debug_log.h"
#include "ui/platform/linux/ui_linux_wayland_integration.h"
#include "base/const_string.h"
#include "base/flat_set.h"
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
#include "base/platform/linux/base_linux_glibmm_helper.h"
@ -23,10 +21,8 @@
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QPoint>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#include <qpa/qplatformnativeinterface.h>
namespace Ui {
namespace Platform {
@ -132,7 +128,7 @@ std::optional<uint> XCBCurrentWorkspace() {
return std::nullopt;
}
const auto root = base::Platform::XCB::GetRootWindowFromQt();
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root.has_value()) {
return std::nullopt;
}
@ -220,7 +216,7 @@ std::optional<bool> XCBIsOverlapped(
return std::nullopt;
}
const auto root = base::Platform::XCB::GetRootWindowFromQt();
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root.has_value()) {
return std::nullopt;
}
@ -345,7 +341,7 @@ bool ShowXCBWindowMenu(QWindow *window) {
return false;
}
const auto root = base::Platform::XCB::GetRootWindowFromQt();
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root.has_value()) {
return false;
}
@ -431,23 +427,36 @@ bool TranslucentWindowsSupported(QPoint globalPosition) {
return true;
}
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
if (const auto native = QGuiApplication::platformNativeInterface()) {
if (const auto screen = QGuiApplication::screenAt(globalPosition)) {
if (native->nativeResourceForScreen(QByteArray("compositingEnabled"), screen)) {
return true;
}
const auto index = QGuiApplication::screens().indexOf(screen);
static auto WarnedAbout = base::flat_set<int>();
if (!WarnedAbout.contains(index)) {
WarnedAbout.emplace(index);
LOG(("WARNING: Compositing is disabled for screen index %1 (for position %2,%3)").arg(index).arg(globalPosition.x()).arg(globalPosition.y()));
}
} else {
LOG(("WARNING: Could not get screen for position %1,%2").arg(globalPosition.x()).arg(globalPosition.y()));
}
const auto connection = base::Platform::XCB::GetConnectionFromQt();
if (!connection) {
return false;
}
const auto atom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_CM_S0");
if (!atom) {
return false;
}
const auto cookie = xcb_get_selection_owner(connection, *atom);
const auto result = base::Platform::XCB::MakeReplyPointer(
xcb_get_selection_owner_reply(
connection,
cookie,
nullptr));
if (!result) {
return false;
}
return result->owner;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}

View file

@ -740,16 +740,7 @@ HWND GetWindowHandle(not_null<QWidget*> widget) {
}
HWND GetWindowHandle(not_null<QWindow*> window) {
if (!window->winId()) {
window->create();
}
const auto native = QGuiApplication::platformNativeInterface();
Assert(native != nullptr);
return static_cast<HWND>(native->nativeResourceForWindow(
QByteArrayLiteral("handle"),
window));
return reinterpret_cast<HWND>(window->winId());
}
void SendWMPaintForce(not_null<QWidget*> widget) {

View file

@ -0,0 +1,40 @@
// 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/spoiler_click_handler.h"
#include "ui/effects/animation_value.h"
#include "ui/text/text_entity.h"
ClickHandler::TextEntity SpoilerClickHandler::getTextEntity() const {
return { EntityType::Spoiler };
}
void SpoilerClickHandler::onClick(ClickContext context) const {
if (!_shown) {
const auto nonconst = const_cast<SpoilerClickHandler*>(this);
nonconst->_shown = true;
}
}
bool SpoilerClickHandler::shown() const {
return _shown;
}
crl::time SpoilerClickHandler::startMs() const {
return _startMs;
}
void SpoilerClickHandler::setStartMs(crl::time value) {
if (anim::Disabled()) {
return;
}
_startMs = value;
}
void SpoilerClickHandler::setShown(bool value) {
_shown = value;
}

View file

@ -0,0 +1,30 @@
// 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
//
#pragma once
#include "ui/click_handler.h"
enum class EntityType : uchar;
class SpoilerClickHandler : public ClickHandler {
public:
SpoilerClickHandler() = default;
TextEntity getTextEntity() const override;
void onClick(ClickContext context) const override;
[[nodiscard]] bool shown() const;
void setShown(bool value);
[[nodiscard]] crl::time startMs() const;
void setStartMs(crl::time value);
private:
bool _shown = false;
crl::time _startMs = 0;
};

View file

@ -11,6 +11,9 @@
#include "ui/text/text_isolated_emoji.h"
#include "ui/emoji_config.h"
#include "ui/integration.h"
#include "ui/round_rect.h"
#include "ui/image/image_prepare.h"
#include "ui/spoiler_click_handler.h"
#include "base/platform/base_platform_info.h"
#include "base/qt_adapters.h"
@ -25,6 +28,7 @@ namespace {
constexpr auto kStringLinkIndexShift = uint16(0x8000);
constexpr auto kMaxDiacAfterSymbol = 2;
constexpr auto kSelectedSpoilerOpacity = 0.5;
Qt::LayoutDirection StringDirection(
const QString &str,
@ -195,6 +199,18 @@ QString textcmdStopSemibold() {
return result.append(TextCommand).append(QChar(TextCommandNoSemibold)).append(TextCommand);
}
QString textcmdStartSpoiler() {
QString result;
result.reserve(3);
return result.append(TextCommand).append(QChar(TextCommandSpoiler)).append(TextCommand);
}
QString textcmdStopSpoiler() {
QString result;
result.reserve(3);
return result.append(TextCommand).append(QChar(TextCommandNoSpoiler)).append(TextCommand);
}
const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink) {
const QChar *result = from + 1;
if (*from != TextCommand || result >= end) return from;
@ -212,6 +228,8 @@ const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink)
case TextCommandNoItalic:
case TextCommandUnderline:
case TextCommandNoUnderline:
case TextCommandSpoiler:
case TextCommandNoSpoiler:
break;
case TextCommandLinkIndex:
@ -273,13 +291,20 @@ private:
class StartedEntity {
public:
explicit StartedEntity(TextBlockFlags flags);
explicit StartedEntity(uint16 lnkIndex);
explicit StartedEntity(uint16 index, bool isLnk = true);
std::optional<TextBlockFlags> flags() const;
std::optional<uint16> lnkIndex() const;
std::optional<uint16> spoilerIndex() const;
private:
int _value = 0;
enum class Type {
Flags,
Link,
Spoiler,
};
const int _value = 0;
const Type _type;
};
@ -334,15 +359,18 @@ private:
const bool _checkTilde = false; // do we need a special text block for tilde symbol
std::vector<EntityLinkData> _links;
std::vector<EntityLinkData> _spoilers;
base::flat_map<
const QChar*,
std::vector<StartedEntity>> _startedEntities;
uint16 _maxLnkIndex = 0;
uint16 _maxSpoilerIndex = 0;
// current state
int32 _flags = 0;
uint16 _lnkIndex = 0;
uint16 _spoilerIndex = 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 _diacs = 0; // diac chars skipped without good char
@ -357,23 +385,36 @@ private:
};
Parser::StartedEntity::StartedEntity(TextBlockFlags flags) : _value(flags) {
Parser::StartedEntity::StartedEntity(TextBlockFlags flags)
: _value(flags)
, _type(Type::Flags) {
Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
}
Parser::StartedEntity::StartedEntity(uint16 lnkIndex) : _value(lnkIndex) {
Expects(_value >= kStringLinkIndexShift);
Parser::StartedEntity::StartedEntity(uint16 index, bool isLnk)
: _value(index)
, _type(isLnk ? Type::Link : Type::Spoiler) {
Expects((_type == Type::Link)
? (_value >= kStringLinkIndexShift)
: (_value < kStringLinkIndexShift));
}
std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
if (_value < int(kStringLinkIndexShift)) {
if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) {
return TextBlockFlags(_value);
}
return std::nullopt;
}
std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
if (_value >= int(kStringLinkIndexShift)) {
if (_value >= int(kStringLinkIndexShift) && (_type == Type::Link)) {
return uint16(_value);
}
return std::nullopt;
}
std::optional<uint16> Parser::StartedEntity::spoilerIndex() const {
if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) {
return uint16(_value);
}
return std::nullopt;
@ -437,6 +478,9 @@ void Parser::createBlock(int32 skipBack) {
if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
_maxLnkIndex = _lnkIndex;
}
if (_spoilerIndex > _maxSpoilerIndex) {
_maxSpoilerIndex = _spoilerIndex;
}
int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
if (len > 0) {
@ -450,13 +494,13 @@ void Parser::createBlock(int32 skipBack) {
}
_lastSkipped = false;
if (_emoji) {
_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _emoji));
_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _spoilerIndex, _emoji));
_emoji = nullptr;
_lastSkipped = true;
} else if (newline) {
_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex));
_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _spoilerIndex));
} else {
_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex));
_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex, _spoilerIndex));
}
_blockStart += len;
blockCreated();
@ -466,7 +510,7 @@ void Parser::createBlock(int32 skipBack) {
void Parser::createSkipBlock(int32 w, int32 h) {
createBlock();
_t->_text.push_back('_');
_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex));
_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex, _spoilerIndex));
blockCreated();
}
@ -509,6 +553,11 @@ void Parser::finishEntities() {
createBlock();
_lnkIndex = 0;
}
} else if (const auto spoilerIndex = list.back().spoilerIndex()) {
if (_spoilerIndex == *spoilerIndex && (_spoilerIndex != 0)) {
createBlock();
_spoilerIndex = 0;
}
}
list.pop_back();
}
@ -594,6 +643,16 @@ bool Parser::checkEntities() {
_flags |= flags;
_startedEntities[entityEnd].emplace_back(flags);
}
} else if (entityType == EntityType::Spoiler) {
createBlock();
_spoilers.push_back(EntityLinkData{
.data = QString::number(_spoilers.size() + 1),
.type = entityType,
});
_spoilerIndex = _spoilers.size();
_startedEntities[entityEnd].emplace_back(_spoilerIndex, false);
}
++_waitingEntity;
@ -738,6 +797,24 @@ bool Parser::readCommand() {
_lnkIndex = kStringLinkIndexShift + _links.size();
} break;
case TextCommandSpoiler: {
if (!_spoilerIndex) {
createBlock();
_spoilers.push_back(EntityLinkData{
.data = QString::number(_spoilers.size() + 1),
.type = EntityType::Spoiler,
});
_spoilerIndex = _spoilers.size();
}
} break;
case TextCommandNoSpoiler:
if (_spoilerIndex == _spoilers.size()) {
createBlock();
_spoilerIndex = 0;
}
break;
case TextCommandSkipBlock:
createSkipBlock(_ptr->unicode(), (_ptr + 1)->unicode());
break;
@ -920,7 +997,16 @@ void Parser::checkForElidedSkipBlock() {
void Parser::finalize(const TextParseOptions &options) {
_t->_links.resize(_maxLnkIndex);
_t->_spoilers.resize(_maxSpoilerIndex);
for (auto &block : _t->_blocks) {
const auto spoilerIndex = block->spoilerIndex();
if (spoilerIndex) {
_t->_spoilers.resize(spoilerIndex);
const auto handler = (options.flags & TextParseLinks)
? std::make_shared<SpoilerClickHandler>()
: nullptr;
_t->setSpoiler(spoilerIndex, std::move(handler));
}
const auto shiftedIndex = block->lnkIndex();
if (shiftedIndex <= kStringLinkIndexShift) {
continue;
@ -941,6 +1027,7 @@ void Parser::finalize(const TextParseOptions &options) {
}
}
_t->_links.squeeze();
_t->_spoilers.squeeze();
_t->_blocks.squeeze();
_t->_text.squeeze();
}
@ -1593,8 +1680,14 @@ private:
TextBlockType _type = currentBlock->type();
if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest
if (_lookupLink) {
if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
? spoilerLink
: _t->_links.at(currentBlock->lnkIndex() - 1);
if (resultLink) {
_lookupResult.link = resultLink;
}
}
}
if (_type != TextBlockTSkip) {
@ -1649,29 +1742,65 @@ private:
if (rtl) {
glyphX += spacesWidth;
}
struct {
QFixed from;
QFixed to;
} fillSelect;
struct {
QFixed from;
QFixed width;
} fillSpoiler;
if (_localFrom + si.position < _selection.to) {
auto chFrom = _str + currentBlock->from();
auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
if (_localFrom + si.position >= _selection.from) { // could be without space
if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) {
fillSelectRange(x, x + si.width);
fillSelect = { x, x + si.width };
} else { // or with space
fillSelectRange(glyphX, glyphX + currentBlock->f_width());
fillSelect = { glyphX, glyphX + currentBlock->f_width() };
}
} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) {
if (rtl) { // rtl space only
fillSelectRange(x, glyphX);
fillSelect = { x, glyphX };
} else { // ltr space only
fillSelectRange(x + currentBlock->f_width(), x + si.width);
fillSelect = { x + currentBlock->f_width(), x + si.width };
}
}
}
Emoji::Draw(
*_p,
static_cast<const EmojiBlock*>(currentBlock)->_emoji,
Emoji::GetSizeNormal(),
(glyphX + st::emojiPadding).toInt(),
_y + _yDelta + emojiY);
const auto hasSpoiler = _background.color &&
(_background.inFront || _background.startMs);
if (hasSpoiler) {
fillSpoiler = { x, si.width };
}
const auto spoilerOpacity = hasSpoiler
? fillSpoilerOpacity()
: 0.;
const auto hasSelect = fillSelect.to != QFixed();
if (hasSelect) {
fillSelectRange(fillSelect.from, fillSelect.to);
}
const auto opacity = _p->opacity();
if (spoilerOpacity < 1.) {
if (hasSpoiler) {
_p->setOpacity(opacity * (1. - spoilerOpacity));
}
Emoji::Draw(
*_p,
static_cast<const EmojiBlock*>(currentBlock)->_emoji,
Emoji::GetSizeNormal(),
(glyphX + st::emojiPadding).toInt(),
_y + _yDelta + emojiY);
}
if (hasSpoiler) {
_p->setOpacity(opacity * spoilerOpacity);
fillSpoilerRange(
fillSpoiler.from,
fillSpoiler.width,
blockIndex,
currentBlock->from(),
(nextBlock ? nextBlock->from() : _t->_text.size()));
_p->setOpacity(opacity);
}
// } else if (_p && currentBlock->type() == TextBlockSkip) { // debug
// _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast<SkipBlock*>(currentBlock)->height()), QColor(0, 0, 0, 32));
}
@ -1699,8 +1828,14 @@ private:
if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest
if (_lookupLink) {
if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
? spoilerLink
: _t->_links.at(currentBlock->lnkIndex() - 1);
if (resultLink) {
_lookupResult.link = resultLink;
}
}
}
_lookupResult.uponSymbol = true;
@ -1801,29 +1936,50 @@ private:
selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight);
fillSelectRange(selX, selX + selWidth);
}
if (Q_UNLIKELY(hasSelected)) {
if (Q_UNLIKELY(hasNotSelected)) {
auto clippingEnabled = _p->hasClipping();
auto clippingRegion = _p->clipRegion();
_p->setClipRect(selectedRect, Qt::IntersectClip);
_p->setPen(*_currentPenSelected);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect((_x - _w).toInt(), _y - _lineHeight, (_x + 2 * _w).toInt(), _y + 2 * _lineHeight));
_p->setClipRegion(externalClipping - selectedRect);
_p->setPen(*_currentPen);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
if (clippingEnabled) {
_p->setClipRegion(clippingRegion);
const auto hasSpoiler = (_background.inFront || _background.startMs);
const auto spoilerOpacity = hasSpoiler
? fillSpoilerOpacity()
: 0.;
const auto opacity = _p->opacity();
if (spoilerOpacity < 1.) {
if (hasSpoiler) {
_p->setOpacity(opacity * (1. - spoilerOpacity));
}
if (Q_UNLIKELY(hasSelected)) {
if (Q_UNLIKELY(hasNotSelected)) {
auto clippingEnabled = _p->hasClipping();
auto clippingRegion = _p->clipRegion();
_p->setClipRect(selectedRect, Qt::IntersectClip);
_p->setPen(*_currentPenSelected);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
auto externalClipping = clippingEnabled ? clippingRegion : QRegion(QRect((_x - _w).toInt(), _y - _lineHeight, (_x + 2 * _w).toInt(), _y + 2 * _lineHeight));
_p->setClipRegion(externalClipping - selectedRect);
_p->setPen(*_currentPen);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
if (clippingEnabled) {
_p->setClipRegion(clippingRegion);
} else {
_p->setClipping(false);
}
} else {
_p->setClipping(false);
_p->setPen(*_currentPenSelected);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
}
} else {
_p->setPen(*_currentPenSelected);
_p->setPen(*_currentPen);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
}
} else {
_p->setPen(*_currentPen);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
}
if (hasSpoiler) {
_p->setOpacity(opacity * spoilerOpacity);
fillSpoilerRange(
x,
itemWidth,
blockIndex,
_localFrom + itemStart,
_localFrom + itemEnd);
_p->setOpacity(opacity);
}
}
@ -1837,6 +1993,91 @@ private:
_p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg);
}
float fillSpoilerOpacity() {
if (!_background.startMs) {
return 1.;
}
const auto progress = float64(crl::now() - _background.startMs)
/ st::fadeWrapDuration;
if ((progress > 1.) && _background.spoilerIndex) {
const auto link = _t->_spoilers.at(_background.spoilerIndex - 1);
if (link) {
link->setStartMs(0);
}
}
return (1. - std::min(progress, 1.));
}
void fillSpoilerRange(
QFixed x,
QFixed width,
int currentBlockIndex,
int positionFrom,
int positionTill) {
if (!_background.color) {
return;
}
const auto elideOffset = (_indexOfElidedBlock == currentBlockIndex)
? (_elideRemoveFromEnd + _f->elidew)
: 0;
const auto &cache = _background.inFront
? _t->_spoilerCache
: _t->_spoilerShownCache;
const auto cornerWidth = cache.corners[0].width()
/ style::DevicePixelRatio();
const auto useWidth = (x + width).toInt() - x.toInt();
const auto rect = QRect(
x.toInt(),
_y + _yDelta,
std::max(useWidth - elideOffset, cornerWidth * 2),
_fontHeight);
if (!rect.isValid()) {
return;
}
const auto parts = [&] {
const auto blockIndex = currentBlockIndex - 1;
const auto block = _t->_blocks[blockIndex].get();
const auto nextBlock = (blockIndex + 1 < _t->_blocks.size())
? _t->_blocks[blockIndex + 1].get()
: nullptr;
const auto blockEnd = nextBlock ? nextBlock->from() : _t->_text.size();
const auto now = block->spoilerIndex();
const auto was = (positionFrom > block->from())
? now
: (blockIndex > 0)
? _t->_blocks[blockIndex - 1]->spoilerIndex()
: 0;
const auto will = (positionTill < blockEnd)
? now
: nextBlock
? nextBlock->spoilerIndex()
: 0;
return RectPart::None
| ((now != was) ? (RectPart::FullLeft) : RectPart::None)
| ((now != will) ? (RectPart::FullRight) : RectPart::None);
}();
if (parts != RectPart::None) {
DrawRoundedRect(
*_p,
rect,
*_background.color,
cache.corners,
parts);
}
const auto hasLeft = (parts & RectPart::Left) != 0;
const auto hasRight = (parts & RectPart::Right) != 0;
_p->fillRect(
rect.left() + (hasLeft ? cornerWidth : 0),
rect.top(),
rect.width()
- (hasRight ? cornerWidth : 0)
- (hasLeft ? cornerWidth : 0),
rect.height(),
*_background.color);
}
void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) {
if (_elideSavedBlock) {
restoreAfterElided();
@ -1845,7 +2086,7 @@ private:
_elideSavedIndex = blockIndex;
auto mutableText = const_cast<String*>(_t);
_elideSavedBlock = std::move(mutableText->_blocks[blockIndex]);
mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex());
mutableText->_blocks[blockIndex] = Block::Text(_t->_st->font, _t->_text, QFIXED_MAX, elideStart, 0, (*_elideSavedBlock)->flags(), (*_elideSavedBlock)->lnkIndex(), (*_elideSavedBlock)->spoilerIndex());
_blocksSize = blockIndex + 1;
_endBlock = (blockIndex + 1 < _t->_blocks.size() ? _t->_blocks[blockIndex + 1].get() : nullptr);
}
@ -1861,8 +2102,6 @@ private:
}
void prepareElidedLine(QString &lineText, int32 lineStart, int32 &lineLength, const AbstractBlock *&_endBlock, int repeat = 0) {
static const auto _Elide = QString::fromLatin1("...");
_f = _t->_st->font;
QStackTextEngine engine(lineText, _f->f);
engine.option.setTextDirection(_parDirection);
@ -1899,10 +2138,11 @@ private:
}
if (_type == TextBlockTEmoji || _type == TextBlockTSkip || _type == TextBlockTNewline) {
if (_wLeft < si.width) {
lineText = lineText.mid(0, currentBlock->from() - _localFrom) + _Elide;
lineLength = currentBlock->from() + _Elide.size() - _lineStart;
lineText = lineText.mid(0, currentBlock->from() - _localFrom) + kQEllipsis;
lineLength = currentBlock->from() + kQEllipsis.size() - _lineStart;
_selection.to = qMin(_selection.to, currentBlock->from());
setElideBidi(currentBlock->from(), _Elide.size());
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(currentBlock->from(), kQEllipsis.size());
elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth);
return;
}
@ -1931,10 +2171,11 @@ private:
}
if (lineText.size() <= pos || repeat > 3) {
lineText += _Elide;
lineLength = _localFrom + pos + _Elide.size() - _lineStart;
lineText += kQEllipsis;
lineLength = _localFrom + pos + kQEllipsis.size() - _lineStart;
_selection.to = qMin(_selection.to, uint16(_localFrom + pos));
setElideBidi(_localFrom + pos, _Elide.size());
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(_localFrom + pos, kQEllipsis.size());
_blocksSize = blockIndex;
_endBlock = nextBlock;
} else {
@ -1954,10 +2195,11 @@ private:
int32 elideStart = _localFrom + lineText.size();
_selection.to = qMin(_selection.to, uint16(elideStart));
setElideBidi(elideStart, _Elide.size());
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(elideStart, kQEllipsis.size());
lineText += _Elide;
lineLength += _Elide.size();
lineText += kQEllipsis;
lineLength += kQEllipsis.size();
if (!repeat) {
for (; blockIndex < _blocksSize && _t->_blocks[blockIndex].get() != _endBlock && _t->_blocks[blockIndex]->from() < elideStart; ++blockIndex) {
@ -2613,6 +2855,33 @@ private:
void applyBlockProperties(const AbstractBlock *block) {
eSetFont(block);
if (_p) {
if (block->spoilerIndex()) {
const auto handler
= _t->_spoilers.at(block->spoilerIndex() - 1);
const auto inBack = (handler && handler->shown());
_background.inFront = !inBack;
_background.color = inBack
? &_textPalette->spoilerActiveBg
: &_textPalette->spoilerBg;
_background.startMs = handler ? handler->startMs() : 0;
_background.spoilerIndex = block->spoilerIndex();
const auto &cache = _background.inFront
? _t->_spoilerCache
: _t->_spoilerShownCache;
if (cache.color != (*_background.color)->c) {
auto mutableText = const_cast<String*>(_t);
auto &mutableCache = _background.inFront
? mutableText->_spoilerCache
: mutableText->_spoilerShownCache;
mutableCache.corners = Images::PrepareCorners(
ImageRoundRadius::Small,
*_background.color);
mutableCache.color = (*_background.color)->c;
}
} else {
_background = {};
}
if (block->lnkIndex()) {
_currentPen = &_textPalette->linkFg->p;
_currentPenSelected = &_textPalette->selectLinkFg->p;
@ -2637,6 +2906,12 @@ private:
QPen _originalPenSelected;
const QPen *_currentPen = nullptr;
const QPen *_currentPenSelected = nullptr;
struct {
const style::color *color = nullptr;
bool inFront = false;
crl::time startMs = 0;
uint16 spoilerIndex = 0;
} _background;
int _yFrom = 0;
int _yTo = 0;
int _yToElide = 0;
@ -2644,6 +2919,8 @@ private:
bool _fullWidthSelection = true;
const QChar *_str = nullptr;
int _indexOfElidedBlock = -1; // For spoilers.
// current paragraph data
String::TextBlocks::const_iterator _parStartBlock;
Qt::LayoutDirection _parDirection;
@ -2867,10 +3144,32 @@ void String::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
_links[lnkIndex - 1] = lnk;
}
void String::setSpoiler(
uint16 lnkIndex,
const std::shared_ptr<SpoilerClickHandler> &lnk) {
if (!lnkIndex || lnkIndex > _spoilers.size()) {
return;
}
_spoilers[lnkIndex - 1] = lnk;
}
void String::setSpoilerShown(uint16 lnkIndex, bool shown) {
if (!lnkIndex
|| (lnkIndex > _spoilers.size())
|| !_spoilers[lnkIndex - 1]) {
return;
}
_spoilers[lnkIndex - 1]->setShown(shown);
}
bool String::hasLinks() const {
return !_links.isEmpty();
}
int String::spoilersCount() const {
return _spoilers.size();
}
bool String::hasSkipBlock() const {
return _blocks.empty() ? false : _blocks.back()->type() == TextBlockTSkip;
}
@ -2891,6 +3190,7 @@ bool String::updateSkipBlock(int width, int height) {
_text.size() - 1,
width,
height,
0,
0));
recountNaturalSize(false);
return true;
@ -3153,9 +3453,14 @@ void String::enumerateText(TextSelection selection, AppendPartCallback appendPar
int lnkIndex = 0;
uint16 lnkFrom = 0;
int spoilerIndex = 0;
uint16 spoilerFrom = 0;
int32 flags = 0;
for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) {
int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex();
int blockSpoilerIndex = (i == e) ? 0 : (*i)->spoilerIndex();
uint16 blockFrom = (i == e) ? _text.size() : (*i)->from();
int32 blockFlags = (i == e) ? 0 : (*i)->flags();
@ -3168,18 +3473,43 @@ void String::enumerateText(TextSelection selection, AppendPartCallback appendPar
auto rangeTo = qMin(selection.to, blockFrom);
if (rangeTo > rangeFrom) { // handle click handler
const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
if (lnkFrom != rangeFrom || blockFrom != rangeTo) {
// Ignore links that are partially copied.
clickHandlerFinishCallback(r, nullptr);
} else {
clickHandlerFinishCallback(r, _links.at(lnkIndex - 1));
}
// Ignore links that are partially copied.
const auto handler = (lnkFrom != rangeFrom || blockFrom != rangeTo)
? nullptr
: _links.at(lnkIndex - 1);
const auto type = handler
? handler->getTextEntity().type
: EntityType::Invalid;
clickHandlerFinishCallback(r, handler, type);
}
}
lnkIndex = blockLnkIndex;
if (lnkIndex) {
lnkFrom = blockFrom;
clickHandlerStartCallback();
const auto handler = _links.at(lnkIndex - 1);
clickHandlerStartCallback(handler
? handler->getTextEntity().type
: EntityType::Invalid);
}
}
if (blockSpoilerIndex != spoilerIndex) {
if (spoilerIndex) {
auto rangeFrom = qMax(selection.from, spoilerFrom);
auto rangeTo = qMin(selection.to, blockFrom);
if (rangeTo > rangeFrom) { // handle click handler
const auto r = base::StringViewMid(_text, rangeFrom, rangeTo - rangeFrom);
// Ignore links that are partially copied.
const auto handler = (spoilerFrom != rangeFrom || blockFrom != rangeTo)
? nullptr
: _spoilers.at(spoilerIndex - 1);
const auto type = EntityType::Spoiler;
clickHandlerFinishCallback(r, handler, type);
}
}
spoilerIndex = blockSpoilerIndex;
if (spoilerIndex) {
spoilerFrom = blockFrom;
clickHandlerStartCallback(EntityType::Spoiler);
}
}
@ -3241,6 +3571,7 @@ TextForMimeData String::toText(
result.rich.entities.insert(i, std::move(entity));
};
auto linkStart = 0;
auto spoilerStart = 0;
auto markdownTrackers = composeEntities
? std::vector<MarkdownTagTracker>{
{ TextBlockFItalic, EntityType::Italic },
@ -3249,7 +3580,7 @@ TextForMimeData String::toText(
{ TextBlockFUnderline, EntityType::Underline },
{ TextBlockFStrikeOut, EntityType::StrikeOut },
{ TextBlockFCode, EntityType::Code }, // #TODO entities
{ TextBlockFPre, EntityType::Pre }
{ TextBlockFPre, EntityType::Pre },
} : std::vector<MarkdownTagTracker>();
const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) {
if (!composeEntities) {
@ -3267,12 +3598,26 @@ TextForMimeData String::toText(
}
}
};
const auto clickHandlerStartCallback = [&] {
linkStart = result.rich.text.size();
const auto clickHandlerStartCallback = [&](EntityType type) {
if (type == EntityType::Spoiler) {
spoilerStart = result.rich.text.size();
} else {
linkStart = result.rich.text.size();
}
};
const auto clickHandlerFinishCallback = [&](
QStringView inText,
const ClickHandlerPtr &handler) {
const ClickHandlerPtr &handler,
EntityType type) {
if (type == EntityType::Spoiler) {
insertEntity({
type,
spoilerStart,
int(result.rich.text.size() - spoilerStart),
QString(),
});
return;
}
if (!handler || (!composeExpanded && !composeEntities)) {
return;
}
@ -3344,7 +3689,8 @@ IsolatedEmoji String::toIsolatedEmoji() const {
auto result = IsolatedEmoji();
const auto skip = (_blocks.empty()
|| _blocks.back()->type() != TextBlockTSkip) ? 0 : 1;
if (_blocks.size() > kIsolatedEmojiLimit + skip) {
if ((_blocks.size() > kIsolatedEmojiLimit + skip)
|| !_spoilers.empty()) {
return IsolatedEmoji();
}
auto index = 0;
@ -3369,10 +3715,19 @@ void String::clear() {
void String::clearFields() {
_blocks.clear();
_links.clear();
_spoilers.clear();
_maxWidth = _minHeight = 0;
_startDir = Qt::LayoutDirectionAuto;
}
ClickHandlerPtr String::spoilerLink(uint16 spoilerIndex) const {
if (spoilerIndex) {
const auto &handler = _spoilers.at(spoilerIndex - 1);
return (handler && !handler->shown()) ? handler : nullptr;
}
return nullptr;
}
bool IsWordSeparator(QChar ch) {
switch (ch.unicode()) {
case QChar::Space:

View file

@ -15,6 +15,12 @@
#include <private/qfixed_p.h>
#include <any>
class SpoilerClickHandler;
namespace Ui {
static const auto kQEllipsis = QStringLiteral("...");
} // namespace Ui
static const QChar TextCommand(0x0010);
enum TextCommands {
TextCommandBold = 0x01,
@ -30,6 +36,8 @@ enum TextCommands {
TextCommandLinkIndex = 0x0B, // 0 - NoLink
TextCommandLinkText = 0x0C,
TextCommandSkipBlock = 0x0D,
TextCommandSpoiler = 0x0E,
TextCommandNoSpoiler = 0x0F,
TextCommandLangTag = 0x20,
};
@ -131,6 +139,11 @@ public:
void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk);
bool hasLinks() const;
void setSpoiler(
uint16 lnkIndex,
const std::shared_ptr<SpoilerClickHandler> &lnk);
void setSpoilerShown(uint16 lnkIndex, bool shown);
int spoilersCount() const;
bool hasSkipBlock() const;
bool updateSkipBlock(int width, int height);
@ -205,6 +218,8 @@ private:
// it is also called from move constructor / assignment operator
void clearFields();
ClickHandlerPtr spoilerLink(uint16 spoilerIndex) const;
TextForMimeData toText(
TextSelection selection,
bool composeExpanded,
@ -220,8 +235,15 @@ private:
TextBlocks _blocks;
TextLinks _links;
QVector<std::shared_ptr<SpoilerClickHandler>> _spoilers;
Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto;
struct {
std::array<QImage, 4> corners;
QColor color;
} _spoilerCache, _spoilerShownCache;
friend class Parser;
friend class Renderer;
@ -264,4 +286,8 @@ QString textcmdLink(ushort lnkIndex, const QString &text);
QString textcmdLink(const QString &url, const QString &text);
QString textcmdStartSemibold();
QString textcmdStopSemibold();
QString textcmdStartSpoiler();
QString textcmdStopSpoiler();
const QChar *textSkipCommand(const QChar *from, const QChar *end, bool canLink = true);

View file

@ -340,6 +340,63 @@ bool BlockParser::isLineBreak(
return lineBreak;
}
AbstractBlock::AbstractBlock(
const style::font &font,
const QString &str,
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex,
uint16 spoilerIndex)
: _from(from)
, _flags((flags & 0xFF) | ((lnkIndex & 0xFFFF) << 12))
, _spoilerIndex(spoilerIndex) {
}
uint16 AbstractBlock::from() const {
return _from;
}
int AbstractBlock::width() const {
return _width.toInt();
}
int AbstractBlock::rpadding() const {
return _rpadding.toInt();
}
QFixed AbstractBlock::f_width() const {
return _width;
}
QFixed AbstractBlock::f_rpadding() const {
return _rpadding;
}
uint16 AbstractBlock::lnkIndex() const {
return (_flags >> 12) & 0xFFFF;
}
void AbstractBlock::setLnkIndex(uint16 lnkIndex) {
_flags = (_flags & ~(0xFFFF << 12)) | (lnkIndex << 12);
}
uint16 AbstractBlock::spoilerIndex() const {
return _spoilerIndex;
}
void AbstractBlock::setSpoilerIndex(uint16 spoilerIndex) {
_spoilerIndex = spoilerIndex;
}
TextBlockType AbstractBlock::type() const {
return TextBlockType((_flags >> 8) & 0x0F);
}
int32 AbstractBlock::flags() const {
return (_flags & 0xFFF);
}
QFixed AbstractBlock::f_rbearing() const {
return (type() == TextBlockTText)
? static_cast<const TextBlock*>(this)->real_f_rbearing()
@ -353,8 +410,9 @@ TextBlock::TextBlock(
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex)
: AbstractBlock(font, str, from, length, flags, lnkIndex) {
uint16 lnkIndex,
uint16 spoilerIndex)
: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) {
_flags |= ((TextBlockTText & 0x0F) << 8);
if (length) {
style::font blockFont = font;
@ -395,8 +453,9 @@ EmojiBlock::EmojiBlock(
uint16 length,
uchar flags,
uint16 lnkIndex,
uint16 spoilerIndex,
EmojiPtr emoji)
: AbstractBlock(font, str, from, length, flags, lnkIndex)
: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex)
, _emoji(emoji) {
_flags |= ((TextBlockTEmoji & 0x0F) << 8);
_width = int(st::emojiSize + 2 * st::emojiPadding);

View file

@ -34,38 +34,23 @@ enum TextBlockFlags {
class AbstractBlock {
public:
uint16 from() const {
return _from;
}
int width() const {
return _width.toInt();
}
int rpadding() const {
return _rpadding.toInt();
}
QFixed f_width() const {
return _width;
}
QFixed f_rpadding() const {
return _rpadding;
}
uint16 from() const;
int width() const;
int rpadding() const;
QFixed f_width() const;
QFixed f_rpadding() const;
// Should be virtual, but optimized throught type() call.
QFixed f_rbearing() const;
uint16 lnkIndex() const {
return (_flags >> 12) & 0xFFFF;
}
void setLnkIndex(uint16 lnkIndex) {
_flags = (_flags & ~(0xFFFF << 12)) | (lnkIndex << 12);
}
uint16 lnkIndex() const;
void setLnkIndex(uint16 lnkIndex);
TextBlockType type() const {
return TextBlockType((_flags >> 8) & 0x0F);
}
int32 flags() const {
return (_flags & 0xFF);
}
uint16 spoilerIndex() const;
void setSpoilerIndex(uint16 spoilerIndex);
TextBlockType type() const;
int32 flags() const;
protected:
AbstractBlock(
@ -74,15 +59,15 @@ protected:
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex)
: _from(from)
, _flags((flags & 0xFF) | ((lnkIndex & 0xFFFF) << 12)) {
}
uint16 lnkIndex,
uint16 spoilerIndex);
uint16 _from = 0;
uint32 _flags = 0; // 4 bits empty, 16 bits lnkIndex, 4 bits type, 8 bits flags
uint16 _spoilerIndex = 0;
QFixed _width = 0;
// Right padding: spaces after the last content of the block (like a word).
@ -102,8 +87,9 @@ public:
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex)
: AbstractBlock(font, str, from, length, flags, lnkIndex) {
uint16 lnkIndex,
uint16 spoilerIndex)
: AbstractBlock(font, str, from, length, flags, lnkIndex, spoilerIndex) {
_flags |= ((TextBlockTNewline & 0x0F) << 8);
}
@ -163,7 +149,8 @@ public:
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex);
uint16 lnkIndex,
uint16 spoilerIndex);
private:
QFixed real_f_rbearing() const {
@ -189,6 +176,7 @@ public:
uint16 length,
uchar flags,
uint16 lnkIndex,
uint16 spoilerIndex,
EmojiPtr emoji);
private:
@ -208,8 +196,9 @@ public:
uint16 from,
int32 w,
int32 h,
uint16 lnkIndex)
: AbstractBlock(font, str, from, 1, 0, lnkIndex)
uint16 lnkIndex,
uint16 spoilerIndex)
: AbstractBlock(font, str, from, 1, 0, lnkIndex, spoilerIndex)
, _height(h) {
_flags |= ((TextBlockTSkip & 0x0F) << 8);
_width = w;
@ -325,8 +314,9 @@ public:
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex) {
return New<NewlineBlock>(font, str, from, length, flags, lnkIndex);
uint16 lnkIndex,
uint16 spoilerIndex) {
return New<NewlineBlock>(font, str, from, length, flags, lnkIndex, spoilerIndex);
}
[[nodiscard]] static Block Text(
@ -336,7 +326,8 @@ public:
uint16 from,
uint16 length,
uchar flags,
uint16 lnkIndex) {
uint16 lnkIndex,
uint16 spoilerIndex) {
return New<TextBlock>(
font,
str,
@ -344,7 +335,8 @@ public:
from,
length,
flags,
lnkIndex);
lnkIndex,
spoilerIndex);
}
[[nodiscard]] static Block Emoji(
@ -354,6 +346,7 @@ public:
uint16 length,
uchar flags,
uint16 lnkIndex,
uint16 spoilerIndex,
EmojiPtr emoji) {
return New<EmojiBlock>(
font,
@ -362,6 +355,7 @@ public:
length,
flags,
lnkIndex,
spoilerIndex,
emoji);
}
@ -371,8 +365,9 @@ public:
uint16 from,
int32 w,
int32 h,
uint16 lnkIndex) {
return New<SkipBlock>(font, str, from, w, h, lnkIndex);
uint16 lnkIndex,
uint16 spoilerIndex) {
return New<SkipBlock>(font, str, from, w, h, lnkIndex, spoilerIndex);
}
template <typename FinalBlock>

View file

@ -22,6 +22,8 @@
namespace TextUtilities {
namespace {
constexpr auto kTagSeparator = '$';
using namespace Ui::Text;
QString ExpressionMailNameAtEnd() {
@ -68,6 +70,10 @@ QString SeparatorsMono() {
return Separators(QString::fromUtf8("*~/"));
}
QString SeparatorsSpoiler() {
return Separators(QString::fromUtf8("|*~/"));
}
QString ExpressionHashtag() {
return QString::fromUtf8("(^|[") + ExpressionSeparators(QString::fromUtf8("`\\*/")) + QString::fromUtf8("])#[\\w]{2,64}([\\W]|$)");
}
@ -1269,6 +1275,14 @@ QString MarkdownPreBadAfter() {
return QString::fromLatin1("`");
}
QString MarkdownSpoilerGoodBefore() {
return SeparatorsSpoiler();
}
QString MarkdownSpoilerBadAfter() {
return QString::fromLatin1("|");
}
bool IsValidProtocol(const QString &protocol) {
static const auto list = CreateValidProtocols();
return list.contains(base::crc32(protocol.constData(), protocol.size() * sizeof(QChar)));
@ -1279,9 +1293,17 @@ bool IsValidTopDomain(const QString &protocol) {
return list.contains(base::crc32(protocol.constData(), protocol.size() * sizeof(QChar)));
}
QString Clean(const QString &text) {
QString Clean(const QString &text, bool keepSpoilers) {
auto result = text;
for (auto s = text.unicode(), ch = s, e = text.unicode() + text.size(); ch != e; ++ch) {
if (keepSpoilers && (*ch == TextCommand)) {
if ((*(ch + 1) == TextCommandSpoiler)
|| (*(ch - 1) == TextCommandSpoiler)
|| (*(ch + 1) == TextCommandNoSpoiler)
|| (*(ch - 1) == TextCommandNoSpoiler)) {
continue;
}
}
if (*ch == TextCommand) {
result[int(ch - s)] = QChar::Space;
}
@ -2041,17 +2063,21 @@ QString JoinTag(const QList<QStringView> &list) {
result.append(list.front());
for (auto i = 1, count = int(list.size()); i != count; ++i) {
if (!IsSeparateTag(list[i])) {
result.append('|').append(list[i]);
result.append(kTagSeparator).append(list[i]);
}
}
return result;
}
QList<QStringView> SplitTags(const QString &tag) {
return QStringView(tag).split(kTagSeparator);
}
QString TagWithRemoved(const QString &tag, const QString &removed) {
if (tag == removed) {
return QString();
}
auto list = QStringView(tag).split('|');
auto list = SplitTags(tag);
list.erase(ranges::remove(list, QStringView(removed)), list.end());
return JoinTag(list);
}
@ -2060,7 +2086,7 @@ QString TagWithAdded(const QString &tag, const QString &added) {
if (tag.isEmpty() || tag == added) {
return added;
}
auto list = QStringView(tag).split('|');
auto list = SplitTags(tag);
const auto ref = QStringView(added);
if (list.contains(ref)) {
return tag;
@ -2081,6 +2107,7 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
EntityType::Italic,
EntityType::Underline,
EntityType::StrikeOut,
EntityType::Spoiler,
EntityType::Code,
EntityType::Pre,
};
@ -2173,7 +2200,7 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
};
const auto stateForTag = [&](const QString &tag) {
auto result = State();
const auto list = QStringView(tag).split('|');
const auto list = SplitTags(tag);
for (const auto &single : list) {
if (single == Ui::InputField::kTagBold) {
result.set(EntityType::Bold);
@ -2187,6 +2214,8 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
result.set(EntityType::Code);
} else if (single == Ui::InputField::kTagPre) {
result.set(EntityType::Pre);
} else if (single == Ui::InputField::kTagSpoiler) {
result.set(EntityType::Spoiler);
} else {
result.link = single.toString();
}
@ -2275,6 +2304,7 @@ TextWithTags::Tags ConvertEntitiesToTextTags(
break;
case EntityType::Code: push(Ui::InputField::kTagCode); break; // #TODO entities
case EntityType::Pre: push(Ui::InputField::kTagPre); break;
case EntityType::Spoiler: push(Ui::InputField::kTagSpoiler); break;
}
}
if (!toRemove.empty()) {
@ -2302,6 +2332,36 @@ void SetClipboardText(
}
}
QString TextWithSpoilerCommands(const TextWithEntities &textWithEntities) {
auto text = textWithEntities.text;
auto offset = 0;
const auto start = textcmdStartSpoiler();
const auto stop = textcmdStopSpoiler();
for (const auto &e : textWithEntities.entities) {
if (e.type() == EntityType::Spoiler) {
text.insert(e.offset() + offset, start);
offset += start.size();
text.insert(e.offset() + e.length() + offset, stop);
offset += stop.size();
}
}
return text;
}
QString CutTextWithCommands(
QString text,
int length,
const QString &start,
const QString &stop) {
text = text.mid(0, length);
const auto lastStart = text.lastIndexOf(start);
const auto lastStop = text.lastIndexOf(stop);
const auto additional = ((lastStart == -1) || (lastStart < lastStop))
? QString()
: stop;
return text + additional + Ui::kQEllipsis;
}
} // namespace TextUtilities
EntityInText::EntityInText(

View file

@ -33,6 +33,7 @@ enum class EntityType : uchar {
StrikeOut,
Code, // inline
Pre, // block
Spoiler,
};
enum class EntityLinkShown : uchar {
@ -293,9 +294,11 @@ QString MarkdownCodeGoodBefore();
QString MarkdownCodeBadAfter();
QString MarkdownPreGoodBefore();
QString MarkdownPreBadAfter();
QString MarkdownSpoilerGoodBefore();
QString MarkdownSpoilerBadAfter();
// Text preprocess.
QString Clean(const QString &text);
QString Clean(const QString &text, bool keepSpoilers = false);
QString EscapeForRichParsing(const QString &text);
QString SingleLine(const QString &text);
TextWithEntities SingleLine(const TextWithEntities &text);
@ -366,6 +369,7 @@ inline const auto kMentionTagStart = qstr("mention://user.");
[[nodiscard]] bool IsMentionLink(QStringView link);
[[nodiscard]] bool IsSeparateTag(QStringView tag);
[[nodiscard]] QString JoinTag(const QList<QStringView> &list);
[[nodiscard]] QList<QStringView> SplitTags(const QString &tag);
[[nodiscard]] QString TagWithRemoved(
const QString &tag,
const QString &removed);
@ -380,4 +384,12 @@ void SetClipboardText(
const TextForMimeData &text,
QClipboard::Mode mode = QClipboard::Clipboard);
[[nodiscard]] QString TextWithSpoilerCommands(
const TextWithEntities &textWithEntities);
[[nodiscard]] QString CutTextWithCommands(
QString text,
int length,
const QString &start,
const QString &stop);
} // namespace TextUtilities

View file

@ -380,7 +380,22 @@ Checkbox::Checkbox(
const style::Check &checkSt)
: Checkbox(
parent,
text,
rpl::single(text) | rpl::map(TextWithEntities::Simple),
st,
std::make_unique<CheckView>(
checkSt,
checked)) {
}
Checkbox::Checkbox(
QWidget *parent,
const TextWithEntities &text,
bool checked,
const style::Checkbox &st,
const style::Check &checkSt)
: Checkbox(
parent,
rpl::single(text),
st,
std::make_unique<CheckView>(
checkSt,
@ -395,7 +410,7 @@ Checkbox::Checkbox(
const style::Toggle &toggleSt)
: Checkbox(
parent,
rpl::single(text),
rpl::single(text) | rpl::map(TextWithEntities::Simple),
st,
std::make_unique<ToggleView>(
toggleSt,
@ -410,7 +425,7 @@ Checkbox::Checkbox(
const style::Check &checkSt)
: Checkbox(
parent,
std::move(text),
std::move(text) | rpl::map(TextWithEntities::Simple),
st,
std::make_unique<CheckView>(
checkSt,
@ -425,7 +440,7 @@ Checkbox::Checkbox(
const style::Toggle &toggleSt)
: Checkbox(
parent,
std::move(text),
std::move(text) | rpl::map(TextWithEntities::Simple),
st,
std::make_unique<ToggleView>(
toggleSt,
@ -439,14 +454,14 @@ Checkbox::Checkbox(
std::unique_ptr<AbstractCheckView> check)
: Checkbox(
parent,
rpl::single(text),
rpl::single(text) | rpl::map(TextWithEntities::Simple),
st,
std::move(check)) {
}
Checkbox::Checkbox(
QWidget *parent,
rpl::producer<QString> &&text,
rpl::producer<TextWithEntities> &&text,
const style::Checkbox &st,
std::unique_ptr<AbstractCheckView> check)
: RippleButton(parent, st.ripple)
@ -462,8 +477,17 @@ Checkbox::Checkbox(
setCursor(style::cur_pointer);
std::move(
text
) | rpl::start_with_next([=](QString &&value) {
setText(std::move(value));
) | rpl::start_with_next([=](TextWithEntities &&value) {
if (value.entities.empty()) {
setText(base::take(value.text));
} else {
_text.setMarkedText(
_st.style,
std::move(value),
_checkboxRichOptions);
resizeToText();
update();
}
}, lifetime());
}

View file

@ -140,6 +140,12 @@ public:
bool checked = false,
const style::Checkbox &st = st::defaultCheckbox,
const style::Check &checkSt = st::defaultCheck);
Checkbox(
QWidget *parent,
const TextWithEntities &text,
bool checked = false,
const style::Checkbox &st = st::defaultCheckbox,
const style::Check &checkSt = st::defaultCheck);
Checkbox(
QWidget *parent,
const QString &text,
@ -165,7 +171,7 @@ public:
std::unique_ptr<AbstractCheckView> check);
Checkbox(
QWidget *parent,
rpl::producer<QString> &&text,
rpl::producer<TextWithEntities> &&text,
const style::Checkbox &st,
std::unique_ptr<AbstractCheckView> check);

View file

@ -46,6 +46,7 @@ const auto &kTagUnderline = InputField::kTagUnderline;
const auto &kTagStrikeOut = InputField::kTagStrikeOut;
const auto &kTagCode = InputField::kTagCode;
const auto &kTagPre = InputField::kTagPre;
const auto &kTagSpoiler = InputField::kTagSpoiler;
const auto kTagCheckLinkMeta = QString("^:/:/:^");
const auto kNewlineChars = QString("\r\n")
+ QChar(0xfdd0) // QTextBeginningOfFrame
@ -129,7 +130,7 @@ bool IsNewline(QChar ch) {
return QString();
}
auto found = false;
for (const auto &single : QStringView(existing.id).split('|')) {
for (const auto &single : TextUtilities::SplitTags(existing.id)) {
const auto normalized = (single == QStringView(kTagPre))
? QStringView(kTagCode)
: single;
@ -234,6 +235,7 @@ constexpr auto kTagItalicIndex = 1;
constexpr auto kTagStrikeOutIndex = 2;
constexpr auto kTagCodeIndex = 3;
constexpr auto kTagPreIndex = 4;
constexpr auto kTagSpoilerIndex = 5;
constexpr auto kInvalidPosition = std::numeric_limits<int>::max() / 2;
class TagSearchItem {
@ -377,6 +379,13 @@ const std::vector<TagStartExpression> &TagStartExpressions() {
TextUtilities::MarkdownPreBadAfter(),
TextUtilities::MarkdownPreGoodBefore()
},
{
kTagSpoiler,
TextUtilities::MarkdownSpoilerGoodBefore(),
TextUtilities::MarkdownSpoilerBadAfter(),
TextUtilities::MarkdownSpoilerBadAfter(),
TextUtilities::MarkdownSpoilerGoodBefore()
},
};
return cached;
}
@ -389,6 +398,7 @@ const std::map<QString, int> &TagIndices() {
{ kTagStrikeOut, kTagStrikeOutIndex },
{ kTagCode, kTagCodeIndex },
{ kTagPre, kTagPreIndex },
{ kTagSpoiler, kTagSpoilerIndex },
};
return cached;
}
@ -706,6 +716,7 @@ QTextCharFormat PrepareTagFormat(
auto result = QTextCharFormat();
auto font = st.font;
auto color = std::optional<style::color>();
auto bg = std::optional<style::color>();
const auto applyOne = [&](QStringView tag) {
if (IsValidMarkdownLink(tag)) {
color = st::defaultTextPalette.linkFg;
@ -720,14 +731,19 @@ QTextCharFormat PrepareTagFormat(
} else if (tag == kTagCode || tag == kTagPre) {
color = st::defaultTextPalette.monoFg;
font = font->monospace();
} else if (tag == kTagSpoiler) {
bg = st::defaultTextPalette.spoilerActiveBg;
}
};
for (const auto &tag : QStringView(tag).split('|')) {
for (const auto &tag : TextUtilities::SplitTags(tag)) {
applyOne(tag);
}
result.setFont(font);
result.setForeground(color.value_or(st.textFg));
result.setProperty(kTagProperty, tag);
if (bg) {
result.setBackground(*bg);
}
return result;
}
@ -850,6 +866,7 @@ 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("||");
class InputField::Inner final : public QTextEdit {
public:
@ -1185,8 +1202,9 @@ void FlatInput::refreshPlaceholder(const QString &text) {
}
void FlatInput::contextMenuEvent(QContextMenuEvent *e) {
if (auto menu = createStandardContextMenu()) {
(new PopupMenu(this, menu))->popup(e->globalPos());
if (const auto menu = createStandardContextMenu()) {
_contextMenu = base::make_unique_q<PopupMenu>(this, menu);
_contextMenu->popup(e->globalPos());
}
}
@ -2869,6 +2887,8 @@ bool InputField::handleMarkdownKey(QKeyEvent *e) {
toggleSelectionMarkdown(kTagStrikeOut);
} else if (matches(kMonospaceSequence)) {
toggleSelectionMarkdown(kTagCode);
} else if (matches(kSpoilerSequence)) {
toggleSelectionMarkdown(kTagSpoiler);
} else if (matches(kClearFormatSequence)) {
clearSelectionMarkdown();
} else if (matches(kEditLinkSequence) && _editLinkCallback) {
@ -2953,8 +2973,8 @@ auto InputField::selectionEditLinkData(EditLinkSelection selection) const
};
const auto stateTagHasLink = [&](const State &state) {
const auto tag = stateTag(state);
return (tag == link) || QStringView(tag).split('|').contains(
QStringView(link));
return (tag == link)
|| TextUtilities::SplitTags(tag).contains(QStringView(link));
};
const auto stateStart = [&](const State &state) {
return state.i.fragment().position();
@ -3162,7 +3182,7 @@ void InputField::commitInstantReplacement(
const auto currentTag = cursor.charFormat().property(
kTagProperty
).toString();
const auto currentTags = QStringView(currentTag).split('|');
const auto currentTags = TextUtilities::SplitTags(currentTag);
if (currentTags.contains(QStringView(kTagPre))
|| currentTags.contains(QStringView(kTagCode))) {
return;
@ -3628,6 +3648,7 @@ void InputField::addMarkdownActions(
addtag(integration.phraseFormattingUnderline(), QKeySequence::Underline, kTagUnderline);
addtag(integration.phraseFormattingStrikeOut(), kStrikeOutSequence, kTagStrikeOut);
addtag(integration.phraseFormattingMonospace(), kMonospaceSequence, kTagCode);
addtag(integration.phraseFormattingSpoiler(), kSpoilerSequence, kTagSpoiler);
if (_editLinkCallback) {
submenu->addSeparator();
@ -4075,7 +4096,8 @@ void MaskedInputField::setPlaceholder(rpl::producer<QString> placeholder) {
void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
if (const auto menu = createStandardContextMenu()) {
(new PopupMenu(this, menu))->popup(e->globalPos());
_contextMenu = base::make_unique_q<PopupMenu>(this, menu);
_contextMenu->popup(e->globalPos());
}
}

View file

@ -28,6 +28,7 @@ const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
const auto kStrikeOutSequence = QKeySequence("ctrl+shift+x");
const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
const auto kEditLinkSequence = QKeySequence("ctrl+k");
const auto kSpoilerSequence = QKeySequence("ctrl+shift+p");
class PopupMenu;
@ -140,6 +141,9 @@ private:
QTimer _touchTimer;
bool _touchPress, _touchRightButton, _touchMove;
QPoint _touchStart;
base::unique_qptr<PopupMenu> _contextMenu;
};
class InputField : public RpWidget {
@ -171,6 +175,7 @@ public:
static const QString kTagStrikeOut;
static const QString kTagCode;
static const QString kTagPre;
static const QString kTagSpoiler;
InputField(
QWidget *parent,
@ -700,6 +705,9 @@ private:
bool _touchRightButton = false;
bool _touchMove = false;
QPoint _touchStart;
base::unique_qptr<PopupMenu> _contextMenu;
};
class PasswordInput : public MaskedInputField {

View file

@ -68,10 +68,12 @@ not_null<QAction*> Menu::addAction(
not_null<QAction*> Menu::addAction(
const QString &text,
std::unique_ptr<QMenu> submenu) {
std::unique_ptr<QMenu> submenu,
const style::icon *icon,
const style::icon *iconOver) {
const auto action = new QAction(text, this);
action->setMenu(submenu.release());
return addAction(action, nullptr, nullptr);
return addAction(action, icon, iconOver);
}
not_null<QAction*> Menu::addAction(
@ -365,8 +367,19 @@ void Menu::mouseReleaseEvent(QMouseEvent *e) {
void Menu::handleMousePress(QPoint globalPosition) {
handleMouseMove(globalPosition);
if (_mousePressDelegate) {
_mousePressDelegate(globalPosition);
const auto margins = style::margins(0, _st.skip, 0, _st.skip);
const auto inner = rect().marginsRemoved(margins);
const auto localPosition = mapFromGlobal(globalPosition);
const auto pressed = (inner.contains(localPosition)
&& _lastSelectedByMouse)
? findSelectedAction()
: nullptr;
if (pressed) {
pressed->setClicked();
} else {
if (_mousePressDelegate) {
_mousePressDelegate(globalPosition);
}
}
}

View file

@ -40,7 +40,9 @@ public:
const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(
const QString &text,
std::unique_ptr<QMenu> submenu);
std::unique_ptr<QMenu> submenu,
const style::icon *icon = nullptr,
const style::icon *iconOver = nullptr);
not_null<QAction*> addSeparator();
void clearActions();
void finishAnimating();

View file

@ -357,14 +357,24 @@ not_null<QAction*> PopupMenu::addAction(
return _menu->addAction(std::move(widget));
}
not_null<QAction*> PopupMenu::addAction(const QString &text, Fn<void()> callback, const style::icon *icon, const style::icon *iconOver) {
not_null<QAction*> PopupMenu::addAction(
const QString &text,
Fn<void()> callback,
const style::icon *icon,
const style::icon *iconOver) {
return _menu->addAction(text, std::move(callback), icon, iconOver);
}
not_null<QAction*> PopupMenu::addAction(
const QString &text,
std::unique_ptr<PopupMenu> submenu) {
const auto action = _menu->addAction(text, std::make_unique<QMenu>());
std::unique_ptr<PopupMenu> submenu,
const style::icon *icon,
const style::icon *iconOver) {
const auto action = _menu->addAction(
text,
std::make_unique<QMenu>(),
icon,
iconOver);
const auto saved = _submenus.emplace(
action,
base::unique_qptr<PopupMenu>(submenu.release())

View file

@ -29,8 +29,16 @@ public:
}
not_null<QAction*> addAction(base::unique_qptr<Menu::ItemBase> widget);
not_null<QAction*> addAction(const QString &text, Fn<void()> callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(const QString &text, std::unique_ptr<PopupMenu> submenu);
not_null<QAction*> addAction(
const QString &text,
Fn<void()> callback,
const style::icon *icon = nullptr,
const style::icon *iconOver = nullptr);
not_null<QAction*> addAction(
const QString &text,
std::unique_ptr<PopupMenu> submenu,
const style::icon *icon = nullptr,
const style::icon *iconOver = nullptr);
not_null<QAction*> addSeparator();
void clearActions();

View file

@ -857,7 +857,7 @@ defaultMenu: Menu {
arrow: defaultMenuArrow;
widthMin: 180px;
widthMin: 140px;
widthMax: 300px;
ripple: defaultRippleAnimation;