diff --git a/Telegram/SourceFiles/chat_helpers/message_field.h b/Telegram/SourceFiles/chat_helpers/message_field.h index a24fa100b3..68c036a3cb 100644 --- a/Telegram/SourceFiles/chat_helpers/message_field.h +++ b/Telegram/SourceFiles/chat_helpers/message_field.h @@ -23,6 +23,11 @@ namespace tr { struct now_t; } // namespace tr +class DocumentData; +class HistoryItem; +class PeerData; +class UserData; + namespace Main { class Session; class SessionShow; diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 0913a660e4..5c54b73a83 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_msg_id.h" #include "base/qt/qt_compare.h" +struct AudioAlbumThumbLocation; class HistoryItem; using HistoryItemsList = std::vector>; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index f5e0ed5c26..08978dd16c 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -8,30 +8,35 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/editor/iv_editor_widget.h" #include "base/qt/qt_common_adapters.h" -#include "data/data_msg_id.h" -#include "ui/image/image_location.h" -#include "data/data_types.h" #include "chat_helpers/message_field.h" +#include "data/data_msg_id.h" +#include "data/data_types.h" #include "data/stickers/data_custom_emoji.h" #include "iv/markdown/iv_markdown_prepare_native_richtext.h" #include "spellcheck/spellcheck_highlight_syntax.h" #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" +#include "ui/image/image_location.h" #include "ui/painter.h" #include "ui/text/text_entity.h" #include "ui/ui_utility.h" #include "ui/widgets/fields/input_field.h" +#include "window/window_session_controller.h" + #include "styles/palette.h" #include "styles/style_chat.h" #include "styles/style_iv.h" +#include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -39,8 +44,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include -#include "window/window_session_controller.h" - #include #include #include @@ -143,6 +146,23 @@ void EnableQTextEditLineMetrics(style::Markdown &style) { }; } +[[nodiscard]] bool RedirectTextToField(const QString &text) { + for (const auto &ch : text) { + if (ch.unicode() >= 32) { + return true; + } + } + return false; +} + +[[nodiscard]] bool ImeEventProducesInput( + const QInputMethodEvent &e, + const QTextCursor &cursor) { + return !e.commitString().isEmpty() + || e.preeditString() != cursor.block().layout()->preeditAreaText() + || e.replacementLength() > 0; +} + using PreparedEditBlockContainerPath = Markdown::PreparedEditBlockContainerPath; using PreparedEditBlockContainerStep @@ -641,6 +661,16 @@ Widget::Widget( setMouseTracking(true); setAttribute(Qt::WA_AcceptTouchEvents); setFocusPolicy(Qt::StrongFocus); + setAttribute(Qt::WA_InputMethodEnabled); + + _controller->imeCompositionStarts( + ) | rpl::filter([=] { + return redirectImeToField(); + }) | rpl::on_next([=] { + if (prepareFieldForInput()) { + _field->setFocusFast(); + } + }, lifetime()); Spellchecker::HighlightReady( ) | rpl::on_next([=](Spellchecker::HighlightProcessId processId) { @@ -702,6 +732,38 @@ void Widget::activateSegment(int segmentIndex, int cursorOffset) { activateTextOrdinal(ordinal, cursorOffset); } +bool Widget::prepareFieldForInput() { + if (hasStructuralSelection()) { + if (const auto target = removeCurrentStructuralSelection(true)) { + activateTextOrdinal(*target, 0); + } else { + activateInitialNode(); + } + } else if (_field->isHidden()) { + activateInitialNode(); + } + return !_field->isHidden(); +} + +bool Widget::replayKeyIntoField(QKeyEvent *e) { + if (!RedirectTextToField(e->text()) || !prepareFieldForInput()) { + return false; + } + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + return true; +} + +bool Widget::replayImeIntoField(QInputMethodEvent *e) { + const auto cursor = _field->rawTextEdit()->textCursor(); + if (!ImeEventProducesInput(*e, cursor) || !prepareFieldForInput()) { + return false; + } + _field->setFocusFast(); + QCoreApplication::sendEvent(_field->rawTextEdit(), e); + return true; +} + void Widget::commitInlineField() { applyFieldTextToState(); } @@ -821,7 +883,8 @@ bool Widget::eventFilter(QObject *object, QEvent *event) { } } else if (type == QEvent::KeyPress) { const auto keyEvent = static_cast(event); - if (handleStructuralSelectionKey(keyEvent) + if (handleTabNavigation(keyEvent) + || handleStructuralSelectionKey(keyEvent) || handleFieldKey(keyEvent)) { return true; } @@ -862,9 +925,21 @@ void Widget::focusInEvent(QFocusEvent *e) { } } +bool Widget::focusNextPrevChild(bool next) { + if (hasFocus() && _field->isHidden() && moveTabBoundary(next)) { + return true; + } + return Ui::RpWidget::focusNextPrevChild(next); +} + void Widget::keyPressEvent(QKeyEvent *e) { if (handleStructuralSelectionKey(e)) { return; + } else if (_field->isHidden() && handleTabNavigation(e)) { + return; + } else if (redirectKeyToField(e) && replayKeyIntoField(e)) { + e->accept(); + return; } Ui::RpWidget::keyPressEvent(e); } @@ -991,6 +1066,39 @@ void Widget::wheelEvent(QWheelEvent *e) { e->ignore(); } +bool Widget::redirectKeyToField(QKeyEvent *e) const { + if (!hasFocus()) { + return false; + } + const auto modifiers = e->modifiers() + & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); + return (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) + && (e->key() != Qt::Key_Shift) + && RedirectTextToField(e->text()); +} + +void Widget::inputMethodEvent(QInputMethodEvent *e) { + const auto cursor = _field->rawTextEdit()->textCursor(); + if (!ImeEventProducesInput(*e, cursor) || !redirectImeToField()) { + Ui::RpWidget::inputMethodEvent(e); + return; + } + if (!replayImeIntoField(e)) { + Ui::RpWidget::inputMethodEvent(e); + return; + } + e->accept(); + return; +} + +QVariant Widget::inputMethodQuery(Qt::InputMethodQuery query) const { + return _field->rawTextEdit()->inputMethodQuery(query); +} + +bool Widget::redirectImeToField() const { + return hasFocus() && (hasStructuralSelection() || _field->isHidden()); +} + void Widget::mouseMoveEvent(QMouseEvent *e) { const auto articlePoint = e->pos() - articleTopLeft(); if (_horizontalScrollDrag == HorizontalScrollDrag::Mouse) { @@ -1625,9 +1733,6 @@ void Widget::activateTextOrdinalAtEnd(int ordinal) { } bool Widget::handleFieldKey(QKeyEvent *e) { - if (handleStructuralSelectionKey(e)) { - return true; - } if (_field->isHidden()) { return false; } @@ -1670,6 +1775,25 @@ bool Widget::handleFieldKey(QKeyEvent *e) { return handled; } +bool Widget::handleTabNavigation(QKeyEvent *e) { + const auto key = e->key(); + if (key != Qt::Key_Tab && key != Qt::Key_Backtab) { + return false; + } + const auto modifiers = e->modifiers() + & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); + if (modifiers != Qt::NoModifier && modifiers != Qt::ShiftModifier) { + return false; + } + const auto forward = (key != Qt::Key_Backtab) + && (modifiers != Qt::ShiftModifier); + if (!moveTabBoundary(forward)) { + return false; + } + e->accept(); + return true; +} + bool Widget::moveBoundary(bool forward, bool allowTrailing) { const auto target = forward ? _state->nextEditableOrdinal() @@ -1719,6 +1843,28 @@ bool Widget::moveBoundaryAfterCommit(bool forward, bool allowTrailing) { return false; } +bool Widget::moveTabBoundary(bool forward) { + if (!_field->isHidden()) { + commitInlineField(); + } + const auto target = forward + ? _state->nextEditableOrdinal() + : _state->previousEditableOrdinal(); + if (target) { + clearSelection(); + refreshPreparedContent(); + activateTextOrdinalAtEnd(*target); + return true; + } else if (!forward || _state->isActiveTopLevelParagraph()) { + return false; + } + clearSelection(); + const auto ordinal = _state->ensureTrailingParagraphActive(); + refreshPreparedContent(); + activateTextOrdinalAtEnd(ordinal); + return true; +} + bool Widget::removeBoundaryOwner(bool forward) { commitInlineField(); if (_state->activeOwnerIsEmpty()) { @@ -2111,17 +2257,7 @@ bool Widget::handleStructuralSelectionKey(QKeyEvent *e) { if (!forward && key != Qt::Key_Backspace) { return false; } - const auto selection = _structuralSelection; - commitInlineField(); - _pendingOrdinal = -1; - _pendingCursorOffset = 0; - hideInlineField(); - _article->clearTextLeafHeightOverride(); - const auto target = _state->removeStructuralSelection( - selection, - forward); - clearSelection(); - refreshPreparedContent(); + const auto target = removeCurrentStructuralSelection(forward); if (target) { if (forward) { activateTextOrdinal(*target, 0); @@ -2135,6 +2271,24 @@ bool Widget::handleStructuralSelectionKey(QKeyEvent *e) { return true; } +std::optional Widget::removeCurrentStructuralSelection(bool forward) { + if (!hasStructuralSelection()) { + return std::nullopt; + } + const auto selection = _structuralSelection; + commitInlineField(); + _pendingOrdinal = -1; + _pendingCursorOffset = 0; + hideInlineField(); + _article->clearTextLeafHeightOverride(); + const auto target = _state->removeStructuralSelection( + selection, + forward); + clearSelection(); + refreshPreparedContent(); + return target; +} + bool Widget::handleFieldMouseEvent(QEvent *event) { if (!_article || _field->isHidden() || _activeSegmentIndex < 0) { return false; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 826667ada3..5e5485e8b0 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include class QEvent; +class QInputMethodEvent; class QKeyEvent; class QObject; class QTouchEvent; @@ -68,7 +69,10 @@ protected: bool eventFilter(QObject *object, QEvent *event) override; bool eventHook(QEvent *e) override; void focusInEvent(QFocusEvent *e) override; + bool focusNextPrevChild(bool next) override; void keyPressEvent(QKeyEvent *e) override; + void inputMethodEvent(QInputMethodEvent *e) override; + QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; void mouseMoveEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; @@ -158,11 +162,20 @@ private: void acceptInlineField(); void hideInlineFieldAndRefresh(); void activateTextOrdinalAtEnd(int ordinal); + [[nodiscard]] bool redirectKeyToField(QKeyEvent *e) const; + [[nodiscard]] bool redirectImeToField() const; + [[nodiscard]] bool prepareFieldForInput(); + [[nodiscard]] std::optional removeCurrentStructuralSelection( + bool forward); + [[nodiscard]] bool replayKeyIntoField(QKeyEvent *e); + [[nodiscard]] bool replayImeIntoField(QInputMethodEvent *e); + [[nodiscard]] bool handleTabNavigation(QKeyEvent *e); [[nodiscard]] bool handleFieldKey(QKeyEvent *e); [[nodiscard]] bool moveBoundary(bool forward, bool allowTrailing); [[nodiscard]] bool moveBoundaryAfterCommit( bool forward, bool allowTrailing); + [[nodiscard]] bool moveTabBoundary(bool forward); [[nodiscard]] bool removeBoundaryOwner(bool forward); void ensurePendingActivation(); void updateInlineFieldHeightOverride(); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index 465002fb45..674a73be0d 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1850,6 +1850,10 @@ not_null<::MainWindow*> SessionController::widget() const { return _window->widget(); } +rpl::producer<> SessionController::imeCompositionStarts() const { + return widget()->imeCompositionStarts(); +} + auto SessionController::sendingAnimation() const -> Ui::MessageSendingAnimationController & { return *_sendingAnimation; diff --git a/Telegram/SourceFiles/window/window_session_controller.h b/Telegram/SourceFiles/window/window_session_controller.h index 6d43ec06a1..b11ed93434 100644 --- a/Telegram/SourceFiles/window/window_session_controller.h +++ b/Telegram/SourceFiles/window/window_session_controller.h @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_type.h" #include "window/window_adaptive.h" +#include #include class PhotoData; @@ -415,6 +416,7 @@ public: [[nodiscard]] SeparateId windowId() const; [[nodiscard]] bool isPrimary() const; [[nodiscard]] not_null<::MainWindow*> widget() const; + [[nodiscard]] rpl::producer<> imeCompositionStarts() const; [[nodiscard]] not_null content() const; [[nodiscard]] Adaptive &adaptive() const; [[nodiscard]] ChatHelpers::EmojiInteractions &emojiInteractions() const {