init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,304 @@
// 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
//
#include "ui/widgets/fields/custom_field_object.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/text/text.h"
#include "ui/text/text_renderer.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/integration.h"
#include "styles/style_basic.h"
#include "styles/style_widgets.h"
#include <QtWidgets/QTextEdit>
#include <QtWidgets/QScrollBar>
namespace Ui {
namespace {
constexpr auto kSpoilerHiddenOpacity = 0.5;
using SpoilerRect = InputFieldSpoilerRect;
} // namespace
class FieldSpoilerOverlay final : public RpWidget {
public:
FieldSpoilerOverlay(
not_null<InputField*> field,
Fn<float64()> shown,
Fn<bool()> paused);
private:
void paintEvent(QPaintEvent *e) override;
const not_null<InputField*> _field;
const Fn<float64()> _shown;
const Fn<bool()> _paused;
SpoilerAnimation _animation;
};
FieldSpoilerOverlay::FieldSpoilerOverlay(
not_null<InputField*> field,
Fn<float64()> shown,
Fn<bool()> paused)
: RpWidget(field->rawTextEdit())
, _field(field)
, _shown(std::move(shown))
, _paused(std::move(paused))
, _animation([=] { update(); }) {
setAttribute(Qt::WA_TransparentForMouseEvents);
show();
}
void FieldSpoilerOverlay::paintEvent(QPaintEvent *e) {
auto p = std::optional<QPainter>();
auto topShift = std::optional<int>();
auto frame = std::optional<SpoilerMessFrame>();
auto blockquoteBg = std::optional<QColor>();
const auto clip = e->rect();
const auto shown = _shown();
const auto bgOpacity = shown;
const auto fgOpacity = 1. * shown + kSpoilerHiddenOpacity * (1. - shown);
for (const auto &rect : _field->_spoilerRects) {
const auto fill = rect.geometry.intersected(clip);
if (fill.isEmpty()) {
continue;
} else if (!p) {
p.emplace(this);
const auto paused = _paused && _paused();
frame.emplace(
Text::DefaultSpoilerCache()->lookup(
st::defaultTextPalette.spoilerFg->c)->frame(
_animation.index(crl::now(), paused)));
topShift = -_field->rawTextEdit()->verticalScrollBar()->value();
}
if (bgOpacity > 0.) {
p->setOpacity(bgOpacity);
if (rect.blockquote && !blockquoteBg) {
const auto bg = _field->_blockquoteBg;
blockquoteBg = (bg.alphaF() < 1.)
? anim::color(
_field->_st.textBg->c,
QColor(bg.red(), bg.green(), bg.blue()),
bg.alphaF())
: bg;
}
p->fillRect(
fill,
rect.blockquote ? *blockquoteBg : _field->_st.textBg->c);
}
p->setOpacity(fgOpacity);
const auto shift = QPoint(0, *topShift) - rect.geometry.topLeft();
FillSpoilerRect(*p, rect.geometry, *frame, shift);
}
}
CustomFieldObject::CustomFieldObject(
not_null<InputField*> field,
Text::MarkedContext context,
Fn<bool()> pausedEmoji,
Fn<bool()> pausedSpoiler)
: _field(field)
, _context(std::move(context))
, _pausedEmoji(std::move(pausedEmoji))
, _pausedSpoiler(std::move(pausedSpoiler))
, _factory(makeFactory())
, _now(crl::now()) {
}
CustomFieldObject::~CustomFieldObject() = default;
void *CustomFieldObject::qt_metacast(const char *iid) {
if (QLatin1String(iid) == qobject_interface_iid<QTextObjectInterface*>()) {
return static_cast<QTextObjectInterface*>(this);
}
return QObject::qt_metacast(iid);
}
QSizeF CustomFieldObject::intrinsicSize(
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) {
const auto line = _field->_st.style.font->height;
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
const auto &padding = _field->_st.style.blockquote.padding;
const auto paddings = padding.left() + padding.right();
const auto skip = 2 * doc->documentMargin();
const auto height = Text::kQuoteCollapsedLines * line;
return QSizeF(doc->pageSize().width() - paddings - skip, height);
}
const auto size = st::emojiSize * 1.;
const auto width = size + st::emojiPadding * 2.;
const auto height = std::max(line * 1., size);
if (!_skip) {
const auto emoji = Text::AdjustCustomEmojiSize(st::emojiSize);
_skip = (st::emojiSize - emoji) / 2;
}
return { width, height };
}
void CustomFieldObject::drawObject(
QPainter *painter,
const QRectF &rect,
QTextDocument *doc,
int posInDocument,
const QTextFormat &format) {
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
const auto left = 0;
const auto top = 0;
const auto id = format.property(InputField::kQuoteId).toInt();
if (const auto i = _quotes.find(id); i != end(_quotes)) {
i->second.string.draw(*painter, {
.position = QPoint(left + rect.x(), top + rect.y()),
.outerWidth = int(base::SafeRound(doc->pageSize().width())),
.availableWidth = int(std::floor(rect.width())),
.palette = nullptr,
.spoiler = Text::DefaultSpoilerCache(),
.now = _now,
.pausedEmoji = _pausedEmoji(),
.pausedSpoiler = _pausedSpoiler(),
.elisionLines = Text::kQuoteCollapsedLines,
.useFullWidth = true,
});
}
return;
}
const auto id = format.property(InputField::kCustomEmojiId).toULongLong();
if (!id) {
return;
}
auto i = _emoji.find(id);
if (i == end(_emoji)) {
const auto link = format.property(InputField::kCustomEmojiLink);
const auto data = InputField::CustomEmojiEntityData(link.toString());
if (auto emoji = _factory(data)) {
i = _emoji.emplace(id, std::move(emoji)).first;
}
}
if (i == end(_emoji)) {
return;
}
i->second->paint(*painter, {
.textColor = format.foreground().color(),
.now = _now,
.position = QPoint(
int(base::SafeRound(rect.x())) + st::emojiPadding + _skip,
int(base::SafeRound(rect.y())) + _skip),
.paused = _pausedEmoji && _pausedEmoji(),
});
}
void CustomFieldObject::clearEmoji() {
_emoji.clear();
}
void CustomFieldObject::clearQuotes() {
_quotes.clear();
}
std::unique_ptr<RpWidget> CustomFieldObject::createSpoilerOverlay() {
return std::make_unique<FieldSpoilerOverlay>(
_field,
[=] { return _spoilerOpacity.value(_spoilerHidden ? 0. : 1.); },
_pausedSpoiler);
}
void CustomFieldObject::refreshSpoilerShown(InputFieldTextRange range) {
auto hidden = false;
using Range = InputFieldTextRange;
const auto intersects = [](Range a, Range b) {
return (a.from < b.till) && (b.from < a.till);
};
if (range.till > range.from) {
const auto check = [&](const std::vector<Range> &list) {
if (!hidden) {
for (const auto &spoiler : list) {
if (intersects(spoiler, range)) {
hidden = true;
break;
}
}
}
};
check(_field->_spoilerRangesText);
check(_field->_spoilerRangesEmoji);
} else {
auto touchesLeft = false;
auto touchesRight = false;
const auto cursor = range.from;
const auto check = [&](const std::vector<Range> &list) {
if (!touchesLeft || !touchesRight) {
for (const auto &spoiler : list) {
if (spoiler.from <= cursor && spoiler.till >= cursor) {
if (spoiler.from < cursor) {
touchesLeft = true;
}
if (spoiler.till >= cursor) {
touchesRight = true;
}
if (touchesLeft && touchesRight) {
break;
}
}
}
}
};
check(_field->_spoilerRangesText);
check(_field->_spoilerRangesEmoji);
hidden = touchesLeft && touchesRight;
}
if (_spoilerHidden != hidden) {
_spoilerHidden = hidden;
_spoilerOpacity.start(
[=] { _field->update(); },
hidden ? 1. : 0.,
hidden ? 0. : 1.,
st::fadeWrapDuration);
}
}
void CustomFieldObject::setCollapsedText(int quoteId, TextWithTags text) {
auto &quote = _quotes[quoteId];
quote.string = Text::String(_field->_st.widthMin);
quote.string.setMarkedText(_field->_st.style, {
text.text,
TextUtilities::ConvertTextTagsToEntities(text.tags),
}, kMarkupTextOptions, makeFieldContext());
quote.text = std::move(text);
}
const TextWithTags &CustomFieldObject::collapsedText(int quoteId) const {
if (const auto i = _quotes.find(quoteId); i != end(_quotes)) {
return i->second.text;
}
static const auto kEmpty = TextWithTags();
return kEmpty;
}
Text::MarkedContext CustomFieldObject::makeFieldContext() {
auto context = _context;
context.repaint = [field = _field] { field->update(); };
return context;
}
CustomFieldObject::Factory CustomFieldObject::makeFactory() {
return [context = makeFieldContext()](QStringView data) {
return Text::MakeCustomEmoji(data, context);
};
}
void CustomFieldObject::setNow(crl::time now) {
_now = now;
}
} // namespace Ui

