// 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 "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 "styles/style_widgets.h" #include #include #include #include #include #include class QTouchEvent; class Painter; namespace Ui::Text { class CustomEmoji; } // namespace Ui::Text namespace Ui { const auto kClearFormatSequence = QKeySequence("ctrl+shift+n"); const auto kStrikeOutSequence = QKeySequence("ctrl+shift+x"); 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 CustomEmojiObject : public QObject, public QTextObjectInterface { Q_OBJECT Q_INTERFACES(QTextObjectInterface) public: using Factory = Fn(QStringView)>; CustomEmojiObject(Factory factory, Fn paused); ~CustomEmojiObject(); QSizeF intrinsicSize( QTextDocument *doc, int posInDocument, const QTextFormat &format) override; void drawObject( QPainter *painter, const QRectF &rect, QTextDocument *doc, int posInDocument, const QTextFormat &format) override; void setNow(crl::time now); void clear(); private: Factory _factory; Fn _paused; base::flat_map> _emoji; crl::time _now = 0; int _skip = 0; }; class InputField : public RpWidget { Q_OBJECT public: enum class Mode { SingleLine, NoNewlines, MultiLine, }; using TagList = TextWithTags::Tags; using CustomEmojiFactory = Text::CustomEmojiFactory; 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 kCustomEmojiTagStart; static const int kCustomEmojiFormat; 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); const TextWithTags &getTextWithTags() const { return _lastTextWithTags; } const std::vector &getMarkdownTags() const { return _lastMarkdownTags; } TextWithTags getTextWithTagsPart(int start, int end = -1) const; TextWithTags getTextWithAppliedMarkdown() const; void insertTag(const QString &text, QString tagId = QString()); 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 setCustomEmojiFactory( CustomEmojiFactory factory, Fn paused = nullptr); struct EditLinkSelection { int from = 0; int till = 0; }; enum class EditLinkAction { Check, Edit, }; void setEditLinkCallback( Fn callback); struct ExtendedContextMenu { QMenu *menu = nullptr; std::shared_ptr event; }; void setAdditionalMargin(int margin); void setAdditionalMargins(QMargins margins); void setInstantReplaces(const InstantReplaces &replaces); void setInstantReplacesEnabled(rpl::producer 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 QString &text, 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); 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; bool isMarkdownEnabled() const { return _markdownEnabled; } 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(); } ~InputField(); private Q_SLOTS: void onTouchTimer(); void onDocumentContentsChange(int position, int charsRemoved, int charsAdded); void onCursorPositionChanged(); void onUndoAvailable(bool avail); void onRedoAvailable(bool avail); void onFocusInner(); Q_SIGNALS: void changed(); void submitted(Qt::KeyboardModifiers); void cancelled(); void tabbed(); void focused(); void blurred(); void resized(); 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; void handleContentsChanged(); bool viewportEventInner(QEvent *e); void handleTouchEvent(QTouchEvent *e); void updatePalette(); void refreshPlaceholder(const QString &text); int placeholderSkipWidth() const; 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 mousePressEventInner(QMouseEvent *e); void mouseReleaseEventInner(QMouseEvent *e); void mouseMoveEventInner(QMouseEvent *e); QMimeData *createMimeDataFromSelectionInner() const; bool canInsertFromMimeDataInner(const QMimeData *source) const; void insertFromMimeDataInner(const QMimeData *source); TextWithTags getTextWithTagsSelected() const; // "start" and "end" are in coordinates of text where emoji are replaced // by ObjectReplacementCharacter. If "end" = -1 means get text till the end. 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); bool commitMarkdownReplacement( int from, int till, const QString &tag, const QString &edge = QString()); void addMarkdownTag(int from, int till, const QString &tag); void removeMarkdownTag(int from, int till, const QString &tag); void finishMarkdownTagChange( int from, int till, const TextWithTags &textWithTags); void toggleSelectionMarkdown(const QString &tag); void clearSelectionMarkdown(); bool revertFormatReplace(); 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(); void touchUpdate(QPoint globalPosition); void touchFinish(); const style::InputField &_st; Mode _mode = Mode::SingleLine; int _maxLength = -1; int _minHeight = -1; int _maxHeight = -1; const std::unique_ptr _inner; Fn _editLinkCallback; TextWithTags _lastTextWithTags; std::vector _lastMarkdownTags; QString _lastPreEditText; std::optional _inputMethodCommit; QMargins _additionalMargins; bool _forcePlaceholderHidden = false; bool _reverseMarkdownReplacement = false; bool _customEmojiRepaintScheduled = false; // Tags list which we should apply while setText() call or insert from mime data. TagList _insertedTags; bool _insertedTagsAreFromMime; // 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 _customEmojiObject; SubmitSettings _submitSettings = SubmitSettings::Enter; bool _markdownEnabled = false; bool _undoAvailable = false; bool _redoAvailable = false; bool _inDrop = 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; QTimer _touchTimer; bool _touchPress = false; bool _touchRightButton = false; bool _touchMove = false; bool _mousePressedInTouch = false; QPoint _touchStart; bool _correcting = false; MimeDataHook _mimeDataHook; base::unique_qptr _contextMenu; QTextCharFormat _defaultCharFormat; rpl::variable _scrollTop; InstantReplaces _mutableInstantReplaces; bool _instantReplacesEnabled = true; rpl::event_stream _documentContentsChanges; rpl::event_stream _markdownTagApplies; }; class MaskedInputField : public RpWidgetBase { Q_OBJECT using Parent = RpWidgetBase; public: MaskedInputField( QWidget *parent, const style::InputField &st, rpl::producer placeholder = nullptr, const QString &val = QString()); void showError(); void showErrorNoFocus(); void hideError(); QSize sizeHint() const override; QSize minimumSizeHint() const override; void customUpDown(bool isCustom); int borderAnimationStart() const; const QString &getLastText() const { return _oldtext; } void setPlaceholder(rpl::producer placeholder); void setPlaceholderHidden(bool forcePlaceholderHidden); void setDisplayFocused(bool focused); void finishAnimating(); void setFocusFast() { setDisplayFocused(true); setFocus(); } void setText(const QString &text) { QLineEdit::setText(text); startPlaceholderAnimation(); } void clear() { QLineEdit::clear(); startPlaceholderAnimation(); } public Q_SLOTS: void onTextChange(const QString &text); void onCursorPositionChanged(int oldPosition, int position); void onTextEdited(); void onTouchTimer(); Q_SIGNALS: void changed(); void cancelled(); void submitted(Qt::KeyboardModifiers); void focused(); void blurred(); protected: QString getDisplayedText() const { auto result = getLastText(); if (!_lastPreEditText.isEmpty()) { result = result.mid(0, _oldcursor) + _lastPreEditText + result.mid(_oldcursor); } return result; } void startBorderAnimation(); void startPlaceholderAnimation(); bool eventHook(QEvent *e) override; void touchEvent(QTouchEvent *e); void paintEvent(QPaintEvent *e) override; void focusInEvent(QFocusEvent *e) override; void focusOutEvent(QFocusEvent *e) override; void keyPressEvent(QKeyEvent *e) override; void resizeEvent(QResizeEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override; void inputMethodEvent(QInputMethodEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; virtual void correctValue( const QString &was, int wasCursor, QString &now, int &nowCursor) { } void setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos); virtual void paintAdditionalPlaceholder(QPainter &p) { } style::font phFont() { return _st.font; } void placeholderAdditionalPrepare(QPainter &p); QRect placeholderRect() const; void setTextMargins(const QMargins &mrg); const style::InputField &_st; private: void updatePalette(); void refreshPlaceholder(const QString &text); void setErrorShown(bool error); void touchUpdate(QPoint globalPosition); void touchFinish(); void setFocused(bool focused); int _maxLength = -1; bool _forcePlaceholderHidden = false; QString _oldtext; int _oldcursor = 0; QString _lastPreEditText; bool _undoAvailable = false; bool _redoAvailable = false; bool _customUpDown = false; rpl::variable _placeholderFull; QString _placeholder; 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; style::margins _textMargins; QTimer _touchTimer; bool _touchPress = false; bool _touchRightButton = false; bool _touchMove = false; bool _mousePressedInTouch = false; QPoint _touchStart; base::unique_qptr _contextMenu; }; class PasswordInput : public MaskedInputField { public: PasswordInput(QWidget *parent, const style::InputField &st, rpl::producer placeholder = nullptr, const QString &val = QString()); }; class NumberInput : public MaskedInputField { public: NumberInput( QWidget *parent, const style::InputField &st, rpl::producer placeholder, const QString &value, int limit); protected: void correctValue( const QString &was, int wasCursor, QString &now, int &nowCursor) override; private: int _limit = 0; }; class HexInput : public MaskedInputField { public: HexInput(QWidget *parent, const style::InputField &st, rpl::producer placeholder, const QString &val); protected: void correctValue( const QString &was, int wasCursor, QString &now, int &nowCursor) override; }; } // namespace Ui