// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // #pragma once #include "base/flat_set.h" #include "base/timer.h" #include "ui/emoji_config.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" #include "ui/text/text_entity.h" #include "ui/text/text_custom_emoji.h" #include #include #include class QMenu; class QShortcut; class QTextEdit; class QTouchEvent; class QContextMenuEvent; class Painter; namespace Ui::Text { struct QuotePaintCache; } // namespace Ui::Text namespace style { struct InputField; } // namespace style namespace Ui { const auto kClearFormatSequence = QKeySequence("ctrl+shift+n"); const auto kStrikeOutSequence = QKeySequence("ctrl+shift+x"); const auto kBlockquoteSequence = QKeySequence("ctrl+shift+."); const auto kMonospaceSequence = QKeySequence("ctrl+shift+m"); const auto kEditLinkSequence = QKeySequence("ctrl+k"); const auto kSpoilerSequence = QKeySequence("ctrl+shift+p"); class PopupMenu; class InputField; void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji); void InsertCustomEmojiAtCursor( not_null field, QTextCursor cursor, const QString &text, const QString &link); struct InstantReplaces { struct Node { QString text; std::map tail; }; void add(const QString &what, const QString &with); static const InstantReplaces &Default(); static const InstantReplaces &TextOnly(); int maxLength = 0; Node reverseMap; }; enum class InputSubmitSettings { Enter, CtrlEnter, Both, None, }; class CustomFieldObject; struct MarkdownEnabled { base::flat_set tagsSubset; friend inline bool operator==( const MarkdownEnabled &, const MarkdownEnabled &) = default; }; struct MarkdownDisabled { friend inline bool operator==( const MarkdownDisabled &, const MarkdownDisabled &) = default; }; struct MarkdownEnabledState { std::variant data; [[nodiscard]] bool disabled() const; [[nodiscard]] bool enabledForTag(QStringView tag) const; friend inline bool operator==( const MarkdownEnabledState &, const MarkdownEnabledState &) = default; }; struct InputFieldTextRange { int from = 0; int till = 0; friend inline bool operator==( InputFieldTextRange, InputFieldTextRange) = default; [[nodiscard]] bool empty() const { return (till <= from); } }; struct InputFieldSpoilerRect { QRect geometry; bool blockquote = false; }; class InputField : public RpWidget { public: enum class Mode { SingleLine, NoNewlines, MultiLine, }; using TagList = TextWithTags::Tags; struct MarkdownTag { // With each emoji being QChar::ObjectReplacementCharacter. int internalStart = 0; int internalLength = 0; // Adjusted by emoji to match _lastTextWithTags. int adjustedStart = 0; int adjustedLength = 0; bool closed = false; QString tag; }; static const QString kTagBold; static const QString kTagItalic; static const QString kTagUnderline; static const QString kTagStrikeOut; static const QString kTagCode; static const QString kTagPre; static const QString kTagSpoiler; static const QString kTagBlockquote; static const QString kTagBlockquoteCollapsed; static const QString kCustomEmojiTagStart; static const int kCollapsedQuoteFormat; // QTextFormat::ObjectTypes static const int kCustomEmojiFormat; // QTextFormat::ObjectTypes static const int kCustomEmojiId; // QTextFormat::Property static const int kCustomEmojiLink; // QTextFormat::Property static const int kQuoteId; // QTextFormat::Property InputField( QWidget *parent, const style::InputField &st, rpl::producer placeholder, const QString &value = QString()); InputField( QWidget *parent, const style::InputField &st, Mode mode, rpl::producer placeholder, const QString &value); InputField( QWidget *parent, const style::InputField &st, Mode mode = Mode::SingleLine, rpl::producer placeholder = nullptr, const TextWithTags &value = TextWithTags()); [[nodiscard]] const style::InputField &st() const { return _st; } void showError(); void showErrorNoFocus(); void hideError(); void setMaxLength(int maxLength); void setMinHeight(int minHeight); void setMaxHeight(int maxHeight); [[nodiscard]] const TextWithTags &getTextWithTags() const { return _lastTextWithTags; } [[nodiscard]] const std::vector &getMarkdownTags() const { return _lastMarkdownTags; } [[nodiscard]] TextWithTags getTextWithTagsPart( int start, int end = -1) const; [[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const; void insertTag(const QString &text, QString tagId = QString()); [[nodiscard]] bool empty() const { return _lastTextWithTags.text.isEmpty(); } enum class HistoryAction { NewEntry, MergeEntry, Clear, }; void setTextWithTags( const TextWithTags &textWithTags, HistoryAction historyAction = HistoryAction::NewEntry); // If you need to make some preparations of tags before putting them to QMimeData // (and then to clipboard or to drag-n-drop object), here is a strategy for that. void setTagMimeProcessor(Fn processor); void setCustomTextContext( Text::MarkedContext context, Fn pausedEmoji = nullptr, Fn pausedSpoiler = nullptr); struct EditLinkSelection { int from = 0; int till = 0; }; enum class EditLinkAction { Check, Edit, }; void setEditLinkCallback( Fn callback); void setEditLanguageCallback( Fn save)> callback); struct ExtendedContextMenu { QMenu *menu = nullptr; std::shared_ptr event; }; void setDocumentMargin(float64 margin); void setAdditionalMargin(int margin); void setAdditionalMargins(QMargins margins); void setInstantReplaces(const InstantReplaces &replaces); void setInstantReplacesEnabled(rpl::producer enabled); void setMarkdownReplacesEnabled(bool enabled); void setMarkdownReplacesEnabled(rpl::producer enabled); void setExtendedContextMenu(rpl::producer value); void commitInstantReplacement( int from, int till, const QString &with, const QString &customEmojiData); void commitMarkdownLinkEdit( EditLinkSelection selection, const TextWithTags &textWithTags, const QString &link); [[nodiscard]] static bool IsValidMarkdownLink(QStringView link); [[nodiscard]] static bool IsCustomEmojiLink(QStringView link); [[nodiscard]] static QString CustomEmojiLink(QStringView entityData); [[nodiscard]] static QString CustomEmojiEntityData(QStringView link); [[nodiscard]] const QString &getLastText() const { return _lastTextWithTags.text; } void setPlaceholder( rpl::producer placeholder, int afterSymbols = 0); void setPlaceholderHidden(bool forcePlaceholderHidden); void setDisplayFocused(bool focused); void finishAnimating(); void setFocusFast() { setDisplayFocused(true); setFocus(); } QSize sizeHint() const override; QSize minimumSizeHint() const override; bool hasText() const; void selectAll(); bool isUndoAvailable() const; bool isRedoAvailable() const; [[nodiscard]] MarkdownEnabledState markdownEnabledState() const { return _markdownEnabledState; } using SubmitSettings = InputSubmitSettings; void setSubmitSettings(SubmitSettings settings); static bool ShouldSubmit( SubmitSettings settings, Qt::KeyboardModifiers modifiers); void customUpDown(bool isCustom); void customTab(bool isCustom); int borderAnimationStart() const; not_null document(); not_null document() const; void setTextCursor(const QTextCursor &cursor); void setCursorPosition(int position); QTextCursor textCursor() const; void setText(const QString &text); void clear(); bool hasFocus() const; void setFocus(); void clearFocus(); void ensureCursorVisible(); not_null rawTextEdit(); not_null rawTextEdit() const; enum class MimeAction { Check, Insert, }; using MimeDataHook = Fn data, MimeAction action)>; void setMimeDataHook(MimeDataHook hook) { _mimeDataHook = std::move(hook); } const rpl::variable &scrollTop() const; int scrollTopMax() const; void scrollTo(int top); struct DocumentChangeInfo { int position = 0; int added = 0; int removed = 0; }; auto documentContentsChanges() { return _documentContentsChanges.events(); } auto markdownTagApplies() { return _markdownTagApplies.events(); } void setPreCache(Fn()> make); void setBlockquoteCache(Fn()> make); [[nodiscard]] bool menuShown() const; [[nodiscard]] rpl::producer menuShownValue() const; [[nodiscard]] rpl::producer<> heightChanges() const; [[nodiscard]] rpl::producer focusedChanges() const; [[nodiscard]] rpl::producer<> tabbed() const; [[nodiscard]] rpl::producer<> cancelled() const; [[nodiscard]] rpl::producer<> changes() const; [[nodiscard]] rpl::producer submits() const; void forceProcessContentsChanges(); ~InputField(); protected: void startPlaceholderAnimation(); void startBorderAnimation(); void paintEvent(QPaintEvent *e) override; void focusInEvent(QFocusEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; void resizeEvent(QResizeEvent *e) override; private: class Inner; friend class Inner; friend class CustomFieldObject; friend class FieldSpoilerOverlay; using TextRange = InputFieldTextRange; using SpoilerRect = InputFieldSpoilerRect; enum class MarkdownActionType { ToggleTag, EditLink, }; struct MarkdownAction { QKeySequence sequence; QString tag; MarkdownActionType type = MarkdownActionType::ToggleTag; }; void handleContentsChanged(); void updateRootFrameFormat(); bool viewportEventInner(QEvent *e); void handleTouchEvent(QTouchEvent *e); void updatePalette(); void refreshPlaceholder(const QString &text); int placeholderSkipWidth() const; [[nodiscard]] static std::vector MarkdownActions(); void setupMarkdownShortcuts(); bool executeMarkdownAction(MarkdownAction action); bool heightAutoupdated(); void checkContentHeight(); void setErrorShown(bool error); void focusInEventInner(QFocusEvent *e); void focusOutEventInner(QFocusEvent *e); void setFocused(bool focused); void keyPressEventInner(QKeyEvent *e); void contextMenuEventInner(QContextMenuEvent *e, QMenu *m = nullptr); void dropEventInner(QDropEvent *e); void inputMethodEventInner(QInputMethodEvent *e); void paintEventInner(QPaintEvent *e); void paintQuotes(QPaintEvent *e); void mousePressEventInner(QMouseEvent *e); void mouseReleaseEventInner(QMouseEvent *e); void mouseMoveEventInner(QMouseEvent *e); void leaveEventInner(QEvent *e); [[nodiscard]] int lookupActionQuoteId(QPoint point) const; void updateCursorShape(); QMimeData *createMimeDataFromSelectionInner() const; bool canInsertFromMimeDataInner(const QMimeData *source) const; void insertFromMimeDataInner(const QMimeData *source); TextWithTags getTextWithTagsSelected() const; void documentContentsChanged( int position, int charsRemoved, int charsAdded); void focusInner(); // "start" and "end" are in coordinates of text where emoji are replaced // by ObjectReplacementCharacter. If "end" = -1 means get text till the end. [[nodiscard]] QString getTextPart( int start, int end, TagList &outTagsList, bool &outTagsChanged, std::vector *outMarkdownTags = nullptr) const; // After any characters added we must postprocess them. This includes: // 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px. // 2. Replacing font family from semibold for all non-~ characters, if we used ... // 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics. // 4. Interrupting tags in which the text was inserted by any char except a letter. // 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text. // Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end). void processFormatting(int changedPosition, int changedEnd); void chopByMaxLength(int insertPosition, int insertLength); bool processMarkdownReplaces(const QString &appended); //bool processMarkdownReplace(const QString &tag); void addMarkdownActions(not_null menu, QContextMenuEvent *e); void addMarkdownMenuAction( not_null menu, not_null action); bool handleMarkdownKey(QKeyEvent *e); // We don't want accidentally detach InstantReplaces map. // So we access it only by const reference from this method. const InstantReplaces &instantReplaces() const; void processInstantReplaces(const QString &appended); void applyInstantReplace(const QString &what, const QString &with); struct EditLinkData { int from = 0; int till = 0; QString link; }; EditLinkData selectionEditLinkData(EditLinkSelection selection) const; EditLinkSelection editLinkSelection(QContextMenuEvent *e) const; void editMarkdownLink(EditLinkSelection selection); void commitInstantReplacement( int from, int till, const QString &with, const QString &customEmojiData, std::optional checkOriginal, bool checkIfInMonospace); #if 0 bool commitMarkdownReplacement( int from, int till, const QString &tag, const QString &edge = QString()); #endif TextRange insertWithTags(TextRange range, TextWithTags text); TextRange addMarkdownTag(TextRange range, const QString &tag); void removeMarkdownTag(TextRange range, const QString &tag); void finishMarkdownTagChange( TextRange range, const TextWithTags &textWithTags); void toggleSelectionMarkdown(const QString &tag); void clearSelectionMarkdown(); bool revertFormatReplace(); bool jumpOutOfBlockByBackspace(); void paintSurrounding( QPainter &p, QRect clip, float64 errorDegree, float64 focusedDegree); void paintRoundSurrounding( QPainter &p, QRect clip, float64 errorDegree, float64 focusedDegree); void paintFlatSurrounding( QPainter &p, QRect clip, float64 errorDegree, float64 focusedDegree); void customEmojiRepaint(); void highlightMarkdown(); bool exitQuoteWithNewBlock(int key); void blockActionClicked(int quoteId); void editPreLanguage(int quoteId, QStringView tag); void toggleBlockquoteCollapsed( int quoteId, QStringView tag, TextRange range); void trippleEnterExitBlock(QTextCursor &cursor); void touchUpdate(QPoint globalPosition); void touchFinish(); const style::InputField &_st; Fn()> _preCache; Fn()> _blockquoteCache; Mode _mode = Mode::SingleLine; int _maxLength = -1; int _minHeight = -1; int _maxHeight = -1; const std::unique_ptr _inner; Fn _editLinkCallback; Fn save)> _editLanguageCallback; TextWithTags _lastTextWithTags; std::vector _lastMarkdownTags; QString _lastPreEditText; std::optional _inputMethodCommit; mutable std::vector _spoilerRangesText; mutable std::vector _spoilerRangesEmoji; mutable std::vector _spoilerRects; mutable QColor _blockquoteBg; std::unique_ptr _spoilerOverlay; QMargins _additionalMargins; QMargins _customFontMargins; int _placeholderCustomFontSkip = 0; int _requestedDocumentTopMargin = 0; bool _forcePlaceholderHidden = false; bool _reverseMarkdownReplacement = false; bool _customEmojiRepaintScheduled = false; bool _settingDocumentMargin = false; // Tags list which we should apply while setText() call or insert from mime data. TagList _insertedTags; bool _insertedTagsAreFromMime = false; bool _insertedTagsReplace = false; // Override insert position and charsAdded from complex text editing // (like drag-n-drop in the same text edit field). int _realInsertPosition = -1; int _realCharsAdded = 0; // Calculate the amount of emoji extra chars // before _documentContentsChanges fire. int _emojiSurrogateAmount = 0; Fn _tagMimeProcessor; std::unique_ptr _customObject; std::optional _formattingCursorUpdate; SubmitSettings _submitSettings = SubmitSettings::Enter; MarkdownEnabledState _markdownEnabledState; bool _undoAvailable = false; bool _redoAvailable = false; bool _insertedTagsDelayClear = false; bool _inHeightCheck = false; bool _customUpDown = false; bool _customTab = false; rpl::variable _placeholderFull; QString _placeholder; int _placeholderAfterSymbols = 0; Animations::Simple _a_placeholderShifted; bool _placeholderShifted = false; QPainterPath _placeholderPath; Animations::Simple _a_borderShown; int _borderAnimationStart = 0; Animations::Simple _a_borderOpacity; bool _borderVisible = false; Animations::Simple _a_focused; Animations::Simple _a_error; bool _focused = false; bool _error = false; base::Timer _touchTimer; bool _touchPress = false; bool _touchRightButton = false; bool _touchMove = false; bool _mousePressedInTouch = false; QPoint _touchStart; bool _correcting = false; MimeDataHook _mimeDataHook; rpl::event_stream _menuShownChanges; base::unique_qptr _contextMenu; QTextCharFormat _defaultCharFormat; int _selectedActionQuoteId = 0; int _pressedActionQuoteId = -1; rpl::variable _scrollTop; InstantReplaces _mutableInstantReplaces; bool _instantReplacesEnabled = true; rpl::event_stream _documentContentsChanges; rpl::event_stream _markdownTagApplies; std::vector> _markdownShortcuts; rpl::event_stream _focusedChanges; rpl::event_stream<> _heightChanges; rpl::event_stream<> _tabbed; rpl::event_stream<> _cancelled; rpl::event_stream<> _changes; rpl::event_stream _submits; }; void PrepareFormattingOptimization(not_null document); [[nodiscard]] int ComputeRealUnicodeCharactersCount(const QString &text); [[nodiscard]] int ComputeFieldCharacterCount(not_null field); void AddLengthLimitLabel( not_null field, int limit, std::optional customThreshold = std::nullopt, int limitLabelTop = 0); } // namespace Ui