mirror of https://github.com/procxx/kepka.git
Display group / userpic thumbnails in MediaView.
This commit is contained in:
parent
5b4694a4eb
commit
2bcbb5a5be
|
@ -20,23 +20,613 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
|||
*/
|
||||
#include "media/view/media_view_group_thumbs.h"
|
||||
|
||||
#include "data/data_shared_media.h"
|
||||
#include "data/data_user_photos.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_document.h"
|
||||
#include "history/history_media.h"
|
||||
#include "styles/style_mediaview.h"
|
||||
|
||||
namespace Media {
|
||||
namespace View {
|
||||
namespace {
|
||||
|
||||
GroupThumbs::GroupThumbs() {
|
||||
constexpr auto kThumbDuration = TimeMs(150);
|
||||
|
||||
int Round(float64 value) {
|
||||
return int(std::round(value));
|
||||
}
|
||||
|
||||
using Context = GroupThumbs::Context;
|
||||
using Key = GroupThumbs::Key;
|
||||
|
||||
Context ComputeContext(const SharedMediaWithLastSlice &slice, int index) {
|
||||
Expects(index >= 0 && index < slice.size());
|
||||
|
||||
const auto value = slice[index];
|
||||
if (const auto photo = base::get_if<not_null<PhotoData*>>(&value)) {
|
||||
if (const auto peer = (*photo)->peer) {
|
||||
return peer->id;
|
||||
}
|
||||
return base::none;
|
||||
} else if (const auto msgId = base::get_if<FullMsgId>(&value)) {
|
||||
if (const auto item = App::histItemById(*msgId)) {
|
||||
if (!item->toHistoryMessage()) {
|
||||
return item->history()->peer->id;
|
||||
} else if (const auto groupId = item->groupId()) {
|
||||
return groupId;
|
||||
}
|
||||
}
|
||||
return base::none;
|
||||
}
|
||||
Unexpected("Variant in ComputeContext(SharedMediaWithLastSlice::Value)");
|
||||
}
|
||||
|
||||
Context ComputeContext(const UserPhotosSlice &slice, int index) {
|
||||
return peerFromUser(slice.key().userId);
|
||||
}
|
||||
|
||||
Key ComputeKey(const SharedMediaWithLastSlice &slice, int index) {
|
||||
Expects(index >= 0 && index < slice.size());
|
||||
|
||||
const auto value = slice[index];
|
||||
if (const auto photo = base::get_if<not_null<PhotoData*>>(&value)) {
|
||||
return (*photo)->id;
|
||||
} else if (const auto msgId = base::get_if<FullMsgId>(&value)) {
|
||||
return *msgId;
|
||||
}
|
||||
Unexpected("Variant in ComputeContext(SharedMediaWithLastSlice::Value)");
|
||||
}
|
||||
|
||||
Key ComputeKey(const UserPhotosSlice &slice, int index) {
|
||||
return slice[index];
|
||||
}
|
||||
|
||||
int ComputeThumbsLimit(int availableWidth) {
|
||||
const auto singleWidth = st::mediaviewGroupWidth
|
||||
+ 2 * st::mediaviewGroupSkip;
|
||||
const auto currentWidth = st::mediaviewGroupWidthMax
|
||||
+ 2 * st::mediaviewGroupSkipCurrent;
|
||||
const auto skipForAnimation = 2 * singleWidth;
|
||||
const auto leftWidth = availableWidth
|
||||
- currentWidth
|
||||
- skipForAnimation;
|
||||
return std::max(leftWidth / (2 * singleWidth), 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class GroupThumbs::Thumb {
|
||||
public:
|
||||
enum class State {
|
||||
Unknown,
|
||||
Current,
|
||||
Alive,
|
||||
Dying,
|
||||
};
|
||||
|
||||
Thumb(Key key, ImagePtr image);
|
||||
|
||||
int leftToUpdate() const;
|
||||
int rightToUpdate() const;
|
||||
|
||||
void animateToLeft(not_null<Thumb*> next);
|
||||
void animateToRight(not_null<Thumb*> prev);
|
||||
|
||||
void setState(State state);
|
||||
State state() const;
|
||||
bool removed() const;
|
||||
|
||||
void paint(Painter &p, int x, int y, int outerWidth, float64 progress);
|
||||
|
||||
private:
|
||||
QSize wantedPixSize() const;
|
||||
void validateImage();
|
||||
int currentLeft() const;
|
||||
int currentWidth() const;
|
||||
int finalLeft() const;
|
||||
int finalWidth() const;
|
||||
void toggle(bool visible);
|
||||
void animateTo(int left, int width);
|
||||
|
||||
const Key _key;
|
||||
ImagePtr _image;
|
||||
State _state = State::Alive;
|
||||
QPixmap _full;
|
||||
int _fullWidth = 0;
|
||||
bool _hiding = true;
|
||||
|
||||
anim::value _left = { 0. };
|
||||
anim::value _width = { 0. };
|
||||
anim::value _opacity = { 0. };
|
||||
|
||||
};
|
||||
|
||||
GroupThumbs::Thumb::Thumb(Key key, ImagePtr image)
|
||||
: _key(key)
|
||||
, _image(image) {
|
||||
_fullWidth = std::min(
|
||||
wantedPixSize().width(),
|
||||
st::mediaviewGroupWidthMax);
|
||||
validateImage();
|
||||
toggle(true);
|
||||
}
|
||||
|
||||
QSize GroupThumbs::Thumb::wantedPixSize() const {
|
||||
const auto originalWidth = std::max(_image->width(), 1);
|
||||
const auto originalHeight = std::max(_image->height(), 1);
|
||||
const auto pixHeight = st::mediaviewGroupHeight;
|
||||
const auto pixWidth = originalWidth * pixHeight / originalHeight;
|
||||
return { pixWidth, pixHeight };
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::validateImage() {
|
||||
if (!_full.isNull()) {
|
||||
return;
|
||||
}
|
||||
_image->load();
|
||||
if (!_image->loaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pixSize = wantedPixSize();
|
||||
if (pixSize.width() > st::mediaviewGroupWidthMax) {
|
||||
const auto originalWidth = _image->width();
|
||||
const auto originalHeight = _image->height();
|
||||
const auto takeWidth = originalWidth * st::mediaviewGroupWidthMax
|
||||
/ pixSize.width();
|
||||
const auto original = _image->pixNoCache().toImage();
|
||||
_full = App::pixmapFromImageInPlace(original.copy(
|
||||
(originalWidth - takeWidth) / 2,
|
||||
0,
|
||||
takeWidth,
|
||||
originalHeight
|
||||
).scaled(
|
||||
st::mediaviewGroupWidthMax * cIntRetinaFactor(),
|
||||
pixSize.height() * cIntRetinaFactor(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation));
|
||||
} else {
|
||||
_full = _image->pixNoCache(
|
||||
pixSize.width() * cIntRetinaFactor(),
|
||||
pixSize.height() * cIntRetinaFactor(),
|
||||
Images::Option::Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::leftToUpdate() const {
|
||||
return Round(std::min(_left.from(), _left.to()));
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::rightToUpdate() const {
|
||||
return Round(std::max(
|
||||
_left.from() + _width.from(),
|
||||
_left.to() + _width.to()));
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::currentLeft() const {
|
||||
return Round(_left.current());
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::currentWidth() const {
|
||||
return Round(_width.current());
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::finalLeft() const {
|
||||
return Round(_left.to());
|
||||
}
|
||||
|
||||
int GroupThumbs::Thumb::finalWidth() const {
|
||||
return Round(_width.to());
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::setState(State state) {
|
||||
const auto isNewThumb = (_state == State::Alive);
|
||||
_state = state;
|
||||
if (_state == State::Current) {
|
||||
if (isNewThumb) {
|
||||
_opacity = anim::value(1.);
|
||||
_left = anim::value(-_fullWidth / 2);
|
||||
_width = anim::value(_fullWidth);
|
||||
} else {
|
||||
toggle(true);
|
||||
}
|
||||
animateTo(-_fullWidth / 2, _fullWidth);
|
||||
} else if (_state == State::Alive) {
|
||||
toggle(true);
|
||||
} else if (_state == State::Dying) {
|
||||
toggle(false);
|
||||
_left.restart();
|
||||
_width.restart();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::toggle(bool visible) {
|
||||
_hiding = !visible;
|
||||
_opacity.start(_hiding ? 0. : 1.);
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::animateTo(int left, int width) {
|
||||
_left.start(left);
|
||||
_width.start(width);
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::animateToLeft(not_null<Thumb*> next) {
|
||||
const auto width = st::mediaviewGroupWidth;
|
||||
if (_state == State::Alive) {
|
||||
// New item animation, start exactly from the next, move only.
|
||||
_left = anim::value(next->currentLeft() - width);
|
||||
_width = anim::value(width);
|
||||
} else if (_state == State::Unknown) {
|
||||
// Existing item animation.
|
||||
setState(State::Alive);
|
||||
}
|
||||
const auto skip1 = st::mediaviewGroupSkip;
|
||||
const auto skip2 = (next->state() == State::Current)
|
||||
? st::mediaviewGroupSkipCurrent
|
||||
: st::mediaviewGroupSkip;
|
||||
animateTo(next->finalLeft() - width - skip1 - skip2, width);
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::animateToRight(not_null<Thumb*> prev) {
|
||||
const auto width = st::mediaviewGroupWidth;
|
||||
if (_state == State::Alive) {
|
||||
// New item animation, start exactly from the next, move only.
|
||||
_left = anim::value(prev->currentLeft() + prev->currentWidth());
|
||||
_width = anim::value(width);
|
||||
} else if (_state == State::Unknown) {
|
||||
// Existing item animation.
|
||||
setState(State::Alive);
|
||||
}
|
||||
const auto skip1 = st::mediaviewGroupSkip;
|
||||
const auto skip2 = (prev->state() == State::Current)
|
||||
? st::mediaviewGroupSkipCurrent
|
||||
: st::mediaviewGroupSkip;
|
||||
animateTo(prev->finalLeft() + prev->finalWidth() + skip1 + skip2, width);
|
||||
}
|
||||
|
||||
auto GroupThumbs::Thumb::state() const -> State {
|
||||
return _state;
|
||||
}
|
||||
|
||||
bool GroupThumbs::Thumb::removed() const {
|
||||
return (_state == State::Dying) && _hiding && !_opacity.current();
|
||||
}
|
||||
|
||||
void GroupThumbs::Thumb::paint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
float64 progress) {
|
||||
validateImage();
|
||||
|
||||
_opacity.update(progress, anim::linear);
|
||||
_left.update(progress, anim::linear);
|
||||
_width.update(progress, anim::linear);
|
||||
|
||||
const auto left = x + currentLeft();
|
||||
const auto width = currentWidth();
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(_opacity.current() * opacity);
|
||||
if (width == _fullWidth) {
|
||||
p.drawPixmap(left, y, _full);
|
||||
} else {
|
||||
const auto takeWidth = width * cIntRetinaFactor();
|
||||
const auto from = QRect(
|
||||
(_full.width() - takeWidth) / 2,
|
||||
0,
|
||||
takeWidth,
|
||||
_full.height());
|
||||
const auto to = QRect(left, y, width, st::mediaviewGroupHeight);
|
||||
p.drawPixmap(to, _full, from);
|
||||
}
|
||||
p.setOpacity(opacity);
|
||||
}
|
||||
|
||||
GroupThumbs::GroupThumbs(Context context)
|
||||
: _context(context) {
|
||||
}
|
||||
|
||||
void GroupThumbs::updateContext(Context context) {
|
||||
if (_context != context) {
|
||||
clear();
|
||||
_context = context;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Slice>
|
||||
void GroupThumbs::RefreshFromSlice(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const Slice &slice,
|
||||
int index,
|
||||
int availableWidth) {
|
||||
const auto context = ComputeContext(slice, index);
|
||||
if (instance) {
|
||||
instance->updateContext(context);
|
||||
}
|
||||
if (!context) {
|
||||
if (instance) {
|
||||
instance->resizeToWidth(availableWidth);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto limit = ComputeThumbsLimit(availableWidth);
|
||||
const auto from = [&] {
|
||||
const auto edge = std::max(index - limit, 0);
|
||||
for (auto result = index; result != edge; --result) {
|
||||
if (ComputeContext(slice, result - 1) != context) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return edge;
|
||||
}();
|
||||
const auto till = [&] {
|
||||
const auto edge = std::min(index + limit + 1, slice.size());
|
||||
for (auto result = index + 1; result != edge; ++result) {
|
||||
if (ComputeContext(slice, result) != context) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return edge;
|
||||
}();
|
||||
if (from + 1 < till) {
|
||||
if (!instance) {
|
||||
instance = std::make_unique<GroupThumbs>(context);
|
||||
}
|
||||
instance->fillItems(slice, from, index, till);
|
||||
instance->resizeToWidth(availableWidth);
|
||||
} else if (instance) {
|
||||
instance->clear();
|
||||
instance->resizeToWidth(availableWidth);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Slice>
|
||||
void GroupThumbs::fillItems(
|
||||
const Slice &slice,
|
||||
int from,
|
||||
int index,
|
||||
int till) {
|
||||
Expects(from <= index);
|
||||
Expects(index < till);
|
||||
Expects(from + 1 < till);
|
||||
|
||||
|
||||
const auto current = (index - from);
|
||||
const auto old = base::take(_items);
|
||||
|
||||
markCacheStale();
|
||||
_items.reserve(till - from);
|
||||
for (auto i = from; i != till; ++i) {
|
||||
_items.push_back(validateCacheEntry(ComputeKey(slice, i)));
|
||||
}
|
||||
animateAliveItems(current);
|
||||
fillDyingItems(old);
|
||||
startDelayedAnimation();
|
||||
}
|
||||
|
||||
void GroupThumbs::animateAliveItems(int current) {
|
||||
Expects(current >= 0 && current < _items.size());
|
||||
|
||||
_items[current]->setState(Thumb::State::Current);
|
||||
for (auto i = current; i != 0;) {
|
||||
const auto prev = _items[i];
|
||||
const auto item = _items[--i];
|
||||
item->animateToLeft(prev);
|
||||
}
|
||||
for (auto i = current + 1; i != _items.size(); ++i) {
|
||||
const auto prev = _items[i - 1];
|
||||
const auto item = _items[i];
|
||||
item->animateToRight(prev);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::fillDyingItems(const std::vector<not_null<Thumb*>> &old) {
|
||||
_dying.reserve(_cache.size() - _items.size());
|
||||
animatePreviouslyAlive(old);
|
||||
markRestAsDying();
|
||||
}
|
||||
|
||||
void GroupThumbs::markRestAsDying() {
|
||||
_dying.reserve(_cache.size() - _items.size());
|
||||
for (const auto &[key, thumb] : _cache) {
|
||||
const auto state = thumb->state();
|
||||
if (state == Thumb::State::Unknown) {
|
||||
markAsDying(thumb.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::markAsDying(not_null<Thumb*> thumb) {
|
||||
thumb->setState(Thumb::State::Dying);
|
||||
_dying.push_back(thumb.get());
|
||||
}
|
||||
|
||||
void GroupThumbs::animatePreviouslyAlive(
|
||||
const std::vector<not_null<Thumb*>> &old) {
|
||||
auto toRight = false;
|
||||
for (auto i = 0; i != old.size(); ++i) {
|
||||
const auto item = old[i];
|
||||
if (item->state() == Thumb::State::Unknown) {
|
||||
if (toRight) {
|
||||
markAsDying(item);
|
||||
item->animateToRight(old[i - 1]);
|
||||
}
|
||||
} else if (!toRight) {
|
||||
for (auto j = i; j != 0;) {
|
||||
const auto next = old[j];
|
||||
const auto prev = old[--j];
|
||||
markAsDying(prev);
|
||||
prev->animateToLeft(next);
|
||||
}
|
||||
toRight = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto GroupThumbs::createThumb(Key key) -> std::unique_ptr<Thumb> {
|
||||
if (const auto photoId = base::get_if<PhotoId>(&key)) {
|
||||
const auto photo = App::photo(*photoId);
|
||||
return createThumb(key, photo->date ? photo->thumb : ImagePtr());
|
||||
} else if (const auto msgId = base::get_if<FullMsgId>(&key)) {
|
||||
if (const auto item = App::histItemById(*msgId)) {
|
||||
if (const auto media = item->getMedia()) {
|
||||
if (const auto photo = media->getPhoto()) {
|
||||
return createThumb(key, photo->thumb);
|
||||
} else if (const auto document = media->getDocument()) {
|
||||
return createThumb(key, document->thumb);
|
||||
}
|
||||
}
|
||||
}
|
||||
return createThumb(key, ImagePtr());
|
||||
}
|
||||
Unexpected("Value of Key in GroupThumbs::createThumb()");
|
||||
}
|
||||
|
||||
auto GroupThumbs::createThumb(Key key, ImagePtr image)
|
||||
-> std::unique_ptr<Thumb> {
|
||||
return std::make_unique<Thumb>(key, image);
|
||||
}
|
||||
|
||||
auto GroupThumbs::validateCacheEntry(Key key) -> not_null<Thumb*> {
|
||||
const auto i = _cache.find(key);
|
||||
return (i != _cache.end())
|
||||
? i->second.get()
|
||||
: _cache.emplace(key, createThumb(key)).first->second.get();
|
||||
}
|
||||
|
||||
void GroupThumbs::markCacheStale() {
|
||||
while (!_dying.empty()) {
|
||||
_dying.pop_back();
|
||||
}
|
||||
for (const auto &[key, thumb] : _cache) {
|
||||
thumb->setState(Thumb::State::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::Refresh(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const SharedMediaWithLastSlice &slice,
|
||||
int index,
|
||||
int availableWidth) {
|
||||
RefreshFromSlice(instance, slice, index, availableWidth);
|
||||
}
|
||||
|
||||
void GroupThumbs::Refresh(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const UserPhotosSlice &slice,
|
||||
int index,
|
||||
int availableWidth) {
|
||||
RefreshFromSlice(instance, slice, index, availableWidth);
|
||||
}
|
||||
|
||||
void GroupThumbs::clear() {
|
||||
if (_items.empty()) {
|
||||
return;
|
||||
}
|
||||
base::take(_items);
|
||||
markCacheStale();
|
||||
markRestAsDying();
|
||||
startDelayedAnimation();
|
||||
}
|
||||
|
||||
void GroupThumbs::startDelayedAnimation() {
|
||||
_animation.finish();
|
||||
_waitingForAnimationStart = true;
|
||||
countUpdatedRect();
|
||||
}
|
||||
|
||||
void GroupThumbs::resizeToWidth(int newWidth) {
|
||||
|
||||
_width = newWidth;
|
||||
}
|
||||
|
||||
int GroupThumbs::height() const {
|
||||
return 0;
|
||||
return st::mediaviewGroupPadding.top()
|
||||
+ st::mediaviewGroupHeight
|
||||
+ st::mediaviewGroupPadding.bottom();
|
||||
}
|
||||
|
||||
QRect GroupThumbs::paintedRect() const {
|
||||
return QRect();
|
||||
bool GroupThumbs::hiding() const {
|
||||
return _items.empty();
|
||||
}
|
||||
|
||||
bool GroupThumbs::hidden() const {
|
||||
return hiding() && !_waitingForAnimationStart && !_animation.animating();
|
||||
}
|
||||
|
||||
void GroupThumbs::checkForAnimationStart() {
|
||||
if (_waitingForAnimationStart) {
|
||||
_waitingForAnimationStart = false;
|
||||
_animation.start([this] { update(); }, 0., 1., kThumbDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::update() {
|
||||
if (_cache.empty()) {
|
||||
return;
|
||||
}
|
||||
_updateRequests.fire_copy(_updatedRect);
|
||||
}
|
||||
|
||||
void GroupThumbs::paint(
|
||||
Painter &p,
|
||||
int x,
|
||||
int y,
|
||||
int outerWidth,
|
||||
TimeMs ms) {
|
||||
const auto progress = _waitingForAnimationStart
|
||||
? 0.
|
||||
: _animation.current(ms, 1.);
|
||||
x += (_width / 2);
|
||||
y += st::mediaviewGroupPadding.top();
|
||||
for (auto i = _cache.begin(); i != _cache.end();) {
|
||||
const auto &thumb = i->second;
|
||||
thumb->paint(p, x, y, outerWidth, progress);
|
||||
if (thumb->removed()) {
|
||||
_dying.erase(
|
||||
ranges::remove(
|
||||
_dying,
|
||||
thumb.get(),
|
||||
[](not_null<Thumb*> thumb) { return thumb.get(); }),
|
||||
_dying.end());
|
||||
i = _cache.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupThumbs::countUpdatedRect() {
|
||||
if (_cache.empty()) {
|
||||
return;
|
||||
}
|
||||
auto min = _width;
|
||||
auto max = 0;
|
||||
const auto left = [](const auto &cacheItem) {
|
||||
const auto &[key, thumb] = cacheItem;
|
||||
return thumb->leftToUpdate();
|
||||
};
|
||||
const auto right = [](const auto &cacheItem) {
|
||||
const auto &[key, thumb] = cacheItem;
|
||||
return thumb->rightToUpdate();
|
||||
};
|
||||
accumulate_min(min, left(*ranges::max_element(
|
||||
_cache,
|
||||
std::greater<>(),
|
||||
left)));
|
||||
accumulate_max(max, right(*ranges::max_element(
|
||||
_cache,
|
||||
std::less<>(),
|
||||
right)));
|
||||
_updatedRect = QRect(
|
||||
min,
|
||||
st::mediaviewGroupPadding.top(),
|
||||
max - min,
|
||||
st::mediaviewGroupHeight);
|
||||
}
|
||||
|
||||
GroupThumbs::~GroupThumbs() = default;
|
||||
|
||||
} // namespace View
|
||||
} // namespace Media
|
||||
|
|
|
@ -20,19 +20,88 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include "history/history_item_components.h"
|
||||
|
||||
class SharedMediaWithLastSlice;
|
||||
class UserPhotosSlice;
|
||||
|
||||
namespace Media {
|
||||
namespace View {
|
||||
|
||||
class GroupThumbs {
|
||||
public:
|
||||
GroupThumbs();
|
||||
static void Refresh(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const SharedMediaWithLastSlice &slice,
|
||||
int index,
|
||||
int availableWidth);
|
||||
static void Refresh(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const UserPhotosSlice &slice,
|
||||
int index,
|
||||
int availableWidth);
|
||||
void clear();
|
||||
|
||||
void resizeToWidth(int newWidth);
|
||||
int height() const;
|
||||
bool hiding() const;
|
||||
bool hidden() const;
|
||||
void checkForAnimationStart();
|
||||
|
||||
QRect paintedRect() const;
|
||||
void paint(Painter &p, int x, int y, int outerWidth, TimeMs ms);
|
||||
|
||||
rpl::producer<QRect> updateRequests() const {
|
||||
return _updateRequests.events();
|
||||
}
|
||||
|
||||
rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
using Context = base::optional_variant<PeerId, MessageGroupId>;
|
||||
using Key = base::variant<PhotoId, FullMsgId>;
|
||||
|
||||
GroupThumbs(Context context);
|
||||
~GroupThumbs();
|
||||
|
||||
private:
|
||||
class Thumb;
|
||||
|
||||
template <typename Slice>
|
||||
static void RefreshFromSlice(
|
||||
std::unique_ptr<GroupThumbs> &instance,
|
||||
const Slice &slice,
|
||||
int index,
|
||||
int availableWidth);
|
||||
template <typename Slice>
|
||||
void fillItems(const Slice &slice, int from, int index, int till);
|
||||
void updateContext(Context context);
|
||||
void markCacheStale();
|
||||
not_null<Thumb*> validateCacheEntry(Key key);
|
||||
std::unique_ptr<Thumb> createThumb(Key key);
|
||||
std::unique_ptr<Thumb> createThumb(Key key, ImagePtr image);
|
||||
|
||||
void update();
|
||||
void countUpdatedRect();
|
||||
void animateAliveItems(int current);
|
||||
void fillDyingItems(const std::vector<not_null<Thumb*>> &old);
|
||||
void markAsDying(not_null<Thumb*> thumb);
|
||||
void markRestAsDying();
|
||||
void animatePreviouslyAlive(const std::vector<not_null<Thumb*>> &old);
|
||||
void startDelayedAnimation();
|
||||
|
||||
Context _context;
|
||||
bool _waitingForAnimationStart = true;
|
||||
Animation _animation;
|
||||
std::vector<not_null<Thumb*>> _items;
|
||||
std::vector<not_null<Thumb*>> _dying;
|
||||
base::flat_map<Key, std::unique_ptr<Thumb>> _cache;
|
||||
int _width = 0;
|
||||
int _limit = 0;
|
||||
rpl::event_stream<QRect> _updateRequests;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
QRect _updatedRect;
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -196,6 +196,13 @@ mediaviewCaptionPadding: margins(18px, 10px, 18px, 10px);
|
|||
mediaviewCaptionMargin: size(11px, 11px);
|
||||
mediaviewCaptionRadius: 2px;
|
||||
|
||||
mediaviewGroupPadding: margins(0px, 14px, 0px, 14px);
|
||||
mediaviewGroupHeight: 80px;
|
||||
mediaviewGroupWidth: 56px;
|
||||
mediaviewGroupWidthMax: 160px;
|
||||
mediaviewGroupSkip: 3px;
|
||||
mediaviewGroupSkipCurrent: 12px;
|
||||
|
||||
themePreviewSize: size(903px, 584px);
|
||||
themePreviewBg: windowBg;
|
||||
themePreviewOverlayOpacity: 0.8;
|
||||
|
|
|
@ -369,45 +369,54 @@ void MediaView::updateControls() {
|
|||
void MediaView::resizeCenteredControls() {
|
||||
const auto bottomSkip = std::max(
|
||||
_dateNav.left() + _dateNav.width(),
|
||||
_headerNav.left() + _headerNav.width());
|
||||
const auto bottomWidth = std::max(
|
||||
width() - 2 * bottomSkip - 2 * st::mediaviewCaptionMargin.width(),
|
||||
_headerNav.left() + _headerNav.width())
|
||||
+ st::mediaviewCaptionMargin.width();
|
||||
_groupThumbsAvailableWidth = std::max(
|
||||
width() - 2 * bottomSkip,
|
||||
st::msgMinWidth
|
||||
+ st::mediaviewCaptionPadding.left()
|
||||
+ st::mediaviewCaptionPadding.right());
|
||||
if (_groupThumbs) {
|
||||
_groupThumbs->resizeToWidth(bottomWidth);
|
||||
_groupThumbsTop = height() - _groupThumbs->height();
|
||||
_groupThumbsLeft = (width() - _groupThumbsAvailableWidth) / 2;
|
||||
refreshGroupThumbs();
|
||||
_groupThumbsTop = _groupThumbs ? (height() - _groupThumbs->height()) : 0;
|
||||
|
||||
refreshClipControllerGeometry();
|
||||
refreshCaptionGeometry();
|
||||
}
|
||||
|
||||
void MediaView::refreshCaptionGeometry() {
|
||||
if (_caption.isEmpty()) {
|
||||
_captionRect = QRect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_caption.isEmpty()) {
|
||||
const auto captionBottom = groupThumbsDisplayed()
|
||||
? _groupThumbsTop
|
||||
: height() - st::mediaviewCaptionMargin.height();
|
||||
const auto captionWidth = std::min(
|
||||
bottomWidth
|
||||
- st::mediaviewCaptionPadding.left()
|
||||
- st::mediaviewCaptionPadding.right(),
|
||||
_caption.maxWidth());
|
||||
const auto captionHeight = std::min(
|
||||
_caption.countHeight(captionWidth),
|
||||
height() / 4
|
||||
- st::mediaviewCaptionPadding.top()
|
||||
- st::mediaviewCaptionPadding.bottom()
|
||||
- 2 * st::mediaviewCaptionMargin.height());
|
||||
_captionRect = QRect(
|
||||
(width() - captionWidth) / 2,
|
||||
captionBottom
|
||||
- captionHeight
|
||||
- st::mediaviewCaptionPadding.bottom(),
|
||||
captionWidth,
|
||||
captionHeight);
|
||||
} else {
|
||||
_captionRect = QRect();
|
||||
}
|
||||
if (_clipController) {
|
||||
setClipControllerGeometry();
|
||||
if (_groupThumbs && _groupThumbs->hiding()) {
|
||||
_groupThumbs = nullptr;
|
||||
_groupThumbsRect = QRect();
|
||||
}
|
||||
const auto captionBottom = _clipController
|
||||
? (_clipController->y() - st::mediaviewCaptionMargin.height())
|
||||
: _groupThumbs
|
||||
? _groupThumbsTop
|
||||
: height() - st::mediaviewCaptionMargin.height();
|
||||
const auto captionWidth = std::min(
|
||||
_groupThumbsAvailableWidth
|
||||
- st::mediaviewCaptionPadding.left()
|
||||
- st::mediaviewCaptionPadding.right(),
|
||||
_caption.maxWidth());
|
||||
const auto captionHeight = std::min(
|
||||
_caption.countHeight(captionWidth),
|
||||
height() / 4
|
||||
- st::mediaviewCaptionPadding.top()
|
||||
- st::mediaviewCaptionPadding.bottom()
|
||||
- 2 * st::mediaviewCaptionMargin.height());
|
||||
_captionRect = QRect(
|
||||
(width() - captionWidth) / 2,
|
||||
captionBottom
|
||||
- captionHeight
|
||||
- st::mediaviewCaptionPadding.bottom(),
|
||||
captionWidth,
|
||||
captionHeight);
|
||||
}
|
||||
|
||||
void MediaView::updateActions() {
|
||||
|
@ -502,7 +511,17 @@ void MediaView::step_state(TimeMs ms, bool timer) {
|
|||
} else {
|
||||
a_cOpacity.update(dt, anim::linear);
|
||||
}
|
||||
QRegion toUpdate = QRegion() + (_over == OverLeftNav ? _leftNav : _leftNavIcon) + (_over == OverRightNav ? _rightNav : _rightNavIcon) + (_over == OverClose ? _closeNav : _closeNavIcon) + _saveNavIcon + _moreNavIcon + _headerNav + _nameNav + _dateNav + _captionRect.marginsAdded(st::mediaviewCaptionPadding);
|
||||
const auto toUpdate = QRegion()
|
||||
+ (_over == OverLeftNav ? _leftNav : _leftNavIcon)
|
||||
+ (_over == OverRightNav ? _rightNav : _rightNavIcon)
|
||||
+ (_over == OverClose ? _closeNav : _closeNavIcon)
|
||||
+ _saveNavIcon
|
||||
+ _moreNavIcon
|
||||
+ _headerNav
|
||||
+ _nameNav
|
||||
+ _dateNav
|
||||
+ _captionRect.marginsAdded(st::mediaviewCaptionPadding)
|
||||
+ _groupThumbsRect;
|
||||
update(toUpdate);
|
||||
if (dt < 1) result = true;
|
||||
}
|
||||
|
@ -512,7 +531,9 @@ void MediaView::step_state(TimeMs ms, bool timer) {
|
|||
}
|
||||
|
||||
void MediaView::updateCursor() {
|
||||
setCursor(_controlsState == ControlsHidden ? Qt::BlankCursor : (_over == OverNone ? style::cur_default : style::cur_pointer));
|
||||
setCursor(_controlsState == ControlsHidden
|
||||
? Qt::BlankCursor
|
||||
: (_over == OverNone ? style::cur_default : style::cur_pointer));
|
||||
}
|
||||
|
||||
float64 MediaView::radialProgress() const {
|
||||
|
@ -566,8 +587,8 @@ void MediaView::step_radial(TimeMs ms, bool timer) {
|
|||
update(radialRect());
|
||||
}
|
||||
const auto ready = _doc && _doc->loaded();
|
||||
const auto streamVideo = _doc->isAnimation() || _doc->isVideoFile();
|
||||
const auto tryOpenImage = (_doc->size < App::kImageSizeLimit);
|
||||
const auto streamVideo = ready && (_doc->isAnimation() || _doc->isVideoFile());
|
||||
const auto tryOpenImage = ready && (_doc->size < App::kImageSizeLimit);
|
||||
if (ready && ((tryOpenImage && !_radial.animating()) || streamVideo)) {
|
||||
if (_doc->isVideoFile() || _doc->isVideoMessage()) {
|
||||
_autoplayVideoDocument = _doc;
|
||||
|
@ -737,6 +758,7 @@ void MediaView::onHideControls(bool force) {
|
|||
}
|
||||
if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) return;
|
||||
|
||||
_lastMouseMovePos = mapFromGlobal(QCursor::pos());
|
||||
_controlsState = ControlsHiding;
|
||||
_controlsAnimStarted = getms();
|
||||
a_cOpacity.start(0);
|
||||
|
@ -1247,6 +1269,43 @@ void MediaView::refreshCaption(HistoryItem *item) {
|
|||
Ui::ItemTextOptions(item));
|
||||
}
|
||||
|
||||
void MediaView::refreshGroupThumbs() {
|
||||
const auto existed = (_groupThumbs != nullptr);
|
||||
if (_index && _sharedMediaData) {
|
||||
Media::View::GroupThumbs::Refresh(
|
||||
_groupThumbs,
|
||||
*_sharedMediaData,
|
||||
*_index,
|
||||
_groupThumbsAvailableWidth);
|
||||
} else if (_index && _userPhotosData) {
|
||||
Media::View::GroupThumbs::Refresh(
|
||||
_groupThumbs,
|
||||
*_userPhotosData,
|
||||
*_index,
|
||||
_groupThumbsAvailableWidth);
|
||||
} else if (_groupThumbs) {
|
||||
_groupThumbs->clear();
|
||||
_groupThumbs->resizeToWidth(_groupThumbsAvailableWidth);
|
||||
}
|
||||
if (_groupThumbs && !existed) {
|
||||
_groupThumbs->updateRequests(
|
||||
) | rpl::start_with_next([this](QRect rect) {
|
||||
const auto shift = (width() / 2);
|
||||
_groupThumbsRect = QRect(
|
||||
shift + rect.x(),
|
||||
_groupThumbsTop,
|
||||
rect.width(),
|
||||
_groupThumbs->height());
|
||||
update(_groupThumbsRect);
|
||||
}, _groupThumbs->lifetime());
|
||||
_groupThumbsRect = QRect(
|
||||
_groupThumbsLeft,
|
||||
_groupThumbsTop,
|
||||
width() - 2 * _groupThumbsLeft,
|
||||
height() - _groupThumbsTop);
|
||||
}
|
||||
}
|
||||
|
||||
void MediaView::showPhoto(not_null<PhotoData*> photo, HistoryItem *context) {
|
||||
if (context) {
|
||||
setContext(context);
|
||||
|
@ -1403,7 +1462,9 @@ void MediaView::displayDocument(DocumentData *doc, HistoryItem *item) { // empty
|
|||
_autoplayVideoDocument = nullptr;
|
||||
}
|
||||
|
||||
_caption = Text();
|
||||
if (documentChanged) {
|
||||
refreshCaption(item);
|
||||
}
|
||||
if (_doc) {
|
||||
if (_doc->sticker()) {
|
||||
_doc->checkSticker();
|
||||
|
@ -1673,7 +1734,7 @@ void MediaView::createClipController() {
|
|||
if (!_doc->isVideoFile() && !_doc->isVideoMessage()) return;
|
||||
|
||||
_clipController.create(this);
|
||||
setClipControllerGeometry();
|
||||
refreshClipControllerGeometry();
|
||||
_clipController->show();
|
||||
|
||||
connect(_clipController, SIGNAL(playPressed()), this, SLOT(onVideoPauseResume()));
|
||||
|
@ -1687,10 +1748,18 @@ void MediaView::createClipController() {
|
|||
connect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
|
||||
}
|
||||
|
||||
void MediaView::setClipControllerGeometry() {
|
||||
Assert(_clipController != nullptr);
|
||||
void MediaView::refreshClipControllerGeometry() {
|
||||
if (!_clipController) {
|
||||
return;
|
||||
}
|
||||
|
||||
int controllerBottom = _captionRect.isEmpty() ? height() : _captionRect.y();
|
||||
if (_groupThumbs && _groupThumbs->hiding()) {
|
||||
_groupThumbs = nullptr;
|
||||
_groupThumbsRect = QRect();
|
||||
}
|
||||
const auto controllerBottom = _groupThumbs
|
||||
? _groupThumbsTop
|
||||
: height();
|
||||
_clipController->setGeometry(
|
||||
(width() - _clipController->width()) / 2,
|
||||
controllerBottom - _clipController->height() - st::mediaviewCaptionPadding.bottom() - st::mediaviewCaptionMargin.height(),
|
||||
|
@ -2111,6 +2180,27 @@ void MediaView::paintEvent(QPaintEvent *e) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_groupThumbs && _groupThumbsRect.intersects(r)) {
|
||||
p.setOpacity(co);
|
||||
_groupThumbs->paint(
|
||||
p,
|
||||
_groupThumbsLeft,
|
||||
_groupThumbsTop,
|
||||
width(),
|
||||
ms);
|
||||
if (_groupThumbs->hidden()) {
|
||||
_groupThumbs = nullptr;
|
||||
_groupThumbsRect = QRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
checkGroupThumbsAnimation();
|
||||
}
|
||||
|
||||
void MediaView::checkGroupThumbsAnimation() {
|
||||
if (_groupThumbs && (!_gif || _gif->started())) {
|
||||
_groupThumbs->checkForAnimationStart();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2223,8 +2313,14 @@ void MediaView::keyPressEvent(QKeyEvent *e) {
|
|||
onVideoPauseResume();
|
||||
}
|
||||
} else if (e->key() == Qt::Key_Left) {
|
||||
if (_controlsHideTimer.isActive()) {
|
||||
activateControls();
|
||||
}
|
||||
moveToNext(-1);
|
||||
} else if (e->key() == Qt::Key_Right) {
|
||||
if (_controlsHideTimer.isActive()) {
|
||||
activateControls();
|
||||
}
|
||||
moveToNext(1);
|
||||
} else if (e->modifiers().testFlag(Qt::ControlModifier) && (e->key() == Qt::Key_Plus || e->key() == Qt::Key_Equal || e->key() == ']' || e->key() == Qt::Key_Asterisk || e->key() == Qt::Key_Minus || e->key() == Qt::Key_Underscore || e->key() == Qt::Key_0)) {
|
||||
if (e->key() == Qt::Key_Plus || e->key() == Qt::Key_Equal || e->key() == Qt::Key_Asterisk || e->key() == ']') {
|
||||
|
@ -2695,7 +2791,9 @@ void MediaView::mouseReleaseEvent(QMouseEvent *e) {
|
|||
_pressed = false;
|
||||
}
|
||||
_down = OverNone;
|
||||
activateControls();
|
||||
if (!isHidden()) {
|
||||
activateControls();
|
||||
}
|
||||
}
|
||||
|
||||
void MediaView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
@ -2804,10 +2902,13 @@ bool MediaView::eventFilter(QObject *obj, QEvent *e) {
|
|||
auto type = e->type();
|
||||
if ((type == QEvent::MouseMove || type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease) && obj->isWidgetType()) {
|
||||
if (isAncestorOf(static_cast<QWidget*>(obj))) {
|
||||
auto mouseEvent = static_cast<QMouseEvent*>(e);
|
||||
auto mousePosition = mapFromGlobal(mouseEvent->globalPos());
|
||||
bool activate = (mousePosition != _lastMouseMovePos);
|
||||
_lastMouseMovePos = mousePosition;
|
||||
const auto mouseEvent = static_cast<QMouseEvent*>(e);
|
||||
const auto mousePosition = mapFromGlobal(mouseEvent->globalPos());
|
||||
const auto delta = (mousePosition - _lastMouseMovePos);
|
||||
auto activate = delta.manhattanLength() >= st::mediaviewDeltaFromLastAction;
|
||||
if (activate) {
|
||||
_lastMouseMovePos = mousePosition;
|
||||
}
|
||||
if (type == QEvent::MouseButtonPress) {
|
||||
_mousePressed = true;
|
||||
activate = true;
|
||||
|
@ -2815,7 +2916,12 @@ bool MediaView::eventFilter(QObject *obj, QEvent *e) {
|
|||
_mousePressed = false;
|
||||
activate = true;
|
||||
}
|
||||
if (activate) activateControls();
|
||||
if (activate) {
|
||||
if (_controlsState == ControlsHiding || _controlsState == ControlsHidden) {
|
||||
int a = 0;
|
||||
}
|
||||
activateControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
return TWidget::eventFilter(obj, e);
|
||||
|
@ -2832,6 +2938,8 @@ void MediaView::setVisible(bool visible) {
|
|||
_controlsHideTimer.stop();
|
||||
_controlsState = ControlsShown;
|
||||
a_cOpacity = anim::value(1, 1);
|
||||
_groupThumbs = nullptr;
|
||||
_groupThumbsRect = QRect();
|
||||
}
|
||||
TWidget::setVisible(visible);
|
||||
if (visible) {
|
||||
|
@ -2936,20 +3044,3 @@ float64 MediaView::overLevel(OverState control) const {
|
|||
auto i = _animOpacities.constFind(control);
|
||||
return (i == _animOpacities.cend()) ? (_over == control ? 1 : 0) : i->current();
|
||||
}
|
||||
|
||||
bool MediaView::groupThumbsDisplayed() const {
|
||||
return _groupThumbs != nullptr;
|
||||
}
|
||||
|
||||
QRect MediaView::groupThumbsRect() const {
|
||||
Expects(_groupThumbs != nullptr);
|
||||
|
||||
auto result = _groupThumbs->paintedRect();
|
||||
result.moveTopLeft(result.topLeft()
|
||||
+ QPoint(_groupThumbsLeft, _groupThumbsTop));
|
||||
return result;
|
||||
}
|
||||
|
||||
QRect MediaView::groupThumbsFullRect() const {
|
||||
return QRect(0, width(), _groupThumbsTop, height() - _groupThumbsTop);
|
||||
}
|
||||
|
|
|
@ -189,6 +189,7 @@ private:
|
|||
void refreshCaption(HistoryItem *item);
|
||||
void refreshMediaViewer();
|
||||
void refreshNavVisibility();
|
||||
void refreshGroupThumbs();
|
||||
|
||||
void dropdownHidden();
|
||||
void updateDocSize();
|
||||
|
@ -210,7 +211,8 @@ private:
|
|||
void toggleVideoPaused();
|
||||
|
||||
void createClipController();
|
||||
void setClipControllerGeometry();
|
||||
void refreshClipControllerGeometry();
|
||||
void refreshCaptionGeometry();
|
||||
|
||||
void initAnimation();
|
||||
void createClipReader();
|
||||
|
@ -248,9 +250,8 @@ private:
|
|||
bool updateOverState(OverState newState);
|
||||
float64 overLevel(OverState control) const;
|
||||
|
||||
bool groupThumbsDisplayed() const;
|
||||
QRect groupThumbsRect() const;
|
||||
QRect groupThumbsFullRect() const;
|
||||
void checkGroupThumbsAnimation();
|
||||
|
||||
QBrush _transparentBrush;
|
||||
|
||||
|
@ -279,6 +280,8 @@ private:
|
|||
int _fullScreenZoomCache = 0;
|
||||
|
||||
std::unique_ptr<Media::View::GroupThumbs> _groupThumbs;
|
||||
QRect _groupThumbsRect;
|
||||
int _groupThumbsAvailableWidth = 0;
|
||||
int _groupThumbsLeft = 0;
|
||||
int _groupThumbsTop = 0;
|
||||
Text _caption;
|
||||
|
|
Loading…
Reference in New Issue