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