diff --git a/Telegram/Resources/icons/menu/rich/media_add.svg b/Telegram/Resources/icons/menu/rich/media_add.svg new file mode 100644 index 0000000000..126d093e33 --- /dev/null +++ b/Telegram/Resources/icons/menu/rich/media_add.svg @@ -0,0 +1,7 @@ + + + Filled / Page / page_collage_add + + + + \ No newline at end of file diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp index 975d3f72e6..19f053c26d 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp @@ -1202,6 +1202,10 @@ void WindowHost::Impl::setupWindow(ShowWindowDescriptor &&descriptor) { = std::move(descriptor.requestPhotoEditSource), .replacePhotoWithList = std::move(descriptor.replacePhotoWithList), + .mediaUploadState = std::move(descriptor.mediaUploadState), + .cancelMediaUpload = std::move(descriptor.cancelMediaUpload), + .addMediaAndGroupWithBlock + = std::move(descriptor.addMediaAndGroupWithBlock), .imeCompositionStarts = window->imeCompositionStarts(), }, descriptor.peer, diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.h b/Telegram/SourceFiles/iv/editor/iv_editor_box.h index 19a75fa2e9..0dfead30a5 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_box.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.h @@ -86,6 +86,10 @@ struct ShowWindowDescriptor { Fn requestPhotoEditSource; Fn, Ui::PreparedList, State::ReplaceTarget)> replacePhotoWithList; + Fn mediaUploadState; + Fn, uint64 /*mediaId*/)> cancelMediaUpload; + Fn, State::BlockPath, QPointer)> + addMediaAndGroupWithBlock; Fn, QPointer, rpl::producer<>)> requestMap; Fn closed; Fn showLimitToast; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp index cd59caadbf..4730301a5f 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp @@ -676,6 +676,8 @@ private: std::optional insertTarget; std::vector items; int nextIndex = 0; + std::optional groupAnchor; + int insertedTopLevel = 0; }; struct QueuedPrepare { @@ -1442,6 +1444,24 @@ private: std::move(list), std::move(replaceTarget)); }, + .mediaUploadState = [session = shared_from_this()]( + uint64 mediaId) { + return session->mediaUploadStateForMedia(mediaId); + }, + .cancelMediaUpload = [session = shared_from_this()]( + not_null editor, + uint64 mediaId) { + session->cancelMediaUploadByMediaId(mediaId); + }, + .addMediaAndGroupWithBlock = [session = shared_from_this()]( + not_null editor, + State::BlockPath path, + QPointer parent) { + session->addMediaAndGroupWithBlock( + editor, + std::move(path), + std::move(parent)); + }, .requestMap = [session = shared_from_this()]( not_null editor, QPointer parent, @@ -1645,13 +1665,102 @@ private: return QImage(); } + [[nodiscard]] MediaUploadState mediaUploadStateForMedia(uint64 mediaId) { + for (const auto &attachment : _attachments) { + if (mediaIdMatchesAttachment(mediaId, attachment)) { + const auto uploading + = (attachment.state == AttachmentState::Uploading) + || (attachment.state == AttachmentState::Finalizing); + return { + .uploading = uploading, + .progress = attachment.progress, + }; + } + } + return {}; + } + + void cancelMediaUploadByMediaId(uint64 mediaId) { + for (const auto &attachment : _attachments) { + if (mediaIdMatchesAttachment(mediaId, attachment)) { + eraseAttachment(attachment.uploadId); + return; + } + } + } + + void addMediaAndGroupWithBlock( + not_null editor, + State::BlockPath anchor, + QPointer parent) { + if (!parent) { + return; + } + _editor = editor; + const auto weak = base::make_weak(this); + const auto editorPointer = QPointer(editor.get()); + auto callback = [weak, editorPointer, anchor = std::move(anchor)]( + FileDialog::OpenResult &&result) mutable { + if (const auto session = weak.get()) { + session->applyAddToCollageList( + editorPointer, + std::move(result), + std::move(anchor)); + } + }; + FileDialog::GetOpenPaths( + std::move(parent), + tr::lng_choose_files(tr::now), + FileDialog::PhotoVideoFilesFilter(), + std::move(callback)); + } + + void applyAddToCollageList( + QPointer editor, + FileDialog::OpenResult &&result, + State::BlockPath anchor) { + if (!editor) { + return; + } + auto showError = [=](tr::phrase<> phrase) { + showToast(phrase(tr::now)); + }; + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + [](const PreparedList &) { + return true; + }, + showError, + st::sendMediaPreviewSize, + _session->premium()); + if (!list) { + return; + } + const auto selection = _state->preparedSelectionForBlock(anchor); + auto target = PreparedMediaPasteTarget{ + .blockDrop = Markdown::PreparedEditBlockDropTarget{ + .container = selection.blocks.container, + .insertIndex = anchor.index + 1, + }, + }; + applyPreparedList( + editor, + std::move(*list), + ++_prepareBatchId, + AttachmentInsertMode::ClipboardPaste, + std::move(target), + std::nullopt, + anchor); + } + void applyPreparedList( QPointer editor, PreparedList list, uint64 batchId, AttachmentInsertMode insertMode = AttachmentInsertMode::Normal, std::optional insertTarget = std::nullopt, - std::optional replaceTarget = std::nullopt) { + std::optional replaceTarget = std::nullopt, + std::optional groupAnchor = std::nullopt) { const auto effectiveInsertMode = replaceTarget ? AttachmentInsertMode::ReplaceBlock : insertMode; @@ -1692,6 +1801,7 @@ private: .insertMode = effectiveInsertMode, .insertTarget = insertTarget, .items = std::vector(totalCount), + .groupAnchor = std::move(groupAnchor), }); } auto order = 0; @@ -2844,9 +2954,11 @@ private: if (uploadIds.empty()) { return; } - if (uploadIds.size() == 1) { - if (const auto attachment = findAttachment(uploadIds.front())) { - blocks.push_back(makeAttachmentBlock(*attachment)); + if (batch->groupAnchor || uploadIds.size() == 1) { + for (const auto &uploadId : uploadIds) { + if (const auto attachment = findAttachment(uploadId)) { + blocks.push_back(makeAttachmentBlock(*attachment)); + } } } else { blocks.push_back(makeGroupedAttachmentBlock(uploadIds)); @@ -2956,6 +3068,7 @@ private: batch->nextIndex = cursor; } if (blocks.empty()) { + groupBatchIntoCollageIfFinished(batchId); if (eraseFinishedMediaBatch(batchId)) { maybeContinueDeferredSubmit(); } @@ -2963,6 +3076,7 @@ private: } const auto editor = batch->editor; _editor = editor; + batch->insertedTopLevel += int(blocks.size()); if (batch->insertMode == AttachmentInsertMode::ClipboardPaste && batch->insertTarget) { auto target = std::move(*batch->insertTarget); @@ -2996,11 +3110,33 @@ private: } } requestEditorUpdate(); + groupBatchIntoCollageIfFinished(batchId); if (eraseFinishedMediaBatch(batchId)) { maybeContinueDeferredSubmit(); } } + void groupBatchIntoCollageIfFinished(uint64 batchId) { + const auto batch = findMediaBatch(batchId); + if (!batch || !batch->groupAnchor || !batch->editor) { + return; + } + const auto finished = std::all_of( + batch->items.begin(), + batch->items.end(), + [](const MediaBatchItem &item) { + return (item.state == MediaBatchItemState::Inserted) + || (item.state == MediaBatchItemState::Skipped); + }); + if (!finished) { + return; + } + const auto anchor = *batch->groupAnchor; + const auto insertedCount = batch->insertedTopLevel; + batch->groupAnchor = std::nullopt; + batch->editor->groupBlocksIntoCollage(anchor, insertedCount); + } + [[nodiscard]] bool mediaIdMatchesAttachment( uint64 id, const AttachmentRecord &attachment) const { diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h index 9ddb0cc0bd..60c43bb771 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -488,6 +488,8 @@ public: [[nodiscard]] bool setGroupedMediaIntent( const BlockPath &path, RichPage::GroupedMediaIntent intent); + [[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForBlock( + const BlockPath &path) const; private: struct StructuralBlockRange { @@ -826,8 +828,6 @@ private: const BlockPath &path, int itemIndex, std::vector *steps) const; - [[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForBlock( - const BlockPath &path) const; [[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForListItem( const BlockPath &path, int itemIndex) const; @@ -894,6 +894,11 @@ enum class RequestMediaType : uchar { PhotoVideoAudio, }; +struct MediaUploadState { + bool uploading = false; + float64 progress = 0.; +}; + [[nodiscard]] bool CanEditRichPage(const RichPage &page); [[nodiscard]] bool CanEditRichPage( const std::shared_ptr &page); diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index 9763492167..c17c4efaaf 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/chat_style.h" #include "ui/chat/chat_theme.h" #include "ui/click_handler.h" +#include "ui/effects/radial_animation.h" #include "ui/image/image.h" #include "ui/image/image_location.h" #include "ui/layers/generic_box.h" @@ -57,6 +58,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/palette.h" #include "styles/style_boxes.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" #include "styles/style_iv.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" @@ -1102,6 +1104,18 @@ template } } +[[nodiscard]] uint64 MediaIdForBlock(const RichPage::Block &block) { + switch (block.kind) { + case RichPage::BlockKind::Photo: + return block.photoId; + case RichPage::BlockKind::Video: + case RichPage::BlockKind::Audio: + return block.documentId; + default: + return uint64(0); + } +} + [[nodiscard]] bool IsPhotoVideoBlockKind(RichPage::BlockKind kind) { return (kind == RichPage::BlockKind::Photo) || (kind == RichPage::BlockKind::Video); @@ -2596,6 +2610,9 @@ Widget::Widget( , _applyPreparedMedia(std::move(services.applyPreparedMedia)) , _requestPhotoEditSource(std::move(services.requestPhotoEditSource)) , _replacePhotoWithList(std::move(services.replacePhotoWithList)) +, _mediaUploadState(std::move(services.mediaUploadState)) +, _cancelMediaUpload(std::move(services.cancelMediaUpload)) +, _addMediaAndGroupWithBlock(std::move(services.addMediaAndGroupWithBlock)) , _peer(peer) , _state(std::move(state)) , _showLimitToast(std::move(showLimitToast)) @@ -5838,6 +5855,174 @@ void Widget::editPhotoBlock(State::BlockPath path) { _show->showLayer(std::move(layer), Ui::LayerOption::KeepOther); } +MediaUploadState Widget::mediaUploadStateForBlock( + const State::BlockPath &path) const { + const auto block = BlockFromPath(_state->richPage(), path); + if (!block) { + return {}; + } + const auto mediaId = MediaIdForBlock(*block); + return _mediaUploadState ? _mediaUploadState(mediaId) : MediaUploadState(); +} + +Widget::MediaControlLayout Widget::mediaControlLayout( + QRect mediaRect) const { + const auto d = st::ivEditorMediaCornerSize; + const auto skip = st::ivEditorMediaCornerSkip; + const auto &r = mediaRect; + const auto threeDots = QRect(r.left() + skip, r.top() + skip, d, d); + const auto plus = QRect(r.right() - skip - d + 1, r.top() + skip, d, d); + const auto rs = st::ivEditorMediaUploadRadialSize; + const auto radial = QRect( + r.center().x() - rs / 2, + r.center().y() - rs / 2, + rs, + rs); + return { threeDots, plus, radial }; +} + +void Widget::paintMediaControls(Painter &p, QPoint topLeft) { + auto anyUploading = false; + for (const auto &geo : _article->mediaBlockGeometries()) { + if (geo.grouped || geo.visibleMediaRect.isEmpty()) { + continue; + } + const auto path = _state->convertBlockPath(geo.block); + if (!path) { + continue; + } + const auto block = BlockFromPath(_state->richPage(), *path); + if (!block || !IsSimpleMediaBlockKind(block->kind)) { + continue; + } + const auto layout = mediaControlLayout(geo.visibleMediaRect); + const auto uploadState = mediaUploadStateForBlock(*path); + if (uploadState.uploading) { + anyUploading = true; + if (!_mediaUploadRadial) { + _mediaUploadRadial = std::make_unique( + [=] { update(); }); + } + if (!_mediaUploadRadial->animating()) { + _mediaUploadRadial->start(uploadState.progress); + } else { + _mediaUploadRadial->update( + uploadState.progress, + false, + crl::now()); + } + const auto radial = layout.radial.translated(topLeft); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::roundedBg); + p.drawEllipse(radial); + _mediaUploadRadial->draw( + p, + QRectF(radial), + st::ivEditorMediaUploadRadialWidth, + st::roundedFg); + } else { + const auto threeDots = layout.threeDots.translated(topLeft); + const auto plus = layout.plus.translated(topLeft); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(st::roundedBg); + p.drawEllipse(threeDots); + p.drawEllipse(plus); + st::sendBoxAlbumButtonMediaMore.paintInCenter(p, threeDots); + st::ivEditorMediaAddIcon.paintInCenter(p, plus); + } + } + if (!anyUploading && _mediaUploadRadial && _mediaUploadRadial->animating()) { + _mediaUploadRadial->stop(); + } +} + +Widget::PressedMediaControl Widget::mediaControlHitTest( + QPoint articlePoint) const { + for (const auto &geo : _article->mediaBlockGeometries()) { + if (geo.grouped || geo.visibleMediaRect.isEmpty()) { + continue; + } + const auto path = _state->convertBlockPath(geo.block); + if (!path) { + continue; + } + const auto block = BlockFromPath(_state->richPage(), *path); + if (!block || !IsSimpleMediaBlockKind(block->kind)) { + continue; + } + const auto layout = mediaControlLayout(geo.visibleMediaRect); + if (mediaUploadStateForBlock(*path).uploading) { + if (layout.radial.contains(articlePoint)) { + return { MediaControl::UploadRadial, *path }; + } + } else if (layout.threeDots.contains(articlePoint)) { + return { MediaControl::ThreeDots, *path }; + } else if (layout.plus.contains(articlePoint)) { + return { MediaControl::Plus, *path }; + } else if ((block->kind == RichPage::BlockKind::Photo) + && geo.visibleMediaRect.contains(articlePoint)) { + return { MediaControl::MediaPixels, *path }; + } + } + return {}; +} + +void Widget::addToCollageFromBlock(const State::BlockPath &path) { + if (_addMediaAndGroupWithBlock) { + _addMediaAndGroupWithBlock( + this, + path, + QPointer(_outer.get())); + } +} + +void Widget::groupBlocksIntoCollage( + State::BlockPath anchor, + int insertedCount) { + if (insertedCount < 1) { + return; + } + auto selection = _state->preparedSelectionForBlock(anchor); + selection.blocks.till += insertedCount; + if (!_state->canGroupPhotoVideoBlocks(selection)) { + return; + } + [[maybe_unused]] const auto changed = applyMediaBlockChange([&] { + return _state->groupPhotoVideoBlocks( + selection, + RichPage::GroupedMediaIntent::Collage); + }); +} + +void Widget::cancelMediaUploadForBlock(const State::BlockPath &path) { + const auto block = BlockFromPath(_state->richPage(), path); + if (!block) { + return; + } + const auto mediaId = MediaIdForBlock(*block); + if (_cancelMediaUpload) { + _cancelMediaUpload(this, mediaId); + } + auto target = std::optional(); + const auto changed = applyMediaBlockChange([=, &target] { + const auto current = BlockFromPath(_state->richPage(), path); + if (!current || !IsSimpleMediaBlockKind(current->kind)) { + return false; + } + target = _state->removeBlock(path, true); + return true; + }); + if (!changed) { + return; + } else if (target) { + activateTextOrdinal(*target, 0); + } else { + activateInitialNode(); + } +} + void Widget::touchEvent(QTouchEvent *e) { if (e->type() == QEvent::TouchCancel) { _pendingTouchHorizontalScrollPoint = std::nullopt; @@ -6032,6 +6217,8 @@ void Widget::mousePressEvent(QMouseEvent *e) { _selectScroll.cancel(); _pressedControl = {}; _pressedControlPoint = std::nullopt; + _pressedMediaControl = {}; + _pressedMediaControlPoint = std::nullopt; auto articlePoint = e->pos() - articleTopLeft(); const auto horizontalScrollHit = _article->horizontalScrollHit( articlePoint); @@ -6049,6 +6236,13 @@ void Widget::mousePressEvent(QMouseEvent *e) { e->accept(); return; } + const auto mediaControl = mediaControlHitTest(articlePoint); + if (mediaControl.valid()) { + _pressedMediaControl = mediaControl; + _pressedMediaControlPoint = articlePoint; + e->accept(); + return; + } auto hit = _article->hitTest( articlePoint, Ui::Text::StateRequest::Flag::LookupSymbol); @@ -6233,6 +6427,38 @@ void Widget::mouseReleaseEvent(QMouseEvent *e) { e->accept(); return; } + if (_pressedMediaControl.valid()) { + const auto pressed = _pressedMediaControl; + const auto pressedPoint = _pressedMediaControlPoint; + _pressedMediaControl = {}; + _pressedMediaControlPoint = std::nullopt; + const auto current = mediaControlHitTest(articlePoint); + const auto matched = pressedPoint + && ((articlePoint - *pressedPoint).manhattanLength() + < QApplication::startDragDistance()) + && (current.control == pressed.control) + && (current.path == pressed.path); + if (matched) { + switch (pressed.control) { + case MediaControl::ThreeDots: + showSimpleMediaMenu(pressed.path, e->globalPos()); + break; + case MediaControl::Plus: + addToCollageFromBlock(pressed.path); + break; + case MediaControl::MediaPixels: + editPhotoBlock(pressed.path); + break; + case MediaControl::UploadRadial: + cancelMediaUploadForBlock(pressed.path); + break; + case MediaControl::None: + break; + } + } + e->accept(); + return; + } const auto hit = _article->hitTest( articlePoint, Ui::Text::StateRequest::Flag::LookupSymbol); @@ -6491,6 +6717,7 @@ void Widget::paintEvent(QPaintEvent *e) { p, textPaintContext(e->rect().translated(-topLeft.x(), -topLeft.y()))); p.restore(); + paintMediaControls(p, topLeft); if (!_articleSelectionDrag.indicatorRect.isEmpty()) { auto color = st::windowActiveTextFg->c; color.setAlphaF(color.alphaF() * 0.7); diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 4af8f809a6..63b851c2c9 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include +class Painter; class QEvent; class QContextMenuEvent; class QInputMethodEvent; @@ -42,6 +43,7 @@ class ChatTheme; class InputField; class PopupMenu; class ElasticScroll; +class RadialAnimation; struct PreparedList; } // namespace Ui @@ -84,6 +86,10 @@ struct WidgetServices { Fn requestPhotoEditSource; Fn, Ui::PreparedList, State::ReplaceTarget)> replacePhotoWithList; + Fn mediaUploadState; + Fn, uint64 /*mediaId*/)> cancelMediaUpload; + Fn, State::BlockPath, QPointer)> + addMediaAndGroupWithBlock; rpl::producer<> imeCompositionStarts; }; @@ -120,6 +126,7 @@ public: void pastePreparedBlocks( std::vector blocks, PreparedMediaPasteTarget target); + void groupBlocksIntoCollage(State::BlockPath anchor, int insertedCount); void insertHeading1(); void insertBlockquote(); void insertEmoji(EmojiPtr emoji); @@ -238,6 +245,22 @@ protected: void requestRelayout(QRect articleRect) override; private: + enum class MediaControl : uchar { + None, + ThreeDots, + Plus, + MediaPixels, + UploadRadial, + }; + struct PressedMediaControl { + MediaControl control = MediaControl::None; + State::BlockPath path; + + [[nodiscard]] bool valid() const { + return control != MediaControl::None; + } + }; + struct InlineFieldStyleData { const style::TextStyle *textStyle = nullptr; int lineHeight = 0; @@ -743,6 +766,20 @@ private: [[nodiscard]] bool applyMediaBlockChange(Fn change); void requestReplaceMedia(State::BlockPath path); void editPhotoBlock(State::BlockPath path); + void paintMediaControls(Painter &p, QPoint topLeft); + struct MediaControlLayout { + QRect threeDots; + QRect plus; + QRect radial; + }; + [[nodiscard]] MediaControlLayout mediaControlLayout( + QRect mediaRect) const; + [[nodiscard]] PressedMediaControl mediaControlHitTest( + QPoint articlePoint) const; + void cancelMediaUploadForBlock(const State::BlockPath &path); + void addToCollageFromBlock(const State::BlockPath &path); + [[nodiscard]] MediaUploadState mediaUploadStateForBlock( + const State::BlockPath &path) const; [[nodiscard]] Markdown::PreparedEditSelection structuralSelectionFromHits( const Markdown::PreparedEditHit &anchor, const Markdown::PreparedEditHit &focus) const; @@ -771,6 +808,10 @@ private: const Fn _requestPhotoEditSource; const Fn, Ui::PreparedList, State::ReplaceTarget)> _replacePhotoWithList; + const Fn _mediaUploadState; + const Fn, uint64)> _cancelMediaUpload; + const Fn, State::BlockPath, QPointer)> + _addMediaAndGroupWithBlock; const not_null _peer; const std::shared_ptr _state; const Fn _showLimitToast; @@ -828,6 +869,9 @@ private: bool _keyboardStructuralSelectionActive = false; Markdown::MarkdownArticleEditControlHit _pressedControl; std::optional _pressedControlPoint; + PressedMediaControl _pressedMediaControl; + std::optional _pressedMediaControlPoint; + std::unique_ptr _mediaUploadRadial; HorizontalScrollDrag _horizontalScrollDrag = HorizontalScrollDrag::None; std::optional _pendingTouchHorizontalScrollPoint; bool _syncingInlineFieldGeometry = false; diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index 73f8bcc0bf..f6563b14ea 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -195,6 +195,11 @@ ivEditorToolbarAttachIcon: icon {{ "menu/rich/attach-24x24", menuIconColor }}; ivEditorToolbarDetailsIcon: icon {{ "menu/rich/details-24x24", menuIconColor }}; ivEditorToolbarTableIcon: icon {{ "menu/rich/table-24x24", menuIconColor }}; ivEditorToolbarLocationIcon: icon {{ "menu/rich/location-24x24", menuIconColor }}; +ivEditorMediaCornerSize: 27px; +ivEditorMediaCornerSkip: 6px; +ivEditorMediaAddIcon: icon {{ "menu/rich/media_add-20x20", windowFgActive }}; +ivEditorMediaUploadRadialSize: 44px; +ivEditorMediaUploadRadialWidth: 4px; ivEditorTableAddRowAboveIcon: icon {{ "menu/rich/table_add_row_above-24x24", menuIconColor }}; ivEditorTableAddRowBelowIcon: icon {{ "menu/rich/table_add_row_below-24x24", menuIconColor }}; ivEditorTableAddColumnLeftIcon: icon {{ "menu/rich/table_add_column_left-24x24", menuIconColor }}; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp index 2044dfbc13..23c618bc15 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp @@ -1843,6 +1843,29 @@ void ApplyOwnerContentGeometry( return fallback; } +void CollectMediaBlockGeometries( + std::vector *out, + const std::vector &blocks) { + for (const auto &block : blocks) { + const auto media = (block.kind == PreparedBlockKind::Photo) + || (block.kind == PreparedBlockKind::Video) + || (block.kind == PreparedBlockKind::Audio) + || (block.kind == PreparedBlockKind::Map) + || (block.kind == PreparedBlockKind::GroupedMedia); + if (media && block.editBlock) { + out->push_back({ + .block = *block.editBlock, + .mediaRect = block.mediaRect, + .visibleMediaRect = block.visibleMediaRect, + .grouped = (block.kind == PreparedBlockKind::GroupedMedia), + }); + } + if (!block.children.empty()) { + CollectMediaBlockGeometries(out, block.children); + } + } +} + [[nodiscard]] PreparedEditHit EditFallbackHitForBlock( const LaidOutBlock &block) { if (block.editListItem) { @@ -3259,6 +3282,8 @@ public: Ui::Text::StateRequest::Flags flags) const; [[nodiscard]] PreparedEditHit editHitTest(QPoint point) const; + [[nodiscard]] std::vector + mediaBlockGeometries() const; [[nodiscard]] MarkdownArticleDropLocation editDropTarget( QPoint point) const; [[nodiscard]] MarkdownArticleDropLocation editStructuralDropTarget( @@ -3949,6 +3974,13 @@ PreparedEditHit MarkdownArticle::Impl::editHitTest(QPoint point) const { return EditHitForBlocks(_blocks, point); } +std::vector +MarkdownArticle::Impl::mediaBlockGeometries() const { + auto result = std::vector(); + CollectMediaBlockGeometries(&result, _blocks); + return result; +} + MarkdownArticleDropLocation MarkdownArticle::Impl::editDropTarget( QPoint point) const { if (const auto result = hitTest( @@ -5881,6 +5913,11 @@ QRect MarkdownArticle::segmentRect(int segmentIndex) const { return _impl->segmentRect(segmentIndex); } +std::vector +MarkdownArticle::mediaBlockGeometries() const { + return _impl->mediaBlockGeometries(); +} + QRect MarkdownArticle::displayMathEditRect(int segmentIndex) const { return _impl->displayMathEditRect(segmentIndex); } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h index 1195626400..56089db194 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h @@ -301,6 +301,13 @@ struct MarkdownArticleAnchorExpansion { bool changed = false; }; +struct MarkdownArticleMediaGeometry { + PreparedEditBlockSource block; + QRect mediaRect; + QRect visibleMediaRect; + bool grouped = false; +}; + class MarkdownArticle { public: MarkdownArticle( @@ -386,6 +393,8 @@ public: [[nodiscard]] QRect textSegmentRect(int segmentIndex) const; [[nodiscard]] QRect logicalSegmentRect(int segmentIndex) const; [[nodiscard]] QRect segmentRect(int segmentIndex) const; + [[nodiscard]] std::vector + mediaBlockGeometries() const; [[nodiscard]] QRect displayMathEditRect(int segmentIndex) const; [[nodiscard]] QRect displayMathBlockRect(int segmentIndex) const; [[nodiscard]] int pullquoteAvailableTextWidthForEditableLeaf(