Allow drag-n-drop of blocks selections.

This commit is contained in:
John Preston
2026-06-18 08:20:34 +04:00
parent 2027d358b6
commit 748bc8236a
9 changed files with 2855 additions and 85 deletions
@@ -183,6 +183,11 @@ void ShowRichMessagesPremiumToast(std::shared_ptr<ChatHelpers::Show> show) {
}
}
[[nodiscard]] bool IsPhotoVideoRichMessageKind(RichPage::BlockKind kind) {
return (kind == RichPage::BlockKind::Photo)
|| (kind == RichPage::BlockKind::Video);
}
void CountRichPageMedia(
const std::vector<RichPage::Block> &blocks,
int *result) {
@@ -221,7 +226,7 @@ template <typename Container>
[[nodiscard]] int CountAcceptedPreparedFiles(const PreparedList &list) {
return CountAcceptedPreparedFiles(list.files)
+ CountAcceptedPreparedFiles(list.filesToProcess);
+ int(list.filesToProcess.size());
}
[[nodiscard]] bool IsReplacing(
@@ -536,12 +541,34 @@ private:
DocumentData *serverDocument = nullptr;
};
enum class MediaBatchItemState : uchar {
Waiting,
Ready,
Skipped,
Inserted,
};
struct MediaBatchItem {
MediaBatchItemState state = MediaBatchItemState::Waiting;
FullMsgId uploadId = FullMsgId();
RichPage::BlockKind blockKind = RichPage::BlockKind::Unsupported;
};
struct MediaBatch {
uint64 id = 0;
QPointer<Widget> editor;
AttachmentInsertMode insertMode = AttachmentInsertMode::Normal;
std::optional<PreparedMediaPasteTarget> insertTarget;
std::vector<MediaBatchItem> items;
int nextIndex = 0;
};
struct QueuedPrepare {
QPointer<Widget> editor;
PreparedFile file;
uint64 batchId = 0;
int order = 0;
AttachmentInsertMode insertMode = AttachmentInsertMode::Normal;
std::optional<PreparedMediaPasteTarget> insertTarget;
std::optional<State::ReplaceTarget> replaceTarget;
};
@@ -994,19 +1021,29 @@ private:
_editor = editor;
const auto weak = base::make_weak(this);
const auto editorPointer = QPointer<Widget>(editor.get());
FileDialog::GetOpenPath(
std::move(parent),
tr::lng_choose_file(tr::now),
FileDialog::PhotoVideoAudioFilesFilter(),
[weak, editorPointer, replaceTarget = std::move(replaceTarget)](
FileDialog::OpenResult &&result) mutable {
if (const auto session = weak.get()) {
session->handleMediaDialogResult(
editorPointer,
std::move(result),
std::move(replaceTarget));
}
});
const auto replacing = replaceTarget.has_value();
auto callback = [weak, editorPointer, replaceTarget = std::move(
replaceTarget)](FileDialog::OpenResult &&result) mutable {
if (const auto session = weak.get()) {
session->handleMediaDialogResult(
editorPointer,
std::move(result),
std::move(replaceTarget));
}
};
if (replacing) {
FileDialog::GetOpenPath(
std::move(parent),
tr::lng_choose_file(tr::now),
FileDialog::PhotoVideoAudioFilesFilter(),
std::move(callback));
} else {
FileDialog::GetOpenPaths(
std::move(parent),
tr::lng_choose_files(tr::now),
FileDialog::PhotoVideoAudioFilesFilter(),
std::move(callback));
}
}
void requestMap(
@@ -1236,13 +1273,47 @@ private:
showRichMessageLimitToast(RichMessageLimitError::Media);
return;
}
if (replacing) {
if (!list.files.empty()) {
applyPreparedFile(
editor,
std::move(list.files.front()),
batchId,
0,
effectiveInsertMode,
std::move(replaceTarget));
} else if (!list.filesToProcess.empty()) {
_prepareQueue.push_back({
.editor = editor,
.file = std::move(list.filesToProcess.front()),
.batchId = batchId,
.order = 0,
.insertMode = effectiveInsertMode,
.replaceTarget = std::move(replaceTarget),
});
enqueueNextPrepare();
}
return;
}
const auto totalCount = int(
list.files.size() + list.filesToProcess.size());
if (totalCount > 0) {
_mediaBatches.push_back({
.id = batchId,
.editor = editor,
.insertMode = effectiveInsertMode,
.insertTarget = insertTarget,
.items = std::vector<MediaBatchItem>(totalCount),
});
}
auto order = 0;
for (auto &file : list.files) {
applyPreparedFile(
editor,
std::move(file),
batchId,
order++,
effectiveInsertMode,
insertTarget,
replaceTarget);
}
for (auto &file : list.filesToProcess) {
@@ -1250,8 +1321,8 @@ private:
.editor = editor,
.file = std::move(file),
.batchId = batchId,
.order = order++,
.insertMode = effectiveInsertMode,
.insertTarget = insertTarget,
.replaceTarget = replaceTarget,
});
}
@@ -1270,8 +1341,8 @@ private:
queued.editor,
std::move(queued.file),
queued.batchId,
queued.order,
queued.insertMode,
std::move(queued.insertTarget),
std::move(queued.replaceTarget));
}
if (_prepareQueue.empty()) {
@@ -1282,7 +1353,6 @@ private:
_prepareQueue.pop_front();
const auto weak = base::make_weak(this);
_preparing = true;
_preparingFileType = queued.file.type;
const auto sideLimit = PhotoSideLimit();
crl::async([weak, queued = std::move(queued), sideLimit]() mutable {
Storage::PrepareDetails(
@@ -1299,13 +1369,12 @@ private:
void preparedAsyncFile(QueuedPrepare queued) {
_preparing = false;
_preparingFileType = PreparedFileType::None;
applyPreparedFile(
queued.editor,
std::move(queued.file),
queued.batchId,
queued.order,
queued.insertMode,
std::move(queued.insertTarget),
std::move(queued.replaceTarget));
enqueueNextPrepare();
}
@@ -1314,10 +1383,14 @@ private:
QPointer<Widget> editor,
PreparedFile file,
uint64 batchId,
int order,
AttachmentInsertMode insertMode,
std::optional<PreparedMediaPasteTarget> insertTarget,
std::optional<State::ReplaceTarget> replaceTarget) {
if (!AcceptedPreparedFileType(file.type)) {
if (!IsReplacing(insertMode, replaceTarget)) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
showUnsupportedMediaToast(batchId);
return;
}
@@ -1325,6 +1398,10 @@ private:
? 0
: 1;
if (exceedsMediaLimitWith(additionalMedia)) {
if (!IsReplacing(insertMode, replaceTarget)) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
showRichMessageLimitToast(RichMessageLimitError::Media);
return;
}
@@ -1332,8 +1409,8 @@ private:
editor,
std::move(file),
batchId,
order,
insertMode,
std::move(insertTarget),
std::move(replaceTarget));
}
@@ -1341,8 +1418,8 @@ private:
QPointer<Widget> editor,
PreparedFile file,
uint64 batchId,
int order,
AttachmentInsertMode insertMode,
std::optional<PreparedMediaPasteTarget> insertTarget,
std::optional<State::ReplaceTarget> replaceTarget) {
const auto meta = BuildAttachmentMeta(file);
const auto weak = base::make_weak(this);
@@ -1350,7 +1427,7 @@ private:
_attachmentPrepareQueue.addTask(
std::make_unique<PrepareAttachmentTask>(
BuildPrepareTaskArgs(_session, _peer->id, std::move(file)),
[weak, editor, meta, batchId, insertMode, insertTarget, replaceTarget](
[weak, editor, meta, batchId, order, insertMode, replaceTarget](
std::shared_ptr<FilePrepareResult> prepared) mutable {
if (const auto session = weak.get()) {
session->attachmentPrepared(
@@ -1358,8 +1435,8 @@ private:
std::move(meta),
std::move(prepared),
batchId,
order,
insertMode,
std::move(insertTarget),
std::move(replaceTarget));
}
}));
@@ -1370,32 +1447,55 @@ private:
AttachmentMeta meta,
std::shared_ptr<FilePrepareResult> prepared,
uint64 batchId,
int order,
AttachmentInsertMode insertMode,
std::optional<PreparedMediaPasteTarget> insertTarget,
std::optional<State::ReplaceTarget> replaceTarget) {
_pendingAttachmentPrepareCount = std::max(
_pendingAttachmentPrepareCount - 1,
0);
if (!prepared) {
if (!IsReplacing(insertMode, replaceTarget)) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
showAttachmentFailedToast();
maybeContinueDeferredSubmit();
return;
}
if (!editor) {
if (!IsReplacing(insertMode, replaceTarget)) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
maybeContinueDeferredSubmit();
return;
}
if (meta.blockKind != BlockKindForPreparedResult(*prepared)) {
if (!IsReplacing(insertMode, replaceTarget)) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
showUnsupportedMediaToast(batchId);
maybeContinueDeferredSubmit();
return;
}
const auto replacing = IsReplacing(insertMode, replaceTarget);
if (exceedsMediaLimitWith(replacing ? 0 : 1)) {
if (!replacing) {
markMediaBatchItemSkipped(batchId, order);
flushMediaBatch(batchId);
}
showRichMessageLimitToast(RichMessageLimitError::Media);
maybeContinueDeferredSubmit();
return;
}
startAttachmentUpload(
editor,
std::move(meta),
std::move(prepared),
batchId,
order,
insertMode,
std::move(insertTarget),
std::move(replaceTarget));
maybeContinueDeferredSubmit();
}
@@ -1404,18 +1504,55 @@ private:
QPointer<Widget> editor,
AttachmentMeta meta,
std::shared_ptr<FilePrepareResult> prepared,
uint64 batchId,
int order,
AttachmentInsertMode insertMode,
std::optional<PreparedMediaPasteTarget> insertTarget,
std::optional<State::ReplaceTarget> replaceTarget) {
if (!editor || !prepared) {
if (!editor) {
return;
}
const auto replacing = IsReplacing(insertMode, replaceTarget);
if (exceedsMediaLimitWith(replacing ? 0 : 1)) {
showRichMessageLimitToast(RichMessageLimitError::Media);
_editor = editor;
const auto blockKind = meta.blockKind;
const auto uploadId = createAttachmentUpload(
std::move(meta),
std::move(prepared));
if (!uploadId) {
return;
}
_editor = editor;
const auto attachment = findAttachment(*uploadId);
if (!attachment) {
return;
}
if (!replacing) {
markMediaBatchItemReady(
batchId,
order,
*uploadId,
blockKind);
flushMediaBatch(batchId);
return;
}
auto block = makeAttachmentBlock(*attachment);
editor->replacePreparedBlock(
std::move(*replaceTarget),
std::move(block));
refreshAttachmentLocatorsAndDropMissing();
const auto updated = findAttachment(*uploadId);
if (!updated) {
requestEditorUpdate();
return;
}
updateAttachmentProgress(*updated);
requestEditorUpdate();
}
[[nodiscard]] std::optional<FullMsgId> createAttachmentUpload(
AttachmentMeta meta,
std::shared_ptr<FilePrepareResult> prepared) {
if (!prepared) {
return std::nullopt;
}
const auto uploadId = FullMsgId(
_peer->id,
_session->data().nextLocalMessageId());
@@ -1473,40 +1610,8 @@ private:
}
_attachments.push_back(std::move(record));
auto &stored = _attachments.back();
_session->uploader().upload(uploadId, prepared);
auto block = makeAttachmentBlock(stored);
if (replacing) {
editor->replacePreparedBlock(
std::move(*replaceTarget),
std::move(block));
} else if (insertMode == AttachmentInsertMode::ClipboardPaste
&& insertTarget) {
editor->pastePreparedBlock(
std::move(block),
std::move(*insertTarget));
} else {
editor->insertPreparedBlock(std::move(block));
}
if (replacing) {
refreshAttachmentLocatorsAndDropMissing();
const auto attachment = findAttachment(uploadId);
if (!attachment) {
requestEditorUpdate();
return;
}
updateAttachmentProgress(*attachment);
requestEditorUpdate();
return;
}
refreshAttachmentLocators(_state->richPage(), stored);
if (stored.blockLocators.empty()) {
_session->uploader().cancel(uploadId);
_attachments.pop_back();
return;
}
updateAttachmentProgress(stored);
requestEditorUpdate();
return uploadId;
}
void applyMapSelection(
@@ -1546,6 +1651,57 @@ private:
return block;
}
[[nodiscard]] auto makeGroupedAttachmentItem(
const AttachmentRecord &attachment) const
-> std::optional<RichPage::GroupedMediaItem> {
auto item = RichPage::GroupedMediaItem();
item.kind = attachment.blockKind;
if (attachment.blockKind == RichPage::BlockKind::Photo) {
item.photoId = attachment.localMediaId;
} else if (attachment.blockKind == RichPage::BlockKind::Video) {
item.documentId = attachment.localMediaId;
} else {
return std::nullopt;
}
item.width = attachment.dimensions.width();
item.height = attachment.dimensions.height();
item.autoplay = attachment.autoplay;
item.loop = attachment.loop;
item.spoiler = attachment.spoiler;
return item;
}
[[nodiscard]] RichPage::Block makeGroupedAttachmentBlock(
const std::vector<FullMsgId> &uploadIds) const {
auto block = RichPage::Block();
block.kind = RichPage::BlockKind::GroupedMedia;
block.mediaIntent = RichPage::GroupedMediaIntent::Collage;
block.mediaItems.reserve(uploadIds.size());
auto caption = QString();
auto captionCount = 0;
for (const auto &uploadId : uploadIds) {
const auto attachment = findAttachment(uploadId);
if (!attachment) {
continue;
}
const auto item = makeGroupedAttachmentItem(*attachment);
if (!item) {
continue;
}
block.mediaItems.push_back(*item);
if (caption.isEmpty() && !attachment->caption.isEmpty()) {
caption = attachment->caption;
}
if (!attachment->caption.isEmpty()) {
++captionCount;
}
}
if (captionCount == 1) {
block.caption = ToRichText(std::move(caption));
}
return block;
}
[[nodiscard]] RichPage::Block makeMapBlock(::Data::InputVenue venue) const {
const auto point = ::Data::LocationPoint(
venue.lat,
@@ -1878,6 +2034,321 @@ private:
return nullptr;
}
[[nodiscard]] const AttachmentRecord *findAttachment(
FullMsgId uploadId) const {
for (const auto &attachment : _attachments) {
if (attachment.uploadId == uploadId) {
return &attachment;
}
}
return nullptr;
}
[[nodiscard]] MediaBatch *findMediaBatch(uint64 batchId) {
for (auto &batch : _mediaBatches) {
if (batch.id == batchId) {
return &batch;
}
}
return nullptr;
}
[[nodiscard]] bool eraseFinishedMediaBatch(uint64 batchId) {
auto erased = false;
_mediaBatches.erase(
std::remove_if(
_mediaBatches.begin(),
_mediaBatches.end(),
[=, &erased](const MediaBatch &batch) {
const auto done = (batch.id == batchId)
&& std::all_of(
batch.items.begin(),
batch.items.end(),
[](const MediaBatchItem &item) {
return (item.state
== MediaBatchItemState::Inserted)
|| (item.state
== MediaBatchItemState::Skipped);
});
if (done) {
erased = true;
}
return done;
}),
_mediaBatches.end());
return erased;
}
void markMediaBatchItemSkipped(uint64 batchId, int order) {
const auto batch = findMediaBatch(batchId);
if (!batch || order < 0 || order >= int(batch->items.size())) {
return;
}
auto &item = batch->items[order];
if (item.state != MediaBatchItemState::Inserted) {
item.state = MediaBatchItemState::Skipped;
}
}
void markMediaBatchItemReady(
uint64 batchId,
int order,
FullMsgId uploadId,
RichPage::BlockKind blockKind) {
const auto batch = findMediaBatch(batchId);
if (!batch
|| order < 0
|| order >= int(batch->items.size())
|| !findAttachment(uploadId)) {
return;
}
auto &item = batch->items[order];
item.state = MediaBatchItemState::Ready;
item.uploadId = uploadId;
item.blockKind = blockKind;
}
void eraseAttachment(FullMsgId uploadId) {
const auto i = std::find_if(
_attachments.begin(),
_attachments.end(),
[=](const AttachmentRecord &attachment) {
return attachment.uploadId == uploadId;
});
if (i == _attachments.end()) {
return;
}
if (i->state != AttachmentState::Ready) {
_session->uploader().cancel(i->uploadId);
}
_attachments.erase(i);
}
[[nodiscard]] bool hasUninsertedMediaBatchUpload(
FullMsgId uploadId) const {
for (const auto &batch : _mediaBatches) {
for (const auto &item : batch.items) {
if (item.state == MediaBatchItemState::Ready
&& item.uploadId == uploadId) {
return true;
}
}
}
return false;
}
void abandonMediaBatch(uint64 batchId) {
const auto batch = findMediaBatch(batchId);
if (!batch) {
return;
}
for (auto &item : batch->items) {
if (item.state == MediaBatchItemState::Ready
&& item.uploadId) {
eraseAttachment(item.uploadId);
}
if (item.state != MediaBatchItemState::Inserted) {
item.state = MediaBatchItemState::Skipped;
}
}
_mediaBatches.erase(
std::remove_if(
_mediaBatches.begin(),
_mediaBatches.end(),
[=](const MediaBatch &batch) {
return batch.id == batchId;
}),
_mediaBatches.end());
maybeContinueDeferredSubmit();
}
void flushMediaBatch(uint64 batchId) {
if (eraseFinishedMediaBatch(batchId)) {
maybeContinueDeferredSubmit();
return;
}
const auto batch = findMediaBatch(batchId);
if (!batch) {
return;
}
if (!batch->editor) {
abandonMediaBatch(batchId);
return;
}
auto blocks = std::vector<RichPage::Block>();
auto emittedUploadIds = std::vector<FullMsgId>();
const auto skipFinished = [&] {
while (batch->nextIndex < int(batch->items.size())) {
const auto state = batch->items[batch->nextIndex].state;
if (state != MediaBatchItemState::Skipped
&& state != MediaBatchItemState::Inserted) {
return;
}
++batch->nextIndex;
}
};
const auto appendSubrun = [&](
const std::vector<FullMsgId> &uploadIds) {
if (uploadIds.empty()) {
return;
}
if (uploadIds.size() == 1) {
if (const auto attachment = findAttachment(uploadIds.front())) {
blocks.push_back(makeAttachmentBlock(*attachment));
}
} else {
blocks.push_back(makeGroupedAttachmentBlock(uploadIds));
}
emittedUploadIds.insert(
emittedUploadIds.end(),
uploadIds.begin(),
uploadIds.end());
};
const auto appendPhotoVideoRun = [&](
const std::vector<FullMsgId> &uploadIds) {
auto subrun = std::vector<FullMsgId>();
auto hasCaption = false;
for (const auto &uploadId : uploadIds) {
const auto attachment = findAttachment(uploadId);
if (!attachment) {
continue;
}
const auto itemHasCaption = !attachment->caption.isEmpty();
if (itemHasCaption && hasCaption) {
appendSubrun(subrun);
subrun.clear();
hasCaption = false;
}
subrun.push_back(uploadId);
hasCaption = hasCaption || itemHasCaption;
}
appendSubrun(subrun);
};
while (true) {
skipFinished();
if (batch->nextIndex >= int(batch->items.size())) {
break;
}
auto &item = batch->items[batch->nextIndex];
if (item.state == MediaBatchItemState::Waiting) {
break;
}
if (item.state != MediaBatchItemState::Ready) {
break;
}
const auto attachment = findAttachment(item.uploadId);
if (!attachment) {
item.state = MediaBatchItemState::Skipped;
++batch->nextIndex;
continue;
}
if (item.blockKind == RichPage::BlockKind::Audio) {
blocks.push_back(makeAttachmentBlock(*attachment));
emittedUploadIds.push_back(item.uploadId);
item.state = MediaBatchItemState::Inserted;
++batch->nextIndex;
continue;
}
if (!IsPhotoVideoRichMessageKind(item.blockKind)) {
item.state = MediaBatchItemState::Skipped;
++batch->nextIndex;
continue;
}
auto cursor = batch->nextIndex;
auto waitingBeforeBoundary = false;
auto runUploadIds = std::vector<FullMsgId>();
auto runIndexes = std::vector<int>();
while (cursor < int(batch->items.size())) {
auto &candidate = batch->items[cursor];
if (candidate.state == MediaBatchItemState::Skipped
|| candidate.state == MediaBatchItemState::Inserted) {
++cursor;
continue;
}
if (candidate.state == MediaBatchItemState::Waiting) {
waitingBeforeBoundary = true;
break;
}
if (candidate.state != MediaBatchItemState::Ready) {
waitingBeforeBoundary = true;
break;
}
if (candidate.blockKind == RichPage::BlockKind::Audio) {
break;
}
if (!IsPhotoVideoRichMessageKind(candidate.blockKind)) {
candidate.state = MediaBatchItemState::Skipped;
++cursor;
continue;
}
if (findAttachment(candidate.uploadId)) {
runUploadIds.push_back(candidate.uploadId);
runIndexes.push_back(cursor);
} else {
candidate.state = MediaBatchItemState::Skipped;
}
++cursor;
}
if (waitingBeforeBoundary) {
break;
}
if (runUploadIds.empty()) {
batch->nextIndex = cursor;
continue;
}
appendPhotoVideoRun(runUploadIds);
for (const auto index : runIndexes) {
batch->items[index].state = MediaBatchItemState::Inserted;
}
batch->nextIndex = cursor;
}
if (blocks.empty()) {
if (eraseFinishedMediaBatch(batchId)) {
maybeContinueDeferredSubmit();
}
return;
}
const auto editor = batch->editor;
_editor = editor;
if (batch->insertMode == AttachmentInsertMode::ClipboardPaste
&& batch->insertTarget) {
auto target = std::move(*batch->insertTarget);
batch->insertTarget = std::nullopt;
editor->pastePreparedBlocks(std::move(blocks), std::move(target));
} else {
editor->insertPreparedBlocks(std::move(blocks));
}
refreshAttachmentLocatorsAndDropMissing();
for (const auto &uploadId : emittedUploadIds) {
if (const auto attachment = findAttachment(uploadId)) {
if (attachment->state == AttachmentState::Ready && _editor) {
auto patched = true;
_editor->applyExternalRichPageMutation([&](
RichPage &page) {
const auto result = patchVisibleAttachmentBlocks(
page,
*attachment);
patched = patched && result;
return result;
});
if (!patched) {
requestEditorUpdate();
}
}
}
if (const auto attachment = findAttachment(uploadId)) {
if (!attachment->blockLocators.empty()) {
updateAttachmentProgress(*attachment);
}
}
}
requestEditorUpdate();
if (eraseFinishedMediaBatch(batchId)) {
maybeContinueDeferredSubmit();
}
}
[[nodiscard]] bool mediaIdMatchesAttachment(
uint64 id,
const AttachmentRecord &attachment) const {
@@ -2006,6 +2477,10 @@ private:
++i;
continue;
}
if (hasUninsertedMediaBatchUpload(i->uploadId)) {
++i;
continue;
}
if (i->state != AttachmentState::Ready) {
_session->uploader().cancel(i->uploadId);
}
@@ -2076,14 +2551,22 @@ private:
[[nodiscard]] int pendingAttachmentPlaceholders() const {
auto result = _pendingAttachmentPrepareCount;
if (AcceptedPreparedFileType(_preparingFileType)) {
if (_preparing) {
++result;
}
for (const auto &queued : _prepareQueue) {
if (AcceptedPreparedFileType(queued.file.type)) {
if (AcceptedPreparedFileType(queued.file.type)
|| !queued.file.information) {
++result;
}
}
for (const auto &batch : _mediaBatches) {
for (const auto &item : batch.items) {
if (item.state == MediaBatchItemState::Ready) {
++result;
}
}
}
return result;
}
@@ -2144,7 +2627,8 @@ private:
[[nodiscard]] bool hasPendingPreparation() const {
return _preparing
|| !_prepareQueue.empty()
|| (_pendingAttachmentPrepareCount > 0);
|| (_pendingAttachmentPrepareCount > 0)
|| !_mediaBatches.empty();
}
void maybeContinueDeferredSubmit() {
@@ -2196,13 +2680,13 @@ private:
std::shared_ptr<const RichPage> _submittedPage;
std::vector<AttachmentRecord> _attachments;
std::deque<QueuedPrepare> _prepareQueue;
std::vector<MediaBatch> _mediaBatches;
TaskQueue _attachmentPrepareQueue;
rpl::lifetime _lifetime;
uint64 _prepareBatchId = 0;
uint64 _rejectedToastBatchId = 0;
int _pendingAttachmentPrepareCount = 0;
bool _preparing = false;
PreparedFileType _preparingFileType = PreparedFileType::None;
bool _submitDeferred = false;
bool _submitApiRequested = false;
@@ -51,6 +51,7 @@ using TableRow = RichPage::TableRow;
using TaskState = RichPage::TaskState;
using TextNodeDescriptor = State::TextNodeDescriptor;
using TextFormattingAction = State::TextFormattingAction;
using TextSelectionDropResult = State::TextSelectionDropResult;
using TextNodeSpan = State::TextNodeSpan;
struct TextRange {
@@ -606,6 +607,44 @@ GroupedItemFromPhotoVideoBlock(const Block &block) {
return ShiftBlockContainerPathAfterRemovedBlock(path.container, removed);
}
[[nodiscard]] bool ShiftBlockContainerPathAfterRemovedListItem(
BlockContainerPath &path,
const BlockPath &list,
int removedItemIndex) {
const auto removed = ListItemChildrenContainer(list, removedItemIndex);
if (ContainerHasPrefix(path, removed)) {
return false;
}
if (!ContainerHasPrefix(path, list.container)) {
return true;
}
const auto size = list.container.steps.size();
if (path.steps.size() <= size) {
return true;
}
auto &step = path.steps[size];
if (step.blockIndex != list.index
|| step.kind != BlockContainerKind::ListItemChildren) {
return true;
}
if (step.listItemIndex == removedItemIndex) {
return false;
} else if (step.listItemIndex > removedItemIndex) {
--step.listItemIndex;
}
return true;
}
[[nodiscard]] bool ShiftBlockPathAfterRemovedListItem(
BlockPath &path,
const BlockPath &list,
int removedItemIndex) {
return ShiftBlockContainerPathAfterRemovedListItem(
path.container,
list,
removedItemIndex);
}
[[nodiscard]] std::optional<int> BlockIndexInContainer(
const LeafPath &leaf,
const BlockContainerPath &container) {
@@ -1327,6 +1366,45 @@ std::optional<PreparedEditLeafSource> State::activePreparedLeafSource() const {
return descriptor ? convertPreparedLeafSource(*descriptor) : std::nullopt;
}
std::vector<TextNodeSpan> State::resolveTextSpansForPreparedLeafRange(
const PreparedEditLeafSource &source,
int from,
int till) const {
if (from < 0 || till <= from) {
return {};
}
const auto firstLeaf = convertLeafPath(source);
if (!firstLeaf) {
return {};
}
const auto firstOrdinal = textOrdinalForLeafPath(*firstLeaf);
if (firstOrdinal < 0) {
return {};
}
auto result = std::vector<TextNodeSpan>();
auto consumed = 0;
for (auto i = firstOrdinal, count = textNodeCount()
; i != count && consumed < till
; ++i) {
const auto current = richText(_textNodes[i].leaf);
if (!current) {
return {};
}
const auto length = int(current->text.text.size());
const auto spanFrom = std::max(from - consumed, 0);
const auto spanTo = std::min(till - consumed, length);
if (spanFrom < spanTo) {
result.push_back(TextNodeSpan{
.leaf = _textNodes[i].leaf,
.from = spanFrom,
.till = spanTo,
});
}
consumed += length;
}
return (consumed >= till) ? result : std::vector<TextNodeSpan>();
}
int State::textNodeCount() const {
return int(_textNodes.size());
}
@@ -5280,6 +5358,586 @@ bool State::replaceStructuralSelectionWithClipboardListItems(
return true;
}
State::StructuralSelectionDropResult State::moveStructuralSelectionToDropTarget(
const PreparedEditSelection &selection,
const Markdown::PreparedEditDropTarget &target) {
struct BlockInsertionTarget {
BlockContainerPath container;
int insertIndex = -1;
};
struct ListInsertionTarget {
BlockPath block;
int insertIndex = -1;
};
const auto failure = StructuralSelectionDropResult{
.result = ApplyResult::Failed,
};
return applyCheckedMutation(failure, [selection, target](State &candidate) {
auto result = StructuralSelectionDropResult{
.result = ApplyResult::Failed,
};
const auto payload = candidate.structuredClipboardDataForSelection(
selection);
if (!payload) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto blockTarget = std::optional<BlockInsertionTarget>();
auto listTarget = std::optional<ListInsertionTarget>();
if (const auto block = std::get_if<Markdown::PreparedEditBlockDropTarget>(
&target)) {
const auto container = candidate.convertBlockContainerPath(
block->container);
const auto blocks = container
? candidate.blockContainer(*container)
: nullptr;
if (!container
|| !blocks
|| block->insertIndex < 0
|| block->insertIndex > int(blocks->size())) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
blockTarget = {
.container = *container,
.insertIndex = block->insertIndex,
};
} else if (const auto list
= std::get_if<Markdown::PreparedEditListItemDropTarget>(&target)) {
const auto blockPath = candidate.convertBlockPath(list->block);
const auto owner = blockPath ? candidate.block(*blockPath) : nullptr;
if (!blockPath
|| !owner
|| owner->kind != BlockKind::List
|| list->insertIndex < 0
|| list->insertIndex > int(owner->listItems.size())) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
listTarget = {
.block = *blockPath,
.insertIndex = list->insertIndex,
};
} else {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks: {
const auto range = candidate.validateBlockRange(selection.blocks);
if (!range || !blockTarget) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (blockTarget->container == range->container) {
if (blockTarget->insertIndex >= range->from
&& blockTarget->insertIndex <= range->till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
} else if (blockTarget->insertIndex > range->till) {
blockTarget->insertIndex -= (range->till - range->from);
}
}
for (auto i = range->till; i != range->from;) {
--i;
const auto removed = BlockPath{
.container = range->container,
.index = i,
};
if (!ShiftBlockContainerPathAfterRemovedBlock(
blockTarget->container,
removed)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
} break;
case PreparedEditSelectionKind::ListItems: {
const auto range = candidate.validateListItemRange(
selection.listItems);
const auto owner = range ? candidate.block(range->block) : nullptr;
if (!range || !owner || owner->kind != BlockKind::List) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
const auto removesWholeList = (range->from == 0)
&& (range->till == int(owner->listItems.size()));
if (blockTarget) {
if (removesWholeList
&& blockTarget->container == range->block.container
&& blockTarget->insertIndex >= range->block.index
&& blockTarget->insertIndex <= range->block.index + 1) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (removesWholeList) {
if (blockTarget->container == range->block.container
&& blockTarget->insertIndex > range->block.index) {
--blockTarget->insertIndex;
}
if (!ShiftBlockContainerPathAfterRemovedBlock(
blockTarget->container,
range->block)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} else {
for (auto i = range->till; i != range->from;) {
--i;
if (!ShiftBlockContainerPathAfterRemovedListItem(
blockTarget->container,
range->block,
i)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
}
} else if (listTarget) {
if (listTarget->block == range->block) {
if (listTarget->insertIndex >= range->from
&& listTarget->insertIndex <= range->till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
} else if (listTarget->insertIndex > range->till) {
listTarget->insertIndex -= (range->till - range->from);
}
} else if (removesWholeList) {
if (!ShiftBlockPathAfterRemovedBlock(
listTarget->block,
range->block)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} else {
for (auto i = range->till; i != range->from;) {
--i;
if (!ShiftBlockPathAfterRemovedListItem(
listTarget->block,
range->block,
i)) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
}
}
} else {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
} break;
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (!candidate.removeStructuralSelection(selection, true)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (const auto blocks = std::get_if<ClipboardBlockData>(&*payload)) {
if (!blockTarget) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto inserted = blocks->blocks;
candidate.normalizeInsertedBlockAnchors(inserted);
const auto count = int(inserted.size());
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(inserted),
blockTarget->container,
blockTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
blockTarget->container,
blockTarget->insertIndex,
count);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
const auto items = std::get_if<ClipboardListItemsData>(&*payload);
if (!items) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto listBlock = Block();
listBlock.kind = BlockKind::List;
listBlock.listKind = items->listKind;
listBlock.listItems = items->items;
auto insertedBlocks = std::vector<Block>();
insertedBlocks.push_back(std::move(listBlock));
candidate.normalizeInsertedBlockAnchors(insertedBlocks);
if (listTarget) {
const auto owner = candidate.block(listTarget->block);
if (!owner || owner->kind != BlockKind::List) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
const auto taskList = std::any_of(
owner->listItems.begin(),
owner->listItems.end(),
[](const ListItem &item) {
return item.taskState != TaskState::None;
});
if (owner->listKind == items->listKind
&& taskList == items->taskList) {
auto insertedItems = std::move(insertedBlocks.front().listItems);
const auto count = int(insertedItems.size());
if (!candidate.insertPreparedListItemsAtExplicitPosition(
std::move(insertedItems),
listTarget->block,
listTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedListItems(
listTarget->block,
listTarget->insertIndex,
count);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
auto container = listTarget->block.container;
auto insertAt = listTarget->block.index;
auto trailingBlocks = std::vector<Block>();
if (listTarget->insertIndex > 0) {
insertAt = listTarget->block.index + 1;
if (listTarget->insertIndex < int(owner->listItems.size())) {
auto trailing = Block();
trailing.kind = BlockKind::List;
trailing.listKind = owner->listKind;
trailing.listItems = std::vector<ListItem>(
std::make_move_iterator(
owner->listItems.begin() + listTarget->insertIndex),
std::make_move_iterator(owner->listItems.end()));
owner->listItems.erase(
owner->listItems.begin() + listTarget->insertIndex,
owner->listItems.end());
trailingBlocks.push_back(std::move(trailing));
}
}
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(insertedBlocks),
container,
insertAt)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (!trailingBlocks.empty()
&& !candidate.insertPreparedBlocksAtExplicitPosition(
std::move(trailingBlocks),
container,
insertAt + 1)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
container,
insertAt,
1);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
}
if (!blockTarget
|| !candidate.insertPreparedBlocksAtExplicitPosition(
std::move(insertedBlocks),
blockTarget->container,
blockTarget->insertIndex)) {
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = false,
.result = result,
};
}
candidate.rebuild();
result.result = ApplyResult::Changed;
result.destination = candidate.destinationTargetForInsertedBlocks(
blockTarget->container,
blockTarget->insertIndex,
1);
return CheckedMutationResult<StructuralSelectionDropResult>{
.apply = true,
.result = result,
};
});
}
State::TextSelectionDropResult State::moveTextSelectionToDropTarget(
const std::vector<TextNodeSpan> &source,
const Markdown::PreparedEditDropTarget &target) {
const auto failure = TextSelectionDropResult{
.result = ApplyResult::Failed,
};
if (source.empty()) {
return failure;
}
return applyCheckedMutation(failure, [source, target](State &candidate) {
struct SourceRewrite {
LeafPath leaf;
TextWithEntities text;
};
auto result = TextSelectionDropResult{
.result = ApplyResult::Failed,
};
auto moved = TextWithEntities();
auto sourceRewrites = std::vector<SourceRewrite>();
sourceRewrites.reserve(source.size());
for (const auto &span : source) {
const auto current = candidate.richText(span.leaf);
if (!current) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto sourceBefore = TextWithEntities();
auto selected = TextWithEntities();
auto sourceAfter = TextWithEntities();
if (!SplitTextSpan(
current->text,
span.from,
span.till,
&sourceBefore,
&selected,
&sourceAfter)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
moved.append(std::move(selected));
sourceRewrites.push_back(SourceRewrite{
.leaf = span.leaf,
.text = JoinText(
std::move(sourceBefore),
TextWithEntities(),
std::move(sourceAfter)),
});
}
const auto movedLength = int(moved.text.size());
const auto applySourceRewrites = [&](std::vector<SourceRewrite> rewrites) {
for (auto &rewrite : rewrites) {
const auto current = candidate.richText(rewrite.leaf);
if (!current) {
return false;
}
current->text = std::move(rewrite.text);
}
return true;
};
const auto finishAtLeaf = [&](
const LeafPath &leaf,
int selectionFrom,
int selectionTo) {
candidate.rebuild();
const auto ordinal = candidate.textOrdinalForLeafPath(leaf);
if (ordinal >= 0) {
(void)candidate.setActiveTextByOrdinal(ordinal);
} else {
candidate.ensureActiveTextOrdinal();
}
result.result = ApplyResult::Changed;
result.destinationLeaf = leaf;
result.selectionFrom = selectionFrom;
result.selectionTo = selectionTo;
return CheckedMutationResult<TextSelectionDropResult>{
.apply = true,
.result = result,
};
};
if (const auto text = std::get_if<Markdown::PreparedEditTextDropTarget>(
&target)) {
const auto destinationLeaf = candidate.convertLeafPath(text->leaf);
const auto destination = destinationLeaf
? candidate.richText(*destinationLeaf)
: nullptr;
if (!destination
|| (text->leaf.kind
== Markdown::PreparedEditLeafKind::MathFormula)
|| !RangeInsideText(destination->text.text, text->offset, 0)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto insertAt = text->offset;
auto destinationRewrite = -1;
for (auto i = 0, count = int(source.size()); i != count; ++i) {
const auto &span = source[i];
if (!(span.leaf == *destinationLeaf)) {
continue;
}
if (text->offset >= span.from && text->offset <= span.till) {
result.result = ApplyResult::Unchanged;
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (span.from < insertAt) {
insertAt -= std::min(span.till, insertAt) - span.from;
}
destinationRewrite = i;
}
const auto destinationText = (destinationRewrite >= 0)
? sourceRewrites[destinationRewrite].text
: destination->text;
if (!RangeInsideText(destinationText.text, insertAt, 0)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto destinationBefore = Ui::Text::Mid(destinationText, 0, insertAt);
auto destinationAfter = Ui::Text::Mid(destinationText, insertAt);
auto updated = JoinText(
std::move(destinationBefore),
std::move(moved),
std::move(destinationAfter));
if (destinationRewrite >= 0) {
sourceRewrites[destinationRewrite].text = std::move(updated);
} else {
destination->text = std::move(updated);
}
if (!applySourceRewrites(std::move(sourceRewrites))) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
return finishAtLeaf(
*destinationLeaf,
insertAt,
insertAt + movedLength);
}
const auto block = std::get_if<Markdown::PreparedEditBlockDropTarget>(
&target);
const auto container = block
? candidate.convertBlockContainerPath(block->container)
: std::nullopt;
const auto destination = container
? candidate.blockContainer(*container)
: nullptr;
if (!block
|| !destination
|| block->insertIndex < 0
|| block->insertIndex > int(destination->size())) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
auto paragraph = MakeParagraphBlock();
paragraph.text.text = std::move(moved);
auto blocks = std::vector<Block>();
blocks.push_back(std::move(paragraph));
if (!applySourceRewrites(std::move(sourceRewrites))) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
if (!candidate.insertPreparedBlocksAtExplicitPosition(
std::move(blocks),
*container,
block->insertIndex)) {
return CheckedMutationResult<TextSelectionDropResult>{
.apply = false,
.result = result,
};
}
return finishAtLeaf(
LeafPath{
.kind = LeafKind::BlockText,
.block = BlockPath{
.container = *container,
.index = block->insertIndex,
},
},
0,
movedLength);
});
}
State::TextSelectionDropResult State::moveTextSelectionToDropTarget(
const TextNodeSpan &source,
const Markdown::PreparedEditDropTarget &target) {
return moveTextSelectionToDropTarget(
std::vector<TextNodeSpan>{ source },
target);
}
void State::insertHeading1AfterActive() {
(void)insertBlockAfterActive({
.type = InsertBlockType::Heading,
@@ -5643,6 +6301,39 @@ bool State::insertBlocksAfterActiveUnchecked(
return true;
}
bool State::insertPreparedBlocksAtExplicitPosition(
std::vector<Block> blocks,
const BlockContainerPath &container,
int insertAt) {
auto *destination = blockContainer(container);
if (!destination || insertAt < 0 || insertAt > int(destination->size())) {
return false;
}
destination->insert(
destination->begin() + insertAt,
std::make_move_iterator(blocks.begin()),
std::make_move_iterator(blocks.end()));
return true;
}
bool State::insertPreparedListItemsAtExplicitPosition(
std::vector<ListItem> items,
const BlockPath &path,
int insertAt) {
auto *owner = block(path);
if (!owner
|| owner->kind != BlockKind::List
|| insertAt < 0
|| insertAt > int(owner->listItems.size())) {
return false;
}
owner->listItems.insert(
owner->listItems.begin() + insertAt,
std::make_move_iterator(items.begin()),
std::make_move_iterator(items.end()));
return true;
}
std::vector<Block> *State::blockContainer(const BlockContainerPath &path) {
auto *blocks = &_richPage->blocks;
for (const auto &step : path.steps) {
@@ -6116,6 +6807,78 @@ void State::focusInsertedBlocks(
ensureActiveTextOrdinal();
}
State::BoundaryTarget State::destinationTargetForInsertedBlocks(
const BlockContainerPath &container,
int from,
int count) {
focusInsertedBlocks(container, from, count);
if (const auto descriptor = textNode(_activeTextOrdinal)) {
for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) {
const auto path = BlockPath{
.container = container,
.index = blockIndex,
};
if (descriptorBelongsToBlock(*descriptor, path)) {
return {
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
};
}
}
}
for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) {
const auto path = BlockPath{
.container = container,
.index = blockIndex,
};
if (const auto owner = block(path); owner && CanEditBlock(*owner)) {
return {
.action = BoundaryTarget::Action::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(path),
};
}
}
return {};
}
State::BoundaryTarget State::destinationTargetForInsertedListItems(
const BlockPath &path,
int from,
int count) {
for (auto i = 0, textCount = textNodeCount(); i != textCount; ++i) {
const auto itemIndex = ListItemIndexForLeaf(_textNodes[i].leaf, path);
if (!itemIndex
|| *itemIndex < from
|| *itemIndex >= from + count
|| !setActiveTextByOrdinal(i)) {
continue;
}
return {
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
};
}
const auto owner = block(path);
if (owner
&& owner->kind == BlockKind::List
&& from >= 0
&& from < int(owner->listItems.size())
&& CanEditBlocks(owner->listItems[from].blocks)) {
ensureActiveTextOrdinal();
return {
.action = BoundaryTarget::Action::StructuralSelection,
.structuralSelection = preparedSelectionForListItem(path, from),
};
}
ensureActiveTextOrdinal();
return (_activeTextOrdinal >= 0)
? BoundaryTarget{
.action = BoundaryTarget::Action::Text,
.textOrdinal = _activeTextOrdinal,
}
: BoundaryTarget();
}
std::optional<int> State::adjacentEditableOrdinal(bool forward) const {
if (_activeTextOrdinal < 0) {
return std::nullopt;
@@ -6694,12 +7457,9 @@ Block State::MakeListBlock(ListKind kind, TaskState taskState) {
auto block = Block();
block.kind = BlockKind::List;
block.listKind = kind;
block.listItems.reserve(3);
for (auto i = 0; i != 3; ++i) {
auto item = ListItem();
item.taskState = taskState;
block.listItems.push_back(std::move(item));
}
auto item = ListItem();
item.taskState = taskState;
block.listItems.push_back(std::move(item));
return block;
}
@@ -328,6 +328,10 @@ public:
int selectionFrom = 0;
int selectionTo = 0;
};
struct StructuralSelectionDropResult {
ApplyResult result = ApplyResult::Failed;
BoundaryTarget destination;
};
[[nodiscard]] DisplayMathEditResult editActiveDisplayMath(
QString source,
bool separateLine);
@@ -353,6 +357,10 @@ public:
const Markdown::PreparedEditSelection &selection,
const ClipboardListItemsData &data,
std::optional<ActiveTextInsertContext> context = std::nullopt);
[[nodiscard]] StructuralSelectionDropResult
moveStructuralSelectionToDropTarget(
const Markdown::PreparedEditSelection &selection,
const Markdown::PreparedEditDropTarget &target);
enum class TextFormattingAction : uchar {
Bold,
Italic,
@@ -366,6 +374,22 @@ public:
int from = 0;
int till = 0;
};
struct TextSelectionDropResult {
ApplyResult result = ApplyResult::Failed;
std::optional<LeafPath> destinationLeaf;
int selectionFrom = 0;
int selectionTo = 0;
};
[[nodiscard]] std::vector<TextNodeSpan> resolveTextSpansForPreparedLeafRange(
const Markdown::PreparedEditLeafSource &source,
int from,
int till) const;
[[nodiscard]] TextSelectionDropResult moveTextSelectionToDropTarget(
const std::vector<TextNodeSpan> &source,
const Markdown::PreparedEditDropTarget &target);
[[nodiscard]] TextSelectionDropResult moveTextSelectionToDropTarget(
const TextNodeSpan &source,
const Markdown::PreparedEditDropTarget &target);
[[nodiscard]] ApplyResult applyFormattingToTextSpans(
const std::vector<TextNodeSpan> &spans,
TextFormattingAction action,
@@ -620,6 +644,14 @@ private:
[[nodiscard]] bool insertBlocksAfterActiveUnchecked(
std::vector<RichPage::Block> blocks,
std::optional<ActiveTextInsertContext> context = std::nullopt);
[[nodiscard]] bool insertPreparedBlocksAtExplicitPosition(
std::vector<RichPage::Block> blocks,
const BlockContainerPath &container,
int insertAt);
[[nodiscard]] bool insertPreparedListItemsAtExplicitPosition(
std::vector<RichPage::ListItem> items,
const BlockPath &path,
int insertAt);
[[nodiscard]] bool insertBlocksAfterActiveWithContextUnchecked(
std::vector<RichPage::Block> &blocks,
const ActiveTextInsertContext &context);
@@ -656,6 +688,14 @@ private:
const BlockContainerPath &container,
int from,
int count);
[[nodiscard]] BoundaryTarget destinationTargetForInsertedBlocks(
const BlockContainerPath &container,
int from,
int count);
[[nodiscard]] BoundaryTarget destinationTargetForInsertedListItems(
const BlockPath &path,
int from,
int count);
[[nodiscard]] std::optional<int> adjacentEditableOrdinal(
bool forward) const;
void collectBoundarySteps(
@@ -357,6 +357,45 @@ struct TextRange {
&& IndexInRange(step.listItemIndex, range.from, range.till);
}
[[nodiscard]] bool PreparedContainerNestedInSelection(
const PreparedBlockContainerPath &container,
const PreparedSelection &selection) {
const auto marker = PreparedBlockPath{
.container = container,
.index = 0,
};
switch (selection.kind) {
case PreparedSelectionKind::Blocks:
return (container.steps.size() > selection.blocks.container.steps.size())
&& PreparedPathInBlockRange(marker, selection.blocks);
case PreparedSelectionKind::ListItems:
return (container.steps.size()
> selection.listItems.block.container.steps.size())
&& PreparedPathInListItemRange(marker, selection.listItems);
case PreparedSelectionKind::TableRows:
case PreparedSelectionKind::TableCells:
case PreparedSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] bool PreparedBlockPathInSelection(
const PreparedBlockPath &path,
const PreparedSelection &selection) {
switch (selection.kind) {
case PreparedSelectionKind::Blocks:
return PreparedPathInBlockRange(path, selection.blocks);
case PreparedSelectionKind::ListItems:
return PreparedPathInListItemRange(path, selection.listItems);
case PreparedSelectionKind::TableRows:
case PreparedSelectionKind::TableCells:
case PreparedSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] const std::vector<RichPage::Block> *BlockContainer(
const RichPage &page,
const StateBlockContainerPath &path) {
@@ -1217,13 +1256,17 @@ using PreparedEditBlockSource = Markdown::PreparedEditBlockSource;
using PreparedEditHit = Markdown::PreparedEditHit;
using PreparedEditHitKind = Markdown::PreparedEditHitKind;
using PreparedEditLeafKind = Markdown::PreparedEditLeafKind;
using PreparedEditDropTarget = Markdown::PreparedEditDropTarget;
using PreparedEditBlockDropTarget = Markdown::PreparedEditBlockDropTarget;
using PreparedEditLeafSource = Markdown::PreparedEditLeafSource;
using PreparedEditListItemDropTarget = Markdown::PreparedEditListItemDropTarget;
using PreparedEditListItemSource = Markdown::PreparedEditListItemSource;
using PreparedEditSelection = Markdown::PreparedEditSelection;
using PreparedEditSelectionKind = Markdown::PreparedEditSelectionKind;
using PreparedEditTableCellRange = Markdown::PreparedEditTableCellRange;
using PreparedEditTableCellSource = Markdown::PreparedEditTableCellSource;
using PreparedEditTableRowSource = Markdown::PreparedEditTableRowSource;
using PreparedEditTextDropTarget = Markdown::PreparedEditTextDropTarget;
using ApplyResult = State::ApplyResult;
using PreparedMutationKind = State::PreparedMutationKind;
@@ -4496,7 +4539,12 @@ void Widget::mouseMoveEvent(QMouseEvent *e) {
}
_articleSelectionDrag.dragStarted = true;
}
updateArticleSelection(articlePoint, hit, editHit);
if (_articleSelectionDrag.operation
== ArticleSelectionOperation::DragSelection) {
updateArticleDropTarget(articlePoint);
} else {
updateArticleSelection(articlePoint, hit, editHit);
}
e->accept();
}
@@ -4530,6 +4578,57 @@ void Widget::mousePressEvent(QMouseEvent *e) {
Ui::Text::StateRequest::Flag::LookupSymbol);
const auto editHit = _article->editHitTest(articlePoint);
const auto startedBelow = (articlePoint.y() >= _articleHeight);
const auto pressedSelectedText = _article->selectionContains(
_selection,
&_selectionEndpoints,
hit);
const auto pressedSelectedStructuralOwner = [&] {
if (_structuralSelection.empty()) {
return false;
}
const auto owner = StructuralOwnerFromHit(editHit);
if (!owner.valid()) {
return false;
}
switch (_structuralSelection.kind) {
case PreparedEditSelectionKind::Blocks:
if (const auto path = BlockPathFromOwner(owner)) {
return PreparedPathInBlockRange(
*path,
_structuralSelection.blocks);
}
return false;
case PreparedEditSelectionKind::ListItems:
if (const auto listItem = ListItemFromOwner(owner)) {
return SamePreparedEditBlockPath(
listItem->block,
_structuralSelection.listItems.block)
&& IndexInRange(
listItem->listItemIndex,
_structuralSelection.listItems.from,
_structuralSelection.listItems.till);
}
if (const auto path = BlockPathFromOwner(owner)) {
return PreparedPathInListItemRange(
*path,
_structuralSelection.listItems);
}
return false;
case PreparedEditSelectionKind::None:
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
return false;
}
return false;
}();
if ((pressedSelectedText || pressedSelectedStructuralOwner)
&& startSelectionDragFromExistingState(
articlePoint,
e->globalPos(),
editHit)) {
e->accept();
return;
}
if (hit.codeHeaderCopy) {
startArticleSelection(articlePoint, e->globalPos(), hit, editHit);
e->accept();
@@ -4749,6 +4848,7 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
const auto fromField = _articleSelectionDrag.fromField;
const auto pendingCodeHeader = _articleSelectionDrag.codeHeader;
const auto startedBelow = _articleSelectionDrag.startedBelow;
const auto operation = _articleSelectionDrag.operation;
const auto clickLike = !_articleSelectionDrag.dragStarted
&& ((e->globalPos()
- _articleSelectionDrag.globalPressPoint).manhattanLength()
@@ -4759,7 +4859,11 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
|| (!pendingCodeHeader
&& (!startedBelow || articlePoint.y() < _articleHeight)));
if (updateOnRelease) {
updateArticleSelection(articlePoint, hit, editHit);
if (operation == ArticleSelectionOperation::DragSelection) {
updateArticleDropTarget(articlePoint);
} else {
updateArticleSelection(articlePoint, hit, editHit);
}
}
if (clickLike) {
if (activateGroupedMediaLinkFromHit(editHit, hit, e->button())) {
@@ -4781,6 +4885,20 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) {
update();
}
}
if (!clickLike
&& (operation == ArticleSelectionOperation::DragSelection)) {
if (_articleSelectionDrag.dropTarget) {
if (_articleSelectionDrag.mode == DragSelectionMode::Structural) {
static_cast<void>(applyStructuralSelectionDrop());
} else if (_articleSelectionDrag.mode
== DragSelectionMode::Text) {
static_cast<void>(applyInlineSelectionDrop());
}
}
clearArticleDropTarget();
e->accept();
return;
}
if (!clickLike && hasStructuralSelection()) {
commitVisibleInlineField();
e->accept();
@@ -4891,10 +5009,19 @@ void Widget::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
p.setTextPalette(st::inTextPalette);
const auto topLeft = articleTopLeft();
p.save();
p.translate(topLeft);
_article->paint(
p,
textPaintContext(e->rect().translated(-topLeft.x(), -topLeft.y())));
p.restore();
if (!_articleSelectionDrag.indicatorRect.isEmpty()) {
auto color = st::windowActiveTextFg->c;
color.setAlphaF(color.alphaF() * 0.7);
auto rect = _articleSelectionDrag.indicatorRect.translated(topLeft);
rect.setHeight(std::max(rect.height(), st::lineWidth));
p.fillRect(rect, color);
}
}
void Widget::resizeEvent(QResizeEvent *e) {
@@ -7398,6 +7525,7 @@ void Widget::startArticleSelection(
.anchorHit = editHit,
.textSegment = -1,
.textOffset = 0,
.operation = ArticleSelectionOperation::GrowSelection,
.mode = DragSelectionMode::None,
};
if (!isTextHit) {
@@ -7428,11 +7556,114 @@ void Widget::startArticleSelection(
update();
}
bool Widget::startSelectionDragFromExistingState(
QPoint pressPoint,
QPoint globalPressPoint,
const PreparedEditHit &editHit,
bool fromField) {
auto drag = ArticleSelectionDrag{
.active = true,
.fromField = fromField,
.startedBelow = false,
.codeHeader = false,
.pressPoint = pressPoint,
.globalPressPoint = globalPressPoint,
.anchorHit = editHit,
.textSegment = -1,
.textOffset = 0,
.operation = ArticleSelectionOperation::DragSelection,
.mode = DragSelectionMode::None,
};
if (fromField) {
if (_settingField
|| _field->isHidden()
|| (_activeSegmentIndex < 0)
|| (_state->activeFieldMode() != State::FieldMode::Rich)) {
return false;
}
const auto sourceLeaf = _state->activeLeafPath();
const auto preparedSource = _state->activePreparedLeafSource();
if (!sourceLeaf || !preparedSource) {
return false;
}
const auto full = ConvertEditorTagsToRichText(
_field->getTextWithAppliedMarkdown());
const auto cursor = _field->textCursor();
if (!cursor.hasSelection()) {
return false;
}
const auto length = int(full.text.size());
auto from = richOffsetForFieldOffset(full, cursor.selectionStart());
auto till = richOffsetForFieldOffset(full, cursor.selectionEnd());
from = std::clamp(from, 0, length);
till = std::clamp(till, from, length);
if (from >= till) {
return false;
}
drag.textSegment = _activeSegmentIndex;
drag.textOffset = std::clamp(
cursor.position(),
0,
int(_field->getLastText().size()));
drag.mode = DragSelectionMode::Text;
drag.inlineSource = TextNodeSpan{
.leaf = *sourceLeaf,
.from = from,
.till = till,
};
drag.sourceLeaf = *preparedSource;
drag.sourceSegment = _activeSegmentIndex;
drag.sourceFrom = from;
drag.sourceTo = till;
_articleSelectionDrag = std::move(drag);
return true;
}
if (!_structuralSelection.empty()
&& ((_structuralSelection.kind == PreparedEditSelectionKind::Blocks)
|| (_structuralSelection.kind
== PreparedEditSelectionKind::ListItems))) {
drag.mode = DragSelectionMode::Structural;
drag.structuralSource = _structuralSelection;
_articleSelectionDrag = std::move(drag);
return true;
}
const auto selection = NormalizeSelection(_selection);
if (selection.empty()
|| !_selectionEndpoints.from.valid()
|| !_selectionEndpoints.to.valid()
|| (selection.from.segment != selection.to.segment)
|| !_article->segmentIsText(selection.from.segment)
|| !editHit.leaf) {
return false;
}
const auto ordinal = editableOrdinalForSegment(selection.from.segment);
const auto &nodes = _state->textNodes();
if ((ordinal < 0) || (ordinal >= int(nodes.size()))) {
return false;
}
drag.textSegment = selection.from.segment;
drag.textOffset = selection.from.offset;
drag.mode = DragSelectionMode::Text;
drag.inlineSource = TextNodeSpan{
.leaf = nodes[ordinal].leaf,
.from = selection.from.offset,
.till = selection.to.offset,
};
drag.sourceLeaf = *editHit.leaf;
drag.sourceSegment = selection.from.segment;
drag.sourceFrom = selection.from.offset;
drag.sourceTo = selection.to.offset;
_articleSelectionDrag = std::move(drag);
return true;
}
void Widget::updateArticleSelection(
QPoint articlePoint,
const Markdown::MarkdownArticleHitTestResult &hit,
const PreparedEditHit &editHit) {
if (!_articleSelectionDrag.active) {
if (!_articleSelectionDrag.active
|| (_articleSelectionDrag.operation
!= ArticleSelectionOperation::GrowSelection)) {
return;
}
const auto dragSegment = _articleSelectionDrag.textSegment;
@@ -7578,8 +7809,319 @@ void Widget::updateArticleSelection(
}
}
void Widget::updateArticleDropTarget(QPoint articlePoint) {
if (!_articleSelectionDrag.active
|| (_articleSelectionDrag.operation
!= ArticleSelectionOperation::DragSelection)) {
clearArticleDropTarget();
return;
}
const auto structuralSource = _articleSelectionDrag.structuralSource;
auto location = (_articleSelectionDrag.mode == DragSelectionMode::Structural
&& structuralSource)
? _article->editStructuralDropTarget(articlePoint, *structuralSource)
: _article->editDropTarget(articlePoint);
auto supported = false;
if (location.valid()) {
const auto &target = *location.target;
switch (_articleSelectionDrag.mode) {
case DragSelectionMode::Structural:
if (const auto source = structuralSource) {
if (const auto block = std::get_if<PreparedEditBlockDropTarget>(
&target)) {
supported = (source->kind
== PreparedEditSelectionKind::Blocks);
if (supported
&& (block->container == source->blocks.container)
&& (block->insertIndex >= source->blocks.from)
&& (block->insertIndex <= source->blocks.till)) {
supported = false;
} else if (supported
&& PreparedContainerNestedInSelection(
block->container,
*source)) {
supported = false;
}
} else if (const auto list
= std::get_if<PreparedEditListItemDropTarget>(&target)) {
supported = (source->kind
== PreparedEditSelectionKind::ListItems);
if (supported
&& SamePreparedEditBlockPath(
list->block,
source->listItems.block)
&& (list->insertIndex >= source->listItems.from)
&& (list->insertIndex <= source->listItems.till)) {
supported = false;
} else if (supported
&& PreparedBlockPathInSelection(
list->block,
*source)) {
supported = false;
}
}
}
break;
case DragSelectionMode::Text:
if (const auto text = std::get_if<PreparedEditTextDropTarget>(
&target)) {
supported = (text->leaf.kind != PreparedEditLeafKind::MathFormula);
if (supported
&& _articleSelectionDrag.sourceLeaf
&& (*_articleSelectionDrag.sourceLeaf == text->leaf)
&& (text->offset >= _articleSelectionDrag.sourceFrom)
&& (text->offset <= _articleSelectionDrag.sourceTo)) {
supported = false;
}
} else {
supported = std::holds_alternative<PreparedEditBlockDropTarget>(
target);
}
break;
case DragSelectionMode::None:
break;
}
}
if (!supported) {
location = {};
}
const auto oldRect = _articleSelectionDrag.indicatorRect;
_articleSelectionDrag.dropTarget = location.valid()
? location.target
: std::nullopt;
_articleSelectionDrag.indicatorRect = location.valid()
? location.indicatorRect
: QRect();
if (oldRect != _articleSelectionDrag.indicatorRect) {
update();
}
}
void Widget::clearArticleDropTarget() {
const auto oldRect = _articleSelectionDrag.indicatorRect;
_articleSelectionDrag.dropTarget = std::nullopt;
_articleSelectionDrag.indicatorRect = QRect();
if (!oldRect.isEmpty()) {
update();
}
}
void Widget::finishArticleSelection() {
const auto repaint = !_articleSelectionDrag.indicatorRect.isEmpty();
_articleSelectionDrag = {};
if (repaint) {
update();
}
}
bool Widget::applyStructuralSelectionDrop() {
if (!_articleSelectionDrag.structuralSource
|| !_articleSelectionDrag.dropTarget) {
return false;
}
const auto clearOverlay = gsl::finally([&] {
clearArticleDropTarget();
});
const auto selection = *_articleSelectionDrag.structuralSource;
const auto target = *_articleSelectionDrag.dropTarget;
auto applied = false;
recordMutationTransaction([&] {
const auto hadVisibleField = !_field->isHidden();
const auto source = hadVisibleField
? _state->activePreparedLeafSource()
: std::optional<PreparedEditLeafSource>();
auto committed = ApplyResult::Unchanged;
if (hadVisibleField) {
committed = commitInlineField();
if (committed == ApplyResult::Failed) {
return MutationTransactionResult{
.committed = committed,
.failed = true,
};
}
}
_pendingOrdinal = -1;
_pendingCursorOffset = 0;
hideInlineField();
clearInlineFieldEditSession();
const auto moved = _state->moveStructuralSelectionToDropTarget(
selection,
target);
if (moved.result == ApplyResult::Failed) {
showLastLimitToast();
if (hadVisibleField) {
refreshAfterInlineFieldCommit(committed, source);
}
return MutationTransactionResult{
.committed = committed,
.changed = (committed == ApplyResult::Changed),
};
} else if (moved.result == ApplyResult::Unchanged) {
if (hadVisibleField) {
refreshAfterInlineFieldCommit(committed, source);
}
return MutationTransactionResult{
.committed = committed,
.changed = (committed == ApplyResult::Changed),
};
}
applied = true;
refreshPreparedContent();
switch (moved.destination.action) {
case State::BoundaryTarget::Action::StructuralSelection:
_boundarySelectionOrigin = std::nullopt;
_selection = {};
_selectionEndpoints = {};
setStructuralSelection(moved.destination.structuralSelection);
update();
break;
case State::BoundaryTarget::Action::Text:
activateTextOrdinal(moved.destination.textOrdinal, 0);
break;
case State::BoundaryTarget::Action::None:
case State::BoundaryTarget::Action::RemoveActiveOwner: {
const auto ordinal = _state->activeTextOrdinal();
if (ordinal >= 0 && ordinal < _state->textNodeCount()) {
activateTextOrdinal(ordinal, 0);
} else {
activateInitialNode();
}
} break;
}
return MutationTransactionResult{
.committed = committed,
.changed = true,
};
});
return applied;
}
bool Widget::applyInlineSelectionDrop() {
if (!_articleSelectionDrag.inlineSource
|| !_articleSelectionDrag.dropTarget) {
return false;
}
const auto clearOverlay = gsl::finally([&] {
clearArticleDropTarget();
});
const auto target = *_articleSelectionDrag.dropTarget;
auto applied = false;
recordMutationTransaction([&] {
const auto restoreField = !_field->isHidden();
const auto restoreLeaf = restoreField
? _fieldLeaf
: std::optional<State::LeafPath>();
const auto restoreStyleKey = restoreField
? _activeFieldStyleKey
: std::optional<InlineFieldStyleKey>();
const auto restoreMode = _fieldMode;
const auto restoreSelection = restoreField
? captureHistoryViewState().leafSelection
: std::optional<HistoryLeafSelection>();
auto committed = ApplyResult::Unchanged;
if (restoreField) {
committed = commitInlineField();
if (committed == ApplyResult::Failed) {
return MutationTransactionResult{
.committed = committed,
.failed = true,
};
}
_pendingOrdinal = -1;
_pendingCursorOffset = 0;
hideInlineField();
clearInlineFieldEditSession(true);
}
const auto sourceSpans = _articleSelectionDrag.fromField
? (_articleSelectionDrag.sourceLeaf
? _state->resolveTextSpansForPreparedLeafRange(
*_articleSelectionDrag.sourceLeaf,
_articleSelectionDrag.sourceFrom,
_articleSelectionDrag.sourceTo)
: std::vector<TextNodeSpan>())
: std::vector<TextNodeSpan>{ *_articleSelectionDrag.inlineSource };
if (sourceSpans.empty()) {
return MutationTransactionResult{
.committed = committed,
.changed = (committed == ApplyResult::Changed),
};
}
auto restore = restoreField;
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 moved = _state->moveTextSelectionToDropTarget(
sourceSpans,
target);
if (moved.result == ApplyResult::Failed) {
showLastLimitToast();
return MutationTransactionResult{
.committed = committed,
.changed = (committed == ApplyResult::Changed),
};
} else if (moved.result == ApplyResult::Unchanged) {
return MutationTransactionResult{
.committed = committed,
.changed = (committed == ApplyResult::Changed),
};
}
restore = false;
applied = true;
refreshPreparedContent();
const auto ordinal = moved.destinationLeaf
? _state->textOrdinalForLeafPath(*moved.destinationLeaf)
: _state->activeTextOrdinal();
if (ordinal >= 0) {
activateTextOrdinal(
ordinal,
moved.selectionFrom,
moved.selectionTo);
} else {
activateInitialNode();
}
return MutationTransactionResult{
.committed = committed,
.changed = true,
};
});
return applied;
}
bool Widget::handleStructuralSelectionKey(QKeyEvent *e) {
@@ -7692,13 +8234,29 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
if (!anchorHit.valid()) {
return false;
}
clearTextSelection();
clearStructuralSelection();
const auto globalPoint = mouse->globalPos();
const auto articlePoint = mapFromGlobal(globalPoint)
- articleTopLeft();
const auto cursor = _field->textCursor();
const auto raw = _field->rawTextEdit();
const auto pressCursor = raw->cursorForPosition(
raw->viewport()->mapFromGlobal(globalPoint));
const auto pressingCurrentSelection
= (_state->activeFieldMode() == State::FieldMode::Rich)
&& cursor.hasSelection()
&& (pressCursor.position() >= cursor.selectionStart())
&& (pressCursor.position() < cursor.selectionEnd());
_trackingPointerPress = true;
if (pressingCurrentSelection
&& startSelectionDragFromExistingState(
articlePoint,
globalPoint,
anchorHit,
true)) {
return false;
}
clearTextSelection();
clearStructuralSelection();
_articleSelectionDrag = {
.active = true,
.fromField = true,
@@ -7712,6 +8270,7 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
cursor.position(),
0,
int(_field->getLastText().size())),
.operation = ArticleSelectionOperation::GrowSelection,
.mode = DragSelectionMode::Text,
};
return false;
@@ -7730,6 +8289,7 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
const auto globalPoint = mouse->globalPos();
const auto articlePoint = mapFromGlobal(globalPoint) - articleTopLeft();
const auto operation = _articleSelectionDrag.operation;
const auto movedFarEnough = (globalPoint
- _articleSelectionDrag.globalPressPoint).manhattanLength()
>= QApplication::startDragDistance();
@@ -7770,11 +8330,13 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
}
};
if (insideActiveField || originalSegmentHit || originalMathFormulaHit) {
if (_articleSelectionDrag.mode == DragSelectionMode::Structural) {
if ((operation == ArticleSelectionOperation::GrowSelection)
&& (_articleSelectionDrag.mode == DragSelectionMode::Structural)) {
clearArticleSelection();
_articleSelectionDrag.mode = DragSelectionMode::Text;
}
if (type == QEvent::MouseButtonRelease) {
clearArticleDropTarget();
finishArticleSelection();
_trackingPointerPress = false;
}
@@ -7782,12 +8344,32 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
}
if (clickLike) {
clearArticleDropTarget();
finishArticleSelection();
_trackingPointerPress = false;
return false;
}
updateArticleSelection(articlePoint, hit, editHit);
if (operation == ArticleSelectionOperation::DragSelection) {
updateArticleDropTarget(articlePoint);
} else {
updateArticleSelection(articlePoint, hit, editHit);
}
if (type == QEvent::MouseButtonRelease) {
if (operation == ArticleSelectionOperation::DragSelection) {
if (_articleSelectionDrag.dropTarget) {
if (_articleSelectionDrag.mode == DragSelectionMode::Structural) {
static_cast<void>(applyStructuralSelectionDrop());
} else if (_articleSelectionDrag.mode
== DragSelectionMode::Text) {
static_cast<void>(applyInlineSelectionDrop());
}
}
clearArticleDropTarget();
finishArticleSelection();
_trackingPointerPress = false;
mouse->accept();
return true;
}
if (hasStructuralSelection()) {
const auto committed = recordMutationTransaction([&] {
return commitInlineField();
@@ -7810,7 +8392,8 @@ bool Widget::handleFieldMouseEvent(QEvent *event) {
_trackingPointerPress = false;
return false;
}
if (_articleSelectionDrag.mode == DragSelectionMode::Structural) {
if ((operation == ArticleSelectionOperation::DragSelection)
|| (_articleSelectionDrag.mode == DragSelectionMode::Structural)) {
mouse->accept();
return true;
}
@@ -227,6 +227,12 @@ private:
Structural,
};
enum class ArticleSelectionOperation {
None,
GrowSelection,
DragSelection,
};
struct ArticleSelectionDrag {
bool active = false;
bool fromField = false;
@@ -238,7 +244,17 @@ private:
Markdown::PreparedEditHit anchorHit;
int textSegment = -1;
int textOffset = 0;
ArticleSelectionOperation operation
= ArticleSelectionOperation::None;
DragSelectionMode mode = DragSelectionMode::None;
std::optional<Markdown::PreparedEditSelection> structuralSource;
std::optional<State::TextNodeSpan> inlineSource;
std::optional<Markdown::PreparedEditLeafSource> sourceLeaf;
int sourceSegment = -1;
int sourceFrom = 0;
int sourceTo = 0;
std::optional<Markdown::PreparedEditDropTarget> dropTarget;
QRect indicatorRect;
};
enum class HorizontalScrollDrag {
@@ -550,11 +566,20 @@ private:
const Markdown::PreparedEditHit &editHit,
bool fromField = false,
bool startedBelow = false);
[[nodiscard]] bool startSelectionDragFromExistingState(
QPoint pressPoint,
QPoint globalPressPoint,
const Markdown::PreparedEditHit &editHit,
bool fromField = false);
void updateArticleSelection(
QPoint articlePoint,
const Markdown::MarkdownArticleHitTestResult &hit,
const Markdown::PreparedEditHit &editHit);
void updateArticleDropTarget(QPoint articlePoint);
void clearArticleDropTarget();
void finishArticleSelection();
[[nodiscard]] bool applyStructuralSelectionDrop();
[[nodiscard]] bool applyInlineSelectionDrop();
[[nodiscard]] bool handleStructuralSelectionKey(QKeyEvent *e);
void addFieldBlockFormatActions(not_null<QMenu*> menu);
void handleFieldContextMenuRequest(
@@ -2108,6 +2108,711 @@ void ApplyOwnerContentGeometry(
return EditFallbackHitForBlock(block);
}
[[nodiscard]] std::optional<PreparedEditLeafSource> EditableLeafForSegment(
const SelectableSegment &segment) {
if (segment.cell) {
return segment.cell->editLeaf;
} else if (segment.block && (segment.leaf == &segment.block->leaf)) {
return segment.block->editLeaf;
}
return std::nullopt;
}
[[nodiscard]] PreparedEditBlockContainerPath BlockChildContainer(
const PreparedEditBlockSource &source) {
auto result = source.path.container;
result.steps.push_back({
.kind = PreparedEditBlockContainerKind::BlockChildren,
.blockIndex = source.path.index,
});
return result;
}
[[nodiscard]] std::optional<PreparedEditBlockContainerPath> ContainerPathForChildBlock(
const LaidOutBlock &block) {
if (block.editBlock && ValidBlockPath(block.editBlock->path)) {
return block.editBlock->path.container;
} else if (block.editListItem && ValidBlockPath(block.editListItem->block)) {
return block.editListItem->block.container;
} else if (block.editLeaf && ValidBlockPath(block.editLeaf->block)) {
return block.editLeaf->block.container;
}
return std::nullopt;
}
[[nodiscard]] std::optional<PreparedEditBlockContainerPath> ContainerPathForBlocks(
const std::vector<LaidOutBlock> &blocks) {
for (const auto &block : blocks) {
if (const auto result = ContainerPathForChildBlock(block)) {
return result;
}
}
return std::nullopt;
}
[[nodiscard]] std::optional<PreparedEditBlockContainerPath> BlockChildrenContainerPath(
const LaidOutBlock &block) {
if (block.editBlock && ValidBlockPath(block.editBlock->path)) {
return BlockChildContainer(*block.editBlock);
}
return ContainerPathForBlocks(block.children);
}
[[nodiscard]] std::optional<PreparedEditBlockContainerPath> ListItemChildrenContainerPath(
const LaidOutBlock &block) {
if (block.editListItem && ValidBlockPath(block.editListItem->block)) {
return ListItemChildContainer(*block.editListItem);
}
return ContainerPathForBlocks(block.children);
}
[[nodiscard]] std::optional<PreparedEditBlockPath> ListBlockPath(
const LaidOutBlock &block) {
if (block.editBlock && ValidBlockPath(block.editBlock->path)) {
return block.editBlock->path;
}
for (const auto &child : block.children) {
if (child.editListItem && ValidBlockPath(child.editListItem->block)) {
return child.editListItem->block;
}
}
return std::nullopt;
}
[[nodiscard]] int DistanceToRect(QPoint point, QRect rect) {
if (rect.isEmpty()) {
return std::numeric_limits<int>::max();
}
auto dx = 0;
if (point.x() < rect.left()) {
dx = rect.left() - point.x();
} else if (point.x() > rect.right()) {
dx = point.x() - rect.right();
}
auto dy = 0;
if (point.y() < rect.top()) {
dy = rect.top() - point.y();
} else if (point.y() > rect.bottom()) {
dy = point.y() - rect.bottom();
}
return dx + dy;
}
[[nodiscard]] int GapIndicatorY(
QRect before,
QRect after,
QRect containerRect) {
auto result = 0;
if (!before.isEmpty() && !after.isEmpty()) {
const auto top = before.y() + before.height();
const auto bottom = after.y();
result = (top <= bottom) ? ((top + bottom) / 2) : top;
} else if (!before.isEmpty()) {
result = before.y() + before.height();
} else if (!after.isEmpty()) {
result = after.y();
} else if (!containerRect.isEmpty()) {
result = containerRect.y();
}
if (!containerRect.isEmpty()) {
result = std::clamp(
result,
containerRect.y(),
containerRect.y() + containerRect.height());
}
return result;
}
[[nodiscard]] QRect GapIndicatorRect(
QRect before,
QRect after,
QRect containerRect) {
auto span = QRect();
if (!before.isEmpty()) {
span = before;
}
if (!after.isEmpty()) {
span = span.isEmpty() ? after : span.united(after);
}
if (span.isEmpty()) {
span = containerRect;
}
if (span.isEmpty()) {
return QRect();
}
auto left = span.x();
auto right = span.x() + span.width();
if (!containerRect.isEmpty()) {
const auto containerLeft = containerRect.x();
const auto containerRight = containerRect.x() + containerRect.width();
left = std::clamp(left, containerLeft, containerRight);
right = std::clamp(right, containerLeft, containerRight);
if (right <= left) {
left = containerLeft;
right = containerRight;
}
}
if (right <= left) {
return QRect();
}
return QRect(
left,
GapIndicatorY(before, after, containerRect),
right - left,
1);
}
[[nodiscard]] QRect ListItemGapSpanRect(const LaidOutBlock &block) {
return block.contentRect.isEmpty() ? block.outer : block.contentRect;
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlock(
const LaidOutBlock &block,
QPoint point);
struct DropGapCandidate {
MarkdownArticleDropLocation location;
int distance = std::numeric_limits<int>::max();
};
[[nodiscard]] bool PreparedEditContainerHasPrefix(
const PreparedEditBlockContainerPath &path,
const PreparedEditBlockContainerPath &prefix) {
if (path.steps.size() < prefix.steps.size()) {
return false;
}
return std::equal(
prefix.steps.begin(),
prefix.steps.end(),
path.steps.begin());
}
[[nodiscard]] bool PreparedEditIndexInRange(int index, int from, int till) {
return (index >= from) && (index < till);
}
[[nodiscard]] bool PreparedEditPathInBlockRange(
const PreparedEditBlockPath &path,
const PreparedEditBlockRange &range) {
if (path.container == range.container) {
return PreparedEditIndexInRange(path.index, range.from, range.till);
}
if (!PreparedEditContainerHasPrefix(path.container, range.container)
|| (path.container.steps.size() <= range.container.steps.size())) {
return false;
}
const auto &step = path.container.steps[range.container.steps.size()];
return PreparedEditIndexInRange(step.blockIndex, range.from, range.till);
}
[[nodiscard]] bool PreparedEditPathInListItemRange(
const PreparedEditBlockPath &path,
const PreparedEditListItemRange &range) {
if (!PreparedEditContainerHasPrefix(path.container, range.block.container)
|| (path.container.steps.size() <= range.block.container.steps.size())) {
return false;
}
const auto &step = path.container.steps[range.block.container.steps.size()];
return (step.kind == PreparedEditBlockContainerKind::ListItemChildren)
&& (step.blockIndex == range.block.index)
&& PreparedEditIndexInRange(step.listItemIndex, range.from, range.till);
}
[[nodiscard]] bool PreparedEditContainerNestedInSelection(
const PreparedEditBlockContainerPath &container,
const PreparedEditSelection &selection) {
const auto marker = PreparedEditBlockPath{
.container = container,
.index = 0,
};
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
return (container.steps.size() > selection.blocks.container.steps.size())
&& PreparedEditPathInBlockRange(marker, selection.blocks);
case PreparedEditSelectionKind::ListItems:
return (container.steps.size()
> selection.listItems.block.container.steps.size())
&& PreparedEditPathInListItemRange(marker, selection.listItems);
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] bool PreparedEditBlockPathInSelection(
const PreparedEditBlockPath &path,
const PreparedEditSelection &selection) {
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
return PreparedEditPathInBlockRange(path, selection.blocks);
case PreparedEditSelectionKind::ListItems:
return PreparedEditPathInListItemRange(path, selection.listItems);
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] bool StructuralDropTargetSupported(
const PreparedEditBlockDropTarget &target,
const PreparedEditSelection &selection) {
if ((selection.kind != PreparedEditSelectionKind::Blocks)
|| selection.blocks.empty()) {
return false;
}
if ((target.container == selection.blocks.container)
&& (target.insertIndex >= selection.blocks.from)
&& (target.insertIndex <= selection.blocks.till)) {
return false;
}
return !PreparedEditContainerNestedInSelection(
target.container,
selection);
}
[[nodiscard]] bool StructuralDropTargetSupported(
const PreparedEditListItemDropTarget &target,
const PreparedEditSelection &selection) {
if ((selection.kind != PreparedEditSelectionKind::ListItems)
|| selection.listItems.empty()) {
return false;
}
if ((target.block == selection.listItems.block)
&& (target.insertIndex >= selection.listItems.from)
&& (target.insertIndex <= selection.listItems.till)) {
return false;
}
return !PreparedEditBlockPathInSelection(target.block, selection);
}
void UniteNonEmptyRect(QRect *result, QRect rect) {
if (!result || rect.isEmpty()) {
return;
}
*result = result->isEmpty() ? rect : result->united(rect);
}
void AddSelectedBlockRects(
QRect *result,
const std::vector<LaidOutBlock> &blocks,
const PreparedEditBlockRange &range) {
for (const auto &block : blocks) {
if (block.editBlock
&& (block.editBlock->path.container == range.container)
&& PreparedEditIndexInRange(
block.editBlock->path.index,
range.from,
range.till)) {
UniteNonEmptyRect(result, block.outer);
}
AddSelectedBlockRects(result, block.children, range);
}
}
[[nodiscard]] bool AddSelectedListItemRects(
QRect *result,
const std::vector<LaidOutBlock> &blocks,
const PreparedEditListItemRange &range) {
for (const auto &block : blocks) {
if (block.kind == PreparedBlockKind::List) {
if (const auto listBlock = ListBlockPath(block);
listBlock && (*listBlock == range.block)) {
const auto count = int(block.children.size());
const auto from = std::clamp(range.from, 0, count);
const auto till = std::clamp(range.till, from, count);
for (auto i = from; i != till; ++i) {
UniteNonEmptyRect(
result,
ListItemGapSpanRect(block.children[i]));
}
return true;
}
}
if (AddSelectedListItemRects(result, block.children, range)) {
return true;
}
}
return false;
}
[[nodiscard]] QRect StructuralSelectionRect(
const std::vector<LaidOutBlock> &blocks,
const PreparedEditSelection &selection) {
auto result = QRect();
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
AddSelectedBlockRects(&result, blocks, selection.blocks);
return result;
case PreparedEditSelectionKind::ListItems:
static_cast<void>(AddSelectedListItemRects(
&result,
blocks,
selection.listItems));
return result;
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return {};
}
return {};
}
[[nodiscard]] bool PointInsideSelectionVerticalSpan(
QPoint point,
const std::vector<LaidOutBlock> &blocks,
const PreparedEditSelection &selection) {
const auto rect = StructuralSelectionRect(blocks, selection);
return !rect.isEmpty()
&& (point.y() >= rect.top())
&& (point.y() <= rect.bottom());
}
template <typename Target>
void ConsiderGapCandidate(
DropGapCandidate *best,
Target target,
QRect before,
QRect after,
QRect containerRect,
QPoint point,
const PreparedEditSelection *selection = nullptr) {
if (!best) {
return;
}
if (selection && !StructuralDropTargetSupported(target, *selection)) {
return;
}
const auto indicatorRect = GapIndicatorRect(before, after, containerRect);
if (indicatorRect.isEmpty()) {
return;
}
const auto distance = DistanceToRect(point, indicatorRect);
if (distance >= best->distance) {
return;
}
best->distance = distance;
best->location.target = PreparedEditDropTarget(std::move(target));
best->location.indicatorRect = indicatorRect;
}
void ConsiderBlockContainerGapCandidates(
DropGapCandidate *best,
const std::vector<LaidOutBlock> &blocks,
QPoint point,
std::optional<PreparedEditBlockContainerPath> container,
QRect containerRect,
const PreparedEditSelection *selection = nullptr) {
if (!container) {
return;
}
const auto count = int(blocks.size());
for (auto i = 0; i != count + 1; ++i) {
ConsiderGapCandidate(
best,
PreparedEditBlockDropTarget{
.container = *container,
.insertIndex = i,
},
(i > 0) ? blocks[i - 1].outer : QRect(),
(i < count) ? blocks[i].outer : QRect(),
containerRect,
point,
selection);
}
}
void ConsiderListItemGapCandidates(
DropGapCandidate *best,
const std::vector<LaidOutBlock> &blocks,
QPoint point,
std::optional<PreparedEditBlockPath> listBlock,
QRect containerRect,
const PreparedEditSelection *selection = nullptr) {
if (!(listBlock && ValidBlockPath(*listBlock))) {
return;
}
const auto count = int(blocks.size());
for (auto i = 0; i != count + 1; ++i) {
ConsiderGapCandidate(
best,
PreparedEditListItemDropTarget{
.block = *listBlock,
.insertIndex = i,
},
(i > 0) ? ListItemGapSpanRect(blocks[i - 1]) : QRect(),
(i < count) ? ListItemGapSpanRect(blocks[i]) : QRect(),
containerRect,
point,
selection);
}
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlockContainer(
const std::vector<LaidOutBlock> &blocks,
QPoint point,
std::optional<PreparedEditBlockContainerPath> container,
QRect containerRect) {
for (const auto &block : blocks) {
if (ContainsPoint(block.outer, point)) {
if (const auto nested = EditDropLocationForBlock(block, point);
nested.valid()) {
return nested;
}
break;
}
}
auto best = DropGapCandidate();
ConsiderBlockContainerGapCandidates(
&best,
blocks,
point,
std::move(container),
containerRect);
return best.location;
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForListItems(
const std::vector<LaidOutBlock> &blocks,
QPoint point,
std::optional<PreparedEditBlockPath> listBlock,
QRect containerRect) {
for (const auto &block : blocks) {
if (ContainsPoint(block.outer, point)) {
if (const auto nested = EditDropLocationForBlock(block, point);
nested.valid()) {
return nested;
}
break;
}
}
auto best = DropGapCandidate();
ConsiderListItemGapCandidates(
&best,
blocks,
point,
std::move(listBlock),
containerRect);
return best.location;
}
void ConsiderStructuralBlockDropTargets(
DropGapCandidate *best,
const std::vector<LaidOutBlock> &blocks,
QPoint point,
std::optional<PreparedEditBlockContainerPath> container,
QRect containerRect,
const PreparedEditSelection &selection) {
ConsiderBlockContainerGapCandidates(
best,
blocks,
point,
std::move(container),
containerRect,
&selection);
for (const auto &block : blocks) {
switch (block.kind) {
case PreparedBlockKind::List:
for (const auto &child : block.children) {
if ((child.kind == PreparedBlockKind::ListItem)
&& ContainsPoint(child.contentRect, point)) {
ConsiderStructuralBlockDropTargets(
best,
child.children,
point,
ListItemChildrenContainerPath(child),
child.contentRect,
selection);
}
}
break;
case PreparedBlockKind::ListItem:
if (ContainsPoint(block.contentRect, point)) {
ConsiderStructuralBlockDropTargets(
best,
block.children,
point,
ListItemChildrenContainerPath(block),
block.contentRect,
selection);
}
break;
case PreparedBlockKind::Quote:
case PreparedBlockKind::Details:
if (ContainsPoint(block.contentRect, point)) {
ConsiderStructuralBlockDropTargets(
best,
block.children,
point,
BlockChildrenContainerPath(block),
block.contentRect,
selection);
}
break;
case PreparedBlockKind::Table:
case PreparedBlockKind::Paragraph:
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia:
case PreparedBlockKind::RelatedArticle:
case PreparedBlockKind::EmbedPost:
case PreparedBlockKind::Placeholder:
break;
}
}
}
void ConsiderStructuralListItemDropTargets(
DropGapCandidate *best,
const std::vector<LaidOutBlock> &blocks,
QPoint point,
const PreparedEditBlockPath &sourceListBlock,
const PreparedEditSelection &selection) {
for (const auto &block : blocks) {
switch (block.kind) {
case PreparedBlockKind::List:
if (const auto listBlock = ListBlockPath(block);
listBlock && (*listBlock == sourceListBlock)) {
ConsiderListItemGapCandidates(
best,
block.children,
point,
sourceListBlock,
block.contentRect.isEmpty()
? block.outer
: block.contentRect,
&selection);
}
for (const auto &child : block.children) {
if (child.kind == PreparedBlockKind::ListItem) {
ConsiderStructuralListItemDropTargets(
best,
child.children,
point,
sourceListBlock,
selection);
}
}
break;
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
case PreparedBlockKind::Details:
ConsiderStructuralListItemDropTargets(
best,
block.children,
point,
sourceListBlock,
selection);
break;
case PreparedBlockKind::Table:
case PreparedBlockKind::Paragraph:
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia:
case PreparedBlockKind::RelatedArticle:
case PreparedBlockKind::EmbedPost:
case PreparedBlockKind::Placeholder:
break;
}
}
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForTableCell(
const LaidOutTableCell &cell,
QPoint point) {
return ContainsPoint(cell.outer, point)
? MarkdownArticleDropLocation()
: MarkdownArticleDropLocation();
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForTableRow(
const LaidOutTableRow &row,
QPoint point) {
for (const auto &cell : row.cells) {
if (ContainsPoint(cell.outer, point)) {
return EditDropLocationForTableCell(cell, point);
}
}
return ContainsPoint(row.outer, point)
? MarkdownArticleDropLocation()
: MarkdownArticleDropLocation();
}
[[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlock(
const LaidOutBlock &block,
QPoint point) {
switch (block.kind) {
case PreparedBlockKind::List:
return EditDropLocationForListItems(
block.children,
point,
ListBlockPath(block),
block.contentRect.isEmpty() ? block.outer : block.contentRect);
case PreparedBlockKind::ListItem:
if (ContainsPoint(block.contentRect, point)) {
return EditDropLocationForBlockContainer(
block.children,
point,
ListItemChildrenContainerPath(block),
block.contentRect);
}
return {};
case PreparedBlockKind::Quote:
case PreparedBlockKind::Details:
if (ContainsPoint(block.contentRect, point)) {
return EditDropLocationForBlockContainer(
block.children,
point,
BlockChildrenContainerPath(block),
block.contentRect);
}
return {};
case PreparedBlockKind::Table:
for (const auto &row : block.tableRows) {
if (ContainsPoint(row.outer, point)) {
return EditDropLocationForTableRow(row, point);
}
}
return {};
case PreparedBlockKind::Paragraph:
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::CodeBlock:
case PreparedBlockKind::Rule:
case PreparedBlockKind::DisplayMath:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia:
case PreparedBlockKind::RelatedArticle:
case PreparedBlockKind::EmbedPost:
case PreparedBlockKind::Placeholder:
return {};
}
return {};
}
[[nodiscard]] bool ToggleDetailsBlock(
std::vector<PreparedBlock> *blocks,
const QString &anchorId) {
@@ -2538,6 +3243,11 @@ public:
Ui::Text::StateRequest::Flags flags) const;
[[nodiscard]] PreparedEditHit editHitTest(QPoint point) const;
[[nodiscard]] MarkdownArticleDropLocation editDropTarget(
QPoint point) const;
[[nodiscard]] MarkdownArticleDropLocation editStructuralDropTarget(
QPoint point,
const PreparedEditSelection &selection) const;
[[nodiscard]] MarkdownArticleEditControlHit editControlHitTest(
QPoint point) const;
@@ -3163,6 +3873,77 @@ PreparedEditHit MarkdownArticle::Impl::editHitTest(QPoint point) const {
return EditHitForBlocks(_blocks, point);
}
MarkdownArticleDropLocation MarkdownArticle::Impl::editDropTarget(
QPoint point) const {
if (const auto result = hitTest(
point,
Ui::Text::StateRequest::Flag::LookupSymbol);
result.valid() && result.direct && !result.codeHeaderCopy) {
if (const auto segment = FindSegment(&_segments, result.segmentIndex)) {
if (const auto leaf = EditableLeafForSegment(*segment)) {
if (leaf->kind != PreparedEditLeafKind::MathFormula) {
auto location = MarkdownArticleDropLocation();
location.target = PreparedEditDropTarget(
PreparedEditTextDropTarget{
.leaf = *leaf,
.offset = selectionOffsetFromHit(
result,
TextSelectType::Letters),
});
return location;
}
}
}
}
return EditDropLocationForBlockContainer(
_blocks,
point,
PreparedEditBlockContainerPath(),
QRect());
}
MarkdownArticleDropLocation MarkdownArticle::Impl::editStructuralDropTarget(
QPoint point,
const PreparedEditSelection &selection) const {
if (PointInsideSelectionVerticalSpan(point, _blocks, selection)) {
return {};
}
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks: {
if (selection.blocks.empty()) {
return {};
}
auto best = DropGapCandidate();
ConsiderStructuralBlockDropTargets(
&best,
_blocks,
point,
PreparedEditBlockContainerPath(),
QRect(),
selection);
return best.location;
}
case PreparedEditSelectionKind::ListItems: {
if (selection.listItems.empty()) {
return {};
}
auto best = DropGapCandidate();
ConsiderStructuralListItemDropTargets(
&best,
_blocks,
point,
selection.listItems.block,
selection);
return best.location;
}
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return {};
}
return {};
}
MarkdownArticleEditControlHit MarkdownArticle::Impl::editControlHitTest(
QPoint point) const {
return EditControlHitForBlocks(_blocks, point);
@@ -4791,6 +5572,17 @@ PreparedEditHit MarkdownArticle::editHitTest(QPoint point) const {
return _impl->editHitTest(point);
}
MarkdownArticleDropLocation MarkdownArticle::editDropTarget(
QPoint point) const {
return _impl->editDropTarget(point);
}
MarkdownArticleDropLocation MarkdownArticle::editStructuralDropTarget(
QPoint point,
const PreparedEditSelection &selection) const {
return _impl->editStructuralDropTarget(point, selection);
}
MarkdownArticleEditControlHit MarkdownArticle::editControlHitTest(
QPoint point) const {
return _impl->editControlHitTest(point);
@@ -261,6 +261,28 @@ inline bool operator!=(
return !(a == b);
}
struct MarkdownArticleDropLocation {
std::optional<PreparedEditDropTarget> target;
QRect indicatorRect;
[[nodiscard]] bool valid() const {
return target.has_value();
}
};
inline bool operator==(
MarkdownArticleDropLocation a,
MarkdownArticleDropLocation b) {
return (a.target == b.target)
&& (a.indicatorRect == b.indicatorRect);
}
inline bool operator!=(
MarkdownArticleDropLocation a,
MarkdownArticleDropLocation b) {
return !(a == b);
}
struct MarkdownArticleTextLeafStyle {
const style::TextStyle *textStyle = nullptr;
style::color textColor;
@@ -316,6 +338,11 @@ public:
QPoint point,
Ui::Text::StateRequest::Flags flags) const;
[[nodiscard]] PreparedEditHit editHitTest(QPoint point) const;
[[nodiscard]] MarkdownArticleDropLocation editDropTarget(
QPoint point) const;
[[nodiscard]] MarkdownArticleDropLocation editStructuralDropTarget(
QPoint point,
const PreparedEditSelection &selection) const;
[[nodiscard]] MarkdownArticleEditControlHit editControlHitTest(
QPoint point) const;
void addTaskMarkerRipple(
@@ -479,6 +479,65 @@ struct PreparedEditHit {
}
};
struct PreparedEditTextDropTarget {
PreparedEditLeafSource leaf;
int offset = 0;
friend inline bool operator==(
const PreparedEditTextDropTarget &a,
const PreparedEditTextDropTarget &b) {
return (a.leaf == b.leaf)
&& (a.offset == b.offset);
}
friend inline bool operator!=(
const PreparedEditTextDropTarget &a,
const PreparedEditTextDropTarget &b) {
return !(a == b);
}
};
struct PreparedEditBlockDropTarget {
PreparedEditBlockContainerPath container;
int insertIndex = -1;
friend inline bool operator==(
const PreparedEditBlockDropTarget &a,
const PreparedEditBlockDropTarget &b) {
return (a.container == b.container)
&& (a.insertIndex == b.insertIndex);
}
friend inline bool operator!=(
const PreparedEditBlockDropTarget &a,
const PreparedEditBlockDropTarget &b) {
return !(a == b);
}
};
struct PreparedEditListItemDropTarget {
PreparedEditBlockPath block;
int insertIndex = -1;
friend inline bool operator==(
const PreparedEditListItemDropTarget &a,
const PreparedEditListItemDropTarget &b) {
return (a.block == b.block)
&& (a.insertIndex == b.insertIndex);
}
friend inline bool operator!=(
const PreparedEditListItemDropTarget &a,
const PreparedEditListItemDropTarget &b) {
return !(a == b);
}
};
using PreparedEditDropTarget = std::variant<
PreparedEditTextDropTarget,
PreparedEditBlockDropTarget,
PreparedEditListItemDropTarget>;
struct PreparedTableCell {
TextWithEntities text;
std::vector<PreparedLink> links;
@@ -228,7 +228,7 @@ PreparedList PrepareMediaList(
result.files.back().size = filesize;
} else {
result.filesToProcess.emplace_back(file);
result.files.back().size = filesize;
result.filesToProcess.back().size = filesize;
}
}
PrepareDetailsInParallel(result, previewWidth);