View File

@@ -0,0 +1,80 @@
// 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 <QtGui/QTextObjectInterface>
#include "ui/effects/animations.h"
#include "ui/text/text.h"
#include "ui/text/text_custom_emoji.h"
namespace Ui {
class InputField;
class RpWidget;
struct InputFieldTextRange;
class CustomFieldObject : public QObject, public QTextObjectInterface {
public:
CustomFieldObject(
not_null<InputField*> field,
Text::MarkedContext context,
Fn<bool()> pausedEmoji,
Fn<bool()> pausedSpoiler);
~CustomFieldObject();
void *qt_metacast(const char *iid) override;
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 setCollapsedText(int quoteId, TextWithTags text);
[[nodiscard]] const TextWithTags &collapsedText(int quoteId) const;
void setNow(crl::time now);
void clearEmoji();
void clearQuotes();
[[nodiscard]] std::unique_ptr<RpWidget> createSpoilerOverlay();
void refreshSpoilerShown(InputFieldTextRange range);
private:
struct Quote {
TextWithTags text;
Text::String string;
};
using Factory = Fn<std::unique_ptr<Text::CustomEmoji>(QStringView)>;
[[nodiscard]] Factory makeFactory();
[[nodiscard]] Text::MarkedContext makeFieldContext();
const not_null<InputField*> _field;
const Text::MarkedContext _context;
const Fn<bool()> _pausedEmoji;
const Fn<bool()> _pausedSpoiler;
const Factory _factory;
base::flat_map<uint64, std::unique_ptr<Text::CustomEmoji>> _emoji;
base::flat_map<int, Quote> _quotes;
crl::time _now = 0;
int _skip = 0;
Animations::Simple _spoilerOpacity;
bool _spoilerHidden = false;
};
} // namespace Ui

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,684 @@
// 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 <rpl/variable.h>
#include <QtGui/QTextCursor>
#include <any>
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<InputField*> field,
QTextCursor cursor,
const QString &text,
const QString &link);
struct InstantReplaces {
struct Node {
QString text;
std::map<QChar, Node> 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,
};
enum class MarkdownSet {
All,
Notes,
};
class CustomFieldObject;
struct MarkdownEnabled {
base::flat_set<QString> 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<MarkdownDisabled, MarkdownEnabled> 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<QString> placeholder,
const QString &value = QString());
InputField(
QWidget *parent,
const style::InputField &st,
Mode mode,
rpl::producer<QString> placeholder,
const QString &value);
InputField(
QWidget *parent,
const style::InputField &st,
Mode mode = Mode::SingleLine,
rpl::producer<QString> placeholder = nullptr,
const TextWithTags &value = TextWithTags());
QAccessible::Role accessibilityRole() override {
return QAccessible::Role::EditableText;
}
QString accessibilityName() override {
return _placeholderFull.current();
}
[[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);
void setMode(Mode mode);
[[nodiscard]] const TextWithTags &getTextWithTags() const {
return _lastTextWithTags;
}
[[nodiscard]] const std::vector<MarkdownTag> &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<QString(QStringView)> processor);
void setCustomTextContext(
Text::MarkedContext context,
Fn<bool()> pausedEmoji = nullptr,
Fn<bool()> pausedSpoiler = nullptr);
struct EditLinkSelection {
int from = 0;
int till = 0;
};
enum class EditLinkAction {
Check,
Edit,
};
void setEditLinkCallback(
Fn<bool(
EditLinkSelection selection,
TextWithTags text,
QString link,
EditLinkAction action)> callback);
void setEditLanguageCallback(
Fn<void(QString now, Fn<void(QString)> save)> callback);
struct ExtendedContextMenu {
QMenu *menu = nullptr;
std::shared_ptr<QContextMenuEvent> event;
};
void setDocumentMargin(float64 margin);
void setAdditionalMargin(int margin);
void setAdditionalMargins(QMargins margins);
void setInstantReplaces(const InstantReplaces &replaces);
void setInstantReplacesEnabled(rpl::producer<bool> enabled);
void setMarkdownReplacesEnabled(bool enabled);
void setMarkdownReplacesEnabled(rpl::producer<MarkdownEnabledState> enabled);
void setExtendedContextMenu(rpl::producer<ExtendedContextMenu> 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<QString> 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;
}
void setMarkdownSet(MarkdownSet set);
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<QTextDocument*> document();
not_null<const QTextDocument*> 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<QTextEdit*> rawTextEdit();
not_null<const QTextEdit*> rawTextEdit() const;
enum class MimeAction {
Check,
Insert,
};
using MimeDataHook = Fn<bool(
not_null<const QMimeData*> data,
MimeAction action)>;
void setMimeDataHook(MimeDataHook hook) {
_mimeDataHook = std::move(hook);
}
const rpl::variable<int> &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<not_null<Ui::Text::QuotePaintCache*>()> make);
void setBlockquoteCache(Fn<not_null<Ui::Text::QuotePaintCache*>()> make);
[[nodiscard]] bool menuShown() const;
[[nodiscard]] rpl::producer<bool> menuShownValue() const;
[[nodiscard]] rpl::producer<> heightChanges() const;
[[nodiscard]] rpl::producer<bool> focusedChanges() const;
[[nodiscard]] rpl::producer<> tabbed() const;
[[nodiscard]] rpl::producer<> cancelled() const;
[[nodiscard]] rpl::producer<> changes() const;
[[nodiscard]] rpl::producer<Qt::KeyboardModifiers> 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<MarkdownAction> MarkdownActions();
[[nodiscard]] static std::vector<MarkdownAction> MarkdownActionsNotes();
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<MarkdownTag> *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<QMenu*> menu, QContextMenuEvent *e);
void addMarkdownMenuAction(
not_null<QMenu*> menu,
not_null<QAction*> 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<QString> 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<not_null<Ui::Text::QuotePaintCache*>()> _preCache;
Fn<not_null<Ui::Text::QuotePaintCache*>()> _blockquoteCache;
Mode _mode = Mode::SingleLine;
int _maxLength = -1;
int _minHeight = -1;
int _maxHeight = -1;
const std::unique_ptr<Inner> _inner;
Fn<bool(
EditLinkSelection selection,
TextWithTags text,
QString link,
EditLinkAction action)> _editLinkCallback;
Fn<void(QString now, Fn<void(QString)> save)> _editLanguageCallback;
TextWithTags _lastTextWithTags;
std::vector<MarkdownTag> _lastMarkdownTags;
QString _lastPreEditText;
std::optional<QString> _inputMethodCommit;
mutable std::vector<TextRange> _spoilerRangesText;
mutable std::vector<TextRange> _spoilerRangesEmoji;
mutable std::vector<SpoilerRect> _spoilerRects;
mutable QColor _blockquoteBg;
std::unique_ptr<RpWidget> _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<QString(QStringView)> _tagMimeProcessor;
std::unique_ptr<CustomFieldObject> _customObject;
std::optional<QTextCursor> _formattingCursorUpdate;
SubmitSettings _submitSettings = SubmitSettings::Enter;
MarkdownEnabledState _markdownEnabledState;
MarkdownSet _markdownSet = MarkdownSet::All;
bool _undoAvailable = false;
bool _redoAvailable = false;
bool _insertedTagsDelayClear = false;
bool _inHeightCheck = false;
bool _customUpDown = false;
bool _customTab = false;
rpl::variable<QString> _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<bool> _menuShownChanges;
base::unique_qptr<PopupMenu> _contextMenu;
QTextCharFormat _defaultCharFormat;
int _selectedActionQuoteId = 0;
int _pressedActionQuoteId = -1;
rpl::variable<int> _scrollTop;
InstantReplaces _mutableInstantReplaces;
bool _instantReplacesEnabled = true;
rpl::event_stream<DocumentChangeInfo> _documentContentsChanges;
rpl::event_stream<MarkdownTag> _markdownTagApplies;
std::vector<std::unique_ptr<QShortcut>> _markdownShortcuts;
rpl::event_stream<bool> _focusedChanges;
rpl::event_stream<> _heightChanges;
rpl::event_stream<> _tabbed;
rpl::event_stream<> _cancelled;
rpl::event_stream<> _changes;
rpl::event_stream<Qt::KeyboardModifiers> _submits;
};
void PrepareFormattingOptimization(not_null<QTextDocument*> document);
[[nodiscard]] int ComputeRealUnicodeCharactersCount(const QString &text);
[[nodiscard]] int ComputeFieldCharacterCount(not_null<InputField*> field);
[[nodiscard]] bool ShouldSubmit(
QKeyEvent *event,
InputSubmitSettings settings);
struct LengthLimitLabelOptions {
RpWidget *customParent = nullptr;
std::optional<int> customThreshold = std::nullopt;
Fn<QPoint(QSize parent, QSize label)> customUpdatePosition;
Fn<int()> customCharactersCount;
int limitLabelTop = 0;
};
void AddLengthLimitLabel(
not_null<InputField*> field,
int limit,
LengthLimitLabelOptions options = {});
} // namespace Ui

