Files
AyuGramDesktop/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp
T
2026-07-10 14:04:29 +04:00

2231 lines
63 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/editor/iv_editor_box.h"
#include <QtCore/QDir>
#include "base/algorithm.h"
#include "base/event_filter.h"
#include "base/flat_map.h"
#include "base/unique_qptr.h"
#include "base/weak_qptr.h"
#include "boxes/create_ai_box.h"
#include "boxes/premium_preview_box.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_file_origin.h"
#include "data/data_msg_id.h"
#include "data/data_types.h"
#include "data/data_emoji_statuses.h"
#include "dialogs/ui/dialogs_pill.h"
#include "history/history_item.h"
#include "history/view/controls/history_view_compose_ai_button.h"
#include "boxes/compose_ai_box.h"
#include "main/main_session.h"
#include "ui/emoji_config.h"
#include "ui/painter.h"
#include "data/data_document.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "chat_helpers/tabbed_selector.h"
#include "iv/editor/iv_editor_state.h"
#include "iv/editor/iv_editor_toolbar_pill.h"
#include "iv/editor/iv_editor_widget.h"
#include "iv/editor/iv_editor_window.h"
#include "lang/lang_keys.h"
#include "menu/menu_checked_action.h"
#include "menu/menu_send_details.h"
#include "styles/style_settings.h"
#include "ui/boxes/confirm_box.h"
#include "ui/controls/send_button.h"
#include "ui/delayed_activation.h"
#include "ui/effects/premium_graphics.h"
#include "ui/layers/generic_box.h"
#include "ui/rp_widget.h"
#include "ui/toast/toast.h"
#include "data/data_peer_values.h"
#include "ui/ui_utility.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/tooltip.h"
#include "ui/rect_part.h"
#include <crl/crl_on_main.h>
#include <rpl/never.h>
#include <QtCore/QEvent>
#include <QtCore/QPointer>
#include <QtGui/QGuiApplication>
#include <QtGui/QKeyEvent>
#include <QtGui/QPainter>
#include <QtGui/QRegion>
#include <QtGui/QScreen>
#include <QtSvg/QSvgRenderer>
#include <algorithm>
#include <array>
#include <memory>
#include <vector>
#include "styles/style_chat_helpers.h"
#include "styles/style_basic.h"
#include "styles/style_editor.h"
#include "styles/style_iv.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_widgets.h"
namespace Iv::Editor {
namespace {
enum class ToolbarActionId : uchar {
Undo,
Redo,
Bold,
Italic,
Underline,
StrikeOut,
Spoiler,
Subscript,
Superscript,
Marked,
PlainText,
Heading,
Link,
Math,
Blockquote,
Pullquote,
CodeBlock,
Details,
OrderedList,
BulletList,
TaskList,
Attach,
Table,
Location,
Divider,
};
[[nodiscard]] QString WithParenShortcut(
const QString &label,
QKeySequence seq) {
const auto shortcut = seq.toString(QKeySequence::NativeText);
return shortcut.isEmpty()
? label
: (label + u" ("_q + shortcut + u")"_q);
}
[[nodiscard]] QString ToolbarActionLabel(
ToolbarActionId action,
Widget::ToolbarLinkMode linkMode
= Widget::ToolbarLinkMode::Create) {
switch (action) {
case ToolbarActionId::Undo:
return tr::lng_wnd_menu_undo(tr::now);
case ToolbarActionId::Redo:
return tr::lng_wnd_menu_redo(tr::now);
case ToolbarActionId::Bold:
return tr::lng_menu_formatting_bold(tr::now);
case ToolbarActionId::Italic:
return tr::lng_menu_formatting_italic(tr::now);
case ToolbarActionId::Underline:
return tr::lng_menu_formatting_underline(tr::now);
case ToolbarActionId::StrikeOut:
return tr::lng_menu_formatting_strike_out(tr::now);
case ToolbarActionId::Spoiler:
return tr::lng_menu_formatting_spoiler(tr::now);
case ToolbarActionId::Subscript:
return tr::lng_menu_formatting_subscript(tr::now);
case ToolbarActionId::Superscript:
return tr::lng_menu_formatting_superscript(tr::now);
case ToolbarActionId::Marked:
return tr::lng_menu_formatting_marked(tr::now);
case ToolbarActionId::PlainText:
return tr::lng_menu_formatting_plain_text(tr::now);
case ToolbarActionId::Heading:
return tr::lng_article_insert_heading(tr::now);
case ToolbarActionId::Link:
return (linkMode == Widget::ToolbarLinkMode::Edit)
? tr::lng_menu_formatting_link_edit(tr::now)
: tr::lng_menu_formatting_link_create(tr::now);
case ToolbarActionId::Math:
return tr::lng_article_insert_math(tr::now);
case ToolbarActionId::Blockquote:
return tr::lng_article_insert_blockquote(tr::now);
case ToolbarActionId::Pullquote:
return tr::lng_article_insert_pullquote(tr::now);
case ToolbarActionId::CodeBlock:
return tr::lng_article_insert_code(tr::now);
case ToolbarActionId::Details:
return tr::lng_article_insert_details(tr::now);
case ToolbarActionId::OrderedList:
return tr::lng_article_insert_ordered_list(tr::now);
case ToolbarActionId::BulletList:
return tr::lng_article_insert_bullet_list(tr::now);
case ToolbarActionId::TaskList:
return tr::lng_article_insert_task_list(tr::now);
case ToolbarActionId::Attach:
return tr::lng_article_insert_media(tr::now);
case ToolbarActionId::Table:
return tr::lng_article_insert_table(tr::now);
case ToolbarActionId::Location:
return tr::lng_maps_point(tr::now);
case ToolbarActionId::Divider:
return tr::lng_article_insert_divider(tr::now);
}
return QString();
}
[[nodiscard]] const style::icon *HeadingIcon(int level) {
switch (level) {
case 1: return &st::ivEditorToolbarHeading1Icon;
case 2: return &st::ivEditorToolbarHeading2Icon;
case 3: return &st::ivEditorToolbarHeading3Icon;
case 4: return &st::ivEditorToolbarHeading4Icon;
case 5: return &st::ivEditorToolbarHeading5Icon;
case 6: return &st::ivEditorToolbarHeading6Icon;
}
return &st::ivEditorToolbarHeadingIcon;
}
class Toolbar final : public Ui::RpWidget {
public:
Toolbar(
QWidget *parent,
not_null<Widget*> editor,
QPointer<QWidget> tooltipParent,
bool hasRequestMedia,
Fn<void(not_null<Widget*>, QPointer<QWidget>, rpl::producer<>)> requestMap,
Fn<void()> toggleEmoji,
not_null<Main::Session*> session);
int resizeGetHeight(int width) override;
bool eventFilter(QObject *object, QEvent *event) override;
void hideShownTooltip();
void setEmojiColumnOpen(bool open);
[[nodiscard]] int minimalWidth() const;
[[nodiscard]] int contentMaxWidth() const;
private:
struct PillButton {
Ui::IconButton *button = nullptr;
Widget::ToolbarFormatAction format = {};
};
not_null<Ui::IconButton*> addPillButton(
not_null<ToolbarPill*> pill,
ToolbarActionId action,
const style::icon *icon,
Fn<void()> callback,
std::optional<Widget::ToolbarFormatAction> format = std::nullopt,
Fn<QString()> tooltip = nullptr);
void addPremiumStar(not_null<Ui::IconButton*> button);
void buildPills();
void scheduleTooltip(not_null<Ui::IconButton*> button);
void showTooltip(not_null<Ui::IconButton*> button);
void hideTooltip();
void updateTooltipGeometry();
void fillHeadingMenu(not_null<Ui::PopupMenu*> menu);
void fillBlockStyleMenu(not_null<Ui::PopupMenu*> menu);
void showBlockStyleMenu(not_null<Ui::IconButton*> button);
void fillTextStyleMenu(not_null<Ui::PopupMenu*> menu);
void showTextStyleMenu(not_null<Ui::IconButton*> button);
void fillListStyleMenu(not_null<Ui::PopupMenu*> menu);
void showListStyleMenu(not_null<Ui::IconButton*> button);
void fillTableStyleMenu(not_null<Ui::PopupMenu*> menu);
void showTableStyleMenu(not_null<Ui::IconButton*> button);
void fillAttachMenu(not_null<Ui::PopupMenu*> menu);
void showAttachMenu(not_null<Ui::IconButton*> button);
void applyBlockText();
void updateFromEditorState();
void updateInputMask();
const QPointer<Widget> _editor;
const not_null<Main::Session*> _session;
const QPointer<QWidget> _tooltipParent;
const bool _hasRequestMedia = false;
const Fn<void(not_null<Widget*>, QPointer<QWidget>, rpl::producer<>)> _requestMap;
const Fn<void()> _toggleEmoji;
Widget::ToolbarState _toolbarState = {};
object_ptr<ToolbarPill> _undoRedoPill = { nullptr };
object_ptr<ToolbarPill> _controlsPill = { nullptr };
object_ptr<ToolbarPill> _emojiPill = { nullptr };
std::vector<PillButton> _stateButtons;
Ui::IconButton *_linkButton = nullptr;
Ui::IconButton *_emojiButton = nullptr;
Ui::IconButton *_listButton = nullptr;
Ui::IconButton *_tableButton = nullptr;
base::flat_map<Ui::IconButton*, Fn<QString()>> _tooltipFactories;
base::unique_qptr<Ui::ImportantTooltip> _tooltip;
Ui::IconButton *_hovered = nullptr;
Ui::IconButton *_scheduledTooltip = nullptr;
base::unique_qptr<Ui::PopupMenu> _menu;
};
[[nodiscard]] QRect DefaultWindowGeometry() {
const auto padding = st::ivEditorBodyPadding;
const auto size = QSize(
std::max(
st::ivEditorWindowMinSize.width(),
st::messageMarkdown.pageMaxWidth
+ padding.left()
+ padding.right()),
st::ivEditorWindowDefaultSize.height());
auto result = QRect(QPoint(), size);
if (const auto screen = QGuiApplication::primaryScreen()) {
result.moveCenter(screen->availableGeometry().center());
}
return result;
}
[[nodiscard]] int MaximalExtendBy(not_null<Window*> window) {
const auto screen = window->screen()
? window->screen()
: QGuiApplication::primaryScreen();
if (!screen) {
return 0;
}
const auto desktop = screen->availableGeometry();
return std::max(desktop.width() - window->body()->width(), 0);
}
[[nodiscard]] bool CanExtendNoMove(
not_null<Window*> window,
int extendBy) {
const auto screen = window->screen()
? window->screen()
: QGuiApplication::primaryScreen();
if (!screen) {
return false;
}
const auto desktop = screen->availableGeometry();
const auto inner = window->body()->mapToGlobal(window->body()->rect());
const auto innerRight = inner.x() + inner.width() + extendBy;
const auto desktopRight = desktop.x() + desktop.width();
return innerRight <= desktopRight;
}
int TryToExtendWidthBy(not_null<Window*> window, int addToWidth) {
const auto screen = window->screen()
? window->screen()
: QGuiApplication::primaryScreen();
if (!screen) {
return 0;
}
const auto desktop = screen->availableGeometry();
const auto inner = window->body()->mapToGlobal(window->body()->rect());
accumulate_min(addToWidth, MaximalExtendBy(window));
const auto newWidth = inner.width() + addToWidth;
const auto newLeft = std::min(
inner.x(),
desktop.x() + desktop.width() - newWidth);
if (inner.x() != newLeft || inner.width() != newWidth) {
window->setGeometry(QRect(newLeft, inner.y(), newWidth, inner.height()));
}
return addToWidth;
}
[[nodiscard]] QString HeadingLabel(int level) {
switch (level) {
case 1: return tr::lng_article_insert_heading1(tr::now);
case 2: return tr::lng_article_insert_heading2(tr::now);
case 3: return tr::lng_article_insert_heading3(tr::now);
case 4: return tr::lng_article_insert_heading4(tr::now);
case 5: return tr::lng_article_insert_heading5(tr::now);
case 6: return tr::lng_article_insert_heading6(tr::now);
}
return tr::lng_article_insert_heading1(tr::now);
}
[[nodiscard]] QString SubmitText(const ShowWindowDescriptor &descriptor) {
if (!descriptor.submitLabel.isEmpty()) {
return descriptor.submitLabel;
}
switch (descriptor.submitType) {
case ShowWindowDescriptor::SubmitType::Send:
return tr::lng_send_button(tr::now);
case ShowWindowDescriptor::SubmitType::Save:
return tr::lng_settings_save(tr::now);
}
return tr::lng_send_button(tr::now);
}
[[nodiscard]] bool IsEmojiDocument(not_null<DocumentData*> document) {
const auto info = document->sticker();
return info && (info->setType == Data::StickersType::Emoji);
}
class WindowContext final : public ChatHelpers::Show {
public:
WindowContext(
not_null<Window*> window,
not_null<Main::Session*> session)
: _window(window.get())
, _session(session) {
}
void activate() override {
if (const auto window = _window.data()) {
Ui::ActivateWindow(window);
}
}
void showOrHideBoxOrLayer(
std::variant<
v::null_t,
object_ptr<Ui::BoxContent>,
std::unique_ptr<Ui::LayerWidget>> &&layer,
Ui::LayerOptions options,
anim::type animated) const override {
using ObjectBox = object_ptr<Ui::BoxContent>;
using UniqueLayer = std::unique_ptr<Ui::LayerWidget>;
const auto window = _window.data();
if (!window) {
return;
} else if (auto layerWidget = std::get_if<UniqueLayer>(&layer)) {
window->showLayer(std::move(*layerWidget), options, animated);
} else if (auto box = std::get_if<ObjectBox>(&layer)) {
window->showBox(std::move(*box), options, animated);
} else {
window->hideLayer(animated);
}
}
not_null<QWidget*> toastParent() const override {
const auto window = _window.data();
Assert(window != nullptr);
return window->body();
}
bool valid() const override {
return !_window.isNull();
}
operator bool() const override {
return valid();
}
Main::Session &session() const override {
return *_session;
}
bool paused(ChatHelpers::PauseReason reason) const override {
const auto window = _window.data();
if (!window
|| window->isHidden()
|| !window->isActiveWindow()) {
return true;
} else if (reason < ChatHelpers::PauseReason::RoundPlaying
&& window->isLayerShown()) {
return true;
}
return false;
}
rpl::producer<> pauseChanged() const override {
return rpl::never<>();
}
rpl::producer<bool> adjustShadowLeft() const override {
return rpl::single(false);
}
SendMenu::Details sendMenuDetails() const override {
return { SendMenu::Type::Disabled };
}
bool showMediaPreview(
Data::FileOrigin,
not_null<DocumentData*>) const override {
return false;
}
bool showMediaPreview(
Data::FileOrigin,
not_null<PhotoData*>) const override {
return false;
}
void processChosenSticker(
ChatHelpers::FileChosen &&) const override {
}
::Window::SessionController *resolveWindow() const override {
return nullptr;
}
private:
const QPointer<Window> _window;
const not_null<Main::Session*> _session;
};
Toolbar::Toolbar(
QWidget *parent,
not_null<Widget*> editor,
QPointer<QWidget> tooltipParent,
bool hasRequestMedia,
Fn<void(not_null<Widget*>, QPointer<QWidget>, rpl::producer<>)> requestMap,
Fn<void()> toggleEmoji,
not_null<Main::Session*> session)
: Ui::RpWidget(parent)
, _editor(editor.get())
, _session(session)
, _tooltipParent(std::move(tooltipParent))
, _hasRequestMedia(hasRequestMedia)
, _requestMap(std::move(requestMap))
, _toggleEmoji(std::move(toggleEmoji))
, _undoRedoPill(this, st::ivEditorPillShadow)
, _controlsPill(this, st::ivEditorPillShadow)
, _emojiPill(this, st::ivEditorPillShadow) {
buildPills();
_undoRedoPill->show();
_controlsPill->show();
_emojiPill->show();
_toolbarState = _editor ? _editor->toolbarStateValue() : Widget::ToolbarState();
if (_editor) {
_editor->toolbarStateChanges() | rpl::on_next([=](const Widget::ToolbarState &state) {
_toolbarState = state;
updateFromEditorState();
}, lifetime());
}
updateFromEditorState();
}
not_null<Ui::IconButton*> Toolbar::addPillButton(
not_null<ToolbarPill*> pill,
ToolbarActionId action,
const style::icon *icon,
Fn<void()> callback,
std::optional<Widget::ToolbarFormatAction> format,
Fn<QString()> tooltip) {
const auto raw = pill->addButton(
st::ivEditorToolbarButton,
icon,
icon,
ToolbarButtonState::Inactive);
raw->setAccessibleName(ToolbarActionLabel(action, _toolbarState.linkMode));
raw->setClickedCallback([=] {
if (callback) {
callback();
}
});
if (format) {
_stateButtons.push_back({ raw.get(), *format });
}
if (tooltip) {
_tooltipFactories.emplace(raw.get(), std::move(tooltip));
raw->installEventFilter(this);
}
return raw;
}
void Toolbar::addPremiumStar(not_null<Ui::IconButton*> button) {
const auto factor = style::DevicePixelRatio();
const auto side = st::ivEditorToolbarPremiumStarSize;
const auto size = QSize(side, side);
auto image = QImage(
size * factor,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
auto svg = QSvgRenderer(
Ui::Premium::ColorizedSvg(Ui::Premium::ButtonGradientStops()));
svg.render(&p, QRectF(QPointF(), QSizeF(size)));
}
const auto star = Ui::CreateChild<Ui::RpWidget>(button.get());
star->setAttribute(Qt::WA_TransparentForMouseEvents);
star->resize(size);
star->paintRequest() | rpl::on_next([=] {
auto p = QPainter(star);
p.drawImage(0, 0, image);
}, star->lifetime());
const auto skip = st::ivEditorToolbarPremiumStarSkip;
star->move(
button->width() - star->width() - skip.x(),
button->height() - star->height() - skip.y());
star->showOn(Data::AmPremiumValue(_session)
| rpl::map([](bool premium) { return !premium; }));
star->raise();
}
void Toolbar::buildPills() {
const auto insertType = [=](State::InsertBlockType type) {
if (_editor) {
_editor->insertBlock({ .type = type });
}
};
addPillButton(
not_null<ToolbarPill*>(_undoRedoPill.data()),
ToolbarActionId::Undo,
&st::ivEditorToolbarUndoIcon,
[=] {
if (_editor) {
_editor->performToolbarUndoRedo(false);
}
},
Widget::ToolbarFormatAction::Undo,
[] {
return WithParenShortcut(
tr::lng_wnd_menu_undo(tr::now),
QKeySequence(QKeySequence::Undo));
});
addPillButton(
not_null<ToolbarPill*>(_undoRedoPill.data()),
ToolbarActionId::Redo,
&st::ivEditorToolbarRedoIcon,
[=] {
if (_editor) {
_editor->performToolbarUndoRedo(true);
}
},
Widget::ToolbarFormatAction::Redo,
[] {
return WithParenShortcut(
tr::lng_wnd_menu_redo(tr::now),
QKeySequence(QKeySequence::Redo));
});
const auto controls = not_null<ToolbarPill*>(_controlsPill.data());
const auto heading = addPillButton(
controls,
ToolbarActionId::Heading,
&st::ivEditorToolbarTextStyleIcon,
nullptr,
std::nullopt,
[] { return tr::lng_menu_formatting(tr::now); });
heading->setIsMenuButton(true);
heading->setClickedCallback([=] {
showBlockStyleMenu(heading);
});
const auto textStyle = addPillButton(
controls,
ToolbarActionId::Bold,
&st::ivEditorToolbarBoldIcon,
nullptr,
std::nullopt,
[] { return tr::lng_article_tooltip_text_style(tr::now); });
textStyle->setIsMenuButton(true);
textStyle->setClickedCallback([=] {
showTextStyleMenu(textStyle);
});
const auto listStyle = addPillButton(
controls,
ToolbarActionId::BulletList,
&st::ivEditorToolbarBulletListIcon,
nullptr,
std::nullopt,
[=] {
const auto active = _editor
&& _editor->currentListRangeAtCaret().has_value();
return active
? tr::lng_article_tooltip_list_style(tr::now)
: tr::lng_article_list_insert(tr::now);
});
listStyle->setIsMenuButton(true);
listStyle->setClickedCallback([=] {
showListStyleMenu(listStyle);
});
_listButton = listStyle;
addPremiumStar(listStyle);
const auto tableStyle = addPillButton(
controls,
ToolbarActionId::Table,
&st::ivEditorToolbarTableIcon,
nullptr,
std::nullopt,
[=] {
const auto active = _editor
&& _editor->currentTableRangeAtCaret().has_value();
return active
? tr::lng_article_tooltip_table_style(tr::now)
: tr::lng_article_tooltip_table_insert(tr::now);
});
tableStyle->setIsMenuButton(true);
tableStyle->setClickedCallback([=] {
if (_editor && _editor->currentTableRangeAtCaret()) {
showTableStyleMenu(tableStyle);
} else {
insertType(State::InsertBlockType::Table);
}
});
_tableButton = tableStyle;
addPremiumStar(tableStyle);
_linkButton = addPillButton(
controls,
ToolbarActionId::Link,
&st::ivEditorToolbarLinkIcon,
[=] {
if (_editor) {
_editor->editLinkFromToolbar();
}
},
Widget::ToolbarFormatAction::Link,
[] {
return WithParenShortcut(
tr::lng_article_tooltip_link(tr::now),
QKeySequence(Ui::kEditLinkSequence));
});
if (_hasRequestMedia) {
const auto attach = addPillButton(
controls,
ToolbarActionId::Attach,
&st::ivEditorToolbarAttachIcon,
nullptr,
std::nullopt,
[] { return tr::lng_article_tooltip_media(tr::now); });
attach->setIsMenuButton(true);
attach->setClickedCallback([=] {
showAttachMenu(attach);
});
addPremiumStar(attach);
}
const auto math = addPillButton(
controls,
ToolbarActionId::Math,
&st::ivEditorToolbarMathIcon,
[=] {
if (!_editor) {
return;
}
if (_editor->inlineToolbarModeActive()) {
_editor->editMathFromToolbar();
} else {
_editor->insertBlock({ .type = State::InsertBlockType::Math });
}
},
std::nullopt,
[] { return tr::lng_article_tooltip_formula(tr::now); });
addPremiumStar(math);
_emojiButton = addPillButton(
not_null<ToolbarPill*>(_emojiPill.data()),
ToolbarActionId::Attach,
&st::ivEditorToolbarEmojiIcon,
[=] {
if (_toggleEmoji) {
_toggleEmoji();
}
},
std::nullopt,
[] { return tr::lng_article_tooltip_emoji(tr::now); });
_emojiButton->setAccessibleName(tr::lng_article_insert_emoji(tr::now));
}
void Toolbar::fillHeadingMenu(not_null<Ui::PopupMenu*> menu) {
const auto starSize = _session->premium()
? 0
: st::ivEditorStyleMenuPremiumStarSize;
for (const auto level : std::array{ 1, 2, 3, 4, 5, 6 }) {
const auto icon = HeadingIcon(level);
Menu::AddActiveColorAction(
menu,
HeadingLabel(level),
[=] {
if (_editor) {
_editor->insertBlock({
.type = State::InsertBlockType::Heading,
.headingLevel = level,
});
}
},
icon,
false,
starSize);
}
}
void Toolbar::fillBlockStyleMenu(not_null<Ui::PopupMenu*> menu) {
const auto info = _editor
? _editor->activeBlockInfo()
: Widget::ActiveBlockInfo();
const auto kind = info.kind;
using Kind = RichPage::BlockKind;
const auto insertType = [=](State::InsertBlockType type) {
if (_editor) {
_editor->insertBlock({ .type = type });
}
};
const auto premium = _session->premium();
const auto starSize = premium
? 0
: st::ivEditorStyleMenuPremiumStarSize;
const auto withShortcut = [&](const QString &label, QKeySequence seq) {
if (!premium) {
return label;
}
const auto shortcut = seq.toString(QKeySequence::NativeText);
return shortcut.isEmpty()
? label
: (label + QChar('\t') + shortcut);
};
auto sub = std::make_unique<Ui::PopupMenu>(menu, st::popupMenuWithIcons);
fillHeadingMenu(not_null<Ui::PopupMenu*>(sub.get()));
menu->addAction(
tr::lng_article_insert_heading(tr::now),
std::move(sub),
&st::ivEditorToolbarHeadingIcon,
&st::ivEditorToolbarHeadingIcon);
Menu::AddActiveColorAction(
menu,
tr::lng_article_insert_text(tr::now),
[=] { applyBlockText(); },
&st::ivEditorToolbarPlainTextIcon,
(kind == Kind::Paragraph));
Menu::AddActiveColorAction(
menu,
withShortcut(
tr::lng_article_insert_blockquote(tr::now),
Ui::kBlockquoteSequence),
[=] { insertType(State::InsertBlockType::Blockquote); },
&st::ivEditorToolbarBlockquoteIcon,
(kind == Kind::Quote && !info.pullquote));
Menu::AddActiveColorAction(
menu,
tr::lng_article_insert_pullquote(tr::now),
[=] { insertType(State::InsertBlockType::Pullquote); },
&st::ivEditorToolbarPullquoteIcon,
(kind == Kind::Quote && info.pullquote),
starSize);
Menu::AddActiveColorAction(
menu,
withShortcut(
tr::lng_article_insert_code(tr::now),
Ui::kMonospaceSequence),
[=] { insertType(State::InsertBlockType::Code); },
&st::ivEditorToolbarCodeIcon,
(kind == Kind::Code));
Menu::AddActiveColorAction(
menu,
tr::lng_article_insert_divider(tr::now),
[=] { insertType(State::InsertBlockType::Divider); },
&st::ivEditorToolbarDividerIcon,
false,
starSize);
}
void Toolbar::applyBlockText() {
if (!_editor) {
return;
}
const auto info = _editor->activeBlockInfo();
using Kind = RichPage::BlockKind;
switch (info.kind) {
case Kind::Quote:
_editor->insertBlock({
.type = info.pullquote
? State::InsertBlockType::Pullquote
: State::InsertBlockType::Blockquote,
});
break;
case Kind::Code:
_editor->insertBlock({ .type = State::InsertBlockType::Code });
break;
case Kind::Heading:
_editor->applyToolbarFormatAction(
Widget::ToolbarFormatAction::PlainText);
break;
default:
break;
}
}
void Toolbar::showBlockStyleMenu(not_null<Ui::IconButton*> button) {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);
fillBlockStyleMenu(not_null<Ui::PopupMenu*>(_menu.get()));
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
}
void Toolbar::fillTextStyleMenu(not_null<Ui::PopupMenu*> menu) {
using Action = Widget::ToolbarFormatAction;
const auto premium = _session->premium();
const auto starSize = premium
? 0
: st::ivEditorStyleMenuPremiumStarSize;
const auto withShortcut = [&](const QString &label, QKeySequence seq) {
const auto shortcut = seq.toString(QKeySequence::NativeText);
return shortcut.isEmpty()
? label
: (label + QChar('\t') + shortcut);
};
const auto add = [&](
Action action,
const QString &label,
const style::icon *icon,
QKeySequence shortcut = QKeySequence(),
bool premiumOnly = false) {
const auto &state = _toolbarState[action];
if (!state.shown) {
return;
}
Menu::AddActiveColorAction(
menu,
withShortcut(label, shortcut),
[=] {
if (_editor) {
_editor->applyToolbarFormatAction(action);
}
},
icon,
state.active,
premiumOnly ? starSize : 0);
};
// Bold..Spoiler exist in regular messages too, so they are not marked
// as premium - only the rich-message-only entities below are.
add(Action::Bold,
tr::lng_menu_formatting_bold(tr::now),
&st::ivEditorToolbarBoldIcon,
QKeySequence::Bold);
add(Action::Italic,
tr::lng_menu_formatting_italic(tr::now),
&st::ivEditorToolbarItalicIcon,
QKeySequence::Italic);
add(Action::Underline,
tr::lng_menu_formatting_underline(tr::now),
&st::ivEditorToolbarUnderlineIcon,
QKeySequence::Underline);
add(Action::StrikeOut,
tr::lng_menu_formatting_strike_out(tr::now),
&st::ivEditorToolbarStrikeOutIcon,
Ui::kStrikeOutSequence);
add(Action::Spoiler,
tr::lng_menu_formatting_spoiler(tr::now),
&st::ivEditorToolbarSpoilerIcon,
Ui::kSpoilerSequence);
add(Action::Subscript,
tr::lng_menu_formatting_subscript(tr::now),
&st::ivEditorToolbarSubscriptIcon,
QKeySequence(),
true);
add(Action::Superscript,
tr::lng_menu_formatting_superscript(tr::now),
&st::ivEditorToolbarSuperscriptIcon,
QKeySequence(),
true);
add(Action::Marked,
tr::lng_menu_formatting_marked(tr::now),
&st::ivEditorToolbarMarkedIcon,
QKeySequence(),
true);
}
void Toolbar::showTextStyleMenu(not_null<Ui::IconButton*> button) {
if (_menu) {
return;
}
auto menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
fillTextStyleMenu(not_null<Ui::PopupMenu*>(menu.get()));
if (menu->empty()) {
return;
}
_menu = std::move(menu);
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
}
void Toolbar::fillAttachMenu(not_null<Ui::PopupMenu*> menu) {
const auto starSize = _session->premium()
? 0
: st::ivEditorStyleMenuPremiumStarSize;
Menu::AddActiveColorAction(
menu,
tr::lng_attach_photo_or_video(tr::now),
[=] {
if (_editor) {
_editor->requestMedia(
std::nullopt,
RequestMediaType::PhotoVideo);
}
},
&st::ivEditorToolbarAttachIcon,
false,
starSize);
Menu::AddActiveColorAction(
menu,
tr::lng_in_dlg_audio_file(tr::now),
[=] {
if (_editor) {
_editor->requestMedia(
std::nullopt,
RequestMediaType::Audio);
}
},
&st::ivEditorToolbarAudioIcon,
false,
starSize);
if (_requestMap) {
Menu::AddActiveColorAction(
menu,
tr::lng_maps_point(tr::now),
[=] {
if (_editor) {
const auto parent = _tooltipParent;
auto closeRequests = parent
? static_cast<Ui::RpWidget*>(parent.data())->death()
: rpl::never<>();
Ui::PreventDelayedActivation();
_requestMap(
not_null<Widget*>(_editor.data()),
parent,
std::move(closeRequests));
}
},
&st::ivEditorToolbarLocationIcon,
false,
starSize);
}
}
void Toolbar::showAttachMenu(not_null<Ui::IconButton*> button) {
if (_menu) {
return;
}
auto menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
fillAttachMenu(not_null<Ui::PopupMenu*>(menu.get()));
_menu = std::move(menu);
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
}
void Toolbar::fillListStyleMenu(not_null<Ui::PopupMenu*> menu) {
const auto insertType = [=](State::InsertBlockType type) {
if (_editor) {
_editor->insertBlock({ .type = type });
}
};
const auto starSize = _session->premium()
? 0
: st::ivEditorStyleMenuPremiumStarSize;
const auto addInserts = [=](not_null<Ui::PopupMenu*> target) {
Menu::AddActiveColorAction(
target,
tr::lng_article_insert_ordered_list(tr::now),
[=] { insertType(State::InsertBlockType::OrderedList); },
&st::ivEditorToolbarOrderedListIcon,
false,
starSize);
Menu::AddActiveColorAction(
target,
tr::lng_article_insert_bullet_list(tr::now),
[=] { insertType(State::InsertBlockType::BulletList); },
&st::ivEditorToolbarBulletListIcon,
false,
starSize);
Menu::AddActiveColorAction(
target,
tr::lng_article_insert_task_list(tr::now),
[=] { insertType(State::InsertBlockType::TaskList); },
&st::ivEditorToolbarTaskListIcon,
false,
starSize);
Menu::AddActiveColorAction(
target,
tr::lng_article_insert_details(tr::now),
[=] { insertType(State::InsertBlockType::Details); },
&st::ivEditorToolbarDetailsIcon,
false,
starSize);
};
const auto range = _editor
? _editor->currentListRangeAtCaret()
: std::optional<Markdown::PreparedEditListItemRange>();
if (!range) {
addInserts(menu);
return;
}
const auto info = _editor->listSelectionInfo(*range);
const auto hasItemMenu = info.valid
&& (info.listKind == RichPage::ListKind::Ordered)
&& !info.taskList;
auto changeSub = std::make_unique<Ui::PopupMenu>(
menu,
st::popupMenuWithIcons);
_editor->fillListChangeMenu(
not_null<Ui::PopupMenu*>(changeSub.get()),
*range);
menu->addAction(
tr::lng_article_list_change(tr::now),
std::move(changeSub),
&st::ivEditorToolbarBulletListIcon,
&st::ivEditorToolbarBulletListIcon);
if (hasItemMenu) {
const auto itemRange = _editor->currentListItemRangeAtCaret();
if (itemRange) {
auto itemSub = std::make_unique<Ui::PopupMenu>(
menu,
st::popupMenuWithIcons);
_editor->fillListItemChangeMenu(
not_null<Ui::PopupMenu*>(itemSub.get()),
*itemRange);
menu->addAction(
tr::lng_article_list_item_change(tr::now),
std::move(itemSub),
&st::ivEditorToolbarOrderedListIcon,
&st::ivEditorToolbarOrderedListIcon);
}
}
auto insertSub = std::make_unique<Ui::PopupMenu>(
menu,
st::popupMenuWithIcons);
addInserts(not_null<Ui::PopupMenu*>(insertSub.get()));
menu->addAction(
tr::lng_article_list_insert(tr::now),
std::move(insertSub),
&st::ivEditorToolbarBulletListIcon,
&st::ivEditorToolbarBulletListIcon);
}
void Toolbar::showListStyleMenu(not_null<Ui::IconButton*> button) {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);
fillListStyleMenu(not_null<Ui::PopupMenu*>(_menu.get()));
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
}
void Toolbar::fillTableStyleMenu(not_null<Ui::PopupMenu*> menu) {
if (!_editor) {
return;
}
const auto range = _editor->currentTableRangeAtCaret();
if (!range) {
return;
}
_editor->fillTableChangeMenu(menu, *range);
}
void Toolbar::showTableStyleMenu(not_null<Ui::IconButton*> button) {
if (_menu) {
return;
}
auto menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
fillTableStyleMenu(not_null<Ui::PopupMenu*>(menu.get()));
if (menu->empty()) {
return;
}
_menu = std::move(menu);
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
}
void Toolbar::updateFromEditorState() {
for (const auto &pb : _stateButtons) {
const auto &state = _toolbarState[pb.format];
const auto value = state.active
? ToolbarButtonState::Active
: state.enabled
? ToolbarButtonState::Inactive
: ToolbarButtonState::Disabled;
SetupToolbarButton(not_null<Ui::IconButton*>(pb.button), value);
}
if (_linkButton) {
_linkButton->setAccessibleName(
ToolbarActionLabel(ToolbarActionId::Link, _toolbarState.linkMode));
}
if (_listButton) {
const auto inList = _editor
&& _editor->currentListRangeAtCaret().has_value();
SetupToolbarButton(
not_null<Ui::IconButton*>(_listButton),
inList
? ToolbarButtonState::Active
: ToolbarButtonState::Inactive);
}
if (_tableButton) {
const auto inTable = _editor
&& _editor->currentTableRangeAtCaret().has_value();
SetupToolbarButton(
not_null<Ui::IconButton*>(_tableButton),
inTable
? ToolbarButtonState::Active
: ToolbarButtonState::Inactive);
}
}
void Toolbar::setEmojiColumnOpen(bool open) {
if (!_emojiButton) {
return;
}
SetupToolbarButton(
not_null<Ui::IconButton*>(_emojiButton),
open ? ToolbarButtonState::Active : ToolbarButtonState::Inactive);
}
int Toolbar::minimalWidth() const {
return st::ivEditorToolbarPadding.left()
+ st::ivEditorToolbarPadding.right()
+ 2 * st::ivEditorToolbarGroupsSkip
+ _undoRedoPill->naturalSize().width()
+ _controlsPill->naturalSize().width()
+ _emojiPill->naturalSize().width();
}
int Toolbar::contentMaxWidth() const {
const auto padding = st::ivEditorToolbarPadding;
return minimalWidth() - padding.left() - padding.right();
}
int Toolbar::resizeGetHeight(int width) {
const auto padding = st::ivEditorToolbarPadding;
const auto top = padding.top();
const auto column = _editor
? _editor->articleColumnForWidth(width)
: Widget::ArticleColumn{ 0, width };
const auto fitsArticle = (column.width >= contentMaxWidth());
const auto left = fitsArticle ? column.left : 0;
const auto right = fitsArticle ? (column.left + column.width) : width;
const auto undoRedoLeft = left;
_undoRedoPill->moveToLeft(undoRedoLeft, top, width);
const auto controlsLeft = undoRedoLeft
+ _undoRedoPill->naturalSize().width()
+ st::ivEditorToolbarGroupsSkip;
_controlsPill->moveToLeft(controlsLeft, top, width);
const auto emojiLeft = right - _emojiPill->naturalSize().width();
_emojiPill->moveToLeft(emojiLeft, top, width);
updateInputMask();
if (_hovered && _hovered->isHidden()) {
hideTooltip();
}
updateTooltipGeometry();
return top + _controlsPill->naturalSize().height() + padding.bottom();
}
void Toolbar::updateInputMask() {
auto region = QRegion();
const auto add = [&](not_null<const ToolbarPill*> pill) {
if (!pill->isHidden()) {
region += pill->geometry();
}
};
add(not_null<const ToolbarPill*>(_undoRedoPill.data()));
add(not_null<const ToolbarPill*>(_controlsPill.data()));
add(not_null<const ToolbarPill*>(_emojiPill.data()));
if (region.isEmpty()) {
clearMask();
} else {
setMask(region);
}
}
void Toolbar::hideShownTooltip() {
hideTooltip();
}
bool Toolbar::eventFilter(QObject *object, QEvent *event) {
const auto button = static_cast<Ui::IconButton*>(object);
if (_tooltipFactories.contains(button)) {
if (event->type() == QEvent::Enter) {
scheduleTooltip(not_null<Ui::IconButton*>(button));
} else if (event->type() == QEvent::Leave) {
if (_scheduledTooltip == button) {
_scheduledTooltip = nullptr;
}
if (_hovered == button) {
hideTooltip();
}
}
}
return Ui::RpWidget::eventFilter(object, event);
}
void Toolbar::scheduleTooltip(not_null<Ui::IconButton*> button) {
// Showing the tooltip synchronously grabs the widget to build an
// animation cache, which crashes if we're inside a widget-tree
// destructor (an Enter event synthesized while a layer is destroyed
// and the mouse ends up over one of the toolbar buttons). Offload the
// actual show to the next event loop iteration, guarded by the button
// still being hovered (cleared on Leave) and by not queuing twice.
const auto already = (_scheduledTooltip != nullptr);
_scheduledTooltip = button;
if (already) {
return;
}
crl::on_main(this, [=] {
if (const auto button = base::take(_scheduledTooltip)) {
showTooltip(not_null<Ui::IconButton*>(button));
}
});
}
void Toolbar::showTooltip(not_null<Ui::IconButton*> button) {
hideTooltip();
const auto i = _tooltipFactories.find(button.get());
if (i == end(_tooltipFactories) || !i->second) {
return;
}
_hovered = button;
const auto parent = _tooltipParent
? _tooltipParent.data()
: (parentWidget() ? parentWidget() : this);
_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(
parent,
Ui::MakeNiceTooltipLabel(
parent,
rpl::single(TextWithEntities::Simple(i->second())),
st::boxWideWidth,
st::defaultImportantTooltipLabel),
st::defaultImportantTooltip));
_tooltip->setAttribute(Qt::WA_TransparentForMouseEvents);
_tooltip->toggleFast(false);
updateTooltipGeometry();
_tooltip->raise();
_tooltip->toggleAnimated(true);
}
void Toolbar::hideTooltip() {
_hovered = nullptr;
_scheduledTooltip = nullptr;
if (_tooltip) {
_tooltip->toggleFast(false);
_tooltip = nullptr;
}
}
void Toolbar::updateTooltipGeometry() {
if (!_tooltip || !_hovered) {
return;
}
const auto parent = _tooltip->parentWidget();
const auto geometry = Ui::MapFrom(parent, _hovered, _hovered->rect());
_tooltip->pointAt(geometry, RectPart::Top | RectPart::Center);
}
} // namespace
struct WindowHost::Impl final {
public:
explicit Impl(ShowWindowDescriptor descriptor);
~Impl();
void close();
void activateClose();
private:
void setupWindow(ShowWindowDescriptor &&descriptor);
void setupEmojiColumn(const ShowWindowDescriptor &descriptor);
void addBottomAiStar(
not_null<Ui::RpWidget*> button,
not_null<Main::Session*> session);
void layout();
void toggleEmojiColumn();
void showEmojiColumn();
void hideEmojiColumn(bool skipResize = false);
void updateEditorVisibleTopBottom();
void setEmojiColumnInteractionActive(bool active);
[[nodiscard]] int emojiColumnWidth() const;
[[nodiscard]] int minimalWindowWidth() const;
[[nodiscard]] int minimalWindowWidthWithEmojiColumn() const;
[[nodiscard]] bool handleCloseRequest();
void finishCloseFromAcceptedEvent();
void finishClose();
[[nodiscard]] bool articleChanged();
[[nodiscard]] bool articleEmptyForDiscard();
[[nodiscard]] bool showCloseConfirmation();
[[nodiscard]] bool showDiscardConfirmation();
[[nodiscard]] bool confirmCancel();
void discard();
void submit();
std::unique_ptr<Window> _window;
std::shared_ptr<ChatHelpers::Show> _show;
std::shared_ptr<State> _state;
RichPage _initialPage;
object_ptr<Ui::RpWidget> _top = { nullptr };
object_ptr<Ui::RpWidget> _bottomFade = { nullptr };
object_ptr<Ui::RpWidget> _bottom = { nullptr };
object_ptr<Ui::ElasticScroll> _scroll = { nullptr };
QPointer<Widget> _editor;
object_ptr<Toolbar> _toolbar = { nullptr };
object_ptr<ToolbarPill> _discard = { nullptr };
object_ptr<ToolbarPill> _cancel = { nullptr };
object_ptr<ToolbarPill> _aiPill = { nullptr };
object_ptr<Ui::SendButton> _send = { nullptr };
object_ptr<ChatHelpers::TabbedSelector> _emojiColumn = { nullptr };
object_ptr<Ui::PlainShadow> _emojiColumnShadow = { nullptr };
object_ptr<ToolbarPill> _emojiColumnClose = { nullptr };
Fn<bool()> _discarded;
Fn<bool()> _cancelled;
Fn<bool()> _changedCancelled;
Fn<bool()> _confirmed;
Fn<void()> _closed;
base::weak_qptr<Ui::GenericBox> _closeConfirmation;
base::weak_qptr<Ui::GenericBox> _discardConfirmation;
rpl::lifetime _lifetime;
int _emojiColumnExtendedBy = 0;
bool _emojiColumnShown = false;
bool _emojiColumnInteractionActive = false;
bool _closingApproved = false;
bool _closedNotified = false;
};
WindowHost::Impl::Impl(ShowWindowDescriptor descriptor) {
setupWindow(std::move(descriptor));
}
WindowHost::Impl::~Impl() {
hideEmojiColumn(true);
}
void WindowHost::Impl::close() {
finishClose();
}
void WindowHost::Impl::activateClose() {
if (confirmCancel()) {
finishClose();
}
}
void WindowHost::Impl::setupWindow(ShowWindowDescriptor &&descriptor) {
const auto title = tr::lng_article_editor_title(tr::now);
if (!descriptor.state) {
descriptor.state = std::make_shared<State>();
}
_state = descriptor.state;
_initialPage = _state->richPage();
_window = std::make_unique<Window>();
const auto window = _window.get();
window->setCloseRequestHandler([=] {
return handleCloseRequest();
});
_show = std::make_shared<WindowContext>(window, descriptor.session);
if (descriptor.showCreated) {
descriptor.showCreated(_show);
}
window->setTitle(title);
window->setWindowTitle(title);
window->setMinimumSize(st::ivEditorWindowMinSize);
window->setGeometry(DefaultWindowGeometry());
window->body()->paintRequest() | rpl::on_next([=](QRect clip) {
QPainter(window->body().get()).fillRect(clip, st::windowBg);
}, window->body()->lifetime());
_top = object_ptr<Ui::RpWidget>(window->body().get());
_top->setAttribute(Qt::WA_TransparentForMouseEvents);
_top->paintRequest() | rpl::on_next([=] {
auto p = QPainter(_top.data());
Dialogs::PaintTopFade(
p,
_top->width(),
_top->height(),
st::windowBg->c);
}, _top->lifetime());
_bottomFade = object_ptr<Ui::RpWidget>(window->body().get());
_bottomFade->setAttribute(Qt::WA_TransparentForMouseEvents);
_bottomFade->paintRequest() | rpl::on_next([=] {
auto p = QPainter(_bottomFade.data());
Dialogs::PaintBottomFade(
p,
_bottomFade->width(),
_bottomFade->height(),
st::windowBg->c);
}, _bottomFade->lifetime());
_bottom = object_ptr<Ui::RpWidget>(window->body().get());
const auto hasRequestMedia = static_cast<bool>(descriptor.requestMedia);
_scroll = object_ptr<Ui::ElasticScroll>(window->body().get(), st::boxScroll);
using OverscrollType = Ui::ElasticScroll::OverscrollType;
_scroll->setOverscrollTypes(OverscrollType::Real, OverscrollType::Real);
const auto scroll = _scroll.data();
scroll->setOverscrollBg(st::windowBg->c);
style::PaletteChanged(
) | rpl::on_next([=] {
scroll->setOverscrollBg(st::windowBg->c);
}, scroll->lifetime());
_editor = _scroll->setOwnedWidget(object_ptr<Widget>(
_scroll.data(),
WidgetServices{
.session = descriptor.session,
.show = _show,
.outer = window->body(),
.customEmojiPaused = [show = _show] {
return show->paused(ChatHelpers::PauseReason::Layer);
},
.requestMedia = std::move(descriptor.requestMedia),
.applyPreparedMedia = std::move(descriptor.applyPreparedMedia),
.requestPhotoEditSource
= std::move(descriptor.requestPhotoEditSource),
.replacePhotoWithList
= std::move(descriptor.replacePhotoWithList),
.mediaUploadState = std::move(descriptor.mediaUploadState),
.cancelMediaUpload = std::move(descriptor.cancelMediaUpload),
.addMediaAndGroupWithBlock
= std::move(descriptor.addMediaAndGroupWithBlock),
.imeCompositionStarts = window->imeCompositionStarts(),
},
descriptor.peer,
descriptor.state,
std::move(descriptor.showLimitToast)));
const auto editor = not_null<Widget*>(_editor.data());
if (descriptor.editorCreated) {
descriptor.editorCreated(editor);
}
const auto body = QPointer<QWidget>(window->body().get());
setupEmojiColumn(descriptor);
_toolbar = object_ptr<Toolbar>(
window->body().get(),
editor,
body,
hasRequestMedia,
std::move(descriptor.requestMap),
[=] {
_toolbar->hideShownTooltip();
toggleEmojiColumn();
},
descriptor.session);
window->setMinimumWidth(minimalWindowWidth());
if (descriptor.discarded) {
_discard = object_ptr<ToolbarPill>(
_bottom.data(),
st::ivEditorPillShadow);
const auto button = _discard->addButton(
st::ivEditorBottomDiscardButton,
&st::ivEditorBottomDiscardIcon,
&st::ivEditorBottomDiscardIcon,
ToolbarButtonState::Inactive);
button->setAccessibleName(tr::lng_record_lock_discard(tr::now));
button->setClickedCallback([=] {
discard();
});
}
if (descriptor.submitType == ShowWindowDescriptor::SubmitType::Save) {
_cancel = object_ptr<ToolbarPill>(
_bottom.data(),
st::ivEditorPillShadow);
const auto button = _cancel->addButton(
st::ivEditorBottomCancelButton,
&st::ivEditorBottomCancelIcon,
&st::ivEditorBottomCancelIcon,
ToolbarButtonState::Inactive);
button->setAccessibleName(tr::lng_cancel(tr::now));
button->setClickedCallback([=] {
if (confirmCancel()) {
finishClose();
}
});
}
{
const auto session = descriptor.session;
_aiPill = object_ptr<ToolbarPill>(
_bottom.data(),
st::ivEditorPillShadow);
auto owned = object_ptr<HistoryView::Controls::ComposeAiButton>(
_aiPill.data(),
st::ivEditorToolbarButton,
st::ivEditorBottomAiIcon,
st::ivEditorBottomAiStar1,
st::ivEditorBottomAiStar2);
const auto button = owned.data();
_aiPill->addButton(std::move(owned), st::ivEditorToolbarButton);
button->setAccessibleName(tr::lng_ai_compose_title(tr::now));
button->setClickedCallback([=] {
if (!session->premium()) {
const auto show = _show;
show->showToast({
.text = tr::lng_article_premium_required(
tr::now,
lt_link,
tr::link(tr::bold(
tr::lng_article_premium_required_link(
tr::now))),
tr::marked),
.filter = [=](
const ClickHandlerPtr &handler,
Qt::MouseButton button) {
if (button != Qt::LeftButton) {
return false;
}
if (show && show->valid()) {
ShowPremiumPreviewToBuy(
show,
PremiumFeature::RichFormatting);
} else if (const auto window
= session->tryResolveWindow(nullptr)) {
ShowPremiumPreviewToBuy(
window,
PremiumFeature::RichFormatting);
}
return true;
},
.icon = &st::settingsToastStarIcon,
.adaptive = true,
.duration = Ui::Toast::kDefaultDuration * 2,
});
return;
}
const auto editor = _editor;
if (editor && editor->hasActiveSelection()) {
auto span = editor->textSpanForCurrentSelection();
if (!span.text.isEmpty()) {
HistoryView::Controls::ShowComposeAiBox(_show, {
.session = session,
.text = std::move(span),
.apply = [editor](TextWithEntities result) {
if (!editor || result.text.isEmpty()) {
return;
}
editor->replaceCurrentSelectionWithText(
std::move(result));
},
});
return;
}
auto source = editor->richPageForCurrentSelection();
if (source && !source->blocks.empty()) {
HistoryView::Controls::ShowComposeAiBox(_show, {
.session = session,
.richSource = std::move(source),
.applyRich = [editor](
std::shared_ptr<const RichPage> page) {
if (!editor || !page || page->blocks.empty()) {
return;
}
editor->replaceCurrentSelectionWithRichPage(
std::move(page));
},
});
return;
}
}
ShowCreateAiBox(_show, {
.session = session,
.applyToPage = [editor](std::shared_ptr<const RichPage> page) {
if (!editor || !page || page->blocks.empty()) {
return;
}
editor->insertPreparedBlocks(page->blocks);
},
});
});
addBottomAiStar(button, session);
}
const auto save = (descriptor.submitType
== ShowWindowDescriptor::SubmitType::Save);
_send = object_ptr<Ui::SendButton>(
_bottom.data(),
save ? st::ivEditorBottomSaveSend : st::ivEditorBottomSend);
const auto raw = _send.data();
raw->setAccessibleName(SubmitText(descriptor));
raw->setClickedCallback([=] { submit(); });
raw->show();
if (descriptor.setupSubmitButton) {
descriptor.setupSubmitButton(
not_null<Ui::RpWidget*>(raw));
}
{
const auto lockIcon = &st::ivEditorSendLockIcon;
const auto lockPadding = st::ivEditorSendLockBadgePadding;
const auto lock = Ui::CreateChild<Ui::RpWidget>(raw);
lock->setAttribute(Qt::WA_TransparentForMouseEvents);
lock->resize(
lockIcon->width() + 2 * lockPadding,
lockIcon->height() + 2 * lockPadding);
lock->move(st::ivEditorSendLockBadgePosition);
lock->paintRequest() | rpl::on_next([=] {
auto p = QPainter(lock);
auto hq = PainterHighQualityEnabler(p);
const auto border = st::lineWidth;
auto pen = QPen(st::windowBg);
pen.setWidth(border);
p.setPen(pen);
p.setBrush(st::windowBgActive);
const auto half = border / 2.;
p.drawEllipse(QRectF(lock->rect()).marginsRemoved(
QMarginsF(half, half, half, half)));
lockIcon->paint(p, lockPadding, lockPadding, lock->width());
}, lock->lifetime());
lock->hide();
const auto session = descriptor.session;
const auto state = _state;
const auto editor = not_null<Widget*>(_editor.data());
const auto premium = lock->lifetime().make_state<bool>(true);
const auto refresh = [=] {
const auto locked = !*premium
&& Iv::RichPageUsesPremiumFormatting(state->richPage());
lock->setVisible(locked);
if (locked) {
lock->raise();
}
};
Data::AmPremiumValue(
session
) | rpl::on_next([=](bool value) {
*premium = value;
refresh();
}, lock->lifetime());
editor->toolbarStateChanges() | rpl::on_next([=] {
refresh();
}, lock->lifetime());
}
_discarded = std::move(descriptor.discarded);
_cancelled = std::move(descriptor.cancelled);
_changedCancelled = std::move(descriptor.changedCancelled);
_confirmed = std::move(descriptor.confirmed);
_closed = std::move(descriptor.closed);
window->body()->sizeValue() | rpl::on_next([=](QSize) {
layout();
}, _lifetime);
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue()
) | rpl::on_next([=](int, int) {
updateEditorVisibleTopBottom();
}, _lifetime);
window->events() | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto event = static_cast<QKeyEvent*>(e.get());
if (event->key() == Qt::Key_Escape) {
event->accept();
if (confirmCancel()) {
finishClose();
}
}
}
}, _lifetime);
layout();
_top->show();
_bottomFade->show();
_bottom->show();
_scroll->show();
_top->raise();
_bottomFade->raise();
_toolbar->show();
_toolbar->raise();
_bottom->raise();
window->show();
editor->activateInitialNode();
}
void WindowHost::Impl::addBottomAiStar(
not_null<Ui::RpWidget*> button,
not_null<Main::Session*> session) {
const auto factor = style::DevicePixelRatio();
const auto side = st::ivEditorToolbarPremiumStarSize;
const auto size = QSize(side, side);
auto image = QImage(
size * factor,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
auto svg = QSvgRenderer(
Ui::Premium::ColorizedSvg(Ui::Premium::ButtonGradientStops()));
svg.render(&p, QRectF(QPointF(), QSizeF(size)));
}
const auto star = Ui::CreateChild<Ui::RpWidget>(button.get());
star->setAttribute(Qt::WA_TransparentForMouseEvents);
star->resize(size);
star->paintRequest() | rpl::on_next([=] {
auto p = QPainter(star);
p.drawImage(0, 0, image);
}, star->lifetime());
const auto skip = st::ivEditorToolbarPremiumStarSkip;
star->move(
button->width() - star->width() - skip.x(),
button->height() - star->height() - skip.y());
star->showOn(Data::AmPremiumValue(session)
| rpl::map([](bool premium) { return !premium; }));
star->raise();
}
void WindowHost::Impl::setupEmojiColumn(const ShowWindowDescriptor &descriptor) {
using Selector = ChatHelpers::TabbedSelector;
_emojiColumnShadow = object_ptr<Ui::PlainShadow>(_window->body().get());
_emojiColumnShadow->hide();
_emojiColumnClose = object_ptr<ToolbarPill>(
_window->body().get(),
st::ivEditorPillShadow);
_emojiColumnClose->addButton(
st::ivEditorEmojiColumnClose,
&st::ivEditorEmojiColumnCloseIcon,
&st::ivEditorEmojiColumnCloseIconOver,
ToolbarButtonState::Inactive
)->setClickedCallback([=] {
hideEmojiColumn();
});
_emojiColumnClose->hide();
const auto closeNatural = _emojiColumnClose->naturalSize();
const auto closeMargins = _emojiColumnClose->shadowMargins();
const auto closeVisibleWidth = closeNatural.width()
- closeMargins.left()
- closeMargins.right();
const auto searchRightReserved = st::ivEditorEmojiColumnCloseScrollSkip
+ closeVisibleWidth
+ st::emojiPanRadius
+ st::defaultEmojiPan.searchMargin.left();
_emojiColumn = object_ptr<Selector>(
_window->body().get(),
ChatHelpers::TabbedSelectorDescriptor{
.show = _show,
.st = st::defaultEmojiPan,
.level = ChatHelpers::PauseReason::Layer,
.mode = Selector::Mode::EmojiOnly,
.features = {
.stickersSettings = false,
.openStickerSets = false,
},
.searchRightReserved = searchRightReserved,
});
_emojiColumn->hide();
_emojiColumn->setCurrentPeer(descriptor.peer);
_emojiColumn->emojiChosen(
) | rpl::on_next([=](ChatHelpers::EmojiChosen data) {
if (_editor) {
_editor->insertEmoji(data.emoji);
}
}, _lifetime);
_emojiColumn->customEmojiChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
const auto document = data.document;
if (!IsEmojiDocument(document)) {
return;
}
if (document->isPremiumEmoji()
&& !descriptor.session->premium()
&& !Data::AllowEmojiWithoutPremium(descriptor.peer, document)) {
ShowPremiumPreviewBox(
_show,
PremiumFeature::AnimatedEmoji);
} else if (_editor) {
_editor->insertCustomEmoji(document);
}
}, _lifetime);
}
void WindowHost::Impl::layout() {
if (!_window || !_top || !_bottomFade || !_bottom || !_toolbar || !_editor
|| !_emojiColumn || !_emojiColumnShadow || !_emojiColumnClose) {
return;
}
const auto width = _window->body()->width();
const auto height = _window->body()->height();
const auto padding = st::ivEditorBottomControlsPadding;
const auto emojiWidth = _emojiColumnShown ? emojiColumnWidth() : 0;
const auto editorWidth = std::max(width - emojiWidth, 0);
_editor->setContentMaxWidth(_toolbar->contentMaxWidth());
const auto toolbarHeight = _toolbar->resizeGetHeight(editorWidth);
auto buttonsHeight = _send->height();
if (_cancel) {
buttonsHeight = std::max(
buttonsHeight,
_cancel->naturalSize().height());
}
if (_discard) {
buttonsHeight = std::max(
buttonsHeight,
_discard->naturalSize().height());
}
const auto bottomHeight = padding.top() + buttonsHeight + padding.bottom();
const auto buttonsTop = padding.top();
_top->setGeometry(0, 0, editorWidth, toolbarHeight);
_toolbar->setGeometry(0, 0, editorWidth, toolbarHeight);
_toolbar->raise();
_bottomFade->setGeometry(0, height - bottomHeight, editorWidth, bottomHeight);
_bottom->setGeometry(0, height - bottomHeight, editorWidth, bottomHeight);
const auto column = _editor->articleColumnForWidth(editorWidth);
const auto fitsArticle = (column.width >= _toolbar->contentMaxWidth());
const auto right = fitsArticle
? (column.left + column.width)
: editorWidth;
const auto left = fitsArticle ? column.left : 0;
const auto leftPill = _discard
? _discard.data()
: _cancel.data();
const auto shadowSkipRight = leftPill
? leftPill->shadowMargins().right()
: 0;
const auto shadowSkipTop = leftPill
? leftPill->shadowMargins().top()
: 0;
_send->moveToLeft(
right - shadowSkipRight - _send->width(),
buttonsTop + shadowSkipTop,
editorWidth);
if (leftPill) {
leftPill->moveToLeft(
right
- shadowSkipRight
- _send->width()
- st::ivEditorToolbarGroupsSkip
- leftPill->naturalSize().width(),
buttonsTop,
editorWidth);
}
if (_aiPill) {
_aiPill->moveToLeft(
left - _aiPill->shadowMargins().left(),
buttonsTop,
editorWidth);
}
auto bottomMask = QRegion();
const auto addMask = [&](Ui::RpWidget *widget) {
if (widget && !widget->isHidden()) {
bottomMask += widget->geometry();
}
};
addMask(_discard.data());
addMask(_cancel.data());
addMask(_aiPill.data());
addMask(_send.data());
if (bottomMask.isEmpty()) {
_bottom->clearMask();
} else {
_bottom->setMask(bottomMask);
}
_scroll->setGeometry(0, 0, editorWidth, std::max(height, 1));
_scroll->setBarTopInset(toolbarHeight);
_scroll->setBarBottomInset(bottomHeight);
if (_emojiColumnShown) {
_emojiColumn->setGeometry(
editorWidth,
0,
emojiWidth,
height);
_emojiColumnShadow->setGeometry(editorWidth, 0, st::lineWidth, height);
_emojiColumnShadow->show();
_emojiColumnShadow->raise();
const auto closeNatural = _emojiColumnClose->naturalSize();
const auto closeMargins = _emojiColumnClose->shadowMargins();
const auto searchCenterY = st::defaultEmojiPan.searchMargin.top()
+ st::defaultTabbedSearch.height / 2;
const auto closeVisibleHeight = closeNatural.height()
- closeMargins.top()
- closeMargins.bottom();
const auto closeX = width
- st::emojiScroll.width
- st::ivEditorEmojiColumnCloseScrollSkip
- closeNatural.width()
+ closeMargins.right();
const auto closeY = searchCenterY
- closeVisibleHeight / 2
- closeMargins.top();
_emojiColumnClose->moveToLeft(closeX, closeY, width);
_emojiColumnClose->show();
_emojiColumnClose->raise();
} else {
_emojiColumn->hide();
_emojiColumnShadow->hide();
_emojiColumnClose->hide();
}
_editor->setTopContentPadding(toolbarHeight);
_editor->setBottomContentPadding(bottomHeight);
_editor->resizeToWidth(std::max(_scroll->width(), 1));
updateEditorVisibleTopBottom();
}
void WindowHost::Impl::toggleEmojiColumn() {
if (_emojiColumnShown) {
hideEmojiColumn();
} else {
showEmojiColumn();
}
}
void WindowHost::Impl::showEmojiColumn() {
if (_emojiColumnShown || !_window || !_emojiColumn) {
return;
}
const auto window = not_null<Window*>(_window.get());
const auto wanted = emojiColumnWidth();
const auto minimal = std::max(
minimalWindowWidthWithEmojiColumn() - window->width(),
0);
_emojiColumnExtendedBy = 0;
if (!window->isMaximized() && !window->isFullScreen()) {
if (CanExtendNoMove(window, wanted)) {
_emojiColumnExtendedBy = TryToExtendWidthBy(window, wanted);
} else if (minimal > 0 && CanExtendNoMove(window, minimal)) {
_emojiColumnExtendedBy = TryToExtendWidthBy(window, minimal);
} else if (window->width() < minimalWindowWidthWithEmojiColumn()) {
_emojiColumnExtendedBy = TryToExtendWidthBy(window, minimal);
}
}
window->setMinimumWidth(minimalWindowWidthWithEmojiColumn());
_emojiColumnShown = true;
if (_toolbar) {
_toolbar->setEmojiColumnOpen(true);
}
_emojiColumn->setRoundRadius(0);
setEmojiColumnInteractionActive(true);
_emojiColumn->showStarted();
_emojiColumn->show();
layout();
_emojiColumn->afterShown();
}
void WindowHost::Impl::hideEmojiColumn(bool skipResize) {
if (!_emojiColumnShown || !_window || !_emojiColumn) {
return;
}
const auto window = not_null<Window*>(_window.get());
_emojiColumn->beforeHiding();
_emojiColumn->hide();
_emojiColumn->hideFinished();
_emojiColumnShown = false;
if (_toolbar) {
_toolbar->setEmojiColumnOpen(false);
}
window->setMinimumWidth(minimalWindowWidth());
setEmojiColumnInteractionActive(false);
if (!skipResize
&& _emojiColumnExtendedBy > 0
&& !window->isMaximized()
&& !window->isFullScreen()) {
window->resize(
window->width() - _emojiColumnExtendedBy,
window->height());
}
_emojiColumnExtendedBy = 0;
layout();
}
void WindowHost::Impl::updateEditorVisibleTopBottom() {
if (!_scroll || !_editor) {
return;
}
const auto scrollTop = _scroll->scrollTop();
_editor->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
}
void WindowHost::Impl::setEmojiColumnInteractionActive(bool active) {
if (_emojiColumnInteractionActive == active || !_editor) {
_emojiColumnInteractionActive = active;
return;
}
_emojiColumnInteractionActive = active;
_editor->setInlineFieldExternalInteractionActive(active);
}
int WindowHost::Impl::emojiColumnWidth() const {
const auto &pan = st::defaultEmojiPan;
const auto columns = 8;
const auto innerWidth = columns * pan.desiredSize;
return innerWidth
+ pan.padding.left()
+ pan.padding.right()
- pan.margin.left()
- pan.margin.right()
+ st::emojiPanRadius
+ st::emojiScroll.width;
}
int WindowHost::Impl::minimalWindowWidth() const {
const auto base = st::ivEditorWindowMinSize.width();
return _toolbar ? std::max(base, _toolbar->minimalWidth()) : base;
}
int WindowHost::Impl::minimalWindowWidthWithEmojiColumn() const {
return minimalWindowWidth() + emojiColumnWidth();
}
bool WindowHost::Impl::handleCloseRequest() {
if (_closingApproved) {
return true;
}
if (confirmCancel()) {
finishCloseFromAcceptedEvent();
return true;
}
return false;
}
void WindowHost::Impl::finishCloseFromAcceptedEvent() {
_closingApproved = true;
if (_closedNotified) {
return;
}
_closedNotified = true;
hideEmojiColumn(true);
if (const auto closed = base::take(_closed)) {
crl::on_main(closed);
}
}
void WindowHost::Impl::finishClose() {
finishCloseFromAcceptedEvent();
if (_window) {
_window->close();
}
}
bool WindowHost::Impl::articleChanged() {
if (!_state) {
return false;
}
if (_editor
&& (_editor->commitInlineFieldForClose()
== State::ApplyResult::Failed)) {
return true;
}
return !RichPagesEqual(_initialPage, _state->richPage());
}
bool WindowHost::Impl::articleEmptyForDiscard() {
if (!_state) {
return true;
} else if (_editor
&& (_editor->commitInlineFieldForClose()
== State::ApplyResult::Failed)) {
return false;
}
return _state->articleEmpty();
}
bool WindowHost::Impl::showCloseConfirmation() {
if (_closeConfirmation) {
return true;
} else if (!_show || !_show->valid()) {
return false;
}
const auto window = QPointer<Window>(_window.get());
const auto close = [=](Fn<void()> closeBox) {
closeBox();
if (!window) {
return;
} else if (!_cancelled || _cancelled()) {
finishClose();
}
};
_closeConfirmation = _show->show(Ui::MakeConfirmBox({
.text = tr::lng_theme_editor_sure_close(),
.confirmed = close,
.confirmText = tr::lng_close(),
}));
return true;
}
bool WindowHost::Impl::showDiscardConfirmation() {
if (_discardConfirmation) {
return true;
} else if (!_show || !_show->valid()) {
return false;
}
const auto window = QPointer<Window>(_window.get());
const auto discard = [=](Fn<void()> closeBox) {
closeBox();
if (!window) {
return;
} else if (!_discarded || _discarded()) {
finishClose();
}
};
_discardConfirmation = _show->show(Ui::MakeConfirmBox({
.text = tr::lng_iv_editor_discard_draft_sure(tr::now),
.confirmed = discard,
.confirmText = tr::lng_record_lock_discard(),
.confirmStyle = &st::attentionBoxButton,
}));
return true;
}
bool WindowHost::Impl::confirmCancel() {
if (!articleChanged()) {
return !_cancelled || _cancelled();
} else if (_changedCancelled) {
return _changedCancelled();
}
return showCloseConfirmation()
? false
: (!_cancelled || _cancelled());
}
void WindowHost::Impl::discard() {
if (!_discard) {
return;
} else if (articleEmptyForDiscard()) {
if (!_discarded || _discarded()) {
finishClose();
}
} else {
[[maybe_unused]] const auto shown = showDiscardConfirmation();
}
}
void WindowHost::Impl::submit() {
if (!_editor) {
return;
}
if (_editor->commitInlineField() == State::ApplyResult::Failed) {
return;
}
if (!_confirmed || _confirmed()) {
finishClose();
}
}
WindowHost::WindowHost(ShowWindowDescriptor descriptor)
: _impl(std::make_unique<Impl>(std::move(descriptor))) {
}
WindowHost::~WindowHost() = default;
void WindowHost::close() {
_impl->close();
}
void WindowHost::activateClose() {
_impl->activateClose();
}
std::unique_ptr<WindowHost> ShowWindow(ShowWindowDescriptor descriptor) {
if (!descriptor.state) {
descriptor.state = std::make_shared<State>();
}
return std::unique_ptr<WindowHost>(new WindowHost(std::move(descriptor)));
}
void SetupToolbarButton(
not_null<Ui::IconButton*> button,
ToolbarButtonState state,
anim::type animated) {
const auto disabled = (state == ToolbarButtonState::Disabled);
const auto active = (state == ToolbarButtonState::Active);
button->setAttribute(Qt::WA_TransparentForMouseEvents, disabled);
button->setPointerCursor(!disabled);
if (active) {
button->setRippleColorOverride(&st::lightButtonBgOver);
button->setForceRippled(true, animated);
button->setIconColorOverride(st::windowActiveTextFg->c);
} else {
button->setForceRippled(false, animated);
button->setRippleColorOverride(nullptr);
button->setIconColorOverride(disabled
? std::optional<QColor>(st::windowSubTextFg->c)
: std::nullopt);
}
}
} // namespace Iv::Editor