init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
267
Telegram/SourceFiles/ui/boxes/auto_delete_settings.cpp
Normal file
267
Telegram/SourceFiles/ui/boxes/auto_delete_settings.cpp
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
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 "ui/boxes/auto_delete_settings.h"
|
||||
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateSliderForTTL(
|
||||
not_null<QWidget*> parent,
|
||||
std::vector<QString> labels,
|
||||
int dashedAfterIndex,
|
||||
int selected,
|
||||
Fn<void(int)> callback) {
|
||||
Expects(labels.size() > 1);
|
||||
Expects(selected >= 0 && selected < labels.size());
|
||||
Expects(dashedAfterIndex >= 0 && dashedAfterIndex < labels.size());
|
||||
|
||||
struct State {
|
||||
std::vector<int> points;
|
||||
std::vector<QString> labels;
|
||||
int selected = 0;
|
||||
};
|
||||
static const auto st = &st::defaultSliderForTTL;
|
||||
const auto height = st->font->height + st->skip + st->chosenSize;
|
||||
const auto count = int(labels.size());
|
||||
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);
|
||||
const auto raw = result.data();
|
||||
const auto slider = Ui::CreateChild<Ui::FixedHeightWidget>(
|
||||
raw,
|
||||
st->chosenSize);
|
||||
slider->setCursor(style::cur_pointer);
|
||||
slider->move(0, height - slider->height());
|
||||
|
||||
auto &lifetime = raw->lifetime();
|
||||
const auto state = lifetime.make_state<State>(State{
|
||||
.labels = std::move(labels),
|
||||
.selected = selected
|
||||
});
|
||||
state->points.resize(count, 0);
|
||||
|
||||
raw->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
state->points[i] = (width * i) / (count - 1);
|
||||
}
|
||||
slider->resize(width, slider->height());
|
||||
}, lifetime);
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
|
||||
p.setFont(st->font);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
// Label
|
||||
p.setPen(st->textFg);
|
||||
const auto &text = state->labels[i];
|
||||
const auto textWidth = st->font->width(text);
|
||||
const auto shift = (i == count - 1)
|
||||
? textWidth
|
||||
: (i > 0)
|
||||
? (textWidth / 2)
|
||||
: 0;
|
||||
const auto x = state->points[i] - shift;
|
||||
const auto y = st->font->ascent;
|
||||
p.drawText(x, y, text);
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
slider->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(slider);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setFont(st->font);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto middle = (st->chosenSize / 2.);
|
||||
|
||||
// Point
|
||||
const auto size = (i == state->selected)
|
||||
? st->chosenSize
|
||||
: st->pointSize;
|
||||
const auto pointfg = (i <= state->selected)
|
||||
? st->activeFg
|
||||
: st->inactiveFg;
|
||||
const auto shift = (i == count - 1)
|
||||
? float64(size)
|
||||
: (i > 0)
|
||||
? (size / 2.)
|
||||
: 0.;
|
||||
const auto pointx = state->points[i] - shift;
|
||||
const auto pointy = middle - (size / 2.);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(pointfg);
|
||||
p.drawEllipse(QRectF{ pointx, pointy, size * 1., size * 1. });
|
||||
|
||||
// Line
|
||||
if (i + 1 == count) {
|
||||
break;
|
||||
}
|
||||
const auto nextSize = (i + 1 == state->selected)
|
||||
? st->chosenSize
|
||||
: st->pointSize;
|
||||
const auto nextShift = (i + 1 == count - 1)
|
||||
? float64(nextSize)
|
||||
: (nextSize / 2.);
|
||||
const auto &linefg = (i + 1 <= state->selected)
|
||||
? st->activeFg
|
||||
: st->inactiveFg;
|
||||
const auto from = pointx + size + st->stroke * 1.5;
|
||||
const auto till = state->points[i + 1] - nextShift - st->stroke * 1.5;
|
||||
|
||||
auto pen = linefg->p;
|
||||
pen.setWidthF(st->stroke);
|
||||
if (i >= dashedAfterIndex) {
|
||||
// Try to fill the line with exact number of dash segments.
|
||||
// UPD Doesn't work so well because it changes when clicking.
|
||||
//const auto length = till - from;
|
||||
//const auto offSegmentsCount = int(base::SafeRound(
|
||||
// (length - st->dashOn) / (st->dashOn + st->dashOff)));
|
||||
//const auto onSegmentsCount = offSegmentsCount + 1;
|
||||
//const auto idealLength = offSegmentsCount * st->dashOff
|
||||
// + onSegmentsCount * st->dashOn;
|
||||
//const auto multiplier = length / float64(idealLength);
|
||||
|
||||
const auto multiplier = 1.;
|
||||
auto dashPattern = QVector<qreal>{
|
||||
st->dashOn * multiplier / st->stroke,
|
||||
st->dashOff * multiplier / st->stroke
|
||||
};
|
||||
pen.setDashPattern(dashPattern);
|
||||
}
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawLine(QPointF(from, middle), QPointF(till, middle));
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
slider->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseButtonPress)
|
||||
&& (static_cast<QMouseEvent*>(e.get())->button()
|
||||
== Qt::LeftButton)
|
||||
&& (state->points[1] > 0);
|
||||
}) | rpl::map([=](not_null<QEvent*> e) {
|
||||
return rpl::single(
|
||||
static_cast<QMouseEvent*>(e.get())->pos()
|
||||
) | rpl::then(slider->events(
|
||||
) | rpl::take_while([=](not_null<QEvent*> e) {
|
||||
return (e->type() != QEvent::MouseButtonRelease)
|
||||
|| (static_cast<QMouseEvent*>(e.get())->button()
|
||||
!= Qt::LeftButton);
|
||||
}) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseMove);
|
||||
}) | rpl::map([=](not_null<QEvent*> e) {
|
||||
return static_cast<QMouseEvent*>(e.get())->pos();
|
||||
}));
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::on_next([=](QPoint position) {
|
||||
state->selected = std::clamp(
|
||||
(position.x() + (state->points[1] / 2)) / state->points[1],
|
||||
0,
|
||||
count - 1);
|
||||
slider->update();
|
||||
callback(state->selected);
|
||||
}, lifetime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AutoDeleteSettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TimeId ttlPeriod,
|
||||
rpl::producer<QString> about,
|
||||
Fn<void(TimeId)> callback) {
|
||||
box->setTitle(tr::lng_manage_messages_ttl_title());
|
||||
|
||||
struct State {
|
||||
TimeId period = 0;
|
||||
};
|
||||
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.period = ttlPeriod,
|
||||
});
|
||||
|
||||
const auto options = std::vector<QString>{
|
||||
tr::lng_manage_messages_ttl_disable(tr::now),
|
||||
//u"5 seconds"_q, AssertIsDebug()
|
||||
tr::lng_manage_messages_ttl_after1(tr::now),
|
||||
tr::lng_manage_messages_ttl_after2(tr::now),
|
||||
tr::lng_manage_messages_ttl_after3(tr::now),
|
||||
};
|
||||
const auto periodToIndex = [&](TimeId period) {
|
||||
return !period
|
||||
? 0
|
||||
//: (period == 5) AssertIsDebug()
|
||||
//? 1 AssertIsDebug()
|
||||
: (period < 2 * 86400)
|
||||
? 1
|
||||
: (period < 8 * 86400)
|
||||
? 2
|
||||
: 3;
|
||||
};
|
||||
const auto indexToPeriod = [&](int index) {
|
||||
return !index
|
||||
? 0
|
||||
//: (index == 1) AssertIsDebug()
|
||||
//? 5 AssertIsDebug()
|
||||
: (index == 1)
|
||||
? 86400
|
||||
: (index == 2)
|
||||
? 7 * 86400
|
||||
: 31 * 86400;
|
||||
};
|
||||
const auto sliderCallback = [=](int index) {
|
||||
state->period = indexToPeriod(index);
|
||||
};
|
||||
box->addRow(
|
||||
CreateSliderForTTL(
|
||||
box,
|
||||
options | ranges::to_vector,
|
||||
options.size() - 1,
|
||||
periodToIndex(ttlPeriod),
|
||||
sliderCallback),
|
||||
{
|
||||
st::boxRowPadding.left(),
|
||||
0,
|
||||
st::boxRowPadding.right(),
|
||||
st::boxMediumSkip });
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(about),
|
||||
st::boxDividerLabel),
|
||||
st::ttlDividerLabelPadding),
|
||||
style::margins());
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto period = state->period;
|
||||
box->closeBox();
|
||||
|
||||
callback(period);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
20
Telegram/SourceFiles/ui/boxes/auto_delete_settings.h
Normal file
20
Telegram/SourceFiles/ui/boxes/auto_delete_settings.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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/layers/generic_box.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void AutoDeleteSettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TimeId ttlPeriod,
|
||||
rpl::producer<QString> about,
|
||||
Fn<void(TimeId)> callback);
|
||||
|
||||
} // namespace Ui
|
||||
986
Telegram/SourceFiles/ui/boxes/boost_box.cpp
Normal file
986
Telegram/SourceFiles/ui/boxes/boost_box.cpp
Normal file
@@ -0,0 +1,986 @@
|
||||
/*
|
||||
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 "ui/boxes/boost_box.h"
|
||||
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/effects/fireworks_animation.h"
|
||||
#include "ui/effects/premium_bubble.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {
|
||||
const auto exact = (data.boosts == data.thisLevelBoosts);
|
||||
const auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);
|
||||
if (reached) {
|
||||
--data.level;
|
||||
data.boosts = data.nextLevelBoosts = std::max({
|
||||
data.boosts,
|
||||
data.thisLevelBoosts,
|
||||
1
|
||||
});
|
||||
data.thisLevelBoosts = 0;
|
||||
} else {
|
||||
data.boosts = std::max(data.thisLevelBoosts, data.boosts);
|
||||
data.nextLevelBoosts = std::max(
|
||||
data.nextLevelBoosts,
|
||||
data.boosts + 1);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> repeated,
|
||||
bool centered = true) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
|
||||
struct State {
|
||||
not_null<Ui::FlatLabel*> title;
|
||||
not_null<Ui::FlatLabel*> repeated;
|
||||
};
|
||||
const auto notEmpty = [](const QString &text) {
|
||||
return !text.isEmpty();
|
||||
};
|
||||
const auto state = parent->lifetime().make_state<State>(State{
|
||||
.title = Ui::CreateChild<Ui::FlatLabel>(
|
||||
result.data(),
|
||||
rpl::duplicate(title),
|
||||
st::boostTitle),
|
||||
.repeated = Ui::CreateChild<Ui::FlatLabel>(
|
||||
result.data(),
|
||||
rpl::duplicate(repeated) | rpl::filter(notEmpty),
|
||||
st::boostTitleBadge),
|
||||
});
|
||||
state->title->show();
|
||||
state->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));
|
||||
|
||||
result->resize(result->width(), st::boostTitle.style.font->height);
|
||||
|
||||
rpl::combine(
|
||||
result->widthValue(),
|
||||
rpl::duplicate(title),
|
||||
state->repeated->shownValue(),
|
||||
state->repeated->widthValue()
|
||||
) | rpl::on_next([=](int outer, auto&&, bool shown, int badge) {
|
||||
const auto repeated = shown ? badge : 0;
|
||||
const auto skip = st::boostTitleBadgeSkip;
|
||||
const auto available = outer - repeated - skip;
|
||||
const auto use = std::min(state->title->textMaxWidth(), available);
|
||||
state->title->resizeToWidth(use);
|
||||
const auto left = centered
|
||||
? (outer - use - skip - repeated) / 2
|
||||
: 0;
|
||||
state->title->moveToLeft(left, 0);
|
||||
const auto mleft = st::boostTitleBadge.margin.left();
|
||||
const auto mtop = st::boostTitleBadge.margin.top();
|
||||
state->repeated->moveToLeft(left + use + skip + mleft, mtop);
|
||||
}, result->lifetime());
|
||||
|
||||
const auto badge = state->repeated;
|
||||
badge->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(badge);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = std::min(badge->width(), badge->height()) / 2;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::premiumButtonBg2);
|
||||
p.drawRoundedRect(badge->rect(), radius, radius);
|
||||
}, badge->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text) {
|
||||
return MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {
|
||||
auto gradient = QLinearGradient(
|
||||
rect.topLeft(),
|
||||
rect.topRight());
|
||||
gradient.setStops(Ui::Premium::GiftGradientStops());
|
||||
return QBrush(gradient);
|
||||
});
|
||||
}
|
||||
|
||||
void AddFeaturesList(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Ui::BoostFeatures &features,
|
||||
int startFromLevel,
|
||||
bool group) {
|
||||
const auto add = [&](
|
||||
rpl::producer<TextWithEntities> text,
|
||||
const style::icon &st) {
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::boostFeatureLabel),
|
||||
st::boostFeaturePadding);
|
||||
object_ptr<Info::Profile::FloatingIcon>(
|
||||
label,
|
||||
st,
|
||||
st::boostFeatureIconPosition);
|
||||
};
|
||||
const auto lowMax = std::max({
|
||||
features.linkLogoLevel,
|
||||
features.profileIconLevel,
|
||||
features.autotranslateLevel,
|
||||
features.transcribeLevel,
|
||||
features.emojiPackLevel,
|
||||
features.emojiStatusLevel,
|
||||
features.wallpaperLevel,
|
||||
features.customWallpaperLevel,
|
||||
(features.nameColorsByLevel.empty()
|
||||
? 0
|
||||
: features.nameColorsByLevel.back().first),
|
||||
(features.linkStylesByLevel.empty()
|
||||
? 0
|
||||
: features.linkStylesByLevel.back().first),
|
||||
(features.profileColorsByLevel.empty()
|
||||
? 0
|
||||
: features.profileColorsByLevel.back().first),
|
||||
});
|
||||
const auto highMax = std::max(lowMax, features.sponsoredLevel);
|
||||
auto nameColors = 0;
|
||||
auto linkStyles = 0;
|
||||
auto profileColors = 0;
|
||||
for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {
|
||||
if ((i > lowMax) && (i < highMax)) {
|
||||
continue;
|
||||
}
|
||||
const auto unlocks = (i == startFromLevel);
|
||||
{
|
||||
const auto badge = container->add(
|
||||
MakeFeaturesBadge(
|
||||
container,
|
||||
(unlocks
|
||||
? tr::lng_boost_level_unlocks
|
||||
: tr::lng_boost_level)(
|
||||
lt_count,
|
||||
rpl::single(float64(i)))),
|
||||
st::boostLevelBadgePadding,
|
||||
style::al_top);
|
||||
const auto padding = st::boxRowPadding;
|
||||
const auto line = Ui::CreateChild<Ui::RpWidget>(container);
|
||||
badge->geometryValue() | rpl::on_next([=](const QRect &r) {
|
||||
line->setGeometry(
|
||||
padding.left(),
|
||||
r.y(),
|
||||
container->width() - rect::m::sum::h(padding),
|
||||
r.height());
|
||||
}, line->lifetime());
|
||||
const auto shift = st::lineWidth * 10;
|
||||
line->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(line);
|
||||
p.setPen(st::windowSubTextFg);
|
||||
const auto y = line->height() / 2;
|
||||
const auto left = badge->x() - shift - padding.left();
|
||||
const auto right = left + badge->width() + shift * 2;
|
||||
if (left > 0) {
|
||||
p.drawLine(0, y, left, y);
|
||||
}
|
||||
if (right < line->width()) {
|
||||
p.drawLine(right, y, line->width(), y);
|
||||
}
|
||||
}, line->lifetime());
|
||||
}
|
||||
if (i >= features.sponsoredLevel) {
|
||||
add(
|
||||
tr::lng_channel_earn_off(tr::rich),
|
||||
st::boostFeatureOffSponsored);
|
||||
}
|
||||
if (i >= features.customWallpaperLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_custom_background_group
|
||||
: tr::lng_feature_custom_background_channel)(tr::rich),
|
||||
st::boostFeatureCustomBackground);
|
||||
}
|
||||
if (i >= features.wallpaperLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_backgrounds_group
|
||||
: tr::lng_feature_backgrounds_channel)(
|
||||
lt_count,
|
||||
rpl::single(float64(features.wallpapersCount)),
|
||||
tr::rich),
|
||||
st::boostFeatureBackground);
|
||||
}
|
||||
if (i >= features.emojiStatusLevel) {
|
||||
add(
|
||||
tr::lng_feature_emoji_status(tr::rich),
|
||||
st::boostFeatureEmojiStatus);
|
||||
}
|
||||
if (const auto j = features.profileColorsByLevel.find(i)
|
||||
; j != end(features.profileColorsByLevel)) {
|
||||
profileColors += j->second;
|
||||
}
|
||||
if (i >= features.profileIconLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_profile_icon_group
|
||||
: tr::lng_feature_profile_icon_channel)(tr::rich),
|
||||
st::boostFeatureProfileIcon);
|
||||
}
|
||||
if (profileColors > 0) {
|
||||
add((group
|
||||
? tr::lng_feature_profile_color_group
|
||||
: tr::lng_feature_profile_color_channel)(
|
||||
lt_count,
|
||||
rpl::single(float64(profileColors)),
|
||||
tr::rich
|
||||
), st::boostFeatureProfileColor);
|
||||
}
|
||||
if (!group) {
|
||||
if (const auto j = features.linkStylesByLevel.find(i)
|
||||
; j != end(features.linkStylesByLevel)) {
|
||||
linkStyles += j->second;
|
||||
}
|
||||
if (i >= features.linkLogoLevel) {
|
||||
add(
|
||||
tr::lng_feature_link_emoji(tr::rich),
|
||||
st::boostFeatureCustomLink);
|
||||
}
|
||||
if (linkStyles > 0) {
|
||||
add(tr::lng_feature_link_style_channel(
|
||||
lt_count,
|
||||
rpl::single(float64(linkStyles)),
|
||||
tr::rich
|
||||
), st::boostFeatureLink);
|
||||
}
|
||||
if (const auto j = features.nameColorsByLevel.find(i)
|
||||
; j != end(features.nameColorsByLevel)) {
|
||||
nameColors += j->second;
|
||||
}
|
||||
if (nameColors > 0) {
|
||||
add(tr::lng_feature_name_color_channel(
|
||||
lt_count,
|
||||
rpl::single(float64(nameColors)),
|
||||
tr::rich
|
||||
), st::boostFeatureName);
|
||||
}
|
||||
add(tr::lng_feature_reactions(
|
||||
lt_count,
|
||||
rpl::single(float64(i)),
|
||||
tr::rich
|
||||
), st::boostFeatureCustomReactions);
|
||||
}
|
||||
add(
|
||||
tr::lng_feature_stories(lt_count, rpl::single(1. * i), tr::rich),
|
||||
st::boostFeatureStories);
|
||||
if (!group && i >= features.autotranslateLevel) {
|
||||
add(
|
||||
tr::lng_feature_autotranslate(tr::rich),
|
||||
st::boostFeatureAutoTranslate);
|
||||
}
|
||||
if (group && i >= features.transcribeLevel) {
|
||||
add(
|
||||
tr::lng_feature_transcribe(tr::rich),
|
||||
st::boostFeatureTranscribe);
|
||||
}
|
||||
if (group && i >= features.emojiPackLevel) {
|
||||
add(
|
||||
tr::lng_feature_custom_emoji_pack(tr::rich),
|
||||
st::boostFeatureCustomEmoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void StartFireworks(not_null<QWidget*> parent) {
|
||||
const auto result = Ui::CreateChild<RpWidget>(parent.get());
|
||||
result->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
result->setGeometry(parent->rect());
|
||||
result->show();
|
||||
|
||||
auto &lifetime = result->lifetime();
|
||||
const auto animation = lifetime.make_state<FireworksAnimation>([=] {
|
||||
result->update();
|
||||
});
|
||||
result->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(result);
|
||||
if (!animation->paint(p, result->rect())) {
|
||||
crl::on_main(result, [=] { delete result; });
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
void BoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
BoostBoxData data,
|
||||
Fn<void(Fn<void(BoostCounters)>)> boost) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::boostBox);
|
||||
|
||||
//AssertIsDebug();
|
||||
//data.boost = {
|
||||
// .level = 2,
|
||||
// .boosts = 3,
|
||||
// .thisLevelBoosts = 2,
|
||||
// .nextLevelBoosts = 5,
|
||||
// .mine = 2,
|
||||
//};
|
||||
|
||||
struct State {
|
||||
rpl::variable<BoostCounters> data;
|
||||
rpl::variable<bool> full;
|
||||
bool submitted = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->data = std::move(data.boost);
|
||||
|
||||
FillBoostLimit(
|
||||
BoxShowFinishes(box),
|
||||
box->verticalLayout(),
|
||||
state->data.value(),
|
||||
st::boxRowPadding);
|
||||
|
||||
box->setMaxHeight(st::boostBoxMaxHeight);
|
||||
const auto close = box->addTopButton(
|
||||
st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
|
||||
const auto name = data.name;
|
||||
|
||||
auto title = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (counters.mine > 0)
|
||||
? tr::lng_boost_channel_you_title(
|
||||
lt_channel,
|
||||
rpl::single(name))
|
||||
: !counters.nextLevelBoosts
|
||||
? tr::lng_boost_channel_title_max()
|
||||
: counters.level
|
||||
? (data.group
|
||||
? tr::lng_boost_channel_title_more_group()
|
||||
: tr::lng_boost_channel_title_more())
|
||||
: (data.group
|
||||
? tr::lng_boost_channel_title_first_group()
|
||||
: tr::lng_boost_channel_title_first());
|
||||
}) | rpl::flatten_latest();
|
||||
auto repeated = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (counters.mine > 1) ? u"x%1"_q.arg(counters.mine) : u""_q;
|
||||
});
|
||||
|
||||
const auto wasMine = state->data.current().mine;
|
||||
const auto wasLifting = data.lifting;
|
||||
auto text = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
const auto lifting = wasLifting
|
||||
? (wasLifting
|
||||
- std::clamp(counters.mine - wasMine, 0, wasLifting - 1))
|
||||
: 0;
|
||||
const auto bold = tr::bold(name);
|
||||
const auto now = counters.boosts;
|
||||
const auto full = !counters.nextLevelBoosts;
|
||||
const auto left = (counters.nextLevelBoosts > now)
|
||||
? (counters.nextLevelBoosts - now)
|
||||
: 0;
|
||||
auto post = tr::lng_boost_channel_post_stories(
|
||||
lt_count,
|
||||
rpl::single(float64(counters.level + (left ? 1 : 0))),
|
||||
tr::rich);
|
||||
return (lifting > 1)
|
||||
? tr::lng_boost_group_lift_restrictions_many(
|
||||
lt_count,
|
||||
rpl::single(float64(lifting)),
|
||||
tr::rich)
|
||||
: lifting
|
||||
? tr::lng_boost_group_lift_restrictions(tr::rich)
|
||||
: (counters.mine || full)
|
||||
? (left
|
||||
? tr::lng_boost_channel_needs_unlock(
|
||||
lt_count,
|
||||
rpl::single(float64(left)),
|
||||
lt_channel,
|
||||
rpl::single(bold),
|
||||
tr::rich)
|
||||
: (!counters.level
|
||||
? (data.group
|
||||
? tr::lng_boost_channel_reached_first_group
|
||||
: tr::lng_boost_channel_reached_first)(
|
||||
tr::rich)
|
||||
: (data.group
|
||||
? tr::lng_boost_channel_reached_more_group
|
||||
: tr::lng_boost_channel_reached_more)(
|
||||
lt_count,
|
||||
rpl::single(float64(counters.level)),
|
||||
lt_post,
|
||||
std::move(post),
|
||||
tr::rich)))
|
||||
: tr::lng_boost_channel_needs_unlock(
|
||||
lt_count,
|
||||
rpl::single(float64(left)),
|
||||
lt_channel,
|
||||
rpl::single(bold),
|
||||
tr::rich);
|
||||
}) | rpl::flatten_latest();
|
||||
if (wasLifting) {
|
||||
state->data.value(
|
||||
) | rpl::on_next([=](BoostCounters counters) {
|
||||
if (counters.mine - wasMine >= wasLifting) {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
auto faded = object_ptr<Ui::FadeWrap<>>(
|
||||
close->parentWidget(),
|
||||
MakeTitle(
|
||||
box,
|
||||
(data.group
|
||||
? tr::lng_boost_group_button
|
||||
: tr::lng_boost_channel_button)(),
|
||||
rpl::duplicate(repeated),
|
||||
false));
|
||||
const auto titleInner = faded.data();
|
||||
titleInner->move(st::boxTitlePosition);
|
||||
titleInner->resizeToWidth(st::boxWideWidth
|
||||
- st::boxTitleClose.width
|
||||
- st::boxTitlePosition.x());
|
||||
titleInner->hide(anim::type::instant);
|
||||
crl::on_main(titleInner, [=] {
|
||||
titleInner->raise();
|
||||
titleInner->toggleOn(rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
box->scrolls()
|
||||
) | rpl::map([=] {
|
||||
return box->scrollTop() > 0;
|
||||
}));
|
||||
});
|
||||
|
||||
box->addRow(
|
||||
MakeTitle(box, std::move(title), std::move(repeated)),
|
||||
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::boostText),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
|
||||
|
||||
const auto current = state->data.current();
|
||||
box->setTitle(rpl::single(QString()));
|
||||
AddFeaturesList(
|
||||
box->verticalLayout(),
|
||||
data.features,
|
||||
current.level + (current.nextLevelBoosts ? 1 : 0),
|
||||
data.group);
|
||||
|
||||
const auto allowMulti = data.allowMulti;
|
||||
auto submit = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (!counters.nextLevelBoosts || (counters.mine && !allowMulti))
|
||||
? tr::lng_box_ok()
|
||||
: (counters.mine > 0)
|
||||
? tr::lng_boost_again_button()
|
||||
: data.group
|
||||
? tr::lng_boost_group_button()
|
||||
: tr::lng_boost_channel_button();
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
box->addButton(rpl::duplicate(submit), [=] {
|
||||
if (state->submitted) {
|
||||
return;
|
||||
} else if (state->data.current().nextLevelBoosts > 0
|
||||
&& (allowMulti || !state->data.current().mine)) {
|
||||
state->submitted = true;
|
||||
const auto was = state->data.current().mine;
|
||||
|
||||
//AssertIsDebug();
|
||||
//state->submitted = false;
|
||||
//if (state->data.current().level == 5
|
||||
// && state->data.current().boosts == 11) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 5,
|
||||
// .boosts = 14,
|
||||
// .thisLevelBoosts = 9,
|
||||
// .nextLevelBoosts = 15,
|
||||
// .mine = 14,
|
||||
// };
|
||||
//} else if (state->data.current().level == 5) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 7,
|
||||
// .boosts = 16,
|
||||
// .thisLevelBoosts = 15,
|
||||
// .nextLevelBoosts = 19,
|
||||
// .mine = 16,
|
||||
// };
|
||||
//} else if (state->data.current().level == 4) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 5,
|
||||
// .boosts = 11,
|
||||
// .thisLevelBoosts = 9,
|
||||
// .nextLevelBoosts = 15,
|
||||
// .mine = 9,
|
||||
// };
|
||||
//} else if (state->data.current().level == 3) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 4,
|
||||
// .boosts = 7,
|
||||
// .thisLevelBoosts = 7,
|
||||
// .nextLevelBoosts = 9,
|
||||
// .mine = 5,
|
||||
// };
|
||||
//} else {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 3,
|
||||
// .boosts = 5,
|
||||
// .thisLevelBoosts = 5,
|
||||
// .nextLevelBoosts = 7,
|
||||
// .mine = 3,
|
||||
// };
|
||||
//}
|
||||
//return;
|
||||
|
||||
boost(crl::guard(box, [=](BoostCounters result) {
|
||||
state->submitted = false;
|
||||
|
||||
if (result.thisLevelBoosts || result.nextLevelBoosts) {
|
||||
if (result.mine > was) {
|
||||
StartFireworks(box->parentWidget());
|
||||
}
|
||||
state->data = result;
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeLinkLabel(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<QString> link,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
object_ptr<Ui::RpWidget> right) {
|
||||
auto result = object_ptr<Ui::AbstractButton>(parent);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto rawRight = right.release();
|
||||
if (rawRight) {
|
||||
rawRight->setParent(raw);
|
||||
rawRight->show();
|
||||
}
|
||||
|
||||
struct State {
|
||||
State(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> value,
|
||||
rpl::producer<QString> link)
|
||||
: text(std::move(value))
|
||||
, link(std::move(link))
|
||||
, label(parent, text.value(), st::giveawayGiftCodeLink)
|
||||
, bg(st::roundRadiusLarge, st::windowBgOver) {
|
||||
}
|
||||
|
||||
rpl::variable<QString> text;
|
||||
rpl::variable<QString> link;
|
||||
Ui::FlatLabel label;
|
||||
Ui::RoundRect bg;
|
||||
};
|
||||
|
||||
const auto state = raw->lifetime().make_state<State>(
|
||||
raw,
|
||||
rpl::duplicate(text),
|
||||
std::move(link));
|
||||
state->label.setSelectable(true);
|
||||
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
std::move(text)
|
||||
) | rpl::on_next([=](int outer, const auto&) {
|
||||
const auto textWidth = state->label.textMaxWidth();
|
||||
const auto skipLeft = st::giveawayGiftCodeLink.margin.left();
|
||||
const auto skipRight = rawRight
|
||||
? rawRight->width()
|
||||
: st::giveawayGiftCodeLink.margin.right();
|
||||
const auto available = outer - skipRight - skipLeft;
|
||||
const auto use = std::min(textWidth, available);
|
||||
state->label.resizeToWidth(use);
|
||||
const auto forCenter = (outer - use) / 2;
|
||||
const auto x = (forCenter < skipLeft)
|
||||
? skipLeft
|
||||
: (forCenter > outer - skipRight - use)
|
||||
? (outer - skipRight - use)
|
||||
: forCenter;
|
||||
state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
state->bg.paint(p, raw->rect());
|
||||
}, raw->lifetime());
|
||||
|
||||
state->label.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight);
|
||||
if (rawRight) {
|
||||
raw->widthValue() | rpl::on_next([=](int width) {
|
||||
rawRight->move(width - rawRight->width(), 0);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
raw->setClickedCallback([=] {
|
||||
QGuiApplication::clipboard()->setText(state->link.current());
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void BoostBoxAlready(not_null<GenericBox*> box, bool group) {
|
||||
ConfirmBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_already_text_group
|
||||
: tr::lng_boost_error_already_text)(tr::rich),
|
||||
.title = tr::lng_boost_error_already_title(),
|
||||
.inform = true,
|
||||
});
|
||||
}
|
||||
|
||||
void GiftForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
QString channel,
|
||||
int receive,
|
||||
bool again) {
|
||||
ConfirmBox(box, {
|
||||
.text = (again
|
||||
? tr::lng_boost_need_more_again
|
||||
: tr::lng_boost_need_more_text)(
|
||||
lt_count,
|
||||
rpl::single(receive) | tr::to_count(),
|
||||
lt_channel,
|
||||
rpl::single(TextWithEntities{ channel }),
|
||||
tr::rich),
|
||||
.title = tr::lng_boost_need_more(),
|
||||
.inform = true,
|
||||
});
|
||||
}
|
||||
|
||||
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group) {
|
||||
InformBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_gifted_text_group
|
||||
: tr::lng_boost_error_gifted_text)(tr::rich),
|
||||
.title = tr::lng_boost_error_gifted_title(),
|
||||
});
|
||||
}
|
||||
|
||||
void PremiumForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
bool group,
|
||||
Fn<void()> buyPremium) {
|
||||
ConfirmBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_premium_text_group
|
||||
: tr::lng_boost_error_premium_text)(tr::rich),
|
||||
.confirmed = buyPremium,
|
||||
.confirmText = tr::lng_boost_error_premium_yes(),
|
||||
.title = tr::lng_boost_error_premium_title(),
|
||||
});
|
||||
}
|
||||
|
||||
void AskBoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
AskBoostBoxData data,
|
||||
Fn<void()> openStatistics,
|
||||
Fn<void()> startGiveaway) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::boostBox);
|
||||
box->setNoContentMargin(true);
|
||||
box->addSkip(st::boxRowPadding.left());
|
||||
|
||||
FillBoostLimit(
|
||||
BoxShowFinishes(box),
|
||||
box->verticalLayout(),
|
||||
rpl::single(data.boost),
|
||||
st::boxRowPadding);
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
|
||||
auto title = v::match(data.reason.data, [](AskBoostChannelColor) {
|
||||
return tr::lng_boost_channel_title_color();
|
||||
}, [](AskBoostAutotranslate) {
|
||||
return tr::lng_boost_channel_title_autotranslate();
|
||||
}, [](AskBoostWallpaper) {
|
||||
return tr::lng_boost_channel_title_wallpaper();
|
||||
}, [](AskBoostEmojiStatus) {
|
||||
return tr::lng_boost_channel_title_status();
|
||||
}, [](AskBoostEmojiPack) {
|
||||
return tr::lng_boost_group_title_emoji();
|
||||
}, [](AskBoostCustomReactions) {
|
||||
return tr::lng_boost_channel_title_reactions();
|
||||
}, [](AskBoostCpm) {
|
||||
return tr::lng_boost_channel_title_cpm();
|
||||
}, [](AskBoostWearCollectible) {
|
||||
return tr::lng_boost_channel_title_wear();
|
||||
});
|
||||
auto isGroup = false;
|
||||
auto reasonText = v::match(data.reason.data, [&](
|
||||
AskBoostChannelColor data) {
|
||||
return tr::lng_boost_channel_needs_level_color(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostAutotranslate data) {
|
||||
return tr::lng_boost_channel_needs_level_autotranslate(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostWallpaper data) {
|
||||
isGroup = data.group;
|
||||
return (data.group
|
||||
? tr::lng_boost_group_needs_level_wallpaper
|
||||
: tr::lng_boost_channel_needs_level_wallpaper)(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostEmojiStatus data) {
|
||||
isGroup = data.group;
|
||||
return (data.group
|
||||
? tr::lng_boost_group_needs_level_status
|
||||
: tr::lng_boost_channel_needs_level_status)(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostEmojiPack data) {
|
||||
isGroup = true;
|
||||
return tr::lng_boost_group_needs_level_emoji(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostCustomReactions data) {
|
||||
return tr::lng_boost_channel_needs_level_reactions(
|
||||
lt_count,
|
||||
rpl::single(float64(data.count)),
|
||||
lt_same_count,
|
||||
rpl::single(TextWithEntities{ QString::number(data.count) }),
|
||||
tr::rich);
|
||||
}, [&](AskBoostCpm data) {
|
||||
return tr::lng_boost_channel_needs_level_cpm(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostWearCollectible data) {
|
||||
return tr::lng_boost_channel_needs_level_wear(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
});
|
||||
auto text = rpl::combine(
|
||||
std::move(reasonText),
|
||||
(isGroup ? tr::lng_boost_group_ask : tr::lng_boost_channel_ask)(
|
||||
tr::rich)
|
||||
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
|
||||
return text.append(u"\n\n"_q).append(std::move(ask));
|
||||
});
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(title),
|
||||
st::boostCenteredTitle),
|
||||
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0),
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::boostText),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
|
||||
style::al_top);
|
||||
|
||||
auto stats = object_ptr<Ui::IconButton>(box, st::boostLinkStatsButton);
|
||||
stats->setClickedCallback(openStatistics);
|
||||
box->addRow(MakeLinkLabel(
|
||||
box,
|
||||
rpl::single(data.link),
|
||||
rpl::single(data.link),
|
||||
box->uiShow(),
|
||||
std::move(stats)));
|
||||
|
||||
AddFeaturesList(
|
||||
box->verticalLayout(),
|
||||
data.features,
|
||||
data.boost.level + (data.boost.nextLevelBoosts ? 1 : 0),
|
||||
data.group);
|
||||
|
||||
auto submit = tr::lng_boost_channel_ask_button();
|
||||
box->addButton(rpl::duplicate(submit), [=] {
|
||||
QGuiApplication::clipboard()->setText(data.link);
|
||||
box->uiShow()->showToast(tr::lng_username_copied(tr::now));
|
||||
});
|
||||
}
|
||||
|
||||
void FillBoostLimit(
|
||||
rpl::producer<> showFinished,
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<BoostCounters> data,
|
||||
style::margins limitLinePadding) {
|
||||
const auto addSkip = [&](int skip) {
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
|
||||
};
|
||||
|
||||
const auto ratio = [=](BoostCounters counters) {
|
||||
const auto min = counters.thisLevelBoosts;
|
||||
const auto max = counters.nextLevelBoosts;
|
||||
|
||||
Assert(counters.boosts >= min && counters.boosts <= max);
|
||||
const auto count = (max - min);
|
||||
const auto index = (counters.boosts - min);
|
||||
if (!index) {
|
||||
return 0.;
|
||||
} else if (index == count) {
|
||||
return 1.;
|
||||
} else if (count == 2) {
|
||||
return 0.5;
|
||||
}
|
||||
const auto available = st::boxWideWidth
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
const auto average = available / float64(count);
|
||||
const auto levelWidth = [&](int add) {
|
||||
return st::normalFont->width(
|
||||
tr::lng_boost_level(
|
||||
tr::now,
|
||||
lt_count,
|
||||
counters.level + add));
|
||||
};
|
||||
const auto paddings = 2 * st::premiumLineTextSkip;
|
||||
const auto labelLeftWidth = paddings + levelWidth(0);
|
||||
const auto labelRightWidth = paddings + levelWidth(1);
|
||||
const auto first = std::max(average, labelLeftWidth * 1.);
|
||||
const auto last = std::max(average, labelRightWidth * 1.);
|
||||
const auto other = (available - first - last) / (count - 2);
|
||||
return (first + (index - 1) * other) / available;
|
||||
};
|
||||
|
||||
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
|
||||
|
||||
auto bubbleRowState = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::combine_previous(
|
||||
BoostCounters()
|
||||
) | rpl::map([=](BoostCounters previous, BoostCounters counters) {
|
||||
return Premium::BubbleRowState{
|
||||
.counter = counters.boosts,
|
||||
.ratio = ratio(counters),
|
||||
.animateFromZero = (counters.level != previous.level),
|
||||
.dynamic = true,
|
||||
};
|
||||
});
|
||||
Premium::AddBubbleRow(
|
||||
container,
|
||||
st::boostBubble,
|
||||
std::move(showFinished),
|
||||
rpl::duplicate(bubbleRowState),
|
||||
Premium::BubbleType::Premium,
|
||||
nullptr,
|
||||
&st::premiumIconBoost,
|
||||
limitLinePadding);
|
||||
addSkip(st::premiumLineTextSkip);
|
||||
|
||||
const auto level = [](int level) {
|
||||
return tr::lng_boost_level(tr::now, lt_count, level);
|
||||
};
|
||||
auto limitState = std::move(
|
||||
bubbleRowState
|
||||
) | rpl::map([](const Premium::BubbleRowState &state) {
|
||||
return Premium::LimitRowState{
|
||||
.ratio = state.ratio,
|
||||
.animateFromZero = state.animateFromZero,
|
||||
.dynamic = state.dynamic
|
||||
};
|
||||
});
|
||||
auto left = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return level(counters.level);
|
||||
});
|
||||
auto right = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return level(counters.level + 1);
|
||||
});
|
||||
Premium::AddLimitRow(
|
||||
container,
|
||||
st::boostLimits,
|
||||
Premium::LimitRowLabels{
|
||||
.leftLabel = std::move(left),
|
||||
.rightLabel = std::move(right),
|
||||
},
|
||||
std::move(limitState),
|
||||
limitLinePadding);
|
||||
}
|
||||
|
||||
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
Fn<QBrush(QRect)> bg) {
|
||||
auto result = object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st::boostLevelBadge);
|
||||
const auto label = result.data();
|
||||
|
||||
label->show();
|
||||
label->paintRequest() | rpl::on_next([=] {
|
||||
const auto size = label->textMaxWidth();
|
||||
const auto rect = QRect(
|
||||
(label->width() - size) / 2,
|
||||
st::boostLevelBadge.margin.top(),
|
||||
size,
|
||||
st::boostLevelBadge.style.font->height
|
||||
).marginsAdded(st::boostLevelBadge.margin);
|
||||
auto p = QPainter(label);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(bg(rect));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
|
||||
|
||||
const auto &lineFg = st::windowBgRipple;
|
||||
const auto line = st::boostLevelBadgeLine;
|
||||
const auto top = st::boostLevelBadge.margin.top()
|
||||
+ ((st::boostLevelBadge.style.font->height - line) / 2);
|
||||
const auto left = 0;
|
||||
const auto skip = st::boostLevelBadgeSkip;
|
||||
if (const auto right = rect.x() - skip; right > left) {
|
||||
p.fillRect(left, top, right - left, line, lineFg);
|
||||
}
|
||||
const auto right = label->width();
|
||||
if (const auto left = rect.x() + rect.width() + skip
|
||||
; left < right) {
|
||||
p.fillRect(left, top, right - left, line, lineFg);
|
||||
}
|
||||
}, label->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
154
Telegram/SourceFiles/ui/boxes/boost_box.h
Normal file
154
Telegram/SourceFiles/ui/boxes/boost_box.h
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void StartFireworks(not_null<QWidget*> parent);
|
||||
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
class VerticalLayout;
|
||||
class FlatLabel;
|
||||
|
||||
struct BoostCounters {
|
||||
int level = 0;
|
||||
int boosts = 0;
|
||||
int thisLevelBoosts = 0;
|
||||
int nextLevelBoosts = 0; // Zero means no next level is available.
|
||||
int mine = 0;
|
||||
|
||||
friend inline constexpr bool operator==(
|
||||
BoostCounters,
|
||||
BoostCounters) = default;
|
||||
};
|
||||
|
||||
struct BoostFeatures {
|
||||
base::flat_map<int, int> nameColorsByLevel;
|
||||
base::flat_map<int, int> linkStylesByLevel;
|
||||
base::flat_map<int, int> profileColorsByLevel;
|
||||
int linkLogoLevel = 0;
|
||||
int profileIconLevel = 0;
|
||||
int autotranslateLevel = 0;
|
||||
int transcribeLevel = 0;
|
||||
int emojiPackLevel = 0;
|
||||
int emojiStatusLevel = 0;
|
||||
int wallpaperLevel = 0;
|
||||
int wallpapersCount = 0;
|
||||
int customWallpaperLevel = 0;
|
||||
int sponsoredLevel = 0;
|
||||
};
|
||||
|
||||
struct BoostBoxData {
|
||||
QString name;
|
||||
BoostCounters boost;
|
||||
BoostFeatures features;
|
||||
int lifting = 0;
|
||||
bool allowMulti = false;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
void BoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
BoostBoxData data,
|
||||
Fn<void(Fn<void(BoostCounters)>)> boost);
|
||||
|
||||
void BoostBoxAlready(not_null<GenericBox*> box, bool group);
|
||||
void GiftForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
QString channel,
|
||||
int receive,
|
||||
bool again);
|
||||
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group);
|
||||
void PremiumForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
bool group,
|
||||
Fn<void()> buyPremium);
|
||||
|
||||
struct AskBoostChannelColor {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostAutotranslate {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostWallpaper {
|
||||
int requiredLevel = 0;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
struct AskBoostEmojiStatus {
|
||||
int requiredLevel = 0;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
struct AskBoostEmojiPack {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostCustomReactions {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct AskBoostCpm {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostWearCollectible {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostReason {
|
||||
std::variant<
|
||||
AskBoostChannelColor,
|
||||
AskBoostAutotranslate,
|
||||
AskBoostWallpaper,
|
||||
AskBoostEmojiStatus,
|
||||
AskBoostEmojiPack,
|
||||
AskBoostCustomReactions,
|
||||
AskBoostCpm,
|
||||
AskBoostWearCollectible> data;
|
||||
};
|
||||
|
||||
struct AskBoostBoxData {
|
||||
QString link;
|
||||
BoostCounters boost;
|
||||
BoostFeatures features;
|
||||
AskBoostReason reason;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
void AskBoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
AskBoostBoxData data,
|
||||
Fn<void()> openStatistics,
|
||||
Fn<void()> startGiveaway);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> MakeLinkLabel(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<QString> link,
|
||||
std::shared_ptr<Show> show,
|
||||
object_ptr<RpWidget> right);
|
||||
|
||||
void FillBoostLimit(
|
||||
rpl::producer<> showFinished,
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<BoostCounters> data,
|
||||
style::margins limitLinePadding);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
Fn<QBrush(QRect)> bg);
|
||||
|
||||
} // namespace Ui
|
||||
1349
Telegram/SourceFiles/ui/boxes/calendar_box.cpp
Normal file
1349
Telegram/SourceFiles/ui/boxes/calendar_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
123
Telegram/SourceFiles/ui/boxes/calendar_box.h
Normal file
123
Telegram/SourceFiles/ui/boxes/calendar_box.h
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
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/layers/box_content.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "base/required.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace style {
|
||||
struct CalendarSizes;
|
||||
struct CalendarColors;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::CalendarSizes &defaultCalendarSizes;
|
||||
extern const style::CalendarColors &defaultCalendarColors;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class IconButton;
|
||||
class ScrollArea;
|
||||
class CalendarBox;
|
||||
|
||||
struct CalendarBoxArgs {
|
||||
template <typename T>
|
||||
using required = base::required<T>;
|
||||
|
||||
required<QDate> month;
|
||||
required<QDate> highlighted;
|
||||
required<Fn<void(QDate date)>> callback;
|
||||
FnMut<void(not_null<CalendarBox*>)> finalize;
|
||||
const style::CalendarSizes &st = st::defaultCalendarSizes;
|
||||
QDate minDate;
|
||||
QDate maxDate;
|
||||
bool allowsSelection = false;
|
||||
Fn<void(
|
||||
not_null<Ui::CalendarBox*>,
|
||||
std::optional<int>)> selectionChanged;
|
||||
const style::CalendarColors &stColors = st::defaultCalendarColors;
|
||||
};
|
||||
|
||||
class CalendarBox final : public BoxContent, private AbstractTooltipShower {
|
||||
public:
|
||||
CalendarBox(QWidget*, CalendarBoxArgs &&args);
|
||||
~CalendarBox();
|
||||
|
||||
void toggleSelectionMode(bool enabled);
|
||||
|
||||
[[nodiscard]] QDate selectedFirstDate() const;
|
||||
[[nodiscard]] QDate selectedLastDate() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void monthChanged(QDate month);
|
||||
|
||||
bool isPreviousEnabled() const;
|
||||
bool isNextEnabled() const;
|
||||
|
||||
void goPreviousMonth();
|
||||
void goNextMonth();
|
||||
void setExactScroll();
|
||||
void processScroll();
|
||||
void createButtons();
|
||||
|
||||
void showJumpTooltip(not_null<IconButton*> button);
|
||||
void jumpAfterDelay(not_null<IconButton*> button);
|
||||
void jump(QPointer<IconButton> button);
|
||||
|
||||
QString tooltipText() const override;
|
||||
QPoint tooltipPos() const override;
|
||||
bool tooltipWindowActive() const override;
|
||||
|
||||
const style::CalendarSizes &_st;
|
||||
const style::CalendarColors &_styleColors;
|
||||
|
||||
class Context;
|
||||
std::unique_ptr<Context> _context;
|
||||
|
||||
std::unique_ptr<ScrollArea> _scroll;
|
||||
|
||||
class Inner;
|
||||
not_null<Inner*> _inner;
|
||||
|
||||
class FloatingDate;
|
||||
std::unique_ptr<FloatingDate> _floatingDate;
|
||||
|
||||
class Title;
|
||||
object_ptr<Title> _title;
|
||||
object_ptr<IconButton> _previous;
|
||||
object_ptr<IconButton> _next;
|
||||
bool _previousEnabled = false;
|
||||
bool _nextEnabled = false;
|
||||
|
||||
Fn<void(QDate date)> _callback;
|
||||
FnMut<void(not_null<CalendarBox*>)> _finalize;
|
||||
bool _watchScroll = false;
|
||||
|
||||
QPointer<IconButton> _tooltipButton;
|
||||
QPointer<IconButton> _jumpButton;
|
||||
base::Timer _jumpTimer;
|
||||
|
||||
bool _selectionMode = false;
|
||||
Fn<void(
|
||||
not_null<Ui::CalendarBox*>,
|
||||
std::optional<int>)> _selectionChanged;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
341
Telegram/SourceFiles/ui/boxes/choose_date_time.cpp
Normal file
341
Telegram/SourceFiles/ui/boxes/choose_date_time.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
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 "ui/boxes/choose_date_time.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "ui/boxes/calendar_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/time_input.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinimalSchedule = TimeId(10);
|
||||
|
||||
QString DayString(const QDate &date) {
|
||||
return tr::lng_month_day(
|
||||
tr::now,
|
||||
lt_month,
|
||||
Lang::MonthDay(date.month())(tr::now),
|
||||
lt_day,
|
||||
QString::number(date.day()));
|
||||
}
|
||||
|
||||
QString TimeString(QTime time) {
|
||||
return QString("%1:%2"
|
||||
).arg(time.hour()
|
||||
).arg(time.minute(), 2, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChooseDateTimeStyleArgs::ChooseDateTimeStyleArgs()
|
||||
: labelStyle(&st::boxLabel)
|
||||
, dateFieldStyle(&st::scheduleDateField)
|
||||
, timeFieldStyle(&st::scheduleTimeField)
|
||||
, separatorStyle(&st::scheduleTimeSeparator)
|
||||
, atStyle(&st::scheduleAtLabel)
|
||||
, calendarStyle(&st::defaultCalendarColors) {
|
||||
}
|
||||
|
||||
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
|
||||
not_null<GenericBox*> box,
|
||||
ChooseDateTimeBoxArgs &&args) {
|
||||
struct State {
|
||||
rpl::variable<QDate> date;
|
||||
rpl::variable<int> width;
|
||||
not_null<InputField*> day;
|
||||
not_null<TimeInput*> time;
|
||||
not_null<FlatLabel*> at;
|
||||
};
|
||||
box->setTitle(std::move(args.title));
|
||||
box->setWidth(st::boxWideWidth);
|
||||
|
||||
const auto content = box->addRow(
|
||||
object_ptr<FixedHeightWidget>(box, st::scheduleHeight),
|
||||
style::al_top);
|
||||
if (args.description) {
|
||||
box->addRow(object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(args.description),
|
||||
*args.style.labelStyle));
|
||||
}
|
||||
const auto parsed = base::unixtime::parse(args.time);
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.date = parsed.date(),
|
||||
.day = CreateChild<InputField>(
|
||||
content,
|
||||
*args.style.dateFieldStyle),
|
||||
.time = CreateChild<TimeInput>(
|
||||
content,
|
||||
TimeString(parsed.time()),
|
||||
*args.style.timeFieldStyle,
|
||||
*args.style.dateFieldStyle,
|
||||
*args.style.separatorStyle,
|
||||
st::scheduleTimeSeparatorPadding),
|
||||
.at = CreateChild<FlatLabel>(
|
||||
content,
|
||||
tr::lng_schedule_at(),
|
||||
*args.style.atStyle),
|
||||
});
|
||||
|
||||
state->date.value(
|
||||
) | rpl::on_next([=](QDate date) {
|
||||
state->day->setText(DayString(date));
|
||||
state->time->setFocusFast();
|
||||
}, state->day->lifetime());
|
||||
|
||||
const auto min = args.min ? args.min : [] {
|
||||
return base::unixtime::now() + kMinimalSchedule;
|
||||
};
|
||||
const auto max = args.max ? args.max : [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime::currentDateTime().addYears(1)) - 1;
|
||||
};
|
||||
const auto minDate = [=] {
|
||||
return base::unixtime::parse(min()).date();
|
||||
};
|
||||
const auto maxDate = [=] {
|
||||
return base::unixtime::parse(max()).date();
|
||||
};
|
||||
|
||||
const auto &dayViewport = state->day->rawTextEdit()->viewport();
|
||||
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
|
||||
if (event->type() == QEvent::Wheel) {
|
||||
const auto e = static_cast<QWheelEvent*>(event.get());
|
||||
const auto direction = Ui::WheelDirection(e);
|
||||
if (!direction) {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
const auto d = state->date.current().addDays(direction);
|
||||
state->date = std::clamp(d, minDate(), maxDate());
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
state->at->widthValue() | rpl::on_next([=](int width) {
|
||||
const auto full = st::scheduleDateWidth
|
||||
+ st::scheduleAtSkip
|
||||
+ width
|
||||
+ st::scheduleAtSkip
|
||||
+ st::scheduleTimeWidth;
|
||||
content->setNaturalWidth(full);
|
||||
state->width = full;
|
||||
}, state->at->lifetime());
|
||||
|
||||
content->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto paddings = width
|
||||
- state->at->width()
|
||||
- 2 * st::scheduleAtSkip
|
||||
- st::scheduleDateWidth
|
||||
- st::scheduleTimeWidth;
|
||||
const auto left = paddings / 2;
|
||||
state->day->resizeToWidth(st::scheduleDateWidth);
|
||||
state->day->moveToLeft(left, st::scheduleDateTop, width);
|
||||
state->at->moveToLeft(
|
||||
left + st::scheduleDateWidth + st::scheduleAtSkip,
|
||||
st::scheduleAtTop,
|
||||
width);
|
||||
state->time->resizeToWidth(st::scheduleTimeWidth);
|
||||
state->time->moveToLeft(
|
||||
width - left - st::scheduleTimeWidth,
|
||||
st::scheduleDateTop,
|
||||
width);
|
||||
}, content->lifetime());
|
||||
|
||||
const auto calendar
|
||||
= content->lifetime().make_state<base::weak_qptr<CalendarBox>>();
|
||||
const auto calendarStyle = args.style.calendarStyle;
|
||||
state->day->focusedChanges(
|
||||
) | rpl::on_next([=](bool focused) {
|
||||
if (*calendar || !focused) {
|
||||
return;
|
||||
}
|
||||
*calendar = box->getDelegate()->show(
|
||||
Box<CalendarBox>(Ui::CalendarBoxArgs{
|
||||
.month = state->date.current(),
|
||||
.highlighted = state->date.current(),
|
||||
.callback = crl::guard(box, [=](QDate chosen) {
|
||||
state->date = chosen;
|
||||
(*calendar)->closeBox();
|
||||
}),
|
||||
.minDate = minDate(),
|
||||
.maxDate = maxDate(),
|
||||
.stColors = *calendarStyle,
|
||||
}));
|
||||
(*calendar)->boxClosing(
|
||||
) | rpl::on_next(crl::guard(state->time, [=] {
|
||||
state->time->setFocusFast();
|
||||
}), (*calendar)->lifetime());
|
||||
}, state->day->lifetime());
|
||||
|
||||
const auto collect = [=] {
|
||||
const auto timeValue = state->time->valueCurrent().split(':');
|
||||
if (timeValue.size() != 2) {
|
||||
return 0;
|
||||
}
|
||||
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
|
||||
if (!time.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
const auto result = base::unixtime::serialize(
|
||||
QDateTime(state->date.current(), time));
|
||||
if (result < min() || result > max()) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const auto save = [=, done = args.done] {
|
||||
if (const auto result = collect()) {
|
||||
done(result);
|
||||
} else {
|
||||
state->time->showError();
|
||||
}
|
||||
};
|
||||
state->time->submitRequests(
|
||||
) | rpl::on_next(save, state->time->lifetime());
|
||||
|
||||
auto result = ChooseDateTimeBoxDescriptor();
|
||||
box->setFocusCallback([=] { state->time->setFocusFast(); });
|
||||
result.width = state->width.value();
|
||||
result.submit = box->addButton(std::move(args.submit), save);
|
||||
result.collect = [=] {
|
||||
if (const auto result = collect()) {
|
||||
return result;
|
||||
}
|
||||
state->time->showError();
|
||||
return 0;
|
||||
};
|
||||
result.values = rpl::combine(
|
||||
state->date.value(),
|
||||
state->time->value()
|
||||
) | rpl::map(collect);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ChooseRepeatPeriodArgs &&args) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent.get());
|
||||
const auto raw = result.data();
|
||||
|
||||
struct Entry {
|
||||
TimeId value = 0;
|
||||
QString text;
|
||||
};
|
||||
auto map = std::vector<Entry>{
|
||||
{ 0, tr::lng_schedule_repeat_never(tr::now) },
|
||||
{ 24 * 60 * 60, tr::lng_schedule_repeat_daily(tr::now) },
|
||||
{ 7 * 24 * 60 * 60, tr::lng_schedule_repeat_weekly(tr::now) },
|
||||
{ 14 * 24 * 60 * 60, tr::lng_schedule_repeat_biweekly(tr::now) },
|
||||
{ 30 * 24 * 60 * 60, tr::lng_schedule_repeat_monthly(tr::now) },
|
||||
{
|
||||
91 * 24 * 60 * 60,
|
||||
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 3)
|
||||
},
|
||||
{
|
||||
182 * 24 * 60 * 60,
|
||||
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 6)
|
||||
},
|
||||
{ 365 * 24 * 60 * 60, tr::lng_schedule_repeat_yearly(tr::now) },
|
||||
};
|
||||
if (args.test) {
|
||||
map.insert(begin(map) + 1, Entry{ 300, u"Every 5 minutes"_q });
|
||||
map.insert(begin(map) + 1, Entry{ 60, u"Every minute"_q });
|
||||
}
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(raw, QString());
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
label->naturalWidthValue()
|
||||
) | rpl::on_next([=](int outer, int natural) {
|
||||
label->resizeToWidth(std::min(outer, natural));
|
||||
}, raw->lifetime());
|
||||
label->heightValue() | rpl::on_next([=](int height) {
|
||||
raw->resize(raw->width(), height);
|
||||
}, label->lifetime());
|
||||
|
||||
struct State {
|
||||
rpl::variable<TimeId> value;
|
||||
rpl::variable<bool> locked;
|
||||
std::unique_ptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>(State{
|
||||
.value = args.value,
|
||||
.locked = std::move(args.locked),
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
state->value.value(),
|
||||
state->locked.value()
|
||||
) | rpl::on_next([=](TimeId value, bool locked) {
|
||||
auto result = tr::lng_schedule_repeat_label(
|
||||
tr::now,
|
||||
tr::marked);
|
||||
|
||||
const auto text = [&] {
|
||||
const auto i = ranges::lower_bound(
|
||||
map,
|
||||
value,
|
||||
ranges::less{},
|
||||
&Entry::value);
|
||||
return (i != end(map)) ? i->text : map.back().text;
|
||||
}();
|
||||
|
||||
label->setMarkedText(result.append(' ').append(tr::link(
|
||||
tr::bold(text).append(
|
||||
Ui::Text::IconEmoji(locked
|
||||
? &st::scheduleRepeatDropdownLock
|
||||
: &st::scheduleRepeatDropdownArrow))
|
||||
)));
|
||||
return result;
|
||||
}, label->lifetime());
|
||||
|
||||
label->setClickHandlerFilter([=](const auto &...) {
|
||||
if (args.filter && args.filter()) {
|
||||
return false;
|
||||
}
|
||||
const auto changed = args.changed;
|
||||
|
||||
state->menu = std::make_unique<Ui::PopupMenu>(label);
|
||||
const auto menu = state->menu.get();
|
||||
|
||||
menu->setDestroyedCallback(crl::guard(label, [=] {
|
||||
if (state->menu.get() == menu) {
|
||||
state->menu.release();
|
||||
}
|
||||
}));
|
||||
for (const auto &entry : map) {
|
||||
const auto value = entry.value;
|
||||
menu->addAction(entry.text, [=] {
|
||||
state->value = value;
|
||||
changed(value);
|
||||
});
|
||||
}
|
||||
menu->popup(QCursor::pos());
|
||||
return false;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
66
Telegram/SourceFiles/ui/boxes/choose_date_time.h
Normal file
66
Telegram/SourceFiles/ui/boxes/choose_date_time.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
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/layers/generic_box.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
struct InputField;
|
||||
struct CalendarColors;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RoundButton;
|
||||
|
||||
struct ChooseDateTimeBoxDescriptor {
|
||||
QPointer<RoundButton> submit;
|
||||
Fn<TimeId()> collect;
|
||||
rpl::producer<TimeId> values;
|
||||
rpl::producer<int> width;
|
||||
};
|
||||
|
||||
struct ChooseDateTimeStyleArgs {
|
||||
ChooseDateTimeStyleArgs();
|
||||
const style::FlatLabel *labelStyle;
|
||||
const style::InputField *dateFieldStyle;
|
||||
const style::InputField *timeFieldStyle;
|
||||
const style::FlatLabel *separatorStyle;
|
||||
const style::FlatLabel *atStyle;
|
||||
const style::CalendarColors *calendarStyle;
|
||||
};
|
||||
|
||||
struct ChooseDateTimeBoxArgs {
|
||||
rpl::producer<QString> title;
|
||||
rpl::producer<QString> submit;
|
||||
Fn<void(TimeId)> done;
|
||||
Fn<TimeId()> min;
|
||||
TimeId time = 0;
|
||||
Fn<TimeId()> max;
|
||||
rpl::producer<QString> description;
|
||||
ChooseDateTimeStyleArgs style;
|
||||
};
|
||||
|
||||
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
|
||||
not_null<GenericBox*> box,
|
||||
ChooseDateTimeBoxArgs &&args);
|
||||
|
||||
struct ChooseRepeatPeriodArgs {
|
||||
TimeId value = 0;
|
||||
rpl::variable<bool> locked;
|
||||
Fn<bool()> filter;
|
||||
Fn<void(TimeId)> changed;
|
||||
bool test = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ChooseRepeatPeriodArgs &&args);
|
||||
|
||||
} // namespace Ui
|
||||
967
Telegram/SourceFiles/ui/boxes/choose_font_box.cpp
Normal file
967
Telegram/SourceFiles/ui/boxes/choose_font_box.cpp
Normal file
@@ -0,0 +1,967 @@
|
||||
/*
|
||||
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 "ui/boxes/choose_font_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/style/style_core_font.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtGui/QFontDatabase>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinTextWidth = 120;
|
||||
constexpr auto kMaxTextWidth = 320;
|
||||
constexpr auto kMaxTextLines = 3;
|
||||
|
||||
struct PreviewRequest {
|
||||
QString family;
|
||||
QColor msgBg;
|
||||
QColor msgShadow;
|
||||
QColor replyBar;
|
||||
QColor replyNameFg;
|
||||
QColor textFg;
|
||||
QImage bubbleTail;
|
||||
};
|
||||
|
||||
class PreviewPainter {
|
||||
public:
|
||||
PreviewPainter(const QImage &bg, PreviewRequest request);
|
||||
|
||||
QImage takeResult();
|
||||
|
||||
private:
|
||||
void layout();
|
||||
|
||||
void paintBubble(Painter &p);
|
||||
void paintContent(Painter &p);
|
||||
void paintReply(Painter &p);
|
||||
void paintMessage(Painter &p);
|
||||
|
||||
void validateBubbleCache();
|
||||
|
||||
const PreviewRequest _request;
|
||||
const style::owned_color _msgBg;
|
||||
const style::owned_color _msgShadow;
|
||||
|
||||
style::owned_font _nameFontOwned;
|
||||
style::font _nameFont;
|
||||
style::TextStyle _nameStyle;
|
||||
style::owned_font _textFontOwned;
|
||||
style::font _textFont;
|
||||
style::TextStyle _textStyle;
|
||||
|
||||
Ui::Text::String _nameText;
|
||||
Ui::Text::String _replyText;
|
||||
Ui::Text::String _messageText;
|
||||
|
||||
QRect _replyRect;
|
||||
QRect _name;
|
||||
QRect _reply;
|
||||
QRect _message;
|
||||
QRect _content;
|
||||
QRect _bubble;
|
||||
QSize _outer;
|
||||
|
||||
Ui::CornersPixmaps _bubbleCorners;
|
||||
QPixmap _bubbleShadowBottomRight;
|
||||
|
||||
QImage _result;
|
||||
|
||||
};
|
||||
|
||||
class Selector final : public Ui::RpWidget {
|
||||
public:
|
||||
Selector(
|
||||
not_null<QWidget*> parent,
|
||||
const QString &now,
|
||||
rpl::producer<QString> filter,
|
||||
rpl::producer<> submits,
|
||||
Fn<void(QString)> chosen,
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo);
|
||||
|
||||
void initScroll(anim::type animated);
|
||||
void setMinHeight(int height);
|
||||
void selectSkip(Qt::Key direction);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QString id;
|
||||
QString key;
|
||||
QString text;
|
||||
QStringList keywords;
|
||||
QImage cache;
|
||||
std::unique_ptr<Ui::RadioView> check;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
int paletteVersion = 0;
|
||||
};
|
||||
[[nodiscard]] static std::vector<Entry> FullList(const QString &now);
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
[[nodiscard]] bool searching() const;
|
||||
[[nodiscard]] int shownRowsCount() const;
|
||||
[[nodiscard]] Entry &shownRowAt(int index);
|
||||
|
||||
void applyFilter(const QString &query);
|
||||
void updateSelected(int selected);
|
||||
void updatePressed(int pressed);
|
||||
void updateRow(int index);
|
||||
void updateRow(not_null<Entry*> row, int hint);
|
||||
void addRipple(int index, QPoint position);
|
||||
void validateCache(Entry &row);
|
||||
void choose(Entry &row);
|
||||
|
||||
const style::SettingsButton &_st;
|
||||
std::vector<Entry> _rows;
|
||||
std::vector<not_null<Entry*>> _filtered;
|
||||
QString _chosen;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
|
||||
std::optional<QPoint> _lastGlobalPoint;
|
||||
bool _selectedByKeyboard = false;
|
||||
|
||||
Fn<void(QString)> _callback;
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;
|
||||
|
||||
int _rowsSkip = 0;
|
||||
int _rowHeight = 0;
|
||||
int _minHeight = 0;
|
||||
|
||||
QString _query;
|
||||
QStringList _queryWords;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
Selector::Selector(
|
||||
not_null<QWidget*> parent,
|
||||
const QString &now,
|
||||
rpl::producer<QString> filter,
|
||||
rpl::producer<> submits,
|
||||
Fn<void(QString)> chosen,
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo)
|
||||
: RpWidget(parent)
|
||||
, _st(st::settingsButton)
|
||||
, _rows(FullList(now))
|
||||
, _chosen(now)
|
||||
, _callback(std::move(chosen))
|
||||
, _scrollTo(std::move(scrollTo))
|
||||
, _rowsSkip(st::settingsInfoPhotoSkip)
|
||||
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
|
||||
setMouseTracking(true);
|
||||
|
||||
std::move(filter) | rpl::on_next([=](const QString &query) {
|
||||
applyFilter(query);
|
||||
}, _lifetime);
|
||||
|
||||
std::move(
|
||||
submits
|
||||
) | rpl::on_next([=] {
|
||||
if (_selected >= 0) {
|
||||
choose(shownRowAt(_selected));
|
||||
} else if (searching() && !_filtered.empty()) {
|
||||
choose(*_filtered.front());
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Selector::applyFilter(const QString &query) {
|
||||
if (_query == query) {
|
||||
return;
|
||||
}
|
||||
_query = query;
|
||||
|
||||
updateSelected(-1);
|
||||
updatePressed(-1);
|
||||
|
||||
_queryWords = TextUtilities::PrepareSearchWords(_query);
|
||||
|
||||
const auto skip = [](
|
||||
const QStringList &haystack,
|
||||
const QStringList &needles) {
|
||||
const auto find = [](
|
||||
const QStringList &haystack,
|
||||
const QString &needle) {
|
||||
for (const auto &item : haystack) {
|
||||
if (item.startsWith(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const auto &needle : needles) {
|
||||
if (!find(haystack, needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
_filtered.clear();
|
||||
if (!_queryWords.isEmpty()) {
|
||||
_filtered.reserve(_rows.size());
|
||||
for (auto &row : _rows) {
|
||||
if (!skip(row.keywords, _queryWords)) {
|
||||
_filtered.push_back(&row);
|
||||
} else {
|
||||
row.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resizeToWidth(width());
|
||||
Ui::SendPendingMoveResizeEvents(this);
|
||||
update();
|
||||
}
|
||||
|
||||
void Selector::updateSelected(int selected) {
|
||||
if (_selected == selected) {
|
||||
return;
|
||||
}
|
||||
const auto was = (_selected >= 0);
|
||||
updateRow(_selected);
|
||||
_selected = selected;
|
||||
updateRow(_selected);
|
||||
const auto now = (_selected >= 0);
|
||||
if (was != now) {
|
||||
setCursor(now ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
if (_selectedByKeyboard) {
|
||||
const auto top = (_selected > 0)
|
||||
? (_rowsSkip + _selected * _rowHeight)
|
||||
: 0;
|
||||
const auto bottom = (_selected > 0)
|
||||
? (top + _rowHeight)
|
||||
: _selected
|
||||
? 0
|
||||
: _rowHeight;
|
||||
_scrollTo({ top, bottom }, anim::type::instant);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::updatePressed(int pressed) {
|
||||
if (_pressed == pressed) {
|
||||
return;
|
||||
} else if (_pressed >= 0) {
|
||||
if (auto &ripple = shownRowAt(_pressed).ripple) {
|
||||
ripple->lastStop();
|
||||
}
|
||||
}
|
||||
updateRow(_pressed);
|
||||
_pressed = pressed;
|
||||
updateRow(_pressed);
|
||||
}
|
||||
|
||||
void Selector::updateRow(int index) {
|
||||
if (index >= 0) {
|
||||
update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::updateRow(not_null<Entry*> row, int hint) {
|
||||
if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {
|
||||
updateRow(hint);
|
||||
} else if (searching()) {
|
||||
const auto i = ranges::find(_filtered, row);
|
||||
if (i != end(_filtered)) {
|
||||
updateRow(int(i - begin(_filtered)));
|
||||
}
|
||||
} else {
|
||||
const auto index = int(row.get() - &_rows[0]);
|
||||
Assert(index >= 0 && index < _rows.size());
|
||||
updateRow(index);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::validateCache(Entry &row) {
|
||||
const auto version = style::PaletteVersion();
|
||||
if (row.cache.isNull()) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
row.cache = QImage(
|
||||
QSize(width(), _rowHeight) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
row.cache.setDevicePixelRatio(ratio);
|
||||
} else if (row.paletteVersion == version) {
|
||||
return;
|
||||
}
|
||||
row.paletteVersion = version;
|
||||
row.cache.fill(Qt::transparent);
|
||||
auto owned = style::owned_font(row.id, 0, st::boxFontSize);
|
||||
const auto font = owned.font();
|
||||
auto p = QPainter(&row.cache);
|
||||
p.setFont(font);
|
||||
p.setPen(st::windowFg);
|
||||
|
||||
const auto textw = width() - _st.padding.left() - _st.padding.right();
|
||||
const auto textt = (_rowHeight - font->height) / 2.;
|
||||
p.drawText(
|
||||
_st.padding.left(),
|
||||
textt + font->ascent,
|
||||
font->elided(row.text, textw));
|
||||
}
|
||||
|
||||
bool Selector::searching() const {
|
||||
return !_queryWords.isEmpty();
|
||||
}
|
||||
|
||||
int Selector::shownRowsCount() const {
|
||||
return searching() ? int(_filtered.size()) : int(_rows.size());
|
||||
}
|
||||
|
||||
Selector::Entry &Selector::shownRowAt(int index) {
|
||||
return searching() ? *_filtered[index] : _rows[index];
|
||||
}
|
||||
|
||||
void Selector::setMinHeight(int height) {
|
||||
_minHeight = height;
|
||||
if (_minHeight > 0) {
|
||||
resizeToWidth(width());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::selectSkip(Qt::Key key) {
|
||||
const auto count = shownRowsCount();
|
||||
if (key == Qt::Key_Down) {
|
||||
if (_selected + 1 < count) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(_selected + 1);
|
||||
}
|
||||
} else if (key == Qt::Key_Up) {
|
||||
if (_selected >= 0) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(_selected - 1);
|
||||
}
|
||||
} else if (key == Qt::Key_PageDown) {
|
||||
const auto change = _minHeight / _rowHeight;
|
||||
if (_selected + 1 < count) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(std::min(_selected + change, count - 1));
|
||||
}
|
||||
} else if (key == Qt::Key_PageUp) {
|
||||
const auto change = _minHeight / _rowHeight;
|
||||
if (_selected > 0) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(std::max(_selected - change, 0));
|
||||
} else if (!_selected) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::initScroll(anim::type animated) {
|
||||
const auto index = [&] {
|
||||
if (searching()) {
|
||||
const auto i = ranges::find(_filtered, _chosen, &Entry::id);
|
||||
if (i != end(_filtered)) {
|
||||
return int(i - begin(_filtered));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
const auto i = ranges::find(_rows, _chosen, &Entry::id);
|
||||
Assert(i != end(_rows));
|
||||
return int(i - begin(_rows));
|
||||
}();
|
||||
if (index >= 0) {
|
||||
const auto top = _rowsSkip + index * _rowHeight;
|
||||
const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);
|
||||
_scrollTo({ use, use + _minHeight }, animated);
|
||||
}
|
||||
}
|
||||
|
||||
int Selector::resizeGetHeight(int newWidth) {
|
||||
const auto added = 2 * _rowsSkip;
|
||||
return std::max(added + shownRowsCount() * _rowHeight, _minHeight);
|
||||
}
|
||||
|
||||
void Selector::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto rows = shownRowsCount();
|
||||
if (!rows) {
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::windowSubTextFg);
|
||||
p.drawText(
|
||||
QRect(0, 0, width(), height() * 2 / 3),
|
||||
tr::lng_font_not_found(tr::now),
|
||||
style::al_center);
|
||||
return;
|
||||
}
|
||||
const auto clip = e->rect();
|
||||
const auto clipped = std::max(clip.y() - _rowsSkip, 0);
|
||||
const auto from = std::min(clipped / _rowHeight, rows);
|
||||
const auto till = std::min(
|
||||
(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,
|
||||
rows);
|
||||
const auto active = (_pressed >= 0) ? _pressed : _selected;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
auto &row = shownRowAt(i);
|
||||
const auto y = _rowsSkip + i * _rowHeight;
|
||||
const auto bg = (i == active) ? st::windowBgOver : st::windowBg;
|
||||
const auto rect = QRect(0, y, width(), _rowHeight);
|
||||
p.fillRect(rect, bg);
|
||||
|
||||
if (row.ripple) {
|
||||
row.ripple->paint(p, 0, y, width());
|
||||
if (row.ripple->empty()) {
|
||||
row.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
validateCache(row);
|
||||
p.drawImage(0, y, row.cache);
|
||||
|
||||
if (!row.check) {
|
||||
row.check = std::make_unique<Ui::RadioView>(
|
||||
st::langsRadio,
|
||||
(row.id == _chosen),
|
||||
[=, row = &row] { updateRow(row, i); });
|
||||
}
|
||||
row.check->paint(
|
||||
p,
|
||||
_st.iconLeft,
|
||||
y + (_rowHeight - st::langsRadio.diameter) / 2,
|
||||
width());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::leaveEventHook(QEvent *e) {
|
||||
_lastGlobalPoint = std::nullopt;
|
||||
if (!_selectedByKeyboard) {
|
||||
updateSelected(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_lastGlobalPoint) {
|
||||
_lastGlobalPoint = e->globalPos();
|
||||
if (_selectedByKeyboard) {
|
||||
return;
|
||||
}
|
||||
} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
|
||||
return;
|
||||
} else {
|
||||
_lastGlobalPoint = e->globalPos();
|
||||
}
|
||||
_selectedByKeyboard = false;
|
||||
const auto y = e->y() - _rowsSkip;
|
||||
const auto index = (y >= 0) ? (y / _rowHeight) : -1;
|
||||
updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);
|
||||
}
|
||||
|
||||
void Selector::mousePressEvent(QMouseEvent *e) {
|
||||
updatePressed(_selected);
|
||||
if (_pressed >= 0) {
|
||||
addRipple(_pressed, e->pos());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto pressed = _pressed;
|
||||
updatePressed(-1);
|
||||
if (pressed >= 0 && pressed == _selected) {
|
||||
choose(shownRowAt(pressed));
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::choose(Entry &row) {
|
||||
const auto id = row.id;
|
||||
if (_chosen != id) {
|
||||
const auto i = ranges::find(_rows, _chosen, &Entry::id);
|
||||
Assert(i != end(_rows));
|
||||
if (i->check) {
|
||||
i->check->setChecked(false, anim::type::normal);
|
||||
}
|
||||
_chosen = id;
|
||||
if (row.check) {
|
||||
row.check->setChecked(true, anim::type::normal);
|
||||
}
|
||||
}
|
||||
const auto animated = searching()
|
||||
? anim::type::instant
|
||||
: anim::type::normal;
|
||||
_callback(id);
|
||||
initScroll(animated);
|
||||
}
|
||||
|
||||
void Selector::addRipple(int index, QPoint position) {
|
||||
Expects(index >= 0 && index < shownRowsCount());
|
||||
|
||||
const auto row = &shownRowAt(index);
|
||||
if (!row->ripple) {
|
||||
row->ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
|
||||
[=] { updateRow(row, index); });
|
||||
}
|
||||
row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
|
||||
}
|
||||
|
||||
std::vector<Selector::Entry> Selector::FullList(const QString &now) {
|
||||
using namespace TextUtilities;
|
||||
|
||||
auto database = QFontDatabase();
|
||||
auto families = database.families();
|
||||
auto result = std::vector<Entry>();
|
||||
result.reserve(families.size() + 3);
|
||||
const auto add = [&](const QString &text, const QString &id = {}) {
|
||||
result.push_back({
|
||||
.id = id,
|
||||
.text = text,
|
||||
.keywords = PrepareSearchWords(text),
|
||||
});
|
||||
};
|
||||
add(tr::lng_font_default(tr::now));
|
||||
add(tr::lng_font_system(tr::now), style::SystemFontTag());
|
||||
for (const auto &family : families) {
|
||||
if (database.isScalable(family)) {
|
||||
result.push_back({ .id = family });
|
||||
}
|
||||
}
|
||||
auto nowIt = ranges::find(result, now, &Entry::id);
|
||||
if (nowIt == end(result)) {
|
||||
result.push_back({ .id = now });
|
||||
nowIt = end(result) - 1;
|
||||
}
|
||||
for (auto i = begin(result) + 2; i != end(result); ++i) {
|
||||
i->key = TextUtilities::RemoveAccents(i->id).toLower();
|
||||
i->text = i->id;
|
||||
i->keywords = TextUtilities::PrepareSearchWords(i->id);
|
||||
}
|
||||
auto skip = 2;
|
||||
if (nowIt - begin(result) >= skip) {
|
||||
std::swap(result[2], *nowIt);
|
||||
++skip;
|
||||
}
|
||||
ranges::sort(
|
||||
begin(result) + skip, end(result),
|
||||
std::less<>(),
|
||||
&Entry::key);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {
|
||||
return {
|
||||
.family = family,
|
||||
.msgBg = st::msgInBg->c,
|
||||
.msgShadow = st::msgInShadow->c,
|
||||
.replyBar = st::msgInReplyBarColor->c,
|
||||
.replyNameFg = st::msgInServiceFg->c,
|
||||
.textFg = st::historyTextInFg->c,
|
||||
.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),
|
||||
};
|
||||
}
|
||||
|
||||
PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)
|
||||
: _request(request)
|
||||
, _msgBg(_request.msgBg)
|
||||
, _msgShadow(_request.msgShadow)
|
||||
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
|
||||
, _nameFont(_nameFontOwned.font())
|
||||
, _nameStyle(st::semiboldTextStyle)
|
||||
, _textFontOwned(_request.family, 0, st::fsize)
|
||||
, _textFont(_textFontOwned.font())
|
||||
, _textStyle(st::defaultTextStyle) {
|
||||
_nameStyle.font = _nameFont;
|
||||
_textStyle.font = _textFont;
|
||||
|
||||
layout();
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
_result = QImage(
|
||||
_outer * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_result.setDevicePixelRatio(ratio);
|
||||
|
||||
auto p = Painter(&_result);
|
||||
p.drawImage(0, 0, bg);
|
||||
|
||||
p.translate(_bubble.topLeft());
|
||||
paintBubble(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::paintBubble(Painter &p) {
|
||||
validateBubbleCache();
|
||||
const auto bubble = QRect(QPoint(), _bubble.size());
|
||||
const auto cornerShadow = _bubbleShadowBottomRight.size()
|
||||
/ _bubbleShadowBottomRight.devicePixelRatio();
|
||||
p.drawPixmap(
|
||||
bubble.width() - cornerShadow.width(),
|
||||
bubble.height() + st::msgShadow - cornerShadow.height(),
|
||||
_bubbleShadowBottomRight);
|
||||
Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);
|
||||
const auto &bubbleTail = _request.bubbleTail;
|
||||
const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();
|
||||
p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);
|
||||
p.fillRect(
|
||||
-tail.width(),
|
||||
bubble.height(),
|
||||
tail.width() + bubble.width() - cornerShadow.width(),
|
||||
st::msgShadow,
|
||||
_request.msgShadow);
|
||||
p.translate(_content.topLeft());
|
||||
const auto local = _content.translated(-_content.topLeft());
|
||||
p.setClipRect(local);
|
||||
paintContent(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::validateBubbleCache() {
|
||||
if (!_bubbleCorners.p[0].isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto radius = st::bubbleRadiusLarge;
|
||||
_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());
|
||||
_bubbleCorners.p[2] = {};
|
||||
_bubbleShadowBottomRight
|
||||
= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];
|
||||
}
|
||||
|
||||
void PreviewPainter::paintContent(Painter &p) {
|
||||
paintReply(p);
|
||||
|
||||
p.translate(_message.topLeft());
|
||||
const auto local = _message.translated(-_message.topLeft());
|
||||
p.setClipRect(local);
|
||||
paintMessage(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::paintReply(Painter &p) {
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_request.replyBar);
|
||||
|
||||
const auto outline = st::messageTextStyle.blockquote.outline;
|
||||
const auto radius = st::messageTextStyle.blockquote.radius;
|
||||
p.setOpacity(Ui::kDefaultOutline1Opacity);
|
||||
p.setClipRect(
|
||||
_replyRect.x(),
|
||||
_replyRect.y(),
|
||||
outline,
|
||||
_replyRect.height());
|
||||
p.drawRoundedRect(_replyRect, radius, radius);
|
||||
p.setOpacity(Ui::kDefaultBgOpacity);
|
||||
p.setClipRect(
|
||||
_replyRect.x() + outline,
|
||||
_replyRect.y(),
|
||||
_replyRect.width() - outline,
|
||||
_replyRect.height());
|
||||
p.drawRoundedRect(_replyRect, radius, radius);
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
p.setClipping(false);
|
||||
|
||||
p.setPen(_request.replyNameFg);
|
||||
_nameText.drawLeftElided(
|
||||
p,
|
||||
_name.x(),
|
||||
_name.y(),
|
||||
_name.width(),
|
||||
_outer.width());
|
||||
|
||||
p.setPen(_request.textFg);
|
||||
_replyText.drawLeftElided(
|
||||
p,
|
||||
_reply.x(),
|
||||
_reply.y(),
|
||||
_reply.width(),
|
||||
_outer.width());
|
||||
}
|
||||
|
||||
void PreviewPainter::paintMessage(Painter &p) {
|
||||
p.setPen(_request.textFg);
|
||||
_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
|
||||
}
|
||||
|
||||
QImage PreviewPainter::takeResult() {
|
||||
return std::move(_result);
|
||||
}
|
||||
|
||||
void PreviewPainter::layout() {
|
||||
const auto skip = st::boxRowPadding.left();
|
||||
const auto minTextWidth = style::ConvertScale(kMinTextWidth);
|
||||
const auto maxTextWidth = st::boxWidth
|
||||
- 2 * skip
|
||||
- st::msgPadding.left()
|
||||
- st::msgPadding.right();
|
||||
|
||||
_nameText = Ui::Text::String(
|
||||
_nameStyle,
|
||||
tr::lng_settings_chat_message_reply_from(tr::now));
|
||||
_replyText = Ui::Text::String(
|
||||
_textStyle,
|
||||
tr::lng_background_text2(tr::now));
|
||||
_messageText = Ui::Text::String(
|
||||
_textStyle,
|
||||
tr::lng_background_text1(tr::now),
|
||||
kDefaultTextOptions,
|
||||
st::msgMinWidth / 2);
|
||||
|
||||
const auto namePosition = QPoint(
|
||||
st::historyReplyPadding.left(),
|
||||
st::historyReplyPadding.top());
|
||||
const auto replyPosition = QPoint(
|
||||
st::historyReplyPadding.left(),
|
||||
(st::historyReplyPadding.top() + _nameFont->height));
|
||||
const auto paddingRight = st::historyReplyPadding.right();
|
||||
|
||||
const auto wantedWidth = std::max({
|
||||
namePosition.x() + _nameText.maxWidth() + paddingRight,
|
||||
replyPosition.x() + _replyText.maxWidth() + paddingRight,
|
||||
_messageText.maxWidth()
|
||||
});
|
||||
|
||||
const auto messageWidth = std::clamp(
|
||||
wantedWidth,
|
||||
minTextWidth,
|
||||
maxTextWidth);
|
||||
const auto messageHeight = _messageText.countHeight(messageWidth);
|
||||
|
||||
_replyRect = QRect(
|
||||
st::msgReplyBarPos.x(),
|
||||
st::historyReplyTop,
|
||||
messageWidth,
|
||||
(st::historyReplyPadding.top()
|
||||
+ _nameFont->height
|
||||
+ _textFont->height
|
||||
+ st::historyReplyPadding.bottom()));
|
||||
|
||||
_name = QRect(
|
||||
_replyRect.topLeft() + namePosition,
|
||||
QSize(messageWidth - namePosition.x(), _nameFont->height));
|
||||
_reply = QRect(
|
||||
_replyRect.topLeft() + replyPosition,
|
||||
QSize(messageWidth - replyPosition.x(), _textFont->height));
|
||||
_message = QRect(0, 0, messageWidth, messageHeight);
|
||||
|
||||
const auto replySkip = _replyRect.y()
|
||||
+ _replyRect.height()
|
||||
+ st::historyReplyBottom;
|
||||
_message.moveTop(replySkip);
|
||||
|
||||
_content = QRect(0, 0, messageWidth, replySkip + messageHeight);
|
||||
|
||||
const auto msgPadding = st::msgPadding;
|
||||
_bubble = _content.marginsAdded(msgPadding);
|
||||
_content.moveTopLeft(-_bubble.topLeft());
|
||||
_bubble.moveTopLeft({});
|
||||
|
||||
_outer = QSize(st::boxWidth, st::boxWidth / 2);
|
||||
|
||||
_bubble.moveTopLeft({ skip, std::max(
|
||||
(_outer.height() - _bubble.height()) / 2,
|
||||
st::msgMargin.top()) });
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage GeneratePreview(
|
||||
const QImage &bg,
|
||||
PreviewRequest request) {
|
||||
return PreviewPainter(bg, request).takeResult();
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(
|
||||
not_null<QWidget*> parent,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
rpl::producer<QString> family) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent.get());
|
||||
const auto raw = result.data();
|
||||
|
||||
struct State {
|
||||
QImage preview;
|
||||
QImage bg;
|
||||
QString family;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
|
||||
state->bg = generatePreviewBg();
|
||||
style::PaletteChanged() | rpl::on_next([=] {
|
||||
state->bg = generatePreviewBg();
|
||||
}, raw->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),
|
||||
std::move(family)
|
||||
) | rpl::on_next([=](const auto &, QString family) {
|
||||
state->family = family;
|
||||
if (state->preview.isNull()) {
|
||||
state->preview = GeneratePreview(
|
||||
state->bg,
|
||||
PrepareRequest(family));
|
||||
const auto ratio = state->preview.devicePixelRatio();
|
||||
raw->resize(state->preview.size() / int(ratio));
|
||||
} else {
|
||||
const auto weak = base::make_weak(raw);
|
||||
const auto request = PrepareRequest(family);
|
||||
crl::async([=, bg = state->bg] {
|
||||
crl::on_main([
|
||||
weak,
|
||||
state,
|
||||
preview = GeneratePreview(bg, request)
|
||||
]() mutable {
|
||||
if (const auto strong = weak.get()) {
|
||||
state->preview = std::move(preview);
|
||||
const auto ratio = state->preview.devicePixelRatio();
|
||||
strong->resize(
|
||||
strong->width(),
|
||||
(state->preview.height() / int(ratio)));
|
||||
strong->update();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(raw).drawImage(0, 0, state->preview);
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseFontBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
const QString &family,
|
||||
Fn<void(QString)> save) {
|
||||
box->setTitle(tr::lng_font_box_title());
|
||||
|
||||
struct State {
|
||||
rpl::variable<QString> family;
|
||||
rpl::variable<QString> query;
|
||||
rpl::event_stream<> submits;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.family = family,
|
||||
});
|
||||
|
||||
const auto top = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
top->add(MakePreview(top, generatePreviewBg, state->family.value()));
|
||||
const auto filter = top->add(object_ptr<Ui::MultiSelect>(
|
||||
top,
|
||||
st::defaultMultiSelect,
|
||||
tr::lng_participant_filter()));
|
||||
top->resizeToWidth(st::boxWidth);
|
||||
|
||||
filter->setSubmittedCallback([=](Qt::KeyboardModifiers) {
|
||||
state->submits.fire({});
|
||||
});
|
||||
filter->setQueryChangedCallback([=](const QString &query) {
|
||||
state->query = query;
|
||||
});
|
||||
filter->setCancelledCallback([=] {
|
||||
filter->clearQuery();
|
||||
});
|
||||
|
||||
const auto chosen = [=](const QString &value) {
|
||||
state->family = value;
|
||||
filter->clearQuery();
|
||||
};
|
||||
const auto scrollTo = [=](
|
||||
Ui::ScrollToRequest request,
|
||||
anim::type animated) {
|
||||
box->scrollTo(request, animated);
|
||||
};
|
||||
const auto selector = box->addRow(
|
||||
object_ptr<Selector>(
|
||||
box,
|
||||
state->family.current(),
|
||||
state->query.value(),
|
||||
state->submits.events(),
|
||||
chosen,
|
||||
scrollTo),
|
||||
QMargins());
|
||||
box->setMinHeight(st::boxMaxListHeight);
|
||||
box->setMaxHeight(st::boxMaxListHeight);
|
||||
|
||||
base::install_event_filter(filter, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
const auto key = static_cast<QKeyEvent*>(e.get())->key();
|
||||
if (key == Qt::Key_Up
|
||||
|| key == Qt::Key_Down
|
||||
|| key == Qt::Key_PageUp
|
||||
|| key == Qt::Key_PageDown) {
|
||||
selector->selectSkip(Qt::Key(key));
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
box->heightValue(),
|
||||
top->heightValue()
|
||||
) | rpl::on_next([=](int box, int top) {
|
||||
selector->setMinHeight(box - top);
|
||||
}, selector->lifetime());
|
||||
|
||||
const auto apply = [=](QString chosen) {
|
||||
if (chosen == family) {
|
||||
box->closeBox();
|
||||
return;
|
||||
}
|
||||
box->getDelegate()->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_settings_need_restart(),
|
||||
.confirmed = [=] { save(chosen); },
|
||||
.confirmText = tr::lng_settings_restart_now(),
|
||||
}));
|
||||
};
|
||||
const auto refreshButtons = [=](QString chosen) {
|
||||
box->clearButtons();
|
||||
// Doesn't fit in most languages.
|
||||
//if (!chosen.isEmpty()) {
|
||||
// box->addLeftButton(tr::lng_background_reset_default(), [=] {
|
||||
// apply(QString());
|
||||
// });
|
||||
//}
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
apply(chosen);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
};
|
||||
state->family.value(
|
||||
) | rpl::on_next(refreshButtons, box->lifetime());
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
filter->setInnerFocus();
|
||||
});
|
||||
box->setInitScrollCallback([=] {
|
||||
SendPendingMoveResizeEvents(box);
|
||||
selector->initScroll(anim::type::instant);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
20
Telegram/SourceFiles/ui/boxes/choose_font_box.h
Normal file
20
Telegram/SourceFiles/ui/boxes/choose_font_box.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
void ChooseFontBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
const QString &family,
|
||||
Fn<void(QString)> save);
|
||||
|
||||
} // namespace Ui
|
||||
382
Telegram/SourceFiles/ui/boxes/choose_language_box.cpp
Normal file
382
Telegram/SourceFiles/ui/boxes/choose_language_box.cpp
Normal file
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
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 "ui/boxes/choose_language_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "spellcheck/spellcheck_types.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/debug_log.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
const auto kLanguageNamePrefix = "cloud_lng_language_";
|
||||
const auto kTranslateToPrefix = "cloud_lng_translate_to_";
|
||||
|
||||
[[nodiscard]] std::vector<LanguageId> TranslationLanguagesList() {
|
||||
// If adding some languages here you need to check that it is
|
||||
// supported on the server. Right now server supports those:
|
||||
//
|
||||
// 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',
|
||||
// 'ca', 'ceb', 'zh-CN', 'zh', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl',
|
||||
// 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu',
|
||||
// 'ht', 'ha', 'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'id',
|
||||
// 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'ko', 'ku', 'ky',
|
||||
// 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',
|
||||
// 'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa', 'pl', 'pt',
|
||||
// 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk',
|
||||
// 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te',
|
||||
// 'th', 'tr', 'tk', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi',
|
||||
// 'yo', 'zu',
|
||||
return {
|
||||
{ QLocale::English },
|
||||
{ QLocale::Arabic },
|
||||
{ QLocale::Belarusian },
|
||||
{ QLocale::Catalan },
|
||||
{ QLocale::Chinese },
|
||||
{ QLocale::Dutch },
|
||||
{ QLocale::French },
|
||||
{ QLocale::German },
|
||||
{ QLocale::Indonesian },
|
||||
{ QLocale::Italian },
|
||||
{ QLocale::Japanese },
|
||||
{ QLocale::Korean },
|
||||
{ QLocale::Polish },
|
||||
{ QLocale::Portuguese },
|
||||
{ QLocale::Russian },
|
||||
{ QLocale::Spanish },
|
||||
{ QLocale::Ukrainian },
|
||||
|
||||
{ QLocale::Afrikaans },
|
||||
{ QLocale::Albanian },
|
||||
{ QLocale::Amharic },
|
||||
{ QLocale::Armenian },
|
||||
{ QLocale::Azerbaijani },
|
||||
{ QLocale::Basque },
|
||||
{ QLocale::Bosnian },
|
||||
{ QLocale::Bulgarian },
|
||||
{ QLocale::Burmese },
|
||||
{ QLocale::Croatian },
|
||||
{ QLocale::Czech },
|
||||
{ QLocale::Danish },
|
||||
{ QLocale::Esperanto },
|
||||
{ QLocale::Estonian },
|
||||
{ QLocale::Finnish },
|
||||
{ QLocale::Gaelic },
|
||||
{ QLocale::Galician },
|
||||
{ QLocale::Georgian },
|
||||
{ QLocale::Greek },
|
||||
{ QLocale::Gusii },
|
||||
{ QLocale::Hausa },
|
||||
{ QLocale::Hebrew },
|
||||
{ QLocale::Hungarian },
|
||||
{ QLocale::Icelandic },
|
||||
{ QLocale::Igbo },
|
||||
{ QLocale::Irish },
|
||||
{ QLocale::Kazakh },
|
||||
{ QLocale::Kinyarwanda },
|
||||
{ QLocale::Kurdish },
|
||||
{ QLocale::Lao },
|
||||
{ QLocale::Latvian },
|
||||
{ QLocale::Lithuanian },
|
||||
{ QLocale::Luxembourgish },
|
||||
{ QLocale::Macedonian },
|
||||
{ QLocale::Malagasy },
|
||||
{ QLocale::Malay },
|
||||
{ QLocale::Maltese },
|
||||
{ QLocale::Maori },
|
||||
{ QLocale::Mongolian },
|
||||
{ QLocale::Nepali },
|
||||
{ QLocale::Pashto },
|
||||
{ QLocale::Persian },
|
||||
{ QLocale::Romanian },
|
||||
{ QLocale::Serbian },
|
||||
{ QLocale::Shona },
|
||||
{ QLocale::Sindhi },
|
||||
{ QLocale::Sinhala },
|
||||
{ QLocale::Slovak },
|
||||
{ QLocale::Slovenian },
|
||||
{ QLocale::Somali },
|
||||
{ QLocale::Sundanese },
|
||||
{ QLocale::Swahili },
|
||||
{ QLocale::Swedish },
|
||||
{ QLocale::Tajik },
|
||||
{ QLocale::Tatar },
|
||||
{ QLocale::Teso },
|
||||
{ QLocale::Thai },
|
||||
{ QLocale::Turkish },
|
||||
{ QLocale::Turkmen },
|
||||
{ QLocale::Urdu },
|
||||
{ QLocale::Uzbek },
|
||||
{ QLocale::Vietnamese },
|
||||
{ QLocale::Welsh },
|
||||
{ QLocale::WesternFrisian },
|
||||
{ QLocale::Xhosa },
|
||||
{ QLocale::Yiddish },
|
||||
};
|
||||
}
|
||||
|
||||
class Row final : public SettingsButton {
|
||||
public:
|
||||
Row(not_null<RpWidget*> parent, LanguageId id);
|
||||
|
||||
[[nodiscard]] bool filtered(const QString &query) const;
|
||||
[[nodiscard]] LanguageId id() const;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::PeerListItem &_st;
|
||||
const LanguageId _id;
|
||||
const QString _status;
|
||||
const QString _titleText;
|
||||
Text::String _title;
|
||||
|
||||
};
|
||||
|
||||
Row::Row(not_null<RpWidget*> parent, LanguageId id)
|
||||
: SettingsButton(parent, rpl::never<QString>())
|
||||
, _st(st::inviteLinkListItem)
|
||||
, _id(id)
|
||||
, _status(LanguageName(id))
|
||||
, _titleText(LanguageNameNative(id))
|
||||
, _title(_st.nameStyle, _titleText) {
|
||||
}
|
||||
|
||||
LanguageId Row::id() const {
|
||||
return _id;
|
||||
}
|
||||
|
||||
bool Row::filtered(const QString &query) const {
|
||||
return _status.startsWith(query, Qt::CaseInsensitive)
|
||||
|| _titleText.startsWith(query, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
int Row::resizeGetHeight(int newWidth) {
|
||||
return _st.height;
|
||||
}
|
||||
|
||||
void Row::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
|
||||
const auto paintOver = (isOver() || isDown()) && !isDisabled();
|
||||
SettingsButton::paintBg(p, e->rect(), paintOver);
|
||||
SettingsButton::paintRipple(p, 0, 0);
|
||||
SettingsButton::paintToggle(p, width());
|
||||
|
||||
const auto &color = st::windowSubTextFg;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(color);
|
||||
|
||||
const auto left = st::defaultSubsectionTitlePadding.left();
|
||||
const auto toggleRect = SettingsButton::maybeToggleRect();
|
||||
const auto right = left
|
||||
+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));
|
||||
|
||||
const auto availableWidth = std::min(
|
||||
_title.maxWidth(),
|
||||
width() - left - right);
|
||||
p.setPen(_st.nameFg);
|
||||
_title.drawLeft(
|
||||
p,
|
||||
left,
|
||||
_st.namePosition.y(),
|
||||
availableWidth,
|
||||
width() - left - right);
|
||||
|
||||
p.setPen(paintOver ? _st.statusFgOver : _st.statusFg);
|
||||
p.setFont(st::contactsStatusFont);
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
_st.statusPosition.y(),
|
||||
width() - left - right,
|
||||
_status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString LanguageNameTranslated(const QString &twoLetterCode) {
|
||||
return Lang::GetNonDefaultValue(
|
||||
kLanguageNamePrefix + twoLetterCode.toUtf8());
|
||||
}
|
||||
|
||||
QString LanguageNameLocal(LanguageId id) {
|
||||
return QLocale::languageToString(id.language());
|
||||
}
|
||||
|
||||
QString LanguageName(LanguageId id) {
|
||||
const auto translated = LanguageNameTranslated(id.twoLetterCode());
|
||||
return translated.isEmpty() ? LanguageNameLocal(id) : translated;
|
||||
}
|
||||
|
||||
QString LanguageNameNative(LanguageId id) {
|
||||
const auto locale = id.locale();
|
||||
if (locale.language() == QLocale::English
|
||||
&& (locale.country() == QLocale::UnitedStates
|
||||
|| locale.country() == QLocale::AnyCountry)) {
|
||||
return u"English"_q;
|
||||
} else if (locale.language() == QLocale::Spanish) {
|
||||
return QString::fromUtf8("\x45\x73\x70\x61\xc3\xb1\x6f\x6c");
|
||||
} else {
|
||||
const auto name = locale.nativeLanguageName();
|
||||
return name.left(1).toUpper() + name.mid(1);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<QString> TranslateBarTo(LanguageId id) {
|
||||
const auto translated = Lang::GetNonDefaultValue(
|
||||
kTranslateToPrefix + id.twoLetterCode().toUtf8());
|
||||
return (translated.isEmpty()
|
||||
? tr::lng_translate_bar_to_other
|
||||
: tr::lng_translate_bar_to)(
|
||||
lt_name,
|
||||
rpl::single(translated.isEmpty()
|
||||
? LanguageNameLocal(id)
|
||||
: translated));
|
||||
}
|
||||
|
||||
QString TranslateMenuDont(tr::now_t, LanguageId id) {
|
||||
const auto translated = Lang::GetNonDefaultValue(
|
||||
kTranslateToPrefix + id.twoLetterCode().toUtf8());
|
||||
return (translated.isEmpty()
|
||||
? tr::lng_translate_menu_dont_other
|
||||
: tr::lng_translate_menu_dont)(
|
||||
tr::now,
|
||||
lt_name,
|
||||
translated.isEmpty() ? LanguageNameLocal(id) : translated);
|
||||
}
|
||||
|
||||
void ChooseLanguageBox(
|
||||
not_null<GenericBox*> box,
|
||||
rpl::producer<QString> title,
|
||||
Fn<void(std::vector<LanguageId>)> callback,
|
||||
std::vector<LanguageId> selected,
|
||||
bool multiselect,
|
||||
Fn<bool(LanguageId)> toggleCheck) {
|
||||
box->setMinHeight(st::boxWidth);
|
||||
box->setMaxHeight(st::boxWidth);
|
||||
box->setTitle(std::move(title));
|
||||
|
||||
const auto multiSelect = box->setPinnedToTopContent(
|
||||
object_ptr<MultiSelect>(
|
||||
box,
|
||||
st::defaultMultiSelect,
|
||||
tr::lng_participant_filter()));
|
||||
box->setFocusCallback([=] { multiSelect->setInnerFocus(); });
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto langs = [&] {
|
||||
auto list = TranslationLanguagesList();
|
||||
for (const auto id : list) {
|
||||
LOG(("cloud_lng_language_%1").arg(id.twoLetterCode()));
|
||||
}
|
||||
const auto current = LanguageId{ QLocale(
|
||||
Lang::LanguageIdOrDefault(Lang::Id())).language() };
|
||||
if (const auto i = ranges::find(list, current); i != end(list)) {
|
||||
base::reorder(list, std::distance(begin(list), i), 0);
|
||||
}
|
||||
ranges::stable_partition(list, [&](LanguageId id) {
|
||||
return ranges::contains(selected, id);
|
||||
});
|
||||
return list;
|
||||
}();
|
||||
struct ToggleOne {
|
||||
LanguageId id;
|
||||
bool selected = false;
|
||||
};
|
||||
struct State {
|
||||
rpl::event_stream<ToggleOne> toggles;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
auto rows = std::vector<not_null<SlideWrap<Row>*>>();
|
||||
rows.reserve(langs.size());
|
||||
for (const auto &id : langs) {
|
||||
const auto button = container->add(
|
||||
object_ptr<SlideWrap<Row>>(
|
||||
container,
|
||||
object_ptr<Row>(container, id)));
|
||||
if (multiselect) {
|
||||
button->entity()->toggleOn(rpl::single(
|
||||
ranges::contains(selected, id)
|
||||
) | rpl::then(state->toggles.events(
|
||||
) | rpl::filter([=](ToggleOne one) {
|
||||
return one.id == id;
|
||||
}) | rpl::map([=](ToggleOne one) {
|
||||
return one.selected;
|
||||
})));
|
||||
|
||||
button->entity()->toggledChanges(
|
||||
) | rpl::on_next([=](bool value) {
|
||||
if (toggleCheck && !toggleCheck(id)) {
|
||||
state->toggles.fire({ .id = id, .selected = !value });
|
||||
}
|
||||
}, button->lifetime());
|
||||
} else {
|
||||
button->entity()->setClickedCallback([=] {
|
||||
callback({ id });
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
rows.push_back(button);
|
||||
}
|
||||
|
||||
multiSelect->setQueryChangedCallback([=](const QString &query) {
|
||||
for (const auto &row : rows) {
|
||||
const auto toggled = row->entity()->filtered(query);
|
||||
if (toggled != row->toggled()) {
|
||||
row->toggle(toggled, anim::type::instant);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_languages_none(),
|
||||
st::membersAbout);
|
||||
box->verticalLayout()->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &geometry) {
|
||||
const auto shown = (geometry.height() <= 0);
|
||||
label->setVisible(shown);
|
||||
if (shown) {
|
||||
label->moveToLeft(
|
||||
(geometry.width() - label->width()) / 2,
|
||||
geometry.y() + st::membersAbout.style.font->height * 4);
|
||||
label->stackUnder(box->verticalLayout());
|
||||
}
|
||||
}, label->lifetime());
|
||||
}
|
||||
|
||||
if (multiselect) {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
auto result = ranges::views::all(
|
||||
rows
|
||||
) | ranges::views::filter([](const auto &row) {
|
||||
return row->entity()->toggled();
|
||||
}) | ranges::views::transform([](const auto &row) {
|
||||
return row->entity()->id();
|
||||
}) | ranges::to_vector;
|
||||
if (!result.empty()) {
|
||||
callback(std::move(result));
|
||||
}
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
36
Telegram/SourceFiles/ui/boxes/choose_language_box.h
Normal file
36
Telegram/SourceFiles/ui/boxes/choose_language_box.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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
|
||||
|
||||
struct LanguageId;
|
||||
|
||||
namespace tr {
|
||||
struct now_t;
|
||||
} // namespace tr
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] QString LanguageNameTranslated(const QString &twoLetterCode);
|
||||
[[nodiscard]] QString LanguageNameLocal(LanguageId id);
|
||||
[[nodiscard]] QString LanguageName(LanguageId id);
|
||||
[[nodiscard]] QString LanguageNameNative(LanguageId id);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> TranslateBarTo(LanguageId id);
|
||||
[[nodiscard]] QString TranslateMenuDont(tr::now_t, LanguageId id);
|
||||
|
||||
void ChooseLanguageBox(
|
||||
not_null<GenericBox*> box,
|
||||
rpl::producer<QString> title,
|
||||
Fn<void(std::vector<LanguageId>)> callback,
|
||||
std::vector<LanguageId> selected,
|
||||
bool multiselect,
|
||||
Fn<bool(LanguageId)> toggleCheck);
|
||||
|
||||
} // namespace Ui
|
||||
145
Telegram/SourceFiles/ui/boxes/choose_time.cpp
Normal file
145
Telegram/SourceFiles/ui/boxes/choose_time.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
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 "ui/boxes/choose_time.h"
|
||||
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/fields/time_part_input_with_placeholder.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ChooseTimeResult ChooseTimeWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
TimeId startSeconds,
|
||||
bool hiddenDaysInput) {
|
||||
using TimeField = Ui::TimePartWithPlaceholder;
|
||||
const auto putNext = [](not_null<TimeField*> field, QChar ch) {
|
||||
field->setCursorPosition(0);
|
||||
if (ch.unicode()) {
|
||||
field->setText(ch + field->getLastText());
|
||||
field->setCursorPosition(1);
|
||||
}
|
||||
field->onTextEdited();
|
||||
field->setFocus();
|
||||
};
|
||||
|
||||
const auto erasePrevious = [](not_null<TimeField*> field) {
|
||||
const auto text = field->getLastText();
|
||||
if (!text.isEmpty()) {
|
||||
field->setCursorPosition(text.size() - 1);
|
||||
field->setText(text.mid(0, text.size() - 1));
|
||||
}
|
||||
field->setFocus();
|
||||
};
|
||||
|
||||
struct State {
|
||||
not_null<TimeField*> day;
|
||||
not_null<TimeField*> hour;
|
||||
not_null<TimeField*> minute;
|
||||
|
||||
rpl::variable<int> valueInSeconds = 0;
|
||||
};
|
||||
|
||||
auto content = object_ptr<Ui::FixedHeightWidget>(
|
||||
parent,
|
||||
st::scheduleHeight);
|
||||
|
||||
const auto startDays = startSeconds / 86400;
|
||||
startSeconds -= startDays * 86400;
|
||||
const auto startHours = startSeconds / 3600;
|
||||
startSeconds -= startHours * 3600;
|
||||
const auto startMinutes = startSeconds / 60;
|
||||
|
||||
const auto state = content->lifetime().make_state<State>(State{
|
||||
.day = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startDays)),
|
||||
.hour = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startHours)),
|
||||
.minute = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startMinutes)),
|
||||
});
|
||||
|
||||
const auto day = base::make_weak(state->day);
|
||||
const auto hour = base::make_weak(state->hour);
|
||||
const auto minute = base::make_weak(state->minute);
|
||||
|
||||
if (hiddenDaysInput) {
|
||||
day->setVisible(false);
|
||||
}
|
||||
|
||||
day->setPhrase(tr::lng_days);
|
||||
day->setMaxValue(31);
|
||||
day->setWheelStep(1);
|
||||
day->putNext() | rpl::on_next([=](QChar ch) {
|
||||
putNext(hour.get(), ch);
|
||||
}, content->lifetime());
|
||||
|
||||
hour->setPhrase(tr::lng_hours);
|
||||
hour->setMaxValue(23);
|
||||
hour->setWheelStep(1);
|
||||
hour->putNext() | rpl::on_next([=](QChar ch) {
|
||||
putNext(minute.get(), ch);
|
||||
}, content->lifetime());
|
||||
hour->erasePrevious() | rpl::on_next([=] {
|
||||
erasePrevious(day.get());
|
||||
}, content->lifetime());
|
||||
|
||||
minute->setPhrase(tr::lng_minutes);
|
||||
minute->setMaxValue(59);
|
||||
minute->setWheelStep(10);
|
||||
minute->erasePrevious() | rpl::on_next([=] {
|
||||
erasePrevious(hour.get());
|
||||
}, content->lifetime());
|
||||
|
||||
content->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto inputWidth = s.width() / (hiddenDaysInput ? 2 : 3);
|
||||
auto rect = QRect(
|
||||
0,
|
||||
(s.height() - day->height()) / 2,
|
||||
inputWidth,
|
||||
day->height());
|
||||
for (const auto &input : { day, hour, minute }) {
|
||||
if (input->isHidden()) {
|
||||
continue;
|
||||
}
|
||||
input->setGeometry(rect - st::muteBoxTimeFieldPadding);
|
||||
rect.translate(inputWidth, 0);
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
rpl::merge(
|
||||
rpl::single(rpl::empty),
|
||||
base::qt_signal_producer(day.get(), &MaskedInputField::changed),
|
||||
base::qt_signal_producer(hour.get(), &MaskedInputField::changed),
|
||||
base::qt_signal_producer(minute.get(), &MaskedInputField::changed)
|
||||
) | rpl::on_next([=] {
|
||||
state->valueInSeconds = 0
|
||||
+ day->getLastText().toUInt() * 3600 * 24
|
||||
+ hour->getLastText().toUInt() * 3600
|
||||
+ minute->getLastText().toUInt() * 60;
|
||||
}, content->lifetime());
|
||||
return {
|
||||
object_ptr<Ui::RpWidget>::fromRaw(content.release()),
|
||||
state->valueInSeconds.value(),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
26
Telegram/SourceFiles/ui/boxes/choose_time.h
Normal file
26
Telegram/SourceFiles/ui/boxes/choose_time.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
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/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
|
||||
struct ChooseTimeResult {
|
||||
object_ptr<RpWidget> widget;
|
||||
rpl::producer<TimeId> secondsValue;
|
||||
};
|
||||
|
||||
ChooseTimeResult ChooseTimeWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
TimeId startSeconds,
|
||||
bool hiddenDaysInput = false);
|
||||
|
||||
} // namespace Ui
|
||||
277
Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp
Normal file
277
Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
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 "ui/boxes/collectible_info_box.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "info/channel_statistics/earn/earn_format.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTonMultiplier = uint64(1000000000);
|
||||
|
||||
[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) {
|
||||
switch (type) {
|
||||
case CollectibleType::Phone: {
|
||||
static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q);
|
||||
entity.replace(kNonDigits, QString());
|
||||
} return Ui::FormatPhone(entity);
|
||||
case CollectibleType::Username:
|
||||
return entity.startsWith('@') ? entity : ('@' + entity);
|
||||
}
|
||||
Unexpected("CollectibleType in FormatEntity.");
|
||||
}
|
||||
|
||||
[[nodiscard]] QString FormatDate(TimeId date) {
|
||||
return langDateTime(base::unixtime::parse(date));
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) {
|
||||
auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount);
|
||||
if (minor.size() == 1 && minor.at(0) == '.') {
|
||||
minor += '0';
|
||||
}
|
||||
auto price = (info.cryptoCurrency == u"TON"_q)
|
||||
? Ui::Text::IconEmoji(
|
||||
&st::tonIconEmoji
|
||||
).append(
|
||||
Info::ChannelEarn::MajorPart(info.cryptoAmount)
|
||||
).append(minor)
|
||||
: TextWithEntities{ ('{'
|
||||
+ info.cryptoCurrency + ':' + QString::number(info.cryptoAmount)
|
||||
+ '}') };
|
||||
const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency);
|
||||
return Ui::Text::Wrapped(
|
||||
price,
|
||||
EntityType::Bold
|
||||
).append(u" ("_q + fiat + ')');
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeOwnerCell(
|
||||
not_null<QWidget*> parent,
|
||||
const CollectibleInfo &info) {
|
||||
const auto st = &st::defaultMultiSelectItem;
|
||||
const auto size = st->height;
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), size);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto name = info.ownerName;
|
||||
const auto userpic = info.ownerUserpic;
|
||||
const auto nameWidth = st->style.font->width(name);
|
||||
const auto added = size + st->padding.left() + st->padding.right();
|
||||
const auto subscribed = std::make_shared<bool>(false);
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
const auto use = std::min(nameWidth + added, raw->width());
|
||||
const auto x = (raw->width() - use) / 2;
|
||||
if (const auto available = use - added; available > 0) {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st->textBg);
|
||||
p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.);
|
||||
|
||||
if (!*subscribed) {
|
||||
*subscribed = true;
|
||||
userpic->subscribeToUpdates([=] { raw->update(); });
|
||||
}
|
||||
p.drawImage(QRect(x, 0, size, size), userpic->image(size));
|
||||
|
||||
const auto textx = x + size + st->padding.left();
|
||||
const auto texty = st->padding.top() + st->style.font->ascent;
|
||||
const auto text = (use == nameWidth + added)
|
||||
? name
|
||||
: st->style.font->elided(name, available);
|
||||
p.setPen(st->textFg);
|
||||
p.setFont(st->style.font);
|
||||
p.drawText(textx, texty, text);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CollectibleType DetectCollectibleType(const QString &entity) {
|
||||
return entity.startsWith('+')
|
||||
? CollectibleType::Phone
|
||||
: CollectibleType::Username;
|
||||
}
|
||||
|
||||
void CollectibleInfoBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
CollectibleInfo info) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::collectibleBox);
|
||||
|
||||
const auto type = DetectCollectibleType(info.entity);
|
||||
|
||||
const auto icon = box->addRow(
|
||||
object_ptr<Ui::FixedHeightWidget>(box, st::collectibleIconDiameter),
|
||||
st::collectibleIconPadding);
|
||||
icon->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
const auto size = icon->height();
|
||||
const auto inner = QRect(
|
||||
(icon->width() - size) / 2,
|
||||
0,
|
||||
size,
|
||||
size);
|
||||
if (!inner.intersects(clip)) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(icon);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(st::defaultActiveButton.textBg);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(inner);
|
||||
}, icon->lifetime());
|
||||
const auto lottieSize = st::collectibleIcon;
|
||||
auto lottie = Settings::CreateLottieIcon(
|
||||
icon,
|
||||
{
|
||||
.name = (type == CollectibleType::Phone
|
||||
? u"collectible_phone"_q
|
||||
: u"collectible_username"_q),
|
||||
.color = &st::defaultActiveButton.textFg,
|
||||
.sizeOverride = { lottieSize, lottieSize },
|
||||
},
|
||||
QMargins());
|
||||
box->showFinishes(
|
||||
) | rpl::on_next([animate = std::move(lottie.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
}, box->lifetime());
|
||||
const auto animation = lottie.widget.release();
|
||||
icon->sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto skip = (type == CollectibleType::Phone)
|
||||
? style::ConvertScale(2)
|
||||
: 0;
|
||||
animation->move(
|
||||
(size.width() - animation->width()) / 2,
|
||||
skip + (size.height() - animation->height()) / 2);
|
||||
}, animation->lifetime());
|
||||
|
||||
const auto formatted = FormatEntity(type, info.entity);
|
||||
const auto header = (type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_title(
|
||||
tr::now,
|
||||
lt_phone,
|
||||
tr::link(formatted),
|
||||
tr::marked)
|
||||
: tr::lng_collectible_username_title(
|
||||
tr::now,
|
||||
lt_username,
|
||||
tr::link(formatted),
|
||||
tr::marked);
|
||||
const auto copyCallback = [box, type, formatted, text = info.copyText](
|
||||
bool copyLink) {
|
||||
QGuiApplication::clipboard()->setText((text.isEmpty() || !copyLink)
|
||||
? formatted
|
||||
: text);
|
||||
box->uiShow()->showToast((type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_copied(tr::now)
|
||||
: copyLink
|
||||
? tr::lng_username_copied(tr::now)
|
||||
: tr::lng_username_text_copied(tr::now));
|
||||
};
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
rpl::single(header),
|
||||
st::collectibleHeader),
|
||||
st::collectibleHeaderPadding,
|
||||
style::al_top
|
||||
)->setClickHandlerFilter([copyCallback](const auto &...) {
|
||||
copyCallback(false);
|
||||
return false;
|
||||
});
|
||||
|
||||
box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding);
|
||||
|
||||
const auto text = ((type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_info
|
||||
: tr::lng_collectible_username_info)(
|
||||
tr::now,
|
||||
lt_date,
|
||||
TextWithEntities{ FormatDate(info.date) },
|
||||
lt_price,
|
||||
FormatPrice(info),
|
||||
tr::rich);
|
||||
const auto label = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(box, st::collectibleInfo),
|
||||
st::collectibleInfoPadding,
|
||||
style::al_top);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->setMarkedText(text);
|
||||
|
||||
const auto more = box->addRow(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
tr::lng_collectible_learn_more(),
|
||||
st::collectibleMore),
|
||||
st::collectibleMorePadding);
|
||||
more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
more->setClickedCallback([url = info.url] {
|
||||
File::OpenUrl(url);
|
||||
});
|
||||
|
||||
const auto phrase = (type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_copy
|
||||
: tr::lng_collectible_username_copy;
|
||||
auto owned = object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
phrase(),
|
||||
st::collectibleCopy);
|
||||
const auto copy = owned.data();
|
||||
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
copy->setClickedCallback([copyCallback] {
|
||||
copyCallback(true);
|
||||
});
|
||||
box->addButton(std::move(owned));
|
||||
|
||||
box->setNoContentMargin(true);
|
||||
const auto buttonsParent = box->verticalLayout().get();
|
||||
const auto close = Ui::CreateChild<Ui::IconButton>(
|
||||
buttonsParent,
|
||||
st::boxTitleClose);
|
||||
close->setClickedCallback([=] {
|
||||
box->closeBox();
|
||||
});
|
||||
box->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
close->moveToRight(0, 0);
|
||||
}, box->lifetime());
|
||||
|
||||
box->widthValue() | rpl::on_next([=](int width) {
|
||||
more->setFullWidth(width
|
||||
- st::collectibleMorePadding.left()
|
||||
- st::collectibleMorePadding.right());
|
||||
copy->setFullWidth(width
|
||||
- st::collectibleBox.buttonPadding.left()
|
||||
- st::collectibleBox.buttonPadding.right());
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
37
Telegram/SourceFiles/ui/boxes/collectible_info_box.h
Normal file
37
Telegram/SourceFiles/ui/boxes/collectible_info_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
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class DynamicImage;
|
||||
|
||||
enum class CollectibleType {
|
||||
Phone,
|
||||
Username,
|
||||
};
|
||||
|
||||
[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity);
|
||||
|
||||
struct CollectibleInfo {
|
||||
QString entity;
|
||||
QString copyText;
|
||||
std::shared_ptr<DynamicImage> ownerUserpic;
|
||||
QString ownerName;
|
||||
uint64 cryptoAmount = 0;
|
||||
uint64 amount = 0;
|
||||
QString cryptoCurrency;
|
||||
QString currency;
|
||||
QString url;
|
||||
TimeId date = 0;
|
||||
};
|
||||
|
||||
void CollectibleInfoBox(not_null<Ui::GenericBox*> box, CollectibleInfo info);
|
||||
|
||||
} // namespace Ui
|
||||
156
Telegram/SourceFiles/ui/boxes/confirm_box.cpp
Normal file
156
Telegram/SourceFiles/ui/boxes/confirm_box.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
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 "ui/boxes/confirm_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
|
||||
const auto weak = base::make_weak(box);
|
||||
const auto lifetime = box->lifetime().make_state<rpl::lifetime>();
|
||||
|
||||
const auto withTitle = !v::is_null(args.title);
|
||||
if (withTitle) {
|
||||
box->setTitle(v::text::take_marked(std::move(args.title)));
|
||||
}
|
||||
|
||||
if (!v::is_null(args.text)) {
|
||||
const auto padding = st::boxPadding;
|
||||
const auto use = args.labelPadding
|
||||
? *args.labelPadding
|
||||
: withTitle
|
||||
? QMargins(padding.left(), 0, padding.right(), padding.bottom())
|
||||
: padding;
|
||||
const auto label = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
v::text::take_marked(std::move(args.text)),
|
||||
args.labelStyle ? *args.labelStyle : st::boxLabel),
|
||||
use);
|
||||
if (args.labelFilter) {
|
||||
label->setClickHandlerFilter(std::move(args.labelFilter));
|
||||
}
|
||||
}
|
||||
|
||||
const auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) {
|
||||
return [=, confirmed = std::move(callback)]() {
|
||||
if (const auto callbackPtr = std::get_if<1>(&confirmed)) {
|
||||
if (auto callback = (*callbackPtr)) {
|
||||
callback();
|
||||
}
|
||||
} else if (const auto callbackPtr = std::get_if<2>(&confirmed)) {
|
||||
if (auto callback = (*callbackPtr)) {
|
||||
callback(crl::guard(weak, [=] { weak->closeBox(); }));
|
||||
}
|
||||
} else if (weak) {
|
||||
weak->closeBox();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const auto &defaultButtonStyle = box->getDelegate()->style().button;
|
||||
const auto confirmTextPlain = v::is_null(args.confirmText)
|
||||
|| v::is<rpl::producer<QString>>(args.confirmText)
|
||||
|| v::is<QString>(args.confirmText);
|
||||
const auto confirmButton = box->addButton(
|
||||
(confirmTextPlain
|
||||
? v::text::take_plain(
|
||||
std::move(args.confirmText),
|
||||
tr::lng_box_ok())
|
||||
: rpl::single(QString())),
|
||||
[=, c = prepareCallback(args.confirmed)]() {
|
||||
lifetime->destroy();
|
||||
c();
|
||||
},
|
||||
args.confirmStyle ? *args.confirmStyle : defaultButtonStyle);
|
||||
if (!confirmTextPlain) {
|
||||
confirmButton->setText(
|
||||
v::text::take_marked(std::move(args.confirmText)));
|
||||
}
|
||||
|
||||
box->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if ((e->type() != QEvent::KeyPress) || !confirmButton) {
|
||||
return;
|
||||
}
|
||||
const auto k = static_cast<QKeyEvent*>(e.get());
|
||||
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
|
||||
confirmButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
if (!args.inform) {
|
||||
const auto cancelButton = box->addButton(
|
||||
v::text::take_plain(std::move(args.cancelText), tr::lng_cancel()),
|
||||
crl::guard(weak, [=, c = prepareCallback(args.cancelled)]() {
|
||||
lifetime->destroy();
|
||||
c();
|
||||
}),
|
||||
args.cancelStyle ? *args.cancelStyle : defaultButtonStyle);
|
||||
|
||||
box->boxClosing(
|
||||
) | rpl::on_next(crl::guard(cancelButton, [=] {
|
||||
cancelButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
|
||||
}), *lifetime);
|
||||
}
|
||||
|
||||
if (args.strictCancel) {
|
||||
lifetime->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
object_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {
|
||||
return Box(ConfirmBox, std::move(args));
|
||||
}
|
||||
|
||||
void IconWithTitle(
|
||||
not_null<VerticalLayout*> container,
|
||||
not_null<RpWidget*> icon,
|
||||
not_null<RpWidget*> title,
|
||||
RpWidget *subtitle) {
|
||||
const auto line = container->add(
|
||||
object_ptr<RpWidget>(container),
|
||||
st::boxRowPadding);
|
||||
icon->setParent(line);
|
||||
title->setParent(line);
|
||||
if (subtitle) {
|
||||
subtitle->setParent(line);
|
||||
}
|
||||
|
||||
icon->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
line->resize(line->width(), height);
|
||||
}, icon->lifetime());
|
||||
|
||||
line->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
icon->moveToLeft(0, 0);
|
||||
const auto skip = st::defaultBoxCheckbox.textPosition.x();
|
||||
title->resizeToWidth(width - rect::right(icon) - skip);
|
||||
if (subtitle) {
|
||||
subtitle->resizeToWidth(title->width());
|
||||
title->moveToLeft(rect::right(icon) + skip, icon->y());
|
||||
subtitle->moveToLeft(
|
||||
title->x(),
|
||||
icon->y() + icon->height() - subtitle->height());
|
||||
} else {
|
||||
title->moveToLeft(
|
||||
rect::right(icon) + skip,
|
||||
((icon->height() - title->height()) / 2));
|
||||
}
|
||||
}, title->lifetime());
|
||||
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
title->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
69
Telegram/SourceFiles/ui/boxes/confirm_box.h
Normal file
69
Telegram/SourceFiles/ui/boxes/confirm_box.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 "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_variant.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ConfirmBoxArgs {
|
||||
using Callback = std::variant<
|
||||
v::null_t,
|
||||
Fn<void()>,
|
||||
Fn<void(Fn<void()>)>>;
|
||||
|
||||
v::text::data text = v::null;
|
||||
Callback confirmed = v::null;
|
||||
Callback cancelled = v::null;
|
||||
|
||||
v::text::data confirmText;
|
||||
v::text::data cancelText;
|
||||
|
||||
const style::RoundButton *confirmStyle = nullptr;
|
||||
const style::RoundButton *cancelStyle = nullptr;
|
||||
|
||||
const style::FlatLabel *labelStyle = nullptr;
|
||||
Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;
|
||||
std::optional<QMargins> labelPadding;
|
||||
|
||||
v::text::data title = v::null;
|
||||
|
||||
bool inform = false;
|
||||
// If strict cancel is set the cancel.callback() is only called
|
||||
// if the cancel button was pressed.
|
||||
bool strictCancel = false;
|
||||
};
|
||||
|
||||
void ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);
|
||||
|
||||
inline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {
|
||||
args.inform = true;
|
||||
ConfirmBox(box, std::move(args));
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
|
||||
ConfirmBoxArgs &&args) {
|
||||
args.inform = true;
|
||||
return MakeConfirmBox(std::move(args));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
|
||||
v::text::data text) {
|
||||
return MakeInformBox({ .text = std::move(text) });
|
||||
}
|
||||
|
||||
void IconWithTitle(
|
||||
not_null<VerticalLayout*> container,
|
||||
not_null<RpWidget*> icon,
|
||||
not_null<RpWidget*> title,
|
||||
RpWidget *subtitle = nullptr);
|
||||
|
||||
} // namespace Ui
|
||||
192
Telegram/SourceFiles/ui/boxes/confirm_phone_box.cpp
Normal file
192
Telegram/SourceFiles/ui/boxes/confirm_phone_box.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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 "ui/boxes/confirm_phone_box.h"
|
||||
|
||||
#include "core/file_utilities.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ConfirmPhoneBox::ConfirmPhoneBox(
|
||||
QWidget*,
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
std::optional<int> timeout)
|
||||
: _phone(phone)
|
||||
, _sentCodeLength(codeLength)
|
||||
, _call([=] { sendCall(); }, [=] { update(); }) {
|
||||
if (!openUrl.isEmpty()) {
|
||||
_fragment.create(
|
||||
this,
|
||||
tr::lng_intro_fragment_button(),
|
||||
st::fragmentBoxButton);
|
||||
_fragment->setClickedCallback([=] { File::OpenUrl(openUrl); });
|
||||
_fragment->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
}
|
||||
if (timeout) {
|
||||
_call.setStatus({ Ui::SentCodeCall::State::Waiting, *timeout });
|
||||
}
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::sendCall() {
|
||||
_resendRequests.fire({});
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::prepare() {
|
||||
_about.create(
|
||||
this,
|
||||
tr::lng_confirm_phone_about(
|
||||
lt_phone,
|
||||
rpl::single(tr::bold(Ui::FormatPhone(_phone))),
|
||||
tr::marked),
|
||||
st::confirmPhoneAboutLabel);
|
||||
|
||||
_code.create(this, st::confirmPhoneCodeField, tr::lng_code_ph());
|
||||
_code->setAutoSubmit(_sentCodeLength, [=] { sendCode(); });
|
||||
_code->setChangedCallback([=] { showError(QString()); });
|
||||
|
||||
setTitle(tr::lng_confirm_phone_title());
|
||||
|
||||
addButton(tr::lng_confirm_phone_send(), [=] { sendCode(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
setDimensions(
|
||||
st::boxWidth,
|
||||
st::usernamePadding.top()
|
||||
+ _code->height()
|
||||
+ st::usernameSkip
|
||||
+ _about->height()
|
||||
+ st::usernameSkip
|
||||
+ (_fragment ? (_fragment->height() + fragmentSkip()) : 0));
|
||||
|
||||
_code->submits(
|
||||
) | rpl::on_next([=] { sendCode(); }, _code->lifetime());
|
||||
|
||||
showChildren();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::sendCode() {
|
||||
if (_isWaitingCheck) {
|
||||
return;
|
||||
}
|
||||
const auto code = _code->getDigitsOnly();
|
||||
if (code.isEmpty()) {
|
||||
_code->showError();
|
||||
return;
|
||||
}
|
||||
|
||||
_code->setDisabled(true);
|
||||
setFocus();
|
||||
|
||||
showError(QString());
|
||||
|
||||
_checkRequests.fire_copy(code);
|
||||
_isWaitingCheck = true;
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::showError(const QString &error) {
|
||||
_error = error;
|
||||
if (!_error.isEmpty()) {
|
||||
_code->showError();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::paintEvent(QPaintEvent *e) {
|
||||
BoxContent::paintEvent(e);
|
||||
|
||||
auto p = QPainter(this);
|
||||
|
||||
p.setFont(st::boxTextFont);
|
||||
const auto callText = _call.getText();
|
||||
if (!callText.isEmpty()) {
|
||||
p.setPen(st::usernameDefaultFg);
|
||||
const auto callTextRect = QRect(
|
||||
st::usernamePadding.left(),
|
||||
_about->y() + _about->height(),
|
||||
width() - 2 * st::usernamePadding.left(),
|
||||
st::usernameSkip);
|
||||
p.drawText(callTextRect, callText, style::al_left);
|
||||
}
|
||||
auto errorText = _error;
|
||||
if (errorText.isEmpty()) {
|
||||
p.setPen(st::usernameDefaultFg);
|
||||
errorText = tr::lng_confirm_phone_enter_code(tr::now);
|
||||
} else {
|
||||
p.setPen(st::boxTextFgError);
|
||||
}
|
||||
const auto errorTextRect = QRect(
|
||||
st::usernamePadding.left(),
|
||||
_code->y() + _code->height(),
|
||||
width() - 2 * st::usernamePadding.left(),
|
||||
st::usernameSkip);
|
||||
p.drawText(errorTextRect, errorText, style::al_left);
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_code->resize(
|
||||
width() - st::usernamePadding.left() - st::usernamePadding.right(),
|
||||
_code->height());
|
||||
_code->moveToLeft(st::usernamePadding.left(), st::usernamePadding.top());
|
||||
|
||||
if (_fragment) {
|
||||
_fragment->setFullWidth(_code->width());
|
||||
_fragment->moveToLeft(
|
||||
(width() - _fragment->width()) / 2,
|
||||
_code->y() + _code->height() + st::usernameSkip);
|
||||
}
|
||||
|
||||
const auto aboutTop = _fragment
|
||||
? (_fragment->y() + _fragment->height() + fragmentSkip())
|
||||
: (_code->y() + _code->height() + st::usernameSkip);
|
||||
_about->moveToLeft(st::usernamePadding.left(), aboutTop);
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::setInnerFocus() {
|
||||
_code->setFocusFast();
|
||||
}
|
||||
|
||||
int ConfirmPhoneBox::fragmentSkip() const {
|
||||
return st::usernamePadding.bottom();
|
||||
}
|
||||
|
||||
rpl::producer<QString> ConfirmPhoneBox::checkRequests() const {
|
||||
return _checkRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ConfirmPhoneBox::resendRequests() const {
|
||||
return _resendRequests.events();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::callDone() {
|
||||
_call.callDone();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::showServerError(const QString &text) {
|
||||
_isWaitingCheck = false;
|
||||
_code->setDisabled(false);
|
||||
_code->setFocus();
|
||||
showError(text);
|
||||
}
|
||||
|
||||
QString ConfirmPhoneBox::getPhone() const {
|
||||
return _phone;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
71
Telegram/SourceFiles/ui/boxes/confirm_phone_box.h
Normal file
71
Telegram/SourceFiles/ui/boxes/confirm_phone_box.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 "ui/layers/box_content.h"
|
||||
#include "ui/widgets/sent_code_field.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FlatLabel;
|
||||
class RoundButton;
|
||||
|
||||
class ConfirmPhoneBox final : public Ui::BoxContent {
|
||||
public:
|
||||
ConfirmPhoneBox(
|
||||
QWidget*,
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
std::optional<int> timeout);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> checkRequests() const;
|
||||
[[nodiscard]] rpl::producer<> resendRequests() const;
|
||||
|
||||
void callDone();
|
||||
void showServerError(const QString &text);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void sendCode();
|
||||
void sendCall();
|
||||
void checkPhoneAndHash();
|
||||
|
||||
[[nodiscard]] int fragmentSkip() const;
|
||||
|
||||
QString getPhone() const;
|
||||
void showError(const QString &error);
|
||||
|
||||
// _hash from the link for account.sendConfirmPhoneCode call.
|
||||
// _phoneHash from auth.sentCode for account.confirmPhone call.
|
||||
const QString _phone;
|
||||
|
||||
// If we receive the code length, we autosubmit _code field when enough symbols is typed.
|
||||
const int _sentCodeLength = 0;
|
||||
|
||||
bool _isWaitingCheck = false;
|
||||
|
||||
object_ptr<Ui::FlatLabel> _about = { nullptr };
|
||||
object_ptr<Ui::SentCodeField> _code = { nullptr };
|
||||
object_ptr<Ui::RoundButton> _fragment = { nullptr };
|
||||
|
||||
QString _error;
|
||||
Ui::SentCodeCall _call;
|
||||
|
||||
rpl::event_stream<QString> _checkRequests;
|
||||
rpl::event_stream<> _resendRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
487
Telegram/SourceFiles/ui/boxes/country_select_box.cpp
Normal file
487
Telegram/SourceFiles/ui/boxes/country_select_box.cpp
Normal file
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
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 "ui/boxes/country_select_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_intro.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
QString LastValidISO;
|
||||
|
||||
} // namespace
|
||||
|
||||
class CountrySelectBox::Inner : public RpWidget {
|
||||
public:
|
||||
Inner(QWidget *parent, const QString &iso, Type type);
|
||||
~Inner();
|
||||
|
||||
void updateFilter(QString filter = QString());
|
||||
|
||||
void selectSkip(int32 dir);
|
||||
void selectSkipPage(int32 h, int32 dir);
|
||||
|
||||
void chooseCountry();
|
||||
|
||||
void refresh();
|
||||
|
||||
[[nodiscard]] rpl::producer<Entry> countryChosen() const {
|
||||
return _countryChosen.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {
|
||||
return _mustScrollTo.events();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
void init();
|
||||
void updateSelected() {
|
||||
updateSelected(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
void updateSelected(QPoint localPos);
|
||||
void updateSelectedRow();
|
||||
void updateRow(int index);
|
||||
void setPressed(int pressed);
|
||||
const std::vector<Entry> ¤t() const;
|
||||
|
||||
Type _type = Type::Phones;
|
||||
int _rowHeight = 0;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
QString _filter;
|
||||
bool _mouseSelection = false;
|
||||
|
||||
std::vector<std::unique_ptr<RippleAnimation>> _ripples;
|
||||
|
||||
std::vector<Entry> _list;
|
||||
std::vector<Entry> _filtered;
|
||||
base::flat_map<QChar, std::vector<int>> _byLetter;
|
||||
std::vector<std::vector<QString>> _namesList;
|
||||
|
||||
rpl::event_stream<Entry> _countryChosen;
|
||||
rpl::event_stream<ScrollToRequest> _mustScrollTo;
|
||||
|
||||
};
|
||||
|
||||
CountrySelectBox::CountrySelectBox(QWidget*)
|
||||
: CountrySelectBox(nullptr, QString(), Type::Phones) {
|
||||
}
|
||||
|
||||
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
|
||||
: _select(this, st::defaultMultiSelect, tr::lng_country_ph())
|
||||
, _ownedInner(this, iso, type) {
|
||||
}
|
||||
|
||||
rpl::producer<QString> CountrySelectBox::countryChosen() const {
|
||||
Expects(_ownedInner != nullptr || _inner != nullptr);
|
||||
|
||||
return (_ownedInner
|
||||
? _ownedInner.data()
|
||||
: _inner.data())->countryChosen() | rpl::map([](const Entry &e) {
|
||||
return e.iso2;
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {
|
||||
Expects(_ownedInner != nullptr || _inner != nullptr);
|
||||
|
||||
return (_ownedInner
|
||||
? _ownedInner.data()
|
||||
: _inner.data())->countryChosen();
|
||||
}
|
||||
|
||||
void CountrySelectBox::prepare() {
|
||||
setTitle(tr::lng_country_select());
|
||||
|
||||
_select->resizeToWidth(st::boxWidth);
|
||||
_select->setQueryChangedCallback([=](const QString &query) {
|
||||
applyFilterUpdate(query);
|
||||
});
|
||||
_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
|
||||
submit();
|
||||
});
|
||||
|
||||
_inner = setInnerWidget(
|
||||
std::move(_ownedInner),
|
||||
st::countriesScroll,
|
||||
_select->height());
|
||||
|
||||
addButton(tr::lng_close(), [=] { closeBox(); });
|
||||
|
||||
setDimensions(st::boxWidth, st::boxMaxListHeight);
|
||||
|
||||
_inner->mustScrollTo(
|
||||
) | rpl::on_next([=](ScrollToRequest request) {
|
||||
scrollToY(request.ymin, request.ymax);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void CountrySelectBox::submit() {
|
||||
_inner->chooseCountry();
|
||||
}
|
||||
|
||||
void CountrySelectBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Down) {
|
||||
_inner->selectSkip(1);
|
||||
} else if (e->key() == Qt::Key_Up) {
|
||||
_inner->selectSkip(-1);
|
||||
} else if (e->key() == Qt::Key_PageDown) {
|
||||
_inner->selectSkipPage(height() - _select->height(), 1);
|
||||
} else if (e->key() == Qt::Key_PageUp) {
|
||||
_inner->selectSkipPage(height() - _select->height(), -1);
|
||||
} else {
|
||||
BoxContent::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_select->resizeToWidth(width());
|
||||
_select->moveToLeft(0, 0);
|
||||
|
||||
_inner->resizeToWidth(width());
|
||||
}
|
||||
|
||||
void CountrySelectBox::applyFilterUpdate(const QString &query) {
|
||||
scrollToY(0);
|
||||
_inner->updateFilter(query);
|
||||
}
|
||||
|
||||
void CountrySelectBox::setInnerFocus() {
|
||||
_select->setInnerFocus();
|
||||
}
|
||||
|
||||
CountrySelectBox::Inner::Inner(
|
||||
QWidget *parent,
|
||||
const QString &iso,
|
||||
Type type)
|
||||
: RpWidget(parent)
|
||||
, _type(type)
|
||||
, _rowHeight(st::countryRowHeight) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
const auto &byISO2 = Countries::Instance().byISO2();
|
||||
|
||||
if (byISO2.contains(iso)) {
|
||||
LastValidISO = iso;
|
||||
}
|
||||
|
||||
rpl::single(
|
||||
) | rpl::then(
|
||||
Countries::Instance().updated()
|
||||
) | rpl::on_next([=] {
|
||||
_mustScrollTo.fire(ScrollToRequest(0, 0));
|
||||
_list.clear();
|
||||
_namesList.clear();
|
||||
init();
|
||||
const auto filter = _filter;
|
||||
_filter = u"a"_q;
|
||||
updateFilter(filter);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::init() {
|
||||
const auto &byISO2 = Countries::Instance().byISO2();
|
||||
|
||||
const auto extractEntries = [&](const Countries::Info &info) {
|
||||
for (const auto &code : info.codes) {
|
||||
_list.push_back(Entry{
|
||||
.country = info.name,
|
||||
.iso2 = info.iso2,
|
||||
.code = code.callingCode,
|
||||
.alternativeName = info.alternativeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_list.reserve(byISO2.size());
|
||||
_namesList.reserve(byISO2.size());
|
||||
|
||||
const auto l = byISO2.constFind(LastValidISO);
|
||||
const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;
|
||||
if (lastValid) {
|
||||
extractEntries(*lastValid);
|
||||
}
|
||||
for (const auto &entry : Countries::Instance().list()) {
|
||||
if (&entry != lastValid) {
|
||||
extractEntries(entry);
|
||||
}
|
||||
}
|
||||
auto index = 0;
|
||||
for (const auto &info : _list) {
|
||||
static const auto RegExp = QRegularExpression("[\\s\\-]");
|
||||
auto full = info.country
|
||||
+ ' '
|
||||
+ (!info.alternativeName.isEmpty()
|
||||
? info.alternativeName
|
||||
: QString());
|
||||
const auto namesList = std::move(full).toLower().split(
|
||||
RegExp,
|
||||
Qt::SkipEmptyParts);
|
||||
auto &names = _namesList.emplace_back();
|
||||
names.reserve(namesList.size());
|
||||
for (const auto &name : namesList) {
|
||||
const auto part = name.trimmed();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ch = part[0];
|
||||
auto &byLetter = _byLetter[ch];
|
||||
if (byLetter.empty() || byLetter.back() != index) {
|
||||
byLetter.push_back(index);
|
||||
}
|
||||
names.push_back(part);
|
||||
}
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
QRect r(e->rect());
|
||||
p.setClipRect(r);
|
||||
|
||||
const auto &list = current();
|
||||
if (list.empty()) {
|
||||
p.fillRect(r, st::boxBg);
|
||||
p.setFont(st::noContactsFont);
|
||||
p.setPen(st::noContactsColor);
|
||||
p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);
|
||||
return;
|
||||
}
|
||||
const auto l = int(list.size());
|
||||
if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {
|
||||
p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);
|
||||
}
|
||||
int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);
|
||||
int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);
|
||||
for (int32 i = from; i < to; ++i) {
|
||||
auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
|
||||
auto y = st::countriesSkip + i * _rowHeight;
|
||||
|
||||
p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);
|
||||
if (_ripples.size() > i && _ripples[i]) {
|
||||
_ripples[i]->paint(p, 0, y, width());
|
||||
if (_ripples[i]->empty()) {
|
||||
_ripples[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
auto code = QString("+") + list[i].code;
|
||||
auto codeWidth = st::countryRowCodeFont->width(code);
|
||||
|
||||
auto name = list[i].country;
|
||||
auto nameWidth = st::countryRowNameFont->width(name);
|
||||
auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;
|
||||
if (nameWidth > availWidth) {
|
||||
name = st::countryRowNameFont->elided(name, availWidth);
|
||||
nameWidth = st::countryRowNameFont->width(name);
|
||||
}
|
||||
|
||||
p.setFont(st::countryRowNameFont);
|
||||
p.setPen(st::countryRowNameFg);
|
||||
p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);
|
||||
|
||||
if (_type == Type::Phones) {
|
||||
p.setFont(st::countryRowCodeFont);
|
||||
p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);
|
||||
p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::leaveEventHook(QEvent *e) {
|
||||
_mouseSelection = false;
|
||||
setMouseTracking(false);
|
||||
if (_selected >= 0) {
|
||||
updateSelectedRow();
|
||||
_selected = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
_mouseSelection = true;
|
||||
updateSelected(e->pos());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {
|
||||
_mouseSelection = true;
|
||||
updateSelected(e->pos());
|
||||
|
||||
setPressed(_selected);
|
||||
const auto &list = current();
|
||||
if (_pressed >= 0 && _pressed < list.size()) {
|
||||
if (_ripples.size() <= _pressed) {
|
||||
_ripples.reserve(_pressed + 1);
|
||||
while (_ripples.size() <= _pressed) {
|
||||
_ripples.push_back(nullptr);
|
||||
}
|
||||
}
|
||||
if (!_ripples[_pressed]) {
|
||||
auto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));
|
||||
_ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {
|
||||
updateRow(index);
|
||||
});
|
||||
_ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
auto pressed = _pressed;
|
||||
setPressed(-1);
|
||||
updateSelectedRow();
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if ((pressed >= 0) && pressed == _selected) {
|
||||
chooseCountry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateFilter(QString filter) {
|
||||
const auto words = TextUtilities::PrepareSearchWords(filter);
|
||||
filter = words.isEmpty() ? QString() : words.join(' ');
|
||||
if (_filter == filter) {
|
||||
return;
|
||||
}
|
||||
_filter = filter;
|
||||
|
||||
const auto findWord = [&](
|
||||
const std::vector<QString> &names,
|
||||
const QString &word) {
|
||||
for (const auto &name : names) {
|
||||
if (name.startsWith(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto hasAllWords = [&](const std::vector<QString> &names) {
|
||||
for (const auto &word : words) {
|
||||
if (!findWord(names, word)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!_filter.isEmpty()) {
|
||||
_filtered.clear();
|
||||
for (const auto index : _byLetter[_filter[0].toLower()]) {
|
||||
if (hasAllWords(_namesList[index])) {
|
||||
_filtered.push_back(_list[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
_selected = current().empty() ? -1 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::selectSkip(int32 dir) {
|
||||
_mouseSelection = false;
|
||||
|
||||
const auto &list = current();
|
||||
int cur = (_selected >= 0) ? _selected : -1;
|
||||
cur += dir;
|
||||
if (cur <= 0) {
|
||||
_selected = list.empty() ? -1 : 0;
|
||||
} else if (cur >= list.size()) {
|
||||
_selected = -1;
|
||||
} else {
|
||||
_selected = cur;
|
||||
}
|
||||
if (_selected >= 0) {
|
||||
_mustScrollTo.fire(ScrollToRequest(
|
||||
st::countriesSkip + _selected * _rowHeight,
|
||||
st::countriesSkip + (_selected + 1) * _rowHeight));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
|
||||
int32 points = h / _rowHeight;
|
||||
if (!points) return;
|
||||
selectSkip(points * dir);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::chooseCountry() {
|
||||
const auto &list = current();
|
||||
_countryChosen.fire_copy((_selected >= 0 && _selected < list.size())
|
||||
? list[_selected]
|
||||
: Entry());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::refresh() {
|
||||
const auto &list = current();
|
||||
resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
|
||||
if (!_mouseSelection) return;
|
||||
|
||||
auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));
|
||||
|
||||
const auto &list = current();
|
||||
auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;
|
||||
if (_selected != selected) {
|
||||
updateSelectedRow();
|
||||
_selected = selected;
|
||||
updateSelectedRow();
|
||||
}
|
||||
}
|
||||
|
||||
auto CountrySelectBox::Inner::current() const
|
||||
-> const std::vector<CountrySelectBox::Entry> & {
|
||||
return _filter.isEmpty() ? _list : _filtered;
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateSelectedRow() {
|
||||
updateRow(_selected);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateRow(int index) {
|
||||
if (index >= 0) {
|
||||
update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::setPressed(int pressed) {
|
||||
if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {
|
||||
_ripples[_pressed]->lastStop();
|
||||
}
|
||||
_pressed = pressed;
|
||||
}
|
||||
|
||||
CountrySelectBox::Inner::~Inner() = default;
|
||||
|
||||
} // namespace Ui
|
||||
59
Telegram/SourceFiles/ui/boxes/country_select_box.h
Normal file
59
Telegram/SourceFiles/ui/boxes/country_select_box.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 "ui/layers/box_content.h"
|
||||
|
||||
namespace Countries {
|
||||
struct Info;
|
||||
} // namespace Countries
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class MultiSelect;
|
||||
class RippleAnimation;
|
||||
|
||||
class CountrySelectBox : public BoxContent {
|
||||
public:
|
||||
enum class Type {
|
||||
Phones,
|
||||
Countries,
|
||||
};
|
||||
struct Entry {
|
||||
QString country;
|
||||
QString iso2;
|
||||
QString code;
|
||||
QString alternativeName;
|
||||
};
|
||||
|
||||
CountrySelectBox(QWidget*);
|
||||
CountrySelectBox(QWidget*, const QString &iso, Type type);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> countryChosen() const;
|
||||
[[nodiscard]] rpl::producer<Entry> entryChosen() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void submit();
|
||||
void applyFilterUpdate(const QString &query);
|
||||
|
||||
object_ptr<MultiSelect> _select;
|
||||
|
||||
class Inner;
|
||||
object_ptr<Inner> _ownedInner;
|
||||
QPointer<Inner> _inner;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
218
Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp
Normal file
218
Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
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 "ui/boxes/edit_birthday_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/vertical_drum_picker.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
void EditBirthdayBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Data::Birthday current,
|
||||
Fn<void(Data::Birthday)> save,
|
||||
EditBirthdayType type) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
|
||||
box,
|
||||
st::settingsWorkingHoursPicker));
|
||||
|
||||
const auto font = st::boxTextFont;
|
||||
const auto itemHeight = st::settingsWorkingHoursPickerItemHeight;
|
||||
const auto picker = [=](
|
||||
int count,
|
||||
int startIndex,
|
||||
Fn<void(QPainter &p, QRectF rect, int index)> paint) {
|
||||
return Ui::CreateChild<Ui::VerticalDrumPicker>(
|
||||
content,
|
||||
Ui::VerticalDrumPicker::DefaultPaintCallback(
|
||||
font,
|
||||
itemHeight,
|
||||
paint),
|
||||
count,
|
||||
itemHeight,
|
||||
startIndex);
|
||||
};
|
||||
|
||||
const auto nowDate = QDate::currentDate();
|
||||
const auto nowYear = nowDate.year();
|
||||
const auto nowMonth = nowDate.month();
|
||||
const auto nowDay = nowDate.day();
|
||||
const auto now = Data::Birthday(nowDay, nowMonth, nowYear);
|
||||
const auto max = current.year() ? std::max(now, current) : now;
|
||||
const auto maxYear = max.year();
|
||||
const auto minYear = Data::Birthday::kYearMin;
|
||||
const auto yearsCount = (maxYear - minYear + 2); // Last - not set.
|
||||
const auto yearsStartIndex = current.year()
|
||||
? (current.year() - minYear)
|
||||
: (yearsCount - 1);
|
||||
const auto yearsPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(
|
||||
rect,
|
||||
(index < yearsCount - 1
|
||||
? QString::number(minYear + index)
|
||||
: QString::fromUtf8("\xe2\x80\x94")),
|
||||
style::al_center);
|
||||
};
|
||||
const auto years = picker(yearsCount, yearsStartIndex, yearsPaint);
|
||||
|
||||
struct State {
|
||||
rpl::variable<Ui::VerticalDrumPicker*> months;
|
||||
rpl::variable<Ui::VerticalDrumPicker*> days;
|
||||
};
|
||||
const auto state = content->lifetime().make_state<State>();
|
||||
|
||||
// years->value() is valid only after size is set.
|
||||
rpl::combine(
|
||||
content->sizeValue(),
|
||||
state->months.value(),
|
||||
state->days.value()
|
||||
) | rpl::on_next([=](
|
||||
QSize s,
|
||||
Ui::VerticalDrumPicker *months,
|
||||
Ui::VerticalDrumPicker *days) {
|
||||
const auto half = s.width() / 2;
|
||||
years->setGeometry(half * 3 / 2, 0, half / 2, s.height());
|
||||
if (months) {
|
||||
months->setGeometry(half / 2, 0, half, s.height());
|
||||
}
|
||||
if (days) {
|
||||
days->setGeometry(0, 0, half / 2, s.height());
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
Ui::SendPendingMoveResizeEvents(years);
|
||||
|
||||
years->value() | rpl::on_next([=](int yearsIndex) {
|
||||
const auto year = (yearsIndex == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + yearsIndex;
|
||||
const auto monthsCount = (year == maxYear)
|
||||
? max.month()
|
||||
: 12;
|
||||
const auto monthsStartIndex = std::clamp(
|
||||
(state->months.current()
|
||||
? state->months.current()->index()
|
||||
: current.month()
|
||||
? (current.month() - 1)
|
||||
: (now.month() - 1)),
|
||||
0,
|
||||
monthsCount - 1);
|
||||
const auto monthsPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(
|
||||
rect,
|
||||
Lang::Month(index + 1)(tr::now),
|
||||
style::al_center);
|
||||
};
|
||||
const auto updated = picker(
|
||||
monthsCount,
|
||||
monthsStartIndex,
|
||||
monthsPaint);
|
||||
delete state->months.current();
|
||||
state->months = updated;
|
||||
state->months.current()->show();
|
||||
}, years->lifetime());
|
||||
|
||||
Ui::SendPendingMoveResizeEvents(state->months.current());
|
||||
|
||||
state->months.value() | rpl::map([=](Ui::VerticalDrumPicker *picker) {
|
||||
return picker ? picker->value() : rpl::single(current.month()
|
||||
? (current.month() - 1)
|
||||
: (now.month() - 1));
|
||||
}) | rpl::flatten_latest() | rpl::on_next([=](int monthIndex) {
|
||||
const auto month = monthIndex + 1;
|
||||
const auto yearsIndex = years->index();
|
||||
const auto year = (yearsIndex == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + yearsIndex;
|
||||
const auto daysCount = (year == maxYear && month == max.month())
|
||||
? max.day()
|
||||
: (month == 2)
|
||||
? ((!year || (!(year % 4) && ((year % 100) || !(year % 400))))
|
||||
? 29
|
||||
: 28)
|
||||
: ((month == 4) || (month == 6) || (month == 9) || (month == 11))
|
||||
? 30
|
||||
: 31;
|
||||
const auto daysStartIndex = std::clamp(
|
||||
(state->days.current()
|
||||
? state->days.current()->index()
|
||||
: current.day()
|
||||
? (current.day() - 1)
|
||||
: (now.day() - 1)),
|
||||
0,
|
||||
daysCount - 1);
|
||||
const auto daysPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(rect, QString::number(index + 1), style::al_center);
|
||||
};
|
||||
const auto updated = picker(
|
||||
daysCount,
|
||||
daysStartIndex,
|
||||
daysPaint);
|
||||
delete state->days.current();
|
||||
state->days = updated;
|
||||
state->days.current()->show();
|
||||
}, years->lifetime());
|
||||
|
||||
content->paintRequest(
|
||||
) | rpl::on_next([=](const QRect &r) {
|
||||
auto p = QPainter(content);
|
||||
|
||||
p.fillRect(r, Qt::transparent);
|
||||
|
||||
const auto lineRect = QRect(
|
||||
0,
|
||||
content->height() / 2,
|
||||
content->width(),
|
||||
st::defaultInputField.borderActive);
|
||||
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
|
||||
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
|
||||
}, content->lifetime());
|
||||
|
||||
base::install_event_filter(box, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
years->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
auto confirmText = (type == EditBirthdayType::Suggest)
|
||||
? tr::lng_suggest_birthday_box_confirm()
|
||||
: tr::lng_settings_save();
|
||||
box->addButton(std::move(confirmText), [=] {
|
||||
const auto result = Data::Birthday(
|
||||
state->days.current()->index() + 1,
|
||||
state->months.current()->index() + 1,
|
||||
((years->index() == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + years->index()));
|
||||
box->closeBox();
|
||||
save(result);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
if (current && type == EditBirthdayType::Edit) {
|
||||
box->addLeftButton(tr::lng_settings_birthday_reset(), [=] {
|
||||
box->closeBox();
|
||||
save(Data::Birthday());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
30
Telegram/SourceFiles/ui/boxes/edit_birthday_box.h
Normal file
30
Telegram/SourceFiles/ui/boxes/edit_birthday_box.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 Data {
|
||||
class Birthday;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
enum class EditBirthdayType {
|
||||
Edit,
|
||||
Suggest,
|
||||
ConfirmSuggestion,
|
||||
};
|
||||
|
||||
void EditBirthdayBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Data::Birthday current,
|
||||
Fn<void(Data::Birthday)> save,
|
||||
EditBirthdayType type = EditBirthdayType::Edit);
|
||||
|
||||
} // namespace Ui
|
||||
89
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp
Normal file
89
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
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 "ui/boxes/edit_factcheck_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
void EditFactcheckBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TextWithEntities current,
|
||||
int limit,
|
||||
Fn<void(TextWithEntities)> save,
|
||||
Fn<void(not_null<Ui::InputField*>)> initField) {
|
||||
box->setTitle(tr::lng_factcheck_title());
|
||||
|
||||
const auto field = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::factcheckField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_factcheck_placeholder(),
|
||||
TextWithTags{
|
||||
current.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(current.entities)
|
||||
}));
|
||||
AddLengthLimitLabel(field, limit);
|
||||
initField(field);
|
||||
|
||||
enum class State {
|
||||
Initial,
|
||||
Changed,
|
||||
Removed,
|
||||
};
|
||||
const auto state = box->lifetime().make_state<rpl::variable<State>>(
|
||||
State::Initial);
|
||||
field->changes() | rpl::on_next([=] {
|
||||
const auto now = field->getLastText().trimmed();
|
||||
*state = !now.isEmpty()
|
||||
? State::Changed
|
||||
: current.empty()
|
||||
? State::Initial
|
||||
: State::Removed;
|
||||
}, field->lifetime());
|
||||
|
||||
state->value() | rpl::on_next([=](State state) {
|
||||
box->clearButtons();
|
||||
if (state == State::Removed) {
|
||||
box->addButton(tr::lng_box_remove(), [=] {
|
||||
box->closeBox();
|
||||
save({});
|
||||
}, st::attentionBoxButton);
|
||||
} else if (state == State::Initial) {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
if (current.empty()) {
|
||||
field->showError();
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
auto result = field->getTextWithAppliedMarkdown();
|
||||
if (result.text.size() > limit) {
|
||||
field->showError();
|
||||
return;
|
||||
}
|
||||
|
||||
box->closeBox();
|
||||
save({
|
||||
result.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(result.tags)
|
||||
});
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}, box->lifetime());
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
field->setFocusFast();
|
||||
});
|
||||
}
|
||||
21
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h
Normal file
21
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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/layers/generic_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
void EditFactcheckBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TextWithEntities current,
|
||||
int limit,
|
||||
Fn<void(TextWithEntities)> save,
|
||||
Fn<void(not_null<Ui::InputField*>)> initField);
|
||||
391
Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp
Normal file
391
Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
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 "ui/boxes/edit_invite_link.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/choose_date_time.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
|
||||
constexpr auto kHour = 3600;
|
||||
constexpr auto kDay = 86400;
|
||||
constexpr auto kMaxLabelLength = 32;
|
||||
|
||||
[[nodiscard]] QString FormatExpireDate(TimeId date) {
|
||||
if (date > 0) {
|
||||
return langDateTime(base::unixtime::parse(date));
|
||||
} else if (-date < kDay) {
|
||||
return tr::lng_hours(tr::now, lt_count, (-date / kHour));
|
||||
} else if (-date < 7 * kDay) {
|
||||
return tr::lng_days(tr::now, lt_count, (-date / kDay));
|
||||
} else {
|
||||
return tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditInviteLinkBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
const InviteLinkFields &data,
|
||||
Fn<void(InviteLinkFields)> done) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto link = data.link;
|
||||
const auto isGroup = data.isGroup;
|
||||
const auto isPublic = data.isPublic;
|
||||
const auto subscriptionLocked = data.subscriptionCredits > 0;
|
||||
box->setTitle(link.isEmpty()
|
||||
? tr::lng_group_invite_new_title()
|
||||
: tr::lng_group_invite_edit_title());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto addTitle = [&](
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<QString> text) {
|
||||
container->add(
|
||||
object_ptr<FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::defaultSubsectionTitle),
|
||||
(st::defaultSubsectionTitlePadding
|
||||
+ style::margins(0, st::defaultVerticalListSkip, 0, 0)));
|
||||
};
|
||||
const auto addDivider = [&](
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<QString> text,
|
||||
style::margins margins = style::margins()) {
|
||||
container->add(
|
||||
object_ptr<DividerLabel>(
|
||||
container,
|
||||
object_ptr<FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::boxDividerLabel),
|
||||
st::defaultBoxDividerLabelPadding),
|
||||
margins);
|
||||
};
|
||||
|
||||
const auto now = base::unixtime::now();
|
||||
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
|
||||
const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
|
||||
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
|
||||
const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
|
||||
|
||||
using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
|
||||
struct State {
|
||||
Buttons expireButtons;
|
||||
Buttons usageButtons;
|
||||
int expireValue = 0;
|
||||
int usageValue = 0;
|
||||
rpl::variable<bool> requestApproval = false;
|
||||
rpl::variable<bool> subscription = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.expireValue = expire,
|
||||
.usageValue = usage,
|
||||
.requestApproval = (data.requestApproval && !isPublic),
|
||||
.subscription = false,
|
||||
});
|
||||
|
||||
const auto requestApproval = (isPublic || subscriptionLocked)
|
||||
? nullptr
|
||||
: container->add(
|
||||
object_ptr<SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_request_approve(),
|
||||
st::settingsButtonNoIcon),
|
||||
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
|
||||
if (requestApproval) {
|
||||
requestApproval->toggleOn(state->requestApproval.value(), true);
|
||||
requestApproval->setClickedCallback([=] {
|
||||
state->requestApproval.force_assign(!requestApproval->toggled());
|
||||
state->subscription.force_assign(false);
|
||||
});
|
||||
addDivider(container, rpl::conditional(
|
||||
state->requestApproval.value(),
|
||||
(isGroup
|
||||
? tr::lng_group_invite_about_approve()
|
||||
: tr::lng_group_invite_about_approve_channel()),
|
||||
(isGroup
|
||||
? tr::lng_group_invite_about_no_approve()
|
||||
: tr::lng_group_invite_about_no_approve_channel())));
|
||||
}
|
||||
auto credits = (Ui::NumberInput*)(nullptr);
|
||||
if (!isPublic && fillSubscription) {
|
||||
Ui::AddSkip(container);
|
||||
const auto &[subscription, input] = fillSubscription();
|
||||
credits = input.get();
|
||||
subscription->toggleOn(state->subscription.value(), true);
|
||||
if (subscriptionLocked) {
|
||||
input->setText(QString::number(data.subscriptionCredits));
|
||||
input->setReadOnly(true);
|
||||
state->subscription.force_assign(true);
|
||||
state->requestApproval.force_assign(false);
|
||||
subscription->setToggleLocked(true);
|
||||
subscription->finishAnimating();
|
||||
}
|
||||
subscription->setClickedCallback([=, show = box->uiShow()] {
|
||||
if (subscriptionLocked) {
|
||||
show->showToast(
|
||||
tr::lng_group_invite_subscription_toast(tr::now));
|
||||
return;
|
||||
}
|
||||
state->subscription.force_assign(!subscription->toggled());
|
||||
state->requestApproval.force_assign(false);
|
||||
});
|
||||
}
|
||||
|
||||
const auto labelField = container->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::defaultInputField,
|
||||
tr::lng_group_invite_label_header(),
|
||||
data.label),
|
||||
style::margins(
|
||||
st::defaultSubsectionTitlePadding.left(),
|
||||
st::defaultVerticalListSkip,
|
||||
st::defaultSubsectionTitlePadding.right(),
|
||||
st::defaultVerticalListSkip * 2));
|
||||
labelField->setMaxLength(kMaxLabelLength);
|
||||
addDivider(container, tr::lng_group_invite_label_about());
|
||||
|
||||
const auto &saveLabel = link.isEmpty()
|
||||
? tr::lng_formatting_link_create
|
||||
: tr::lng_settings_save;
|
||||
box->addButton(saveLabel(), [=] {
|
||||
const auto label = labelField->getLastText();
|
||||
const auto expireDate = (state->expireValue == kMaxLimit)
|
||||
? 0
|
||||
: (state->expireValue < 0)
|
||||
? (base::unixtime::now() - state->expireValue)
|
||||
: state->expireValue;
|
||||
const auto usageLimit = (state->usageValue == kMaxLimit)
|
||||
? 0
|
||||
: state->usageValue;
|
||||
done(InviteLinkFields{
|
||||
.link = link,
|
||||
.label = label,
|
||||
.expireDate = expireDate,
|
||||
.usageLimit = usageLimit,
|
||||
.subscriptionCredits = credits
|
||||
? credits->getLastText().toInt()
|
||||
: 0,
|
||||
.requestApproval = state->requestApproval.current(),
|
||||
.isGroup = isGroup,
|
||||
.isPublic = isPublic,
|
||||
});
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
if (subscriptionLocked) {
|
||||
return;
|
||||
}
|
||||
addTitle(container, tr::lng_group_invite_expire_title());
|
||||
const auto expiresWrap = container->add(
|
||||
object_ptr<VerticalLayout>(container),
|
||||
style::margins(0, 0, 0, st::defaultVerticalListSkip));
|
||||
addDivider(
|
||||
container,
|
||||
tr::lng_group_invite_expire_about());
|
||||
|
||||
const auto usagesSlide = container->add(
|
||||
object_ptr<SlideWrap<VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<VerticalLayout>(container)));
|
||||
const auto usagesInner = usagesSlide->entity();
|
||||
addTitle(usagesInner, tr::lng_group_invite_usage_title());
|
||||
const auto usagesWrap = usagesInner->add(
|
||||
object_ptr<VerticalLayout>(usagesInner),
|
||||
style::margins(0, 0, 0, st::defaultVerticalListSkip));
|
||||
addDivider(usagesInner, tr::lng_group_invite_usage_about());
|
||||
|
||||
static const auto addButton = [](
|
||||
not_null<VerticalLayout*> container,
|
||||
const std::shared_ptr<RadiobuttonGroup> &group,
|
||||
int value,
|
||||
const QString &text) {
|
||||
return container->add(
|
||||
object_ptr<Radiobutton>(
|
||||
container,
|
||||
group,
|
||||
value,
|
||||
text),
|
||||
st::inviteLinkLimitMargin);
|
||||
};
|
||||
|
||||
const auto regenerate = [=] {
|
||||
expireGroup->setValue(state->expireValue);
|
||||
usageGroup->setValue(state->usageValue);
|
||||
|
||||
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
|
||||
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
|
||||
auto defaults = State();
|
||||
for (auto i = begin(expires); i != end(expires); ++i) {
|
||||
if (*i == state->expireValue) {
|
||||
break;
|
||||
} else if (*i == kMaxLimit) {
|
||||
continue;
|
||||
} else if (!*i || (now - *i >= state->expireValue)) {
|
||||
expires.insert(i, state->expireValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto i = begin(usages); i != end(usages); ++i) {
|
||||
if (*i == state->usageValue) {
|
||||
break;
|
||||
} else if (*i == kMaxLimit) {
|
||||
continue;
|
||||
} else if (!*i || *i > state->usageValue) {
|
||||
usages.insert(i, state->usageValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
state->expireButtons.clear();
|
||||
state->usageButtons.clear();
|
||||
for (const auto limit : expires) {
|
||||
const auto text = (limit == kMaxLimit)
|
||||
? tr::lng_group_invite_expire_never(tr::now)
|
||||
: !limit
|
||||
? tr::lng_group_invite_expire_custom(tr::now)
|
||||
: FormatExpireDate(limit);
|
||||
state->expireButtons.emplace(
|
||||
limit,
|
||||
addButton(expiresWrap, expireGroup, limit, text));
|
||||
}
|
||||
for (const auto limit : usages) {
|
||||
const auto text = (limit == kMaxLimit)
|
||||
? tr::lng_group_invite_usage_any(tr::now)
|
||||
: !limit
|
||||
? tr::lng_group_invite_usage_custom(tr::now)
|
||||
: Lang::FormatCountDecimal(limit);
|
||||
state->usageButtons.emplace(
|
||||
limit,
|
||||
addButton(usagesWrap, usageGroup, limit, text));
|
||||
}
|
||||
};
|
||||
|
||||
const auto guard = base::make_weak(box);
|
||||
expireGroup->setChangedCallback([=](int value) {
|
||||
if (value) {
|
||||
state->expireValue = value;
|
||||
return;
|
||||
}
|
||||
expireGroup->setValue(state->expireValue);
|
||||
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
|
||||
const auto save = [=](TimeId result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
if (guard) {
|
||||
state->expireValue = result;
|
||||
regenerate();
|
||||
}
|
||||
box->closeBox();
|
||||
};
|
||||
const auto now = base::unixtime::now();
|
||||
const auto time = (state->expireValue == kMaxLimit)
|
||||
? (now + kDay)
|
||||
: (state->expireValue > now)
|
||||
? state->expireValue
|
||||
: (state->expireValue < 0)
|
||||
? (now - state->expireValue)
|
||||
: (now + kDay);
|
||||
ChooseDateTimeBox(box, {
|
||||
.title = tr::lng_group_invite_expire_after(),
|
||||
.submit = tr::lng_settings_save(),
|
||||
.done = save,
|
||||
.time = time,
|
||||
});
|
||||
}));
|
||||
});
|
||||
usageGroup->setChangedCallback([=](int value) {
|
||||
if (value) {
|
||||
state->usageValue = value;
|
||||
return;
|
||||
}
|
||||
usageGroup->setValue(state->usageValue);
|
||||
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
|
||||
const auto height = st::boxPadding.bottom()
|
||||
+ st::defaultInputField.heightMin
|
||||
+ st::boxPadding.bottom();
|
||||
box->setTitle(tr::lng_group_invite_expire_after());
|
||||
const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
|
||||
box,
|
||||
height));
|
||||
const auto input = CreateChild<NumberInput>(
|
||||
wrap,
|
||||
st::defaultInputField,
|
||||
tr::lng_group_invite_custom_limit(),
|
||||
(state->usageValue == kMaxLimit
|
||||
? QString()
|
||||
: QString::number(state->usageValue)),
|
||||
200'000);
|
||||
wrap->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
input->resize(width, input->height());
|
||||
input->moveToLeft(0, st::boxPadding.bottom());
|
||||
}, input->lifetime());
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
|
||||
const auto save = [=] {
|
||||
const auto value = input->getLastText().toInt();
|
||||
if (value <= 0) {
|
||||
input->showError();
|
||||
return;
|
||||
}
|
||||
if (guard) {
|
||||
state->usageValue = value;
|
||||
regenerate();
|
||||
}
|
||||
box->closeBox();
|
||||
};
|
||||
QObject::connect(input, &NumberInput::submitted, save);
|
||||
box->addButton(tr::lng_settings_save(), save);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
});
|
||||
|
||||
regenerate();
|
||||
|
||||
usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));
|
||||
usagesSlide->finishAnimating();
|
||||
}
|
||||
|
||||
void CreateInviteLinkBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
bool isGroup,
|
||||
bool isPublic,
|
||||
Fn<void(InviteLinkFields)> done) {
|
||||
EditInviteLinkBox(
|
||||
box,
|
||||
std::move(fillSubscription),
|
||||
InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
|
||||
std::move(done));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
45
Telegram/SourceFiles/ui/boxes/edit_invite_link.h
Normal file
45
Telegram/SourceFiles/ui/boxes/edit_invite_link.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
class GenericBox;
|
||||
class NumberInput;
|
||||
class SettingsButton;
|
||||
|
||||
struct InviteLinkFields {
|
||||
QString link;
|
||||
QString label;
|
||||
TimeId expireDate = 0;
|
||||
int usageLimit = 0;
|
||||
int subscriptionCredits = 0;
|
||||
bool requestApproval = false;
|
||||
bool isGroup = false;
|
||||
bool isPublic = false;
|
||||
};
|
||||
|
||||
struct InviteLinkSubscriptionToggle final {
|
||||
not_null<Ui::SettingsButton*> button;
|
||||
not_null<Ui::NumberInput*> amount;
|
||||
};
|
||||
|
||||
void EditInviteLinkBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
const InviteLinkFields &data,
|
||||
Fn<void(InviteLinkFields)> done);
|
||||
|
||||
void CreateInviteLinkBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
bool isGroup,
|
||||
bool isPublic,
|
||||
Fn<void(InviteLinkFields)> done);
|
||||
|
||||
} // namespace Ui
|
||||
155
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp
Normal file
155
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
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 "ui/boxes/edit_invite_link_session.h"
|
||||
|
||||
#include "core/ui_integration.h" // TextContext
|
||||
#include "data/components/credits.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/edit_invite_link.h" // InviteLinkSubscriptionToggle
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_channel_earn.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer) {
|
||||
struct State final {
|
||||
rpl::variable<float64> usdRate = 0;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
const auto currency = u"USD"_q;
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto toggle = container->add(
|
||||
object_ptr<SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_subscription(),
|
||||
st::settingsButtonNoIconLocked),
|
||||
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
|
||||
|
||||
const auto maxCredits = peer->session().appConfig().get<int>(
|
||||
u"stars_subscription_amount_max"_q,
|
||||
2500);
|
||||
|
||||
const auto &st = st::inviteLinkCreditsField;
|
||||
const auto skip = st.textMargins.top() / 2;
|
||||
const auto wrap = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container)));
|
||||
box->setShowFinishedCallback([=] {
|
||||
wrap->toggleOn(toggle->toggledValue());
|
||||
wrap->finishAnimating();
|
||||
});
|
||||
const auto inputContainer = wrap->entity()->add(
|
||||
CreateSkipWidget(container, st.heightMin - skip));
|
||||
const auto input = CreateChild<NumberInput>(
|
||||
inputContainer,
|
||||
st,
|
||||
tr::lng_group_invite_subscription_ph(),
|
||||
QString(),
|
||||
std::pow(QString::number(maxCredits).size(), 10));
|
||||
wrap->toggledValue() | rpl::on_next([=](bool shown) {
|
||||
if (shown) {
|
||||
input->setFocus();
|
||||
}
|
||||
}, input->lifetime());
|
||||
const auto icon = CreateSingleStarWidget(
|
||||
inputContainer,
|
||||
st.style.font->height);
|
||||
const auto priceOverlay = Ui::CreateChild<Ui::RpWidget>(inputContainer);
|
||||
priceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
inputContainer->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
input->resize(
|
||||
size.width() - rect::m::sum::h(st::boxRowPadding),
|
||||
st.heightMin);
|
||||
input->moveToLeft(st::boxRowPadding.left(), -skip);
|
||||
icon->moveToLeft(
|
||||
st::boxRowPadding.left(),
|
||||
input->pos().y() + st.textMargins.top());
|
||||
priceOverlay->resize(size);
|
||||
}, input->lifetime());
|
||||
ToggleChildrenVisibility(inputContainer, true);
|
||||
QObject::connect(input, &Ui::MaskedInputField::changed, [=] {
|
||||
const auto amount = input->getLastText().toDouble();
|
||||
if (amount > maxCredits) {
|
||||
input->setText(QString::number(maxCredits));
|
||||
}
|
||||
priceOverlay->update();
|
||||
});
|
||||
priceOverlay->paintRequest(
|
||||
) | rpl::on_next([=, right = st::boxRowPadding.right()] {
|
||||
if (state->usdRate.current() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto amount = input->getLastText().toDouble();
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto text = tr::lng_group_invite_subscription_price(
|
||||
tr::now,
|
||||
lt_cost,
|
||||
Ui::FillAmountAndCurrency(
|
||||
amount * state->usdRate.current(),
|
||||
currency));
|
||||
auto p = QPainter(priceOverlay);
|
||||
p.setFont(st.placeholderFont);
|
||||
p.setPen(st.placeholderFg);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto m = QMargins(0, skip, right, 0);
|
||||
p.drawText(priceOverlay->rect() - m, text, style::al_right);
|
||||
}, priceOverlay->lifetime());
|
||||
|
||||
state->usdRate = peer->session().credits().rateValue(peer);
|
||||
|
||||
auto about = object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_group_invite_subscription_about(
|
||||
lt_link,
|
||||
tr::lng_group_invite_subscription_about_link(
|
||||
lt_emoji,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
|
||||
tr::rich
|
||||
) | rpl::map([](TextWithEntities text) {
|
||||
return tr::link(
|
||||
std::move(text),
|
||||
tr::lng_group_invite_subscription_about_url(tr::now));
|
||||
}),
|
||||
tr::rich),
|
||||
st::boxDividerLabel);
|
||||
Ui::AddSkip(wrap->entity());
|
||||
Ui::AddSkip(wrap->entity());
|
||||
container->add(object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
std::move(about),
|
||||
st::defaultBoxDividerLabelPadding));
|
||||
return { toggle, input };
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
23
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h
Normal file
23
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class SettingsButton;
|
||||
|
||||
struct InviteLinkSubscriptionToggle;
|
||||
|
||||
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
} // namespace Ui
|
||||
997
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
997
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
@@ -0,0 +1,997 @@
|
||||
/*
|
||||
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 "ui/boxes/peer_qr_box.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "data/data_cloud_themes.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "qr/qr_generate.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_intro.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtCore/QMimeData>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtSvg/QSvgRenderer>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
using Colors = std::vector<QColor>;
|
||||
|
||||
[[nodiscard]] QMargins NoPhotoBackgroundMargins() {
|
||||
return QMargins(
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.right(),
|
||||
st::profileQrBackgroundMargins.bottom());
|
||||
}
|
||||
|
||||
[[nodiscard]] style::font CreateFont(int size, int scale) {
|
||||
return style::font(
|
||||
style::ConvertScale(size, scale),
|
||||
st::profileQrFont->flags(),
|
||||
st::profileQrFont->family());
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage TelegramQr(
|
||||
const Qr::Data &data,
|
||||
int pixel,
|
||||
int max,
|
||||
bool hasWhiteBackground) {
|
||||
Expects(data.size > 0);
|
||||
|
||||
constexpr auto kCenterRatio = 0.175;
|
||||
|
||||
if (max > 0 && data.size * pixel > max) {
|
||||
pixel = std::max(max / data.size, 1);
|
||||
}
|
||||
auto qr = Qr::Generate(
|
||||
data,
|
||||
pixel * style::DevicePixelRatio(),
|
||||
hasWhiteBackground ? Qt::transparent : Qt::black,
|
||||
hasWhiteBackground ? Qt::white : Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&qr);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto svg = QSvgRenderer(u":/gui/plane_white.svg"_q);
|
||||
const auto size = qr.rect().size();
|
||||
const auto centerRect = Rect(size)
|
||||
- Margins((size.width() - (size.width() * kCenterRatio)) / 2);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
if (hasWhiteBackground) {
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
p.drawEllipse(centerRect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
svg.render(&p, centerRect);
|
||||
} else {
|
||||
p.drawEllipse(centerRect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
svg.render(&p, centerRect);
|
||||
}
|
||||
}
|
||||
return qr;
|
||||
}
|
||||
|
||||
[[nodiscard]] QMargins RoundedMargins(
|
||||
const QMargins &backgroundMargins,
|
||||
int photoSize,
|
||||
int textMaxHeight) {
|
||||
return (textMaxHeight
|
||||
? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))
|
||||
: photoSize
|
||||
? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)
|
||||
: Margins(backgroundMargins.left()));
|
||||
}
|
||||
|
||||
void Paint(
|
||||
QPainter &p,
|
||||
const style::font &font,
|
||||
const QString &text,
|
||||
const Colors &backgroundColors,
|
||||
const QMargins &backgroundMargins,
|
||||
const QImage &qrImage,
|
||||
const QRect &qrRect,
|
||||
int qrMaxSize,
|
||||
int qrPixel,
|
||||
int radius,
|
||||
int textMaxHeight,
|
||||
int photoSize,
|
||||
bool hasWhiteBackground) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(hasWhiteBackground ? Qt::white : Qt::transparent);
|
||||
const auto roundedRect = qrRect
|
||||
+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);
|
||||
p.drawRoundedRect(roundedRect, radius, radius);
|
||||
if (!qrImage.isNull() && !backgroundColors.empty()) {
|
||||
constexpr auto kDuration = crl::time(10000);
|
||||
const auto angle = (crl::now() % kDuration)
|
||||
/ float64(kDuration) * 360.0;
|
||||
const auto gradientRotation = int(angle / 45.) * 45;
|
||||
const auto gradientRotationAdd = angle - gradientRotation;
|
||||
|
||||
const auto textAdditionalWidth = backgroundMargins.left();
|
||||
auto back = Images::GenerateGradient(
|
||||
qrRect.size() + QSize(textAdditionalWidth, 0),
|
||||
backgroundColors,
|
||||
gradientRotation,
|
||||
1. - (gradientRotationAdd / 45.));
|
||||
if (hasWhiteBackground) {
|
||||
p.drawImage(qrRect, back);
|
||||
}
|
||||
const auto coloredSize = QSize(back.width(), textMaxHeight);
|
||||
auto colored = QImage(
|
||||
coloredSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
colored.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
colored.fill(Qt::transparent);
|
||||
if (textMaxHeight) {
|
||||
// '@' + QString(32, 'W');
|
||||
auto p = QPainter(&colored);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::black);
|
||||
p.setFont(font);
|
||||
auto option = QTextOption(style::al_center);
|
||||
option.setWrapMode(QTextOption::WrapAnywhere);
|
||||
p.drawText(Rect(coloredSize), text, option);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(0, -back.height() + textMaxHeight, back);
|
||||
}
|
||||
if (!hasWhiteBackground) {
|
||||
auto copy = qrImage;
|
||||
{
|
||||
auto p = QPainter(©);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(Rect(copy.size()), back);
|
||||
}
|
||||
p.drawImage(qrRect, copy);
|
||||
} else {
|
||||
p.drawImage(qrRect, qrImage);
|
||||
}
|
||||
if (textMaxHeight) {
|
||||
p.drawImage(
|
||||
qrRect.x() - textAdditionalWidth / 2,
|
||||
rect::bottom(qrRect)
|
||||
+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))
|
||||
- textMaxHeight) / 2,
|
||||
colored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> PrepareQrWidget(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Ui::RpWidget*> topWidget,
|
||||
rpl::producer<int> fontSizeValue,
|
||||
rpl::producer<bool> userpicToggled,
|
||||
rpl::producer<bool> backgroundToggled,
|
||||
rpl::producer<QString> username,
|
||||
rpl::producer<QString> links,
|
||||
rpl::producer<Colors> bgs,
|
||||
rpl::producer<QString> about) {
|
||||
const auto divider = container->add(
|
||||
object_ptr<Ui::BoxContentDivider>(container));
|
||||
struct State final {
|
||||
explicit State(Fn<void()> callback) : updating(callback) {
|
||||
updating.start();
|
||||
}
|
||||
|
||||
Ui::Animations::Basic updating;
|
||||
style::font font;
|
||||
QImage qrImage;
|
||||
Colors backgroundColors;
|
||||
QString text;
|
||||
QMargins backgroundMargins;
|
||||
int textWidth = 0;
|
||||
int textMaxHeight = 0;
|
||||
int photoSize = 0;
|
||||
bool backgroundToggled = false;
|
||||
};
|
||||
const auto result = Ui::CreateChild<Ui::RpWidget>(divider);
|
||||
topWidget->setParent(result);
|
||||
topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
const auto state = result->lifetime().make_state<State>(
|
||||
[=] { result->update(); });
|
||||
const auto qrMaxSize = st::boxWideWidth
|
||||
- rect::m::sum::h(st::boxRowPadding)
|
||||
- rect::m::sum::h(st::profileQrBackgroundMargins);
|
||||
const auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
divider,
|
||||
st::creditsBoxAboutDivider);
|
||||
rpl::combine(
|
||||
std::move(fontSizeValue),
|
||||
std::move(userpicToggled),
|
||||
std::move(backgroundToggled),
|
||||
std::move(username),
|
||||
std::move(bgs),
|
||||
std::move(links),
|
||||
std::move(about),
|
||||
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged())
|
||||
) | rpl::on_next([=](
|
||||
int fontSize,
|
||||
bool userpicToggled,
|
||||
bool backgroundToggled,
|
||||
const QString &username,
|
||||
const Colors &backgroundColors,
|
||||
const QString &link,
|
||||
const QString &about,
|
||||
const auto &) {
|
||||
state->font = CreateFont(fontSize, style::Scale());
|
||||
state->backgroundToggled = backgroundToggled;
|
||||
state->backgroundMargins = userpicToggled
|
||||
? st::profileQrBackgroundMargins
|
||||
: NoPhotoBackgroundMargins();
|
||||
state->photoSize = userpicToggled
|
||||
? st::defaultUserpicButton.photoSize
|
||||
: 0;
|
||||
state->backgroundColors = backgroundColors;
|
||||
state->text = username.toUpper();
|
||||
state->textWidth = state->font->width(state->text);
|
||||
if (!link.isEmpty()) {
|
||||
const auto remainder = qrMaxSize % st::introQrPixel;
|
||||
const auto downTo = remainder
|
||||
? qrMaxSize - remainder
|
||||
: qrMaxSize;
|
||||
state->qrImage = TelegramQr(
|
||||
Qr::Encode(link.toUtf8(), Qr::Redundancy::Default),
|
||||
st::introQrPixel,
|
||||
downTo,
|
||||
backgroundToggled).scaled(
|
||||
Size(qrMaxSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
} else {
|
||||
auto image = QImage(
|
||||
Size(qrMaxSize * style::DevicePixelRatio()),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::white);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
state->qrImage = std::move(image);
|
||||
}
|
||||
const auto resultWidth = qrMaxSize
|
||||
+ rect::m::sum::h(state->backgroundMargins);
|
||||
{
|
||||
aboutLabel->setText(about);
|
||||
aboutLabel->resizeToWidth(resultWidth);
|
||||
}
|
||||
const auto textMaxWidth = state->backgroundMargins.left()
|
||||
+ (state->qrImage.width() / style::DevicePixelRatio());
|
||||
const auto lines = int(state->textWidth / textMaxWidth) + 1;
|
||||
state->textMaxHeight = state->textWidth
|
||||
? (state->font->height * lines)
|
||||
: 0;
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
state->backgroundMargins,
|
||||
state->photoSize,
|
||||
state->textMaxHeight);
|
||||
result->resize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize
|
||||
+ rect::m::sum::v(whiteMargins) // White.
|
||||
+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.
|
||||
+ state->photoSize / 2
|
||||
+ aboutLabel->height());
|
||||
|
||||
divider->resize(container->width(), result->height());
|
||||
result->moveToLeft((container->width() - result->width()) / 2, 0);
|
||||
topWidget->setVisible(userpicToggled);
|
||||
topWidget->moveToLeft(0, std::numeric_limits<int>::min());
|
||||
topWidget->raise();
|
||||
|
||||
aboutLabel->raise();
|
||||
aboutLabel->moveToLeft(
|
||||
result->x(),
|
||||
divider->height()
|
||||
- aboutLabel->height()
|
||||
- st::defaultBoxDividerLabelPadding.top());
|
||||
}, container->lifetime());
|
||||
result->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
auto p = QPainter(result);
|
||||
const auto size = (state->qrImage.size() / style::DevicePixelRatio());
|
||||
const auto qrRect = Rect(
|
||||
(result->width() - size.width()) / 2,
|
||||
state->backgroundMargins.top() + state->photoSize / 2,
|
||||
size);
|
||||
p.translate(
|
||||
0,
|
||||
st::profileQrBackgroundPadding.top() + state->photoSize / 2);
|
||||
Paint(
|
||||
p,
|
||||
state->font,
|
||||
state->text,
|
||||
state->backgroundColors,
|
||||
state->backgroundMargins,
|
||||
state->qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
st::introQrPixel,
|
||||
st::profileQrBackgroundRadius,
|
||||
state->textMaxHeight,
|
||||
state->photoSize,
|
||||
state->backgroundToggled);
|
||||
if (!state->photoSize) {
|
||||
return;
|
||||
}
|
||||
const auto photoSize = state->photoSize;
|
||||
const auto top = Ui::GrabWidget(
|
||||
topWidget,
|
||||
QRect(),
|
||||
Qt::transparent).scaled(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
p.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);
|
||||
}, result->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Fn<void(int)> AddDotsToSlider(
|
||||
not_null<Ui::ContinuousSlider*> slider,
|
||||
const style::MediaSlider &st,
|
||||
int count) {
|
||||
const auto lineWidth = st::lineWidth;
|
||||
const auto smallSize = Size(st.seekSize.height() - st.width);
|
||||
auto smallDots = std::vector<not_null<Ui::RpWidget*>>();
|
||||
smallDots.reserve(count - 1);
|
||||
const auto paintSmall = [=](QPainter &p, const QBrush &brush) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(brush);
|
||||
p.drawEllipse(Rect(smallSize) - Margins(lineWidth));
|
||||
};
|
||||
for (auto i = 0; i < count - 1; i++) {
|
||||
smallDots.push_back(
|
||||
Ui::CreateChild<Ui::RpWidget>(slider->parentWidget()));
|
||||
const auto dot = smallDots.back();
|
||||
dot->resize(smallSize);
|
||||
dot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
dot->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(dot);
|
||||
const auto fg = (slider->value() > (i / float64(count - 1)))
|
||||
? st.activeFg
|
||||
: st.inactiveFg;
|
||||
paintSmall(p, fg);
|
||||
}, dot->lifetime());
|
||||
}
|
||||
const auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());
|
||||
bigDot->resize(st.seekSize);
|
||||
bigDot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
bigDot->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(bigDot);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(st.activeFg);
|
||||
p.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));
|
||||
}, bigDot->lifetime());
|
||||
|
||||
return [=](int index) {
|
||||
const auto g = slider->geometry();
|
||||
const auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;
|
||||
const auto smallTop = g.y()
|
||||
+ (g.height() - smallSize.height()) / 2;
|
||||
for (auto i = 0; i < count; ++i) {
|
||||
if (index == i) {
|
||||
const auto x = ((g.width() - bigDot->width()) * i)
|
||||
/ float64(count - 1);
|
||||
bigDot->move(g.x() + std::round(x), bigTop);
|
||||
} else {
|
||||
const auto k = (i < index) ? i : i - 1;
|
||||
const auto w = smallDots[k]->width();
|
||||
smallDots[k]->move(
|
||||
g.x() + ((g.width() - w) * i) / (count - 1),
|
||||
smallTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about) {
|
||||
const auto window = Core::App().findWindow(box);
|
||||
const auto controller = window ? window->sessionController() : nullptr;
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
box->setStyle(st::giveawayGiftCodeBox);
|
||||
box->setNoContentMargin(true);
|
||||
box->setWidth(st::aboutWidth);
|
||||
box->setTitle(tr::lng_group_invite_context_qr());
|
||||
box->verticalLayout()->resizeToWidth(box->width());
|
||||
struct State {
|
||||
Ui::RpWidget* saveButton = nullptr;
|
||||
rpl::variable<bool> saveButtonBusy = false;
|
||||
rpl::variable<bool> userpicToggled = true;
|
||||
rpl::variable<bool> backgroundToggled = true;
|
||||
rpl::variable<Colors> bgs;
|
||||
Ui::Animations::Simple animation;
|
||||
rpl::variable<int> chosen = 0;
|
||||
rpl::variable<int> scaleValue = 0;
|
||||
rpl::variable<int> fontSizeValue = 28;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->userpicToggled = !(customLink || !peer);
|
||||
|
||||
const auto usernameValue = [=] {
|
||||
return (customLink || !peer)
|
||||
? (rpl::single(QString()) | rpl::type_erased)
|
||||
: Info::Profile::UsernameValue(peer, true) | rpl::map(
|
||||
[](const auto &username) { return username.text; });
|
||||
};
|
||||
const auto linkValue = [=] {
|
||||
return customLink
|
||||
? rpl::single(*customLink)
|
||||
: peer
|
||||
? Info::Profile::LinkValue(peer, true) | rpl::map(
|
||||
[](const auto &link) { return link.text; })
|
||||
: (rpl::single(QString()) | rpl::type_erased);
|
||||
};
|
||||
|
||||
const auto userpic = Ui::CreateChild<Ui::RpWidget>(box);
|
||||
const auto userpicSize = st::defaultUserpicButton.photoSize;
|
||||
userpic->resize(Size(userpicSize));
|
||||
const auto userpicMedia = Ui::MakeUserpicThumbnail(peer
|
||||
? peer
|
||||
: controller->session().user().get());
|
||||
userpicMedia->subscribeToUpdates(
|
||||
crl::guard(userpic, [=] { userpic->update(); }));
|
||||
userpic->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(userpic);
|
||||
p.drawImage(0, 0, userpicMedia->image(userpicSize));
|
||||
}, userpic->lifetime());
|
||||
|
||||
linkValue() | rpl::on_next([=](const QString &link) {
|
||||
if (link.isEmpty()) {
|
||||
box->showFinishes() | rpl::on_next([=] {
|
||||
box->closeBox();
|
||||
}, box->lifetime());
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
userpic->setVisible(peer != nullptr);
|
||||
PrepareQrWidget(
|
||||
box->verticalLayout(),
|
||||
userpic,
|
||||
state->fontSizeValue.value(),
|
||||
state->userpicToggled.value(),
|
||||
state->backgroundToggled.value(),
|
||||
usernameValue(),
|
||||
linkValue(),
|
||||
state->bgs.value(),
|
||||
about ? std::move(about) : rpl::single(QString()));
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_userpic_builder_color_subtitle());
|
||||
|
||||
const auto themesContainer = box->addRow(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
|
||||
const auto activewidth = int(
|
||||
(st::defaultInputField.borderActive + st::lineWidth) * 0.9);
|
||||
const auto size = st::chatThemePreviewSize.width();
|
||||
|
||||
const auto fill = [=](const std::vector<Data::CloudTheme> &cloudThemes) {
|
||||
while (themesContainer->count()) {
|
||||
delete themesContainer->widgetAt(0);
|
||||
}
|
||||
|
||||
struct State {
|
||||
Colors colors;
|
||||
QImage image;
|
||||
};
|
||||
constexpr auto kMaxInRow = 4;
|
||||
constexpr auto kMaxColors = 4;
|
||||
auto row = (Ui::RpWidget*)(nullptr);
|
||||
auto counter = 0;
|
||||
const auto spacing = (0
|
||||
+ (box->width() - rect::m::sum::h(st::boxRowPadding))
|
||||
- (kMaxInRow * size)) / (kMaxInRow + 1);
|
||||
|
||||
auto colorsCollection = ranges::views::all(
|
||||
cloudThemes
|
||||
) | ranges::views::transform([](const auto &cloudTheme) -> Colors {
|
||||
const auto it = cloudTheme.settings.find(
|
||||
Data::CloudThemeType::Light);
|
||||
if (it == end(cloudTheme.settings)) {
|
||||
return Colors();
|
||||
}
|
||||
const auto colors = it->second.paper
|
||||
? it->second.paper->backgroundColors()
|
||||
: Colors();
|
||||
if (colors.size() != kMaxColors) {
|
||||
return Colors();
|
||||
}
|
||||
return colors;
|
||||
}) | ranges::views::filter([](const Colors &colors) {
|
||||
return !colors.empty();
|
||||
}) | ranges::to_vector;
|
||||
Expects(!colorsCollection.empty());
|
||||
colorsCollection[0] = Colors{
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg2->c,
|
||||
st::premiumButtonBg3->c,
|
||||
};
|
||||
// colorsCollection.push_back(Colors{
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// });
|
||||
|
||||
for (const auto &colors : colorsCollection) {
|
||||
if (state->bgs.current().empty()) {
|
||||
state->bgs = colors;
|
||||
}
|
||||
|
||||
if (counter % kMaxInRow == 0) {
|
||||
Ui::AddSkip(themesContainer);
|
||||
row = themesContainer->add(
|
||||
object_ptr<Ui::RpWidget>(themesContainer));
|
||||
row->resize(size, size);
|
||||
}
|
||||
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
|
||||
widget->setClickedCallback([=] {
|
||||
state->chosen = counter;
|
||||
widget->update();
|
||||
state->animation.stop();
|
||||
state->animation.start([=](float64 value) {
|
||||
const auto was = state->bgs.current();
|
||||
const auto &now = colors;
|
||||
if (was.size() == now.size()
|
||||
&& was.size() == kMaxColors) {
|
||||
state->bgs = Colors({
|
||||
anim::color(was[0], now[0], value),
|
||||
anim::color(was[1], now[1], value),
|
||||
anim::color(was[2], now[2], value),
|
||||
anim::color(was[3], now[3], value),
|
||||
});
|
||||
}
|
||||
},
|
||||
0.,
|
||||
1.,
|
||||
st::shakeDuration);
|
||||
});
|
||||
state->chosen.value() | rpl::combine_previous(
|
||||
) | rpl::filter([=](int i, int k) {
|
||||
return i == counter || k == counter;
|
||||
}) | rpl::on_next([=] {
|
||||
widget->update();
|
||||
}, widget->lifetime());
|
||||
widget->resize(size, size);
|
||||
widget->moveToLeft(
|
||||
spacing + ((counter % kMaxInRow) * (size + spacing)),
|
||||
0);
|
||||
widget->show();
|
||||
|
||||
const auto cornersMask = Images::CornersMask(
|
||||
st::roundRadiusLarge * style::DevicePixelRatio());
|
||||
const auto back = [&] {
|
||||
auto gradient = Images::GenerateGradient(
|
||||
Size(size - activewidth * 5) * style::DevicePixelRatio(),
|
||||
colors,
|
||||
0,
|
||||
0);
|
||||
gradient.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
auto result = Images::Round(std::move(gradient), cornersMask);
|
||||
const auto rect = Rect(
|
||||
result.size() / style::DevicePixelRatio());
|
||||
auto colored = result;
|
||||
colored.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&colored);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
st::profileQrIcon.paintInCenter(p, rect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(0, 0, result);
|
||||
}
|
||||
auto temp = result;
|
||||
temp.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&temp);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(st::premiumButtonFg);
|
||||
p.setBrush(st::premiumButtonFg);
|
||||
const auto size = st::profileQrIcon.width() * 1.5;
|
||||
const auto margins = Margins((rect.width() - size) / 2);
|
||||
const auto inner = rect - margins;
|
||||
p.drawRoundedRect(
|
||||
inner,
|
||||
st::roundRadiusLarge,
|
||||
st::roundRadiusLarge);
|
||||
p.drawImage(0, 0, colored);
|
||||
}
|
||||
{
|
||||
auto p = QPainter(&result);
|
||||
p.drawImage(0, 0, temp);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
widget->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(widget);
|
||||
const auto rect = widget->rect() - Margins(activewidth * 2.5);
|
||||
p.drawImage(rect.x(), rect.y(), back);
|
||||
if (state->chosen.current() == counter) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::activeLineFg->p;
|
||||
pen.setWidth(st::defaultInputField.borderActive);
|
||||
p.setPen(pen);
|
||||
const auto r = st::roundRadiusLarge
|
||||
+ activewidth * 2.1 * style::DevicePixelRatio();
|
||||
p.drawRoundedRect(
|
||||
widget->rect() - Margins(pen.width()),
|
||||
r,
|
||||
r);
|
||||
}
|
||||
}, widget->lifetime());
|
||||
counter++;
|
||||
}
|
||||
Ui::AddSkip(themesContainer);
|
||||
Ui::AddSkip(themesContainer);
|
||||
themesContainer->resizeToWidth(box->width());
|
||||
};
|
||||
|
||||
const auto themes = &controller->session().data().cloudThemes();
|
||||
const auto &list = themes->chatThemes();
|
||||
if (!list.empty()) {
|
||||
fill(list);
|
||||
} else {
|
||||
themes->refreshChatThemes();
|
||||
themes->chatThemesUpdated(
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
fill(themes->chatThemes());
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddDivider(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_quality());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
constexpr auto kMaxQualities = 3;
|
||||
{
|
||||
const auto seekSize = st::settingsScale.seekSize.height();
|
||||
const auto &labelSt = st::defaultFlatLabel;
|
||||
const auto labels = box->verticalLayout()->add(
|
||||
Ui::CreateSkipWidget(
|
||||
box,
|
||||
labelSt.style.font->height + labelSt.style.font->descent),
|
||||
st::boxRowPadding);
|
||||
const auto left = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality1(),
|
||||
labelSt);
|
||||
const auto middle = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality2(),
|
||||
labelSt);
|
||||
const auto right = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality3(),
|
||||
labelSt);
|
||||
labels->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
left->moveToLeft(0, 0);
|
||||
middle->moveToLeft((size.width() - middle->width()) / 2, 0);
|
||||
right->moveToRight(0, 0);
|
||||
}, labels->lifetime());
|
||||
|
||||
const auto slider = box->verticalLayout()->add(
|
||||
object_ptr<Ui::MediaSliderWheelless>(
|
||||
box->verticalLayout(),
|
||||
st::settingsScale),
|
||||
st::boxRowPadding);
|
||||
slider->resize(slider->width(), seekSize);
|
||||
const auto active = st::windowActiveTextFg->c;
|
||||
const auto inactive = st::windowSubTextFg->c;
|
||||
const auto colorize = [=](int index) {
|
||||
if (index == 0) {
|
||||
left->setTextColorOverride(active);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 1) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(active);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 2) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(active);
|
||||
}
|
||||
};
|
||||
const auto updateGeometry = AddDotsToSlider(
|
||||
slider,
|
||||
st::settingsScale,
|
||||
kMaxQualities);
|
||||
slider->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
updateGeometry(int(slider->value() * (kMaxQualities - 1)));
|
||||
}, box->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=] {
|
||||
colorize(0);
|
||||
updateGeometry(0);
|
||||
});
|
||||
slider->setPseudoDiscrete(
|
||||
kMaxQualities,
|
||||
[=](int index) { return index; },
|
||||
0,
|
||||
[=](int scale) {
|
||||
state->scaleValue = scale;
|
||||
colorize(scale);
|
||||
updateGeometry(scale);
|
||||
},
|
||||
[](int) {});
|
||||
}
|
||||
{
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_font_size());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
const auto seekSize = st::settingsScale.seekSize.height();
|
||||
|
||||
const auto slider = box->verticalLayout()->add(
|
||||
object_ptr<Ui::MediaSliderWheelless>(
|
||||
box->verticalLayout(),
|
||||
st::settingsScale),
|
||||
st::boxRowPadding);
|
||||
slider->resize(slider->width(), seekSize);
|
||||
const auto kSizeAmount = 8;
|
||||
const auto kMinSize = 20;
|
||||
const auto kMaxSize = 36;
|
||||
const auto kStep = (kMaxSize - kMinSize) / (kSizeAmount - 1);
|
||||
const auto updateGeometry = AddDotsToSlider(
|
||||
slider,
|
||||
st::settingsScale,
|
||||
kSizeAmount);
|
||||
const auto fontSizeToIndex = [=](int fontSize) {
|
||||
return (fontSize - kMinSize) / kStep;
|
||||
};
|
||||
const auto indexToFontSize = [=](int index) {
|
||||
return kMinSize + index * kStep;
|
||||
};
|
||||
slider->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
|
||||
}, box->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=] {
|
||||
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
|
||||
});
|
||||
slider->setPseudoDiscrete(
|
||||
kSizeAmount,
|
||||
[=](int index) { return indexToFontSize(index); },
|
||||
state->fontSizeValue.current(),
|
||||
[=](int fontSize) {
|
||||
state->fontSizeValue = fontSize;
|
||||
updateGeometry(fontSizeToIndex(fontSize));
|
||||
},
|
||||
[](int) {});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
if (peer) {
|
||||
const auto userpicToggle = box->verticalLayout()->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
box->verticalLayout(),
|
||||
(peer->isUser()
|
||||
? tr::lng_mediaview_profile_photo
|
||||
: (peer->isChannel() && !peer->isMegagroup())
|
||||
? tr::lng_mediaview_channel_photo
|
||||
: tr::lng_mediaview_group_photo)(),
|
||||
st::settingsButtonNoIcon));
|
||||
userpicToggle->toggleOn(state->userpicToggled.value(), true);
|
||||
userpicToggle->setClickedCallback([=] {
|
||||
state->userpicToggled = !state->userpicToggled.current();
|
||||
});
|
||||
}
|
||||
{
|
||||
const auto backgroundToggle = box->verticalLayout()->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_transparent_background(),
|
||||
st::settingsButtonNoIcon));
|
||||
backgroundToggle->toggleOn(
|
||||
state->backgroundToggled.value() | rpl::map(!rpl::mappers::_1),
|
||||
true);
|
||||
backgroundToggle->setClickedCallback([=] {
|
||||
state->backgroundToggled = !state->backgroundToggled.current();
|
||||
});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
auto buttonText = rpl::conditional(
|
||||
state->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),
|
||||
rpl::single(QString()),
|
||||
tr::lng_chat_link_copy());
|
||||
const auto show = controller->uiShow();
|
||||
state->saveButton = box->addButton(std::move(buttonText), [=] {
|
||||
if (state->saveButtonBusy.current()) {
|
||||
return;
|
||||
}
|
||||
state->saveButtonBusy = true;
|
||||
const auto userpicToggled = state->userpicToggled.current();
|
||||
const auto backgroundToggled = state->backgroundToggled.current();
|
||||
const auto scale = style::kScaleDefault
|
||||
* (kMaxQualities + int(state->scaleValue.current() * 2));
|
||||
const auto divider = std::max(100, style::Scale())
|
||||
/ style::kScaleDefault;
|
||||
const auto profileQrBackgroundRadius = style::ConvertScale(
|
||||
st::profileQrBackgroundRadius / divider,
|
||||
scale);
|
||||
const auto introQrPixel = style::ConvertScale(
|
||||
st::introQrPixel / divider,
|
||||
scale);
|
||||
const auto lineWidth = style::ConvertScale(
|
||||
st::lineWidth / divider,
|
||||
scale);
|
||||
const auto boxWideWidth = style::ConvertScale(
|
||||
st::boxWideWidth / divider,
|
||||
scale);
|
||||
const auto createMargins = [&](const style::margins &margins) {
|
||||
return QMargins(
|
||||
style::ConvertScale(margins.left() / divider, scale),
|
||||
style::ConvertScale(margins.top() / divider, scale),
|
||||
style::ConvertScale(margins.right() / divider, scale),
|
||||
style::ConvertScale(margins.bottom() / divider, scale));
|
||||
};
|
||||
const auto boxRowPadding = createMargins(st::boxRowPadding);
|
||||
const auto backgroundMargins = userpicToggled
|
||||
? createMargins(st::profileQrBackgroundMargins)
|
||||
: createMargins(NoPhotoBackgroundMargins());
|
||||
const auto qrMaxSize = boxWideWidth
|
||||
- rect::m::sum::h(boxRowPadding)
|
||||
- rect::m::sum::h(backgroundMargins);
|
||||
const auto photoSize = userpicToggled
|
||||
? style::ConvertScale(
|
||||
st::defaultUserpicButton.photoSize / divider,
|
||||
scale)
|
||||
: 0;
|
||||
|
||||
const auto font = CreateFont(state->fontSizeValue.current(), scale);
|
||||
const auto username = rpl::variable<QString>(
|
||||
usernameValue()).current().toUpper();
|
||||
const auto link = rpl::variable<QString>(linkValue());
|
||||
const auto textWidth = font->width(username);
|
||||
const auto top = photoSize
|
||||
? userpicMedia->image(photoSize)
|
||||
: QImage();
|
||||
const auto weak = base::make_weak(box);
|
||||
|
||||
crl::async([=] {
|
||||
const auto qrImage = TelegramQr(
|
||||
Qr::Encode(
|
||||
link.current().toUtf8(),
|
||||
Qr::Redundancy::Default),
|
||||
introQrPixel,
|
||||
qrMaxSize,
|
||||
backgroundToggled);
|
||||
const auto textMaxWidth = backgroundMargins.left()
|
||||
+ (qrImage.width() / style::DevicePixelRatio());
|
||||
const auto lines = int(textWidth / textMaxWidth) + 1;
|
||||
const auto textMaxHeight = textWidth ? font->height * lines : 0;
|
||||
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
backgroundMargins,
|
||||
photoSize,
|
||||
textMaxHeight);
|
||||
const auto resultSize = QSize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);
|
||||
|
||||
const auto qrImageSize = qrImage.size()
|
||||
/ style::DevicePixelRatio();
|
||||
const auto qrRect = Rect(
|
||||
(resultSize.width() - qrImageSize.width()) / 2,
|
||||
whiteMargins.top() + photoSize / 2,
|
||||
qrImageSize);
|
||||
|
||||
auto image = QImage(
|
||||
resultSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
p.translate(0, lineWidth); // Bad.
|
||||
Paint(
|
||||
p,
|
||||
font,
|
||||
username,
|
||||
state->bgs.current(),
|
||||
backgroundMargins,
|
||||
qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
introQrPixel,
|
||||
profileQrBackgroundRadius,
|
||||
textMaxHeight,
|
||||
photoSize,
|
||||
backgroundToggled);
|
||||
|
||||
if (userpicToggled) {
|
||||
p.drawImage((resultSize.width() - photoSize) / 2, 0, top);
|
||||
}
|
||||
}
|
||||
crl::on_main(weak, [=] {
|
||||
state->saveButtonBusy = false;
|
||||
auto mime = std::make_unique<QMimeData>();
|
||||
mime->setImageData(std::move(image));
|
||||
QGuiApplication::clipboard()->setMimeData(mime.release());
|
||||
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (const auto saveButton = state->saveButton) {
|
||||
using namespace Info::Statistics;
|
||||
const auto loadingAnimation = InfiniteRadialAnimationWidget(
|
||||
saveButton,
|
||||
saveButton->height() / 2);
|
||||
AddChildToWidgetCenter(saveButton, loadingAnimation);
|
||||
loadingAnimation->showOn(state->saveButtonBusy.value());
|
||||
}
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void DefaultShowFillPeerQrBoxCallback(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
PeerData *peer) {
|
||||
if (peer && !peer->username().isEmpty()) {
|
||||
show->show(Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
27
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
27
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class Show;
|
||||
|
||||
void DefaultShowFillPeerQrBoxCallback(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
PeerData *peer);
|
||||
|
||||
void FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about);
|
||||
|
||||
} // namespace Ui
|
||||
149
Telegram/SourceFiles/ui/boxes/rate_call_box.cpp
Normal file
149
Telegram/SourceFiles/ui/boxes/rate_call_box.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 "ui/boxes/rate_call_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxRating = 5;
|
||||
constexpr auto kRateCallCommentLengthMax = 200;
|
||||
|
||||
} // namespace
|
||||
|
||||
RateCallBox::RateCallBox(QWidget*, InputSubmitSettings sendWay)
|
||||
: _sendWay(sendWay) {
|
||||
}
|
||||
|
||||
void RateCallBox::prepare() {
|
||||
setTitle(tr::lng_call_rate_label());
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
for (auto i = 0; i < kMaxRating; ++i) {
|
||||
_stars.emplace_back(this, st::callRatingStar);
|
||||
_stars.back()->setClickedCallback([this, value = i + 1] {
|
||||
ratingChanged(value);
|
||||
});
|
||||
_stars.back()->show();
|
||||
}
|
||||
|
||||
updateMaxHeight();
|
||||
}
|
||||
|
||||
void RateCallBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
const auto starsWidth = (_stars.size() * st::callRatingStar.width);
|
||||
auto starLeft = (width() - starsWidth) / 2;
|
||||
const auto starTop = st::callRatingStarTop;
|
||||
for (auto &star : _stars) {
|
||||
star->moveToLeft(starLeft, starTop);
|
||||
starLeft += star->width();
|
||||
}
|
||||
if (_comment) {
|
||||
_comment->moveToLeft(
|
||||
st::callRatingPadding.left(),
|
||||
_stars.back()->bottomNoMargins() + st::callRatingCommentTop);
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::ratingChanged(int value) {
|
||||
Expects(value > 0 && value <= kMaxRating);
|
||||
if (!_rating) {
|
||||
clearButtons();
|
||||
addButton(tr::lng_send_button(), [=] { send(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
}
|
||||
_rating = value;
|
||||
|
||||
for (auto i = 0; i < kMaxRating; ++i) {
|
||||
_stars[i]->setIconOverride((i < value)
|
||||
? &st::callRatingStarFilled
|
||||
: nullptr);
|
||||
_stars[i]->setRippleColorOverride((i < value)
|
||||
? &st::lightButtonBgOver
|
||||
: nullptr);
|
||||
}
|
||||
if (value < kMaxRating) {
|
||||
if (!_comment) {
|
||||
_comment.create(
|
||||
this,
|
||||
st::callRatingComment,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_call_rate_comment());
|
||||
_comment->show();
|
||||
_comment->setSubmitSettings(_sendWay);
|
||||
_comment->setMaxLength(kRateCallCommentLengthMax);
|
||||
_comment->resize(
|
||||
width()
|
||||
- st::callRatingPadding.left()
|
||||
- st::callRatingPadding.right(),
|
||||
_comment->height());
|
||||
|
||||
updateMaxHeight();
|
||||
_comment->heightChanges(
|
||||
) | rpl::on_next([=] {
|
||||
commentResized();
|
||||
}, _comment->lifetime());
|
||||
_comment->submits(
|
||||
) | rpl::on_next([=] { send(); }, _comment->lifetime());
|
||||
_comment->cancelled(
|
||||
) | rpl::on_next([=] {
|
||||
closeBox();
|
||||
}, _comment->lifetime());
|
||||
}
|
||||
_comment->setFocusFast();
|
||||
} else if (_comment) {
|
||||
_comment.destroy();
|
||||
updateMaxHeight();
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::setInnerFocus() {
|
||||
if (_comment) {
|
||||
_comment->setFocusFast();
|
||||
} else {
|
||||
BoxContent::setInnerFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::commentResized() {
|
||||
updateMaxHeight();
|
||||
update();
|
||||
}
|
||||
|
||||
void RateCallBox::send() {
|
||||
Expects(_rating > 0 && _rating <= kMaxRating);
|
||||
_sends.fire({
|
||||
.rating = _rating,
|
||||
.comment = _comment ? _comment->getLastText().trimmed() : QString(),
|
||||
});
|
||||
}
|
||||
|
||||
void RateCallBox::updateMaxHeight() {
|
||||
auto newHeight = st::callRatingPadding.top()
|
||||
+ st::callRatingStarTop
|
||||
+ _stars.back()->heightNoMargins()
|
||||
+ st::callRatingPadding.bottom();
|
||||
if (_comment) {
|
||||
newHeight += st::callRatingCommentTop + _comment->height();
|
||||
}
|
||||
setDimensions(st::boxWideWidth, newHeight);
|
||||
}
|
||||
|
||||
rpl::producer<RateCallBox::Result> RateCallBox::sends() const {
|
||||
return _sends.events();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
51
Telegram/SourceFiles/ui/boxes/rate_call_box.h
Normal file
51
Telegram/SourceFiles/ui/boxes/rate_call_box.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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/layers/box_content.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class InputField;
|
||||
class IconButton;
|
||||
enum class InputSubmitSettings;
|
||||
|
||||
class RateCallBox : public Ui::BoxContent {
|
||||
public:
|
||||
RateCallBox(QWidget*, InputSubmitSettings sendWay);
|
||||
|
||||
struct Result {
|
||||
int rating = 0;
|
||||
QString comment;
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<Result> sends() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void updateMaxHeight();
|
||||
void ratingChanged(int value);
|
||||
void send();
|
||||
void commentResized();
|
||||
|
||||
const InputSubmitSettings _sendWay;
|
||||
int _rating = 0;
|
||||
|
||||
std::vector<object_ptr<Ui::IconButton>> _stars;
|
||||
object_ptr<Ui::InputField> _comment = { nullptr };
|
||||
|
||||
rpl::event_stream<Result> _sends;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
223
Telegram/SourceFiles/ui/boxes/report_box_graphics.cpp
Normal file
223
Telegram/SourceFiles/ui/boxes/report_box_graphics.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
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 "ui/boxes/report_box_graphics.h"
|
||||
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_channel_earn.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kReportReasonLengthMax = 512;
|
||||
|
||||
using Source = ReportSource;
|
||||
using Reason = ReportReason;
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReportReasonBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
ReportSource source,
|
||||
Fn<void(Reason)> done) {
|
||||
box->setTitle([&] {
|
||||
switch (source) {
|
||||
case Source::Message: return tr::lng_report_message_title();
|
||||
case Source::Channel: return tr::lng_report_title();
|
||||
case Source::Group: return tr::lng_report_group_title();
|
||||
case Source::Bot: return tr::lng_report_bot_title();
|
||||
case Source::ProfilePhoto:
|
||||
return tr::lng_report_profile_photo_title();
|
||||
case Source::ProfileVideo:
|
||||
return tr::lng_report_profile_video_title();
|
||||
case Source::GroupPhoto: return tr::lng_report_group_photo_title();
|
||||
case Source::GroupVideo: return tr::lng_report_group_video_title();
|
||||
case Source::ChannelPhoto:
|
||||
return tr::lng_report_channel_photo_title();
|
||||
case Source::ChannelVideo:
|
||||
return tr::lng_report_channel_video_title();
|
||||
case Source::Story:
|
||||
return tr::lng_report_story();
|
||||
}
|
||||
Unexpected("'source' in ReportReasonBox.");
|
||||
}());
|
||||
auto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };
|
||||
const auto add = [&](
|
||||
Reason reason,
|
||||
tr::phrase<> text,
|
||||
const style::icon &icon) {
|
||||
const auto layout = box->verticalLayout();
|
||||
const auto button = layout->add(
|
||||
object_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),
|
||||
margin);
|
||||
margin = {};
|
||||
button->setClickedCallback([=] {
|
||||
done(reason);
|
||||
});
|
||||
const auto height = st.button.padding.top()
|
||||
+ st.button.height
|
||||
+ st.button.padding.bottom();
|
||||
object_ptr<Info::Profile::FloatingIcon>(
|
||||
button,
|
||||
icon,
|
||||
QPoint{
|
||||
st::infoSharedMediaButtonIconPosition.x(),
|
||||
(height - icon.height()) / 2,
|
||||
});
|
||||
};
|
||||
add(Reason::Spam, tr::lng_report_reason_spam, st.spam);
|
||||
if (source == Source::Channel
|
||||
|| source == Source::Group
|
||||
|| source == Source::Bot) {
|
||||
add(Reason::Fake, tr::lng_report_reason_fake, st.fake);
|
||||
}
|
||||
add(
|
||||
Reason::Violence,
|
||||
tr::lng_report_reason_violence,
|
||||
st.violence);
|
||||
add(
|
||||
Reason::ChildAbuse,
|
||||
tr::lng_report_reason_child_abuse,
|
||||
st.children);
|
||||
add(
|
||||
Reason::Pornography,
|
||||
tr::lng_report_reason_pornography,
|
||||
st.pornography);
|
||||
add(
|
||||
Reason::Copyright,
|
||||
tr::lng_report_reason_copyright,
|
||||
st.copyright);
|
||||
if (source == Source::Message || source == Source::Story) {
|
||||
add(
|
||||
Reason::IllegalDrugs,
|
||||
tr::lng_report_reason_illegal_drugs,
|
||||
st.drugs);
|
||||
add(
|
||||
Reason::PersonalDetails,
|
||||
tr::lng_report_reason_personal_details,
|
||||
st.personal);
|
||||
}
|
||||
add(Reason::Other, tr::lng_report_reason_other, st.other);
|
||||
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void ReportDetailsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(tr::lng_profile_report());
|
||||
AddReportDetailsIconButton(box);
|
||||
Ui::AddSkip(
|
||||
box->verticalLayout(),
|
||||
st::settingsBlockedListIconPadding.bottom());
|
||||
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box, // #TODO reports
|
||||
tr::lng_report_details_about(),
|
||||
st.label),
|
||||
{
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom(),
|
||||
});
|
||||
const auto details = box->addRow(
|
||||
object_ptr<InputField>(
|
||||
box,
|
||||
st.field,
|
||||
InputField::Mode::MultiLine,
|
||||
tr::lng_report_details(),
|
||||
QString()));
|
||||
details->setMaxLength(kReportReasonLengthMax);
|
||||
box->setFocusCallback([=] {
|
||||
details->setFocusFast();
|
||||
});
|
||||
|
||||
const auto submit = [=] {
|
||||
const auto text = details->getLastText();
|
||||
done(text);
|
||||
};
|
||||
details->submits() | rpl::on_next(submit, details->lifetime());
|
||||
box->addButton(tr::lng_report_button(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
not_null<Ui::AbstractButton*> AddReportOptionButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const QString &text,
|
||||
const style::ReportBox *stOverride) {
|
||||
const auto button = container->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
rpl::single(QString()),
|
||||
(stOverride ? stOverride : &st::defaultReportBox)->noIconButton));
|
||||
const auto textFg = (stOverride
|
||||
? stOverride->label
|
||||
: st::sponsoredReportLabel).textFg->c;
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
rpl::single(text),
|
||||
st::sponsoredReportLabel);
|
||||
label->setTextColorOverride(textFg);
|
||||
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
|
||||
icon->resize(st::settingsPremiumArrow.size());
|
||||
icon->paintRequest() | rpl::on_next([=, w = icon->width()] {
|
||||
auto p = Painter(icon);
|
||||
st::settingsPremiumArrow.paint(p, 0, 0, w, textFg);
|
||||
}, icon->lifetime());
|
||||
button->sizeValue() | rpl::on_next([=](const QSize &size) {
|
||||
const auto left = button->st().padding.left();
|
||||
const auto right = button->st().padding.right();
|
||||
icon->moveToRight(right, (size.height() - icon->height()) / 2);
|
||||
label->resizeToWidth(size.width()
|
||||
- icon->width()
|
||||
- left
|
||||
- st::settingsButtonRightSkip
|
||||
- right);
|
||||
label->moveToLeft(left, (size.height() - label->height()) / 2);
|
||||
button->resize(
|
||||
button->width(),
|
||||
rect::m::sum::v(button->st().padding) + label->height());
|
||||
}, button->lifetime());
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
return button;
|
||||
}
|
||||
|
||||
void AddReportDetailsIconButton(not_null<GenericBox*> box) {
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box->verticalLayout(),
|
||||
{
|
||||
.name = u"blocked_peers_empty"_q,
|
||||
.sizeOverride = st::normalBoxLottieSize,
|
||||
},
|
||||
{});
|
||||
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
box->addRow(std::move(icon.widget));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
64
Telegram/SourceFiles/ui/boxes/report_box_graphics.h
Normal file
64
Telegram/SourceFiles/ui/boxes/report_box_graphics.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 style {
|
||||
struct ReportBox;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractButton;
|
||||
class GenericBox;
|
||||
class VerticalLayout;
|
||||
|
||||
enum class ReportSource {
|
||||
Message,
|
||||
Channel,
|
||||
Group,
|
||||
Bot,
|
||||
ProfilePhoto,
|
||||
ProfileVideo,
|
||||
GroupPhoto,
|
||||
GroupVideo,
|
||||
ChannelPhoto,
|
||||
ChannelVideo,
|
||||
Story,
|
||||
};
|
||||
|
||||
enum class ReportReason {
|
||||
Spam,
|
||||
Fake,
|
||||
Violence,
|
||||
ChildAbuse,
|
||||
Pornography,
|
||||
Copyright,
|
||||
IllegalDrugs,
|
||||
PersonalDetails,
|
||||
Other,
|
||||
};
|
||||
|
||||
void ReportReasonBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
ReportSource source,
|
||||
Fn<void(ReportReason)> done);
|
||||
|
||||
void ReportDetailsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
[[nodiscard]] not_null<Ui::AbstractButton*> AddReportOptionButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const QString &text,
|
||||
const style::ReportBox *stOverride);
|
||||
|
||||
void AddReportDetailsIconButton(not_null<GenericBox*> box);
|
||||
|
||||
} // namespace Ui
|
||||
219
Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp
Normal file
219
Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
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 "ui/boxes/show_or_premium_box.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/gradient_round_button.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kShowOrLineOpacity = 0.3;
|
||||
|
||||
} // namespace
|
||||
|
||||
object_ptr<RpWidget> MakeShowOrLabel(
|
||||
not_null<RpWidget*> parent,
|
||||
rpl::producer<QString> text) {
|
||||
auto result = object_ptr<FlatLabel>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st::showOrLabel);
|
||||
const auto raw = result.data();
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
|
||||
const auto full = st::showOrLineWidth;
|
||||
const auto left = (raw->width() - full) / 2;
|
||||
const auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip;
|
||||
const auto fill = (full - text) / 2;
|
||||
const auto stroke = st::lineWidth;
|
||||
const auto top = st::showOrLineTop;
|
||||
p.setOpacity(kShowOrLineOpacity);
|
||||
p.fillRect(left, top, fill, stroke, st::windowSubTextFg);
|
||||
const auto start = left + full - fill;
|
||||
p.fillRect(start, top, fill, stroke, st::windowSubTextFg);
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShowOrPremiumBox(
|
||||
not_null<GenericBox*> box,
|
||||
ShowOrPremium type,
|
||||
QString shortName,
|
||||
Fn<void()> justShow,
|
||||
Fn<void()> toPremium) {
|
||||
struct Skin {
|
||||
rpl::producer<QString> showTitle;
|
||||
rpl::producer<TextWithEntities> showAbout;
|
||||
rpl::producer<QString> showButton;
|
||||
rpl::producer<QString> orPremium;
|
||||
rpl::producer<QString> premiumTitle;
|
||||
rpl::producer<TextWithEntities> premiumAbout;
|
||||
rpl::producer<QString> premiumButton;
|
||||
QString toast;
|
||||
QString lottie;
|
||||
};
|
||||
auto skin = (type == ShowOrPremium::LastSeen)
|
||||
? Skin{
|
||||
tr::lng_lastseen_show_title(),
|
||||
tr::lng_lastseen_show_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_lastseen_show_button(),
|
||||
tr::lng_lastseen_or(),
|
||||
tr::lng_lastseen_premium_title(),
|
||||
tr::lng_lastseen_premium_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_lastseen_premium_button(),
|
||||
tr::lng_lastseen_shown_toast(tr::now),
|
||||
u"show_or_premium_lastseen"_q,
|
||||
}
|
||||
: Skin{
|
||||
tr::lng_readtime_show_title(),
|
||||
tr::lng_readtime_show_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_readtime_show_button(),
|
||||
tr::lng_readtime_or(),
|
||||
tr::lng_readtime_premium_title(),
|
||||
tr::lng_readtime_premium_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_readtime_premium_button(),
|
||||
tr::lng_readtime_shown_toast(tr::now),
|
||||
u"show_or_premium_readtime"_q,
|
||||
};
|
||||
|
||||
box->setStyle(st::showOrBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
const auto buttonPadding = QMargins(
|
||||
st::showOrBox.buttonPadding.left(),
|
||||
0,
|
||||
st::showOrBox.buttonPadding.right(),
|
||||
0);
|
||||
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box,
|
||||
{
|
||||
.name = skin.lottie,
|
||||
.sizeOverride = st::normalBoxLottieSize
|
||||
- Size(st::showOrTitleIconMargin * 2),
|
||||
},
|
||||
{ 0, st::showOrTitleIconMargin, 0, st::showOrTitleIconMargin });
|
||||
Settings::AddLottieIconWithCircle(
|
||||
box->verticalLayout(),
|
||||
std::move(icon.widget),
|
||||
st::settingsBlockedListIconPadding,
|
||||
st::normalBoxLottieSize);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.showTitle),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.showAbout),
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
const auto show = box->addRow(
|
||||
object_ptr<RoundButton>(
|
||||
box,
|
||||
std::move(skin.showButton),
|
||||
st::showOrShowButton),
|
||||
buttonPadding);
|
||||
show->setTextTransform(RoundButton::TextTransform::NoTransform);
|
||||
box->addRow(
|
||||
MakeShowOrLabel(box, std::move(skin.orPremium)),
|
||||
st::showOrLabelPadding + buttonPadding,
|
||||
style::al_justify);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.premiumTitle),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.premiumAbout),
|
||||
st::boostText),
|
||||
st::showOrPremiumAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
|
||||
const auto premium = CreateChild<GradientButton>(
|
||||
box.get(),
|
||||
Premium::ButtonGradientStops());
|
||||
|
||||
premium->resize(st::showOrShowButton.width, st::showOrShowButton.height);
|
||||
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
premium,
|
||||
std::move(skin.premiumButton),
|
||||
st::premiumPreviewButtonLabel);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
rpl::combine(
|
||||
premium->widthValue(),
|
||||
label->widthValue()
|
||||
) | rpl::on_next([=](int outer, int width) {
|
||||
label->moveToLeft(
|
||||
(outer - width) / 2,
|
||||
st::premiumPreviewBox.button.textTop,
|
||||
outer);
|
||||
}, label->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
|
||||
premium->startGlareAnimation();
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
|
||||
box->addButton(
|
||||
object_ptr<AbstractButton>::fromRaw(premium));
|
||||
|
||||
show->setClickedCallback([box, justShow, toast = skin.toast] {
|
||||
justShow();
|
||||
box->uiShow()->showToast(toast);
|
||||
box->closeBox();
|
||||
});
|
||||
premium->setClickedCallback(std::move(toPremium));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
32
Telegram/SourceFiles/ui/boxes/show_or_premium_box.h
Normal file
32
Telegram/SourceFiles/ui/boxes/show_or_premium_box.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
|
||||
enum class ShowOrPremium : uchar {
|
||||
LastSeen,
|
||||
ReadTime,
|
||||
};
|
||||
void ShowOrPremiumBox(
|
||||
not_null<GenericBox*> box,
|
||||
ShowOrPremium type,
|
||||
QString shortName,
|
||||
Fn<void()> justShow,
|
||||
Fn<void()> toPremium);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
|
||||
not_null<RpWidget*> parent,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
} // namespace Ui
|
||||
55
Telegram/SourceFiles/ui/boxes/single_choice_box.cpp
Normal file
55
Telegram/SourceFiles/ui/boxes/single_choice_box.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 "ui/boxes/single_choice_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
void SingleChoiceBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SingleChoiceBoxArgs &&args) {
|
||||
box->setTitle(std::move(args.title));
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
|
||||
|
||||
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
|
||||
args.initialSelection);
|
||||
|
||||
const auto layout = box->verticalLayout();
|
||||
layout->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
layout,
|
||||
st::boxOptionListPadding.top() + st::autolockButton.margin.top()));
|
||||
auto &&ints = ranges::views::ints(0, ranges::unreachable);
|
||||
for (const auto &[i, text] : ranges::views::zip(ints, args.options)) {
|
||||
layout->add(
|
||||
object_ptr<Ui::Radiobutton>(
|
||||
layout,
|
||||
group,
|
||||
i,
|
||||
text,
|
||||
args.st ? *args.st : st::defaultBoxCheckbox,
|
||||
args.radioSt ? *args.radioSt : st::defaultRadio),
|
||||
QMargins(
|
||||
st::boxPadding.left() + st::boxOptionListPadding.left(),
|
||||
0,
|
||||
st::boxPadding.right(),
|
||||
st::boxOptionListSkip));
|
||||
}
|
||||
const auto callback = args.callback.value();
|
||||
group->setChangedCallback([=](int value) {
|
||||
const auto weak = base::make_weak(box);
|
||||
callback(value);
|
||||
if (weak) {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
32
Telegram/SourceFiles/ui/boxes/single_choice_box.h
Normal file
32
Telegram/SourceFiles/ui/boxes/single_choice_box.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
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/layers/generic_box.h"
|
||||
#include "base/required.h"
|
||||
|
||||
namespace style {
|
||||
struct Checkbox;
|
||||
struct Radio;
|
||||
} // namespace style
|
||||
|
||||
struct SingleChoiceBoxArgs {
|
||||
template <typename T>
|
||||
using required = base::required<T>;
|
||||
|
||||
required<rpl::producer<QString>> title;
|
||||
const std::vector<QString> &options;
|
||||
int initialSelection = 0;
|
||||
required<Fn<void(int)>> callback;
|
||||
const style::Checkbox *st = nullptr;
|
||||
const style::Radio *radioSt = nullptr;
|
||||
};
|
||||
|
||||
void SingleChoiceBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SingleChoiceBoxArgs &&args);
|
||||
133
Telegram/SourceFiles/ui/boxes/time_picker_box.cpp
Normal file
133
Telegram/SourceFiles/ui/boxes/time_picker_box.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
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 "ui/boxes/time_picker_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/vertical_drum_picker.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
std::vector<TimeId> DefaultTimePickerValues() {
|
||||
return {
|
||||
(60 * 15),
|
||||
(60 * 30),
|
||||
(3600 * 1),
|
||||
(3600 * 2),
|
||||
(3600 * 3),
|
||||
(3600 * 4),
|
||||
(3600 * 8),
|
||||
(3600 * 12),
|
||||
(86400 * 1),
|
||||
(86400 * 2),
|
||||
(86400 * 3),
|
||||
(86400 * 7 * 1),
|
||||
(86400 * 7 * 2),
|
||||
(86400 * 31 * 1),
|
||||
(86400 * 31 * 2),
|
||||
(86400 * 31 * 3),
|
||||
};
|
||||
}
|
||||
|
||||
Fn<TimeId()> TimePickerBox(
|
||||
not_null<GenericBox*> box,
|
||||
std::vector<TimeId> values,
|
||||
std::vector<QString> phrases,
|
||||
TimeId startValue) {
|
||||
Expects(phrases.size() == values.size());
|
||||
|
||||
const auto startIndex = [&, &v = startValue] {
|
||||
const auto it = ranges::lower_bound(values, v);
|
||||
if (it == begin(values)) {
|
||||
return 0;
|
||||
}
|
||||
const auto left = *(it - 1);
|
||||
const auto right = *it;
|
||||
const auto shift = (std::abs(v - left) < std::abs(v - right))
|
||||
? -1
|
||||
: 0;
|
||||
return int(std::distance(begin(values), it - shift));
|
||||
}();
|
||||
|
||||
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
|
||||
box,
|
||||
st::historyMessagesTTLPickerHeight));
|
||||
|
||||
const auto font = st::boxTextFont;
|
||||
const auto maxPhraseWidth = [&] {
|
||||
// We have to use QFontMetricsF instead of
|
||||
// FontData::width for more precise calculation.
|
||||
const auto mf = QFontMetricsF(font->f);
|
||||
const auto maxPhrase = ranges::max_element(
|
||||
phrases,
|
||||
std::less<>(),
|
||||
[&](const QString &s) { return mf.horizontalAdvance(s); });
|
||||
return std::ceil(mf.horizontalAdvance(*maxPhrase));
|
||||
}();
|
||||
const auto itemHeight = st::historyMessagesTTLPickerItemHeight;
|
||||
auto paintCallback = Ui::VerticalDrumPicker::DefaultPaintCallback(
|
||||
font,
|
||||
itemHeight,
|
||||
[=](QPainter &p, QRectF r, int index) {
|
||||
p.drawText(r, phrases[index], style::al_center);
|
||||
});
|
||||
|
||||
const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(
|
||||
content,
|
||||
std::move(paintCallback),
|
||||
phrases.size(),
|
||||
itemHeight,
|
||||
startIndex);
|
||||
|
||||
content->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
picker->resize(maxPhraseWidth, s.height());
|
||||
picker->moveToLeft((s.width() - picker->width()) / 2, 0);
|
||||
}, content->lifetime());
|
||||
|
||||
content->paintRequest(
|
||||
) | rpl::on_next([=](const QRect &r) {
|
||||
auto p = QPainter(content);
|
||||
|
||||
p.fillRect(r, Qt::transparent);
|
||||
|
||||
const auto lineRect = QRect(
|
||||
0,
|
||||
content->height() / 2,
|
||||
content->width(),
|
||||
st::defaultInputField.borderActive);
|
||||
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
|
||||
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
|
||||
}, content->lifetime());
|
||||
|
||||
base::install_event_filter(content, [=](not_null<QEvent*> e) {
|
||||
if ((e->type() == QEvent::MouseButtonPress)
|
||||
|| (e->type() == QEvent::MouseButtonRelease)
|
||||
|| (e->type() == QEvent::MouseMove)) {
|
||||
picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (e->type() == QEvent::Wheel) {
|
||||
picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
base::install_event_filter(box, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
return [=] { return values[picker->index()]; };
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
22
Telegram/SourceFiles/ui/boxes/time_picker_box.h
Normal file
22
Telegram/SourceFiles/ui/boxes/time_picker_box.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] std::vector<TimeId> DefaultTimePickerValues();
|
||||
|
||||
[[nodiscard]] Fn<TimeId()> TimePickerBox(
|
||||
not_null<GenericBox*> box,
|
||||
std::vector<TimeId> values,
|
||||
std::vector<QString> phrases,
|
||||
TimeId startValue);
|
||||
|
||||
} // namespace Ui
|
||||
285
Telegram/SourceFiles/ui/cached_round_corners.cpp
Normal file
285
Telegram/SourceFiles/ui/cached_round_corners.cpp
Normal file
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
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 "ui/cached_round_corners.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_overview.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kCachedCornerRadiusCount = int(CachedCornerRadius::kCount);
|
||||
|
||||
std::vector<CornersPixmaps> Corners;
|
||||
QImage CornersMaskLarge[4], CornersMaskSmall[4];
|
||||
rpl::lifetime PaletteChangedLifetime;
|
||||
|
||||
std::array<std::array<QImage, 4>, kCachedCornerRadiusCount> CachedMasks;
|
||||
|
||||
[[nodiscard]] std::array<QImage, 4> PrepareCorners(int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
|
||||
int32 r = radius * style::DevicePixelRatio(), s = st::msgShadow * style::DevicePixelRatio();
|
||||
QImage rect(r * 3, r * 3 + (shadow ? s : 0), QImage::Format_ARGB32_Premultiplied);
|
||||
rect.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&rect);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setPen(Qt::NoPen);
|
||||
if (shadow) {
|
||||
p.setBrush((*shadow)->b);
|
||||
p.drawRoundedRect(0, s, r * 3, r * 3, r, r);
|
||||
}
|
||||
p.setBrush(brush);
|
||||
p.drawRoundedRect(0, 0, r * 3, r * 3, r, r);
|
||||
}
|
||||
auto result = std::array<QImage, 4>();
|
||||
result[0] = rect.copy(0, 0, r, r);
|
||||
result[1] = rect.copy(r * 2, 0, r, r);
|
||||
result[2] = rect.copy(0, r * 2, r, r + (shadow ? s : 0));
|
||||
result[3] = rect.copy(r * 2, r * 2, r, r + (shadow ? s : 0));
|
||||
return result;
|
||||
}
|
||||
|
||||
void PrepareCorners(CachedRoundCorners index, int32 radius, const QBrush &brush, const style::color *shadow = nullptr) {
|
||||
Expects(index < Corners.size());
|
||||
|
||||
auto images = PrepareCorners(radius, brush, shadow);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
Corners[index].p[i] = PixmapFromImage(std::move(images[i]));
|
||||
Corners[index].p[i].setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
}
|
||||
|
||||
void CreateMaskCorners() {
|
||||
auto mask = PrepareCorners(st::roundRadiusSmall, QColor(255, 255, 255), nullptr);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
CornersMaskSmall[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
CornersMaskSmall[i].setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
mask = PrepareCorners(st::roundRadiusLarge, QColor(255, 255, 255), nullptr);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
CornersMaskLarge[i] = mask[i].convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
CornersMaskLarge[i].setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
}
|
||||
|
||||
void CreatePaletteCorners() {
|
||||
PrepareCorners(MenuCorners, st::roundRadiusSmall, st::menuBg);
|
||||
PrepareCorners(BoxCorners, st::boxRadius, st::boxBg);
|
||||
PrepareCorners(DateCorners, st::dateRadius, st::msgDateImgBg);
|
||||
PrepareCorners(OverviewVideoCorners, st::overviewVideoStatusRadius, st::msgDateImgBg);
|
||||
PrepareCorners(OverviewVideoSelectedCorners, st::overviewVideoStatusRadius, st::msgDateImgBgSelected);
|
||||
PrepareCorners(ForwardCorners, st::roundRadiusLarge, st::historyForwardChooseBg);
|
||||
PrepareCorners(MediaviewSaveCorners, st::mediaviewControllerRadius, st::mediaviewSaveMsgBg);
|
||||
PrepareCorners(StickerHoverCorners, st::roundRadiusSmall, st::emojiPanHover);
|
||||
PrepareCorners(BotKeyboardCorners, st::roundRadiusSmall, st::botKbBg);
|
||||
|
||||
PrepareCorners(Doc1Corners, st::roundRadiusSmall, st::msgFile1Bg);
|
||||
PrepareCorners(Doc2Corners, st::roundRadiusSmall, st::msgFile2Bg);
|
||||
PrepareCorners(Doc3Corners, st::roundRadiusSmall, st::msgFile3Bg);
|
||||
PrepareCorners(Doc4Corners, st::roundRadiusSmall, st::msgFile4Bg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void StartCachedCorners() {
|
||||
Corners.resize(RoundCornersCount);
|
||||
CreateMaskCorners();
|
||||
CreatePaletteCorners();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
CreatePaletteCorners();
|
||||
}, PaletteChangedLifetime);
|
||||
}
|
||||
|
||||
void FinishCachedCorners() {
|
||||
Corners.clear();
|
||||
PaletteChangedLifetime.destroy();
|
||||
}
|
||||
|
||||
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corners) {
|
||||
using namespace Images;
|
||||
|
||||
const auto fillBg = [&](QRect rect) {
|
||||
p.fillRect(rect, bg);
|
||||
};
|
||||
const auto fillCorner = [&](int x, int y, int index) {
|
||||
if (const auto &pix = corners.p[index]; !pix.isNull()) {
|
||||
p.drawPixmap(x, y, pix);
|
||||
}
|
||||
};
|
||||
|
||||
if (corners.p[kTopLeft].isNull()
|
||||
&& corners.p[kTopRight].isNull()
|
||||
&& corners.p[kBottomLeft].isNull()
|
||||
&& corners.p[kBottomRight].isNull()) {
|
||||
p.fillRect(x, y, w, h, bg);
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto cornerSize = [&](int index) {
|
||||
return corners.p[index].isNull()
|
||||
? 0
|
||||
: (corners.p[index].width() / ratio);
|
||||
};
|
||||
const auto verticalSkip = [&](int left, int right) {
|
||||
return std::max(cornerSize(left), cornerSize(right));
|
||||
};
|
||||
const auto top = verticalSkip(kTopLeft, kTopRight);
|
||||
const auto bottom = verticalSkip(kBottomLeft, kBottomRight);
|
||||
if (top) {
|
||||
const auto left = cornerSize(kTopLeft);
|
||||
const auto right = cornerSize(kTopRight);
|
||||
if (left) {
|
||||
fillCorner(x, y, kTopLeft);
|
||||
if (const auto add = top - left) {
|
||||
fillBg({ x, y + left, left, add });
|
||||
}
|
||||
}
|
||||
if (const auto fill = w - left - right; fill > 0) {
|
||||
fillBg({ x + left, y, fill, top });
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(x + w - right, y, kTopRight);
|
||||
if (const auto add = top - right) {
|
||||
fillBg({ x + w - right, y + right, right, add });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const auto fill = h - top - bottom; fill > 0) {
|
||||
fillBg({ x, y + top, w, fill });
|
||||
}
|
||||
if (bottom) {
|
||||
const auto left = cornerSize(kBottomLeft);
|
||||
const auto right = cornerSize(kBottomRight);
|
||||
if (left) {
|
||||
fillCorner(x, y + h - left, kBottomLeft);
|
||||
if (const auto add = bottom - left) {
|
||||
fillBg({ x, y + h - bottom, left, add });
|
||||
}
|
||||
}
|
||||
if (const auto fill = w - left - right; fill > 0) {
|
||||
fillBg({ x + left, y + h - bottom, fill, bottom });
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(x + w - right, y + h - right, kBottomRight);
|
||||
if (const auto add = bottom - right) {
|
||||
fillBg({ x + w - right, y + h - bottom, right, add });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, CachedRoundCorners index) {
|
||||
FillRoundRect(p, x, y, w, h, bg, CachedCornerPixmaps(index));
|
||||
}
|
||||
|
||||
void FillRoundShadow(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color shadow, const CornersPixmaps &corners) {
|
||||
constexpr auto kLeft = 2;
|
||||
constexpr auto kRight = 3;
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto size = [&](int index) {
|
||||
const auto &pix = corners.p[index];
|
||||
return pix.isNull() ? 0 : (pix.width() / ratio);
|
||||
};
|
||||
const auto fillCorner = [&](int left, int bottom, int index) {
|
||||
const auto &pix = corners.p[index];
|
||||
if (pix.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto size = pix.width() / ratio;
|
||||
p.drawPixmap(left, bottom - size, pix);
|
||||
};
|
||||
const auto left = size(kLeft);
|
||||
const auto right = size(kRight);
|
||||
const auto from = x + left;
|
||||
fillCorner(x, y + h + st::msgShadow, kLeft);
|
||||
if (const auto width = w - left - right; width > 0) {
|
||||
p.fillRect(from, y + h, width, st::msgShadow, shadow);
|
||||
}
|
||||
fillCorner(x + w - right, y + h + st::msgShadow, kRight);
|
||||
}
|
||||
|
||||
const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index) {
|
||||
Expects(index >= 0 && index < RoundCornersCount);
|
||||
|
||||
return Corners[index];
|
||||
}
|
||||
|
||||
CornersPixmaps PrepareCornerPixmaps(int radius, style::color bg, const style::color *sh) {
|
||||
auto images = PrepareCorners(radius, bg, sh);
|
||||
auto result = CornersPixmaps();
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
result.p[j] = PixmapFromImage(std::move(images[j]));
|
||||
result.p[j].setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, const style::color *sh) {
|
||||
switch (radius) {
|
||||
case ImageRoundRadius::Small:
|
||||
return PrepareCornerPixmaps(st::roundRadiusSmall, bg, sh);
|
||||
case ImageRoundRadius::Large:
|
||||
return PrepareCornerPixmaps(st::roundRadiusLarge, bg, sh);
|
||||
}
|
||||
Unexpected("Image round radius in PrepareCornerPixmaps.");
|
||||
}
|
||||
|
||||
CornersPixmaps PrepareInvertedCornerPixmaps(int radius, style::color bg) {
|
||||
const auto size = radius * style::DevicePixelRatio();
|
||||
auto circle = style::colorizeImage(
|
||||
style::createInvertedCircleMask(radius * 2),
|
||||
bg);
|
||||
circle.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
auto result = CornersPixmaps();
|
||||
const auto fill = [&](int index, int xoffset, int yoffset) {
|
||||
result.p[index] = PixmapFromImage(
|
||||
circle.copy(QRect(xoffset, yoffset, size, size)));
|
||||
};
|
||||
fill(0, 0, 0);
|
||||
fill(1, size, 0);
|
||||
fill(2, size, size);
|
||||
fill(3, 0, size);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag) {
|
||||
using Radius = CachedCornerRadius;
|
||||
switch (tag) {
|
||||
case Radius::Small: return st::roundRadiusSmall;
|
||||
case Radius::ThumbSmall: return MsgFileThumbRadiusSmall();
|
||||
case Radius::ThumbLarge: return MsgFileThumbRadiusLarge();
|
||||
case Radius::BubbleSmall: return BubbleRadiusSmall();
|
||||
case Radius::BubbleLarge: return BubbleRadiusLarge();
|
||||
}
|
||||
Unexpected("Radius tag in CachedCornerRadiusValue.");
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(
|
||||
CachedCornerRadius radius) {
|
||||
const auto index = static_cast<int>(radius);
|
||||
Assert(index >= 0 && index < kCachedCornerRadiusCount);
|
||||
|
||||
if (CachedMasks[index][0].isNull()) {
|
||||
CachedMasks[index] = Images::CornersMask(
|
||||
CachedCornerRadiusValue(CachedCornerRadius(index)));
|
||||
}
|
||||
return CachedMasks[index];
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
82
Telegram/SourceFiles/ui/cached_round_corners.h
Normal file
82
Telegram/SourceFiles/ui/cached_round_corners.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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/rect_part.h"
|
||||
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct CornersPixmaps {
|
||||
QPixmap p[4];
|
||||
};
|
||||
|
||||
enum CachedRoundCorners : int {
|
||||
BoxCorners,
|
||||
MenuCorners,
|
||||
DateCorners,
|
||||
OverviewVideoCorners,
|
||||
OverviewVideoSelectedCorners,
|
||||
ForwardCorners,
|
||||
MediaviewSaveCorners,
|
||||
StickerHoverCorners,
|
||||
BotKeyboardCorners,
|
||||
|
||||
Doc1Corners,
|
||||
Doc2Corners,
|
||||
Doc3Corners,
|
||||
Doc4Corners,
|
||||
|
||||
RoundCornersCount
|
||||
};
|
||||
|
||||
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, CachedRoundCorners index);
|
||||
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, CachedRoundCorners index) {
|
||||
FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, index);
|
||||
}
|
||||
|
||||
[[nodiscard]] const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index);
|
||||
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
|
||||
int radius,
|
||||
style::color bg,
|
||||
const style::color *sh = nullptr);
|
||||
[[nodiscard]] CornersPixmaps PrepareCornerPixmaps(
|
||||
ImageRoundRadius radius,
|
||||
style::color bg,
|
||||
const style::color *sh = nullptr);
|
||||
[[nodiscard]] CornersPixmaps PrepareInvertedCornerPixmaps(
|
||||
int radius,
|
||||
style::color bg);
|
||||
void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, const CornersPixmaps &corners);
|
||||
inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, const CornersPixmaps &corners) {
|
||||
return FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, corners);
|
||||
}
|
||||
void FillRoundShadow(QPainter &p, int x, int y, int w, int h, style::color shadow, const CornersPixmaps &corners);
|
||||
inline void FillRoundShadow(QPainter &p, const QRect &rect, style::color shadow, const CornersPixmaps &corners) {
|
||||
FillRoundShadow(p, rect.x(), rect.y(), rect.width(), rect.height(), shadow, corners);
|
||||
}
|
||||
|
||||
enum class CachedCornerRadius {
|
||||
Small,
|
||||
ThumbSmall,
|
||||
ThumbLarge,
|
||||
BubbleSmall,
|
||||
BubbleLarge,
|
||||
|
||||
kCount,
|
||||
};
|
||||
[[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag);
|
||||
|
||||
[[nodiscard]] const std::array<QImage, 4> &CachedCornersMasks(
|
||||
CachedCornerRadius radius);
|
||||
|
||||
void StartCachedCorners();
|
||||
void FinishCachedCorners();
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
#include "base/timer_rpl.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AbstractSingleFilePreview::AbstractSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type)
|
||||
: AbstractSinglePreview(parent)
|
||||
, _st(st)
|
||||
, _type(type)
|
||||
, _editMedia(this, _st.files.buttonFile)
|
||||
, _deleteMedia(this, _st.files.buttonFile) {
|
||||
|
||||
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
|
||||
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
|
||||
|
||||
if (type == AttachControls::Type::Full) {
|
||||
_deleteMedia->show();
|
||||
_editMedia->show();
|
||||
} else if (type == AttachControls::Type::EditOnly) {
|
||||
_deleteMedia->hide();
|
||||
_editMedia->show();
|
||||
} else if (type == AttachControls::Type::None) {
|
||||
_deleteMedia->hide();
|
||||
_editMedia->hide();
|
||||
}
|
||||
}
|
||||
|
||||
AbstractSingleFilePreview::~AbstractSingleFilePreview() = default;
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::editRequests() const {
|
||||
return _editMedia->clicks() | rpl::map([] {
|
||||
return base::timer_once(st::historyAttach.ripple.hideDuration);
|
||||
}) | rpl::flatten_latest();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::deleteRequests() const {
|
||||
return _deleteMedia->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::modifyRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::editCoverRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleFilePreview::clearCoverRequests() const {
|
||||
return rpl::never<>();
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::prepareThumbFor(
|
||||
Data &data,
|
||||
const QImage &preview) {
|
||||
if (preview.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto originalWidth = preview.width();
|
||||
auto originalHeight = preview.height();
|
||||
const auto &st = st::attachPreviewThumbLayout;
|
||||
auto thumbWidth = st.thumbSize;
|
||||
if (originalWidth > originalHeight) {
|
||||
thumbWidth = (originalWidth * st.thumbSize) / originalHeight;
|
||||
}
|
||||
const auto options = Images::Option::RoundSmall;
|
||||
data.fileThumb = PixmapFromImage(Images::Prepare(
|
||||
preview,
|
||||
thumbWidth * style::DevicePixelRatio(),
|
||||
{ .options = options, .outer = { st.thumbSize, st.thumbSize } }));
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
const auto w = width()
|
||||
- st::boxPhotoPadding.left()
|
||||
- st::boxPhotoPadding.right();
|
||||
const auto &st = !isThumbedLayout(_data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
const auto nameleft = st.thumbSize + st.thumbSkip;
|
||||
const auto nametop = st.nameTop;
|
||||
const auto statustop = st.statusTop;
|
||||
const auto x = (width() - w) / 2, y = 0;
|
||||
|
||||
if (!isThumbedLayout(_data)) {
|
||||
QRect inner(
|
||||
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
if (_data.fileIsAudio && !_data.fileThumb.isNull()) {
|
||||
p.drawPixmap(inner.topLeft(), _data.fileThumb);
|
||||
} else {
|
||||
p.setBrush(_st.files.iconBg);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
auto &icon = _data.fileIsAudio
|
||||
? (_data.fileThumb.isNull()
|
||||
? _st.files.iconPlay
|
||||
: st::historyFileThumbPlay)
|
||||
: _data.fileIsImage
|
||||
? _st.files.iconImage
|
||||
: _st.files.iconDocument;
|
||||
icon.paintInCenter(p, inner);
|
||||
} else {
|
||||
QRect rthumb(
|
||||
style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
|
||||
p.drawPixmap(rthumb.topLeft(), _data.fileThumb);
|
||||
}
|
||||
p.setFont(st::semiboldFont);
|
||||
p.setPen(_st.files.nameFg);
|
||||
p.drawTextLeft(
|
||||
x + nameleft,
|
||||
y + nametop, width(),
|
||||
_data.name,
|
||||
_data.nameWidth);
|
||||
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(_st.files.statusFg);
|
||||
p.drawTextLeft(
|
||||
x + nameleft,
|
||||
y + statustop,
|
||||
width(),
|
||||
_data.statusText,
|
||||
_data.statusWidth);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::resizeEvent(QResizeEvent *e) {
|
||||
const auto w = width()
|
||||
- st::boxPhotoPadding.left()
|
||||
- st::boxPhotoPadding.right();
|
||||
const auto x = (width() - w) / 2;
|
||||
const auto top = st::sendBoxFileGroupSkipTop;
|
||||
auto right = st::sendBoxFileGroupSkipRight + x;
|
||||
if (_type != AttachControls::Type::EditOnly) {
|
||||
_deleteMedia->moveToRight(right, top);
|
||||
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
|
||||
}
|
||||
_editMedia->moveToRight(right, top);
|
||||
}
|
||||
|
||||
bool AbstractSingleFilePreview::isThumbedLayout(Data &data) const {
|
||||
return (!data.fileThumb.isNull() && !data.fileIsAudio);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::updateTextWidthFor(Data &data) {
|
||||
const auto &st = !isThumbedLayout(data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
const auto buttonsCount = (_type == AttachControls::Type::EditOnly)
|
||||
? 1
|
||||
: (_type == AttachControls::Type::Full)
|
||||
? 2
|
||||
: 0;
|
||||
const auto availableFileWidth = st::sendMediaPreviewSize
|
||||
- st.thumbSize
|
||||
- st.thumbSkip
|
||||
// Right buttons.
|
||||
- _st.files.buttonFile.width * buttonsCount
|
||||
- st::sendBoxAlbumGroupEditInternalSkip * buttonsCount
|
||||
- st::sendBoxAlbumGroupSkipRight;
|
||||
data.nameWidth = st::semiboldFont->width(data.name);
|
||||
if (data.nameWidth > availableFileWidth) {
|
||||
data.name = st::semiboldFont->elided(
|
||||
data.name,
|
||||
availableFileWidth,
|
||||
Qt::ElideMiddle);
|
||||
data.nameWidth = st::semiboldFont->width(data.name);
|
||||
}
|
||||
data.statusWidth = st::normalFont->width(data.statusText);
|
||||
}
|
||||
|
||||
void AbstractSingleFilePreview::setData(const Data &data) {
|
||||
_data = data;
|
||||
|
||||
updateTextWidthFor(_data);
|
||||
|
||||
const auto &st = !isThumbedLayout(_data)
|
||||
? st::attachPreviewLayout
|
||||
: st::attachPreviewThumbLayout;
|
||||
resize(width(), st.thumbSize);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_preview.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class IconButton;
|
||||
|
||||
class AbstractSingleFilePreview : public AbstractSinglePreview {
|
||||
public:
|
||||
AbstractSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type);
|
||||
~AbstractSingleFilePreview();
|
||||
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> modifyRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
|
||||
|
||||
protected:
|
||||
struct Data {
|
||||
QPixmap fileThumb;
|
||||
QString name;
|
||||
QString statusText;
|
||||
int nameWidth = 0;
|
||||
int statusWidth = 0;
|
||||
bool fileIsAudio = false;
|
||||
bool fileIsImage = false;
|
||||
};
|
||||
|
||||
void prepareThumbFor(Data &data, const QImage &preview);
|
||||
bool isThumbedLayout(Data &data) const;
|
||||
|
||||
void setData(const Data &data);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void updateTextWidthFor(Data &data);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
const AttachControls::Type _type;
|
||||
|
||||
Data _data;
|
||||
|
||||
object_ptr<IconButton> _editMedia = { nullptr };
|
||||
object_ptr<IconButton> _deleteMedia = { nullptr };
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_abstract_single_media_preview.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinPreviewWidth = 20;
|
||||
|
||||
} // namespace
|
||||
|
||||
AbstractSingleMediaPreview::AbstractSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed)
|
||||
: AbstractSinglePreview(parent)
|
||||
, _st(st)
|
||||
, _actionAllowed(std::move(actionAllowed))
|
||||
, _minThumbH(st::sendBoxAlbumGroupSize.height()
|
||||
+ st::sendBoxAlbumGroupSkipTop * 2)
|
||||
, _controls(base::make_unique_q<AttachControlsWidget>(this, type)) {
|
||||
}
|
||||
|
||||
AbstractSingleMediaPreview::~AbstractSingleMediaPreview() = default;
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::deleteRequests() const {
|
||||
return _controls->deleteRequests();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::editRequests() const {
|
||||
return _controls->editRequests();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::modifyRequests() const {
|
||||
return _photoEditorRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::editCoverRequests() const {
|
||||
return _editCoverRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> AbstractSingleMediaPreview::clearCoverRequests() const {
|
||||
return _clearCoverRequests.events();
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setSendWay(SendFilesWay way) {
|
||||
_sendWay = way;
|
||||
update();
|
||||
}
|
||||
|
||||
SendFilesWay AbstractSingleMediaPreview::sendWay() const {
|
||||
return _sendWay;
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setSpoiler(bool spoiler) {
|
||||
_spoiler = spoiler
|
||||
? std::make_unique<SpoilerAnimation>([=] { update(); })
|
||||
: nullptr;
|
||||
update();
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::hasSpoiler() const {
|
||||
return _spoiler != nullptr;
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::canHaveSpoiler() const {
|
||||
return supportsSpoilers();
|
||||
}
|
||||
|
||||
rpl::producer<bool> AbstractSingleMediaPreview::spoileredChanges() const {
|
||||
return _spoileredChanges.events();
|
||||
}
|
||||
|
||||
QImage AbstractSingleMediaPreview::generatePriceTagBackground() const {
|
||||
return (_previewBlurred.isNull() ? _preview : _previewBlurred).toImage();
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::preparePreview(QImage preview) {
|
||||
auto maxW = 0;
|
||||
auto maxH = 0;
|
||||
if (_animated && drawBackground()) {
|
||||
auto limitW = st::sendMediaPreviewSize;
|
||||
auto limitH = st::confirmMaxHeight;
|
||||
maxW = qMax(preview.width(), 1);
|
||||
maxH = qMax(preview.height(), 1);
|
||||
if (maxW * limitH > maxH * limitW) {
|
||||
if (maxW < limitW) {
|
||||
maxH = maxH * limitW / maxW;
|
||||
maxW = limitW;
|
||||
}
|
||||
} else {
|
||||
if (maxH < limitH) {
|
||||
maxW = maxW * limitH / maxH;
|
||||
maxH = limitH;
|
||||
}
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
preview = Images::Prepare(
|
||||
std::move(preview),
|
||||
QSize(maxW, maxH) * ratio,
|
||||
{ .outer = { maxW, maxH } });
|
||||
}
|
||||
auto originalWidth = preview.width();
|
||||
auto originalHeight = preview.height();
|
||||
if (!originalWidth || !originalHeight) {
|
||||
originalWidth = originalHeight = 1;
|
||||
}
|
||||
_previewWidth = st::sendMediaPreviewSize;
|
||||
if (preview.width() < _previewWidth) {
|
||||
_previewWidth = qMax(preview.width(), kMinPreviewWidth);
|
||||
}
|
||||
auto maxthumbh = qMin(qRound(1.5 * _previewWidth), st::confirmMaxHeight);
|
||||
_previewHeight = qRound(originalHeight
|
||||
* float64(_previewWidth)
|
||||
/ originalWidth);
|
||||
if (_previewHeight > maxthumbh) {
|
||||
_previewWidth = qRound(_previewWidth
|
||||
* float64(maxthumbh)
|
||||
/ _previewHeight);
|
||||
accumulate_max(_previewWidth, kMinPreviewWidth);
|
||||
_previewHeight = maxthumbh;
|
||||
}
|
||||
_previewLeft = (st::boxWideWidth - _previewWidth) / 2;
|
||||
if (_previewHeight < _minThumbH) {
|
||||
_previewTop = (_minThumbH - _previewHeight) / 2;
|
||||
}
|
||||
|
||||
preview = std::move(preview).scaled(
|
||||
_previewWidth * style::DevicePixelRatio(),
|
||||
_previewHeight * style::DevicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
preview = Images::Opaque(std::move(preview));
|
||||
_preview = PixmapFromImage(std::move(preview));
|
||||
_preview.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_previewBlurred = QPixmap();
|
||||
|
||||
resize(width(), std::max(_previewHeight, _minThumbH));
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::isOverPreview(QPoint position) const {
|
||||
return QRect(
|
||||
_previewLeft,
|
||||
_previewTop,
|
||||
_previewWidth,
|
||||
_previewHeight).contains(position);
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::resizeEvent(QResizeEvent *e) {
|
||||
_controls->moveToRight(
|
||||
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
|
||||
st::sendBoxAlbumGroupSkipTop,
|
||||
width());
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto takenSpoiler = supportsSpoilers()
|
||||
? nullptr
|
||||
: base::take(_spoiler);
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (takenSpoiler) {
|
||||
_spoiler = base::take(takenSpoiler);
|
||||
}
|
||||
});
|
||||
|
||||
if (drawBackground()) {
|
||||
const auto &padding = st::boxPhotoPadding;
|
||||
if (_previewLeft > padding.left()) {
|
||||
p.fillRect(
|
||||
padding.left(),
|
||||
_previewTop,
|
||||
_previewLeft - padding.left(),
|
||||
_previewHeight,
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
if ((_previewLeft + _previewWidth) < (width() - padding.right())) {
|
||||
p.fillRect(
|
||||
_previewLeft + _previewWidth,
|
||||
_previewTop,
|
||||
width() - padding.right() - _previewLeft - _previewWidth,
|
||||
_previewHeight,
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
if (_previewTop > 0) {
|
||||
p.fillRect(
|
||||
padding.left(),
|
||||
0,
|
||||
width() - padding.right() - padding.left(),
|
||||
height(),
|
||||
_st.files.confirmBg);
|
||||
}
|
||||
}
|
||||
|
||||
if (_spoiler && _previewBlurred.isNull()) {
|
||||
_previewBlurred = BlurredPreviewFromPixmap(_preview, RectPart::None);
|
||||
}
|
||||
if (_spoiler || !tryPaintAnimation(p)) {
|
||||
const auto &pixmap = _spoiler ? _previewBlurred : _preview;
|
||||
const auto position = QPoint(_previewLeft, _previewTop);
|
||||
p.drawPixmap(position, pixmap);
|
||||
if (_spoiler) {
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
QRect(position, pixmap.size() / pixmap.devicePixelRatio()),
|
||||
DefaultImageSpoiler().frame(
|
||||
_spoiler->index(crl::now(), paused)));
|
||||
}
|
||||
}
|
||||
if (_animated && !isAnimatedPreviewReady() && !_spoiler) {
|
||||
const auto innerSize = st::msgFileLayout.thumbSize;
|
||||
auto inner = QRect(
|
||||
_previewLeft + (_previewWidth - innerSize) / 2,
|
||||
_previewTop + (_previewHeight - innerSize) / 2,
|
||||
innerSize,
|
||||
innerSize);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::msgDateImgBg);
|
||||
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
|
||||
auto icon = &st::historyFileInPlay;
|
||||
icon->paintInCenter(p, inner);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mousePressEvent(QMouseEvent *e) {
|
||||
if (isOverPreview(e->pos())) {
|
||||
_pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mouseMoveEvent(QMouseEvent *e) {
|
||||
applyCursor((isPhoto() && isOverPreview(e->pos()))
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (base::take(_pressed) && isOverPreview(e->pos())) {
|
||||
if (e->button() == Qt::RightButton) {
|
||||
showContextMenu(e->globalPos());
|
||||
} else if (isPhoto()) {
|
||||
_photoEditorRequests.fire({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::applyCursor(style::cursor cursor) {
|
||||
if (_cursor != cursor) {
|
||||
_cursor = cursor;
|
||||
setCursor(_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::showContextMenu(QPoint position) {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
_st.tabbed.menu);
|
||||
|
||||
const auto &icons = _st.tabbed.icons;
|
||||
if (_actionAllowed(AttachActionType::ToggleSpoiler)
|
||||
&& _sendWay.sendImagesAsPhotos()
|
||||
&& supportsSpoilers()) {
|
||||
const auto spoilered = hasSpoiler();
|
||||
_menu->addAction(spoilered
|
||||
? tr::lng_context_disable_spoiler(tr::now)
|
||||
: tr::lng_context_spoiler_effect(tr::now), [=] {
|
||||
setSpoiler(!spoilered);
|
||||
_spoileredChanges.fire_copy(!spoilered);
|
||||
}, spoilered ? &icons.menuSpoilerOff : &icons.menuSpoiler);
|
||||
}
|
||||
if (_actionAllowed(AttachActionType::EditCover)) {
|
||||
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
||||
_editCoverRequests.fire({});
|
||||
}, &st::menuIconEdit);
|
||||
|
||||
if (_actionAllowed(AttachActionType::ClearCover)) {
|
||||
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
|
||||
_clearCoverRequests.fire({});
|
||||
}, &st::menuIconCancel);
|
||||
}
|
||||
}
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
} else {
|
||||
_menu->popup(position);
|
||||
}
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewLeft() const {
|
||||
return _previewLeft;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewTop() const {
|
||||
return _previewTop;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewWidth() const {
|
||||
return _previewWidth;
|
||||
}
|
||||
|
||||
int AbstractSingleMediaPreview::previewHeight() const {
|
||||
return _previewHeight;
|
||||
}
|
||||
|
||||
void AbstractSingleMediaPreview::setAnimated(bool animated) {
|
||||
_animated = animated;
|
||||
}
|
||||
|
||||
bool AbstractSingleMediaPreview::isPhoto() const {
|
||||
return drawBackground()
|
||||
&& !isAnimatedPreviewReady()
|
||||
&& !_animated;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_preview.h"
|
||||
#include "ui/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/abstract_button.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PopupMenu;
|
||||
|
||||
class AbstractSingleMediaPreview : public AbstractSinglePreview {
|
||||
public:
|
||||
AbstractSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed);
|
||||
~AbstractSingleMediaPreview();
|
||||
|
||||
void setSendWay(SendFilesWay way);
|
||||
[[nodiscard]] SendFilesWay sendWay() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> modifyRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> editCoverRequests() const override;
|
||||
[[nodiscard]] rpl::producer<> clearCoverRequests() const override;
|
||||
|
||||
[[nodiscard]] bool isPhoto() const;
|
||||
|
||||
void setSpoiler(bool spoiler);
|
||||
[[nodiscard]] bool hasSpoiler() const;
|
||||
[[nodiscard]] bool canHaveSpoiler() const;
|
||||
[[nodiscard]] rpl::producer<bool> spoileredChanges() const;
|
||||
|
||||
[[nodiscard]] QImage generatePriceTagBackground() const;
|
||||
|
||||
protected:
|
||||
virtual bool supportsSpoilers() const = 0;
|
||||
virtual bool drawBackground() const = 0;
|
||||
virtual bool tryPaintAnimation(QPainter &p) = 0;
|
||||
virtual bool isAnimatedPreviewReady() const = 0;
|
||||
|
||||
void preparePreview(QImage preview);
|
||||
|
||||
int previewLeft() const;
|
||||
int previewTop() const;
|
||||
int previewWidth() const;
|
||||
int previewHeight() const;
|
||||
|
||||
void setAnimated(bool animated);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
[[nodiscard]] bool isOverPreview(QPoint position) const;
|
||||
void applyCursor(style::cursor cursor);
|
||||
void showContextMenu(QPoint position);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
SendFilesWay _sendWay;
|
||||
Fn<bool(AttachActionType)> _actionAllowed;
|
||||
bool _animated = false;
|
||||
QPixmap _preview;
|
||||
QPixmap _previewBlurred;
|
||||
int _previewLeft = 0;
|
||||
int _previewTop = 0;
|
||||
int _previewWidth = 0;
|
||||
int _previewHeight = 0;
|
||||
|
||||
std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
rpl::event_stream<bool> _spoileredChanges;
|
||||
|
||||
const int _minThumbH;
|
||||
const base::unique_qptr<AttachControlsWidget> _controls;
|
||||
rpl::event_stream<> _photoEditorRequests;
|
||||
rpl::event_stream<> _editCoverRequests;
|
||||
rpl::event_stream<> _clearCoverRequests;
|
||||
|
||||
style::cursor _cursor = style::cur_default;
|
||||
bool _pressed = false;
|
||||
|
||||
base::unique_qptr<PopupMenu> _menu;
|
||||
|
||||
rpl::event_stream<> _modifyRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
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/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractSinglePreview : public RpWidget {
|
||||
public:
|
||||
using RpWidget::RpWidget;
|
||||
|
||||
[[nodiscard]] virtual rpl::producer<> deleteRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> editRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> modifyRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> editCoverRequests() const = 0;
|
||||
[[nodiscard]] virtual rpl::producer<> clearCoverRequests() const = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
675
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp
Normal file
675
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp
Normal file
@@ -0,0 +1,675 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_album_preview.h"
|
||||
|
||||
#include "ui/chat/attach/attach_album_thumbnail.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Media::Streaming {
|
||||
|
||||
[[nodiscard]] QImage PrepareBlurredBackground(QSize outer, QImage frame);
|
||||
|
||||
} // namespace Media::Streaming
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDragDuration = crl::time(200);
|
||||
|
||||
} // namespace
|
||||
|
||||
AlbumPreview::AlbumPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
gsl::span<Ui::PreparedFile> items,
|
||||
SendFilesWay way,
|
||||
Fn<bool(int, AttachActionType)> actionAllowed)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _sendWay(way)
|
||||
, _actionAllowed(std::move(actionAllowed))
|
||||
, _dragTimer([=] { switchToDrag(); }) {
|
||||
setMouseTracking(true);
|
||||
prepareThumbs(items);
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
}
|
||||
|
||||
AlbumPreview::~AlbumPreview() = default;
|
||||
|
||||
void AlbumPreview::setSendWay(SendFilesWay way) {
|
||||
if (_sendWay != way) {
|
||||
cancelDrag();
|
||||
_sendWay = way;
|
||||
}
|
||||
updateSize();
|
||||
updateFileRows();
|
||||
update();
|
||||
}
|
||||
|
||||
void AlbumPreview::updateFileRows() {
|
||||
Expects(_order.size() == _thumbs.size());
|
||||
|
||||
const auto isFile = !_sendWay.sendImagesAsPhotos();
|
||||
auto top = 0;
|
||||
for (auto i = 0; i < _order.size(); i++) {
|
||||
const auto &thumb = _thumbs[_order[i]];
|
||||
thumb->setButtonVisible(isFile && !thumb->isCompressedSticker());
|
||||
thumb->moveButtons(top);
|
||||
top += thumb->fileHeight() + st::sendMediaRowSkip;
|
||||
}
|
||||
}
|
||||
|
||||
base::flat_set<int> AlbumPreview::collectSpoileredIndices() {
|
||||
auto result = base::flat_set<int>();
|
||||
result.reserve(_thumbs.size());
|
||||
auto i = 0;
|
||||
for (const auto &thumb : _thumbs) {
|
||||
if (thumb->hasSpoiler()) {
|
||||
result.emplace(i);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AlbumPreview::canHaveSpoiler(int index) const {
|
||||
return _sendWay.sendImagesAsPhotos();
|
||||
}
|
||||
|
||||
void AlbumPreview::toggleSpoilers(bool enabled) {
|
||||
for (auto &thumb : _thumbs) {
|
||||
thumb->setSpoiler(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> AlbumPreview::takeOrder() {
|
||||
//Expects(_thumbs.size() == _order.size());
|
||||
//Expects(_itemsShownDimensions.size() == _order.size());
|
||||
|
||||
auto reordered = std::vector<std::unique_ptr<AlbumThumbnail>>();
|
||||
auto reorderedShownDimensions = std::vector<QSize>();
|
||||
reordered.reserve(_thumbs.size());
|
||||
reorderedShownDimensions.reserve(_itemsShownDimensions.size());
|
||||
for (auto index : _order) {
|
||||
reordered.push_back(std::move(_thumbs[index]));
|
||||
reorderedShownDimensions.push_back(_itemsShownDimensions[index]);
|
||||
}
|
||||
_thumbs = std::move(reordered);
|
||||
_itemsShownDimensions = std::move(reorderedShownDimensions);
|
||||
return std::exchange(_order, defaultOrder());
|
||||
}
|
||||
|
||||
auto AlbumPreview::generateOrderedLayout() const
|
||||
-> std::vector<GroupMediaLayout> {
|
||||
auto layout = LayoutMediaGroup(
|
||||
_itemsShownDimensions,
|
||||
st::sendMediaPreviewSize,
|
||||
st::historyGroupWidthMin / 2,
|
||||
st::historyGroupSkip / 2);
|
||||
Assert(layout.size() == _order.size());
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::vector<int> AlbumPreview::defaultOrder(int count) const {
|
||||
if (count < 0) {
|
||||
count = _order.size();
|
||||
}
|
||||
return ranges::views::ints(0, count) | ranges::to_vector;
|
||||
}
|
||||
|
||||
void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> items) {
|
||||
_order = defaultOrder(items.size());
|
||||
_itemsShownDimensions = ranges::views::all(
|
||||
_order
|
||||
) | ranges::views::transform([&](int index) {
|
||||
return items[index].shownDimensions;
|
||||
}) | ranges::to_vector;
|
||||
|
||||
const auto count = int(_order.size());
|
||||
const auto layout = generateOrderedLayout();
|
||||
_thumbs.reserve(count);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
_thumbs.push_back(std::make_unique<AlbumThumbnail>(
|
||||
_st,
|
||||
items[i],
|
||||
layout[i],
|
||||
this,
|
||||
[=] { update(); },
|
||||
[=] { changeThumbByIndex(orderIndex(thumbUnderCursor())); },
|
||||
[=] { deleteThumbByIndex(orderIndex(thumbUnderCursor())); }));
|
||||
if (_thumbs.back()->isCompressedSticker()) {
|
||||
_hasMixedFileHeights = true;
|
||||
}
|
||||
}
|
||||
_thumbsHeight = countLayoutHeight(layout);
|
||||
_photosHeight = ranges::accumulate(ranges::views::all(
|
||||
_thumbs
|
||||
) | ranges::views::transform([](const auto &thumb) {
|
||||
return thumb->photoHeight();
|
||||
}), 0) + (count - 1) * st::sendMediaRowSkip;
|
||||
|
||||
if (!_hasMixedFileHeights) {
|
||||
_filesHeight = count * _thumbs.front()->fileHeight()
|
||||
+ (count - 1) * st::sendMediaRowSkip;
|
||||
} else {
|
||||
_filesHeight = ranges::accumulate(ranges::views::all(
|
||||
_thumbs
|
||||
) | ranges::views::transform([](const auto &thumb) {
|
||||
return thumb->fileHeight();
|
||||
}), 0) + (count - 1) * st::sendMediaRowSkip;
|
||||
}
|
||||
}
|
||||
|
||||
int AlbumPreview::contentLeft() const {
|
||||
return (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
}
|
||||
|
||||
int AlbumPreview::contentTop() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
AlbumThumbnail *AlbumPreview::findThumb(QPoint position) const {
|
||||
position -= QPoint(contentLeft(), contentTop());
|
||||
|
||||
auto top = 0;
|
||||
const auto isPhotosWay = _sendWay.sendImagesAsPhotos();
|
||||
const auto skip = st::sendMediaRowSkip;
|
||||
auto find = [&](const auto &thumb) {
|
||||
if (_sendWay.groupFiles() && _sendWay.sendImagesAsPhotos()) {
|
||||
return thumb->containsPoint(position);
|
||||
} else {
|
||||
const auto bottom = top + (isPhotosWay
|
||||
? thumb->photoHeight()
|
||||
: thumb->fileHeight());
|
||||
const auto isUnderTop = (position.y() > top);
|
||||
top = bottom + skip;
|
||||
return isUnderTop && (position.y() < bottom);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto i = ranges::find_if(_thumbs, std::move(find));
|
||||
return (i == _thumbs.end()) ? nullptr : i->get();
|
||||
}
|
||||
|
||||
not_null<AlbumThumbnail*> AlbumPreview::findClosestThumb(
|
||||
QPoint position) const {
|
||||
Expects(_draggedThumb != nullptr);
|
||||
|
||||
if (const auto exact = findThumb(position)) {
|
||||
return exact;
|
||||
}
|
||||
auto result = _draggedThumb;
|
||||
auto distance = _draggedThumb->distanceTo(position);
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto check = thumb->distanceTo(position);
|
||||
if (check < distance) {
|
||||
distance = check;
|
||||
result = thumb.get();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int AlbumPreview::orderIndex(not_null<AlbumThumbnail*> thumb) const {
|
||||
const auto i = ranges::find_if(_order, [&](int index) {
|
||||
return (_thumbs[index].get() == thumb);
|
||||
});
|
||||
Assert(i != _order.end());
|
||||
return int(i - _order.begin());
|
||||
}
|
||||
|
||||
void AlbumPreview::cancelDrag() {
|
||||
_thumbsHeightAnimation.stop();
|
||||
_finishDragAnimation.stop();
|
||||
_shrinkAnimation.stop();
|
||||
if (_draggedThumb) {
|
||||
_draggedThumb->moveInAlbum({ 0, 0 });
|
||||
_draggedThumb = nullptr;
|
||||
}
|
||||
if (_suggestedThumb) {
|
||||
const auto suggestedIndex = orderIndex(_suggestedThumb);
|
||||
if (suggestedIndex > 0) {
|
||||
_thumbs[_order[suggestedIndex - 1]]->suggestMove(0., [] {});
|
||||
}
|
||||
if (suggestedIndex < int(_order.size() - 1)) {
|
||||
_thumbs[_order[suggestedIndex + 1]]->suggestMove(0., [] {});
|
||||
}
|
||||
_suggestedThumb->suggestMove(0., [] {});
|
||||
_suggestedThumb->finishAnimations();
|
||||
_suggestedThumb = nullptr;
|
||||
}
|
||||
_paintedAbove = nullptr;
|
||||
update();
|
||||
}
|
||||
|
||||
void AlbumPreview::finishDrag() {
|
||||
Expects(_draggedThumb != nullptr);
|
||||
Expects(_suggestedThumb != nullptr);
|
||||
|
||||
if (_suggestedThumb != _draggedThumb) {
|
||||
const auto currentIndex = orderIndex(_draggedThumb);
|
||||
const auto newIndex = orderIndex(_suggestedThumb);
|
||||
const auto delta = (currentIndex < newIndex) ? 1 : -1;
|
||||
const auto realIndex = _order[currentIndex];
|
||||
for (auto i = currentIndex; i != newIndex; i += delta) {
|
||||
_order[i] = _order[i + delta];
|
||||
}
|
||||
_order[newIndex] = realIndex;
|
||||
const auto layout = generateOrderedLayout();
|
||||
for (auto i = 0, count = int(_order.size()); i != count; ++i) {
|
||||
_thumbs[_order[i]]->moveToLayout(layout[i]);
|
||||
}
|
||||
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
|
||||
|
||||
updateSizeAnimated(layout);
|
||||
_orderUpdated.fire({});
|
||||
} else {
|
||||
for (const auto &thumb : _thumbs) {
|
||||
thumb->resetLayoutAnimation();
|
||||
}
|
||||
_draggedThumb->animateLayoutToInitial();
|
||||
_finishDragAnimation.start([=] { update(); }, 0., 1., kDragDuration);
|
||||
}
|
||||
}
|
||||
|
||||
int AlbumPreview::countLayoutHeight(
|
||||
const std::vector<GroupMediaLayout> &layout) const {
|
||||
const auto accumulator = [](int current, const auto &item) {
|
||||
return std::max(current, item.geometry.y() + item.geometry.height());
|
||||
};
|
||||
return ranges::accumulate(layout, 0, accumulator);
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSizeAnimated(
|
||||
const std::vector<GroupMediaLayout> &layout) {
|
||||
const auto newHeight = countLayoutHeight(layout);
|
||||
if (newHeight != _thumbsHeight) {
|
||||
_thumbsHeightAnimation.start(
|
||||
[=] { updateSize(); },
|
||||
_thumbsHeight,
|
||||
newHeight,
|
||||
kDragDuration);
|
||||
_thumbsHeight = newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSize() {
|
||||
const auto newHeight = [&] {
|
||||
if (!_sendWay.sendImagesAsPhotos()) {
|
||||
return _filesHeight;
|
||||
} else if (!_sendWay.groupFiles()) {
|
||||
return _photosHeight;
|
||||
} else {
|
||||
return int(base::SafeRound(_thumbsHeightAnimation.value(
|
||||
_thumbsHeight)));
|
||||
}
|
||||
}();
|
||||
if (height() != newHeight) {
|
||||
resize(st::boxWideWidth, newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
if (!_sendWay.sendImagesAsPhotos()) {
|
||||
paintFiles(p, e->rect());
|
||||
} else if (!_sendWay.groupFiles()) {
|
||||
paintPhotos(p, e->rect());
|
||||
} else {
|
||||
paintAlbum(p);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintAlbum(Painter &p) const {
|
||||
const auto shrink = _shrinkAnimation.value(_draggedThumb ? 1. : 0.);
|
||||
const auto moveProgress = _finishDragAnimation.value(1.);
|
||||
const auto left = contentLeft();
|
||||
const auto top = contentTop();
|
||||
for (const auto &thumb : _thumbs) {
|
||||
if (thumb.get() != _paintedAbove) {
|
||||
thumb->paintInAlbum(p, left, top, shrink, moveProgress);
|
||||
}
|
||||
}
|
||||
if (_paintedAbove) {
|
||||
_paintedAbove->paintInAlbum(p, left, top, shrink, moveProgress);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintPhotos(Painter &p, QRect clip) const {
|
||||
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
auto top = 0;
|
||||
const auto outerWidth = width();
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto bottom = top + thumb->photoHeight();
|
||||
const auto guard = gsl::finally([&] {
|
||||
top = bottom + st::sendMediaRowSkip;
|
||||
});
|
||||
if (top >= clip.y() + clip.height()) {
|
||||
break;
|
||||
} else if (bottom <= clip.y()) {
|
||||
continue;
|
||||
}
|
||||
thumb->paintPhoto(p, left, top, outerWidth);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::paintFiles(Painter &p, QRect clip) const {
|
||||
const auto left = (st::boxWideWidth - st::sendMediaPreviewSize) / 2;
|
||||
const auto outerWidth = width();
|
||||
if (!_hasMixedFileHeights) {
|
||||
const auto fileHeight = st::attachPreviewThumbLayout.thumbSize
|
||||
+ st::sendMediaRowSkip;
|
||||
const auto bottom = clip.y() + clip.height();
|
||||
const auto from = std::clamp(
|
||||
clip.y() / fileHeight,
|
||||
0,
|
||||
int(_thumbs.size()));
|
||||
const auto till = std::clamp(
|
||||
(bottom + fileHeight - 1) / fileHeight,
|
||||
0,
|
||||
int(_thumbs.size()));
|
||||
|
||||
auto top = from * fileHeight;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
_thumbs[i]->paintFile(p, left, top, outerWidth);
|
||||
top += fileHeight;
|
||||
}
|
||||
} else {
|
||||
auto top = 0;
|
||||
for (const auto &thumb : _thumbs) {
|
||||
const auto bottom = top + thumb->fileHeight();
|
||||
const auto guard = gsl::finally([&] {
|
||||
top = bottom + st::sendMediaRowSkip;
|
||||
});
|
||||
if (top >= clip.y() + clip.height()) {
|
||||
break;
|
||||
} else if (bottom <= clip.y()) {
|
||||
continue;
|
||||
}
|
||||
thumb->paintFile(p, left, top, outerWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlbumThumbnail *AlbumPreview::thumbUnderCursor() {
|
||||
return findThumb(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
|
||||
void AlbumPreview::deleteThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbDeleted.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::changeThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbChanged.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::modifyThumbByIndex(int index) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
_thumbModified.fire(std::move(index));
|
||||
}
|
||||
|
||||
void AlbumPreview::thumbButtonsCallback(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
AttachButtonType type) {
|
||||
const auto index = orderIndex(thumb);
|
||||
|
||||
switch (type) {
|
||||
case AttachButtonType::None: return;
|
||||
case AttachButtonType::Edit: changeThumbByIndex(index); break;
|
||||
case AttachButtonType::Delete: deleteThumbByIndex(index); break;
|
||||
case AttachButtonType::Modify:
|
||||
cancelDrag();
|
||||
modifyThumbByIndex(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mousePressEvent(QMouseEvent *e) {
|
||||
if (_finishDragAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
const auto position = e->pos();
|
||||
cancelDrag();
|
||||
if (const auto thumb = findThumb(position)) {
|
||||
_draggedStartPosition = position;
|
||||
_pressedThumb = thumb;
|
||||
_pressedButtonType = thumb->buttonTypeFromPoint(position);
|
||||
|
||||
const auto isAlbum = _sendWay.sendImagesAsPhotos()
|
||||
&& _sendWay.groupFiles();
|
||||
if (!isAlbum || e->button() != Qt::LeftButton) {
|
||||
_dragTimer.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pressedButtonType == AttachButtonType::None) {
|
||||
switchToDrag();
|
||||
} else if (_pressedButtonType == AttachButtonType::Modify) {
|
||||
_dragTimer.callOnce(QApplication::startDragTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_sendWay.sendImagesAsPhotos() && !_hasMixedFileHeights) {
|
||||
applyCursor(style::cur_default);
|
||||
return;
|
||||
}
|
||||
if (_dragTimer.isActive()) {
|
||||
_dragTimer.cancel();
|
||||
switchToDrag();
|
||||
}
|
||||
const auto isAlbum = _sendWay.sendImagesAsPhotos()
|
||||
&& _sendWay.groupFiles();
|
||||
if (isAlbum && _draggedThumb) {
|
||||
const auto position = e->pos();
|
||||
_draggedThumb->moveInAlbum(position - _draggedStartPosition);
|
||||
updateSuggestedDrag(_draggedThumb->center());
|
||||
update();
|
||||
} else {
|
||||
const auto thumb = findThumb(e->pos());
|
||||
const auto regularCursor = isAlbum
|
||||
? style::cur_pointer
|
||||
: style::cur_default;
|
||||
const auto cursor = thumb
|
||||
? (thumb->buttonsContainPoint(e->pos())
|
||||
? style::cur_pointer
|
||||
: regularCursor)
|
||||
: style::cur_default;
|
||||
applyCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::applyCursor(style::cursor cursor) {
|
||||
if (_cursor != cursor) {
|
||||
_cursor = cursor;
|
||||
setCursor(_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::updateSuggestedDrag(QPoint position) {
|
||||
auto closest = findClosestThumb(position);
|
||||
auto closestIndex = orderIndex(closest);
|
||||
|
||||
const auto draggedIndex = orderIndex(_draggedThumb);
|
||||
const auto closestIsBeforePoint = closest->isPointAfter(position);
|
||||
if (closestIndex < draggedIndex && closestIsBeforePoint) {
|
||||
closest = _thumbs[_order[++closestIndex]].get();
|
||||
} else if (closestIndex > draggedIndex && !closestIsBeforePoint) {
|
||||
closest = _thumbs[_order[--closestIndex]].get();
|
||||
}
|
||||
|
||||
if (_suggestedThumb == closest) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto last = int(_order.size()) - 1;
|
||||
if (_suggestedThumb) {
|
||||
const auto suggestedIndex = orderIndex(_suggestedThumb);
|
||||
if (suggestedIndex < draggedIndex && suggestedIndex > 0) {
|
||||
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
|
||||
previous->suggestMove(0., [=] { update(); });
|
||||
} else if (suggestedIndex > draggedIndex && suggestedIndex < last) {
|
||||
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
|
||||
next->suggestMove(0., [=] { update(); });
|
||||
}
|
||||
_suggestedThumb->suggestMove(0., [=] { update(); });
|
||||
}
|
||||
_suggestedThumb = closest;
|
||||
const auto suggestedIndex = closestIndex;
|
||||
if (_suggestedThumb != _draggedThumb) {
|
||||
const auto delta = (suggestedIndex < draggedIndex) ? 1. : -1.;
|
||||
if (delta > 0. && suggestedIndex > 0) {
|
||||
const auto previous = _thumbs[_order[suggestedIndex - 1]].get();
|
||||
previous->suggestMove(-delta, [=] { update(); });
|
||||
} else if (delta < 0. && suggestedIndex < last) {
|
||||
const auto next = _thumbs[_order[suggestedIndex + 1]].get();
|
||||
next->suggestMove(-delta, [=] { update(); });
|
||||
}
|
||||
_suggestedThumb->suggestMove(delta, [=] { update(); });
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (_draggedThumb) {
|
||||
finishDrag();
|
||||
_shrinkAnimation.start(
|
||||
[=] { update(); },
|
||||
1.,
|
||||
0.,
|
||||
AlbumThumbnail::kShrinkDuration);
|
||||
_draggedThumb = nullptr;
|
||||
_suggestedThumb = nullptr;
|
||||
update();
|
||||
} else if (const auto thumb = base::take(_pressedThumb)) {
|
||||
const auto was = _pressedButtonType;
|
||||
const auto now = thumb->buttonTypeFromPoint(e->pos());
|
||||
if (e->button() == Qt::RightButton) {
|
||||
showContextMenu(thumb, e->globalPos());
|
||||
} else if (was == now) {
|
||||
thumbButtonsCallback(thumb, now);
|
||||
}
|
||||
}
|
||||
_pressedButtonType = AttachButtonType::None;
|
||||
}
|
||||
|
||||
void AlbumPreview::showContextMenu(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
QPoint position) {
|
||||
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
this,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
const auto index = orderIndex(thumb);
|
||||
if (_actionAllowed(index, AttachActionType::ToggleSpoiler)
|
||||
&& _sendWay.sendImagesAsPhotos()) {
|
||||
const auto spoilered = thumb->hasSpoiler();
|
||||
_menu->addAction(spoilered
|
||||
? tr::lng_context_disable_spoiler(tr::now)
|
||||
: tr::lng_context_spoiler_effect(tr::now), [=] {
|
||||
thumb->setSpoiler(!spoilered);
|
||||
}, spoilered ? &st::menuIconSpoilerOff : &st::menuIconSpoiler);
|
||||
}
|
||||
if (_actionAllowed(index, AttachActionType::EditCover)) {
|
||||
_menu->addAction(tr::lng_context_edit_cover(tr::now), [=] {
|
||||
_thumbEditCoverRequested.fire_copy(index);
|
||||
}, &st::menuIconEdit);
|
||||
|
||||
if (_actionAllowed(index, AttachActionType::ClearCover)) {
|
||||
_menu->addAction(tr::lng_context_clear_cover(tr::now), [=] {
|
||||
_thumbClearCoverRequested.fire_copy(index);
|
||||
}, &st::menuIconCancel);
|
||||
}
|
||||
}
|
||||
|
||||
if (_menu->empty()) {
|
||||
_menu = nullptr;
|
||||
} else {
|
||||
_menu->popup(position);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumPreview::switchToDrag() {
|
||||
_paintedAbove
|
||||
= _suggestedThumb
|
||||
= _draggedThumb
|
||||
= base::take(_pressedThumb);
|
||||
_shrinkAnimation.start(
|
||||
[=] { update(); },
|
||||
0.,
|
||||
1.,
|
||||
AlbumThumbnail::kShrinkDuration);
|
||||
applyCursor(style::cur_sizeall);
|
||||
update();
|
||||
}
|
||||
|
||||
QImage AlbumPreview::generatePriceTagBackground() const {
|
||||
auto wmax = 0;
|
||||
auto hmax = 0;
|
||||
for (auto &thumb : _thumbs) {
|
||||
const auto geometry = thumb->geometry();
|
||||
accumulate_max(wmax, geometry.x() + geometry.width());
|
||||
accumulate_max(hmax, geometry.y() + geometry.height());
|
||||
}
|
||||
const auto size = QSize(wmax, hmax);
|
||||
if (size.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto full = size * ratio;
|
||||
const auto skip = st::historyGroupSkip;
|
||||
auto result = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(ratio);
|
||||
result.fill(Qt::black);
|
||||
auto p = QPainter(&result);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
for (auto &thumb : _thumbs) {
|
||||
const auto geometry = thumb->geometry();
|
||||
if (geometry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const auto w = geometry.width();
|
||||
const auto h = geometry.height();
|
||||
const auto wscale = (w + skip) / float64(w);
|
||||
const auto hscale = (h + skip) / float64(h);
|
||||
p.save();
|
||||
p.translate(geometry.center());
|
||||
p.scale(wscale, hscale);
|
||||
p.translate(-geometry.center());
|
||||
thumb->paintInAlbum(p, 0, 0, 1., 1.);
|
||||
p.restore();
|
||||
}
|
||||
p.end();
|
||||
|
||||
return ::Media::Streaming::PrepareBlurredBackground(
|
||||
full,
|
||||
std::move(result));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
142
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h
Normal file
142
Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
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/rp_widget.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
struct GroupMediaLayout;
|
||||
class AlbumThumbnail;
|
||||
class PopupMenu;
|
||||
|
||||
class AlbumPreview final : public RpWidget {
|
||||
public:
|
||||
AlbumPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
gsl::span<Ui::PreparedFile> items,
|
||||
SendFilesWay way,
|
||||
Fn<bool(int, AttachActionType)> actionAllowed);
|
||||
~AlbumPreview();
|
||||
|
||||
void setSendWay(SendFilesWay way);
|
||||
|
||||
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
|
||||
[[nodiscard]] bool canHaveSpoiler(int index) const;
|
||||
void toggleSpoilers(bool enabled);
|
||||
[[nodiscard]] std::vector<int> takeOrder();
|
||||
|
||||
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
|
||||
return _thumbDeleted.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbChanged() const {
|
||||
return _thumbChanged.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbModified() const {
|
||||
return _thumbModified.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbEditCoverRequested() const {
|
||||
return _thumbEditCoverRequested.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> thumbClearCoverRequested() const {
|
||||
return _thumbClearCoverRequested.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> orderUpdated() const {
|
||||
return _orderUpdated.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage generatePriceTagBackground() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
int countLayoutHeight(
|
||||
const std::vector<GroupMediaLayout> &layout) const;
|
||||
std::vector<GroupMediaLayout> generateOrderedLayout() const;
|
||||
std::vector<int> defaultOrder(int count = -1) const;
|
||||
void prepareThumbs(gsl::span<Ui::PreparedFile> items);
|
||||
void updateSizeAnimated(const std::vector<GroupMediaLayout> &layout);
|
||||
void updateSize();
|
||||
void updateFileRows();
|
||||
|
||||
AlbumThumbnail *thumbUnderCursor();
|
||||
void deleteThumbByIndex(int index);
|
||||
void changeThumbByIndex(int index);
|
||||
void modifyThumbByIndex(int index);
|
||||
void thumbButtonsCallback(
|
||||
not_null<AlbumThumbnail*> thumb,
|
||||
AttachButtonType type);
|
||||
|
||||
void switchToDrag();
|
||||
|
||||
void paintAlbum(Painter &p) const;
|
||||
void paintPhotos(Painter &p, QRect clip) const;
|
||||
void paintFiles(Painter &p, QRect clip) const;
|
||||
|
||||
void applyCursor(style::cursor cursor);
|
||||
int contentLeft() const;
|
||||
int contentTop() const;
|
||||
AlbumThumbnail *findThumb(QPoint position) const;
|
||||
not_null<AlbumThumbnail*> findClosestThumb(QPoint position) const;
|
||||
void updateSuggestedDrag(QPoint position);
|
||||
int orderIndex(not_null<AlbumThumbnail*> thumb) const;
|
||||
void cancelDrag();
|
||||
void finishDrag();
|
||||
|
||||
void showContextMenu(not_null<AlbumThumbnail*> thumb, QPoint position);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
SendFilesWay _sendWay;
|
||||
Fn<bool(int, AttachActionType)> _actionAllowed;
|
||||
style::cursor _cursor = style::cur_default;
|
||||
std::vector<int> _order;
|
||||
std::vector<QSize> _itemsShownDimensions;
|
||||
std::vector<std::unique_ptr<AlbumThumbnail>> _thumbs;
|
||||
int _thumbsHeight = 0;
|
||||
int _photosHeight = 0;
|
||||
int _filesHeight = 0;
|
||||
|
||||
bool _hasMixedFileHeights = false;
|
||||
|
||||
AlbumThumbnail *_draggedThumb = nullptr;
|
||||
AlbumThumbnail *_suggestedThumb = nullptr;
|
||||
AlbumThumbnail *_paintedAbove = nullptr;
|
||||
AlbumThumbnail *_pressedThumb = nullptr;
|
||||
QPoint _draggedStartPosition;
|
||||
|
||||
base::Timer _dragTimer;
|
||||
AttachButtonType _pressedButtonType = AttachButtonType::None;
|
||||
|
||||
rpl::event_stream<int> _thumbDeleted;
|
||||
rpl::event_stream<int> _thumbChanged;
|
||||
rpl::event_stream<int> _thumbModified;
|
||||
rpl::event_stream<int> _thumbEditCoverRequested;
|
||||
rpl::event_stream<int> _thumbClearCoverRequested;
|
||||
rpl::event_stream<> _orderUpdated;
|
||||
|
||||
base::unique_qptr<PopupMenu> _menu;
|
||||
|
||||
mutable Animations::Simple _thumbsHeightAnimation;
|
||||
mutable Animations::Simple _shrinkAnimation;
|
||||
mutable Animations::Simple _finishDragAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
626
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp
Normal file
626
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp
Normal file
@@ -0,0 +1,626 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_album_thumbnail.h"
|
||||
|
||||
#include "core/mime_type.h" // Core::IsMimeSticker.
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AlbumThumbnail::AlbumThumbnail(
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
const GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> repaint,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback)
|
||||
: _st(st)
|
||||
, _layout(layout)
|
||||
, _fullPreview(file.videoCover ? file.videoCover->preview : file.preview)
|
||||
, _shrinkSize(int(std::ceil(st::roundRadiusLarge / 1.4)))
|
||||
, _isPhoto(file.type == PreparedFile::Type::Photo)
|
||||
, _isVideo(file.type == PreparedFile::Type::Video)
|
||||
, _isCompressedSticker(Core::IsMimeSticker(file.information->filemime))
|
||||
, _repaint(std::move(repaint)) {
|
||||
Expects(!_fullPreview.isNull());
|
||||
|
||||
moveToLayout(layout);
|
||||
|
||||
using Option = Images::Option;
|
||||
const auto previewWidth = _fullPreview.width();
|
||||
const auto previewHeight = _fullPreview.height();
|
||||
const auto imageWidth = std::max(
|
||||
previewWidth / style::DevicePixelRatio(),
|
||||
st::minPhotoSize);
|
||||
const auto imageHeight = std::max(
|
||||
previewHeight / style::DevicePixelRatio(),
|
||||
st::minPhotoSize);
|
||||
_photo = PixmapFromImage(Images::Prepare(
|
||||
_fullPreview,
|
||||
QSize(previewWidth, previewHeight),
|
||||
{
|
||||
.options = Option::RoundLarge,
|
||||
.outer = { imageWidth, imageHeight },
|
||||
}));
|
||||
|
||||
const auto &layoutSt = st::attachPreviewThumbLayout;
|
||||
const auto idealSize = layoutSt.thumbSize * style::DevicePixelRatio();
|
||||
const auto fileThumbSize = (previewWidth > previewHeight)
|
||||
? QSize(previewWidth * idealSize / previewHeight, idealSize)
|
||||
: QSize(idealSize, previewHeight * idealSize / previewWidth);
|
||||
_fileThumb = PixmapFromImage(Images::Prepare(
|
||||
_fullPreview,
|
||||
fileThumbSize,
|
||||
{
|
||||
.options = Option::RoundSmall,
|
||||
.outer = { layoutSt.thumbSize, layoutSt.thumbSize },
|
||||
}));
|
||||
|
||||
const auto availableFileWidth = st::sendMediaPreviewSize
|
||||
- layoutSt.thumbSize
|
||||
- layoutSt.thumbSkip
|
||||
// Right buttons.
|
||||
- st::sendBoxAlbumGroupButtonFile.width * 2
|
||||
- st::sendBoxAlbumGroupEditInternalSkip * 2
|
||||
- st::sendBoxAlbumGroupSkipRight;
|
||||
const auto filepath = file.path;
|
||||
if (filepath.isEmpty()) {
|
||||
_name = "image.png";
|
||||
_status = FormatImageSizeText(file.originalDimensions);
|
||||
} else {
|
||||
auto fileinfo = QFileInfo(filepath);
|
||||
_name = fileinfo.fileName();
|
||||
_status = FormatSizeText(fileinfo.size());
|
||||
}
|
||||
_nameWidth = st::semiboldFont->width(_name);
|
||||
if (_nameWidth > availableFileWidth) {
|
||||
_name = st::semiboldFont->elided(
|
||||
_name,
|
||||
availableFileWidth,
|
||||
Qt::ElideMiddle);
|
||||
_nameWidth = st::semiboldFont->width(_name);
|
||||
}
|
||||
_statusWidth = st::normalFont->width(_status);
|
||||
|
||||
_editMedia.create(parent, _st.files.buttonFile);
|
||||
_deleteMedia.create(parent, _st.files.buttonFile);
|
||||
|
||||
const auto duration = st::historyAttach.ripple.hideDuration;
|
||||
_editMedia->setClickedCallback([=] {
|
||||
base::call_delayed(duration, parent, editCallback);
|
||||
});
|
||||
_deleteMedia->setClickedCallback(deleteCallback);
|
||||
|
||||
_editMedia->setIconOverride(&_st.files.buttonFileEdit);
|
||||
_deleteMedia->setIconOverride(&_st.files.buttonFileDelete);
|
||||
|
||||
setSpoiler(file.spoiler);
|
||||
setButtonVisible(false);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::setSpoiler(bool spoiler) {
|
||||
Expects(_repaint != nullptr);
|
||||
|
||||
_spoiler = spoiler
|
||||
? std::make_unique<SpoilerAnimation>(_repaint)
|
||||
: nullptr;
|
||||
_repaint();
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::hasSpoiler() const {
|
||||
return _spoiler != nullptr;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::setButtonVisible(bool value) {
|
||||
_editMedia->setVisible(value);
|
||||
_deleteMedia->setVisible(value);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveButtons(int thumbTop) {
|
||||
const auto top = thumbTop + st::sendBoxFileGroupSkipTop;
|
||||
|
||||
auto right = st::sendBoxFileGroupSkipRight + st::boxPhotoPadding.right();
|
||||
_deleteMedia->moveToRight(right, top);
|
||||
right += st::sendBoxFileGroupEditInternalSkip + _deleteMedia->width();
|
||||
_editMedia->moveToRight(right, top);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::resetLayoutAnimation() {
|
||||
_animateFromGeometry = std::nullopt;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::animateLayoutToInitial() {
|
||||
_animateFromGeometry = countRealGeometry();
|
||||
_suggestedMove = 0.;
|
||||
_albumPosition = QPoint(0, 0);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveToLayout(const GroupMediaLayout &layout) {
|
||||
using namespace Images;
|
||||
|
||||
animateLayoutToInitial();
|
||||
_layout = layout;
|
||||
|
||||
const auto width = _layout.geometry.width();
|
||||
const auto height = _layout.geometry.height();
|
||||
_albumCorners = GetCornersFromSides(_layout.sides);
|
||||
const auto pixSize = GetImageScaleSizeForGeometry(
|
||||
{ _fullPreview.width(), _fullPreview.height() },
|
||||
{ width, height });
|
||||
const auto pixWidth = pixSize.width() * style::DevicePixelRatio();
|
||||
const auto pixHeight = pixSize.height() * style::DevicePixelRatio();
|
||||
|
||||
_albumImage = PixmapFromImage(Prepare(
|
||||
_fullPreview,
|
||||
QSize(pixWidth, pixHeight),
|
||||
{
|
||||
.options = RoundOptions(ImageRoundRadius::Large, _albumCorners),
|
||||
.outer = { width, height },
|
||||
}));
|
||||
_albumImageBlurred = QPixmap();
|
||||
}
|
||||
|
||||
int AlbumThumbnail::photoHeight() const {
|
||||
return _photo.height() / style::DevicePixelRatio();
|
||||
}
|
||||
|
||||
int AlbumThumbnail::fileHeight() const {
|
||||
return _isCompressedSticker
|
||||
? photoHeight()
|
||||
: st::attachPreviewThumbLayout.thumbSize;
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::isCompressedSticker() const {
|
||||
return _isCompressedSticker;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintInAlbum(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 shrinkProgress,
|
||||
float64 moveProgress) {
|
||||
const auto shrink = anim::interpolate(0, _shrinkSize, shrinkProgress);
|
||||
_lastShrinkValue = shrink;
|
||||
const auto geometry = countCurrentGeometry(
|
||||
moveProgress
|
||||
).translated(left, top);
|
||||
auto paintedTo = geometry;
|
||||
const auto revealed = _spoiler ? shrinkProgress : 1.;
|
||||
if (revealed > 0.) {
|
||||
if (shrink > 0 || moveProgress < 1.) {
|
||||
const auto size = geometry.size();
|
||||
paintedTo = geometry.marginsRemoved(
|
||||
{ shrink, shrink, shrink, shrink }
|
||||
);
|
||||
if (shrinkProgress < 1 && _albumCorners != RectPart::None) {
|
||||
prepareCache(size, shrink);
|
||||
p.drawImage(geometry.topLeft(), _albumCache);
|
||||
} else {
|
||||
drawSimpleFrame(p, paintedTo, size);
|
||||
}
|
||||
} else {
|
||||
p.drawPixmap(geometry.topLeft(), _albumImage);
|
||||
}
|
||||
if (_isVideo) {
|
||||
paintPlayVideo(p, geometry);
|
||||
}
|
||||
}
|
||||
if (revealed < 1.) {
|
||||
auto corners = Images::CornersMaskRef(
|
||||
Images::CornersMask(ImageRoundRadius::Large));
|
||||
if (!(_albumCorners & RectPart::TopLeft)) {
|
||||
corners.p[0] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::TopRight)) {
|
||||
corners.p[1] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::BottomLeft)) {
|
||||
corners.p[2] = nullptr;
|
||||
}
|
||||
if (!(_albumCorners & RectPart::BottomRight)) {
|
||||
corners.p[3] = nullptr;
|
||||
}
|
||||
p.setOpacity(1. - revealed);
|
||||
if (_albumImageBlurred.isNull()) {
|
||||
_albumImageBlurred = BlurredPreviewFromPixmap(
|
||||
_albumImage,
|
||||
_albumCorners);
|
||||
}
|
||||
p.drawPixmap(paintedTo, _albumImageBlurred);
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
paintedTo,
|
||||
corners,
|
||||
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
|
||||
_cornerCache);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
|
||||
_lastRectOfButtons = paintButtons(
|
||||
p,
|
||||
geometry,
|
||||
shrinkProgress);
|
||||
_lastRectOfModify = geometry;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintPlayVideo(QPainter &p, QRect geometry) {
|
||||
const auto innerSize = st::msgFileLayout.thumbSize;
|
||||
const auto inner = QRect(
|
||||
geometry.x() + (geometry.width() - innerSize) / 2,
|
||||
geometry.y() + (geometry.height() - innerSize) / 2,
|
||||
innerSize,
|
||||
innerSize);
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::msgDateImgBg);
|
||||
p.drawEllipse(inner);
|
||||
}
|
||||
st::historyFileThumbPlay.paintInCenter(p, inner);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::prepareCache(QSize size, int shrink) {
|
||||
const auto width = std::max(
|
||||
_layout.geometry.width(),
|
||||
_animateFromGeometry ? _animateFromGeometry->width() : 0);
|
||||
const auto height = std::max(
|
||||
_layout.geometry.height(),
|
||||
_animateFromGeometry ? _animateFromGeometry->height() : 0);
|
||||
const auto cacheSize = QSize(width, height) * style::DevicePixelRatio();
|
||||
|
||||
if (_albumCache.width() < cacheSize.width()
|
||||
|| _albumCache.height() < cacheSize.height()) {
|
||||
_albumCache = QImage(cacheSize, QImage::Format_ARGB32_Premultiplied);
|
||||
_albumCache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
_albumCache.fill(Qt::transparent);
|
||||
{
|
||||
Painter p(&_albumCache);
|
||||
const auto to = QRect(QPoint(), size).marginsRemoved(
|
||||
{ shrink, shrink, shrink, shrink }
|
||||
);
|
||||
drawSimpleFrame(p, to, size);
|
||||
}
|
||||
_albumCache = Images::Round(
|
||||
std::move(_albumCache),
|
||||
ImageRoundRadius::Large,
|
||||
_albumCorners,
|
||||
QRect(QPoint(), size * style::DevicePixelRatio()));
|
||||
}
|
||||
|
||||
void AlbumThumbnail::drawSimpleFrame(QPainter &p, QRect to, QSize size) const {
|
||||
const auto fullWidth = _fullPreview.width();
|
||||
const auto fullHeight = _fullPreview.height();
|
||||
const auto previewSize = GetImageScaleSizeForGeometry(
|
||||
{ fullWidth, fullHeight },
|
||||
{ size.width(), size.height() });
|
||||
const auto previewWidth = previewSize.width() * style::DevicePixelRatio();
|
||||
const auto previewHeight = previewSize.height() * style::DevicePixelRatio();
|
||||
const auto width = size.width() * style::DevicePixelRatio();
|
||||
const auto height = size.height() * style::DevicePixelRatio();
|
||||
const auto scaleWidth = to.width() / float64(width);
|
||||
const auto scaleHeight = to.height() / float64(height);
|
||||
const auto Round = [](float64 value) {
|
||||
return int(base::SafeRound(value));
|
||||
};
|
||||
const auto &[from, fillBlack] = [&] {
|
||||
if (previewWidth < width && previewHeight < height) {
|
||||
const auto toWidth = Round(previewWidth * scaleWidth);
|
||||
const auto toHeight = Round(previewHeight * scaleHeight);
|
||||
return std::make_pair(
|
||||
QRect(0, 0, fullWidth, fullHeight),
|
||||
QMargins(
|
||||
(to.width() - toWidth) / 2,
|
||||
(to.height() - toHeight) / 2,
|
||||
to.width() - toWidth - (to.width() - toWidth) / 2,
|
||||
to.height() - toHeight - (to.height() - toHeight) / 2));
|
||||
} else if (previewWidth * height > previewHeight * width) {
|
||||
if (previewHeight >= height) {
|
||||
const auto takeWidth = previewWidth * height / previewHeight;
|
||||
const auto useWidth = fullWidth * width / takeWidth;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
(fullWidth - useWidth) / 2,
|
||||
0,
|
||||
useWidth,
|
||||
fullHeight),
|
||||
QMargins(0, 0, 0, 0));
|
||||
} else {
|
||||
const auto takeWidth = previewWidth;
|
||||
const auto useWidth = fullWidth * width / takeWidth;
|
||||
const auto toHeight = Round(previewHeight * scaleHeight);
|
||||
const auto toSkip = (to.height() - toHeight) / 2;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
(fullWidth - useWidth) / 2,
|
||||
0,
|
||||
useWidth,
|
||||
fullHeight),
|
||||
QMargins(
|
||||
0,
|
||||
toSkip,
|
||||
0,
|
||||
to.height() - toHeight - toSkip));
|
||||
}
|
||||
} else {
|
||||
if (previewWidth >= width) {
|
||||
const auto takeHeight = previewHeight * width / previewWidth;
|
||||
const auto useHeight = fullHeight * height / takeHeight;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
0,
|
||||
(fullHeight - useHeight) / 2,
|
||||
fullWidth,
|
||||
useHeight),
|
||||
QMargins(0, 0, 0, 0));
|
||||
} else {
|
||||
const auto takeHeight = previewHeight;
|
||||
const auto useHeight = fullHeight * height / takeHeight;
|
||||
const auto toWidth = Round(previewWidth * scaleWidth);
|
||||
const auto toSkip = (to.width() - toWidth) / 2;
|
||||
return std::make_pair(
|
||||
QRect(
|
||||
0,
|
||||
(fullHeight - useHeight) / 2,
|
||||
fullWidth,
|
||||
useHeight),
|
||||
QMargins(
|
||||
toSkip,
|
||||
0,
|
||||
to.width() - toWidth - toSkip,
|
||||
0));
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
p.drawImage(to.marginsRemoved(fillBlack), _fullPreview, from);
|
||||
if (fillBlack.top() > 0) {
|
||||
p.fillRect(to.x(), to.y(), to.width(), fillBlack.top(), st::imageBg);
|
||||
}
|
||||
if (fillBlack.bottom() > 0) {
|
||||
p.fillRect(
|
||||
to.x(),
|
||||
to.y() + to.height() - fillBlack.bottom(),
|
||||
to.width(),
|
||||
fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
if (fillBlack.left() > 0) {
|
||||
p.fillRect(
|
||||
to.x(),
|
||||
to.y() + fillBlack.top(),
|
||||
fillBlack.left(),
|
||||
to.height() - fillBlack.top() - fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
if (fillBlack.right() > 0) {
|
||||
p.fillRect(
|
||||
to.x() + to.width() - fillBlack.right(),
|
||||
to.y() + fillBlack.top(),
|
||||
fillBlack.right(),
|
||||
to.height() - fillBlack.top() - fillBlack.bottom(),
|
||||
st::imageBg);
|
||||
}
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintPhoto(Painter &p, int left, int top, int outerWidth) {
|
||||
const auto size = _photo.size() / style::DevicePixelRatio();
|
||||
if (_spoiler && _photoBlurred.isNull()) {
|
||||
_photoBlurred = BlurredPreviewFromPixmap(
|
||||
_photo,
|
||||
RectPart::AllCorners);
|
||||
}
|
||||
const auto &pixmap = _spoiler ? _photoBlurred : _photo;
|
||||
const auto rect = QRect(
|
||||
left + (st::sendMediaPreviewSize - size.width()) / 2,
|
||||
top,
|
||||
pixmap.width() / pixmap.devicePixelRatio(),
|
||||
pixmap.height() / pixmap.devicePixelRatio());
|
||||
p.drawPixmapLeft(
|
||||
left + (st::sendMediaPreviewSize - size.width()) / 2,
|
||||
top,
|
||||
outerWidth,
|
||||
pixmap);
|
||||
if (_spoiler) {
|
||||
const auto paused = On(PowerSaving::kChatSpoiler);
|
||||
FillSpoilerRect(
|
||||
p,
|
||||
rect,
|
||||
Images::CornersMaskRef(
|
||||
Images::CornersMask(ImageRoundRadius::Large)),
|
||||
DefaultImageSpoiler().frame(_spoiler->index(crl::now(), paused)),
|
||||
_cornerCache);
|
||||
} else if (_isVideo) {
|
||||
paintPlayVideo(p, rect);
|
||||
}
|
||||
|
||||
const auto topLeft = QPoint{ left, top };
|
||||
|
||||
_lastRectOfButtons = paintButtons(
|
||||
p,
|
||||
QRect(left, top, st::sendMediaPreviewSize, size.height()),
|
||||
0);
|
||||
|
||||
_lastRectOfModify = QRect(topLeft, size);
|
||||
}
|
||||
|
||||
void AlbumThumbnail::paintFile(
|
||||
Painter &p,
|
||||
int left,
|
||||
int top,
|
||||
int outerWidth) {
|
||||
|
||||
if (isCompressedSticker()) {
|
||||
auto spoiler = base::take(_spoiler);
|
||||
paintPhoto(p, left, top, outerWidth);
|
||||
_spoiler = base::take(spoiler);
|
||||
return;
|
||||
}
|
||||
const auto &st = st::attachPreviewThumbLayout;
|
||||
const auto textLeft = left + st.thumbSize + st.thumbSkip;
|
||||
|
||||
p.drawPixmap(left, top, _fileThumb);
|
||||
p.setFont(st::semiboldFont);
|
||||
p.setPen(_st.files.nameFg);
|
||||
p.drawTextLeft(
|
||||
textLeft,
|
||||
top + st.nameTop,
|
||||
outerWidth,
|
||||
_name,
|
||||
_nameWidth);
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(_st.files.statusFg);
|
||||
p.drawTextLeft(
|
||||
textLeft,
|
||||
top + st.statusTop,
|
||||
outerWidth,
|
||||
_status,
|
||||
_statusWidth);
|
||||
|
||||
_lastRectOfModify = QRect(
|
||||
QPoint(left, top),
|
||||
_fileThumb.size() / style::DevicePixelRatio());
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::geometry() const {
|
||||
return _layout.geometry;
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::containsPoint(QPoint position) const {
|
||||
return _layout.geometry.contains(position);
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::buttonsContainPoint(QPoint position) const {
|
||||
return ((_isPhoto && !_isCompressedSticker)
|
||||
? _lastRectOfModify
|
||||
: _lastRectOfButtons).contains(position);
|
||||
}
|
||||
|
||||
AttachButtonType AlbumThumbnail::buttonTypeFromPoint(QPoint position) const {
|
||||
if (!buttonsContainPoint(position)) {
|
||||
return AttachButtonType::None;
|
||||
}
|
||||
return (!_lastRectOfButtons.contains(position) && !_isCompressedSticker)
|
||||
? AttachButtonType::Modify
|
||||
: (_buttons.vertical()
|
||||
? (position.y() < _lastRectOfButtons.center().y())
|
||||
: (position.x() < _lastRectOfButtons.center().x()))
|
||||
? AttachButtonType::Edit
|
||||
: AttachButtonType::Delete;
|
||||
}
|
||||
|
||||
int AlbumThumbnail::distanceTo(QPoint position) const {
|
||||
const auto delta = (_layout.geometry.center() - position);
|
||||
return QPoint::dotProduct(delta, delta);
|
||||
}
|
||||
|
||||
bool AlbumThumbnail::isPointAfter(QPoint position) const {
|
||||
return position.x() > _layout.geometry.center().x();
|
||||
}
|
||||
|
||||
void AlbumThumbnail::moveInAlbum(QPoint to) {
|
||||
_albumPosition = to;
|
||||
}
|
||||
|
||||
QPoint AlbumThumbnail::center() const {
|
||||
auto realGeometry = _layout.geometry;
|
||||
realGeometry.moveTopLeft(realGeometry.topLeft() + _albumPosition);
|
||||
return realGeometry.center();
|
||||
}
|
||||
|
||||
void AlbumThumbnail::suggestMove(float64 delta, Fn<void()> callback) {
|
||||
if (_suggestedMove != delta) {
|
||||
_suggestedMoveAnimation.start(
|
||||
std::move(callback),
|
||||
_suggestedMove,
|
||||
delta,
|
||||
kShrinkDuration);
|
||||
_suggestedMove = delta;
|
||||
}
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::countRealGeometry() const {
|
||||
const auto addLeft = int(base::SafeRound(
|
||||
_suggestedMoveAnimation.value(_suggestedMove) * _lastShrinkValue));
|
||||
const auto current = _layout.geometry;
|
||||
const auto realTopLeft = current.topLeft()
|
||||
+ _albumPosition
|
||||
+ QPoint(addLeft, 0);
|
||||
return { realTopLeft, current.size() };
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::countCurrentGeometry(float64 progress) const {
|
||||
const auto now = countRealGeometry();
|
||||
if (_animateFromGeometry && progress < 1.) {
|
||||
return {
|
||||
anim::interpolate(_animateFromGeometry->x(), now.x(), progress),
|
||||
anim::interpolate(_animateFromGeometry->y(), now.y(), progress),
|
||||
anim::interpolate(_animateFromGeometry->width(), now.width(), progress),
|
||||
anim::interpolate(_animateFromGeometry->height(), now.height(), progress)
|
||||
};
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
void AlbumThumbnail::finishAnimations() {
|
||||
_suggestedMoveAnimation.stop();
|
||||
}
|
||||
|
||||
QRect AlbumThumbnail::paintButtons(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
float64 shrinkProgress) {
|
||||
const auto &skipRight = st::sendBoxAlbumGroupSkipRight;
|
||||
const auto &skipTop = st::sendBoxAlbumGroupSkipTop;
|
||||
const auto outerWidth = geometry.width();
|
||||
const auto outerHeight = geometry.height();
|
||||
if (st::sendBoxAlbumGroupSize.width() <= outerWidth) {
|
||||
_buttons.setVertical(false);
|
||||
} else if (st::sendBoxAlbumGroupSize.height() <= outerHeight) {
|
||||
_buttons.setVertical(true);
|
||||
} else {
|
||||
// If the size is tiny, skip the buttons.
|
||||
return QRect();
|
||||
}
|
||||
const auto groupWidth = _buttons.width();
|
||||
const auto groupHeight = _buttons.height();
|
||||
|
||||
// If the width is too small,
|
||||
// it would be better to display the buttons in the center.
|
||||
const auto groupX = geometry.x() + ((groupWidth + skipRight * 2 > outerWidth)
|
||||
? (outerWidth - groupWidth) / 2
|
||||
: outerWidth - skipRight - groupWidth);
|
||||
const auto groupY = geometry.y() + ((groupHeight + skipTop * 2 > outerHeight)
|
||||
? (outerHeight - groupHeight) / 2
|
||||
: skipTop);
|
||||
|
||||
const auto opacity = p.opacity();
|
||||
p.setOpacity(1.0 - shrinkProgress);
|
||||
_buttons.paint(p, groupX, groupY);
|
||||
p.setOpacity(opacity);
|
||||
|
||||
return QRect(groupX, groupY, groupWidth, _buttons.height());
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
124
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h
Normal file
124
Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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/chat/attach/attach_controls.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/grouped_layout.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace style {
|
||||
struct ComposeControls;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
class IconButton;
|
||||
class SpoilerAnimation;
|
||||
|
||||
class AlbumThumbnail final {
|
||||
public:
|
||||
AlbumThumbnail(
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
const GroupMediaLayout &layout,
|
||||
QWidget *parent,
|
||||
Fn<void()> repaint,
|
||||
Fn<void()> editCallback,
|
||||
Fn<void()> deleteCallback);
|
||||
|
||||
void moveToLayout(const GroupMediaLayout &layout);
|
||||
void animateLayoutToInitial();
|
||||
void resetLayoutAnimation();
|
||||
|
||||
void setSpoiler(bool spoiler);
|
||||
[[nodiscard]] bool hasSpoiler() const;
|
||||
|
||||
[[nodiscard]] int photoHeight() const;
|
||||
[[nodiscard]] int fileHeight() const;
|
||||
|
||||
void paintInAlbum(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
float64 shrinkProgress,
|
||||
float64 moveProgress);
|
||||
void paintPhoto(Painter &p, int left, int top, int outerWidth);
|
||||
void paintFile(Painter &p, int left, int top, int outerWidth);
|
||||
|
||||
[[nodiscard]] QRect geometry() const;
|
||||
[[nodiscard]] bool containsPoint(QPoint position) const;
|
||||
[[nodiscard]] bool buttonsContainPoint(QPoint position) const;
|
||||
[[nodiscard]] AttachButtonType buttonTypeFromPoint(
|
||||
QPoint position) const;
|
||||
[[nodiscard]] int distanceTo(QPoint position) const;
|
||||
[[nodiscard]] bool isPointAfter(QPoint position) const;
|
||||
void moveInAlbum(QPoint to);
|
||||
[[nodiscard]] QPoint center() const;
|
||||
void suggestMove(float64 delta, Fn<void()> callback);
|
||||
void finishAnimations();
|
||||
|
||||
void setButtonVisible(bool value);
|
||||
void moveButtons(int thumbTop);
|
||||
|
||||
[[nodiscard]] bool isCompressedSticker() const;
|
||||
|
||||
static constexpr auto kShrinkDuration = crl::time(150);
|
||||
|
||||
private:
|
||||
QRect countRealGeometry() const;
|
||||
QRect countCurrentGeometry(float64 progress) const;
|
||||
void prepareCache(QSize size, int shrink);
|
||||
void drawSimpleFrame(QPainter &p, QRect to, QSize size) const;
|
||||
QRect paintButtons(
|
||||
QPainter &p,
|
||||
QRect geometry,
|
||||
float64 shrinkProgress);
|
||||
void paintPlayVideo(QPainter &p, QRect geometry);
|
||||
|
||||
const style::ComposeControls &_st;
|
||||
GroupMediaLayout _layout;
|
||||
std::optional<QRect> _animateFromGeometry;
|
||||
const QImage _fullPreview;
|
||||
const int _shrinkSize;
|
||||
const bool _isPhoto;
|
||||
const bool _isVideo;
|
||||
QPixmap _albumImage;
|
||||
QPixmap _albumImageBlurred;
|
||||
QImage _albumCache;
|
||||
QPoint _albumPosition;
|
||||
RectParts _albumCorners = RectPart::None;
|
||||
QPixmap _photo;
|
||||
QPixmap _photoBlurred;
|
||||
QPixmap _fileThumb;
|
||||
QString _name;
|
||||
QString _status;
|
||||
int _nameWidth = 0;
|
||||
int _statusWidth = 0;
|
||||
float64 _suggestedMove = 0.;
|
||||
Animations::Simple _suggestedMoveAnimation;
|
||||
int _lastShrinkValue = 0;
|
||||
AttachControls _buttons;
|
||||
|
||||
bool _isCompressedSticker = false;
|
||||
std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
QImage _cornerCache;
|
||||
Fn<void()> _repaint;
|
||||
|
||||
QRect _lastRectOfModify;
|
||||
QRect _lastRectOfButtons;
|
||||
|
||||
object_ptr<IconButton> _editMedia = { nullptr };
|
||||
object_ptr<IconButton> _deleteMedia = { nullptr };
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.cpp
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_bot_downloads.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
namespace {
|
||||
|
||||
class Action final : public Menu::ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback);
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override { return _dummyAction; }
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
void refresh(const DownloadsEntry &entry);
|
||||
|
||||
private:
|
||||
QPoint prepareRippleStartPosition() const override {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
QImage prepareRippleMask() const override {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
int contentHeight() const override { return _height; }
|
||||
|
||||
void prepare();
|
||||
void paint(Painter &p);
|
||||
|
||||
const not_null<QAction*> _dummyAction;
|
||||
const style::Menu &_st = st::defaultMenu;
|
||||
|
||||
DownloadsEntry _entry;
|
||||
Text::String _name;
|
||||
FlatLabel _progress;
|
||||
IconButton _cancel;
|
||||
int _textWidth = 0;
|
||||
const int _height;
|
||||
};
|
||||
|
||||
Action::Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const DownloadsEntry &entry,
|
||||
Fn<void(DownloadsAction)> callback)
|
||||
: ItemBase(parent, st::defaultMenu)
|
||||
, _dummyAction(new QAction(parent))
|
||||
, _progress(this, st::botDownloadProgress)
|
||||
, _cancel(this, st::botDownloadCancel)
|
||||
, _height(st::ttlItemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ st::ttlItemTimerFont->height
|
||||
+ st::ttlItemPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
initResizeHook(parent->sizeValue());
|
||||
setClickedCallback([=] {
|
||||
if (isEnabled()) {
|
||||
callback(DownloadsAction::Open);
|
||||
}
|
||||
});
|
||||
_cancel.setClickedCallback([=] {
|
||||
callback(DownloadsAction::Cancel);
|
||||
});
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}, lifetime());
|
||||
|
||||
widthValue() | rpl::on_next([=](int width) {
|
||||
_progress.moveToLeft(
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top() + _st.itemStyle.font->height,
|
||||
width);
|
||||
|
||||
_cancel.moveToRight(
|
||||
_st.itemPadding.right(),
|
||||
(_height - _cancel.height()) / 2,
|
||||
width);
|
||||
}, lifetime());
|
||||
|
||||
_progress.setClickHandlerFilter([=](const auto &...) {
|
||||
callback(DownloadsAction::Retry);
|
||||
return false;
|
||||
});
|
||||
|
||||
enableMouseSelecting();
|
||||
refresh(entry);
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto selected = isSelected();
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(0, 0, width(), _height, selected ? _st.itemBgOver : _st.itemBg);
|
||||
if (isEnabled()) {
|
||||
paintRipple(p, 0, 0);
|
||||
}
|
||||
|
||||
p.setPen(selected ? _st.itemFgOver : _st.itemFg);
|
||||
_name.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
st::ttlItemPadding.top(),
|
||||
_textWidth,
|
||||
width());
|
||||
|
||||
_progress.setTextColorOverride(
|
||||
selected ? _st.itemFgShortcutOver->c : _st.itemFgShortcut->c);
|
||||
}
|
||||
|
||||
void Action::prepare() {
|
||||
const auto filenameWidth = _name.maxWidth();
|
||||
const auto progressWidth = _progress.textMaxWidth();
|
||||
const auto &padding = _st.itemPadding;
|
||||
|
||||
const auto goodWidth = std::max(filenameWidth, progressWidth);
|
||||
|
||||
// Example max width: "4000 / 4000 MB"
|
||||
const auto countWidth = [&](const QString &text) {
|
||||
return st::ttlItemTimerFont->width(text);
|
||||
};
|
||||
const auto maxProgressWidth = countWidth(tr::lng_media_save_progress(
|
||||
tr::now,
|
||||
lt_ready,
|
||||
"0000",
|
||||
lt_total,
|
||||
"0000",
|
||||
lt_mb,
|
||||
"MB"));
|
||||
const auto maxStartingWidth = countWidth(
|
||||
tr::lng_bot_download_starting(tr::now));
|
||||
const auto maxFailedWidth = countWidth(tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
tr::lng_bot_download_retry(tr::now)));
|
||||
|
||||
const auto cancel = _cancel.width() + padding.right();
|
||||
const auto paddings = padding.left() + padding.right() + cancel;
|
||||
const auto w = std::clamp(
|
||||
paddings + std::max({
|
||||
goodWidth,
|
||||
maxProgressWidth,
|
||||
maxStartingWidth,
|
||||
maxFailedWidth,
|
||||
}),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
_textWidth = w - paddings;
|
||||
_progress.resizeToWidth(_textWidth);
|
||||
setMinWidth(w);
|
||||
update();
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return _entry.total > 0 && _entry.ready == _entry.total;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(Menu::TriggeredSource::Keyboard);
|
||||
}
|
||||
}
|
||||
|
||||
void Action::refresh(const DownloadsEntry &entry) {
|
||||
_entry = entry;
|
||||
const auto filename = entry.path.split('/').last();
|
||||
_name.setMarkedText(_st.itemStyle, { filename }, kDefaultTextOptions);
|
||||
|
||||
const auto progressText = (entry.total && entry.total == entry.ready)
|
||||
? TextWithEntities{ FormatSizeText(entry.total) }
|
||||
: entry.loading
|
||||
? (entry.total
|
||||
? TextWithEntities{
|
||||
FormatProgressText(entry.ready, entry.total),
|
||||
}
|
||||
: tr::lng_bot_download_starting(tr::now, tr::marked))
|
||||
: tr::lng_bot_download_failed(
|
||||
tr::now,
|
||||
lt_retry,
|
||||
Text::Link(tr::lng_bot_download_retry(tr::now)),
|
||||
tr::marked);
|
||||
_progress.setMarkedText(progressText);
|
||||
|
||||
const auto enabled = isEnabled();
|
||||
setCursor(enabled ? style::cur_pointer : style::cur_default);
|
||||
_cancel.setVisible(!enabled && _entry.loading);
|
||||
_progress.setAttribute(Qt::WA_TransparentForMouseEvents, enabled);
|
||||
|
||||
prepare();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FnMut<void(not_null<PopupMenu*>)> FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback) {
|
||||
return [callback, moved = std::move(content)](
|
||||
not_null<PopupMenu*> menu) mutable {
|
||||
struct Row {
|
||||
not_null<Action*> action;
|
||||
uint32 id = 0;
|
||||
};
|
||||
struct State {
|
||||
std::vector<Row> rows;
|
||||
};
|
||||
const auto state = menu->lifetime().make_state<State>();
|
||||
std::move(
|
||||
moved
|
||||
) | rpl::on_next([=](
|
||||
const std::vector<DownloadsEntry> &entries) {
|
||||
auto found = base::flat_set<uint32>();
|
||||
for (const auto &entry : entries | ranges::views::reverse) {
|
||||
const auto id = entry.id;
|
||||
const auto path = entry.path;
|
||||
const auto i = ranges::find(state->rows, id, &Row::id);
|
||||
found.emplace(id);
|
||||
|
||||
if (i != end(state->rows)) {
|
||||
i->action->refresh(entry);
|
||||
} else {
|
||||
auto action = base::make_unique_q<Action>(
|
||||
menu,
|
||||
entry,
|
||||
[=](DownloadsAction type) { callback(id, type); });
|
||||
state->rows.push_back({
|
||||
.action = action.get(),
|
||||
.id = id,
|
||||
});
|
||||
menu->addAction(std::move(action));
|
||||
}
|
||||
}
|
||||
for (auto i = begin(state->rows); i != end(state->rows);) {
|
||||
if (!found.contains(i->id)) {
|
||||
menu->removeAction(i - begin(state->rows));
|
||||
i = state->rows.erase(i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}, menu->lifetime());
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
47
Telegram/SourceFiles/ui/chat/attach/attach_bot_downloads.h
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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 Ui {
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
|
||||
struct DownloadsProgress {
|
||||
uint64 ready = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
const DownloadsProgress &a,
|
||||
const DownloadsProgress &b) = default;
|
||||
};
|
||||
|
||||
struct DownloadsEntry {
|
||||
uint32 id = 0;
|
||||
QString url;
|
||||
QString path;
|
||||
uint64 ready : 63 = 0;
|
||||
uint64 loading : 1 = 0;
|
||||
uint64 total : 63 = 0;
|
||||
uint64 failed : 1 = 0;
|
||||
};
|
||||
|
||||
enum class DownloadsAction {
|
||||
Open,
|
||||
Retry,
|
||||
Cancel,
|
||||
};
|
||||
|
||||
[[nodiscard]] auto FillAttachBotDownloadsSubmenu(
|
||||
rpl::producer<std::vector<DownloadsEntry>> content,
|
||||
Fn<void(uint32, DownloadsAction)> callback)
|
||||
-> FnMut<void(not_null<PopupMenu*>)>;
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
2150
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
Normal file
2150
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp
Normal file
File diff suppressed because it is too large
Load Diff
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
Normal file
268
Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
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/expected.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "base/flags.h"
|
||||
#include "ui/rect_part.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "webview/webview_common.h"
|
||||
|
||||
class QJsonObject;
|
||||
class QJsonValue;
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class BoxContent;
|
||||
class RpWidget;
|
||||
class SeparatePanel;
|
||||
class IconButton;
|
||||
enum class LayerOption;
|
||||
using LayerOptions = base::flags<LayerOption>;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Webview {
|
||||
struct Available;
|
||||
} // namespace Webview
|
||||
|
||||
namespace Ui::BotWebView {
|
||||
|
||||
struct DownloadsProgress;
|
||||
struct DownloadsEntry;
|
||||
enum class DownloadsAction;
|
||||
|
||||
[[nodiscard]] TextWithEntities ErrorText(const Webview::Available &info);
|
||||
|
||||
enum class MenuButton {
|
||||
None = 0x00,
|
||||
OpenBot = 0x01,
|
||||
RemoveFromMenu = 0x02,
|
||||
RemoveFromMainMenu = 0x04,
|
||||
ShareGame = 0x08,
|
||||
};
|
||||
inline constexpr bool is_flag_type(MenuButton) { return true; }
|
||||
using MenuButtons = base::flags<MenuButton>;
|
||||
|
||||
using CustomMethodResult = base::expected<QByteArray, QString>;
|
||||
struct CustomMethodRequest {
|
||||
QString method;
|
||||
QByteArray params;
|
||||
Fn<void(CustomMethodResult)> callback;
|
||||
};
|
||||
|
||||
struct SetEmojiStatusRequest {
|
||||
uint64 customEmojiId = 0;
|
||||
TimeId duration = 0;
|
||||
Fn<void(QString)> callback;
|
||||
};
|
||||
|
||||
struct DownloadFileRequest {
|
||||
QString url;
|
||||
QString name;
|
||||
Fn<void(bool)> callback;
|
||||
};
|
||||
|
||||
struct SendPreparedMessageRequest {
|
||||
QString id = 0;
|
||||
Fn<void(QString)> callback;
|
||||
};
|
||||
|
||||
class Delegate {
|
||||
public:
|
||||
[[nodiscard]] virtual Webview::ThemeParams botThemeParams() = 0;
|
||||
[[nodiscard]] virtual auto botDownloads(bool forceCheck = false)
|
||||
-> const std::vector<DownloadsEntry> & = 0;
|
||||
virtual void botDownloadsAction(uint32 id, DownloadsAction type) = 0;
|
||||
virtual bool botHandleLocalUri(QString uri, bool keepOpen) = 0;
|
||||
virtual void botHandleInvoice(QString slug) = 0;
|
||||
virtual void botHandleMenuButton(MenuButton button) = 0;
|
||||
virtual bool botValidateExternalLink(QString uri) = 0;
|
||||
virtual void botOpenIvLink(QString uri) = 0;
|
||||
virtual void botSendData(QByteArray data) = 0;
|
||||
virtual void botSwitchInlineQuery(
|
||||
std::vector<QString> chatTypes,
|
||||
QString query) = 0;
|
||||
virtual void botCheckWriteAccess(Fn<void(bool allowed)> callback) = 0;
|
||||
virtual void botAllowWriteAccess(Fn<void(bool allowed)> callback) = 0;
|
||||
virtual bool botStorageWrite(
|
||||
QString key,
|
||||
std::optional<QString> value) = 0;
|
||||
[[nodiscard]] virtual std::optional<QString> botStorageRead(
|
||||
QString key) = 0;
|
||||
virtual void botStorageClear() = 0;
|
||||
virtual void botRequestEmojiStatusAccess(
|
||||
Fn<void(bool allowed)> callback) = 0;
|
||||
virtual void botSharePhone(Fn<void(bool shared)> callback) = 0;
|
||||
virtual void botInvokeCustomMethod(CustomMethodRequest request) = 0;
|
||||
virtual void botSetEmojiStatus(SetEmojiStatusRequest request) = 0;
|
||||
virtual void botDownloadFile(DownloadFileRequest request) = 0;
|
||||
virtual void botSendPreparedMessage(
|
||||
SendPreparedMessageRequest request) = 0;
|
||||
virtual void botVerifyAge(int age) = 0;
|
||||
virtual void botOpenPrivacyPolicy() = 0;
|
||||
virtual void botClose() = 0;
|
||||
};
|
||||
|
||||
struct Args {
|
||||
QString url;
|
||||
Webview::StorageId storageId;
|
||||
rpl::producer<QString> title;
|
||||
object_ptr<Ui::RpWidget> titleBadge = { nullptr };
|
||||
rpl::producer<QString> bottom;
|
||||
not_null<Delegate*> delegate;
|
||||
MenuButtons menuButtons;
|
||||
bool fullscreen = false;
|
||||
bool allowClipboardRead = false;
|
||||
rpl::producer<DownloadsProgress> downloadsProgress;
|
||||
};
|
||||
|
||||
class Panel final : public base::has_weak_ptr {
|
||||
public:
|
||||
explicit Panel(Args &&args);
|
||||
~Panel();
|
||||
|
||||
void requestActivate();
|
||||
void toggleProgress(bool shown);
|
||||
|
||||
void showBox(object_ptr<BoxContent> box);
|
||||
void showBox(
|
||||
object_ptr<BoxContent> box,
|
||||
LayerOptions options,
|
||||
anim::type animated);
|
||||
void hideLayer(anim::type animated);
|
||||
void showToast(TextWithEntities &&text);
|
||||
not_null<QWidget*> toastParent() const;
|
||||
void showCriticalError(const TextWithEntities &text);
|
||||
void showWebviewError(
|
||||
const QString &text,
|
||||
const Webview::Available &information);
|
||||
|
||||
void updateThemeParams(const Webview::ThemeParams ¶ms);
|
||||
|
||||
void hideForPayment();
|
||||
void invoiceClosed(const QString &slug, const QString &status);
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
class Button;
|
||||
struct Progress;
|
||||
struct WebviewWithLifetime;
|
||||
|
||||
bool showWebview(Args &&args, const Webview::ThemeParams ¶ms);
|
||||
|
||||
bool createWebview(const Webview::ThemeParams ¶ms);
|
||||
void createWebviewBottom();
|
||||
void showWebviewProgress();
|
||||
void hideWebviewProgress();
|
||||
void setupDownloadsProgress(
|
||||
not_null<RpWidget*> button,
|
||||
rpl::producer<DownloadsProgress> progress,
|
||||
bool fullscreen);
|
||||
void setTitle(rpl::producer<QString> title);
|
||||
void sendDataMessage(const QJsonObject &args);
|
||||
void switchInlineQueryMessage(const QJsonObject &args);
|
||||
void processSendMessageRequest(const QJsonObject &args);
|
||||
void processEmojiStatusRequest(const QJsonObject &args);
|
||||
void processEmojiStatusAccessRequest();
|
||||
void processStorageSaveKey(const QJsonObject &args);
|
||||
void processStorageGetKey(const QJsonObject &args);
|
||||
void processStorageClear(const QJsonObject &args);
|
||||
void processButtonMessage(
|
||||
std::unique_ptr<Button> &button,
|
||||
const QJsonObject &args);
|
||||
void processBackButtonMessage(const QJsonObject &args);
|
||||
void processSettingsButtonMessage(const QJsonObject &args);
|
||||
void processHeaderColor(const QJsonObject &args);
|
||||
void processBackgroundColor(const QJsonObject &args);
|
||||
void processBottomBarColor(const QJsonObject &args);
|
||||
void processDownloadRequest(const QJsonObject &args);
|
||||
void openTgLink(const QJsonObject &args);
|
||||
void openExternalLink(const QJsonObject &args);
|
||||
void openInvoice(const QJsonObject &args);
|
||||
void openPopup(const QJsonObject &args);
|
||||
void openScanQrPopup(const QJsonObject &args);
|
||||
void openShareStory(const QJsonObject &args);
|
||||
void requestWriteAccess();
|
||||
void replyRequestWriteAccess(bool allowed);
|
||||
void requestPhone();
|
||||
void replyRequestPhone(bool shared);
|
||||
void invokeCustomMethod(const QJsonObject &args);
|
||||
void replyCustomMethod(QJsonValue requestId, QJsonObject response);
|
||||
void requestClipboardText(const QJsonObject &args);
|
||||
void setupClosingBehaviour(const QJsonObject &args);
|
||||
void replyDeviceStorage(
|
||||
const QJsonObject &args,
|
||||
const QString &event,
|
||||
QJsonObject response);
|
||||
void deviceStorageFailed(const QJsonObject &args, QString error);
|
||||
void secureStorageFailed(const QJsonObject &args);
|
||||
void createButton(std::unique_ptr<Button> &button);
|
||||
void scheduleCloseWithConfirmation();
|
||||
void closeWithConfirmation();
|
||||
void sendViewport();
|
||||
void sendSafeArea();
|
||||
void sendContentSafeArea();
|
||||
void sendFullScreen();
|
||||
|
||||
void updateColorOverrides(const Webview::ThemeParams ¶ms);
|
||||
void overrideBodyColor(std::optional<QColor> color);
|
||||
|
||||
using EventData = std::variant<QString, QJsonObject>;
|
||||
void postEvent(const QString &event);
|
||||
void postEvent(const QString &event, EventData data);
|
||||
|
||||
[[nodiscard]] bool allowOpenLink() const;
|
||||
[[nodiscard]] bool allowClipboardQuery() const;
|
||||
[[nodiscard]] bool progressWithBackground() const;
|
||||
[[nodiscard]] QRect progressRect() const;
|
||||
void setupProgressGeometry();
|
||||
void layoutButtons();
|
||||
|
||||
Webview::StorageId _storageId;
|
||||
const not_null<Delegate*> _delegate;
|
||||
bool _closeNeedConfirmation = false;
|
||||
bool _hasSettingsButton = false;
|
||||
MenuButtons _menuButtons = {};
|
||||
std::unique_ptr<SeparatePanel> _widget;
|
||||
std::unique_ptr<WebviewWithLifetime> _webview;
|
||||
std::unique_ptr<RpWidget> _webviewBottom;
|
||||
QPointer<FlatLabel> _webviewBottomLabel;
|
||||
rpl::variable<QString> _bottomText;
|
||||
QPointer<RpWidget> _webviewParent;
|
||||
std::unique_ptr<RpWidget> _bottomButtonsBg;
|
||||
std::unique_ptr<Button> _mainButton;
|
||||
std::unique_ptr<Button> _secondaryButton;
|
||||
RectPart _secondaryPosition = RectPart::Left;
|
||||
rpl::variable<int> _footerHeight = 0;
|
||||
std::unique_ptr<Progress> _progress;
|
||||
rpl::event_stream<> _themeUpdateForced;
|
||||
std::optional<QColor> _bottomBarColor;
|
||||
rpl::lifetime _headerColorLifetime;
|
||||
rpl::lifetime _bodyColorLifetime;
|
||||
rpl::lifetime _bottomBarColorLifetime;
|
||||
rpl::event_stream<> _downloadsUpdated;
|
||||
rpl::variable<bool> _fullscreen = false;
|
||||
bool _layerShown : 1 = false;
|
||||
bool _webviewProgress : 1 = false;
|
||||
bool _themeUpdateScheduled : 1 = false;
|
||||
bool _hiddenForPayment : 1 = false;
|
||||
bool _closeWithConfirmationScheduled : 1 = false;
|
||||
bool _allowClipboardRead : 1 = false;
|
||||
bool _inBlockingRequest : 1 = false;
|
||||
bool _headerColorReceived : 1 = false;
|
||||
bool _bodyColorReceived : 1 = false;
|
||||
bool _bottomColorReceived : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Panel> Show(Args &&args);
|
||||
|
||||
} // namespace Ui::BotWebView
|
||||
117
Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp
Normal file
117
Telegram/SourceFiles/ui/chat/attach/attach_controls.cpp
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_controls.h"
|
||||
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
AttachControls::AttachControls()
|
||||
: _rect(st::sendBoxAlbumGroupRadius, st::roundedBg) {
|
||||
}
|
||||
|
||||
void AttachControls::paint(QPainter &p, int x, int y) {
|
||||
const auto groupWidth = width();
|
||||
const auto groupHeight = height();
|
||||
const auto full = (_type == Type::Full);
|
||||
|
||||
QRect groupRect(x, y, groupWidth, groupHeight);
|
||||
_rect.paint(p, groupRect);
|
||||
|
||||
if (full) {
|
||||
const auto groupHalfWidth = groupWidth / 2;
|
||||
const auto groupHalfHeight = groupHeight / 2;
|
||||
const auto editRect = _vertical
|
||||
? QRect(x, y, groupWidth, groupHalfHeight)
|
||||
: QRect(x, y, groupHalfWidth, groupHeight);
|
||||
st::sendBoxAlbumGroupButtonMediaEdit.paintInCenter(p, editRect);
|
||||
const auto deleteRect = _vertical
|
||||
? QRect(x, y + groupHalfHeight, groupWidth, groupHalfHeight)
|
||||
: QRect(x + groupHalfWidth, y, groupHalfWidth, groupHeight);
|
||||
st::sendBoxAlbumGroupButtonMediaDelete.paintInCenter(p, deleteRect);
|
||||
} else if (_type == Type::EditOnly) {
|
||||
st::sendBoxAlbumButtonMediaEdit.paintInCenter(p, groupRect);
|
||||
}
|
||||
}
|
||||
|
||||
int AttachControls::width() const {
|
||||
return (_type == Type::Full)
|
||||
? (_vertical
|
||||
? st::sendBoxAlbumGroupSizeVertical.width()
|
||||
: st::sendBoxAlbumGroupSize.width())
|
||||
: (_type == Type::EditOnly)
|
||||
? st::sendBoxAlbumSmallGroupSize.width()
|
||||
: 0;
|
||||
}
|
||||
|
||||
int AttachControls::height() const {
|
||||
return (_type == Type::Full)
|
||||
? (_vertical
|
||||
? st::sendBoxAlbumGroupSizeVertical.height()
|
||||
: st::sendBoxAlbumGroupSize.height())
|
||||
: (_type == Type::EditOnly)
|
||||
? st::sendBoxAlbumSmallGroupSize.height()
|
||||
: 0;
|
||||
}
|
||||
|
||||
AttachControls::Type AttachControls::type() const {
|
||||
return _type;
|
||||
}
|
||||
|
||||
bool AttachControls::vertical() const {
|
||||
return _vertical;
|
||||
}
|
||||
|
||||
void AttachControls::setType(Type type) {
|
||||
if (_type != type) {
|
||||
_type = type;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachControls::setVertical(bool vertical) {
|
||||
_vertical = vertical;
|
||||
}
|
||||
|
||||
AttachControlsWidget::AttachControlsWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
AttachControls::Type type)
|
||||
: RpWidget(parent)
|
||||
, _edit(base::make_unique_q<AbstractButton>(this))
|
||||
, _delete(base::make_unique_q<AbstractButton>(this)) {
|
||||
_controls.setType(type);
|
||||
|
||||
const auto w = _controls.width();
|
||||
resize(w, _controls.height());
|
||||
|
||||
if (type == AttachControls::Type::Full) {
|
||||
_edit->resize(w / 2, _controls.height());
|
||||
_delete->resize(w / 2, _controls.height());
|
||||
|
||||
_edit->moveToLeft(0, 0, w);
|
||||
_delete->moveToRight(0, 0, w);
|
||||
} else if (type == AttachControls::Type::EditOnly) {
|
||||
_edit->resize(w, _controls.height());
|
||||
_edit->moveToLeft(0, 0, w);
|
||||
}
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(this);
|
||||
_controls.paint(p, 0, 0);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
rpl::producer<> AttachControlsWidget::editRequests() const {
|
||||
return _edit->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
rpl::producer<> AttachControlsWidget::deleteRequests() const {
|
||||
return _delete->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
58
Telegram/SourceFiles/ui/chat/attach/attach_controls.h
Normal file
58
Telegram/SourceFiles/ui/chat/attach/attach_controls.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 "ui/abstract_button.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AttachControls final {
|
||||
public:
|
||||
enum class Type {
|
||||
Full,
|
||||
EditOnly,
|
||||
None,
|
||||
};
|
||||
|
||||
AttachControls();
|
||||
|
||||
void paint(QPainter &p, int x, int y);
|
||||
void setType(Type type);
|
||||
void setVertical(bool vertical);
|
||||
|
||||
[[nodiscard]] int width() const;
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] Type type() const;
|
||||
[[nodiscard]] bool vertical() const;
|
||||
|
||||
private:
|
||||
RoundRect _rect;
|
||||
Type _type = Type::Full;
|
||||
bool _vertical = false;
|
||||
|
||||
};
|
||||
|
||||
class AttachControlsWidget final : public RpWidget {
|
||||
public:
|
||||
AttachControlsWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
[[nodiscard]] rpl::producer<> editRequests() const;
|
||||
[[nodiscard]] rpl::producer<> deleteRequests() const;
|
||||
|
||||
private:
|
||||
const base::unique_qptr<AbstractButton> _edit;
|
||||
const base::unique_qptr<AbstractButton> _delete;
|
||||
AttachControls _controls;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
30
Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp
Normal file
30
Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_extensions.h"
|
||||
|
||||
#include <QtCore/QMimeDatabase>
|
||||
#include <QtGui/QImageReader>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
const QStringList &ImageExtensions() {
|
||||
static const auto result = [] {
|
||||
const auto formats = QImageReader::supportedImageFormats();
|
||||
return formats | ranges::views::transform([](const auto &format) {
|
||||
return '.' + format.toLower();
|
||||
}) | ranges::views::filter([](const auto &format) {
|
||||
const auto mimes = QMimeDatabase().mimeTypesForFileName(
|
||||
u"test"_q + format);
|
||||
return !mimes.isEmpty()
|
||||
&& mimes.front().name().startsWith(u"image/"_q);
|
||||
}) | ranges::to<QStringList>;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
14
Telegram/SourceFiles/ui/chat/attach/attach_extensions.h
Normal file
14
Telegram/SourceFiles/ui/chat/attach/attach_extensions.h
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
[[nodiscard]] const QStringList &ImageExtensions();
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_item_single_file_preview.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_document.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_song_name.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
AttachControls::Type CheckControlsType(
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
return media->allowsEditMedia()
|
||||
? type
|
||||
: AttachControls::Type::None;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSingleFilePreview::ItemSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleFilePreview(parent, st, CheckControlsType(item, type)) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
const auto document = media->document();
|
||||
Assert(document != nullptr);
|
||||
|
||||
_documentMedia = document->createMediaView();
|
||||
_documentMedia->thumbnailWanted(item->fullId());
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
document->session().downloaderTaskFinished()
|
||||
) | rpl::on_next([=] {
|
||||
if (_documentMedia->thumbnail()) {
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
preparePreview(document);
|
||||
}, _lifetimeDownload);
|
||||
}
|
||||
|
||||
void ItemSingleFilePreview::preparePreview(not_null<DocumentData*> document) {
|
||||
AbstractSingleFilePreview::Data data;
|
||||
|
||||
const auto preview = _documentMedia->thumbnail()
|
||||
? _documentMedia->thumbnail()->original()
|
||||
: QImage();
|
||||
|
||||
prepareThumbFor(data, preview);
|
||||
data.fileIsImage = document->isImage();
|
||||
data.fileIsAudio = document->isAudioFile() || document->isVoiceMessage();
|
||||
|
||||
if (data.fileIsImage) {
|
||||
data.name = document->filename();
|
||||
// data.statusText = FormatImageSizeText(preview.size()
|
||||
// / preview.devicePixelRatio());
|
||||
} else if (data.fileIsAudio) {
|
||||
auto filename = document->filename();
|
||||
|
||||
auto songTitle = QString();
|
||||
auto songPerformer = QString();
|
||||
if (const auto song = document->song()) {
|
||||
songTitle = song->title;
|
||||
songPerformer = song->performer;
|
||||
|
||||
if (document->isSongWithCover()) {
|
||||
const auto size = QSize(
|
||||
st::attachPreviewLayout.thumbSize,
|
||||
st::attachPreviewLayout.thumbSize);
|
||||
auto thumb = QPixmap(size);
|
||||
thumb.fill(Qt::transparent);
|
||||
Painter p(&thumb);
|
||||
|
||||
HistoryView::DrawThumbnailAsSongCover(
|
||||
p,
|
||||
st::songCoverOverlayFg,
|
||||
_documentMedia,
|
||||
QRect(QPoint(), size));
|
||||
data.fileThumb = std::move(thumb);
|
||||
}
|
||||
} else if (document->isVoiceMessage()) {
|
||||
songTitle = tr::lng_media_audio(tr::now);
|
||||
}
|
||||
|
||||
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
|
||||
.string();
|
||||
} else {
|
||||
data.name = document->filename();
|
||||
}
|
||||
data.statusText = FormatSizeText(document->size);
|
||||
|
||||
setData(data);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
class HistoryItem;
|
||||
class DocumentData;
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
class IconButton;
|
||||
|
||||
class ItemSingleFilePreview final : public AbstractSingleFilePreview {
|
||||
public:
|
||||
ItemSingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type);
|
||||
|
||||
private:
|
||||
void preparePreview(not_null<DocumentData*> document);
|
||||
|
||||
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
|
||||
|
||||
rpl::lifetime _lifetimeDownload;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_item_single_media_preview.h"
|
||||
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_file_origin.h"
|
||||
#include "data/data_photo.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_streaming.h"
|
||||
#include "history/history_item.h"
|
||||
#include "history/view/media/history_view_document.h"
|
||||
#include "main/main_session.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 "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
using namespace ::Media::Streaming;
|
||||
|
||||
} // namespace
|
||||
|
||||
ItemSingleMediaPreview::ItemSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleMediaPreview(parent, st, type, [=](AttachActionType type) {
|
||||
if (type == AttachActionType::EditCover) {
|
||||
return _isVideoFile;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
, _gifPaused(std::move(gifPaused))
|
||||
, _isVideoFile(item->media()->document()
|
||||
&& item->media()->document()->isVideoFile())
|
||||
, _fullId(item->fullId()) {
|
||||
const auto media = item->media();
|
||||
Assert(media != nullptr);
|
||||
|
||||
Main::Session *session = nullptr;
|
||||
|
||||
if (const auto photo = media->photo()) {
|
||||
_photoMedia = photo->createMediaView();
|
||||
_photoMedia->wanted(Data::PhotoSize::Large, item->fullId());
|
||||
|
||||
session = &photo->session();
|
||||
} else if (const auto document = media->document()) {
|
||||
_documentMedia = document->createMediaView();
|
||||
_documentMedia->thumbnailWanted(item->fullId());
|
||||
|
||||
session = &document->session();
|
||||
if (document->isAnimation() || document->isVideoFile()) {
|
||||
setAnimated(true);
|
||||
prepareStreamedPreview();
|
||||
}
|
||||
} else {
|
||||
Unexpected("Photo or document should be set.");
|
||||
}
|
||||
|
||||
struct ThumbInfo {
|
||||
bool loaded = false;
|
||||
Image *image = nullptr;
|
||||
};
|
||||
|
||||
const auto computeThumbInfo = [=]() -> ThumbInfo {
|
||||
using Size = Data::PhotoSize;
|
||||
if (_documentMedia) {
|
||||
return { true, _documentMedia->thumbnail() };
|
||||
} else if (const auto large = _photoMedia->image(Size::Large)) {
|
||||
return { true, large };
|
||||
} else if (const auto thumbnail = _photoMedia->image(
|
||||
Size::Thumbnail)) {
|
||||
return { false, thumbnail };
|
||||
} else if (const auto small = _photoMedia->image(Size::Small)) {
|
||||
return { false, small };
|
||||
} else {
|
||||
return { false, _photoMedia->thumbnailInline() };
|
||||
}
|
||||
};
|
||||
|
||||
rpl::single(rpl::empty) | rpl::then(
|
||||
session->downloaderTaskFinished()
|
||||
) | rpl::on_next([=] {
|
||||
const auto computed = computeThumbInfo();
|
||||
if (!computed.image) {
|
||||
if (_documentMedia && !_documentMedia->owner()->hasThumbnail()) {
|
||||
const auto size = _documentMedia->owner()->dimensions.scaled(
|
||||
st::sendMediaPreviewSize,
|
||||
st::confirmMaxHeight,
|
||||
Qt::KeepAspectRatio);
|
||||
if (!size.isEmpty()) {
|
||||
auto empty = QImage(
|
||||
size,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
empty.fill(Qt::black);
|
||||
preparePreview(empty);
|
||||
}
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
return;
|
||||
} else if (computed.loaded) {
|
||||
_lifetimeDownload.destroy();
|
||||
}
|
||||
preparePreview(computed.image->original());
|
||||
}, _lifetimeDownload);
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::prepareStreamedPreview() {
|
||||
if (_streamed || !_documentMedia) {
|
||||
return;
|
||||
}
|
||||
const auto document = _documentMedia
|
||||
? _documentMedia->owner().get()
|
||||
: nullptr;
|
||||
if (document && document->isAnimation()) {
|
||||
setupStreamedPreview(
|
||||
document->owner().streaming().sharedDocument(
|
||||
document,
|
||||
_fullId));
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::setupStreamedPreview(
|
||||
std::shared_ptr<Document> shared) {
|
||||
if (!shared) {
|
||||
return;
|
||||
}
|
||||
_streamed = std::make_unique<Instance>(
|
||||
std::move(shared),
|
||||
[=] { update(); });
|
||||
_streamed->lockPlayer();
|
||||
_streamed->player().updates(
|
||||
) | rpl::on_next_error([=](Update &&update) {
|
||||
handleStreamingUpdate(std::move(update));
|
||||
}, [=](Error &&error) {
|
||||
handleStreamingError(std::move(error));
|
||||
}, _streamed->lifetime());
|
||||
|
||||
if (_streamed->ready()) {
|
||||
streamingReady(base::duplicate(_streamed->info()));
|
||||
}
|
||||
checkStreamedIsStarted();
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::handleStreamingUpdate(Update &&update) {
|
||||
v::match(update.data, [&](Information &update) {
|
||||
streamingReady(std::move(update));
|
||||
}, [](PreloadedVideo) {
|
||||
}, [&](UpdateVideo) {
|
||||
this->update();
|
||||
}, [](PreloadedAudio) {
|
||||
}, [](UpdateAudio) {
|
||||
}, [](WaitingForData) {
|
||||
}, [](SpeedEstimate) {
|
||||
}, [](MutedByOther) {
|
||||
}, [](Finished) {
|
||||
});
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::handleStreamingError(Error &&error) {
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::streamingReady(Information &&info) {
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::checkStreamedIsStarted() {
|
||||
if (!_streamed) {
|
||||
return;
|
||||
} else if (_streamed->paused()) {
|
||||
_streamed->resume();
|
||||
}
|
||||
if (!_streamed->active() && !_streamed->failed()) {
|
||||
startStreamedPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
void ItemSingleMediaPreview::startStreamedPlayer() {
|
||||
auto options = ::Media::Streaming::PlaybackOptions();
|
||||
options.audioId = _documentMedia
|
||||
? AudioMsgId(_documentMedia->owner(), _fullId)
|
||||
: AudioMsgId();
|
||||
options.waitForMarkAsShown = true;
|
||||
//if (!_streamed->withSound) {
|
||||
options.mode = ::Media::Streaming::Mode::Video;
|
||||
options.loop = true;
|
||||
//}
|
||||
_streamed->play(options);
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::supportsSpoilers() const {
|
||||
return false; // We are not allowed to change existing spoiler setting.
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::drawBackground() const {
|
||||
return true; // A sticker can't be here.
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::tryPaintAnimation(QPainter &p) {
|
||||
checkStreamedIsStarted();
|
||||
if (_streamed
|
||||
&& _streamed->player().ready()
|
||||
&& !_streamed->player().videoSize().isEmpty()) {
|
||||
const auto s = QSize(previewWidth(), previewHeight());
|
||||
const auto paused = _gifPaused();
|
||||
|
||||
auto request = ::Media::Streaming::FrameRequest();
|
||||
request.outer = s * style::DevicePixelRatio();
|
||||
request.resize = s * style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
QRect(
|
||||
previewLeft(),
|
||||
previewTop(),
|
||||
previewWidth(),
|
||||
previewHeight()),
|
||||
_streamed->frame(request));
|
||||
if (!paused) {
|
||||
_streamed->markFrameShown();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemSingleMediaPreview::isAnimatedPreviewReady() const {
|
||||
return _streamed != nullptr;
|
||||
}
|
||||
|
||||
auto ItemSingleMediaPreview::sharedPhotoMedia() const
|
||||
-> std::shared_ptr<::Data::PhotoMedia> {
|
||||
return _photoMedia;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_media_preview.h"
|
||||
|
||||
namespace Data {
|
||||
class DocumentMedia;
|
||||
class PhotoMedia;
|
||||
} // namespace Data
|
||||
|
||||
namespace Media {
|
||||
namespace Streaming {
|
||||
class Instance;
|
||||
class Document;
|
||||
struct Update;
|
||||
enum class Error;
|
||||
struct Information;
|
||||
} // namespace Streaming
|
||||
} // namespace Media
|
||||
|
||||
class HistoryItem;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ItemSingleMediaPreview final : public AbstractSingleMediaPreview {
|
||||
public:
|
||||
ItemSingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
not_null<HistoryItem*> item,
|
||||
AttachControls::Type type);
|
||||
|
||||
std::shared_ptr<::Data::PhotoMedia> sharedPhotoMedia() const;
|
||||
|
||||
protected:
|
||||
bool supportsSpoilers() const override;
|
||||
bool drawBackground() const override;
|
||||
bool tryPaintAnimation(QPainter &p) override;
|
||||
bool isAnimatedPreviewReady() const override;
|
||||
|
||||
private:
|
||||
void prepareStreamedPreview();
|
||||
void checkStreamedIsStarted();
|
||||
void setupStreamedPreview(
|
||||
std::shared_ptr<::Media::Streaming::Document> shared);
|
||||
void handleStreamingUpdate(::Media::Streaming::Update &&update);
|
||||
void handleStreamingError(::Media::Streaming::Error &&error);
|
||||
void streamingReady(::Media::Streaming::Information &&info);
|
||||
void startStreamedPlayer();
|
||||
|
||||
const Fn<bool()> _gifPaused;
|
||||
const bool _isVideoFile;
|
||||
const FullMsgId _fullId;
|
||||
|
||||
std::shared_ptr<::Data::PhotoMedia> _photoMedia;
|
||||
std::shared_ptr<::Data::DocumentMedia> _documentMedia;
|
||||
|
||||
std::unique_ptr<::Media::Streaming::Instance> _streamed;
|
||||
|
||||
|
||||
rpl::lifetime _lifetimeDownload;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
390
Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
Normal file
390
Telegram/SourceFiles/ui/chat/attach/attach_prepare.cpp
Normal file
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_prepare.h"
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "core/mime_type.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxAlbumCount = 10;
|
||||
|
||||
} // namespace
|
||||
|
||||
PreparedFile::PreparedFile(const QString &path) : path(path) {
|
||||
}
|
||||
|
||||
PreparedFile::PreparedFile(PreparedFile &&other) = default;
|
||||
|
||||
PreparedFile &PreparedFile::operator=(PreparedFile &&other) = default;
|
||||
|
||||
PreparedFile::~PreparedFile() = default;
|
||||
|
||||
bool PreparedFile::canBeInAlbumType(AlbumType album) const {
|
||||
return CanBeInAlbumType(type, album);
|
||||
}
|
||||
|
||||
bool PreparedFile::isSticker() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
return (type == PreparedFile::Type::Photo)
|
||||
&& Core::IsMimeSticker(information->filemime);
|
||||
}
|
||||
|
||||
bool PreparedFile::isVideoFile() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
using Video = Ui::PreparedFileInformation::Video;
|
||||
return (type == PreparedFile::Type::Video)
|
||||
&& v::is<Video>(information->media)
|
||||
&& !v::get<Video>(information->media).isGifv;
|
||||
}
|
||||
|
||||
bool PreparedFile::isGifv() const {
|
||||
Expects(information != nullptr);
|
||||
|
||||
using Video = Ui::PreparedFileInformation::Video;
|
||||
return (type == PreparedFile::Type::Video)
|
||||
&& v::is<Video>(information->media)
|
||||
&& v::get<Video>(information->media).isGifv;
|
||||
}
|
||||
|
||||
AlbumType PreparedFile::albumType(bool sendImagesAsPhotos) const {
|
||||
switch (type) {
|
||||
case Type::Photo:
|
||||
return sendImagesAsPhotos ? AlbumType::PhotoVideo : AlbumType::File;
|
||||
case Type::Video:
|
||||
return AlbumType::PhotoVideo;
|
||||
case Type::Music:
|
||||
return AlbumType::Music;
|
||||
case Type::File:
|
||||
return AlbumType::File;
|
||||
case Type::None:
|
||||
return AlbumType::None;
|
||||
}
|
||||
Unexpected("PreparedFile::type in PreparedFile::albumType().");
|
||||
}
|
||||
|
||||
bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album) {
|
||||
Expects(album != AlbumType::None);
|
||||
|
||||
using Type = PreparedFile::Type;
|
||||
switch (album) {
|
||||
case AlbumType::PhotoVideo:
|
||||
return (type == Type::Photo) || (type == Type::Video);
|
||||
case AlbumType::Music:
|
||||
return (type == Type::Music);
|
||||
case AlbumType::File:
|
||||
return (type == Type::Photo) || (type == Type::File);
|
||||
}
|
||||
Unexpected("AlbumType in CanBeInAlbumType.");
|
||||
}
|
||||
|
||||
bool InsertTextOnImageCancel(const QString &text) {
|
||||
return !text.isEmpty() && !text.startsWith(u"data:image"_q);
|
||||
}
|
||||
|
||||
PreparedList PreparedList::Reordered(
|
||||
PreparedList &&list,
|
||||
std::vector<int> order) {
|
||||
Expects(list.error == PreparedList::Error::None);
|
||||
Expects(list.files.size() == order.size());
|
||||
|
||||
auto result = PreparedList(list.error, list.errorData);
|
||||
result.files.reserve(list.files.size());
|
||||
for (auto index : order) {
|
||||
result.files.push_back(std::move(list.files[index]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PreparedList::mergeToEnd(PreparedList &&other, bool cutToAlbumSize) {
|
||||
if (error != Error::None) {
|
||||
return;
|
||||
}
|
||||
if (other.error != Error::None) {
|
||||
error = other.error;
|
||||
errorData = other.errorData;
|
||||
return;
|
||||
}
|
||||
files.reserve(std::min(
|
||||
size_t(cutToAlbumSize ? kMaxAlbumCount : INT_MAX),
|
||||
files.size() + other.files.size()));
|
||||
for (auto &file : other.files) {
|
||||
if (cutToAlbumSize && files.size() == kMaxAlbumCount) {
|
||||
break;
|
||||
}
|
||||
files.push_back(std::move(file));
|
||||
}
|
||||
}
|
||||
|
||||
bool PreparedList::canBeSentInSlowmode() const {
|
||||
return canBeSentInSlowmodeWith(PreparedList());
|
||||
}
|
||||
|
||||
bool PreparedList::canBeSentInSlowmodeWith(const PreparedList &other) const {
|
||||
if (!filesToProcess.empty() || !other.filesToProcess.empty()) {
|
||||
return false;
|
||||
} else if (files.size() + other.files.size() < 2) {
|
||||
return true;
|
||||
} else if (files.size() + other.files.size() > kMaxAlbumCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
using Type = PreparedFile::Type;
|
||||
auto &&all = ranges::views::concat(files, other.files);
|
||||
const auto has = [&](Type type) {
|
||||
return ranges::contains(all, type, &PreparedFile::type);
|
||||
};
|
||||
const auto hasNonGrouping = has(Type::None);
|
||||
const auto hasPhotos = has(Type::Photo);
|
||||
const auto hasFiles = has(Type::File);
|
||||
const auto hasVideos = has(Type::Video);
|
||||
const auto hasMusic = has(Type::Music);
|
||||
|
||||
// File-s and Video-s never can be grouped.
|
||||
// Music-s can be grouped only with themselves.
|
||||
if (hasNonGrouping) {
|
||||
return false;
|
||||
} else if (hasFiles) {
|
||||
return !hasMusic && !hasVideos;
|
||||
} else if (hasVideos) {
|
||||
return !hasMusic && !hasFiles;
|
||||
} else if (hasMusic) {
|
||||
return !hasVideos && !hasFiles && !hasPhotos;
|
||||
}
|
||||
return !hasNonGrouping && (!hasFiles || !hasVideos);
|
||||
}
|
||||
|
||||
bool PreparedList::canAddCaption(bool sendingAlbum, bool compress) const {
|
||||
if (!filesToProcess.empty()
|
||||
|| files.empty()
|
||||
|| files.size() > kMaxAlbumCount) {
|
||||
return false;
|
||||
}
|
||||
if (files.size() == 1) {
|
||||
Assert(files.front().information != nullptr);
|
||||
const auto isSticker = (!compress
|
||||
&& Core::IsMimeSticker(files.front().information->filemime))
|
||||
|| files.front().path.endsWith(u".tgs"_q, Qt::CaseInsensitive);
|
||||
return !isSticker;
|
||||
} else if (!sendingAlbum) {
|
||||
return false;
|
||||
}
|
||||
const auto hasFiles = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::File,
|
||||
&PreparedFile::type);
|
||||
const auto hasMusic = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::Music,
|
||||
&PreparedFile::type);
|
||||
const auto hasNotGrouped = ranges::contains(
|
||||
files,
|
||||
PreparedFile::Type::None,
|
||||
&PreparedFile::type);
|
||||
return !hasFiles && !hasMusic && !hasNotGrouped;
|
||||
}
|
||||
|
||||
bool PreparedList::canMoveCaption(bool sendingAlbum, bool compress) const {
|
||||
if (!canAddCaption(sendingAlbum, compress)) {
|
||||
return false;
|
||||
} else if (files.size() != 1) {
|
||||
return true;
|
||||
}
|
||||
const auto &file = files.front();
|
||||
return (file.type == PreparedFile::Type::Video)
|
||||
|| (file.type == PreparedFile::Type::Photo && compress);
|
||||
}
|
||||
|
||||
bool PreparedList::canChangePrice(bool sendingAlbum, bool compress) const {
|
||||
return canMoveCaption(sendingAlbum, compress);
|
||||
}
|
||||
|
||||
bool PreparedList::hasGroupOption(bool slowmode) const {
|
||||
if (slowmode || files.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
using Type = PreparedFile::Type;
|
||||
auto lastType = Type::None;
|
||||
for (const auto &file : files) {
|
||||
if ((file.type == lastType)
|
||||
|| (file.type == Type::Video && lastType == Type::Photo)
|
||||
|| (file.type == Type::Photo && lastType == Type::Video)
|
||||
|| (file.type == Type::File && lastType == Type::Photo)
|
||||
|| (file.type == Type::Photo && lastType == Type::File)) {
|
||||
if (lastType != Type::None) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
lastType = file.type;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreparedList::hasSendImagesAsPhotosOption(bool slowmode) const {
|
||||
using Type = PreparedFile::Type;
|
||||
return slowmode
|
||||
? ((files.size() == 1) && (files.front().type == Type::Photo))
|
||||
: ranges::contains(files, Type::Photo, &PreparedFile::type);
|
||||
}
|
||||
|
||||
bool PreparedList::canHaveEditorHintLabel() const {
|
||||
for (const auto &file : files) {
|
||||
if ((file.type == PreparedFile::Type::Photo)
|
||||
&& !Core::IsMimeSticker(file.information->filemime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreparedList::hasSticker() const {
|
||||
return ranges::any_of(files, &PreparedFile::isSticker);
|
||||
}
|
||||
|
||||
bool PreparedList::hasSpoilerMenu(bool compress) const {
|
||||
const auto allAreVideo = !ranges::any_of(files, [](const auto &f) {
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
return (f.type != Type::Video);
|
||||
});
|
||||
const auto allAreMedia = !ranges::any_of(files, [](const auto &f) {
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
return (f.type != Type::Photo) && (f.type != Type::Video);
|
||||
});
|
||||
return allAreVideo || (allAreMedia && compress);
|
||||
}
|
||||
|
||||
std::shared_ptr<PreparedBundle> PrepareFilesBundle(
|
||||
std::vector<PreparedGroup> groups,
|
||||
SendFilesWay way,
|
||||
TextWithTags caption,
|
||||
bool ctrlShiftEnter) {
|
||||
auto totalCount = 0;
|
||||
for (const auto &group : groups) {
|
||||
totalCount += group.list.files.size();
|
||||
}
|
||||
const auto sendComment = !caption.text.isEmpty()
|
||||
&& (groups.size() != 1 || !groups.front().sentWithCaption());
|
||||
return std::make_shared<PreparedBundle>(PreparedBundle{
|
||||
.groups = std::move(groups),
|
||||
.way = way,
|
||||
.caption = std::move(caption),
|
||||
.totalCount = totalCount + (sendComment ? 1 : 0),
|
||||
.sendComment = sendComment,
|
||||
.ctrlShiftEnter = ctrlShiftEnter,
|
||||
});
|
||||
}
|
||||
|
||||
int MaxAlbumItems() {
|
||||
return kMaxAlbumCount;
|
||||
}
|
||||
|
||||
bool ValidateThumbDimensions(int width, int height) {
|
||||
return (width > 0)
|
||||
&& (height > 0)
|
||||
&& (width <= 20 * height)
|
||||
&& (height <= 20 * width);
|
||||
}
|
||||
|
||||
std::vector<PreparedGroup> DivideByGroups(
|
||||
PreparedList &&list,
|
||||
SendFilesWay way,
|
||||
bool slowmode) {
|
||||
const auto sendImagesAsPhotos = way.sendImagesAsPhotos();
|
||||
const auto groupFiles = way.groupFiles() || slowmode;
|
||||
|
||||
auto group = Ui::PreparedList();
|
||||
|
||||
using Type = Ui::PreparedFile::Type;
|
||||
auto groupType = AlbumType::None;
|
||||
|
||||
auto result = std::vector<PreparedGroup>();
|
||||
auto pushGroup = [&] {
|
||||
const auto type = (group.files.size() > 1)
|
||||
? groupType
|
||||
: AlbumType::None;
|
||||
result.push_back(PreparedGroup{
|
||||
.list = base::take(group),
|
||||
.type = type,
|
||||
});
|
||||
};
|
||||
for (auto i = 0; i != list.files.size(); ++i) {
|
||||
auto &file = list.files[i];
|
||||
const auto fileGroupType = (file.type == Type::Music)
|
||||
? (groupFiles ? AlbumType::Music : AlbumType::None)
|
||||
: (file.type == Type::Video)
|
||||
? (groupFiles ? AlbumType::PhotoVideo : AlbumType::None)
|
||||
: (file.type == Type::Photo)
|
||||
? ((groupFiles && sendImagesAsPhotos)
|
||||
? AlbumType::PhotoVideo
|
||||
: (groupFiles && !sendImagesAsPhotos)
|
||||
? AlbumType::File
|
||||
: AlbumType::None)
|
||||
: (file.type == Type::File)
|
||||
? (groupFiles ? AlbumType::File : AlbumType::None)
|
||||
: AlbumType::None;
|
||||
if ((!group.files.empty() && groupType != fileGroupType)
|
||||
|| ((groupType != AlbumType::None)
|
||||
&& (group.files.size() == Ui::MaxAlbumItems()))) {
|
||||
pushGroup();
|
||||
}
|
||||
group.files.push_back(std::move(file));
|
||||
groupType = fileGroupType;
|
||||
}
|
||||
if (!group.files.empty()) {
|
||||
pushGroup();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
|
||||
const auto scaledSize = image.size().scaled(
|
||||
size,
|
||||
size,
|
||||
Qt::KeepAspectRatioByExpanding);
|
||||
using Option = Images::Option;
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
return PixmapFromImage(Images::Prepare(
|
||||
std::move(image),
|
||||
scaledSize * ratio,
|
||||
{
|
||||
.colored = &st::songCoverOverlayFg,
|
||||
.options = Option::RoundCircle,
|
||||
.outer = { size, size },
|
||||
}));
|
||||
}
|
||||
|
||||
QPixmap BlurredPreviewFromPixmap(QPixmap pixmap, RectParts corners) {
|
||||
const auto image = pixmap.toImage();
|
||||
const auto skip = st::roundRadiusLarge * image.devicePixelRatio();
|
||||
auto small = image.copy(
|
||||
skip,
|
||||
skip,
|
||||
image.width() - 2 * skip,
|
||||
image.height() - 2 * skip
|
||||
).scaled(
|
||||
40,
|
||||
40,
|
||||
Qt::KeepAspectRatioByExpanding,
|
||||
Qt::SmoothTransformation);
|
||||
|
||||
using namespace Images;
|
||||
return PixmapFromImage(Prepare(
|
||||
Blur(std::move(small), true),
|
||||
image.size(),
|
||||
{ .options = RoundOptions(ImageRoundRadius::Large, corners) }));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
180
Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
Normal file
180
Telegram/SourceFiles/ui/chat/attach/attach_prepare.h
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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_common.h"
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
#include "ui/rect_part.h"
|
||||
|
||||
#include <QtCore/QSemaphore>
|
||||
#include <deque>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class SendFilesWay;
|
||||
|
||||
struct PreparedFileInformation {
|
||||
struct Image {
|
||||
QImage data;
|
||||
QByteArray bytes;
|
||||
QByteArray format;
|
||||
bool animated = false;
|
||||
Editor::PhotoModifications modifications;
|
||||
};
|
||||
struct Song {
|
||||
crl::time duration = -1;
|
||||
QString title;
|
||||
QString performer;
|
||||
QImage cover;
|
||||
};
|
||||
struct Video {
|
||||
bool isGifv = false;
|
||||
bool isWebmSticker = false;
|
||||
bool supportsStreaming = false;
|
||||
crl::time duration = -1;
|
||||
QImage thumbnail;
|
||||
};
|
||||
|
||||
QString filemime;
|
||||
std::variant<v::null_t, Image, Song, Video> media;
|
||||
};
|
||||
|
||||
enum class AlbumType {
|
||||
None,
|
||||
PhotoVideo,
|
||||
Music,
|
||||
File,
|
||||
};
|
||||
|
||||
struct PreparedFile {
|
||||
// File-s can be grouped if 'groupFiles'.
|
||||
// File-s + Photo-s can be grouped if 'groupFiles && !sendImagesAsPhotos'.
|
||||
// Photo-s can be grouped if 'groupFiles'.
|
||||
// Photo-s + Video-s can be grouped if 'groupFiles && sendImagesAsPhotos'.
|
||||
// Video-s can be grouped if 'groupFiles'.
|
||||
// Music-s can be grouped if 'groupFiles'.
|
||||
enum class Type {
|
||||
None,
|
||||
Photo,
|
||||
Video,
|
||||
Music,
|
||||
File,
|
||||
};
|
||||
|
||||
PreparedFile(const QString &path);
|
||||
PreparedFile(PreparedFile &&other);
|
||||
PreparedFile &operator=(PreparedFile &&other);
|
||||
~PreparedFile();
|
||||
|
||||
[[nodiscard]] bool canBeInAlbumType(AlbumType album) const;
|
||||
[[nodiscard]] AlbumType albumType(bool sendImagesAsPhotos) const;
|
||||
[[nodiscard]] bool isSticker() const;
|
||||
[[nodiscard]] bool isVideoFile() const;
|
||||
[[nodiscard]] bool isGifv() const;
|
||||
|
||||
QString path;
|
||||
QByteArray content;
|
||||
int64 size = 0;
|
||||
std::unique_ptr<PreparedFileInformation> information;
|
||||
std::unique_ptr<PreparedFile> videoCover;
|
||||
QImage preview;
|
||||
QSize shownDimensions;
|
||||
QSize originalDimensions;
|
||||
Type type = Type::File;
|
||||
bool spoiler = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool CanBeInAlbumType(PreparedFile::Type type, AlbumType album);
|
||||
[[nodiscard]] bool InsertTextOnImageCancel(const QString &text);
|
||||
|
||||
struct PreparedList {
|
||||
enum class Error {
|
||||
None,
|
||||
NonLocalUrl,
|
||||
Directory,
|
||||
EmptyFile,
|
||||
TooLargeFile,
|
||||
};
|
||||
|
||||
PreparedList() = default;
|
||||
PreparedList(Error error, QString errorData)
|
||||
: error(error)
|
||||
, errorData(errorData) {
|
||||
}
|
||||
PreparedList(PreparedList &&other) = default;
|
||||
PreparedList &operator=(PreparedList &&other) = default;
|
||||
|
||||
[[nodiscard]] static PreparedList Reordered(
|
||||
PreparedList &&list,
|
||||
std::vector<int> order);
|
||||
void mergeToEnd(PreparedList &&other, bool cutToAlbumSize = false);
|
||||
|
||||
[[nodiscard]] bool canAddCaption(bool sendingAlbum, bool compress) const;
|
||||
[[nodiscard]] bool canMoveCaption(
|
||||
bool sendingAlbum,
|
||||
bool compress) const;
|
||||
[[nodiscard]] bool canChangePrice(
|
||||
bool sendingAlbum,
|
||||
bool compress) const;
|
||||
[[nodiscard]] bool canBeSentInSlowmode() const;
|
||||
[[nodiscard]] bool canBeSentInSlowmodeWith(
|
||||
const PreparedList &other) const;
|
||||
|
||||
[[nodiscard]] bool hasGroupOption(bool slowmode) const;
|
||||
[[nodiscard]] bool hasSendImagesAsPhotosOption(bool slowmode) const;
|
||||
[[nodiscard]] bool canHaveEditorHintLabel() const;
|
||||
[[nodiscard]] bool hasSticker() const;
|
||||
[[nodiscard]] bool hasSpoilerMenu(bool compress) const;
|
||||
|
||||
Error error = Error::None;
|
||||
QString errorData;
|
||||
std::vector<PreparedFile> files;
|
||||
std::deque<PreparedFile> filesToProcess;
|
||||
std::optional<bool> overrideSendImagesAsPhotos;
|
||||
};
|
||||
|
||||
struct PreparedGroup {
|
||||
PreparedList list;
|
||||
AlbumType type = AlbumType::None;
|
||||
|
||||
[[nodiscard]] bool sentWithCaption() const {
|
||||
return (list.files.size() == 1)
|
||||
|| (type == AlbumType::PhotoVideo);
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<PreparedGroup> DivideByGroups(
|
||||
PreparedList &&list,
|
||||
SendFilesWay way,
|
||||
bool slowmode);
|
||||
|
||||
struct PreparedBundle {
|
||||
std::vector<PreparedGroup> groups;
|
||||
SendFilesWay way;
|
||||
TextWithTags caption;
|
||||
int totalCount = 0;
|
||||
bool sendComment = false;
|
||||
bool ctrlShiftEnter = false;
|
||||
};
|
||||
[[nodiscard]] std::shared_ptr<PreparedBundle> PrepareFilesBundle(
|
||||
std::vector<PreparedGroup> groups,
|
||||
SendFilesWay way,
|
||||
TextWithTags caption,
|
||||
bool ctrlShiftEnter);
|
||||
|
||||
[[nodiscard]] int MaxAlbumItems();
|
||||
[[nodiscard]] bool ValidateThumbDimensions(int width, int height);
|
||||
|
||||
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
|
||||
|
||||
[[nodiscard]] QPixmap BlurredPreviewFromPixmap(
|
||||
QPixmap pixmap,
|
||||
RectParts corners);
|
||||
|
||||
} // namespace Ui
|
||||
@@ -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
|
||||
*/
|
||||
#include "ui/chat/attach/attach_send_files_way.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void SendFilesWay::setSendImagesAsPhotos(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::SendImagesAsPhotos;
|
||||
} else {
|
||||
if (hasCompressedStickers()) {
|
||||
setGroupFiles(false);
|
||||
}
|
||||
_flags &= ~Flag::SendImagesAsPhotos;
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesWay::setGroupFiles(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::GroupFiles;
|
||||
if (hasCompressedStickers()) {
|
||||
setSendImagesAsPhotos(true);
|
||||
}
|
||||
} else {
|
||||
_flags &= ~Flag::GroupFiles;
|
||||
}
|
||||
}
|
||||
|
||||
void SendFilesWay::setHasCompressedStickers(bool value) {
|
||||
if (value) {
|
||||
_flags |= Flag::HasCompressedStickers;
|
||||
} else {
|
||||
_flags &= ~Flag::HasCompressedStickers;
|
||||
}
|
||||
}
|
||||
|
||||
//enum class SendFilesWay { // Old way. Serialize should be compatible.
|
||||
// Album,
|
||||
// Photos,
|
||||
// Files,
|
||||
//};
|
||||
|
||||
int32 SendFilesWay::serialize() const {
|
||||
auto result = (sendImagesAsPhotos() && groupFiles())
|
||||
? int32(0)
|
||||
: sendImagesAsPhotos()
|
||||
? int32(1)
|
||||
: groupFiles()
|
||||
? int32(3)
|
||||
: int32(2);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<SendFilesWay> SendFilesWay::FromSerialized(int32 value) {
|
||||
if (value < 0 || value > 3) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto result = SendFilesWay();
|
||||
result.setGroupFiles((value == 0) || (value == 3));
|
||||
result.setSendImagesAsPhotos((value == 0) || (value == 1));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
80
Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.h
Normal file
80
Telegram/SourceFiles/ui/chat/attach/attach_send_files_way.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/flags.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
enum class AttachActionType {
|
||||
ToggleSpoiler,
|
||||
EditCover,
|
||||
ClearCover,
|
||||
};
|
||||
|
||||
enum class AttachButtonType {
|
||||
Edit,
|
||||
Delete,
|
||||
Modify,
|
||||
None,
|
||||
};
|
||||
|
||||
class SendFilesWay final {
|
||||
public:
|
||||
[[nodiscard]] bool groupFiles() const {
|
||||
return (_flags & Flag::GroupFiles) != 0;
|
||||
}
|
||||
[[nodiscard]] bool sendImagesAsPhotos() const {
|
||||
return (_flags & Flag::SendImagesAsPhotos) != 0;
|
||||
}
|
||||
void setGroupFiles(bool value);
|
||||
void setSendImagesAsPhotos(bool value);
|
||||
void setHasCompressedStickers(bool value);
|
||||
|
||||
[[nodiscard]] inline bool operator<(const SendFilesWay &other) const {
|
||||
return _flags < other._flags;
|
||||
}
|
||||
[[nodiscard]] inline bool operator>(const SendFilesWay &other) const {
|
||||
return other < *this;
|
||||
}
|
||||
[[nodiscard]] inline bool operator<=(const SendFilesWay &other) const {
|
||||
return !(other < *this);
|
||||
}
|
||||
[[nodiscard]] inline bool operator>=(const SendFilesWay &other) const {
|
||||
return !(*this < other);
|
||||
}
|
||||
[[nodiscard]] inline bool operator==(const SendFilesWay &other) const {
|
||||
return _flags == other._flags;
|
||||
}
|
||||
[[nodiscard]] inline bool operator!=(const SendFilesWay &other) const {
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
[[nodiscard]] int32 serialize() const;
|
||||
[[nodiscard]] static std::optional<SendFilesWay> FromSerialized(
|
||||
int32 value);
|
||||
|
||||
private:
|
||||
[[nodiscard]] bool hasCompressedStickers() const {
|
||||
return (_flags & Flag::HasCompressedStickers) != 0;
|
||||
}
|
||||
|
||||
enum class Flag : uchar {
|
||||
GroupFiles = (1 << 0),
|
||||
SendImagesAsPhotos = (1 << 1),
|
||||
HasCompressedStickers = (1 << 2),
|
||||
|
||||
Default = GroupFiles | SendImagesAsPhotos,
|
||||
};
|
||||
friend inline constexpr bool is_flag_type(Flag) { return true; };
|
||||
|
||||
base::flags<Flag> _flags = Flag::Default;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -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 "ui/chat/attach/attach_single_file_preview.h"
|
||||
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "ui/text/format_song_name.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
#include <QtCore/QFileInfo>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
SingleFilePreview::SingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
AttachControls::Type type)
|
||||
: AbstractSingleFilePreview(parent, st, type) {
|
||||
preparePreview(file);
|
||||
}
|
||||
|
||||
void SingleFilePreview::preparePreview(const PreparedFile &file) {
|
||||
AbstractSingleFilePreview::Data data;
|
||||
|
||||
auto preview = QImage();
|
||||
if (const auto image = std::get_if<PreparedFileInformation::Image>(
|
||||
&file.information->media)) {
|
||||
preview = image->data;
|
||||
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
|
||||
&file.information->media)) {
|
||||
preview = video->thumbnail;
|
||||
}
|
||||
prepareThumbFor(data, preview);
|
||||
const auto filepath = file.path;
|
||||
if (filepath.isEmpty()) {
|
||||
auto filename = "image.png";
|
||||
data.name = filename;
|
||||
data.statusText = FormatImageSizeText(file.originalDimensions);
|
||||
data.fileIsImage = true;
|
||||
} else {
|
||||
auto fileinfo = QFileInfo(filepath);
|
||||
auto filename = fileinfo.fileName();
|
||||
data.fileIsImage = Core::FileIsImage(
|
||||
filename,
|
||||
Core::MimeTypeForFile(fileinfo).name());
|
||||
|
||||
auto songTitle = QString();
|
||||
auto songPerformer = QString();
|
||||
if (file.information) {
|
||||
if (const auto song = std::get_if<PreparedFileInformation::Song>(
|
||||
&file.information->media)) {
|
||||
songTitle = song->title;
|
||||
songPerformer = song->performer;
|
||||
data.fileIsAudio = true;
|
||||
|
||||
if (auto cover = song->cover; !cover.isNull()) {
|
||||
data.fileThumb = Ui::PrepareSongCoverForThumbnail(
|
||||
cover,
|
||||
st::attachPreviewLayout.thumbSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.name = Text::FormatSongName(filename, songTitle, songPerformer)
|
||||
.string();
|
||||
data.statusText = FormatSizeText(fileinfo.size());
|
||||
}
|
||||
|
||||
setData(data);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_file_preview.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
|
||||
class SingleFilePreview final : public AbstractSingleFilePreview {
|
||||
public:
|
||||
SingleFilePreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
const PreparedFile &file,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
private:
|
||||
void preparePreview(const PreparedFile &file);
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
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 "ui/chat/attach/attach_single_media_preview.h"
|
||||
|
||||
#include "editor/photo_editor_common.h"
|
||||
#include "ui/chat/attach/attach_prepare.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
SingleMediaPreview *SingleMediaPreview::Create(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
const PreparedFile &file,
|
||||
Fn<bool(AttachActionType)> actionAllowed,
|
||||
AttachControls::Type type) {
|
||||
auto preview = QImage();
|
||||
auto animated = false;
|
||||
auto animationPreview = false;
|
||||
auto hasModifications = false;
|
||||
if (const auto image = std::get_if<PreparedFileInformation::Image>(
|
||||
&file.information->media)) {
|
||||
preview = Editor::ImageModified(image->data, image->modifications);
|
||||
animated = animationPreview = image->animated;
|
||||
hasModifications = !image->modifications.empty();
|
||||
} else if (const auto video = std::get_if<PreparedFileInformation::Video>(
|
||||
&file.information->media)) {
|
||||
preview = file.videoCover
|
||||
? file.videoCover->preview
|
||||
: video->thumbnail;
|
||||
animated = true;
|
||||
animationPreview = video->isGifv;
|
||||
}
|
||||
if (preview.isNull()) {
|
||||
return nullptr;
|
||||
} else if (!animated
|
||||
&& !ValidateThumbDimensions(preview.width(), preview.height())
|
||||
&& !hasModifications) {
|
||||
return nullptr;
|
||||
}
|
||||
return CreateChild<SingleMediaPreview>(
|
||||
parent,
|
||||
st,
|
||||
std::move(gifPaused),
|
||||
preview,
|
||||
animated,
|
||||
Core::IsMimeSticker(file.information->filemime),
|
||||
file.spoiler,
|
||||
animationPreview ? file.path : QString(),
|
||||
type,
|
||||
std::move(actionAllowed));
|
||||
}
|
||||
|
||||
SingleMediaPreview::SingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
QImage preview,
|
||||
bool animated,
|
||||
bool sticker,
|
||||
bool spoiler,
|
||||
const QString &animatedPreviewPath,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed)
|
||||
: AbstractSingleMediaPreview(parent, st, type, std::move(actionAllowed))
|
||||
, _gifPaused(std::move(gifPaused))
|
||||
, _sticker(sticker) {
|
||||
Expects(!preview.isNull());
|
||||
setAnimated(animated);
|
||||
|
||||
preparePreview(preview);
|
||||
prepareAnimatedPreview(animatedPreviewPath, animated);
|
||||
setSpoiler(spoiler);
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::supportsSpoilers() const {
|
||||
return !_sticker || sendWay().sendImagesAsPhotos();
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::drawBackground() const {
|
||||
return !_sticker;
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::tryPaintAnimation(QPainter &p) {
|
||||
if (_gifPreview && _gifPreview->started()) {
|
||||
const auto paused = _gifPaused();
|
||||
const auto frame = _gifPreview->current({
|
||||
.frame = QSize(previewWidth(), previewHeight()),
|
||||
}, paused ? 0 : crl::now());
|
||||
p.drawImage(previewLeft(), previewTop(), frame);
|
||||
return true;
|
||||
} else if (_lottiePreview && _lottiePreview->ready()) {
|
||||
const auto frame = _lottiePreview->frame();
|
||||
const auto size = frame.size() / style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
QRect(
|
||||
previewLeft() + (previewWidth() - size.width()) / 2,
|
||||
(previewHeight() - size.height()) / 2,
|
||||
size.width(),
|
||||
size.height()),
|
||||
frame);
|
||||
_lottiePreview->markFrameShown();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SingleMediaPreview::isAnimatedPreviewReady() const {
|
||||
return _gifPreview || _lottiePreview;
|
||||
}
|
||||
|
||||
void SingleMediaPreview::prepareAnimatedPreview(
|
||||
const QString &animatedPreviewPath,
|
||||
bool animated) {
|
||||
if (_sticker && animated) {
|
||||
const auto box = QSize(previewWidth(), previewHeight())
|
||||
* style::DevicePixelRatio();
|
||||
_lottiePreview = std::make_unique<Lottie::SinglePlayer>(
|
||||
Lottie::ReadContent(QByteArray(), animatedPreviewPath),
|
||||
Lottie::FrameRequest{ box });
|
||||
_lottiePreview->updates(
|
||||
) | rpl::on_next([=] {
|
||||
update();
|
||||
}, lifetime());
|
||||
} else if (!animatedPreviewPath.isEmpty()) {
|
||||
auto callback = [=](Media::Clip::Notification notification) {
|
||||
clipCallback(notification);
|
||||
};
|
||||
_gifPreview = Media::Clip::MakeReader(
|
||||
animatedPreviewPath,
|
||||
std::move(callback));
|
||||
}
|
||||
}
|
||||
|
||||
void SingleMediaPreview::clipCallback(
|
||||
Media::Clip::Notification notification) {
|
||||
using namespace Media::Clip;
|
||||
switch (notification) {
|
||||
case Notification::Reinit: {
|
||||
if (_gifPreview && _gifPreview->state() == State::Error) {
|
||||
_gifPreview.setBad();
|
||||
}
|
||||
|
||||
if (_gifPreview && _gifPreview->ready() && !_gifPreview->started()) {
|
||||
_gifPreview->start({
|
||||
.frame = QSize(previewWidth(), previewHeight()),
|
||||
});
|
||||
}
|
||||
|
||||
update();
|
||||
} break;
|
||||
|
||||
case Notification::Repaint: {
|
||||
if (_gifPreview && !_gifPreview->currentDisplayed()) {
|
||||
update();
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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/chat/attach/attach_abstract_single_media_preview.h"
|
||||
#include "media/clip/media_clip_reader.h"
|
||||
|
||||
namespace Lottie {
|
||||
class SinglePlayer;
|
||||
} // namespace Lottie
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct PreparedFile;
|
||||
|
||||
class SingleMediaPreview final : public AbstractSingleMediaPreview {
|
||||
public:
|
||||
static SingleMediaPreview *Create(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
const PreparedFile &file,
|
||||
Fn<bool(AttachActionType)> actionAllowed,
|
||||
AttachControls::Type type = AttachControls::Type::Full);
|
||||
|
||||
SingleMediaPreview(
|
||||
QWidget *parent,
|
||||
const style::ComposeControls &st,
|
||||
Fn<bool()> gifPaused,
|
||||
QImage preview,
|
||||
bool animated,
|
||||
bool sticker,
|
||||
bool spoiler,
|
||||
const QString &animatedPreviewPath,
|
||||
AttachControls::Type type,
|
||||
Fn<bool(AttachActionType)> actionAllowed);
|
||||
|
||||
protected:
|
||||
bool supportsSpoilers() const override;
|
||||
bool drawBackground() const override;
|
||||
bool tryPaintAnimation(QPainter &p) override;
|
||||
bool isAnimatedPreviewReady() const override;
|
||||
|
||||
private:
|
||||
void prepareAnimatedPreview(
|
||||
const QString &animatedPreviewPath,
|
||||
bool animated);
|
||||
void clipCallback(Media::Clip::Notification notification);
|
||||
|
||||
const Fn<bool()> _gifPaused;
|
||||
const bool _sticker = false;
|
||||
Media::Clip::ReaderPointer _gifPreview;
|
||||
std::unique_ptr<Lottie::SinglePlayer> _lottiePreview;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1461
Telegram/SourceFiles/ui/chat/chat.style
Normal file
1461
Telegram/SourceFiles/ui/chat/chat.style
Normal file
File diff suppressed because it is too large
Load Diff
1309
Telegram/SourceFiles/ui/chat/chat_style.cpp
Normal file
1309
Telegram/SourceFiles/ui/chat/chat_style.cpp
Normal file
File diff suppressed because it is too large
Load Diff
668
Telegram/SourceFiles/ui/chat/chat_style.h
Normal file
668
Telegram/SourceFiles/ui/chat/chat_style.h
Normal file
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
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/cached_round_corners.h"
|
||||
#include "ui/chat/message_bubble.h"
|
||||
#include "ui/chat/chat_style_radius.h"
|
||||
#include "ui/controls/swipe_handler_data.h"
|
||||
#include "ui/style/style_core_palette.h"
|
||||
#include "layout/layout_selection.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
enum class ImageRoundRadius;
|
||||
|
||||
namespace style {
|
||||
struct TwoIconButton;
|
||||
struct ScrollArea;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ChatTheme;
|
||||
class ChatStyle;
|
||||
struct BubblePattern;
|
||||
|
||||
inline constexpr auto kColorPatternsCount = Text::kMaxQuoteOutlines;
|
||||
inline constexpr auto kColorIndexCount = uint8(1 << 6);
|
||||
inline constexpr auto kSimpleColorIndexCount = uint8(7);
|
||||
|
||||
inline constexpr auto kDefaultBgOpacity = 0.12;
|
||||
inline constexpr auto kDefaultOutline1Opacity = 0.9;
|
||||
inline constexpr auto kDefaultOutline2Opacity = 0.3;
|
||||
inline constexpr auto kDefaultOutline3Opacity = 0.6;
|
||||
inline constexpr auto kDefaultOutlineOpacitySecond = 0.5;
|
||||
|
||||
struct MessageStyle {
|
||||
CornersPixmaps msgBgCornersSmall;
|
||||
CornersPixmaps msgBgCornersLarge;
|
||||
style::color msgBg;
|
||||
style::color msgShadow;
|
||||
style::color msgServiceFg;
|
||||
style::color msgDateFg;
|
||||
style::color msgFileThumbLinkFg;
|
||||
style::color msgFileBg;
|
||||
style::color msgReplyBarColor;
|
||||
style::color msgWaveformActive;
|
||||
style::color msgWaveformInactive;
|
||||
style::color historyTextFg;
|
||||
style::color historyFileNameFg;
|
||||
style::color historyFileRadialFg;
|
||||
style::color mediaFg;
|
||||
style::TextPalette textPalette;
|
||||
style::TextPalette semiboldPalette;
|
||||
style::TextPalette fwdTextPalette;
|
||||
style::TextPalette replyTextPalette;
|
||||
style::icon tailLeft = { Qt::Uninitialized };
|
||||
style::icon tailRight = { Qt::Uninitialized };
|
||||
style::icon historyRepliesIcon = { Qt::Uninitialized };
|
||||
style::icon historyViewsIcon = { Qt::Uninitialized };
|
||||
style::icon historyPinIcon = { Qt::Uninitialized };
|
||||
style::icon historySentIcon = { Qt::Uninitialized };
|
||||
style::icon historyReceivedIcon = { Qt::Uninitialized };
|
||||
style::icon historyPsaIcon = { Qt::Uninitialized };
|
||||
style::icon historyCommentsOpen = { Qt::Uninitialized };
|
||||
style::icon historyComments = { Qt::Uninitialized };
|
||||
style::icon historyCallArrow = { Qt::Uninitialized };
|
||||
style::icon historyCallArrowMissed = { Qt::Uninitialized };
|
||||
style::icon historyCallIcon = { Qt::Uninitialized };
|
||||
style::icon historyCallCameraIcon = { Qt::Uninitialized };
|
||||
style::icon historyCallGroupIcon = { Qt::Uninitialized };
|
||||
style::icon historyFilePlay = { Qt::Uninitialized };
|
||||
style::icon historyFileWaiting = { Qt::Uninitialized };
|
||||
style::icon historyFileDownload = { Qt::Uninitialized };
|
||||
style::icon historyFileCancel = { Qt::Uninitialized };
|
||||
style::icon historyFilePause = { Qt::Uninitialized };
|
||||
style::icon historyFileImage = { Qt::Uninitialized };
|
||||
style::icon historyFileDocument = { Qt::Uninitialized };
|
||||
style::icon historyAudioDownload = { Qt::Uninitialized };
|
||||
style::icon historyAudioCancel = { Qt::Uninitialized };
|
||||
style::icon historyQuizTimer = { Qt::Uninitialized };
|
||||
style::icon historyQuizExplain = { Qt::Uninitialized };
|
||||
style::icon historyPollChosen = { Qt::Uninitialized };
|
||||
style::icon historyPollChoiceRight = { Qt::Uninitialized };
|
||||
style::icon historyTranscribeIcon = { Qt::Uninitialized };
|
||||
style::icon historyTranscribeLock = { Qt::Uninitialized };
|
||||
style::icon historyTranscribeHide = { Qt::Uninitialized };
|
||||
style::icon historyVoiceMessageTTL = { Qt::Uninitialized };
|
||||
style::icon liveLocationLongIcon = { Qt::Uninitialized };
|
||||
std::array<
|
||||
std::unique_ptr<Text::QuotePaintCache>,
|
||||
kColorPatternsCount> quoteCache;
|
||||
std::array<
|
||||
std::unique_ptr<Text::QuotePaintCache>,
|
||||
kColorPatternsCount> replyCache;
|
||||
std::unique_ptr<Text::QuotePaintCache> preCache;
|
||||
};
|
||||
|
||||
struct MessageImageStyle {
|
||||
CornersPixmaps msgDateImgBgCorners;
|
||||
CornersPixmaps msgServiceBgCornersSmall;
|
||||
CornersPixmaps msgServiceBgCornersLarge;
|
||||
CornersPixmaps msgShadowCornersSmall;
|
||||
CornersPixmaps msgShadowCornersLarge;
|
||||
style::color msgServiceBg;
|
||||
style::color msgDateImgBg;
|
||||
style::color msgShadow;
|
||||
style::color historyFileThumbRadialFg;
|
||||
style::icon historyFileThumbPlay = { Qt::Uninitialized };
|
||||
style::icon historyFileThumbWaiting = { Qt::Uninitialized };
|
||||
style::icon historyFileThumbDownload = { Qt::Uninitialized };
|
||||
style::icon historyFileThumbCancel = { Qt::Uninitialized };
|
||||
style::icon historyFileThumbPause = { Qt::Uninitialized };
|
||||
style::icon historyVideoDownload = { Qt::Uninitialized };
|
||||
style::icon historyVideoCancel = { Qt::Uninitialized };
|
||||
style::icon historyVideoMessageMute = { Qt::Uninitialized };
|
||||
style::icon historyVideoMessageTtlIcon = { Qt::Uninitialized };
|
||||
style::icon historyPageEnlarge = { Qt::Uninitialized };
|
||||
};
|
||||
|
||||
struct ColorCollectible {
|
||||
uint64 collectibleId = 0;
|
||||
uint64 giftEmojiId = 0;
|
||||
uint64 backgroundEmojiId = 0;
|
||||
QColor accentColor;
|
||||
std::vector<QColor> strip;
|
||||
QColor darkAccentColor;
|
||||
std::vector<QColor> darkStrip;
|
||||
|
||||
friend inline bool operator==(
|
||||
const ColorCollectible &,
|
||||
const ColorCollectible &) = default;
|
||||
};
|
||||
|
||||
struct ColorCollectiblePtrCompare {
|
||||
bool operator()(
|
||||
const std::weak_ptr<ColorCollectible> &a,
|
||||
const std::weak_ptr<ColorCollectible> &b) const {
|
||||
return a.owner_before(b);
|
||||
}
|
||||
};
|
||||
|
||||
struct ReactionPaintInfo {
|
||||
QPoint position;
|
||||
QPoint effectOffset;
|
||||
Fn<QRect(QPainter&)> effectPaint;
|
||||
};
|
||||
|
||||
struct BackgroundEmojiCache {
|
||||
QColor color;
|
||||
std::array<QImage, 3> frames;
|
||||
};
|
||||
|
||||
struct BackgroundEmojiData {
|
||||
std::unique_ptr<Text::CustomEmoji> emoji;
|
||||
QImage firstFrameMask;
|
||||
base::flat_map<int, BackgroundEmojiCache> caches;
|
||||
base::flat_map<
|
||||
std::weak_ptr<ColorCollectible>,
|
||||
BackgroundEmojiCache,
|
||||
ColorCollectiblePtrCompare> collectibleCaches;
|
||||
std::unique_ptr<Text::CustomEmoji> gift;
|
||||
QImage firstGiftFrame;
|
||||
|
||||
[[nodiscard]] static int CacheIndex(
|
||||
bool selected,
|
||||
bool outbg,
|
||||
bool inbubble,
|
||||
uint8 colorIndexPlusOne);
|
||||
};
|
||||
|
||||
struct ChatPaintHighlight {
|
||||
float64 opacity = 0.;
|
||||
float64 collapsion = 0.;
|
||||
TextSelection range;
|
||||
int todoItemId = 0;
|
||||
};
|
||||
|
||||
struct ChatPaintContext {
|
||||
not_null<const ChatStyle*> st;
|
||||
const BubblePattern *bubblesPattern = nullptr;
|
||||
ReactionPaintInfo *reactionInfo = nullptr;
|
||||
QRect viewport;
|
||||
QRect clip;
|
||||
TextSelection selection;
|
||||
ChatPaintHighlight highlight;
|
||||
QPainterPath *highlightPathCache = nullptr;
|
||||
mutable QRect highlightInterpolateTo;
|
||||
crl::time now = 0;
|
||||
Ui::Controls::SwipeContextData gestureHorizontal;
|
||||
|
||||
void translate(int x, int y) {
|
||||
viewport.translate(x, y);
|
||||
clip.translate(x, y);
|
||||
highlightInterpolateTo.translate(x, y);
|
||||
}
|
||||
void translate(QPoint point) {
|
||||
translate(point.x(), point.y());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool selected() const {
|
||||
return (selection == FullSelection);
|
||||
}
|
||||
[[nodiscard]] not_null<const MessageStyle*> messageStyle() const;
|
||||
[[nodiscard]] not_null<const MessageImageStyle*> imageStyle() const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> quoteCache(
|
||||
const std::shared_ptr<ColorCollectible> &colorCollectible,
|
||||
uint8 colorIndex) const;
|
||||
|
||||
[[nodiscard]] ChatPaintContext translated(int x, int y) const {
|
||||
auto result = *this;
|
||||
result.translate(x, y);
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] ChatPaintContext translated(QPoint point) const {
|
||||
return translated(point.x(), point.y());
|
||||
}
|
||||
[[nodiscard]] ChatPaintContext withSelection(
|
||||
TextSelection selection) const {
|
||||
auto result = *this;
|
||||
result.selection = selection;
|
||||
return result;
|
||||
}
|
||||
[[nodiscard]] auto computeHighlightCache() const
|
||||
-> std::optional<Ui::Text::HighlightInfoRequest> {
|
||||
if (highlight.range.empty() || highlight.collapsion <= 0.) {
|
||||
return {};
|
||||
}
|
||||
return Ui::Text::HighlightInfoRequest{
|
||||
.range = highlight.range,
|
||||
.interpolateTo = highlightInterpolateTo,
|
||||
.interpolateProgress = (1. - highlight.collapsion),
|
||||
.outPath = highlightPathCache,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// This is supported only in unwrapped media and text messages for now.
|
||||
enum class SkipDrawingParts {
|
||||
None,
|
||||
Content,
|
||||
Surrounding,
|
||||
Bubble,
|
||||
};
|
||||
SkipDrawingParts skipDrawingParts = SkipDrawingParts::None;
|
||||
|
||||
bool outbg = false;
|
||||
bool paused = false;
|
||||
|
||||
};
|
||||
|
||||
struct ChatPaintContextArgs {
|
||||
not_null<ChatTheme*> theme;
|
||||
QRect clip;
|
||||
QPoint visibleAreaPositionGlobal;
|
||||
int visibleAreaTop = 0;
|
||||
int visibleAreaWidth = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] int HistoryServiceMsgRadius();
|
||||
[[nodiscard]] int HistoryServiceMsgInvertedRadius();
|
||||
[[nodiscard]] int HistoryServiceMsgInvertedShrink();
|
||||
|
||||
struct ColorIndexData {
|
||||
std::array<uint32, kColorPatternsCount> light = {};
|
||||
std::array<uint32, kColorPatternsCount> dark = {};
|
||||
|
||||
friend inline bool operator==(
|
||||
const ColorIndexData&,
|
||||
const ColorIndexData&) = default;
|
||||
};
|
||||
|
||||
struct ColorIndicesCompressed {
|
||||
std::shared_ptr<std::array<ColorIndexData, kColorIndexCount>> colors;
|
||||
};
|
||||
|
||||
[[nodiscard]] int ColorPatternIndex(
|
||||
const ColorIndicesCompressed &indices,
|
||||
uint8 colorIndex,
|
||||
bool dark);
|
||||
|
||||
struct ColorIndexValues {
|
||||
std::array<QColor, kColorPatternsCount> outlines;
|
||||
QColor name;
|
||||
QColor bg;
|
||||
};
|
||||
|
||||
[[nodiscard]] ColorIndexValues SimpleColorIndexValues(
|
||||
QColor color,
|
||||
int patternIndex);
|
||||
|
||||
class ChatStyle final : public style::palette {
|
||||
public:
|
||||
explicit ChatStyle(rpl::producer<ColorIndicesCompressed> colorIndices);
|
||||
explicit ChatStyle(not_null<const style::palette*> isolated);
|
||||
ChatStyle(const ChatStyle &other) = delete;
|
||||
ChatStyle &operator=(const ChatStyle &other) = delete;
|
||||
~ChatStyle();
|
||||
|
||||
void apply(not_null<ChatTheme*> theme);
|
||||
void applyCustomPalette(const style::palette *palette);
|
||||
void applyAdjustedServiceBg(QColor serviceBg);
|
||||
|
||||
[[nodiscard]] bool dark() const {
|
||||
return _dark;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<Text::SpecialColor> highlightColors() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> paletteChanged() const {
|
||||
return _paletteChanged.events();
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
[[nodiscard]] Type value(const Type &original) const {
|
||||
auto my = Type();
|
||||
make(my, original);
|
||||
return my;
|
||||
}
|
||||
|
||||
template <typename Type>
|
||||
[[nodiscard]] const Type &value(
|
||||
rpl::lifetime &parentLifetime,
|
||||
const Type &original) const {
|
||||
const auto my = parentLifetime.make_state<Type>();
|
||||
make(*my, original);
|
||||
return *my;
|
||||
}
|
||||
|
||||
[[nodiscard]] const CornersPixmaps &serviceBgCornersNormal() const;
|
||||
[[nodiscard]] const CornersPixmaps &serviceBgCornersInverted() const;
|
||||
|
||||
[[nodiscard]] const MessageStyle &messageStyle(
|
||||
bool outbg,
|
||||
bool selected) const;
|
||||
[[nodiscard]] const MessageImageStyle &imageStyle(bool selected) const;
|
||||
|
||||
[[nodiscard]] int colorPatternIndex(uint8 colorIndex) const;
|
||||
[[nodiscard]] int collectiblePatternIndex(
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
[[nodiscard]] ColorIndexValues computeColorIndexValues(
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
|
||||
[[nodiscard]] auto serviceQuoteCache(bool twoColored) const
|
||||
-> not_null<Text::QuotePaintCache*>;
|
||||
[[nodiscard]] auto serviceReplyCache(bool twoColored) const
|
||||
-> not_null<Text::QuotePaintCache*>;
|
||||
[[nodiscard]] const ColorIndexValues &coloredValues(
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
[[nodiscard]] QColor collectibleNameColor(
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredQuoteCache(
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredReplyCache(
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleQuoteCache(
|
||||
bool selected,
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleReplyCache(
|
||||
bool selected,
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
|
||||
[[nodiscard]] const style::TextPalette &coloredTextPalette(
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
[[nodiscard]] const style::TextPalette &collectibleTextPalette(
|
||||
bool selected,
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
|
||||
[[nodiscard]] not_null<BackgroundEmojiData*> backgroundEmojiData(
|
||||
uint64 emojiId,
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
|
||||
[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersSmall() const;
|
||||
[[nodiscard]] const CornersPixmaps &msgBotKbOverBgAddCornersLarge() const;
|
||||
[[nodiscard]] const CornersPixmaps &msgSelectOverlayCorners(
|
||||
CachedCornerRadius radius) const;
|
||||
|
||||
[[nodiscard]] const style::TextPalette &historyPsaForwardPalette() const {
|
||||
return _historyPsaForwardPalette;
|
||||
}
|
||||
[[nodiscard]] const style::TextPalette &imgReplyTextPalette() const {
|
||||
return _imgReplyTextPalette;
|
||||
}
|
||||
[[nodiscard]] const style::TextPalette &serviceTextPalette() const {
|
||||
return _serviceTextPalette;
|
||||
}
|
||||
[[nodiscard]] const style::TextPalette &priceTagTextPalette() const {
|
||||
return _priceTagTextPalette;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyRepliesInvertedIcon() const {
|
||||
return _historyRepliesInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyViewsInvertedIcon() const {
|
||||
return _historyViewsInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyViewsSendingIcon() const {
|
||||
return _historyViewsSendingIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyViewsSendingInvertedIcon() const {
|
||||
return _historyViewsSendingInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyPinInvertedIcon() const {
|
||||
return _historyPinInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historySendingIcon() const {
|
||||
return _historySendingIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historySendingInvertedIcon() const {
|
||||
return _historySendingInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historySentInvertedIcon() const {
|
||||
return _historySentInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyReceivedInvertedIcon() const {
|
||||
return _historyReceivedInvertedIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &msgBotKbUrlIcon() const {
|
||||
return _msgBotKbUrlIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &msgBotKbPaymentIcon() const {
|
||||
return _msgBotKbPaymentIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &msgBotKbSwitchPmIcon() const {
|
||||
return _msgBotKbSwitchPmIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &msgBotKbWebviewIcon() const {
|
||||
return _msgBotKbWebviewIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &msgBotKbCopyIcon() const {
|
||||
return _msgBotKbCopyIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastCommentsIcon() const {
|
||||
return _historyFastCommentsIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastShareIcon() const {
|
||||
return _historyFastShareIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastTranscribeIcon() const {
|
||||
return _historyFastTranscribeIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastTranscribeLock() const {
|
||||
return _historyFastTranscribeLock;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyGoToOriginalIcon() const {
|
||||
return _historyGoToOriginalIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastCloseIcon() const {
|
||||
return _historyFastCloseIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyFastMoreIcon() const {
|
||||
return _historyFastMoreIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyMapPoint() const {
|
||||
return _historyMapPoint;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyMapPointInner() const {
|
||||
return _historyMapPointInner;
|
||||
}
|
||||
[[nodiscard]] const style::icon &youtubeIcon() const {
|
||||
return _youtubeIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &videoIcon() const {
|
||||
return _videoIcon;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyPollChoiceRight() const {
|
||||
return _historyPollChoiceRight;
|
||||
}
|
||||
[[nodiscard]] const style::icon &historyPollChoiceWrong() const {
|
||||
return _historyPollChoiceWrong;
|
||||
}
|
||||
|
||||
private:
|
||||
using ColoredQuotePaintCaches = std::array<
|
||||
std::unique_ptr<Text::QuotePaintCache>,
|
||||
kColorIndexCount * 2>;
|
||||
|
||||
struct ColoredPalette {
|
||||
ColoredPalette();
|
||||
ColoredPalette(const ColoredPalette &other);
|
||||
ColoredPalette &operator=(const ColoredPalette &other);
|
||||
|
||||
std::optional<style::owned_color> linkFg;
|
||||
style::TextPalette data;
|
||||
};
|
||||
|
||||
struct CollectibleColors {
|
||||
std::unique_ptr<Text::QuotePaintCache> quote;
|
||||
std::unique_ptr<Text::QuotePaintCache> quoteSelected;
|
||||
std::unique_ptr<Text::QuotePaintCache> reply;
|
||||
std::unique_ptr<Text::QuotePaintCache> replySelected;
|
||||
ColoredPalette palette;
|
||||
ColoredPalette paletteSelected;
|
||||
};
|
||||
|
||||
void assignPalette(not_null<const style::palette*> palette);
|
||||
void clearColorIndexCaches();
|
||||
void updateDarkValue();
|
||||
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> coloredCache(
|
||||
ColoredQuotePaintCaches &caches,
|
||||
bool selected,
|
||||
uint8 colorIndex) const;
|
||||
[[nodiscard]] not_null<Text::QuotePaintCache*> collectibleCache(
|
||||
std::unique_ptr<Text::QuotePaintCache> &cache,
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
[[nodiscard]] CollectibleColors &resolveCollectibleCaches(
|
||||
const std::shared_ptr<ColorCollectible> &collectible) const;
|
||||
|
||||
void make(style::color &my, const style::color &original) const;
|
||||
void make(style::icon &my, const style::icon &original) const;
|
||||
void make(
|
||||
style::TextPalette &my,
|
||||
const style::TextPalette &original) const;
|
||||
void make(
|
||||
style::TwoIconButton &my,
|
||||
const style::TwoIconButton &original) const;
|
||||
void make(
|
||||
style::ScrollArea &my,
|
||||
const style::ScrollArea &original) const;
|
||||
|
||||
[[nodiscard]] MessageStyle &messageStyleRaw(
|
||||
bool outbg,
|
||||
bool selected) const;
|
||||
[[nodiscard]] MessageStyle &messageIn();
|
||||
[[nodiscard]] MessageStyle &messageInSelected();
|
||||
[[nodiscard]] MessageStyle &messageOut();
|
||||
[[nodiscard]] MessageStyle &messageOutSelected();
|
||||
|
||||
[[nodiscard]] MessageImageStyle &imageStyleRaw(bool selected) const;
|
||||
[[nodiscard]] MessageImageStyle &image();
|
||||
[[nodiscard]] MessageImageStyle &imageSelected();
|
||||
|
||||
template <typename Type>
|
||||
void make(
|
||||
Type MessageStyle::*my,
|
||||
const Type &originalIn,
|
||||
const Type &originalInSelected,
|
||||
const Type &originalOut,
|
||||
const Type &originalOutSelected);
|
||||
|
||||
template <typename Type>
|
||||
void make(
|
||||
Type MessageImageStyle::*my,
|
||||
const Type &original,
|
||||
const Type &originalSelected);
|
||||
|
||||
mutable CornersPixmaps _serviceBgCornersNormal;
|
||||
mutable CornersPixmaps _serviceBgCornersInverted;
|
||||
|
||||
mutable std::array<MessageStyle, 4> _messageStyles;
|
||||
mutable std::array<MessageImageStyle, 2> _imageStyles;
|
||||
|
||||
mutable CornersPixmaps _msgBotKbOverBgAddCornersSmall;
|
||||
mutable CornersPixmaps _msgBotKbOverBgAddCornersLarge;
|
||||
mutable CornersPixmaps _msgSelectOverlayCorners[
|
||||
int(CachedCornerRadius::kCount)];
|
||||
|
||||
mutable std::vector<Text::SpecialColor> _highlightColors;
|
||||
mutable std::array<
|
||||
std::unique_ptr<Text::QuotePaintCache>,
|
||||
2> _serviceQuoteCache;
|
||||
mutable std::array<
|
||||
std::unique_ptr<Text::QuotePaintCache>,
|
||||
2> _serviceReplyCache;
|
||||
mutable std::array<
|
||||
std::optional<ColorIndexValues>,
|
||||
2 * kColorIndexCount> _coloredValues;
|
||||
mutable ColoredQuotePaintCaches _coloredQuoteCaches;
|
||||
mutable ColoredQuotePaintCaches _coloredReplyCaches;
|
||||
mutable std::array<
|
||||
ColoredPalette,
|
||||
2 * kColorIndexCount> _coloredTextPalettes;
|
||||
mutable base::flat_map<
|
||||
std::weak_ptr<ColorCollectible>,
|
||||
CollectibleColors,
|
||||
ColorCollectiblePtrCompare> _collectibleCaches;
|
||||
mutable base::flat_map<uint64, BackgroundEmojiData> _backgroundEmojis;
|
||||
|
||||
style::TextPalette _historyPsaForwardPalette;
|
||||
style::TextPalette _imgReplyTextPalette;
|
||||
style::TextPalette _serviceTextPalette;
|
||||
style::TextPalette _priceTagTextPalette;
|
||||
style::icon _historyRepliesInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historyViewsInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historyViewsSendingIcon = { Qt::Uninitialized };
|
||||
style::icon _historyViewsSendingInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historyPinInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historySendingIcon = { Qt::Uninitialized };
|
||||
style::icon _historySendingInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historySentInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _historyReceivedInvertedIcon = { Qt::Uninitialized };
|
||||
style::icon _msgBotKbUrlIcon = { Qt::Uninitialized };
|
||||
style::icon _msgBotKbPaymentIcon = { Qt::Uninitialized };
|
||||
style::icon _msgBotKbSwitchPmIcon = { Qt::Uninitialized };
|
||||
style::icon _msgBotKbWebviewIcon = { Qt::Uninitialized };
|
||||
style::icon _msgBotKbCopyIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastCommentsIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastShareIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastMoreIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastTranscribeIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastTranscribeLock = { Qt::Uninitialized };
|
||||
style::icon _historyGoToOriginalIcon = { Qt::Uninitialized };
|
||||
style::icon _historyFastCloseIcon = { Qt::Uninitialized };
|
||||
style::icon _historyMapPoint = { Qt::Uninitialized };
|
||||
style::icon _historyMapPointInner = { Qt::Uninitialized };
|
||||
style::icon _youtubeIcon = { Qt::Uninitialized };
|
||||
style::icon _videoIcon = { Qt::Uninitialized };
|
||||
style::icon _historyPollChoiceRight = { Qt::Uninitialized };
|
||||
style::icon _historyPollChoiceWrong = { Qt::Uninitialized };
|
||||
|
||||
ColorIndicesCompressed _colorIndices;
|
||||
|
||||
bool _dark = false;
|
||||
|
||||
rpl::event_stream<> _paletteChanged;
|
||||
|
||||
rpl::lifetime _defaultPaletteChangeLifetime;
|
||||
rpl::lifetime _colorIndicesLifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] uint8 DecideColorIndex(uint64 id);
|
||||
[[nodiscard]] uint8 ColorIndexToPaletteIndex(uint8 colorIndex);
|
||||
|
||||
[[nodiscard]] QColor FromNameFg(
|
||||
not_null<const ChatStyle*> st,
|
||||
bool selected,
|
||||
uint8 colorIndex,
|
||||
const std::shared_ptr<Ui::ColorCollectible> &colorCollectible);
|
||||
|
||||
[[nodiscard]] inline QColor FromNameFg(
|
||||
const ChatPaintContext &context,
|
||||
uint8 colorIndex,
|
||||
const std::shared_ptr<Ui::ColorCollectible> &colorCollectible) {
|
||||
return context.outbg
|
||||
? context.messageStyle()->msgServiceFg->c
|
||||
: FromNameFg(
|
||||
context.st,
|
||||
context.selected(),
|
||||
colorIndex,
|
||||
colorCollectible);
|
||||
}
|
||||
|
||||
void FillComplexOverlayRect(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const style::color &color,
|
||||
const CornersPixmaps &corners);
|
||||
|
||||
void FillComplexEllipse(
|
||||
QPainter &p,
|
||||
not_null<const ChatStyle*> st,
|
||||
QRect rect);
|
||||
|
||||
} // namespace Ui
|
||||
61
Telegram/SourceFiles/ui/chat/chat_style_radius.cpp
Normal file
61
Telegram/SourceFiles/ui/chat/chat_style_radius.cpp
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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 "ui/chat/chat_style_radius.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "base/options.h"
|
||||
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
base::options::toggle UseSmallMsgBubbleRadius({
|
||||
.id = kOptionUseSmallMsgBubbleRadius,
|
||||
.name = "Use small message bubble radius",
|
||||
.description = "Makes most message bubbles square-ish.",
|
||||
.restartRequired = true,
|
||||
});
|
||||
|
||||
} // namespace
|
||||
|
||||
const char kOptionUseSmallMsgBubbleRadius[] = "use-small-msg-bubble-radius";
|
||||
|
||||
int BubbleRadiusSmall() {
|
||||
return st::bubbleRadiusSmall;
|
||||
}
|
||||
|
||||
int BubbleRadiusLarge() {
|
||||
static const auto result = [] {
|
||||
if (UseSmallMsgBubbleRadius.value()) {
|
||||
return st::bubbleRadiusSmall;
|
||||
} else {
|
||||
return st::bubbleRadiusLarge;
|
||||
}
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
int MsgFileThumbRadiusSmall() {
|
||||
return st::msgFileThumbRadiusSmall;
|
||||
}
|
||||
|
||||
int MsgFileThumbRadiusLarge() {
|
||||
static const auto result = [] {
|
||||
if (UseSmallMsgBubbleRadius.value()) {
|
||||
return st::msgFileThumbRadiusSmall;
|
||||
} else {
|
||||
return st::msgFileThumbRadiusLarge;
|
||||
}
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
20
Telegram/SourceFiles/ui/chat/chat_style_radius.h
Normal file
20
Telegram/SourceFiles/ui/chat/chat_style_radius.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
[[nodiscard]] int BubbleRadiusSmall();
|
||||
[[nodiscard]] int BubbleRadiusLarge();
|
||||
|
||||
[[nodiscard]] int MsgFileThumbRadiusSmall();
|
||||
[[nodiscard]] int MsgFileThumbRadiusLarge();
|
||||
|
||||
extern const char kOptionUseSmallMsgBubbleRadius[];
|
||||
|
||||
} // namespace Ui
|
||||
1284
Telegram/SourceFiles/ui/chat/chat_theme.cpp
Normal file
1284
Telegram/SourceFiles/ui/chat/chat_theme.cpp
Normal file
File diff suppressed because it is too large
Load Diff
292
Telegram/SourceFiles/ui/chat/chat_theme.h
Normal file
292
Telegram/SourceFiles/ui/chat/chat_theme.h
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
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/effects/animations.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/weak_ptr.h"
|
||||
|
||||
namespace style {
|
||||
class palette;
|
||||
struct colorizer;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Text {
|
||||
class CustomEmoji;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ChatStyle;
|
||||
struct ChatPaintContext;
|
||||
struct BubblePattern;
|
||||
|
||||
struct ChatThemeGiftSymbol {
|
||||
QRectF area;
|
||||
float64 rotation = 0.;
|
||||
};
|
||||
|
||||
struct ChatThemeBackground {
|
||||
QString key;
|
||||
QImage prepared;
|
||||
QImage preparedForTiled;
|
||||
QImage gradientForFill;
|
||||
std::optional<QColor> colorForFill;
|
||||
std::vector<QColor> colors;
|
||||
std::vector<ChatThemeGiftSymbol> giftSymbols;
|
||||
QImage giftSymbolFrame;
|
||||
uint64_t giftId = 0;
|
||||
float64 patternOpacity = 1.;
|
||||
int gradientRotation = 0;
|
||||
bool isPattern = false;
|
||||
bool tile = false;
|
||||
|
||||
[[nodiscard]] bool waitingForNegativePattern() const {
|
||||
return isPattern && prepared.isNull() && (patternOpacity < 0.);
|
||||
}
|
||||
};
|
||||
|
||||
bool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b);
|
||||
bool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b);
|
||||
|
||||
struct ChatThemeBackgroundData {
|
||||
QString key;
|
||||
QString path;
|
||||
QByteArray bytes;
|
||||
QImage giftSymbolFrame;
|
||||
uint64 giftId = 0;
|
||||
bool gzipSvg = false;
|
||||
std::vector<QColor> colors;
|
||||
bool isPattern = false;
|
||||
float64 patternOpacity = 0.;
|
||||
int darkModeDimming = 0;
|
||||
bool isBlurred = false;
|
||||
bool forDarkMode = false;
|
||||
bool generateGradient = false;
|
||||
int gradientRotation = 0;
|
||||
};
|
||||
|
||||
struct ChatThemeBubblesData {
|
||||
std::vector<QColor> colors;
|
||||
std::optional<QColor> accent;
|
||||
};
|
||||
|
||||
struct CacheBackgroundRequest {
|
||||
ChatThemeBackground background;
|
||||
QSize area;
|
||||
int gradientRotationAdd = 0;
|
||||
float64 gradientProgress = 1.;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !background.prepared.isNull()
|
||||
|| !background.gradientForFill.isNull();
|
||||
}
|
||||
};
|
||||
|
||||
bool operator==(
|
||||
const CacheBackgroundRequest &a,
|
||||
const CacheBackgroundRequest &b);
|
||||
bool operator!=(
|
||||
const CacheBackgroundRequest &a,
|
||||
const CacheBackgroundRequest &b);
|
||||
|
||||
struct CacheBackgroundResult {
|
||||
QImage image;
|
||||
QImage gradient;
|
||||
QSize area;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
QRect giftArea;
|
||||
float64 giftRotation = 0;
|
||||
bool waitingForNegativePattern = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] CacheBackgroundResult CacheBackground(
|
||||
const CacheBackgroundRequest &request);
|
||||
|
||||
struct CachedBackground {
|
||||
CachedBackground() = default;
|
||||
CachedBackground(CacheBackgroundResult &&result);
|
||||
|
||||
QPixmap pixmap;
|
||||
QSize area;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
QRect giftArea;
|
||||
float64 giftRotation = 0.;
|
||||
mutable std::unique_ptr<Text::CustomEmoji> gift;
|
||||
bool waitingForNegativePattern = false;
|
||||
};
|
||||
|
||||
struct BackgroundState {
|
||||
CachedBackground was;
|
||||
CachedBackground now;
|
||||
float64 shown = 1.;
|
||||
};
|
||||
|
||||
struct ChatThemeKey {
|
||||
uint64 id = 0;
|
||||
bool dark = false;
|
||||
|
||||
explicit operator bool() const {
|
||||
return (id != 0);
|
||||
}
|
||||
|
||||
friend inline auto operator<=>(ChatThemeKey, ChatThemeKey) = default;
|
||||
friend inline bool operator==(ChatThemeKey, ChatThemeKey) = default;
|
||||
};
|
||||
|
||||
struct ChatThemeDescriptor {
|
||||
ChatThemeKey key;
|
||||
Fn<void(style::palette&)> preparePalette;
|
||||
ChatThemeBackgroundData backgroundData;
|
||||
ChatThemeBubblesData bubblesData;
|
||||
bool basedOnDark = false;
|
||||
};
|
||||
|
||||
class ChatTheme final : public base::has_weak_ptr {
|
||||
public:
|
||||
ChatTheme();
|
||||
|
||||
// Expected to be invoked on a background thread. Invokes callbacks there.
|
||||
ChatTheme(ChatThemeDescriptor &&descriptor);
|
||||
|
||||
~ChatTheme();
|
||||
|
||||
[[nodiscard]] ChatThemeKey key() const;
|
||||
[[nodiscard]] const style::palette *palette() const {
|
||||
return _palette.get();
|
||||
}
|
||||
|
||||
void setBackground(ChatThemeBackground &&background);
|
||||
void updateBackgroundImageFrom(ChatThemeBackground &&background);
|
||||
[[nodiscard]] const ChatThemeBackground &background() const {
|
||||
return _mutableBackground;
|
||||
}
|
||||
|
||||
void setBubblesBackground(QImage image);
|
||||
[[nodiscard]] const BubblePattern *bubblesBackgroundPattern() const {
|
||||
return _bubblesBackgroundPattern.get();
|
||||
}
|
||||
void finishCreateOnMain(); // Called on_main after setBubblesBackground.
|
||||
|
||||
[[nodiscard]] ChatPaintContext preparePaintContext(
|
||||
not_null<const ChatStyle*> st,
|
||||
QRect viewport,
|
||||
QRect clip,
|
||||
bool paused);
|
||||
[[nodiscard]] const BackgroundState &backgroundState(QSize area);
|
||||
void clearBackgroundState();
|
||||
[[nodiscard]] rpl::producer<> repaintBackgroundRequests() const;
|
||||
void rotateComplexGradientBackground();
|
||||
|
||||
[[nodiscard]] CacheBackgroundRequest cacheBackgroundRequest(
|
||||
QSize area,
|
||||
int addRotation = 0) const;
|
||||
|
||||
private:
|
||||
void cacheBackground();
|
||||
void cacheBackgroundNow();
|
||||
void cacheBackgroundAsync(
|
||||
const CacheBackgroundRequest &request,
|
||||
Fn<void(CacheBackgroundResult&&)> done = nullptr);
|
||||
void setCachedBackground(CacheBackgroundResult &&cached);
|
||||
[[nodiscard]] bool readyForBackgroundRotation() const;
|
||||
void generateNextBackgroundRotation();
|
||||
|
||||
void cacheBubbles();
|
||||
void cacheBubblesNow();
|
||||
void cacheBubblesAsync(
|
||||
const CacheBackgroundRequest &request);
|
||||
[[nodiscard]] CacheBackgroundRequest cacheBubblesRequest(
|
||||
QSize area) const;
|
||||
|
||||
[[nodiscard]] style::colorizer bubblesAccentColorizer(
|
||||
const QColor &accent) const;
|
||||
void adjustPalette(const ChatThemeDescriptor &descriptor);
|
||||
void set(const style::color &my, const QColor &color);
|
||||
void adjust(const style::color &my, const QColor &by);
|
||||
void adjust(const style::color &my, const style::colorizer &by);
|
||||
|
||||
ChatThemeKey _key;
|
||||
std::unique_ptr<style::palette> _palette;
|
||||
ChatThemeBackground _mutableBackground;
|
||||
BackgroundState _backgroundState;
|
||||
Animations::Simple _backgroundFade;
|
||||
CacheBackgroundRequest _backgroundCachingRequest;
|
||||
CacheBackgroundRequest _nextCachingRequest;
|
||||
CacheBackgroundResult _backgroundNext;
|
||||
int _backgroundVersion = 0;
|
||||
QSize _cacheBackgroundArea;
|
||||
crl::time _lastBackgroundAreaChangeTime = 0;
|
||||
std::optional<base::Timer> _cacheBackgroundTimer;
|
||||
|
||||
CachedBackground _bubblesBackground;
|
||||
QImage _bubblesBackgroundPrepared;
|
||||
CacheBackgroundRequest _bubblesCachingRequest;
|
||||
QSize _cacheBubblesArea;
|
||||
crl::time _lastBubblesAreaChangeTime = 0;
|
||||
std::optional<base::Timer> _cacheBubblesTimer;
|
||||
std::unique_ptr<BubblePattern> _bubblesBackgroundPattern;
|
||||
|
||||
rpl::event_stream<> _repaintBackgroundRequests;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
struct ChatBackgroundRects {
|
||||
QRect from;
|
||||
QRect to;
|
||||
};
|
||||
[[nodiscard]] ChatBackgroundRects ComputeChatBackgroundRects(
|
||||
QSize fillSize,
|
||||
QSize imageSize);
|
||||
|
||||
[[nodiscard]] QColor CountAverageColor(const QImage &image);
|
||||
[[nodiscard]] QColor CountAverageColor(const std::vector<QColor> &colors);
|
||||
[[nodiscard]] bool IsPatternInverted(
|
||||
const std::vector<QColor> &background,
|
||||
float64 patternOpacity);
|
||||
[[nodiscard]] QColor ThemeAdjustedColor(QColor original, QColor background);
|
||||
[[nodiscard]] QImage PreprocessBackgroundImage(QImage image);
|
||||
[[nodiscard]] std::optional<QColor> CalculateImageMonoColor(
|
||||
const QImage &image);
|
||||
[[nodiscard]] QImage PrepareImageForTiled(const QImage &prepared);
|
||||
|
||||
struct BackgroundImageFields {
|
||||
QImage image;
|
||||
std::vector<ChatThemeGiftSymbol> giftSymbols;
|
||||
};
|
||||
[[nodiscard]] BackgroundImageFields ReadBackgroundImage(
|
||||
const QString &path,
|
||||
const QByteArray &content,
|
||||
bool gzipSvg,
|
||||
bool findGiftSymbols = false);
|
||||
[[nodiscard]] QImage GenerateBackgroundImage(
|
||||
QSize size,
|
||||
const std::vector<QColor> &bg,
|
||||
int gradientRotation,
|
||||
float64 patternOpacity = 1.,
|
||||
Fn<void(QPainter&,bool)> drawPattern = nullptr);
|
||||
[[nodiscard]] QImage InvertPatternImage(QImage pattern);
|
||||
[[nodiscard]] QImage PreparePatternImage(
|
||||
QImage pattern,
|
||||
const std::vector<QColor> &bg,
|
||||
int gradientRotation,
|
||||
float64 patternOpacity);
|
||||
[[nodiscard]] QImage PrepareBlurredBackground(QImage image);
|
||||
[[nodiscard]] QImage GenerateDitheredGradient(
|
||||
const std::vector<QColor> &colors,
|
||||
int rotation);
|
||||
[[nodiscard]] ChatThemeBackground PrepareBackgroundImage(
|
||||
const ChatThemeBackgroundData &data);
|
||||
[[nodiscard]] QImage PrepareGiftSymbol(
|
||||
const std::unique_ptr<Text::CustomEmoji> &emoji);
|
||||
|
||||
} // namespace Ui
|
||||
288
Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp
Normal file
288
Telegram/SourceFiles/ui/chat/chats_filter_tag.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
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 "ui/chat/chats_filter_tag.h"
|
||||
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/integration.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
class ScaledSimpleEmoji final : public Ui::Text::CustomEmoji {
|
||||
public:
|
||||
ScaledSimpleEmoji(EmojiPtr emoji);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const EmojiPtr _emoji;
|
||||
QImage _frame;
|
||||
QPoint _shift;
|
||||
|
||||
};
|
||||
|
||||
class ScaledCustomEmoji final : public Ui::Text::CustomEmoji {
|
||||
public:
|
||||
ScaledCustomEmoji(std::unique_ptr<Ui::Text::CustomEmoji> wrapped);
|
||||
|
||||
int width() override;
|
||||
QString entityData() override;
|
||||
void paint(QPainter &p, const Context &context) override;
|
||||
void unload() override;
|
||||
bool ready() override;
|
||||
bool readyInDefaultState() override;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
||||
QImage _frame;
|
||||
QPoint _shift;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] int ScaledSize() {
|
||||
return st::dialogRowFilterTagStyle.font->height - 2 * st::lineWidth;
|
||||
}
|
||||
|
||||
ScaledSimpleEmoji::ScaledSimpleEmoji(EmojiPtr emoji)
|
||||
: _emoji(emoji) {
|
||||
}
|
||||
|
||||
int ScaledSimpleEmoji::width() {
|
||||
return ScaledSize();
|
||||
}
|
||||
|
||||
QString ScaledSimpleEmoji::entityData() {
|
||||
return u"scaled-simple:"_q + _emoji->text();
|
||||
}
|
||||
|
||||
void ScaledSimpleEmoji::paint(QPainter &p, const Context &context) {
|
||||
if (_frame.isNull()) {
|
||||
const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);
|
||||
const auto xskip = (st::emojiSize - adjusted) / 2;
|
||||
const auto yskip = xskip + (width() - st::emojiSize) / 2;
|
||||
_shift = { xskip, yskip };
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto large = Emoji::GetSizeLarge();
|
||||
const auto size = QSize(large, large);
|
||||
_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
_frame.setDevicePixelRatio(ratio);
|
||||
_frame.fill(Qt::transparent);
|
||||
|
||||
auto p = QPainter(&_frame);
|
||||
Emoji::Draw(p, _emoji, large, 0, 0);
|
||||
p.end();
|
||||
|
||||
_frame = _frame.scaled(
|
||||
QSize(width(), width()) * ratio,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
p.drawImage(context.position - _shift, _frame);
|
||||
}
|
||||
|
||||
void ScaledSimpleEmoji::unload() {
|
||||
}
|
||||
|
||||
bool ScaledSimpleEmoji::ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScaledSimpleEmoji::readyInDefaultState() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ScaledCustomEmoji::ScaledCustomEmoji(
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> wrapped)
|
||||
: _wrapped(std::move(wrapped)) {
|
||||
}
|
||||
|
||||
int ScaledCustomEmoji::width() {
|
||||
return ScaledSize();
|
||||
}
|
||||
|
||||
QString ScaledCustomEmoji::entityData() {
|
||||
return u"scaled-custom:"_q + _wrapped->entityData();
|
||||
}
|
||||
|
||||
void ScaledCustomEmoji::paint(QPainter &p, const Context &context) {
|
||||
if (_frame.isNull()) {
|
||||
if (!_wrapped->ready()) {
|
||||
return;
|
||||
}
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
const auto large = Emoji::GetSizeLarge();
|
||||
const auto largeadjust = Text::AdjustCustomEmojiSize(large / ratio);
|
||||
const auto size = QSize(largeadjust, largeadjust) * ratio;
|
||||
_frame = QImage(size, QImage::Format_ARGB32_Premultiplied);
|
||||
_frame.setDevicePixelRatio(ratio);
|
||||
_frame.fill(Qt::transparent);
|
||||
|
||||
auto p = QPainter(&_frame);
|
||||
p.translate(-context.position);
|
||||
const auto was = context.internal.forceFirstFrame;
|
||||
context.internal.forceFirstFrame = true;
|
||||
_wrapped->paint(p, context);
|
||||
context.internal.forceFirstFrame = was;
|
||||
p.end();
|
||||
|
||||
const auto smalladjust = Text::AdjustCustomEmojiSize(width());
|
||||
_frame = _frame.scaled(
|
||||
QSize(smalladjust, smalladjust) * ratio,
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
_wrapped->unload();
|
||||
|
||||
const auto adjusted = Text::AdjustCustomEmojiSize(st::emojiSize);
|
||||
const auto xskip = (st::emojiSize - adjusted) / 2;
|
||||
const auto yskip = xskip + (width() - st::emojiSize) / 2;
|
||||
|
||||
const auto add = (width() - smalladjust) / 2;
|
||||
_shift = QPoint(xskip, yskip) - QPoint(add, add);
|
||||
}
|
||||
p.drawImage(context.position - _shift, _frame);
|
||||
}
|
||||
|
||||
void ScaledCustomEmoji::unload() {
|
||||
_wrapped->unload();
|
||||
}
|
||||
|
||||
bool ScaledCustomEmoji::ready() {
|
||||
return !_frame.isNull() || _wrapped->ready();
|
||||
}
|
||||
|
||||
bool ScaledCustomEmoji::readyInDefaultState() {
|
||||
return !_frame.isNull() || _wrapped->ready();
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities PrepareSmallEmojiText(
|
||||
TextWithEntities text,
|
||||
ChatsFilterTagContext &context) {
|
||||
auto i = text.entities.begin();
|
||||
auto ch = text.text.constData();
|
||||
context.loading = false;
|
||||
const auto end = text.text.constData() + text.text.size();
|
||||
const auto adjust = [&](EntityInText &entity) {
|
||||
if (entity.type() != EntityType::CustomEmoji) {
|
||||
return;
|
||||
}
|
||||
const auto data = entity.data();
|
||||
if (data.startsWith(u"scaled-simple:"_q)) {
|
||||
return;
|
||||
}
|
||||
auto &emoji = context.emoji[data];
|
||||
if (!emoji) {
|
||||
emoji = Text::MakeCustomEmoji(data, context.textContext);
|
||||
}
|
||||
if (!emoji->ready()) {
|
||||
context.loading = true;
|
||||
}
|
||||
entity = EntityInText(
|
||||
entity.type(),
|
||||
entity.offset(),
|
||||
entity.length(),
|
||||
u"scaled-custom:"_q + entity.data());
|
||||
};
|
||||
const auto till = [](EntityInText &entity) {
|
||||
return entity.offset() + entity.length();
|
||||
};
|
||||
while (ch != end) {
|
||||
auto emojiLength = 0;
|
||||
if (const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength)) {
|
||||
const auto f = int(ch - text.text.constData());
|
||||
const auto l = f + emojiLength;
|
||||
while (i != text.entities.end() && till(*i) <= f) {
|
||||
adjust(*i);
|
||||
++i;
|
||||
}
|
||||
|
||||
ch += emojiLength;
|
||||
if (i != text.entities.end() && i->offset() < l) {
|
||||
continue;
|
||||
}
|
||||
i = text.entities.insert(i, EntityInText{
|
||||
EntityType::CustomEmoji,
|
||||
f,
|
||||
emojiLength,
|
||||
u"scaled-simple:"_q + emoji->text(),
|
||||
});
|
||||
} else {
|
||||
++ch;
|
||||
}
|
||||
}
|
||||
for (; i != text.entities.end(); ++i) {
|
||||
adjust(*i);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QImage ChatsFilterTag(
|
||||
const TextWithEntities &text,
|
||||
ChatsFilterTagContext &context) {
|
||||
const auto &roundedFont = st::dialogRowFilterTagStyle.font;
|
||||
const auto additionalWidth = roundedFont->spacew * 3;
|
||||
auto rich = Text::String(
|
||||
st::dialogRowFilterTagStyle,
|
||||
PrepareSmallEmojiText(text, context),
|
||||
kMarkupTextOptions,
|
||||
kQFixedMax,
|
||||
context.textContext);
|
||||
const auto roundedWidth = rich.maxWidth() + additionalWidth;
|
||||
const auto rect = QRect(0, 0, roundedWidth, roundedFont->height);
|
||||
auto cache = QImage(
|
||||
rect.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
cache.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&cache);
|
||||
const auto pen = QPen(context.active
|
||||
? st::dialogsBgActive->c
|
||||
: context.color);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(context.active
|
||||
? st::dialogsTextFgActive->c
|
||||
: anim::with_alpha(pen.color(), .15));
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = roundedFont->height / 3.;
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
}
|
||||
p.setPen(pen);
|
||||
p.setFont(roundedFont);
|
||||
const auto dx = (rect.width() - rich.maxWidth()) / 2;
|
||||
const auto dy = (rect.height() - roundedFont->height) / 2;
|
||||
rich.draw(p, {
|
||||
.position = rect.topLeft() + QPoint(dx, dy),
|
||||
.availableWidth = rich.maxWidth(),
|
||||
});
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(EmojiPtr emoji) {
|
||||
return std::make_unique<ScaledSimpleEmoji>(emoji);
|
||||
}
|
||||
|
||||
std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
|
||||
std::unique_ptr<Text::CustomEmoji> wrapped) {
|
||||
return std::make_unique<ScaledCustomEmoji>(std::move(wrapped));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
33
Telegram/SourceFiles/ui/chat/chats_filter_tag.h
Normal file
33
Telegram/SourceFiles/ui/chat/chats_filter_tag.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 "emoji.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ChatsFilterTagContext {
|
||||
base::flat_map<QString, std::unique_ptr<Text::CustomEmoji>> emoji;
|
||||
Text::MarkedContext textContext;
|
||||
QColor color;
|
||||
bool active = false;
|
||||
bool loading = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] QImage ChatsFilterTag(
|
||||
const TextWithEntities &text,
|
||||
ChatsFilterTagContext &context);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledSimpleEmoji(
|
||||
EmojiPtr emoji);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Text::CustomEmoji> MakeScaledCustomEmoji(
|
||||
std::unique_ptr<Text::CustomEmoji> wrapped);
|
||||
|
||||
} // namespace Ui
|
||||
352
Telegram/SourceFiles/ui/chat/choose_send_as.cpp
Normal file
352
Telegram/SourceFiles/ui/chat/choose_send_as.cpp
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
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 "ui/chat/choose_send_as.h"
|
||||
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "data/data_group_call.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer_values.h"
|
||||
#include "history/history.h"
|
||||
#include "ui/controls/send_as_button.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "main/main_session.h"
|
||||
#include "main/session/send_as_peers.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "settings/settings_premium.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
class Row final : public PeerListRow {
|
||||
public:
|
||||
explicit Row(const Main::SendAsPeer &sendAsPeer);
|
||||
|
||||
int paintNameIconGetWidth(
|
||||
Painter &p,
|
||||
Fn<void()> repaint,
|
||||
crl::time now,
|
||||
int nameLeft,
|
||||
int nameTop,
|
||||
int nameWidth,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) override;
|
||||
|
||||
private:
|
||||
bool _premiumRequired = false;
|
||||
|
||||
};
|
||||
|
||||
class ListController final : public PeerListController {
|
||||
public:
|
||||
ListController(
|
||||
std::vector<Main::SendAsPeer> list,
|
||||
not_null<PeerData*> selected);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void prepare() override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
[[nodiscard]] rpl::producer<not_null<PeerData*>> clicked() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Row> createRow(const Main::SendAsPeer &sendAsPeer);
|
||||
|
||||
std::vector<Main::SendAsPeer> _list;
|
||||
not_null<PeerData*> _selected;
|
||||
rpl::event_stream<not_null<PeerData*>> _clicked;
|
||||
|
||||
};
|
||||
|
||||
Row::Row(const Main::SendAsPeer &sendAsPeer)
|
||||
: PeerListRow(sendAsPeer.peer)
|
||||
, _premiumRequired(sendAsPeer.premiumRequired) {
|
||||
}
|
||||
|
||||
int Row::paintNameIconGetWidth(
|
||||
Painter &p,
|
||||
Fn<void()> repaint,
|
||||
crl::time now,
|
||||
int nameLeft,
|
||||
int nameTop,
|
||||
int nameWidth,
|
||||
int availableWidth,
|
||||
int outerWidth,
|
||||
bool selected) {
|
||||
if (_premiumRequired && !peer()->session().premium()) {
|
||||
const auto &icon = st::emojiPremiumRequired;
|
||||
availableWidth -= icon.width();
|
||||
const auto x = nameLeft + std::min(nameWidth, availableWidth);
|
||||
icon.paint(p, x, nameTop, outerWidth);
|
||||
return icon.width();
|
||||
}
|
||||
return PeerListRow::paintNameIconGetWidth(
|
||||
p,
|
||||
std::move(repaint),
|
||||
now,
|
||||
nameLeft,
|
||||
nameTop,
|
||||
nameWidth,
|
||||
availableWidth,
|
||||
outerWidth,
|
||||
selected);
|
||||
}
|
||||
|
||||
ListController::ListController(
|
||||
std::vector<Main::SendAsPeer> list,
|
||||
not_null<PeerData*> selected)
|
||||
: PeerListController()
|
||||
, _list(std::move(list))
|
||||
, _selected(selected) {
|
||||
Data::AmPremiumValue(
|
||||
&selected->session()
|
||||
) | rpl::skip(1) | rpl::on_next([=] {
|
||||
const auto count = delegate()->peerListFullRowsCount();
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
delegate()->peerListUpdateRow(
|
||||
delegate()->peerListRowAt(i));
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
Main::Session &ListController::session() const {
|
||||
return _selected->session();
|
||||
}
|
||||
|
||||
std::unique_ptr<Row> ListController::createRow(
|
||||
const Main::SendAsPeer &sendAsPeer) {
|
||||
auto result = std::make_unique<Row>(sendAsPeer);
|
||||
if (sendAsPeer.peer->isSelf()) {
|
||||
result->setCustomStatus(
|
||||
tr::lng_group_call_join_as_personal(tr::now));
|
||||
} else if (sendAsPeer.peer->isMegagroup()) {
|
||||
result->setCustomStatus(tr::lng_send_as_anonymous_admin(tr::now));
|
||||
} else if (const auto channel = sendAsPeer.peer->asChannel()) {
|
||||
result->setCustomStatus(tr::lng_chat_status_subscribers(
|
||||
tr::now,
|
||||
lt_count,
|
||||
channel->membersCount()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void ListController::prepare() {
|
||||
delegate()->peerListSetSearchMode(PeerListSearchMode::Disabled);
|
||||
for (const auto &sendAsPeer : _list) {
|
||||
auto row = createRow(sendAsPeer);
|
||||
const auto raw = row.get();
|
||||
delegate()->peerListAppendRow(std::move(row));
|
||||
if (sendAsPeer.peer == _selected) {
|
||||
delegate()->peerListSetRowChecked(raw, true);
|
||||
raw->finishCheckedAnimation();
|
||||
}
|
||||
}
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void ListController::rowClicked(not_null<PeerListRow*> row) {
|
||||
const auto peer = row->peer();
|
||||
if (peer == _selected) {
|
||||
return;
|
||||
}
|
||||
_clicked.fire_copy(peer);
|
||||
}
|
||||
|
||||
rpl::producer<not_null<PeerData*>> ListController::clicked() const {
|
||||
return _clicked.events();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseSendAsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ChooseSendAs &st,
|
||||
std::vector<Main::SendAsPeer> list,
|
||||
not_null<PeerData*> chosen,
|
||||
Fn<bool(not_null<PeerData*>)> done) {
|
||||
Expects(ranges::contains(list, chosen, &Main::SendAsPeer::peer));
|
||||
Expects(done != nullptr);
|
||||
|
||||
box->setWidth(st::groupCallJoinAsWidth);
|
||||
box->setTitle(tr::lng_send_as_title());
|
||||
box->addRow(object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
tr::lng_group_call_join_as_about(),
|
||||
st.label));
|
||||
|
||||
auto &lifetime = box->lifetime();
|
||||
const auto delegate = lifetime.make_state<
|
||||
PeerListContentDelegateSimple
|
||||
>();
|
||||
const auto controller = lifetime.make_state<ListController>(
|
||||
list,
|
||||
chosen);
|
||||
controller->setStyleOverrides(&st.list, nullptr);
|
||||
|
||||
controller->clicked(
|
||||
) | rpl::on_next([=](not_null<PeerData*> peer) {
|
||||
const auto weak = base::make_weak(box);
|
||||
if (done(peer) && weak) {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
const auto content = box->addRow(
|
||||
object_ptr<PeerListContent>(box, controller),
|
||||
style::margins());
|
||||
delegate->setContent(content);
|
||||
controller->setDelegate(delegate);
|
||||
box->addButton(tr::lng_box_done(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
rpl::producer<PeerData*> active,
|
||||
std::shared_ptr<ChatHelpers::Show> show) {
|
||||
using namespace rpl::mappers;
|
||||
const auto current = button->lifetime().make_state<
|
||||
rpl::variable<PeerData*>
|
||||
>(std::move(active));
|
||||
button->setClickedCallback([=, &st] {
|
||||
const auto peer = current->current();
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
const auto key = Main::SendAsKey{ peer, Main::SendAsType::Message };
|
||||
const auto session = &peer->session();
|
||||
const auto &list = session->sendAsPeers().list(key);
|
||||
if (list.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto done = [=](not_null<PeerData*> sendAs) {
|
||||
const auto i = ranges::find(
|
||||
list,
|
||||
sendAs,
|
||||
&Main::SendAsPeer::peer);
|
||||
if (i != end(list)
|
||||
&& i->premiumRequired
|
||||
&& !sendAs->session().premium()) {
|
||||
Settings::ShowPremiumPromoToast(
|
||||
show,
|
||||
tr::lng_send_as_premium_required(
|
||||
tr::now,
|
||||
lt_link,
|
||||
tr::link(
|
||||
tr::bold(
|
||||
tr::lng_send_as_premium_required_link(
|
||||
tr::now))),
|
||||
tr::marked),
|
||||
u"send_as"_q);
|
||||
return false;
|
||||
}
|
||||
session->sendAsPeers().saveChosen(peer, sendAs);
|
||||
return true;
|
||||
};
|
||||
show->show(Box(
|
||||
Ui::ChooseSendAsBox,
|
||||
st,
|
||||
list,
|
||||
session->sendAsPeers().resolveChosen(peer),
|
||||
done));
|
||||
});
|
||||
|
||||
const auto size = st.button.size;
|
||||
auto userpic = current->value(
|
||||
) | rpl::filter([=](PeerData *peer) {
|
||||
return peer && peer->isChannel();
|
||||
}) | rpl::map([=](not_null<PeerData*> peer) {
|
||||
const auto channel = peer->asChannel();
|
||||
|
||||
auto updates = rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(channel->session().sendAsPeers().updated(
|
||||
) | rpl::filter(
|
||||
_1 == Main::SendAsKey(channel, Main::SendAsType::Message)
|
||||
) | rpl::to_empty);
|
||||
|
||||
return rpl::combine(
|
||||
std::move(updates),
|
||||
channel->adminRightsValue()
|
||||
) | rpl::map([=] {
|
||||
return channel->session().sendAsPeers().resolveChosen(channel);
|
||||
}) | rpl::distinct_until_changed(
|
||||
) | rpl::map([=](not_null<PeerData*> chosen) {
|
||||
return Data::PeerUserpicImageValue(
|
||||
chosen,
|
||||
size * style::DevicePixelRatio());
|
||||
}) | rpl::flatten_latest();
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
std::move(
|
||||
userpic
|
||||
) | rpl::on_next([=](QImage &&userpic) {
|
||||
button->setUserpic(std::move(userpic));
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
std::shared_ptr<Data::GroupCall> videoStream,
|
||||
std::shared_ptr<ChatHelpers::Show> show) {
|
||||
const auto peer = videoStream->peer();
|
||||
const auto type = Main::SendAsType::VideoStream;
|
||||
const auto key = Main::SendAsKey{ peer, type };
|
||||
button->setClickedCallback([=, &st] {
|
||||
const auto session = &peer->session();
|
||||
const auto &list = session->sendAsPeers().list(key);
|
||||
if (list.size() < 2) {
|
||||
return;
|
||||
}
|
||||
const auto done = [=](not_null<PeerData*> sendAs) {
|
||||
videoStream->saveSendAs(sendAs);
|
||||
return true;
|
||||
};
|
||||
show->show(Box(
|
||||
Ui::ChooseSendAsBox,
|
||||
st,
|
||||
list,
|
||||
videoStream->resolveSendAs(),
|
||||
done));
|
||||
});
|
||||
|
||||
const auto size = st.button.size;
|
||||
auto userpic = videoStream->sendAsValue(
|
||||
) | rpl::distinct_until_changed(
|
||||
) | rpl::map([=](not_null<PeerData*> chosen) {
|
||||
return Data::PeerUserpicImageValue(
|
||||
chosen,
|
||||
size * style::DevicePixelRatio());
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
std::move(
|
||||
userpic
|
||||
) | rpl::on_next([=](QImage &&userpic) {
|
||||
button->setUserpic(std::move(userpic));
|
||||
}, button->lifetime());
|
||||
}
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
not_null<Window::SessionController*> window) {
|
||||
auto active = window->activeChatValue(
|
||||
) | rpl::map([=](Dialogs::Key key) {
|
||||
return key.history() ? key.history()->peer.get() : nullptr;
|
||||
});
|
||||
SetupSendAsButton(button, st, std::move(active), window->uiShow());
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
63
Telegram/SourceFiles/ui/chat/choose_send_as.h
Normal file
63
Telegram/SourceFiles/ui/chat/choose_send_as.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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/object_ptr.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace style {
|
||||
struct ChooseSendAs;
|
||||
} // namespace style
|
||||
|
||||
namespace Data {
|
||||
class GroupCall;
|
||||
} // namespace Data
|
||||
|
||||
namespace ChatHelpers {
|
||||
class Show;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Main {
|
||||
struct SendAsPeer;
|
||||
} // namespace Main
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class SendAsButton;
|
||||
|
||||
void ChooseSendAsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ChooseSendAs &st,
|
||||
std::vector<Main::SendAsPeer> list,
|
||||
not_null<PeerData*> chosen,
|
||||
Fn<bool(not_null<PeerData*>)> done);
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
rpl::producer<PeerData*> active,
|
||||
std::shared_ptr<ChatHelpers::Show> show);
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
std::shared_ptr<Data::GroupCall> videoStream,
|
||||
std::shared_ptr<ChatHelpers::Show> show);
|
||||
|
||||
void SetupSendAsButton(
|
||||
not_null<SendAsButton*> button,
|
||||
const style::ChooseSendAs &st,
|
||||
not_null<Window::SessionController*> window);
|
||||
|
||||
} // namespace Ui
|
||||
807
Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp
Normal file
807
Telegram/SourceFiles/ui/chat/choose_theme_controller.cpp
Normal file
@@ -0,0 +1,807 @@
|
||||
/*
|
||||
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 "ui/chat/choose_theme_controller.h"
|
||||
|
||||
#include "boxes/background_box.h"
|
||||
#include "boxes/transfer_gift_box.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/chat_theme.h"
|
||||
#include "ui/chat/message_bubble.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/painter.h"
|
||||
#include "main/main_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "window/themes/window_theme.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_cloud_themes.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "apiwrap.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_layers.h" // boxTitle.
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
const auto kDisableElement = [] { return u"disable"_q; };
|
||||
|
||||
struct Preview {
|
||||
QImage preview;
|
||||
QRect userpic;
|
||||
};
|
||||
|
||||
[[nodiscard]] Preview GeneratePreview(
|
||||
not_null<Ui::ChatTheme*> theme,
|
||||
const std::shared_ptr<Ui::DynamicImage> &takenUserpic) {
|
||||
const auto &background = theme->background();
|
||||
const auto &colors = background.colors;
|
||||
const auto size = st::chatThemePreviewSize;
|
||||
auto prepared = background.prepared;
|
||||
const auto paintPattern = [&](QPainter &p, bool inverted) {
|
||||
if (prepared.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto w = prepared.width();
|
||||
const auto h = prepared.height();
|
||||
const auto scaled = size.scaled(
|
||||
st::windowMinWidth / 2,
|
||||
st::windowMinHeight / 2,
|
||||
Qt::KeepAspectRatio);
|
||||
const auto use = (scaled.width() > w || scaled.height() > h)
|
||||
? scaled.scaled({ w, h }, Qt::KeepAspectRatio)
|
||||
: scaled;
|
||||
const auto good = QSize(
|
||||
std::max(use.width(), 1),
|
||||
std::max(use.height(), 1));
|
||||
auto small = prepared.copy(QRect(
|
||||
QPoint(
|
||||
(w - good.width()) / 2,
|
||||
(h - good.height()) / 2),
|
||||
good));
|
||||
if (inverted) {
|
||||
small = Ui::InvertPatternImage(std::move(small));
|
||||
}
|
||||
p.drawImage(
|
||||
QRect(QPoint(), size * style::DevicePixelRatio()),
|
||||
small);
|
||||
};
|
||||
auto userpic = QRect();
|
||||
const auto fullsize = size * style::DevicePixelRatio();
|
||||
auto result = background.waitingForNegativePattern()
|
||||
? QImage(
|
||||
fullsize,
|
||||
QImage::Format_ARGB32_Premultiplied)
|
||||
: Ui::GenerateBackgroundImage(
|
||||
fullsize,
|
||||
colors.empty() ? std::vector{ 1, QColor(0, 0, 0) } : colors,
|
||||
background.gradientRotation,
|
||||
background.patternOpacity,
|
||||
paintPattern);
|
||||
if (background.waitingForNegativePattern()) {
|
||||
result.fill(Qt::black);
|
||||
}
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
auto p = QPainter(&result);
|
||||
const auto sent = QRect(
|
||||
QPoint(
|
||||
(size.width()
|
||||
- st::chatThemeBubbleSize.width()
|
||||
- st::chatThemeBubblePosition.x()),
|
||||
st::chatThemeBubblePosition.y()),
|
||||
st::chatThemeBubbleSize);
|
||||
const auto received = QRect(
|
||||
st::chatThemeBubblePosition.x(),
|
||||
sent.y() + sent.height() + st::chatThemeBubbleSkip,
|
||||
sent.width(),
|
||||
sent.height());
|
||||
const auto radius = st::chatThemeBubbleRadius;
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
if (const auto pattern = theme->bubblesBackgroundPattern()) {
|
||||
auto bubble = pattern->pixmap.toImage().scaled(
|
||||
sent.size() * style::DevicePixelRatio(),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation
|
||||
).convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
const auto corners = Images::CornersMask(radius);
|
||||
p.drawImage(sent, Images::Round(std::move(bubble), corners));
|
||||
} else {
|
||||
p.setBrush(theme->palette()->msgOutBg()->c);
|
||||
p.drawRoundedRect(sent, radius, radius);
|
||||
}
|
||||
p.setBrush(theme->palette()->msgInBg()->c);
|
||||
p.drawRoundedRect(received, radius, radius);
|
||||
|
||||
if (takenUserpic) {
|
||||
const auto border = 2 * st::lineWidth;
|
||||
const auto inner = received.marginsRemoved(
|
||||
{ border, border, border, border });
|
||||
userpic = inner;
|
||||
userpic.setWidth(userpic.height());
|
||||
|
||||
st::chatThemeGiftTaken.paintInCenter(
|
||||
p,
|
||||
QRect(
|
||||
inner.x() + inner.width() - inner.height() - border,
|
||||
inner.y(),
|
||||
inner.height(),
|
||||
inner.height()),
|
||||
theme->palette()->msgFileInBg()->c);
|
||||
}
|
||||
}
|
||||
return {
|
||||
.preview = Images::Round(std::move(result), ImageRoundRadius::Large),
|
||||
.userpic = userpic,
|
||||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage GenerateEmptyPreview() {
|
||||
auto result = QImage(
|
||||
st::chatThemePreviewSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(st::settingsThemeNotSupportedBg->c);
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
auto p = QPainter(&result);
|
||||
p.setPen(st::menuIconFg);
|
||||
p.setFont(st::semiboldFont);
|
||||
const auto top = st::chatThemeEmptyPreviewTop;
|
||||
const auto width = st::chatThemePreviewSize.width();
|
||||
const auto height = st::chatThemePreviewSize.height() - top;
|
||||
p.drawText(
|
||||
QRect(0, top, width, height),
|
||||
tr::lng_chat_theme_none(tr::now),
|
||||
style::al_top);
|
||||
}
|
||||
return Images::Round(std::move(result), ImageRoundRadius::Large);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ChooseThemeController::Entry {
|
||||
QString token;
|
||||
Ui::ChatThemeKey key;
|
||||
std::shared_ptr<Ui::ChatTheme> theme;
|
||||
std::shared_ptr<Data::DocumentMedia> media;
|
||||
std::shared_ptr<Data::UniqueGift> gift;
|
||||
std::shared_ptr<Ui::DynamicImage> takenUserpic;
|
||||
std::unique_ptr<Ui::Text::CustomEmoji> custom;
|
||||
EmojiPtr emoji = nullptr;
|
||||
QImage preview;
|
||||
QRect userpic;
|
||||
QRect geometry;
|
||||
bool chosen = false;
|
||||
};
|
||||
|
||||
ChooseThemeController::ChooseThemeController(
|
||||
not_null<RpWidget*> parent,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<PeerData*> peer)
|
||||
: _controller(window)
|
||||
, _peer(peer)
|
||||
, _wrap(std::make_unique<VerticalLayout>(parent))
|
||||
, _topShadow(std::make_unique<PlainShadow>(parent))
|
||||
, _content(_wrap->add(object_ptr<RpWidget>(_wrap.get())))
|
||||
, _inner(CreateChild<RpWidget>(_content.get()))
|
||||
, _disabledEmoji(Ui::Emoji::Find(QString::fromUtf8("\xe2\x9d\x8c")))
|
||||
, _dark(Window::Theme::IsThemeDarkValue()) {
|
||||
init(parent->sizeValue());
|
||||
}
|
||||
|
||||
ChooseThemeController::~ChooseThemeController() {
|
||||
_controller->clearPeerThemeOverride(_peer);
|
||||
}
|
||||
|
||||
void ChooseThemeController::init(rpl::producer<QSize> outer) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto themes = &_controller->session().data().cloudThemes();
|
||||
if (themes->myGiftThemesTokens().empty()) {
|
||||
themes->myGiftThemesLoadMore();
|
||||
}
|
||||
|
||||
const auto &list = themes->chatThemes();
|
||||
if (!list.empty()) {
|
||||
fill(list);
|
||||
} else {
|
||||
themes->refreshChatThemes();
|
||||
themes->chatThemesUpdated(
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
fill(themes->chatThemes());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
const auto titleWrap = _wrap->insert(
|
||||
0,
|
||||
object_ptr<FixedHeightWidget>(
|
||||
_wrap.get(),
|
||||
st::boxTitle.style.font->height),
|
||||
st::chatThemeTitlePadding);
|
||||
auto title = CreateChild<FlatLabel>(
|
||||
titleWrap,
|
||||
tr::lng_chat_theme_title(),
|
||||
st::boxTitle);
|
||||
_wrap->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_wrap.get()).fillRect(clip, st::windowBg);
|
||||
}, lifetime());
|
||||
|
||||
const auto close = Ui::CreateChild<Ui::IconButton>(
|
||||
_wrap.get(),
|
||||
st::boxTitleClose);
|
||||
close->setClickedCallback([=] { this->close(); });
|
||||
rpl::combine(
|
||||
_wrap->widthValue(),
|
||||
titleWrap->positionValue()
|
||||
) | rpl::on_next([=](int width, QPoint position) {
|
||||
close->moveToRight(0, 0, width);
|
||||
}, close->lifetime());
|
||||
|
||||
initButtons();
|
||||
initList();
|
||||
|
||||
_inner->positionValue(
|
||||
) | rpl::on_next([=](QPoint position) {
|
||||
title->move(std::max(position.x(), 0), 0);
|
||||
}, title->lifetime());
|
||||
|
||||
std::move(
|
||||
outer
|
||||
) | rpl::on_next([=](QSize outer) {
|
||||
_wrap->resizeToWidth(outer.width());
|
||||
_wrap->move(0, outer.height() - _wrap->height());
|
||||
const auto line = st::lineWidth;
|
||||
_topShadow->setGeometry(0, _wrap->y() - line, outer.width(), line);
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_shouldBeShown.value(),
|
||||
_forceHidden.value(),
|
||||
_1 && !_2
|
||||
) | rpl::on_next([=](bool shown) {
|
||||
_wrap->setVisible(shown);
|
||||
_topShadow->setVisible(shown);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ChooseThemeController::initButtons() {
|
||||
const auto controls = _wrap->add(object_ptr<RpWidget>(_wrap.get()));
|
||||
const auto apply = CreateChild<RoundButton>(
|
||||
controls,
|
||||
tr::lng_chat_theme_apply(),
|
||||
st::defaultLightButton);
|
||||
apply->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
const auto choose = CreateChild<RoundButton>(
|
||||
controls,
|
||||
tr::lng_chat_theme_change_wallpaper(),
|
||||
st::defaultLightButton);
|
||||
choose->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
|
||||
const auto &margin = st::chatThemeButtonMargin;
|
||||
controls->resize(
|
||||
margin.left() + choose->width() + margin.right(),
|
||||
margin.top() + choose->height() + margin.bottom());
|
||||
rpl::combine(
|
||||
controls->widthValue(),
|
||||
apply->widthValue(),
|
||||
choose->widthValue(),
|
||||
_chosen.value()
|
||||
) | rpl::on_next([=](
|
||||
int outer,
|
||||
int applyWidth,
|
||||
int chooseWidth,
|
||||
QString chosen) {
|
||||
const auto was = _peer->themeToken();
|
||||
const auto now = (chosen == kDisableElement()) ? QString() : chosen;
|
||||
const auto changed = (now != was);
|
||||
apply->setVisible(changed);
|
||||
choose->setVisible(!changed);
|
||||
const auto shown = changed ? apply : choose;
|
||||
const auto shownWidth = changed ? applyWidth : chooseWidth;
|
||||
const auto inner = margin.left() + shownWidth + margin.right();
|
||||
const auto left = (outer - inner) / 2;
|
||||
shown->moveToLeft(left, margin.top());
|
||||
}, controls->lifetime());
|
||||
|
||||
const auto setTheme = crl::guard(apply, [=](
|
||||
const QString &token,
|
||||
const std::shared_ptr<Ui::ChatTheme> &theme) {
|
||||
SetPeerTheme(_controller, _peer, token, theme);
|
||||
_controller->toggleChooseChatTheme(_peer);
|
||||
});
|
||||
const auto confirmTakeGiftTheme = crl::guard(apply, [=](
|
||||
const QString &token,
|
||||
const std::shared_ptr<Ui::ChatTheme> &theme,
|
||||
not_null<PeerData*> nowHasTheme) {
|
||||
_controller->show(Box([=](not_null<Ui::GenericBox*> box) {
|
||||
const auto confirmed = [=](Fn<void()> close) {
|
||||
setTheme(token, theme);
|
||||
close();
|
||||
};
|
||||
Ui::ConfirmBox(box, {
|
||||
.text = tr::lng_chat_theme_gift_replace(
|
||||
lt_name,
|
||||
rpl::single(tr::bold(nowHasTheme->shortName())),
|
||||
tr::marked),
|
||||
.confirmed = confirmed,
|
||||
.confirmText = tr::lng_box_yes(),
|
||||
});
|
||||
}));
|
||||
});
|
||||
apply->setClickedCallback([=] {
|
||||
if (const auto chosen = findChosen()) {
|
||||
const auto was = _peer->themeToken();
|
||||
const auto now = chosen->key ? _chosen.current() : QString();
|
||||
const auto user = chosen->gift
|
||||
? chosen->gift->themeUser
|
||||
: nullptr;
|
||||
if (was != now) {
|
||||
if (!user || user == _peer) {
|
||||
setTheme(now, chosen->theme);
|
||||
} else {
|
||||
confirmTakeGiftTheme(now, chosen->theme, user);
|
||||
}
|
||||
} else {
|
||||
_controller->toggleChooseChatTheme(_peer);
|
||||
}
|
||||
} else {
|
||||
_controller->toggleChooseChatTheme(_peer);
|
||||
}
|
||||
});
|
||||
choose->setClickedCallback([=] {
|
||||
_controller->show(Box<BackgroundBox>(_controller, _peer));
|
||||
});
|
||||
}
|
||||
|
||||
void ChooseThemeController::paintEntry(QPainter &p, const Entry &entry) {
|
||||
const auto geometry = entry.geometry;
|
||||
p.drawImage(geometry, entry.preview);
|
||||
if (const auto userpic = entry.takenUserpic.get()) {
|
||||
userpic->subscribeToUpdates([=] {
|
||||
_inner->update();
|
||||
});
|
||||
p.drawImage(
|
||||
entry.userpic.translated(geometry.topLeft()),
|
||||
userpic->image(entry.userpic.height()));
|
||||
}
|
||||
|
||||
const auto size = Ui::Emoji::GetSizeLarge();
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto esize = size / factor;
|
||||
const auto emojiLeft = geometry.x() + (geometry.width() - esize) / 2;
|
||||
const auto emojiTop = geometry.y()
|
||||
+ geometry.height()
|
||||
- esize
|
||||
- st::chatThemeEmojiBottom;
|
||||
const auto customSize = Ui::Text::AdjustCustomEmojiSize(esize);
|
||||
const auto customSkip = (esize - customSize) / 2;
|
||||
|
||||
if (const auto emoji = entry.emoji) {
|
||||
Ui::Emoji::Draw(p, emoji, size, emojiLeft, emojiTop);
|
||||
} else if (const auto custom = entry.custom.get()) {
|
||||
custom->paint(p, {
|
||||
.textColor = st::windowFg->c,
|
||||
.position = { emojiLeft + customSkip, emojiTop + customSkip },
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.chosen) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::activeLineFg->p;
|
||||
const auto width = st::defaultInputField.borderActive;
|
||||
pen.setWidth(width);
|
||||
p.setPen(pen);
|
||||
const auto add = st::lineWidth + width;
|
||||
p.drawRoundedRect(
|
||||
entry.geometry.marginsAdded({ add, add, add, add }),
|
||||
st::roundRadiusLarge + add,
|
||||
st::roundRadiusLarge + add);
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseThemeController::initList() {
|
||||
_content->resize(
|
||||
_content->width(),
|
||||
(st::chatThemeEntryMargin.top()
|
||||
+ st::chatThemePreviewSize.height()
|
||||
+ st::chatThemeEntryMargin.bottom()));
|
||||
_inner->setMouseTracking(true);
|
||||
|
||||
_inner->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
auto p = QPainter(_inner.get());
|
||||
for (const auto &entry : _entries) {
|
||||
if (entry.preview.isNull() || !clip.intersects(entry.geometry)) {
|
||||
continue;
|
||||
}
|
||||
paintEntry(p, entry);
|
||||
}
|
||||
}, lifetime());
|
||||
const auto byPoint = [=](QPoint position) -> Entry* {
|
||||
for (auto &entry : _entries) {
|
||||
if (entry.geometry.contains(position)) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const auto chosenText = [=](const Entry *entry) {
|
||||
if (!entry) {
|
||||
return QString();
|
||||
} else if (entry->key) {
|
||||
return entry->token;
|
||||
} else {
|
||||
return kDisableElement();
|
||||
}
|
||||
};
|
||||
_inner->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
if (type == QEvent::MouseMove) {
|
||||
const auto mouse = static_cast<QMouseEvent*>(event.get());
|
||||
const auto skip = _inner->width() - _content->width();
|
||||
if (skip <= 0) {
|
||||
_dragStartPosition = _pressPosition = std::nullopt;
|
||||
} else if (_pressPosition.has_value()
|
||||
&& ((mouse->globalPos() - *_pressPosition).manhattanLength()
|
||||
>= QApplication::startDragDistance())) {
|
||||
_dragStartPosition = base::take(_pressPosition);
|
||||
_dragStartInnerLeft = _inner->x();
|
||||
}
|
||||
if (_dragStartPosition.has_value()) {
|
||||
const auto shift = mouse->globalPos().x()
|
||||
- _dragStartPosition->x();
|
||||
updateInnerLeft(_dragStartInnerLeft + shift);
|
||||
} else {
|
||||
_inner->setCursor(byPoint(mouse->pos())
|
||||
? style::cur_pointer
|
||||
: style::cur_default);
|
||||
}
|
||||
} else if (type == QEvent::MouseButtonPress) {
|
||||
const auto mouse = static_cast<QMouseEvent*>(event.get());
|
||||
if (mouse->button() == Qt::LeftButton) {
|
||||
_pressPosition = mouse->globalPos();
|
||||
}
|
||||
_pressed = chosenText(byPoint(mouse->pos()));
|
||||
} else if (type == QEvent::MouseButtonRelease) {
|
||||
_pressPosition = _dragStartPosition = std::nullopt;
|
||||
const auto mouse = static_cast<QMouseEvent*>(event.get());
|
||||
const auto entry = byPoint(mouse->pos());
|
||||
const auto chosen = chosenText(entry);
|
||||
if (entry && chosen == _pressed && chosen != _chosen.current()) {
|
||||
clearCurrentBackgroundState();
|
||||
if (const auto was = findChosen()) {
|
||||
was->chosen = false;
|
||||
}
|
||||
_chosen = chosen;
|
||||
entry->chosen = true;
|
||||
if (entry->theme || !entry->key) {
|
||||
_controller->overridePeerTheme(
|
||||
_peer,
|
||||
entry->theme,
|
||||
entry->token);
|
||||
}
|
||||
_inner->update();
|
||||
}
|
||||
_pressed = QString();
|
||||
} else if (type == QEvent::Wheel) {
|
||||
const auto wheel = static_cast<QWheelEvent*>(event.get());
|
||||
const auto was = _inner->x();
|
||||
updateInnerLeft((wheel->angleDelta().x() != 0)
|
||||
? (was + (wheel->pixelDelta().x()
|
||||
? wheel->pixelDelta().x()
|
||||
: wheel->angleDelta().x()))
|
||||
: (wheel->angleDelta().y() != 0)
|
||||
? (was + (wheel->pixelDelta().y()
|
||||
? wheel->pixelDelta().y()
|
||||
: wheel->angleDelta().y()))
|
||||
: was);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_content->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> event) {
|
||||
const auto type = event->type();
|
||||
if (type == QEvent::KeyPress) {
|
||||
const auto key = static_cast<QKeyEvent*>(event.get());
|
||||
if (key->key() == Qt::Key_Escape) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
rpl::combine(
|
||||
_content->widthValue(),
|
||||
_inner->widthValue()
|
||||
) | rpl::on_next([=](int content, int inner) {
|
||||
if (!content || !inner) {
|
||||
return;
|
||||
} else if (!_entries.empty() && !_initialInnerLeftApplied) {
|
||||
applyInitialInnerLeft();
|
||||
} else {
|
||||
updateInnerLeft(_inner->x());
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ChooseThemeController::applyInitialInnerLeft() {
|
||||
if (const auto chosen = findChosen()) {
|
||||
updateInnerLeft(
|
||||
_content->width() / 2 - chosen->geometry.center().x());
|
||||
}
|
||||
_initialInnerLeftApplied = true;
|
||||
}
|
||||
|
||||
void ChooseThemeController::updateInnerLeft(int now) {
|
||||
const auto skip = _content->width() - _inner->width();
|
||||
const auto clamped = (skip >= 0)
|
||||
? (skip / 2)
|
||||
: std::clamp(now, skip, 0);
|
||||
_inner->move(clamped, 0);
|
||||
const auto visibleTill = -clamped + _content->width();
|
||||
if (_giftsFinishAt - visibleTill < _content->width()) {
|
||||
_peer->owner().cloudThemes().myGiftThemesLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
void ChooseThemeController::close() {
|
||||
if (const auto chosen = findChosen()) {
|
||||
if (_peer->themeToken() != chosen->token) {
|
||||
clearCurrentBackgroundState();
|
||||
}
|
||||
}
|
||||
_controller->toggleChooseChatTheme(_peer);
|
||||
}
|
||||
|
||||
void ChooseThemeController::clearCurrentBackgroundState() {
|
||||
if (const auto entry = findChosen()) {
|
||||
if (entry->theme) {
|
||||
entry->theme->clearBackgroundState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto ChooseThemeController::findChosen() -> Entry* {
|
||||
const auto chosen = _chosen.current();
|
||||
if (chosen.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto &entry : _entries) {
|
||||
if (!entry.key && chosen == kDisableElement()) {
|
||||
return &entry;
|
||||
} else if (chosen == entry.token) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ChooseThemeController::findChosen() const -> const Entry* {
|
||||
return const_cast<ChooseThemeController*>(this)->findChosen();
|
||||
}
|
||||
|
||||
void ChooseThemeController::fill(
|
||||
const std::vector<Data::CloudTheme> &themes) {
|
||||
if (themes.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto single = st::chatThemePreviewSize;
|
||||
const auto skip = st::chatThemeEntrySkip;
|
||||
const auto &margin = st::chatThemeEntryMargin;
|
||||
const auto initial = _peer->themeToken();
|
||||
if (initial.isEmpty()) {
|
||||
_chosen = kDisableElement();
|
||||
}
|
||||
|
||||
const auto cloudThemes = &_controller->session().data().cloudThemes();
|
||||
rpl::combine(
|
||||
_dark.value(),
|
||||
rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(cloudThemes->myGiftThemesUpdated())
|
||||
) | rpl::on_next([=](bool dark, auto) {
|
||||
if (!cloudThemes->myGiftThemesReady()) {
|
||||
return;
|
||||
}
|
||||
clearCurrentBackgroundState();
|
||||
if (_chosen.current().isEmpty() && !initial.isEmpty()) {
|
||||
_chosen = initial;
|
||||
}
|
||||
|
||||
_cachingLifetime.destroy();
|
||||
const auto old = base::take(_entries);
|
||||
auto x = margin.left();
|
||||
_entries.push_back({
|
||||
.emoji = _disabledEmoji,
|
||||
.preview = GenerateEmptyPreview(),
|
||||
.geometry = QRect(QPoint(x, margin.top()), single),
|
||||
.chosen = (_chosen.current() == kDisableElement()),
|
||||
});
|
||||
Assert(_entries.front().emoji != nullptr);
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_entries.front().preview = GenerateEmptyPreview();
|
||||
}, _cachingLifetime);
|
||||
|
||||
const auto type = dark
|
||||
? Data::CloudThemeType::Dark
|
||||
: Data::CloudThemeType::Light;
|
||||
|
||||
x += single.width() + skip;
|
||||
|
||||
const auto owner = &_controller->session().data();
|
||||
const auto manager = &owner->customEmojiManager();
|
||||
const auto push = [&](
|
||||
const Data::CloudTheme &theme,
|
||||
const QString &token) {
|
||||
if (token.isEmpty() || !theme.settings.contains(type)) {
|
||||
return;
|
||||
}
|
||||
const auto key = ChatThemeKey{ theme.id, dark };
|
||||
const auto isChosen = (_chosen.current() == token);
|
||||
const auto themeUser = theme.unique
|
||||
? theme.unique->themeUser
|
||||
: nullptr;
|
||||
_entries.push_back({
|
||||
.token = token,
|
||||
.key = key,
|
||||
.gift = theme.unique,
|
||||
.takenUserpic = (themeUser
|
||||
? Ui::MakeUserpicThumbnail(themeUser, true)
|
||||
: nullptr),
|
||||
.custom = (theme.unique
|
||||
? manager->create(
|
||||
theme.unique->model.document,
|
||||
[=] { _inner->update(); },
|
||||
Data::CustomEmojiSizeTag::Large)
|
||||
: nullptr),
|
||||
.emoji = (theme.emoticon.isEmpty()
|
||||
? nullptr
|
||||
: Ui::Emoji::Find(theme.emoticon)),
|
||||
.geometry = QRect(QPoint(x, skip), single),
|
||||
.chosen = isChosen,
|
||||
});
|
||||
_controller->cachedChatThemeValue(
|
||||
theme,
|
||||
Data::WallPaper(0),
|
||||
type
|
||||
) | rpl::filter([=](const std::shared_ptr<ChatTheme> &data) {
|
||||
return data && (data->key() == key);
|
||||
}) | rpl::take(
|
||||
1
|
||||
) | rpl::on_next([=](std::shared_ptr<ChatTheme> &&data) {
|
||||
const auto key = data->key();
|
||||
const auto i = ranges::find(_entries, key, &Entry::key);
|
||||
if (i == end(_entries)) {
|
||||
return;
|
||||
}
|
||||
const auto theme = data.get();
|
||||
const auto token = i->token;
|
||||
i->theme = std::move(data);
|
||||
auto generated = GeneratePreview(theme, i->takenUserpic);
|
||||
i->preview = std::move(generated.preview);
|
||||
i->userpic = generated.userpic;
|
||||
if (_chosen.current() == token) {
|
||||
_controller->overridePeerTheme(_peer, i->theme, token);
|
||||
}
|
||||
_inner->update();
|
||||
|
||||
if (!theme->background().isPattern
|
||||
|| !theme->background().prepared.isNull()) {
|
||||
return;
|
||||
}
|
||||
// Subscribe to pattern loading if needed.
|
||||
theme->repaintBackgroundRequests(
|
||||
) | rpl::filter([=] {
|
||||
const auto i = ranges::find(
|
||||
_entries,
|
||||
key,
|
||||
&Entry::key);
|
||||
return (i == end(_entries))
|
||||
|| !i->theme->background().prepared.isNull();
|
||||
}) | rpl::take(1) | rpl::on_next([=] {
|
||||
const auto i = ranges::find(
|
||||
_entries,
|
||||
key,
|
||||
&Entry::key);
|
||||
if (i == end(_entries)) {
|
||||
return;
|
||||
}
|
||||
auto generated = GeneratePreview(theme, i->takenUserpic);
|
||||
i->preview = std::move(generated.preview);
|
||||
i->userpic = generated.userpic;
|
||||
_inner->update();
|
||||
}, _cachingLifetime);
|
||||
}, _cachingLifetime);
|
||||
x += single.width() + skip;
|
||||
};
|
||||
|
||||
_giftsFinishAt = 0;
|
||||
if (const auto now = cloudThemes->themeForToken(initial)) {
|
||||
push(*now, initial);
|
||||
}
|
||||
for (const auto &token : cloudThemes->myGiftThemesTokens()) {
|
||||
if (const auto found = cloudThemes->themeForToken(token)) {
|
||||
if (token != initial) {
|
||||
push(*found, token);
|
||||
_giftsFinishAt = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto &theme : themes) {
|
||||
if (const auto emoji = Ui::Emoji::Find(theme.emoticon)) {
|
||||
const auto token = emoji->text();
|
||||
if (token != initial) {
|
||||
push(theme, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto full = x - skip + margin.right();
|
||||
_inner->resize(
|
||||
full,
|
||||
margin.top() + single.height() + margin.bottom());
|
||||
|
||||
if (!_initialInnerLeftApplied && _content->width() > 0) {
|
||||
applyInitialInnerLeft();
|
||||
}
|
||||
}, lifetime());
|
||||
_shouldBeShown = true;
|
||||
}
|
||||
|
||||
bool ChooseThemeController::shouldBeShown() const {
|
||||
return _shouldBeShown.current();
|
||||
}
|
||||
|
||||
rpl::producer<bool> ChooseThemeController::shouldBeShownValue() const {
|
||||
return _shouldBeShown.value();
|
||||
}
|
||||
|
||||
int ChooseThemeController::height() const {
|
||||
return shouldBeShown() ? _wrap->height() : 0;
|
||||
}
|
||||
|
||||
void ChooseThemeController::hide() {
|
||||
_forceHidden = true;
|
||||
}
|
||||
|
||||
void ChooseThemeController::show() {
|
||||
_forceHidden = false;
|
||||
}
|
||||
|
||||
void ChooseThemeController::raise() {
|
||||
_wrap->raise();
|
||||
_topShadow->raise();
|
||||
}
|
||||
|
||||
void ChooseThemeController::setFocus() {
|
||||
_content->setFocus();
|
||||
}
|
||||
|
||||
rpl::lifetime &ChooseThemeController::lifetime() {
|
||||
return _wrap->lifetime();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
86
Telegram/SourceFiles/ui/chat/choose_theme_controller.h
Normal file
86
Telegram/SourceFiles/ui/chat/choose_theme_controller.h
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Data {
|
||||
struct CloudTheme;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class PlainShadow;
|
||||
class VerticalLayout;
|
||||
|
||||
class ChooseThemeController final {
|
||||
public:
|
||||
ChooseThemeController(
|
||||
not_null<RpWidget*> parent,
|
||||
not_null<Window::SessionController*> window,
|
||||
not_null<PeerData*> peer);
|
||||
~ChooseThemeController();
|
||||
|
||||
[[nodiscard]] bool shouldBeShown() const;
|
||||
[[nodiscard]] rpl::producer<bool> shouldBeShownValue() const;
|
||||
[[nodiscard]] int height() const;
|
||||
|
||||
void hide();
|
||||
void show();
|
||||
void raise();
|
||||
void setFocus();
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime();
|
||||
|
||||
private:
|
||||
struct Entry;
|
||||
|
||||
void init(rpl::producer<QSize> outer);
|
||||
void initButtons();
|
||||
void initList();
|
||||
void fill(const std::vector<Data::CloudTheme> &themes);
|
||||
void close();
|
||||
|
||||
void clearCurrentBackgroundState();
|
||||
void paintEntry(QPainter &p, const Entry &entry);
|
||||
void applyInitialInnerLeft();
|
||||
void updateInnerLeft(int now);
|
||||
|
||||
[[nodiscard]] Entry *findChosen();
|
||||
[[nodiscard]] const Entry *findChosen() const;
|
||||
|
||||
const not_null<Window::SessionController*> _controller;
|
||||
const not_null<PeerData*> _peer;
|
||||
const std::unique_ptr<VerticalLayout> _wrap;
|
||||
const std::unique_ptr<PlainShadow> _topShadow;
|
||||
|
||||
const not_null<RpWidget*> _content;
|
||||
const not_null<RpWidget*> _inner;
|
||||
const EmojiPtr _disabledEmoji = nullptr;
|
||||
std::vector<Entry> _entries;
|
||||
QString _pressed;
|
||||
rpl::variable<QString> _chosen;
|
||||
std::optional<QPoint> _pressPosition;
|
||||
std::optional<QPoint> _dragStartPosition;
|
||||
int _dragStartInnerLeft = 0;
|
||||
int _giftsFinishAt = 0;
|
||||
bool _initialInnerLeftApplied = false;
|
||||
|
||||
rpl::variable<bool> _shouldBeShown = false;
|
||||
rpl::variable<bool> _forceHidden = false;
|
||||
rpl::variable<bool> _dark = false;
|
||||
rpl::lifetime _cachingLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
74
Telegram/SourceFiles/ui/chat/continuous_scroll.cpp
Normal file
74
Telegram/SourceFiles/ui/chat/continuous_scroll.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 "ui/chat/continuous_scroll.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void ContinuousScroll::wheelEvent(QWheelEvent *e) {
|
||||
if (_tracking
|
||||
&& !e->angleDelta().isNull()
|
||||
&& (e->angleDelta().y() < 0)
|
||||
&& (scrollTopMax() == scrollTop())) {
|
||||
_addContentRequests.fire({});
|
||||
if (base::take(_contentAdded)) {
|
||||
viewportEvent(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ScrollArea::wheelEvent(e);
|
||||
}
|
||||
|
||||
void ContinuousScroll::setTrackingContent(bool value) {
|
||||
if (_tracking == value) {
|
||||
return;
|
||||
}
|
||||
_tracking = value;
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void ContinuousScroll::reconnect() {
|
||||
if (!_tracking) {
|
||||
_connection.release();
|
||||
return;
|
||||
}
|
||||
const auto handleAction = [=](int action) {
|
||||
const auto scroll = verticalScrollBar();
|
||||
const auto step = (action == QAbstractSlider::SliderSingleStepAdd)
|
||||
? scroll->singleStep()
|
||||
: (action == QAbstractSlider::SliderPageStepAdd)
|
||||
? scroll->pageStep()
|
||||
: 0;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
const auto newTop = scrollTop() + step;
|
||||
if (newTop > scrollTopMax()) {
|
||||
_addContentRequests.fire({});
|
||||
if (base::take(_contentAdded)) {
|
||||
scroll->setSliderPosition(newTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
_connection = QObject::connect(
|
||||
verticalScrollBar(),
|
||||
&QAbstractSlider::actionTriggered,
|
||||
handleAction);
|
||||
}
|
||||
|
||||
void ContinuousScroll::contentAdded() {
|
||||
_contentAdded = true;
|
||||
}
|
||||
|
||||
rpl::producer<> ContinuousScroll::addContentRequests() const {
|
||||
return _addContentRequests.events();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
41
Telegram/SourceFiles/ui/chat/continuous_scroll.h
Normal file
41
Telegram/SourceFiles/ui/chat/continuous_scroll.h
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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/widgets/scroll_area.h"
|
||||
#include "base/qt_connection.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
// This class is designed for seamless scrolling of
|
||||
// on-demand augmented content.
|
||||
class ContinuousScroll final : public ScrollArea {
|
||||
public:
|
||||
using ScrollArea::ScrollArea;
|
||||
|
||||
[[nodiscard]] rpl::producer<> addContentRequests() const;
|
||||
void contentAdded();
|
||||
|
||||
void setTrackingContent(bool value);
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
private:
|
||||
void reconnect();
|
||||
|
||||
base::qt_connection _connection;
|
||||
|
||||
bool _contentAdded = false;
|
||||
bool _tracking = false;
|
||||
|
||||
rpl::event_stream<> _addContentRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
68
Telegram/SourceFiles/ui/chat/forward_options_box.cpp
Normal file
68
Telegram/SourceFiles/ui/chat/forward_options_box.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 "ui/chat/forward_options_box.h"
|
||||
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void FillForwardOptions(
|
||||
Fn<not_null<AbstractCheckView*>(
|
||||
rpl::producer<QString> &&,
|
||||
bool)> createView,
|
||||
ForwardOptions options,
|
||||
Fn<void(ForwardOptions)> optionsChanged,
|
||||
rpl::lifetime &lifetime) {
|
||||
Expects(optionsChanged != nullptr);
|
||||
|
||||
const auto names = createView(
|
||||
(options.sendersCount == 1
|
||||
? tr::lng_forward_show_sender
|
||||
: tr::lng_forward_show_senders)(),
|
||||
!options.dropNames);
|
||||
const auto captions = options.captionsCount
|
||||
? createView(
|
||||
(options.captionsCount == 1
|
||||
? tr::lng_forward_show_caption
|
||||
: tr::lng_forward_show_captions)(),
|
||||
!options.dropCaptions).get()
|
||||
: nullptr;
|
||||
|
||||
const auto notify = [=] {
|
||||
optionsChanged({
|
||||
.sendersCount = options.sendersCount,
|
||||
.captionsCount = options.captionsCount,
|
||||
.dropNames = !names->checked(),
|
||||
.dropCaptions = (captions && !captions->checked()),
|
||||
});
|
||||
};
|
||||
names->checkedChanges(
|
||||
) | rpl::on_next([=](bool showNames) {
|
||||
if (showNames && captions && !captions->checked()) {
|
||||
captions->setChecked(true, anim::type::normal);
|
||||
} else {
|
||||
notify();
|
||||
}
|
||||
}, lifetime);
|
||||
if (captions) {
|
||||
captions->checkedChanges(
|
||||
) | rpl::on_next([=](bool showCaptions) {
|
||||
if (!showCaptions && names->checked()) {
|
||||
names->setChecked(false, anim::type::normal);
|
||||
} else {
|
||||
notify();
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
31
Telegram/SourceFiles/ui/chat/forward_options_box.h
Normal file
31
Telegram/SourceFiles/ui/chat/forward_options_box.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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/layers/generic_box.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractCheckView;
|
||||
|
||||
struct ForwardOptions {
|
||||
int sendersCount = 0;
|
||||
int captionsCount = 0;
|
||||
bool dropNames = false;
|
||||
bool dropCaptions = false;
|
||||
};
|
||||
|
||||
void FillForwardOptions(
|
||||
Fn<not_null<AbstractCheckView*>(
|
||||
rpl::producer<QString> &&,
|
||||
bool)> createView,
|
||||
ForwardOptions options,
|
||||
Fn<void(ForwardOptions)> optionsChanged,
|
||||
rpl::lifetime &lifetime);
|
||||
|
||||
} // namespace Ui
|
||||
455
Telegram/SourceFiles/ui/chat/group_call_bar.cpp
Normal file
455
Telegram/SourceFiles/ui/chat/group_call_bar.cpp
Normal file
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
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 "ui/chat/group_call_bar.h"
|
||||
|
||||
#include "ui/chat/group_call_userpics.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
|
||||
#include "styles/style_window.h" // st::columnMinimalWidthLeft
|
||||
#include "styles/palette.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
GroupCallScheduledLeft::GroupCallScheduledLeft(TimeId date)
|
||||
: _date(date)
|
||||
, _datePrecise(computePreciseDate())
|
||||
, _timer([=] { update(); }) {
|
||||
update();
|
||||
base::unixtime::updates(
|
||||
) | rpl::on_next([=] {
|
||||
restart();
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
crl::time GroupCallScheduledLeft::computePreciseDate() const {
|
||||
return crl::now() + (_date - base::unixtime::now()) * crl::time(1000);
|
||||
}
|
||||
|
||||
void GroupCallScheduledLeft::setDate(TimeId date) {
|
||||
if (_date == date) {
|
||||
return;
|
||||
}
|
||||
_date = date;
|
||||
restart();
|
||||
}
|
||||
|
||||
void GroupCallScheduledLeft::restart() {
|
||||
_datePrecise = computePreciseDate();
|
||||
_timer.cancel();
|
||||
update();
|
||||
}
|
||||
|
||||
rpl::producer<QString> GroupCallScheduledLeft::text(Negative negative) const {
|
||||
return (negative == Negative::Show)
|
||||
? _text.value()
|
||||
: _textNonNegative.value();
|
||||
}
|
||||
|
||||
rpl::producer<bool> GroupCallScheduledLeft::late() const {
|
||||
return _late.value();
|
||||
}
|
||||
|
||||
void GroupCallScheduledLeft::update() {
|
||||
const auto now = crl::now();
|
||||
const auto duration = (_datePrecise - now);
|
||||
const auto left = crl::time(base::SafeRound(std::abs(duration) / 1000.));
|
||||
const auto late = (duration < 0) && (left > 0);
|
||||
_late = late;
|
||||
constexpr auto kDay = 24 * 60 * 60;
|
||||
if (left >= kDay) {
|
||||
const auto days = (left / kDay);
|
||||
_textNonNegative = tr::lng_days(tr::now, lt_count, days);
|
||||
_text = late
|
||||
? tr::lng_days(tr::now, lt_count, -days)
|
||||
: _textNonNegative.current();
|
||||
} else {
|
||||
const auto hours = left / (60 * 60);
|
||||
const auto minutes = (left % (60 * 60)) / 60;
|
||||
const auto seconds = (left % 60);
|
||||
_textNonNegative = (hours > 0)
|
||||
? (u"%1:%2:%3"_q
|
||||
.arg(hours, 2, 10, QChar('0'))
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')))
|
||||
: (u"%1:%2"_q
|
||||
.arg(minutes, 2, 10, QChar('0'))
|
||||
.arg(seconds, 2, 10, QChar('0')));
|
||||
_text = (late ? QString(QChar(0x2212)) : QString())
|
||||
+ _textNonNegative.current();
|
||||
}
|
||||
if (left >= kDay) {
|
||||
_timer.callOnce((left % kDay) * crl::time(1000));
|
||||
} else {
|
||||
const auto fraction = (std::abs(duration) + 500) % 1000;
|
||||
if (fraction < 400 || fraction > 600) {
|
||||
const auto next = std::abs(duration) % 1000;
|
||||
_timer.callOnce((duration < 0) ? (1000 - next) : next);
|
||||
} else if (!_timer.isActive()) {
|
||||
_timer.callEach(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GroupCallBar::GroupCallBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<GroupCallBarContent> content,
|
||||
rpl::producer<bool> &&hideBlobs)
|
||||
: _wrap(parent, object_ptr<RpWidget>(parent))
|
||||
, _inner(_wrap.entity())
|
||||
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
|
||||
, _userpics(std::make_unique<GroupCallUserpics>(
|
||||
st::historyGroupCallUserpics,
|
||||
std::move(hideBlobs),
|
||||
[=] { updateUserpics(); })) {
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
|
||||
_wrap.entity()->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
|
||||
}, lifetime());
|
||||
_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
auto copy = std::move(
|
||||
content
|
||||
) | rpl::start_spawning(_wrap.lifetime());
|
||||
|
||||
rpl::duplicate(
|
||||
copy
|
||||
) | rpl::on_next([=](GroupCallBarContent &&content) {
|
||||
_content = content;
|
||||
_userpics->update(_content.users, !_wrap.isHidden());
|
||||
_inner->update();
|
||||
refreshScheduledProcess();
|
||||
}, lifetime());
|
||||
if (!_open && !_join) {
|
||||
refreshScheduledProcess();
|
||||
}
|
||||
|
||||
std::move(
|
||||
copy
|
||||
) | rpl::map([=](const GroupCallBarContent &content) {
|
||||
return !content.shown;
|
||||
}) | rpl::on_next_done([=](bool hidden) {
|
||||
_shouldBeShown = !hidden;
|
||||
if (!_forceHidden) {
|
||||
_wrap.toggle(_shouldBeShown, anim::type::normal);
|
||||
}
|
||||
}, [=] {
|
||||
_forceHidden = true;
|
||||
_wrap.toggle(false, anim::type::normal);
|
||||
}, lifetime());
|
||||
|
||||
setupInner();
|
||||
}
|
||||
|
||||
GroupCallBar::~GroupCallBar() = default;
|
||||
|
||||
void GroupCallBar::refreshOpenBrush() {
|
||||
Expects(_open != nullptr);
|
||||
|
||||
const auto width = _open->width();
|
||||
if (_openBrushForWidth == width) {
|
||||
return;
|
||||
}
|
||||
auto gradient = QLinearGradient(QPoint(width, 0), QPoint(0, 0));
|
||||
gradient.setStops(QGradientStops{
|
||||
{ 0.0, st::groupCallForceMutedBar1->c },
|
||||
{ .7, st::groupCallForceMutedBar2->c },
|
||||
{ 1.0, st::groupCallForceMutedBar3->c }
|
||||
});
|
||||
_openBrushOverride = QBrush(std::move(gradient));
|
||||
_openBrushForWidth = width;
|
||||
_open->setBrushOverride(_openBrushOverride);
|
||||
}
|
||||
|
||||
void GroupCallBar::refreshScheduledProcess() {
|
||||
const auto date = _content.scheduleDate;
|
||||
if (!date) {
|
||||
if (_scheduledProcess) {
|
||||
_scheduledProcess = nullptr;
|
||||
_open = nullptr;
|
||||
_openBrushForWidth = 0;
|
||||
}
|
||||
if (!_join) {
|
||||
_join = std::make_unique<RoundButton>(
|
||||
_inner.get(),
|
||||
tr::lng_group_call_join(),
|
||||
st::groupCallTopBarJoin);
|
||||
setupRightButton(_join.get());
|
||||
}
|
||||
} else if (!_scheduledProcess) {
|
||||
_scheduledProcess = std::make_unique<GroupCallScheduledLeft>(date);
|
||||
_join = nullptr;
|
||||
_open = std::make_unique<RoundButton>(
|
||||
_inner.get(),
|
||||
_scheduledProcess->text(GroupCallScheduledLeft::Negative::Show),
|
||||
st::groupCallTopBarOpen);
|
||||
setupRightButton(_open.get());
|
||||
_open->widthValue(
|
||||
) | rpl::on_next([=] {
|
||||
refreshOpenBrush();
|
||||
}, _open->lifetime());
|
||||
} else {
|
||||
_scheduledProcess->setDate(date);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::setupInner() {
|
||||
_inner->resize(0, st::historyReplyHeight);
|
||||
_inner->paintRequest(
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
auto p = Painter(_inner);
|
||||
paint(p);
|
||||
}, _inner->lifetime());
|
||||
|
||||
// Clicks.
|
||||
_inner->setCursor(style::cur_pointer);
|
||||
_inner->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return (event->type() == QEvent::MouseButtonPress)
|
||||
&& (static_cast<QMouseEvent*>(event.get())->button()
|
||||
== Qt::LeftButton);
|
||||
}) | rpl::map([=] {
|
||||
return _inner->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return (event->type() == QEvent::MouseButtonRelease);
|
||||
}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return _inner->rect().contains(
|
||||
static_cast<QMouseEvent*>(event.get())->pos());
|
||||
});
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());
|
||||
|
||||
_wrap.geometryValue(
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
updateShadowGeometry(rect);
|
||||
updateControlsGeometry(rect);
|
||||
}, _inner->lifetime());
|
||||
}
|
||||
|
||||
void GroupCallBar::setupRightButton(not_null<RoundButton*> button) {
|
||||
button->setFullRadius(true);
|
||||
rpl::combine(
|
||||
_inner->widthValue(),
|
||||
button->widthValue()
|
||||
) | rpl::on_next([=](int outerWidth, int buttonWidth) {
|
||||
// Skip shadow of the bar above.
|
||||
const auto top = (st::historyReplyHeight
|
||||
- st::lineWidth
|
||||
- button->height()) / 2 + st::lineWidth;
|
||||
const auto narrow = (outerWidth < st::columnMinimalWidthLeft / 2);
|
||||
if (narrow) {
|
||||
button->moveToLeft(
|
||||
(outerWidth - buttonWidth) / 2,
|
||||
top,
|
||||
outerWidth);
|
||||
} else {
|
||||
button->moveToRight(top, top, outerWidth);
|
||||
}
|
||||
}, button->lifetime());
|
||||
|
||||
button->clicks() | rpl::start_to_stream(_joinClicks, button->lifetime());
|
||||
}
|
||||
|
||||
void GroupCallBar::paint(Painter &p) {
|
||||
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
|
||||
|
||||
const auto narrow = (_inner->width() < st::columnMinimalWidthLeft / 2);
|
||||
if (!narrow) {
|
||||
paintTitleAndStatus(p);
|
||||
paintUserpics(p);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::paintTitleAndStatus(Painter &p) {
|
||||
const auto left = st::topBarArrowPadding.right();
|
||||
const auto titleTop = st::msgReplyPadding.top();
|
||||
const auto textTop = titleTop + st::msgServiceNameFont->height;
|
||||
const auto width = _inner->width();
|
||||
const auto &font = st::defaultMessageBar.title.font;
|
||||
p.setPen(st::defaultMessageBar.textFg);
|
||||
p.setFont(font);
|
||||
|
||||
const auto available = (_join ? _join->x() : _open->x()) - left;
|
||||
const auto titleWidth = font->width(_content.title);
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
titleTop,
|
||||
width,
|
||||
(!_content.scheduleDate
|
||||
? (_content.livestream
|
||||
? tr::lng_group_call_title_channel
|
||||
: tr::lng_group_call_title)(tr::now)
|
||||
: _content.title.isEmpty()
|
||||
? (_content.livestream
|
||||
? tr::lng_group_call_scheduled_title_channel
|
||||
: tr::lng_group_call_scheduled_title)(tr::now)
|
||||
: (titleWidth > available)
|
||||
? font->elided(_content.title, available)
|
||||
: _content.title));
|
||||
p.setPen(st::historyStatusFg);
|
||||
p.setFont(st::defaultMessageBar.text.font);
|
||||
const auto when = [&] {
|
||||
if (!_content.scheduleDate) {
|
||||
return QString();
|
||||
}
|
||||
const auto parsed = base::unixtime::parse(_content.scheduleDate);
|
||||
const auto date = parsed.date();
|
||||
const auto time = QLocale().toString(
|
||||
parsed.time(),
|
||||
QLocale::ShortFormat);
|
||||
const auto today = QDate::currentDate();
|
||||
if (date == today) {
|
||||
return tr::lng_group_call_starts_today(tr::now, lt_time, time);
|
||||
} else if (date == today.addDays(1)) {
|
||||
return tr::lng_group_call_starts_tomorrow(
|
||||
tr::now,
|
||||
lt_time,
|
||||
time);
|
||||
} else {
|
||||
return tr::lng_group_call_starts_date(
|
||||
tr::now,
|
||||
lt_date,
|
||||
langDayOfMonthFull(date),
|
||||
lt_time,
|
||||
time);
|
||||
}
|
||||
}();
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
textTop,
|
||||
width,
|
||||
(_content.scheduleDate
|
||||
? (_content.title.isEmpty()
|
||||
? tr::lng_group_call_starts_short
|
||||
: _content.livestream
|
||||
? tr::lng_group_call_starts_channel
|
||||
: tr::lng_group_call_starts)(tr::now, lt_when, when)
|
||||
: _content.count > 0
|
||||
? tr::lng_group_call_members(
|
||||
tr::now,
|
||||
lt_count_decimal,
|
||||
_content.count)
|
||||
: tr::lng_group_call_no_members(tr::now)));
|
||||
}
|
||||
|
||||
void GroupCallBar::paintUserpics(Painter &p) {
|
||||
const auto size = st::historyGroupCallUserpics.size;
|
||||
// Skip shadow of the bar above.
|
||||
const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2
|
||||
+ st::lineWidth;
|
||||
_userpics->paint(p, _inner->width() / 2, top, size);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
|
||||
const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
|
||||
if (_shadow->isHidden() != hidden) {
|
||||
_shadow->setVisible(!hidden);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
|
||||
_shadowGeometryPostprocess = std::move(postprocess);
|
||||
updateShadowGeometry(_wrap.geometry());
|
||||
}
|
||||
|
||||
void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
|
||||
const auto regular = QRect(
|
||||
wrapGeometry.x(),
|
||||
wrapGeometry.y() + wrapGeometry.height(),
|
||||
wrapGeometry.width(),
|
||||
st::lineWidth);
|
||||
_shadow->setGeometry(_shadowGeometryPostprocess
|
||||
? _shadowGeometryPostprocess(regular)
|
||||
: regular);
|
||||
}
|
||||
|
||||
void GroupCallBar::updateUserpics() {
|
||||
const auto widget = _wrap.entity();
|
||||
const auto middle = widget->width() / 2;
|
||||
const auto width = _userpics->maxWidth();
|
||||
widget->update(
|
||||
(middle - width / 2),
|
||||
0,
|
||||
width,
|
||||
widget->height());
|
||||
}
|
||||
|
||||
void GroupCallBar::show() {
|
||||
if (!_forceHidden) {
|
||||
return;
|
||||
}
|
||||
_forceHidden = false;
|
||||
if (_shouldBeShown) {
|
||||
_wrap.show(anim::type::instant);
|
||||
_shadow->show();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallBar::hide() {
|
||||
if (_forceHidden) {
|
||||
return;
|
||||
}
|
||||
_forceHidden = true;
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
}
|
||||
|
||||
void GroupCallBar::raise() {
|
||||
_wrap.raise();
|
||||
_shadow->raise();
|
||||
}
|
||||
|
||||
void GroupCallBar::finishAnimating() {
|
||||
_wrap.finishAnimating();
|
||||
}
|
||||
|
||||
void GroupCallBar::move(int x, int y) {
|
||||
_wrap.move(x, y);
|
||||
}
|
||||
|
||||
void GroupCallBar::resizeToWidth(int width) {
|
||||
_wrap.entity()->resizeToWidth(width);
|
||||
_inner->resizeToWidth(width);
|
||||
}
|
||||
|
||||
int GroupCallBar::height() const {
|
||||
return !_forceHidden
|
||||
? _wrap.height()
|
||||
: _shouldBeShown
|
||||
? st::historyReplyHeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
rpl::producer<int> GroupCallBar::heightValue() const {
|
||||
return _wrap.heightValue();
|
||||
}
|
||||
|
||||
rpl::producer<> GroupCallBar::barClicks() const {
|
||||
return _barClicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<> GroupCallBar::joinClicks() const {
|
||||
using namespace rpl::mappers;
|
||||
return _joinClicks.events()
|
||||
| rpl::filter(_1 == Qt::LeftButton)
|
||||
| rpl::to_empty;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
120
Telegram/SourceFiles/ui/chat/group_call_bar.h
Normal file
120
Telegram/SourceFiles/ui/chat/group_call_bar.h
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
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/wrap/slide_wrap.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PlainShadow;
|
||||
class RoundButton;
|
||||
struct GroupCallUser;
|
||||
class GroupCallUserpics;
|
||||
|
||||
struct GroupCallBarContent {
|
||||
QString title;
|
||||
TimeId scheduleDate = 0;
|
||||
int count = 0;
|
||||
bool shown = false;
|
||||
bool livestream = false;
|
||||
std::vector<GroupCallUser> users;
|
||||
};
|
||||
|
||||
class GroupCallScheduledLeft final {
|
||||
public:
|
||||
enum class Negative {
|
||||
Show,
|
||||
Ignore,
|
||||
};
|
||||
explicit GroupCallScheduledLeft(TimeId date);
|
||||
|
||||
void setDate(TimeId date);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> text(Negative negative) const;
|
||||
[[nodiscard]] rpl::producer<bool> late() const;
|
||||
|
||||
private:
|
||||
[[nodiscard]] crl::time computePreciseDate() const;
|
||||
void restart();
|
||||
void update();
|
||||
|
||||
rpl::variable<QString> _text;
|
||||
rpl::variable<QString> _textNonNegative;
|
||||
rpl::variable<bool> _late;
|
||||
TimeId _date = 0;
|
||||
crl::time _datePrecise = 0;
|
||||
base::Timer _timer;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
class GroupCallBar final {
|
||||
public:
|
||||
GroupCallBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<GroupCallBarContent> content,
|
||||
rpl::producer<bool> &&hideBlobs);
|
||||
~GroupCallBar();
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void raise();
|
||||
void finishAnimating();
|
||||
|
||||
void setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);
|
||||
|
||||
void move(int x, int y);
|
||||
void resizeToWidth(int width);
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
[[nodiscard]] rpl::producer<> barClicks() const;
|
||||
[[nodiscard]] rpl::producer<> joinClicks() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _wrap.lifetime();
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallUser;
|
||||
|
||||
void refreshOpenBrush();
|
||||
void refreshScheduledProcess();
|
||||
void updateShadowGeometry(QRect wrapGeometry);
|
||||
void updateControlsGeometry(QRect wrapGeometry);
|
||||
void updateUserpics();
|
||||
void setupInner();
|
||||
void setupRightButton(not_null<RoundButton*> button);
|
||||
void paint(Painter &p);
|
||||
void paintTitleAndStatus(Painter &p);
|
||||
void paintUserpics(Painter &p);
|
||||
|
||||
SlideWrap<> _wrap;
|
||||
not_null<RpWidget*> _inner;
|
||||
std::unique_ptr<RoundButton> _join;
|
||||
std::unique_ptr<RoundButton> _open;
|
||||
rpl::event_stream<Qt::MouseButton> _joinClicks;
|
||||
QBrush _openBrushOverride;
|
||||
int _openBrushForWidth = 0;
|
||||
std::unique_ptr<PlainShadow> _shadow;
|
||||
rpl::event_stream<> _barClicks;
|
||||
Fn<QRect(QRect)> _shadowGeometryPostprocess;
|
||||
bool _shouldBeShown = false;
|
||||
bool _forceHidden = false;
|
||||
|
||||
GroupCallBarContent _content;
|
||||
std::unique_ptr<GroupCallScheduledLeft> _scheduledProcess;
|
||||
std::unique_ptr<GroupCallUserpics> _userpics;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
428
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
428
Telegram/SourceFiles/ui/chat/group_call_userpics.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
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 "ui/chat/group_call_userpics.h"
|
||||
|
||||
#include "ui/paint/blobs.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "base/random.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kDuration = 160;
|
||||
constexpr auto kMaxUserpics = 4;
|
||||
constexpr auto kWideScale = 5;
|
||||
|
||||
constexpr auto kBlobsEnterDuration = crl::time(250);
|
||||
constexpr auto kLevelDuration = 100. + 500. * 0.23;
|
||||
constexpr auto kBlobScale = 0.605;
|
||||
constexpr auto kMinorBlobFactor = 0.9f;
|
||||
constexpr auto kUserpicMinScale = 0.8;
|
||||
constexpr auto kMaxLevel = 1.;
|
||||
constexpr auto kSendRandomLevelInterval = crl::time(100);
|
||||
|
||||
auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
|
||||
return { {
|
||||
{
|
||||
.segmentsCount = 6,
|
||||
.minScale = kBlobScale * kMinorBlobFactor,
|
||||
.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
|
||||
.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
|
||||
.speedScale = 1.,
|
||||
.alpha = .5,
|
||||
},
|
||||
{
|
||||
.segmentsCount = 8,
|
||||
.minScale = kBlobScale,
|
||||
.minRadius = (float)st::historyGroupCallBlobMinRadius,
|
||||
.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
|
||||
.speedScale = 1.,
|
||||
.alpha = .2,
|
||||
},
|
||||
} };
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct GroupCallUserpics::BlobsAnimation {
|
||||
BlobsAnimation(
|
||||
std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
|
||||
float levelDuration,
|
||||
float maxLevel)
|
||||
: blobs(std::move(blobDatas), levelDuration, maxLevel) {
|
||||
}
|
||||
|
||||
Ui::Paint::Blobs blobs;
|
||||
crl::time lastTime = 0;
|
||||
crl::time lastSpeakingUpdateTime = 0;
|
||||
float64 enter = 0.;
|
||||
};
|
||||
|
||||
struct GroupCallUserpics::Userpic {
|
||||
User data;
|
||||
std::pair<uint64, uint64> cacheKey;
|
||||
crl::time speakingStarted = 0;
|
||||
QImage cache;
|
||||
Animations::Simple leftAnimation;
|
||||
Animations::Simple shownAnimation;
|
||||
std::unique_ptr<BlobsAnimation> blobsAnimation;
|
||||
int left = 0;
|
||||
bool positionInited = false;
|
||||
bool topMost = false;
|
||||
bool hiding = false;
|
||||
bool cacheMasked = false;
|
||||
};
|
||||
|
||||
GroupCallUserpics::GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint)
|
||||
: _st(st)
|
||||
, _randomSpeakingTimer([=] { sendRandomLevels(); })
|
||||
, _repaint(std::move(repaint)) {
|
||||
const auto limit = kMaxUserpics;
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
_maxWidth = 2 * single + (limit - 1) * (single - shift);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.cache = QImage();
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_speakingAnimation.init([=](crl::time now) {
|
||||
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
||||
&& (now - last >= kBlobsEnterDuration)) {
|
||||
_speakingAnimation.stop();
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.updateLevel(now - blobs->lastTime);
|
||||
blobs->lastTime = now;
|
||||
}
|
||||
}
|
||||
if (const auto onstack = _repaint) {
|
||||
onstack();
|
||||
}
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
PowerSaving::OnValue(PowerSaving::kCalls),
|
||||
std::move(hideBlobs)
|
||||
) | rpl::on_next([=](bool disabled, bool deactivated) {
|
||||
const auto hide = disabled || deactivated;
|
||||
|
||||
if (!(hide && _speakingAnimationHideLastTime)) {
|
||||
_speakingAnimationHideLastTime = hide ? crl::now() : 0;
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
blobs->blobs.setLevel(0.);
|
||||
}
|
||||
}
|
||||
if (!hide && !_speakingAnimation.animating()) {
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
_skipLevelUpdate = hide;
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
GroupCallUserpics::~GroupCallUserpics() = default;
|
||||
|
||||
void GroupCallUserpics::paint(QPainter &p, int x, int y, int size) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto &minScale = kUserpicMinScale;
|
||||
for (auto &userpic : ranges::views::reverse(_list)) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown == 0.) {
|
||||
continue;
|
||||
}
|
||||
validateCache(userpic);
|
||||
p.setOpacity(shown);
|
||||
const auto left = x + userpic.leftAnimation.value(userpic.left);
|
||||
const auto blobs = userpic.blobsAnimation.get();
|
||||
const auto shownScale = 0.5 + shown / 2.;
|
||||
const auto scale = shownScale * (!blobs
|
||||
? 1.
|
||||
: (minScale
|
||||
+ (1. - minScale) * (_speakingAnimationHideLastTime
|
||||
? (1. - blobs->blobs.currentLevel())
|
||||
: blobs->blobs.currentLevel())));
|
||||
if (blobs) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto shift = QPointF(left + size / 2., y + size / 2.);
|
||||
p.translate(shift);
|
||||
blobs->blobs.paint(p, st::windowActiveTextFg);
|
||||
p.translate(-shift);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (std::abs(scale - 1.) < 0.001) {
|
||||
const auto skip = ((kWideScale - 1) / 2) * size * factor;
|
||||
p.drawImage(
|
||||
QRect(left, y, size, size),
|
||||
userpic.cache,
|
||||
QRect(skip, skip, size * factor, size * factor));
|
||||
} else {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
auto target = QRect(
|
||||
left + (1 - kWideScale) / 2 * size,
|
||||
y + (1 - kWideScale) / 2 * size,
|
||||
kWideScale * size,
|
||||
kWideScale * size);
|
||||
auto shrink = anim::interpolate(
|
||||
(1 - kWideScale) / 2 * size,
|
||||
0,
|
||||
scale);
|
||||
auto margins = QMargins(shrink, shrink, shrink, shrink);
|
||||
p.drawImage(target.marginsAdded(margins), userpic.cache);
|
||||
}
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
|
||||
const auto hidden = [](const Userpic &userpic) {
|
||||
return userpic.hiding && !userpic.shownAnimation.animating();
|
||||
};
|
||||
_list.erase(ranges::remove_if(_list, hidden), end(_list));
|
||||
}
|
||||
|
||||
int GroupCallUserpics::maxWidth() const {
|
||||
return _maxWidth;
|
||||
}
|
||||
|
||||
rpl::producer<int> GroupCallUserpics::widthValue() const {
|
||||
return _width.value();
|
||||
}
|
||||
|
||||
bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
|
||||
if (userpic.cache.isNull()) {
|
||||
return true;
|
||||
} else if (userpic.hiding) {
|
||||
return false;
|
||||
} else if (userpic.cacheKey != userpic.data.userpicKey) {
|
||||
return true;
|
||||
}
|
||||
const auto shouldBeMasked = !userpic.topMost;
|
||||
if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
|
||||
return true;
|
||||
}
|
||||
return !userpic.leftAnimation.animating();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
|
||||
if (userpic.blobsAnimation) {
|
||||
return;
|
||||
}
|
||||
userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
|
||||
Blobs() | ranges::to_vector,
|
||||
kLevelDuration,
|
||||
kMaxLevel);
|
||||
userpic.blobsAnimation->lastTime = crl::now();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::sendRandomLevels() {
|
||||
if (_skipLevelUpdate) {
|
||||
return;
|
||||
}
|
||||
for (auto &userpic : _list) {
|
||||
if (const auto blobs = userpic.blobsAnimation.get()) {
|
||||
const auto value = 30 + base::RandomIndex(70);
|
||||
blobs->blobs.setLevel(float64(value) / 100.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::validateCache(Userpic &userpic) {
|
||||
if (!needCacheRefresh(userpic)) {
|
||||
return;
|
||||
}
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
const auto size = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
const auto full = QSize(size, size) * kWideScale * factor;
|
||||
if (userpic.cache.isNull()) {
|
||||
userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
||||
userpic.cache.setDevicePixelRatio(factor);
|
||||
}
|
||||
userpic.cacheKey = userpic.data.userpicKey;
|
||||
userpic.cacheMasked = !userpic.topMost;
|
||||
userpic.cache.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&userpic.cache);
|
||||
const auto skip = (kWideScale - 1) / 2 * size;
|
||||
p.drawImage(QRect(skip, skip, size, size), userpic.data.userpic);
|
||||
|
||||
if (userpic.cacheMasked) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(_st.stroke);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
p.setPen(pen);
|
||||
p.drawEllipse(skip - size + shift, skip, size, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible) {
|
||||
const auto idFromUserpic = [](const Userpic &userpic) {
|
||||
return userpic.data.id;
|
||||
};
|
||||
|
||||
// Use "topMost" as "willBeHidden" flag.
|
||||
for (auto &userpic : _list) {
|
||||
userpic.topMost = true;
|
||||
}
|
||||
for (const auto &user : users) {
|
||||
const auto i = ranges::find(_list, user.id, idFromUserpic);
|
||||
if (i == end(_list)) {
|
||||
_list.push_back(Userpic{ user });
|
||||
toggle(_list.back(), true);
|
||||
continue;
|
||||
}
|
||||
i->topMost = false;
|
||||
|
||||
if (i->hiding) {
|
||||
toggle(*i, true);
|
||||
}
|
||||
i->data = user;
|
||||
|
||||
// Put this one after the last we are not hiding.
|
||||
for (auto j = end(_list) - 1; j != i; --j) {
|
||||
if (!j->topMost) {
|
||||
ranges::rotate(i, i + 1, j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the ones that "willBeHidden" (currently having "topMost" flag).
|
||||
// Set correct real values of "topMost" flag.
|
||||
const auto userpicsBegin = begin(_list);
|
||||
const auto userpicsEnd = end(_list);
|
||||
auto markedTopMost = userpicsEnd;
|
||||
auto hasBlobs = false;
|
||||
for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
|
||||
auto &userpic = *i;
|
||||
if (userpic.data.speaking) {
|
||||
ensureBlobsAnimation(userpic);
|
||||
hasBlobs = true;
|
||||
} else {
|
||||
userpic.blobsAnimation = nullptr;
|
||||
}
|
||||
if (userpic.topMost) {
|
||||
toggle(userpic, false);
|
||||
userpic.topMost = false;
|
||||
} else if (markedTopMost == userpicsEnd) {
|
||||
userpic.topMost = true;
|
||||
markedTopMost = i;
|
||||
}
|
||||
}
|
||||
if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
|
||||
// Bring the topMost userpic to the very beginning, above all hiding.
|
||||
std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
|
||||
}
|
||||
updatePositions();
|
||||
|
||||
if (!hasBlobs) {
|
||||
_randomSpeakingTimer.cancel();
|
||||
_speakingAnimation.stop();
|
||||
} else if (!_randomSpeakingTimer.isActive()) {
|
||||
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
||||
_speakingAnimation.start();
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
recountAndRepaint();
|
||||
} else {
|
||||
finishAnimating();
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::finishAnimating() {
|
||||
for (auto &userpic : _list) {
|
||||
userpic.shownAnimation.stop();
|
||||
userpic.leftAnimation.stop();
|
||||
}
|
||||
recountAndRepaint();
|
||||
}
|
||||
|
||||
void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
|
||||
if (userpic.hiding == !shown && !userpic.shownAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
userpic.hiding = !shown;
|
||||
userpic.shownAnimation.start(
|
||||
[=] { recountAndRepaint(); },
|
||||
shown ? 0. : 1.,
|
||||
shown ? 1. : 0.,
|
||||
kDuration);
|
||||
}
|
||||
|
||||
void GroupCallUserpics::updatePositions() {
|
||||
const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
|
||||
if (!shownCount) {
|
||||
return;
|
||||
}
|
||||
const auto single = _st.size;
|
||||
const auto shift = _st.shift;
|
||||
// + 1 * single for the blobs.
|
||||
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
||||
auto left = (_st.align & Qt::AlignLeft)
|
||||
? 0
|
||||
: (_st.align & Qt::AlignHCenter)
|
||||
? (-fullWidth / 2)
|
||||
: -fullWidth;
|
||||
for (auto &userpic : _list) {
|
||||
if (userpic.hiding) {
|
||||
continue;
|
||||
}
|
||||
if (!userpic.positionInited) {
|
||||
userpic.positionInited = true;
|
||||
userpic.left = left;
|
||||
} else if (userpic.left != left) {
|
||||
userpic.leftAnimation.start(
|
||||
_repaint,
|
||||
userpic.left,
|
||||
left,
|
||||
kDuration);
|
||||
userpic.left = left;
|
||||
}
|
||||
left += (single - shift);
|
||||
}
|
||||
}
|
||||
|
||||
void GroupCallUserpics::recountAndRepaint() {
|
||||
auto width = 0;
|
||||
auto maxShown = 0.;
|
||||
for (const auto &userpic : _list) {
|
||||
const auto shown = userpic.shownAnimation.value(
|
||||
userpic.hiding ? 0. : 1.);
|
||||
if (shown > maxShown) {
|
||||
maxShown = shown;
|
||||
}
|
||||
width += anim::interpolate(0, _st.size - _st.shift, shown);
|
||||
}
|
||||
_width = width + anim::interpolate(0, _st.shift, maxShown);
|
||||
if (_repaint) {
|
||||
_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
74
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
74
Telegram/SourceFiles/ui/chat/group_call_userpics.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 style {
|
||||
struct GroupCallUserpics;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct GroupCallUser {
|
||||
QImage userpic;
|
||||
std::pair<uint64, uint64> userpicKey = {};
|
||||
uint64 id = 0;
|
||||
bool speaking = false;
|
||||
};
|
||||
|
||||
class GroupCallUserpics final {
|
||||
public:
|
||||
GroupCallUserpics(
|
||||
const style::GroupCallUserpics &st,
|
||||
rpl::producer<bool> &&hideBlobs,
|
||||
Fn<void()> repaint);
|
||||
~GroupCallUserpics();
|
||||
|
||||
void update(
|
||||
const std::vector<GroupCallUser> &users,
|
||||
bool visible);
|
||||
void paint(QPainter &p, int x, int y, int size);
|
||||
void finishAnimating();
|
||||
|
||||
[[nodiscard]] int maxWidth() const;
|
||||
[[nodiscard]] rpl::producer<int> widthValue() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
private:
|
||||
using User = GroupCallUser;
|
||||
struct BlobsAnimation;
|
||||
struct Userpic;
|
||||
|
||||
void toggle(Userpic &userpic, bool shown);
|
||||
void updatePositions();
|
||||
void validateCache(Userpic &userpic);
|
||||
[[nodiscard]] bool needCacheRefresh(Userpic &userpic);
|
||||
void ensureBlobsAnimation(Userpic &userpic);
|
||||
void sendRandomLevels();
|
||||
void recountAndRepaint();
|
||||
|
||||
const style::GroupCallUserpics &_st;
|
||||
std::vector<Userpic> _list;
|
||||
base::Timer _randomSpeakingTimer;
|
||||
Fn<void()> _repaint;
|
||||
Ui::Animations::Basic _speakingAnimation;
|
||||
int _maxWidth = 0;
|
||||
bool _skipLevelUpdate = false;
|
||||
crl::time _speakingAnimationHideLastTime = 0;
|
||||
|
||||
rpl::variable<int> _width;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
669
Telegram/SourceFiles/ui/chat/message_bar.cpp
Normal file
669
Telegram/SourceFiles/ui/chat/message_bar.cpp
Normal file
@@ -0,0 +1,669 @@
|
||||
/*
|
||||
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 "ui/chat/message_bar.h"
|
||||
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int SameFirstPartLength(const QString &a, const QString &b) {
|
||||
const auto &[i, j] = ranges::mismatch(a, b);
|
||||
return (i - a.begin());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool MuchDifferent(
|
||||
int same,
|
||||
const QString &a,
|
||||
const QString &b) {
|
||||
return (same * 2 < a.size()) || (same * 2 < b.size());
|
||||
}
|
||||
|
||||
[[nodiscard]] bool MuchDifferent(const QString &a, const QString &b) {
|
||||
return MuchDifferent(SameFirstPartLength(a, b), a, b);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ComplexTitleAnimation(
|
||||
int same,
|
||||
const QString &a,
|
||||
const QString &b) {
|
||||
return !MuchDifferent(same, a, b)
|
||||
&& (same != a.size() || same != b.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MessageBar::MessageBar(
|
||||
not_null<QWidget*> parent,
|
||||
const style::MessageBar &st,
|
||||
Fn<bool()> customEmojiPaused)
|
||||
: _st(st)
|
||||
, _widget(parent)
|
||||
, _customEmojiPaused(std::move(customEmojiPaused)) {
|
||||
setup();
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
_topBarGradient = _bottomBarGradient = QPixmap();
|
||||
}, _widget.lifetime());
|
||||
}
|
||||
|
||||
void MessageBar::customEmojiRepaint() {
|
||||
if (_customEmojiRepaintScheduled) {
|
||||
return;
|
||||
}
|
||||
_customEmojiRepaintScheduled = true;
|
||||
_widget.update();
|
||||
}
|
||||
|
||||
void MessageBar::setup() {
|
||||
_widget.resize(0, st::historyReplyHeight);
|
||||
_widget.paintRequest(
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
auto p = Painter(&_widget);
|
||||
p.setInactive(_customEmojiPaused());
|
||||
_customEmojiRepaintScheduled = false;
|
||||
paint(p);
|
||||
}, _widget.lifetime());
|
||||
}
|
||||
|
||||
void MessageBar::set(MessageBarContent &&content) {
|
||||
_contentLifetime.destroy();
|
||||
tweenTo(std::move(content));
|
||||
}
|
||||
|
||||
void MessageBar::set(rpl::producer<MessageBarContent> content) {
|
||||
_contentLifetime.destroy();
|
||||
std::move(
|
||||
content
|
||||
) | rpl::on_next([=](MessageBarContent &&content) {
|
||||
tweenTo(std::move(content));
|
||||
}, _contentLifetime);
|
||||
}
|
||||
|
||||
MessageBar::BodyAnimation MessageBar::DetectBodyAnimationType(
|
||||
Animation *currentAnimation,
|
||||
const MessageBarContent ¤tContent,
|
||||
const MessageBarContent &nextContent) {
|
||||
const auto now = currentAnimation
|
||||
? currentAnimation->bodyAnimation
|
||||
: BodyAnimation::None;
|
||||
const auto somethingChanged = (currentContent.text != nextContent.text)
|
||||
|| (currentContent.title != nextContent.title)
|
||||
|| (currentContent.index != nextContent.index)
|
||||
|| (currentContent.count != nextContent.count);
|
||||
return (now == BodyAnimation::Full
|
||||
|| MuchDifferent(currentContent.title, nextContent.title)
|
||||
|| (currentContent.title.isEmpty() && somethingChanged))
|
||||
? BodyAnimation::Full
|
||||
: (now == BodyAnimation::Text || somethingChanged)
|
||||
? BodyAnimation::Text
|
||||
: BodyAnimation::None;
|
||||
}
|
||||
|
||||
void MessageBar::tweenTo(MessageBarContent &&content) {
|
||||
Expects(content.count > 0);
|
||||
Expects(content.index >= 0 && content.index < content.count);
|
||||
|
||||
_widget.update();
|
||||
if (!_st.duration || anim::Disabled() || _widget.size().isEmpty()) {
|
||||
updateFromContent(std::move(content));
|
||||
return;
|
||||
}
|
||||
const auto hasImageChanged = (_content.preview.isNull()
|
||||
!= content.preview.isNull());
|
||||
const auto bodyChanged = (_content.index != content.index
|
||||
|| _content.count != content.count
|
||||
|| _content.title != content.title
|
||||
|| _content.text != content.text
|
||||
|| _content.preview.constBits() != content.preview.constBits());
|
||||
const auto barCountChanged = (_content.count != content.count);
|
||||
const auto barFrom = _content.index;
|
||||
const auto barTo = content.index;
|
||||
auto animation = Animation();
|
||||
animation.bodyAnimation = DetectBodyAnimationType(
|
||||
_animation.get(),
|
||||
_content,
|
||||
content);
|
||||
animation.movingTo = (content.index > _content.index)
|
||||
? RectPart::Top
|
||||
: (content.index < _content.index)
|
||||
? RectPart::Bottom
|
||||
: RectPart::None;
|
||||
animation.imageFrom = grabImagePart();
|
||||
animation.spoilerFrom = std::move(_spoiler);
|
||||
animation.bodyOrTextFrom = grabBodyOrTextPart(animation.bodyAnimation);
|
||||
const auto sameLength = SameFirstPartLength(
|
||||
_content.title,
|
||||
content.title);
|
||||
if (animation.bodyAnimation == BodyAnimation::Text
|
||||
&& ComplexTitleAnimation(sameLength, _content.title, content.title)) {
|
||||
animation.titleSame = grabTitleBase(sameLength);
|
||||
animation.titleFrom = grabTitlePart(sameLength);
|
||||
}
|
||||
auto was = std::move(_animation);
|
||||
updateFromContent(std::move(content));
|
||||
animation.imageTo = grabImagePart();
|
||||
animation.bodyOrTextTo = grabBodyOrTextPart(animation.bodyAnimation);
|
||||
if (!animation.titleSame.isNull()) {
|
||||
animation.titleTo = grabTitlePart(sameLength);
|
||||
}
|
||||
if (was) {
|
||||
_animation = std::move(was);
|
||||
std::swap(*_animation, animation);
|
||||
_animation->imageShown = std::move(animation.imageShown);
|
||||
_animation->barScroll = std::move(animation.barScroll);
|
||||
_animation->barTop = std::move(animation.barTop);
|
||||
} else {
|
||||
_animation = std::make_unique<Animation>(std::move(animation));
|
||||
}
|
||||
|
||||
if (hasImageChanged) {
|
||||
_animation->imageShown.start(
|
||||
[=] { _widget.update(); },
|
||||
_image.isNull() ? 1. : 0.,
|
||||
_image.isNull() ? 0. : 1.,
|
||||
_st.duration);
|
||||
}
|
||||
if (bodyChanged) {
|
||||
_animation->bodyMoved.start(
|
||||
[=] { _widget.update(); },
|
||||
0.,
|
||||
1.,
|
||||
_st.duration);
|
||||
}
|
||||
if (barCountChanged) {
|
||||
_animation->barScroll.stop();
|
||||
_animation->barTop.stop();
|
||||
} else if (barFrom != barTo) {
|
||||
const auto wasState = countBarState(barFrom);
|
||||
const auto nowState = countBarState(barTo);
|
||||
_animation->barScroll.start(
|
||||
[=] { _widget.update(); },
|
||||
wasState.scroll,
|
||||
nowState.scroll,
|
||||
_st.duration);
|
||||
_animation->barTop.start(
|
||||
[] {},
|
||||
wasState.offset,
|
||||
nowState.offset,
|
||||
_st.duration);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageBar::updateFromContent(MessageBarContent &&content) {
|
||||
_content = std::move(content);
|
||||
_title.setText(_st.title, _content.title);
|
||||
_text.setMarkedText(
|
||||
_st.text,
|
||||
_content.text,
|
||||
Ui::DialogTextOptions(),
|
||||
_content.context);
|
||||
_image = prepareImage(_content.preview);
|
||||
if (!_content.spoilerRepaint) {
|
||||
_spoiler = nullptr;
|
||||
} else if (!_spoiler) {
|
||||
_spoiler = std::make_unique<SpoilerAnimation>(
|
||||
_content.spoilerRepaint);
|
||||
}
|
||||
}
|
||||
|
||||
QRect MessageBar::imageRect() const {
|
||||
const auto left = st::msgReplyBarSkip + st::msgReplyBarSkip;
|
||||
const auto top = (st::historyReplyHeight - st::historyReplyPreview) / 2;
|
||||
const auto size = st::historyReplyPreview;
|
||||
return QRect(left, top, size, size);
|
||||
}
|
||||
|
||||
QRect MessageBar::titleRangeRect(int from, int till) const {
|
||||
auto result = bodyRect();
|
||||
result.setHeight(st::msgServiceNameFont->height);
|
||||
const auto left = from
|
||||
? st::msgServiceNameFont->width(_content.title.mid(0, from))
|
||||
: 0;
|
||||
const auto right = (till <= _content.title.size())
|
||||
? st::msgServiceNameFont->width(_content.title.mid(0, till))
|
||||
: result.width();
|
||||
result.setLeft(result.left() + left);
|
||||
result.setWidth(right - left);
|
||||
return result;
|
||||
}
|
||||
|
||||
QRect MessageBar::bodyRect(bool withImage) const {
|
||||
const auto innerLeft = st::msgReplyBarSkip + st::msgReplyBarSkip;
|
||||
const auto imageSkip = st::historyReplyPreview + st::msgReplyBarSkip;
|
||||
const auto left = innerLeft + (withImage ? imageSkip : 0);
|
||||
const auto top = st::msgReplyPadding.top();
|
||||
const auto width = _widget.width() - left - st::msgReplyPadding.right();
|
||||
const auto height = (st::historyReplyHeight - 2 * top);
|
||||
return QRect(left, top, width, height) - _content.margins;
|
||||
}
|
||||
|
||||
QRect MessageBar::bodyRect() const {
|
||||
return bodyRect(!_image.isNull());
|
||||
}
|
||||
|
||||
QRect MessageBar::textRect() const {
|
||||
auto result = bodyRect();
|
||||
result.setTop(result.top() + st::msgServiceNameFont->height);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto MessageBar::makeGrabGuard() {
|
||||
auto imageShown = _animation
|
||||
? std::move(_animation->imageShown)
|
||||
: Ui::Animations::Simple();
|
||||
auto spoiler = std::move(_spoiler);
|
||||
auto fromSpoiler = _animation
|
||||
? std::move(_animation->spoilerFrom)
|
||||
: nullptr;
|
||||
return gsl::finally([
|
||||
&,
|
||||
shown = std::move(imageShown),
|
||||
spoiler = std::move(spoiler),
|
||||
fromSpoiler = std::move(fromSpoiler)
|
||||
]() mutable {
|
||||
if (_animation) {
|
||||
_animation->imageShown = std::move(shown);
|
||||
_animation->spoilerFrom = std::move(fromSpoiler);
|
||||
}
|
||||
_spoiler = std::move(spoiler);
|
||||
});
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabBodyOrTextPart(BodyAnimation type) {
|
||||
return (type == BodyAnimation::Full)
|
||||
? grabBodyPart()
|
||||
: (type == BodyAnimation::Text)
|
||||
? grabTextPart()
|
||||
: QPixmap();
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabTitleBase(int till) {
|
||||
return grabTitleRange(0, till);
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabTitlePart(int from) {
|
||||
Expects(from <= _content.title.size());
|
||||
|
||||
return grabTitleRange(from, _content.title.size());
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabTitleRange(int from, int till) {
|
||||
const auto guard = makeGrabGuard();
|
||||
return GrabWidget(widget(), titleRangeRect(from, till));
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabBodyPart() {
|
||||
const auto guard = makeGrabGuard();
|
||||
return GrabWidget(widget(), bodyRect());
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabTextPart() {
|
||||
const auto guard = makeGrabGuard();
|
||||
return GrabWidget(widget(), textRect());
|
||||
}
|
||||
|
||||
QPixmap MessageBar::grabImagePart() {
|
||||
if (!_animation) {
|
||||
return _image;
|
||||
}
|
||||
const auto guard = makeGrabGuard();
|
||||
return (_animation->bodyMoved.animating()
|
||||
&& !_animation->imageFrom.isNull()
|
||||
&& !_animation->imageTo.isNull())
|
||||
? GrabWidget(widget(), imageRect())
|
||||
: _animation->imageFrom;
|
||||
}
|
||||
|
||||
void MessageBar::finishAnimating() {
|
||||
if (_animation) {
|
||||
_animation = nullptr;
|
||||
_widget.update();
|
||||
}
|
||||
}
|
||||
|
||||
QPixmap MessageBar::prepareImage(const QImage &preview) {
|
||||
return QPixmap::fromImage(preview, Qt::ColorOnly);
|
||||
}
|
||||
|
||||
void MessageBar::paint(Painter &p) {
|
||||
const auto progress = _animation ? _animation->bodyMoved.value(1.) : 1.;
|
||||
const auto imageFinal = _image.isNull() ? 0. : 1.;
|
||||
const auto imageShown = _animation
|
||||
? _animation->imageShown.value(imageFinal)
|
||||
: imageFinal;
|
||||
if (progress == 1. && imageShown == imageFinal && _animation) {
|
||||
_animation = nullptr;
|
||||
}
|
||||
const auto body = [&] {
|
||||
if (!_animation || !_animation->imageShown.animating()) {
|
||||
return bodyRect();
|
||||
}
|
||||
const auto noImage = bodyRect(false);
|
||||
const auto withImage = bodyRect(true);
|
||||
return QRect(
|
||||
anim::interpolate(noImage.x(), withImage.x(), imageShown),
|
||||
noImage.y(),
|
||||
anim::interpolate(noImage.width(), withImage.width(), imageShown),
|
||||
noImage.height());
|
||||
}();
|
||||
const auto text = textRect();
|
||||
const auto image = imageRect();
|
||||
const auto width = _widget.width();
|
||||
const auto noShift = !_animation
|
||||
|| (_animation->movingTo == RectPart::None);
|
||||
const auto shiftFull = st::msgReplyBarSkip;
|
||||
const auto shiftTo = noShift
|
||||
? 0
|
||||
: (_animation->movingTo == RectPart::Top)
|
||||
? anim::interpolate(shiftFull, 0, progress)
|
||||
: anim::interpolate(-shiftFull, 0, progress);
|
||||
const auto shiftFrom = noShift
|
||||
? 0
|
||||
: (_animation->movingTo == RectPart::Top)
|
||||
? (shiftTo - shiftFull)
|
||||
: (shiftTo + shiftFull);
|
||||
const auto now = crl::now();
|
||||
const auto paused = p.inactive();
|
||||
const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);
|
||||
|
||||
paintLeftBar(p);
|
||||
|
||||
if (!_animation) {
|
||||
if (!_image.isNull()) {
|
||||
paintImageWithSpoiler(
|
||||
p,
|
||||
image,
|
||||
_image,
|
||||
_spoiler.get(),
|
||||
now,
|
||||
pausedSpoiler);
|
||||
}
|
||||
} else if (!_animation->imageTo.isNull()
|
||||
|| (!_animation->imageFrom.isNull()
|
||||
&& _animation->imageShown.animating())) {
|
||||
const auto rect = [&] {
|
||||
if (!_animation->imageShown.animating()) {
|
||||
return image;
|
||||
}
|
||||
const auto size = anim::interpolate(0, image.width(), imageShown);
|
||||
return QRect(
|
||||
image.x(),
|
||||
image.y() + (image.height() - size) / 2,
|
||||
size,
|
||||
size);
|
||||
}();
|
||||
if (_animation->bodyMoved.animating()) {
|
||||
p.setOpacity(1. - progress);
|
||||
paintImageWithSpoiler(
|
||||
p,
|
||||
rect.translated(0, shiftFrom),
|
||||
_animation->imageFrom,
|
||||
_animation->spoilerFrom.get(),
|
||||
now,
|
||||
pausedSpoiler);
|
||||
p.setOpacity(progress);
|
||||
paintImageWithSpoiler(
|
||||
p,
|
||||
rect.translated(0, shiftTo),
|
||||
_animation->imageTo,
|
||||
_spoiler.get(),
|
||||
now,
|
||||
pausedSpoiler);
|
||||
p.setOpacity(1.);
|
||||
} else {
|
||||
paintImageWithSpoiler(
|
||||
p,
|
||||
rect,
|
||||
_image,
|
||||
_spoiler.get(),
|
||||
now,
|
||||
pausedSpoiler);
|
||||
}
|
||||
}
|
||||
if (!_animation || _animation->bodyAnimation == BodyAnimation::None) {
|
||||
if (_title.isEmpty()) {
|
||||
// "Loading..." state.
|
||||
p.setPen(st::historyComposeAreaFgService);
|
||||
_text.draw(p, {
|
||||
.position = {
|
||||
body.x(),
|
||||
body.y() + (body.height() - st::normalFont->height) / 2,
|
||||
},
|
||||
.outerWidth = width,
|
||||
.availableWidth = body.width(),
|
||||
.elisionLines = 1,
|
||||
});
|
||||
} else {
|
||||
p.setPen(_st.textFg);
|
||||
_text.draw(p, {
|
||||
.position = { body.x(), text.y() },
|
||||
.outerWidth = width,
|
||||
.availableWidth = body.width(),
|
||||
.palette = &_st.textPalette,
|
||||
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||
.now = now,
|
||||
.pausedEmoji = paused || On(PowerSaving::kEmojiChat),
|
||||
.pausedSpoiler = pausedSpoiler,
|
||||
.elisionLines = 1,
|
||||
});
|
||||
}
|
||||
} else if (_animation->bodyAnimation == BodyAnimation::Text) {
|
||||
p.setOpacity(1. - progress);
|
||||
p.drawPixmap(
|
||||
body.x(),
|
||||
text.y() + shiftFrom,
|
||||
_animation->bodyOrTextFrom);
|
||||
p.setOpacity(progress);
|
||||
p.drawPixmap(body.x(), text.y() + shiftTo, _animation->bodyOrTextTo);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
if (!_animation || _animation->bodyAnimation != BodyAnimation::Full) {
|
||||
if (_animation && !_animation->titleSame.isNull()) {
|
||||
const auto factor = style::DevicePixelRatio();
|
||||
p.drawPixmap(body.x(), body.y(), _animation->titleSame);
|
||||
p.setOpacity(1. - progress);
|
||||
p.drawPixmap(
|
||||
body.x() + (_animation->titleSame.width() / factor),
|
||||
body.y() + shiftFrom,
|
||||
_animation->titleFrom);
|
||||
p.setOpacity(progress);
|
||||
p.drawPixmap(
|
||||
body.x() + (_animation->titleSame.width() / factor),
|
||||
body.y() + shiftTo,
|
||||
_animation->titleTo);
|
||||
p.setOpacity(1.);
|
||||
} else {
|
||||
p.setPen(_st.titleFg);
|
||||
_title.drawLeftElided(p, body.x(), body.y(), body.width(), width);
|
||||
}
|
||||
} else {
|
||||
p.setOpacity(1. - progress);
|
||||
p.drawPixmap(
|
||||
body.x(),
|
||||
body.y() + shiftFrom,
|
||||
_animation->bodyOrTextFrom);
|
||||
p.setOpacity(progress);
|
||||
p.drawPixmap(body.x(), body.y() + shiftTo, _animation->bodyOrTextTo);
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
}
|
||||
|
||||
auto MessageBar::countBarState(int index) const -> BarState {
|
||||
Expects(index >= 0 && index < _content.count);
|
||||
|
||||
auto result = BarState();
|
||||
const auto line = st::msgReplyBarSize.width();
|
||||
const auto height = st::msgReplyBarSize.height();
|
||||
const auto count = _content.count;
|
||||
const auto shownCount = std::min(count, 4);
|
||||
const auto dividers = (shownCount - 1) * line;
|
||||
const auto size = float64(st::msgReplyBarSize.height() - dividers)
|
||||
/ shownCount;
|
||||
const auto fullHeight = count * size + (count - 1) * line;
|
||||
const auto topByIndex = [&](int index) {
|
||||
return index * (size + line);
|
||||
};
|
||||
result.scroll = (count < 5 || index < 2)
|
||||
? 0
|
||||
: (index >= count - 2)
|
||||
? (fullHeight - height)
|
||||
: (topByIndex(index) - (height - size) / 2);
|
||||
result.size = size;
|
||||
result.skip = line;
|
||||
result.offset = topByIndex(index);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto MessageBar::countBarState() const -> BarState {
|
||||
return countBarState(_content.index);
|
||||
}
|
||||
|
||||
void MessageBar::ensureGradientsCreated(int size) {
|
||||
if (!_topBarGradient.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto rows = size * style::DevicePixelRatio() - 2;
|
||||
auto bottomMask = QImage(
|
||||
QSize(1, size) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
const auto step = ((1ULL << 24) - 1) / rows;
|
||||
const auto limit = step * rows;
|
||||
auto bits = bottomMask.bits();
|
||||
const auto perLine = bottomMask.bytesPerLine();
|
||||
for (auto counter = uint32(0); counter != limit; counter += step) {
|
||||
const auto value = (counter >> 16);
|
||||
memset(bits, int(value), perLine);
|
||||
bits += perLine;
|
||||
}
|
||||
memset(bits, 255, perLine * 2);
|
||||
auto bottom = style::colorizeImage(bottomMask, st::historyPinnedBg);
|
||||
bottom.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
auto top = bottom.mirrored();
|
||||
_bottomBarGradient = Images::PixmapFast(std::move(bottom));
|
||||
_topBarGradient = Images::PixmapFast(std::move(top));
|
||||
}
|
||||
|
||||
void MessageBar::paintImageWithSpoiler(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const QPixmap &image,
|
||||
SpoilerAnimation *spoiler,
|
||||
crl::time now,
|
||||
bool paused) const {
|
||||
p.drawPixmap(rect, image);
|
||||
if (spoiler) {
|
||||
const auto frame = DefaultImageSpoiler().frame(
|
||||
spoiler->index(now, paused));
|
||||
FillSpoilerRect(p, rect, frame);
|
||||
}
|
||||
}
|
||||
|
||||
void MessageBar::paintLeftBar(Painter &p) {
|
||||
const auto state = countBarState();
|
||||
const auto gradientSize = int(std::ceil(state.size * 2.5));
|
||||
if (_content.count > 4) {
|
||||
ensureGradientsCreated(gradientSize);
|
||||
}
|
||||
|
||||
const auto scroll = _animation
|
||||
? _animation->barScroll.value(state.scroll)
|
||||
: state.scroll;
|
||||
const auto offset = _animation
|
||||
? _animation->barTop.value(state.offset)
|
||||
: state.offset;
|
||||
const auto line = st::msgReplyBarSize.width();
|
||||
const auto height = st::msgReplyBarSize.height();
|
||||
const auto activeFrom = offset - scroll;
|
||||
const auto activeTill = activeFrom + state.size;
|
||||
const auto single = state.size + state.skip;
|
||||
|
||||
const auto barSkip = st::msgReplyPadding.top() + st::msgReplyBarPos.y();
|
||||
const auto fullHeight = barSkip + height + barSkip;
|
||||
const auto bar = QRect(
|
||||
st::msgReplyBarSkip + st::msgReplyBarPos.x(),
|
||||
barSkip,
|
||||
line,
|
||||
state.size);
|
||||
const auto paintFromScroll = std::max(scroll - barSkip, 0.);
|
||||
const auto paintFrom = int(std::floor(paintFromScroll / single));
|
||||
const auto paintTillScroll = (scroll + height + barSkip);
|
||||
const auto paintTill = std::min(
|
||||
int(std::floor(paintTillScroll / single)) + 1,
|
||||
_content.count);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
const auto activeBrush = QBrush(st::msgInReplyBarColor);
|
||||
const auto inactiveBrush = QBrush(QColor(
|
||||
st::msgInReplyBarColor->c.red(),
|
||||
st::msgInReplyBarColor->c.green(),
|
||||
st::msgInReplyBarColor->c.blue(),
|
||||
st::msgInReplyBarColor->c.alpha() / 3));
|
||||
const auto radius = line / 2.;
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setClipRect(bar.x(), 0, bar.width(), fullHeight);
|
||||
for (auto i = paintFrom; i != paintTill; ++i) {
|
||||
const auto top = i * single - scroll;
|
||||
const auto bottom = top + state.size;
|
||||
const auto active = (top == activeFrom);
|
||||
p.setBrush(active ? activeBrush : inactiveBrush);
|
||||
p.drawRoundedRect(bar.translated(0, top), radius, radius);
|
||||
if (active
|
||||
|| bottom - line <= activeFrom
|
||||
|| top + line >= activeTill) {
|
||||
continue;
|
||||
}
|
||||
const auto partFrom = std::max(top, activeFrom);
|
||||
const auto partTill = std::min(bottom, activeTill);
|
||||
p.setBrush(activeBrush);
|
||||
p.drawRoundedRect(
|
||||
QRect(bar.x(), bar.y() + partFrom, line, partTill - partFrom),
|
||||
radius,
|
||||
radius);
|
||||
}
|
||||
p.setClipping(false);
|
||||
if (_content.count > 4) {
|
||||
const auto firstScroll = countBarState(2).scroll;
|
||||
const auto gradientTop = (scroll >= firstScroll)
|
||||
? 0
|
||||
: anim::interpolate(-gradientSize, 0, scroll / firstScroll);
|
||||
const auto lastScroll = countBarState(_content.count - 3).scroll;
|
||||
const auto largestScroll = countBarState(_content.count - 1).scroll;
|
||||
const auto gradientBottom = (scroll <= lastScroll)
|
||||
? fullHeight
|
||||
: anim::interpolate(
|
||||
fullHeight,
|
||||
fullHeight + gradientSize,
|
||||
(scroll - lastScroll) / (largestScroll - lastScroll));
|
||||
if (gradientTop > -gradientSize) {
|
||||
p.drawPixmap(
|
||||
QRect(bar.x(), gradientTop, bar.width(), gradientSize),
|
||||
_topBarGradient);
|
||||
}
|
||||
if (gradientBottom < fullHeight + gradientSize) {
|
||||
p.drawPixmap(
|
||||
QRect(
|
||||
bar.x(),
|
||||
gradientBottom - gradientSize,
|
||||
bar.width(),
|
||||
gradientSize),
|
||||
_bottomBarGradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
132
Telegram/SourceFiles/ui/chat/message_bar.h
Normal file
132
Telegram/SourceFiles/ui/chat/message_bar.h
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
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/effects/animations.h"
|
||||
#include "ui/rect_part.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace style {
|
||||
struct MessageBar;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class SpoilerAnimation;
|
||||
|
||||
struct MessageBarContent {
|
||||
int index = 0;
|
||||
int count = 1;
|
||||
QString title;
|
||||
TextWithEntities text;
|
||||
Text::MarkedContext context;
|
||||
QImage preview;
|
||||
Fn<void()> spoilerRepaint;
|
||||
style::margins margins;
|
||||
};
|
||||
|
||||
class MessageBar final {
|
||||
public:
|
||||
MessageBar(
|
||||
not_null<QWidget*> parent,
|
||||
const style::MessageBar &st,
|
||||
Fn<bool()> customEmojiPaused);
|
||||
|
||||
void set(MessageBarContent &&content);
|
||||
void set(rpl::producer<MessageBarContent> content);
|
||||
|
||||
[[nodiscard]] not_null<RpWidget*> widget() {
|
||||
return &_widget;
|
||||
}
|
||||
|
||||
void customEmojiRepaint();
|
||||
void finishAnimating();
|
||||
|
||||
private:
|
||||
enum class BodyAnimation : char {
|
||||
Full,
|
||||
Text,
|
||||
None,
|
||||
};
|
||||
struct Animation {
|
||||
Animations::Simple bodyMoved;
|
||||
Animations::Simple imageShown;
|
||||
Animations::Simple barScroll;
|
||||
Animations::Simple barTop;
|
||||
QPixmap bodyOrTextFrom;
|
||||
QPixmap bodyOrTextTo;
|
||||
QPixmap titleSame;
|
||||
QPixmap titleFrom;
|
||||
QPixmap titleTo;
|
||||
QPixmap imageFrom;
|
||||
QPixmap imageTo;
|
||||
std::unique_ptr<SpoilerAnimation> spoilerFrom;
|
||||
BodyAnimation bodyAnimation = BodyAnimation::None;
|
||||
RectPart movingTo = RectPart::None;
|
||||
};
|
||||
struct BarState {
|
||||
float64 scroll = 0.;
|
||||
float64 size = 0.;
|
||||
float64 skip = 0.;
|
||||
float64 offset = 0.;
|
||||
};
|
||||
void setup();
|
||||
void paint(Painter &p);
|
||||
void paintLeftBar(Painter &p);
|
||||
void tweenTo(MessageBarContent &&content);
|
||||
void updateFromContent(MessageBarContent &&content);
|
||||
[[nodiscard]] QPixmap prepareImage(const QImage &preview);
|
||||
|
||||
[[nodiscard]] QRect imageRect() const;
|
||||
[[nodiscard]] QRect titleRangeRect(int from, int till) const;
|
||||
[[nodiscard]] QRect bodyRect(bool withImage) const;
|
||||
[[nodiscard]] QRect bodyRect() const;
|
||||
[[nodiscard]] QRect textRect() const;
|
||||
|
||||
auto makeGrabGuard();
|
||||
[[nodiscard]] QPixmap grabBodyOrTextPart(BodyAnimation type);
|
||||
[[nodiscard]] QPixmap grabTitleBase(int till);
|
||||
[[nodiscard]] QPixmap grabTitlePart(int from);
|
||||
[[nodiscard]] QPixmap grabTitleRange(int from, int till);
|
||||
[[nodiscard]] QPixmap grabImagePart();
|
||||
[[nodiscard]] QPixmap grabBodyPart();
|
||||
[[nodiscard]] QPixmap grabTextPart();
|
||||
|
||||
[[nodiscard]] BarState countBarState(int index) const;
|
||||
[[nodiscard]] BarState countBarState() const;
|
||||
void ensureGradientsCreated(int size);
|
||||
|
||||
void paintImageWithSpoiler(
|
||||
QPainter &p,
|
||||
QRect rect,
|
||||
const QPixmap &image,
|
||||
SpoilerAnimation *spoiler,
|
||||
crl::time now,
|
||||
bool paused) const;
|
||||
|
||||
[[nodiscard]] static BodyAnimation DetectBodyAnimationType(
|
||||
Animation *currentAnimation,
|
||||
const MessageBarContent ¤tContent,
|
||||
const MessageBarContent &nextContent);
|
||||
|
||||
const style::MessageBar &_st;
|
||||
RpWidget _widget;
|
||||
Fn<bool()> _customEmojiPaused;
|
||||
MessageBarContent _content;
|
||||
rpl::lifetime _contentLifetime;
|
||||
Text::String _title, _text;
|
||||
QPixmap _image, _topBarGradient, _bottomBarGradient;
|
||||
std::unique_ptr<Animation> _animation;
|
||||
std::unique_ptr<SpoilerAnimation> _spoiler;
|
||||
bool _customEmojiRepaintScheduled = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
461
Telegram/SourceFiles/ui/chat/message_bubble.cpp
Normal file
461
Telegram/SourceFiles/ui/chat/message_bubble.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
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 "ui/chat/message_bubble.h"
|
||||
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "styles/style_chat.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
using Corner = BubbleCornerRounding;
|
||||
|
||||
template <
|
||||
typename FillBg, // fillBg(QRect rect)
|
||||
typename FillSh, // fillSh(QRect rect)
|
||||
typename FillCorner, // fillCorner(int x, int y, int index, Corner size)
|
||||
typename PaintTail> // paintTail(QPoint bottomPosition) -> tailWidth
|
||||
void PaintBubbleGeneric(
|
||||
const SimpleBubble &args,
|
||||
FillBg &&fillBg,
|
||||
FillSh &&fillSh,
|
||||
FillCorner &&fillCorner,
|
||||
PaintTail &&paintTail) {
|
||||
using namespace Images;
|
||||
|
||||
const auto topLeft = args.rounding.topLeft;
|
||||
const auto topRight = args.rounding.topRight;
|
||||
const auto bottomWithTailLeft = args.rounding.bottomLeft;
|
||||
const auto bottomWithTailRight = args.rounding.bottomRight;
|
||||
if (topLeft == Corner::None
|
||||
&& topRight == Corner::None
|
||||
&& bottomWithTailLeft == Corner::None
|
||||
&& bottomWithTailRight == Corner::None) {
|
||||
fillBg(args.geometry);
|
||||
return;
|
||||
}
|
||||
const auto bottomLeft = (bottomWithTailLeft == Corner::Tail)
|
||||
? Corner::None
|
||||
: bottomWithTailLeft;
|
||||
const auto bottomRight = (bottomWithTailRight == Corner::Tail)
|
||||
? Corner::None
|
||||
: bottomWithTailRight;
|
||||
const auto rect = args.geometry;
|
||||
const auto small = BubbleRadiusSmall();
|
||||
const auto large = BubbleRadiusLarge();
|
||||
const auto cornerSize = [&](Corner corner) {
|
||||
return (corner == Corner::Large)
|
||||
? large
|
||||
: (corner == Corner::Small)
|
||||
? small
|
||||
: 0;
|
||||
};
|
||||
const auto verticalSkip = [&](Corner left, Corner right) {
|
||||
return std::max(cornerSize(left), cornerSize(right));
|
||||
};
|
||||
const auto top = verticalSkip(topLeft, topRight);
|
||||
const auto bottom = verticalSkip(bottomLeft, bottomRight);
|
||||
if (top) {
|
||||
const auto left = cornerSize(topLeft);
|
||||
const auto right = cornerSize(topRight);
|
||||
if (left) {
|
||||
fillCorner(rect.left(), rect.top(), kTopLeft, topLeft);
|
||||
if (const auto add = top - left) {
|
||||
fillBg({ rect.left(), rect.top() + left, left, add });
|
||||
}
|
||||
}
|
||||
if (const auto fill = rect.width() - left - right; fill > 0) {
|
||||
fillBg({ rect.left() + left, rect.top(), fill, top });
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(
|
||||
rect.left() + rect.width() - right,
|
||||
rect.top(),
|
||||
kTopRight,
|
||||
topRight);
|
||||
if (const auto add = top - right) {
|
||||
fillBg({
|
||||
rect.left() + rect.width() - right,
|
||||
rect.top() + right,
|
||||
right,
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (const auto fill = rect.height() - top - bottom; fill > 0) {
|
||||
fillBg({ rect.left(), rect.top() + top, rect.width(), fill });
|
||||
}
|
||||
if (bottom) {
|
||||
const auto left = cornerSize(bottomLeft);
|
||||
const auto right = cornerSize(bottomRight);
|
||||
if (left) {
|
||||
fillCorner(
|
||||
rect.left(),
|
||||
rect.top() + rect.height() - left,
|
||||
kBottomLeft,
|
||||
bottomLeft);
|
||||
if (const auto add = bottom - left) {
|
||||
fillBg({
|
||||
rect.left(),
|
||||
rect.top() + rect.height() - bottom,
|
||||
left,
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (const auto fill = rect.width() - left - right; fill > 0) {
|
||||
fillBg({
|
||||
rect.left() + left,
|
||||
rect.top() + rect.height() - bottom,
|
||||
fill,
|
||||
bottom,
|
||||
});
|
||||
}
|
||||
if (right) {
|
||||
fillCorner(
|
||||
rect.left() + rect.width() - right,
|
||||
rect.top() + rect.height() - right,
|
||||
kBottomRight,
|
||||
bottomRight);
|
||||
if (const auto add = bottom - right) {
|
||||
fillBg({
|
||||
rect.left() + rect.width() - right,
|
||||
rect.top() + rect.height() - bottom,
|
||||
right,
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto leftTail = (bottomWithTailLeft == Corner::Tail)
|
||||
? paintTail({ rect.x(), rect.y() + rect.height() })
|
||||
: 0;
|
||||
const auto rightTail = (bottomWithTailRight == Corner::Tail)
|
||||
? paintTail({ rect.x() + rect.width(), rect.y() + rect.height() })
|
||||
: 0;
|
||||
if (!args.shadowed) {
|
||||
return;
|
||||
}
|
||||
const auto shLeft = rect.x() + cornerSize(bottomLeft) - leftTail;
|
||||
const auto shWidth = rect.x()
|
||||
+ rect.width()
|
||||
- cornerSize(bottomRight)
|
||||
+ rightTail
|
||||
- shLeft;
|
||||
if (shWidth > 0) {
|
||||
fillSh({ shLeft, rect.y() + rect.height(), shWidth, st::msgShadow });
|
||||
}
|
||||
}
|
||||
|
||||
void PaintPatternBubble(QPainter &p, const SimpleBubble &args) {
|
||||
const auto wasOpacity = p.opacity();
|
||||
const auto opacity = args.st->msgOutBg()->c.alphaF() * wasOpacity;
|
||||
const auto shadowOpacity = opacity * args.st->msgOutShadow()->c.alphaF();
|
||||
const auto pattern = args.pattern;
|
||||
const auto &tail = (args.rounding.bottomRight == Corner::Tail)
|
||||
? pattern->tailRight
|
||||
: pattern->tailLeft;
|
||||
const auto tailShift = (args.rounding.bottomRight == Corner::Tail
|
||||
? QPoint(0, tail.height())
|
||||
: QPoint(tail.width(), tail.height())) / int(tail.devicePixelRatio());
|
||||
const auto fillBg = [&](const QRect &rect) {
|
||||
const auto fill = rect.intersected(args.patternViewport);
|
||||
if (!fill.isEmpty()) {
|
||||
PaintPatternBubblePart(
|
||||
p,
|
||||
args.patternViewport,
|
||||
pattern->pixmap,
|
||||
fill);
|
||||
}
|
||||
};
|
||||
const auto fillSh = [&](const QRect &rect) {
|
||||
p.setOpacity(shadowOpacity);
|
||||
fillBg(rect);
|
||||
p.setOpacity(opacity);
|
||||
};
|
||||
const auto fillPattern = [&](
|
||||
int x,
|
||||
int y,
|
||||
const QImage &mask,
|
||||
QImage &cache) {
|
||||
PaintPatternBubblePart(
|
||||
p,
|
||||
args.patternViewport,
|
||||
pattern->pixmap,
|
||||
QRect(QPoint(x, y), mask.size() / int(mask.devicePixelRatio())),
|
||||
mask,
|
||||
cache);
|
||||
};
|
||||
const auto fillCorner = [&](int x, int y, int index, Corner size) {
|
||||
auto &corner = (size == Corner::Large)
|
||||
? pattern->cornersLarge[index]
|
||||
: pattern->cornersSmall[index];
|
||||
auto &cache = (size == Corner::Large)
|
||||
? (index < 2
|
||||
? pattern->cornerTopLargeCache
|
||||
: pattern->cornerBottomLargeCache)
|
||||
: (index < 2
|
||||
? pattern->cornerTopSmallCache
|
||||
: pattern->cornerBottomSmallCache);
|
||||
fillPattern(x, y, corner, cache);
|
||||
};
|
||||
const auto paintTail = [&](QPoint bottomPosition) {
|
||||
const auto position = bottomPosition - tailShift;
|
||||
fillPattern(position.x(), position.y(), tail, pattern->tailCache);
|
||||
return tail.width() / int(tail.devicePixelRatio());
|
||||
};
|
||||
|
||||
p.setOpacity(opacity);
|
||||
PaintBubbleGeneric(args, fillBg, fillSh, fillCorner, paintTail);
|
||||
p.setOpacity(wasOpacity);
|
||||
}
|
||||
|
||||
void PaintSolidBubble(QPainter &p, const SimpleBubble &args) {
|
||||
const auto &st = args.st->messageStyle(args.outbg, args.selected);
|
||||
const auto &bg = st.msgBg;
|
||||
const auto sh = (args.rounding.bottomRight == Corner::None)
|
||||
? nullptr
|
||||
: &st.msgShadow;
|
||||
const auto &tail = (args.rounding.bottomRight == Corner::Tail)
|
||||
? st.tailRight
|
||||
: st.tailLeft;
|
||||
const auto tailShift = (args.rounding.bottomRight == Corner::Tail)
|
||||
? QPoint(0, tail.height())
|
||||
: QPoint(tail.width(), tail.height());
|
||||
|
||||
PaintBubbleGeneric(args, [&](const QRect &rect) {
|
||||
p.fillRect(rect, bg);
|
||||
}, [&](const QRect &rect) {
|
||||
p.fillRect(rect, *sh);
|
||||
}, [&](int x, int y, int index, Corner size) {
|
||||
auto &corners = (size == Corner::Large)
|
||||
? st.msgBgCornersLarge
|
||||
: st.msgBgCornersSmall;
|
||||
p.drawPixmap(x, y, corners.p[index]);
|
||||
}, [&](const QPoint &bottomPosition) {
|
||||
tail.paint(p, bottomPosition - tailShift, args.outerWidth);
|
||||
return tail.width();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<BubblePattern> PrepareBubblePattern(
|
||||
not_null<const style::palette*> st) {
|
||||
auto result = std::make_unique<Ui::BubblePattern>();
|
||||
result->cornersSmall = Images::CornersMask(BubbleRadiusSmall());
|
||||
result->cornersLarge = Images::CornersMask(BubbleRadiusLarge());
|
||||
const auto addShadow = [&](QImage &bottomCorner) {
|
||||
auto result = QImage(
|
||||
bottomCorner.width(),
|
||||
(bottomCorner.height()
|
||||
+ st::msgShadow * int(bottomCorner.devicePixelRatio())),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result.fill(Qt::transparent);
|
||||
result.setDevicePixelRatio(bottomCorner.devicePixelRatio());
|
||||
auto p = QPainter(&result);
|
||||
p.setOpacity(st->msgInShadow()->c.alphaF());
|
||||
p.drawImage(0, st::msgShadow, bottomCorner);
|
||||
p.setOpacity(1.);
|
||||
p.drawImage(0, 0, bottomCorner);
|
||||
p.end();
|
||||
|
||||
bottomCorner = std::move(result);
|
||||
};
|
||||
addShadow(result->cornersSmall[2]);
|
||||
addShadow(result->cornersSmall[3]);
|
||||
result->cornerTopSmallCache = QImage(
|
||||
result->cornersSmall[0].size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result->cornerTopLargeCache = QImage(
|
||||
result->cornersLarge[0].size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result->cornerBottomSmallCache = QImage(
|
||||
result->cornersSmall[2].size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
result->cornerBottomLargeCache = QImage(
|
||||
result->cornersLarge[2].size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
return result;
|
||||
}
|
||||
|
||||
void FinishBubblePatternOnMain(not_null<BubblePattern*> pattern) {
|
||||
pattern->tailLeft = st::historyBubbleTailOutLeft.instance(Qt::white);
|
||||
pattern->tailRight = st::historyBubbleTailOutRight.instance(Qt::white);
|
||||
pattern->tailCache = QImage(
|
||||
pattern->tailLeft.size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
|
||||
void PaintBubble(QPainter &p, const SimpleBubble &args) {
|
||||
if (!args.selected
|
||||
&& args.outbg
|
||||
&& args.pattern
|
||||
&& !args.patternViewport.isEmpty()
|
||||
&& !args.pattern->pixmap.size().isEmpty()) {
|
||||
PaintPatternBubble(p, args);
|
||||
} else {
|
||||
PaintSolidBubble(p, args);
|
||||
}
|
||||
}
|
||||
|
||||
void PaintBubble(QPainter &p, const ComplexBubble &args) {
|
||||
if (args.selection.empty()) {
|
||||
PaintBubble(p, args.simple);
|
||||
return;
|
||||
}
|
||||
const auto rect = args.simple.geometry;
|
||||
const auto left = rect.x();
|
||||
const auto width = rect.width();
|
||||
const auto top = rect.y();
|
||||
const auto bottom = top + rect.height();
|
||||
const auto paintOne = [&](
|
||||
QRect geometry,
|
||||
bool selected,
|
||||
bool fromTop,
|
||||
bool tillBottom) {
|
||||
auto simple = args.simple;
|
||||
simple.geometry = geometry;
|
||||
simple.selected = selected;
|
||||
if (!fromTop) {
|
||||
simple.rounding.topLeft
|
||||
= simple.rounding.topRight
|
||||
= Corner::None;
|
||||
}
|
||||
if (!tillBottom) {
|
||||
simple.rounding.bottomLeft
|
||||
= simple.rounding.bottomRight
|
||||
= Corner::None;
|
||||
simple.shadowed = false;
|
||||
}
|
||||
PaintBubble(p, simple);
|
||||
};
|
||||
auto from = top;
|
||||
for (const auto &selected : args.selection) {
|
||||
if (selected.top > from) {
|
||||
paintOne(
|
||||
QRect(left, from, width, selected.top - from),
|
||||
false,
|
||||
(from <= top),
|
||||
false);
|
||||
}
|
||||
paintOne(
|
||||
QRect(left, selected.top, width, selected.height),
|
||||
true,
|
||||
(selected.top <= top),
|
||||
(selected.top + selected.height >= bottom));
|
||||
from = selected.top + selected.height;
|
||||
}
|
||||
if (from < bottom) {
|
||||
paintOne(
|
||||
QRect(left, from, width, bottom - from),
|
||||
false,
|
||||
false,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target) {
|
||||
const auto factor = pixmap.devicePixelRatio();
|
||||
if (viewport.size() * factor == pixmap.size()) {
|
||||
const auto fill = target.intersected(viewport);
|
||||
if (fill.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
p.drawPixmap(fill, pixmap, QRect(
|
||||
(fill.topLeft() - viewport.topLeft()) * factor,
|
||||
fill.size() * factor));
|
||||
} else {
|
||||
const auto to = viewport;
|
||||
const auto from = QRect(QPoint(), pixmap.size());
|
||||
const auto deviceRect = QRect(
|
||||
QPoint(),
|
||||
QSize(p.device()->width(), p.device()->height()));
|
||||
const auto clip = (target != deviceRect);
|
||||
if (clip) {
|
||||
p.setClipRect(target);
|
||||
}
|
||||
p.drawPixmap(to, pixmap, from);
|
||||
if (clip) {
|
||||
p.setClipping(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target,
|
||||
const QImage &mask,
|
||||
QImage &cache) {
|
||||
Expects(mask.bytesPerLine() == mask.width() * 4);
|
||||
Expects(mask.format() == QImage::Format_ARGB32_Premultiplied);
|
||||
|
||||
if (cache.size() != mask.size()) {
|
||||
cache = QImage(
|
||||
mask.size(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
cache.setDevicePixelRatio(mask.devicePixelRatio());
|
||||
Assert(cache.bytesPerLine() == cache.width() * 4);
|
||||
memcpy(cache.bits(), mask.constBits(), mask.sizeInBytes());
|
||||
|
||||
auto q = QPainter(&cache);
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
PaintPatternBubblePart(
|
||||
q,
|
||||
viewport.translated(-target.topLeft()),
|
||||
pixmap,
|
||||
QRect(QPoint(), cache.size() / int(cache.devicePixelRatio())));
|
||||
q.end();
|
||||
|
||||
p.drawImage(target, cache);
|
||||
}
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target,
|
||||
Fn<void(QPainter&)> paintContent,
|
||||
QImage &cache) {
|
||||
Expects(paintContent != nullptr);
|
||||
|
||||
const auto targetOrigin = target.topLeft();
|
||||
const auto targetSize = target.size();
|
||||
if (cache.size() != targetSize * style::DevicePixelRatio()) {
|
||||
cache = QImage(
|
||||
target.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
cache.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
cache.fill(Qt::transparent);
|
||||
auto q = QPainter(&cache);
|
||||
q.translate(-targetOrigin);
|
||||
paintContent(q);
|
||||
q.translate(targetOrigin);
|
||||
q.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
PaintPatternBubblePart(
|
||||
q,
|
||||
viewport.translated(-targetOrigin),
|
||||
pixmap,
|
||||
QRect(QPoint(), targetSize));
|
||||
q.end();
|
||||
|
||||
p.drawImage(target, cache);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
149
Telegram/SourceFiles/ui/chat/message_bubble.h
Normal file
149
Telegram/SourceFiles/ui/chat/message_bubble.h
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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 Ui {
|
||||
|
||||
class ChatTheme;
|
||||
class ChatStyle;
|
||||
|
||||
enum class BubbleCornerRounding : uchar {
|
||||
None,
|
||||
Tail,
|
||||
Small,
|
||||
Large,
|
||||
};
|
||||
|
||||
struct BubbleRounding {
|
||||
BubbleCornerRounding topLeft : 2 = BubbleCornerRounding();
|
||||
BubbleCornerRounding topRight : 2 = BubbleCornerRounding();
|
||||
BubbleCornerRounding bottomLeft : 2 = BubbleCornerRounding();
|
||||
BubbleCornerRounding bottomRight : 2 = BubbleCornerRounding();
|
||||
|
||||
struct ConstProxy {
|
||||
constexpr ConstProxy(
|
||||
not_null<const BubbleRounding*> that,
|
||||
int index) noexcept
|
||||
: that(that)
|
||||
, index(index) {
|
||||
Expects(index >= 0 && index < 4);
|
||||
}
|
||||
|
||||
constexpr operator BubbleCornerRounding() const noexcept {
|
||||
switch (index) {
|
||||
case 0: return that->topLeft;
|
||||
case 1: return that->topRight;
|
||||
case 2: return that->bottomLeft;
|
||||
case 3: return that->bottomRight;
|
||||
}
|
||||
Unexpected("Index value in BubbleRounding::ConstProxy.");
|
||||
}
|
||||
|
||||
not_null<const BubbleRounding*> that;
|
||||
int index = 0;
|
||||
};
|
||||
struct Proxy : ConstProxy {
|
||||
constexpr Proxy(not_null<BubbleRounding*> that, int index) noexcept
|
||||
: ConstProxy(that, index) {
|
||||
}
|
||||
|
||||
using ConstProxy::operator BubbleCornerRounding;
|
||||
|
||||
constexpr Proxy &operator=(BubbleCornerRounding value) noexcept {
|
||||
const auto nonconst = const_cast<BubbleRounding*>(that.get());
|
||||
switch (index) {
|
||||
case 0: nonconst->topLeft = value; break;
|
||||
case 1: nonconst->topRight = value; break;
|
||||
case 2: nonconst->bottomLeft = value; break;
|
||||
case 3: nonconst->bottomRight = value; break;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
[[nodiscard]] constexpr ConstProxy operator[](int index) const {
|
||||
return { this, index };
|
||||
}
|
||||
[[nodiscard]] constexpr Proxy operator[](int index) {
|
||||
return { this, index };
|
||||
}
|
||||
|
||||
[[nodiscard]] uchar key() const {
|
||||
static_assert(sizeof(*this) == sizeof(uchar));
|
||||
return uchar(*reinterpret_cast<const std::byte*>(this));
|
||||
}
|
||||
|
||||
inline friend constexpr auto operator<=>(
|
||||
BubbleRounding,
|
||||
BubbleRounding) = default;
|
||||
};
|
||||
|
||||
struct BubbleSelectionInterval {
|
||||
int top = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct BubblePattern {
|
||||
QPixmap pixmap;
|
||||
std::array<QImage, 4> cornersSmall;
|
||||
std::array<QImage, 4> cornersLarge;
|
||||
QImage tailLeft;
|
||||
QImage tailRight;
|
||||
mutable QImage cornerTopSmallCache;
|
||||
mutable QImage cornerTopLargeCache;
|
||||
mutable QImage cornerBottomSmallCache;
|
||||
mutable QImage cornerBottomLargeCache;
|
||||
mutable QImage tailCache;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::unique_ptr<BubblePattern> PrepareBubblePattern(
|
||||
not_null<const style::palette*> st);
|
||||
void FinishBubblePatternOnMain(not_null<BubblePattern*> pattern);
|
||||
|
||||
struct SimpleBubble {
|
||||
not_null<const ChatStyle*> st;
|
||||
QRect geometry;
|
||||
const BubblePattern *pattern = nullptr;
|
||||
QRect patternViewport;
|
||||
int outerWidth = 0;
|
||||
bool selected = false;
|
||||
bool shadowed = true;
|
||||
bool outbg = false;
|
||||
BubbleRounding rounding;
|
||||
};
|
||||
|
||||
struct ComplexBubble {
|
||||
SimpleBubble simple;
|
||||
const std::vector<BubbleSelectionInterval> &selection;
|
||||
};
|
||||
|
||||
void PaintBubble(QPainter &p, const SimpleBubble &args);
|
||||
void PaintBubble(QPainter &p, const ComplexBubble &args);
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target);
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target,
|
||||
const QImage &mask,
|
||||
QImage &cache);
|
||||
|
||||
void PaintPatternBubblePart(
|
||||
QPainter &p,
|
||||
const QRect &viewport,
|
||||
const QPixmap &pixmap,
|
||||
const QRect &target,
|
||||
Fn<void(QPainter&)> paintContent,
|
||||
QImage &cache);
|
||||
|
||||
} // namespace Ui
|
||||
223
Telegram/SourceFiles/ui/chat/more_chats_bar.cpp
Normal file
223
Telegram/SourceFiles/ui/chat/more_chats_bar.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
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 "ui/chat/more_chats_bar.h"
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/text/text_options.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_window.h" // st::columnMinimalWidthLeft
|
||||
|
||||
namespace Ui {
|
||||
|
||||
MoreChatsBar::MoreChatsBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<MoreChatsBarContent> content)
|
||||
: _wrap(parent, object_ptr<RpWidget>(parent))
|
||||
, _inner(_wrap.entity())
|
||||
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
|
||||
, _close(_inner.get(), st::moreChatsBarClose) {
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
|
||||
_wrap.entity()->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
|
||||
}, lifetime());
|
||||
_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
auto copy = std::move(
|
||||
content
|
||||
) | rpl::start_spawning(_wrap.lifetime());
|
||||
|
||||
rpl::duplicate(
|
||||
copy
|
||||
) | rpl::on_next([=](MoreChatsBarContent &&content) {
|
||||
_content = content;
|
||||
if (_content.count > 0) {
|
||||
_text.setText(
|
||||
st::defaultMessageBar.title,
|
||||
tr::lng_filters_bar_you_can(
|
||||
tr::now,
|
||||
lt_count,
|
||||
_content.count),
|
||||
Ui::NameTextOptions());
|
||||
_status.setText(
|
||||
st::defaultMessageBar.text,
|
||||
tr::lng_filters_bar_view(
|
||||
tr::now,
|
||||
lt_count,
|
||||
_content.count),
|
||||
Ui::NameTextOptions());
|
||||
}
|
||||
_inner->update();
|
||||
}, lifetime());
|
||||
|
||||
std::move(
|
||||
copy
|
||||
) | rpl::map([=](const MoreChatsBarContent &content) {
|
||||
return !content.count;
|
||||
}) | rpl::on_next_done([=](bool hidden) {
|
||||
_shouldBeShown = !hidden;
|
||||
if (!_forceHidden) {
|
||||
_wrap.toggle(_shouldBeShown, anim::type::normal);
|
||||
}
|
||||
}, [=] {
|
||||
_forceHidden = true;
|
||||
_wrap.toggle(false, anim::type::normal);
|
||||
}, lifetime());
|
||||
|
||||
setupInner();
|
||||
}
|
||||
|
||||
MoreChatsBar::~MoreChatsBar() = default;
|
||||
|
||||
void MoreChatsBar::setupInner() {
|
||||
_inner->resize(0, st::moreChatsBarHeight);
|
||||
_inner->paintRequest(
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
auto p = Painter(_inner);
|
||||
paint(p);
|
||||
}, _inner->lifetime());
|
||||
|
||||
// Clicks.
|
||||
_inner->setCursor(style::cur_pointer);
|
||||
_inner->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return (event->type() == QEvent::MouseButtonPress);
|
||||
}) | rpl::map([=] {
|
||||
return _inner->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return (event->type() == QEvent::MouseButtonRelease);
|
||||
}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
|
||||
return _inner->rect().contains(
|
||||
static_cast<QMouseEvent*>(event.get())->pos());
|
||||
});
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::to_empty | rpl::start_to_stream(_barClicks, _inner->lifetime());
|
||||
|
||||
_wrap.geometryValue(
|
||||
) | rpl::on_next([=](QRect rect) {
|
||||
updateShadowGeometry(rect);
|
||||
updateControlsGeometry(rect);
|
||||
}, _inner->lifetime());
|
||||
}
|
||||
|
||||
void MoreChatsBar::paint(Painter &p) {
|
||||
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
|
||||
|
||||
const auto width = std::max(
|
||||
_inner->width(),
|
||||
st::columnMinimalWidthLeft);
|
||||
const auto available = width
|
||||
- st::moreChatsBarTextPosition.x()
|
||||
- st::moreChatsBarClose.width;
|
||||
|
||||
p.setPen(st::defaultMessageBar.titleFg);
|
||||
_text.drawElided(
|
||||
p,
|
||||
st::moreChatsBarTextPosition.x(),
|
||||
st::moreChatsBarTextPosition.y(),
|
||||
available);
|
||||
|
||||
p.setPen(st::defaultMessageBar.textFg);
|
||||
_status.drawElided(
|
||||
p,
|
||||
st::moreChatsBarStatusPosition.x(),
|
||||
st::moreChatsBarStatusPosition.y(),
|
||||
available);
|
||||
}
|
||||
|
||||
void MoreChatsBar::updateControlsGeometry(QRect wrapGeometry) {
|
||||
const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
|
||||
if (_shadow->isHidden() != hidden) {
|
||||
_shadow->setVisible(!hidden);
|
||||
}
|
||||
const auto width = std::max(
|
||||
wrapGeometry.width(),
|
||||
st::columnMinimalWidthLeft);
|
||||
_close->move(width - _close->width(), 0);
|
||||
}
|
||||
|
||||
void MoreChatsBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
|
||||
_shadowGeometryPostprocess = std::move(postprocess);
|
||||
updateShadowGeometry(_wrap.geometry());
|
||||
}
|
||||
|
||||
void MoreChatsBar::updateShadowGeometry(QRect wrapGeometry) {
|
||||
const auto regular = QRect(
|
||||
wrapGeometry.x(),
|
||||
wrapGeometry.y() + wrapGeometry.height(),
|
||||
wrapGeometry.width(),
|
||||
st::lineWidth);
|
||||
_shadow->setGeometry(_shadowGeometryPostprocess
|
||||
? _shadowGeometryPostprocess(regular)
|
||||
: regular);
|
||||
}
|
||||
|
||||
void MoreChatsBar::show() {
|
||||
if (!_forceHidden) {
|
||||
return;
|
||||
}
|
||||
_forceHidden = false;
|
||||
if (_shouldBeShown) {
|
||||
_wrap.show(anim::type::instant);
|
||||
_shadow->show();
|
||||
}
|
||||
}
|
||||
|
||||
void MoreChatsBar::hide() {
|
||||
if (_forceHidden) {
|
||||
return;
|
||||
}
|
||||
_forceHidden = true;
|
||||
_wrap.hide(anim::type::instant);
|
||||
_shadow->hide();
|
||||
}
|
||||
|
||||
void MoreChatsBar::raise() {
|
||||
_wrap.raise();
|
||||
_shadow->raise();
|
||||
}
|
||||
|
||||
void MoreChatsBar::finishAnimating() {
|
||||
_wrap.finishAnimating();
|
||||
}
|
||||
|
||||
void MoreChatsBar::move(int x, int y) {
|
||||
_wrap.move(x, y);
|
||||
}
|
||||
|
||||
void MoreChatsBar::resizeToWidth(int width) {
|
||||
_wrap.entity()->resizeToWidth(width);
|
||||
_inner->resizeToWidth(width);
|
||||
}
|
||||
|
||||
int MoreChatsBar::height() const {
|
||||
return !_forceHidden
|
||||
? _wrap.height()
|
||||
: _shouldBeShown
|
||||
? st::moreChatsBarHeight
|
||||
: 0;
|
||||
}
|
||||
|
||||
rpl::producer<int> MoreChatsBar::heightValue() const {
|
||||
return _wrap.heightValue();
|
||||
}
|
||||
|
||||
rpl::producer<> MoreChatsBar::barClicks() const {
|
||||
return _barClicks.events();
|
||||
}
|
||||
|
||||
rpl::producer<> MoreChatsBar::closeClicks() const {
|
||||
return _close->clicks() | rpl::to_empty;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
77
Telegram/SourceFiles/ui/chat/more_chats_bar.h
Normal file
77
Telegram/SourceFiles/ui/chat/more_chats_bar.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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/wrap/slide_wrap.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PlainShadow;
|
||||
class IconButton;
|
||||
|
||||
struct MoreChatsBarContent {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
class MoreChatsBar final {
|
||||
public:
|
||||
MoreChatsBar(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<MoreChatsBarContent> content);
|
||||
~MoreChatsBar();
|
||||
|
||||
[[nodiscard]] not_null<RpWidget*> wrap() {
|
||||
return &_wrap;
|
||||
}
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void raise();
|
||||
void finishAnimating();
|
||||
|
||||
void setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess);
|
||||
|
||||
void move(int x, int y);
|
||||
void resizeToWidth(int width);
|
||||
[[nodiscard]] int height() const;
|
||||
[[nodiscard]] rpl::producer<int> heightValue() const;
|
||||
[[nodiscard]] rpl::producer<> barClicks() const;
|
||||
[[nodiscard]] rpl::producer<> closeClicks() const;
|
||||
|
||||
[[nodiscard]] rpl::lifetime &lifetime() {
|
||||
return _wrap.lifetime();
|
||||
}
|
||||
|
||||
private:
|
||||
void updateShadowGeometry(QRect wrapGeometry);
|
||||
void updateControlsGeometry(QRect wrapGeometry);
|
||||
void setupInner();
|
||||
void paint(Painter &p);
|
||||
|
||||
SlideWrap<> _wrap;
|
||||
not_null<RpWidget*> _inner;
|
||||
std::unique_ptr<PlainShadow> _shadow;
|
||||
object_ptr<IconButton> _close;
|
||||
rpl::event_stream<> _barClicks;
|
||||
Fn<QRect(QRect)> _shadowGeometryPostprocess;
|
||||
bool _shouldBeShown = false;
|
||||
bool _forceHidden = false;
|
||||
|
||||
MoreChatsBarContent _content;
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::String _status;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user