Updated lib_ui sources to TDesktop version 3.4.3
This commit is contained in:
commit
039cbd656d
24 changed files with 867 additions and 178 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -144,4 +144,8 @@ QString Integration::phraseFormattingMonospace() {
|
|||
return "Monospace";
|
||||
}
|
||||
|
||||
QString Integration::phraseFormattingSpoiler() {
|
||||
return "Spoiler";
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ public:
|
|||
[[nodiscard]] virtual QString phraseFormattingUnderline();
|
||||
[[nodiscard]] virtual QString phraseFormattingStrikeOut();
|
||||
[[nodiscard]] virtual QString phraseFormattingMonospace();
|
||||
[[nodiscard]] virtual QString phraseFormattingSpoiler();
|
||||
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
40
ui/spoiler_click_handler.cpp
Normal file
40
ui/spoiler_click_handler.cpp
Normal 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;
|
||||
}
|
||||
30
ui/spoiler_click_handler.h
Normal file
30
ui/spoiler_click_handler.h
Normal 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;
|
||||
|
||||
};
|
||||
489
ui/text/text.cpp
489
ui/text/text.cpp
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -857,7 +857,7 @@ defaultMenu: Menu {
|
|||
|
||||
arrow: defaultMenuArrow;
|
||||
|
||||
widthMin: 180px;
|
||||
widthMin: 140px;
|
||||
widthMax: 300px;
|
||||
|
||||
ripple: defaultRippleAnimation;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue