Allow inserting links with custom text.

Fixes #4737.
This commit is contained in:
John Preston 2018-05-26 17:58:21 +03:00
parent cca46448fe
commit 07d8dafa5e
13 changed files with 603 additions and 97 deletions

View File

@ -1259,8 +1259,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_menu_formatting_bold" = "Bold";
"lng_menu_formatting_italic" = "Italic";
"lng_menu_formatting_monospace" = "Monospace";
//"lng_menu_formatting_link" = "Create link";
"lng_menu_formatting_link_create" = "Create link";
"lng_menu_formatting_link_edit" = "Edit link";
"lng_menu_formatting_clear" = "Plain text";
"lng_formatting_link_create_title" = "Create link";
"lng_formatting_link_edit_title" = "Create link";
"lng_formatting_link_text" = "Text";
"lng_formatting_link_url" = "URL";
"lng_formatting_link_create" = "Create";
"lng_full_name" = "{first_name} {last_name}";

View File

@ -782,3 +782,5 @@ proxyDropdownUpPosition: point(-2px, 20px);
proxyAboutPadding: margins(22px, 7px, 22px, 14px);
proxyAboutSponsorPadding: margins(22px, 7px, 22px, 0px);
markdownLinkFieldPadding: margins(22px, 0px, 22px, 10px);

View File