View File

@@ -0,0 +1,555 @@
// 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
//
#include "ui/widgets/fields/masked_input_field.h"
#include "base/qt/qt_common_adapters.h"
#include "ui/painter.h"
#include "ui/widgets/popup_menu.h"
#include "ui/integration.h"
#include "styles/palette.h"
#include "styles/style_widgets.h"
#include <QtWidgets/QCommonStyle>
#include <QtWidgets/QApplication>
#include <QtGui/QClipboard>
namespace Ui {
namespace {
class InputStyle final : public QCommonStyle {
public:
InputStyle() {
setParent(QCoreApplication::instance());
}
void drawPrimitive(
PrimitiveElement element,
const QStyleOption *option,
QPainter *painter,
const QWidget *widget = nullptr) const override {
}
static InputStyle *instance() {
if (!_instance) {
if (!QGuiApplication::instance()) {
return nullptr;
}
_instance = new InputStyle();
}
return _instance;
}
~InputStyle() {
_instance = nullptr;
}
private:
static InputStyle *_instance;
};
InputStyle *InputStyle::_instance = nullptr;
} // namespace
MaskedInputField::MaskedInputField(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &val)
: Parent(val, parent)
, _st(st)
, _oldtext(val)
, _placeholderFull(std::move(placeholder)) {
resize(_st.width, _st.heightMin);
setFont(_st.style.font);
setAlignment(_st.textAlign);
_placeholderFull.value(
) | rpl::on_next([=](const QString &text) {
refreshPlaceholder(text);
setAccessibleName(text);
}, lifetime());
style::PaletteChanged(
) | rpl::on_next([=] {
updatePalette();
}, lifetime());
updatePalette();
if (_st.textBg->c.alphaF() >= 1. && !_st.borderRadius) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
connect(this, SIGNAL(textChanged(QString)), this, SLOT(onTextChange(QString)));
connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int)));
connect(this, SIGNAL(textEdited(QString)), this, SLOT(onTextEdited()));
connect(this, &MaskedInputField::selectionChanged, [] {
Integration::Instance().textActionsUpdated();
});
setStyle(InputStyle::instance());
QLineEdit::setTextMargins(0, 0, 0, 0);
setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1));
setFrame(false);
setAttribute(Qt::WA_AcceptTouchEvents);
_touchTimer.setSingleShot(true);
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
setTextMargins(_st.textMargins);
startPlaceholderAnimation();
startBorderAnimation();
finishAnimating();
}
void MaskedInputField::updatePalette() {
auto p = palette();
p.setColor(QPalette::Text, _st.textFg->c);
p.setColor(QPalette::Highlight, st::msgInBgSelected->c);
p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c);
setPalette(p);
}
void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) {
if (newPos < 0 || newPos > newText.size()) {
newPos = newText.size();
}
auto updateText = (newText != now);
if (updateText) {
now = newText;
setText(now);
startPlaceholderAnimation();
}
auto updateCursorPosition = (newPos != nowCursor) || updateText;
if (updateCursorPosition) {
nowCursor = newPos;
setCursorPosition(nowCursor);
}
}
void MaskedInputField::customUpDown(bool custom) {
_customUpDown = custom;
}
int MaskedInputField::borderAnimationStart() const {
return _borderAnimationStart;
}
void MaskedInputField::setTextMargins(const QMargins &mrg) {
_textMargins = mrg;
setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1));
refreshPlaceholder(_placeholderFull.current());
}
void MaskedInputField::onTouchTimer() {
_touchRightButton = true;
}
bool MaskedInputField::eventHook(QEvent *e) {
auto type = e->type();
if (type == QEvent::TouchBegin
|| type == QEvent::TouchUpdate
|| type == QEvent::TouchEnd
|| type == QEvent::TouchCancel) {
auto event = static_cast<QTouchEvent*>(e);
if (event->device()->type() == base::TouchDevice::TouchScreen) {
touchEvent(event);
}
}
return Parent::eventHook(e);
}
void MaskedInputField::touchEvent(QTouchEvent *e) {
switch (e->type()) {
case QEvent::TouchBegin: {
if (_touchPress || e->touchPoints().isEmpty()) {
return;
}
_touchTimer.start(QApplication::startDragTime());
_touchPress = true;
_touchMove = _touchRightButton = _mousePressedInTouch = false;
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
} break;
case QEvent::TouchUpdate: {
if (!e->touchPoints().isEmpty()) {
touchUpdate(e->touchPoints().cbegin()->screenPos().toPoint());
}
} break;
case QEvent::TouchEnd: {
touchFinish();
} break;
case QEvent::TouchCancel: {
_touchPress = false;
_touchTimer.stop();
} break;
}
}
void MaskedInputField::touchUpdate(QPoint globalPosition) {
if (_touchPress
&& !_touchMove
&& ((globalPosition - _touchStart).manhattanLength()
>= QApplication::startDragDistance())) {
_touchMove = true;
}
}
void MaskedInputField::touchFinish() {
if (!_touchPress) {
return;
}
const auto weak = base::make_weak(this);
if (!_touchMove && window()) {
QPoint mapped(mapFromGlobal(_touchStart));
if (_touchRightButton) {
QContextMenuEvent contextEvent(
QContextMenuEvent::Mouse,
mapped,
_touchStart);
contextMenuEvent(&contextEvent);
} else {
QGuiApplication::inputMethod()->show();
}
}
if (weak) {
_touchTimer.stop();
_touchPress
= _touchMove
= _touchRightButton
= _mousePressedInTouch = false;
}
}
void MaskedInputField::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto r = rect().intersected(e->rect());
p.fillRect(r, _st.textBg);
if (_st.border) {
p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg->b);
}
auto errorDegree = _a_error.value(_error ? 1. : 0.);
auto focusedDegree = _a_focused.value(_focused ? 1. : 0.);
auto borderShownDegree = _a_borderShown.value(1.);
auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
if (_st.borderActive && (borderOpacity > 0.)) {
auto borderStart = std::clamp(_borderAnimationStart, 0, width());
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
if (borderTo > borderFrom) {
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
p.setOpacity(borderOpacity);
p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
p.setOpacity(1);
}
}
p.setClipRect(r);
if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
auto placeholderShiftDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
p.save();
p.setClipRect(r);
auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
r.moveTop(r.top() + placeholderTop);
if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(placeholderFg);
p.translate(r.topLeft());
p.scale(placeholderScale, placeholderScale);
p.drawPath(_placeholderPath);
p.restore();
} else if (!_placeholder.isEmpty()) {
auto placeholderHiddenDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
if (placeholderHiddenDegree < 1.) {
p.setOpacity(1. - placeholderHiddenDegree);
p.save();
p.setClipRect(r);
auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
r.moveLeft(r.left() + placeholderLeft);
if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
p.setFont(_st.placeholderFont);
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
p.drawText(r, _placeholder, _st.placeholderAlign);
p.restore();
p.setOpacity(1.);
}
}
paintAdditionalPlaceholder(p);
QLineEdit::paintEvent(e);
}
void MaskedInputField::mousePressEvent(QMouseEvent *e) {
if (_touchPress && e->button() == Qt::LeftButton) {
_mousePressedInTouch = true;
_touchStart = e->globalPos();
}
return QLineEdit::mousePressEvent(e);
}
void MaskedInputField::mouseReleaseEvent(QMouseEvent *e) {
if (_mousePressedInTouch) {
touchFinish();
}
return QLineEdit::mouseReleaseEvent(e);
}
void MaskedInputField::mouseMoveEvent(QMouseEvent *e) {
if (_mousePressedInTouch) {
touchUpdate(e->globalPos());
}
return QLineEdit::mouseMoveEvent(e);
}
QString MaskedInputField::getDisplayedText() const {
auto result = getLastText();
if (!_lastPreEditText.isEmpty()) {
result = result.mid(0, _oldcursor)
+ _lastPreEditText
+ result.mid(_oldcursor);
}
return result;
}
void MaskedInputField::startBorderAnimation() {
auto borderVisible = (_error || _focused);
if (_borderVisible != borderVisible) {
_borderVisible = borderVisible;
if (_borderVisible) {
if (_a_borderOpacity.animating()) {
_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
} else {
_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
}
} else if (qFuzzyCompare(_a_borderShown.value(1.), 0.)) {
_a_borderShown.stop();
_a_borderOpacity.stop();
} else {
_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
}
}
}
void MaskedInputField::focusInEvent(QFocusEvent *e) {
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2);
setFocused(true);
QLineEdit::focusInEvent(e);
focused();
}
void MaskedInputField::focusOutEvent(QFocusEvent *e) {
setFocused(false);
QLineEdit::focusOutEvent(e);
blurred();
}
void MaskedInputField::setFocused(bool focused) {
if (_focused != focused) {
_focused = focused;
_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
startPlaceholderAnimation();
startBorderAnimation();
}
}
void MaskedInputField::resizeEvent(QResizeEvent *e) {
refreshPlaceholder(_placeholderFull.current());
_borderAnimationStart = width() / 2;
QLineEdit::resizeEvent(e);
}
void MaskedInputField::refreshPlaceholder(const QString &text) {
const auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
if (_st.placeholderScale > 0.) {
auto placeholderFont = _st.placeholderFont->f;
placeholderFont.setStyleStrategy(QFont::PreferMatch);
const auto metrics = QFontMetrics(placeholderFont);
_placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth);
_placeholderPath = QPainterPath();
if (!_placeholder.isEmpty()) {
const auto result = style::FindAdjustResult(placeholderFont);
const auto ascent = result ? result->iascent : metrics.ascent();
_placeholderPath.addText(0, ascent, placeholderFont, _placeholder);
}
} else {
_placeholder = _st.placeholderFont->elided(text, availableWidth);
}
update();
}
void MaskedInputField::setPlaceholder(rpl::producer<QString> placeholder) {
_placeholderFull = std::move(placeholder);
}
void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
if (const auto menu = createStandardContextMenu()) {
_contextMenu = base::make_unique_q<PopupMenu>(this, menu);
_contextMenu->popup(e->globalPos());
}
}
void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) {
QLineEdit::inputMethodEvent(e);
_lastPreEditText = e->preeditString();
update();
}
void MaskedInputField::showError() {
showErrorNoFocus();
if (!hasFocus()) {
setFocus();
}
}
void MaskedInputField::showErrorNoFocus() {
setErrorShown(true);
}
void MaskedInputField::hideError() {
setErrorShown(false);
}
void MaskedInputField::setErrorShown(bool error) {
if (_error != error) {
_error = error;
_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
startBorderAnimation();
}
}
QSize MaskedInputField::sizeHint() const {
return geometry().size();
}
QSize MaskedInputField::minimumSizeHint() const {
return geometry().size();
}
void MaskedInputField::setDisplayFocused(bool focused) {
setFocused(focused);
finishAnimating();
}
void MaskedInputField::finishAnimating() {
_a_focused.stop();
_a_error.stop();
_a_placeholderShifted.stop();
_a_borderShown.stop();
_a_borderOpacity.stop();
update();
}
void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
_forcePlaceholderHidden = forcePlaceholderHidden;
startPlaceholderAnimation();
}
void MaskedInputField::startPlaceholderAnimation() {
auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty();
if (_placeholderShifted != placeholderShifted) {
_placeholderShifted = placeholderShifted;
_a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration);
}
}
QRect MaskedInputField::placeholderRect() const {
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
}
style::font MaskedInputField::phFont() {
return _st.style.font;
}
void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) {
p.setFont(_st.style.font);
p.setPen(_st.placeholderFg);
}
void MaskedInputField::keyPressEvent(QKeyEvent *e) {
QString wasText(_oldtext);
int32 wasCursor(_oldcursor);
if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
e->ignore();
} else if (e == QKeySequence::DeleteStartOfWord && hasSelectedText()) {
e->accept();
backspace();
} else {
QLineEdit::keyPressEvent(e);
}
auto newText = text();
auto newCursor = cursorPosition();
if (wasText == newText && wasCursor == newCursor) { // call correct manually
correctValue(wasText, wasCursor, newText, newCursor);
_oldtext = newText;
_oldcursor = newCursor;
if (wasText != _oldtext) changed();
startPlaceholderAnimation();
}
if (e->key() == Qt::Key_Escape) {
e->ignore();
cancelled();
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
submitted(e->modifiers());
#ifdef Q_OS_MAC
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
auto selected = selectedText();
if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
QGuiApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
}
#endif // Q_OS_MAC
}
}
void MaskedInputField::onTextEdited() {
QString wasText(_oldtext), newText(text());
int32 wasCursor(_oldcursor), newCursor(cursorPosition());
correctValue(wasText, wasCursor, newText, newCursor);
_oldtext = newText;
_oldcursor = newCursor;
if (wasText != _oldtext) changed();
startPlaceholderAnimation();
Integration::Instance().textActionsUpdated();
}
void MaskedInputField::onTextChange(const QString &text) {
_oldtext = QLineEdit::text();
setErrorShown(false);
Integration::Instance().textActionsUpdated();
}
void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) {
_oldcursor = position;
}
} // namespace Ui

