/* 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 #include "base/algorithm.h" #include "base/event_filter.h" #include "base/unique_qptr.h" #include "boxes/premium_preview_box.h" #include "chat_helpers/compose/compose_show.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_panel.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 "ui/rect_part.h" #include "ui/rp_widget.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 #include #include #include #include #include #include #include #include #include #include #include #include #include "styles/style_chat_helpers.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 { struct ToolbarButton { object_ptr widget; Fn()> tooltipFactory; }; class Toolbar final : public Ui::RpWidget { public: Toolbar( QWidget *parent, not_null editor, QPointer tooltipParent, Fn, QPointer)> requestMedia, Fn, QPointer, rpl::producer<>)> requestMap, Fn)> toggleEmoji); int resizeGetHeight(int width) override; [[nodiscard]] Ui::IconButton *emojiButton() const; void hideShownTooltip(); protected: bool eventFilter(QObject *object, QEvent *event) override; private: not_null addButton( QString label, Fn()> tooltipFactory, const style::icon *icon, Fn callback); void addInsertButtons(); void showHeadingMenu(not_null button); void showTooltip(not_null button); void hideTooltip(); void updateTooltipGeometry(); [[nodiscard]] ToolbarButton *buttonData(not_null button); const QPointer _editor; const QPointer _tooltipParent; const Fn, QPointer)> _requestMedia; const Fn, QPointer, rpl::producer<>)> _requestMap; const Fn)> _toggleEmoji; std::vector _buttons; base::unique_qptr _tooltip; base::unique_qptr _menu; Ui::IconButton *_emojiButton = nullptr; Ui::IconButton *_hovered = nullptr; }; [[nodiscard]] QRect DefaultWindowGeometry() { const auto size = st::ivEditorWindowDefaultSize; auto result = QRect(QPoint(), size); if (const auto screen = QGuiApplication::primaryScreen()) { result.moveCenter(screen->availableGeometry().center()); } return result; } [[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 document) { const auto info = document->sticker(); return info && (info->setType == Data::StickersType::Emoji); } Toolbar::Toolbar( QWidget *parent, not_null editor, QPointer tooltipParent, Fn, QPointer)> requestMedia, Fn, QPointer, rpl::producer<>)> requestMap, Fn)> toggleEmoji) : Ui::RpWidget(parent) , _editor(editor.get()) , _tooltipParent(std::move(tooltipParent)) , _requestMedia(std::move(requestMedia)) , _requestMap(std::move(requestMap)) , _toggleEmoji(std::move(toggleEmoji)) { setMouseTracking(true); addInsertButtons(); } not_null Toolbar::addButton( QString label, Fn()> tooltipFactory, const style::icon *icon, Fn callback) { auto button = object_ptr(this, st::ivEditorToolbarButton); const auto raw = button.data(); raw->setAccessibleName(label); raw->setIconOverride(icon, icon); raw->setClickedCallback([=, callback = std::move(callback)] { hideTooltip(); callback(); }); raw->installEventFilter(this); _buttons.push_back({ std::move(button), std::move(tooltipFactory), }); return raw; } void Toolbar::addInsertButtons() { const auto insert = [=](State::InsertAction action) { if (_editor) { _editor->insertBlock(action); } }; const auto insertType = [=](State::InsertBlockType type) { insert({ .type = type }); }; const auto heading = addButton( tr::lng_article_insert_heading(tr::now), [] { return tr::lng_article_insert_heading(tr::marked); }, &st::ivEditorToolbarHeadingIcon, [] {}); heading->setClickedCallback([=] { hideTooltip(); showHeadingMenu(heading); }); _emojiButton = addButton( tr::lng_article_insert_emoji(tr::now), [] { return tr::lng_article_insert_emoji(tr::marked); }, &st::ivEditorToolbarEmojiIcon, [=] { if (_toggleEmoji && _emojiButton) { _toggleEmoji(not_null(_emojiButton)); } }); addButton( tr::lng_article_insert_blockquote(tr::now), [] { return tr::lng_article_insert_blockquote(tr::marked); }, &st::ivEditorToolbarBlockquoteIcon, [=] { insertType(State::InsertBlockType::Blockquote); }); addButton( tr::lng_article_insert_code(tr::now), [] { return tr::lng_article_insert_code(tr::marked); }, &st::ivEditorToolbarCodeIcon, [=] { insertType(State::InsertBlockType::Code); }); addButton( tr::lng_article_insert_math(tr::now), [] { return tr::lng_article_insert_math(tr::marked); }, &st::ivEditorToolbarMathIcon, [=] { insertType(State::InsertBlockType::Math); }); addButton( tr::lng_article_insert_divider(tr::now), [] { return tr::lng_article_insert_divider(tr::marked); }, &st::ivEditorToolbarDividerIcon, [=] { insertType(State::InsertBlockType::Divider); }); addButton( tr::lng_article_insert_ordered_list(tr::now), [] { return tr::lng_article_insert_ordered_list(tr::marked); }, &st::ivEditorToolbarOrderedListIcon, [=] { insertType(State::InsertBlockType::OrderedList); }); addButton( tr::lng_article_insert_bullet_list(tr::now), [] { return tr::lng_article_insert_bullet_list(tr::marked); }, &st::ivEditorToolbarBulletListIcon, [=] { insertType(State::InsertBlockType::BulletList); }); addButton( tr::lng_article_insert_task_list(tr::now), [] { return tr::lng_article_insert_task_list(tr::marked); }, &st::ivEditorToolbarTaskListIcon, [=] { insertType(State::InsertBlockType::TaskList); }); addButton( tr::lng_article_insert_pullquote(tr::now), [] { return tr::lng_article_insert_pullquote(tr::marked); }, &st::ivEditorToolbarPullquoteIcon, [=] { insertType(State::InsertBlockType::Pullquote); }); if (_requestMedia) { addButton( tr::lng_article_insert_media(tr::now), [] { return tr::lng_article_insert_media(tr::marked); }, &st::ivEditorToolbarAttachIcon, [=] { if (_editor) { _requestMedia( not_null(_editor.data()), _tooltipParent); } }); } addButton( tr::lng_article_insert_details(tr::now), [] { return tr::lng_article_insert_details(tr::marked); }, &st::ivEditorToolbarDetailsIcon, [=] { insertType(State::InsertBlockType::Details); }); addButton( tr::lng_article_insert_table(tr::now), [] { return tr::lng_article_insert_table(tr::marked); }, &st::ivEditorToolbarTableIcon, [=] { insertType(State::InsertBlockType::Table); }); if (_requestMap) { addButton( tr::lng_article_insert_map(tr::now), [] { return tr::lng_article_insert_map(tr::marked); }, &st::menuIconAddress, [=] { if (_editor) { const auto parent = _tooltipParent; auto closeRequests = parent ? static_cast(parent.data())->death() : rpl::never<>(); _requestMap( not_null(_editor.data()), parent, std::move(closeRequests)); } }); } } void Toolbar::showHeadingMenu(not_null button) { _menu = base::make_unique_q(this); for (const auto level : std::array{ 1, 2, 3, 4, 5, 6 }) { _menu->addAction( HeadingLabel(level), [=] { if (_editor) { _editor->insertBlock({ .type = State::InsertBlockType::Heading, .headingLevel = level, }); } }); } _menu->popup(button->mapToGlobal(QPoint(0, button->height()))); } int Toolbar::resizeGetHeight(int width) { const auto padding = st::ivEditorToolbarPadding; const auto buttonWidth = st::ivEditorToolbarButton.width; const auto buttonHeight = st::ivEditorToolbarButton.height; const auto right = width - padding.right(); auto left = padding.left(); auto top = padding.top(); for (const auto &button : _buttons) { if (left > padding.left() && left + buttonWidth > right) { left = padding.left(); top += buttonHeight + st::ivEditorToolbarRowSkip; } button.widget->moveToLeft(left, top, width); left += buttonWidth + st::ivEditorToolbarButtonSkip; } updateTooltipGeometry(); return top + buttonHeight + padding.bottom(); } Ui::IconButton *Toolbar::emojiButton() const { return _emojiButton; } void Toolbar::hideShownTooltip() { hideTooltip(); } bool Toolbar::eventFilter(QObject *object, QEvent *event) { for (const auto &data : _buttons) { if (data.widget.data() != object) { continue; } const auto button = data.widget.data(); if (event->type() == QEvent::Enter) { showTooltip(not_null(button)); } else if (event->type() == QEvent::Leave && _hovered == button) { hideTooltip(); } break; } return Ui::RpWidget::eventFilter(object, event); } void Toolbar::showTooltip(not_null 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( 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); } ToolbarButton *Toolbar::buttonData(not_null button) { for (auto &data : _buttons) { if (data.widget.data() == button.get()) { return &data; } } return nullptr; } } // namespace struct WindowHost::Impl final { public: explicit Impl(ShowWindowDescriptor descriptor); ~Impl(); private: void setupWindow(ShowWindowDescriptor &&descriptor); void setupEmojiPanel(const ShowWindowDescriptor &descriptor); void layout(); void updateEmojiPanelGeometry(); void updateEditorVisibleTopBottom(); void setEmojiPanelInteractionActive(bool active); void finishCloseFromAcceptedEvent(); void finishClose(); [[nodiscard]] bool confirmCancel(); void submit(); std::unique_ptr _window; object_ptr _top = { nullptr }; object_ptr _scroll = { nullptr }; QPointer _editor; object_ptr _toolbar = { nullptr }; object_ptr _cancel = { nullptr }; object_ptr _submit = { nullptr }; base::unique_qptr _emojiPanel; Fn _cancelled; Fn _confirmed; Fn _closed; rpl::lifetime _lifetime; bool _emojiPanelInteractionActive = false; bool _closingApproved = false; bool _closedNotified = false; }; WindowHost::Impl::Impl(ShowWindowDescriptor descriptor) { setupWindow(std::move(descriptor)); } WindowHost::Impl::~Impl() { setEmojiPanelInteractionActive(false); } void WindowHost::Impl::setupWindow(ShowWindowDescriptor &&descriptor) { const auto title = tr::lng_article_editor_title(tr::now); _window = std::make_unique(); const auto window = _window.get(); 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(window->body().get()); _top->paintRequest() | rpl::on_next([=](QRect clip) { QPainter(_top.data()).fillRect(clip, st::windowBg); }, _top->lifetime()); _scroll = object_ptr(window->body().get(), st::boxScroll); _editor = _scroll->setOwnedWidget(object_ptr( _scroll.data(), WidgetServices{ .session = descriptor.session, .show = descriptor.show, .outer = window->body(), .customEmojiPaused = std::move(descriptor.customEmojiPaused), .imeCompositionStarts = window->imeCompositionStarts(), }, descriptor.peer, descriptor.state, std::move(descriptor.showLimitToast))); const auto editor = not_null(_editor.data()); const auto body = QPointer(window->body().get()); _toolbar = object_ptr( _top.data(), editor, body, std::move(descriptor.requestMedia), std::move(descriptor.requestMap), [=](not_null) { _toolbar->hideShownTooltip(); setEmojiPanelInteractionActive(true); updateEmojiPanelGeometry(); _emojiPanel->toggleAnimated(); }); _cancel = object_ptr( _top.data(), tr::lng_cancel(), st::ivEditorCancelButton); _cancel->setClickedCallback([=] { if (confirmCancel()) { finishClose(); } }); _submit = object_ptr( _top.data(), rpl::single(SubmitText(descriptor)), st::ivEditorSubmitButton); _submit->setClickedCallback([=] { submit(); }); if (descriptor.setupSubmitButton) { descriptor.setupSubmitButton(not_null(_submit.data())); } _cancelled = std::move(descriptor.cancelled); _confirmed = std::move(descriptor.confirmed); _closed = std::move(descriptor.closed); setupEmojiPanel(descriptor); 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 e) { if (e->type() == QEvent::Close) { const auto event = static_cast(e.get()); if (_closingApproved) { event->accept(); return; } if (confirmCancel()) { event->accept(); finishCloseFromAcceptedEvent(); } else { event->ignore(); } } else if (e->type() == QEvent::KeyPress) { const auto event = static_cast(e.get()); if (event->key() == Qt::Key_Escape) { event->accept(); if (confirmCancel()) { finishClose(); } } } }, _lifetime); layout(); _top->show(); _scroll->show(); window->show(); editor->activateInitialNode(); } void WindowHost::Impl::setupEmojiPanel(const ShowWindowDescriptor &descriptor) { using Selector = ChatHelpers::TabbedSelector; _emojiPanel = base::make_unique_q( _window->body().get(), ChatHelpers::TabbedPanelDescriptor{ .ownedSelector = object_ptr( nullptr, ChatHelpers::TabbedSelectorDescriptor{ .show = descriptor.show, .st = st::defaultEmojiPan, .level = ChatHelpers::PauseReason::Layer, .mode = Selector::Mode::EmojiOnly, .features = { .stickersSettings = false, .openStickerSets = false, }, }), }); const auto panel = _emojiPanel.get(); panel->setDesiredHeightValues( 1., st::emojiPanMinHeight / 2, st::emojiPanMinHeight); panel->setDropDown(true); panel->hide(); panel->selector()->setCurrentPeer(descriptor.peer); panel->selector()->emojiChosen( ) | rpl::on_next([=](ChatHelpers::EmojiChosen data) { setEmojiPanelInteractionActive(true); if (_editor) { _editor->insertEmoji(data.emoji); } }, _lifetime); panel->selector()->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( descriptor.show, PremiumFeature::AnimatedEmoji); } else if (_editor) { setEmojiPanelInteractionActive(true); _editor->insertCustomEmoji(document); } }, _lifetime); if (const auto button = _toolbar->emojiButton()) { button->installEventFilter(panel); base::install_event_filter(panel, button, [=](not_null event) { const auto type = event->type(); if (type == QEvent::Enter || type == QEvent::MouseButtonPress) { setEmojiPanelInteractionActive(true); updateEmojiPanelGeometry(); } else if (type == QEvent::Leave && panel->isHidden()) { setEmojiPanelInteractionActive(false); } return base::EventFilterResult::Continue; }); } base::install_event_filter(panel, [=](not_null event) { const auto type = event->type(); if (type == QEvent::Hide || type == QEvent::Close) { setEmojiPanelInteractionActive(false); } else if (type == QEvent::Show) { setEmojiPanelInteractionActive(true); } return base::EventFilterResult::Continue; }); } void WindowHost::Impl::layout() { if (!_window || !_top || !_toolbar || !_editor) { return; } const auto width = _window->body()->width(); const auto height = _window->body()->height(); const auto padding = st::ivEditorTopControlsPadding; const auto buttonsWidth = _cancel->width() + st::ivEditorTopControlsButtonSkip + _submit->width(); const auto toolbarWidth = std::max( width - padding.right() - buttonsWidth - st::ivEditorTopControlsContentSkip, 0); const auto toolbarHeight = _toolbar->resizeGetHeight(toolbarWidth); const auto buttonsHeight = std::max(_cancel->height(), _submit->height()); const auto topHeight = std::max( toolbarHeight, padding.top() + buttonsHeight + padding.bottom()); const auto buttonsTop = padding.top() + (topHeight - padding.top() - padding.bottom() - buttonsHeight) / 2; _top->setGeometry(0, 0, width, topHeight); _toolbar->setGeometry(0, 0, toolbarWidth, toolbarHeight); _submit->moveToRight(padding.right(), buttonsTop, width); _cancel->moveToRight( padding.right() + _submit->width() + st::ivEditorTopControlsButtonSkip, buttonsTop, width); _scroll->setGeometry(0, topHeight, width, std::max(height - topHeight, 0)); _editor->resizeToWidth(std::max(_scroll->width(), 1)); updateEditorVisibleTopBottom(); updateEmojiPanelGeometry(); } void WindowHost::Impl::updateEmojiPanelGeometry() { if (!_emojiPanel || !_toolbar) { return; } const auto button = _toolbar->emojiButton(); if (!button) { return; } const auto local = _emojiPanel->parentWidget()->mapFromGlobal( button->mapToGlobal(QPoint())); _emojiPanel->setDropDown(true); _emojiPanel->moveTopRight( local.y() + button->height(), local.x() + button->width()); } void WindowHost::Impl::updateEditorVisibleTopBottom() { if (!_scroll || !_editor) { return; } const auto scrollTop = _scroll->scrollTop(); _editor->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height()); } void WindowHost::Impl::setEmojiPanelInteractionActive(bool active) { if (_emojiPanelInteractionActive == active || !_editor) { _emojiPanelInteractionActive = active; return; } _emojiPanelInteractionActive = active; _editor->setInlineFieldExternalInteractionActive(active); } void WindowHost::Impl::finishCloseFromAcceptedEvent() { _closingApproved = true; if (_closedNotified) { return; } _closedNotified = true; setEmojiPanelInteractionActive(false); if (_emojiPanel) { _emojiPanel->hideFast(); } if (const auto closed = base::take(_closed)) { crl::on_main(closed); } } void WindowHost::Impl::finishClose() { finishCloseFromAcceptedEvent(); if (_window) { _window->close(); } } bool WindowHost::Impl::confirmCancel() { return !_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(std::move(descriptor))) { } WindowHost::~WindowHost() = default; std::unique_ptr ShowWindow(ShowWindowDescriptor descriptor) { if (!descriptor.state) { descriptor.state = std::make_shared(); } return std::unique_ptr(new WindowHost(std::move(descriptor))); } } // namespace Iv::Editor