Slightly improved photo editor. (Combined commit.)
@@ -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
|
||||
|
||||
BIN
Telegram/Resources/animations/photo_editor/trim_shortest.tgs
Normal file
BIN
Telegram/Resources/animations/sun_outline.tgs
Normal file
16
Telegram/Resources/icons/photo_editor/keyboard.svg
Normal 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 |
BIN
Telegram/Resources/icons/photo_editor/link.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
Telegram/Resources/icons/photo_editor/link@2x.png
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
Telegram/Resources/icons/photo_editor/link@3x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/photo_editor/widget_music.png
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
Telegram/Resources/icons/photo_editor/widget_music@2x.png
Normal file
|
After Width: | Height: | Size: 592 B |
BIN
Telegram/Resources/icons/photo_editor/widget_music@3x.png
Normal file
|
After Width: | Height: | Size: 724 B |
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,51 +983,116 @@ 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()) {
|
||||
int StickersListWidget::mediaButtonIndexAt(QPoint point) const {
|
||||
if (!mediaButtonsRowHeight()) {
|
||||
return -1;
|
||||
}
|
||||
for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
|
||||
if (myrtlrect(mediaButtonRect(i)).contains(point)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void StickersListWidget::paintMediaButtons(Painter &p, QRect clip) {
|
||||
if (!mediaButtonsRowHeight()) {
|
||||
return;
|
||||
}
|
||||
const auto rect = photoButtonRect();
|
||||
for (auto i = 0; i != int(_mediaButtons.size()); ++i) {
|
||||
auto &button = _mediaButtons[i];
|
||||
const auto rect = mediaButtonRect(i);
|
||||
if (!rect.intersects(clip)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
_photoButtonBg.paint(p, myrtlrect(rect));
|
||||
if (_photoButtonRipple) {
|
||||
_photoButtonRipple->paint(p, myrtlrect(rect).x(), rect.y(), width());
|
||||
if (_photoButtonRipple->empty()) {
|
||||
_photoButtonRipple.reset();
|
||||
_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 = st().photoButtonIcon;
|
||||
const auto &icon = *button.icon;
|
||||
icon.paint(
|
||||
p,
|
||||
rect.x() + padding.left(),
|
||||
@@ -1037,8 +1109,9 @@ void StickersListWidget::paintPhotoButton(Painter &p, QRect clip) {
|
||||
textLeft,
|
||||
rect.y() + (rect.height() - font->height) / 2,
|
||||
width(),
|
||||
_photoButtonText,
|
||||
_photoButtonTextWidth);
|
||||
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)) {
|
||||
} 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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -210,6 +210,8 @@ struct GiftCode {
|
||||
|
||||
inline constexpr auto kTimeToLiveSingleView = crl::time(0x7FFFFFFF);
|
||||
|
||||
class MediaDice;
|
||||
|
||||
class Media {
|
||||
public:
|
||||
Media(not_null<HistoryItem*> parent);
|
||||
|
||||
80
Telegram/SourceFiles/editor/audio_track_timeline.cpp
Normal 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
|
||||
42
Telegram/SourceFiles/editor/audio_track_timeline.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
154
Telegram/SourceFiles/editor/editor_audio_disc_button.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/editor/editor_audio_disc_button.h
Normal 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
|
||||
228
Telegram/SourceFiles/editor/editor_audio_menu.cpp
Normal 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
|
||||
80
Telegram/SourceFiles/editor/editor_audio_menu.h
Normal 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
|
||||
298
Telegram/SourceFiles/editor/editor_audio_timeline.cpp
Normal 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
|
||||
69
Telegram/SourceFiles/editor/editor_audio_timeline.h
Normal 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
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
513
Telegram/SourceFiles/editor/editor_keys_legend.cpp
Normal 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 §ion,
|
||||
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 §ion,
|
||||
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 §ion : 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 §ion : 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
|
||||
49
Telegram/SourceFiles/editor/editor_keys_legend.h
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
1005
Telegram/SourceFiles/editor/editor_link_box.cpp
Normal file
37
Telegram/SourceFiles/editor/editor_link_box.h
Normal 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
|
||||
135
Telegram/SourceFiles/editor/editor_link_pill.cpp
Normal 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
|
||||
58
Telegram/SourceFiles/editor/editor_link_pill.h
Normal 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
|
||||
397
Telegram/SourceFiles/editor/editor_message_render.cpp
Normal 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
|
||||
70
Telegram/SourceFiles/editor/editor_message_render.h
Normal 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
|
||||
172
Telegram/SourceFiles/editor/editor_message_source.cpp
Normal 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
|
||||
58
Telegram/SourceFiles/editor/editor_message_source.h
Normal 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
|
||||
206
Telegram/SourceFiles/editor/editor_message_video.cpp
Normal 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
|
||||
57
Telegram/SourceFiles/editor/editor_message_video.h
Normal 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
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
1205
Telegram/SourceFiles/editor/editor_trim_timeline.cpp
Normal file
207
Telegram/SourceFiles/editor/editor_trim_timeline.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
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;
|
||||
}
|
||||
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);
|
||||
_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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
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,9 +542,15 @@ void ItemBase::performForSelectedItems(Action action) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemBase::flippable() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ItemBase::actionFlip() {
|
||||
if (flippable()) {
|
||||
setFlip(!flipped());
|
||||
}
|
||||
}
|
||||
|
||||
void ItemBase::actionDelete() {
|
||||
if (const auto s = static_cast<Scene*>(scene())) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
181
Telegram/SourceFiles/editor/scene/scene_item_link.cpp
Normal 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
|
||||
59
Telegram/SourceFiles/editor/scene/scene_item_link.h
Normal 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
|
||||
499
Telegram/SourceFiles/editor/scene/scene_item_message.cpp
Normal 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
|
||||
121
Telegram/SourceFiles/editor/scene/scene_item_message.h
Normal 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
|
||||
@@ -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.;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
147
Telegram/SourceFiles/editor/video/video_clip.cpp
Normal 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
|
||||
71
Telegram/SourceFiles/editor/video/video_clip.h
Normal 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
|
||||
@@ -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());
|
||||
|
||||
restart(_from);
|
||||
if (_player->held()) {
|
||||
updateBubble();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
259
Telegram/SourceFiles/editor/video/video_segment_player.cpp
Normal 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
|
||||
98
Telegram/SourceFiles/editor/video/video_segment_player.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
86
Telegram/SourceFiles/editor/video/video_timeline_seeker.cpp
Normal 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
|
||||
39
Telegram/SourceFiles/editor/video/video_timeline_seeker.h
Normal 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
|
||||