View File

@@ -0,0 +1,174 @@
// 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/rp_widget.h"
#include "ui/effects/animations.h"
#include <QtWidgets/QLineEdit>
#include <QtCore/QTimer>
namespace style {
struct InputField;
} // namespace style
namespace Ui {
class PopupMenu;
class MaskedInputField : public RpWidgetBase<QLineEdit> {
Q_OBJECT
using Parent = RpWidgetBase<QLineEdit>;
public:
MaskedInputField(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> 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<QString> 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;
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();
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<QString> _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<PopupMenu> _contextMenu;
};
} // namespace Ui

View File

@@ -0,0 +1,53 @@
// 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
//
#include "ui/widgets/fields/number_input.h"
namespace Ui {
NumberInput::NumberInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &value,
int limit)
: MaskedInputField(parent, st, std::move(placeholder), value)
, _limit(limit) {
if (!value.toInt() || (limit > 0 && value.toInt() > limit)) {
setText(QString());
}
}
void NumberInput::changeLimit(int limit) {
_limit = limit;
}
void NumberInput::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
QString newText;
newText.reserve(now.size());
auto newPos = nowCursor;
for (auto i = 0, l = int(now.size()); i < l; ++i) {
if (now.at(i).isDigit()) {
newText.append(now.at(i));
} else if (i < nowCursor) {
--newPos;
}
}
if (!newText.toInt()) {
newText = QString();
newPos = 0;
} else if (_limit > 0 && newText.toInt() > _limit) {
newText = was;
newPos = wasCursor;
}
setCorrectedText(now, nowCursor, newText, newPos);
}
} // namespace Ui

