641 lines
17 KiB
C++
641 lines
17 KiB
C++
/*
|
|
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 "window/themes/window_themes_cloud_list.h"
|
|
|
|
#include "window/themes/window_themes_embedded.h"
|
|
#include "window/themes/window_theme_editor_box.h"
|
|
#include "window/themes/window_theme.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "window/window_controller.h"
|
|
#include "data/data_cloud_themes.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_session.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "main/main_session.h"
|
|
#include "styles/style_settings.h"
|
|
#include "styles/style_boxes.h"
|
|
#include "styles/style_history.h"
|
|
|
|
namespace Window {
|
|
namespace Theme {
|
|
namespace {
|
|
|
|
constexpr auto kFakeCloudThemeId = 0xFFFFFFFFFFFFFFFAULL;
|
|
constexpr auto kShowPerRow = 4;
|
|
|
|
[[nodiscard]] Data::CloudTheme FakeCloudTheme(const Object &object) {
|
|
auto result = Data::CloudTheme();
|
|
result.id = result.documentId = kFakeCloudThemeId;
|
|
result.slug = object.pathAbsolute;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QImage ColorsBackgroundFromImage(const QImage &source) {
|
|
if (source.isNull()) {
|
|
return source;
|
|
}
|
|
const auto from = source.size();
|
|
const auto to = st::settingsThemePreviewSize * cIntRetinaFactor();
|
|
if (to.width() * from.height() > to.height() * from.width()) {
|
|
const auto small = (from.width() > to.width())
|
|
? source.scaledToWidth(to.width(), Qt::SmoothTransformation)
|
|
: source;
|
|
const auto takew = small.width();
|
|
const auto takeh = std::max(
|
|
takew * to.height() / to.width(),
|
|
1);
|
|
return (small.height() != takeh)
|
|
? small.copy(0, (small.height() - takeh) / 2, takew, takeh)
|
|
: small;
|
|
} else {
|
|
const auto small = (from.height() > to.height())
|
|
? source.scaledToHeight(to.height(), Qt::SmoothTransformation)
|
|
: source;
|
|
const auto takeh = small.height();
|
|
const auto takew = std::max(
|
|
takeh * to.width() / to.height(),
|
|
1);
|
|
return (small.width() != takew)
|
|
? small.copy((small.width() - takew) / 2, 0, takew, takeh)
|
|
: small;
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] std::optional<CloudListColors> ColorsFromTheme(
|
|
const QString &path,
|
|
const QByteArray &theme) {
|
|
const auto content = [&] {
|
|
if (!theme.isEmpty()) {
|
|
return theme;
|
|
}
|
|
auto file = QFile(path);
|
|
return file.open(QIODevice::ReadOnly)
|
|
? file.readAll()
|
|
: QByteArray();
|
|
}();
|
|
if (content.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
auto instance = Instance();
|
|
if (!LoadFromContent(content, &instance)) {
|
|
return std::nullopt;
|
|
}
|
|
auto result = CloudListColors();
|
|
result.background = ColorsBackgroundFromImage(instance.background);
|
|
result.sent = st::msgOutBg[instance.palette]->c;
|
|
result.received = st::msgInBg[instance.palette]->c;
|
|
result.radiobuttonActive
|
|
= result.radiobuttonInactive
|
|
= st::msgServiceFg[instance.palette]->c;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] CloudListColors ColorsFromCurrentTheme() {
|
|
auto result = CloudListColors();
|
|
auto background = Background()->createCurrentImage();
|
|
result.background = ColorsBackgroundFromImage(background);
|
|
result.sent = st::msgOutBg->c;
|
|
result.received = st::msgInBg->c;
|
|
result.radiobuttonActive
|
|
= result.radiobuttonInactive
|
|
= st::msgServiceFg->c;
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CloudListColors ColorsFromScheme(const EmbeddedScheme &scheme) {
|
|
auto result = CloudListColors();
|
|
result.sent = scheme.sent;
|
|
result.received = scheme.received;
|
|
result.radiobuttonActive = scheme.radiobuttonActive;
|
|
result.radiobuttonInactive = scheme.radiobuttonInactive;
|
|
result.background = QImage(
|
|
QSize(1, 1) * cIntRetinaFactor(),
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.background.fill(scheme.background);
|
|
return result;
|
|
}
|
|
|
|
CloudListColors ColorsFromScheme(
|
|
const EmbeddedScheme &scheme,
|
|
const Colorizer &colorizer) {
|
|
if (!colorizer) {
|
|
return ColorsFromScheme(scheme);
|
|
}
|
|
auto copy = scheme;
|
|
Colorize(copy, colorizer);
|
|
return ColorsFromScheme(copy);
|
|
}
|
|
|
|
CloudListCheck::CloudListCheck(const Colors &colors, bool checked)
|
|
: CloudListCheck(checked) {
|
|
setColors(colors);
|
|
}
|
|
|
|
CloudListCheck::CloudListCheck(bool checked)
|
|
: AbstractCheckView(st::defaultRadio.duration, checked, nullptr)
|
|
, _radio(st::defaultRadio, checked, [=] { update(); }) {
|
|
}
|
|
|
|
void CloudListCheck::setColors(const Colors &colors) {
|
|
_colors = colors;
|
|
_radio.setToggledOverride(_colors->radiobuttonActive);
|
|
_radio.setUntoggledOverride(_colors->radiobuttonInactive);
|
|
const auto size = st::settingsThemePreviewSize * cIntRetinaFactor();
|
|
_backgroundFull = (_colors->background.size() == size)
|
|
? _colors->background
|
|
: _colors->background.scaled(
|
|
size,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
_backgroundCacheWidth = -1;
|
|
update();
|
|
}
|
|
|
|
QSize CloudListCheck::getSize() const {
|
|
return st::settingsThemePreviewSize;
|
|
}
|
|
|
|
void CloudListCheck::validateBackgroundCache(int width) {
|
|
if (_backgroundCacheWidth == width || width <= 0) {
|
|
return;
|
|
}
|
|
_backgroundCacheWidth = width;
|
|
const auto imageWidth = width * cIntRetinaFactor();
|
|
_backgroundCache = (width == st::settingsThemePreviewSize.width())
|
|
? _backgroundFull
|
|
: _backgroundFull.copy(
|
|
(_backgroundFull.width() - imageWidth) / 2,
|
|
0,
|
|
imageWidth,
|
|
_backgroundFull.height());
|
|
Images::prepareRound(_backgroundCache, ImageRoundRadius::Large);
|
|
}
|
|
|
|
void CloudListCheck::paint(Painter &p, int left, int top, int outerWidth) {
|
|
if (!_colors) {
|
|
return;
|
|
} else if (_colors->background.isNull()) {
|
|
paintNotSupported(p, left, top, outerWidth);
|
|
} else {
|
|
paintWithColors(p, left, top, outerWidth);
|
|
}
|
|
}
|
|
|
|
void CloudListCheck::paintNotSupported(
|
|
Painter &p,
|
|
int left,
|
|
int top,
|
|
int outerWidth) {
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(st::settingsThemeNotSupportedBg);
|
|
|
|
const auto height = st::settingsThemePreviewSize.height();
|
|
const auto rect = QRect(0, 0, outerWidth, height);
|
|
const auto radius = st::historyMessageRadius;
|
|
p.drawRoundedRect(rect, radius, radius);
|
|
st::settingsThemeNotSupportedIcon.paintInCenter(p, rect);
|
|
}
|
|
|
|
void CloudListCheck::paintWithColors(
|
|
Painter &p,
|
|
int left,
|
|
int top,
|
|
int outerWidth) {
|
|
Expects(_colors.has_value());
|
|
|
|
validateBackgroundCache(outerWidth);
|
|
p.drawImage(
|
|
QRect(0, 0, outerWidth, st::settingsThemePreviewSize.height()),
|
|
_backgroundCache);
|
|
|
|
const auto received = QRect(
|
|
st::settingsThemeBubblePosition,
|
|
st::settingsThemeBubbleSize);
|
|
const auto sent = QRect(
|
|
outerWidth - received.width() - st::settingsThemeBubblePosition.x(),
|
|
received.y() + received.height() + st::settingsThemeBubbleSkip,
|
|
received.width(),
|
|
received.height());
|
|
const auto radius = st::settingsThemeBubbleRadius;
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
p.setPen(Qt::NoPen);
|
|
|
|
p.setBrush(_colors->received);
|
|
p.drawRoundedRect(rtlrect(received, outerWidth), radius, radius);
|
|
p.setBrush(_colors->sent);
|
|
p.drawRoundedRect(rtlrect(sent, outerWidth), radius, radius);
|
|
|
|
const auto skip = st::settingsThemeRadioBottom / 2;
|
|
|
|
const auto radio = _radio.getSize();
|
|
_radio.paint(
|
|
p,
|
|
(outerWidth - radio.width()) / 2,
|
|
getSize().height() - radio.height() - st::settingsThemeRadioBottom,
|
|
outerWidth);
|
|
}
|
|
|
|
QImage CloudListCheck::prepareRippleMask() const {
|
|
return QImage();
|
|
}
|
|
|
|
bool CloudListCheck::checkRippleStartPosition(QPoint position) const {
|
|
return false;
|
|
}
|
|
|
|
void CloudListCheck::checkedChangedHook(anim::type animated) {
|
|
_radio.setChecked(checked(), animated);
|
|
}
|
|
|
|
CloudList::CloudList(
|
|
not_null<QWidget*> parent,
|
|
not_null<Window::SessionController*> window)
|
|
: _window(window)
|
|
, _owned(parent)
|
|
, _outer(_owned.data())
|
|
, _group(std::make_shared<Ui::RadiobuttonGroup>()) {
|
|
setup();
|
|
}
|
|
|
|
void CloudList::showAll() {
|
|
_showAll = true;
|
|
}
|
|
|
|
object_ptr<Ui::RpWidget> CloudList::takeWidget() {
|
|
return std::move(_owned);
|
|
}
|
|
|
|
rpl::producer<bool> CloudList::empty() const {
|
|
using namespace rpl::mappers;
|
|
return _count.value() | rpl::map(_1 == 0);
|
|
}
|
|
|
|
rpl::producer<bool> CloudList::allShown() const {
|
|
using namespace rpl::mappers;
|
|
|
|
return rpl::combine(
|
|
_showAll.value(),
|
|
_count.value(),
|
|
_1 || (_2 <= kShowPerRow));
|
|
}
|
|
|
|
void CloudList::setup() {
|
|
_group->setChangedCallback([=](int selected) {
|
|
const auto &object = Background()->themeObject();
|
|
_group->setValue(groupValueForId(
|
|
object.cloud.id ? object.cloud.id : kFakeCloudThemeId));
|
|
});
|
|
|
|
auto cloudListChanges = rpl::single(
|
|
rpl::empty_value()
|
|
) | rpl::then(
|
|
_window->session().data().cloudThemes().updated()
|
|
);
|
|
|
|
auto themeChanges = rpl::single(BackgroundUpdate(
|
|
BackgroundUpdate::Type::ApplyingTheme,
|
|
Background()->tile()
|
|
)) | rpl::then(base::ObservableViewer(
|
|
*Background()
|
|
)) | rpl::filter([](const BackgroundUpdate &update) {
|
|
return (update.type == BackgroundUpdate::Type::ApplyingTheme);
|
|
});
|
|
|
|
rpl::combine(
|
|
std::move(cloudListChanges),
|
|
std::move(themeChanges),
|
|
allShown()
|
|
) | rpl::map([=] {
|
|
return collectAll();
|
|
}) | rpl::start_with_next([=](std::vector<Data::CloudTheme> &&list) {
|
|
rebuildUsing(std::move(list));
|
|
}, _outer->lifetime());
|
|
|
|
_outer->widthValue(
|
|
) | rpl::start_with_next([=](int width) {
|
|
updateGeometry();
|
|
}, _outer->lifetime());
|
|
}
|
|
|
|
std::vector<Data::CloudTheme> CloudList::collectAll() const {
|
|
const auto &object = Background()->themeObject();
|
|
const auto isDefault = IsEmbeddedTheme(object.pathAbsolute);
|
|
auto result = _window->session().data().cloudThemes().list();
|
|
if (!isDefault) {
|
|
const auto i = ranges::find(
|
|
result,
|
|
object.cloud.id,
|
|
&Data::CloudTheme::id);
|
|
if (i == end(result)) {
|
|
if (object.cloud.id) {
|
|
result.push_back(object.cloud);
|
|
} else {
|
|
result.push_back(FakeCloudTheme(object));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void CloudList::rebuildUsing(std::vector<Data::CloudTheme> &&list) {
|
|
const auto fullCount = int(list.size());
|
|
const auto changed = applyChangesFrom(std::move(list));
|
|
_count = fullCount;
|
|
if (changed) {
|
|
updateGeometry();
|
|
}
|
|
}
|
|
|
|
bool CloudList::applyChangesFrom(std::vector<Data::CloudTheme> &&list) {
|
|
if (list.empty()) {
|
|
if (_elements.empty()) {
|
|
return false;
|
|
}
|
|
_elements.clear();
|
|
return true;
|
|
}
|
|
auto changed = false;
|
|
const auto limit = _showAll.current() ? list.size() : kShowPerRow;
|
|
const auto &object = Background()->themeObject();
|
|
const auto id = object.cloud.id ? object.cloud.id : kFakeCloudThemeId;
|
|
ranges::stable_sort(list, std::less<>(), [&](const Data::CloudTheme &t) {
|
|
if (t.id == id) {
|
|
return 0;
|
|
} else if (t.documentId) {
|
|
return 1;
|
|
} else {
|
|
return 2;
|
|
}
|
|
});
|
|
if (list.front().id == id) {
|
|
const auto j = ranges::find(_elements, id, &Element::id);
|
|
if (j == end(_elements)) {
|
|
insert(0, list.front());
|
|
changed = true;
|
|
} else if (j - begin(_elements) >= limit) {
|
|
std::rotate(
|
|
begin(_elements) + limit - 1,
|
|
j,
|
|
j + 1);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (removeStaleUsing(list)) {
|
|
changed = true;
|
|
}
|
|
if (insertTillLimit(list, limit)) {
|
|
changed = true;
|
|
}
|
|
_group->setValue(groupValueForId(id));
|
|
return changed;
|
|
}
|
|
|
|
bool CloudList::removeStaleUsing(const std::vector<Data::CloudTheme> &list) {
|
|
const auto check = [&](Element &element) {
|
|
const auto j = ranges::find(
|
|
list,
|
|
element.theme.id,
|
|
&Data::CloudTheme::id);
|
|
if (j == end(list)) {
|
|
return true;
|
|
}
|
|
refreshElementUsing(element, *j);
|
|
return false;
|
|
};
|
|
const auto from = ranges::remove_if(_elements, check);
|
|
if (from == end(_elements)) {
|
|
return false;
|
|
}
|
|
_elements.erase(from, end(_elements));
|
|
return true;
|
|
}
|
|
|
|
bool CloudList::insertTillLimit(
|
|
const std::vector<Data::CloudTheme> &list,
|
|
int limit) {
|
|
const auto insertCount = (limit - int(_elements.size()));
|
|
if (insertCount < 0) {
|
|
_elements.erase(end(_elements) + insertCount, end(_elements));
|
|
return true;
|
|
} else if (!insertCount) {
|
|
return false;
|
|
}
|
|
const auto isGood = [](const Data::CloudTheme &theme) {
|
|
return (theme.documentId != 0);
|
|
};
|
|
auto positionForGood = ranges::find_if(_elements, [&](const Element &e) {
|
|
return !isGood(e.theme);
|
|
}) - begin(_elements);
|
|
auto positionForBad = end(_elements) - begin(_elements);
|
|
|
|
auto insertElements = ranges::view::all(
|
|
list
|
|
) | ranges::view::filter([&](const Data::CloudTheme &theme) {
|
|
const auto i = ranges::find(_elements, theme.id, &Element::id);
|
|
return (i == end(_elements));
|
|
}) | ranges::view::take(insertCount);
|
|
|
|
for (const auto &theme : insertElements) {
|
|
auto &index = isGood(theme) ? positionForGood : positionForBad;
|
|
insert(index, theme);
|
|
++index;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void CloudList::insert(int index, const Data::CloudTheme &theme) {
|
|
const auto id = theme.id;
|
|
const auto value = groupValueForId(id);
|
|
const auto checked = _group->hasValue() && (_group->value() == value);
|
|
auto check = std::make_unique<CloudListCheck>(checked);
|
|
const auto raw = check.get();
|
|
auto button = std::make_unique<Ui::Radiobutton>(
|
|
_outer,
|
|
_group,
|
|
value,
|
|
theme.title,
|
|
st::settingsTheme,
|
|
std::move(check));
|
|
button->setCheckAlignment(style::al_top);
|
|
button->setAllowTextLines(2);
|
|
button->setTextBreakEverywhere();
|
|
button->show();
|
|
button->addClickHandler([=] {
|
|
const auto i = ranges::find(_elements, id, &Element::id);
|
|
if (i == end(_elements)
|
|
|| id == kFakeCloudThemeId
|
|
|| i->waiting) {
|
|
return;
|
|
}
|
|
const auto documentId = i->theme.documentId;
|
|
if (!documentId) {
|
|
if (amCreator(i->theme)) {
|
|
_window->window().show(
|
|
Box(CreateForExistingBox, &_window->window(), i->theme));
|
|
}
|
|
return;
|
|
}
|
|
const auto document = _window->session().data().document(documentId);
|
|
DocumentOpenClickHandler::Open(
|
|
Data::FileOrigin(),
|
|
document,
|
|
nullptr);
|
|
});
|
|
auto &element = *_elements.insert(
|
|
begin(_elements) + index,
|
|
Element{ theme, raw, std::move(button) });
|
|
refreshColors(element);
|
|
}
|
|
|
|
void CloudList::refreshElementUsing(
|
|
Element &element,
|
|
const Data::CloudTheme &data) {
|
|
const auto colorsChanged = (element.theme.documentId != data.documentId)
|
|
|| ((element.id() == kFakeCloudThemeId)
|
|
&& (element.theme.slug != data.slug));
|
|
const auto titleChanged = (element.theme.title != data.title);
|
|
element.theme = data;
|
|
if (colorsChanged) {
|
|
setWaiting(element, false);
|
|
refreshColors(element);
|
|
}
|
|
if (titleChanged) {
|
|
element.button->setText(data.title);
|
|
}
|
|
}
|
|
|
|
void CloudList::refreshColors(Element &element) {
|
|
if (element.id() == kFakeCloudThemeId) {
|
|
element.check->setColors(ColorsFromCurrentTheme());
|
|
} else if (const auto documentId = element.theme.documentId) {
|
|
const auto document = _window->session().data().document(documentId);
|
|
document->save(Data::FileOrigin(), QString()); // #TODO themes
|
|
if (document->loaded()) {
|
|
refreshColorsFromDocument(element, document);
|
|
} else {
|
|
setWaiting(element, true);
|
|
subscribeToDownloadFinished();
|
|
}
|
|
} else {
|
|
element.check->setColors(CloudListColors());
|
|
}
|
|
}
|
|
|
|
void CloudList::setWaiting(Element &element, bool waiting) {
|
|
element.waiting = waiting;
|
|
element.button->setPointerCursor(
|
|
!waiting && (element.theme.documentId || amCreator(element.theme)));
|
|
}
|
|
|
|
bool CloudList::amCreator(const Data::CloudTheme &theme) const {
|
|
return (_window->session().userId() == theme.createdBy);
|
|
}
|
|
|
|
void CloudList::refreshColorsFromDocument(
|
|
Element &element,
|
|
not_null<DocumentData*> document) {
|
|
auto colors = ColorsFromTheme(
|
|
document->filepath(),
|
|
document->data());
|
|
if (!colors) {
|
|
return;
|
|
}
|
|
if (colors->background.isNull()) {
|
|
colors->background = ColorsFromCurrentTheme().background;
|
|
}
|
|
element.check->setColors(*colors);
|
|
}
|
|
|
|
void CloudList::subscribeToDownloadFinished() {
|
|
if (_downloadFinishedLifetime) {
|
|
return;
|
|
}
|
|
base::ObservableViewer(
|
|
_window->session().downloaderTaskFinished()
|
|
) | rpl::start_with_next([=] {
|
|
auto &&waiting = _elements | ranges::view::filter(&Element::waiting);
|
|
const auto still = ranges::count_if(waiting, [&](Element &element) {
|
|
const auto id = element.theme.documentId;
|
|
const auto document = _window->session().data().document(id);
|
|
if (!document->loaded()) {
|
|
return true;
|
|
}
|
|
refreshColorsFromDocument(element, document);
|
|
element.waiting = false;
|
|
return false;
|
|
});
|
|
if (!still) {
|
|
_downloadFinishedLifetime.destroy();
|
|
}
|
|
}, _downloadFinishedLifetime);
|
|
}
|
|
|
|
int CloudList::groupValueForId(uint64 id) {
|
|
const auto i = _groupValueById.find(id);
|
|
if (i != end(_groupValueById)) {
|
|
return i->second;
|
|
}
|
|
const auto result = int(_idByGroupValue.size());
|
|
_groupValueById.emplace(id, result);
|
|
_idByGroupValue.push_back(id);
|
|
return result;
|
|
}
|
|
|
|
void CloudList::updateGeometry() {
|
|
const auto width = _outer->width();
|
|
if (!width) {
|
|
return;
|
|
}
|
|
const auto height = resizeGetHeight(width);
|
|
if (height != _outer->height()) {
|
|
_outer->resize(width, height);
|
|
}
|
|
}
|
|
|
|
int CloudList::resizeGetHeight(int newWidth) {
|
|
const auto desired = st::settingsThemePreviewSize.width();
|
|
const auto minSkip = st::settingsThemeMinSkip;
|
|
const auto single = std::min(
|
|
st::settingsThemePreviewSize.width(),
|
|
(newWidth - minSkip * (kShowPerRow - 1)) / kShowPerRow);
|
|
const auto skip = (newWidth - kShowPerRow * single)
|
|
/ float64(kShowPerRow - 1);
|
|
|
|
auto x = 0.;
|
|
auto y = 0;
|
|
|
|
auto index = 0;
|
|
auto rowHeight = 0;
|
|
for (const auto &element : _elements) {
|
|
const auto button = element.button.get();
|
|
button->moveToLeft(int(std::round(x)), y);
|
|
accumulate_max(rowHeight, button->height());
|
|
x += single + skip;
|
|
if (++index == kShowPerRow) {
|
|
x = 0.;
|
|
index = 0;
|
|
y += rowHeight + st::themesSmallSkip;
|
|
rowHeight = 0;
|
|
}
|
|
}
|
|
return rowHeight
|
|
? (y + rowHeight)
|
|
: (y > 0)
|
|
? (y - st::themesSmallSkip)
|
|
: 0;
|
|
}
|
|
|
|
} // namespace Theme
|
|
} // namespace Window
|