init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
1663
Telegram/SourceFiles/window/themes/window_theme.cpp
Normal file
1663
Telegram/SourceFiles/window/themes/window_theme.cpp
Normal file
File diff suppressed because it is too large
Load Diff
317
Telegram/SourceFiles/window/themes/window_theme.h
Normal file
317
Telegram/SourceFiles/window/themes/window_theme.h
Normal 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
|
||||
938
Telegram/SourceFiles/window/themes/window_theme_editor.cpp
Normal file
938
Telegram/SourceFiles/window/themes/window_theme_editor.cpp
Normal 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 ©Of, 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 ©Of, 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
|
||||
95
Telegram/SourceFiles/window/themes/window_theme_editor.h
Normal file
95
Telegram/SourceFiles/window/themes/window_theme_editor.h
Normal 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
|
||||
834
Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp
Normal file
834
Telegram/SourceFiles/window/themes/window_theme_editor_block.cpp
Normal 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 ©Of, QColor value);
|
||||
|
||||
QString name() const {
|
||||
return _name;
|
||||
}
|
||||
|
||||
void setCopyOf(const QString ©Of) {
|
||||
_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 ©Of, 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 ©OfExisting) {
|
||||
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 ©Of) {
|
||||
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 ©Of, 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
|
||||
169
Telegram/SourceFiles/window/themes/window_theme_editor_block.h
Normal file
169
Telegram/SourceFiles/window/themes/window_theme_editor_block.h
Normal 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 ©OfExisting = QString());
|
||||
bool feedCopy(const QString &name, const QString ©Of);
|
||||
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 ©Of, 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
|
||||
1010
Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp
Normal file
1010
Telegram/SourceFiles/window/themes/window_theme_editor_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
60
Telegram/SourceFiles/window/themes/window_theme_editor_box.h
Normal file
60
Telegram/SourceFiles/window/themes/window_theme_editor_box.h
Normal 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
|
||||
1270
Telegram/SourceFiles/window/themes/window_theme_preview.cpp
Normal file
1270
Telegram/SourceFiles/window/themes/window_theme_preview.cpp
Normal file
File diff suppressed because it is too large
Load Diff
54
Telegram/SourceFiles/window/themes/window_theme_preview.h
Normal file
54
Telegram/SourceFiles/window/themes/window_theme_preview.h
Normal 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
|
||||
150
Telegram/SourceFiles/window/themes/window_theme_warning.cpp
Normal file
150
Telegram/SourceFiles/window/themes/window_theme_warning.cpp
Normal 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
|
||||
64
Telegram/SourceFiles/window/themes/window_theme_warning.h
Normal file
64
Telegram/SourceFiles/window/themes/window_theme_warning.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#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
|
||||
751
Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp
Normal file
751
Telegram/SourceFiles/window/themes/window_themes_cloud_list.cpp
Normal 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
|
||||
138
Telegram/SourceFiles/window/themes/window_themes_cloud_list.h
Normal file
138
Telegram/SourceFiles/window/themes/window_themes_cloud_list.h
Normal 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
|
||||
431
Telegram/SourceFiles/window/themes/window_themes_embedded.cpp
Normal file
431
Telegram/SourceFiles/window/themes/window_themes_embedded.cpp
Normal 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(©, 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
|
||||
68
Telegram/SourceFiles/window/themes/window_themes_embedded.h
Normal file
68
Telegram/SourceFiles/window/themes/window_themes_embedded.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user