Slightly improved photo editor. (Combined commit.)

This commit is contained in:
23rd
2026-09-16 09:12:40 +03:00
parent dabf2da19f
commit 38a11be3c0
164 changed files with 11909 additions and 1279 deletions

View File

@@ -870,11 +870,25 @@ PRIVATE
dialogs/suggestions/suggestion_userpic_setup.cpp
dialogs/dialogs_widget.cpp
dialogs/dialogs_widget.h
editor/audio_track_timeline.cpp
editor/audio_track_timeline.h
editor/color_picker.cpp
editor/color_picker.h
editor/controllers/controllers.h
editor/controllers/stickers_panel_controller.cpp
editor/controllers/stickers_panel_controller.h
editor/editor_audio_timeline.cpp
editor/editor_audio_timeline.h
editor/editor_keys_legend.cpp
editor/editor_keys_legend.h
editor/editor_link_box.cpp
editor/editor_link_box.h
editor/editor_message_render.cpp
editor/editor_message_render.h
editor/editor_message_source.cpp
editor/editor_message_source.h
editor/editor_message_video.cpp
editor/editor_message_video.h
editor/editor_paint.cpp
editor/editor_paint.h
editor/photo_editor.cpp
@@ -885,8 +899,14 @@ PRIVATE
editor/photo_editor_controls.h
editor/photo_editor_layer_widget.cpp
editor/photo_editor_layer_widget.h
editor/scene/scene_item_message.cpp
editor/scene/scene_item_message.h
editor/scene/scene_item_sticker.cpp
editor/scene/scene_item_sticker.h
editor/scene/scene_item_video.cpp
editor/scene/scene_item_video.h
editor/video/video_clip.cpp
editor/video/video_clip.h
editor/video/video_editor.cpp
editor/video/video_editor.h
editor/video/video_editor_common.cpp
@@ -897,8 +917,14 @@ PRIVATE
editor/video/video_editor_quality.h
editor/video/video_quality_slider.cpp
editor/video/video_quality_slider.h
editor/video/video_segment_player.cpp
editor/video/video_segment_player.h
editor/video/video_timeline.cpp
editor/video/video_timeline.h
editor/video/video_timeline_seeker.cpp
editor/video/video_timeline_seeker.h
editor/video_item_timeline.cpp
editor/video_item_timeline.h
export/export_manager.cpp
export/export_manager.h
export/view/export_view_content.cpp

Binary file not shown.

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
<g fill="white" transform="translate(3 5.5) scale(0.942857) translate(-2.3333 -5)">
<path fill-rule="evenodd" d="M6.6667 5h14.6666c2.3933 0 4.3334 1.9401 4.3334 4.3333v9.3334c0 2.3932-1.9401 4.3333-4.3334 4.3333H6.6667c-2.3933 0-4.3334-1.9401-4.3334-4.3333V9.3333C2.3333 6.9401 4.2734 5 6.6667 5zm0 1.6667c-1.4728 0-2.6667 1.1939-2.6667 2.6666v9.3334c0 1.4727 1.1939 2.6666 2.6667 2.6666h14.6666c1.4728 0 2.6667-1.1939 2.6667-2.6666V9.3333c0-1.4727-1.1939-2.6666-2.6667-2.6666H6.6667z"/>
<rect x="7" y="10" width="2" height="2" rx="0.5"/>
<rect x="10" y="10" width="2" height="2" rx="0.5"/>
<rect x="13" y="10" width="2" height="2" rx="0.5"/>
<rect x="16" y="10" width="2" height="2" rx="0.5"/>
<rect x="19" y="10" width="2" height="2" rx="0.5"/>
<rect x="7" y="13" width="2" height="2" rx="0.5"/>
<rect x="10" y="13" width="2" height="2" rx="0.5"/>
<rect x="13" y="13" width="2" height="2" rx="0.5"/>
<rect x="16" y="13" width="2" height="2" rx="0.5"/>
<rect x="19" y="13" width="2" height="2" rx="0.5"/>
<rect x="10.3333" y="17.3333" width="7.3334" height="1.6667" rx="0.8333"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

View File

@@ -3176,6 +3176,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_attach_failed" = "Failed";
"lng_attach_file" = "File";
"lng_attach_photo" = "Photo";
"lng_attach_audio" = "Audio";
"lng_attach_camera" = "Camera";
"lng_attach_document" = "Document";
"lng_attach_photo_or_video" = "Photo or video";
@@ -5355,6 +5356,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_duration_played" = "{played} / {duration}";
"lng_date_and_duration" = "{date}, {duration}";
"lng_choose_image" = "Choose an image";
"lng_choose_audio" = "Choose an audio file";
"lng_choose_file" = "Choose a file";
"lng_choose_files" = "Choose Files";
"lng_music_attach_choose_from_files" = "Choose from files";
@@ -7954,6 +7956,50 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_photo_editor_shape_arrow" = "Arrow";
"lng_photo_editor_shape_filled" = "Filled";
"lng_photo_editor_audio_remove" = "Remove Audio";
"lng_photo_editor_trim_shortest" = "Trim to shortest";
"lng_photo_editor_keys_tooltip" = "Keyboard shortcuts";
"lng_photo_editor_keys_gesture" = "{modifier}-{gesture}";
"lng_photo_editor_keys_gesture_drag" = "drag";
"lng_photo_editor_keys_gesture_drag_edge" = "drag edge";
"lng_photo_editor_keys_gesture_drag_handle" = "drag handle";
"lng_photo_editor_keys_gesture_click" = "click";
"lng_photo_editor_keys_gesture_wheel" = "wheel";
"lng_photo_editor_keys_wheel" = "Wheel";
"lng_photo_editor_keys_middle_drag" = "Middle drag";
"lng_photo_editor_keys_double_click" = "Double click";
"lng_photo_editor_keys_legend" = "Show these shortcuts";
"lng_photo_editor_keys_crop" = "Crop";
"lng_photo_editor_keys_crop_room" = "Zoom out for room around the photo";
"lng_photo_editor_keys_crop_expand" = "Grow canvas past the photo";
"lng_photo_editor_keys_fixed_zoom" = "Zoom picture inside the frame";
"lng_photo_editor_keys_fixed_zoom_fine" = "Zoom in finer steps";
"lng_photo_editor_keys_fixed_pan" = "Move picture inside the frame";
"lng_photo_editor_keys_canvas" = "Canvas";
"lng_photo_editor_keys_canvas_zoom" = "Zoom canvas at cursor";
"lng_photo_editor_keys_canvas_pan" = "Pan zoomed canvas";
"lng_photo_editor_keys_canvas_paste" = "Paste image, sticker, file or link";
"lng_photo_editor_keys_canvas_undo" = "Undo";
"lng_photo_editor_keys_canvas_redo" = "Redo";
"lng_photo_editor_keys_shapes" = "Shapes";
"lng_photo_editor_keys_shapes_proportions" = "Keep proportions or snap arrow angle";
"lng_photo_editor_keys_shapes_instant" = "Place shape from the menu right away";
"lng_photo_editor_keys_objects" = "Objects";
"lng_photo_editor_keys_objects_snap" = "Snap to canvas guides";
"lng_photo_editor_keys_objects_rotate" = "Rotate only, in 45° steps";
"lng_photo_editor_keys_objects_stretch" = "Stretch shape freely";
"lng_photo_editor_keys_objects_duplicate" = "Duplicate selection";
"lng_photo_editor_keys_objects_flip" = "Flip selection horizontally";
"lng_photo_editor_keys_objects_delete" = "Delete selection";
"lng_photo_editor_keys_objects_text" = "Edit text";
"lng_photo_editor_keys_timeline" = "Timeline";
"lng_photo_editor_keys_timeline_zoom" = "Zoom timeline";
"lng_photo_editor_keys_timeline_scroll" = "Scroll timeline";
"lng_photo_editor_keys_timeline_window" = "Move trimmed range";
"lng_photo_editor_link_customize" = "Customize Link Text";
"lng_voice_speed_slow" = "Slow";
"lng_voice_speed_normal" = "Normal";
"lng_voice_speed_medium" = "Medium";

View File

@@ -30,6 +30,7 @@
<file alias="hours.tgs">../../animations/hours.tgs</file>
<file alias="phone.tgs">../../animations/phone.tgs</file>
<file alias="call_rate.tgs">../../animations/call_rate.tgs</file>
<file alias="sun_outline.tgs">../../animations/sun_outline.tgs</file>
<file alias="chat_link.tgs">../../animations/chat_link.tgs</file>
<file alias="diamond.tgs">../../animations/diamond.tgs</file>
<file alias="collectible_username.tgs">../../animations/collectible_username.tgs</file>
@@ -105,6 +106,7 @@
<file alias="photo_editor_text_align.tgs">../../animations/photo_editor/text_align.tgs</file>
<file alias="photo_editor_eraser.tgs">../../animations/photo_editor/eraser.tgs</file>
<file alias="photo_editor_blur.tgs">../../animations/photo_editor/blur.tgs</file>
<file alias="photo_editor_trim_shortest.tgs">../../animations/photo_editor/trim_shortest.tgs</file>
<file alias="swipe_archive.tgs">../../animations/swipe_action/archive.tgs</file>
<file alias="swipe_unarchive.tgs">../../animations/swipe_action/unarchive.tgs</file>

View File

@@ -1431,6 +1431,8 @@ void SendFilesBox::pushBlock(int from, int till) {
show->session().saveSettings();
}
*openedOnce = true;
const auto videos = (_limits & SendFilesAllow::Videos);
const auto animated = videos || (_limits & SendFilesAllow::Gifs);
Editor::OpenWithPreparedFile(
this,
show,
@@ -1439,7 +1441,8 @@ void SendFilesBox::pushBlock(int from, int till) {
std::move(done),
PhotoSideLimit(true),
QSize(),
true);
animated,
videos);
};
const auto replaceAttachment = [=, show = _show](int index) {
applyBlockChanges();

View File

@@ -154,6 +154,8 @@ EmojiPan {
searchBackTextLeft: pixels;
searchBackTextTop: pixels;
photoButtonIcon: icon;
audioButtonIcon: icon;
linkButtonIcon: icon;
colorAll: IconButton;
colorAllLabel: FlatLabel;
removeSet: IconButton;
@@ -636,7 +638,8 @@ stickersPhotoButtonHeight: 36px;
stickersPhotoButtonRadius: 8px;
stickersPhotoButtonPadding: margins(6px, 0px, 12px, 0px);
stickersPhotoButtonIconSkip: 4px;
stickersPhotoButtonFont: font(21px bold);
stickersPhotoButtonFont: font(14px bold);
stickersPhotoButtonSkip: 10px;
mediaPreviewPhotoSkip: 48px;
emojiPanColorAll: IconButton(stickerPanRemoveSet) {
@@ -831,6 +834,8 @@ defaultEmojiPan: EmojiPan {
searchBackTextLeft: 40px;
searchBackTextTop: 10px;
photoButtonIcon: icon {{ "photo_editor/camera-24x24", windowFg }};
audioButtonIcon: icon {{ "photo_editor/widget_music", windowFg }};
linkButtonIcon: icon {{ "limits/links", windowFg }};
colorAll: emojiPanColorAll;
colorAllLabel: emojiPanColorAllLabel;
removeSet: stickerPanRemoveSet;

View File

@@ -23,6 +23,8 @@ struct ComposeFeatures {
bool stickersSettings : 1 = true;
bool openStickerSets : 1 = true;
bool photoButton : 1 = false;
bool audioButton : 1 = false;
bool linkButton : 1 = false;
bool autocompleteHashtags : 1 = true;
bool autocompleteMentions : 1 = true;
bool autocompleteCommands : 1 = true;

View File

@@ -240,9 +240,8 @@ StickersListWidget::StickersListWidget(
, _megagroupSetAbout(st::columnMinimalWidthThird
- st::emojiScroll.width
- st().headerLeft)
, _photoButtonBg(st::stickersPhotoButtonRadius, st().overBg)
, _photoButtonText(tr::lng_attach_photo(tr::now).toUpper())
, _photoButtonTextWidth(st::stickersPhotoButtonFont->width(_photoButtonText))
, _mediaButtonBg(st::stickersPhotoButtonRadius, st().overBg)
, _mediaButtons(MakeMediaButtons(_features, st()))
, _addText(tr::lng_stickers_featured_add(tr::now))
, _addWidth(st::stickersTrendingAdd.style.font->width(_addText))
, _installedText(tr::lng_stickers_featured_installed(tr::now))
@@ -324,6 +323,14 @@ rpl::producer<> StickersListWidget::photoRequests() const {
return _photoRequests.events();
}
rpl::producer<> StickersListWidget::audioRequests() const {
return _audioRequests.events();
}
rpl::producer<> StickersListWidget::linkRequests() const {
return _linkRequests.events();
}
rpl::producer<> StickersListWidget::scrollUpdated() const {
return _scrollUpdated.events();
}
@@ -509,7 +516,7 @@ template <typename Callback>
bool StickersListWidget::enumerateSections(Callback callback) const {
auto info = SectionInfo();
info.top = _search ? _search->height() : 0;
info.top += photoRowHeight() + searchShortcutsHeight();
info.top += mediaButtonsRowHeight() + searchShortcutsHeight();
const auto &sets = shownSets();
for (auto i = 0; i != sets.size(); ++i) {
auto &set = sets[i];
@@ -976,69 +983,135 @@ void StickersListWidget::startSearchSwapAnimation(
}
int StickersListWidget::searchShortcutsTop() const {
return (_search ? _search->height() : 0) + photoRowHeight();
return (_search ? _search->height() : 0) + mediaButtonsRowHeight();
}
int StickersListWidget::photoRowHeight() const {
if (!_features.photoButton || _isMasks || _section == Section::Search) {
std::vector<StickersListWidget::MediaButton>
StickersListWidget::MakeMediaButtons(
const ComposeFeatures &features,
const style::EmojiPan &st) {
auto result = std::vector<MediaButton>();
const auto add = [&](
MediaButton::Kind kind,
const QString &text,
const style::icon *icon) {
auto button = MediaButton{
.kind = kind,
.text = text.toUpper(),
.icon = icon,
};
button.textWidth = st::stickersPhotoButtonFont->width(button.text);
result.push_back(std::move(button));
};
if (features.linkButton) {
add(
MediaButton::Kind::Link,
tr::lng_link_header_short(tr::now),
&st.linkButtonIcon);
}
if (features.photoButton) {
add(
MediaButton::Kind::Photo,
tr::lng_attach_photo(tr::now),
&st.photoButtonIcon);
}
if (features.audioButton) {
add(
MediaButton::Kind::Audio,
tr::lng_attach_audio(tr::now),
&st.audioButtonIcon);
}
return result;
}
int StickersListWidget::mediaButtonsRowHeight() const {
if (_mediaButtons.empty() || _isMasks || _section == Section::Search) {
return 0;
}
const auto &padding = st::stickersPhotoRowPadding;
return padding.top() + st::stickersPhotoButtonHeight + padding.bottom();
}
QRect StickersListWidget::photoButtonRect() const {
int StickersListWidget::mediaButtonWidth(const MediaButton &button) const {
const auto &padding = st::stickersPhotoButtonPadding;
const auto &icon = st().photoButtonIcon;
const auto buttonWidth = padding.left()
+ icon.width()
return padding.left()
+ button.icon->width()
+ st::stickersPhotoButtonIconSkip
+ _photoButtonTextWidth
+ button.textWidth
+ padding.right();
}
QRect StickersListWidget::mediaButtonRect(int index) const {
Expects(index >= 0 && index < int(_mediaButtons.size()));
const auto skip = st::stickersPhotoButtonSkip;
auto full = -skip;
for (const auto &button : _mediaButtons) {
full += mediaButtonWidth(button) + skip;
}
auto left = (width() - full) / 2;
for (auto i = 0; i != index; ++i) {
left += mediaButtonWidth(_mediaButtons[i]) + skip;
}
const auto top = (_search ? _search->height() : 0)
+ st::stickersPhotoRowPadding.top();
return QRect(
(width() - buttonWidth) / 2,
left,
top,
buttonWidth,
mediaButtonWidth(_mediaButtons[index]),
st::stickersPhotoButtonHeight);
}
void StickersListWidget::paintPhotoButton(Painter &p, QRect clip) {
if (!photoRowHeight()) {
return;
int StickersListWidget::mediaButtonIndexAt(QPoint point) const {
if (!mediaButtonsRowHeight()) {
return -1;
}
const auto rect = photoButtonRect();
if (!rect.intersects(clip)) {
return;
}
_photoButtonBg.paint(p, myrtlrect(rect));
if (_photoButtonRipple) {
_photoButtonRipple->paint(p, myrtlrect(rect).x(), rect.y(), width());
if (_photoButtonRipple->empty()) {
_photoButtonRipple.reset();
for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
if (myrtlrect(mediaButtonRect(i)).contains(point)) {
return i;
}
}
const auto &padding = st::stickersPhotoButtonPadding;
const auto &icon = st().photoButtonIcon;
icon.paint(
p,
rect.x() + padding.left(),
rect.y() + (rect.height() - icon.height()) / 2,
width());
const auto &font = st::stickersPhotoButtonFont;
const auto textLeft = rect.x()
+ padding.left()
+ icon.width()
+ st::stickersPhotoButtonIconSkip;
p.setFont(font);
p.setPen(st().textFg);
p.drawTextLeft(
textLeft,
rect.y() + (rect.height() - font->height) / 2,
width(),
_photoButtonText,
_photoButtonTextWidth);
return -1;
}
void StickersListWidget::paintMediaButtons(Painter &p, QRect clip) {
if (!mediaButtonsRowHeight()) {
return;
}
for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
auto &button = _mediaButtons[i];
const auto rect = mediaButtonRect(i);
if (!rect.intersects(clip)) {
continue;
}
_mediaButtonBg.paint(p, myrtlrect(rect));
if (button.ripple) {
button.ripple->paint(p, myrtlrect(rect).x(), rect.y(), width());
if (button.ripple->empty()) {
button.ripple.reset();
}
}
const auto &padding = st::stickersPhotoButtonPadding;
const auto &icon = *button.icon;
icon.paint(
p,
rect.x() + padding.left(),
rect.y() + (rect.height() - icon.height()) / 2,
width());
const auto &font = st::stickersPhotoButtonFont;
const auto textLeft = rect.x()
+ padding.left()
+ icon.width()
+ st::stickersPhotoButtonIconSkip;
p.setFont(font);
p.setPen(st().textFg);
p.drawTextLeft(
textLeft,
rect.y() + (rect.height() - font->height) / 2,
width(),
button.text,
button.textWidth);
}
}
int StickersListWidget::searchShortcutsHeight() const {
@@ -1624,7 +1697,7 @@ void StickersListWidget::paintStickers(Painter &p, QRect clip) {
_paintAsPremium = session().premium();
_pathGradient->startFrame(0, width(), width() / 2);
paintSearchShortcuts(p, clip);
paintPhotoButton(p, clip);
paintMediaButtons(p, clip);
auto &sets = shownSets();
const auto selectedSticker = std::get_if<OverSticker>(&_selected);
@@ -2482,9 +2555,11 @@ void StickersListWidget::setPressed(OverState newPressed) {
if (_megagroupSetButtonRipple) {
_megagroupSetButtonRipple->lastStop();
}
} else if (std::get_if<OverPhotoButton>(&_pressed)) {
if (_photoButtonRipple) {
_photoButtonRipple->lastStop();
} else if (const auto media = std::get_if<OverMediaButton>(&_pressed)) {
if (media->index >= 0 && media->index < int(_mediaButtons.size())) {
if (const auto &ripple = _mediaButtons[media->index].ripple) {
ripple->lastStop();
}
}
}
_pressed = newPressed;
@@ -2519,18 +2594,23 @@ void StickersListWidget::setPressed(OverState newPressed) {
}
_megagroupSetButtonRipple->add(mapFromGlobal(QCursor::pos())
- myrtlrect(megagroupSetButtonRectFinal()).topLeft());
} else if (std::get_if<OverPhotoButton>(&_pressed)) {
if (!_photoButtonRipple) {
} else if (const auto media = std::get_if<OverMediaButton>(&_pressed)) {
const auto index = media->index;
if (index < 0 || index >= int(_mediaButtons.size())) {
return;
}
auto &button = _mediaButtons[index];
if (!button.ripple) {
auto mask = Ui::RippleAnimation::RoundRectMask(
photoButtonRect().size(),
mediaButtonRect(index).size(),
st::stickersPhotoButtonRadius);
_photoButtonRipple = std::make_unique<Ui::RippleAnimation>(
button.ripple = std::make_unique<Ui::RippleAnimation>(
st().searchPackRipple,
std::move(mask),
[this] { rtlupdate(photoButtonRect()); });
[this, index] { rtlupdate(mediaButtonRect(index)); });
}
_photoButtonRipple->add(mapFromGlobal(QCursor::pos())
- myrtlrect(photoButtonRect()).topLeft());
button.ripple->add(mapFromGlobal(QCursor::pos())
- myrtlrect(mediaButtonRect(index)).topLeft());
}
}
@@ -2865,8 +2945,19 @@ void StickersListWidget::mouseReleaseEvent(QMouseEvent *e) {
if (std::get_if<OverSearchBack>(&pressed)) {
backToSearchResults();
return;
} else if (std::get_if<OverPhotoButton>(&pressed)) {
_photoRequests.fire({});
} else if (const auto media = std::get_if<OverMediaButton>(
&pressed)) {
const auto index = media->index;
if (index >= 0 && index < int(_mediaButtons.size())) {
const auto kind = _mediaButtons[index].kind;
if (kind == MediaButton::Kind::Photo) {
_photoRequests.fire({});
} else if (kind == MediaButton::Kind::Audio) {
_audioRequests.fire({});
} else if (kind == MediaButton::Kind::Link) {
_linkRequests.fire({});
}
}
return;
} else if (auto shortcut = std::get_if<OverSearchShortcut>(&pressed)) {
toggleSearchShortcut(shortcut->index);
@@ -3645,8 +3736,8 @@ void StickersListWidget::updateSelected() {
}
setSelected(newSelected);
return;
} else if (photoRowHeight() && myrtlrect(photoButtonRect()).contains(p)) {
setSelected(OverPhotoButton{});
} else if (const auto media = mediaButtonIndexAt(p); media >= 0) {
setSelected(OverMediaButton{ media });
return;
}
const auto &sets = shownSets();
@@ -3759,8 +3850,12 @@ void StickersListWidget::setSelected(OverState newSelected) {
rtlupdate(searchBackRect());
} else if (std::get_if<OverGroupAdd>(&_selected)) {
rtlupdate(megagroupSetButtonRectFinal());
} else if (std::get_if<OverPhotoButton>(&_selected)) {
rtlupdate(photoButtonRect());
} else if (const auto media = std::get_if<OverMediaButton>(
&_selected)) {
if (media->index >= 0
&& media->index < int(_mediaButtons.size())) {
rtlupdate(mediaButtonRect(media->index));
}
}
};
updateSelected();

View File

@@ -105,6 +105,8 @@ public:
rpl::producer<FileChosen> chosen() const;
[[nodiscard]] rpl::producer<> photoRequests() const;
[[nodiscard]] rpl::producer<> audioRequests() const;
[[nodiscard]] rpl::producer<> linkRequests() const;
rpl::producer<> scrollUpdated() const;
rpl::producer<TabbedSelector::Action> choosingUpdated() const;
@@ -233,11 +235,13 @@ private:
return !(*this == other);
}
};
struct OverPhotoButton {
inline bool operator==(OverPhotoButton other) const {
return true;
struct OverMediaButton {
int index = 0;
inline bool operator==(OverMediaButton other) const {
return (index == other.index);
}
inline bool operator!=(OverPhotoButton other) const {
inline bool operator!=(OverMediaButton other) const {
return !(*this == other);
}
};
@@ -249,7 +253,7 @@ private:
OverSearchShortcut,
OverSearchBack,
OverGroupAdd,
OverPhotoButton>;
OverMediaButton>;
struct SectionInfo {
int section = 0;
@@ -320,9 +324,26 @@ private:
void readVisibleFeatured(int visibleTop, int visibleBottom);
void paintStickers(Painter &p, QRect clip);
void paintPhotoButton(Painter &p, QRect clip);
[[nodiscard]] int photoRowHeight() const;
[[nodiscard]] QRect photoButtonRect() const;
struct MediaButton {
enum class Kind {
Photo,
Audio,
Link,
};
Kind kind = Kind::Photo;
QString text;
int textWidth = 0;
const style::icon *icon = nullptr;
std::unique_ptr<Ui::RippleAnimation> ripple;
};
[[nodiscard]] static std::vector<MediaButton> MakeMediaButtons(
const ComposeFeatures &features,
const style::EmojiPan &st);
void paintMediaButtons(Painter &p, QRect clip);
[[nodiscard]] int mediaButtonsRowHeight() const;
[[nodiscard]] int mediaButtonWidth(const MediaButton &button) const;
[[nodiscard]] QRect mediaButtonRect(int index) const;
[[nodiscard]] int mediaButtonIndexAt(QPoint point) const;
void paintMegagroupEmptySet(Painter &p, int y, bool buttonSelected);
void paintSticker(
Painter &p,
@@ -507,10 +528,8 @@ private:
QRect _megagroupSetButtonRect;
std::unique_ptr<Ui::RippleAnimation> _megagroupSetButtonRipple;
Ui::RoundRect _photoButtonBg;
QString _photoButtonText;
int _photoButtonTextWidth = 0;
std::unique_ptr<Ui::RippleAnimation> _photoButtonRipple;
Ui::RoundRect _mediaButtonBg;
std::vector<MediaButton> _mediaButtons;
QString _addText;
int _addWidth;
@@ -551,6 +570,8 @@ private:
rpl::event_stream<FileChosen> _chosen;
rpl::event_stream<> _photoRequests;
rpl::event_stream<> _audioRequests;
rpl::event_stream<> _linkRequests;
rpl::event_stream<> _scrollUpdated;
rpl::event_stream<TabbedSelector::Action> _choosingUpdated;

View File

@@ -760,6 +760,14 @@ rpl::producer<> TabbedSelector::photoRequests() const {
return hasStickersTab() ? stickers()->photoRequests() : rpl::never<>();
}
rpl::producer<> TabbedSelector::audioRequests() const {
return hasStickersTab() ? stickers()->audioRequests() : rpl::never<>();
}
rpl::producer<> TabbedSelector::linkRequests() const {
return hasStickersTab() ? stickers()->linkRequests() : rpl::never<>();
}
auto TabbedSelector::choosingStickerUpdated() const
-> rpl::producer<TabbedSelector::Action>{
return hasStickersTab()

View File

@@ -151,6 +151,8 @@ public:
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
[[nodiscard]] rpl::producer<InlineChosen> inlineResultChosen() const;
[[nodiscard]] rpl::producer<> photoRequests() const;
[[nodiscard]] rpl::producer<> audioRequests() const;
[[nodiscard]] rpl::producer<> linkRequests() const;
[[nodiscard]] rpl::producer<> cancelled() const;
[[nodiscard]] rpl::producer<> checkForHide() const;

View File

@@ -696,6 +696,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != soundOverridesCount; ++i) {
QString key, value;
stream >> key >> value;
if (stream.status() != QDataStream::Ok) {
break;
}
soundOverrides.emplace(key, value);
}
}
@@ -719,6 +722,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != dictionariesEnabledCount; ++i) {
qint64 langId;
stream >> langId;
if (stream.status() != QDataStream::Ok) {
break;
}
dictionariesEnabled.emplace_back(langId);
}
}
@@ -843,6 +849,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != accountsOrderCount; ++i) {
quint64 sessionUniqueId;
stream >> sessionUniqueId;
if (stream.status() != QDataStream::Ok) {
break;
}
accountsOrder.emplace_back(sessionUniqueId);
}
}
@@ -872,6 +881,9 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != skipTranslationLanguagesCount; ++i) {
quint64 language;
stream >> language;
if (stream.status() != QDataStream::Ok) {
break;
}
skipTranslationLanguages.push_back({
QLocale::Language(language)
});
@@ -911,9 +923,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
for (auto i = 0; i != count; ++i) {
auto id = QString();
stream >> id;
if (stream.status() == QDataStream::Ok) {
recentEmojiSkip.emplace(id);
if (stream.status() != QDataStream::Ok) {
break;
}
recentEmojiSkip.emplace(id);
}
}
}
@@ -1021,11 +1034,13 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
auto prefsCount = quint32();
stream >> prefsCount;
auto prefs = base::flat_map<QByteArray, QByteArray>();
prefs.reserve(prefsCount);
for (auto i = quint32(); i != prefsCount; ++i) {
auto key = QByteArray();
auto value = QByteArray();
stream >> key >> value;
if (stream.status() != QDataStream::Ok) {
break;
}
prefs.emplace(std::move(key), std::move(value));
}
if (stream.status() == QDataStream::Ok) {

View File

@@ -504,7 +504,9 @@ SendError FileRestrictionError(
}
switch (file.type) {
case Type::Photo:
if (compress == true && photos) {
if (compress == true && videos && file.hasAudioEditScene()) {
return videos;
} else if (compress == true && photos) {
return photos;
} else if (const auto other = file.isSticker() ? stickers : files) {
if ((compress == false || photos) && other) {

View File

@@ -1270,21 +1270,14 @@ void DocumentData::save(
status = FileReady;
auto reader = owner().streaming().sharedReader(this, origin, true);
if (reader) {
_loader = std::make_unique<Storage::StreamedFileDownloader>(
&session(),
id,
_dc,
origin,
Data::DocumentCacheKey(_dc, id),
mediaKey(),
_loader = createStreamedDownloader(
std::move(reader),
origin,
mediaKey(),
toFile,
size,
locationType(),
(saveToCache() ? LoadToCacheAsWell : LoadToFileOnly),
fromCloud,
autoLoading,
cacheTag());
autoLoading);
} else if (hasWebLocation()) {
_loader = std::make_unique<mtpFileLoader>(
&session(),
@@ -1655,6 +1648,32 @@ bool DocumentData::hasRemoteLocation() const {
return (_dc != 0 && _access != 0);
}
auto DocumentData::createStreamedDownloader(
std::shared_ptr<Media::Streaming::Reader> reader,
Data::FileOrigin origin,
std::optional<MediaKey> fileLocationKey,
const QString &toFile,
LoadToCacheSetting toCache,
LoadFromCloudSetting fromCloud,
bool autoLoading) const
-> std::unique_ptr<Storage::StreamedFileDownloader> {
return std::make_unique<Storage::StreamedFileDownloader>(
&session(),
id,
_dc,
origin,
Data::DocumentCacheKey(_dc, id),
fileLocationKey,
std::move(reader),
toFile,
size,
locationType(),
toCache,
fromCloud,
autoLoading,
cacheTag());
}
bool DocumentData::useStreamingLoader() const {
if (size <= 0) {
return false;

View File

@@ -27,6 +27,7 @@ enum class NameType : uchar;
} // namespace Core
namespace Storage {
class StreamedFileDownloader;
namespace Cache {
struct Key;
} // namespace Cache
@@ -38,6 +39,7 @@ struct VideoQuality;
namespace Media::Streaming {
class Loader;
class Reader;
} // namespace Media::Streaming
namespace Data {
@@ -301,6 +303,7 @@ public:
[[nodiscard]] MediaKey mediaKey() const;
[[nodiscard]] Storage::Cache::Key cacheKey() const;
[[nodiscard]] LocationType locationType() const;
[[nodiscard]] uint8 cacheTag() const;
[[nodiscard]] bool canBeStreamed() const;
@@ -309,6 +312,15 @@ public:
bool forceRemoteLoader) const
-> std::unique_ptr<Media::Streaming::Loader>;
[[nodiscard]] bool useStreamingLoader() const;
[[nodiscard]] auto createStreamedDownloader(
std::shared_ptr<Media::Streaming::Reader> reader,
Data::FileOrigin origin,
std::optional<MediaKey> fileLocationKey,
const QString &toFile,
LoadToCacheSetting toCache,
LoadFromCloudSetting fromCloud,
bool autoLoading) const
-> std::unique_ptr<Storage::StreamedFileDownloader>;
void setInappPlaybackFailed();
[[nodiscard]] bool inappPlaybackFailed() const;
@@ -370,7 +382,6 @@ private:
friend class Serialize::Document;
[[nodiscard]] LocationType locationType() const;
void validateLottieSticker();
void setMaybeSupportsStreaming(bool supports);
void setLoadedInMediaCacheLocation();

View File

@@ -210,6 +210,8 @@ struct GiftCode {
inline constexpr auto kTimeToLiveSingleView = crl::time(0x7FFFFFFF);
class MediaDice;
class Media {
public:
Media(not_null<HistoryItem*> parent);

View File

@@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/audio_track_timeline.h"
#include "editor/editor_audio_timeline.h"
#include "editor/photo_editor_common.h"
#include "styles/style_editor.h"
namespace Editor {
AudioTrackTimeline::AudioTrackTimeline(not_null<QWidget*> parent)
: RpWidget(parent) {
}
AudioTrackTimeline::~AudioTrackTimeline() = default;
void AudioTrackTimeline::setTrack(std::shared_ptr<AudioTrack> track) {
if (!track) {
if (_timeline) {
_timeline->setPlaying(false);
}
return;
} else if (_track != track) {
_track = std::move(track);
_timeline = base::make_unique_q<AudioTimeline>(this, _track);
_timeline->removeRequests(
) | rpl::start_to_stream(_removeRequests, _timeline->lifetime());
_timeline->trimChanges(
) | rpl::on_next([=] {
_lengthChanges.fire(_track->length());
}, _timeline->lifetime());
_timeline->show();
if (width() > 0) {
resizeToWidth(width());
}
}
_timeline->setPlaying(true);
}
rpl::producer<crl::time> AudioTrackTimeline::lengthChanges() const {
return _lengthChanges.events();
}
void AudioTrackTimeline::refreshTrim() {
if (_timeline) {
_timeline->refreshTrim();
}
}
void AudioTrackTimeline::commitPendingEdit() {
if (_timeline) {
_timeline->commitPendingEdit();
}
}
void AudioTrackTimeline::refreshVolume() {
if (_timeline) {
_timeline->refreshVolume();
}
}
rpl::producer<> AudioTrackTimeline::removeRequests() const {
return _removeRequests.events();
}
int AudioTrackTimeline::resizeGetHeight(int newWidth) {
const auto height = st::photoEditorButtonBarHeight;
if (_timeline) {
_timeline->resizeToWidth(std::max(newWidth, 1));
_timeline->moveToLeft(0, (height - _timeline->height()) / 2);
}
return height;
}
} // namespace Editor

View File

@@ -0,0 +1,42 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/rp_widget.h"
namespace Editor {
class AudioTimeline;
struct AudioTrack;
class AudioTrackTimeline final : public Ui::RpWidget {
public:
explicit AudioTrackTimeline(not_null<QWidget*> parent);
~AudioTrackTimeline();
void setTrack(std::shared_ptr<AudioTrack> track);
void refreshTrim();
void commitPendingEdit();
void refreshVolume();
[[nodiscard]] rpl::producer<> removeRequests() const;
[[nodiscard]] rpl::producer<crl::time> lengthChanges() const;
int resizeGetHeight(int newWidth) override;
private:
std::shared_ptr<AudioTrack> _track;
base::unique_qptr<AudioTimeline> _timeline;
rpl::event_stream<> _removeRequests;
rpl::event_stream<crl::time> _lengthChanges;
};
} // namespace Editor

View File

@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "ui/ui_utility.h"
#include "ui/widgets/color_editor.h"
#include "styles/style_editor.h"
@@ -293,6 +294,7 @@ ColorPicker::ColorPicker(
, _sizeControlHoverArea(std::in_place, parent)
, _sizeControl(std::in_place, parent)
, _toolSelection(std::in_place, parent)
, _rowFade(std::in_place, parent)
, _brush(savedBrushes[ToolIndex(savedTool)])
, _toolBrushes(savedBrushes) {
_colorButton->resize(Size(st::photoEditorColorButtonSize));
@@ -328,6 +330,13 @@ ColorPicker::ColorPicker(
-padding - half);
p.drawEllipse(rect);
});
_rowFade->setAttribute(Qt::WA_TransparentForMouseEvents);
_rowFade->setAttribute(Qt::WA_TranslucentBackground, true);
_rowFade->setVisible(false);
_rowFade->paintOn([=](QPainter &p) {
p.setOpacity(_rowFadeAnimation.value(_rowFadeShown ? 1. : 0.));
p.drawImage(0, 0, _rowFadeImage);
});
_toolButtons.push_back(base::make_unique_q<ToolLottieButton>(
parent,
@@ -636,7 +645,15 @@ void ColorPicker::paintSizeControl(QPainter &p) {
p.drawEllipse(handleRect);
}
void ColorPicker::setVisible(bool visible) {
void ColorPicker::setVisible(bool visible, anim::type animated) {
_rowFadeAnimation.stop();
_rowFade->hide();
const auto fade = (animated == anim::type::normal)
&& !_paletteVisible
&& (visible != _colorButton->isVisible());
if (fade && !visible) {
_rowFadeImage = grabRow();
}
if (!visible) {
_paletteVisible = false;
_sizeDown.pressed = false;
@@ -647,10 +664,21 @@ void ColorPicker::setVisible(bool visible) {
_sizeControlPositionAnimation.stop();
_toolSelectionAnimation.stop();
}
_colorButton->setVisible(visible && !_paletteVisible);
_paletteWrap->setVisible(visible && _paletteVisible);
_sizeControlHoverArea->setVisible(visible);
_sizeControl->setVisible(visible);
setRowVisible(visible);
if (!fade) {
return;
} else if (visible) {
_rowFadeImage = grabRow();
setRowVisible(false);
}
startRowFade(visible);
}
void ColorPicker::setRowVisible(bool visible) {
_colorButton->setVisible(visible && !_paletteVisible);
const auto showTools = visible
&& !_paletteVisible
&& !_toolSelectionSuppressed;
@@ -663,6 +691,57 @@ void ColorPicker::setVisible(bool visible) {
}
}
QRect ColorPicker::rowRect() const {
auto result = _colorButton->geometry();
for (const auto &button : _toolButtons) {
result = result.united(button->geometry());
}
return result.united(_toolSelection->geometry());
}
QImage ColorPicker::grabRow() const {
const auto rect = rowRect();
const auto ratio = style::DevicePixelRatio();
auto result = QImage(
rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto p = QPainter(&result);
const auto draw = [&](not_null<QWidget*> widget) {
if (widget->isVisible()) {
p.drawImage(
widget->pos() - rect.topLeft(),
Ui::GrabWidgetToImage(widget));
}
};
draw(_colorButton.get());
for (const auto &button : _toolButtons) {
draw(button.get());
}
draw(_toolSelection.get());
return result;
}
void ColorPicker::startRowFade(bool shown) {
_rowFadeShown = shown;
_rowFade->setGeometry(rowRect());
_rowFade->raise();
_rowFade->show();
_rowFadeAnimation.start([=] {
_rowFade->update();
if (!_rowFadeAnimation.animating()) {
_rowFade->hide();
if (_rowFadeShown) {
setRowVisible(true);
}
}
},
shown ? 0. : 1.,
shown ? 1. : 0.,
st::photoEditorBarAnimationDuration);
}
rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
return _saveBrushRequests.events_starting_with_copy(_brush);
}

View File

@@ -29,7 +29,9 @@ public:
void moveLine(const QPoint &position);
void setCanvasRect(const QRect &rect);
void setVisible(bool visible);
void setVisible(
bool visible,
anim::type animated = anim::type::instant);
void setColor(const QColor &color);
void setToolSelectionVisible(bool visible);
bool preventHandleKeyPress() const;
@@ -42,6 +44,10 @@ private:
void rebuildPalette();
void updateToolButtonsGeometry();
void updateToolSelection(bool animated);
void setRowVisible(bool visible);
[[nodiscard]] QRect rowRect() const;
[[nodiscard]] QImage grabRow() const;
void startRowFade(bool shown);
void setTool(Brush::Tool tool);
void storeCurrentBrush();
void updateColorButtonColor(const QColor &color, bool animated);
@@ -73,6 +79,9 @@ private:
const base::unique_qptr<Ui::RpWidget> _sizeControl;
const base::unique_qptr<Ui::RpWidget> _toolSelection;
std::vector<base::unique_qptr<Ui::AbstractButton>> _toolButtons;
const base::unique_qptr<Ui::RpWidget> _rowFade;
QImage _rowFadeImage;
bool _rowFadeShown = false;
struct {
int y = 0;
@@ -96,6 +105,7 @@ private:
Ui::Animations::Simple _sizeControlPositionAnimation;
Ui::Animations::Simple _colorButtonAnimation;
Ui::Animations::Simple _toolSelectionAnimation;
Ui::Animations::Simple _rowFadeAnimation;
rpl::event_stream<Brush> _saveBrushRequests;
rpl::event_stream<> _toolClicks;

View File

@@ -11,16 +11,24 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/controllers/undo_controller.h"
#include "ui/layers/show.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Editor {
struct Controllers final {
Controllers(
std::unique_ptr<StickersPanelController> stickersPanelController,
std::unique_ptr<UndoController> undoController,
std::shared_ptr<Ui::Show> show)
std::shared_ptr<Ui::Show> show,
std::shared_ptr<Ui::Show> layerShow,
std::shared_ptr<ChatHelpers::Show> sessionShow)
: stickersPanelController(std::move(stickersPanelController))
, undoController(std::move(undoController))
, show(std::move(show)) {
, show(std::move(show))
, layerShow(std::move(layerShow))
, sessionShow(std::move(sessionShow)) {
}
~Controllers() {
};
@@ -28,6 +36,8 @@ struct Controllers final {
const std::unique_ptr<StickersPanelController> stickersPanelController;
const std::unique_ptr<UndoController> undoController;
const std::shared_ptr<Ui::Show> show;
const std::shared_ptr<Ui::Show> layerShow;
const std::shared_ptr<ChatHelpers::Show> sessionShow;
};
} // namespace Editor

View File

@@ -15,10 +15,26 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_media_view.h"
namespace Editor {
namespace {
// Works around MSVC 14.44 ICE on bitfields set in a member-init list.
[[nodiscard]] ChatHelpers::ComposeFeatures PrepareFeatures(bool withAudio) {
return {
.megagroupSet = false,
.stickersSettings = false,
.openStickerSets = false,
.photoButton = true,
.audioButton = withAudio,
.linkButton = true,
};
}
} // namespace
StickersPanelController::StickersPanelController(
not_null<Ui::RpWidget*> panelContainer,
std::shared_ptr<ChatHelpers::Show> show)
std::shared_ptr<ChatHelpers::Show> show,
bool withAudio)
: _stickersPanel(
base::make_unique_q<ChatHelpers::TabbedPanel>(
panelContainer,
@@ -30,12 +46,7 @@ StickersPanelController::StickersPanelController(
.st = st::mediaviewEmojiPan,
.level = Window::GifPauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::MediaEditor,
.features = {
.megagroupSet = false,
.stickersSettings = false,
.openStickerSets = false,
.photoButton = true,
},
.features = PrepareFeatures(withAudio),
}),
})) {
_stickersPanel->setDesiredHeightValues(
@@ -57,6 +68,14 @@ rpl::producer<> StickersPanelController::photoRequests() const {
return _stickersPanel->selector()->photoRequests();
}
rpl::producer<> StickersPanelController::audioRequests() const {
return _stickersPanel->selector()->audioRequests();
}
rpl::producer<> StickersPanelController::linkRequests() const {
return _stickersPanel->selector()->linkRequests();
}
rpl::producer<bool> StickersPanelController::panelShown() const {
return _stickersPanel->shownValue();
}

View File

@@ -31,11 +31,14 @@ public:
StickersPanelController(
not_null<Ui::RpWidget*> panelContainer,
std::shared_ptr<ChatHelpers::Show> show);
std::shared_ptr<ChatHelpers::Show> show,
bool withAudio);
[[nodiscard]] auto stickerChosen() const
-> rpl::producer<not_null<DocumentData*>>;
[[nodiscard]] rpl::producer<> photoRequests() const;
[[nodiscard]] rpl::producer<> audioRequests() const;
[[nodiscard]] rpl::producer<> linkRequests() const;
[[nodiscard]] rpl::producer<bool> panelShown() const;
void setShowRequestChanges(rpl::producer<ShowRequest> &&showRequest);

View File

@@ -206,18 +206,96 @@ photoEditorCropPointSize: 10px;
photoEditorCropMinSize: 20px;
photoEditorItemHandleSize: 10px;
photoEditorStickyTrigger: 12px;
photoEditorStickyLineWidth: 2px;
photoEditorKeysButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/keyboard", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/keyboard", photoEditorButtonIconFgOver }};
}
photoEditorKeysButtonLeft: 16px;
photoEditorKeysMenuSkip: 6px;
photoEditorKeysMenu: PopupMenu(photoEditorCropRatioMenu) {
scrollPadding: margins(0px, 4px, 0px, 4px);
menu: Menu(mediaviewMenu) {
widthMin: 220px;
widthMax: 600px;
itemPadding: margins(14px, 8px, 14px, 8px);
arrow: photoEditorMenuSubmenuArrow;
}
}
photoEditorKeysTitleStyle: TextStyle(semiboldTextStyle) {
font: font(12px semibold);
}
photoEditorKeysTitleFg: groupCallMemberNotJoinedStatus;
photoEditorKeysTitleSkip: 6px;
photoEditorKeysRowSkip: 6px;
photoEditorKeysCapStyle: TextStyle(semiboldTextStyle) {
font: font(12px semibold);
}
photoEditorKeysCapHeight: 20px;
photoEditorKeysCapMinWidth: 22px;
photoEditorKeysCapPadding: 6px;
photoEditorKeysCapRadius: 4px;
photoEditorKeysCapSkip: 4px;
photoEditorKeysCapBg: groupCallMenuBgOver;
photoEditorKeysCapBorder: groupCallMemberNotJoinedStatus;
photoEditorKeysCapFg: groupCallMembersFg;
photoEditorKeysTextSkip: 12px;
photoEditorKeysTextWidth: 360px;
photoEditorKeysTextMinWidth: 100px;
photoEditorKeysTextFg: groupCallMembersFg;
photoEditorAudioDiscSize: 48px;
photoEditorAudioDiscSkip: 12px;
photoEditorAudioDiscCoverSize: 30px;
photoEditorAudioDiscRing: 2px;
photoEditorAudioDiscRingFg: mediaviewControlFg;
photoEditorAudioDiscIcon: icon {{ "photo_editor/widget_music", photoEditorButtonIconFg }};
photoEditorAudioDiscIconOver: icon {{ "photo_editor/widget_music", photoEditorButtonIconFgOver }};
photoEditorMenuDelete: icon {{ "menu/delete", mediaviewMenuFg }};
photoEditorMediaMenu: PopupMenu(photoEditorCropRatioMenu) {
scrollPadding: margins(0px, 3px, 0px, 8px);
menu: Menu(mediaviewMenu) {
widthMin: 210px;
arrow: photoEditorMenuSubmenuArrow;
}
}
photoEditorAudioVolumeItem: Menu(mediaviewMenu) {
itemBgOver: groupCallMenuBg;
arrow: photoEditorMenuSubmenuArrow;
}
photoEditorAudioVolumePadding: margins(15px, 6px, 17px, 5px);
photoEditorAudioVolumeMargin: margins(54px, 0px, 17px, 0px);
photoEditorAudioTimelineBarWidth: 2px;
photoEditorAudioTimelineBarSkip: 3px;
photoEditorAudioTimelineBarMin: 1px;
photoEditorAudioTimelineBarRatio: 0.6;
photoEditorAudioTimelineBarOpacity: 0.4;
photoEditorAudioTimelineIcon: 13px;
photoEditorAudioTimelineIconSkip: 3px;
photoEditorAudioTimelineDotSkip: 10px;
photoEditorAudioTimelineDotRadius: 1px;
photoEditorAudioTimelineTextSkip: 12px;
photoEditorAudioTimelineFade: 8px;
photoEditorAudioTimelineAuthorFont: font(12px semibold);
photoEditorAudioTimelineFont: font(12px);
photoEditorTrimShortestIconSize: 24px;
videoTimelineStripHeight: 54px;
videoTimelineLabelHeight: 18px;
// Wide enough to hold the cover dot without it touching either side.
videoTimelineLabelSkip: 17px;
videoTimelineHeight: videoTimelineLabelHeight + videoTimelineLabelSkip + videoTimelineStripHeight;
videoTimelineTrimStripHeight: 32px;
videoTimelineTrimLabelSkip: 10px;
videoTimelineDotSize: 5px;
videoTimelineDotActiveSize: 9px;
videoTimelineDotSkip: 2px;
videoTimelineDotFg: callAnswerBg;
videoTimelineRadius: 6px;
videoTimelineHandleWidth: 11px;
videoTimelineHandleMinWidth: 4px;
videoTimelineHandleGripWidth: 2px;
videoTimelineHandleGripHeight: 16px;
videoTimelineHandleHitSlop: 12px;
@@ -230,9 +308,30 @@ videoTimelinePlaceholderBg: mediaviewPipPlaybackInactive;
videoTimelineDurationFg: mediaviewCaptionFg;
videoTimelineSizeFg: mediaviewCaptionFg;
videoTimelineSizeSkip: 12px;
videoTimelineScrollHeight: 2px;
videoTimelineScrollSkip: 3px;
videoTimelineScrollMinWidth: 16px;
videoTimelineOverviewSelectionHeight: 3px;
videoTimelineOverviewFg: windowBgActive;
videoTimelineHintSkip: 4px;
videoTimelineHintWidth: 18px;
videoTimelineHintHeight: 20px;
videoTimelineHintRadius: 6px;
videoTimelineHintStroke: 2px;
videoTimelineHintArrowWidth: 5px;
videoTimelineHintArrowHeight: 10px;
videoTimelineDurationStyle: TextStyle(defaultTextStyle) {
font: font(12px semibold);
}
videoTimelineDurationFieldSkip: 4px;
videoTimelineDurationField: InputField(defaultInputField) {
textBg: transparent;
textFg: videoTimelineDurationFg;
textMargins: margins(0px, 0px, 0px, 0px);
border: 0px;
borderActive: 0px;
style: videoTimelineDurationStyle;
}
videoEditorGifButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/gif", photoEditorButtonIconFg }};
@@ -259,6 +358,7 @@ videoQualitySliderStyle: TextStyle(defaultTextStyle) {
videoQualitySliderHeight: 32px;
videoEditorTimelineTop: 10px;
photoEditorTimelineSkip: 6px;
videoEditorHintSkip: 8px;
videoEditorQualitySkip: 6px;
videoEditorBarSkip: 12px;
@@ -279,3 +379,19 @@ videoEditorPlayBadgeBg: mediaviewCaptionBg;
videoEditorAbout: FlatLabel(photoEditorAbout) {
}
videoEditorAboutMargin: margins(10px, 10px, 10px, 0px);
photoEditorMessageReferenceWidth: 600px;
photoEditorLinkPreviewMargin: margins(24px, 12px, 24px, 6px);
photoEditorLinkPreviewMaxHeight: 320px;
photoEditorLinkPreviewPadding: 12px;
photoEditorLinkPillIcon: icon {{ "photo_editor/link", windowFg }};
photoEditorLinkThemeSize: 36px;
photoEditorLinkThemeIconSize: 28px;
photoEditorLinkThemeSkip: 6px;
photoEditorLinkThemeBg: mediaviewCaptionBg;
photoEditorLinkLoadingRadius: bubbleRadiusLarge;
photoEditorLinkLoadingPadding: margins(12px, 11px, 12px, 11px);
photoEditorLinkLoadingLine: 10px;
photoEditorLinkLoadingSkip: 8px;
photoEditorLinkLoadingQuote: 3px;
photoEditorLinkLoadingQuoteSkip: 8px;

View File

@@ -0,0 +1,154 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_audio_disc_button.h"
#include "editor/editor_audio_menu.h"
#include "editor/photo_editor_common.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_editor.h"
#include <QtGui/QContextMenuEvent>
namespace Editor {
namespace {
constexpr auto kSpinDuration = crl::time(4000);
} // namespace
AudioDiscButton::AudioDiscButton(QWidget *parent)
: RippleButton(parent, st::photoEditorRotateButton.ripple) {
resize(Size(st::photoEditorAudioDiscSize));
_spin.init([=](crl::time now) {
update();
return _active;
});
}
AudioDiscButton::~AudioDiscButton() = default;
void AudioDiscButton::setTrack(std::shared_ptr<AudioTrack> track) {
if (_track == track) {
return;
}
_track = std::move(track);
if (!_track || _track->cover.isNull()) {
_cover = QImage();
} else {
const auto ratio = style::DevicePixelRatio();
const auto side = st::photoEditorAudioDiscCoverSize * ratio;
auto scaled = _track->cover.scaled(
side,
side,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
const auto crop = QRect(
(scaled.width() - side) / 2,
(scaled.height() - side) / 2,
side,
side);
_cover = Images::Circle(scaled.copy(crop));
_cover.setDevicePixelRatio(ratio);
}
update();
}
void AudioDiscButton::setActive(bool active) {
if (_active == active) {
return;
}
_spinBase = angle();
_active = active;
_spinStarted = crl::now();
if (_active) {
_spin.start();
} else {
_spin.stop();
}
update();
}
rpl::producer<> AudioDiscButton::volumeChanges() const {
return _volumeChanges.events();
}
rpl::producer<> AudioDiscButton::removeRequests() const {
return _removeRequests.events();
}
float64 AudioDiscButton::angle() const {
if (!_active) {
return _spinBase;
}
const auto passed = crl::now() - _spinStarted;
return std::fmod(_spinBase + passed * 360. / kSpinDuration, 360.);
}
void AudioDiscButton::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto full = QRectF(rect());
p.setPen(Qt::NoPen);
p.setBrush(st::roundedBg);
p.drawEllipse(full);
paintRipple(p, QPoint());
if (_active) {
const auto ring = float64(st::photoEditorAudioDiscRing);
auto pen = QPen(st::photoEditorAudioDiscRingFg->c);
pen.setWidthF(ring);
p.setPen(pen);
p.setBrush(Qt::NoBrush);
p.drawEllipse(full.marginsRemoved(
{ ring / 2., ring / 2., ring / 2., ring / 2. }));
p.setPen(Qt::NoPen);
}
if (_cover.isNull()) {
const auto &icon = isOver()
? st::photoEditorAudioDiscIconOver
: st::photoEditorAudioDiscIcon;
icon.paintInCenter(p, rect());
return;
}
const auto cover = float64(st::photoEditorAudioDiscCoverSize);
p.save();
p.translate(rect::center(full));
p.rotate(angle());
p.drawImage(QRectF(-cover / 2., -cover / 2., cover, cover), _cover);
p.restore();
}
void AudioDiscButton::contextMenuEvent(QContextMenuEvent *e) {
if (!_track) {
return;
}
_menu = CreateAudioMenu(
this,
_track,
[=] { _volumeChanges.fire({}); },
[=] { _removeRequests.fire({}); });
_menu->popup(e->globalPos());
e->accept();
}
void AudioDiscButton::onStateChanged(State was, StateChangeSource source) {
RippleButton::onStateChanged(was, source);
update();
}
QImage AudioDiscButton::prepareRippleMask() const {
return Images::EllipseMask(size());
}
QPoint AudioDiscButton::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos());
}
} // namespace Editor

View File

@@ -0,0 +1,57 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/widgets/buttons.h"
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Editor {
struct AudioTrack;
class AudioDiscButton final : public Ui::RippleButton {
public:
explicit AudioDiscButton(QWidget *parent);
~AudioDiscButton();
void setTrack(std::shared_ptr<AudioTrack> track);
void setActive(bool active);
[[nodiscard]] rpl::producer<> volumeChanges() const;
[[nodiscard]] rpl::producer<> removeRequests() const;
protected:
void paintEvent(QPaintEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void onStateChanged(State was, StateChangeSource source) override;
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
private:
[[nodiscard]] float64 angle() const;
std::shared_ptr<AudioTrack> _track;
QImage _cover;
Ui::Animations::Basic _spin;
crl::time _spinStarted = 0;
float64 _spinBase = 0.;
bool _active = false;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<> _volumeChanges;
rpl::event_stream<> _removeRequests;
};
} // namespace Editor

View File

@@ -0,0 +1,228 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_audio_menu.h"
#include "editor/photo_editor_common.h"
#include "lang/lang_keys.h"
#include "ui/effects/animation_value.h"
#include "ui/effects/cross_line.h"
#include "ui/paint/arcs.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/popup_menu.h"
#include "ui/color_int_conversion.h"
#include "ui/rect.h"
#include "styles/style_calls.h"
#include "styles/style_editor.h"
namespace Editor {
namespace {
constexpr auto kMuteDuration = crl::time(150);
const auto kSpeakerThresholds = std::vector<float>{ 0.05f, 0.25f, 0.75f };
constexpr auto kVolumeStickedValues
= std::array<std::pair<float64, float64>, 3>{{
{ 0.25, 0.02 },
{ 0.5, 0.02 },
{ 0.75, 0.02 },
}};
} // namespace
MenuVolumeItem::MenuVolumeItem(
not_null<Ui::Menu::Menu*> parent,
const style::Menu &st,
float64 volume)
: Ui::Menu::ItemBase(parent, st)
, _muted(volume <= 0.)
, _slider(base::make_unique_q<Ui::MediaSlider>(
this,
st::groupCallMenuVolumeSlider))
, _dummyAction(new QAction(parent))
, _st(st)
, _stCross(st::groupCallMuteCrossLine)
, _crossLineMute(std::make_unique<Ui::CrossLineAnimation>(_stCross, true))
, _arcs(std::make_unique<Ui::Paint::ArcsAnimation>(
st::groupCallSpeakerArcsAnimation,
kSpeakerThresholds,
volume,
Ui::Paint::ArcsAnimation::Direction::Right)) {
fitToMenuWidth();
enableMouseSelecting();
enableMouseSelecting(_slider.get());
_slider->setAlwaysDisplayMarker(true);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
const auto &margin = st::photoEditorAudioVolumeMargin;
_itemRect = Rect(size) - st::photoEditorAudioVolumePadding;
_speakerRect = QRect(_itemRect.topLeft(), _stCross.icon.size());
_arcPosition = rect::center(_speakerRect)
+ QPoint(0, st::groupCallMenuSpeakerArcsSkip);
_slider->setGeometry(
margin.left(),
_speakerRect.y(),
size.width() - margin.left() - margin.right(),
_speakerRect.height());
}, lifetime());
_slider->setValue(volume);
updateSliderColor(volume);
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(this);
const auto muteProgress = _crossLineAnimation.value(_muted ? 1. : 0.);
p.fillRect(clip, isSelected() ? _st.itemBgOver : _st.itemBg);
const auto mutePen = anim::color(
unmuteColor(),
muteColor(),
muteProgress);
_crossLineMute->paint(
p,
_speakerRect.topLeft(),
muteProgress,
(muteProgress > 0) ? std::make_optional(mutePen) : std::nullopt);
p.translate(_arcPosition);
_arcs->paint(p);
}, lifetime());
_slider->setChangeProgressCallback([=](float64 value) {
applyVolume(value);
});
_slider->setAdjustCallback([=](float64 value) {
for (const auto &snap : kVolumeStickedValues) {
if (value > (snap.first - snap.second)
&& value < (snap.first + snap.second)) {
return snap.first;
}
}
return value;
});
initArcsAnimation();
}
void MenuVolumeItem::initArcsAnimation() {
_arcsAnimation.init([=](crl::time now) {
_arcs->update(now);
update(_speakerRect);
});
_arcs->startUpdateRequests(
) | rpl::on_next([=] {
if (!_arcsAnimation.animating()) {
_arcsAnimation.start();
}
}, lifetime());
_arcs->stopUpdateRequests(
) | rpl::on_next([=] {
_arcsAnimation.stop();
}, lifetime());
}
void MenuVolumeItem::applyVolume(float64 value) {
const auto muted = (value <= 0.);
if (_muted != muted) {
_muted = muted;
_crossLineAnimation.start(
[=] { update(_speakerRect); },
muted ? 0. : 1.,
muted ? 1. : 0.,
kMuteDuration);
}
_arcs->setValue(value);
updateSliderColor(value);
_changes.fire_copy(value);
}
QColor MenuVolumeItem::unmuteColor() const {
return (isSelected()
? _st.itemFgOver
: isEnabled()
? _st.itemFg
: _st.itemFgDisabled)->c;
}
QColor MenuVolumeItem::muteColor() const {
return (isSelected()
? st::attentionButtonFgOver
: st::attentionButtonFg)->c;
}
void MenuVolumeItem::updateSliderColor(float64 value) {
value = std::clamp(value, 0., 1.);
const auto colors = std::array<QColor, 4>{ {
Ui::ColorFromSerialized(0xF66464),
Ui::ColorFromSerialized(0xD0B738),
Ui::ColorFromSerialized(0x24CD80),
Ui::ColorFromSerialized(0x3BBCEC),
} };
_slider->setColorOverrides({
.activeFg = (value < 0.25)
? anim::color(colors[0], colors[1], value / 0.25)
: (value < 0.5)
? anim::color(colors[1], colors[2], (value - 0.25) / 0.25)
: anim::color(colors[2], colors[3], (value - 0.5) / 0.5),
});
}
not_null<QAction*> MenuVolumeItem::action() const {
return _dummyAction;
}
bool MenuVolumeItem::isEnabled() const {
return true;
}
int MenuVolumeItem::contentHeight() const {
return rect::m::sum::v(st::photoEditorAudioVolumePadding)
+ _stCross.icon.height();
}
rpl::producer<float64> MenuVolumeItem::changes() const {
return _changes.events();
}
void AddVolumeAction(
not_null<Ui::PopupMenu*> menu,
float64 volume,
Fn<void(float64)> changed) {
auto item = base::make_unique_q<MenuVolumeItem>(
menu->menu(),
st::photoEditorAudioVolumeItem,
volume);
item->changes(
) | rpl::on_next(std::move(changed), item->lifetime());
menu->addAction(std::move(item));
}
base::unique_qptr<Ui::PopupMenu> CreateAudioMenu(
not_null<QWidget*> parent,
std::shared_ptr<AudioTrack> track,
Fn<void()> volumeChanged,
Fn<void()> remove) {
auto result = base::make_unique_q<Ui::PopupMenu>(
parent,
st::photoEditorMediaMenu);
AddVolumeAction(result.get(), track->volume, [=](float64 value) {
track->volume = value;
volumeChanged();
});
result->addSeparator();
result->addAction(
tr::lng_photo_editor_audio_remove(tr::now),
std::move(remove),
&st::photoEditorMenuDelete);
return result;
}
} // namespace Editor

View File

@@ -0,0 +1,80 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "ui/effects/animations.h"
#include "ui/widgets/menu/menu_item_base.h"
namespace Ui {
class CrossLineAnimation;
class MediaSlider;
class PopupMenu;
namespace Paint {
class ArcsAnimation;
} // namespace Paint
} // namespace Ui
namespace Editor {
struct AudioTrack;
class MenuVolumeItem final : public Ui::Menu::ItemBase {
public:
MenuVolumeItem(
not_null<Ui::Menu::Menu*> parent,
const style::Menu &st,
float64 volume);
not_null<QAction*> action() const override;
bool isEnabled() const override;
[[nodiscard]] rpl::producer<float64> changes() const;
protected:
int contentHeight() const override;
private:
void initArcsAnimation();
void applyVolume(float64 value);
void updateSliderColor(float64 value);
[[nodiscard]] QColor unmuteColor() const;
[[nodiscard]] QColor muteColor() const;
QRect _itemRect;
QRect _speakerRect;
QPoint _arcPosition;
bool _muted = false;
const base::unique_qptr<Ui::MediaSlider> _slider;
const not_null<QAction*> _dummyAction;
const style::Menu &_st;
const style::CrossLineAnimation &_stCross;
const std::unique_ptr<Ui::CrossLineAnimation> _crossLineMute;
Ui::Animations::Simple _crossLineAnimation;
const std::unique_ptr<Ui::Paint::ArcsAnimation> _arcs;
Ui::Animations::Basic _arcsAnimation;
rpl::event_stream<float64> _changes;
};
void AddVolumeAction(
not_null<Ui::PopupMenu*> menu,
float64 volume,
Fn<void(float64)> changed);
[[nodiscard]] base::unique_qptr<Ui::PopupMenu> CreateAudioMenu(
not_null<QWidget*> parent,
std::shared_ptr<AudioTrack> track,
Fn<void()> volumeChanged,
Fn<void()> remove);
} // namespace Editor

View File

@@ -0,0 +1,298 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_audio_timeline.h"
#include "editor/editor_audio_menu.h"
#include "editor/photo_editor_common.h"
#include "editor/video/video_segment_player.h"
#include "editor/video/video_timeline_seeker.h"
#include "media/media_audio_waveform.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_editor.h"
#include <QtCore/QFileInfo>
#include <QtGui/QContextMenuEvent>
#include <QtGui/QLinearGradient>
namespace Editor {
namespace {
constexpr auto kWaveformBarDuration = crl::time(20);
constexpr auto kMaxWaveformBars = 16384;
constexpr auto kLoadedDuration = crl::time(600);
[[nodiscard]] TrimTimelineDescriptor TrimDescriptor(const AudioTrack &track) {
return {
.duration = track.duration,
.from = track.from,
.till = track.till,
.trimOnly = true,
};
}
[[nodiscard]] QString TitleFor(const AudioTrack &track) {
return !track.title.isEmpty()
? track.title
: !track.path.isEmpty()
? QFileInfo(track.path).completeBaseName()
: QString();
}
} // namespace
AudioTimeline::AudioTimeline(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<AudioTrack> track)
: TrimTimeline(parent, TrimDescriptor(*track))
, _track(std::move(track))
, _title(TitleFor(*_track))
, _performer(_track->performer) {
_track->from = from();
_track->till = till();
trimChanges(
) | rpl::on_next([=] {
_track->from = from();
_track->till = till();
}, lifetime());
loadWaveform();
}
AudioTimeline::~AudioTimeline() {
if (_waveformCancel) {
_waveformCancel->store(true);
}
}
const std::shared_ptr<AudioTrack> &AudioTimeline::track() const {
return _track;
}
void AudioTimeline::setPlaying(bool playing) {
if (playing == (_player != nullptr)) {
return;
} else if (!playing) {
_seeker = nullptr;
_player = nullptr;
return;
}
_player = std::make_unique<SegmentPlayer>(
_track->path,
_track->content,
SegmentPlayerOptions{ .audio = true, .volume = _track->volume });
_seeker = std::make_unique<TimelineSeeker>(this, _player.get());
_player->start();
}
void AudioTimeline::refreshTrim() {
setTrim(_track->from, _track->till);
}
void AudioTimeline::refreshVolume() {
if (_player) {
_player->setVolume(_track->volume);
}
}
rpl::producer<> AudioTimeline::removeRequests() const {
return _removeRequests.events();
}
void AudioTimeline::contextMenuEvent(QContextMenuEvent *e) {
_menu = CreateAudioMenu(
this,
_track,
[=] { refreshVolume(); },
[=] { _removeRequests.fire({}); });
_menu->popup(e->globalPos());
e->accept();
}
void AudioTimeline::loadWaveform() {
const auto count = int(std::clamp(
duration() / kWaveformBarDuration,
crl::time(1),
crl::time(kMaxWaveformBars)));
const auto weak = base::make_weak(this);
_waveformCancel = std::make_shared<std::atomic<bool>>(false);
const auto cancel = _waveformCancel;
crl::async([=, path = _track->path, content = _track->content] {
auto result = std::make_shared<Media::Audio::Waveform>(
Media::Audio::LoadWaveform(path, content, count, cancel));
crl::on_main(weak, [=] {
if (result->empty()) {
return;
}
_waveform = result;
_waveformMax = 0;
for (const auto bar : result->bars) {
accumulate_max(_waveformMax, int(bar));
}
_loadedAnimation.start(
[=] { update(); },
0.,
1.,
kLoadedDuration);
update();
});
});
}
void AudioTimeline::paintStrip(QPainter &p, const QRect &strip) {
p.fillRect(strip, st::videoTimelinePlaceholderBg);
paintWaveform(p, strip);
paintText(p, strip);
}
void AudioTimeline::paintWaveform(QPainter &p, const QRect &strip) {
if (!_waveform || _waveform->bars.empty() || !_waveformMax) {
return;
}
const auto loaded = _loadedAnimation.value(1.);
if (loaded <= 0.) {
return;
}
const auto &bars = _waveform->bars;
const auto count = int(bars.size());
const auto slots = std::max(
strip.width() / st::photoEditorAudioTimelineBarSkip,
1);
const auto spacing = strip.width() / float64(slots);
const auto from = float64(visibleFrom());
const auto span = visibleSpan();
const auto barDuration = float64(duration()) / count;
const auto barWidth = float64(st::photoEditorAudioTimelineBarWidth);
const auto minHeight = float64(st::photoEditorAudioTimelineBarMin);
const auto ratio = st::photoEditorAudioTimelineBarRatio;
auto color = st::videoTimelineFg->c;
color.setAlphaF(st::photoEditorAudioTimelineBarOpacity * loaded);
p.setPen(Qt::NoPen);
p.setBrush(color);
const auto bottom = float64(rect::bottom(strip));
for (auto i = 0; i != slots; ++i) {
const auto slotFrom = (from + i * span / slots) / barDuration;
const auto slotTill = (from + (i + 1) * span / slots) / barDuration;
const auto first = std::clamp(
int(std::floor(slotFrom)),
0,
count - 1);
const auto last = std::clamp(
int(std::ceil(slotTill)) - 1,
first,
count - 1);
auto peak = uint16(0);
for (auto j = first; j <= last; ++j) {
accumulate_max(peak, bars[j]);
}
const auto x = strip.x()
+ i * spacing
+ (spacing - barWidth) / 2.;
const auto height = std::max(
peak / float64(_waveformMax) * strip.height() * ratio,
minHeight);
p.drawRoundedRect(
QRectF(x, bottom - height, barWidth, height),
barWidth / 2.,
barWidth / 2.);
}
}
void AudioTimeline::paintText(QPainter &p, const QRect &strip) {
const auto left = std::max(xAt(from()), strip.x());
const auto right = std::min(xAt(till()), rect::right(strip));
const auto skip = st::photoEditorAudioTimelineTextSkip;
const auto available = right - left - 2 * skip;
const auto icon = st::photoEditorAudioTimelineIcon;
if (available <= icon) {
return;
}
const auto &authorFont = st::photoEditorAudioTimelineAuthorFont;
const auto &titleFont = st::photoEditorAudioTimelineFont;
const auto dotSkip = (_performer.isEmpty() || _title.isEmpty())
? 0
: int(st::photoEditorAudioTimelineDotSkip);
const auto full = icon
+ st::photoEditorAudioTimelineIconSkip
+ (_performer.isEmpty() ? 0 : authorFont->width(_performer))
+ dotSkip
+ (_title.isEmpty() ? 0 : titleFont->width(_title));
const auto shown = std::min(full, available);
const auto x = (left + right) / 2 - shown / 2;
const auto centerY = rect::center(strip).y();
if (full <= available) {
paintTextLine(p, x, centerY, shown);
return;
}
const auto ratio = style::DevicePixelRatio();
auto image = QImage(
QSize(shown, strip.height()) * ratio,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(ratio);
image.fill(Qt::transparent);
{
auto q = QPainter(&image);
auto hq = PainterHighQualityEnabler(q);
paintTextLine(q, 0, strip.height() / 2, shown);
const auto fade = float64(st::photoEditorAudioTimelineFade);
auto gradient = QLinearGradient(shown - fade, 0, shown, 0);
gradient.setColorAt(0., QColor(0, 0, 0, 255));
gradient.setColorAt(1., QColor(0, 0, 0, 0));
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
q.fillRect(QRectF(shown - fade, 0, fade, strip.height()), gradient);
}
p.drawImage(x, strip.y(), image);
}
void AudioTimeline::paintTextLine(
QPainter &p,
int x,
int centerY,
int width) {
const auto icon = st::photoEditorAudioTimelineIcon;
const auto image = st::photoEditorAudioDiscIcon.instance(
st::videoTimelineFg->c);
p.setClipRect(x, centerY - height(), width, height() * 2);
p.setRenderHint(QPainter::SmoothPixmapTransform);
p.drawImage(
QRectF(x, centerY - icon / 2., icon, icon),
image);
x += icon + st::photoEditorAudioTimelineIconSkip;
p.setPen(st::videoTimelineFg);
if (!_performer.isEmpty()) {
const auto &font = st::photoEditorAudioTimelineAuthorFont;
p.setFont(font);
p.drawText(
QPointF(x, centerY - font->height / 2. + font->ascent),
_performer);
x += font->width(_performer);
}
if (!_performer.isEmpty() && !_title.isEmpty()) {
const auto dotSkip = st::photoEditorAudioTimelineDotSkip;
const auto radius = float64(st::photoEditorAudioTimelineDotRadius);
auto dot = st::videoTimelineFg->c;
dot.setAlphaF(dot.alphaF() / 2.);
p.setPen(Qt::NoPen);
p.setBrush(dot);
p.drawEllipse(QPointF(x + dotSkip / 2., centerY), radius, radius);
p.setPen(st::videoTimelineFg);
x += dotSkip;
}
if (!_title.isEmpty()) {
const auto &font = st::photoEditorAudioTimelineFont;
p.setFont(font);
p.drawText(
QPointF(x, centerY - font->height / 2. + font->ascent),
_title);
}
p.setClipping(false);
}
} // namespace Editor

View File

@@ -0,0 +1,69 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/editor_trim_timeline.h"
#include "ui/effects/animations.h"
namespace Media::Audio {
struct Waveform;
} // namespace Media::Audio
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Editor {
class SegmentPlayer;
class TimelineSeeker;
struct AudioTrack;
class AudioTimeline final : public TrimTimeline {
public:
AudioTimeline(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<AudioTrack> track);
~AudioTimeline();
[[nodiscard]] const std::shared_ptr<AudioTrack> &track() const;
void setPlaying(bool playing);
void refreshTrim();
void refreshVolume();
[[nodiscard]] rpl::producer<> removeRequests() const;
private:
void paintStrip(QPainter &p, const QRect &strip) override;
void contextMenuEvent(QContextMenuEvent *e) override;
void loadWaveform();
void paintWaveform(QPainter &p, const QRect &strip);
void paintText(QPainter &p, const QRect &strip);
void paintTextLine(QPainter &p, int x, int centerY, int width);
const std::shared_ptr<AudioTrack> _track;
const QString _title;
const QString _performer;
std::unique_ptr<SegmentPlayer> _player;
std::unique_ptr<TimelineSeeker> _seeker;
std::shared_ptr<Media::Audio::Waveform> _waveform;
std::shared_ptr<std::atomic<bool>> _waveformCancel;
int _waveformMax = 0;
Ui::Animations::Simple _loadedAnimation;
base::unique_qptr<Ui::PopupMenu> _menu;
rpl::event_stream<> _removeRequests;
};
} // namespace Editor

View File

@@ -26,6 +26,7 @@ constexpr auto kEAll = Qt::TopEdge
| Qt::LeftEdge
| Qt::BottomEdge
| Qt::RightEdge;
constexpr auto kMaxCanvasRatio = 2.;
std::tuple<int, int, int, int> RectEdges(const QRectF &r) {
return { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() };
@@ -92,7 +93,6 @@ Crop::Crop(
, _pointSizeH(_pointSize / 2.)
, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH)
.toMargins())
, _offset(_innerMargins.left(), _innerMargins.top())
, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH)
, _imageSize(imageSize)
, _data(std::move(data))
@@ -125,6 +125,7 @@ Crop::Crop(
void Crop::applyTransform(
const QRect &geometry,
QPoint imagePosition,
int angle,
bool flipped,
const QSizeF &scaledImageSize) {
@@ -132,7 +133,9 @@ void Crop::applyTransform(
return;
}
setGeometry(geometry);
_innerRect = QRectF(_offset, FlipSizeByRotation(scaledImageSize, angle));
_innerRect = QRectF(
imagePosition,
FlipSizeByRotation(scaledImageSize, angle));
_ratio.w = scaledImageSize.width() / float64(_imageSize.width());
_ratio.h = scaledImageSize.height() / float64(_imageSize.height());
_flipped = flipped;
@@ -153,25 +156,21 @@ void Crop::applyTransform(
.scale(_ratio.w, _ratio.h)
.mapRect(_cropOriginal)
.translated(
-cropHolderRotated.x() + _offset.x(),
-cropHolderRotated.y() + _offset.y());
imagePosition.x() - cropHolderRotated.x(),
imagePosition.y() - cropHolderRotated.y());
auto adjusted = false;
const auto min = float64(st::photoEditorCropMinSize);
if ((cropPaint.width() < min) || (cropPaint.height() < min)) {
const auto bounds = _innerRect.united(cropPaint);
cropPaint.setWidth(std::max(
std::min(min, _innerRect.width()),
std::min(min, bounds.width()),
cropPaint.width()));
cropPaint.setHeight(std::max(
std::min(min, _innerRect.height()),
std::min(min, bounds.height()),
cropPaint.height()));
adjusted = true;
AdjustCropToInner(cropPaint, bounds);
}
adjusted = AdjustCropToInner(cropPaint, _innerRect) || adjusted;
setCropPaint(std::move(cropPaint));
if (adjusted) {
convertCropPaintToOriginal();
}
}
QPainterPath Crop::cropPath() const {
@@ -282,14 +281,16 @@ void Crop::paintGrid(QPainter &p, float64 opacity) {
}
void Crop::setCropPaint(QRectF &&rect) {
AdjustCropToInner(rect, _innerRect);
_cropPaint = std::move(rect);
updateEdges();
updatePainterPath();
}
_painterPath.clear();
_painterPath.addRect(_innerRect);
_painterPath.addPath(cropPath());
void Crop::updatePainterPath() {
auto inner = QPainterPath();
inner.addRect(_innerRect);
_painterPath = inner.subtracted(cropPath());
}
void Crop::convertCropPaintToOriginal() {
@@ -307,7 +308,7 @@ void Crop::convertCropPaintToOriginal() {
const auto cropHolderRotated = matrix.mapRect(cropHolder);
_cropOriginal = matrix
.mapRect(QRectF(_cropPaint).translated(-_offset))
.mapRect(QRectF(_cropPaint).translated(-_innerRect.topLeft()))
.translated(
-cropHolderRotated.x(),
-cropHolderRotated.y());
@@ -369,7 +370,13 @@ void Crop::mousePressEvent(QMouseEvent *e) {
if (_data.fixedCrop && e->button() != Qt::LeftButton) {
return;
}
computeDownState(e->pos());
const auto edge = mouseState(e->pos());
if (edge) {
_dragChanges.fire(true);
}
const auto expanding = _expansionAllowed
&& e->modifiers().testFlag(Qt::ControlModifier);
computeDownState(e->pos(), edge, expanding);
if (_down.edge) {
setGridVisible(true, false);
}
@@ -379,29 +386,43 @@ void Crop::mouseReleaseEvent(QMouseEvent *e) {
if (_data.fixedCrop && e->button() != Qt::LeftButton) {
return;
}
const auto hadEdge = bool(_down.edge);
if (hadEdge) {
setGridVisible(false, true);
finishDrag(true);
}
void Crop::hideEvent(QHideEvent *e) {
finishDrag(false);
}
void Crop::finishDrag(bool animated) {
if (!_down.edge) {
return;
}
setGridVisible(false, animated);
clearDownState();
const auto was = saveCropRect();
convertCropPaintToOriginal();
_dragChanges.fire(false);
if (saveCropRect() != was) {
_changes.fire({});
}
}
void Crop::computeDownState(const QPoint &p) {
const auto edge = mouseState(p);
const auto &inner = _innerRect;
void Crop::computeDownState(
const QPoint &p,
Qt::Edges edge,
bool expanding) {
const auto &crop = _cropPaint;
const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);
const auto bounds = (expanding && (edge != kEAll))
? expansionBounds()
: _innerRect.united(crop);
const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(bounds);
const auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);
_down = InfoAtDown{
.rect = crop,
.edge = edge,
.point = (p - PointOfEdge(edge, crop)),
.cropRatio = (_cropOriginal.width() / _cropOriginal.height()),
.bounds = bounds,
.borders = InfoAtDown::Borders{
.left = iLeft - cLeft,
.right = iRight - cRight,
@@ -419,15 +440,11 @@ void Crop::computeDownState(const QPoint &p) {
auto &xSide = (hasLeft ? _down.borders.left : _down.borders.right);
auto &ySide = (hasTop ? _down.borders.top : _down.borders.bottom);
const auto min = std::abs(std::min(xSign * xSide, ySign * ySide));
const auto xIsMin = ((xSign * xSide) < (ySign * ySide));
xSide = xSign * min;
ySide = ySign * min;
if (!xIsMin) {
xSide *= _down.cropRatio;
} else {
ySide /= _down.cropRatio;
}
const auto xRoom = std::min(
float64(xSign * xSide),
ySign * ySide * _down.cropRatio);
xSide = int(xSign * xRoom);
ySide = int(ySign * xRoom / _down.cropRatio);
}
}
@@ -435,6 +452,20 @@ void Crop::clearDownState() {
_down = InfoAtDown();
}
QRectF Crop::expansionBounds() const {
const auto united = _innerRect.united(_cropPaint);
const auto maxWidth = _innerRect.width() * kMaxCanvasRatio;
const auto maxHeight = _innerRect.height() * kMaxCanvasRatio;
const auto limit = QRectF(
QPointF(
std::min(united.right() - maxWidth, united.left()),
std::min(united.bottom() - maxHeight, united.top())),
QPointF(
std::max(united.left() + maxWidth, united.right()),
std::max(united.top() + maxHeight, united.bottom())));
return (QRectF(rect()) - QMarginsF(_innerMargins)).intersected(limit);
}
void Crop::setGridVisible(bool visible, bool animated) {
if ((_gridVisible == visible) && !_gridOpacityAnimation.animating()) {
return;
@@ -483,10 +514,18 @@ void Crop::performCrop(const QPoint &pos) {
const auto minH = (_keepAspectRatio && cropRatio < 1.)
? (minSize / cropRatio)
: float64(minSize);
const auto xMin = xFactor * int(
crop.width() - std::min(minW, crop.width()));
const auto yMin = yFactor * int(
crop.height() - std::min(minH, crop.height()));
const auto xInward = std::min(
crop.width() - minW,
hasLeft
? (_innerRect.right() - minW - crop.left())
: (crop.right() - minW - _innerRect.left()));
const auto yInward = std::min(
crop.height() - minH,
hasTop
? (_innerRect.bottom() - minH - crop.top())
: (crop.bottom() - minH - _innerRect.top()));
const auto xMin = xFactor * int(std::max(xInward, 0.));
const auto yMin = yFactor * int(std::max(yInward, 0.));
const auto x = std::clamp(
diff.x(),
@@ -498,11 +537,17 @@ void Crop::performCrop(const QPoint &pos) {
hasTop ? yMin : borders.bottom);
return QPoint(x, y);
}();
setCropPaint(crop - QMargins(
auto result = crop - QMargins(
hasLeft ? diff.x() : 0,
hasTop ? diff.y() : 0,
hasRight ? -diff.x() : 0,
hasBottom ? -diff.y() : 0));
hasBottom ? -diff.y() : 0);
const auto &bounds = _down.bounds;
result.setLeft(std::max(result.left(), bounds.left()));
result.setTop(std::max(result.top(), bounds.top()));
result.setRight(std::min(result.right(), bounds.right()));
result.setBottom(std::min(result.bottom(), bounds.bottom()));
setCropPaint(std::move(result));
}
void Crop::performMove(const QPoint &pos) {
@@ -510,7 +555,9 @@ void Crop::performMove(const QPoint &pos) {
const auto &b = _down.borders;
const auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right);
const auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom);
setCropPaint(inner.translated(diffX, diffY));
auto result = inner.translated(diffX, diffY);
AdjustCropToInner(result, _down.bounds);
setCropPaint(std::move(result));
}
void Crop::mouseMoveEvent(QMouseEvent *e) {
@@ -555,8 +602,9 @@ void Crop::setAspectRatio(float64 ratio) {
_keepAspectRatio = !free;
if (!free) {
const auto maxW = _innerRect.width();
const auto maxH = _innerRect.height();
const auto bounds = _innerRect.united(_cropPaint);
const auto maxW = bounds.width();
const auto maxH = bounds.height();
auto newW = maxW;
auto newH = maxW / ratio;
if (newH > maxH) {
@@ -571,23 +619,29 @@ void Crop::setAspectRatio(float64 ratio) {
newW,
newH);
AdjustCropToInner(adjusted, _innerRect);
AdjustCropToInner(adjusted, bounds);
setCropPaint(std::move(adjusted));
const auto was = saveCropRect();
convertCropPaintToOriginal();
if (saveCropRect() != was) {
_changes.fire({});
}
} else {
updateEdges();
}
update();
}
void Crop::setExpansionAllowed(bool allowed) {
_expansionAllowed = allowed;
}
void Crop::setCornersLevel(RoundedCornersLevel level) {
if (_cornersLevel == level) {
return;
}
_cornersLevel = level;
_painterPath.clear();
_painterPath.addRect(_innerRect);
_painterPath.addPath(cropPath());
updatePainterPath();
update();
}
@@ -595,6 +649,10 @@ QRect Crop::paintRect() const {
return _cropPaint.toRect();
}
QRect Crop::cropRect() const {
return _cropOriginal.toRect();
}
QRect Crop::saveCropRect() {
const auto savedCrop = _cropOriginal.toRect();
return (!savedCrop.topLeft().isNull() || (savedCrop.size() != _imageSize))

View File

@@ -26,22 +26,30 @@ public:
void applyTransform(
const QRect &geometry,
QPoint imagePosition,
int angle,
bool flipped,
const QSizeF &scaledImageSize);
[[nodiscard]] QRect saveCropRect();
[[nodiscard]] QRect cropRect() const;
[[nodiscard]] rpl::producer<> changes() const {
return _changes.events();
}
[[nodiscard]] rpl::producer<bool> dragChanges() const {
return _dragChanges.events();
}
[[nodiscard]] QRect paintRect() const;
[[nodiscard]] QPainterPath cropPath() const;
[[nodiscard]] style::margins cropMargins() const;
void setAspectRatio(float64 ratio);
void setCornersLevel(RoundedCornersLevel level);
void setExpansionAllowed(bool allowed);
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void hideEvent(QHideEvent *e) override;
private:
struct InfoAtDown {
@@ -49,6 +57,7 @@ private:
Qt::Edges edge;
QPoint point;
float64 cropRatio = 0.;
QRectF bounds;
struct Borders {
int left = 0;
@@ -61,25 +70,27 @@ private:
void paintFrame(QPainter &p);
void paintGrid(QPainter &p, float64 opacity);
void setGridVisible(bool visible, bool animated);
[[nodiscard]] QPainterPath cropPath() const;
void updateEdges();
void updatePainterPath();
[[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const;
[[nodiscard]] QRectF expansionBounds() const;
void setCropPaint(QRectF &&rect);
void convertCropPaintToOriginal();
void computeDownState(const QPoint &p);
void computeDownState(const QPoint &p, Qt::Edges edge, bool expanding);
void clearDownState();
[[nodiscard]] Qt::Edges mouseState(const QPoint &p);
void performCrop(const QPoint &pos);
void performMove(const QPoint &pos);
void finishDrag(bool animated);
rpl::event_stream<> _changes;
rpl::event_stream<bool> _dragChanges;
const int _pointSize;
const float _pointSizeH;
const style::margins _innerMargins;
const QPoint _offset;
const QMarginsF _edgePointMargins;
const QSize _imageSize;
const EditorData _data;
@@ -105,6 +116,7 @@ private:
bool _gridVisible = false;
bool _keepAspectRatio = false;
bool _expansionAllowed = false;
RoundedCornersLevel _cornersLevel = RoundedCornersLevel::Large;

View File

@@ -0,0 +1,513 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_keys_legend.h"
#include "base/platform/base_platform_info.h"
#include "lang/lang_keys.h"
#include "ui/effects/panel_animation.h"
#include "ui/painter.h"
#include "ui/qt_object_factory.h"
#include "ui/rect.h"
#include "ui/text/text.h"
#include "ui/ui_utility.h"
#include "ui/widgets/menu/menu.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_editor.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QScreen>
namespace Editor {
namespace {
struct Row {
std::vector<QString> keys;
QString text;
};
struct Section {
QString title;
std::vector<Row> rows;
};
[[nodiscard]] QString CtrlKey() {
return Platform::IsMac() ? QString(QChar(0x2318)) : u"Ctrl"_q;
}
[[nodiscard]] QString ShiftKey() {
return Platform::IsMac() ? QString(QChar(0x21E7)) : u"Shift"_q;
}
[[nodiscard]] QString DeleteKey() {
return Platform::IsMac() ? QString(QChar(0x232B)) : u"Del"_q;
}
[[nodiscard]] QString Gesture(
const QString &modifier,
const QString &gesture) {
return tr::lng_photo_editor_keys_gesture(
tr::now,
lt_modifier,
modifier,
lt_gesture,
gesture);
}
[[nodiscard]] QString DragEdge() {
return tr::lng_photo_editor_keys_gesture_drag_edge(tr::now);
}
[[nodiscard]] QString DragHandle() {
return tr::lng_photo_editor_keys_gesture_drag_handle(tr::now);
}
[[nodiscard]] QString Drag() {
return tr::lng_photo_editor_keys_gesture_drag(tr::now);
}
[[nodiscard]] QString Click() {
return tr::lng_photo_editor_keys_gesture_click(tr::now);
}
[[nodiscard]] QString Wheel() {
return tr::lng_photo_editor_keys_gesture_wheel(tr::now);
}
[[nodiscard]] QString WheelKey() {
return tr::lng_photo_editor_keys_wheel(tr::now);
}
[[nodiscard]] QString MiddleDragKey() {
return tr::lng_photo_editor_keys_middle_drag(tr::now);
}
[[nodiscard]] QString DoubleClickKey() {
return tr::lng_photo_editor_keys_double_click(tr::now);
}
[[nodiscard]] std::vector<Row> FixedCropRows() {
return {
{ { WheelKey() }, tr::lng_photo_editor_keys_fixed_zoom(tr::now) },
{
{ Gesture(ShiftKey(), Wheel()) },
tr::lng_photo_editor_keys_fixed_zoom_fine(tr::now),
},
{
{ MiddleDragKey() },
tr::lng_photo_editor_keys_fixed_pan(tr::now),
},
};
}
[[nodiscard]] Section CropSection(const KeysLegendContext &context) {
auto result = Section{ .title = tr::lng_photo_editor_keys_crop(tr::now) };
if (context.fixedCrop) {
result.rows = FixedCropRows();
} else {
result.rows = {
{ { CtrlKey() }, tr::lng_photo_editor_keys_crop_room(tr::now) },
{
{ Gesture(CtrlKey(), DragEdge()) },
tr::lng_photo_editor_keys_crop_expand(tr::now),
},
};
}
return result;
}
[[nodiscard]] Section CanvasSection(const KeysLegendContext &context) {
auto result = Section{
.title = tr::lng_photo_editor_keys_canvas(tr::now),
};
if (context.fixedCrop) {
result.rows = FixedCropRows();
} else {
result.rows = {
{
{ WheelKey() },
tr::lng_photo_editor_keys_canvas_zoom(tr::now),
},
{
{ MiddleDragKey() },
tr::lng_photo_editor_keys_canvas_pan(tr::now),
},
};
}
result.rows.push_back({
{ CtrlKey(), u"V"_q },
tr::lng_photo_editor_keys_canvas_paste(tr::now),
});
result.rows.push_back({
{ CtrlKey(), u"Z"_q },
tr::lng_photo_editor_keys_canvas_undo(tr::now),
});
result.rows.push_back({
{ CtrlKey(), ShiftKey(), u"Z"_q },
tr::lng_photo_editor_keys_canvas_redo(tr::now),
});
return result;
}
[[nodiscard]] Section ShapesSection() {
return {
.title = tr::lng_photo_editor_keys_shapes(tr::now),
.rows = {
{
{ Gesture(ShiftKey(), Drag()) },
tr::lng_photo_editor_keys_shapes_proportions(tr::now),
},
{
{ Gesture(ShiftKey(), Click()) },
tr::lng_photo_editor_keys_shapes_instant(tr::now),
},
},
};
}
[[nodiscard]] Section ObjectsSection() {
return {
.title = tr::lng_photo_editor_keys_objects(tr::now),
.rows = {
{
{ Gesture(ShiftKey(), Drag()) },
tr::lng_photo_editor_keys_objects_snap(tr::now),
},
{
{ Gesture(ShiftKey(), DragHandle()) },
tr::lng_photo_editor_keys_objects_rotate(tr::now),
},
{
{ Gesture(CtrlKey(), DragHandle()) },
tr::lng_photo_editor_keys_objects_stretch(tr::now),
},
{
{ CtrlKey(), u"D"_q },
tr::lng_photo_editor_keys_objects_duplicate(tr::now),
},
{
{ CtrlKey(), u"S"_q },
tr::lng_photo_editor_keys_objects_flip(tr::now),
},
{
{ DeleteKey() },
tr::lng_photo_editor_keys_objects_delete(tr::now),
},
{
{ DoubleClickKey() },
tr::lng_photo_editor_keys_objects_text(tr::now),
},
},
};
}
[[nodiscard]] Section TimelineSection() {
return {
.title = tr::lng_photo_editor_keys_timeline(tr::now),
.rows = {
{
{ WheelKey() },
tr::lng_photo_editor_keys_timeline_zoom(tr::now),
},
{
{ Gesture(ShiftKey(), Wheel()) },
tr::lng_photo_editor_keys_timeline_scroll(tr::now),
},
{
{ Gesture(ShiftKey(), Drag()) },
tr::lng_photo_editor_keys_timeline_window(tr::now),
},
},
};
}
[[nodiscard]] std::vector<Section> Sections(
const KeysLegendContext &context) {
auto result = (context.mode == PhotoEditorMode::Mode::Transform)
? std::vector<Section>{ CropSection(context) }
: std::vector<Section>{
CanvasSection(context),
ShapesSection(),
ObjectsSection(),
};
if (context.timeline
&& (context.mode == PhotoEditorMode::Mode::Paint)) {
result.push_back(TimelineSection());
}
return result;
}
[[nodiscard]] Section LegendSection() {
return {
.rows = {
{ { u"?"_q }, tr::lng_photo_editor_keys_legend(tr::now) },
},
};
}
struct Cap {
QString text;
int width = 0;
};
[[nodiscard]] Cap MakeCap(const QString &text) {
const auto &st = st::photoEditorKeysCapStyle;
return {
.text = text,
.width = std::max(
st.font->width(text) + 2 * st::photoEditorKeysCapPadding,
st::photoEditorKeysCapMinWidth),
};
}
[[nodiscard]] int CapsWidth(const std::vector<Cap> &caps) {
auto result = 0;
for (const auto &cap : caps) {
result += cap.width;
}
return result + (int(caps.size()) - 1) * st::photoEditorKeysCapSkip;
}
class SectionItem final : public Ui::Menu::ItemBase {
public:
SectionItem(
not_null<Ui::Menu::Menu*> parent,
const style::Menu &st,
const Section &section,
int keysWidth);
not_null<QAction*> action() const override;
bool isEnabled() const override;
private:
struct PreparedRow {
std::vector<Cap> caps;
Ui::Text::String text;
};
int contentHeight() const override;
void paintEvent(QPaintEvent *e) override;
void paintCap(QPainter &p, const Cap &cap, int x, int y) const;
const style::Menu &_st;
const int _keysWidth;
const Ui::Text::String _title;
std::vector<PreparedRow> _rows;
int _textWidth = 0;
const not_null<QAction*> _dummyAction;
};
SectionItem::SectionItem(
not_null<Ui::Menu::Menu*> parent,
const style::Menu &st,
const Section &section,
int keysWidth)
: ItemBase(parent, st)
, _st(st)
, _keysWidth(keysWidth)
, _title(st::photoEditorKeysTitleStyle, section.title)
, _dummyAction(Ui::CreateChild<QAction>(parent.get())) {
setPointerCursor(false);
auto textWidth = 0;
for (const auto &row : section.rows) {
auto caps = std::vector<Cap>();
for (const auto &key : row.keys) {
caps.push_back(MakeCap(key));
}
auto text = Ui::Text::String(
_st.itemStyle,
row.text,
kDefaultTextOptions,
st::photoEditorKeysTextMinWidth);
textWidth = std::max(textWidth, text.maxWidth());
_rows.push_back({ std::move(caps), std::move(text) });
}
const auto available = _st.widthMax
- rect::m::sum::h(_st.itemPadding)
- _keysWidth
- st::photoEditorKeysTextSkip;
_textWidth = std::max(
std::min({ textWidth, st::photoEditorKeysTextWidth, available }),
st::photoEditorKeysTextMinWidth);
setMinWidth(rect::m::sum::h(_st.itemPadding)
+ _keysWidth
+ st::photoEditorKeysTextSkip
+ _textWidth);
parent->widthValue() | rpl::on_next([=](int width) {
resize(width, contentHeight());
}, lifetime());
}
not_null<QAction*> SectionItem::action() const {
return _dummyAction;
}
bool SectionItem::isEnabled() const {
return false;
}
int SectionItem::contentHeight() const {
auto result = rect::m::sum::v(_st.itemPadding);
if (!_title.isEmpty()) {
result += _title.minHeight() + st::photoEditorKeysTitleSkip;
}
for (const auto &row : _rows) {
result += std::max(
st::photoEditorKeysCapHeight,
row.text.countHeight(_textWidth));
}
return result + (int(_rows.size()) - 1) * st::photoEditorKeysRowSkip;
}
void SectionItem::paintCap(
QPainter &p,
const Cap &cap,
int x,
int y) const {
const auto rect = QRect(x, y, cap.width, st::photoEditorKeysCapHeight);
const auto radius = st::photoEditorKeysCapRadius;
auto hq = PainterHighQualityEnabler(p);
p.setPen(QPen(st::photoEditorKeysCapBorder, st::lineWidth));
p.setBrush(st::photoEditorKeysCapBg);
p.drawRoundedRect(
QRectF(rect).adjusted(.5, .5, -.5, -.5),
radius,
radius);
p.setPen(st::photoEditorKeysCapFg);
p.setFont(st::photoEditorKeysCapStyle.font);
p.drawText(rect, cap.text, style::al_center);
}
void SectionItem::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
p.fillRect(rect(), _st.itemBg);
const auto &padding = _st.itemPadding;
const auto textLeft = padding.left()
+ _keysWidth
+ st::photoEditorKeysTextSkip;
auto top = padding.top();
if (!_title.isEmpty()) {
p.setPen(st::photoEditorKeysTitleFg);
_title.draw(
p,
padding.left(),
top,
width() - rect::m::sum::h(padding));
top += _title.minHeight() + st::photoEditorKeysTitleSkip;
}
for (const auto &row : _rows) {
const auto textHeight = row.text.countHeight(_textWidth);
const auto rowHeight = std::max(
st::photoEditorKeysCapHeight,
textHeight);
auto capLeft = padding.left();
const auto capTop = top
+ (rowHeight - st::photoEditorKeysCapHeight) / 2;
for (const auto &cap : row.caps) {
paintCap(p, cap, capLeft, capTop);
capLeft += cap.width + st::photoEditorKeysCapSkip;
}
p.setPen(st::photoEditorKeysTextFg);
row.text.draw(
p,
textLeft,
top + (rowHeight - textHeight) / 2,
_textWidth);
top += rowHeight + st::photoEditorKeysRowSkip;
}
}
void FillKeysLegend(
not_null<Ui::PopupMenu*> menu,
const KeysLegendContext &context) {
auto sections = Sections(context);
sections.push_back(LegendSection());
auto keysWidth = 0;
for (const auto &section : sections) {
for (const auto &row : section.rows) {
auto caps = std::vector<Cap>();
for (const auto &key : row.keys) {
caps.push_back(MakeCap(key));
}
keysWidth = std::max(keysWidth, CapsWidth(caps));
}
}
for (const auto &section : sections) {
if (section.title.isEmpty()) {
menu->addSeparator();
}
menu->addAction(base::make_unique_q<SectionItem>(
menu->menu(),
menu->st().menu,
section,
keysWidth));
}
}
} // namespace
KeysLegendButton::KeysLegendButton(
not_null<QWidget*> parent,
Fn<KeysLegendContext()> context)
: IconButton(parent, st::photoEditorKeysButton)
, _context(std::move(context)) {
events(
) | rpl::on_next([=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter) {
Ui::Tooltip::Show(1000, this);
} else if (event->type() == QEvent::Leave) {
Ui::Tooltip::Hide();
}
}, lifetime());
setClickedCallback([=] { toggle(); });
}
void KeysLegendButton::toggle() {
if (_menu) {
_menu->hideMenu();
} else {
showLegend();
}
}
QString KeysLegendButton::tooltipText() const {
return tr::lng_photo_editor_keys_tooltip(tr::now);
}
QPoint KeysLegendButton::tooltipPos() const {
return QCursor::pos();
}
bool KeysLegendButton::tooltipWindowActive() const {
return Ui::AppInFocus() && Ui::InFocusChain(window());
}
void KeysLegendButton::showLegend() {
Ui::Tooltip::Hide();
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::photoEditorKeysMenu);
FillKeysLegend(_menu.get(), _context());
const auto skip = st::photoEditorKeysMenuSkip;
const auto above = mapToGlobal(QPoint(0, -skip));
const auto screen = QGuiApplication::screenAt(above);
const auto fitsAbove = !screen
|| (above.y() - _menu->height() >= screen->availableGeometry().y());
if (fitsAbove) {
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::BottomLeft);
_menu->popup(above);
} else {
_menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopLeft);
_menu->popup(mapToGlobal(QPoint(0, height())));
}
}
} // namespace Editor

View File

@@ -0,0 +1,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/photo_editor_inner_common.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/tooltip.h"
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Editor {
struct KeysLegendContext {
PhotoEditorMode::Mode mode = PhotoEditorMode::Mode::Transform;
bool fixedCrop = false;
bool timeline = false;
};
class KeysLegendButton final
: public Ui::IconButton
, public Ui::AbstractTooltipShower {
public:
KeysLegendButton(
not_null<QWidget*> parent,
Fn<KeysLegendContext()> context);
void toggle();
QString tooltipText() const override;
QPoint tooltipPos() const override;
bool tooltipWindowActive() const override;
private:
void showLegend();
const Fn<KeysLegendContext()> _context;
base::unique_qptr<Ui::PopupMenu> _menu;
};
} // namespace Editor

View File

@@ -208,7 +208,25 @@ void LayerWidget::parentResized() {
}
void LayerWidget::keyPressEvent(QKeyEvent *e) {
forwardKeyEvent(e);
}
void LayerWidget::keyReleaseEvent(QKeyEvent *e) {
forwardKeyEvent(e);
}
void LayerWidget::forwardKeyEvent(not_null<QKeyEvent*> e) {
// Qt bubbles a key event ignored by the content back to its parent.
if (_forwardingKeyEvent) {
_keyEventIgnored = true;
e->accept();
return;
}
_forwardingKeyEvent = true;
_keyEventIgnored = false;
QGuiApplication::sendEvent(_content.get(), e);
_forwardingKeyEvent = false;
e->setAccepted(!_keyEventIgnored);
}
int LayerWidget::resizeGetHeight(int newWidth) {

View File

@@ -34,7 +34,9 @@ public:
private:
bool eventHook(QEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
int resizeGetHeight(int newWidth) override;
void forwardKeyEvent(not_null<QKeyEvent*> e);
void start();
void cacheBackground();
@@ -53,6 +55,8 @@ private:
crl::time _lastAreaChangeTime = 0;
bool _backgroundCaching = false;
bool _backgroundNight = false;
bool _forwardingKeyEvent = false;
bool _keyEventIgnored = false;
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/editor_message_source.h"
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class BoxContent;
} // namespace Ui
namespace Editor {
struct LinkBoxResult {
std::shared_ptr<MessageSource> message;
std::optional<LinkPreview> pill;
bool dark = false;
};
struct LinkBoxArgs {
std::shared_ptr<ChatHelpers::Show> show;
QString url;
std::optional<LinkPreview> editing;
Fn<void(LinkBoxResult)> done;
};
[[nodiscard]] object_ptr<Ui::BoxContent> LinkBox(LinkBoxArgs &&args);
} // namespace Editor

View File

@@ -0,0 +1,135 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_link_pill.h"
#include "ui/painter.h"
#include "styles/style_editor.h"
namespace Editor {
namespace {
constexpr auto kDpPerWidth = 360.;
constexpr auto kPaddingLeft = 4.;
constexpr auto kPaddingTop = 4.33;
constexpr auto kPaddingRight = 7.66;
constexpr auto kPaddingBottom = 3.;
constexpr auto kIconSize = 30.;
constexpr auto kIconPadding = 3.25;
constexpr auto kFontSize = 24.;
constexpr auto kRadiusRatio = 0.2;
constexpr auto kSemiTransparentAlpha = 0x4C;
[[nodiscard]] QString WithoutScheme(const QString &url) {
auto result = url;
const auto scheme = result.indexOf(u"://"_q);
if (scheme >= 0) {
result = result.mid(scheme + 3);
}
while (result.endsWith('/')) {
result.chop(1);
}
return result;
}
} // namespace
LinkPill::LinkPill(const LinkPreview &link, float64 density, int maxWidth)
: _density(density)
, _font(
int(base::SafeRound(kFontSize * density)),
style::FontFlag::Bold,
QString()) {
const auto text = link.name.isEmpty()
? WithoutScheme(link.url).toUpper()
: link.name;
const auto fixed = (kPaddingLeft
+ kIconSize
+ kIconPadding
+ kPaddingRight) * _density;
const auto maxTextWidth = std::max(maxWidth - int(fixed), 1);
_text = _font->elided(text, maxTextWidth);
_textWidth = _font->width(_text);
_size = QSizeF(
fixed + _textWidth,
(kPaddingTop + kPaddingBottom) * _density
+ std::max(kIconSize * _density, float64(_font->height)));
updateIcon();
}
QSizeF LinkPill::size() const {
return _size;
}
LinkStyle LinkPill::style() const {
return _style;
}
void LinkPill::setStyle(LinkStyle style) {
_style = style;
updateIcon();
}
QColor LinkPill::background() const {
switch (_style) {
case LinkStyle::Framed:
return QColor(255, 255, 255);
case LinkStyle::SemiTransparent:
return QColor(0, 0, 0, kSemiTransparentAlpha);
case LinkStyle::Opaque:
return QColor(0, 0, 0);
}
Unexpected("Link style in LinkPill::background.");
}
QColor LinkPill::foreground() const {
return (_style == LinkStyle::Framed)
? QColor(0x33, 0x91, 0xD4)
: QColor(255, 255, 255);
}
float64 LinkPill::DensityFor(int canvasWidth) {
return canvasWidth / kDpPerWidth;
}
void LinkPill::updateIcon() {
_icon = st::photoEditorLinkPillIcon.instance(
foreground(),
style::kScaleMax);
}
void LinkPill::paint(QPainter &p, QPointF origin, float64 scale) const {
p.save();
auto hq = PainterHighQualityEnabler(p);
p.translate(origin);
p.scale(scale, scale);
const auto pill = QRectF(QPointF(), _size);
const auto radius = pill.height() * kRadiusRatio;
p.setPen(Qt::NoPen);
p.setBrush(background());
p.drawRoundedRect(pill, radius, radius);
const auto iconSide = kIconSize * _density;
const auto icon = QRectF(
kPaddingLeft * _density,
(pill.height() - iconSide) / 2.,
iconSide,
iconSide);
p.drawImage(icon, _icon);
p.setFont(_font);
p.setPen(foreground());
p.drawText(
QPointF(
icon.x() + icon.width() + kIconPadding * _density,
(pill.height() - _font->height) / 2. + _font->ascent),
_text);
p.restore();
}
} // namespace Editor

View File

@@ -0,0 +1,58 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Editor {
struct LinkPreview {
QString url;
QString name;
bool captionAbove = true;
bool largePhoto = false;
bool preview = true;
bool dark = false;
friend inline bool operator==(
const LinkPreview &,
const LinkPreview &) = default;
};
enum class LinkStyle : uchar {
Framed,
SemiTransparent,
Opaque,
};
class LinkPill final {
public:
LinkPill(const LinkPreview &link, float64 density, int maxWidth);
[[nodiscard]] static float64 DensityFor(int canvasWidth);
[[nodiscard]] QSizeF size() const;
[[nodiscard]] LinkStyle style() const;
void setStyle(LinkStyle style);
void paint(QPainter &p, QPointF origin, float64 scale) const;
private:
[[nodiscard]] QColor background() const;
[[nodiscard]] QColor foreground() const;
void updateIcon();
float64 _density = 1.;
style::font _font;
QString _text;
int _textWidth = 0;
QSizeF _size;
QImage _icon;
LinkStyle _style = LinkStyle::Framed;
};
} // namespace Editor

View File

@@ -0,0 +1,397 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_message_render.h"
#include "data/data_cloud_file.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "editor/editor_message_source.h"
#include "editor/editor_message_video.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_media.h"
#include "main/main_session.h"
#include "ui/cached_round_corners.h"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/empty_userpic.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_themes_embedded.h"
#include "styles/style_chat.h"
namespace Editor {
namespace {
[[nodiscard]] QRect OpaqueBounds(const QImage &image) {
Expects(image.format() == QImage::Format_ARGB32_Premultiplied);
const auto width = image.width();
const auto height = image.height();
const auto rowHasContent = [&](int y) {
const auto row = reinterpret_cast<const uint32*>(
image.constScanLine(y));
for (auto x = 0; x != width; ++x) {
if (row[x]) {
return true;
}
}
return false;
};
auto top = 0;
while (top != height && !rowHasContent(top)) {
++top;
}
if (top == height) {
return QRect();
}
auto bottom = height - 1;
while (bottom > top && !rowHasContent(bottom)) {
--bottom;
}
auto left = width;
auto right = -1;
for (auto y = top; y <= bottom; ++y) {
const auto row = reinterpret_cast<const uint32*>(
image.constScanLine(y));
auto x = 0;
while (x < left && !row[x]) {
++x;
}
left = std::min(left, x);
x = width - 1;
while (x > right && !row[x]) {
--x;
}
right = std::max(right, x);
}
return QRect(left, top, right - left + 1, bottom - top + 1);
}
[[nodiscard]] std::unique_ptr<Ui::ChatTheme> MakeChatTheme(
std::optional<bool> dark,
rpl::lifetime &lifetime) {
if (!dark) {
return Window::Theme::DefaultChatThemeOn(lifetime);
}
return std::make_unique<Ui::ChatTheme>(Ui::ChatThemeDescriptor{
.preparePalette = Window::Theme::PreparePaletteCallback(
*dark,
std::nullopt),
.basedOnDark = *dark,
});
}
[[nodiscard]] float64 CornerRadius(
Ui::BubbleCornerRounding corner,
int ratio) {
using Corner = Ui::BubbleCornerRounding;
using Radius = Ui::CachedCornerRadius;
return (corner == Corner::Small)
? Ui::CachedCornerRadiusValue(Radius::BubbleSmall) * ratio
: (corner == Corner::Large)
? Ui::CachedCornerRadiusValue(Radius::BubbleLarge) * ratio
: 0.;
}
[[nodiscard]] QPainterPath HolePath(
QRect rect,
Ui::BubbleRounding rounding,
int ratio) {
const auto topLeft = CornerRadius(rounding.topLeft, ratio);
const auto topRight = CornerRadius(rounding.topRight, ratio);
const auto bottomLeft = CornerRadius(rounding.bottomLeft, ratio);
const auto bottomRight = CornerRadius(rounding.bottomRight, ratio);
const auto left = float64(rect.x());
const auto top = float64(rect.y());
const auto right = float64(rect.x() + rect.width());
const auto bottom = float64(rect.y() + rect.height());
const auto corner = [](float64 x, float64 y, float64 radius) {
return QRectF(x, y, 2 * radius, 2 * radius);
};
auto result = QPainterPath();
result.moveTo(left + topLeft, top);
result.lineTo(right - topRight, top);
result.arcTo(corner(right - 2 * topRight, top, topRight), 90, -90);
result.lineTo(right, bottom - bottomRight);
result.arcTo(
corner(
right - 2 * bottomRight,
bottom - 2 * bottomRight,
bottomRight),
0,
-90);
result.lineTo(left + bottomLeft, bottom);
result.arcTo(corner(left, bottom - 2 * bottomLeft, bottomLeft), 270, -90);
result.lineTo(left, top + topLeft);
result.arcTo(corner(left, top, topLeft), 180, -90);
result.closeSubpath();
return result;
}
} // namespace
class MessageRenderer::Delegate final
: public HistoryView::DefaultElementDelegate {
public:
explicit Delegate(not_null<Ui::PathShiftGradient*> gradient)
: _gradient(gradient) {
}
private:
HistoryView::Context elementContext() override {
return HistoryView::Context::MediaEditor;
}
bool elementAnimationsPaused() override {
return true;
}
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override {
return _gradient;
}
const not_null<Ui::PathShiftGradient*> _gradient;
};
MessageRenderer::MessageRenderer(std::shared_ptr<MessageSource> source)
: _source(std::move(source))
, _theme(Window::Theme::DefaultChatThemeOn(_lifetime))
, _style(
std::make_unique<Ui::ChatStyle>(
_source->session().colorIndicesValue()))
, _pathGradient(
HistoryView::MakePathShiftGradient(_style.get(), [=] { repaint(); }))
, _delegate(std::make_unique<Delegate>(_pathGradient.get())) {
_style->apply(_theme.get());
const auto session = &_source->session();
const auto data = &session->data();
data->viewRepaintRequest(
) | rpl::filter([=](const Data::RequestViewRepaint &request) {
return (request.view == _element.get());
}) | rpl::on_next([=] {
repaint();
}, _lifetime);
data->viewResizeRequest(
) | rpl::filter([=](not_null<HistoryView::Element*> view) {
return (view == _element.get());
}) | rpl::on_next([=] {
_layoutDirty = true;
repaint();
}, _lifetime);
data->itemViewRefreshRequest(
) | rpl::filter([=](not_null<const HistoryItem*> item) {
return (item == _source->item());
}) | rpl::on_next([=] {
_recreate = true;
repaint();
}, _lifetime);
data->itemDataChanges(
) | rpl::filter([=](not_null<HistoryItem*> item) {
return (item == _source->item());
}) | rpl::on_next([=] {
if (_element) {
_element->itemDataChanged();
_layoutDirty = true;
repaint();
}
}, _lifetime);
session->downloaderTaskFinished(
) | rpl::on_next([=] {
repaint();
}, _lifetime);
style::PaletteChanged(
) | rpl::on_next([=] {
_layoutDirty = true;
repaint();
}, _lifetime);
_source->removed(
) | rpl::on_next([=] {
_element = nullptr;
}, _lifetime);
createView();
}
MessageRenderer::~MessageRenderer() = default;
void MessageRenderer::setRepaintCallback(Fn<void()> callback) {
_repaint = std::move(callback);
}
void MessageRenderer::setDark(std::optional<bool> dark) {
if (_dark == dark) {
return;
}
_dark = dark;
_theme = MakeChatTheme(dark, _lifetime);
_style->apply(_theme.get());
_layoutDirty = true;
repaint();
}
bool MessageRenderer::ready() const {
return (_element != nullptr);
}
void MessageRenderer::createView() {
const auto item = _source->item();
_element = item ? item->createView(_delegate.get()) : nullptr;
_layoutDirty = true;
}
void MessageRenderer::repaint() {
_base = QImage();
if (_repaint) {
_repaint();
}
}
void MessageRenderer::layout() {
if (base::take(_recreate)) {
createView();
}
if (!_layoutDirty || !_element) {
return;
}
_layoutDirty = false;
_element->initDimensions();
_width = st::msgMargin.left()
+ st::msgMaxWidth
+ st::msgMargin.right()
+ (_element->hasFromPhoto() ? st::msgPhotoSkip : 0);
_element->resizeGetHeight(_width);
_base = renderFull(1);
_bounds = OpaqueBounds(_base).marginsAdded(Margins(1)) & _base.rect();
}
QSize MessageRenderer::size() {
layout();
return _bounds.size();
}
QRect MessageRenderer::mediaRect() {
layout();
return _bounds.isEmpty()
? QRect()
: elementMediaRect().translated(-_bounds.topLeft());
}
QRect MessageRenderer::elementMediaRect() const {
const auto media = _element ? _element->media() : nullptr;
return media
? media->contentRectForReactions().translated(
_element->mediaTopLeft())
: QRect();
}
QImage MessageRenderer::videoMask(int ratio) {
Expects(ratio > 0);
layout();
const auto video = _source->video();
const auto rect = elementMediaRect();
if (!video || rect.isEmpty()) {
return QImage();
}
auto mask = QImage(
rect.size() * ratio,
QImage::Format_ARGB32_Premultiplied);
mask.fill(Qt::transparent);
auto p = QPainter(&mask);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(Qt::white);
if (video->document()->isVideoMessage()) {
p.drawEllipse(mask.rect());
} else {
p.drawPath(HolePath(
mask.rect(),
_element->media()->adjustedBubbleRounding(),
ratio));
}
return mask;
}
QImage MessageRenderer::render(int ratio) {
Expects(ratio > 0);
layout();
if (!_element || _bounds.isEmpty()) {
return QImage();
}
const auto full = (ratio == 1 && !_base.isNull())
? _base
: renderFull(ratio);
const auto bounds = QRect(
_bounds.topLeft() * ratio,
_bounds.size() * ratio);
auto result = full.copy(bounds);
result.setDevicePixelRatio(ratio);
return result;
}
QImage MessageRenderer::renderFull(int ratio) {
const auto full = QSize(_width, _element->height());
auto result = QImage(
full * ratio,
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(ratio);
result.fill(Qt::transparent);
auto p = Painter(&result);
PainterHighQualityEnabler hq(p);
const auto rect = Rect(full);
auto context = _theme->preparePaintContext(
_style.get(),
rect,
rect,
rect,
true);
context.outbg = _element->hasOutLayout();
_element->draw(p, context);
paintUserpic(p, full.width(), full.height());
return result;
}
void MessageRenderer::paintUserpic(Painter &p, int width, int height) {
if (!_element->displayFromPhoto()) {
return;
}
const auto item = _element->data();
const auto left = st::historyPhotoLeft;
const auto top = height - _element->marginBottom() - st::msgPhotoSize;
const auto size = st::msgPhotoSize;
if (const auto from = _element->displayFrom()) {
from->paintUserpicLeft(p, _userpic, left, top, width, size);
} else if (const auto info = item->displayHiddenSenderInfo()) {
if (info->customUserpic.empty()) {
info->emptyUserpic.paintCircle(p, left, top, width, size);
} else if (!info->paintCustomUserpic(
p,
_userpic,
left,
top,
width,
size)) {
info->customUserpic.load(&_source->session(), item->fullId());
}
}
}
} // namespace Editor

View File

@@ -0,0 +1,70 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/userpic_view.h"
class Painter;
namespace HistoryView {
class Element;
} // namespace HistoryView
namespace Ui {
class ChatTheme;
class ChatStyle;
class PathShiftGradient;
} // namespace Ui
namespace Editor {
class MessageSource;
class MessageRenderer final {
public:
explicit MessageRenderer(std::shared_ptr<MessageSource> source);
~MessageRenderer();
void setRepaintCallback(Fn<void()> callback);
void setDark(std::optional<bool> dark);
[[nodiscard]] bool ready() const;
[[nodiscard]] QSize size();
[[nodiscard]] QRect mediaRect();
[[nodiscard]] QImage videoMask(int ratio);
[[nodiscard]] QImage render(int ratio);
private:
class Delegate;
void createView();
void layout();
void repaint();
[[nodiscard]] QImage renderFull(int ratio);
[[nodiscard]] QRect elementMediaRect() const;
void paintUserpic(Painter &p, int width, int height);
rpl::lifetime _lifetime;
const std::shared_ptr<MessageSource> _source;
std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
const std::unique_ptr<Delegate> _delegate;
std::unique_ptr<HistoryView::Element> _element;
Fn<void()> _repaint;
Ui::PeerUserpicView _userpic;
QImage _base;
QRect _bounds;
std::optional<bool> _dark;
int _width = 0;
bool _layoutDirty = true;
bool _recreate = false;
};
} // namespace Editor

View File

@@ -0,0 +1,172 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_message_source.h"
#include "base/unixtime.h"
#include "data/data_groups.h"
#include "data/data_media_types.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "editor/editor_message_video.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/history_view_fake_items.h"
#include "main/main_session.h"
namespace Editor {
namespace {
[[nodiscard]] TextWithEntities LinkText(const LinkPreview &link) {
auto result = TextWithEntities{
link.name.isEmpty() ? link.url : link.name,
};
result.entities.push_back(link.name.isEmpty()
? EntityInText(EntityType::Url, 0, result.text.size())
: EntityInText(
EntityType::CustomUrl,
0,
result.text.size(),
link.url));
return result;
}
[[nodiscard]] MTPMessageMedia LinkMedia(
const LinkPreview &link,
WebPageData *webpage) {
if (!webpage) {
return MTP_messageMediaEmpty();
}
using Flag = MTPDmessageMediaWebPage::Flag;
return MTP_messageMediaWebPage(
MTP_flags(link.largePhoto
? Flag::f_force_large_media
: Flag::f_force_small_media),
MTP_webPagePending(
MTP_flags(0),
MTP_long(webpage->id),
MTPstring(),
MTP_int(0)));
}
[[nodiscard]] not_null<History*> LinkHistory(
not_null<Main::Session*> session) {
const auto peerId = HistoryView::GenerateUser(
session->data().history(PeerData::kServiceNotificationsId),
u"Link"_q);
const auto result = session->data().history(peerId);
result->peer->setBarSettings(PeerBarSettings());
return result;
}
[[nodiscard]] not_null<HistoryItem*> MakeLinkItem(
not_null<Main::Session*> session,
const LinkPreview &link,
WebPageData *webpage) {
const auto history = LinkHistory(session);
return history->makeMessage(
{
.id = history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::HasFromId
| (link.captionAbove
? MessageFlag()
: MessageFlag::InvertMedia)),
.from = session->userPeerId(),
.date = base::unixtime::now(),
},
LinkText(link),
LinkMedia(link, webpage));
}
} // namespace
bool MessageForbidsRender(not_null<HistoryItem*> item) {
return item->isSponsored()
|| item->forbidsSaving()
|| !item->history()->peer->allowsForwarding();
}
bool CanRenderMessage(not_null<HistoryItem*> item) {
const auto media = item->media();
return !MessageForbidsRender(item)
&& !item->isEphemeral()
&& !item->showSimilarChannels()
&& !item->isLegacyMessage()
&& !dynamic_cast<const Data::MediaDice*>(media);
}
not_null<HistoryItem*> MessageToRender(not_null<HistoryItem*> item) {
const auto group = item->history()->owner().groups().find(item);
return group ? group->items.front() : item;
}
MessageSource::MessageSource(not_null<HistoryItem*> item)
: _session(&item->history()->session())
, _item(item) {
if (MessageVideo::Find(item)) {
_video = std::make_unique<MessageVideo>(item);
}
watchRemoval();
}
MessageSource::MessageSource(
not_null<Main::Session*> session,
LinkPreview link,
WebPageData *webpage)
: _session(session)
, _link(std::move(link))
, _webpage(webpage)
, _item(MakeLinkItem(session, *_link, webpage))
, _owned(true) {
watchRemoval();
}
MessageSource::~MessageSource() {
if (_owned) {
if (const auto item = base::take(_item)) {
item->destroy();
}
}
}
void MessageSource::watchRemoval() {
_session->data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> item) {
return (item == _item);
}) | rpl::on_next([=] {
_item = nullptr;
_removed.fire({});
}, _lifetime);
}
Main::Session &MessageSource::session() const {
return *_session;
}
HistoryItem *MessageSource::item() const {
return _item;
}
const std::optional<LinkPreview> &MessageSource::link() const {
return _link;
}
WebPageData *MessageSource::webpage() const {
return _webpage;
}
MessageVideo *MessageSource::video() const {
return _video.get();
}
rpl::producer<> MessageSource::removed() const {
return _removed.events();
}
} // namespace Editor

View File

@@ -0,0 +1,58 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/editor_link_pill.h"
class HistoryItem;
struct WebPageData;
namespace Main {
class Session;
} // namespace Main
namespace Editor {
class MessageVideo;
[[nodiscard]] bool MessageForbidsRender(not_null<HistoryItem*> item);
[[nodiscard]] bool CanRenderMessage(not_null<HistoryItem*> item);
[[nodiscard]] not_null<HistoryItem*> MessageToRender(
not_null<HistoryItem*> item);
class MessageSource final {
public:
explicit MessageSource(not_null<HistoryItem*> item);
MessageSource(
not_null<Main::Session*> session,
LinkPreview link,
WebPageData *webpage);
~MessageSource();
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] HistoryItem *item() const;
[[nodiscard]] const std::optional<LinkPreview> &link() const;
[[nodiscard]] WebPageData *webpage() const;
[[nodiscard]] MessageVideo *video() const;
[[nodiscard]] rpl::producer<> removed() const;
private:
void watchRemoval();
const not_null<Main::Session*> _session;
const std::optional<LinkPreview> _link;
WebPageData *_webpage = nullptr;
HistoryItem *_item = nullptr;
std::unique_ptr<MessageVideo> _video;
bool _owned = false;
rpl::event_stream<> _removed;
rpl::lifetime _lifetime;
};
} // namespace Editor

View File

@@ -0,0 +1,206 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_message_video.h"
#include "core/mime_type.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_file_origin.h"
#include "data/data_groups.h"
#include "data/data_media_types.h"
#include "data/data_session.h"
#include "data/data_streaming.h"
#include "editor/video/video_clip.h"
#include "history/history.h"
#include "history/history_item.h"
#include "main/main_session.h"
#include "media/media_video_encode.h"
#include "storage/storage_media_prepare.h"
#include "storage/streamed_file_downloader.h"
#include "ui/image/image_prepare.h"
#include <QtCore/QTemporaryFile>
namespace Editor {
namespace {
constexpr auto kMaxExtensionLength = 5;
[[nodiscard]] QString VideoExtension(not_null<DocumentData*> document) {
const auto extension = Core::FileExtension(document->filename());
const auto valid = !extension.isEmpty()
&& (extension.size() <= kMaxExtensionLength)
&& ranges::all_of(extension, [](QChar ch) {
return ch.isLetterOrNumber();
});
return valid ? extension : u"mp4"_q;
}
[[nodiscard]] QString TempVideoPath(not_null<DocumentData*> document) {
auto temp = QTemporaryFile(
Media::Encode::TempFileTemplate(VideoExtension(document)));
if (!temp.open()) {
return QString();
}
temp.setAutoRemove(false);
return temp.fileName();
}
} // namespace
DocumentData *MessageVideo::Find(not_null<HistoryItem*> item) {
const auto media = item->media();
const auto document = media ? media->document() : nullptr;
if (!document
|| media->webpage()
|| item->history()->owner().groups().find(item)) {
return nullptr;
}
const auto video = document->isVideoFile()
|| document->isGifv()
|| document->isVideoMessage();
const auto fits = (document->size > 0)
&& (document->size <= Images::kReadBytesLimit);
return (video && fits) ? document : nullptr;
}
MessageVideo::MessageVideo(not_null<HistoryItem*> item)
: _document(Find(item))
, _itemId(item->fullId()) {
}
MessageVideo::~MessageVideo() {
_lifetime.destroy();
_loader = nullptr;
if (!_tempPath.isEmpty()) {
QFile::remove(_tempPath);
}
}
not_null<DocumentData*> MessageVideo::document() const {
return _document;
}
void MessageVideo::load() {
if (_source || _loader || _waiting || _reading || _failed) {
return;
}
_media = _document->createMediaView();
if (_media->loaded(true)) {
const auto path = _document->filepath(true);
readSource(path, path.isEmpty() ? _media->bytes() : QByteArray());
return;
} else if (_document->loading()) {
waitForLoader();
return;
}
const auto origin = Data::FileOrigin(_itemId);
auto reader = _document->owner().streaming().sharedReader(
_document,
origin,
true);
const auto path = reader ? TempVideoPath(_document) : QString();
if (path.isEmpty()) {
fail();
return;
}
_tempPath = path;
_loader = _document->createStreamedDownloader(
std::move(reader),
origin,
std::nullopt,
path,
LoadToFileOnly,
LoadFromCloudOrLocal,
false);
_loader->updates(
) | rpl::on_next_error_done([=] {
_changes.fire({});
}, [=](FileLoader::Error) {
crl::on_main(base::make_weak(this), [=] { fail(); });
}, [=] {
readSource(_tempPath, QByteArray());
}, _lifetime);
_loader->start();
_changes.fire({});
}
bool MessageVideo::loading() const {
return (_loader != nullptr) || _waiting || _reading;
}
float64 MessageVideo::progress() const {
return _loader
? _loader->currentProgress()
: _waiting
? _document->progress()
: _reading
? 1.
: 0.;
}
std::shared_ptr<VideoClipSource> MessageVideo::source() const {
return _source;
}
rpl::producer<> MessageVideo::changes() const {
return _changes.events();
}
void MessageVideo::waitForLoader() {
_waiting = true;
const auto weak = base::make_weak(this);
const auto lifetime = std::make_shared<rpl::lifetime>();
_document->session().downloaderTaskFinished(
) | rpl::on_next_done([=] {
const auto strong = weak.get();
if (strong && strong->_document->loading()) {
return;
}
lifetime->destroy();
if (strong) {
strong->_waiting = false;
strong->load();
}
}, [=] {
lifetime->destroy();
}, *lifetime);
_changes.fire({});
}
void MessageVideo::readSource(
const QString &path,
const QByteArray &content) {
_reading = true;
const auto done = crl::guard(this, [=](
Storage::PhotoEditorMedia &&media) {
_reading = false;
_loader = nullptr;
if (!media.video()) {
fail();
return;
}
_source = std::make_shared<VideoClipSource>(VideoClipSource{
.path = std::move(media.videoPath),
.content = std::move(media.videoContent),
.thumbnail = std::move(media.image),
.duration = media.videoDuration,
.hasAudio = media.videoHasAudio,
});
_changes.fire({});
});
Storage::ReadPhotoEditorMediaAsync(path, content, done);
}
void MessageVideo::fail() {
_failed = true;
_loader = nullptr;
_changes.fire({});
}
} // namespace Editor

View File

@@ -0,0 +1,57 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
class DocumentData;
class FileLoader;
class HistoryItem;
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Editor {
struct VideoClipSource;
class MessageVideo final : public base::has_weak_ptr {
public:
[[nodiscard]] static DocumentData *Find(not_null<HistoryItem*> item);
explicit MessageVideo(not_null<HistoryItem*> item);
~MessageVideo();
[[nodiscard]] not_null<DocumentData*> document() const;
void load();
[[nodiscard]] bool loading() const;
[[nodiscard]] float64 progress() const;
[[nodiscard]] std::shared_ptr<VideoClipSource> source() const;
[[nodiscard]] rpl::producer<> changes() const;
private:
void waitForLoader();
void readSource(const QString &path, const QByteArray &content);
void fail();
const not_null<DocumentData*> _document;
const FullMsgId _itemId;
std::shared_ptr<Data::DocumentMedia> _media;
std::unique_ptr<FileLoader> _loader;
std::shared_ptr<VideoClipSource> _source;
QString _tempPath;
bool _waiting = false;
bool _reading = false;
bool _failed = false;
rpl::event_stream<> _changes;
rpl::lifetime _lifetime;
};
} // namespace Editor

View File

@@ -7,25 +7,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_paint.h"
#include "apiwrap.h"
#include "base/platform/base_platform_haptic.h"
#include "base/qthelp_url.h"
#include "chat_helpers/compose/compose_show.h"
#include "core/file_utilities.h"
#include "core/mime_type.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "editor/controllers/controllers.h"
#include "editor/editor_link_box.h"
#include "editor/editor_message_render.h"
#include "editor/editor_message_source.h"
#include "editor/scene/scene_item_canvas.h"
#include "editor/scene/scene_item_image.h"
#include "editor/scene/scene_item_link.h"
#include "editor/scene/scene_item_message.h"
#include "editor/scene/scene_item_shape.h"
#include "editor/scene/scene_item_sticker.h"
#include "editor/scene/scene_item_text.h"
#include "editor/scene/scene_item_video.h"
#include "editor/scene/scene.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_single_player.h"
#include "main/main_session.h"
#include "platform/platform_file_utilities.h"
#include "storage/storage_media_prepare.h"
#include "ui/boxes/confirm_box.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "window/themes/window_theme.h"
#include "styles/style_editor.h"
#include <QGraphicsView>
#include <QNativeGestureEvent>
@@ -40,6 +55,28 @@ constexpr auto kMaxBrush = 25.;
constexpr auto kMinBrush = 1.;
constexpr auto kShapeSizeRatio = 2. / 5.;
constexpr auto kMediaSizeRatio = 1. / 2.;
constexpr auto kImageMaxSizeRatio = 4.;
constexpr auto kMessageMaxWidthRatio = 0.88;
constexpr auto kMessageMaxHeightRatio = 0.7;
constexpr auto kMessagesCascadeRatio = 1. / 20.;
[[nodiscard]] bool IsForwardMimeData(not_null<const QMimeData*> data) {
return data->hasFormat(u"application/x-td-forward"_q);
}
[[nodiscard]] QString MimeLinkUrl(not_null<const QMimeData*> data) {
const auto urls = Core::ReadMimeUrls(data);
const auto text = (urls.size() == 1 && !urls.front().isLocalFile())
? urls.front().toString()
: urls.isEmpty()
? Core::ReadMimeText(data).trimmed()
: QString();
if (text.isEmpty() || text.contains('\n') || text.contains(' ')) {
return QString();
}
return qthelp::validate_url(text);
}
[[nodiscard]] float64 BrushSize(const Brush &brush) {
return kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio;
@@ -90,7 +127,8 @@ Paint::Paint(
, _viewport(_view->viewport())
, _imageSize(imageSize)
, _fixedCrop(data.fixedCrop)
, _composeAnimated(data.composeAnimated) {
, _composeAnimated(data.composeAnimated)
, _composeSound(data.composeSound) {
Expects(modifications.paint != nullptr);
_scene->setBlurSource(std::move(blurSource));
@@ -176,6 +214,10 @@ Paint::Paint(
controllers->stickersPanelController->stickerChosen(
) | rpl::map_to(ShowRequest::HideAnimated),
controllers->stickersPanelController->photoRequests(
) | rpl::map_to(ShowRequest::HideAnimated),
controllers->stickersPanelController->audioRequests(
) | rpl::map_to(ShowRequest::HideAnimated),
controllers->stickersPanelController->linkRequests(
) | rpl::map_to(ShowRequest::HideAnimated)));
controllers->stickersPanelController->stickerChosen(
@@ -189,6 +231,16 @@ Paint::Paint(
) | rpl::on_next([=] {
choosePhotoFile();
}, lifetime());
controllers->stickersPanelController->audioRequests(
) | rpl::on_next([=] {
chooseAudioFile();
}, lifetime());
controllers->stickersPanelController->linkRequests(
) | rpl::on_next([=] {
chooseLink(QString());
}, lifetime());
}
_scene->pendingShapeStates(
@@ -235,7 +287,7 @@ bool Paint::zoomSceneItems(float64 wheelDelta, bool fine) {
}
bool Paint::zoomSceneItemsByFactor(float64 factor) {
const auto center = rect::center(_scene->sceneRect());
const auto center = rect::center(_scene->canvasRect());
auto applied = false;
for (const auto &item : _scene->items()) {
const auto raw = item.get();
@@ -378,28 +430,37 @@ Paint::~Paint() {
}
void Paint::updateViewGeometry() {
if (_imageGeometry.isEmpty()) {
if (_canvasGeometry.isEmpty()) {
return;
}
const auto target = (_transform.userZoom - kMinCanvasZoom) > kZoomEpsilon
? _outerGeometry
: _imageGeometry;
: _canvasGeometry;
if (geometry() != target) {
setGeometry(target);
}
_view->setGeometry(rect());
}
void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
if (geometry.isEmpty()) {
void Paint::applyTransform(
QRect geometry,
QRect canvasGeometry,
QRectF canvas,
int angle,
bool flipped) {
if (geometry.isEmpty() || canvasGeometry.isEmpty()) {
return;
}
_imageGeometry = geometry;
_canvasGeometry = canvasGeometry;
_canvas = canvas;
_outerGeometry = parentWidget() ? parentWidget()->rect() : geometry;
_view->setSceneRect((canvas == _scene->sceneRect()) ? QRectF() : canvas);
_scene->setCanvasRect(canvas);
const auto center = (_transform.fitZoom <= 0.)
|| _view->viewport()->rect().isEmpty()
? rect::center(_scene->sceneRect())
|| (_transform.userZoom == kMinCanvasZoom)
? rect::center(canvas)
: _view->mapToScene(_view->viewport()->rect().center());
const auto size = geometry.size();
@@ -431,7 +492,7 @@ void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
std::shared_ptr<Scene> Paint::saveScene() const {
_scene->save(SaveState::Save);
return _scene->items().empty()
return (_scene->items().empty() && !_scene->audio())
? nullptr
: _scene;
}
@@ -510,9 +571,18 @@ void Paint::disarmShapeTool() {
}
bool Paint::handleKeyPress(not_null<QKeyEvent*> e) {
if ((e->key() == Qt::Key_Escape) && _scene->hasPendingShape()) {
const auto key = e->key();
if ((key == Qt::Key_Escape) && _scene->hasPendingShape()) {
disarmShapeTool();
return true;
} else if (!_scene->audioSelected()) {
return false;
} else if (key == Qt::Key_Escape) {
_scene->setAudioSelected(false);
return true;
} else if ((key == Qt::Key_Delete) || (key == Qt::Key_Backspace)) {
removeAudio();
return true;
}
return false;
}
@@ -521,6 +591,50 @@ void Paint::clearSelection() {
_scene->clearSelection();
}
void Paint::removeAudio() {
_scene->setAudio(nullptr);
}
void Paint::setAudioSelected(bool selected) {
_scene->setAudioSelected(selected);
}
bool Paint::canEqualizeDurations() const {
return _scene->canEqualizeDurations();
}
void Paint::matchDurations(crl::time duration) {
_scene->matchDurations(duration);
}
bool Paint::durationsLinked() const {
return _scene->durationsLinked();
}
void Paint::setDurationsLinked(bool linked) {
_scene->setDurationsLinked(linked);
}
rpl::producer<> Paint::durationsLinkChanges() const {
return _scene->durationsLinkChanges();
}
std::shared_ptr<AudioTrack> Paint::audio() const {
return _scene->audio();
}
bool Paint::audioSelected() const {
return _scene->audioSelected();
}
rpl::producer<> Paint::audioChanges() const {
return _scene->audioChanges();
}
rpl::producer<bool> Paint::audioSelectedChanges() const {
return _scene->audioSelectedChanges();
}
void Paint::applyTextPrefs(const TextPrefs &prefs) {
_scene->applyTextPrefs(prefs);
}
@@ -557,6 +671,11 @@ rpl::producer<QColor> Paint::shapeItemSelections() const {
return _scene->shapeItemSelections();
}
auto Paint::videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>> {
return _scene->videoClipSelections();
}
rpl::producer<> Paint::shapeItemDeselections() const {
return _scene->shapeItemDeselections();
}
@@ -566,12 +685,25 @@ rpl::producer<bool> Paint::shapeToolStates() const {
}
bool Paint::canHandleMimeData(const QMimeData *data) const {
return data
&& !_textEditing.current()
&& Storage::ValidatePhotoEditorMediaDragData(data, _composeAnimated);
if (!data || _textEditing.current()) {
return false;
} else if (session()
&& (IsForwardMimeData(data) || !MimeLinkUrl(data).isEmpty())) {
return true;
}
return Storage::ValidatePhotoEditorMediaDragData(
data,
_composeAnimated,
_composeSound);
}
void Paint::handleMimeData(const QMimeData *data) {
if (IsForwardMimeData(data)) {
if (const auto session = Paint::session()) {
addMessages(session->data().takeMimeForwardIds());
}
return;
}
const auto urls = Core::ReadMimeUrls(data);
if (urls.size() == 1 && urls.front().isLocalFile()) {
readMediaFile(
@@ -579,22 +711,168 @@ void Paint::handleMimeData(const QMimeData *data) {
QByteArray());
} else if (auto read = Core::ReadMimeImage(data)) {
addMedia({ .image = std::move(read.image) });
} else if (const auto url = MimeLinkUrl(data); !url.isEmpty()) {
chooseLink(url);
} else {
addMedia({});
}
}
Main::Session *Paint::session() const {
const auto &show = _controllers->sessionShow;
return show ? &show->session() : nullptr;
}
void Paint::addMessages(const MessageIdsList &ids) {
const auto session = Paint::session();
if (!session) {
return;
}
auto added = base::flat_set<not_null<HistoryItem*>>();
auto forbidden = (HistoryItem*)nullptr;
auto index = 0;
for (const auto &id : ids) {
const auto item = session->data().message(id);
if (!item) {
continue;
}
const auto render = MessageToRender(item);
if (!added.emplace(render).second) {
continue;
} else if (MessageForbidsRender(render)) {
forbidden = render;
continue;
} else if (!CanRenderMessage(render)) {
if (render->hasDirectLink()) {
addLinkItem({
.url = session->api().exportDirectMessageLink(
render,
false),
.preview = false,
});
}
continue;
}
addMessageItem(std::make_shared<MessageSource>(render), index++);
}
if (forbidden) {
_controllers->show->showBox(Ui::MakeInformBox(
forbidden->history()->peer->isBroadcast()
? tr::lng_error_noforwards_channel()
: tr::lng_error_noforwards_group()));
}
}
void Paint::addMessageItem(
std::shared_ptr<MessageSource> source,
int index,
std::optional<bool> dark,
std::optional<QPointF> position) {
auto renderer = std::make_unique<MessageRenderer>(source);
renderer->setDark(dark);
auto data = messageItemData(renderer->size());
const auto scene = _scene->sceneRect().size();
const auto shift = int(std::min(scene.width(), scene.height())
* kMessagesCascadeRatio) * index;
data.x = position ? int(position->x()) : (data.x + shift);
data.y = position ? int(position->y()) : (data.y + shift);
const auto item = std::make_shared<ItemMessage>(
std::move(source),
std::move(renderer),
std::move(data),
dark,
MessageVideoOptions{
.play = _composeAnimated,
.sound = _composeSound,
});
item->setEditCallback(crl::guard(this, [=](
not_null<ItemMessage*> item) {
const auto &link = item->source()->link();
chooseLink(link ? link->url : QString(), item);
}));
addMediaItem(item);
}
void Paint::addLinkItem(LinkPreview link, std::optional<QPointF> position) {
auto data = itemBaseData();
data.size = std::max(
int(std::ceil(ItemLink::MakePill(link, _imageSize).size().width())),
1);
if (position) {
data.x = int(position->x());
data.y = int(position->y());
}
const auto item = std::make_shared<ItemLink>(std::move(link), data);
item->setEditCallback(crl::guard(this, [=](not_null<ItemLink*> item) {
chooseLink(item->link().url, item);
}));
addMediaItem(item);
}
void Paint::chooseLink(const QString &url, ItemBase *editing) {
const auto &show = _controllers->sessionShow;
if (!show) {
return;
}
auto link = std::optional<LinkPreview>();
if (!editing) {
} else if (editing->type() == ItemMessage::Type) {
const auto item = static_cast<ItemMessage*>(editing);
link = item->source()->link();
if (link) {
link->dark = item->dark().value_or(
Window::Theme::IsNightMode());
}
} else if (editing->type() == ItemLink::Type) {
link = static_cast<ItemLink*>(editing)->link();
}
const auto weak = editing
? std::weak_ptr(_scene->itemShared(editing))
: std::weak_ptr<NumberedItem>();
_controllers->layerShow->showBox(LinkBox({
.show = show,
.url = url,
.editing = link,
.done = crl::guard(this, [=](LinkBoxResult &&result) {
applyLinkResult(std::move(result), weak);
}),
}));
}
void Paint::applyLinkResult(
LinkBoxResult &&result,
std::weak_ptr<NumberedItem> editing) {
const auto strong = editing.lock();
const auto raw = (strong && strong->isNormalStatus())
? strong.get()
: nullptr;
if (raw && result.message && raw->type() == ItemMessage::Type) {
const auto item = static_cast<ItemMessage*>(raw);
item->setSource(std::move(result.message));
item->setDark(result.dark);
return;
} else if (raw && result.pill && raw->type() == ItemLink::Type) {
static_cast<ItemLink*>(raw)->setLink(std::move(*result.pill));
return;
}
const auto position = raw
? std::make_optional(raw->scenePos())
: std::nullopt;
if (raw) {
_scene->removeItem(strong);
}
if (result.message) {
addMessageItem(std::move(result.message), 0, result.dark, position);
} else if (result.pill) {
addLinkItem(std::move(*result.pill), position);
}
}
void Paint::readMediaFile(const QString &path, const QByteArray &content) {
const auto done = crl::guard(this, [=](
Storage::ReadPhotoEditorMediaAsync(path, content, crl::guard(this, [=](
Storage::PhotoEditorMedia &&media) {
addMedia(std::move(media));
});
crl::async([=] {
auto media = Storage::ReadPhotoEditorMedia(path, content);
crl::on_main([=, media = std::move(media)]() mutable {
done(std::move(media));
});
});
}));
}
void Paint::choosePhotoFile() {
@@ -615,9 +893,52 @@ void Paint::choosePhotoFile() {
crl::guard(this, callback));
}
void Paint::chooseAudioFile() {
const auto callback = [=](FileDialog::OpenResult &&result) {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
return;
}
readAudioFile(
result.paths.isEmpty() ? QString() : result.paths.front(),
result.remoteContent);
};
FileDialog::GetOpenPath(
this,
tr::lng_choose_audio(tr::now),
FileDialog::AudioFilesFilter(),
crl::guard(this, callback));
}
void Paint::readAudioFile(const QString &path, const QByteArray &content) {
const auto done = crl::guard(this, [=](AudioTrack &&track) {
addAudio(std::move(track));
});
crl::async([=] {
auto track = Storage::ReadPhotoEditorAudio(path, content);
crl::on_main([=, track = std::move(track)]() mutable {
done(std::move(track));
});
});
}
void Paint::addAudio(AudioTrack &&track) {
if (track.empty() || (track.duration <= 0)) {
_controllers->show->showBox(
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
return;
}
disarmShapeTool();
_scene->setAudio(std::make_shared<AudioTrack>(std::move(track)));
_scene->setAudioSelected(true);
}
void Paint::addMedia(Storage::PhotoEditorMedia &&media) {
const auto &image = media.image;
if (!media
if (!media.audio.empty() && _composeSound) {
addAudio(std::move(media.audio));
return;
} else if (!media
|| !media.audio.empty()
|| (media.video() && !_composeAnimated)
|| !Ui::ValidateThumbDimensions(image.width(), image.height())) {
_controllers->show->showBox(
@@ -633,11 +954,12 @@ void Paint::addMedia(Storage::PhotoEditorMedia &&media) {
void Paint::addVideoItem(Storage::PhotoEditorMedia &&media) {
const auto data = mediaItemData(media.image.size());
addMediaItem(std::make_shared<ItemVideo>(
std::make_shared<ItemVideo::Source>(ItemVideo::Source{
std::make_shared<VideoClipSource>(VideoClipSource{
.path = std::move(media.videoPath),
.content = std::move(media.videoContent),
.thumbnail = std::move(media.image),
.duration = media.videoDuration,
.hasAudio = media.videoHasAudio && _composeSound,
}),
data));
}
@@ -651,7 +973,8 @@ void Paint::addImageItem(QImage &&image) {
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
const auto data = mediaItemData(image.size());
auto data = mediaItemData(image.size());
data.maxSizeRatio = kImageMaxSizeRatio;
addMediaItem(std::make_shared<ItemImage>(
Ui::PixmapFromImage(std::move(image)),
data));
@@ -666,6 +989,34 @@ void Paint::addMediaItem(std::shared_ptr<ItemBase> item) {
_view->setFocus();
}
void Paint::setCanvasBackground(
const Media::Encode::CanvasBackground &background) {
_background = background;
}
void Paint::setCropRect(QRectF crop) {
_cropRect = crop;
}
void Paint::paintCanvas(QPainter &p) const {
const auto image = QRectF(Rect(_imageSize));
if (_view->geometry().isEmpty()
|| !_background.valid()
|| image.contains(_canvas)) {
return;
}
const auto transform = _view->viewportTransform();
const auto imageDisplay = transform.mapRect(image).toRect();
const auto cropDisplay = transform.mapRect(_cropRect).toAlignedRect();
p.save();
p.translate(pos());
p.setClipRegion(
QRegion(rect()) - QRegion(imageDisplay),
Qt::IntersectClip);
Media::Encode::PaintCanvasBackground(p, cropDisplay, _background);
p.restore();
}
void Paint::paintImage(QPainter &p, const QPixmap &image) const {
if (_view->geometry().isEmpty()) {
return;
@@ -687,7 +1038,7 @@ void Paint::resetView() {
_transform.userZoom = kMinCanvasZoom;
updateViewGeometry();
applyViewTransform();
_view->centerOn(rect::center(_scene->sceneRect()));
_view->centerOn(rect::center(_canvas));
if (const auto parent = parentWidget()) {
parent->update(geometry());
}
@@ -696,20 +1047,38 @@ void Paint::resetView() {
ItemBase::Data Paint::itemBaseData() const {
const auto s = _scene->sceneRect().toRect().size();
const auto size = std::min(s.width(), s.height()) / 2;
const auto x = s.width() / 2;
const auto y = s.height() / 2;
const auto center = rect::center(_scene->canvasRect().toRect());
return ItemBase::Data{
.initialZoom = _transform.zoom,
.zPtr = _scene->lastZ(),
.size = size,
.x = x,
.y = y,
.x = center.x(),
.y = center.y(),
.flipped = _transform.flipped,
.rotation = -_transform.angle,
.imageSize = _imageSize,
};
}
ItemBase::Data Paint::messageItemData(QSize bubbleSize) const {
auto result = itemBaseData();
if (bubbleSize.isEmpty()) {
return result;
}
const auto scene = _scene->sceneRect().size();
const auto scale = scene.width()
/ float64(st::photoEditorMessageReferenceWidth);
const auto width = bubbleSize.width() * scale;
const auto height = bubbleSize.height() * scale;
const auto fit = std::min({
1.,
scene.width() * kMessageMaxWidthRatio / width,
scene.height() * kMessageMaxHeightRatio / height,
});
result.size = std::max(int(std::ceil(width * fit)), 1);
return result;
}
ItemBase::Data Paint::mediaItemData(QSize mediaSize) const {
auto result = itemBaseData();
if (mediaSize.isEmpty()) {

View File

@@ -8,17 +8,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "base/timer.h"
#include "data/data_types.h"
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
#include "editor/scene/scene_item_base.h"
#include "media/media_video_canvas.h"
class QGraphicsItem;
class QGraphicsView;
class QKeyEvent;
namespace Main {
class Session;
} // namespace Main
namespace Storage {
struct PhotoEditorMedia;
} // namespace Storage
@@ -26,7 +32,11 @@ struct PhotoEditorMedia;
namespace Editor {
struct Controllers;
struct LinkBoxResult;
struct LinkPreview;
class MessageSource;
class Scene;
class VideoClip;
// Paint control.
class Paint final : public Ui::RpWidget {
@@ -43,7 +53,12 @@ public:
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
void restoreScene();
void applyTransform(QRect geometry, int angle, bool flipped);
void applyTransform(
QRect geometry,
QRect canvasGeometry,
QRectF canvas,
int angle,
bool flipped);
void applyBrush(const Brush &brush);
void cancel();
void keepResult();
@@ -54,6 +69,17 @@ public:
void armShapeTool(ShapeType shape, const Brush &brush, bool fill);
void disarmShapeTool();
void clearSelection();
void removeAudio();
void setAudioSelected(bool selected);
[[nodiscard]] bool canEqualizeDurations() const;
void matchDurations(crl::time duration);
[[nodiscard]] bool durationsLinked() const;
void setDurationsLinked(bool linked);
[[nodiscard]] rpl::producer<> durationsLinkChanges() const;
[[nodiscard]] std::shared_ptr<AudioTrack> audio() const;
[[nodiscard]] bool audioSelected() const;
[[nodiscard]] rpl::producer<> audioChanges() const;
[[nodiscard]] rpl::producer<bool> audioSelectedChanges() const;
void applyTextPrefs(const TextPrefs &prefs);
void setTextColor(const QColor &color);
void setSelectedTextColor(const QColor &color);
@@ -69,9 +95,15 @@ public:
[[nodiscard]] rpl::producer<QColor> shapeItemSelections() const;
[[nodiscard]] rpl::producer<> shapeItemDeselections() const;
[[nodiscard]] rpl::producer<bool> shapeToolStates() const;
[[nodiscard]] auto videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>>;
[[nodiscard]] bool canHandleMimeData(const QMimeData *data) const;
void handleMimeData(const QMimeData *data);
void setCanvasBackground(
const Media::Encode::CanvasBackground &background);
void setCropRect(QRectF crop);
void paintCanvas(QPainter &p) const;
void paintImage(QPainter &p, const QPixmap &image) const;
void resetView();
@@ -92,14 +124,32 @@ private:
bool undid = false;
};
[[nodiscard]] Main::Session *session() const;
ItemBase::Data itemBaseData() const;
ItemBase::Data mediaItemData(QSize mediaSize) const;
ItemBase::Data messageItemData(QSize bubbleSize) const;
void addMediaItem(std::shared_ptr<ItemBase> item);
void addMessages(const MessageIdsList &ids);
void addMessageItem(
std::shared_ptr<MessageSource> source,
int index = 0,
std::optional<bool> dark = std::nullopt,
std::optional<QPointF> position = std::nullopt);
void addLinkItem(
LinkPreview link,
std::optional<QPointF> position = std::nullopt);
void chooseLink(const QString &url, ItemBase *editing = nullptr);
void applyLinkResult(
LinkBoxResult &&result,
std::weak_ptr<NumberedItem> editing);
void addMedia(Storage::PhotoEditorMedia &&media);
void readMediaFile(const QString &path, const QByteArray &content);
void addImageItem(QImage &&image);
void addVideoItem(Storage::PhotoEditorMedia &&media);
void choosePhotoFile();
void chooseAudioFile();
void readAudioFile(const QString &path, const QByteArray &content);
void addAudio(AudioTrack &&track);
void applyViewTransform();
void bakeTextScales();
@@ -112,8 +162,12 @@ private:
const QSize _imageSize;
const bool _fixedCrop = false;
const bool _composeAnimated = false;
QRect _imageGeometry;
const bool _composeSound = false;
QRect _canvasGeometry;
QRect _outerGeometry;
QRectF _canvas;
QRectF _cropRect;
Media::Encode::CanvasBackground _background;
struct {
int angle = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,207 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "base/weak_ptr.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
#include "ui/ui_utility.h"
namespace Ui {
class MaskedInputField;
} // namespace Ui
namespace Editor {
struct TrimTimelineDescriptor {
crl::time duration = 0;
crl::time maxDuration = 0;
crl::time minDuration = 0;
// A zero |till| means the whole allowed window is selected.
crl::time from = 0;
crl::time till = 0;
crl::time cover = 0;
bool trimOnly = false;
};
class TrimTimeline
: public Ui::RpWidget
, public base::has_weak_ptr {
public:
TrimTimeline(
not_null<Ui::RpWidget*> parent,
TrimTimelineDescriptor descriptor);
[[nodiscard]] crl::time from() const {
return _from;
}
[[nodiscard]] crl::time till() const {
return _till;
}
[[nodiscard]] crl::time cover() const {
return _cover;
}
[[nodiscard]] crl::time playbackPosition() const {
return _playback;
}
[[nodiscard]] float64 zoom() const {
return _zoom;
}
[[nodiscard]] crl::time visibleFrom() const;
[[nodiscard]] crl::time visibleTill() const;
[[nodiscard]] rpl::producer<crl::time> trimChanges() const {
return _trimChanges.events();
}
[[nodiscard]] rpl::producer<crl::time> coverChanges() const {
return _coverChanges.events();
}
[[nodiscard]] rpl::producer<bool> draggingChanges() const {
return _draggingChanges.events();
}
void setTrim(crl::time from, crl::time till);
void setPlaybackPosition(crl::time position);
void commitPendingEdit();
void setSizeLabel(const QString &text);
[[nodiscard]] bool draggingHead() const;
[[nodiscard]] int resizeGetHeight(int newWidth) override;
protected:
[[nodiscard]] crl::time duration() const {
return _duration;
}
[[nodiscard]] bool trimOnly() const {
return _trimOnly;
}
[[nodiscard]] QRect stripRect() const;
[[nodiscard]] crl::time timeAt(int x) const;
[[nodiscard]] int xAt(crl::time time) const;
[[nodiscard]] float64 visibleSpan() const;
virtual void paintStrip(QPainter &p, const QRect &strip) = 0;
virtual void paintOverlay(QPainter &p);
virtual void headGrabChanged(bool grabbed);
virtual void visibleRangeChanged();
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
void leaveEventHook(QEvent *e) override;
bool eventHook(QEvent *e) override;
private:
enum class Grab {
None,
Left,
Right,
Head,
Window,
Scroll,
Hint,
Label,
};
struct DurationLabel {
QString text;
QRect rect;
bool sizeShown = false;
};
[[nodiscard]] QRect labelRect() const;
[[nodiscard]] DurationLabel durationLabel() const;
[[nodiscard]] QRect durationHitRect() const;
[[nodiscard]] QRect durationFieldRect() const;
[[nodiscard]] QString durationEditText() const;
void updateDurationFieldGeometry();
void editDuration();
void finishDurationEdit(bool apply, bool restoreFocus);
void applyDurationText(const QString &text);
[[nodiscard]] crl::time minSelection() const;
void moveWindowTo(crl::time center);
[[nodiscard]] Grab grabAt(
QPoint position,
Qt::KeyboardModifiers modifiers) const;
[[nodiscard]] float64 maxZoom() const;
bool setVisibleRange(float64 zoom, float64 from);
void zoomBy(float64 factor, int anchorX);
bool scrollBy(float64 pixels);
void updateEdgeScroll(QPoint position);
bool edgeScrollStep(crl::time now);
[[nodiscard]] bool grabClamped(bool forward) const;
[[nodiscard]] bool selectionHiddenLeft() const;
[[nodiscard]] bool selectionHiddenRight() const;
[[nodiscard]] QRect hintRect(bool left) const;
void updateHints();
void scrollToSelection();
[[nodiscard]] bool grabMovesSelection() const;
void applyGrab(QPoint position);
void releaseGrab();
void setCover(crl::time cover, bool notify);
void updateCursor(Grab grab);
void paintSelection(QPainter &p, const QRect &strip);
void paintOverview(QPainter &p, const QRect &strip);
void paintHints(QPainter &p);
void paintHead(QPainter &p, const QRect &strip);
void paintDuration(QPainter &p);
const crl::time _duration = 0;
const crl::time _maxDuration = 0;
const crl::time _minDuration = 0;
const bool _trimOnly = false;
crl::time _from = 0;
crl::time _till = 0;
crl::time _cover = 0;
// Negative means nothing played yet; zero is a real position.
crl::time _playback = -1;
float64 _zoom = 1.;
float64 _visibleFrom = 0.;
QString _sizeLabel;
int _labelWidth = 0;
int _durationFieldWidth = 0;
base::unique_qptr<Ui::MaskedInputField> _durationField;
QPointer<QWidget> _durationFocusReturn;
Grab _grab = Grab::None;
Qt::MouseButton _grabButton = Qt::NoButton;
int _grabShift = 0;
QPoint _dragPosition;
int _edgeOvershoot = 0;
crl::time _edgeScrollLast = 0;
Ui::Animations::Basic _edgeScrollAnimation;
Ui::ScrollDirectionLock _wheelDirectionLock;
bool _hintLeftShown = false;
bool _hintRightShown = false;
bool _hintGrabLeft = false;
Ui::Animations::Simple _hintLeft;
Ui::Animations::Simple _hintRight;
Ui::Animations::Simple _scrollAnimation;
rpl::event_stream<crl::time> _trimChanges;
rpl::event_stream<crl::time> _coverChanges;
rpl::event_stream<bool> _draggingChanges;
};
} // namespace Editor

View File

@@ -15,7 +15,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/photo_editor_controls.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "ui/layers/layer_manager.h"
#include "ui/layers/layer_widget.h"
#include "styles/style_calls.h"
#include "styles/style_editor.h"
namespace Editor {
@@ -25,6 +27,14 @@ constexpr auto kPrecision = 100000;
constexpr auto kBrushesVersion = -2;
constexpr auto kDefaultBrushSizeRatio = 0.9;
[[nodiscard]] std::unique_ptr<Ui::LayerManager> MakeLayers(
not_null<Ui::RpWidget*> parent) {
auto result = std::make_unique<Ui::LayerManager>(parent);
result->setStyleOverrides(&st::groupCallBox, &st::groupCallLayerBox);
result->setHideByBackgroundClick(true);
return result;
}
[[nodiscard]] int ToolIndex(Brush::Tool tool) {
switch (tool) {
case Brush::Tool::Pen: return 0;
@@ -274,14 +284,18 @@ PhotoEditor::PhotoEditor(
EditorData data)
: RpWidget(parent)
, _modifications(std::move(modifications))
, _layers(MakeLayers(this))
, _controllers(std::make_shared<Controllers>(
sessionShow
? std::make_unique<StickersPanelController>(
this,
std::move(sessionShow))
sessionShow,
data.composeSound)
: nullptr,
std::make_unique<UndoController>(),
show))
show,
_layers->uiShow(),
sessionShow))
, _content(base::make_unique_q<PhotoEditorContent>(
this,
photo,
@@ -308,6 +322,13 @@ PhotoEditor::PhotoEditor(
_modifications.cropType = data.cropType;
_modifications.cropMode = data.cropMode;
events(
) | rpl::on_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::WindowDeactivate) {
_content->setExpansionRoom(false);
}
}, lifetime());
sizeValue(
) | rpl::on_next([=](const QSize &size) {
if (size.isEmpty()) {
@@ -316,6 +337,73 @@ PhotoEditor::PhotoEditor(
_content->setGeometry(rect() - st::photoEditorContentMargins);
}, lifetime());
_content->videoClipSelections(
) | rpl::on_next([=](std::shared_ptr<VideoClip> clip) {
_videoClipSelected = (clip != nullptr);
updateColorPickerVisibility(anim::type::normal);
_controls->setVideoClip(std::move(clip));
_controls->setTrimShortestAvailable(
_content->canEqualizeDurations());
_controls->setTrimShortestActive(
_content->durationsLinked(),
anim::type::instant);
}, lifetime());
rpl::merge(
_content->audioSelectedChanges(),
_content->audioChanges() | rpl::map([=] {
return _content->audioSelected();
})
) | rpl::on_next([=](bool selected) {
_audioSelected = selected;
updateColorPickerVisibility(anim::type::normal);
_controls->setAudioTrack(selected ? _content->audio() : nullptr);
_controls->setTrimShortestAvailable(
_content->canEqualizeDurations());
_controls->setTrimShortestActive(
_content->durationsLinked(),
anim::type::instant);
}, lifetime());
_content->audioVolumeChanges(
) | rpl::on_next([=] {
_controls->refreshAudioVolume();
}, lifetime());
_content->durationsLinkChanges(
) | rpl::on_next([=] {
_controls->setTrimShortestAvailable(
_content->canEqualizeDurations());
_controls->setTrimShortestActive(
_content->durationsLinked(),
anim::type::instant);
_controls->refreshTimelines();
}, lifetime());
_controls->audioRemoveRequests(
) | rpl::on_next([=] {
_content->removeAudio();
}, lifetime());
_controls->trimShortestRequests(
) | rpl::on_next([=] {
const auto linked = !_content->durationsLinked();
_content->setDurationsLinked(linked);
_controls->setTrimShortestActive(linked, anim::type::normal);
_controls->refreshTimelines();
}, lifetime());
_controls->trimLengthChanges(
) | rpl::on_next([=](crl::time length) {
if (!_content->durationsLinked() || _matchingDurations) {
return;
}
_matchingDurations = true;
_content->matchDurations(length);
_controls->refreshTimelines();
_matchingDurations = false;
}, lifetime());
_content->innerRect(
) | rpl::on_next([=](QRect inner) {
if (inner.isEmpty()) {
@@ -336,7 +424,8 @@ PhotoEditor::PhotoEditor(
_controls->colorLineShownValue(
) | rpl::on_next([=](bool shown) {
_colorPicker->setVisible(shown);
_colorLineShown = shown;
updateColorPickerVisibility(anim::type::instant);
}, _controls->lifetime());
_mode.value(
@@ -424,6 +513,7 @@ PhotoEditor::PhotoEditor(
_controls->doneRequests(
) | rpl::on_next([=] {
_controls->commitTimelineEdits();
const auto mode = _mode.current().mode;
if (mode == PhotoEditorMode::Mode::Paint) {
_mode = PhotoEditorMode{
@@ -560,11 +650,27 @@ PhotoEditor::PhotoEditor(
}
void PhotoEditor::keyPressEvent(QKeyEvent *e) {
_content->setExpansionRoom(e->modifiers().testFlag(Qt::ControlModifier));
if (!_colorPicker->preventHandleKeyPress()) {
_content->handleKeyPress(e) || _controls->handleKeyPress(e);
}
}
void PhotoEditor::keyReleaseEvent(QKeyEvent *e) {
_content->setExpansionRoom(e->modifiers().testFlag(Qt::ControlModifier));
}
void PhotoEditor::updateColorPickerVisibility(anim::type animated) {
const auto painting
= (_mode.current().mode == PhotoEditorMode::Mode::Paint);
_colorPicker->setVisible(
painting
&& _colorLineShown
&& !_videoClipSelected
&& !_audioSelected,
animated);
}
void PhotoEditor::save() {
_content->save(_modifications);
_done.fire_copy(_modifications);

View File

@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <array>
namespace Ui {
class LayerManager;
class LayerWidget;
class Show;
} // namespace Ui
@@ -57,9 +58,12 @@ public:
private:
void keyPressEvent(QKeyEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
void updateColorPickerVisibility(anim::type animated);
PhotoModifications _modifications;
const std::unique_ptr<Ui::LayerManager> _layers;
const std::shared_ptr<Controllers> _controllers;
base::unique_qptr<PhotoEditorContent> _content;
@@ -77,6 +81,10 @@ private:
bool _textItemSelected = false;
bool _textEditing = false;
bool _shapeItemSelected = false;
bool _videoClipSelected = false;
bool _audioSelected = false;
bool _matchingDurations = false;
bool _colorLineShown = false;
rpl::event_stream<PhotoModifications> _done;
rpl::event_stream<> _cancel;

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/scene/scene.h"
#include "editor/scene/scene_item_animated.h"
#include "editor/video/video_clip.h"
#include "ui/painter.h"
#include "ui/userpic_view.h"
@@ -18,7 +19,36 @@ namespace {
constexpr auto kAnimatedMaxSide = 854;
constexpr auto kAnimatedFps = 30.;
constexpr auto kAnimatedMinDuration = crl::time(1000);
constexpr auto kAnimatedMaxDuration = crl::time(3000);
[[nodiscard]] QImage ExpandCanvas(
const QImage &image,
QRect crop,
Scene *scene = nullptr) {
auto result = QImage(crop.size(), QImage::Format_ARGB32_Premultiplied);
result.fill(Qt::transparent);
auto p = Painter(&result);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.drawImage(-crop.topLeft(), image);
if (scene) {
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
PainterHighQualityEnabler hq(p);
scene->render(&p, QRectF(result.rect()), QRectF(crop));
}
return result;
}
void FillCanvasBackground(
QImage &canvas,
const Media::Encode::CanvasBackground &background,
QRect keep = QRect()) {
auto p = QPainter(&canvas);
p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
if (!keep.isEmpty()) {
p.setClipRegion(QRegion(canvas.rect()) - QRegion(keep));
}
Media::Encode::PaintCanvasBackground(p, canvas.rect(), background);
}
} // namespace
@@ -76,7 +106,9 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
if (!mods) {
return image;
}
if (mods.paint) {
const auto expanded = mods.crop.isValid()
&& !image.rect().contains(mods.crop);
if (mods.paint && !expanded) {
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(
QImage::Format_ARGB32_Premultiplied);
@@ -87,7 +119,9 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
mods.paint->render(&p, image.rect());
}
auto cropped = mods.crop.isValid()
auto cropped = expanded
? ExpandCanvas(image, mods.crop, mods.paint.get())
: mods.crop.isValid()
? image.copy(mods.crop)
: image;
QTransform transform;
@@ -97,7 +131,18 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
if (mods.angle) {
transform.rotate(mods.angle);
}
return cropped.transformed(transform);
auto result = cropped.transformed(transform);
if (expanded) {
const auto matrix = QImage::trueMatrix(
transform,
cropped.width(),
cropped.height());
FillCanvasBackground(
result,
Media::Encode::DominantCanvasBackground(image),
matrix.mapRect(QRect(-mods.crop.topLeft(), image.size())));
}
return result;
}
Media::Encode::Job ComposeAnimatedJob(
@@ -141,9 +186,8 @@ Media::Encode::Job ComposeAnimatedJob(
target.width() / rotated.width(),
target.height() / rotated.height());
const auto bake = [&](const QImage &source) {
auto cropped = source.copy(crop);
return cropped.transformed(transform, Qt::SmoothTransformation)
const auto bake = [&](const QImage &canvas) {
return canvas.transformed(transform, Qt::SmoothTransformation)
.scaled(
target,
Qt::IgnoreAspectRatio,
@@ -171,13 +215,13 @@ Media::Encode::Job ComposeAnimatedJob(
item->setVisible(true);
}
auto layer = QImage(
image.size(),
crop.size(),
QImage::Format_ARGB32_Premultiplied);
layer.fill(Qt::transparent);
{
auto p = Painter(&layer);
PainterHighQualityEnabler hq(p);
scene->render(&p, layer.rect());
scene->render(&p, QRectF(layer.rect()), QRectF(crop));
}
for (const auto item : normal) {
item->setVisible(true);
@@ -187,8 +231,9 @@ Media::Encode::Job ComposeAnimatedJob(
};
auto longest = crl::time(0);
auto music = std::vector<Media::Encode::MusicTrack>();
for (const auto item : normal) {
const auto animated = dynamic_cast<ItemAnimated*>(item);
const auto animated = item->asAnimated();
if (!animated || !animated->animated()) {
run.push_back(item);
continue;
@@ -199,21 +244,47 @@ Media::Encode::Job ComposeAnimatedJob(
continue;
}
flushRun();
const auto clip = item->videoClip();
const auto loop = animated->loopDuration();
if (clip && (loop > 0)) {
entity.till = entity.from + loop;
}
if (clip && clip->sounding()) {
const auto trim = clip->trim();
music.push_back({
.bytes = entity.bytes,
.from = trim.from,
.till = trim.from + loop,
.volume = clip->volume(),
.loop = true,
});
}
job.overlay.push_back(std::move(entity));
const auto duration = animated->loopDuration();
longest = std::max(
longest,
duration ? duration : kAnimatedMaxDuration);
longest = std::max(longest, loop);
}
flushRun();
if (const auto audio = scene->audio()) {
if (audio->volume > 0.) {
music.push_back({
.path = audio->path,
.bytes = audio->content,
.from = audio->from,
.till = (audio->till > audio->from) ? audio->till : 0,
.volume = audio->volume,
});
}
longest = std::max(longest, audio->length());
}
auto base = bake(ExpandCanvas(image, crop));
FillCanvasBackground(
base,
Media::Encode::DominantCanvasBackground(image));
job.source = Media::Encode::StillSource{
.base = bake(image),
.duration = std::clamp(
longest,
kAnimatedMinDuration,
kAnimatedMaxDuration),
.base = std::move(base),
.duration = std::max(longest, kAnimatedMinDuration),
.fps = kAnimatedFps,
.music = std::move(music),
};
job.silentLoop = true;
return job;

View File

@@ -7,12 +7,34 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "media/media_video_canvas.h"
#include "media/media_video_encode.h"
namespace Editor {
class Scene;
struct AudioTrack {
QString path;
QByteArray content;
QString title;
QString performer;
QImage cover;
crl::time duration = 0;
crl::time from = 0;
// Zero means the end of the track.
crl::time till = 0;
float64 volume = 1.;
[[nodiscard]] bool empty() const {
return path.isEmpty() && content.isEmpty();
}
[[nodiscard]] crl::time length() const {
const auto end = (till > from) ? till : duration;
return std::max(end - from, crl::time(0));
}
};
enum class RoundedCornersLevel {
Large,
Medium,
@@ -45,6 +67,7 @@ struct EditorData {
bool fixedCrop = false;
bool forOtherUser = false;
bool composeAnimated = false;
bool composeSound = false;
};
struct PhotoModifications {

View File

@@ -7,11 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_content.h"
#include "editor/editor_audio_disc_button.h"
#include "editor/editor_crop.h"
#include "editor/editor_paint.h"
#include "history/history_drag_area.h"
#include "media/view/media_view_pip.h"
#include "storage/storage_media_prepare.h"
#include "ui/effects/animation_value_f.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_editor.h"
#include <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
@@ -20,6 +25,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QWheelEvent>
namespace Editor {
namespace {
constexpr auto kExpansionRoomRatio = 0.7;
constexpr auto kLayoutSmoothTau = 60.;
constexpr auto kLayoutMaxFrameDelta = crl::time(64);
constexpr auto kLayoutEpsilon = 0.5;
[[nodiscard]] float64 MaxDistance(const QRectF &a, const QRectF &b) {
return std::max({
std::abs(a.x() - b.x()),
std::abs(a.y() - b.y()),
std::abs(a.width() - b.width()),
std::abs(a.height() - b.height()),
});
}
} // namespace
using Media::View::FlipSizeByRotation;
using Media::View::RotatedRect;
@@ -57,53 +79,94 @@ PhotoEditorContent::PhotoEditorContent(
modifications,
_photoSize,
std::move(data)))
, _audioDisc(base::make_unique_q<AudioDiscButton>(this))
, _photo(std::move(photo))
, _modifications(modifications) {
, _background(Media::Encode::DominantCanvasBackground(_photo->original()))
, _modifications(modifications)
, _canvas(QRect(QPoint(), _photoSize) | modifications.crop) {
_crop->setExpansionAllowed(!_fixedCrop);
_paint->setCanvasBackground(_background);
_paint->setCropRect(QRectF(_crop->cropRect()));
_layoutAnimation.init([=](crl::time now) {
return layoutAnimationStep(now);
});
_crop->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::MouseMove
|| type == QEvent::MouseButtonRelease) {
const auto mouse = static_cast<QMouseEvent*>(e.get());
setExpansionRoom(
mouse->modifiers().testFlag(Qt::ControlModifier));
}
}, _crop->lifetime());
_crop->changes(
) | rpl::on_next([=] {
updateCanvas();
}, lifetime());
_crop->dragChanges(
) | rpl::on_next([=](bool dragging) {
_dragging = dragging;
if (dragging && _layoutAnimation.animating()) {
_layoutAnimation.stop();
applyLayout(_layoutTarget);
} else if (!dragging) {
updateRoom();
}
}, lifetime());
_audioDisc->hide();
_audioDisc->setClickedCallback([=] {
_paint->setAudioSelected(!_paint->audioSelected());
});
_audioDisc->removeRequests(
) | rpl::on_next([=] {
_paint->removeAudio();
}, _audioDisc->lifetime());
_paint->audioChanges(
) | rpl::on_next([=] {
updateAudioDisc();
}, lifetime());
_paint->audioSelectedChanges(
) | rpl::on_next([=](bool selected) {
_audioDisc->setActive(selected);
}, lifetime());
rpl::merge(
_innerRect.value() | rpl::to_empty,
sizeValue() | rpl::to_empty
) | rpl::on_next([=] {
updateAudioDiscGeometry();
}, lifetime());
rpl::combine(
_modifications.value(),
sizeValue()
sizeValue(),
_canvas.value(),
_room.value()
) | rpl::on_next([=](
const PhotoModifications &mods, const QSize &size) {
const PhotoModifications &mods,
const QSize &size,
const QRect &canvas,
bool room) {
if (size.isEmpty()) {
return;
}
const auto imageSizeF = [&] {
const auto rotatedSize
= FlipSizeByRotation(size, mods.angle);
const auto m = _crop->cropMargins();
const auto sizeForCrop = rotatedSize
- QSize(m.left() + m.right(), m.top() + m.bottom());
const auto originalSize = QSizeF(_photoSize);
if ((originalSize.width() > sizeForCrop.width())
|| (originalSize.height() > sizeForCrop.height())) {
return originalSize.scaled(
sizeForCrop,
Qt::KeepAspectRatio);
}
return originalSize;
}();
const auto imageSize = QSize(imageSizeF.width(), imageSizeF.height());
_imageRect = QRect(
QPoint(-imageSize.width() / 2, -imageSize.height() / 2),
imageSize);
_imageMatrix.reset();
_imageMatrix.translate(size.width() / 2, size.height() / 2);
if (mods.flipped) {
_imageMatrix.scale(-1, 1);
const auto target = layoutTarget(mods, size, canvas, room);
const auto animate = _animateLayout
&& !_dragging
&& !anim::Disabled()
&& !_imageRectF.isEmpty()
&& (target != _imageRectF);
if (!animate) {
_layoutAnimation.stop();
applyLayout(target);
return;
}
_layoutTarget = target;
if (!_layoutAnimation.animating()) {
_layoutLastFrame = crl::now();
_layoutAnimation.start();
}
_imageMatrix.rotate(mods.angle);
const auto geometry = _imageMatrix.mapRect(_imageRect);
_crop->applyTransform(
geometry + _crop->cropMargins(),
mods.angle,
mods.flipped, imageSizeF);
_crop->setCornersLevel(mods.cornersLevel);
_paint->applyTransform(geometry, mods.angle, mods.flipped);
_innerRect = geometry;
}, lifetime());
paintRequest(
@@ -112,10 +175,18 @@ PhotoEditorContent::PhotoEditorContent(
p.fillRect(clip, Qt::transparent);
if (_mode.mode == PhotoEditorMode::Mode::Paint) {
_paint->paintCanvas(p);
_paint->paintImage(p, _photo->pix(_photoSize));
} else {
paintCanvasFill(p);
p.setTransform(_imageMatrix);
p.drawPixmap(_imageRect, _photo->pix(_imageRect.size()));
const auto size = _layoutAnimation.animating()
? _layoutTarget.size().toSize()
: _imageRect.size();
p.setRenderHint(
QPainter::SmoothPixmapTransform,
(size != _imageRect.size()));
p.drawPixmap(_imageRect, _photo->pixSingle(size));
}
}, lifetime());
@@ -164,6 +235,143 @@ PhotoEditorContent::PhotoEditorContent(
}
}
QRectF PhotoEditorContent::layoutTarget(
const PhotoModifications &mods,
QSize size,
QRect canvas,
bool room) const {
const auto m = _crop->cropMargins();
const auto full = FlipSizeByRotation(size, mods.angle)
- QSize(m.left() + m.right(), m.top() + m.bottom());
const auto fit = room
? (QSizeF(full) * kExpansionRoomRatio)
: QSizeF(full);
const auto scale = std::min({
1.,
fit.width() / canvas.width(),
fit.height() / canvas.height(),
});
const auto canvasSize = QSizeF(canvas.size()) * scale;
return QRectF(
QPointF(-canvasSize.width() / 2., -canvasSize.height() / 2.)
- QPointF(canvas.topLeft()) * scale,
QSizeF(_photoSize) * scale);
}
void PhotoEditorContent::applyLayout(QRectF imageRect) {
const auto size = this->size();
if (size.isEmpty() || imageRect.isEmpty()) {
return;
}
const auto &mods = _modifications.current();
const auto canvas = _canvas.current();
const auto scale = imageRect.width() / _photoSize.width();
const auto rounded = [](float64 value) {
return int(base::SafeRound(value));
};
_imageRectF = imageRect;
_imageRect = QRect(
QPoint(rounded(imageRect.x()), rounded(imageRect.y())),
QSize(rounded(imageRect.width()), rounded(imageRect.height())));
const auto canvasRect = QRect(
_imageRect.topLeft() + QPoint(
rounded(canvas.x() * scale),
rounded(canvas.y() * scale)),
QSize(
rounded(canvas.width() * scale),
rounded(canvas.height() * scale)));
_imageMatrix.reset();
_imageMatrix.translate(size.width() / 2, size.height() / 2);
if (mods.flipped) {
_imageMatrix.scale(-1, 1);
}
_imageMatrix.rotate(mods.angle);
const auto geometry = _imageMatrix.mapRect(_imageRect);
_crop->applyTransform(
rect(),
geometry.topLeft(),
mods.angle,
mods.flipped,
imageRect.size());
_crop->setCornersLevel(mods.cornersLevel);
const auto canvasGeometry = _imageMatrix.mapRect(canvasRect);
_paint->applyTransform(
geometry,
canvasGeometry,
QRectF(canvas),
mods.angle,
mods.flipped);
_innerRect = canvasGeometry;
update();
}
bool PhotoEditorContent::layoutAnimationStep(crl::time now) {
const auto delta = std::clamp(
now - _layoutLastFrame,
crl::time(0),
kLayoutMaxFrameDelta);
_layoutLastFrame = now;
if (delta <= 0) {
return true;
}
const auto ratio = 1. - std::exp(-float64(delta) / kLayoutSmoothTau);
const auto next = anim::interpolatedRectF(
_imageRectF,
_layoutTarget,
ratio);
const auto finished = MaxDistance(next, _layoutTarget) < kLayoutEpsilon;
applyLayout(finished ? _layoutTarget : next);
return !finished;
}
void PhotoEditorContent::updateCanvas() {
_paint->setCropRect(QRectF(_crop->cropRect()));
_animateLayout = true;
_canvas = QRect(QPoint(), _photoSize) | _crop->cropRect();
_animateLayout = false;
}
void PhotoEditorContent::updateRoom() {
if (_dragging) {
return;
}
_animateLayout = true;
_room = _roomRequested
&& !_fixedCrop
&& (_mode.mode == PhotoEditorMode::Mode::Transform);
_animateLayout = false;
}
void PhotoEditorContent::setExpansionRoom(bool room) {
_roomRequested = room;
updateRoom();
}
void PhotoEditorContent::paintCanvasFill(QPainter &p) const {
if (Rect(_photoSize).contains(_crop->cropRect())) {
return;
}
auto image = QPainterPath();
image.addRect(_imageMatrix.mapRect(QRectF(_imageRect)));
const auto fill = _crop->cropPath()
.translated(_crop->pos())
.subtracted(image);
if (fill.isEmpty()) {
return;
}
p.save();
auto hq = PainterHighQualityEnabler(p);
p.setClipPath(fill);
Media::Encode::PaintCanvasBackground(
p,
_crop->paintRect().translated(_crop->pos()),
_background);
p.restore();
}
void PhotoEditorContent::applyModifications(
PhotoModifications modifications) {
_modifications = std::move(modifications);
@@ -206,9 +414,38 @@ void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
_paint->keepResult();
}
_mode = mode;
updateCanvas();
updateRoom();
updateAudioDisc();
update();
}
void PhotoEditorContent::updateAudioDiscGeometry() {
const auto inner = _innerRect.current();
const auto skip = st::photoEditorAudioDiscSkip;
const auto size = _audioDisc->width();
const auto right = rect::right(inner);
const auto bottom = rect::bottom(inner);
if (width() - right >= size + skip) {
_audioDisc->moveToLeft(right + skip, bottom - size);
} else if (height() - bottom >= size + skip) {
_audioDisc->moveToLeft(right - size, bottom + skip);
} else {
_audioDisc->moveToLeft(right - skip - size, bottom - skip - size);
}
}
void PhotoEditorContent::updateAudioDisc() {
const auto audio = _paint->audio();
const auto shown = (audio != nullptr)
&& (_mode.mode == PhotoEditorMode::Mode::Paint);
_audioDisc->setTrack(audio);
_audioDisc->setVisible(shown);
if (shown) {
_audioDisc->raise();
}
}
void PhotoEditorContent::applyAspectRatio(float64 ratio) {
_crop->setAspectRatio(ratio);
}
@@ -283,10 +520,59 @@ rpl::producer<QColor> PhotoEditorContent::shapeItemSelections() const {
return _paint->shapeItemSelections();
}
auto PhotoEditorContent::videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>> {
return _paint->videoClipSelections();
}
rpl::producer<> PhotoEditorContent::shapeItemDeselections() const {
return _paint->shapeItemDeselections();
}
rpl::producer<> PhotoEditorContent::audioChanges() const {
return _paint->audioChanges();
}
rpl::producer<bool> PhotoEditorContent::audioSelectedChanges() const {
return _paint->audioSelectedChanges();
}
rpl::producer<> PhotoEditorContent::audioVolumeChanges() const {
return _audioDisc->volumeChanges();
}
bool PhotoEditorContent::audioSelected() const {
return _paint->audioSelected();
}
std::shared_ptr<AudioTrack> PhotoEditorContent::audio() const {
return _paint->audio();
}
void PhotoEditorContent::removeAudio() {
_paint->removeAudio();
}
bool PhotoEditorContent::canEqualizeDurations() const {
return _paint->canEqualizeDurations();
}
void PhotoEditorContent::matchDurations(crl::time duration) {
_paint->matchDurations(duration);
}
bool PhotoEditorContent::durationsLinked() const {
return _paint->durationsLinked();
}
void PhotoEditorContent::setDurationsLinked(bool linked) {
_paint->setDurationsLinked(linked);
}
rpl::producer<> PhotoEditorContent::durationsLinkChanges() const {
return _paint->durationsLinkChanges();
}
rpl::producer<bool> PhotoEditorContent::shapeToolStates() const {
return _paint->shapeToolStates();
}

View File

@@ -8,14 +8,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
#include "media/media_video_canvas.h"
#include "ui/image/image.h"
namespace Editor {
class AudioDiscButton;
class Crop;
class VideoClip;
class Paint;
struct Controllers;
@@ -50,7 +54,21 @@ public:
[[nodiscard]] rpl::producer<> shapeItemDeselections() const;
[[nodiscard]] rpl::producer<bool> shapeToolStates() const;
[[nodiscard]] rpl::producer<> paintModeRequests() const;
[[nodiscard]] auto videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>>;
[[nodiscard]] rpl::producer<> audioChanges() const;
[[nodiscard]] rpl::producer<bool> audioSelectedChanges() const;
[[nodiscard]] rpl::producer<> audioVolumeChanges() const;
[[nodiscard]] std::shared_ptr<AudioTrack> audio() const;
[[nodiscard]] bool audioSelected() const;
void removeAudio();
[[nodiscard]] bool canEqualizeDurations() const;
void matchDurations(crl::time duration);
[[nodiscard]] bool durationsLinked() const;
void setDurationsLinked(bool linked);
[[nodiscard]] rpl::producer<> durationsLinkChanges() const;
void applyAspectRatio(float64 ratio);
void setExpansionRoom(bool room);
void save(PhotoModifications &modifications);
bool handleKeyPress(not_null<QKeyEvent*> e);
@@ -64,22 +82,45 @@ public:
}
private:
void updateCanvas();
void updateRoom();
[[nodiscard]] QRectF layoutTarget(
const PhotoModifications &mods,
QSize size,
QRect canvas,
bool room) const;
void applyLayout(QRectF imageRect);
bool layoutAnimationStep(crl::time now);
void paintCanvasFill(QPainter &p) const;
void updateAudioDisc();
void updateAudioDiscGeometry();
const QSize _photoSize;
const bool _fixedCrop = false;
const bool _composeAnimated = false;
const base::unique_qptr<Paint> _paint;
const base::unique_qptr<Crop> _crop;
const base::unique_qptr<AudioDiscButton> _audioDisc;
const std::shared_ptr<Image> _photo;
const Media::Encode::CanvasBackground _background;
rpl::variable<QRect> _innerRect;
rpl::variable<PhotoModifications> _modifications;
rpl::variable<QRect> _canvas;
rpl::variable<bool> _room;
rpl::event_stream<int> _keyPresses;
rpl::event_stream<> _paintModeRequests;
QRect _imageRect;
QRectF _imageRectF;
QRectF _layoutTarget;
Ui::Animations::Basic _layoutAnimation;
crl::time _layoutLastFrame = 0;
QTransform _imageMatrix;
PhotoEditorMode _mode;
bool _roomRequested = false;
bool _dragging = false;
bool _animateLayout = false;
};

View File

@@ -7,8 +7,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_controls.h"
#include "editor/audio_track_timeline.h"
#include "editor/controllers/controllers.h"
#include "editor/editor_keys_legend.h"
#include "editor/video_item_timeline.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "ui/effects/round_checkbox.h"
#include "ui/image/image_prepare.h"
#include "ui/qt_object_factory.h"
@@ -18,9 +22,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/menu/menu_multiline_action.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/ui_utility.h"
#include "styles/style_editor.h"
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
@@ -470,6 +476,74 @@ ButtonBar::ButtonBar(
}, lifetime());
}
class TrimShortestButton final
: public Ui::IconButton
, public Ui::AbstractTooltipShower {
public:
explicit TrimShortestButton(QWidget *parent)
: IconButton(parent, st::photoEditorRotateButton)
, _icon(Lottie::MakeIcon({
.path = u":/animations/photo_editor_trim_shortest.tgs"_q,
.color = &st::photoEditorButtonIconFg,
.sizeOverride = QSize(
st::photoEditorTrimShortestIconSize,
st::photoEditorTrimShortestIconSize),
})) {
events(
) | rpl::on_next([=](not_null<QEvent*> event) {
if (event->type() == QEvent::Enter) {
Ui::Tooltip::Show(1000, this);
} else if (event->type() == QEvent::Leave) {
Ui::Tooltip::Hide();
}
}, lifetime());
}
void setActive(bool active, anim::type animated) {
if (_active == active) {
return;
}
_active = active;
const auto last = _icon->framesCount() - 1;
const auto repaint = [=] { update(); };
if (animated == anim::type::instant) {
_icon->jumpTo(active ? last : 0, repaint);
} else {
_icon->animate(repaint, active ? 0 : last, active ? last : 0);
}
}
QString tooltipText() const override {
return tr::lng_photo_editor_trim_shortest(tr::now);
}
QPoint tooltipPos() const override {
return QCursor::pos();
}
bool tooltipWindowActive() const override {
return Ui::AppInFocus() && Ui::InFocusChain(window());
}
protected:
void paintEvent(QPaintEvent *e) override {
auto p = Painter(this);
paintRipple(p, st().rippleAreaPosition);
const auto last = std::max(_icon->framesCount() - 1, 1);
const auto progress = _icon->frameIndex() / float64(last);
const auto idle = anim::color(
st::photoEditorButtonIconFg,
st::photoEditorButtonIconFgOver,
iconOverOpacity());
_icon->paintInCenter(p, rect(), anim::color(
idle,
st::photoEditorButtonIconFgActive,
progress));
}
private:
const std::unique_ptr<Lottie::Icon> _icon;
bool _active = false;
};
class TextToolButton final : public Ui::AbstractButton {
public:
TextToolButton(not_null<QWidget*> parent)
@@ -511,6 +585,7 @@ PhotoEditorControls::PhotoEditorControls(
: RpWidget(parent)
, _imageSize(imageSize)
, _originalRatio(data.originalRatio)
, _fixedCrop(data.fixedCrop)
, _bg(st::roundedBg)
, _buttonHeight(st::photoEditorButtonBarHeight)
, _transformButtons(base::make_unique_q<ButtonBar>(this, _bg))
@@ -589,9 +664,35 @@ PhotoEditorControls::PhotoEditorControls(
_buttonHeight,
st::photoEditorEdgeButtonBg,
st::mediaviewTextLinkFg,
st::photoEditorRotateButton.ripple)) {
st::photoEditorRotateButton.ripple))
, _videoTimeline(base::make_unique_q<Ui::FadeWrap<VideoItemTimeline>>(
this,
object_ptr<VideoItemTimeline>(this)))
, _audioTimeline(base::make_unique_q<Ui::FadeWrap<AudioTrackTimeline>>(
this,
object_ptr<AudioTrackTimeline>(this)))
, _trimShortest(base::make_unique_q<Ui::FadeWrap<Ui::IconButton>>(
this,
object_ptr<TrimShortestButton>(this)))
, _keysLegend(base::make_unique_q<KeysLegendButton>(this, [=] {
return KeysLegendContext{
.mode = _mode.current().mode,
.fixedCrop = _fixedCrop,
.timeline = _videoTimelineShown || _audioTimelineShown,
};
})) {
_shapesFilled = shapesFilled;
_videoTimeline->hide(anim::type::instant);
_videoTimeline->setDuration(st::photoEditorBarAnimationDuration);
_audioTimeline->hide(anim::type::instant);
_audioTimeline->setDuration(st::photoEditorBarAnimationDuration);
_trimShortest->hide(anim::type::instant);
_trimShortest->setDuration(st::photoEditorBarAnimationDuration);
_paintTopButtons->geometryValue(
) | rpl::on_next([=] {
updateTimelinesGeometry();
}, lifetime());
_shapesButton->setClickedCallback([=] {
if (_shapeToolActive) {
_shapeRequests.fire({ .action = ShapeRequest::Action::Cancel });
@@ -630,6 +731,10 @@ PhotoEditorControls::PhotoEditorControls(
(size.width() - current->width()) / 2,
buttonsTop);
_keysLegend->moveToLeft(st::photoEditorKeysButtonLeft, buttonsTop);
_keysLegend->setVisible(
current->x() >= _keysLegend->x() + _keysLegend->width());
if (_about) {
const auto &margin = st::photoEditorAboutMargin;
const auto skip = st::photoEditorCropPointSize;
@@ -680,6 +785,8 @@ PhotoEditorControls::PhotoEditorControls(
_paintBottomButtons->shownValue() | rpl::to_empty,
_paintTopButtons->geometryValue() | rpl::to_empty,
_paintTopButtons->shownValue() | rpl::to_empty,
_keysLegend->geometryValue() | rpl::to_empty,
_keysLegend->shownValue() | rpl::to_empty,
std::move(aboutChanges)
) | rpl::on_next([=] {
updateInputMask();
@@ -1128,6 +1235,7 @@ void PhotoEditorControls::updateInputMask() {
add(_transformButtons);
add(_paintBottomButtons);
add(_paintTopButtons);
add(_keysLegend);
if (_about && !_about->isHidden()) {
const auto geometry = _about->geometry() & visibleRect;
if (!geometry.isEmpty()) {
@@ -1160,7 +1268,117 @@ rpl::producer<bool> PhotoEditorControls::colorLineShownValue() const {
return _paintTopButtons->shownValue();
}
void PhotoEditorControls::setVideoClip(std::shared_ptr<VideoClip> clip) {
const auto shown = (clip != nullptr);
_videoTimelineShown = shown;
updateTrimShortest();
if (shown) {
_videoTimeline->entity()->setClip(std::move(clip));
updateTimelineGeometry(_videoTimeline.get());
}
_videoTimeline->toggle(shown, anim::type::normal);
if (!shown) {
_videoTimeline->entity()->setClip(nullptr);
}
}
void PhotoEditorControls::setAudioTrack(std::shared_ptr<AudioTrack> track) {
const auto shown = (track != nullptr);
_audioTimelineShown = shown;
updateTrimShortest();
_audioTimeline->entity()->setTrack(std::move(track));
if (shown) {
updateTimelineGeometry(_audioTimeline.get());
}
_audioTimeline->toggle(shown, anim::type::normal);
}
void PhotoEditorControls::setTrimShortestAvailable(bool available) {
if (_trimShortestAvailable == available) {
return;
}
_trimShortestAvailable = available;
updateTrimShortest();
}
void PhotoEditorControls::setTrimShortestActive(
bool active,
anim::type animated) {
const auto button = static_cast<TrimShortestButton*>(
_trimShortest->entity());
button->setActive(active, animated);
}
void PhotoEditorControls::refreshTimelines() {
_videoTimeline->entity()->refreshTrim();
_audioTimeline->entity()->refreshTrim();
}
void PhotoEditorControls::refreshAudioVolume() {
_audioTimeline->entity()->refreshVolume();
}
void PhotoEditorControls::commitTimelineEdits() {
_videoTimeline->entity()->commitPendingEdit();
_audioTimeline->entity()->commitPendingEdit();
}
rpl::producer<> PhotoEditorControls::trimShortestRequests() const {
return _trimShortest->entity()->clicks() | rpl::to_empty;
}
rpl::producer<crl::time> PhotoEditorControls::trimLengthChanges() const {
return rpl::merge(
_videoTimeline->entity()->lengthChanges(),
_audioTimeline->entity()->lengthChanges());
}
rpl::producer<> PhotoEditorControls::audioRemoveRequests() const {
return _audioTimeline->entity()->removeRequests();
}
void PhotoEditorControls::updateTrimShortest() {
const auto shown = _trimShortestAvailable
&& (_videoTimelineShown || _audioTimelineShown);
if (shown == _trimShortest->toggled()) {
return;
}
_trimShortest->toggle(shown, anim::type::normal);
if (shown || _videoTimelineShown || _audioTimelineShown) {
updateTimelinesGeometry();
}
}
void PhotoEditorControls::updateTimelinesGeometry() {
updateTimelineGeometry(_videoTimeline.get());
updateTimelineGeometry(_audioTimeline.get());
}
void PhotoEditorControls::updateTimelineGeometry(
not_null<Ui::RpWidget*> timeline) {
const auto bar = _paintTopButtons->geometry();
const auto skip = st::photoEditorTimelineSkip;
const auto left = _undoButton->width() + skip;
const auto trimShortest = _trimShortest->toggled()
? (_trimShortest->width() + skip)
: 0;
const auto width = bar.width()
- left
- trimShortest
- _redoButton->width()
- skip;
timeline->resizeToWidth(std::max(width, 1));
timeline->moveToLeft(bar.x() + left, bar.y());
_trimShortest->moveToLeft(
bar.x() + left + std::max(width, 1) + skip,
bar.y());
}
bool PhotoEditorControls::handleKeyPress(not_null<QKeyEvent*> e) const {
if ((e->key() == Qt::Key_Question) || (e->text() == u"?"_q)) {
_keysLegend->toggle();
return true;
}
_keyPresses.fire(std::move(e));
return true;
}

View File

@@ -25,8 +25,13 @@ class FadeWrap;
namespace Editor {
class AudioTrackTimeline;
class EdgeButton;
class ButtonBar;
class VideoClip;
class KeysLegendButton;
class VideoItemTimeline;
struct AudioTrack;
struct Controllers;
struct EditorData;
@@ -60,6 +65,16 @@ public:
void applyMode(const PhotoEditorMode &mode);
void setShapeToolActive(bool active);
void setVideoClip(std::shared_ptr<VideoClip> clip);
void setAudioTrack(std::shared_ptr<AudioTrack> track);
void setTrimShortestAvailable(bool available);
void setTrimShortestActive(bool active, anim::type animated);
void refreshTimelines();
void refreshAudioVolume();
void commitTimelineEdits();
[[nodiscard]] rpl::producer<> trimShortestRequests() const;
[[nodiscard]] rpl::producer<crl::time> trimLengthChanges() const;
[[nodiscard]] rpl::producer<> audioRemoveRequests() const;
private:
void showAnimated(
@@ -67,11 +82,15 @@ private:
anim::type animated = anim::type::normal);
void showShapesMenu();
void updateInputMask();
void updateTimelinesGeometry();
void updateTimelineGeometry(not_null<Ui::RpWidget*> timeline);
void updateTrimShortest();
int bottomButtonsTop() const;
const QSize _imageSize;
const float64 _originalRatio = 0.;
const bool _fixedCrop = false;
const style::color &_bg;
const int _buttonHeight;
const base::unique_qptr<ButtonBar> _transformButtons;
@@ -96,6 +115,10 @@ private:
const base::unique_qptr<Ui::AbstractButton> _textButton;
const base::unique_qptr<Ui::IconButton> _shapesButton;
const base::unique_qptr<EdgeButton> _paintDone;
const base::unique_qptr<Ui::FadeWrap<VideoItemTimeline>> _videoTimeline;
const base::unique_qptr<Ui::FadeWrap<AudioTrackTimeline>> _audioTimeline;
const base::unique_qptr<Ui::FadeWrap<Ui::IconButton>> _trimShortest;
const base::unique_qptr<KeysLegendButton> _keysLegend;
base::unique_qptr<Ui::PopupMenu> _ratioMenu;
base::unique_qptr<Ui::PopupMenu> _cornersMenu;
@@ -107,6 +130,9 @@ private:
bool _shapesFilled = false;
bool _shapeToolActive = false;
bool _keepOriginalRatio = false;
bool _trimShortestAvailable = false;
bool _videoTimelineShown = false;
bool _audioTimelineShown = false;
Ui::Animations::Simple _toggledBarAnimation;

View File

@@ -93,4 +93,13 @@ enum class SaveState {
Keep,
};
struct VideoTrim {
crl::time from = 0;
crl::time till = 0;
friend inline bool operator==(
const VideoTrim &,
const VideoTrim &) = default;
};
} // namespace Editor

View File

@@ -29,7 +29,8 @@ void OpenWithPreparedFile(
Fn<void(bool ok)> &&doneCallback,
int sideLimit,
QSize exactSize,
bool composeAnimated) {
bool composeAnimated,
bool composeSound) {
using ImageInfo = Ui::PreparedFileInformation::Image;
const auto image = std::get_if<ImageInfo>(&file->information->media);
if (!image) {
@@ -72,6 +73,7 @@ void OpenWithPreparedFile(
.exactSize = exactSize,
.keepAspectRatio = keepRatio,
.composeAnimated = composeAnimated,
.composeSound = composeSound,
});
const auto raw = editor.get();
auto layer = std::make_unique<LayerWidget>(parent, std::move(editor));

View File

@@ -37,7 +37,8 @@ void OpenWithPreparedFile(
Fn<void(bool ok)> &&doneCallback,
int sideLimit = 0,
QSize exactSize = {},
bool composeAnimated = false);
bool composeAnimated = false,
bool composeSound = false);
void PrepareProfilePhoto(
not_null<QWidget*> parent,

View File

@@ -7,14 +7,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene.h"
#include "editor/photo_editor_common.h"
#include "editor/scene/scene_item_animated.h"
#include "editor/scene/scene_item_canvas.h"
#include "editor/scene/scene_item_line.h"
#include "editor/scene/scene_item_shape.h"
#include "editor/scene/scene_item_text.h"
#include "editor/scene/scene_text_editing.h"
#include "editor/video/video_clip.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"
@@ -22,9 +25,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtMath>
namespace Editor {
namespace {
using ItemPtr = std::shared_ptr<NumberedItem>;
class ItemAction : public NumberedItem {
public:
@@ -33,6 +33,10 @@ public:
virtual void apply() = 0;
virtual void revert() = 0;
ItemAction *asAction() override {
return this;
}
QRectF boundingRect() const override {
return QRectF();
}
@@ -71,6 +75,10 @@ private:
} _saved, _keeped;
};
namespace {
using ItemPtr = std::shared_ptr<NumberedItem>;
class ItemEraser final : public ItemAction {
public:
struct Target {
@@ -152,16 +160,48 @@ bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
constexpr auto kShapeDragThreshold = 4.;
constexpr auto kShapeSnapAngle = 45.;
constexpr auto kDraftShapeOpacity = 0.5;
constexpr auto kStickyGuideDuration = crl::time(150);
constexpr auto kItemsBaseZ = 9000.;
} // namespace
class Scene::StickyGuidesItem final : public QGraphicsItem {
public:
explicit StickyGuidesItem(not_null<Scene*> scene)
: _scene(scene) {
setAcceptedMouseButtons(Qt::NoButton);
setZValue(kItemsBaseZ - 1.);
}
QRectF boundingRect() const override {
return _scene->canvasRect() + Margins(_scene->stickyGuideMargin());
}
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *,
QWidget *) override {
_scene->paintStickyGuide(*p, Qt::Horizontal);
_scene->paintStickyGuide(*p, Qt::Vertical);
}
void updateGeometry() {
prepareGeometryChange();
}
private:
const not_null<Scene*> _scene;
};
Scene::Scene(const QRectF &rect)
: QGraphicsScene(rect)
, _canvas(std::make_shared<ItemCanvas>())
, _lastZ(std::make_shared<float64>(9000.))
, _lastZ(std::make_shared<float64>(kItemsBaseZ))
, _stickyGuides(std::make_unique<StickyGuidesItem>(this))
, _textEdit(std::make_unique<TextEditController>(this)) {
QGraphicsScene::addItem(_canvas.get());
_canvas->clearPixmap();
QGraphicsScene::addItem(_stickyGuides.get());
_canvas->grabContentRequests(
) | rpl::on_next([=](ItemCanvas::Content &&content) {
@@ -242,6 +282,7 @@ Scene::Scene(const QRectF &rect)
}
const auto canvasVisible = _canvas->isVisible();
_canvas->setVisible(false);
_stickyGuides->setVisible(false);
{
auto p = QPainter(&source);
render(
@@ -250,6 +291,7 @@ Scene::Scene(const QRectF &rect)
QRectF(captureRect),
Qt::IgnoreAspectRatio);
}
_stickyGuides->setVisible(true);
_canvas->setVisible(canvasVisible);
auto blurred = Images::BlurLargeImage(
std::move(source),
@@ -297,6 +339,9 @@ Scene::Scene(const QRectF &rect)
&QGraphicsScene::selectionChanged,
[=] {
const auto selected = selectedItems();
if (!selected.empty()) {
setAudioSelected(false);
}
auto *textItem = (ItemText*)(nullptr);
auto *shapeItem = (ItemShape*)(nullptr);
if (selected.size() == 1) {
@@ -322,9 +367,28 @@ Scene::Scene(const QRectF &rect)
_shapeItemDeselections.fire({});
}
}
refreshVideoClipSelection();
});
}
void Scene::refreshVideoClipSelection() {
const auto selected = selectedItems();
auto clip = std::shared_ptr<VideoClip>();
if (selected.size() == 1) {
if (const auto item = itemShared(selected.front())) {
if (const auto raw = item->videoClip()) {
clip = std::shared_ptr<VideoClip>(item, raw);
}
}
}
if (clip.get() == _selectedVideoClip) {
return;
}
_selectedVideoClip = clip.get();
updateVideoClipsSound();
_videoClipSelections.fire(std::move(clip));
}
void Scene::cancelDrawing() {
_textEdit->finishEditing(false);
_canvas->cancelDrawing();
@@ -339,12 +403,15 @@ void Scene::addItem(ItemPtr item) {
return;
}
item->setNumber(_itemNumber++);
if (item->scene() != this) {
QGraphicsScene::addItem(item.get());
}
const auto raw = item.get();
_items.push_back(std::move(item));
_itemsByPointer.emplace(raw, _items.back());
if (raw->scene() != this) {
QGraphicsScene::addItem(raw);
}
if (raw->videoClip()) {
checkDurationsLink();
}
_addsItem.fire({});
}
@@ -360,10 +427,21 @@ void Scene::removeItem(not_null<QGraphicsItem*> item) {
void Scene::removeItem(const ItemPtr &item) {
item->setStatus(NumberedItem::Status::Removed);
if (item->videoClip()) {
checkDurationsLink();
}
_removesItem.fire({});
}
void Scene::videoClipChanged(not_null<NumberedItem*> item) {
checkDurationsLink();
if (item->isSelected()) {
refreshVideoClipSelection();
}
}
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
setAudioSelected(false);
if (_shapeTool.pending) {
if (event->button() == Qt::LeftButton) {
event->accept();
@@ -389,12 +467,116 @@ void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
QGraphicsScene::mousePressEvent(event);
capturePlacements();
if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {
if (SkipMouseEvent(event)
|| !_canvas->drawableRect().contains(event->scenePos())) {
return;
}
_canvas->handleMousePressEvent(event);
}
void Scene::setCanvasRect(const QRectF &rect) {
if (_canvasRect == rect) {
return;
}
_stickyGuides->updateGeometry();
_canvasRect = rect;
_canvas->setCanvasRect(canvasRect());
}
QRectF Scene::canvasRect() const {
return _canvasRect.isNull() ? sceneRect() : _canvasRect;
}
void Scene::setStickyGuides(
std::optional<float64> x,
std::optional<float64> y) {
setStickyGuide(Qt::Horizontal, x);
setStickyGuide(Qt::Vertical, y);
}
void Scene::setStickyGuide(
Qt::Orientation orientation,
std::optional<float64> position) {
auto &guide = (orientation == Qt::Horizontal)
? _stickyGuideX
: _stickyGuideY;
const auto shown = position.has_value();
if (!shown && !guide.shown) {
return;
}
const auto was = stickyGuideRect(orientation);
if (shown) {
guide.position = *position;
}
if (guide.shown != shown) {
guide.shown = shown;
guide.animation.start(
[=] { _stickyGuides->update(stickyGuideRect(orientation)); },
shown ? 0. : 1.,
shown ? 1. : 0.,
kStickyGuideDuration);
}
_stickyGuides->update(was);
_stickyGuides->update(stickyGuideRect(orientation));
}
void Scene::hideStickyGuides() {
for (const auto guide : { &_stickyGuideX, &_stickyGuideY }) {
guide->animation.stop();
guide->shown = false;
}
_stickyGuides->update();
}
float64 Scene::stickyGuideMargin() const {
const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.;
return st::photoEditorStickyLineWidth / zoom;
}
QRectF Scene::stickyGuideRect(Qt::Orientation orientation) const {
const auto &guide = (orientation == Qt::Horizontal)
? _stickyGuideX
: _stickyGuideY;
const auto canvas = canvasRect();
const auto margin = stickyGuideMargin();
return (orientation == Qt::Horizontal)
? QRectF(
guide.position - margin,
canvas.top(),
margin * 2,
canvas.height())
: QRectF(
canvas.left(),
guide.position - margin,
canvas.width(),
margin * 2);
}
void Scene::paintStickyGuide(
QPainter &p,
Qt::Orientation orientation) const {
const auto &guide = (orientation == Qt::Horizontal)
? _stickyGuideX
: _stickyGuideY;
const auto opacity = guide.animation.value(guide.shown ? 1. : 0.);
if (opacity <= 0.) {
return;
}
const auto canvas = canvasRect();
auto color = QColor(Qt::white);
color.setAlphaF(opacity);
p.setPen(QPen(color, stickyGuideMargin()));
if (orientation == Qt::Horizontal) {
p.drawLine(
QPointF(guide.position, canvas.top()),
QPointF(guide.position, canvas.bottom()));
} else {
p.drawLine(
QPointF(canvas.left(), guide.position),
QPointF(canvas.right(), guide.position));
}
}
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
if (_shapeTool.dragging && (event->button() == Qt::LeftButton)) {
event->accept();
@@ -436,6 +618,7 @@ void Scene::setPendingShape(std::optional<PendingShape> pending) {
_textEdit->finishEditing(true);
clearSelection();
clearFocus();
setAudioSelected(false);
}
if (was != now) {
_pendingShapeStates.fire_copy(now);
@@ -659,6 +842,11 @@ rpl::producer<> Scene::shapeItemDeselections() const {
return _shapeItemDeselections.events();
}
auto Scene::videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>> {
return _videoClipSelections.events();
}
void Scene::setBlurSource(Fn<QImage(QRect)> source) {
_blurSource = std::move(source);
}
@@ -687,7 +875,7 @@ std::vector<ItemPtr> Scene::items(
bool Scene::hasAnimatedItems() const {
for (const auto &item : _items) {
const auto animated = item->isNormalStatus()
? dynamic_cast<ItemAnimated*>(item.get())
? item->asAnimated()
: nullptr;
if (animated && animated->animated() && animated->hasContent()) {
return true;
@@ -696,14 +884,177 @@ bool Scene::hasAnimatedItems() const {
return false;
}
bool Scene::hasAnimatedResult() const {
return (_audio != nullptr) || hasAnimatedItems();
}
void Scene::updateVideoClipsSound() {
const auto apply = [&](bool enabled) {
for (const auto &item : _items) {
const auto clip = item->videoClip();
if (clip && ((clip == _selectedVideoClip) == enabled)) {
clip->setSoundEnabled(enabled);
}
}
};
apply(false);
apply(true);
}
bool Scene::hasSoundResult() const {
if (_audio && (_audio->volume > 0.)) {
return true;
}
for (const auto &item : _items) {
const auto clip = item->videoClip();
if (clip
&& item->isNormalStatus()
&& clip->animated()
&& clip->sounding()) {
return true;
}
}
return false;
}
void Scene::releaseAnimations() {
for (const auto &item : _items) {
if (const auto animated = dynamic_cast<ItemAnimated*>(item.get())) {
if (const auto animated = item->asAnimated()) {
animated->releasePlayers();
}
}
}
void Scene::setAudio(std::shared_ptr<AudioTrack> audio) {
if (audio && audio->empty()) {
audio = nullptr;
}
if (_audio == audio) {
return;
}
_audio = std::move(audio);
if (!_audio) {
setAudioSelected(false);
}
_audioChanges.fire({});
checkDurationsLink();
}
std::shared_ptr<AudioTrack> Scene::audio() const {
return _audio;
}
rpl::producer<> Scene::audioChanges() const {
return _audioChanges.events();
}
void Scene::setAudioSelected(bool selected) {
if (selected && !_audio) {
return;
} else if (_audioSelected == selected) {
return;
}
if (selected) {
setPendingShape(std::nullopt);
_textEdit->finishEditing(true);
clearSelection();
clearFocus();
}
_audioSelected = selected;
_audioSelectedChanges.fire_copy(selected);
}
bool Scene::audioSelected() const {
return _audioSelected;
}
rpl::producer<bool> Scene::audioSelectedChanges() const {
return _audioSelectedChanges.events();
}
bool Scene::canEqualizeDurations() const {
if (!_audio) {
return false;
}
for (const auto &item : _items) {
if (item->isNormalStatus() && item->videoClip()) {
return true;
}
}
return false;
}
void Scene::equalizeDurations() {
auto shortest = _audio ? _audio->length() : crl::time(0);
for (const auto &item : _items) {
const auto clip = item->isNormalStatus()
? item->videoClip()
: nullptr;
if (clip) {
const auto loop = clip->loopDuration();
if (loop > 0 && (!shortest || loop < shortest)) {
shortest = loop;
}
}
}
matchDurations(shortest);
}
void Scene::matchDurations(crl::time duration) {
auto clips = std::vector<VideoClip*>();
auto shortest = duration;
if (_audio) {
shortest = std::min(shortest, _audio->duration - _audio->from);
}
for (const auto &item : _items) {
const auto clip = item->isNormalStatus()
? item->videoClip()
: nullptr;
const auto full = clip ? clip->duration() : crl::time(0);
if (full > 0) {
shortest = std::min(shortest, full - clip->trim().from);
clips.push_back(clip);
}
}
if (shortest <= 0) {
return;
}
for (const auto clip : clips) {
const auto from = clip->trim().from;
clip->setTrim({ from, from + shortest });
}
if (_audio) {
_audio->till = _audio->from + shortest;
}
}
bool Scene::durationsLinked() const {
return _durationsLinked;
}
void Scene::setDurationsLinked(bool linked) {
if (_durationsLinked == linked) {
return;
}
_durationsLinked = linked;
if (linked) {
equalizeDurations();
}
}
rpl::producer<> Scene::durationsLinkChanges() const {
return _durationsLinkChanges.events();
}
void Scene::checkDurationsLink() {
if (_durationsLinked && !canEqualizeDurations()) {
_durationsLinked = false;
} else if (_durationsLinked) {
equalizeDurations();
}
_durationsLinkChanges.fire({});
}
std::shared_ptr<float64> Scene::lastZ() const {
return _lastZ;
}
@@ -720,6 +1071,7 @@ float64 Scene::currentZoom() const {
void Scene::updateZoom(float64 zoom) {
_currentZoom = zoom;
_canvas->updateZoom(zoom);
_stickyGuides->updateGeometry();
for (const auto &item : items()) {
if (item->type() >= ItemBase::Type) {
static_cast<ItemBase*>(item.get())->updateZoom(zoom);
@@ -744,10 +1096,13 @@ void Scene::performUndo() {
return item->isNormalStatus() && item->undoable();
});
if (it != filtered.end()) {
if (const auto action = dynamic_cast<ItemAction*>(it->get())) {
if (const auto action = (*it)->asAction()) {
action->revert();
}
(*it)->setStatus(NumberedItem::Status::Undid);
if ((*it)->videoClip()) {
checkDurationsLink();
}
}
}
@@ -756,10 +1111,13 @@ void Scene::performRedo() {
const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);
if (it != filtered.end()) {
if (const auto action = dynamic_cast<ItemAction*>(it->get())) {
if (const auto action = (*it)->asAction()) {
action->apply();
}
(*it)->setStatus(NumberedItem::Status::Normal);
if ((*it)->videoClip()) {
checkDurationsLink();
}
}
}
@@ -805,6 +1163,11 @@ void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
// Scene loses ownership of an item.
// It seems for some reason this line causes a crash. =(
// QGraphicsScene::removeItem(item.get());
item->setSelected(false);
item->setVisible(false);
if (const auto animated = item->asAnimated()) {
animated->releasePlayers();
}
} else {
copy.push_back(item);
}
@@ -842,8 +1205,16 @@ void Scene::save(SaveState state) {
for (const auto &item : _items) {
item->save(state);
}
auto &saved = (state == SaveState::Keep) ? _keptAudio : _savedAudio;
saved = _audio ? std::make_shared<AudioTrack>(*_audio) : nullptr;
auto &savedLinked = (state == SaveState::Keep)
? _keptDurationsLinked
: _savedDurationsLinked;
savedLinked = _durationsLinked;
setAudioSelected(false);
clearSelection();
cancelDrawing();
hideStickyGuides();
}
void Scene::restore(SaveState state) {
@@ -854,6 +1225,15 @@ void Scene::restore(SaveState state) {
for (const auto &item : _items) {
item->restore(state);
}
const auto &saved = (state == SaveState::Keep)
? _keptAudio
: _savedAudio;
setAudioSelected(false);
setAudio(saved ? std::make_shared<AudioTrack>(*saved) : nullptr);
_durationsLinked = (state == SaveState::Keep)
? _keptDurationsLinked
: _savedDurationsLinked;
checkDurationsLink();
clearSelection();
cancelDrawing();
}
@@ -873,6 +1253,7 @@ Scene::~Scene() {
QGraphicsScene::removeItem(pending.get());
}
QGraphicsScene::removeItem(_canvas.get());
QGraphicsScene::removeItem(_stickyGuides.get());
for (const auto &item : items()) {
QGraphicsScene::removeItem(item.get());
}

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <editor/photo_editor_inner_common.h>
#include <editor/scene/scene_item_base.h>
#include "ui/effects/animations.h"
#include <QGraphicsScene>
@@ -25,6 +26,8 @@ class ItemShape;
class ItemText;
class NumberedItem;
class TextEditController;
class VideoClip;
struct AudioTrack;
class Scene final : public QGraphicsScene {
public:
@@ -42,6 +45,9 @@ public:
Scene(const QRectF &rect);
~Scene();
void setCanvasRect(const QRectF &rect);
[[nodiscard]] QRectF canvasRect() const;
void setStickyGuides(std::optional<float64> x, std::optional<float64> y);
void applyBrush(const QColor &color, float64 size, Brush::Tool tool);
void setBlurSource(Fn<QImage(QRect)> source);
void setTextDefaults(
@@ -61,10 +67,26 @@ public:
[[nodiscard]] std::vector<ItemPtr> items(
Qt::SortOrder order = Qt::DescendingOrder) const;
[[nodiscard]] bool hasAnimatedItems() const;
[[nodiscard]] bool hasAnimatedResult() const;
[[nodiscard]] bool hasSoundResult() const;
void releaseAnimations();
void setAudio(std::shared_ptr<AudioTrack> audio);
[[nodiscard]] std::shared_ptr<AudioTrack> audio() const;
[[nodiscard]] rpl::producer<> audioChanges() const;
void setAudioSelected(bool selected);
[[nodiscard]] bool audioSelected() const;
[[nodiscard]] rpl::producer<bool> audioSelectedChanges() const;
[[nodiscard]] bool canEqualizeDurations() const;
void equalizeDurations();
void matchDurations(crl::time duration);
[[nodiscard]] bool durationsLinked() const;
void setDurationsLinked(bool linked);
[[nodiscard]] rpl::producer<> durationsLinkChanges() const;
void addItem(ItemPtr item);
void removeItem(not_null<QGraphicsItem*> item);
void removeItem(const ItemPtr &item);
void videoClipChanged(not_null<NumberedItem*> item);
[[nodiscard]] rpl::producer<> addsItem() const;
[[nodiscard]] rpl::producer<> removesItem() const;
@@ -90,6 +112,8 @@ public:
[[nodiscard]] rpl::producer<bool> textEditStates() const;
[[nodiscard]] rpl::producer<QColor> shapeItemSelections() const;
[[nodiscard]] rpl::producer<> shapeItemDeselections() const;
[[nodiscard]] auto videoClipSelections() const
-> rpl::producer<std::shared_ptr<VideoClip>>;
[[nodiscard]] bool hasUndo() const;
[[nodiscard]] bool hasRedo() const;
@@ -106,13 +130,29 @@ protected:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
private:
class StickyGuidesItem;
struct CapturedPlacement {
std::shared_ptr<ItemBase> item;
ItemBase::Placement placement;
};
struct StickyGuide {
Ui::Animations::Simple animation;
float64 position = 0.;
bool shown = false;
};
void removeIf(Fn<bool(const ItemPtr &)> proj);
void setStickyGuide(
Qt::Orientation orientation,
std::optional<float64> position);
void hideStickyGuides();
[[nodiscard]] float64 stickyGuideMargin() const;
[[nodiscard]] QRectF stickyGuideRect(Qt::Orientation orientation) const;
void paintStickyGuide(QPainter &p, Qt::Orientation orientation) const;
void capturePlacements();
void refreshVideoClipSelection();
void updateVideoClipsSound();
void checkDurationsLink();
void commitPlacements();
void startShapeDrawing(const QPointF &position);
void updateShapeDrawing(
@@ -126,12 +166,16 @@ private:
const std::shared_ptr<ItemCanvas> _canvas;
const std::shared_ptr<float64> _lastZ;
const std::unique_ptr<StickyGuidesItem> _stickyGuides;
const std::unique_ptr<TextEditController> _textEdit;
Fn<QImage(QRect)> _blurSource;
std::vector<ItemPtr> _items;
std::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;
std::vector<CapturedPlacement> _capturedPlacements;
QRectF _canvasRect;
StickyGuide _stickyGuideX;
StickyGuide _stickyGuideY;
float64 _lastLineZ = 0.;
float64 _currentZoom = 1.;
@@ -152,8 +196,20 @@ private:
rpl::event_stream<QColor> _shapeItemSelections;
rpl::event_stream<> _shapeItemDeselections;
rpl::event_stream<bool> _pendingShapeStates;
rpl::event_stream<std::shared_ptr<VideoClip>> _videoClipSelections;
rpl::event_stream<> _audioChanges;
rpl::event_stream<bool> _audioSelectedChanges;
rpl::event_stream<> _durationsLinkChanges;
ItemText *_selectedTextItem = nullptr;
ItemShape *_selectedShapeItem = nullptr;
VideoClip *_selectedVideoClip = nullptr;
std::shared_ptr<AudioTrack> _audio;
std::shared_ptr<AudioTrack> _keptAudio;
std::shared_ptr<AudioTrack> _savedAudio;
bool _audioSelected = false;
bool _durationsLinked = false;
bool _keptDurationsLinked = false;
bool _savedDurationsLinked = false;
rpl::lifetime _lifetime;
};

View File

@@ -13,12 +13,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Editor {
ItemAnimated *ItemAnimated::asAnimated() {
return this;
}
VideoTrim ItemAnimated::trim() const {
return {};
}
Media::Encode::AnimatedEntity ItemAnimated::animatedEntity(
const QTransform &sceneToCanvas) const {
const auto composed = QTransform().scale(flipped() ? -1. : 1., 1.)
* sceneTransform()
* sceneToCanvas;
const auto inner = contentRect();
const auto inner = entityRect();
const auto m11 = composed.m11();
const auto m12 = composed.m12();
const auto m21 = composed.m21();
@@ -30,6 +38,7 @@ Media::Encode::AnimatedEntity ItemAnimated::animatedEntity(
: (std::atan2(m12, m11) * 180. / M_PI);
const auto size = inner.size() * scale;
const auto center = composed.map(inner.center());
const auto segment = trim();
return {
.kind = entityKind(),
.bytes = content(),
@@ -38,9 +47,19 @@ Media::Encode::AnimatedEntity ItemAnimated::animatedEntity(
size),
.rotation = rotation,
.flipped = mirrored,
.from = segment.from,
.till = segment.till,
};
}
QRectF ItemAnimated::entityRect() const {
return contentRect();
}
QRectF ItemAnimated::visibleRect() const {
return fittedRect(_paintedFrameSize);
}
void ItemAnimated::paintFrame(
QPainter *p,
const QImage &frame,
@@ -49,13 +68,9 @@ void ItemAnimated::paintFrame(
if (frame.isNull()) {
return;
}
const auto rect = contentRect();
_paintedFrameSize = frame.size();
const auto ratio = style::DevicePixelRatio();
const auto fitted = QSizeF(frame.size())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), fitted).translated(
(rect.width() - fitted.width()) / 2.,
(rect.height() - fitted.height()) / 2.);
const auto resultRect = visibleRect();
if (live) {
p->save();
p->setRenderHint(QPainter::SmoothPixmapTransform);
@@ -68,7 +83,7 @@ void ItemAnimated::paintFrame(
p->restore();
return;
}
auto pixelSize = (fitted * ratio).toSize();
auto pixelSize = (resultRect.size() * ratio).toSize();
if (pixelSize.width() > frame.width()) {
pixelSize = frame.size();
}

View File

@@ -20,14 +20,18 @@ public:
[[nodiscard]] virtual bool hasContent() const = 0;
[[nodiscard]] virtual QByteArray content() const = 0;
[[nodiscard]] virtual crl::time loopDuration() const = 0;
[[nodiscard]] virtual VideoTrim trim() const;
virtual void releasePlayers() = 0;
ItemAnimated *asAnimated() override;
[[nodiscard]] Media::Encode::AnimatedEntity animatedEntity(
[[nodiscard]] virtual Media::Encode::AnimatedEntity animatedEntity(
const QTransform &sceneToCanvas) const;
protected:
[[nodiscard]] virtual Media::Encode::AnimatedEntity::Kind entityKind()
const = 0;
[[nodiscard]] virtual QRectF entityRect() const;
[[nodiscard]] QRectF visibleRect() const override;
void paintFrame(
QPainter *p,
@@ -42,6 +46,7 @@ private:
QSize size;
bool flipped = false;
} _preview;
QSize _paintedFrameSize;
};

View File

@@ -12,7 +12,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "styles/style_editor.h"
#include "styles/style_media_view.h"
#include "styles/style_menu_icons.h"
#include <QGraphicsScene>
@@ -30,15 +29,38 @@ constexpr auto kSnapAngle = 45.;
const auto kDuplicateSequence = QKeySequence("ctrl+d");
const auto kFlipSequence = QKeySequence("ctrl+s");
const auto kDeleteSequence = QKeySequence("delete");
const auto kBackspaceSequence = QKeySequence("backspace");
constexpr auto kMinSizeRatio = 0.05;
constexpr auto kMaxSizeRatio = 1.00;
constexpr auto kStickyDuration = crl::time(150);
constexpr auto kStickyLines = 5;
auto Normalized(float64 angle) {
return angle
+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));
}
[[nodiscard]] float64 StickyStart(
const QRectF &rect,
Qt::Orientation orientation) {
return (orientation == Qt::Horizontal) ? rect.left() : rect.top();
}
[[nodiscard]] float64 StickyLength(
const QRectF &rect,
Qt::Orientation orientation) {
return (orientation == Qt::Horizontal) ? rect.width() : rect.height();
}
[[nodiscard]] float64 StickyLine(
const QRectF &canvas,
Qt::Orientation orientation,
int line) {
return StickyStart(canvas, orientation)
+ StickyLength(canvas, orientation) * line / (kStickyLines - 1);
}
} // namespace
int NumberedItem::type() const {
@@ -69,6 +91,18 @@ bool NumberedItem::isRemovedStatus() const {
return _status == Status::Removed;
}
ItemAction *NumberedItem::asAction() {
return nullptr;
}
ItemAnimated *NumberedItem::asAnimated() {
return nullptr;
}
VideoClip *NumberedItem::videoClip() {
return nullptr;
}
void NumberedItem::save(SaveState state) {
}
@@ -97,6 +131,7 @@ bool NumberedItem::undoable() const {
ItemBase::ItemBase(Data data)
: _lastZ(data.zPtr)
, _imageSize(data.imageSize)
, _maxSizeRatio(data.maxSizeRatio)
, _contentMargins(data.contentMargins)
, _horizontalSize(data.size) {
setFlags(QGraphicsItem::ItemIsMovable
@@ -122,6 +157,21 @@ QRectF ItemBase::innerRect() const {
return QRectF(-hSize / 2, -vSize / 2, hSize, vSize);
}
QRectF ItemBase::fittedRect(QSizeF size) const {
const auto rect = contentRect();
if (size.isEmpty()) {
return rect;
}
const auto fitted = size.scaled(rect.size(), Qt::KeepAspectRatio);
return QRectF(rect.topLeft(), fitted).translated(
(rect.width() - fitted.width()) / 2.,
(rect.height() - fitted.height()) / 2.);
}
QRectF ItemBase::visibleRect() const {
return contentRect();
}
void ItemBase::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
@@ -180,6 +230,7 @@ void ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
: angle);
} else {
QGraphicsItem::mouseMoveEvent(event);
updateSticky(event->modifiers().testFlag(Qt::ShiftModifier));
}
}
@@ -205,6 +256,9 @@ void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {
setCursor(Qt::ClosedHandCursor);
} else {
QGraphicsItem::mousePressEvent(event);
if (event->button() == Qt::LeftButton) {
startStickyDrag();
}
}
}
@@ -212,10 +266,221 @@ void ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
if ((event->button() == Qt::LeftButton) && isHandling()) {
_handle = HandleType::None;
} else {
if (event->button() == Qt::LeftButton) {
finishStickyDrag();
}
QGraphicsItem::mouseReleaseEvent(event);
}
}
ItemBase::StickyAxis &ItemBase::stickyAxis(Qt::Orientation orientation) {
return (orientation == Qt::Horizontal) ? _stickyX : _stickyY;
}
const ItemBase::StickyAxis &ItemBase::stickyAxis(
Qt::Orientation orientation) const {
return (orientation == Qt::Horizontal) ? _stickyX : _stickyY;
}
void ItemBase::startStickyDrag() {
resetStickyAxis(Qt::Horizontal);
resetStickyAxis(Qt::Vertical);
_stickyDrag = { .raw = pos(), .active = true };
notifyStickyGuides();
}
void ItemBase::resetStickyAxis(Qt::Orientation orientation) {
auto &axis = stickyAxis(orientation);
axis.animation.stop();
axis.current = axis.last = Sticky();
}
void ItemBase::updateSticky(bool enabled) {
if (!_stickyDrag.active) {
return;
}
_stickyDrag.raw = pos();
_stickyDrag.others.clear();
if (const auto s = scene()) {
for (const auto item : s->selectedItems()) {
if ((item != this)
&& (item->flags() & QGraphicsItem::ItemIsMovable)) {
_stickyDrag.others.emplace_back(item, item->pos());
}
}
}
applyStickyState(enabled);
}
void ItemBase::applyStickyState(bool enabled) {
if (!_stickyDrag.active) {
return;
}
for (const auto orientation : { Qt::Horizontal, Qt::Vertical }) {
applySticky(
orientation,
enabled ? computeSticky(orientation) : Sticky());
}
applyStickyPosition();
}
void ItemBase::finishStickyDrag() {
if (!_stickyDrag.active) {
return;
}
for (const auto orientation : { Qt::Horizontal, Qt::Vertical }) {
stickyAxis(orientation).animation.stop();
}
applyStickyPosition();
_stickyDrag = {};
resetStickyAxis(Qt::Horizontal);
resetStickyAxis(Qt::Vertical);
notifyStickyGuides();
}
void ItemBase::applySticky(Qt::Orientation orientation, Sticky sticky) {
auto &axis = stickyAxis(orientation);
if (axis.current == sticky) {
return;
}
const auto to = sticky.valid();
const auto continues = !to || (sticky == axis.last);
const auto from = continues
? axis.animation.value(axis.current.valid() ? 1. : 0.)
: 0.;
axis.current = sticky;
if (to) {
axis.last = axis.current;
}
axis.animation = {};
axis.animation.start(
[=] { applyStickyPosition(); },
from,
to ? 1. : 0.,
kStickyDuration,
anim::easeOutCubic);
notifyStickyGuides();
}
void ItemBase::applyStickyPosition() {
if (!_stickyDrag.active) {
return;
}
const auto offset = QPointF(
stickyOffset(Qt::Horizontal),
stickyOffset(Qt::Vertical));
setPos(_stickyDrag.raw + offset);
for (const auto &[item, raw] : _stickyDrag.others) {
item->setPos(raw + offset);
}
}
void ItemBase::notifyStickyGuides() {
if (const auto owner = static_cast<Scene*>(scene())) {
owner->setStickyGuides(
stickyGuide(Qt::Horizontal),
stickyGuide(Qt::Vertical));
}
}
QRectF ItemBase::stickyBounds() const {
return mapToScene(visibleRect()).boundingRect().translated(
_stickyDrag.raw - pos());
}
ItemBase::Sticky ItemBase::computeSticky(Qt::Orientation orientation) const {
struct Candidate {
Sticky sticky;
float64 shift = 0.;
int priority = 0;
};
const auto priority = [](const Sticky &sticky) {
const auto anchor = (sticky.anchor == StickyAnchor::Center) ? 0 : 1;
const auto line = (sticky.line * 2 == kStickyLines - 1)
? 0
: (sticky.line == 0 || sticky.line == kStickyLines - 1)
? 1
: 2;
return anchor * 3 + line;
};
auto candidates = std::vector<Candidate>();
candidates.reserve(kStickyLines * 3);
for (auto line = 0; line != kStickyLines; ++line) {
for (const auto anchor : {
StickyAnchor::Start,
StickyAnchor::Center,
StickyAnchor::End }) {
const auto sticky = Sticky{ line, anchor };
candidates.push_back({
.sticky = sticky,
.shift = stickyShift(orientation, sticky),
.priority = priority(sticky),
});
}
}
ranges::sort(candidates, ranges::less(), &Candidate::priority);
auto kept = std::vector<float64>();
auto result = Sticky();
auto best = _scaledStickyTrigger;
for (const auto &candidate : candidates) {
if (std::abs(candidate.shift) > _scaledStickyTrigger) {
continue;
}
const auto overlaps = ranges::any_of(kept, [&](float64 shift) {
return std::abs(shift - candidate.shift)
< _scaledStickyTrigger * 2;
});
if (overlaps) {
continue;
}
kept.push_back(candidate.shift);
if (std::abs(candidate.shift) <= best) {
best = std::abs(candidate.shift);
result = candidate.sticky;
}
}
return result;
}
float64 ItemBase::stickyShift(
Qt::Orientation orientation,
Sticky sticky) const {
const auto owner = static_cast<Scene*>(scene());
if (!owner || !sticky.valid()) {
return 0.;
}
const auto bounds = stickyBounds();
const auto start = StickyStart(bounds, orientation);
const auto length = StickyLength(bounds, orientation);
const auto anchor = (sticky.anchor == StickyAnchor::Start)
? start
: (sticky.anchor == StickyAnchor::Center)
? (start + length / 2.)
: (start + length);
return StickyLine(owner->canvasRect(), orientation, sticky.line) - anchor;
}
float64 ItemBase::stickyOffset(Qt::Orientation orientation) const {
const auto &axis = stickyAxis(orientation);
const auto stuck = axis.current.valid();
const auto sticky = stuck ? axis.current : axis.last;
if (!sticky.valid()) {
return 0.;
}
const auto progress = axis.animation.value(stuck ? 1. : 0.);
return stickyShift(orientation, sticky) * progress;
}
std::optional<float64> ItemBase::stickyGuide(
Qt::Orientation orientation) const {
const auto owner = static_cast<Scene*>(scene());
const auto sticky = stickyAxis(orientation).current;
if (!owner || !sticky.valid()) {
return std::nullopt;
}
return StickyLine(owner->canvasRect(), orientation, sticky.line);
}
void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
if (scene()) {
scene()->clearSelection();
@@ -238,17 +503,23 @@ void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
_menu = base::make_unique_q<Ui::PopupMenu>(
nullptr,
st::mediaviewPopupMenu);
st::photoEditorMediaMenu);
fillContextMenu(_menu.get());
if (!_menu->empty()) {
_menu->addSeparator();
}
add(
tr::lng_photo_editor_menu_delete,
kDeleteSequence,
[=] { actionDelete(); },
&st::mediaMenuIconDelete);
add(
tr::lng_photo_editor_menu_flip,
kFlipSequence,
[=] { actionFlip(); },
&st::mediaMenuIconFlip);
if (flippable()) {
add(
tr::lng_photo_editor_menu_flip,
kFlipSequence,
[=] { actionFlip(); },
&st::mediaMenuIconFlip);
}
add(
tr::lng_photo_editor_menu_duplicate,
kDuplicateSequence,
@@ -258,6 +529,9 @@ void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
_menu->popup(event->screenPos());
}
void ItemBase::fillContextMenu(not_null<Ui::PopupMenu*> menu) {
}
void ItemBase::performForSelectedItems(Action action) {
if (const auto s = scene()) {
for (const auto item : s->selectedItems()) {
@@ -268,8 +542,14 @@ void ItemBase::performForSelectedItems(Action action) {
}
}
bool ItemBase::flippable() const {
return true;
}
void ItemBase::actionFlip() {
setFlip(!flipped());
if (flippable()) {
setFlip(!flipped());
}
}
void ItemBase::actionDelete() {
@@ -298,8 +578,25 @@ void ItemBase::raiseToTop() {
setZValue((*_lastZ)++);
}
bool ItemBase::sceneEvent(QEvent *event) {
if (event->type() == QEvent::UngrabMouse) {
_handle = HandleType::None;
finishStickyDrag();
}
return NumberedItem::sceneEvent(event);
}
void ItemBase::keyReleaseEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Shift) {
applyStickyState(e->modifiers().testFlag(Qt::ShiftModifier));
}
NumberedItem::keyReleaseEvent(e);
}
void ItemBase::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
if (e->key() == Qt::Key_Shift) {
applyStickyState(e->modifiers().testFlag(Qt::ShiftModifier));
} else if (e->key() == Qt::Key_Escape) {
if (const auto s = scene()) {
s->clearSelection();
s->clearFocus();
@@ -318,7 +615,7 @@ void ItemBase::handleActionKey(not_null<QKeyEvent*> e) {
};
if (matches(kDuplicateSequence)) {
performForSelectedItems(&ItemBase::actionDuplicate);
} else if (matches(kDeleteSequence)) {
} else if (matches(kDeleteSequence) || matches(kBackspaceSequence)) {
performForSelectedItems(&ItemBase::actionDelete);
} else if (matches(kFlipSequence)) {
performForSelectedItems(&ItemBase::actionFlip);
@@ -450,6 +747,7 @@ int ItemBase::type() const {
void ItemBase::updateZoom(float64 zoom) {
_scaledHandleSize = st::photoEditorItemHandleSize / zoom;
_scaledStickyTrigger = st::photoEditorStickyTrigger / zoom;
_scaledInnerMargins = QMarginsF(
_scaledHandleSize,
_scaledHandleSize,
@@ -461,7 +759,7 @@ void ItemBase::updateZoom(float64 zoom) {
_imageSize.height());
_sizeLimits = {
.min = std::max(int(maxSide * kMinSizeRatio), 1),
.max = std::max(int(maxSide * kMaxSizeRatio), 1),
.max = std::max(int(maxSide * _maxSizeRatio), 1),
};
_horizontalSize = std::clamp(
_horizontalSize,
@@ -505,6 +803,7 @@ ItemBase::Data ItemBase::generateData() const {
.flipped = flipped(),
.rotation = int(rotation()),
.imageSize = _imageSize,
.maxSizeRatio = _maxSizeRatio,
.contentMargins = _contentMargins,
};
}
@@ -562,6 +861,9 @@ void ItemBase::restore(SaveState state) {
}
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
applyData(saved.data);
if (const auto owner = static_cast<Scene*>(scene())) {
updateZoom(owner->currentZoom());
}
setZValue(saved.zValue);
setStatus(saved.status);
}

View File

@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/unique_qptr.h"
#include "editor/photo_editor_inner_common.h"
#include "ui/effects/animations.h"
#include <QGraphicsItem>
@@ -22,6 +23,10 @@ class PopupMenu;
namespace Editor {
class ItemAction;
class ItemAnimated;
class VideoClip;
class NumberedItem : public QGraphicsItem {
public:
enum class Status {
@@ -46,6 +51,10 @@ public:
void setUndoable(bool undoable);
[[nodiscard]] bool undoable() const;
[[nodiscard]] virtual ItemAction *asAction();
[[nodiscard]] virtual ItemAnimated *asAnimated();
[[nodiscard]] virtual VideoClip *videoClip();
virtual void save(SaveState state);
virtual void restore(SaveState state);
virtual bool hasState(SaveState state) const;
@@ -68,6 +77,7 @@ public:
bool flipped = false;
int rotation = 0;
QSize imageSize;
float64 maxSizeRatio = 1.;
bool contentMargins = true;
};
@@ -118,9 +128,13 @@ protected:
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
void keyPressEvent(QKeyEvent *e) override;
void keyReleaseEvent(QKeyEvent *e) override;
bool sceneEvent(QEvent *event) override;
virtual void fillContextMenu(not_null<Ui::PopupMenu*> menu);
using Action = void(ItemBase::*)();
void performForSelectedItems(Action action);
[[nodiscard]] virtual bool flippable() const;
virtual void actionFlip();
void actionDelete();
void actionDuplicate();
@@ -128,6 +142,8 @@ protected:
QRectF contentRect() const;
QRectF innerRect() const;
[[nodiscard]] QRectF fittedRect(QSizeF size) const;
[[nodiscard]] virtual QRectF visibleRect() const;
float64 size() const;
float64 horizontalSize() const;
float64 verticalSize() const;
@@ -148,6 +164,33 @@ protected:
virtual void performFlip();
virtual std::shared_ptr<ItemBase> duplicate(Data data) const = 0;
private:
enum class StickyAnchor {
Start,
Center,
End,
};
struct Sticky {
int line = -1;
StickyAnchor anchor = StickyAnchor::Start;
[[nodiscard]] bool valid() const {
return line >= 0;
}
friend inline bool operator==(
const Sticky &,
const Sticky &) = default;
};
struct StickyAxis {
Sticky current;
Sticky last;
Ui::Animations::Simple animation;
};
struct StickyDrag {
std::vector<std::pair<QGraphicsItem*, QPointF>> others;
QPointF raw;
bool active = false;
};
HandleType handleType(const QPointF &pos) const;
QRectF rightHandleRect() const;
QRectF leftHandleRect() const;
@@ -156,11 +199,32 @@ private:
void updatePens(QPen pen);
void handleActionKey(not_null<QKeyEvent*> e);
[[nodiscard]] StickyAxis &stickyAxis(Qt::Orientation orientation);
[[nodiscard]] const StickyAxis &stickyAxis(
Qt::Orientation orientation) const;
void startStickyDrag();
void updateSticky(bool enabled);
void applyStickyState(bool enabled);
void finishStickyDrag();
void resetStickyAxis(Qt::Orientation orientation);
void applySticky(Qt::Orientation orientation, Sticky sticky);
void applyStickyPosition();
void notifyStickyGuides();
[[nodiscard]] QRectF stickyBounds() const;
[[nodiscard]] Sticky computeSticky(Qt::Orientation orientation) const;
[[nodiscard]] float64 stickyShift(
Qt::Orientation orientation,
Sticky sticky) const;
[[nodiscard]] float64 stickyOffset(Qt::Orientation orientation) const;
[[nodiscard]] std::optional<float64> stickyGuide(
Qt::Orientation orientation) const;
Data generateData() const;
void applyData(const Data &data);
const std::shared_ptr<float64> _lastZ;
const QSize _imageSize;
const float64 _maxSizeRatio;
const bool _contentMargins;
struct {
@@ -183,7 +247,11 @@ private:
int max = 0;
} _sizeLimits;
float64 _scaledHandleSize = 1.0;
float64 _scaledStickyTrigger = 0.;
QMarginsF _scaledInnerMargins;
StickyAxis _stickyX;
StickyAxis _stickyY;
StickyDrag _stickyDrag;
float64 _horizontalSize = 0;
float64 _verticalSize = 0;

View File

@@ -40,17 +40,42 @@ ItemCanvas::ItemCanvas() {
setAcceptedMouseButtons({});
}
void ItemCanvas::clearPixmap() {
void ItemCanvas::setCanvasRect(const QRectF &rect) {
prepareGeometryChange();
_canvasRect = rect;
releasePixmap();
update();
}
QRectF ItemCanvas::canvasRect() const {
return _canvasRect.isNull() ? scene()->sceneRect() : _canvasRect;
}
QRectF ItemCanvas::drawableRect() const {
return (_brushData.tool == Brush::Tool::Blur)
? scene()->sceneRect()
: canvasRect();
}
void ItemCanvas::releasePixmap() {
_hq = nullptr;
_p = nullptr;
_pixmap = QPixmap();
}
void ItemCanvas::preparePixmap() {
releasePixmap();
const auto canvas = canvasRect();
_pixmap = QPixmap(
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
(canvas.size() * style::DevicePixelRatio()).toSize());
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
_pixmap.fill(Qt::transparent);
_p = std::make_unique<Painter>(&_pixmap);
_hq = std::make_unique<PainterHighQualityEnabler>(*_p);
_p->translate(-canvas.topLeft());
_p->setClipRect(drawableRect());
_p->setPen(Qt::NoPen);
_p->setBrush(_brushData.color);
}
@@ -59,7 +84,6 @@ void ItemCanvas::applyBrush(const QColor &color, float size, Brush::Tool tool) {
_brushData.color = color;
_brushData.size = size;
_brushData.tool = tool;
_p->setBrush(color);
const auto width = strokeWidth(1.0);
const auto extra = (tool == Brush::Tool::Arrow)
? arrowHeadLength()
@@ -69,31 +93,37 @@ void ItemCanvas::applyBrush(const QColor &color, float size, Brush::Tool tool) {
}
QRectF ItemCanvas::boundingRect() const {
return scene()->sceneRect();
return canvasRect();
}
void ItemCanvas::computeContentRect(const QPointF &p) {
if (!scene() || !IsValidPoint(p)) {
return;
}
const auto sceneSize = scene()->sceneRect().size();
const auto contentLeft = std::max(0., _contentRect.x());
const auto contentTop = std::max(0., _contentRect.y());
const auto canvas = drawableRect();
const auto contentLeft = std::max(canvas.left(), _contentRect.x());
const auto contentTop = std::max(canvas.top(), _contentRect.y());
const auto contentRight = contentLeft + _contentRect.width();
const auto contentBottom = contentTop + _contentRect.height();
_contentRect = QRectF(
QPointF(
std::clamp(p.x() - _brushMargins.left(), 0., contentLeft),
std::clamp(p.y() - _brushMargins.top(), 0., contentTop)),
std::clamp(
p.x() - _brushMargins.left(),
canvas.left(),
contentLeft),
std::clamp(
p.y() - _brushMargins.top(),
canvas.top(),
contentTop)),
QPointF(
std::clamp(
p.x() + _brushMargins.right(),
std::min(contentRight, sceneSize.width()),
sceneSize.width()),
std::min(contentRight, canvas.right()),
canvas.right()),
std::clamp(
p.y() + _brushMargins.bottom(),
std::min(contentBottom, sceneSize.height()),
sceneSize.height())));
std::min(contentBottom, canvas.bottom()),
canvas.bottom())));
}
std::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(
@@ -356,6 +386,7 @@ void ItemCanvas::handleMousePressEvent(
_drawing = false;
return;
}
preparePixmap();
_rectToUpdate = QRectF();
_contentRect = QRectF();
_currentStroke.clear();
@@ -404,11 +435,12 @@ void ItemCanvas::handleMouseReleaseEvent(
drawArrowHead();
update(_rectToUpdate);
if ((_currentStroke.size() >= 2) && _contentRect.isValid()) {
const auto local = _contentRect.translated(-canvasRect().topLeft());
const auto scaledContentRect = QRectF(
_contentRect.x() * style::DevicePixelRatio(),
_contentRect.y() * style::DevicePixelRatio(),
_contentRect.width() * style::DevicePixelRatio(),
_contentRect.height() * style::DevicePixelRatio());
local.x() * style::DevicePixelRatio(),
local.y() * style::DevicePixelRatio(),
local.width() * style::DevicePixelRatio(),
local.height() * style::DevicePixelRatio());
_grabContentRequests.fire({
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
.position = _contentRect.topLeft(),
@@ -420,7 +452,7 @@ void ItemCanvas::handleMouseReleaseEvent(
_lastRenderedIndex = 0;
_lastPointTime = 0;
_currentPath = QPainterPath();
clearPixmap();
releasePixmap();
update();
}
@@ -429,18 +461,19 @@ void ItemCanvas::paint(
const QStyleOptionGraphicsItem *,
QWidget *) {
p->fillRect(_rectToUpdate, Qt::transparent);
const auto origin = canvasRect().topLeft();
if (_brushData.tool == Brush::Tool::Eraser) {
p->save();
p->setOpacity(st::photoEditorEraserPreviewOpacity);
p->drawPixmap(0, 0, _pixmap);
p->drawPixmap(origin, _pixmap);
p->restore();
} else if (_brushData.tool == Brush::Tool::Blur) {
p->save();
p->setOpacity(st::photoEditorBlurPreviewOpacity);
p->drawPixmap(0, 0, _pixmap);
p->drawPixmap(origin, _pixmap);
p->restore();
} else {
p->drawPixmap(0, 0, _pixmap);
p->drawPixmap(origin, _pixmap);
}
_rectToUpdate = QRectF();
}
@@ -469,7 +502,7 @@ void ItemCanvas::cancelDrawing() {
_lastPointTime = 0;
_currentPath = QPainterPath();
_contentRect = QRectF();
clearPixmap();
releasePixmap();
update();
}

View File

@@ -29,7 +29,9 @@ public:
~ItemCanvas();
void applyBrush(const QColor &color, float size, Brush::Tool tool);
void clearPixmap();
void setCanvasRect(const QRectF &rect);
[[nodiscard]] QRectF canvasRect() const;
[[nodiscard]] QRectF drawableRect() const;
void cancelDrawing();
void updateZoom(float64 zoom);
@@ -60,6 +62,8 @@ private:
int64 time = 0;
};
void preparePixmap();
void releasePixmap();
void computeContentRect(const QPointF &p);
void addStrokePoint(const QPointF &point, int64 time);
void drawIncrementalStroke();
@@ -85,6 +89,7 @@ private:
QRectF _rectToUpdate;
QRectF _contentRect;
QRectF _canvasRect;
QMarginsF _brushMargins;
QPointF _lastPoint;

View File

@@ -26,16 +26,15 @@ void ItemImage::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
const auto rect = contentRect();
const auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated(
(rect.width() - pixmapSize.width()) / 2.,
(rect.height() - pixmapSize.height()) / 2.);
p->drawPixmap(resultRect.toRect(), _pixmap);
p->setRenderHint(QPainter::SmoothPixmapTransform);
p->drawPixmap(visibleRect().toRect(), _pixmap);
ItemBase::paint(p, option, w);
}
QRectF ItemImage::visibleRect() const {
return fittedRect(_pixmap.size() / style::DevicePixelRatio());
}
void ItemImage::performFlip() {
_pixmap = _pixmap.transformed(QTransform().scale(-1, 1));
update();

View File

@@ -21,6 +21,7 @@ public:
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
protected:
[[nodiscard]] QRectF visibleRect() const override;
void performFlip() override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:

View File

@@ -0,0 +1,181 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_link.h"
#include "lang/lang_keys.h"
#include "ui/widgets/popup_menu.h"
#include "styles/style_media_player.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QGraphicsSceneMouseEvent>
namespace Editor {
namespace {
constexpr auto kMaxWidthRatio = 0.9;
[[nodiscard]] LinkStyle NextStyle(LinkStyle style) {
switch (style) {
case LinkStyle::Framed: return LinkStyle::SemiTransparent;
case LinkStyle::SemiTransparent: return LinkStyle::Opaque;
case LinkStyle::Opaque: return LinkStyle::Framed;
}
Unexpected("Link style in NextStyle.");
}
[[nodiscard]] LinkStyle PreviousStyle(LinkStyle style) {
switch (style) {
case LinkStyle::Framed: return LinkStyle::Opaque;
case LinkStyle::SemiTransparent: return LinkStyle::Framed;
case LinkStyle::Opaque: return LinkStyle::SemiTransparent;
}
Unexpected("Link style in PreviousStyle.");
}
} // namespace
ItemLink::ItemLink(LinkPreview link, ItemBase::Data data)
: ItemBase(data)
, _link(std::move(link))
, _imageSize(data.imageSize)
, _pill(MakePill(_link, _imageSize)) {
setAspectRatio(_pill.size().height() / _pill.size().width());
}
LinkPill ItemLink::MakePill(const LinkPreview &link, QSize imageSize) {
return LinkPill(
link,
LinkPill::DensityFor(imageSize.width()),
int(imageSize.width() * kMaxWidthRatio));
}
int ItemLink::type() const {
return Type;
}
const LinkPreview &ItemLink::link() const {
return _link;
}
void ItemLink::setLink(LinkPreview link) {
_link = std::move(link);
const auto style = _pill.style();
const auto scale = size() / _pill.size().width();
_pill = MakePill(_link, _imageSize);
_pill.setStyle(style);
applyStretch(
_pill.size().width() * scale,
_pill.size().height() * scale,
false);
update();
}
LinkStyle ItemLink::style() const {
return _pill.style();
}
void ItemLink::setStyle(LinkStyle style) {
_pill.setStyle(style);
update();
}
void ItemLink::nextStyle() {
setStyle(NextStyle(_pill.style()));
}
void ItemLink::setEditCallback(EditCallback callback) {
_edit = std::move(callback);
}
QRectF ItemLink::visibleRect() const {
return fittedRect(_pill.size());
}
bool ItemLink::flippable() const {
return false;
}
void ItemLink::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
const auto rect = visibleRect();
_pill.paint(*p, rect.topLeft(), rect.width() / _pill.size().width());
ItemBase::paint(p, option, w);
}
void ItemLink::mousePressEvent(QGraphicsSceneMouseEvent *event) {
_wasSelected = isSelected() && (event->button() == Qt::LeftButton);
_styleCycled = false;
ItemBase::mousePressEvent(event);
}
void ItemLink::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
const auto clicked = _wasSelected
&& (event->button() == Qt::LeftButton)
&& !isHandling()
&& !dragThresholdPassed(event);
ItemBase::mouseReleaseEvent(event);
if (clicked) {
_styleCycled = true;
nextStyle();
}
}
void ItemLink::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
if (base::take(_styleCycled)) {
setStyle(PreviousStyle(_pill.style()));
}
_wasSelected = false;
if (_edit) {
_edit(this);
} else {
ItemBase::mouseDoubleClickEvent(event);
}
}
void ItemLink::fillContextMenu(not_null<Ui::PopupMenu*> menu) {
const auto current = _pill.style();
const auto addStyle = [&](
const QString &text,
LinkStyle style,
const style::icon *icon) {
const auto checked = (current == style);
menu->addAction(
text,
[=] { setStyle(style); },
checked ? &st::mediaPlayerMenuCheck : icon);
};
addStyle(
tr::lng_photo_editor_text_style_framed(tr::now),
LinkStyle::Framed,
&st::mediaMenuIconTextStyleFramed);
addStyle(
tr::lng_photo_editor_text_style_semi_transparent(tr::now),
LinkStyle::SemiTransparent,
&st::mediaMenuIconTextStyleSemiTransparent);
addStyle(
tr::lng_photo_editor_text_style_opaque(tr::now),
LinkStyle::Opaque,
&st::mediaMenuIconTextStyleOpaque);
if (_edit) {
menu->addAction(
tr::lng_menu_formatting_link_edit(tr::now),
[=] { _edit(this); },
&st::mediaMenuIconEdit);
}
}
std::shared_ptr<ItemBase> ItemLink::duplicate(ItemBase::Data data) const {
auto result = std::make_shared<ItemLink>(_link, std::move(data));
result->_pill.setStyle(_pill.style());
result->_edit = _edit;
return result;
}
} // namespace Editor

View File

@@ -0,0 +1,59 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/editor_link_pill.h"
#include "editor/scene/scene_item_base.h"
namespace Editor {
class ItemLink final : public ItemBase {
public:
enum { Type = ItemBase::Type + 6 };
using EditCallback = Fn<void(not_null<ItemLink*>)>;
ItemLink(LinkPreview link, ItemBase::Data data);
[[nodiscard]] static LinkPill MakePill(
const LinkPreview &link,
QSize imageSize);
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
int type() const override;
[[nodiscard]] const LinkPreview &link() const;
void setLink(LinkPreview link);
[[nodiscard]] LinkStyle style() const;
void setStyle(LinkStyle style);
void nextStyle();
void setEditCallback(EditCallback callback);
protected:
[[nodiscard]] QRectF visibleRect() const override;
[[nodiscard]] bool flippable() const override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
void fillContextMenu(not_null<Ui::PopupMenu*> menu) override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
LinkPreview _link;
const QSize _imageSize;
LinkPill _pill;
EditCallback _edit;
bool _wasSelected = false;
bool _styleCycled = false;
};
} // namespace Editor

View File

@@ -0,0 +1,499 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_message.h"
#include "data/data_document.h"
#include "editor/editor_audio_menu.h"
#include "editor/editor_message_render.h"
#include "editor/editor_message_source.h"
#include "editor/editor_message_video.h"
#include "editor/scene/scene.h"
#include "editor/video/video_segment_player.h"
#include "lang/lang_keys.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/widgets/popup_menu.h"
#include "window/themes/window_theme.h"
#include "styles/style_chat.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QCoreApplication>
#include <QGraphicsSceneMouseEvent>
namespace Editor {
namespace {
constexpr auto kMaxRatio = 8;
constexpr auto kPlaybackFrameSide = 1024;
[[nodiscard]] bool OnMainThread() {
return QThread::currentThread() == QCoreApplication::instance()->thread();
}
[[nodiscard]] int RatioFor(float64 width, int logicalWidth) {
return std::clamp(int(std::ceil(width / logicalWidth)), 1, kMaxRatio);
}
[[nodiscard]] QRect CoverRect(QRect hole, QSize video) {
if (video.isEmpty() || hole.isEmpty()) {
return hole;
}
const auto scale = std::max(
hole.width() / float64(video.width()),
hole.height() / float64(video.height()));
const auto size = QSize(
int(std::ceil(video.width() * scale)),
int(std::ceil(video.height() * scale)));
return QRect(
rect::center(hole) - QPoint(size.width() / 2, size.height() / 2),
size);
}
[[nodiscard]] QSize PlaybackFrameSize(QSize frames) {
const auto ratio = style::DevicePixelRatio();
const auto side = std::max(frames.width(), frames.height());
const auto fit = std::min(1., kPlaybackFrameSide / float64(side));
return QSize(
std::max(int(std::round(frames.width() * fit / ratio)), 1),
std::max(int(std::round(frames.height() * fit / ratio)), 1));
}
} // namespace
ItemMessage::ItemMessage(
std::shared_ptr<MessageSource> source,
std::unique_ptr<MessageRenderer> renderer,
ItemBase::Data data,
std::optional<bool> dark,
MessageVideoOptions video)
: ItemAnimated(std::move(data))
, _source(std::move(source))
, _renderer(std::move(renderer))
, _videoOptions(video)
, _dark(dark) {
attachRenderer();
watchVideo();
}
ItemMessage::~ItemMessage() = default;
void ItemMessage::attachRenderer() {
_renderer->setDark(_dark);
_renderer->setRepaintCallback([=] { scheduleRefresh(); });
setImage(_renderer->render(1), 1);
updateSize();
}
void ItemMessage::scheduleRefresh() {
if (_refreshScheduled) {
return;
}
_refreshScheduled = true;
crl::on_main(crl::guard(this, [=] {
_refreshScheduled = false;
refresh();
}));
}
void ItemMessage::refresh() {
if (!_renderer->ready()) {
return;
}
const auto ratio = std::max(_ratio, 1);
auto image = _renderer->render(ratio);
if (image.isNull()) {
return;
}
setImage(std::move(image), ratio);
updateSize();
update();
}
void ItemMessage::setImage(QImage image, int ratio) {
_image = std::move(image);
_image.setDevicePixelRatio(1.);
_ratio = _image.isNull() ? 0 : ratio;
_mediaRect = _renderer->mediaRect();
_mask = QImage();
_composite = QImage();
}
void ItemMessage::updateSize() {
const auto size = _renderer->size();
if (size.isEmpty() || (size == _size)) {
return;
}
_size = size;
setAspectRatio(_size.height() / float64(_size.width()));
}
int ItemMessage::neededRatio(not_null<QPainter*> p) const {
if (_size.isEmpty()) {
return 1;
}
const auto &transform = p->worldTransform();
const auto device = p->device();
const auto scale = std::hypot(transform.m11(), transform.m12())
* (device ? device->devicePixelRatio() : 1.);
return RatioFor(visibleRect().width() * scale, _size.width());
}
void ItemMessage::ensureRatio(int ratio) {
if (ratio <= _ratio || !_renderer->ready() || !OnMainThread()) {
return;
}
auto image = _renderer->render(ratio);
if (!image.isNull()) {
setImage(std::move(image), ratio);
}
}
void ItemMessage::watchVideo() {
_videoLifetime.destroy();
const auto video = _videoOptions.play ? _source->video() : nullptr;
if (!video) {
return;
}
video->changes(
) | rpl::on_next([=] {
checkVideo();
}, _videoLifetime);
video->load();
checkVideo();
}
void ItemMessage::checkVideo() {
const auto video = _source->video();
if (!video) {
return;
} else if (!isNormalStatus()) {
_radial = nullptr;
return;
} else if (!_clip) {
if (auto source = video->source()) {
createClip(std::move(source));
}
}
const auto loading = !_clip && video->loading();
if (loading && !_radial) {
_radial = std::make_unique<Ui::RadialAnimation>([=](crl::time now) {
updateRadial(now);
});
_radial->start(video->progress());
} else if (loading) {
_radial->update(video->progress(), false, crl::now());
} else {
_radial = nullptr;
}
update();
}
void ItemMessage::updateRadial(crl::time now) {
const auto video = _source->video();
if (!_radial || !video) {
return;
}
const auto loading = !_clip && video->loading();
_radial->update(video->progress(), !loading, now);
update();
}
void ItemMessage::createClip(std::shared_ptr<VideoClipSource> source) {
auto copy = std::make_shared<VideoClipSource>(*source);
copy->hasAudio = copy->hasAudio && _videoOptions.sound;
_clip = std::make_unique<VideoClip>(std::move(copy), [=] {
checkCutout();
update();
});
if (_playersReleased) {
_clip->stop();
}
notifyVideoClipChanged();
}
void ItemMessage::checkCutout() {
if (_cutout || !_clip || !_clip->player()->ready()) {
return;
}
_cutout = true;
update();
}
void ItemMessage::notifyVideoClipChanged() {
if (const auto owner = static_cast<Scene*>(scene())) {
owner->videoClipChanged(this);
}
}
QRect ItemMessage::holeRect() const {
return QRect(_mediaRect.topLeft() * _ratio, _mediaRect.size() * _ratio);
}
QRect ItemMessage::framesRect() const {
const auto hole = holeRect();
if (!_clip) {
return hole;
}
const auto thumbnail = _clip->source()->thumbnail.size();
const auto video = _source->video();
return CoverRect(
hole,
(!thumbnail.isEmpty()
? thumbnail
: video
? video->document()->dimensions
: QSize()));
}
Media::Encode::AnimatedEntity::Cutout ItemMessage::cutout() const {
const auto hole = holeRect();
if (_mask.isNull()) {
_mask = _renderer->videoMask(std::max(_ratio, 1));
}
return {
.picture = _image,
.mask = _mask,
.hole = hole,
.frames = framesRect(),
};
}
const QImage &ItemMessage::composeFrame() {
Expects(_clip != nullptr);
const auto hole = holeRect();
const auto frame = hole.isEmpty()
? QImage()
: _clip->frame(PlaybackFrameSize(framesRect().size()));
if (frame.isNull()) {
return _composite.isNull() ? _image : _composite;
}
return Media::Encode::ComposeCutout(cutout(), frame, _composite);
}
void ItemMessage::paintLoading(QPainter *p) const {
if (_mediaRect.isEmpty() || _size.isEmpty()) {
return;
}
const auto rect = visibleRect();
const auto scale = rect.width() / _size.width();
const auto center = rect.topLeft()
+ rect::center(QRectF(_mediaRect)) * scale;
const auto side = st::msgFileLayout.thumbSize * scale;
const auto line = st::msgFileRadialLine * scale;
const auto inner = QRectF(
center.x() - side / 2. + line,
center.y() - side / 2. + line,
side - 2 * line,
side - 2 * line);
PainterHighQualityEnabler hq(*p);
_radial->draw(*p, inner, line, st::historyFileThumbRadialFg);
}
void ItemMessage::save(SaveState state) {
ItemBase::save(state);
if (!_size.isEmpty()) {
ensureRatio(RatioFor(visibleRect().width(), _size.width()));
}
((state == SaveState::Keep) ? _kept : _saved) = _clip
? std::make_optional(_clip->state())
: std::nullopt;
}
void ItemMessage::restore(SaveState state) {
if (!hasState(state)) {
return;
}
ItemBase::restore(state);
const auto &saved = (state == SaveState::Keep) ? _kept : _saved;
if (_clip && saved) {
_clip->restore(*saved);
}
}
void ItemMessage::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
ensureRatio(neededRatio(p));
if (w) {
_playersReleased = false;
if (_clip) {
_clip->resume();
}
}
if (!_image.isNull()) {
const auto &image = (_cutout && w)
? composeFrame()
: (_cutout && !_composite.isNull())
? _composite
: _image;
p->setRenderHint(QPainter::SmoothPixmapTransform);
p->drawImage(visibleRect(), image);
}
if (_radial && w) {
paintLoading(p);
}
ItemBase::paint(p, option, w);
}
int ItemMessage::type() const {
return Type;
}
bool ItemMessage::animated() const {
return _cutout && _clip && _clip->animated();
}
bool ItemMessage::hasContent() const {
return _clip && _clip->hasContent();
}
QByteArray ItemMessage::content() const {
return _clip ? _clip->content() : QByteArray();
}
crl::time ItemMessage::loopDuration() const {
return _clip ? _clip->loopDuration() : 0;
}
VideoTrim ItemMessage::trim() const {
return _clip ? _clip->trim() : VideoTrim();
}
void ItemMessage::releasePlayers() {
_playersReleased = true;
if (_clip) {
_clip->stop();
}
}
void ItemMessage::setStatus(Status status) {
if (status != Status::Normal) {
releasePlayers();
}
ItemBase::setStatus(status);
if (status == Status::Normal) {
checkVideo();
}
}
VideoClip *ItemMessage::videoClip() {
return _clip.get();
}
Media::Encode::AnimatedEntity ItemMessage::animatedEntity(
const QTransform &sceneToCanvas) const {
auto result = ItemAnimated::animatedEntity(sceneToCanvas);
if (_cutout && !_image.isNull()) {
result.cutout = cutout();
}
return result;
}
Media::Encode::AnimatedEntity::Kind ItemMessage::entityKind() const {
return Media::Encode::AnimatedEntity::Kind::Webm;
}
QRectF ItemMessage::entityRect() const {
return visibleRect();
}
const std::shared_ptr<MessageSource> &ItemMessage::source() const {
return _source;
}
void ItemMessage::setSource(std::shared_ptr<MessageSource> source) {
_videoLifetime.destroy();
_clip = nullptr;
_radial = nullptr;
_cutout = false;
_source = std::move(source);
_renderer = std::make_unique<MessageRenderer>(_source);
_ratio = 0;
attachRenderer();
watchVideo();
notifyVideoClipChanged();
update();
}
std::optional<bool> ItemMessage::dark() const {
return _dark;
}
void ItemMessage::setDark(std::optional<bool> dark) {
if (_dark == dark) {
return;
}
_dark = dark;
_ratio = 0;
_renderer->setDark(dark);
refresh();
}
void ItemMessage::setEditCallback(EditCallback callback) {
_edit = std::move(callback);
}
bool ItemMessage::editable() const {
return _edit && _source->link().has_value();
}
QRectF ItemMessage::visibleRect() const {
return fittedRect(_size);
}
bool ItemMessage::flippable() const {
return false;
}
void ItemMessage::fillContextMenu(not_null<Ui::PopupMenu*> menu) {
const auto dark = _dark.value_or(Window::Theme::IsNightMode());
menu->addAction(
(dark
? tr::lng_settings_theme_day(tr::now)
: tr::lng_settings_theme_night(tr::now)),
[=] { setDark(!dark); },
&st::mediaMenuIconNightMode);
if (editable()) {
menu->addAction(
tr::lng_menu_formatting_link_edit(tr::now),
[=] { _edit(this); },
&st::mediaMenuIconEdit);
}
if (_clip && _clip->hasAudio()) {
AddVolumeAction(menu, _clip->volume(), [=](float64 volume) {
_clip->setVolume(volume);
});
}
}
void ItemMessage::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
if (editable()) {
_edit(this);
} else {
ItemBase::mouseDoubleClickEvent(event);
}
}
std::shared_ptr<ItemBase> ItemMessage::duplicate(ItemBase::Data data) const {
auto result = std::make_shared<ItemMessage>(
_source,
std::make_unique<MessageRenderer>(_source),
std::move(data),
_dark,
_videoOptions);
result->_edit = _edit;
if (_clip && result->_clip) {
result->_clip->restore(_clip->state());
}
return result;
}
} // namespace Editor

View File

@@ -0,0 +1,121 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "editor/scene/scene_item_animated.h"
#include "editor/video/video_clip.h"
namespace Ui {
class RadialAnimation;
} // namespace Ui
namespace Editor {
class MessageRenderer;
class MessageSource;
struct MessageVideoOptions {
bool play = false;
bool sound = false;
};
class ItemMessage final : public ItemAnimated, public base::has_weak_ptr {
public:
enum { Type = ItemBase::Type + 5 };
using EditCallback = Fn<void(not_null<ItemMessage*>)>;
ItemMessage(
std::shared_ptr<MessageSource> source,
std::unique_ptr<MessageRenderer> renderer,
ItemBase::Data data,
std::optional<bool> dark = std::nullopt,
MessageVideoOptions video = {});
~ItemMessage();
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
int type() const override;
[[nodiscard]] bool animated() const override;
[[nodiscard]] bool hasContent() const override;
[[nodiscard]] QByteArray content() const override;
[[nodiscard]] crl::time loopDuration() const override;
[[nodiscard]] VideoTrim trim() const override;
void releasePlayers() override;
void setStatus(Status status) override;
void save(SaveState state) override;
void restore(SaveState state) override;
[[nodiscard]] VideoClip *videoClip() override;
[[nodiscard]] Media::Encode::AnimatedEntity animatedEntity(
const QTransform &sceneToCanvas) const override;
[[nodiscard]] const std::shared_ptr<MessageSource> &source() const;
void setSource(std::shared_ptr<MessageSource> source);
[[nodiscard]] std::optional<bool> dark() const;
void setDark(std::optional<bool> dark);
void setEditCallback(EditCallback callback);
protected:
[[nodiscard]] Media::Encode::AnimatedEntity::Kind entityKind()
const override;
[[nodiscard]] QRectF entityRect() const override;
[[nodiscard]] QRectF visibleRect() const override;
[[nodiscard]] bool flippable() const override;
void fillContextMenu(not_null<Ui::PopupMenu*> menu) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
void attachRenderer();
void scheduleRefresh();
void refresh();
void setImage(QImage image, int ratio);
void updateSize();
void ensureRatio(int ratio);
[[nodiscard]] bool editable() const;
[[nodiscard]] int neededRatio(not_null<QPainter*> p) const;
void watchVideo();
void checkVideo();
void createClip(std::shared_ptr<VideoClipSource> source);
void checkCutout();
void updateRadial(crl::time now);
void notifyVideoClipChanged();
[[nodiscard]] QRect holeRect() const;
[[nodiscard]] QRect framesRect() const;
[[nodiscard]] Media::Encode::AnimatedEntity::Cutout cutout() const;
[[nodiscard]] const QImage &composeFrame();
void paintLoading(QPainter *p) const;
std::shared_ptr<MessageSource> _source;
std::unique_ptr<MessageRenderer> _renderer;
EditCallback _edit;
const MessageVideoOptions _videoOptions;
std::unique_ptr<VideoClip> _clip;
std::unique_ptr<Ui::RadialAnimation> _radial;
std::optional<VideoClip::State> _saved;
std::optional<VideoClip::State> _kept;
QImage _image;
mutable QImage _mask;
QImage _composite;
QSize _size;
QRect _mediaRect;
std::optional<bool> _dark;
int _ratio = 0;
bool _refreshScheduled = false;
bool _cutout = false;
bool _playersReleased = false;
rpl::lifetime _videoLifetime;
};
} // namespace Editor

View File

@@ -156,6 +156,14 @@ QRectF ItemShape::shapeRect() const {
return result;
}
QRectF ItemShape::visibleRect() const {
const auto bounds = (_shape == ShapeType::Arrow)
? (arrowShaftPath().boundingRect()
| arrowHeadPath().boundingRect())
: shapePath().boundingRect();
return bounds + Margins(_strokeWidth / 2.);
}
float64 ItemShape::direction() const {
return flipped() ? -1. : 1.;
}

View File

@@ -49,6 +49,7 @@ protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
[[nodiscard]] QRectF visibleRect() const override;
private:
struct ArrowPoints {
QPointF start;

View File

@@ -684,16 +684,7 @@ void ItemText::paint(
const QStyleOptionGraphicsItem *option,
QWidget *w) {
if (!_pixmap.isNull()) {
const auto rect = contentRect();
const auto pixmapSize = QSizeF(
_pixmap.size() / style::DevicePixelRatio()
).scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(
rect.topLeft(),
pixmapSize
).translated(
(rect.width() - pixmapSize.width()) / 2.,
(rect.height() - pixmapSize.height()) / 2.);
const auto resultRect = visibleRect();
p->save();
p->setRenderHint(QPainter::SmoothPixmapTransform);
if (flipped()) {
@@ -708,6 +699,10 @@ void ItemText::paint(
ItemBase::paint(p, option, w);
}
QRectF ItemText::visibleRect() const {
return fittedRect(_pixmap.size() / style::DevicePixelRatio());
}
int ItemText::type() const {
return Type;
}

View File

@@ -100,6 +100,7 @@ public:
void restore(SaveState state) override;
protected:
[[nodiscard]] QRectF visibleRect() const override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;

View File

@@ -7,11 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_video.h"
#include "ui/image/image_prepare.h"
#include "editor/editor_audio_menu.h"
#include "ui/rect.h"
#include <QtCore/QFile>
namespace Editor {
namespace {
@@ -30,75 +28,49 @@ constexpr auto kPlaybackFrameSide = 512;
} // namespace
ItemVideo::ItemVideo(std::shared_ptr<Source> source, ItemBase::Data data)
ItemVideo::ItemVideo(
std::shared_ptr<VideoClipSource> source,
ItemBase::Data data)
: ItemAnimated(std::move(data))
, _source(std::move(source))
, _frameSize(FrameSizeFor(_source->thumbnail))
, _image(_source->thumbnail) {
, _clip(std::make_unique<VideoClip>(std::move(source), [=] { update(); }))
, _frameSize(FrameSizeFor(_clip->source()->thumbnail))
, _image(_clip->source()->thumbnail) {
if (flipped()) {
performFlip();
}
setAspectRatio(_image.isNull()
? 1.
: (_image.height() / float64(_image.width())));
createPlayer();
}
void ItemVideo::createPlayer() {
if (!hasContent()) {
return;
}
const auto callback = [=](::Media::Clip::Notification value) {
clipCallback(value);
};
_reader = _source->path.isEmpty()
? ::Media::Clip::MakeReader(_source->content, callback)
: ::Media::Clip::MakeReader(_source->path, callback);
ItemVideo::~ItemVideo() = default;
VideoClip *ItemVideo::videoClip() {
return _clip.get();
}
void ItemVideo::clipCallback(::Media::Clip::Notification notification) {
using namespace ::Media::Clip;
if (notification == Notification::Reinit) {
if (_reader && _reader->state() == State::Error) {
_reader.setBad();
} else if (_reader && _reader->ready() && !_reader->started()) {
_reader->start({ .frame = _frameSize });
}
}
update();
VideoTrim ItemVideo::trim() const {
return _clip->trim();
}
bool ItemVideo::animated() const {
return _reader.valid() || _releasedAnimation;
return _clip->animated();
}
bool ItemVideo::hasContent() const {
return !_source->path.isEmpty() || !_source->content.isEmpty();
return _clip->hasContent();
}
QByteArray ItemVideo::content() const {
if (_source->path.isEmpty()) {
return _source->content;
}
auto file = QFile(_source->path);
if (file.size() > Images::kReadBytesLimit
|| !file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return file.readAll();
return _clip->content();
}
crl::time ItemVideo::loopDuration() const {
return _source->duration;
return _clip->loopDuration();
}
void ItemVideo::releasePlayers() {
if (!animated()) {
return;
}
_releasedAnimation = true;
_pendingRecreate = true;
_reader.reset();
_clip->stop();
}
void ItemVideo::setStatus(Status status) {
@@ -108,6 +80,19 @@ void ItemVideo::setStatus(Status status) {
ItemBase::setStatus(status);
}
void ItemVideo::save(SaveState state) {
ItemBase::save(state);
((state == SaveState::Keep) ? _kept : _saved) = _clip->state();
}
void ItemVideo::restore(SaveState state) {
if (!hasState(state)) {
return;
}
ItemBase::restore(state);
_clip->restore((state == SaveState::Keep) ? _kept : _saved);
}
int ItemVideo::type() const {
return Type;
}
@@ -116,36 +101,41 @@ Media::Encode::AnimatedEntity::Kind ItemVideo::entityKind() const {
return Media::Encode::AnimatedEntity::Kind::Webm;
}
QImage ItemVideo::currentFrame() {
if (_reader && _reader->started()) {
auto result = _reader->current({ .frame = _frameSize }, crl::now());
if (!result.isNull()) {
return result;
}
}
return _image;
}
void ItemVideo::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
if (_pendingRecreate && w) {
_pendingRecreate = false;
createPlayer();
if (w) {
_clip->resume();
}
const auto frame = _clip->frame(_frameSize);
if (frame.isNull()) {
paintFrame(p, _image, false, false);
} else {
paintFrame(p, frame, true, flipped());
}
const auto live = _reader && _reader->started();
paintFrame(p, currentFrame(), live, live && flipped());
ItemBase::paint(p, option, w);
}
void ItemVideo::fillContextMenu(not_null<Ui::PopupMenu*> menu) {
if (_clip->hasAudio()) {
AddVolumeAction(menu, _clip->volume(), [=](float64 volume) {
_clip->setVolume(volume);
});
}
}
void ItemVideo::performFlip() {
_image = _image.transformed(QTransform().scale(-1, 1));
update();
}
std::shared_ptr<ItemBase> ItemVideo::duplicate(ItemBase::Data data) const {
return std::make_shared<ItemVideo>(_source, std::move(data));
auto result = std::make_shared<ItemVideo>(
_clip->source(),
std::move(data));
result->_clip->restore(_clip->state());
return result;
}
} // namespace Editor

View File

@@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "editor/scene/scene_item_animated.h"
#include "media/clip/media_clip_reader.h"
#include "editor/video/video_clip.h"
namespace Editor {
@@ -16,14 +16,9 @@ class ItemVideo final : public ItemAnimated {
public:
enum { Type = ItemBase::Type + 4 };
struct Source {
QString path;
QByteArray content;
QImage thumbnail;
crl::time duration = 0;
};
ItemVideo(std::shared_ptr<VideoClipSource> source, ItemBase::Data data);
~ItemVideo();
ItemVideo(std::shared_ptr<Source> source, ItemBase::Data data);
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
@@ -32,27 +27,27 @@ public:
[[nodiscard]] bool hasContent() const override;
[[nodiscard]] QByteArray content() const override;
[[nodiscard]] crl::time loopDuration() const override;
[[nodiscard]] VideoTrim trim() const override;
void releasePlayers() override;
void setStatus(Status status) override;
void save(SaveState state) override;
void restore(SaveState state) override;
int type() const override;
[[nodiscard]] VideoClip *videoClip() override;
protected:
[[nodiscard]] Media::Encode::AnimatedEntity::Kind entityKind()
const override;
void fillContextMenu(not_null<Ui::PopupMenu*> menu) override;
void performFlip() override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
void createPlayer();
void clipCallback(::Media::Clip::Notification notification);
[[nodiscard]] QImage currentFrame();
const std::shared_ptr<Source> _source;
const std::unique_ptr<VideoClip> _clip;
const QSize _frameSize;
::Media::Clip::ReaderPointer _reader;
QImage _image;
bool _releasedAnimation = false;
bool _pendingRecreate = false;
VideoClip::State _saved;
VideoClip::State _kept;
};

View File

@@ -426,7 +426,7 @@ void TextEditController::createAtCenter(int rotation, bool flipped) {
_defaultFontSize);
const auto emojiDoc = proxy->document();
const auto sceneCenter = sceneRect.center();
const auto sceneCenter = _scene->canvasRect().center();
const auto adjustWidth = [=] {
applyAutoShrink();
const auto maxTextWidth = sessionWrapWidth();

View File

@@ -0,0 +1,147 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/video/video_clip.h"
#include "editor/video/video_segment_player.h"
#include "editor/video/video_timeline.h"
#include "ui/image/image_prepare.h"
namespace Editor {
VideoClip::VideoClip(
std::shared_ptr<VideoClipSource> source,
Fn<void()> repaint)
: _source(std::move(source))
, _player(std::make_unique<SegmentPlayer>(
_source->path,
_source->content)) {
_player->repaints(
) | rpl::on_next(std::move(repaint), _lifetime);
_player->start();
}
VideoClip::~VideoClip() = default;
const std::shared_ptr<VideoClipSource> &VideoClip::source() const {
return _source;
}
crl::time VideoClip::duration() const {
return _source->duration;
}
not_null<SegmentPlayer*> VideoClip::player() const {
return _player.get();
}
auto VideoClip::timelineFrames()
-> const std::shared_ptr<VideoTimelineFramesCache> & {
if (!_timelineFrames) {
_timelineFrames = std::make_shared<VideoTimelineFramesCache>();
}
return _timelineFrames;
}
VideoTrim VideoClip::trim() const {
return _trim;
}
void VideoClip::setTrim(VideoTrim trim) {
const auto full = duration();
if (full > 0) {
trim.from = std::clamp(trim.from, crl::time(0), full);
trim.till = (trim.till > 0)
? std::clamp(trim.till, trim.from, full)
: 0;
}
if (_trim == trim) {
return;
}
_trim = trim;
_player->setSegment(_trim.from, _trim.till);
}
crl::time VideoClip::loopDuration() const {
const auto full = duration();
if (full <= 0) {
return 0;
}
const auto till = (_trim.till > _trim.from) ? _trim.till : full;
return till - _trim.from;
}
bool VideoClip::hasAudio() const {
return _source->hasAudio;
}
float64 VideoClip::volume() const {
return _volume;
}
void VideoClip::setVolume(float64 volume) {
_volume = std::clamp(volume, 0., 1.);
_player->setVolume(_volume);
}
bool VideoClip::sounding() const {
return hasAudio() && (_volume > 0.);
}
void VideoClip::setSoundEnabled(bool enabled) {
_player->setSound(enabled && hasAudio());
}
VideoClip::State VideoClip::state() const {
return { .trim = _trim, .volume = _volume };
}
void VideoClip::restore(const State &state) {
setTrim(state.trim);
setVolume(state.volume);
}
bool VideoClip::animated() const {
return _player->valid() || _released;
}
bool VideoClip::hasContent() const {
return !_source->path.isEmpty() || !_source->content.isEmpty();
}
QByteArray VideoClip::content() const {
if (_source->path.isEmpty()) {
return _source->content;
}
auto file = QFile(_source->path);
if (file.size() > Images::kReadBytesLimit
|| !file.open(QIODevice::ReadOnly)) {
return QByteArray();
}
return file.readAll();
}
QImage VideoClip::frame(QSize size) {
return _player->frame(size);
}
void VideoClip::stop() {
if (!animated()) {
return;
}
_released = true;
_stopped = true;
_player->stop();
}
void VideoClip::resume() {
if (base::take(_stopped)) {
_player->start();
}
}
} // namespace Editor

View File

@@ -0,0 +1,71 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/photo_editor_inner_common.h"
namespace Editor {
class SegmentPlayer;
class VideoTimelineFramesCache;
struct VideoClipSource {
QString path;
QByteArray content;
QImage thumbnail;
crl::time duration = 0;
bool hasAudio = false;
};
class VideoClip final {
public:
struct State {
VideoTrim trim;
float64 volume = 1.;
};
VideoClip(std::shared_ptr<VideoClipSource> source, Fn<void()> repaint);
~VideoClip();
[[nodiscard]] const std::shared_ptr<VideoClipSource> &source() const;
[[nodiscard]] crl::time duration() const;
[[nodiscard]] not_null<SegmentPlayer*> player() const;
[[nodiscard]] auto timelineFrames()
-> const std::shared_ptr<VideoTimelineFramesCache> &;
[[nodiscard]] VideoTrim trim() const;
void setTrim(VideoTrim trim);
[[nodiscard]] crl::time loopDuration() const;
[[nodiscard]] bool hasAudio() const;
[[nodiscard]] float64 volume() const;
void setVolume(float64 volume);
[[nodiscard]] bool sounding() const;
void setSoundEnabled(bool enabled);
[[nodiscard]] State state() const;
void restore(const State &state);
[[nodiscard]] bool animated() const;
[[nodiscard]] bool hasContent() const;
[[nodiscard]] QByteArray content() const;
[[nodiscard]] QImage frame(QSize size);
void stop();
void resume();
private:
const std::shared_ptr<VideoClipSource> _source;
const std::unique_ptr<SegmentPlayer> _player;
std::shared_ptr<VideoTimelineFramesCache> _timelineFrames;
VideoTrim _trim;
float64 _volume = 1.;
bool _released = false;
bool _stopped = false;
rpl::lifetime _lifetime;
};
} // namespace Editor

View File

@@ -10,12 +10,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer.h"
#include "editor/editor_crop.h"
#include "editor/video/video_quality_slider.h"
#include "editor/video/video_segment_player.h"
#include "editor/video/video_timeline.h"
#include "editor/video/video_timeline_seeker.h"
#include "lang/lang_keys.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "media/streaming/media_streaming_player.h"
#include "media/view/media_view_pip.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/layer_widget.h"
@@ -34,8 +32,6 @@ namespace {
using Media::View::FlipSizeByRotation;
constexpr auto kSeekThrottle = crl::time(120);
constexpr auto kBubbleMinVisible = crl::time(600);
constexpr auto kBubbleDuration = crl::time(500);
@@ -171,16 +167,6 @@ void BarTextButton::paintEvent(QPaintEvent *e) {
return result;
}
[[nodiscard]] Media::Streaming::FrameRequest FrameRequestFor(
QSize size,
bool keepAlpha) {
auto result = Media::Streaming::FrameRequest();
result.resize = size * style::DevicePixelRatio();
result.outer = result.resize;
result.keepAlpha = keepAlpha;
return result;
}
} // namespace
VideoEditor::VideoEditor(
@@ -192,7 +178,11 @@ VideoEditor::VideoEditor(
, _dimensions(descriptor.dimensions)
, _duration(std::max(descriptor.duration, crl::time(1)))
, _data(descriptor.data)
, _initial(descriptor.initial) {
, _initial(descriptor.initial)
, _player(std::make_unique<SegmentPlayer>(
_path,
_content,
SegmentPlayerOptions{ .keepAlpha = _data.webmSticker })) {
_geometry = _initial.geometry;
_gif = _initial.gif;
_geometry.cropType = _data.editor.cropType;
@@ -263,40 +253,21 @@ void VideoEditor::setupTimeline() {
_from = _timeline->from();
_till = _timeline->till();
_cover = _timeline->cover();
_position = _from;
struct State {
base::Timer timer;
crl::time pending = -1;
};
const auto state = lifetime().make_state<State>();
state->timer.setCallback([=] {
if (state->pending >= 0) {
const auto position = state->pending;
state->pending = -1;
restart(position);
}
});
const auto seek = [=](crl::time position) {
state->pending = position;
if (!state->timer.isActive()) {
state->timer.callOnce(kSeekThrottle);
}
};
_seeker = std::make_unique<TimelineSeeker>(
_timeline.get(),
_player.get());
_timeline->trimChanges(
) | rpl::on_next([=](crl::time edge) {
) | rpl::on_next([=] {
_from = _timeline->from();
_till = _timeline->till();
_cover = _timeline->cover();
refreshSizeEstimate();
seek(edge);
}, _timeline->lifetime());
_timeline->coverChanges(
) | rpl::on_next([=](crl::time cover) {
_cover = cover;
seek(cover);
updateBubble();
refreshCoverPreview();
}, _timeline->lifetime());
@@ -306,17 +277,6 @@ void VideoEditor::setupTimeline() {
_dragging = dragging;
if (dragging) {
_draggingHead = _timeline->draggingHead();
if (_instance) {
_instance->pause();
}
} else {
state->timer.cancel();
const auto latest = (state->pending >= 0)
? state->pending
: _position;
state->pending = -1;
const auto resume = std::clamp(latest, _from, _till);
restart((resume >= _till) ? _from : resume);
}
if (!dragging && _draggingHead) {
_draggingHead = false;
@@ -558,40 +518,21 @@ void VideoEditor::setupControls() {
_cancel.fire({});
});
_confirm->setClickedCallback([=] {
_timeline->commitPendingEdit();
_done.fire(collect());
});
}
void VideoEditor::setupStreaming() {
using namespace Media::Streaming;
auto loader = _path.isEmpty()
? MakeBytesLoader(_content)
: MakeFileLoader(_path);
if (!loader) {
return;
}
_instance = std::make_unique<Instance>(
std::make_shared<Document>(std::move(loader)),
nullptr);
if (!_instance->valid()) {
_instance = nullptr;
return;
}
_instance->lockPlayer();
_instance->player().updates(
) | rpl::on_next_error([=](Update &&update) {
handleUpdate(std::move(update));
}, [=](Error &&) {
_instance = nullptr;
_player->repaints(
) | rpl::on_next([=] {
update();
}, _instance->lifetime());
if (_player->held()) {
updateBubble();
}
}, lifetime());
restart(_from);
}
bool VideoEditor::held() const {
return _dragging || _userPaused;
_player->start();
}
void VideoEditor::setupTapToPause() {
@@ -630,15 +571,11 @@ void VideoEditor::setupTapToPause() {
}
void VideoEditor::togglePause() {
if (!_instance) {
if (!_player->valid()) {
return;
}
_userPaused = !_userPaused;
if (_userPaused) {
_instance->pause();
} else {
_instance->resume();
}
_player->setPaused(_userPaused);
update();
}
@@ -795,64 +732,6 @@ void VideoEditor::updateBubble() {
_bubble->update();
}
void VideoEditor::restart(crl::time position) {
if (!_instance) {
return;
}
using namespace Media::Streaming;
if (!_frameRect.isEmpty()
&& _instance->player().ready()
&& !_instance->player().videoSize().isEmpty()) {
_lastFrame = _instance->frame(FrameRequestFor(
_frameRect.size(),
_data.webmSticker)).copy();
}
_position = std::clamp(position, _from, _till);
auto options = PlaybackOptions();
options.mode = Mode::Video;
options.position = _position;
options.loop = false;
_instance->play(options);
if (held()) {
_instance->pause();
}
if (!_dragging && _timeline) {
_timeline->setPlaybackPosition(_position);
}
update();
}
void VideoEditor::handleUpdate(Media::Streaming::Update &&update) {
using namespace Media::Streaming;
v::match(update.data, [&](Information &) {
this->update();
}, [&](PreloadedVideo) {
}, [&](UpdateVideo &data) {
_position = data.position;
if (held()) {
_instance->pause();
this->update();
updateBubble();
return;
}
if (_position >= _till) {
restart(_from);
return;
}
if (_timeline) {
_timeline->setPlaybackPosition(_position);
}
this->update();
}, [&](PreloadedAudio) {
}, [&](UpdateAudio) {
}, [&](WaitingForData) {
}, [&](SpeedEstimate) {
}, [&](MutedByOther) {
}, [&](Finished) {
restart(_from);
});
}
void VideoEditor::applyGeometry() {
const auto size = this->size();
if (size.isEmpty()) {
@@ -919,8 +798,10 @@ void VideoEditor::applyGeometry() {
_frameMatrix.rotate(_geometry.angle);
const auto geometry = _frameMatrix.mapRect(_frameRect);
const auto m = _crop->cropMargins();
_crop->applyTransform(
geometry + _crop->cropMargins(),
geometry + m,
QPoint(m.left(), m.top()),
_geometry.angle,
_geometry.flipped,
frameSizeF);
@@ -962,21 +843,7 @@ void VideoEditor::paint(QPainter &p) {
if (_frameRect.isEmpty()) {
return;
}
auto frame = QImage();
if (_instance
&& _instance->player().ready()
&& !_instance->player().videoSize().isEmpty()) {
frame = _instance->frame(FrameRequestFor(
_frameRect.size(),
_data.webmSticker));
if (!held()) {
// Marking a frame shown lets the player walk past a pause.
_instance->markFrameShown();
}
}
if (frame.isNull()) {
frame = _lastFrame;
}
const auto frame = _player->frame(_frameRect.size());
if (frame.isNull()) {
return;
}
@@ -1012,6 +879,7 @@ void VideoEditor::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
_cancel.fire({});
} else if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
_timeline->commitPendingEdit();
_done.fire(collect());
}
}

View File

@@ -14,11 +14,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "editor/video/video_editor_common.h"
#include "ui/rp_widget.h"
namespace Media::Streaming {
class Instance;
struct Update;
} // namespace Media::Streaming
namespace Ui {
class IconButton;
class LayerWidget;
@@ -29,6 +24,8 @@ class FlatLabel;
namespace Editor {
class Crop;
class SegmentPlayer;
class TimelineSeeker;
class VideoTimeline;
class VideoQualitySlider;
@@ -68,8 +65,6 @@ private:
void setupSizeEstimate();
void refreshSizeEstimate();
void refreshQualityLevels();
void handleUpdate(Media::Streaming::Update &&update);
void restart(crl::time position);
void setupTapToPause();
void updateBubble();
void startCapture();
@@ -78,7 +73,6 @@ private:
void refreshCoverPreview();
void invalidateCoverPreview();
void togglePause();
[[nodiscard]] bool held() const;
void paintPlayBadge(QPainter &p);
void applyGeometry();
void paint(QPainter &p);
@@ -98,12 +92,12 @@ private:
crl::time _from = 0;
crl::time _till = 0;
crl::time _cover = 0;
crl::time _position = 0;
bool _gif = false;
std::unique_ptr<Media::Streaming::Instance> _instance;
std::unique_ptr<SegmentPlayer> _player;
base::unique_qptr<Crop> _crop;
base::unique_qptr<VideoTimeline> _timeline;
std::unique_ptr<TimelineSeeker> _seeker;
base::unique_qptr<VideoQualitySlider> _quality;
base::unique_qptr<Ui::RpWidget> _controls;
base::unique_qptr<Ui::RpWidget> _bar;
@@ -122,7 +116,6 @@ private:
QRect _frameRect;
QTransform _frameMatrix;
QRect _innerRect;
QImage _lastFrame;
bool _dragging = false;
bool _userPaused = false;
crl::time _bubbleCover = -1;

View File

@@ -0,0 +1,259 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/video/video_segment_player.h"
#include "media/player/media_player_instance.h"
#include "media/streaming/media_streaming_common.h"
#include "media/streaming/media_streaming_document.h"
#include "media/streaming/media_streaming_instance.h"
#include "media/streaming/media_streaming_loader_local.h"
#include "media/streaming/media_streaming_player.h"
namespace Editor {
namespace {
[[nodiscard]] Media::Streaming::FrameRequest FrameRequestFor(
QSize size,
bool keepAlpha) {
auto result = Media::Streaming::FrameRequest();
result.resize = size * style::DevicePixelRatio();
result.outer = result.resize;
result.keepAlpha = keepAlpha;
return result;
}
} // namespace
SegmentPlayer::SegmentPlayer(
QString path,
QByteArray content,
SegmentPlayerOptions options)
: _path(std::move(path))
, _content(std::move(content))
, _options(options)
, _volume(options.volume) {
}
SegmentPlayer::~SegmentPlayer() = default;
void SegmentPlayer::start() {
using namespace Media::Streaming;
if (_instance) {
return;
}
auto loader = _path.isEmpty()
? MakeBytesLoader(_content)
: MakeFileLoader(_path);
if (!loader) {
return;
}
_instance = std::make_unique<Instance>(
std::make_shared<Document>(std::move(loader)),
nullptr);
if (!_instance->valid()) {
_instance = nullptr;
return;
}
_instance->lockPlayer();
_instance->player().updates(
) | rpl::on_next_error([=](Update &&update) {
handleUpdate(std::move(update));
}, [=](Error &&) {
crl::on_main(base::make_weak(this), [=] { handleError(); });
}, _instance->lifetime());
restart(_from);
}
void SegmentPlayer::stop() {
_instance = nullptr;
}
bool SegmentPlayer::valid() const {
return (_instance != nullptr);
}
bool SegmentPlayer::ready() const {
return _instance
&& _instance->player().ready()
&& (_options.audio || !_instance->player().videoSize().isEmpty());
}
crl::time SegmentPlayer::segmentTill() const {
return (_till > _from)
? _till
: std::numeric_limits<crl::time>::max();
}
void SegmentPlayer::setSegment(crl::time from, crl::time till) {
_from = std::max(from, crl::time(0));
_till = (till > _from) ? till : 0;
const auto outside = (_position < _from)
|| (_position >= segmentTill());
if (outside && !held()) {
restart(_from);
}
}
void SegmentPlayer::keepLastFrame() {
if (_options.audio || !ready() || _frameSize.isEmpty()) {
return;
}
const auto frame = _instance->frame(
FrameRequestFor(_frameSize, _options.keepAlpha));
if (!frame.isNull()) {
_lastFrame = frame.copy();
}
}
void SegmentPlayer::restart(crl::time position) {
if (!_instance) {
return;
}
using namespace Media::Streaming;
keepLastFrame();
_position = std::clamp(position, _from, segmentTill());
auto options = PlaybackOptions();
options.mode = _options.audio
? Mode::Audio
: (_sound && !_soundFailed)
? Mode::Both
: Mode::Video;
options.position = _position;
options.volume = _volume;
options.loop = false;
if (options.mode != Mode::Video) {
pauseOtherPlayback();
}
_instance->play(options);
if (held()) {
_instance->pause();
}
_positionUpdates.fire_copy(_position);
_repaints.fire({});
}
void SegmentPlayer::pauseOtherPlayback() {
if (std::exchange(_pausedOthers, true)) {
return;
}
const auto player = Media::Player::instance();
player->pause(AudioMsgId::Type::Voice);
player->pause(AudioMsgId::Type::Song);
}
void SegmentPlayer::applyHeld() {
if (!_instance) {
return;
} else if (held()) {
_instance->pause();
} else {
_instance->resume();
}
}
void SegmentPlayer::setPaused(bool paused) {
if (_paused == paused) {
return;
}
_paused = paused;
applyHeld();
}
void SegmentPlayer::setSeeking(bool seeking) {
if (_seeking == seeking) {
return;
}
_seeking = seeking;
applyHeld();
}
void SegmentPlayer::setVolume(float64 volume) {
if (_volume == volume) {
return;
}
_volume = volume;
if (_instance) {
_instance->setVolume(volume);
}
}
void SegmentPlayer::setSound(bool sound) {
if (_options.audio || (_sound == sound)) {
return;
}
_sound = sound;
if (!sound) {
_pausedOthers = false;
}
restart(_position);
}
void SegmentPlayer::handleUpdate(Media::Streaming::Update &&update) {
using namespace Media::Streaming;
v::match(update.data, [&](Information &) {
_repaints.fire({});
}, [&](PreloadedVideo) {
}, [&](UpdateVideo &data) {
if (!_options.audio) {
handlePosition(data.position);
}
}, [&](PreloadedAudio) {
}, [&](UpdateAudio &data) {
if (_options.audio) {
handlePosition(data.position);
}
}, [&](WaitingForData) {
}, [&](SpeedEstimate) {
}, [&](MutedByOther) {
}, [&](Finished) {
restart(_from);
});
}
void SegmentPlayer::handleError() {
_instance = nullptr;
if (_sound && !_soundFailed && !_options.audio) {
_soundFailed = true;
start();
}
_repaints.fire({});
}
void SegmentPlayer::handlePosition(crl::time position) {
_position = position;
if (held()) {
_instance->pause();
_repaints.fire({});
return;
} else if (_position >= segmentTill()) {
restart(_from);
return;
}
_positionUpdates.fire_copy(_position);
_repaints.fire({});
}
QImage SegmentPlayer::frame(QSize size) {
_frameSize = size;
if (_options.audio) {
return QImage();
} else if (ready()) {
auto result = _instance->frame(
FrameRequestFor(size, _options.keepAlpha));
if (!held()) {
_instance->markFrameShown();
}
if (!result.isNull()) {
return result;
}
}
return _lastFrame;
}
} // namespace Editor

View File

@@ -0,0 +1,98 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
namespace Media::Streaming {
class Instance;
struct Update;
} // namespace Media::Streaming
namespace Editor {
struct SegmentPlayerOptions {
bool keepAlpha = false;
bool audio = false;
float64 volume = 1.;
};
class SegmentPlayer final : public base::has_weak_ptr {
public:
SegmentPlayer(
QString path,
QByteArray content,
SegmentPlayerOptions options = {});
~SegmentPlayer();
void start();
void stop();
[[nodiscard]] bool valid() const;
[[nodiscard]] bool ready() const;
[[nodiscard]] crl::time from() const {
return _from;
}
[[nodiscard]] crl::time till() const {
return _till;
}
[[nodiscard]] crl::time position() const {
return _position;
}
[[nodiscard]] bool held() const {
return _paused || _seeking;
}
void setSegment(crl::time from, crl::time till);
void restart(crl::time position);
void setPaused(bool paused);
void setSeeking(bool seeking);
void setVolume(float64 volume);
void setSound(bool sound);
[[nodiscard]] QImage frame(QSize size);
[[nodiscard]] rpl::producer<crl::time> positionUpdates() const {
return _positionUpdates.events();
}
[[nodiscard]] rpl::producer<> repaints() const {
return _repaints.events();
}
private:
void handleUpdate(Media::Streaming::Update &&update);
void handleError();
void handlePosition(crl::time position);
void pauseOtherPlayback();
void applyHeld();
void keepLastFrame();
[[nodiscard]] crl::time segmentTill() const;
const QString _path;
const QByteArray _content;
const SegmentPlayerOptions _options;
std::unique_ptr<Media::Streaming::Instance> _instance;
QImage _lastFrame;
QSize _frameSize;
crl::time _from = 0;
crl::time _till = 0;
crl::time _position = 0;
float64 _volume = 1.;
bool _sound = false;
bool _soundFailed = false;
bool _pausedOthers = false;
bool _paused = false;
bool _seeking = false;
rpl::event_stream<crl::time> _positionUpdates;
rpl::event_stream<> _repaints;
};
} // namespace Editor

View File

@@ -9,49 +9,77 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/media_video_frames.h"
#include "ui/painter.h"
#include "ui/text/format_values.h"
#include "ui/rect.h"
#include "styles/style_editor.h"
#include <QtGui/QPainterPath>
#include <QtGui/QtEvents>
namespace Editor {
namespace {
constexpr auto kMaxFrames = 24;
constexpr auto kMaxCachedFrameSets = 8;
constexpr auto kDotDuration = crl::time(500);
constexpr auto kReloadDelay = crl::time(150);
// Frames are scaled on paint, so small width drift needs no re-extract.
constexpr auto kFrameWidthTolerance = 0.25;
[[nodiscard]] TrimTimelineDescriptor TrimDescriptor(
const VideoTimelineDescriptor &descriptor) {
return {
.duration = descriptor.duration,
.maxDuration = descriptor.maxDuration,
.minDuration = descriptor.minDuration,
.from = descriptor.from,
.till = descriptor.till,
.cover = descriptor.cover,
.trimOnly = descriptor.trimOnly,
};
}
void PaintFramePart(
QPainter &p,
const QImage &frame,
const QRect &target,
int sourceLeft) {
const auto ratio = frame.devicePixelRatio();
p.drawImage(target, frame, QRect(
int(base::SafeRound(sourceLeft * ratio)),
0,
int(base::SafeRound(target.width() * ratio)),
frame.height()));
}
} // namespace
const VideoTimelineFrames *VideoTimelineFramesCache::find(
Fn<bool(const VideoTimelineFrames &set)> matches) const {
for (const auto &set : ranges::views::reverse(_sets)) {
if (matches(set)) {
return &set;
}
}
return nullptr;
}
void VideoTimelineFramesCache::add(VideoTimelineFrames set) {
_sets.push_back(std::move(set));
if (_sets.size() > kMaxCachedFrameSets) {
_sets.erase(_sets.begin());
}
}
int VideoTimelineFramesCache::size() const {
return int(_sets.size());
}
VideoTimeline::VideoTimeline(
not_null<Ui::RpWidget*> parent,
VideoTimelineDescriptor descriptor)
: RpWidget(parent)
, _descriptor(std::move(descriptor))
, _duration(std::max(_descriptor.duration, crl::time(1)))
, _maxDuration((_descriptor.maxDuration > 0)
? std::min(_descriptor.maxDuration, _duration)
: _duration)
, _minDuration(std::min(_descriptor.minDuration, _maxDuration))
, _from(0)
, _till(_maxDuration)
, _cover(0) {
setMouseTracking(true);
// Restore the selection when the editor is opened for a second time.
_from = std::clamp(
_descriptor.from,
crl::time(0),
std::max(_duration - _minDuration, crl::time(0)));
const auto limit = std::min(_from + _maxDuration, _duration);
_till = (_descriptor.till > _from)
? std::min(_descriptor.till, limit)
: limit;
_cover = std::clamp(_descriptor.cover, _from, _till);
: TrimTimeline(parent, TrimDescriptor(descriptor))
, _path(descriptor.path)
, _content(descriptor.content)
, _dimensions(descriptor.dimensions)
, _cache(descriptor.cache)
, _reloadTimer([=] { reloadFrames(); }) {
sizeValue(
) | rpl::filter([=](QSize size) {
return !size.isEmpty();
@@ -61,69 +89,14 @@ VideoTimeline::VideoTimeline(
}
VideoTimeline::~VideoTimeline() {
if (_framesCancel) {
_framesCancel->store(true);
if (_loading) {
_loading->cancel->store(true);
}
}
int VideoTimeline::resizeGetHeight(int newWidth) {
return st::videoTimelineHeight
+ st::videoTimelinePlayheadOverflow
+ st::videoTimelinePlayheadOutline;
}
QRect VideoTimeline::stripRect() const {
const auto top = st::videoTimelineLabelHeight
+ st::videoTimelineLabelSkip;
const auto handle = st::videoTimelineHandleWidth;
return QRect(
handle,
top,
std::max(width() - handle * 2, 1),
st::videoTimelineStripHeight);
}
QRect VideoTimeline::labelRect() const {
// Full width, so the size lines up with the quality labels below.
return QRect(0, 0, width(), st::videoTimelineLabelHeight);
}
void VideoTimeline::moveWindowTo(crl::time center) {
const auto span = _till - _from;
const auto half = span / 2;
const auto from = std::clamp(
center - half,
crl::time(0),
std::max(_duration - span, crl::time(0)));
if (_from == from) {
return;
}
_from = from;
_till = from + span;
setCover(std::clamp(_cover, _from, _till), true);
_trimChanges.fire_copy(_from);
}
crl::time VideoTimeline::timeAt(int x) const {
const auto strip = stripRect();
if (strip.width() <= 0) {
return 0;
}
const auto shift = std::clamp(x - strip.x(), 0, strip.width());
return crl::time(
base::SafeRound(shift * float64(_duration) / strip.width()));
}
int VideoTimeline::xAt(crl::time time) const {
const auto strip = stripRect();
const auto clamped = std::clamp(time, crl::time(0), _duration);
return strip.x() + int(base::SafeRound(
clamped * float64(strip.width()) / _duration));
}
QPoint VideoTimeline::coverDot() const {
return QPoint(
xAt(_cover),
xAt(cover()),
stripRect().y()
- st::videoTimelinePlayheadOverflow
- st::videoTimelinePlayheadOutline
@@ -131,69 +104,82 @@ QPoint VideoTimeline::coverDot() const {
- st::videoTimelineDotActiveSize / 2);
}
bool VideoTimeline::draggingHead() const {
return (_grab == Grab::Head);
void VideoTimeline::headGrabChanged(bool grabbed) {
_dotActive.start(
[=] { update(); },
grabbed ? 0. : 1.,
grabbed ? 1. : 0.,
kDotDuration,
anim::easeOutQuint);
}
void VideoTimeline::setPlaybackPosition(crl::time position) {
const auto clamped = std::clamp(position, _from, _till);
if (_playback == clamped) {
return;
}
_playback = clamped;
update();
}
void VideoTimeline::setSizeLabel(const QString &text) {
if (_sizeLabel == text) {
return;
}
_sizeLabel = text;
update();
void VideoTimeline::visibleRangeChanged() {
_reloadTimer.callOnce(kReloadDelay);
}
void VideoTimeline::reloadFrames() {
const auto strip = stripRect();
const auto height = strip.height();
const auto dimensions = _descriptor.dimensions;
if (strip.isEmpty() || dimensions.isEmpty() || height <= 0) {
if (strip.isEmpty() || _dimensions.isEmpty() || height <= 0) {
return;
}
const auto aspectWidth = std::max(
int(base::SafeRound(
height * dimensions.width() / float64(dimensions.height()))),
height * _dimensions.width() / float64(_dimensions.height()))),
1);
const auto count = std::clamp(
(strip.width() + aspectWidth - 1) / aspectWidth,
1,
kMaxFrames);
const auto frameWidth = (strip.width() + count - 1) / count;
const auto kept = (int(_frames.size()) == count)
&& (_framesBox.height() == height)
&& (std::abs(frameWidth - _framesBox.width())
<= _framesBox.width() * kFrameWidthTolerance);
_frameWidth = frameWidth;
if (kept) {
const auto from = visibleFrom();
const auto span = visibleTill() - from;
if (span <= 0) {
return;
}
if (_framesCancel) {
_framesCancel->store(true);
const auto matches = [&](const VideoTimelineFrames &set) {
return (int(set.frames.size()) == count)
&& (set.from == from)
&& (set.span == span)
&& (set.box.height() == height)
&& (std::abs(frameWidth - set.box.width())
<= set.box.width() * kFrameWidthTolerance);
};
if (_loading && matches(_loading->set)) {
return;
} else if (_loading) {
_loading->cancel->store(true);
_loading = nullptr;
}
_framesBox = QSize(frameWidth, height);
_frames = std::vector<QImage>(count);
if (matches(_frames)) {
return;
}
const auto cached = _cache ? _cache->find(matches) : nullptr;
if (cached) {
_frames = *cached;
update();
return;
}
_loading = std::make_unique<Loading>(Loading{
.set = {
.frames = std::vector<QImage>(count),
.from = from,
.span = span,
.box = QSize(frameWidth, height),
},
.cancel = std::make_shared<std::atomic<bool>>(false),
});
auto positions = std::vector<crl::time>();
positions.reserve(count);
for (auto i = 0; i != count; ++i) {
positions.push_back(crl::time(
base::SafeRound((i + 0.5) * _duration / count)));
positions.push_back(from + crl::time(
base::SafeRound((i + 0.5) * span / count)));
}
const auto cancel = std::make_shared<std::atomic<bool>>(false);
_framesCancel = cancel;
const auto path = _descriptor.path;
const auto content = _descriptor.content;
const auto box = _framesBox * style::DevicePixelRatio();
const auto cancel = _loading->cancel;
const auto path = _path;
const auto content = _content;
const auto box = _loading->set.box * style::DevicePixelRatio();
crl::async([=, weak = base::make_weak(this)] {
Media::Video::ExtractFrames(path, content, {
.positions = positions,
@@ -205,275 +191,90 @@ void VideoTimeline::reloadFrames() {
}
frame.setDevicePixelRatio(style::DevicePixelRatio());
crl::on_main(weak, [=, frame = std::move(frame)]() mutable {
if (cancel->load() || index >= int(_frames.size())) {
if (cancel->load()
|| !_loading
|| index >= int(_loading->set.frames.size())) {
return;
}
_frames[index] = std::move(frame);
_loading->set.frames[index] = std::move(frame);
update();
});
return true;
});
crl::on_main(weak, [=] {
if (cancel->load() || !_loading) {
return;
}
_frames = std::move(_loading->set);
_loading = nullptr;
if (_cache) {
_cache->add(_frames);
}
update();
});
});
}
VideoTimeline::Grab VideoTimeline::grabAt(QPoint position) const {
const auto slop = st::videoTimelineHandleHitSlop;
const auto handle = st::videoTimelineHandleWidth;
const auto x = position.x();
const auto left = xAt(_from);
const auto right = xAt(_till);
const auto span = std::max(right - left, 1);
const auto inside = std::min(int(slop), span / 3);
if (x >= left - handle - slop && x <= left + inside) {
return Grab::Left;
} else if (x <= right + handle + slop && x >= right - inside) {
return Grab::Right;
} else if (x > left && x < right) {
return Grab::Head;
}
return Grab::Window;
}
crl::time VideoTimeline::minSelection() const {
const auto strip = stripRect();
// Keeps the head reachable when a long clip squeezes the window.
const auto pixels = st::videoTimelinePlayheadWidth
+ st::videoTimelineHandleHitSlop;
const auto byPixels = (strip.width() > pixels)
? crl::time(base::SafeRound(
pixels * float64(_duration) / strip.width()))
: _duration;
return std::clamp(
std::max(_minDuration, byPixels),
crl::time(0),
_maxDuration);
}
void VideoTimeline::updateCursor(Grab grab) {
setCursor((grab == Grab::None) ? style::cur_default : style::cur_sizehor);
}
void VideoTimeline::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
const auto position = e->pos();
_grab = grabAt(position);
if (_grab == Grab::Left) {
_grabShift = position.x() - xAt(_from);
} else if (_grab == Grab::Right) {
_grabShift = position.x() - xAt(_till);
} else {
_grabShift = 0;
}
updateCursor(_grab);
if (_grab == Grab::Head) {
_dotActive.start(
[=] { update(); },
0.,
1.,
kDotDuration,
anim::easeOutQuint);
}
_draggingChanges.fire(true);
applyGrab(position);
}
void VideoTimeline::mouseMoveEvent(QMouseEvent *e) {
if (_grab == Grab::None) {
updateCursor(grabAt(e->pos()));
return;
}
applyGrab(e->pos());
}
void VideoTimeline::mouseReleaseEvent(QMouseEvent *e) {
if (_grab == Grab::None) {
return;
}
const auto wasHead = (_grab == Grab::Head);
_grab = Grab::None;
_grabShift = 0;
if (wasHead) {
_dotActive.start(
[=] { update(); },
1.,
0.,
kDotDuration,
anim::easeOutQuint);
}
updateCursor(grabAt(e->pos()));
_draggingChanges.fire(false);
}
void VideoTimeline::leaveEventHook(QEvent *e) {
if (_grab == Grab::None) {
setCursor(style::cur_default);
}
}
void VideoTimeline::applyGrab(QPoint position) {
const auto at = timeAt(position.x() - _grabShift);
const auto minimum = minSelection();
switch (_grab) {
case Grab::Left: {
const auto highest = std::max(_till - minimum, crl::time(0));
_from = std::clamp(at, crl::time(0), highest);
if (_till - _from > _maxDuration) {
_till = _from + _maxDuration;
}
setCover(std::clamp(_cover, _from, _till), true);
_trimChanges.fire_copy(_from);
} break;
case Grab::Right: {
const auto lowest = std::min(_from + minimum, _duration);
_till = std::clamp(at, lowest, _duration);
if (_till - _from > _maxDuration) {
_from = _till - _maxDuration;
}
setCover(std::clamp(_cover, _from, _till), true);
_trimChanges.fire_copy(_till);
} break;
case Grab::Head: {
setCover(std::clamp(at, _from, _till), true);
} break;
case Grab::Window: {
moveWindowTo(at);
} break;
case Grab::None: return;
}
update();
}
void VideoTimeline::setCover(crl::time cover, bool notify) {
if (_cover == cover) {
return;
}
_cover = cover;
_playback = cover;
if (notify) {
_coverChanges.fire_copy(_cover);
}
update();
}
void VideoTimeline::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto hq = PainterHighQualityEnabler(p);
const auto strip = stripRect();
if (strip.isEmpty()) {
return;
}
auto path = QPainterPath();
const auto radius = st::videoTimelineRadius;
path.addRoundedRect(QRectF(strip), radius, radius);
p.setClipPath(path);
paintFrames(p, strip);
p.setClipping(false);
paintSelection(p, strip);
paintHead(p, strip);
paintDuration(p, strip);
paintCoverDot(p);
}
void VideoTimeline::paintFrames(QPainter &p, const QRect &strip) {
void VideoTimeline::paintStrip(QPainter &p, const QRect &strip) {
p.fillRect(strip, st::videoTimelinePlaceholderBg);
if (_frameWidth <= 0) {
paintFrames(p, strip, _frames);
if (_loading) {
paintFrames(p, strip, _loading->set);
}
}
void VideoTimeline::paintFrames(
QPainter &p,
const QRect &strip,
const VideoTimelineFrames &set) {
const auto count = int(set.frames.size());
if (!count || set.span <= 0) {
return;
}
const auto count = int(_frames.size());
const auto stripRight = rect::right(strip);
for (auto i = 0; i != count; ++i) {
const auto &frame = _frames[i];
const auto &frame = set.frames[i];
if (frame.isNull()) {
continue;
}
const auto x = strip.x() + i * strip.width() / count;
p.drawImage(QRect(x, strip.y(), _frameWidth, strip.height()), frame);
const auto left = xAt(set.from + i * set.span / count);
const auto right = xAt(set.from + (i + 1) * set.span / count);
if (right <= strip.x() || left >= stripRight) {
continue;
}
const auto natural = set.box.width();
const auto slot = std::max(right - left, 1);
if (slot <= natural) {
PaintFramePart(
p,
frame,
QRect(left, strip.y(), slot, strip.height()),
(natural - slot) / 2);
continue;
}
for (auto x = left; x < right; x += natural) {
PaintFramePart(
p,
frame,
QRect(x, strip.y(), std::min(natural, right - x), strip.height()),
0);
}
}
}
void VideoTimeline::paintSelection(QPainter &p, const QRect &strip) {
const auto left = xAt(_from);
const auto right = xAt(_till);
const auto radius = st::videoTimelineRadius;
if (left > strip.x()) {
p.fillRect(
QRect(strip.x(), strip.y(), left - strip.x(), strip.height()),
st::videoTimelineDimBg);
void VideoTimeline::paintOverlay(QPainter &p) {
if (trimOnly()) {
return;
}
const auto stripRight = strip.x() + strip.width();
if (right < stripRight) {
p.fillRect(
QRect(right, strip.y(), stripRight - right, strip.height()),
st::videoTimelineDimBg);
const auto strip = stripRect();
const auto centre = coverDot();
if (centre.x() < strip.x() || centre.x() > rect::right(strip)) {
return;
}
const auto handle = st::videoTimelineHandleWidth;
const auto border = st::videoTimelineHandleGripWidth;
const auto outer = QRectF(
left - handle,
strip.y(),
(right - left) + handle * 2,
strip.height());
auto frame = QPainterPath();
frame.addRoundedRect(outer, radius, radius);
auto inner = QPainterPath();
inner.addRect(QRectF(
left,
strip.y() + border,
std::max(right - left, 0),
std::max(strip.height() - border * 2, 0)));
p.setPen(Qt::NoPen);
p.setBrush(st::videoTimelineFg);
p.drawPath(frame.subtracted(inner));
const auto gripWidth = st::videoTimelineHandleGripWidth;
const auto gripHeight = std::min(
int(st::videoTimelineHandleGripHeight),
strip.height() / 2);
const auto gripY = strip.y() + (strip.height() - gripHeight) / 2;
p.setBrush(st::videoTimelineDimBg);
for (const auto x : { left - handle + (handle - gripWidth) / 2,
right + (handle - gripWidth) / 2 }) {
p.drawRoundedRect(
QRectF(x, gripY, gripWidth, gripHeight),
gripWidth / 2.,
gripWidth / 2.);
}
}
void VideoTimeline::paintHead(QPainter &p, const QRect &strip) {
const auto width = st::videoTimelinePlayheadWidth;
const auto outline = st::videoTimelinePlayheadOutline;
const auto overflow = st::videoTimelinePlayheadOverflow;
const auto x = std::clamp(
xAt((_playback >= 0) ? _playback : _cover) - width / 2.,
1. * xAt(_from),
1. * std::max(xAt(_till) - width, xAt(_from)));
const auto head = QRectF(
x,
strip.y() - overflow,
width,
strip.height() + overflow * 2);
const auto full = head.marginsAdded(
{ 1. * outline, 1. * outline, 1. * outline, 1. * outline });
p.setPen(Qt::NoPen);
p.setBrush(st::videoTimelineDimBg);
p.drawRoundedRect(full, width / 2. + outline, width / 2. + outline);
p.setBrush(st::videoTimelineFg);
p.drawRoundedRect(head, width / 2., width / 2.);
}
void VideoTimeline::paintCoverDot(QPainter &p) {
const auto active = _dotActive.value((_grab == Grab::Head) ? 1. : 0.);
const auto active = _dotActive.value(draggingHead() ? 1. : 0.);
const auto size = st::videoTimelineDotSize
+ (st::videoTimelineDotActiveSize - st::videoTimelineDotSize)
* active;
const auto centre = coverDot();
p.setPen(Qt::NoPen);
p.setBrush(st::videoTimelineDotFg);
p.drawEllipse(QRectF(
@@ -483,48 +284,4 @@ void VideoTimeline::paintCoverDot(QPainter &p) {
size));
}
void VideoTimeline::paintDuration(QPainter &p, const QRect &strip) {
const auto stamp = [](crl::time value) {
return Ui::FormatDurationText(int(value / 1000))
+ '.'
+ QString::number((value % 1000) / 100);
};
const auto text = (_till - _from >= _duration)
? stamp(_till - _from)
: (stamp(_from) + QString::fromUtf8(" ") + stamp(_till));
const auto label = labelRect();
const auto &font = st::videoTimelineDurationStyle.font;
const auto width = font->width(text);
p.setFont(font);
// They must never overlap, so the size goes whole or not at all.
const auto sizeWidth = _sizeLabel.isEmpty()
? 0
: font->width(_sizeLabel);
const auto skip = st::videoTimelineSizeSkip;
const auto sizeShown = sizeWidth
&& (width + skip + sizeWidth <= label.width());
if (sizeShown) {
p.setPen(st::videoTimelineSizeFg);
p.drawText(label, Qt::AlignVCenter | Qt::AlignRight, _sizeLabel);
}
const auto available = sizeShown
? (label.width() - sizeWidth - skip)
: label.width();
const auto shown = (width <= available)
? text
: font->elided(text, available);
const auto shownWidth = std::min(width, available);
const auto center = (xAt(_from) + xAt(_till)) / 2;
const auto x = std::clamp(
center - shownWidth / 2,
label.x(),
label.x() + std::max(available - shownWidth, 0));
p.setPen(st::videoTimelineDurationFg);
p.drawText(
QRect(x, label.y(), shownWidth, label.height()),
Qt::AlignVCenter | Qt::AlignLeft,
shown);
}
} // namespace Editor

View File

@@ -7,16 +7,36 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "base/timer.h"
#include "editor/editor_trim_timeline.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace Editor {
struct VideoTimelineFrames {
std::vector<QImage> frames;
crl::time from = 0;
crl::time span = 0;
QSize box;
};
class VideoTimelineFramesCache final {
public:
[[nodiscard]] const VideoTimelineFrames *find(
Fn<bool(const VideoTimelineFrames &set)> matches) const;
void add(VideoTimelineFrames set);
[[nodiscard]] int size() const;
private:
std::vector<VideoTimelineFrames> _sets;
};
struct VideoTimelineDescriptor {
QString path;
QByteArray content;
QSize dimensions;
std::shared_ptr<VideoTimelineFramesCache> cache;
crl::time duration = 0;
crl::time maxDuration = 0;
@@ -26,107 +46,47 @@ struct VideoTimelineDescriptor {
crl::time from = 0;
crl::time till = 0;
crl::time cover = 0;
bool trimOnly = false;
};
class VideoTimeline final
: public Ui::RpWidget
, public base::has_weak_ptr {
class VideoTimeline final : public TrimTimeline {
public:
VideoTimeline(
not_null<Ui::RpWidget*> parent,
VideoTimelineDescriptor descriptor);
~VideoTimeline();
[[nodiscard]] crl::time from() const {
return _from;
}
[[nodiscard]] crl::time till() const {
return _till;
}
[[nodiscard]] crl::time cover() const {
return _cover;
}
[[nodiscard]] rpl::producer<crl::time> trimChanges() const {
return _trimChanges.events();
}
[[nodiscard]] rpl::producer<crl::time> coverChanges() const {
return _coverChanges.events();
}
[[nodiscard]] rpl::producer<bool> draggingChanges() const {
return _draggingChanges.events();
}
void setPlaybackPosition(crl::time position);
void setSizeLabel(const QString &text);
[[nodiscard]] QPoint coverDot() const;
[[nodiscard]] bool draggingHead() const;
[[nodiscard]] int resizeGetHeight(int newWidth) override;
private:
enum class Grab {
None,
Left,
Right,
Head,
Window,
struct Loading {
VideoTimelineFrames set;
std::shared_ptr<std::atomic<bool>> cancel;
};
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
void paintStrip(QPainter &p, const QRect &strip) override;
void paintOverlay(QPainter &p) override;
void headGrabChanged(bool grabbed) override;
void visibleRangeChanged() override;
[[nodiscard]] QRect stripRect() const;
[[nodiscard]] QRect labelRect() const;
[[nodiscard]] crl::time minSelection() const;
void moveWindowTo(crl::time center);
[[nodiscard]] crl::time timeAt(int x) const;
[[nodiscard]] int xAt(crl::time time) const;
[[nodiscard]] Grab grabAt(QPoint position) const;
void applyGrab(QPoint position);
void setCover(crl::time cover, bool notify);
void updateCursor(Grab grab);
void reloadFrames();
void paintFrames(QPainter &p, const QRect &strip);
void paintSelection(QPainter &p, const QRect &strip);
void paintHead(QPainter &p, const QRect &strip);
void paintCoverDot(QPainter &p);
void paintDuration(QPainter &p, const QRect &strip);
void paintFrames(
QPainter &p,
const QRect &strip,
const VideoTimelineFrames &set);
const VideoTimelineDescriptor _descriptor;
const crl::time _duration = 0;
const crl::time _maxDuration = 0;
const crl::time _minDuration = 0;
const QString _path;
const QByteArray _content;
const QSize _dimensions;
const std::shared_ptr<VideoTimelineFramesCache> _cache;
crl::time _from = 0;
crl::time _till = 0;
crl::time _cover = 0;
// Negative means nothing played yet; zero is a real position.
crl::time _playback = -1;
VideoTimelineFrames _frames;
std::unique_ptr<Loading> _loading;
base::Timer _reloadTimer;
QString _sizeLabel;
std::vector<QImage> _frames;
QSize _framesBox;
int _frameWidth = 0;
std::shared_ptr<std::atomic<bool>> _framesCancel;
Grab _grab = Grab::None;
int _grabShift = 0;
Ui::Animations::Simple _dotActive;
rpl::event_stream<crl::time> _trimChanges;
rpl::event_stream<crl::time> _coverChanges;
rpl::event_stream<bool> _draggingChanges;
};
} // namespace Editor

View File

@@ -0,0 +1,86 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/video/video_timeline_seeker.h"
#include "editor/editor_trim_timeline.h"
#include "editor/video/video_segment_player.h"
namespace Editor {
namespace {
constexpr auto kSeekThrottle = crl::time(120);
} // namespace
TimelineSeeker::TimelineSeeker(
not_null<TrimTimeline*> timeline,
not_null<SegmentPlayer*> player)
: _timeline(timeline)
, _player(player)
, _timer([=] {
const auto position = std::exchange(_pending, crl::time(-1));
if (position >= 0) {
_player->restart(position);
}
}) {
_player->setSegment(_timeline->from(), _timeline->till());
_timeline->trimChanges(
) | rpl::on_next([=](crl::time edge) {
_player->setSegment(_timeline->from(), _timeline->till());
seek(edge);
}, _lifetime);
_timeline->coverChanges(
) | rpl::on_next([=](crl::time position) {
seek(position);
}, _lifetime);
_timeline->draggingChanges(
) | rpl::on_next([=](bool dragging) {
_dragging = dragging;
if (dragging) {
_player->setSeeking(true);
} else {
finishDragging();
}
}, _lifetime);
_player->positionUpdates(
) | rpl::on_next([=](crl::time position) {
if (!_dragging) {
_timeline->setPlaybackPosition(position);
}
}, _lifetime);
}
TimelineSeeker::~TimelineSeeker() {
if (_dragging) {
_player->setSeeking(false);
}
}
void TimelineSeeker::seek(crl::time position) {
_pending = position;
if (!_timer.isActive()) {
_timer.callOnce(kSeekThrottle);
}
}
void TimelineSeeker::finishDragging() {
_timer.cancel();
const auto latest = (_pending >= 0) ? _pending : _player->position();
_pending = -1;
const auto from = _timeline->from();
const auto till = _timeline->till();
const auto resume = std::clamp(latest, from, till);
_player->setSeeking(false);
_player->restart((resume >= till) ? from : resume);
}
} // namespace Editor

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
namespace Editor {
class SegmentPlayer;
class TrimTimeline;
class TimelineSeeker final {
public:
TimelineSeeker(
not_null<TrimTimeline*> timeline,
not_null<SegmentPlayer*> player);
~TimelineSeeker();
private:
void seek(crl::time position);
void finishDragging();
const not_null<TrimTimeline*> _timeline;
const not_null<SegmentPlayer*> _player;
base::Timer _timer;
crl::time _pending = -1;
bool _dragging = false;
rpl::lifetime _lifetime;
};
} // namespace Editor

Some files were not shown because too many files have changed in this diff Show More