View File

@@ -0,0 +1,40 @@
// 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/widgets/fields/masked_input_field.h"
namespace style {
struct InputField;
} // namespace style
namespace Ui {
class NumberInput final : public MaskedInputField {
public:
NumberInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &value,
int limit);
void changeLimit(int limit);
protected:
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
int _limit = 0;
};
} // namespace Ui

View File

@@ -0,0 +1,20 @@
// 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
//
#include "ui/widgets/fields/password_input.h"
namespace Ui {
PasswordInput::PasswordInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder,
const QString &val)
: MaskedInputField(parent, st, std::move(placeholder), val) {
QLineEdit::setEchoMode(QLineEdit::Password);
}
} // namespace Ui

View File

@@ -0,0 +1,27 @@
// 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/widgets/fields/masked_input_field.h"
namespace style {
struct InputField;
} // namespace style
namespace Ui {
class PasswordInput final : public MaskedInputField {
public:
PasswordInput(
QWidget *parent,
const style::InputField &st,
rpl::producer<QString> placeholder = nullptr,
const QString &val = QString());
};
} // namespace Ui

View File

@@ -0,0 +1,140 @@
// 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
//
#include "ui/widgets/fields/time_part_input.h"
#include "base/qt/qt_string_view.h"
#include "ui/ui_utility.h" // WheelDirection
#include <QtCore/QRegularExpression>
namespace Ui {
std::optional<int> TimePart::number() {
static const auto RegExp = QRegularExpression("^\\d+$");
const auto text = getLastText();
auto view = QStringView(text);
while (view.size() > 1 && view.at(0) == '0') {
view = base::StringViewMid(view, 1);
}
return RegExp.match(view).hasMatch()
? std::make_optional(view.toInt())
: std::nullopt;
}
void TimePart::setMaxValue(int value) {
_maxValue = value;
_maxDigits = 0;
while (value > 0) {
++_maxDigits;
value /= 10;
}
}
void TimePart::setWheelStep(int value) {
_wheelStep = value;
}
rpl::producer<> TimePart::erasePrevious() const {
return _erasePrevious.events();
}
rpl::producer<> TimePart::jumpToPrevious() const {
return _jumpToPrevious.events();
}
rpl::producer<QChar> TimePart::putNext() const {
return _putNext.events();
}
void TimePart::keyPressEvent(QKeyEvent *e) {
const auto position = cursorPosition();
const auto selection = hasSelectedText();
if (!selection && !position) {
if (e->key() == Qt::Key_Backspace) {
_erasePrevious.fire({});
return;
} else if (e->key() == Qt::Key_Left) {
_jumpToPrevious.fire({});
return;
}
} else if (!selection && position == getLastText().size()) {
if (e->key() == Qt::Key_Right) {
_putNext.fire(QChar(0));
return;
}
}
MaskedInputField::keyPressEvent(e);
}
void TimePart::wheelEvent(QWheelEvent *e) {
const auto direction = WheelDirection(e);
const auto now = number();
if (!now.has_value()) {
return;
}
auto time = *now + (direction * _wheelStep);
const auto max = _maxValue + 1;
if (time < 0) {
time += max;
} else if (time >= max) {
time -= max;
}
setText(QString::number(time));
Ui::MaskedInputField::changed();
}
void TimePart::correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) {
const auto oldCursor = nowCursor;
const auto oldLength = now.size();
auto newCursor = (oldCursor > 0) ? -1 : 0;
auto newText = QString();
auto accumulated = 0;
auto limit = 0;
for (; limit != oldLength; ++limit) {
if (now[limit].isDigit()) {
accumulated *= 10;
accumulated += (now[limit].unicode() - '0');
if (accumulated > _maxValue || limit == _maxDigits) {
break;
}
}
}
for (auto i = 0; i != limit;) {
if (now[i].isDigit()) {
newText += now[i];
}
if (++i == oldCursor) {
newCursor = newText.size();
}
}
if (newCursor < 0) {
newCursor = newText.size();
}
if (newText != now) {
now = newText;
setText(now);
startPlaceholderAnimation();
}
if (newCursor != nowCursor) {
nowCursor = newCursor;
setCursorPosition(nowCursor);
}
if (accumulated > _maxValue
|| (limit == _maxDigits && oldLength > _maxDigits)) {
if (oldCursor > limit) {
_putNext.fire(QChar('0' + (accumulated % 10)));
} else {
_putNext.fire(QChar(0));
}
}
}
} // namespace Ui

View File

@@ -0,0 +1,46 @@
// 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/widgets/fields/masked_input_field.h"
namespace Ui {
class TimePart : public MaskedInputField {
public:
using MaskedInputField::MaskedInputField;
void setMaxValue(int value);
void setWheelStep(int value);
[[nodiscard]] rpl::producer<> erasePrevious() const;
[[nodiscard]] rpl::producer<> jumpToPrevious() const;
[[nodiscard]] rpl::producer<QChar> putNext() const;
[[nodiscard]] std::optional<int> number();
protected:
void keyPressEvent(QKeyEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void correctValue(
const QString &was,
int wasCursor,
QString &now,
int &nowCursor) override;
private:
int _maxValue = 0;
int _maxDigits = 0;
int _wheelStep = 0;
rpl::event_stream<> _erasePrevious;
rpl::event_stream<> _jumpToPrevious;
rpl::event_stream<QChar> _putNext;
};
} // namespace Ui