@ -25,8 +25,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
EditCaptionBox::EditCaptionBox(
QWidget*,
not_null<Window::Controller*> controller,
not_null<HistoryItem*> item)
: _msgId(item->fullId()) {
: _controller(controller)
, _msgId(item->fullId()) {
Expects(item->media() != nullptr);
Expects(item->media()->allowsEditCaption());
@ -146,6 +148,8 @@ EditCaptionBox::EditCaptionBox(
_field->setInstantReplaces(Ui::InstantReplaces::Default());
_field->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
_field->setMarkdownReplacesEnabled(rpl::single(true));
_field->setEditLinkCallback(
DefaultEditLinkCallback(_controller, _field));
}
void EditCaptionBox::prepareGifPreview(DocumentData *document) {

View File

@ -9,6 +9,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/abstract_box.h"
namespace Window {
class Controller;
} // namespace Window
namespace Data {
class Media;
} // namespace Data
@ -19,7 +23,10 @@ class InputField;
class EditCaptionBox : public BoxContent, public RPCSender {
public:
EditCaptionBox(QWidget*, not_null<HistoryItem*> item);
EditCaptionBox(
QWidget*,
not_null<Window::Controller*> controller,
not_null<HistoryItem*> item);
protected:
void prepare() override;
@ -41,6 +48,7 @@ private:
int errorTopSkip() const;
not_null<Window::Controller*> _controller;
FullMsgId _msgId;
bool _animated = false;
bool _photo = false;

View File

@ -1319,10 +1319,12 @@ void SendFilesBox::AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
SendFilesBox::SendFilesBox(
QWidget*,
not_null<Window::Controller*> controller,
Storage::PreparedList &&list,
const TextWithTags &caption,
CompressConfirm compressed)
: _list(std::move(list))
: _controller(controller)
, _list(std::move(list))
, _compressConfirmInitial(compressed)
, _compressConfirm(compressed)
, _caption(
@ -1579,6 +1581,8 @@ void SendFilesBox::setupCaption() {
_caption->setInstantReplaces(Ui::InstantReplaces::Default());
_caption->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
_caption->setMarkdownReplacesEnabled(rpl::single(true));
_caption->setEditLinkCallback(
DefaultEditLinkCallback(_controller, _caption));
}
void SendFilesBox::captionResized() {

View File

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "storage/localimageloader.h"
#include "storage/storage_media_prepare.h"
namespace Window {
class Controller;
} // namespace Window
namespace Ui {
template <typename Enum>
class Radioenum;
@ -32,6 +36,7 @@ class SendFilesBox : public BoxContent {
public:
SendFilesBox(
QWidget*,
not_null<Window::Controller*> controller,
Storage::PreparedList &&list,
const TextWithTags &caption,
CompressConfirm compressed);
@ -88,6 +93,8 @@ private:
bool canAddUrls(const QList<QUrl> &urls) const;
bool addFiles(not_null<const QMimeData*> data);
not_null<Window::Controller*> _controller;
QString _titleText;
int _titleHeight = 0;

View File

@ -399,11 +399,11 @@ QString SuggestionsController::getEmojiQuery() {
}
auto cursor = _field->textCursor();
auto position = _field->textCursor().position();
if (cursor.anchor() != position) {
if (cursor.hasSelection()) {
return QString();
}
auto position = cursor.position();
auto findTextPart = [this, &position] {
auto document = _field->document();
auto block = document->findBlock(position);

View File

@ -9,14 +9,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_widget.h"
#include "base/qthelp_regex.h"
#include "boxes/abstract_box.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_controller.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "auth_session.h"
#include "styles/style_boxes.h"
#include "styles/style_history.h"
namespace {
using EditLinkAction = Ui::InputField::EditLinkAction;
using EditLinkSelection = Ui::InputField::EditLinkSelection;
constexpr auto kParseLinksTimeout = TimeMs(1000);
const auto kMentionTagStart = qstr("mention://user.");
bool IsMentionLink(const QString &link) {
return link.startsWith(kMentionTagStart);
}
// For mention tags save and validate userId, ignore tags for different userId.
class FieldTagMimeProcessor : public Ui::InputField::TagMimeProcessor {
@ -26,7 +38,7 @@ public:
}
QString tagFromMimeTag(const QString &mimeTag) override {
if (mimeTag.startsWith(qstr("mention://"))) {
if (IsMentionLink(mimeTag)) {
auto match = QRegularExpression(":(\\d+)$").match(mimeTag);
if (!match.hasMatch()
|| match.capturedRef(1).toInt() != Auth().userId()) {
@ -39,15 +51,167 @@ public:
};
class EditLinkBox : public BoxContent {
public:
EditLinkBox(
QWidget*,
const QString &text,
const QString &link,
base::lambda<void(QString, QString)> callback);
void setInnerFocus() override;
protected:
void prepare() override;
private:
QString _startText;
QString _startLink;
base::lambda<void(QString, QString)> _callback;
base::lambda<void()> _setInnerFocus;
};
QRegularExpression RegExpProtocol() {
static const auto result = QRegularExpression("^([a-zA-Z]+)://");
return result;
}
bool IsGoodProtocol(const QString &protocol) {
const auto equals = [&](QLatin1String string) {
return protocol.compare(string, Qt::CaseInsensitive) == 0;
};
return equals(qstr("http"))
|| equals(qstr("https"))
|| equals(qstr("tg"));
}
QString NormalizeUrl(const QString &value) {
const auto trimmed = value.trimmed();
if (trimmed.isEmpty()) {
return QString();
}
const auto match = TextUtilities::RegExpDomainExplicit().match(trimmed);
if (!match.hasMatch()) {
const auto domain = TextUtilities::RegExpDomain().match(trimmed);
if (!domain.hasMatch() || domain.capturedStart() != 0) {
return QString();
}
return qstr("http://") + trimmed;
} else if (match.capturedStart() != 0) {
return QString();
}
const auto protocolMatch = RegExpProtocol().match(trimmed);
Assert(protocolMatch.hasMatch());
return IsGoodProtocol(protocolMatch.captured(1)) ? trimmed : QString();
}
//bool ValidateUrl(const QString &value) {
// const auto match = TextUtilities::RegExpDomain().match(value);
// if (!match.hasMatch() || match.capturedStart() != 0) {
// return false;
// }
// const auto protocolMatch = RegExpProtocol().match(value);
// return protocolMatch.hasMatch()
// && IsGoodProtocol(protocolMatch.captured(1));
//}
EditLinkBox::EditLinkBox(
QWidget*,
const QString &text,
const QString &link,
base::lambda<void(QString, QString)> callback)
: _startText(text)
, _startLink(link)
, _callback(std::move(callback)) {
Expects(_callback != nullptr);
}
void EditLinkBox::setInnerFocus() {
Expects(_setInnerFocus != nullptr);
_setInnerFocus();
}
void EditLinkBox::prepare() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
const auto text = content->add(
object_ptr<Ui::InputField>(
content,
st::defaultInputField,
langFactory(lng_formatting_link_text),
_startText),
st::markdownLinkFieldPadding);
text->setInstantReplaces(Ui::InstantReplaces::Default());
text->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
const auto url = content->add(
object_ptr<Ui::InputField>(
content,
st::defaultInputField,
langFactory(lng_formatting_link_url),
_startLink.trimmed()),
st::markdownLinkFieldPadding);
const auto submit = [=] {
const auto linkText = text->getLastText();
const auto linkUrl = NormalizeUrl(url->getLastText());
if (linkText.isEmpty()) {
text->showError();
return;
} else if (linkUrl.isEmpty()) {
url->showError();
return;
}
const auto weak = make_weak(this);
_callback(linkText, linkUrl);
if (weak) {
closeBox();
}
};
connect(text, &Ui::InputField::submitted, [=] {
url->setFocusFast();
});
connect(url, &Ui::InputField::submitted, [=] {
if (text->getLastText().isEmpty()) {
text->setFocusFast();
} else {
submit();
}
});
setTitle(langFactory(lng_formatting_link_create_title));
addButton(langFactory(lng_formatting_link_create), submit);
addButton(langFactory(lng_cancel), [=] { closeBox(); });
content->resizeToWidth(st::boxWidth);
content->moveToLeft(0, 0);
setDimensions(st::boxWidth, content->height());
_setInnerFocus = [=] {
(_startText.isEmpty() ? text : url)->setFocusFast();
};
}
} // namespace
QString ConvertTagToMimeTag(const QString &tagId) {
if (tagId.startsWith(qstr("mention://"))) {
if (IsMentionLink(tagId)) {
return tagId + ':' + QString::number(Auth().userId());
}
return tagId;
}
QString PrepareMentionTag(not_null<UserData*> user) {
return kMentionTagStart
+ QString::number(user->bareId())
+ '.'
+ QString::number(user->accessHash());
}
EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
EntitiesInText result;
if (tags.isEmpty()) {
@ -55,7 +219,6 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
}
result.reserve(tags.size());
auto mentionStart = qstr("mention://user.");
for (const auto &tag : tags) {
const auto push = [&](
EntityInTextType type,
@ -63,8 +226,8 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
result.push_back(
EntityInText(type, tag.offset, tag.length, data));
};
if (tag.id.startsWith(mentionStart)) {
if (auto match = qthelp::regex_match("^(\\d+\\.\\d+)(/|$)", tag.id.midRef(mentionStart.size()))) {
if (IsMentionLink(tag.id)) {
if (auto match = qthelp::regex_match("^(\\d+\\.\\d+)(/|$)", tag.id.midRef(kMentionTagStart.size()))) {
push(EntityInTextMentionName, match->captured(1));
}
} else if (tag.id == Ui::InputField::kTagBold) {
@ -75,6 +238,8 @@ EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags) {
push(EntityInTextCode);
} else if (tag.id == Ui::InputField::kTagPre) {
push(EntityInTextPre);
} else /*if (ValidateUrl(tag.id)) */{ // We validate when we insert.
push(EntityInTextCustomUrl, tag.id);
}
}
return result;
@ -95,7 +260,14 @@ TextWithTags::Tags ConvertEntitiesToTextTags(const EntitiesInText &entities) {
case EntityInTextMentionName: {
auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data());
if (match.hasMatch()) {
push(qstr("mention://user.") + entity.data());
push(kMentionTagStart + entity.data());
}
} break;
case EntityInTextCustomUrl: {
const auto url = entity.data();
if (Ui::InputField::IsValidMarkdownLink(url)
&& !IsMentionLink(url)) {
push(url);
}
} break;
case EntityInTextBold: push(Ui::InputField::kTagBold); break;
@ -135,7 +307,38 @@ void SetClipboardWithEntities(
}
}
void InitMessageField(not_null<Ui::InputField*> field) {
base::lambda<bool(
Ui::InputField::EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action)> DefaultEditLinkCallback(
not_null<Window::Controller*> controller,
not_null<Ui::InputField*> field) {
const auto weak = make_weak(field);
return [=](
EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action) {
if (action == EditLinkAction::Check) {
return Ui::InputField::IsValidMarkdownLink(link)
&& !IsMentionLink(link);
}
Ui::show(Box<EditLinkBox>(text, link, [=](
const QString &text,
const QString &link) {
if (const auto strong = weak.data()) {
strong->commitMarkdownLinkEdit(selection, text, link);
}
}), LayerOption::KeepOther);
return true;
};
}
void InitMessageField(
not_null<Window::Controller*> controller,
not_null<Ui::InputField*> field) {
field->setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);
field->setMaxHeight(st::historyComposeFieldMaxHeight);
@ -148,6 +351,8 @@ void InitMessageField(not_null<Ui::InputField*> field) {
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
field->setMarkdownReplacesEnabled(rpl::single(true));
field->setEditLinkCallback(
DefaultEditLinkCallback(controller, field));
}
bool HasSendText(not_null<const Ui::InputField*> field) {
@ -237,11 +442,11 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery(
auto result = AutocompleteQuery();
const auto cursor = field->textCursor();
const auto position = cursor.position();
if (cursor.anchor() != position) {
if (cursor.hasSelection()) {
return result;
}
const auto position = cursor.position();
const auto document = field->document();
const auto block = document->findBlock(position);
for (auto item = block.begin(); !item.atEnd(); ++item) {
@ -363,13 +568,43 @@ const rpl::variable<QStringList> &MessageLinksParser::list() const {
}
void MessageLinksParser::parse() {
const auto &text = _field->getTextWithTags().text;
const auto &textWithTags = _field->getTextWithTags();
const auto &text = textWithTags.text;
const auto &tags = textWithTags.tags;
if (text.isEmpty()) {
_list = QStringList();
return;
}
auto ranges = QVector<LinkRange>();
const auto tagsBegin = tags.begin();
const auto tagsEnd = tags.end();
auto tag = tagsBegin;
const auto processTag = [&] {
Expects(tag != tagsEnd);
if (Ui::InputField::IsValidMarkdownLink(tag->id)
&& !IsMentionLink(tag->id)) {
ranges.push_back({ tag->offset, tag->length, tag->id });
}
++tag;
};
const auto processTagsBefore = [&](int offset) {
while (tag != tagsEnd && tag->offset + tag->length <= offset) {
processTag();
}
};
const auto hasTagsIntersection = [&](int till) {
if (tag == tagsEnd || tag->offset >= till) {
return false;
}
while (tag != tagsEnd && tag->offset < till) {
processTag();
}
return true;
};
const auto len = text.size();
const QChar *start = text.unicode(), *end = start + text.size();
for (auto offset = 0, matchOffset = offset; offset < len;) {
@ -429,7 +664,15 @@ void MessageLinksParser::parse() {
continue;
}
}
ranges.push_back({ domainOffset, static_cast<int>(p - start - domainOffset) });
const auto range = LinkRange {
domainOffset,
static_cast<int>(p - start - domainOffset),
QString()
};
processTagsBefore(domainOffset);
if (!hasTagsIntersection(range.start + range.length)) {
ranges.push_back(range);
}
offset = matchOffset = p - start;
}
@ -441,13 +684,17 @@ void MessageLinksParser::apply(
const QVector<LinkRange> &ranges) {
const auto count = int(ranges.size());
const auto current = _list.current();
const auto computeLink = [&](const LinkRange &range) {
return range.custom.isEmpty()
? text.midRef(range.start, range.length)
: range.custom.midRef(0);
};
const auto changed = [&] {
if (current.size() != count) {
return true;
}
for (auto i = 0; i != count; ++i) {
const auto &range = ranges[i];
if (text.midRef(range.start, range.length) != current[i]) {
if (computeLink(ranges[i]) != current[i]) {
return true;
}
}
@ -459,7 +706,7 @@ void MessageLinksParser::apply(
auto parsed = QStringList();
parsed.reserve(count);
for (const auto &range : ranges) {
parsed.push_back(text.mid(range.start, range.length));
parsed.push_back(computeLink(range).toString());
}
_list = std::move(parsed);
}

View File

@ -16,6 +16,7 @@ class Controller;
} // namespace Window
QString ConvertTagToMimeTag(const QString &tagId);
QString PrepareMentionTag(not_null<UserData*> user);
EntitiesInText ConvertTextTagsToEntities(const TextWithTags::Tags &tags);
TextWithTags::Tags ConvertEntitiesToTextTags(
@ -26,7 +27,16 @@ void SetClipboardWithEntities(
const TextWithEntities &forClipboard,
QClipboard::Mode mode = QClipboard::Clipboard);
void InitMessageField(not_null<Ui::InputField*> field);
base::lambda<bool(
Ui::InputField::EditLinkSelection selection,
QString text,
QString link,
Ui::InputField::EditLinkAction action)> DefaultEditLinkCallback(
not_null<Window::Controller*> controller,
not_null<Ui::InputField*> field);
void InitMessageField(
not_null<Window::Controller*> controller,
not_null<Ui::InputField*> field);
bool HasSendText(not_null<const Ui::InputField*> field);
struct InlineBotQuery {
@ -71,9 +81,12 @@ private:
struct LinkRange {
int start;
int length;
QString custom;
};
friend inline bool operator==(const LinkRange &a, const LinkRange &b) {
return (a.start == b.start) && (a.length == b.length);
return (a.start == b.start)
&& (a.length == b.length)
&& (a.custom == b.custom);
}
friend inline bool operator!=(const LinkRange &a, const LinkRange &b) {
return !(a == b);

View File

@ -513,7 +513,7 @@ HistoryWidget::HistoryWidget(
_historyDown->installEventFilter(this);
_unreadMentions->installEventFilter(this);
InitMessageField(_field);
InitMessageField(controller, _field);
_fieldAutocomplete->hide();
connect(_fieldAutocomplete, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onMentionInsert(UserData*)));
connect(_fieldAutocomplete, SIGNAL(hashtagChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod)));
@ -997,10 +997,7 @@ void HistoryWidget::onMentionInsert(UserData *user) {
if (replacement.isEmpty()) {
replacement = App::peerName(user);
}
entityTag = qsl("mention://user.")
+ QString::number(user->bareId())
+ '.'
+ QString::number(user->accessHash());
entityTag = PrepareMentionTag(user);
} else {
replacement = '@' + user->username;
}
@ -1172,8 +1169,8 @@ void HistoryWidget::onDraftSaveDelayed() {
if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) {
return;
}
if (!_field->textCursor().anchor()
&& !_field->textCursor().position()
if (!_field->textCursor().position()
&& !_field->textCursor().anchor()
&& !_field->scrollTop().current()) {
if (!Local::hasDraftCursors(_peer->id)) {
return;
@ -4168,6 +4165,7 @@ bool HistoryWidget::confirmSendingFiles(
const auto anchor = cursor.anchor();
const auto text = _field->getTextWithTags();
auto box = Box<SendFilesBox>(
controller(),
std::move(list),
text,
boxCompressConfirm);
@ -5846,7 +5844,7 @@ void HistoryWidget::editMessage(FullMsgId itemId) {
void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
if (const auto media = item->media()) {
if (media->allowsEditCaption()) {
Ui::show(Box<EditCaptionBox>(item));
Ui::show(Box<EditCaptionBox>(controller(), item));
return;
}
}

View File

@ -1534,7 +1534,8 @@ MTPVector<MTPMessageEntity> EntitiesToMTP(const EntitiesInText &entities, Conver
&& entity.type() != EntityInTextItalic
&& entity.type() != EntityInTextCode
&& entity.type() != EntityInTextPre
&& entity.type() != EntityInTextMentionName) {
&& entity.type() != EntityInTextMentionName
&& entity.type() != EntityInTextCustomUrl) {
continue;
}

View File

@ -41,6 +41,7 @@ const auto kNewlineChars = QString("\r\n")
+ QChar(QChar::LineSeparator);
const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
const auto kEditLinkSequence = QKeySequence("ctrl+k");
bool IsNewline(QChar ch) {
return (kNewlineChars.indexOf(ch) >= 0);
@ -84,36 +85,21 @@ public:
}
if (!_currentTagId.isEmpty()) {
const auto randomPartPosition = _currentTagId.lastIndexOf('/');
const auto tagId = _currentTagId.midRef(
0,
(randomPartPosition > 0
? randomPartPosition
: _currentTagId.size()));
bool tagChanged = true;
if (_currentTag < _tags.size()) {
auto &alreadyTag = _tags[_currentTag];
if (alreadyTag.offset == _currentStart &&
alreadyTag.length == currentPosition - _currentStart &&
alreadyTag.id == tagId) {
tagChanged = false;
}
}
if (tagChanged) {
_changed = true;
const auto tag = TextWithTags::Tag {
_currentStart,
currentPosition - _currentStart,
tagId.toString()
};
if (_currentTag < _tags.size()) {
_tags[_currentTag] = tag;
} else {
const auto tag = TextWithTags::Tag {
_currentStart,
currentPosition - _currentStart,
_currentTagId
};
if (tag.length > 0) {
if (_currentTag >= _tags.size()) {
_changed = true;
_tags.push_back(tag);
} else if (_tags[_currentTag] != tag) {
_changed = true;
_tags[_currentTag] = tag;
}
++_currentTag;
}
++_currentTag;
}
_currentTagId = randomTagId;
_currentStart = currentPosition;
@ -523,12 +509,15 @@ style::font AdjustFont(
: font;
}
bool IsValidMarkdownLink(const QString &link) {
return (link.indexOf('.') >= 0) || (link.indexOf(':') >= 0);
}
QTextCharFormat PrepareTagFormat(
const style::InputField &st,
QString tag) {
auto result = QTextCharFormat();
if (tag.indexOf(':') >= 0) {
tag += '/' + QString::number(rand_value<uint32>());
if (IsValidMarkdownLink(tag)) {
result.setForeground(st::defaultTextPalette.linkFg);
result.setFont(st.font);
} else if (tag == kTagBold) {
@ -1614,7 +1603,6 @@ QString InputField::getTextPart(
end = possibleLength;
}
bool tillFragmentEnd = full;
for (auto block = from; block != till;) {
for (auto item = block.begin(); !item.atEnd(); ++item) {
const auto fragment = item.fragment();
@ -1628,7 +1616,6 @@ QString InputField::getTextPart(
: (fragmentPosition + fragment.length());
const auto format = fragment.charFormat();
if (!full) {
tillFragmentEnd = (fragmentEnd <= end);
if (fragmentPosition == end) {
tagAccumulator.feed(
format.property(kTagProperty).toString(),
@ -1662,7 +1649,7 @@ QString InputField::getTextPart(
return result;
}();
if (full || fragmentPosition >= start) {
if (full || !text.isEmpty()) {
lastTag = format.property(kTagProperty).toString();
tagAccumulator.feed(lastTag, result.size());
possibleTagAccumulator.feed(text, lastTag);
@ -1700,9 +1687,7 @@ QString InputField::getTextPart(
}
}
if (tillFragmentEnd) {
tagAccumulator.feed(QString(), result.size());
}
tagAccumulator.feed(QString(), result.size());
tagAccumulator.finish();
possibleTagAccumulator.finish();
@ -2301,6 +2286,13 @@ void InputField::keyPressEventInner(QKeyEvent *e) {
}
}
TextWithTags InputField::getTextWithTagsSelected() const {
const auto cursor = textCursor();
const auto start = cursor.selectionStart();
const auto end = cursor.selectionEnd();
return (end > start) ? getTextWithTagsPart(start, end) : TextWithTags();
}
bool InputField::handleMarkdownKey(QKeyEvent *e) {
if (!_markdownEnabled) {
return false;
@ -2319,12 +2311,152 @@ bool InputField::handleMarkdownKey(QKeyEvent *e) {
toggleSelectionMarkdown(kTagCode);
} else if (matches(kClearFormatSequence)) {
clearSelectionMarkdown();
} else if (matches(kEditLinkSequence) && _editLinkCallback) {
const auto cursor = textCursor();
editMarkdownLink({
cursor.selectionStart(),
cursor.selectionEnd()
});
} else {
return false;
}
return true;
}
auto InputField::selectionEditLinkData(EditLinkSelection selection) const
-> EditLinkData {
Expects(_editLinkCallback != nullptr);
const auto position = (selection.from == selection.till
&& selection.from > 0)
? (selection.from - 1)
: selection.from;
const auto link = [&] {
return (position != selection.till)
? GetFullSimpleTextTag(
getTextWithTagsPart(position, selection.till))
: QString();
}();
const auto simple = EditLinkData {
selection.from,
selection.till,
QString()
};
if (!_editLinkCallback(selection, {}, link, EditLinkAction::Check)) {
return simple;
}
Assert(!link.isEmpty());
struct State {
QTextBlock block;
QTextBlock::iterator i;
};
const auto document = _inner->document();
const auto skipInvalid = [&](State &state) {
if (state.block == document->end()) {
return false;
}
while (state.i.atEnd()) {
state.block = state.block.next();
if (state.block == document->end()) {
return false;
}
state.i = state.block.begin();
}
return true;
};
const auto moveToNext = [&](State &state) {
Expects(state.block != document->end());
Expects(!state.i.atEnd());
++state.i;
};
const auto moveToPrevious = [&](State &state) {
Expects(state.block != document->end());
Expects(!state.i.atEnd());
while (state.i == state.block.begin()) {
if (state.block == document->begin()) {
state.block = document->end();
return false;
}
state.block = state.block.previous();
state.i = state.block.end();
}
--state.i;
return true;
};
const auto stateTag = [&](const State &state) {
const auto format = state.i.fragment().charFormat();
return format.property(kTagProperty).toString();
};
const auto stateStart = [&](const State &state) {
return state.i.fragment().position();
};
const auto stateEnd = [&](const State &state) {
const auto fragment = state.i.fragment();
return fragment.position() + fragment.length();
};
auto state = State{ document->findBlock(position) };
if (state.block != document->end()) {
state.i = state.block.begin();
}
for (; skipInvalid(state); moveToNext(state)) {
const auto fragmentStart = stateStart(state);
const auto fragmentEnd = stateEnd(state);
if (fragmentEnd <= position) {
continue;
} else if (fragmentStart >= selection.till) {
break;
}
if (stateTag(state) == link) {
auto start = fragmentStart;
auto finish = fragmentEnd;
auto copy = state;
while (moveToPrevious(copy) && (stateTag(copy) == link)) {
start = stateStart(copy);
}
while (skipInvalid(state) && (stateTag(state) == link)) {
finish = stateEnd(state);
moveToNext(state);
}
return { start, finish, link };
}
}
return simple;
}
auto InputField::editLinkSelection(QContextMenuEvent *e) const
-> EditLinkSelection {
const auto cursor = textCursor();
if (!cursor.hasSelection() && e->reason() == QContextMenuEvent::Mouse) {
const auto clickCursor = _inner->cursorForPosition(
_inner->viewport()->mapFromGlobal(e->globalPos()));
if (!clickCursor.isNull() && !clickCursor.hasSelection()) {
return {
clickCursor.position(),
clickCursor.position()
};
}
}
return {
cursor.selectionStart(),
cursor.selectionEnd()
};
}
void InputField::editMarkdownLink(EditLinkSelection selection) {
if (!_editLinkCallback) {
return;
}
const auto data = selectionEditLinkData(selection);
_editLinkCallback(
selection,
getTextWithTagsPart(data.from, data.till).text,
data.link,
EditLinkAction::Edit);
}
void InputField::inputMethodEventInner(QInputMethodEvent *e) {
const auto preedit = e->preeditString();
if (_lastPreEditText != preedit) {
@ -2425,7 +2557,7 @@ void InputField::applyInstantReplace(
const auto length = int(what.size());
const auto cursor = textCursor();
const auto position = cursor.position();
if (cursor.anchor() != position) {
if (cursor.hasSelection()) {
return;
} else if (position < length) {
return;
@ -2546,19 +2678,19 @@ bool InputField::commitMarkdownReplacement(
+ outer.mid(innerLeft, innerLength).toString()
+ (newlineright ? "\n" : "");
// Trim inserted tag, so that all spaces and newlines are left outside.
// Trim inserted tag, so that all newlines are left outside.
_insertedTags.clear();
auto tagFrom = newlineleft ? 1 : 0;
auto tagTill = insert.size() - (newlineright ? 1 : 0);
for (; tagFrom != tagTill; ++tagFrom) {
const auto ch = insert.at(tagFrom);
if (!IsNewline(ch) && !chIsSpace(ch)) {
if (!IsNewline(ch)) {
break;
}
}
for (; tagTill != tagFrom; --tagTill) {
const auto ch = insert.at(tagTill - 1);
if (!IsNewline(ch) && !chIsSpace(ch)) {
if (!IsNewline(ch)) {
break;
}
}
@ -2588,18 +2720,49 @@ bool InputField::commitMarkdownReplacement(
return true;
}
bool InputField::IsValidMarkdownLink(const QString &link) {
return ::Ui::IsValidMarkdownLink(link);
}
void InputField::commitMarkdownLinkEdit(
EditLinkSelection selection,
const QString &text,
const QString &link) {
if (text.isEmpty()
|| !IsValidMarkdownLink(link)
|| !_editLinkCallback) {
return;
}
_insertedTags.clear();
_insertedTags.push_back({ 0, text.size(), link });
auto cursor = textCursor();
const auto editData = selectionEditLinkData(selection);
cursor.setPosition(editData.from);
cursor.setPosition(editData.till, QTextCursor::KeepAnchor);
auto format = _defaultCharFormat;
_insertedTagsAreFromMime = false;
cursor.insertText(
(editData.from == editData.till) ? (text + QChar(' ')) : text,
_defaultCharFormat);
_insertedTags.clear();
_reverseMarkdownReplacement = false;
cursor.setCharFormat(_defaultCharFormat);
_inner->setTextCursor(cursor);
}
void InputField::toggleSelectionMarkdown(const QString &tag) {
_reverseMarkdownReplacement = false;
const auto cursor = textCursor();
const auto anchor = cursor.anchor();
const auto position = cursor.position();
const auto from = std::min(anchor, position);
const auto till = std::max(anchor, position);
const auto from = cursor.selectionStart();
const auto till = cursor.selectionEnd();
if (from == till) {
return;
}
if (tag.isEmpty()
|| GetFullSimpleTextTag(getTextWithTagsPart(from, till)) == tag) {
|| GetFullSimpleTextTag(getTextWithTagsSelected()) == tag) {
RemoveDocumentTags(_st, document(), from, till);
return;
}
@ -2626,7 +2789,7 @@ void InputField::toggleSelectionMarkdown(const QString &tag) {
}();
commitMarkdownReplacement(from, till, commitTag);
auto restorePosition = textCursor();
restorePosition.setPosition(anchor);
restorePosition.setPosition((position == till) ? from : till);
restorePosition.setPosition(position, QTextCursor::KeepAnchor);
setTextCursor(restorePosition);
}
@ -2638,7 +2801,7 @@ void InputField::clearSelectionMarkdown() {
bool InputField::revertFormatReplace() {
const auto cursor = textCursor();
const auto position = cursor.position();
if (position <= 0 || cursor.anchor() != position) {
if (position <= 0 || cursor.hasSelection()) {
return false;
}
const auto inside = position - 1;
@ -2726,13 +2889,15 @@ bool InputField::revertFormatReplace() {
void InputField::contextMenuEventInner(QContextMenuEvent *e) {
if (const auto menu = _inner->createStandardContextMenu()) {
addMarkdownActions(menu);
addMarkdownActions(menu, e);
_contextMenu = base::make_unique_q<Ui::PopupMenu>(nullptr, menu);
_contextMenu->popup(e->globalPos());
}
}
void InputField::addMarkdownActions(not_null<QMenu*> menu) {
void InputField::addMarkdownActions(
not_null<QMenu*> menu,
QContextMenuEvent *e) {
if (!_markdownEnabled) {
return;
}
@ -2742,17 +2907,16 @@ void InputField::addMarkdownActions(not_null<QMenu*> menu) {
const auto submenu = new QMenu(menu);
formatting->setMenu(submenu);
const auto cursor = textCursor();
const auto from = std::min(cursor.anchor(), cursor.position());
const auto till = std::max(cursor.anchor(), cursor.position());
const auto textWithTags = getTextWithTagsPart(from, till);
const auto textWithTags = getTextWithTagsSelected();
const auto &text = textWithTags.text;
const auto &tags = textWithTags.tags;
formatting->setDisabled(text.isEmpty());
if (text.isEmpty()) {
const auto hasText = !text.isEmpty();
const auto hasTags = !tags.isEmpty();
const auto disabled = (!_editLinkCallback && !hasText);
formatting->setDisabled(disabled);
if (disabled) {
return;
}
const auto hasTags = !textWithTags.tags.isEmpty();
const auto fullTag = GetFullSimpleTextTag(textWithTags);
const auto add = [&](
LangKey key,
@ -2773,27 +2937,35 @@ void InputField::addMarkdownActions(not_null<QMenu*> menu) {
const QString &tag) {
const auto disabled = (fullTag == tag)
|| (fullTag == kTagPre && tag == kTagCode);
add(key, sequence, (fullTag == tag), [=] {
add(key, sequence, (!hasText || fullTag == tag), [=] {
toggleSelectionMarkdown(tag);
});
};
//const auto addlink = [&] {
// add(lng_menu_formatting_link, QKeySequence("ctrl+k"), false, [=] {
// createMarkdownLink();
// });
//};
const auto addlink = [&] {
const auto selection = editLinkSelection(e);
const auto data = selectionEditLinkData(selection);
const auto key = data.link.isEmpty()
? lng_menu_formatting_link_create
: lng_menu_formatting_link_edit;
add(key, kEditLinkSequence, false, [=] {
editMarkdownLink(selection);
});
};
const auto addclear = [&] {
add(lng_menu_formatting_clear, kClearFormatSequence, !hasTags, [=] {
const auto disabled = !hasText || !hasTags;
add(lng_menu_formatting_clear, kClearFormatSequence, disabled, [=] {
clearSelectionMarkdown();
});
};
addtag(lng_menu_formatting_bold, QKeySequence::Bold, kTagBold);
addtag(lng_menu_formatting_italic, QKeySequence::Italic, kTagItalic);
addtag(lng_menu_formatting_monospace, kMonospaceSequence, kTagCode);
//submenu->addSeparator();
//addlink();
if (_editLinkCallback) {
submenu->addSeparator();
addlink();
}
submenu->addSeparator();
addclear();
@ -2853,7 +3025,7 @@ void InputField::insertFromMimeDataInner(const QMimeData *source) {
_insertedTags.clear();
}
auto cursor = textCursor();
_realInsertPosition = qMin(cursor.position(), cursor.anchor());
_realInsertPosition = cursor.selectionStart();
_realCharsAdded = text.size();
_inner->QTextEdit::insertFromMimeData(source);
if (!_inDrop) {
@ -2899,6 +3071,15 @@ void InputField::setPlaceholder(
refreshPlaceholder();
}
void InputField::setEditLinkCallback(
base::lambda<bool(
EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action)> callback) {
_editLinkCallback = std::move(callback);
}
void InputField::showError() {
setErrorShown(true);
if (!hasFocus()) {

View File

@ -186,6 +186,21 @@ public:
};
void setTagMimeProcessor(std::unique_ptr<TagMimeProcessor> &&processor);
struct EditLinkSelection {
int from = 0;
int till = 0;
};
enum class EditLinkAction {
Check,
Edit,
};
void setEditLinkCallback(
base::lambda<bool(
EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action)> callback);
void setAdditionalMargin(int margin);
void setInstantReplaces(const InstantReplaces &replaces);
@ -201,8 +216,13 @@ public:
int till,
const QString &tag,
const QString &edge = QString());
void commitMarkdownLinkEdit(
EditLinkSelection selection,
const QString &text,
const QString &link);
void toggleSelectionMarkdown(const QString &tag);
void clearSelectionMarkdown();
static bool IsValidMarkdownLink(const QString &link);
const QString &getLastText() const {
return _lastTextWithTags.text;
@ -322,6 +342,7 @@ private:
QMimeData *createMimeDataFromSelectionInner() const;
bool canInsertFromMimeDataInner(const QMimeData *source) const;
void insertFromMimeDataInner(const QMimeData *source);
TextWithTags getTextWithTagsSelected() const;
// "start" and "end" are in coordinates of text where emoji are replaced
// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
@ -345,7 +366,7 @@ private:
bool processMarkdownReplaces(const QString &appended);
bool processMarkdownReplace(const QString &tag);
void addMarkdownActions(not_null<QMenu*> menu);
void addMarkdownActions(not_null<QMenu*> menu, QContextMenuEvent *e);
void addMarkdownMenuAction(
not_null<QMenu*> menu,
not_null<QAction*> action);
@ -357,6 +378,15 @@ private:
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);
bool revertFormatReplace();
const style::InputField &_st;
@ -373,6 +403,11 @@ private:
TextWithTags _lastTextWithTags;
std::vector<PossibleTag> _textAreaPossibleTags;
QString _lastPreEditText;
base::lambda<bool(
EditLinkSelection selection,
QString text,
QString link,
EditLinkAction action)> _editLinkCallback;
// Tags list which we should apply while setText() call or insert from mime data.
TagList _insertedTags;