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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
/*
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 "data/data_wall_paper.h"
#include "data/data_cloud_themes.h"
#include "ui/style/style_core_palette.h"
class QFileSystemWatcher;
struct FilePrepareResult;
namespace style {
struct colorizer;
} // namespace style
namespace Main {
class Session;
} // namespace Main
namespace Window {
class Controller;
} // namespace Window
namespace Ui {
struct ChatThemeBackground;
class ChatTheme;
} // namespace Ui
namespace Webview {
struct ThemeParams;
} // namespace Webview
namespace Window {
namespace Theme {
inline constexpr auto kThemeSchemeSizeLimit = 1024 * 1024;
inline constexpr auto kThemeBackgroundSizeLimit = 4 * 1024 * 1024;
struct ParsedTheme;
[[nodiscard]] bool IsEmbeddedTheme(const QString &path);
struct Object {
QString pathRelative;
QString pathAbsolute;
QByteArray content;
Data::CloudTheme cloud;
};
struct Cached {
QByteArray colors;
QByteArray background;
bool tiled = false;
int32 paletteChecksum = 0;
int32 contentChecksum = 0;
};
struct Saved {
Object object;
Cached cache;
};
bool Initialize(Saved &&saved);
void Uninitialize();
struct Instance {
style::palette palette;
QImage background;
Cached cached;
bool tiled = false;
};
struct Preview {
Object object;
Instance instance;
QImage preview;
};
bool Apply(
const QString &filepath,
const Data::CloudTheme &cloud = Data::CloudTheme());
bool Apply(std::unique_ptr<Preview> preview);
void ApplyDefaultWithPath(const QString &themePath);
bool ApplyEditedPalette(const QByteArray &content);
void KeepApplied();
void KeepFromEditor(
const QByteArray &originalContent,
const ParsedTheme &originalParsed,
const Data::CloudTheme &cloud,
const QByteArray &themeContent,
const ParsedTheme &themeParsed,
const QImage &background);
QString NightThemePath();
[[nodiscard]] bool IsNightMode();
void SetNightModeValue(bool nightMode);
[[nodiscard]] rpl::producer<bool> IsNightModeValue();
void ToggleNightMode();
void ToggleNightMode(const QString &themePath);
void ToggleNightModeWithConfirmation(
not_null<Controller*> window,
Fn<void()> toggle);
void ResetToSomeDefault();
[[nodiscard]] bool IsNonDefaultBackground();
void Revert();
[[nodiscard]] rpl::producer<bool> IsThemeDarkValue();
[[nodiscard]] QString EditingPalettePath();
// NB! This method looks to Core::App().settings() to get colorizer by 'file'.
bool LoadFromFile(
const QString &path,
not_null<Instance*> out,
Cached *outCache,
QByteArray *outContent);
bool LoadFromFile(
const QString &path,
not_null<Instance*> out,
Cached *outCache,
QByteArray *outContent,
const style::colorizer &colorizer);
bool LoadFromContent(
const QByteArray &content,
not_null<Instance*> out,
Cached *outCache);
struct BackgroundUpdate {
enum class Type {
New,
Changed,
Start,
TestingTheme,
RevertingTheme,
ApplyingTheme,
ApplyingEdit,
};
BackgroundUpdate(Type type, bool tiled) : type(type), tiled(tiled) {
}
[[nodiscard]] bool paletteChanged() const {
return (type == Type::TestingTheme)
|| (type == Type::RevertingTheme)
|| (type == Type::ApplyingEdit)
|| (type == Type::New);
}
Type type;
bool tiled;
};
enum class ClearEditing {
Temporary,
RevertChanges,
KeepChanges,
};
class ChatBackground final {
public:
ChatBackground();
~ChatBackground();
[[nodiscard]] rpl::producer<BackgroundUpdate> updates() const {
return _updates.events();
}
void start();
// This method is allowed to (and should) be called before start().
void setThemeData(QImage &&themeImage, bool themeTile);
// This method is setting the default (themed) image if none was set yet.
void set(const Data::WallPaper &paper, QImage image = QImage());
void setTile(bool tile);
void setTileDayValue(bool tile);
void setTileNightValue(bool tile);
void setThemeObject(const Object &object);
[[nodiscard]] const Object &themeObject() const;
[[nodiscard]] std::optional<Data::CloudTheme> editingTheme() const;
void setEditingTheme(const Data::CloudTheme &editing);
void clearEditingTheme(ClearEditing clear = ClearEditing::Temporary);
void reset();
void setTestingTheme(Instance &&theme);
void saveAdjustableColors();
void setTestingDefaultTheme();
void revert();
void appliedEditedPalette();
void downloadingStarted(bool tile);
[[nodiscard]] const Data::WallPaper &paper() const {
return _paper;
}
[[nodiscard]] WallPaperId id() const {
return _paper.id();
}
[[nodiscard]] const QImage &prepared() const {
return _prepared;
}
[[nodiscard]] const QImage &preparedForTiled() const {
return _preparedForTiled;
}
[[nodiscard]] std::optional<QColor> colorForFill() const;
[[nodiscard]] QImage gradientForFill() const;
void recacheGradientForFill(QImage gradient);
[[nodiscard]] QImage createCurrentImage() const;
[[nodiscard]] bool tile() const;
[[nodiscard]] bool tileDay() const;
[[nodiscard]] bool tileNight() const;
[[nodiscard]] std::optional<QColor> imageMonoColor() const;
[[nodiscard]] bool nightModeChangeAllowed() const;
private:
struct AdjustableColor {
AdjustableColor(style::color data);
style::color item;
QColor original;
};
[[nodiscard]] bool started() const;
void initialRead();
void saveForRevert();
void setPreparedAfterPaper(QImage image);
void setPrepared(QImage original, QImage prepared, QImage gradient);
void prepareImageForTiled();
void writeNewBackgroundSettings();
void setPaper(const Data::WallPaper &paper);
[[nodiscard]] bool adjustPaletteRequired();
void adjustPaletteUsingBackground(const QImage &image);
void adjustPaletteUsingColors(const std::vector<QColor> &colors);
void adjustPaletteUsingColor(QColor color);
void restoreAdjustableColors();
void setNightModeValue(bool nightMode);
[[nodiscard]] bool nightMode() const;
void toggleNightMode(std::optional<QString> themePath);
void reapplyWithNightMode(
std::optional<QString> themePath,
bool newNightMode);
void keepApplied(const Object &object, bool write);
[[nodiscard]] bool isNonDefaultThemeOrBackground();
[[nodiscard]] bool isNonDefaultBackground();
void checkUploadWallPaper();
[[nodiscard]] QImage postprocessBackgroundImage(QImage image);
void refreshThemeWatcher();
friend bool IsNightMode();
friend void SetNightModeValue(bool nightMode);
friend void ToggleNightMode();
friend void ToggleNightMode(const QString &themePath);
friend void ResetToSomeDefault();
friend void KeepApplied();
friend void KeepFromEditor(
const QByteArray &originalContent,
const ParsedTheme &originalParsed,
const Data::CloudTheme &cloud,
const QByteArray &themeContent,
const ParsedTheme &themeParsed,
const QImage &background);
friend bool IsNonDefaultBackground();
Main::Session *_session = nullptr;
rpl::event_stream<BackgroundUpdate> _updates;
Data::WallPaper _paper = Data::details::UninitializedWallPaper();
std::optional<QColor> _paperColor;
QImage _gradient;
QImage _original;
QImage _prepared;
QImage _preparedForTiled;
bool _nightMode = false;
bool _tileDayValue = false;
bool _tileNightValue = true;
std::optional<bool> _localStoredTileDayValue;
std::optional<bool> _localStoredTileNightValue;
std::optional<QColor> _imageMonoColor;
Object _themeObject;
QImage _themeImage;
bool _themeTile = false;
std::optional<Data::CloudTheme> _editingTheme;
std::unique_ptr<QFileSystemWatcher> _themeWatcher;
Data::WallPaper _paperForRevert
= Data::details::UninitializedWallPaper();
QImage _originalForRevert;
bool _tileForRevert = false;
std::vector<AdjustableColor> _adjustableColors;
FullMsgId _wallPaperUploadId;
mtpRequestId _wallPaperRequestId = 0;
rpl::lifetime _wallPaperUploadLifetime;
rpl::lifetime _lifetime;
};
[[nodiscard]] std::shared_ptr<FilePrepareResult> PrepareWallPaper(
MTP::DcId dcId,
const QImage &image);
[[nodiscard]] ChatBackground *Background();
bool ReadPaletteValues(
const QByteArray &content,
Fn<bool(QLatin1String name, QLatin1String value)> callback);
[[nodiscard]] Webview::ThemeParams WebViewParams();
[[nodiscard]] std::unique_ptr<Ui::ChatTheme> DefaultChatThemeOn(
rpl::lifetime &lifetime);
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,938 @@
/*
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_theme_editor.h"
#include "window/themes/window_theme.h"
#include "window/themes/window_theme_editor_block.h"
#include "window/themes/window_theme_editor_box.h"
#include "window/themes/window_themes_embedded.h"
#include "window/window_controller.h"
#include "main/main_account.h"
#include "mainwindow.h"
#include "storage/localstorage.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/color_editor.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/multi_select.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/toast/toast.h"
#include "ui/style/style_palette_colorizer.h"
#include "ui/image/image_prepare.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "boxes/abstract_box.h"
#include "base/parse_helper.h"
#include "base/zlib_help.h"
#include "base/call_delayed.h"
#include "core/file_utilities.h"
#include "core/application.h"
#include "lang/lang_keys.h"
#include "styles/style_window.h"
#include "styles/style_dialogs.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"
namespace Window {
namespace Theme {
namespace {
template <size_t Size>
QByteArray qba(const char(&string)[Size]) {
return QByteArray::fromRawData(string, Size - 1);
}
const auto kCloudInTextStart = qba("// THEME EDITOR SERVICE INFO START\n");
const auto kCloudInTextEnd = qba("// THEME EDITOR SERVICE INFO END\n\n");
struct ReadColorResult {
ReadColorResult(QColor color, bool error = false) : color(color), error(error) {
}
QColor color;
bool error = false;
};
ReadColorResult colorError(const QString &name) {
return { QColor(), true };
}
ReadColorResult readColor(const QString &name, const char *data, int size) {
if (size != 6 && size != 8) {
return colorError(name);
}
auto readHex = [](char ch) {
if (ch >= '0' && ch <= '9') {
return (ch - '0');
} else if (ch >= 'a' && ch <= 'f') {
return (ch - 'a' + 10);
} else if (ch >= 'A' && ch <= 'F') {
return (ch - 'A' + 10);
}
return -1;
};
auto readValue = [readHex](const char *data) {
auto high = readHex(data[0]);
auto low = readHex(data[1]);
return (high >= 0 && low >= 0) ? (high * 0x10 + low) : -1;
};
auto r = readValue(data);
auto g = readValue(data + 2);
auto b = readValue(data + 4);
auto a = (size == 8) ? readValue(data + 6) : 255;
if (r < 0 || g < 0 || b < 0 || a < 0) {
return colorError(name);
}
return { QColor(r, g, b, a) };
}
bool skipComment(const char *&data, const char *end) {
if (data == end) return false;
if (*data == '/' && data + 1 != end) {
if (*(data + 1) == '/') {
data += 2;
while (data != end && *data != '\n') {
++data;
}
return true;
} else if (*(data + 1) == '*') {
data += 2;
while (true) {
while (data != end && *data != '*') {
++data;
}
if (data != end) {
++data;
if (data != end && *data == '/') {
++data;
break;
}
}
if (data == end) {
break;
}
}
return true;
}
}
return false;
}
void skipWhitespacesAndComments(const char *&data, const char *end) {
while (data != end) {
if (!base::parse::skipWhitespaces(data, end)) return;
if (!skipComment(data, end)) return;
}
}
QLatin1String readValue(const char *&data, const char *end) {
auto start = data;
if (data != end && *data == '#') {
++data;
}
base::parse::readName(data, end);
return QLatin1String(start, data - start);
}
bool isValidColorValue(QLatin1String value) {
auto isValidHexChar = [](char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f');
};
auto data = value.data();
auto size = value.size();
if ((size != 7 && size != 9) || data[0] != '#') {
return false;
}
for (auto i = 1; i != size; ++i) {
if (!isValidHexChar(data[i])) {
return false;
}
}
return true;
}
[[nodiscard]] QByteArray ColorizeInContent(
QByteArray content,
const style::colorizer &colorizer) {
auto validNames = OrderedSet<QLatin1String>();
content.detach();
auto start = content.constBegin(), data = start, end = data + content.size();
while (data != end) {
skipWhitespacesAndComments(data, end);
if (data == end) break;
[[maybe_unused]] auto foundName = base::parse::readName(data, end);
skipWhitespacesAndComments(data, end);
if (data == end || *data != ':') {
return "error";
}
++data;
skipWhitespacesAndComments(data, end);
auto value = readValue(data, end);
if (value.size() == 0) {
return "error";
}
if (isValidColorValue(value)) {
const auto colorized = style::colorize(value, colorizer);
Assert(colorized.size() == value.size());
memcpy(
content.data() + (data - start) - value.size(),
colorized.data(),
value.size());
}
skipWhitespacesAndComments(data, end);
if (data == end || *data != ';') {
return "error";
}
++data;
}
return content;
}
QString bytesToUtf8(QLatin1String bytes) {
return QString::fromUtf8(bytes.data(), bytes.size());
}
} // namespace
class Editor::Inner final : public Ui::RpWidget {
public:
Inner(QWidget *parent, const QString &path);
void setErrorCallback(Fn<void()> callback) {
_errorCallback = std::move(callback);
}
void setFocusCallback(Fn<void()> callback) {
_focusCallback = std::move(callback);
}
void setScrollCallback(Fn<void(int top, int bottom)> callback) {
_scrollCallback = std::move(callback);
}
void prepare();
[[nodiscard]] QByteArray paletteContent() const {
return _paletteContent;
}
void filterRows(const QString &query);
void chooseRow();
void selectSkip(int direction);
void selectSkipPage(int delta, int direction);
void applyNewPalette(const QByteArray &newContent);
void recreateRows();
~Inner() {
if (_context.colorEditor.box) {
_context.colorEditor.box->closeBox();
}
}
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
private:
bool readData();
bool readExistingRows();
bool feedExistingRow(const QString &name, QLatin1String value);
void error() {
if (_errorCallback) {
_errorCallback();
}
}
void applyEditing(const QString &name, const QString &copyOf, QColor value);
void sortByAccentDistance();
EditorBlock::Context _context;
QString _path;
QByteArray _paletteContent;
Fn<void()> _errorCallback;
Fn<void()> _focusCallback;
Fn<void(int top, int bottom)> _scrollCallback;
object_ptr<EditorBlock> _existingRows;
object_ptr<EditorBlock> _newRows;
bool _applyingUpdate = false;
};
QByteArray ColorHexString(const QColor &color) {
auto result = QByteArray();
result.reserve(9);
result.append('#');
const auto addHex = [&](int code) {
if (code >= 0 && code < 10) {
result.append('0' + code);
} else if (code >= 10 && code < 16) {
result.append('a' + (code - 10));
}
};
const auto addValue = [&](int code) {
addHex(code / 16);
addHex(code % 16);
};
addValue(color.red());
addValue(color.green());
addValue(color.blue());
if (color.alpha() != 255) {
addValue(color.alpha());
}
return result;
}
QByteArray ReplaceValueInPaletteContent(
const QByteArray &content,
const QByteArray &name,
const QByteArray &value) {
auto validNames = OrderedSet<QLatin1String>();
auto start = content.constBegin(), data = start, end = data + content.size();
auto lastValidValueStart = end, lastValidValueEnd = end;
while (data != end) {
skipWhitespacesAndComments(data, end);
if (data == end) break;
auto foundName = base::parse::readName(data, end);
skipWhitespacesAndComments(data, end);
if (data == end || *data != ':') {
return "error";
}
++data;
skipWhitespacesAndComments(data, end);
auto valueStart = data;
auto value = readValue(data, end);
auto valueEnd = data;
if (value.size() == 0) {
return "error";
}
auto validValue = validNames.contains(value) || isValidColorValue(value);
if (validValue) {
validNames.insert(foundName);
if (foundName == name) {
lastValidValueStart = valueStart;
lastValidValueEnd = valueEnd;
}
}
skipWhitespacesAndComments(data, end);
if (data == end || *data != ';') {
return "error";
}
++data;
}
if (lastValidValueStart != end) {
auto result = QByteArray();
result.reserve((lastValidValueStart - start) + value.size() + (end - lastValidValueEnd));
result.append(start, lastValidValueStart - start);
result.append(value);
if (end - lastValidValueEnd > 0) result.append(lastValidValueEnd, end - lastValidValueEnd);
return result;
}
auto newline = (content.indexOf("\r\n") >= 0 ? "\r\n" : "\n");
auto addedline = (content.endsWith('\n') ? "" : newline);
return content + addedline + name + ": " + value + ";" + newline;
}
[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud) {
auto result = QByteArray();
const auto add = [&](const QByteArray &key, const QString &value) {
result.append("// " + key + ": " + value.toLatin1() + "\n");
};
result.append(kCloudInTextStart);
add("ID", QString::number(cloud.id));
add("ACCESS", QString::number(cloud.accessHash));
result.append(kCloudInTextEnd);
return result;
}
[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text) {
const auto index = text.indexOf(kCloudInTextEnd);
if (index <= 1) {
return Data::CloudTheme();
}
auto result = Data::CloudTheme();
const auto list = text.mid(0, index - 1).split('\n');
const auto take = [&](uint64 &value, int index) {
if (list.size() <= index) {
return false;
}
const auto &entry = list[index];
const auto position = entry.indexOf(": ");
if (position < 0) {
return false;
}
value = QString::fromLatin1(entry.mid(position + 2)).toULongLong();
return true;
};
if (!take(result.id, 1) || !take(result.accessHash, 2)) {
return Data::CloudTheme();
}
return result;
}
QByteArray StripCloudTextFields(const QByteArray &text) {
const auto firstValue = text.indexOf(": #");
auto start = 0;
while (true) {
const auto index = text.indexOf(kCloudInTextEnd, start);
if (index < 0 || index > firstValue) {
break;
}
start = index + kCloudInTextEnd.size();
}
return (start > 0) ? text.mid(start) : text;
}
Editor::Inner::Inner(QWidget *parent, const QString &path)
: RpWidget(parent)
, _path(path)
, _existingRows(this, EditorBlock::Type::Existing, &_context)
, _newRows(this, EditorBlock::Type::New, &_context) {
resize(st::windowMinWidth, st::windowMinHeight);
_context.resized.events(
) | rpl::on_next([=] {
resizeToWidth(width());
}, lifetime());
using Context = EditorBlock::Context;
_context.pending.events(
) | rpl::on_next([=](const Context::EditionData &data) {
applyEditing(data.name, data.copyOf, data.value);
}, lifetime());
_context.updated.events(
) | rpl::on_next([=] {
if (_context.name.isEmpty() && _focusCallback) {
_focusCallback();
}
}, lifetime());
_context.scroll.events(
) | rpl::on_next([=](const Context::ScrollData &data) {
if (_scrollCallback) {
auto top = (data.type == EditorBlock::Type::Existing
? _existingRows
: _newRows)->y();
top += data.position;
_scrollCallback(top, top + data.height);
}
}, lifetime());
Background()->updates(
) | rpl::on_next([=](const BackgroundUpdate &update) {
if (_applyingUpdate || !Background()->editingTheme()) {
return;
}
if (update.type == BackgroundUpdate::Type::TestingTheme) {
Revert();
base::call_delayed(st::slideDuration, this, [] {
Ui::show(Ui::MakeInformBox(
tr::lng_theme_editor_cant_change_theme()));
});
}
}, lifetime());
}
void Editor::Inner::recreateRows() {
_existingRows.create(this, EditorBlock::Type::Existing, &_context);
_existingRows->show();
_newRows.create(this, EditorBlock::Type::New, &_context);
_newRows->show();
if (!readData()) {
error();
}
}
void Editor::Inner::prepare() {
QFile f(_path);
if (!f.open(QIODevice::ReadOnly)) {
LOG(("Theme Error: could not open color palette file '%1'").arg(_path));
error();
return;
}
_paletteContent = f.readAll();
if (f.error() != QFileDevice::NoError) {
LOG(("Theme Error: could not read content from palette file '%1'").arg(_path));
error();
return;
}
f.close();
if (!readData()) {
error();
}
}
void Editor::Inner::filterRows(const QString &query) {
if (query == ":sort-for-accent") {
sortByAccentDistance();
filterRows(QString());
return;
}
_existingRows->filterRows(query);
_newRows->filterRows(query);
}
void Editor::Inner::chooseRow() {
if (!_existingRows->hasSelected() && !_newRows->hasSelected()) {
selectSkip(1);
}
if (_existingRows->hasSelected()) {
_existingRows->chooseRow();
} else if (_newRows->hasSelected()) {
_newRows->chooseRow();
}
}
// Block::selectSkip(-1) removes the selection if it can't select anything
// Block::selectSkip(1) leaves the selection if it can't select anything
void Editor::Inner::selectSkip(int direction) {
if (direction > 0) {
if (_newRows->hasSelected()) {
_existingRows->clearSelected();
_newRows->selectSkip(direction);
} else if (_existingRows->hasSelected()) {
if (!_existingRows->selectSkip(direction)) {
if (_newRows->selectSkip(direction)) {
_existingRows->clearSelected();
}
}
} else {
if (!_existingRows->selectSkip(direction)) {
_newRows->selectSkip(direction);
}
}
} else {
if (_existingRows->hasSelected()) {
_newRows->clearSelected();
_existingRows->selectSkip(direction);
} else if (_newRows->hasSelected()) {
if (!_newRows->selectSkip(direction)) {
_existingRows->selectSkip(direction);
}
}
}
}
void Editor::Inner::selectSkipPage(int delta, int direction) {
auto defaultRowHeight = st::themeEditorMargin.top()
+ st::themeEditorSampleSize.height()
+ st::themeEditorDescriptionSkip
+ st::defaultTextStyle.font->height
+ st::themeEditorMargin.bottom();
for (auto i = 0, count = ceilclamp(delta, defaultRowHeight, 1, delta); i != count; ++i) {
selectSkip(direction);
}
}
void Editor::Inner::paintEvent(QPaintEvent *e) {
Painter p(this);
p.setFont(st::boxTitleFont);
p.setPen(st::windowFg);
if (!_newRows->isHidden()) {
p.drawTextLeft(st::themeEditorMargin.left(), _existingRows->y() + _existingRows->height() + st::boxTitlePosition.y(), width(), tr::lng_theme_editor_new_keys(tr::now));
}
}
int Editor::Inner::resizeGetHeight(int newWidth) {
auto rowsWidth = newWidth;
_existingRows->resizeToWidth(rowsWidth);
_newRows->resizeToWidth(rowsWidth);
_existingRows->moveToLeft(0, 0);
_newRows->moveToLeft(0, _existingRows->height() + st::boxTitleHeight);
auto lowest = (_newRows->isHidden() ? _existingRows : _newRows).data();
return lowest->y() + lowest->height();
}
bool Editor::Inner::readData() {
if (!readExistingRows()) {
return false;
}
const auto rows = style::main_palette::data();
for (const auto &row : rows) {
auto name = bytesToUtf8(row.name);
auto description = bytesToUtf8(row.description);
if (!_existingRows->feedDescription(name, description)) {
if (row.value.data()[0] == '#') {
auto result = readColor(name, row.value.data() + 1, row.value.size() - 1);
Assert(!result.error);
_newRows->feed(name, result.color);
//if (!_newRows->feedFallbackName(name, row.fallback.utf16())) {
// Unexpected("Row for fallback not found");
//}
} else {
auto copyOf = bytesToUtf8(row.value);
if (auto result = _existingRows->find(copyOf)) {
_newRows->feed(name, *result, copyOf);
} else if (!_newRows->feedCopy(name, copyOf)) {
Unexpected("Copy of unknown value in the default palette");
}
Assert(row.fallback.size() == 0);
}
if (!_newRows->feedDescription(name, description)) {
Unexpected("Row for description not found");
}
}
}
return true;
}
void Editor::Inner::sortByAccentDistance() {
const auto accent = *_existingRows->find("windowBgActive");
_existingRows->sortByDistance(accent);
_newRows->sortByDistance(accent);
}
bool Editor::Inner::readExistingRows() {
return ReadPaletteValues(_paletteContent, [this](QLatin1String name, QLatin1String value) {
return feedExistingRow(name, value);
});
}
bool Editor::Inner::feedExistingRow(const QString &name, QLatin1String value) {
auto data = value.data();
auto size = value.size();
if (data[0] != '#') {
return _existingRows->feedCopy(name, QString(value));
}
auto result = readColor(name, data + 1, size - 1);
if (result.error) {
LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name).arg(value));
} else {
_existingRows->feed(name, result.color);
}
return true;
}
void Editor::Inner::applyEditing(const QString &name, const QString &copyOf, QColor value) {
auto plainName = name.toLatin1();
auto plainValue = copyOf.isEmpty() ? ColorHexString(value) : copyOf.toLatin1();
auto newContent = ReplaceValueInPaletteContent(_paletteContent, plainName, plainValue);
if (newContent == "error") {
LOG(("Theme Error: could not replace '%1: %2' in content").arg(name, copyOf.isEmpty() ? QString::fromLatin1(ColorHexString(value)) : copyOf));
error();
return;
}
applyNewPalette(newContent);
}
void Editor::Inner::applyNewPalette(const QByteArray &newContent) {
QFile f(_path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open '%1' for writing a palette update.").arg(_path));
error();
return;
}
if (f.write(newContent) != newContent.size()) {
LOG(("Theme Error: could not write all content to '%1' while writing a palette update.").arg(_path));
error();
return;
}
f.close();
_applyingUpdate = true;
if (!ApplyEditedPalette(newContent)) {
LOG(("Theme Error: could not apply newly composed content :("));
error();
return;
}
_applyingUpdate = false;
_paletteContent = newContent;
}
Editor::Editor(
QWidget*,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud)
: _window(window)
, _cloud(cloud)
, _scroll(this)
, _close(this, st::defaultMultiSelect.fieldCancel)
, _menuToggle(this, st::themesMenuToggle)
, _select(this, st::defaultMultiSelect, tr::lng_country_ph())
, _leftShadow(this)
, _topShadow(this)
, _save(
this,
tr::lng_theme_editor_save_button(tr::now),
st::dialogsUpdateButton) {
const auto path = EditingPalettePath();
_inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, path));
_save->setClickedCallback(base::fn_delayed(
st::defaultRippleAnimation.hideDuration,
this,
[=] { save(); }));
_inner->setErrorCallback([=] {
window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
// This could be from inner->_context observable notification.
// We should not destroy it while iterating in subscribers.
crl::on_main(this, [=] {
closeEditor();
});
});
_inner->setFocusCallback([this] {
base::call_delayed(2 * st::boxDuration, this, [this] {
_select->setInnerFocus();
});
});
_inner->setScrollCallback([this](int top, int bottom) {
_scroll->scrollToY(top, bottom);
});
_menuToggle->setClickedCallback([=] {
showMenu();
});
_close->setClickedCallback([=] {
closeWithConfirmation();
});
_close->show(anim::type::instant);
_select->resizeToWidth(st::windowMinWidth);
_select->setQueryChangedCallback([this](const QString &query) { _inner->filterRows(query); _scroll->scrollToY(0); });
_select->setSubmittedCallback([this](Qt::KeyboardModifiers) { _inner->chooseRow(); });
_inner->prepare();
resizeToWidth(st::windowMinWidth);
}
void Editor::showMenu() {
if (_menu) {
return;
}
_menu = base::make_unique_q<Ui::DropdownMenu>(
this,
st::dropdownMenuWithIcons);
_menu->setHiddenCallback([weak = base::make_weak(this), menu = _menu.get()]{
menu->deleteLater();
if (weak && weak->_menu == menu) {
weak->_menu = nullptr;
weak->_menuToggle->setForceRippled(false);
}
});
_menu->setShowStartCallback(crl::guard(this, [this, menu = _menu.get()]{
if (_menu == menu) {
_menuToggle->setForceRippled(true);
}
}));
_menu->setHideStartCallback(crl::guard(this, [this, menu = _menu.get()]{
if (_menu == menu) {
_menuToggle->setForceRippled(false);
}
}));
_menuToggle->installEventFilter(_menu);
_menu->addAction(tr::lng_theme_editor_menu_export(tr::now), [=] {
base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {
exportTheme();
});
}, &st::menuIconExportTheme);
_menu->addAction(tr::lng_theme_editor_menu_import(tr::now), [=] {
base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {
importTheme();
});
}, &st::menuIconImportTheme);
_menu->addAction(tr::lng_theme_editor_menu_show(tr::now), [=] {
File::ShowInFolder(EditingPalettePath());
}, &st::menuIconPalette);
_menu->moveToRight(st::themesMenuPosition.x(), st::themesMenuPosition.y());
_menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
}
void Editor::exportTheme() {
auto caption = tr::lng_theme_editor_choose_name(tr::now);
auto filter = "Themes (*.tdesktop-theme)";
auto name = "awesome.tdesktop-theme";
FileDialog::GetWritePath(this, caption, filter, name, crl::guard(this, [=](const QString &path) {
const auto result = CollectForExport(_inner->paletteContent());
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
LOG(("Theme Error: could not open zip-ed theme file '%1' for writing").arg(path));
_window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
return;
}
if (f.write(result) != result.size()) {
LOG(("Theme Error: could not write zip-ed theme to file '%1'").arg(path));
_window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
return;
}
_window->showToast(tr::lng_theme_editor_done(tr::now));
}));
}
void Editor::importTheme() {
auto filters = QStringList(
u"Theme files (*.tdesktop-theme *.tdesktop-palette)"_q);
filters.push_back(FileDialog::AllFilesFilter());
const auto callback = crl::guard(this, [=](
const FileDialog::OpenResult &result) {
const auto path = result.paths.isEmpty()
? QString()
: result.paths.front();
if (path.isEmpty()) {
return;
}
auto f = QFile(path);
if (!f.open(QIODevice::ReadOnly)) {
return;
}
auto object = Object();
object.pathAbsolute = QFileInfo(path).absoluteFilePath();
object.pathRelative = QDir().relativeFilePath(path);
object.content = f.readAll();
if (object.content.isEmpty()) {
return;
}
_select->clearQuery();
const auto parsed = ParseTheme(object, false, false);
_inner->applyNewPalette(parsed.palette);
_inner->recreateRows();
updateControlsGeometry();
auto image = Images::Read({
.content = parsed.background,
.forceOpaque = true,
}).image;
if (!image.isNull() && !image.size().isEmpty()) {
Background()->set(Data::CustomWallPaper(), std::move(image));
Background()->setTile(parsed.tiled);
Ui::ForceFullRepaint(_window->widget());
}
});
FileDialog::GetOpenPath(
this,
tr::lng_theme_editor_menu_import(tr::now),
filters.join(u";;"_q),
crl::guard(this, callback));
}
QByteArray Editor::ColorizeInContent(
QByteArray content,
const style::colorizer &colorizer) {
return Window::Theme::ColorizeInContent(content, colorizer);
}
void Editor::save() {
if (Core::App().passcodeLocked()) {
_window->showToast(tr::lng_theme_editor_need_unlock(tr::now));
return;
} else if (!_window->account().sessionExists()) {
_window->showToast(tr::lng_theme_editor_need_auth(tr::now));
return;
} else if (_saving) {
return;
}
_saving = true;
const auto unlock = crl::guard(this, [=] { _saving = false; });
SaveTheme(_window, _cloud, _inner->paletteContent(), unlock);
}
void Editor::resizeEvent(QResizeEvent *e) {
updateControlsGeometry();
}
void Editor::updateControlsGeometry() {
_save->resizeToWidth(width());
_close->moveToRight(0, 0);
_menuToggle->moveToRight(_close->width(), 0);
_select->resizeToWidth(width());
_select->moveToLeft(0, _close->height());
auto shadowTop = _select->y() + _select->height();
_topShadow->resize(width() - st::lineWidth, st::lineWidth);
_topShadow->moveToLeft(st::lineWidth, shadowTop);
_leftShadow->resize(st::lineWidth, height());
_leftShadow->moveToLeft(0, 0);
auto scrollSize = QSize(width(), height() - shadowTop - _save->height());
if (_scroll->size() != scrollSize) {
_scroll->resize(scrollSize);
}
_inner->resizeToWidth(width());
_scroll->moveToLeft(0, shadowTop);
if (!_scroll->isHidden()) {
auto scrollTop = _scroll->scrollTop();
_inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
}
_save->moveToLeft(0, _scroll->y() + _scroll->height());
}
void Editor::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
if (!_select->getQuery().isEmpty()) {
_select->clearQuery();
} else {
_window->widget()->setInnerFocus();
}
} else 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(_scroll->height(), 1);
} else if (e->key() == Qt::Key_PageUp) {
_inner->selectSkipPage(_scroll->height(), -1);
}
}
void Editor::focusInEvent(QFocusEvent *e) {
_select->setInnerFocus();
}
void Editor::paintEvent(QPaintEvent *e) {
Painter p(this);
p.fillRect(e->rect(), st::dialogsBg);
p.setFont(st::boxTitleFont);
p.setPen(st::windowFg);
p.drawTextLeft(st::themeEditorMargin.left(), st::themeEditorMargin.top(), width(), tr::lng_theme_editor_title(tr::now));
}
void Editor::closeWithConfirmation() {
if (!PaletteChanged(_inner->paletteContent(), _cloud)) {
Background()->clearEditingTheme(ClearEditing::KeepChanges);
closeEditor();
return;
}
const auto close = crl::guard(this, [=](Fn<void()> &&close) {
Background()->clearEditingTheme(ClearEditing::RevertChanges);
closeEditor();
close();
});
_window->show(Ui::MakeConfirmBox({
.text = tr::lng_theme_editor_sure_close(),
.confirmed = close,
.confirmText = tr::lng_close(),
}));
}
void Editor::closeEditor() {
_window->widget()->showRightColumn(nullptr);
Background()->clearEditingTheme();
}
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,95 @@
/*
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 "data/data_cloud_themes.h"
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
namespace style {
struct colorizer;
} // namespace style
namespace Ui {
class FlatButton;
class ScrollArea;
class CrossButton;
class MultiSelect;
class PlainShadow;
class DropdownMenu;
class IconButton;
} // namespace Ui
namespace Window {
class Controller;
namespace Theme {
struct ParsedTheme {
QByteArray palette;
QByteArray background;
bool isPng = false;
bool tiled = false;
};
[[nodiscard]] QByteArray ColorHexString(const QColor &color);
[[nodiscard]] QByteArray ReplaceValueInPaletteContent(
const QByteArray &content,
const QByteArray &name,
const QByteArray &value);
[[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud);
[[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text);
[[nodiscard]] QByteArray StripCloudTextFields(const QByteArray &text);
class Editor : public Ui::RpWidget {
public:
Editor(
QWidget*,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud);
[[nodiscard]] static QByteArray ColorizeInContent(
QByteArray content,
const style::colorizer &colorizer);
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
void focusInEvent(QFocusEvent *e) override;
private:
void save();
void showMenu();
void exportTheme();
void importTheme();
void closeEditor();
void closeWithConfirmation();
void updateControlsGeometry();
const not_null<Window::Controller*> _window;
const Data::CloudTheme _cloud;
object_ptr<Ui::ScrollArea> _scroll;
class Inner;
QPointer<Inner> _inner;
object_ptr<Ui::CrossButton> _close;
object_ptr<Ui::IconButton> _menuToggle;
base::unique_qptr<Ui::DropdownMenu> _menu;
object_ptr<Ui::MultiSelect> _select;
object_ptr<Ui::PlainShadow> _leftShadow;
object_ptr<Ui::PlainShadow> _topShadow;
object_ptr<Ui::FlatButton> _save;
bool _saving = false;
};
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,834 @@
/*
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_theme_editor_block.h"
#include "base/call_delayed.h"
#include "boxes/abstract_box.h"
#include "lang/lang_keys.h"
#include "ui/effects/ripple_animation.h"
#include "ui/layers/generic_box.h"
#include "ui/painter.h"
#include "ui/widgets/color_editor.h"
#include "ui/widgets/shadow.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
namespace Window {
namespace Theme {
namespace {
auto SearchSplitter = QRegularExpression(u"[\\@\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\:\\!\\_\\;\\\"\\'\\x0\\#]"_q);
} // namespace
class EditorBlock::Row {
public:
Row(const QString &name, const QString &copyOf, QColor value);
QString name() const {
return _name;
}
void setCopyOf(const QString &copyOf) {
_copyOf = copyOf;
fillSearchIndex();
}
QString copyOf() const {
return _copyOf;
}
void setValue(QColor value);
const QColor &value() const {
return _value;
}
QString description() const {
return _description.toString();
}
const Ui::Text::String &descriptionText() const {
return _description;
}
void setDescription(const QString &description) {
_description.setText(st::defaultTextStyle, description);
fillSearchIndex();
}
const base::flat_set<QString> &searchWords() const {
return _searchWords;
}
bool searchWordsContain(const QString &needle) const {
for (const auto &word : _searchWords) {
if (word.startsWith(needle)) {
return true;
}
}
return false;
}
const base::flat_set<QChar> &searchStartChars() const {
return _searchStartChars;
}
void setTop(int top) {
_top = top;
}
int top() const {
return _top;
}
void setHeight(int height) {
_height = height;
}
int height() const {
return _height;
}
Ui::RippleAnimation *ripple() const {
return _ripple.get();
}
Ui::RippleAnimation *setRipple(std::unique_ptr<Ui::RippleAnimation> ripple) const {
_ripple = std::move(ripple);
return _ripple.get();
}
void resetRipple() const {
_ripple = nullptr;
}
private:
void fillValueString();
void fillSearchIndex();
QString _name;
QString _copyOf;
QColor _value;
QString _valueString;
Ui::Text::String _description = { st::windowMinWidth / 2 };
base::flat_set<QString> _searchWords;
base::flat_set<QChar> _searchStartChars;
int _top = 0;
int _height = 0;
mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
};
EditorBlock::Row::Row(const QString &name, const QString &copyOf, QColor value)
: _name(name)
, _copyOf(copyOf) {
setValue(value);
}
void EditorBlock::Row::setValue(QColor value) {
_value = value;
fillValueString();
fillSearchIndex();
}
void EditorBlock::Row::fillValueString() {
auto addHex = [=](int code) {
if (code >= 0 && code < 10) {
_valueString.append(QChar('0' + code));
} else if (code >= 10 && code < 16) {
_valueString.append(QChar('a' + (code - 10)));
}
};
auto addCode = [=](int code) {
addHex(code / 16);
addHex(code % 16);
};
_valueString.resize(0);
_valueString.reserve(9);
_valueString.append('#');
addCode(_value.red());
addCode(_value.green());
addCode(_value.blue());
if (_value.alpha() != 255) {
addCode(_value.alpha());
}
}
void EditorBlock::Row::fillSearchIndex() {
_searchWords.clear();
_searchStartChars.clear();
const auto toIndex = _name
+ ' ' + _copyOf
+ ' ' + TextUtilities::RemoveAccents(_description.toString())
+ ' ' + _valueString;
const auto words = toIndex.toLower().split(
SearchSplitter,
Qt::SkipEmptyParts);
for (const auto &word : words) {
_searchWords.emplace(word);
_searchStartChars.emplace(word[0]);
}
}
EditorBlock::EditorBlock(QWidget *parent, Type type, Context *context)
: RpWidget(parent)
, _type(type)
, _context(context)
, _transparent(style::TransparentPlaceholder()) {
setMouseTracking(true);
_context->updated.events(
) | rpl::on_next([=] {
if (_mouseSelection) {
_lastGlobalPos = QCursor::pos();
updateSelected(mapFromGlobal(_lastGlobalPos));
}
update();
}, lifetime());
if (_type == Type::Existing) {
_context->appended.events(
) | rpl::on_next([=](const Context::AppendData &added) {
auto name = added.name;
auto value = added.value;
feed(name, value);
feedDescription(name, added.description);
auto row = findRow(name);
Assert(row != nullptr);
auto possibleCopyOf = added.possibleCopyOf;
auto copyOf = checkCopyOf(findRowIndex(row), possibleCopyOf) ? possibleCopyOf : QString();
removeFromSearch(*row);
row->setCopyOf(copyOf);
addToSearch(*row);
_context->changed.fire({ QStringList(name), value });
_context->resized.fire({});
_context->pending.fire({ name, copyOf, value });
}, lifetime());
} else {
_context->changed.events(
) | rpl::on_next([=](const Context::ChangeData &data) {
checkCopiesChanged(0, data.names, data.value);
}, lifetime());
}
}
void EditorBlock::feed(const QString &name, QColor value, const QString &copyOfExisting) {
if (findRow(name)) {
// Remove the existing row and mark all its copies as unique keys.
LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name));
removeRow(name);
}
addRow(name, copyOfExisting, value);
}
bool EditorBlock::feedCopy(const QString &name, const QString &copyOf) {
if (auto row = findRow(copyOf)) {
if (copyOf == name) {
LOG(("Theme Warning: Skipping value '%1: %2' (the value refers to itself.)").arg(name, copyOf));
return true;
}
if (findRow(name)) {
// Remove the existing row and mark all its copies as unique keys.
LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name));
removeRow(name);
// row was invalidated by removeRow() call.
row = findRow(copyOf);
// Should not happen, but still check.
if (!row) {
return true;
}
}
addRow(name, copyOf, row->value());
} else {
LOG(("Theme Warning: Skipping value '%1: %2' (expected a color value in #rrggbb or #rrggbbaa or a previously defined key in the color scheme)").arg(name, copyOf));
}
return true;
}
void EditorBlock::removeRow(const QString &name, bool removeCopyReferences) {
auto it = _indices.find(name);
Assert(it != _indices.cend());
auto index = it.value();
for (auto i = index + 1, count = static_cast<int>(_data.size()); i != count; ++i) {
auto &row = _data[i];
removeFromSearch(row);
_indices[row.name()] = i - 1;
if (removeCopyReferences && row.copyOf() == name) {
row.setCopyOf(QString());
}
}
removeFromSearch(_data[index]);
_data.erase(_data.begin() + index);
_indices.erase(it);
for (auto i = index, count = static_cast<int>(_data.size()); i != count; ++i) {
addToSearch(_data[i]);
}
}
void EditorBlock::addToSearch(const Row &row) {
auto query = _searchQuery;
if (!query.isEmpty()) resetSearch();
auto index = findRowIndex(&row);
for (const auto &ch : row.searchStartChars()) {
_searchIndex[ch].insert(index);
}
if (!query.isEmpty()) searchByQuery(query);
}
void EditorBlock::removeFromSearch(const Row &row) {
auto query = _searchQuery;
if (!query.isEmpty()) resetSearch();
auto index = findRowIndex(&row);
for (const auto &ch : row.searchStartChars()) {
const auto i = _searchIndex.find(ch);
if (i != end(_searchIndex)) {
i->second.remove(index);
if (i->second.empty()) {
_searchIndex.erase(i);
}
}
}
if (!query.isEmpty()) searchByQuery(query);
}
void EditorBlock::filterRows(const QString &query) {
searchByQuery(query);
}
void EditorBlock::chooseRow() {
if (_selected < 0) {
return;
}
activateRow(rowAtIndex(_selected));
}
void EditorBlock::activateRow(const Row &row) {
if (_context->colorEditor.editor) {
if (_type == Type::Existing) {
_context->possibleCopyOf = row.name();
_context->colorEditor.editor->showColor(row.value());
}
} else {
_editing = findRowIndex(&row);
const auto name = row.name();
const auto value = row.value();
Ui::show(Box([=](not_null<Ui::GenericBox*> box) {
const auto editor = box->addRow(object_ptr<ColorEditor>(
box,
ColorEditor::Mode::RGBA,
value));
struct State {
rpl::lifetime cancelLifetime;
};
const auto state = editor->lifetime().make_state<State>();
const auto save = crl::guard(this, [=] {
state->cancelLifetime.destroy();
saveEditing(editor->color());
});
box->boxClosing(
) | rpl::on_next(crl::guard(this, [=] {
cancelEditing();
}), state->cancelLifetime);
editor->submitRequests(
) | rpl::on_next(save, editor->lifetime());
box->setFocusCallback([=] {
editor->setInnerFocus();
});
box->addButton(tr::lng_settings_save(), save);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->setTitle(rpl::single(name));
box->setWidth(editor->width());
_context->colorEditor.box = box;
_context->colorEditor.editor = editor;
_context->name = name;
_context->updated.fire({});
}));
}
}
bool EditorBlock::selectSkip(int direction) {
_mouseSelection = false;
auto maxSelected = size_type(isSearch()
? _searchResults.size()
: _data.size()) - 1;
auto newSelected = _selected + direction;
if (newSelected < -1 || newSelected > maxSelected) {
newSelected = maxSelected;
}
if (newSelected != _selected) {
setSelected(newSelected);
scrollToSelected();
return (newSelected >= 0);
}
return false;
}
void EditorBlock::scrollToSelected() {
if (_selected >= 0) {
const auto &row = rowAtIndex(_selected);
_context->scroll.fire({ _type, row.top(), row.height() });
}
}
void EditorBlock::searchByQuery(QString query) {
const auto words = TextUtilities::PrepareSearchWords(
query,
&SearchSplitter);
query = words.isEmpty() ? QString() : words.join(' ');
if (_searchQuery != query) {
setSelected(-1);
setPressed(-1);
_searchQuery = query;
_searchResults.clear();
auto toFilter = (base::flat_set<int>*)nullptr;
for (const auto &word : words) {
if (word.isEmpty()) continue;
const auto i = _searchIndex.find(word[0]);
if (i == end(_searchIndex) || i->second.empty()) {
toFilter = nullptr;
break;
} else if (!toFilter || i->second.size() < toFilter->size()) {
toFilter = &i->second;
}
}
if (toFilter) {
const auto allWordsFound = [&](const Row &row) {
for (const auto &word : words) {
if (!row.searchWordsContain(word)) {
return false;
}
}
return true;
};
for (const auto index : *toFilter) {
if (allWordsFound(_data[index])) {
_searchResults.push_back(index);
}
}
}
_context->resized.fire({});
}
}
const QColor *EditorBlock::find(const QString &name) {
if (auto row = findRow(name)) {
return &row->value();
}
return nullptr;
}
bool EditorBlock::feedDescription(const QString &name, const QString &description) {
if (auto row = findRow(name)) {
removeFromSearch(*row);
row->setDescription(description);
addToSearch(*row);
return true;
}
return false;
}
void EditorBlock::sortByDistance(const QColor &to) {
auto toHue = int();
auto toSaturation = int();
auto toLightness = int();
to.getHsl(&toHue, &toSaturation, &toLightness);
ranges::sort(_data, ranges::less(), [&](const Row &row) {
auto fromHue = int();
auto fromSaturation = int();
auto fromLightness = int();
row.value().getHsl(&fromHue, &fromSaturation, &fromLightness);
if (!row.copyOf().isEmpty()) {
return 365;
}
const auto a = std::abs(fromHue - toHue);
const auto b = 360 + fromHue - toHue;
const auto c = 360 + toHue - fromHue;
if (std::min(a, std::min(b, c)) > 15) {
return 363;
}
return 255 - fromSaturation;
});
}
template <typename Callback>
void EditorBlock::enumerateRows(Callback callback) {
if (isSearch()) {
for (const auto index : _searchResults) {
if (!callback(_data[index])) {
break;
}
}
} else {
for (auto &row : _data) {
if (!callback(row)) {
break;
}
}
}
}
template <typename Callback>
void EditorBlock::enumerateRows(Callback callback) const {
if (isSearch()) {
for (const auto index : _searchResults) {
if (!callback(_data[index])) {
break;
}
}
} else {
for (const auto &row : _data) {
if (!callback(row)) {
break;
}
}
}
}
template <typename Callback>
void EditorBlock::enumerateRowsFrom(int top, Callback callback) {
auto started = false;
auto index = 0;
enumerateRows([top, callback, &started, &index](Row &row) {
if (!started) {
if (row.top() + row.height() <= top) {
++index;
return true;
}
started = true;
}
return callback(index++, row);
});
}
template <typename Callback>
void EditorBlock::enumerateRowsFrom(int top, Callback callback) const {
auto started = false;
enumerateRows([top, callback, &started](const Row &row) {
if (!started) {
if (row.top() + row.height() <= top) {
return true;
}
started = true;
}
return callback(row);
});
}
int EditorBlock::resizeGetHeight(int newWidth) {
auto result = 0;
auto descriptionWidth = newWidth - st::themeEditorMargin.left() - st::themeEditorMargin.right();
enumerateRows([&](Row &row) {
row.setTop(result);
auto height = row.height();
if (!height) {
height = st::themeEditorMargin.top() + st::themeEditorSampleSize.height();
if (!row.descriptionText().isEmpty()) {
height += st::themeEditorDescriptionSkip + row.descriptionText().countHeight(descriptionWidth);
}
height += st::themeEditorMargin.bottom();
row.setHeight(height);
}
result += row.height();
return true;
});
if (_type == Type::New) {
setHidden(!result);
}
if (_type == Type::Existing && !result && !isSearch()) {
return st::noContactsHeight;
}
return result;
}
void EditorBlock::mousePressEvent(QMouseEvent *e) {
updateSelected(e->pos());
setPressed(_selected);
}
void EditorBlock::mouseReleaseEvent(QMouseEvent *e) {
auto pressed = _pressed;
setPressed(-1);
if (pressed == _selected) {
if (_context->colorEditor.box) {
chooseRow();
} else if (_selected >= 0) {
base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [this, index = findRowIndex(&rowAtIndex(_selected))] {
if (index >= 0 && index < _data.size()) {
activateRow(_data[index]);
}
});
}
}
}
void EditorBlock::saveEditing(QColor value) {
if (_editing < 0) {
return;
}
auto &row = _data[_editing];
auto name = row.name();
if (_type == Type::New) {
setSelected(-1);
setPressed(-1);
auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;
auto color = value;
auto description = row.description();
removeRow(name, false);
_context->appended.fire({ name, possibleCopyOf, color, description });
} else if (_type == Type::Existing) {
removeFromSearch(row);
auto valueChanged = (row.value() != value);
if (valueChanged) {
row.setValue(value);
}
auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;
auto copyOf = checkCopyOf(_editing, possibleCopyOf) ? possibleCopyOf : QString();
auto copyOfChanged = (row.copyOf() != copyOf);
if (copyOfChanged) {
row.setCopyOf(copyOf);
}
addToSearch(row);
if (valueChanged || copyOfChanged) {
checkCopiesChanged(_editing + 1, QStringList(name), value);
_context->pending.fire({ name, copyOf, value });
}
}
cancelEditing();
}
void EditorBlock::checkCopiesChanged(int startIndex, QStringList names, QColor value) {
for (auto i = startIndex, count = static_cast<int>(_data.size()); i != count; ++i) {
auto &checkIfIsCopy = _data[i];
if (names.contains(checkIfIsCopy.copyOf())) {
removeFromSearch(checkIfIsCopy);
checkIfIsCopy.setValue(value);
names.push_back(checkIfIsCopy.name());
addToSearch(checkIfIsCopy);
}
}
if (_type == Type::Existing) {
_context->changed.fire({ names, value });
}
}
void EditorBlock::cancelEditing() {
if (_editing >= 0) {
updateRow(_data[_editing]);
}
_editing = -1;
if (const auto box = base::take(_context->colorEditor.box)) {
box->closeBox();
}
_context->possibleCopyOf = QString();
if (!_context->name.isEmpty()) {
_context->name = QString();
_context->updated.fire({});
}
}
bool EditorBlock::checkCopyOf(int index, const QString &possibleCopyOf) {
auto copyOfIndex = findRowIndex(possibleCopyOf);
return (copyOfIndex >= 0
&& index > copyOfIndex
&& _data[copyOfIndex].value().toRgb() == _data[index].value().toRgb());
}
void EditorBlock::mouseMoveEvent(QMouseEvent *e) {
if (_lastGlobalPos != e->globalPos() || _mouseSelection) {
_lastGlobalPos = e->globalPos();
updateSelected(e->pos());
}
}
void EditorBlock::updateSelected(QPoint localPosition) {
_mouseSelection = true;
auto top = localPosition.y();
auto underMouseIndex = -1;
enumerateRowsFrom(top, [&underMouseIndex, top](int index, const Row &row) {
if (row.top() <= top) {
underMouseIndex = index;
}
return false;
});
setSelected(underMouseIndex);
}
void EditorBlock::leaveEventHook(QEvent *e) {
_mouseSelection = false;
setSelected(-1);
}
void EditorBlock::paintEvent(QPaintEvent *e) {
Painter p(this);
auto clip = e->rect();
if (_data.empty()) {
p.fillRect(clip, st::dialogsBg);
p.setFont(st::noContactsFont);
p.setPen(st::noContactsColor);
p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_theme_editor_no_keys(tr::now));
}
auto cliptop = clip.y();
auto clipbottom = cliptop + clip.height();
enumerateRowsFrom(cliptop, [&](int index, const Row &row) {
if (row.top() >= clipbottom) {
return false;
}
paintRow(p, index, row);
return true;
});
}
void EditorBlock::paintRow(Painter &p, int index, const Row &row) {
auto rowTop = row.top() + st::themeEditorMargin.top();
auto rect = QRect(0, row.top(), width(), row.height());
auto selected = (_pressed >= 0) ? (index == _pressed) : (index == _selected);
auto active = (findRowIndex(&row) == _editing);
p.fillRect(rect, active ? st::dialogsBgActive : selected ? st::dialogsBgOver : st::dialogsBg);
if (auto ripple = row.ripple()) {
ripple->paint(p, 0, row.top(), width(), &(active ? st::activeButtonBgRipple : st::windowBgRipple)->c);
if (ripple->empty()) {
row.resetRipple();
}
}
auto sample = QRect(width() - st::themeEditorMargin.right() - st::themeEditorSampleSize.width(), rowTop, st::themeEditorSampleSize.width(), st::themeEditorSampleSize.height());
Ui::Shadow::paint(p, sample, width(), st::defaultRoundShadow);
if (row.value().alpha() != 255) {
p.fillRect(myrtlrect(sample), _transparent);
}
p.fillRect(myrtlrect(sample), row.value());
auto rowWidth = width() - st::themeEditorMargin.left() - st::themeEditorMargin.right();
auto nameWidth = rowWidth - st::themeEditorSampleSize.width() - st::themeEditorDescriptionSkip;
p.setFont(st::themeEditorNameFont);
p.setPen(active ? st::dialogsNameFgActive : selected ? st::dialogsNameFgOver : st::dialogsNameFg);
p.drawTextLeft(st::themeEditorMargin.left(), rowTop, width(), st::themeEditorNameFont->elided(row.name(), nameWidth));
if (!row.copyOf().isEmpty()) {
auto copyTop = rowTop + st::themeEditorNameFont->height;
p.setFont(st::themeEditorCopyNameFont);
p.drawTextLeft(st::themeEditorMargin.left(), copyTop, width(), st::themeEditorCopyNameFont->elided("= " + row.copyOf(), nameWidth));
}
if (!row.descriptionText().isEmpty()) {
auto descriptionTop = rowTop + st::themeEditorSampleSize.height() + st::themeEditorDescriptionSkip;
p.setPen(active ? st::dialogsTextFgActive : selected ? st::dialogsTextFgOver : st::dialogsTextFg);
row.descriptionText().drawLeft(p, st::themeEditorMargin.left(), descriptionTop, rowWidth, width());
}
if (isEditing() && !active && (_type == Type::New || (_editing >= 0 && findRowIndex(&row) >= _editing))) {
p.fillRect(rect, st::layerBg);
}
}
void EditorBlock::setSelected(int selected) {
if (isEditing()) {
if (_type == Type::New) {
selected = -1;
} else if (_editing >= 0 && selected >= 0 && findRowIndex(&rowAtIndex(selected)) >= _editing) {
selected = -1;
}
}
if (_selected != selected) {
if (_selected >= 0) updateRow(rowAtIndex(_selected));
_selected = selected;
if (_selected >= 0) updateRow(rowAtIndex(_selected));
setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default);
}
}
void EditorBlock::setPressed(int pressed) {
if (_pressed != pressed) {
if (_pressed >= 0) {
updateRow(rowAtIndex(_pressed));
stopLastRipple(_pressed);
}
_pressed = pressed;
if (_pressed >= 0) {
addRowRipple(_pressed);
updateRow(rowAtIndex(_pressed));
}
}
}
void EditorBlock::addRowRipple(int index) {
auto &row = rowAtIndex(index);
auto ripple = row.ripple();
if (!ripple) {
auto mask = Ui::RippleAnimation::RectMask(QSize(width(), row.height()));
ripple = row.setRipple(std::make_unique<Ui::RippleAnimation>(st::defaultRippleAnimation, std::move(mask), [this, index = findRowIndex(&row)] {
updateRow(_data[index]);
}));
}
auto origin = mapFromGlobal(QCursor::pos()) - QPoint(0, row.top());
ripple->add(origin);
}
void EditorBlock::stopLastRipple(int index) {
auto &row = rowAtIndex(index);
if (row.ripple()) {
row.ripple()->lastStop();
}
}
void EditorBlock::updateRow(const Row &row) {
update(0, row.top(), width(), row.height());
}
void EditorBlock::addRow(const QString &name, const QString &copyOf, QColor value) {
_data.push_back({ name, copyOf, value });
_indices.insert(name, _data.size() - 1);
addToSearch(_data.back());
}
EditorBlock::Row &EditorBlock::rowAtIndex(int index) {
if (isSearch()) {
return _data[_searchResults[index]];
}
return _data[index];
}
int EditorBlock::findRowIndex(const QString &name) const {
return _indices.value(name, -1);
}
EditorBlock::Row *EditorBlock::findRow(const QString &name) {
auto index = findRowIndex(name);
return (index >= 0) ? &_data[index] : nullptr;
}
int EditorBlock::findRowIndex(const Row *row) {
return row ? (row - &_data[0]) : -1;
}
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,169 @@
/*
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 BoxContent;
} // namespace Ui;
class ColorEditor;
namespace Window {
namespace Theme {
class EditorBlock final : public Ui::RpWidget {
public:
enum class Type {
Existing,
New,
};
struct Context {
struct {
base::weak_qptr<Ui::BoxContent> box;
base::weak_qptr<ColorEditor> editor;
} colorEditor;
QString name;
QString possibleCopyOf;
rpl::event_stream<> updated;
rpl::event_stream<> resized;
struct AppendData {
QString name;
QString possibleCopyOf;
QColor value;
QString description;
};
rpl::event_stream<AppendData> appended;
struct ChangeData {
QStringList names;
QColor value;
};
rpl::event_stream<ChangeData> changed;
struct EditionData {
QString name;
QString copyOf;
QColor value;
};
rpl::event_stream<EditionData> pending;
struct ScrollData {
Type type = {};
int position = 0;
int height = 0;
};
rpl::event_stream<ScrollData> scroll;
};
EditorBlock(QWidget *parent, Type type, Context *context);
void filterRows(const QString &query);
void chooseRow();
bool hasSelected() const {
return (_selected >= 0);
}
void clearSelected() {
setSelected(-1);
}
bool selectSkip(int direction);
void feed(const QString &name, QColor value, const QString &copyOfExisting = QString());
bool feedCopy(const QString &name, const QString &copyOf);
const QColor *find(const QString &name);
bool feedDescription(const QString &name, const QString &description);
void sortByDistance(const QColor &to);
protected:
void paintEvent(QPaintEvent *e) override;
int resizeGetHeight(int newWidth) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void leaveEventHook(QEvent *e) override;
private:
class Row;
void addRow(const QString &name, const QString &copyOf, QColor value);
void removeRow(const QString &name, bool removeCopyReferences = true);
void addToSearch(const Row &row);
void removeFromSearch(const Row &row);
template <typename Callback>
void enumerateRows(Callback callback);
template <typename Callback>
void enumerateRows(Callback callback) const;
template <typename Callback>
void enumerateRowsFrom(int top, Callback callback);
template <typename Callback>
void enumerateRowsFrom(int top, Callback callback) const;
Row &rowAtIndex(int index);
int findRowIndex(const QString &name) const;
Row *findRow(const QString &name);
int findRowIndex(const Row *row);
void updateRow(const Row &row);
void paintRow(Painter &p, int index, const Row &row);
void updateSelected(QPoint localPosition);
void setSelected(int selected);
void setPressed(int pressed);
void addRowRipple(int index);
void stopLastRipple(int index);
void scrollToSelected();
bool isEditing() const {
return !_context->name.isEmpty();
}
void saveEditing(QColor value);
void cancelEditing();
bool checkCopyOf(int index, const QString &possibleCopyOf);
void checkCopiesChanged(int startIndex, QStringList names, QColor value);
void activateRow(const Row &row);
bool isSearch() const {
return !_searchQuery.isEmpty();
}
void searchByQuery(QString query);
void resetSearch() {
searchByQuery(QString());
}
Type _type = Type::Existing;
Context *_context = nullptr;
std::vector<Row> _data;
QMap<QString, int> _indices;
QString _searchQuery;
std::vector<int> _searchResults;
base::flat_map<QChar, base::flat_set<int>> _searchIndex;
int _selected = -1;
int _pressed = -1;
int _editing = -1;
QPoint _lastGlobalPos;
bool _mouseSelection = false;
QBrush _transparent;
};
} // namespace Theme
} // namespace Window

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
/*
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 Data {
struct CloudTheme;
} // namespace Data
namespace Window {
class Controller;
namespace Theme {
struct Object;
struct ParsedTheme;
void StartEditor(
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud);
void CreateBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window);
void CreateForExistingBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud);
void SaveTheme(
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud,
const QByteArray &palette,
Fn<void()> unlock);
void SaveThemeBox(
not_null<Ui::GenericBox*> box,
not_null<Window::Controller*> window,
const Data::CloudTheme &cloud,
const QByteArray &palette);
[[nodiscard]] bool PaletteChanged(
const QByteArray &editorPalette,
const Data::CloudTheme &cloud);
[[nodiscard]] QByteArray CollectForExport(const QByteArray &palette);
[[nodiscard]] ParsedTheme ParseTheme(
const Object &theme,
bool onlyPalette = false,
bool parseCurrent = true);
[[nodiscard]] QString GenerateSlug();
} // namespace Theme
} // namespace Window

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
/*
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 "window/themes/window_theme.h"
namespace Data {
struct CloudTheme;
} // namespace Data
namespace Window {
namespace Theme {
struct CurrentData {
WallPaperId backgroundId = 0;
QImage backgroundImage;
bool backgroundTiled = false;
};
enum class PreviewType {
Normal,
Extended,
};
[[nodiscard]] QString CachedThemePath(uint64 documentId);
std::unique_ptr<Preview> PreviewFromFile(
const QByteArray &bytes,
const QString &filepath,
const Data::CloudTheme &cloud);
std::unique_ptr<Preview> GeneratePreview(
const QByteArray &bytes,
const QString &filepath,
const Data::CloudTheme &cloud,
CurrentData &&data,
PreviewType type);
QImage GeneratePreview(
const QByteArray &bytes,
const QString &filepath);
int DefaultPreviewTitleHeight();
void DefaultPreviewWindowFramePaint(
QImage &preview,
const style::palette &palette,
QRect body,
int outerWidth);
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,150 @@
/*
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_theme_warning.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "ui/cached_round_corners.h"
#include "window/themes/window_theme.h"
#include "lang/lang_keys.h"
#include "styles/style_layers.h"
#include "styles/style_boxes.h"
namespace Window {
namespace Theme {
namespace {
constexpr int kWaitBeforeRevertMs = 15999;
} // namespace
WarningWidget::WarningWidget(QWidget *parent)
: RpWidget(parent)
, _timer([=] { handleTimer(); })
, _secondsLeft(kWaitBeforeRevertMs / 1000)
, _keepChanges(this, tr::lng_theme_keep_changes(), st::defaultBoxButton)
, _revert(this, tr::lng_theme_revert(), st::defaultBoxButton) {
using TextTransform = Ui::RoundButton::TextTransform;
_keepChanges->setTextTransform(TextTransform::NoTransform);
_keepChanges->setClickedCallback([] { KeepApplied(); });
_revert->setTextTransform(TextTransform::NoTransform);
_revert->setClickedCallback([] { Revert(); });
updateText();
}
void WarningWidget::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
Window::Theme::Revert();
}
}
void WarningWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
if (!_cache.isNull()) {
if (!_animation.animating()) {
if (isHidden()) {
return;
}
}
p.setOpacity(_animation.value(_hiding ? 0. : 1.));
p.drawPixmap(_outer.topLeft(), _cache);
if (!_animation.animating()) {
_cache = QPixmap();
showChildren();
_started = crl::now();
_timer.callOnce(100);
}
return;
}
Ui::Shadow::paint(p, _inner, width(), st::boxRoundShadow);
Ui::FillRoundRect(p, _inner, st::boxBg, Ui::BoxCorners);
p.setFont(st::boxTitleFont);
p.setPen(st::boxTitleFg);
p.drawTextLeft(_inner.x() + st::boxTitlePosition.x(), _inner.y() + st::boxTitlePosition.y(), width(), tr::lng_theme_sure_keep(tr::now));
p.setFont(st::boxTextFont);
p.setPen(st::boxTextFg);
p.drawTextLeft(_inner.x() + st::boxTitlePosition.x(), _inner.y() + st::themeWarningTextTop, width(), _text);
}
void WarningWidget::resizeEvent(QResizeEvent *e) {
_inner = QRect((width() - st::themeWarningWidth) / 2, (height() - st::themeWarningHeight) / 2, st::themeWarningWidth, st::themeWarningHeight);
_outer = _inner.marginsAdded(st::boxRoundShadow.extend);
updateControlsGeometry();
update();
}
void WarningWidget::updateControlsGeometry() {
auto left = _inner.x() + _inner.width() - st::defaultBox.buttonPadding.right() - _keepChanges->width();
_keepChanges->moveToLeft(left, _inner.y() + _inner.height() - st::defaultBox.buttonPadding.bottom() - _keepChanges->height());
_revert->moveToLeft(left - st::defaultBox.buttonPadding.left() - _revert->width(), _keepChanges->y());
}
void WarningWidget::refreshLang() {
InvokeQueued(this, [this] { updateControlsGeometry(); });
}
void WarningWidget::handleTimer() {
auto msPassed = crl::now() - _started;
setSecondsLeft((kWaitBeforeRevertMs - msPassed) / 1000);
}
void WarningWidget::setSecondsLeft(int secondsLeft) {
if (secondsLeft <= 0) {
Window::Theme::Revert();
} else {
if (_secondsLeft != secondsLeft) {
_secondsLeft = secondsLeft;
updateText();
update();
}
_timer.callOnce(100);
}
}
void WarningWidget::updateText() {
_text = tr::lng_theme_reverting(tr::now, lt_count, _secondsLeft);
}
void WarningWidget::showAnimated() {
startAnimation(false);
show();
setFocus();
}
void WarningWidget::hideAnimated() {
startAnimation(true);
}
void WarningWidget::startAnimation(bool hiding) {
_timer.cancel();
_hiding = hiding;
if (_cache.isNull()) {
showChildren();
Ui::SendPendingMoveResizeEvents(this);
_cache = Ui::GrabWidget(this, _outer);
}
hideChildren();
_animation.start([this] {
update();
if (_hiding) {
hide();
if (_hiddenCallback) {
_hiddenCallback();
}
}
}, _hiding ? 1. : 0., _hiding ? 0. : 1., st::boxDuration);
}
} // namespace Theme
} // namespace Window

View 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
#include "base/timer.h"
#include "base/object_ptr.h"
#include "ui/effects/animations.h"
#include "ui/rp_widget.h"
namespace Ui {
class RoundButton;
} // namespace Ui
namespace Window {
namespace Theme {
class WarningWidget : public Ui::RpWidget {
public:
WarningWidget(QWidget *parent);
void setHiddenCallback(Fn<void()> callback) {
_hiddenCallback = std::move(callback);
}
void showAnimated();
void hideAnimated();
protected:
void keyPressEvent(QKeyEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
private:
void refreshLang();
void updateControlsGeometry();
void setSecondsLeft(int secondsLeft);
void startAnimation(bool hiding);
void updateText();
void handleTimer();
bool _hiding = false;
Ui::Animations::Simple _animation;
QPixmap _cache;
QRect _inner, _outer;
base::Timer _timer;
crl::time _started = 0;
int _secondsLeft = 0;
QString _text;
object_ptr<Ui::RoundButton> _keepChanges;
object_ptr<Ui::RoundButton> _revert;
Fn<void()> _hiddenCallback;
};
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,751 @@
/*
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_document_media.h"
#include "data/data_session.h"
#include "ui/chat/chat_theme.h"
#include "ui/image/image_prepare.h"
#include "ui/widgets/popup_menu.h"
#include "ui/toast/toast.h"
#include "ui/style/style_palette_colorizer.h"
#include "ui/boxes/confirm_box.h"
#include "ui/painter.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "core/application.h"
#include "styles/style_settings.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
#include "styles/style_menu_icons.h"
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
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 * style::DevicePixelRatio();
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, nullptr)) {
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) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.background.fill(scheme.background);
return result;
}
CloudListColors ColorsFromScheme(
const EmbeddedScheme &scheme,
const style::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;
if (!_colors->background.isNull()) {
const auto size = st::settingsThemePreviewSize
* style::DevicePixelRatio();
_backgroundFull = (_colors->background.size() == size)
? _colors->background
: _colors->background.scaled(
size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
_backgroundCacheWidth = -1;
ensureContrast();
_radio.setToggledOverride(_colors->radiobuttonActive);
_radio.setUntoggledOverride(_colors->radiobuttonInactive);
}
update();
}
void CloudListCheck::ensureContrast() {
const auto radio = _radio.getSize();
const auto x = (getSize().width() - radio.width()) / 2;
const auto y = getSize().height()
- radio.height()
- st::settingsThemeRadioBottom;
const auto under = QRect(
QPoint(x, y) * style::DevicePixelRatio(),
radio * style::DevicePixelRatio());
const auto image = _backgroundFull.copy(under).convertToFormat(
QImage::Format_ARGB32_Premultiplied);
const auto active = style::internal::EnsureContrast(
_colors->radiobuttonActive,
Ui::CountAverageColor(image));
_colors->radiobuttonInactive = _colors->radiobuttonActive = QColor(
active.red(),
active.green(),
active.blue(),
255);
_colors->radiobuttonInactive.setAlpha(192);
}
QSize CloudListCheck::getSize() const {
return st::settingsThemePreviewSize;
}
void CloudListCheck::validateBackgroundCache(int width) {
if (_backgroundCacheWidth == width || width <= 0) {
return;
}
_backgroundCacheWidth = width;
const auto imageWidth = width * style::DevicePixelRatio();
_backgroundCache = (width == st::settingsThemePreviewSize.width())
? _backgroundFull
: _backgroundFull.copy(
(_backgroundFull.width() - imageWidth) / 2,
0,
imageWidth,
_backgroundFull.height());
_backgroundCache = Images::Round(
std::move(_backgroundCache),
ImageRoundRadius::Large);
_backgroundCache.setDevicePixelRatio(style::DevicePixelRatio());
}
void CloudListCheck::paint(QPainter &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(
QPainter &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::roundRadiusLarge;
p.drawRoundedRect(rect, radius, radius);
st::settingsThemeNotSupportedIcon.paintInCenter(p, rect);
}
void CloudListCheck::paintWithColors(
QPainter &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(style::rtlrect(received, outerWidth), radius, radius);
p.setBrush(_colors->sent);
p.drawRoundedRect(style::rtlrect(sent, outerWidth), radius, radius);
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) | rpl::then(
_window->session().data().cloudThemes().updated()
);
auto themeChanges = rpl::single(BackgroundUpdate(
BackgroundUpdate::Type::ApplyingTheme,
Background()->tile()
)) | rpl::then(
Background()->updates()
) | 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::on_next([=](std::vector<Data::CloudTheme> &&list) {
rebuildUsing(std::move(list));
}, _outer->lifetime());
_outer->widthValue(
) | rpl::on_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::views::all(
list
) | ranges::views::filter([&](const Data::CloudTheme &theme) {
const auto i = ranges::find(_elements, theme.id, &Element::id);
return (i == end(_elements));
}) | ranges::views::take(insertCount);
for (const auto &theme : insertElements) {
const auto good = isGood(theme);
insert(good ? positionForGood : positionForBad, theme);
if (good) {
++positionForGood;
}
++positionForBad;
}
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->current() == 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->setAcceptBoth(true);
button->addClickHandler([=](Qt::MouseButton button) {
const auto i = ranges::find(_elements, id, &Element::id);
if (i == end(_elements)
|| id == kFakeCloudThemeId
|| i->waiting) {
return;
}
const auto &cloud = i->theme;
if (button == Qt::RightButton) {
showMenu(*i);
} else if (cloud.documentId) {
_window->session().data().cloudThemes().applyFromDocument(cloud);
} else {
_window->session().data().cloudThemes().showPreview(
&_window->window(),
cloud);
}
});
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) {
const auto currentId = Background()->themeObject().cloud.id;
const auto &theme = element.theme;
const auto document = theme.documentId
? _window->session().data().document(theme.documentId).get()
: nullptr;
if (element.id() == kFakeCloudThemeId
|| ((element.id() == currentId)
&& (!document || !document->isTheme()))) {
element.check->setColors(ColorsFromCurrentTheme());
} else if (document) {
element.media = document ? document->createMediaView() : nullptr;
document->save(
Data::FileOriginTheme(theme.id, theme.accessHash),
QString());
if (element.media->loaded()) {
refreshColorsFromDocument(element);
} else {
setWaiting(element, true);
subscribeToDownloadFinished();
}
} else {
element.check->setColors(CloudListColors());
}
}
void CloudList::showMenu(Element &element) {
if (_contextMenu) {
_contextMenu = nullptr;
return;
}
_contextMenu = base::make_unique_q<Ui::PopupMenu>(
element.button.get(),
st::popupMenuWithIcons);
const auto cloud = element.theme;
if (const auto slug = element.theme.slug; !slug.isEmpty()) {
_contextMenu->addAction(tr::lng_theme_share(tr::now), [=] {
QGuiApplication::clipboard()->setText(
_window->session().createInternalLinkFull("addtheme/" + slug));
_window->window().showToast(
tr::lng_background_link_copied(tr::now));
}, &st::menuIconShare);
}
if (cloud.documentId
&& cloud.createdBy == _window->session().userId()
&& Background()->themeObject().cloud.id == cloud.id) {
_contextMenu->addAction(tr::lng_theme_edit(tr::now), [=] {
StartEditor(&_window->window(), cloud);
}, &st::menuIconChangeColors);
}
const auto id = cloud.id;
_contextMenu->addAction(tr::lng_theme_delete(tr::now), [=] {
const auto remove = [=](Fn<void()> &&close) {
close();
if (Background()->themeObject().cloud.id == id
|| id == kFakeCloudThemeId) {
if (Background()->editingTheme().has_value()) {
Background()->clearEditingTheme(
ClearEditing::KeepChanges);
_window->window().showRightColumn(nullptr);
}
ResetToSomeDefault();
KeepApplied();
}
if (id != kFakeCloudThemeId) {
_window->session().data().cloudThemes().remove(id);
}
};
_window->window().show(Ui::MakeConfirmBox({
.text = tr::lng_theme_delete_sure(),
.confirmed = remove,
.confirmText = tr::lng_theme_delete(),
}));
}, &st::menuIconDelete);
_contextMenu->popup(QCursor::pos());
}
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) {
Expects(element.media != nullptr);
Expects(element.media->loaded());
const auto id = element.id();
const auto path = element.media->owner()->filepath();
const auto data = base::take(element.media)->bytes();
crl::async([=, guard = element.generating.make_guard()]() mutable {
crl::on_main(std::move(guard), [
=,
result = ColorsFromTheme(path, data)
]() mutable {
const auto i = ranges::find(_elements, id, &Element::id);
if (i == end(_elements) || !result) {
return;
}
auto &element = *i;
if (result->background.isNull()) {
result->background = ColorsFromCurrentTheme().background;
}
element.check->setColors(*result);
setWaiting(element, false);
});
});
}
void CloudList::subscribeToDownloadFinished() {
if (_downloadFinishedLifetime) {
return;
}
_window->session().downloaderTaskFinished(
) | rpl::on_next([=] {
auto &&waiting = _elements | ranges::views::filter(&Element::waiting);
const auto still = ranges::count_if(waiting, [&](Element &element) {
if (!element.media) {
element.waiting = false;
return false;
} else if (!element.media->loaded()) {
return true;
}
refreshColorsFromDocument(element);
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 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->resizeToWidth(single);
button->moveToLeft(int(base::SafeRound(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

View File

@@ -0,0 +1,138 @@
/*
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 "data/data_cloud_themes.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/checkbox.h"
#include "base/unique_qptr.h"
#include "base/binary_guard.h"
namespace style {
struct colorizer;
} // namespace style
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Window {
class SessionController;
namespace Theme {
struct EmbeddedScheme;
struct CloudListColors {
QImage background;
QColor sent;
QColor received;
QColor radiobuttonInactive;
QColor radiobuttonActive;
};
[[nodiscard]] CloudListColors ColorsFromScheme(const EmbeddedScheme &scheme);
[[nodiscard]] CloudListColors ColorsFromScheme(
const EmbeddedScheme &scheme,
const style::colorizer &colorizer);
class CloudListCheck final : public Ui::AbstractCheckView {
public:
using Colors = CloudListColors;
explicit CloudListCheck(bool checked);
CloudListCheck(const Colors &colors, bool checked);
QSize getSize() const override;
void paint(
QPainter &p,
int left,
int top,
int outerWidth) override;
QImage prepareRippleMask() const override;
bool checkRippleStartPosition(QPoint position) const override;
void setColors(const Colors &colors);
private:
void paintNotSupported(QPainter &p, int left, int top, int outerWidth);
void paintWithColors(QPainter &p, int left, int top, int outerWidth);
void checkedChangedHook(anim::type animated) override;
void validateBackgroundCache(int width);
void ensureContrast();
std::optional<Colors> _colors;
Ui::RadioView _radio;
QImage _backgroundFull;
QImage _backgroundCache;
int _backgroundCacheWidth = -1;
};
class CloudList final {
public:
CloudList(
not_null<QWidget*> parent,
not_null<Window::SessionController*> window);
void showAll();
[[nodiscard]] rpl::producer<bool> empty() const;
[[nodiscard]] rpl::producer<bool> allShown() const;
[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();
private:
struct Element {
Data::CloudTheme theme;
not_null<CloudListCheck*> check;
std::unique_ptr<Ui::Radiobutton> button;
std::shared_ptr<Data::DocumentMedia> media;
base::binary_guard generating;
bool waiting = false;
uint64 id() const {
return theme.id;
}
};
void setup();
[[nodiscard]] std::vector<Data::CloudTheme> collectAll() const;
void rebuildUsing(std::vector<Data::CloudTheme> &&list);
bool applyChangesFrom(std::vector<Data::CloudTheme> &&list);
bool removeStaleUsing(const std::vector<Data::CloudTheme> &list);
bool insertTillLimit(
const std::vector<Data::CloudTheme> &list,
int limit);
void refreshElementUsing(Element &element, const Data::CloudTheme &data);
void insert(int index, const Data::CloudTheme &theme);
void refreshColors(Element &element);
void showMenu(Element &element);
void refreshColorsFromDocument(Element &element);
void setWaiting(Element &element, bool waiting);
void subscribeToDownloadFinished();
int resizeGetHeight(int newWidth);
void updateGeometry();
[[nodiscard]] bool amCreator(const Data::CloudTheme &theme) const;
[[nodiscard]] int groupValueForId(uint64 id);
const not_null<Window::SessionController*> _window;
object_ptr<Ui::RpWidget> _owned;
const not_null<Ui::RpWidget*> _outer;
const std::shared_ptr<Ui::RadiobuttonGroup> _group;
rpl::variable<bool> _showAll = false;
rpl::variable<int> _count = 0;
std::vector<Element> _elements;
std::vector<uint64> _idByGroupValue;
base::flat_map<uint64, int> _groupValueById;
rpl::lifetime _downloadFinishedLifetime;
base::unique_qptr<Ui::PopupMenu> _contextMenu;
};
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,431 @@
/*
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_embedded.h"
#include "window/themes/window_theme.h"
#include "lang/lang_keys.h"
#include "storage/serialize_common.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "ui/style/style_palette_colorizer.h"
namespace Window {
namespace Theme {
namespace {
constexpr auto kMaxAccentColors = 3;
constexpr auto kDayBaseFile = ":/gui/day-custom-base.tdesktop-theme"_cs;
constexpr auto kNightBaseFile = ":/gui/night-custom-base.tdesktop-theme"_cs;
const auto kColorizeIgnoredKeys = base::flat_set<QLatin1String>{ {
qstr("boxTextFgGood"),
qstr("boxTextFgError"),
qstr("callIconFg"),
qstr("historyPeer1NameFg"),
qstr("historyPeer1NameFgSelected"),
qstr("historyPeer1UserpicBg"),
qstr("historyPeer2NameFg"),
qstr("historyPeer2NameFgSelected"),
qstr("historyPeer2UserpicBg"),
qstr("historyPeer3NameFg"),
qstr("historyPeer3NameFgSelected"),
qstr("historyPeer3UserpicBg"),
qstr("historyPeer4NameFg"),
qstr("historyPeer4NameFgSelected"),
qstr("historyPeer4UserpicBg"),
qstr("historyPeer5NameFg"),
qstr("historyPeer5NameFgSelected"),
qstr("historyPeer5UserpicBg"),
qstr("historyPeer6NameFg"),
qstr("historyPeer6NameFgSelected"),
qstr("historyPeer6UserpicBg"),
qstr("historyPeer7NameFg"),
qstr("historyPeer7NameFgSelected"),
qstr("historyPeer7UserpicBg"),
qstr("historyPeer8NameFg"),
qstr("historyPeer8NameFgSelected"),
qstr("historyPeer8UserpicBg"),
qstr("historyPeer1UserpicBg2"),
qstr("historyPeer2UserpicBg2"),
qstr("historyPeer3UserpicBg2"),
qstr("historyPeer4UserpicBg2"),
qstr("historyPeer5UserpicBg2"),
qstr("historyPeer6UserpicBg2"),
qstr("historyPeer7UserpicBg2"),
qstr("historyPeer8UserpicBg2"),
qstr("msgFile1Bg"),
qstr("msgFile1BgDark"),
qstr("msgFile1BgOver"),
qstr("msgFile1BgSelected"),
qstr("msgFile2Bg"),
qstr("msgFile2BgDark"),
qstr("msgFile2BgOver"),
qstr("msgFile2BgSelected"),
qstr("msgFile3Bg"),
qstr("msgFile3BgDark"),
qstr("msgFile3BgOver"),
qstr("msgFile3BgSelected"),
qstr("msgFile4Bg"),
qstr("msgFile4BgDark"),
qstr("msgFile4BgOver"),
qstr("msgFile4BgSelected"),
qstr("mediaviewFileRedCornerFg"),
qstr("mediaviewFileYellowCornerFg"),
qstr("mediaviewFileGreenCornerFg"),
qstr("mediaviewFileBlueCornerFg"),
qstr("settingsIconBg1"),
qstr("settingsIconBg2"),
qstr("settingsIconBg3"),
qstr("settingsIconBg4"),
qstr("settingsIconBg5"),
qstr("settingsIconBg6"),
qstr("settingsIconBg8"),
qstr("settingsIconBgArchive"),
qstr("premiumButtonBg1"),
qstr("premiumButtonBg2"),
qstr("premiumButtonBg3"),
qstr("premiumIconBg1"),
qstr("premiumIconBg2"),
} };
style::colorizer::Color cColor(std::string_view hex) {
const auto q = style::ColorFromHex(hex);
auto hue = int();
auto saturation = int();
auto value = int();
q.getHsv(&hue, &saturation, &value);
return style::colorizer::Color{ hue, saturation, value };
}
} // namespace
style::colorizer ColorizerFrom(
const EmbeddedScheme &scheme,
const QColor &color) {
using Color = style::colorizer::Color;
using Pair = std::pair<Color, Color>;
auto result = style::colorizer();
result.ignoreKeys = kColorizeIgnoredKeys;
result.hueThreshold = 15;
scheme.accentColor.getHsv(
&result.was.hue,
&result.was.saturation,
&result.was.value);
color.getHsv(
&result.now.hue,
&result.now.saturation,
&result.now.value);
switch (scheme.type) {
case EmbeddedType::Default:
result.lightnessMax = 160;
break;
case EmbeddedType::DayBlue:
result.lightnessMax = 160;
break;
case EmbeddedType::Night:
result.keepContrast = base::flat_map<QLatin1String, Pair>{ {
//{ qstr("windowFgActive"), Pair{ cColor("5288c1"), cColor("17212b") } }, // windowBgActive
{ qstr("activeButtonFg"), Pair{ cColor("2f6ea5"), cColor("17212b") } }, // activeButtonBg
{ qstr("profileVerifiedCheckFg"), Pair{ cColor("5288c1"), cColor("17212b") } }, // profileVerifiedCheckBg
{ qstr("overviewCheckFgActive"), Pair{ cColor("5288c1"), cColor("17212b") } }, // overviewCheckBgActive
{ qstr("historyFileInIconFg"), Pair{ cColor("3f96d0"), cColor("182533") } }, // msgFileInBg, msgInBg
{ qstr("historyFileInIconFgSelected"), Pair{ cColor("6ab4f4"), cColor("2e70a5") } }, // msgFileInBgSelected, msgInBgSelected
{ qstr("historyFileInRadialFg"), Pair{ cColor("3f96d0"), cColor("182533") } }, // msgFileInBg, msgInBg
{ qstr("historyFileInRadialFgSelected"), Pair{ cColor("6ab4f4"), cColor("2e70a5") } }, // msgFileInBgSelected, msgInBgSelected
{ qstr("historyFileOutIconFg"), Pair{ cColor("4c9ce2"), cColor("2b5278") } }, // msgFileOutBg, msgOutBg
{ qstr("historyFileOutIconFgSelected"), Pair{ cColor("58abf3"), cColor("2e70a5") } }, // msgFileOutBgSelected, msgOutBgSelected
{ qstr("historyFileOutRadialFg"), Pair{ cColor("4c9ce2"), cColor("2b5278") } }, // msgFileOutBg, msgOutBg
{ qstr("historyFileOutRadialFgSelected"), Pair{ cColor("58abf3"), cColor("2e70a5") } }, // msgFileOutBgSelected, msgOutBgSelected
} };
result.lightnessMin = 64;
break;
case EmbeddedType::NightGreen:
result.keepContrast = base::flat_map<QLatin1String, Pair>{ {
//{ qstr("windowFgActive"), Pair{ cColor("3fc1b0"), cColor("282e33") } }, // windowBgActive, windowBg
{ qstr("activeButtonFg"), Pair{ cColor("2da192"), cColor("282e33") } }, // activeButtonBg, windowBg
{ qstr("profileVerifiedCheckFg"), Pair{ cColor("3fc1b0"), cColor("282e33") } }, // profileVerifiedCheckBg, windowBg
{ qstr("overviewCheckFgActive"), Pair{ cColor("3fc1b0"), cColor("282e33") } }, // overviewCheckBgActive
// callIconFg is used not only over callAnswerBg,
// so this contrast-forcing breaks other buttons.
//{ qstr("callIconFg"), Pair{ cColor("5ad1c1"), cColor("1b1f23") } }, // callAnswerBg, callBgOpaque
} };
result.lightnessMin = 64;
break;
}
const auto nowLightness = color.lightness();
const auto limitedLightness = std::clamp(
nowLightness,
result.lightnessMin,
result.lightnessMax);
if (limitedLightness != nowLightness) {
QColor::fromHsl(
color.hslHue(),
color.hslSaturation(),
limitedLightness).getHsv(
&result.now.hue,
&result.now.saturation,
&result.now.value);
}
return result;
}
style::colorizer ColorizerForTheme(const QString &absolutePath) {
if (!IsEmbeddedTheme(absolutePath)) {
return {};
}
const auto schemes = EmbeddedThemes();
const auto i = ranges::find(
schemes,
absolutePath,
&EmbeddedScheme::path);
if (i == end(schemes)) {
return {};
}
const auto &colors = Core::App().settings().themesAccentColors();
if (const auto accent = colors.get(i->type)) {
return ColorizerFrom(*i, *accent);
}
return {};
}
void Colorize(EmbeddedScheme &scheme, const style::colorizer &colorizer) {
const auto colors = {
&EmbeddedScheme::background,
&EmbeddedScheme::sent,
&EmbeddedScheme::received,
&EmbeddedScheme::radiobuttonActive,
&EmbeddedScheme::radiobuttonInactive
};
for (const auto color : colors) {
if (const auto changed = style::colorize(scheme.*color, colorizer)) {
scheme.*color = changed->toRgb();
}
}
}
std::vector<EmbeddedScheme> EmbeddedThemes() {
const auto qColor = [](auto hex) {
return style::ColorFromHex(hex);
};
const auto name = [](auto key) {
return rpl::deferred([=] { return key(); });
};
return {
EmbeddedScheme{
EmbeddedType::Default,
qColor("9bd494"),
qColor("eaffdc"),
qColor("ffffff"),
qColor("eaffdc"),
qColor("ffffff"),
name(tr::lng_settings_theme_classic),
QString(),
qColor("40a7e3")
},
EmbeddedScheme{
EmbeddedType::DayBlue,
qColor("7ec4ea"),
qColor("d7f0ff"),
qColor("ffffff"),
qColor("d7f0ff"),
qColor("ffffff"),
name(tr::lng_settings_theme_day),
":/gui/day-blue.tdesktop-theme",
qColor("40a7e3")
},
EmbeddedScheme{
EmbeddedType::Night,
qColor("485761"),
qColor("5ca7d4"),
qColor("6b808d"),
qColor("6b808d"),
qColor("5ca7d4"),
name(tr::lng_settings_theme_tinted),
":/gui/night.tdesktop-theme",
qColor("5288c1")
},
EmbeddedScheme{
EmbeddedType::NightGreen,
qColor("485761"),
qColor("6b808d"),
qColor("6b808d"),
qColor("6b808d"),
qColor("75bfb5"),
name(tr::lng_settings_theme_night),
":/gui/night-green.tdesktop-theme",
qColor("3fc1b0")
},
};
}
std::vector<QColor> DefaultAccentColors(EmbeddedType type) {
const auto qColor = [](auto hex) {
return style::ColorFromHex(hex);
};
switch (type) {
case EmbeddedType::DayBlue:
return {
qColor("45bce7"),
qColor("52b440"),
qColor("d46c99"),
qColor("df8a49"),
qColor("9978c8"),
qColor("c55245"),
qColor("687b98"),
qColor("dea922"),
};
case EmbeddedType::Default:
return {
qColor("45bce7"),
qColor("52b440"),
qColor("d46c99"),
qColor("df8a49"),
qColor("9978c8"),
qColor("c55245"),
qColor("687b98"),
qColor("dea922"),
};
case EmbeddedType::Night:
return {
qColor("58bfe8"),
qColor("466f42"),
qColor("aa6084"),
qColor("a46d3c"),
qColor("917bbd"),
qColor("ab5149"),
qColor("697b97"),
qColor("9b834b"),
};
case EmbeddedType::NightGreen:
return {
qColor("60a8e7"),
qColor("4e9c57"),
qColor("ca7896"),
qColor("cc925c"),
qColor("a58ed2"),
qColor("d27570"),
qColor("7b8799"),
qColor("cbac67"),
};
}
Unexpected("Type in Window::Theme::AccentColors.");
}
Fn<void(style::palette&)> PreparePaletteCallback(
bool dark,
std::optional<QColor> accent) {
return [=](style::palette &palette) {
using namespace Theme;
const auto &embedded = EmbeddedThemes();
const auto i = ranges::find(
embedded,
dark ? EmbeddedType::Night : EmbeddedType::Default,
&EmbeddedScheme::type);
Assert(i != end(embedded));
const auto colorizer = accent
? ColorizerFrom(*i, *accent)
: style::colorizer();
auto instance = Instance();
const auto loaded = LoadFromFile(
(dark ? kNightBaseFile : kDayBaseFile).utf16(),
&instance,
nullptr,
nullptr,
colorizer);
Assert(loaded);
palette.finalize();
palette = instance.palette;
};
}
Fn<void(style::palette&)> PrepareCurrentPaletteCallback() {
return [=, data = style::main_palette::save()](style::palette &palette) {
palette.load(data);
};
}
QByteArray AccentColors::serialize() const {
auto result = QByteArray();
if (_data.empty()) {
return result;
}
const auto count = _data.size();
auto size = sizeof(qint32) * (count + 1)
+ Serialize::colorSize() * count;
result.reserve(size);
auto stream = QDataStream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream << qint32(_data.size());
for (const auto &[type, color] : _data) {
stream << static_cast<qint32>(type);
Serialize::writeColor(stream, color);
}
stream.device()->close();
return result;
}
bool AccentColors::setFromSerialized(const QByteArray &serialized) {
if (serialized.isEmpty()) {
_data.clear();
return true;
}
auto copy = QByteArray(serialized);
auto stream = QDataStream(&copy, QIODevice::ReadOnly);
stream.setVersion(QDataStream::Qt_5_1);
auto count = qint32();
stream >> count;
if (stream.status() != QDataStream::Ok) {
return false;
} else if (count <= 0 || count > kMaxAccentColors) {
return false;
}
auto data = base::flat_map<EmbeddedType, QColor>();
for (auto i = 0; i != count; ++i) {
auto type = qint32();
stream >> type;
const auto color = Serialize::readColor(stream);
const auto uncheckedType = static_cast<EmbeddedType>(type);
switch (uncheckedType) {
case EmbeddedType::Default:
case EmbeddedType::DayBlue:
case EmbeddedType::Night:
case EmbeddedType::NightGreen:
data.emplace(uncheckedType, color);
break;
default:
return false;
}
}
if (stream.status() != QDataStream::Ok) {
return false;
}
_data = std::move(data);
return true;
}
void AccentColors::set(EmbeddedType type, const QColor &value) {
_data.emplace_or_assign(type, value);
}
void AccentColors::clear(EmbeddedType type) {
_data.remove(type);
}
std::optional<QColor> AccentColors::get(EmbeddedType type) const {
const auto i = _data.find(type);
return (i != end(_data)) ? std::make_optional(i->second) : std::nullopt;
}
} // namespace Theme
} // namespace Window

View 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
*/
#pragma once
namespace style {
struct colorizer;
} // namespace style
namespace Window {
namespace Theme {
enum class EmbeddedType {
DayBlue,
Default,
Night,
NightGreen,
};
struct EmbeddedScheme {
EmbeddedType type = EmbeddedType();
QColor background;
QColor sent;
QColor received;
QColor radiobuttonInactive;
QColor radiobuttonActive;
rpl::producer<QString> name;
QString path;
QColor accentColor;
};
class AccentColors final {
public:
[[nodiscard]] QByteArray serialize() const;
bool setFromSerialized(const QByteArray &serialized);
void set(EmbeddedType type, const QColor &value);
void clear(EmbeddedType type);
[[nodiscard]] std::optional<QColor> get(EmbeddedType type) const;
private:
base::flat_map<EmbeddedType, QColor> _data;
};
[[nodiscard]] style::colorizer ColorizerFrom(
const EmbeddedScheme &scheme,
const QColor &color);
[[nodiscard]] style::colorizer ColorizerForTheme(const QString &absolutePath);
void Colorize(
EmbeddedScheme &scheme,
const style::colorizer &colorizer);
[[nodiscard]] std::vector<EmbeddedScheme> EmbeddedThemes();
[[nodiscard]] std::vector<QColor> DefaultAccentColors(EmbeddedType type);
[[nodiscard]] Fn<void(style::palette&)> PreparePaletteCallback(
bool dark,
std::optional<QColor> accent);
[[nodiscard]] Fn<void(style::palette&)> PrepareCurrentPaletteCallback();
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,354 @@
/*
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_generate_name.h"
#include "base/random.h"
namespace Window {
namespace Theme {
namespace {
const auto kColors = base::flat_map<uint32, const char*>{
{ 0x8e0000U, "Berry" },
{ 0xdec196U, "Brandy" },
{ 0x800b47U, "Cherry" },
{ 0xff7f50U, "Coral" },
{ 0xdb5079U, "Cranberry" },
{ 0xdc143cU, "Crimson" },
{ 0xe0b0ffU, "Mauve" },
{ 0xffc0cbU, "Pink" },
{ 0xff0000U, "Red" },
{ 0xff007fU, "Rose" },
{ 0x80461bU, "Russet" },
{ 0xff2400U, "Scarlet" },
{ 0xf1f1f1U, "Seashell" },
{ 0xff3399U, "Strawberry" },
{ 0xffbf00U, "Amber" },
{ 0xeb9373U, "Apricot" },
{ 0xfbe7b2U, "Banana" },
{ 0xa1c50aU, "Citrus" },
{ 0xb06500U, "Ginger" },
{ 0xffd700U, "Gold" },
{ 0xfde910U, "Lemon" },
{ 0xffa500U, "Orange" },
{ 0xffe5b4U, "Peach" },
{ 0xff6b53U, "Persimmon" },
{ 0xe4d422U, "Sunflower" },
{ 0xf28500U, "Tangerine" },
{ 0xffc87cU, "Topaz" },
{ 0xffff00U, "Yellow" },
{ 0x384910U, "Clover" },
{ 0x83aa5dU, "Cucumber" },
{ 0x50c878U, "Emerald" },
{ 0xb5b35cU, "Olive" },
{ 0x00ff00U, "Green" },
{ 0x00a86bU, "Jade" },
{ 0x29ab87U, "Jungle" },
{ 0xbfff00U, "Lime" },
{ 0x0bda51U, "Malachite" },
{ 0x98ff98U, "Mint" },
{ 0xaddfadU, "Moss" },
{ 0x315ba1U, "Azure" },
{ 0x0000ffU, "Blue" },
{ 0x0047abU, "Cobalt" },
{ 0x4f69c6U, "Indigo" },
{ 0x017987U, "Lagoon" },
{ 0x71d9e2U, "Aquamarine" },
{ 0x120a8fU, "Ultramarine" },
{ 0x000080U, "Navy" },
{ 0x2f519eU, "Sapphire" },
{ 0x76d7eaU, "Sky" },
{ 0x008080U, "Teal" },
{ 0x40e0d0U, "Turquoise" },
{ 0x9966ccU, "Amethyst" },
{ 0x4d0135U, "Blackberry" },
{ 0x614051U, "Eggplant" },
{ 0xc8a2c8U, "Lilac" },
{ 0xb57edcU, "Lavender" },
{ 0xccccffU, "Periwinkle" },
{ 0x843179U, "Plum" },
{ 0x660099U, "Purple" },
{ 0xd8bfd8U, "Thistle" },
{ 0xda70d6U, "Orchid" },
{ 0x240a40U, "Violet" },
{ 0x3f2109U, "Bronze" },
{ 0x370202U, "Chocolate" },
{ 0x7b3f00U, "Cinnamon" },
{ 0x301f1eU, "Cocoa" },
{ 0x706555U, "Coffee" },
{ 0x796989U, "Rum" },
{ 0x4e0606U, "Mahogany" },
{ 0x782d19U, "Mocha" },
{ 0xc2b280U, "Sand" },
{ 0x882d17U, "Sienna" },
{ 0x780109U, "Maple" },
{ 0xf0e68cU, "Khaki" },
{ 0xb87333U, "Copper" },
{ 0xb94e48U, "Chestnut" },
{ 0xeed9c4U, "Almond" },
{ 0xfffdd0U, "Cream" },
{ 0xb9f2ffU, "Diamond" },
{ 0xa98307U, "Honey" },
{ 0xfffff0U, "Ivory" },
{ 0xeae0c8U, "Pearl" },
{ 0xeff2f3U, "Porcelain" },
{ 0xd1bea8U, "Vanilla" },
{ 0xffffffU, "White" },
{ 0x808080U, "Gray" },
{ 0x000000U, "Black" },
{ 0xe8f1d4U, "Chrome" },
{ 0x36454fU, "Charcoal" },
{ 0x0c0b1dU, "Ebony" },
{ 0xc0c0c0U, "Silver" },
{ 0xf5f5f5U, "Smoke" },
{ 0x262335U, "Steel" },
{ 0x4fa83dU, "Apple" },
{ 0x80b3c4U, "Glacier" },
{ 0xfebaadU, "Melon" },
{ 0xc54b8cU, "Mulberry" },
{ 0xa9c6c2U, "Opal" },
{ 0x54a5f8U, "Blue" }
};
const auto kAdjectives = std::vector<const char*>{
"Ancient",
"Antique",
"Autumn",
"Baby",
"Barely",
"Baroque",
"Blazing",
"Blushing",
"Bohemian",
"Bubbly",
"Burning",
"Buttered",
"Classic",
"Clear",
"Cool",
"Cosmic",
"Cotton",
"Cozy",
"Crystal",
"Dark",
"Daring",
"Darling",
"Dawn",
"Dazzling",
"Deep",
"Deepest",
"Delicate",
"Delightful",
"Divine",
"Double",
"Downtown",
"Dreamy",
"Dusky",
"Dusty",
"Electric",
"Enchanted",
"Endless",
"Evening",
"Fantastic",
"Flirty",
"Forever",
"Frigid",
"Frosty",
"Frozen",
"Gentle",
"Heavenly",
"Hyper",
"Icy",
"Infinite",
"Innocent",
"Instant",
"Luscious",
"Lunar",
"Lustrous",
"Magic",
"Majestic",
"Mambo",
"Midnight",
"Millennium",
"Morning",
"Mystic",
"Natural",
"Neon",
"Night",
"Opaque",
"Paradise",
"Perfect",
"Perky",
"Polished",
"Powerful",
"Rich",
"Royal",
"Sheer",
"Simply",
"Sizzling",
"Solar",
"Sparkling",
"Splendid",
"Spicy",
"Spring",
"Stellar",
"Sugared",
"Summer",
"Sunny",
"Super",
"Sweet",
"Tender",
"Tenacious",
"Tidal",
"Toasted",
"Totally",
"Tranquil",
"Tropical",
"True",
"Twilight",
"Twinkling",
"Ultimate",
"Ultra",
"Velvety",
"Vibrant",
"Vintage",
"Virtual",
"Warm",
"Warmest",
"Whipped",
"Wild",
"Winsome"
};
const auto kSubjectives = std::vector<const char*>{
"Ambrosia",
"Attack",
"Avalanche",
"Blast",
"Bliss",
"Blossom",
"Blush",
"Burst",
"Butter",
"Candy",
"Carnival",
"Charm",
"Chiffon",
"Cloud",
"Comet",
"Delight",
"Dream",
"Dust",
"Fantasy",
"Flame",
"Flash",
"Fire",
"Freeze",
"Frost",
"Glade",
"Glaze",
"Gleam",
"Glimmer",
"Glitter",
"Glow",
"Grande",
"Haze",
"Highlight",
"Ice",
"Illusion",
"Intrigue",
"Jewel",
"Jubilee",
"Kiss",
"Lights",
"Lollypop",
"Love",
"Luster",
"Madness",
"Matte",
"Mirage",
"Mist",
"Moon",
"Muse",
"Myth",
"Nectar",
"Nova",
"Parfait",
"Passion",
"Pop",
"Rain",
"Reflection",
"Rhapsody",
"Romance",
"Satin",
"Sensation",
"Silk",
"Shine",
"Shadow",
"Shimmer",
"Sky",
"Spice",
"Star",
"Sugar",
"Sunrise",
"Sunset",
"Sun",
"Twist",
"Unbound",
"Velvet",
"Vibrant",
"Waters",
"Wine",
"Wink",
"Wonder",
"Zone"
};
} // namespace
QString GenerateName(const QColor &accent) {
const auto r1 = accent.red();
const auto g1 = accent.green();
const auto b1 = accent.blue();
const auto distance = [&](const auto &pair) {
const auto &[color, name] = pair;
const auto b2 = int(color & 0xFFU);
const auto g2 = int((color >> 8) & 0xFFU);
const auto r2 = int((color >> 16) & 0xFFU);
const auto rMean = (r1 + r2) / 2;
const auto r = r1 - r2;
const auto g = g1 - g2;
const auto b = b1 - b2;
return (((512 + rMean) * r * r) >> 8)
+ (4 * g * g)
+ (((767 - rMean) * b * b) >> 8);
};
const auto pred = [&](const auto &a, const auto &b) {
return distance(a) < distance(b);
};
const auto capitalized = [](const char *value) {
Expects(*value != 0);
auto result = QString::fromLatin1(value);
result[0] = result[0].toUpper();
return result;
};
const auto random = [&](const std::vector<const char*> &values) {
const auto index = base::RandomValue<size_t>() % values.size();
return capitalized(values[index]);
};
const auto min = ranges::min_element(kColors, pred);
Assert(min != end(kColors));
const auto color = capitalized(min->second);
return (base::RandomValue<uint8>() % 2 == 0)
? random(kAdjectives) + ' ' + color
: color + ' ' + random(kSubjectives);
}
} // namespace Theme
} // namespace Window

View File

@@ -0,0 +1,16 @@
/*
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 Window {
namespace Theme {
[[nodiscard]] QString GenerateName(const QColor &accent);
} // namespace Theme
} // namespace Window