From 748bc8236ac5243237e487516f5ebf751ccda56d Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 18 Jun 2026 08:20:34 +0400 Subject: [PATCH] Allow drag-n-drop of blocks selections. --- .../iv/editor/iv_editor_session.cpp | 624 ++++++++++++-- .../SourceFiles/iv/editor/iv_editor_state.cpp | 772 ++++++++++++++++- .../SourceFiles/iv/editor/iv_editor_state.h | 40 + .../iv/editor/iv_editor_widget.cpp | 599 ++++++++++++- .../SourceFiles/iv/editor/iv_editor_widget.h | 25 + .../iv/markdown/iv_markdown_article.cpp | 792 ++++++++++++++++++ .../iv/markdown/iv_markdown_article.h | 27 + .../iv/markdown/iv_markdown_prepare.h | 59 ++ .../storage/storage_media_prepare.cpp | 2 +- 9 files changed, 2855 insertions(+), 85 deletions(-) diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp index 9a66fe0496..cf1c82fabe 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp @@ -183,6 +183,11 @@ void ShowRichMessagesPremiumToast(std::shared_ptr show) { } } +[[nodiscard]] bool IsPhotoVideoRichMessageKind(RichPage::BlockKind kind) { + return (kind == RichPage::BlockKind::Photo) + || (kind == RichPage::BlockKind::Video); +} + void CountRichPageMedia( const std::vector &blocks, int *result) { @@ -221,7 +226,7 @@ template [[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 editor; + AttachmentInsertMode insertMode = AttachmentInsertMode::Normal; + std::optional insertTarget; + std::vector items; + int nextIndex = 0; + }; + struct QueuedPrepare { QPointer editor; PreparedFile file; uint64 batchId = 0; + int order = 0; AttachmentInsertMode insertMode = AttachmentInsertMode::Normal; - std::optional insertTarget; std::optional replaceTarget; }; @@ -994,19 +1021,29 @@ private: _editor = editor; const auto weak = base::make_weak(this); const auto editorPointer = QPointer(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(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 editor, PreparedFile file, uint64 batchId, + int order, AttachmentInsertMode insertMode, - std::optional insertTarget, std::optional 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 editor, PreparedFile file, uint64 batchId, + int order, AttachmentInsertMode insertMode, - std::optional insertTarget, std::optional replaceTarget) { const auto meta = BuildAttachmentMeta(file); const auto weak = base::make_weak(this); @@ -1350,7 +1427,7 @@ private: _attachmentPrepareQueue.addTask( std::make_unique( BuildPrepareTaskArgs(_session, _peer->id, std::move(file)), - [weak, editor, meta, batchId, insertMode, insertTarget, replaceTarget]( + [weak, editor, meta, batchId, order, insertMode, replaceTarget]( std::shared_ptr 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 prepared, uint64 batchId, + int order, AttachmentInsertMode insertMode, - std::optional insertTarget, std::optional 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 editor, AttachmentMeta meta, std::shared_ptr prepared, + uint64 batchId, + int order, AttachmentInsertMode insertMode, - std::optional insertTarget, std::optional 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 createAttachmentUpload( + AttachmentMeta meta, + std::shared_ptr 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 { + 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 &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(); + auto emittedUploadIds = std::vector(); + 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 &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 &uploadIds) { + auto subrun = std::vector(); + 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(); + auto runIndexes = std::vector(); + 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 _submittedPage; std::vector _attachments; std::deque _prepareQueue; + std::vector _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; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index b93f96afd6..5fc55a664e 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -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 BlockIndexInContainer( const LeafPath &leaf, const BlockContainerPath &container) { @@ -1327,6 +1366,45 @@ std::optional State::activePreparedLeafSource() const { return descriptor ? convertPreparedLeafSource(*descriptor) : std::nullopt; } +std::vector 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(); + 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(); +} + 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{ + .apply = false, + .result = result, + }; + } + auto blockTarget = std::optional(); + auto listTarget = std::optional(); + if (const auto block = std::get_if( + &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{ + .apply = false, + .result = result, + }; + } + blockTarget = { + .container = *container, + .insertIndex = block->insertIndex, + }; + } else if (const auto list + = std::get_if(&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{ + .apply = false, + .result = result, + }; + } + listTarget = { + .block = *blockPath, + .insertIndex = list->insertIndex, + }; + } else { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + switch (selection.kind) { + case PreparedEditSelectionKind::Blocks: { + const auto range = candidate.validateBlockRange(selection.blocks); + if (!range || !blockTarget) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + if (blockTarget->container == range->container) { + if (blockTarget->insertIndex >= range->from + && blockTarget->insertIndex <= range->till) { + result.result = ApplyResult::Unchanged; + return CheckedMutationResult{ + .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{ + .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{ + .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{ + .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{ + .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{ + .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{ + .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{ + .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{ + .apply = false, + .result = result, + }; + } + } + } + } else { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + } break; + case PreparedEditSelectionKind::TableRows: + case PreparedEditSelectionKind::TableCells: + case PreparedEditSelectionKind::None: + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + if (!candidate.removeStructuralSelection(selection, true)) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + if (const auto blocks = std::get_if(&*payload)) { + if (!blockTarget) { + return CheckedMutationResult{ + .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{ + .apply = false, + .result = result, + }; + } + candidate.rebuild(); + result.result = ApplyResult::Changed; + result.destination = candidate.destinationTargetForInsertedBlocks( + blockTarget->container, + blockTarget->insertIndex, + count); + return CheckedMutationResult{ + .apply = true, + .result = result, + }; + } + const auto items = std::get_if(&*payload); + if (!items) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + auto listBlock = Block(); + listBlock.kind = BlockKind::List; + listBlock.listKind = items->listKind; + listBlock.listItems = items->items; + auto insertedBlocks = std::vector(); + 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{ + .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{ + .apply = false, + .result = result, + }; + } + candidate.rebuild(); + result.result = ApplyResult::Changed; + result.destination = candidate.destinationTargetForInsertedListItems( + listTarget->block, + listTarget->insertIndex, + count); + return CheckedMutationResult{ + .apply = true, + .result = result, + }; + } + auto container = listTarget->block.container; + auto insertAt = listTarget->block.index; + auto trailingBlocks = std::vector(); + 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( + 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{ + .apply = false, + .result = result, + }; + } + if (!trailingBlocks.empty() + && !candidate.insertPreparedBlocksAtExplicitPosition( + std::move(trailingBlocks), + container, + insertAt + 1)) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + candidate.rebuild(); + result.result = ApplyResult::Changed; + result.destination = candidate.destinationTargetForInsertedBlocks( + container, + insertAt, + 1); + return CheckedMutationResult{ + .apply = true, + .result = result, + }; + } + if (!blockTarget + || !candidate.insertPreparedBlocksAtExplicitPosition( + std::move(insertedBlocks), + blockTarget->container, + blockTarget->insertIndex)) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + candidate.rebuild(); + result.result = ApplyResult::Changed; + result.destination = candidate.destinationTargetForInsertedBlocks( + blockTarget->container, + blockTarget->insertIndex, + 1); + return CheckedMutationResult{ + .apply = true, + .result = result, + }; + }); +} + +State::TextSelectionDropResult State::moveTextSelectionToDropTarget( + const std::vector &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(); + sourceRewrites.reserve(source.size()); + for (const auto &span : source) { + const auto current = candidate.richText(span.leaf); + if (!current) { + return CheckedMutationResult{ + .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{ + .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 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{ + .apply = true, + .result = result, + }; + }; + if (const auto text = std::get_if( + &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{ + .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{ + .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{ + .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{ + .apply = false, + .result = result, + }; + } + return finishAtLeaf( + *destinationLeaf, + insertAt, + insertAt + movedLength); + } + const auto block = std::get_if( + &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{ + .apply = false, + .result = result, + }; + } + auto paragraph = MakeParagraphBlock(); + paragraph.text.text = std::move(moved); + auto blocks = std::vector(); + blocks.push_back(std::move(paragraph)); + if (!applySourceRewrites(std::move(sourceRewrites))) { + return CheckedMutationResult{ + .apply = false, + .result = result, + }; + } + if (!candidate.insertPreparedBlocksAtExplicitPosition( + std::move(blocks), + *container, + block->insertIndex)) { + return CheckedMutationResult{ + .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{ source }, + target); +} + void State::insertHeading1AfterActive() { (void)insertBlockAfterActive({ .type = InsertBlockType::Heading, @@ -5643,6 +6301,39 @@ bool State::insertBlocksAfterActiveUnchecked( return true; } +bool State::insertPreparedBlocksAtExplicitPosition( + std::vector 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 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 *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 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; } diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h index 4ed9d2025d..aad9e84917 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -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 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 destinationLeaf; + int selectionFrom = 0; + int selectionTo = 0; + }; + [[nodiscard]] std::vector resolveTextSpansForPreparedLeafRange( + const Markdown::PreparedEditLeafSource &source, + int from, + int till) const; + [[nodiscard]] TextSelectionDropResult moveTextSelectionToDropTarget( + const std::vector &source, + const Markdown::PreparedEditDropTarget &target); + [[nodiscard]] TextSelectionDropResult moveTextSelectionToDropTarget( + const TextNodeSpan &source, + const Markdown::PreparedEditDropTarget &target); [[nodiscard]] ApplyResult applyFormattingToTextSpans( const std::vector &spans, TextFormattingAction action, @@ -620,6 +644,14 @@ private: [[nodiscard]] bool insertBlocksAfterActiveUnchecked( std::vector blocks, std::optional context = std::nullopt); + [[nodiscard]] bool insertPreparedBlocksAtExplicitPosition( + std::vector blocks, + const BlockContainerPath &container, + int insertAt); + [[nodiscard]] bool insertPreparedListItemsAtExplicitPosition( + std::vector items, + const BlockPath &path, + int insertAt); [[nodiscard]] bool insertBlocksAfterActiveWithContextUnchecked( std::vector &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 adjacentEditableOrdinal( bool forward) const; void collectBoundarySteps( diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index 4890d74f21..ff5673e50c 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -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 *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(applyStructuralSelectionDrop()); + } else if (_articleSelectionDrag.mode + == DragSelectionMode::Text) { + static_cast(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( + &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(&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( + &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( + 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(); + 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(); + const auto restoreStyleKey = restoreField + ? _activeFieldStyleKey + : std::optional(); + const auto restoreMode = _fieldMode; + const auto restoreSelection = restoreField + ? captureHistoryViewState().leafSelection + : std::optional(); + 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()) + : std::vector{ *_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(applyStructuralSelectionDrop()); + } else if (_articleSelectionDrag.mode + == DragSelectionMode::Text) { + static_cast(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; } diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 9ea6e06283..62cc91cc69 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -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 structuralSource; + std::optional inlineSource; + std::optional sourceLeaf; + int sourceSegment = -1; + int sourceFrom = 0; + int sourceTo = 0; + std::optional 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 menu); void handleFieldContextMenuRequest( diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp index 56479d1aff..d02c836983 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp @@ -2108,6 +2108,711 @@ void ApplyOwnerContentGeometry( return EditFallbackHitForBlock(block); } +[[nodiscard]] std::optional 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 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 ContainerPathForBlocks( + const std::vector &blocks) { + for (const auto &block : blocks) { + if (const auto result = ContainerPathForChildBlock(block)) { + return result; + } + } + return std::nullopt; +} + +[[nodiscard]] std::optional BlockChildrenContainerPath( + const LaidOutBlock &block) { + if (block.editBlock && ValidBlockPath(block.editBlock->path)) { + return BlockChildContainer(*block.editBlock); + } + return ContainerPathForBlocks(block.children); +} + +[[nodiscard]] std::optional ListItemChildrenContainerPath( + const LaidOutBlock &block) { + if (block.editListItem && ValidBlockPath(block.editListItem->block)) { + return ListItemChildContainer(*block.editListItem); + } + return ContainerPathForBlocks(block.children); +} + +[[nodiscard]] std::optional 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::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::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 &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 &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 &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(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 &blocks, + const PreparedEditSelection &selection) { + const auto rect = StructuralSelectionRect(blocks, selection); + return !rect.isEmpty() + && (point.y() >= rect.top()) + && (point.y() <= rect.bottom()); +} + +template +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 &blocks, + QPoint point, + std::optional 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 &blocks, + QPoint point, + std::optional 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 &blocks, + QPoint point, + std::optional 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 &blocks, + QPoint point, + std::optional 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 &blocks, + QPoint point, + std::optional 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 &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 *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); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h index dee8567380..43db4cb747 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h @@ -261,6 +261,28 @@ inline bool operator!=( return !(a == b); } +struct MarkdownArticleDropLocation { + std::optional 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( diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h index 2ed94638af..2ff1ce5a90 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare.h @@ -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 links; diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.cpp b/Telegram/SourceFiles/storage/storage_media_prepare.cpp index dfb03616e3..6e119a172b 100644 --- a/Telegram/SourceFiles/storage/storage_media_prepare.cpp +++ b/Telegram/SourceFiles/storage/storage_media_prepare.cpp @@ -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);