Overlay three-dot and add buttons on rich editor media

This commit is contained in:
John Preston
2026-06-27 08:18:09 +04:00
parent 36a6f83c99
commit 7603a8b0ba
10 changed files with 484 additions and 6 deletions
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Filled / Page / page_collage_add</title>
<g id="Filled-/-Page-/-page_collage_add" stroke="none" fill="none" fill-rule="nonzero">
<path d="M56.8489899,36 C56.8489899,37.6844685 55.4834584,39.05 53.7989899,39.05 L39.05,39.05 L39.05,53.7989899 C39.05,55.4834584 37.6844685,56.8489899 36,56.8489899 C34.3155315,56.8489899 32.95,55.4834584 32.95,53.7989899 L32.95,39.05 L18.2010101,39.05 C16.5165416,39.05 15.1510101,37.6844685 15.1510101,36 C15.1510101,34.3155315 16.5165416,32.95 18.2010101,32.95 L32.95,32.95 L32.95,18.2010101 C32.95,16.5165416 34.3155315,15.1510101 36,15.1510101 C37.6844685,15.1510101 39.05,16.5165416 39.05,18.2010101 L39.05,32.95 L53.7989899,32.95 C55.4834584,32.95 56.8489899,34.3155315 56.8489899,36 Z" id="Shape-Copy" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 985 B

@@ -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,
@@ -86,6 +86,10 @@ struct ShowWindowDescriptor {
Fn<QImage(uint64 /*photoId*/)> requestPhotoEditSource;
Fn<void(not_null<Widget*>, Ui::PreparedList, State::ReplaceTarget)>
replacePhotoWithList;
Fn<MediaUploadState(uint64 /*mediaId*/)> mediaUploadState;
Fn<void(not_null<Widget*>, uint64 /*mediaId*/)> cancelMediaUpload;
Fn<void(not_null<Widget*>, State::BlockPath, QPointer<QWidget>)>
addMediaAndGroupWithBlock;
Fn<void(not_null<Widget*>, QPointer<QWidget>, rpl::producer<>)> requestMap;
Fn<void()> closed;
Fn<void(RichMessageLimitError)> showLimitToast;
@@ -676,6 +676,8 @@ private:
std::optional<PreparedMediaPasteTarget> insertTarget;
std::vector<MediaBatchItem> items;
int nextIndex = 0;
std::optional<State::BlockPath> 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<Widget*> editor,
uint64 mediaId) {
session->cancelMediaUploadByMediaId(mediaId);
},
.addMediaAndGroupWithBlock = [session = shared_from_this()](
not_null<Widget*> editor,
State::BlockPath path,
QPointer<QWidget> parent) {
session->addMediaAndGroupWithBlock(
editor,
std::move(path),
std::move(parent));
},
.requestMap = [session = shared_from_this()](
not_null<Widget*> editor,
QPointer<QWidget> 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<Widget*> editor,
State::BlockPath anchor,
QPointer<QWidget> parent) {
if (!parent) {
return;
}
_editor = editor;
const auto weak = base::make_weak(this);
const auto editorPointer = QPointer<Widget>(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<Widget> 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<Widget> editor,
PreparedList list,
uint64 batchId,
AttachmentInsertMode insertMode = AttachmentInsertMode::Normal,
std::optional<PreparedMediaPasteTarget> insertTarget = std::nullopt,
std::optional<State::ReplaceTarget> replaceTarget = std::nullopt) {
std::optional<State::ReplaceTarget> replaceTarget = std::nullopt,
std::optional<State::BlockPath> groupAnchor = std::nullopt) {
const auto effectiveInsertMode = replaceTarget
? AttachmentInsertMode::ReplaceBlock
: insertMode;
@@ -1692,6 +1801,7 @@ private:
.insertMode = effectiveInsertMode,
.insertTarget = insertTarget,
.items = std::vector<MediaBatchItem>(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 {
@@ -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<BoundaryTarget> *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<const RichPage> &page);
@@ -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 <typename Range>
}
}
[[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<Ui::RadialAnimation>(
[=] { 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<QWidget>(_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<int>();
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);
@@ -26,6 +26,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <optional>
#include <vector>
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<QImage(uint64 /*photoId*/)> requestPhotoEditSource;
Fn<void(not_null<Widget*>, Ui::PreparedList, State::ReplaceTarget)>
replacePhotoWithList;
Fn<MediaUploadState(uint64 /*mediaId*/)> mediaUploadState;
Fn<void(not_null<Widget*>, uint64 /*mediaId*/)> cancelMediaUpload;
Fn<void(not_null<Widget*>, State::BlockPath, QPointer<QWidget>)>
addMediaAndGroupWithBlock;
rpl::producer<> imeCompositionStarts;
};
@@ -120,6 +126,7 @@ public:
void pastePreparedBlocks(
std::vector<RichPage::Block> 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<bool()> 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<QImage(uint64)> _requestPhotoEditSource;
const Fn<void(not_null<Widget*>, Ui::PreparedList, State::ReplaceTarget)>
_replacePhotoWithList;
const Fn<MediaUploadState(uint64)> _mediaUploadState;
const Fn<void(not_null<Widget*>, uint64)> _cancelMediaUpload;
const Fn<void(not_null<Widget*>, State::BlockPath, QPointer<QWidget>)>
_addMediaAndGroupWithBlock;
const not_null<PeerData*> _peer;
const std::shared_ptr<State> _state;
const Fn<void(RichMessageLimitError)> _showLimitToast;
@@ -828,6 +869,9 @@ private:
bool _keyboardStructuralSelectionActive = false;
Markdown::MarkdownArticleEditControlHit _pressedControl;
std::optional<QPoint> _pressedControlPoint;
PressedMediaControl _pressedMediaControl;
std::optional<QPoint> _pressedMediaControlPoint;
std::unique_ptr<Ui::RadialAnimation> _mediaUploadRadial;
HorizontalScrollDrag _horizontalScrollDrag = HorizontalScrollDrag::None;
std::optional<QPoint> _pendingTouchHorizontalScrollPoint;
bool _syncingInlineFieldGeometry = false;
+5
View File
@@ -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 }};
@@ -1843,6 +1843,29 @@ void ApplyOwnerContentGeometry(
return fallback;
}
void CollectMediaBlockGeometries(
std::vector<MarkdownArticleMediaGeometry> *out,
const std::vector<LaidOutBlock> &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<MarkdownArticleMediaGeometry>
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<MarkdownArticleMediaGeometry>
MarkdownArticle::Impl::mediaBlockGeometries() const {
auto result = std::vector<MarkdownArticleMediaGeometry>();
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<MarkdownArticleMediaGeometry>
MarkdownArticle::mediaBlockGeometries() const {
return _impl->mediaBlockGeometries();
}
QRect MarkdownArticle::displayMathEditRect(int segmentIndex) const {
return _impl->displayMathEditRect(segmentIndex);
}
@@ -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<MarkdownArticleMediaGeometry>
mediaBlockGeometries() const;
[[nodiscard]] QRect displayMathEditRect(int segmentIndex) const;
[[nodiscard]] QRect displayMathBlockRect(int segmentIndex) const;
[[nodiscard]] int pullquoteAvailableTextWidthForEditableLeaf(