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
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:
304
Telegram/lib_ui/ui/widgets/fields/custom_field_object.cpp
Normal file
304
Telegram/lib_ui/ui/widgets/fields/custom_field_object.cpp
Normal 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 "e = _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
|
||||
80
Telegram/lib_ui/ui/widgets/fields/custom_field_object.h
Normal file
80
Telegram/lib_ui/ui/widgets/fields/custom_field_object.h
Normal 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
|
||||
5469
Telegram/lib_ui/ui/widgets/fields/input_field.cpp
Normal file
5469
Telegram/lib_ui/ui/widgets/fields/input_field.cpp
Normal file
File diff suppressed because it is too large
Load Diff
684
Telegram/lib_ui/ui/widgets/fields/input_field.h
Normal file
684
Telegram/lib_ui/ui/widgets/fields/input_field.h
Normal 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
|
||||
555
Telegram/lib_ui/ui/widgets/fields/masked_input_field.cpp
Normal file
555
Telegram/lib_ui/ui/widgets/fields/masked_input_field.cpp
Normal 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
|
||||
174
Telegram/lib_ui/ui/widgets/fields/masked_input_field.h
Normal file
174
Telegram/lib_ui/ui/widgets/fields/masked_input_field.h
Normal 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
|
||||
53
Telegram/lib_ui/ui/widgets/fields/number_input.cpp
Normal file
53
Telegram/lib_ui/ui/widgets/fields/number_input.cpp
Normal 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
|
||||
40
Telegram/lib_ui/ui/widgets/fields/number_input.h
Normal file
40
Telegram/lib_ui/ui/widgets/fields/number_input.h
Normal 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
|
||||
20
Telegram/lib_ui/ui/widgets/fields/password_input.cpp
Normal file
20
Telegram/lib_ui/ui/widgets/fields/password_input.cpp
Normal 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
|
||||
27
Telegram/lib_ui/ui/widgets/fields/password_input.h
Normal file
27
Telegram/lib_ui/ui/widgets/fields/password_input.h
Normal 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
|
||||
140
Telegram/lib_ui/ui/widgets/fields/time_part_input.cpp
Normal file
140
Telegram/lib_ui/ui/widgets/fields/time_part_input.cpp
Normal 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
|
||||
46
Telegram/lib_ui/ui/widgets/fields/time_part_input.h
Normal file
46
Telegram/lib_ui/ui/widgets/fields/time_part_input.h
Normal 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
|
||||
Reference in New Issue
Block a user