2
0
mirror of https://github.com/kotatogram/kotatogram-desktop synced 2025-09-02 23:55:12 +00:00

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

@@ -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()) {