mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-07 20:26:25 +00:00
1726 lines
47 KiB
C++
1726 lines
47 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/unique_qptr.h"
|
|
#include "base/weak_qptr.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 "history/history_item.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_widget.h"
|
|
#include "iv/editor/iv_editor_window.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "menu/menu_send_details.h"
|
|
#include "ui/boxes/confirm_box.h"
|
|
#include "ui/delayed_activation.h"
|
|
#include "ui/rect_part.h"
|
|
#include "ui/rp_widget.h"
|
|
#include "ui/wrap/fade_wrap.h"
|
|
#include "ui/ui_utility.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "ui/widgets/popup_menu.h"
|
|
#include "ui/widgets/scroll_area.h"
|
|
#include "ui/widgets/tooltip.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/QScreen>
|
|
|
|
#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 ToolbarGroupId : uchar {
|
|
UndoRedo,
|
|
TextModifiers,
|
|
HeadingLinkMath,
|
|
BlockInsertion,
|
|
Lists,
|
|
Insertions,
|
|
};
|
|
|
|
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]] auto SimpleTooltipFactory(QString text) {
|
|
return [text = std::move(text)] {
|
|
return rpl::single(TextWithEntities::Simple(text));
|
|
};
|
|
}
|
|
|
|
[[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_article_insert_map(tr::now);
|
|
case ToolbarActionId::Divider:
|
|
return tr::lng_article_insert_divider(tr::now);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
[[nodiscard]] QString OverflowLabel() {
|
|
return tr::lng_profile_action_short_more(tr::now);
|
|
}
|
|
|
|
[[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;
|
|
}
|
|
|
|
[[nodiscard]] std::optional<Widget::ToolbarFormatAction> ToolbarFormatAction(
|
|
ToolbarActionId action) {
|
|
switch (action) {
|
|
case ToolbarActionId::Undo:
|
|
return Widget::ToolbarFormatAction::Undo;
|
|
case ToolbarActionId::Redo:
|
|
return Widget::ToolbarFormatAction::Redo;
|
|
case ToolbarActionId::Bold:
|
|
return Widget::ToolbarFormatAction::Bold;
|
|
case ToolbarActionId::Italic:
|
|
return Widget::ToolbarFormatAction::Italic;
|
|
case ToolbarActionId::Underline:
|
|
return Widget::ToolbarFormatAction::Underline;
|
|
case ToolbarActionId::StrikeOut:
|
|
return Widget::ToolbarFormatAction::StrikeOut;
|
|
case ToolbarActionId::Spoiler:
|
|
return Widget::ToolbarFormatAction::Spoiler;
|
|
case ToolbarActionId::Subscript:
|
|
return Widget::ToolbarFormatAction::Subscript;
|
|
case ToolbarActionId::Superscript:
|
|
return Widget::ToolbarFormatAction::Superscript;
|
|
case ToolbarActionId::Marked:
|
|
return Widget::ToolbarFormatAction::Marked;
|
|
case ToolbarActionId::PlainText:
|
|
return Widget::ToolbarFormatAction::PlainText;
|
|
case ToolbarActionId::Link:
|
|
return Widget::ToolbarFormatAction::Link;
|
|
case ToolbarActionId::Math:
|
|
return Widget::ToolbarFormatAction::Math;
|
|
case ToolbarActionId::Heading:
|
|
case ToolbarActionId::Blockquote:
|
|
case ToolbarActionId::Pullquote:
|
|
case ToolbarActionId::CodeBlock:
|
|
case ToolbarActionId::Details:
|
|
case ToolbarActionId::OrderedList:
|
|
case ToolbarActionId::BulletList:
|
|
case ToolbarActionId::TaskList:
|
|
case ToolbarActionId::Attach:
|
|
case ToolbarActionId::Table:
|
|
case ToolbarActionId::Location:
|
|
case ToolbarActionId::Divider:
|
|
return std::nullopt;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
struct ToolbarButton {
|
|
ToolbarActionId action = ToolbarActionId::Undo;
|
|
ToolbarGroupId group = ToolbarGroupId::UndoRedo;
|
|
QString label;
|
|
Fn<rpl::producer<TextWithEntities>()> tooltipFactory;
|
|
const style::icon *icon = nullptr;
|
|
const style::icon *iconOver = nullptr;
|
|
Fn<void()> callback;
|
|
object_ptr<Ui::RippleButton> widget = { nullptr };
|
|
object_ptr<Ui::FadeWrapScaled<Ui::IconButton>> wrap = { nullptr };
|
|
Ui::RippleButton *button = nullptr;
|
|
Ui::IconButton *iconButton = nullptr;
|
|
bool stateInitialized = false;
|
|
bool shown = false;
|
|
bool enabled = false;
|
|
|
|
[[nodiscard]] Ui::RpWidget *host() const {
|
|
return wrap
|
|
? static_cast<Ui::RpWidget*>(wrap.data())
|
|
: static_cast<Ui::RpWidget*>(widget.data());
|
|
}
|
|
};
|
|
|
|
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);
|
|
|
|
int resizeGetHeight(int width) override;
|
|
void hideShownTooltip();
|
|
|
|
protected:
|
|
bool eventFilter(QObject *object, QEvent *event) override;
|
|
void paintEvent(QPaintEvent *e) override;
|
|
|
|
private:
|
|
not_null<Ui::RippleButton*> addAction(
|
|
ToolbarActionId action,
|
|
ToolbarGroupId group,
|
|
const style::icon *icon,
|
|
const style::icon *iconOver,
|
|
Fn<void()> callback,
|
|
bool wrapped = false);
|
|
void addButtons();
|
|
void addEmojiButton();
|
|
void fillHeadingMenu(not_null<Ui::PopupMenu*> menu);
|
|
void showHeadingMenu(not_null<Ui::IconButton*> button);
|
|
void showOverflowMenu(not_null<Ui::IconButton*> button);
|
|
void showTooltip(not_null<Ui::RippleButton*> button);
|
|
void hideTooltip();
|
|
void updateTooltipGeometry();
|
|
void refreshButtonText(ToolbarButton &button);
|
|
void updateFromEditorState();
|
|
void updateOverflowButtonState();
|
|
[[nodiscard]] Widget::ToolbarActionState actionState(
|
|
const ToolbarButton &button) const;
|
|
[[nodiscard]] ToolbarButton *buttonData(not_null<Ui::RippleButton*> button);
|
|
|
|
const QPointer<Widget> _editor;
|
|
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 = {};
|
|
std::vector<ToolbarButton> _buttons;
|
|
ToolbarButton _emoji;
|
|
object_ptr<Ui::IconButton> _overflow = { nullptr };
|
|
std::vector<int> _overflowActions;
|
|
std::vector<int> _dividerXs;
|
|
base::unique_qptr<Ui::ImportantTooltip> _tooltip;
|
|
base::unique_qptr<Ui::PopupMenu> _menu;
|
|
Ui::RippleButton *_hovered = nullptr;
|
|
bool _overflowStateInitialized = false;
|
|
bool _overflowEnabled = false;
|
|
|
|
};
|
|
|
|
[[nodiscard]] QRect DefaultWindowGeometry() {
|
|
const auto padding = st::ivEditorBodyPadding;
|
|
const auto size = QSize(
|
|
std::max(
|
|
st::ivEditorWindowMinSize.width(),
|
|
(st::defaultMarkdown.pageMaxWidth * 3) / 4
|
|
+ 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)
|
|
: Ui::RpWidget(parent)
|
|
, _editor(editor.get())
|
|
, _tooltipParent(std::move(tooltipParent))
|
|
, _hasRequestMedia(hasRequestMedia)
|
|
, _requestMap(std::move(requestMap))
|
|
, _toggleEmoji(std::move(toggleEmoji)) {
|
|
setMouseTracking(true);
|
|
_buttons.reserve(25);
|
|
addButtons();
|
|
addEmojiButton();
|
|
_overflow = object_ptr<Ui::IconButton>(this, st::ivEditorToolbarOverflowButton);
|
|
_overflow->setAccessibleName(OverflowLabel());
|
|
_overflow->setIsMenuButton(true);
|
|
_overflow->setClickedCallback([=] {
|
|
hideTooltip();
|
|
showOverflowMenu(not_null<Ui::IconButton*>(_overflow.data()));
|
|
});
|
|
_overflow->hide();
|
|
_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::RippleButton*> Toolbar::addAction(
|
|
ToolbarActionId action,
|
|
ToolbarGroupId group,
|
|
const style::icon *icon,
|
|
const style::icon *iconOver,
|
|
Fn<void()> callback,
|
|
bool wrapped) {
|
|
auto data = ToolbarButton{
|
|
.action = action,
|
|
.group = group,
|
|
.label = ToolbarActionLabel(action),
|
|
.tooltipFactory = SimpleTooltipFactory(ToolbarActionLabel(action)),
|
|
.icon = icon,
|
|
.iconOver = iconOver,
|
|
.callback = std::move(callback),
|
|
};
|
|
Assert(icon != nullptr);
|
|
auto button = object_ptr<Ui::IconButton>(this, st::ivEditorToolbarButton);
|
|
data.iconButton = button.data();
|
|
data.button = button.data();
|
|
data.iconButton->setIconOverride(icon, iconOver ? iconOver : icon);
|
|
if (wrapped) {
|
|
data.wrap = object_ptr<Ui::FadeWrapScaled<Ui::IconButton>>(
|
|
this,
|
|
std::move(button));
|
|
data.wrap->toggle(false, anim::type::instant);
|
|
} else {
|
|
data.widget = std::move(button);
|
|
}
|
|
const auto raw = not_null<Ui::RippleButton*>(data.button);
|
|
raw->setAccessibleName(data.label);
|
|
raw->setClickedCallback([=] {
|
|
if (const auto stored = buttonData(raw)) {
|
|
if (stored->callback) {
|
|
stored->callback();
|
|
}
|
|
}
|
|
});
|
|
raw->installEventFilter(this);
|
|
_buttons.push_back(std::move(data));
|
|
refreshButtonText(_buttons.back());
|
|
return raw;
|
|
}
|
|
|
|
void Toolbar::addButtons() {
|
|
const auto insert = [=](State::InsertAction action) {
|
|
if (_editor) {
|
|
_editor->insertBlock(action);
|
|
}
|
|
};
|
|
const auto insertType = [=](State::InsertBlockType type) {
|
|
insert({ .type = type });
|
|
};
|
|
const auto addEditorAction = [=](
|
|
ToolbarActionId action,
|
|
ToolbarGroupId group,
|
|
const style::icon *icon = nullptr,
|
|
const style::icon *iconOver = nullptr,
|
|
bool wrapped = false) {
|
|
return addAction(
|
|
action,
|
|
group,
|
|
icon,
|
|
iconOver,
|
|
[=] {
|
|
if (_editor) {
|
|
if (const auto mapped = ToolbarFormatAction(action)) {
|
|
_editor->applyToolbarFormatAction(*mapped);
|
|
}
|
|
}
|
|
},
|
|
wrapped);
|
|
};
|
|
const auto undo = addAction(
|
|
ToolbarActionId::Undo,
|
|
ToolbarGroupId::UndoRedo,
|
|
&st::ivEditorToolbarUndoIcon,
|
|
&st::ivEditorToolbarUndoIconOver,
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->performToolbarUndoRedo(false);
|
|
}
|
|
});
|
|
undo->setAccessibleName(ToolbarActionLabel(ToolbarActionId::Undo));
|
|
addAction(
|
|
ToolbarActionId::Redo,
|
|
ToolbarGroupId::UndoRedo,
|
|
&st::ivEditorToolbarRedoIcon,
|
|
&st::ivEditorToolbarRedoIconOver,
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->performToolbarUndoRedo(true);
|
|
}
|
|
},
|
|
true);
|
|
addEditorAction(
|
|
ToolbarActionId::Bold,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarBoldIcon,
|
|
&st::ivEditorToolbarBoldIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Italic,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarItalicIcon,
|
|
&st::ivEditorToolbarItalicIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Underline,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarUnderlineIcon,
|
|
&st::ivEditorToolbarUnderlineIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::StrikeOut,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarStrikeOutIcon,
|
|
&st::ivEditorToolbarStrikeOutIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Spoiler,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarSpoilerIcon,
|
|
&st::ivEditorToolbarSpoilerIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Subscript,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarSubscriptIcon,
|
|
&st::ivEditorToolbarSubscriptIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Superscript,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarSuperscriptIcon,
|
|
&st::ivEditorToolbarSuperscriptIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::Marked,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarMarkedIcon,
|
|
&st::ivEditorToolbarMarkedIcon);
|
|
addEditorAction(
|
|
ToolbarActionId::PlainText,
|
|
ToolbarGroupId::TextModifiers,
|
|
&st::ivEditorToolbarPlainTextIcon,
|
|
&st::ivEditorToolbarPlainTextIcon);
|
|
const auto heading = addAction(
|
|
ToolbarActionId::Heading,
|
|
ToolbarGroupId::HeadingLinkMath,
|
|
&st::ivEditorToolbarHeadingIcon,
|
|
&st::ivEditorToolbarHeadingIcon,
|
|
[] {});
|
|
if (const auto data = buttonData(heading)) {
|
|
data->callback = [=] {
|
|
showHeadingMenu(not_null<Ui::IconButton*>(
|
|
static_cast<Ui::IconButton*>(heading.get())));
|
|
};
|
|
}
|
|
static_cast<Ui::IconButton*>(heading.get())->setIsMenuButton(true);
|
|
heading->setClickedCallback([=] {
|
|
hideTooltip();
|
|
if (const auto data = buttonData(heading)) {
|
|
if (data->callback) {
|
|
data->callback();
|
|
}
|
|
}
|
|
});
|
|
addAction(
|
|
ToolbarActionId::Link,
|
|
ToolbarGroupId::HeadingLinkMath,
|
|
&st::ivEditorToolbarLinkIcon,
|
|
&st::ivEditorToolbarLinkIcon,
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->editLinkFromToolbar();
|
|
}
|
|
});
|
|
addAction(
|
|
ToolbarActionId::Math,
|
|
ToolbarGroupId::HeadingLinkMath,
|
|
&st::ivEditorToolbarMathIcon,
|
|
&st::ivEditorToolbarMathIcon,
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->editMathFromToolbar();
|
|
}
|
|
});
|
|
addAction(
|
|
ToolbarActionId::Blockquote,
|
|
ToolbarGroupId::BlockInsertion,
|
|
&st::ivEditorToolbarBlockquoteIcon,
|
|
&st::ivEditorToolbarBlockquoteIcon,
|
|
[=] { insertType(State::InsertBlockType::Blockquote); });
|
|
addAction(
|
|
ToolbarActionId::Pullquote,
|
|
ToolbarGroupId::BlockInsertion,
|
|
&st::ivEditorToolbarPullquoteIcon,
|
|
&st::ivEditorToolbarPullquoteIcon,
|
|
[=] { insertType(State::InsertBlockType::Pullquote); });
|
|
addAction(
|
|
ToolbarActionId::CodeBlock,
|
|
ToolbarGroupId::BlockInsertion,
|
|
&st::ivEditorToolbarCodeIcon,
|
|
&st::ivEditorToolbarCodeIcon,
|
|
[=] { insertType(State::InsertBlockType::Code); });
|
|
addAction(
|
|
ToolbarActionId::Details,
|
|
ToolbarGroupId::BlockInsertion,
|
|
&st::ivEditorToolbarDetailsIcon,
|
|
&st::ivEditorToolbarDetailsIcon,
|
|
[=] { insertType(State::InsertBlockType::Details); });
|
|
addAction(
|
|
ToolbarActionId::OrderedList,
|
|
ToolbarGroupId::Lists,
|
|
&st::ivEditorToolbarOrderedListIcon,
|
|
&st::ivEditorToolbarOrderedListIcon,
|
|
[=] { insertType(State::InsertBlockType::OrderedList); });
|
|
addAction(
|
|
ToolbarActionId::BulletList,
|
|
ToolbarGroupId::Lists,
|
|
&st::ivEditorToolbarBulletListIcon,
|
|
&st::ivEditorToolbarBulletListIcon,
|
|
[=] { insertType(State::InsertBlockType::BulletList); });
|
|
addAction(
|
|
ToolbarActionId::TaskList,
|
|
ToolbarGroupId::Lists,
|
|
&st::ivEditorToolbarTaskListIcon,
|
|
&st::ivEditorToolbarTaskListIcon,
|
|
[=] { insertType(State::InsertBlockType::TaskList); });
|
|
if (_hasRequestMedia) {
|
|
addAction(
|
|
ToolbarActionId::Attach,
|
|
ToolbarGroupId::Insertions,
|
|
&st::ivEditorToolbarAttachIcon,
|
|
&st::ivEditorToolbarAttachIcon,
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->requestMedia(std::nullopt);
|
|
}
|
|
});
|
|
}
|
|
addAction(
|
|
ToolbarActionId::Table,
|
|
ToolbarGroupId::Insertions,
|
|
&st::ivEditorToolbarTableIcon,
|
|
&st::ivEditorToolbarTableIcon,
|
|
[=] { insertType(State::InsertBlockType::Table); });
|
|
if (_requestMap) {
|
|
addAction(
|
|
ToolbarActionId::Location,
|
|
ToolbarGroupId::Insertions,
|
|
&st::ivEditorToolbarLocationIcon,
|
|
&st::ivEditorToolbarLocationIcon,
|
|
[=] {
|
|
if (_editor) {
|
|
const auto parent = _tooltipParent;
|
|
auto closeRequests = parent
|
|
? static_cast<Ui::RpWidget*>(parent.data())->death()
|
|
: rpl::never<>();
|
|
_requestMap(
|
|
not_null<Widget*>(_editor.data()),
|
|
parent,
|
|
std::move(closeRequests));
|
|
}
|
|
});
|
|
}
|
|
addAction(
|
|
ToolbarActionId::Divider,
|
|
ToolbarGroupId::Insertions,
|
|
&st::ivEditorToolbarDividerIcon,
|
|
&st::ivEditorToolbarDividerIcon,
|
|
[=] { insertType(State::InsertBlockType::Divider); });
|
|
}
|
|
|
|
void Toolbar::addEmojiButton() {
|
|
auto data = ToolbarButton{
|
|
.action = ToolbarActionId::Attach,
|
|
.group = ToolbarGroupId::Insertions,
|
|
.label = tr::lng_article_insert_emoji(tr::now),
|
|
.tooltipFactory = SimpleTooltipFactory(tr::lng_article_insert_emoji(tr::now)),
|
|
.icon = &st::ivEditorToolbarEmojiIcon,
|
|
.iconOver = &st::ivEditorToolbarEmojiIcon,
|
|
.callback = [=] {
|
|
if (_toggleEmoji) {
|
|
_toggleEmoji();
|
|
}
|
|
},
|
|
};
|
|
auto button = object_ptr<Ui::IconButton>(this, st::ivEditorToolbarButton);
|
|
data.iconButton = button.data();
|
|
data.button = button.data();
|
|
data.widget = std::move(button);
|
|
data.iconButton->setIconOverride(data.icon, data.iconOver);
|
|
data.button->setAccessibleName(data.label);
|
|
data.button->setClickedCallback([=] {
|
|
if (_emoji.callback) {
|
|
_emoji.callback();
|
|
}
|
|
});
|
|
data.button->installEventFilter(this);
|
|
_emoji = std::move(data);
|
|
}
|
|
|
|
void Toolbar::fillHeadingMenu(not_null<Ui::PopupMenu*> menu) {
|
|
for (const auto level : std::array{ 1, 2, 3, 4, 5, 6 }) {
|
|
const auto icon = HeadingIcon(level);
|
|
menu->addAction(
|
|
HeadingLabel(level),
|
|
[=] {
|
|
if (_editor) {
|
|
_editor->insertBlock({
|
|
.type = State::InsertBlockType::Heading,
|
|
.headingLevel = level,
|
|
});
|
|
}
|
|
},
|
|
icon,
|
|
icon);
|
|
}
|
|
}
|
|
|
|
void Toolbar::showHeadingMenu(not_null<Ui::IconButton*> button) {
|
|
if (_menu) {
|
|
return;
|
|
}
|
|
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);
|
|
fillHeadingMenu(not_null<Ui::PopupMenu*>(_menu.get()));
|
|
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
|
|
}
|
|
|
|
void Toolbar::showOverflowMenu(not_null<Ui::IconButton*> button) {
|
|
if (_menu || _overflowActions.empty()) {
|
|
return;
|
|
}
|
|
_menu = base::make_unique_q<Ui::PopupMenu>(this, st::popupMenuWithIcons);
|
|
updateOverflowButtonState();
|
|
const auto self = QPointer<Toolbar>(this);
|
|
_menu->setDestroyedCallback([=] {
|
|
if (!self) {
|
|
return;
|
|
}
|
|
self->updateOverflowButtonState();
|
|
});
|
|
auto previousGroup = std::optional<ToolbarGroupId>();
|
|
for (const auto index : _overflowActions) {
|
|
const auto &data = _buttons[index];
|
|
if (previousGroup && (*previousGroup != data.group)) {
|
|
_menu->addSeparator();
|
|
}
|
|
if (data.action == ToolbarActionId::Heading) {
|
|
auto submenu = std::make_unique<Ui::PopupMenu>(
|
|
this,
|
|
st::popupMenuWithIcons);
|
|
fillHeadingMenu(not_null<Ui::PopupMenu*>(submenu.get()));
|
|
_menu->addAction(
|
|
data.label,
|
|
std::move(submenu),
|
|
data.icon,
|
|
data.iconOver);
|
|
} else {
|
|
const auto callback = data.callback;
|
|
const auto action = _menu->addAction(
|
|
data.label,
|
|
[callback] {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
},
|
|
data.icon,
|
|
data.iconOver);
|
|
const auto state = actionState(data);
|
|
action->setEnabled(state.enabled);
|
|
action->setCheckable(state.active);
|
|
action->setChecked(state.active);
|
|
}
|
|
previousGroup = data.group;
|
|
}
|
|
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight);
|
|
_menu->popup(button->mapToGlobal(QPoint(0, button->height())));
|
|
}
|
|
|
|
Widget::ToolbarActionState Toolbar::actionState(
|
|
const ToolbarButton &button) const {
|
|
if (const auto mapped = ToolbarFormatAction(button.action)) {
|
|
return _toolbarState[*mapped];
|
|
}
|
|
return {
|
|
.shown = true,
|
|
.enabled = true,
|
|
.active = false,
|
|
};
|
|
}
|
|
|
|
void Toolbar::refreshButtonText(ToolbarButton &button) {
|
|
const auto label = ToolbarActionLabel(button.action, _toolbarState.linkMode);
|
|
if (button.label == label) {
|
|
return;
|
|
}
|
|
if (_hovered == button.button) {
|
|
hideTooltip();
|
|
}
|
|
button.label = label;
|
|
button.tooltipFactory = SimpleTooltipFactory(label);
|
|
if (button.button) {
|
|
button.button->setAccessibleName(label);
|
|
}
|
|
}
|
|
|
|
void Toolbar::updateFromEditorState() {
|
|
auto shownChanged = false;
|
|
for (auto &button : _buttons) {
|
|
refreshButtonText(button);
|
|
const auto state = actionState(button);
|
|
const auto initialized = button.stateInitialized;
|
|
if (!initialized || button.shown != state.shown) {
|
|
if (button.wrap) {
|
|
button.wrap->toggle(
|
|
state.shown,
|
|
initialized ? anim::type::normal : anim::type::instant);
|
|
}
|
|
button.shown = state.shown;
|
|
shownChanged = true;
|
|
}
|
|
if (!initialized || button.enabled != state.enabled) {
|
|
button.button->setPointerCursor(state.enabled);
|
|
button.button->setAttribute(
|
|
Qt::WA_TransparentForMouseEvents,
|
|
!state.enabled);
|
|
if (state.enabled) {
|
|
button.button->setDisabled(false);
|
|
} else {
|
|
button.button->clearState();
|
|
button.button->setDisabled(true);
|
|
}
|
|
if (button.iconButton) {
|
|
button.iconButton->setIconColorOverride(
|
|
state.enabled
|
|
? std::nullopt
|
|
: std::make_optional(st::windowSubTextFg->c));
|
|
}
|
|
button.enabled = state.enabled;
|
|
}
|
|
button.stateInitialized = true;
|
|
}
|
|
if (shownChanged && width() > 0) {
|
|
resizeGetHeight(width());
|
|
} else {
|
|
updateOverflowButtonState();
|
|
}
|
|
}
|
|
|
|
int Toolbar::resizeGetHeight(int width) {
|
|
const auto padding = st::ivEditorToolbarPadding;
|
|
const auto buttonSkip = st::ivEditorToolbarButtonSkip;
|
|
const auto dividerWidth = st::lineWidth;
|
|
const auto dividerSkip = st::ivEditorToolbarDividerSkip;
|
|
const auto emojiSkip = st::ivEditorToolbarEmojiSkip;
|
|
const auto separatorWidth = dividerSkip + dividerWidth + dividerSkip;
|
|
const auto top = padding.top();
|
|
const auto buttonHeight = st::ivEditorToolbarButton.height;
|
|
for (auto &button : _buttons) {
|
|
if (const auto host = button.host()) {
|
|
host->hide();
|
|
}
|
|
}
|
|
if (_overflow) {
|
|
_overflow->hide();
|
|
}
|
|
_dividerXs.clear();
|
|
_overflowActions.clear();
|
|
const auto emojiHost = _emoji.host();
|
|
const auto emojiWidth = emojiHost ? emojiHost->width() : 0;
|
|
const auto emojiLeft = emojiHost
|
|
? std::max(width - padding.right() - emojiWidth, padding.left())
|
|
: width - padding.right();
|
|
if (emojiHost) {
|
|
emojiHost->moveToLeft(emojiLeft, top, width);
|
|
emojiHost->show();
|
|
}
|
|
const auto available = std::max(
|
|
emojiLeft - padding.left() - (emojiHost ? emojiSkip : 0),
|
|
0);
|
|
auto groups = std::vector<std::vector<int>>();
|
|
for (auto i = 0, count = int(_buttons.size()); i != count; ++i) {
|
|
const auto state = actionState(_buttons[i]);
|
|
if (!state.shown) {
|
|
continue;
|
|
}
|
|
if (groups.empty()
|
|
|| (_buttons[groups.back().back()].group != _buttons[i].group)) {
|
|
groups.push_back({ i });
|
|
} else {
|
|
groups.back().push_back(i);
|
|
}
|
|
}
|
|
const auto widthForAction = [&](int index) {
|
|
const auto host = _buttons[index].host();
|
|
return host ? host->width() : st::ivEditorToolbarButton.width;
|
|
};
|
|
const auto widthWithOverflow = [&](
|
|
const std::vector<int> &visible,
|
|
int overflowStart) {
|
|
auto result = 0;
|
|
auto previous = std::optional<ToolbarGroupId>();
|
|
const auto addWidth = [&](ToolbarGroupId group, int itemWidth) {
|
|
if (previous) {
|
|
result += (*previous == group) ? buttonSkip : separatorWidth;
|
|
}
|
|
result += itemWidth;
|
|
previous = group;
|
|
};
|
|
for (const auto index : visible) {
|
|
addWidth(_buttons[index].group, widthForAction(index));
|
|
}
|
|
if (overflowStart >= 0 && _overflow) {
|
|
addWidth(_buttons[overflowStart].group, _overflow->width());
|
|
}
|
|
return result;
|
|
};
|
|
auto visible = std::vector<int>();
|
|
auto overflowStart = -1;
|
|
auto used = 0;
|
|
auto previous = std::optional<ToolbarGroupId>();
|
|
auto done = false;
|
|
for (const auto &group : groups) {
|
|
for (const auto index : group) {
|
|
const auto extra = previous
|
|
? (*previous == _buttons[index].group)
|
|
? buttonSkip
|
|
: separatorWidth
|
|
: 0;
|
|
const auto needed = extra + widthForAction(index);
|
|
if (used + needed > available) {
|
|
done = true;
|
|
break;
|
|
}
|
|
used += needed;
|
|
visible.push_back(index);
|
|
previous = _buttons[index].group;
|
|
}
|
|
if (done) {
|
|
break;
|
|
}
|
|
}
|
|
if (done) {
|
|
if (!visible.empty()) {
|
|
overflowStart = visible.back();
|
|
visible.pop_back();
|
|
} else if (!groups.empty() && !groups.front().empty()) {
|
|
overflowStart = groups.front().front();
|
|
}
|
|
while ((overflowStart >= 0)
|
|
&& (widthWithOverflow(visible, overflowStart) > available)
|
|
&& !visible.empty()) {
|
|
overflowStart = visible.back();
|
|
visible.pop_back();
|
|
}
|
|
if ((overflowStart >= 0) && (widthWithOverflow({}, overflowStart) > available)) {
|
|
overflowStart = -1;
|
|
}
|
|
}
|
|
if (overflowStart >= 0) {
|
|
auto append = false;
|
|
for (const auto &group : groups) {
|
|
for (const auto index : group) {
|
|
if (index == overflowStart) {
|
|
append = true;
|
|
}
|
|
if (append) {
|
|
_overflowActions.push_back(index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
auto left = padding.left();
|
|
previous = std::nullopt;
|
|
for (const auto index : visible) {
|
|
const auto group = _buttons[index].group;
|
|
if (previous) {
|
|
if (*previous == group) {
|
|
left += buttonSkip;
|
|
} else {
|
|
left += dividerSkip;
|
|
_dividerXs.push_back(left);
|
|
left += dividerWidth + dividerSkip;
|
|
}
|
|
}
|
|
if (const auto host = _buttons[index].host()) {
|
|
host->moveToLeft(left, top, width);
|
|
host->show();
|
|
left += host->width();
|
|
}
|
|
previous = group;
|
|
}
|
|
if (!_overflowActions.empty() && _overflow) {
|
|
const auto overflowGroup = _buttons[_overflowActions.front()].group;
|
|
if (previous) {
|
|
if (*previous == overflowGroup) {
|
|
left += buttonSkip;
|
|
} else {
|
|
left += dividerSkip;
|
|
_dividerXs.push_back(left);
|
|
left += dividerWidth + dividerSkip;
|
|
}
|
|
}
|
|
_overflow->moveToLeft(left, top, width);
|
|
_overflow->show();
|
|
}
|
|
updateOverflowButtonState();
|
|
if (_hovered && !_hovered->isVisible()) {
|
|
hideTooltip();
|
|
}
|
|
updateTooltipGeometry();
|
|
update();
|
|
return padding.top() + buttonHeight + padding.bottom();
|
|
}
|
|
|
|
void Toolbar::hideShownTooltip() {
|
|
hideTooltip();
|
|
}
|
|
|
|
bool Toolbar::eventFilter(QObject *object, QEvent *event) {
|
|
for (auto &data : _buttons) {
|
|
if (data.button != object) {
|
|
continue;
|
|
}
|
|
if (event->type() == QEvent::Enter) {
|
|
showTooltip(not_null<Ui::RippleButton*>(data.button));
|
|
} else if (event->type() == QEvent::Leave && _hovered == data.button) {
|
|
hideTooltip();
|
|
}
|
|
return Ui::RpWidget::eventFilter(object, event);
|
|
}
|
|
if ((_emoji.button == object) && _emoji.button) {
|
|
if (event->type() == QEvent::Enter) {
|
|
showTooltip(not_null<Ui::RippleButton*>(_emoji.button));
|
|
} else if (event->type() == QEvent::Leave && _hovered == _emoji.button) {
|
|
hideTooltip();
|
|
}
|
|
}
|
|
return Ui::RpWidget::eventFilter(object, event);
|
|
}
|
|
|
|
void Toolbar::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
const auto lineTop = st::ivEditorToolbarPadding.top()
|
|
+ ((st::ivEditorToolbarButton.height
|
|
- st::ivEditorToolbarDividerHeight) / 2);
|
|
const auto lineHeight = st::ivEditorToolbarDividerHeight;
|
|
for (const auto x : _dividerXs) {
|
|
p.fillRect(
|
|
QRect(x, lineTop, st::lineWidth, lineHeight),
|
|
st::defaultMarkdown.rule.fg);
|
|
}
|
|
}
|
|
|
|
void Toolbar::showTooltip(not_null<Ui::RippleButton*> button) {
|
|
hideTooltip();
|
|
const auto data = buttonData(button);
|
|
if (!data) {
|
|
return;
|
|
}
|
|
_hovered = button;
|
|
const auto tooltipParent = _tooltipParent
|
|
? _tooltipParent.data()
|
|
: (parentWidget() ? parentWidget() : this);
|
|
_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(
|
|
tooltipParent,
|
|
Ui::MakeNiceTooltipLabel(
|
|
tooltipParent,
|
|
data->tooltipFactory(),
|
|
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;
|
|
if (_tooltip) {
|
|
_tooltip->toggleFast(false);
|
|
_tooltip = nullptr;
|
|
}
|
|
}
|
|
|
|
void Toolbar::updateTooltipGeometry() {
|
|
if (!_tooltip || !_hovered) {
|
|
return;
|
|
}
|
|
const auto tooltipParent = _tooltip->parentWidget();
|
|
const auto geometry = Ui::MapFrom(
|
|
tooltipParent,
|
|
_hovered,
|
|
_hovered->rect());
|
|
_tooltip->pointAt(geometry, RectPart::Top | RectPart::Center);
|
|
}
|
|
|
|
void Toolbar::updateOverflowButtonState() {
|
|
if (!_overflow) {
|
|
return;
|
|
}
|
|
const auto enabled = !_overflowActions.empty();
|
|
if (!_overflowStateInitialized || _overflowEnabled != enabled) {
|
|
_overflow->setPointerCursor(enabled);
|
|
_overflow->setAttribute(Qt::WA_TransparentForMouseEvents, !enabled);
|
|
if (enabled) {
|
|
_overflow->setDisabled(false);
|
|
} else {
|
|
_overflow->clearState();
|
|
_overflow->setDisabled(true);
|
|
}
|
|
_overflowEnabled = enabled;
|
|
}
|
|
_overflowStateInitialized = true;
|
|
}
|
|
|
|
ToolbarButton *Toolbar::buttonData(not_null<Ui::RippleButton*> button) {
|
|
for (auto &data : _buttons) {
|
|
if (data.button == button.get()) {
|
|
return &data;
|
|
}
|
|
}
|
|
return (_emoji.button == button.get()) ? &_emoji : nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct WindowHost::Impl final {
|
|
public:
|
|
explicit Impl(ShowWindowDescriptor descriptor);
|
|
~Impl();
|
|
|
|
private:
|
|
void setupWindow(ShowWindowDescriptor &&descriptor);
|
|
void setupEmojiColumn(const ShowWindowDescriptor &descriptor);
|
|
void layout();
|
|
void toggleEmojiColumn();
|
|
void showEmojiColumn();
|
|
void hideEmojiColumn(bool skipResize = false);
|
|
void updateEditorVisibleTopBottom();
|
|
void setEmojiColumnInteractionActive(bool active);
|
|
[[nodiscard]] int emojiColumnWidth() const;
|
|
[[nodiscard]] int minimalWindowWidthWithEmojiColumn() const;
|
|
[[nodiscard]] bool handleCloseRequest();
|
|
void finishCloseFromAcceptedEvent();
|
|
void finishClose();
|
|
[[nodiscard]] bool articleChanged();
|
|
[[nodiscard]] bool showCloseConfirmation();
|
|
[[nodiscard]] bool confirmCancel();
|
|
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> _bottom = { nullptr };
|
|
object_ptr<Ui::ScrollArea> _scroll = { nullptr };
|
|
QPointer<Widget> _editor;
|
|
object_ptr<Toolbar> _toolbar = { nullptr };
|
|
object_ptr<Ui::RoundButton> _cancel = { nullptr };
|
|
object_ptr<Ui::RoundButton> _submit = { nullptr };
|
|
object_ptr<ChatHelpers::TabbedSelector> _emojiColumn = { nullptr };
|
|
Fn<bool()> _cancelled;
|
|
Fn<bool()> _confirmed;
|
|
Fn<void()> _closed;
|
|
base::weak_qptr<Ui::GenericBox> _closeConfirmation;
|
|
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::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->paintRequest() | rpl::on_next([=](QRect clip) {
|
|
QPainter(_top.data()).fillRect(clip, st::windowBg);
|
|
}, _top->lifetime());
|
|
_bottom = object_ptr<Ui::RpWidget>(window->body().get());
|
|
_bottom->paintRequest() | rpl::on_next([=](QRect clip) {
|
|
QPainter(_bottom.data()).fillRect(clip, st::windowBg);
|
|
}, _bottom->lifetime());
|
|
|
|
const auto hasRequestMedia = static_cast<bool>(descriptor.requestMedia);
|
|
_scroll = object_ptr<Ui::ScrollArea>(window->body().get(), st::boxScroll);
|
|
_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),
|
|
.imeCompositionStarts = window->imeCompositionStarts(),
|
|
},
|
|
descriptor.peer,
|
|
descriptor.state,
|
|
std::move(descriptor.showLimitToast)));
|
|
const auto editor = not_null<Widget*>(_editor.data());
|
|
const auto body = QPointer<QWidget>(window->body().get());
|
|
setupEmojiColumn(descriptor);
|
|
|
|
_toolbar = object_ptr<Toolbar>(
|
|
_top.data(),
|
|
editor,
|
|
body,
|
|
hasRequestMedia,
|
|
std::move(descriptor.requestMap),
|
|
[=] {
|
|
_toolbar->hideShownTooltip();
|
|
toggleEmojiColumn();
|
|
});
|
|
_cancel = object_ptr<Ui::RoundButton>(
|
|
_bottom.data(),
|
|
tr::lng_cancel(),
|
|
st::ivEditorCancelButton);
|
|
_cancel->setClickedCallback([=] {
|
|
if (confirmCancel()) {
|
|
finishClose();
|
|
}
|
|
});
|
|
_submit = object_ptr<Ui::RoundButton>(
|
|
_bottom.data(),
|
|
rpl::single(SubmitText(descriptor)),
|
|
st::ivEditorSubmitButton);
|
|
_submit->setClickedCallback([=] { submit(); });
|
|
if (descriptor.setupSubmitButton) {
|
|
descriptor.setupSubmitButton(not_null<Ui::RpWidget*>(_submit.data()));
|
|
}
|
|
|
|
_cancelled = std::move(descriptor.cancelled);
|
|
_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();
|
|
_bottom->show();
|
|
_scroll->show();
|
|
window->show();
|
|
editor->activateInitialNode();
|
|
}
|
|
|
|
void WindowHost::Impl::setupEmojiColumn(const ShowWindowDescriptor &descriptor) {
|
|
using Selector = ChatHelpers::TabbedSelector;
|
|
_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,
|
|
},
|
|
});
|
|
_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 || !_bottom || !_toolbar || !_editor || !_emojiColumn) {
|
|
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);
|
|
const auto toolbarHeight = _toolbar->resizeGetHeight(editorWidth);
|
|
const auto buttonsHeight = std::max(_cancel->height(), _submit->height());
|
|
const auto bottomHeight = padding.top() + buttonsHeight + padding.bottom();
|
|
const auto contentHeight = std::max(height - toolbarHeight - bottomHeight, 0);
|
|
const auto buttonsTop = padding.top();
|
|
_top->setGeometry(0, 0, editorWidth, toolbarHeight);
|
|
_toolbar->setGeometry(0, 0, editorWidth, toolbarHeight);
|
|
_bottom->setGeometry(
|
|
0,
|
|
std::max(height - bottomHeight, toolbarHeight),
|
|
editorWidth,
|
|
bottomHeight);
|
|
_submit->moveToRight(padding.right(), buttonsTop, editorWidth);
|
|
_cancel->moveToRight(
|
|
padding.right()
|
|
+ _submit->width()
|
|
+ st::ivEditorBottomControlsButtonSkip,
|
|
buttonsTop,
|
|
editorWidth);
|
|
_scroll->setGeometry(
|
|
0,
|
|
toolbarHeight,
|
|
editorWidth,
|
|
contentHeight);
|
|
if (_emojiColumnShown) {
|
|
_emojiColumn->setGeometry(
|
|
editorWidth,
|
|
0,
|
|
emojiWidth,
|
|
height);
|
|
} else {
|
|
_emojiColumn->hide();
|
|
}
|
|
_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;
|
|
_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;
|
|
window->setMinimumWidth(st::ivEditorWindowMinSize.width());
|
|
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 {
|
|
return st::emojiPanWidth;
|
|
}
|
|
|
|
int WindowHost::Impl::minimalWindowWidthWithEmojiColumn() const {
|
|
return st::ivEditorWindowMinSize.width() + 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::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::confirmCancel() {
|
|
if (!articleChanged()) {
|
|
return !_cancelled || _cancelled();
|
|
}
|
|
return showCloseConfirmation()
|
|
? false
|
|
: (!_cancelled || _cancelled());
|
|
}
|
|
|
|
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;
|
|
|
|
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)));
|
|
}
|
|
|
|
} // namespace Iv::Editor
|