mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 11:52:58 +00:00
Edit text-span selections with the plain AI Editor
This commit is contained in:
@@ -1227,7 +1227,7 @@ ComposeAiContent::ComposeAiContent(
|
||||
, _authorLabel(Ui::CreateChild<Ui::FlatLabel>(
|
||||
this,
|
||||
st::aiComposeAuthorLabel)) {
|
||||
if (_richSource && _tones.empty()) {
|
||||
if (_tones.empty()) {
|
||||
_session->data().aiComposeTones().refresh();
|
||||
}
|
||||
_preview->setResizeCallback([=] { refreshLayout(); });
|
||||
|
||||
@@ -1583,6 +1583,21 @@ void WindowHost::Impl::setupWindow(ShowWindowDescriptor &&descriptor) {
|
||||
}
|
||||
const auto editor = _editor;
|
||||
if (editor && editor->hasActiveSelection()) {
|
||||
auto span = editor->textSpanForCurrentSelection();
|
||||
if (!span.text.isEmpty()) {
|
||||
HistoryView::Controls::ShowComposeAiBox(_show, {
|
||||
.session = session,
|
||||
.text = std::move(span),
|
||||
.apply = [editor](TextWithEntities result) {
|
||||
if (!editor || result.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
editor->replaceCurrentSelectionWithText(
|
||||
std::move(result));
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
auto source = editor->richPageForCurrentSelection();
|
||||
if (source && !source->blocks.empty()) {
|
||||
HistoryView::Controls::ShowComposeAiBox(_show, {
|
||||
|
||||
@@ -7588,23 +7588,21 @@ State::ActiveTextBlockActionResult State::applyActiveTextBlockAction(
|
||||
});
|
||||
}
|
||||
|
||||
State::ActiveTextBlockActionResult
|
||||
State::replaceActiveTextSelectionWithRichPage(
|
||||
std::shared_ptr<const RichPage> page,
|
||||
State::ActiveTextBlockActionResult State::replaceActiveTextSelectionWithText(
|
||||
TextWithEntities text,
|
||||
ActiveTextInsertContext context) {
|
||||
return applyCheckedMutation(ActiveTextBlockActionResult{
|
||||
.result = ApplyResult::Failed,
|
||||
}, [page = std::move(page), context = std::move(context)](
|
||||
}, [text = std::move(text), context = std::move(context)](
|
||||
State &candidate) mutable {
|
||||
ActiveTextBlockActionResult result{
|
||||
.result = ApplyResult::Failed,
|
||||
};
|
||||
const auto failed = [&] {
|
||||
return CheckedMutationResult<ActiveTextBlockActionResult>{
|
||||
.result = result,
|
||||
.result = ActiveTextBlockActionResult{
|
||||
.result = ApplyResult::Failed,
|
||||
},
|
||||
};
|
||||
};
|
||||
if (!page || page->blocks.empty()) {
|
||||
if (text.text.isEmpty()) {
|
||||
return failed();
|
||||
}
|
||||
const auto descriptor = candidate.textNode(
|
||||
@@ -7612,40 +7610,13 @@ State::replaceActiveTextSelectionWithRichPage(
|
||||
if (!descriptor || (descriptor->leaf.kind == LeafKind::MathFormula)) {
|
||||
return failed();
|
||||
}
|
||||
const auto singleParagraph = (page->blocks.size() == 1)
|
||||
&& (page->blocks.front().kind == BlockKind::Paragraph)
|
||||
&& page->blocks.front().blocks.empty()
|
||||
&& (descriptor->leaf.kind != LeafKind::TableCellText);
|
||||
if (singleParagraph) {
|
||||
const auto &inner = page->blocks.front().text.text;
|
||||
const auto selectionFrom = int(context.before.text.size());
|
||||
const auto selectionTo = selectionFrom + int(inner.text.size());
|
||||
const auto applied = candidate.applyActiveTextUnchecked(JoinText(
|
||||
context.before,
|
||||
inner,
|
||||
context.after));
|
||||
if (applied == ApplyResult::Failed) {
|
||||
return failed();
|
||||
}
|
||||
const auto updated = candidate.textNode(
|
||||
candidate._activeTextOrdinal);
|
||||
return CheckedMutationResult<ActiveTextBlockActionResult>{
|
||||
.apply = true,
|
||||
.result = {
|
||||
.result = ApplyResult::Changed,
|
||||
.destinationLeaf = (updated
|
||||
? updated->leaf
|
||||
: descriptor->leaf),
|
||||
.selectionFrom = selectionFrom,
|
||||
.selectionTo = selectionTo,
|
||||
},
|
||||
};
|
||||
}
|
||||
auto blocks = page->blocks;
|
||||
const auto applied = candidate.insertBlocksAfterActiveUnchecked(
|
||||
std::move(blocks),
|
||||
context);
|
||||
if (!applied) {
|
||||
const auto selectionFrom = int(context.before.text.size());
|
||||
const auto selectionTo = selectionFrom + int(text.text.size());
|
||||
const auto applied = candidate.applyActiveTextUnchecked(JoinText(
|
||||
std::move(context.before),
|
||||
std::move(text),
|
||||
std::move(context.after)));
|
||||
if (applied == ApplyResult::Failed) {
|
||||
return failed();
|
||||
}
|
||||
const auto updated = candidate.textNode(candidate._activeTextOrdinal);
|
||||
@@ -7656,8 +7627,8 @@ State::replaceActiveTextSelectionWithRichPage(
|
||||
.destinationLeaf = (updated
|
||||
? updated->leaf
|
||||
: descriptor->leaf),
|
||||
.selectionFrom = 0,
|
||||
.selectionTo = 0,
|
||||
.selectionFrom = selectionFrom,
|
||||
.selectionTo = selectionTo,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -413,8 +413,8 @@ public:
|
||||
InsertAction action,
|
||||
ActiveTextInsertContext context);
|
||||
[[nodiscard]] ActiveTextBlockActionResult
|
||||
replaceActiveTextSelectionWithRichPage(
|
||||
std::shared_ptr<const RichPage> page,
|
||||
replaceActiveTextSelectionWithText(
|
||||
TextWithEntities text,
|
||||
ActiveTextInsertContext context);
|
||||
[[nodiscard]] bool insertPreparedBlockAfterActive(RichPage::Block block);
|
||||
[[nodiscard]] bool insertPreparedBlocksAfterActive(
|
||||
|
||||
@@ -232,6 +232,18 @@ struct CommittedFieldSelectionRestore {
|
||||
int cursorOffset = 0;
|
||||
};
|
||||
|
||||
void RemoveBlockLevelEntities(TextWithEntities *text) {
|
||||
auto &list = text->entities;
|
||||
for (auto i = list.begin(); i != list.end();) {
|
||||
const auto type = i->type();
|
||||
if (type == EntityType::Blockquote || type == EntityType::Pre) {
|
||||
i = list.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constexpr auto kMaxRichTextNodeLength = 16000;
|
||||
constexpr auto kMaxCommittedFieldLength = 256 * 1024;
|
||||
|
||||
@@ -3570,8 +3582,54 @@ void Widget::pasteStructuredClipboardData(const ClipboardData &data) {
|
||||
});
|
||||
}
|
||||
|
||||
bool Widget::hasFieldTextSpanSelection() const {
|
||||
return !_settingField
|
||||
&& !_field->isHidden()
|
||||
&& (_activeSegmentIndex >= 0)
|
||||
&& (_state->activeFieldMode() != State::FieldMode::Raw)
|
||||
&& _field->textCursor().hasSelection();
|
||||
}
|
||||
|
||||
bool Widget::hasActiveSelection() const {
|
||||
return hasStructuralSelection() || !_selection.empty();
|
||||
return hasStructuralSelection()
|
||||
|| !_selection.empty()
|
||||
|| hasFieldTextSpanSelection();
|
||||
}
|
||||
|
||||
TextWithEntities Widget::textSpanForCurrentSelection() {
|
||||
if (hasStructuralSelection()) {
|
||||
return {};
|
||||
}
|
||||
if (hasFieldTextSpanSelection()) {
|
||||
if (const auto context = activeTextInsertContext()) {
|
||||
return context->selected;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
const auto selection = _selection;
|
||||
const auto sameSegmentSelection = !selection.empty()
|
||||
&& _article
|
||||
&& (selection.from.segment == selection.to.segment)
|
||||
&& _article->segmentIsText(selection.from.segment);
|
||||
const auto ordinal = sameSegmentSelection
|
||||
? editableOrdinalForSegment(selection.from.segment)
|
||||
: -1;
|
||||
if (ordinal < 0) {
|
||||
return {};
|
||||
}
|
||||
const auto selectionFrom = selection.from.offset;
|
||||
const auto selectionTo = selection.to.offset;
|
||||
clearTextSelection();
|
||||
if (!commitAndActivateTextOrdinal(
|
||||
ordinal,
|
||||
selectionFrom,
|
||||
selectionTo)) {
|
||||
return {};
|
||||
}
|
||||
if (const auto context = activeTextInsertContext()) {
|
||||
return context->selected;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<const RichPage> Widget::richPageForCurrentSelection() const {
|
||||
@@ -3603,15 +3661,7 @@ std::shared_ptr<const RichPage> Widget::richPageForCurrentSelection() const {
|
||||
}
|
||||
return page;
|
||||
}
|
||||
if (_selection.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
auto rich = currentSelectionTextForClipboard().rich;
|
||||
if (rich.text.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
return std::make_shared<RichPage>(
|
||||
Iv::SplitTextIntoRichPage(std::move(rich)));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Widget::replaceCurrentSelectionWithRichPage(
|
||||
@@ -3678,106 +3728,110 @@ void Widget::replaceCurrentSelectionWithRichPage(
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto data = ClipboardBlockData();
|
||||
data.blocks = page->blocks;
|
||||
pasteStructuredClipboardData(ClipboardData(std::move(data)));
|
||||
return;
|
||||
}
|
||||
if (auto context = activeTextInsertContext()) {
|
||||
recordMutationTransaction([&] {
|
||||
const auto restoreLeaf = _fieldLeaf;
|
||||
const auto restoreStyleKey = _activeFieldStyleKey;
|
||||
const auto restoreMode = _fieldMode;
|
||||
const auto restoreSelection
|
||||
= captureHistoryViewState().leafSelection;
|
||||
_pendingOrdinal = -1;
|
||||
_pendingCursorOffset = 0;
|
||||
hideInlineField();
|
||||
clearInlineFieldEditSession(true);
|
||||
auto restore = true;
|
||||
const auto restoreInlineField = gsl::finally([&] {
|
||||
if (!restore) {
|
||||
return;
|
||||
}
|
||||
if (restoreLeaf && restoreStyleKey) {
|
||||
if (auto revived = reviveRetainedLeafField(
|
||||
_historyIndex,
|
||||
*restoreLeaf,
|
||||
restoreMode,
|
||||
*restoreStyleKey)) {
|
||||
_field = std::move(revived);
|
||||
_activeFieldStyleKey = restoreStyleKey;
|
||||
_fieldMode = restoreMode;
|
||||
_fieldLeaf = *restoreLeaf;
|
||||
refreshInlineFieldPlaceholder();
|
||||
_fieldUndoAvailable = _field->isUndoAvailable();
|
||||
_fieldRedoAvailable = _field->isRedoAvailable();
|
||||
clearFieldUndoRedoNoopState();
|
||||
}
|
||||
}
|
||||
if (!_fieldLeaf && restoreSelection) {
|
||||
const auto ordinal = _state->textOrdinalForLeafPath(
|
||||
restoreSelection->leaf);
|
||||
if (ordinal >= 0) {
|
||||
activateTextOrdinal(
|
||||
ordinal,
|
||||
restoreSelection->anchorOffset,
|
||||
restoreSelection->cursorOffset);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_field->show();
|
||||
syncInlineFieldGeometry();
|
||||
updateInlineFieldHeightOverride();
|
||||
syncArticleVisibleTopBottom();
|
||||
revealActiveInlineField();
|
||||
_field->raise();
|
||||
_field->setFocusFast();
|
||||
notifyToolbarStateChanged();
|
||||
});
|
||||
const auto applied = _state->replaceActiveTextSelectionWithRichPage(
|
||||
page,
|
||||
*context);
|
||||
if (applied.result != ApplyResult::Changed) {
|
||||
showLastLimitToast();
|
||||
return MutationTransactionResult{
|
||||
.committed = ApplyResult::Unchanged,
|
||||
};
|
||||
}
|
||||
restore = false;
|
||||
refreshPreparedContent();
|
||||
auto restored = false;
|
||||
if (applied.destinationLeaf) {
|
||||
const auto ordinal = _state->textOrdinalForLeafPath(
|
||||
*applied.destinationLeaf);
|
||||
if (ordinal >= 0) {
|
||||
activateTextOrdinal(
|
||||
ordinal,
|
||||
applied.selectionFrom,
|
||||
applied.selectionTo);
|
||||
restored = true;
|
||||
}
|
||||
}
|
||||
if (!restored) {
|
||||
const auto ordinal = _state->activeTextOrdinal();
|
||||
if (ordinal >= 0 && ordinal < _state->textNodeCount()) {
|
||||
activateTextOrdinal(ordinal, 0);
|
||||
} else {
|
||||
activateInitialNode();
|
||||
}
|
||||
}
|
||||
return MutationTransactionResult{
|
||||
.committed = ApplyResult::Unchanged,
|
||||
.changed = true,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
auto data = ClipboardBlockData();
|
||||
data.blocks = page->blocks;
|
||||
pasteStructuredClipboardData(ClipboardData(std::move(data)));
|
||||
}
|
||||
|
||||
void Widget::replaceCurrentSelectionWithText(TextWithEntities text) {
|
||||
RemoveBlockLevelEntities(&text);
|
||||
if (text.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto context = activeTextInsertContext();
|
||||
if (!context || context->selected.text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
recordMutationTransaction([&] {
|
||||
const auto restoreLeaf = _fieldLeaf;
|
||||
const auto restoreStyleKey = _activeFieldStyleKey;
|
||||
const auto restoreMode = _fieldMode;
|
||||
const auto restoreSelection
|
||||
= captureHistoryViewState().leafSelection;
|
||||
_pendingOrdinal = -1;
|
||||
_pendingCursorOffset = 0;
|
||||
hideInlineField();
|
||||
clearInlineFieldEditSession(true);
|
||||
auto restore = true;
|
||||
const auto restoreInlineField = gsl::finally([&] {
|
||||
if (!restore) {
|
||||
return;
|
||||
}
|
||||
if (restoreLeaf && restoreStyleKey) {
|
||||
if (auto revived = reviveRetainedLeafField(
|
||||
_historyIndex,
|
||||
*restoreLeaf,
|
||||
restoreMode,
|
||||
*restoreStyleKey)) {
|
||||
_field = std::move(revived);
|
||||
_activeFieldStyleKey = restoreStyleKey;
|
||||
_fieldMode = restoreMode;
|
||||
_fieldLeaf = *restoreLeaf;
|
||||
refreshInlineFieldPlaceholder();
|
||||
_fieldUndoAvailable = _field->isUndoAvailable();
|
||||
_fieldRedoAvailable = _field->isRedoAvailable();
|
||||
clearFieldUndoRedoNoopState();
|
||||
}
|
||||
}
|
||||
if (!_fieldLeaf && restoreSelection) {
|
||||
const auto ordinal = _state->textOrdinalForLeafPath(
|
||||
restoreSelection->leaf);
|
||||
if (ordinal >= 0) {
|
||||
activateTextOrdinal(
|
||||
ordinal,
|
||||
restoreSelection->anchorOffset,
|
||||
restoreSelection->cursorOffset);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_field->show();
|
||||
syncInlineFieldGeometry();
|
||||
updateInlineFieldHeightOverride();
|
||||
syncArticleVisibleTopBottom();
|
||||
revealActiveInlineField();
|
||||
_field->raise();
|
||||
_field->setFocusFast();
|
||||
notifyToolbarStateChanged();
|
||||
});
|
||||
const auto applied = _state->replaceActiveTextSelectionWithText(
|
||||
std::move(text),
|
||||
*context);
|
||||
if (applied.result != ApplyResult::Changed) {
|
||||
showLastLimitToast();
|
||||
return MutationTransactionResult{
|
||||
.committed = ApplyResult::Unchanged,
|
||||
};
|
||||
}
|
||||
restore = false;
|
||||
refreshPreparedContent();
|
||||
auto restored = false;
|
||||
if (applied.destinationLeaf) {
|
||||
const auto ordinal = _state->textOrdinalForLeafPath(
|
||||
*applied.destinationLeaf);
|
||||
if (ordinal >= 0) {
|
||||
activateTextOrdinal(
|
||||
ordinal,
|
||||
applied.selectionFrom,
|
||||
applied.selectionTo);
|
||||
restored = true;
|
||||
}
|
||||
}
|
||||
if (!restored) {
|
||||
const auto ordinal = _state->activeTextOrdinal();
|
||||
if (ordinal >= 0 && ordinal < _state->textNodeCount()) {
|
||||
activateTextOrdinal(ordinal, 0);
|
||||
} else {
|
||||
activateInitialNode();
|
||||
}
|
||||
}
|
||||
return MutationTransactionResult{
|
||||
.committed = ApplyResult::Unchanged,
|
||||
.changed = true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
bool Widget::handleClipboardKey(QKeyEvent *e) {
|
||||
if (e == QKeySequence::Copy) {
|
||||
if (_selection.empty() && !hasStructuralSelection()) {
|
||||
|
||||
@@ -126,6 +126,8 @@ public:
|
||||
richPageForCurrentSelection() const;
|
||||
void replaceCurrentSelectionWithRichPage(
|
||||
std::shared_ptr<const RichPage> page);
|
||||
[[nodiscard]] TextWithEntities textSpanForCurrentSelection();
|
||||
void replaceCurrentSelectionWithText(TextWithEntities text);
|
||||
void pastePreparedBlock(
|
||||
RichPage::Block block,
|
||||
PreparedMediaPasteTarget target);
|
||||
@@ -475,6 +477,7 @@ private:
|
||||
};
|
||||
[[nodiscard]] std::optional<State::ActiveTextInsertContext>
|
||||
activeTextInsertContext() const;
|
||||
[[nodiscard]] bool hasFieldTextSpanSelection() const;
|
||||
[[nodiscard]] PreparedMediaPasteTarget preparedMediaPasteTarget() const;
|
||||
struct PreparedMediaPasteActivation {
|
||||
bool resolved = false;
|
||||
|
||||
Reference in New Issue
Block a user