mirror of
https://github.com/vale981/tdesktop
synced 2025-03-09 12:36:39 -04:00
1597 lines
46 KiB
C++
1597 lines
46 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "window/themes/window_theme.h"
|
|
|
|
#include "window/themes/window_theme_preview.h"
|
|
#include "mainwidget.h"
|
|
#include "auth_session.h"
|
|
#include "core/application.h"
|
|
#include "storage/serialize_common.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_session.h"
|
|
#include "storage/localstorage.h"
|
|
#include "base/parse_helper.h"
|
|
#include "base/zlib_help.h"
|
|
#include "ui/image/image.h"
|
|
#include "boxes/background_box.h"
|
|
#include "styles/style_widgets.h"
|
|
#include "styles/style_history.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
constexpr auto FromLegacyBackgroundId(int32 legacyId) -> WallPaperId {
|
|
return uint64(0xFFFFFFFF00000000ULL) | uint64(uint32(legacyId));
|
|
}
|
|
|
|
constexpr auto kUninitializedBackground = FromLegacyBackgroundId(-999);
|
|
constexpr auto kTestingThemeBackground = FromLegacyBackgroundId(-666);
|
|
constexpr auto kTestingDefaultBackground = FromLegacyBackgroundId(-665);
|
|
constexpr auto kTestingEditorBackground = FromLegacyBackgroundId(-664);
|
|
constexpr auto kThemeBackground = FromLegacyBackgroundId(-2);
|
|
constexpr auto kCustomBackground = FromLegacyBackgroundId(-1);
|
|
constexpr auto kLegacy1DefaultBackground = FromLegacyBackgroundId(0);
|
|
constexpr auto kDefaultBackground = FromLegacyBackgroundId(105);
|
|
|
|
[[nodiscard]] bool ValidateFlags(MTPDwallPaper::Flags flags) {
|
|
using Flag = MTPDwallPaper::Flag;
|
|
const auto all = Flag(0)
|
|
| Flag::f_creator
|
|
| Flag::f_default
|
|
| Flag::f_pattern
|
|
| Flag::f_settings;
|
|
return !(flags & ~all);
|
|
}
|
|
|
|
[[nodiscard]] bool ValidateFlags(MTPDwallPaperSettings::Flags flags) {
|
|
using Flag = MTPDwallPaperSettings::Flag;
|
|
const auto all = Flag(0)
|
|
| Flag::f_background_color
|
|
| Flag::f_blur
|
|
| Flag::f_intensity
|
|
| Flag::f_motion;
|
|
return !(flags & ~all);
|
|
}
|
|
|
|
quint32 SerializeMaybeColor(std::optional<QColor> color) {
|
|
return color
|
|
? ((quint32(std::clamp(color->red(), 0, 255)) << 16)
|
|
| (quint32(std::clamp(color->green(), 0, 255)) << 8)
|
|
| quint32(std::clamp(color->blue(), 0, 255)))
|
|
: quint32(-1);
|
|
}
|
|
|
|
std::optional<QColor> MaybeColorFromSerialized(quint32 serialized) {
|
|
return (serialized == quint32(-1))
|
|
? std::nullopt
|
|
: std::make_optional(QColor(
|
|
int((serialized >> 16) & 0xFFU),
|
|
int((serialized >> 8) & 0xFFU),
|
|
int(serialized & 0xFFU)));
|
|
}
|
|
|
|
std::optional<QColor> ColorFromString(const QString &string) {
|
|
if (string.size() != 6) {
|
|
return {};
|
|
} else if (ranges::find_if(string, [](QChar ch) {
|
|
return (ch < 'a' || ch > 'f')
|
|
&& (ch < 'A' || ch > 'F')
|
|
&& (ch < '0' || ch > '9');
|
|
}) != string.end()) {
|
|
return {};
|
|
}
|
|
const auto component = [](const QString &text, int index) {
|
|
const auto decimal = [](QChar hex) {
|
|
const auto code = hex.unicode();
|
|
return (code >= '0' && code <= '9')
|
|
? int(code - '0')
|
|
: (code >= 'a' && code <= 'f')
|
|
? int(code - 'a' + 0x0a)
|
|
: int(code - 'A' + 0x0a);
|
|
};
|
|
index *= 2;
|
|
return decimal(text[index]) * 0x10 + decimal(text[index + 1]);
|
|
};
|
|
return QColor(
|
|
component(string, 0),
|
|
component(string, 1),
|
|
component(string, 2),
|
|
255);
|
|
}
|
|
|
|
QImage PreparePatternImage(QImage image, QColor bg, QColor fg, int intensity) {
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = std::move(image).convertToFormat(
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
// Similar to ColorizePattern.
|
|
// But here we set bg to all 'alpha=0' pixels and fg to opaque ones.
|
|
|
|
const auto width = image.width();
|
|
const auto height = image.height();
|
|
const auto alpha = anim::interpolate(
|
|
0,
|
|
255,
|
|
fg.alphaF() * std::clamp(intensity / 100., 0., 1.));
|
|
if (!alpha) {
|
|
image.fill(bg);
|
|
return std::move(image);
|
|
}
|
|
fg.setAlpha(255);
|
|
const auto patternBg = anim::shifted(bg);
|
|
const auto patternFg = anim::shifted(fg);
|
|
|
|
const auto resultBytesPerPixel = (image.depth() >> 3);
|
|
constexpr auto resultIntsPerPixel = 1;
|
|
const auto resultIntsPerLine = (image.bytesPerLine() >> 2);
|
|
const auto resultIntsAdded = resultIntsPerLine - width * resultIntsPerPixel;
|
|
auto resultInts = reinterpret_cast<uint32*>(image.bits());
|
|
Assert(resultIntsAdded >= 0);
|
|
Assert(image.depth() == static_cast<int>((resultIntsPerPixel * sizeof(uint32)) << 3));
|
|
Assert(image.bytesPerLine() == (resultIntsPerLine << 2));
|
|
|
|
const auto maskBytesPerPixel = (image.depth() >> 3);
|
|
const auto maskBytesPerLine = image.bytesPerLine();
|
|
const auto maskBytesAdded = maskBytesPerLine - width * maskBytesPerPixel;
|
|
|
|
// We want to read the last byte of four available.
|
|
// This is the difference with style::colorizeImage.
|
|
auto maskBytes = image.constBits() + (maskBytesPerPixel - 1);
|
|
Assert(maskBytesAdded >= 0);
|
|
Assert(image.depth() == (maskBytesPerPixel << 3));
|
|
for (auto y = 0; y != height; ++y) {
|
|
for (auto x = 0; x != width; ++x) {
|
|
const auto maskOpacity = static_cast<anim::ShiftedMultiplier>(
|
|
*maskBytes) + 1;
|
|
const auto fgOpacity = (maskOpacity * alpha) >> 8;
|
|
const auto bgOpacity = 256 - fgOpacity;
|
|
*resultInts = anim::unshifted(
|
|
patternBg * bgOpacity + patternFg * fgOpacity);
|
|
maskBytes += maskBytesPerPixel;
|
|
resultInts += resultIntsPerPixel;
|
|
}
|
|
maskBytes += maskBytesAdded;
|
|
resultInts += resultIntsAdded;
|
|
}
|
|
return std::move(image);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
WallPaper::WallPaper(WallPaperId id) : _id(id) {
|
|
}
|
|
|
|
void WallPaper::setLocalImageAsThumbnail(not_null<Image*> image) {
|
|
Expects(IsDefaultWallPaper(*this)
|
|
|| IsLegacy1DefaultWallPaper(*this)
|
|
|| IsCustomWallPaper(*this));
|
|
Expects(_thumbnail == nullptr);
|
|
|
|
_thumbnail = image;
|
|
}
|
|
|
|
WallPaperId WallPaper::id() const {
|
|
return _id;
|
|
}
|
|
|
|
std::optional<QColor> WallPaper::backgroundColor() const {
|
|
return _backgroundColor;
|
|
}
|
|
|
|
DocumentData *WallPaper::document() const {
|
|
return _document;
|
|
}
|
|
|
|
Image *WallPaper::thumbnail() const {
|
|
return _thumbnail;
|
|
}
|
|
|
|
bool WallPaper::isPattern() const {
|
|
return _flags & MTPDwallPaper::Flag::f_pattern;
|
|
}
|
|
|
|
bool WallPaper::isDefault() const {
|
|
return _flags & MTPDwallPaper::Flag::f_default;
|
|
}
|
|
|
|
bool WallPaper::isCreator() const {
|
|
return _flags & MTPDwallPaper::Flag::f_creator;
|
|
}
|
|
|
|
int WallPaper::patternIntensity() const {
|
|
return _intensity;
|
|
}
|
|
|
|
bool WallPaper::hasShareUrl() const {
|
|
return !_slug.isEmpty();
|
|
}
|
|
|
|
QString WallPaper::shareUrl() const {
|
|
return hasShareUrl()
|
|
? Core::App().createInternalLinkFull("bg/" + _slug)
|
|
: QString();
|
|
}
|
|
|
|
void WallPaper::loadThumbnail() const {
|
|
if (_thumbnail) {
|
|
_thumbnail->load(fileOrigin());
|
|
}
|
|
}
|
|
|
|
void WallPaper::loadDocument() const {
|
|
if (_document) {
|
|
_document->save(fileOrigin(), QString());
|
|
}
|
|
}
|
|
|
|
FileOrigin WallPaper::fileOrigin() const {
|
|
return FileOriginWallpaper(_id, _accessHash);
|
|
}
|
|
|
|
WallPaper WallPaper::withUrlParams(
|
|
const QMap<QString, QString> ¶ms) const {
|
|
using Flag = MTPDwallPaperSettings::Flag;
|
|
|
|
auto result = *this;
|
|
result._settings = Flag(0);
|
|
result._backgroundColor = ColorFromString(_slug);
|
|
result._intensity = kDefaultIntensity;
|
|
|
|
if (auto mode = params.value("mode"); !mode.isEmpty()) {
|
|
const auto list = mode.replace('+', ' ').split(' ');
|
|
for (const auto &change : list) {
|
|
if (change == qstr("blur")) {
|
|
result._settings |= Flag::f_blur;
|
|
} else if (change == qstr("motion")) {
|
|
result._settings |= Flag::f_motion;
|
|
}
|
|
}
|
|
}
|
|
if (const auto color = ColorFromString(params.value("bg_color"))) {
|
|
result._backgroundColor = color;
|
|
}
|
|
if (const auto string = params.value("intensity"); !string.isEmpty()) {
|
|
auto ok = false;
|
|
const auto intensity = string.toInt(&ok);
|
|
if (ok && base::in_range(intensity, 0, 100)) {
|
|
result._intensity = intensity;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::Create(const MTPWallPaper &data) {
|
|
return data.match([](const MTPDwallPaper &data) {
|
|
return Create(data);
|
|
});
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::Create(const MTPDwallPaper &data) {
|
|
using Flag = MTPDwallPaper::Flag;
|
|
|
|
const auto document = Auth().data().processDocument(
|
|
data.vdocument);
|
|
if (!document->checkWallPaperProperties()) {
|
|
return std::nullopt;
|
|
}
|
|
auto result = WallPaper(data.vid.v);
|
|
result._accessHash = data.vaccess_hash.v;
|
|
result._flags = data.vflags.v;
|
|
result._slug = qs(data.vslug);
|
|
result._document = document;
|
|
result._thumbnail = document->thumbnail();
|
|
if (data.has_settings()) {
|
|
const auto isPattern = ((result._flags & Flag::f_pattern) != 0);
|
|
data.vsettings.match([&](const MTPDwallPaperSettings &data) {
|
|
using Flag = MTPDwallPaperSettings::Flag;
|
|
|
|
result._settings = data.vflags.v;
|
|
if (isPattern && data.has_background_color()) {
|
|
result._backgroundColor = MaybeColorFromSerialized(
|
|
data.vbackground_color.v);
|
|
} else {
|
|
result._settings &= ~Flag::f_background_color;
|
|
}
|
|
if (isPattern && data.has_intensity()) {
|
|
result._intensity = data.vintensity.v;
|
|
} else {
|
|
result._settings &= ~Flag::f_intensity;
|
|
}
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QByteArray WallPaper::serialize() const {
|
|
auto size = sizeof(quint64) // _id
|
|
+ sizeof(quint64) // _accessHash
|
|
+ sizeof(qint32) // _flags
|
|
+ Serialize::stringSize(_slug)
|
|
+ sizeof(qint32) // _settings
|
|
+ sizeof(quint32) // _backgroundColor
|
|
+ sizeof(qint32); // _intensity
|
|
|
|
auto result = QByteArray();
|
|
result.reserve(size);
|
|
{
|
|
auto stream = QDataStream(&result, QIODevice::WriteOnly);
|
|
stream.setVersion(QDataStream::Qt_5_1);
|
|
stream
|
|
<< quint64(_id)
|
|
<< quint64(_accessHash)
|
|
<< qint32(_flags)
|
|
<< _slug
|
|
<< qint32(_settings)
|
|
<< SerializeMaybeColor(_backgroundColor)
|
|
<< qint32(_intensity);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::FromSerialized(
|
|
const QByteArray &serialized) {
|
|
if (serialized.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto id = quint64();
|
|
auto accessHash = quint64();
|
|
auto flags = qint32();
|
|
auto slug = QString();
|
|
auto settings = qint32();
|
|
auto backgroundColor = quint32();
|
|
auto intensity = qint32();
|
|
|
|
auto stream = QDataStream(serialized);
|
|
stream.setVersion(QDataStream::Qt_5_1);
|
|
stream
|
|
>> id
|
|
>> accessHash
|
|
>> flags
|
|
>> slug
|
|
>> settings
|
|
>> backgroundColor
|
|
>> intensity;
|
|
if (stream.status() != QDataStream::Ok) {
|
|
return std::nullopt;
|
|
} else if (intensity < 0 || intensity > 100) {
|
|
return std::nullopt;
|
|
}
|
|
auto result = WallPaper(id);
|
|
result._accessHash = accessHash;
|
|
result._flags = MTPDwallPaper::Flags::from_raw(flags);
|
|
result._slug = slug;
|
|
result._settings = MTPDwallPaperSettings::Flags::from_raw(settings);
|
|
result._backgroundColor = MaybeColorFromSerialized(backgroundColor);
|
|
result._intensity = intensity;
|
|
if (!ValidateFlags(result._flags) || !ValidateFlags(result._settings)) {
|
|
return std::nullopt;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::FromLegacySerialized(
|
|
quint64 id,
|
|
quint64 accessHash,
|
|
quint32 flags,
|
|
QString slug) {
|
|
auto result = WallPaper(id);
|
|
result._accessHash = accessHash;
|
|
result._flags = MTPDwallPaper::Flags::from_raw(flags);
|
|
result._slug = slug;
|
|
result._backgroundColor = ColorFromString(slug);
|
|
if (!ValidateFlags(result._flags)) {
|
|
return std::nullopt;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::FromLegacyId(qint32 legacyId) {
|
|
auto result = WallPaper(FromLegacyBackgroundId(legacyId));
|
|
if (!IsCustomWallPaper(result)) {
|
|
result._flags = MTPDwallPaper::Flag::f_default;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::optional<WallPaper> WallPaper::FromColorSlug(const QString &slug) {
|
|
if (const auto color = ColorFromString(slug)) {
|
|
auto result = CustomWallPaper();
|
|
result._slug = slug;
|
|
result._backgroundColor = color;
|
|
return result;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
WallPaper ThemeWallPaper() {
|
|
return WallPaper(kThemeBackground);
|
|
}
|
|
|
|
bool IsThemeWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kThemeBackground);
|
|
}
|
|
|
|
WallPaper CustomWallPaper() {
|
|
return WallPaper(kCustomBackground);
|
|
}
|
|
|
|
bool IsCustomWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kCustomBackground);
|
|
}
|
|
|
|
WallPaper Legacy1DefaultWallPaper() {
|
|
return WallPaper(kLegacy1DefaultBackground);
|
|
}
|
|
|
|
bool IsLegacy1DefaultWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kLegacy1DefaultBackground);
|
|
}
|
|
|
|
WallPaper DefaultWallPaper() {
|
|
return WallPaper(kDefaultBackground);
|
|
}
|
|
|
|
bool IsDefaultWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kDefaultBackground);
|
|
}
|
|
|
|
namespace details {
|
|
|
|
WallPaper UninitializedWallPaper() {
|
|
return WallPaper(kUninitializedBackground);
|
|
}
|
|
|
|
bool IsUninitializedWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kUninitializedBackground);
|
|
}
|
|
|
|
WallPaper TestingThemeWallPaper() {
|
|
return WallPaper(kTestingThemeBackground);
|
|
}
|
|
|
|
bool IsTestingThemeWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kTestingThemeBackground);
|
|
}
|
|
|
|
WallPaper TestingDefaultWallPaper() {
|
|
return WallPaper(kTestingDefaultBackground);
|
|
}
|
|
|
|
bool IsTestingDefaultWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kTestingDefaultBackground);
|
|
}
|
|
|
|
WallPaper TestingEditorWallPaper() {
|
|
return WallPaper(kTestingEditorBackground);
|
|
}
|
|
|
|
bool IsTestingEditorWallPaper(const WallPaper &paper) {
|
|
return (paper.id() == kTestingEditorBackground);
|
|
}
|
|
|
|
} // namespace details
|
|
} // namespace Data
|
|
|
|
namespace Window {
|
|
namespace Theme {
|
|
namespace {
|
|
|
|
constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
|
|
constexpr auto kThemeBackgroundSizeLimit = 4 * 1024 * 1024;
|
|
constexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024;
|
|
constexpr auto kThemeSchemeSizeLimit = 1024 * 1024;
|
|
constexpr auto kNightThemeFile = str_const(":/gui/night.tdesktop-theme");
|
|
|
|
struct Applying {
|
|
QString pathRelative;
|
|
QString pathAbsolute;
|
|
QByteArray content;
|
|
QByteArray paletteForRevert;
|
|
Cached cached;
|
|
Fn<void()> overrideKeep;
|
|
};
|
|
|
|
NeverFreedPointer<ChatBackground> GlobalBackground;
|
|
Applying GlobalApplying;
|
|
|
|
inline bool AreTestingTheme() {
|
|
return !GlobalApplying.paletteForRevert.isEmpty();
|
|
};
|
|
|
|
QByteArray readThemeContent(const QString &path) {
|
|
QFile file(path);
|
|
if (!file.exists()) {
|
|
LOG(("Theme Error: theme file not found: %1").arg(path));
|
|
return QByteArray();
|
|
}
|
|
|
|
if (file.size() > kThemeFileSizeLimit) {
|
|
LOG(("Theme Error: theme file too large: %1 (should be less than 5 MB, got %2)").arg(path).arg(file.size()));
|
|
return QByteArray();
|
|
}
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
LOG(("Theme Error: could not open theme file: %1").arg(path));
|
|
return QByteArray();
|
|
}
|
|
|
|
return file.readAll();
|
|
}
|
|
|
|
inline uchar readHexUchar(char code, bool &error) {
|
|
if (code >= '0' && code <= '9') {
|
|
return ((code - '0') & 0xFF);
|
|
} else if (code >= 'a' && code <= 'f') {
|
|
return ((code + 10 - 'a') & 0xFF);
|
|
} else if (code >= 'A' && code <= 'F') {
|
|
return ((code + 10 - 'A') & 0xFF);
|
|
}
|
|
error = true;
|
|
return 0xFF;
|
|
}
|
|
|
|
inline uchar readHexUchar(char char1, char char2, bool &error) {
|
|
return ((readHexUchar(char1, error) & 0x0F) << 4) | (readHexUchar(char2, error) & 0x0F);
|
|
}
|
|
|
|
bool readNameAndValue(const char *&from, const char *end, QLatin1String *outName, QLatin1String *outValue) {
|
|
using base::parse::skipWhitespaces;
|
|
using base::parse::readName;
|
|
|
|
if (!skipWhitespaces(from, end)) return true;
|
|
|
|
*outName = readName(from, end);
|
|
if (outName->size() == 0) {
|
|
LOG(("Theme Error: Could not read name in the color scheme."));
|
|
return false;
|
|
}
|
|
if (!skipWhitespaces(from, end)) {
|
|
LOG(("Theme Error: Unexpected end of the color scheme."));
|
|
return false;
|
|
}
|
|
if (*from != ':') {
|
|
LOG(("Theme Error: Expected ':' between each name and value in the color scheme (while reading key '%1')").arg(*outName));
|
|
return false;
|
|
}
|
|
if (!skipWhitespaces(++from, end)) {
|
|
LOG(("Theme Error: Unexpected end of the color scheme."));
|
|
return false;
|
|
}
|
|
auto valueStart = from;
|
|
if (*from == '#') ++from;
|
|
|
|
if (readName(from, end).size() == 0) {
|
|
LOG(("Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme (while reading key '%1')").arg(*outName));
|
|
return false;
|
|
}
|
|
*outValue = QLatin1String(valueStart, from - valueStart);
|
|
|
|
if (!skipWhitespaces(from, end)) {
|
|
LOG(("Theme Error: Unexpected end of the color scheme."));
|
|
return false;
|
|
}
|
|
if (*from != ';') {
|
|
LOG(("Theme Error: Expected ';' after each value in the color scheme (while reading key '%1')").arg(*outName));
|
|
return false;
|
|
}
|
|
++from;
|
|
return true;
|
|
}
|
|
|
|
enum class SetResult {
|
|
Ok,
|
|
Bad,
|
|
NotFound,
|
|
};
|
|
SetResult setColorSchemeValue(QLatin1String name, QLatin1String value, Instance *out) {
|
|
auto result = style::palette::SetResult::Ok;
|
|
auto size = value.size();
|
|
auto data = value.data();
|
|
if (data[0] == '#' && (size == 7 || size == 9)) {
|
|
auto error = false;
|
|
auto r = readHexUchar(data[1], data[2], error);
|
|
auto g = readHexUchar(data[3], data[4], error);
|
|
auto b = readHexUchar(data[5], data[6], error);
|
|
auto a = (size == 9) ? readHexUchar(data[7], data[8], error) : uchar(255);
|
|
if (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));
|
|
return SetResult::Ok;
|
|
} else if (out) {
|
|
result = out->palette.setColor(name, r, g, b, a);
|
|
} else {
|
|
result = style::main_palette::setColor(name, r, g, b, a);
|
|
}
|
|
} else {
|
|
if (out) {
|
|
result = out->palette.setColor(name, value);
|
|
} else {
|
|
result = style::main_palette::setColor(name, value);
|
|
}
|
|
}
|
|
if (result == style::palette::SetResult::Ok) {
|
|
return SetResult::Ok;
|
|
} else if (result == style::palette::SetResult::KeyNotFound) {
|
|
return SetResult::NotFound;
|
|
} else if (result == style::palette::SetResult::ValueNotFound) {
|
|
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));
|
|
return SetResult::Ok;
|
|
} else if (result == style::palette::SetResult::Duplicate) {
|
|
LOG(("Theme Warning: Color value appears more than once in the color scheme (while applying '%1: %2')").arg(name).arg(value));
|
|
return SetResult::Ok;
|
|
} else {
|
|
LOG(("Theme Error: Unexpected internal error."));
|
|
}
|
|
return SetResult::Bad;
|
|
}
|
|
|
|
bool loadColorScheme(const QByteArray &content, Instance *out) {
|
|
auto unsupported = QMap<QLatin1String, QLatin1String>();
|
|
return ReadPaletteValues(content, [&unsupported, out](QLatin1String name, QLatin1String value) {
|
|
// Find the named value in the already read unsupported list.
|
|
value = unsupported.value(value, value);
|
|
|
|
auto result = setColorSchemeValue(name, value, out);
|
|
if (result == SetResult::Bad) {
|
|
return false;
|
|
} else if (result == SetResult::NotFound) {
|
|
unsupported.insert(name, value);
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
void applyBackground(QImage &&background, bool tiled, Instance *out) {
|
|
if (out) {
|
|
out->background = std::move(background);
|
|
out->tiled = tiled;
|
|
} else {
|
|
Background()->setThemeData(std::move(background), tiled);
|
|
}
|
|
}
|
|
|
|
bool loadThemeFromCache(const QByteArray &content, const Cached &cache) {
|
|
if (cache.paletteChecksum != style::palette::Checksum()) {
|
|
return false;
|
|
}
|
|
if (cache.contentChecksum != hashCrc32(content.constData(), content.size())) {
|
|
return false;
|
|
}
|
|
|
|
QImage background;
|
|
if (!cache.background.isEmpty()) {
|
|
QDataStream stream(cache.background);
|
|
QImageReader reader(stream.device());
|
|
#ifndef OS_MAC_OLD
|
|
reader.setAutoTransform(true);
|
|
#endif // OS_MAC_OLD
|
|
if (!reader.read(&background) || background.isNull()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!style::main_palette::load(cache.colors)) {
|
|
return false;
|
|
}
|
|
Background()->saveAdjustableColors();
|
|
if (!background.isNull()) {
|
|
applyBackground(std::move(background), cache.tiled, nullptr);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
enum class LoadResult {
|
|
Loaded,
|
|
Failed,
|
|
NotFound,
|
|
};
|
|
|
|
LoadResult loadBackgroundFromFile(zlib::FileToRead &file, const char *filename, QByteArray *outBackground) {
|
|
*outBackground = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);
|
|
if (file.error() == UNZ_OK) {
|
|
return LoadResult::Loaded;
|
|
} else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
|
|
file.clearError();
|
|
return LoadResult::NotFound;
|
|
}
|
|
LOG(("Theme Error: could not read '%1' in the theme file.").arg(filename));
|
|
return LoadResult::Failed;
|
|
}
|
|
|
|
bool loadBackground(zlib::FileToRead &file, QByteArray *outBackground, bool *outTiled) {
|
|
auto result = loadBackgroundFromFile(file, "background.jpg", outBackground);
|
|
if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
|
|
|
|
result = loadBackgroundFromFile(file, "background.png", outBackground);
|
|
if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
|
|
|
|
*outTiled = true;
|
|
result = loadBackgroundFromFile(file, "tiled.jpg", outBackground);
|
|
if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
|
|
|
|
result = loadBackgroundFromFile(file, "tiled.png", outBackground);
|
|
if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
|
|
return true;
|
|
}
|
|
|
|
bool loadTheme(const QByteArray &content, Cached &cache, Instance *out = nullptr) {
|
|
cache = Cached();
|
|
zlib::FileToRead file(content);
|
|
|
|
unz_global_info globalInfo = { 0 };
|
|
file.getGlobalInfo(&globalInfo);
|
|
if (file.error() == UNZ_OK) {
|
|
auto schemeContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
|
|
if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
|
|
file.clearError();
|
|
schemeContent = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
|
|
}
|
|
if (file.error() != UNZ_OK) {
|
|
LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file."));
|
|
return false;
|
|
}
|
|
if (!loadColorScheme(schemeContent, out)) {
|
|
return false;
|
|
}
|
|
Background()->saveAdjustableColors();
|
|
|
|
auto backgroundTiled = false;
|
|
auto backgroundContent = QByteArray();
|
|
if (!loadBackground(file, &backgroundContent, &backgroundTiled)) {
|
|
return false;
|
|
}
|
|
|
|
if (!backgroundContent.isEmpty()) {
|
|
auto check = QBuffer(&backgroundContent);
|
|
auto reader = QImageReader(&check);
|
|
const auto size = reader.size();
|
|
if (size.isEmpty()
|
|
|| (size.width() * size.height() > kBackgroundSizeLimit)) {
|
|
LOG(("Theme Error: bad background image size in the theme file."));
|
|
return false;
|
|
}
|
|
auto background = App::readImage(backgroundContent);
|
|
if (background.isNull()) {
|
|
LOG(("Theme Error: could not read background image in the theme file."));
|
|
return false;
|
|
}
|
|
auto buffer = QBuffer(&cache.background);
|
|
if (!background.save(&buffer, "BMP")) {
|
|
LOG(("Theme Error: could not write background image as a BMP to cache."));
|
|
return false;
|
|
}
|
|
cache.tiled = backgroundTiled;
|
|
|
|
applyBackground(std::move(background), cache.tiled, out);
|
|
}
|
|
} else {
|
|
// Looks like it is not a .zip theme.
|
|
if (!loadColorScheme(content, out)) {
|
|
return false;
|
|
}
|
|
Background()->saveAdjustableColors();
|
|
}
|
|
if (out) {
|
|
cache.colors = out->palette.save();
|
|
} else {
|
|
cache.colors = style::main_palette::save();
|
|
}
|
|
cache.paletteChecksum = style::palette::Checksum();
|
|
cache.contentChecksum = hashCrc32(content.constData(), content.size());
|
|
|
|
return true;
|
|
}
|
|
|
|
QImage prepareBackgroundImage(QImage &&image) {
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = std::move(image).convertToFormat(
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
image.setDevicePixelRatio(cRetinaFactor());
|
|
return std::move(image);
|
|
}
|
|
|
|
void adjustColor(style::color color, float64 hue, float64 saturation) {
|
|
auto original = color->c;
|
|
original.setHslF(hue, saturation, original.lightnessF(), original.alphaF());
|
|
color.set(original.red(), original.green(), original.blue(), original.alpha());
|
|
}
|
|
|
|
void WriteAppliedTheme() {
|
|
auto saved = Saved();
|
|
saved.pathRelative = GlobalApplying.pathRelative;
|
|
saved.pathAbsolute = GlobalApplying.pathAbsolute;
|
|
saved.content = std::move(GlobalApplying.content);
|
|
saved.cache = std::move(GlobalApplying.cached);
|
|
Local::writeTheme(saved);
|
|
}
|
|
|
|
void ClearApplying() {
|
|
GlobalApplying = Applying();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ChatBackground::AdjustableColor::AdjustableColor(style::color data)
|
|
: item(data)
|
|
, original(data->c) {
|
|
}
|
|
|
|
ChatBackground::ChatBackground() : _adjustableColors({
|
|
st::msgServiceBg,
|
|
st::msgServiceBgSelected,
|
|
st::historyScrollBg,
|
|
st::historyScrollBgOver,
|
|
st::historyScrollBarBg,
|
|
st::historyScrollBarBgOver }) {
|
|
saveAdjustableColors();
|
|
}
|
|
|
|
void ChatBackground::setThemeData(QImage &&themeImage, bool themeTile) {
|
|
_themeImage = prepareBackgroundImage(std::move(themeImage));
|
|
_themeTile = themeTile;
|
|
}
|
|
|
|
void ChatBackground::start() {
|
|
if (Data::details::IsUninitializedWallPaper(_paper)) {
|
|
if (!Local::readBackground()) {
|
|
setImage(Data::ThemeWallPaper());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setImage(
|
|
const Data::WallPaper &paper,
|
|
QImage &&image) {
|
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
|
image = std::move(image).convertToFormat(
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
}
|
|
|
|
const auto needResetAdjustable = Data::IsDefaultWallPaper(paper)
|
|
&& !Data::IsDefaultWallPaper(_paper)
|
|
&& !nightMode()
|
|
&& _themeAbsolutePath.isEmpty();
|
|
if (Data::IsThemeWallPaper(paper) && _themeImage.isNull()) {
|
|
setPaper(Data::DefaultWallPaper());
|
|
} else {
|
|
setPaper(paper);
|
|
if (needResetAdjustable) {
|
|
// If we had a default color theme with non-default background,
|
|
// and we switch to default background we must somehow switch from
|
|
// adjusted service colors to default (non-adjusted) service colors.
|
|
// The only way to do that right now is through full palette reset.
|
|
restoreAdjustableColors();
|
|
}
|
|
}
|
|
if (Data::IsThemeWallPaper(_paper)) {
|
|
(nightMode() ? _tileNightValue : _tileDayValue) = _themeTile;
|
|
setPreparedImage(QImage(_themeImage));
|
|
} else if (Data::details::IsTestingThemeWallPaper(_paper)
|
|
|| Data::details::IsTestingDefaultWallPaper(_paper)
|
|
|| Data::details::IsTestingEditorWallPaper(_paper)) {
|
|
if (Data::details::IsTestingDefaultWallPaper(_paper) || image.isNull()) {
|
|
image.load(qsl(":/gui/art/bg.jpg"));
|
|
setPaper(Data::details::TestingDefaultWallPaper());
|
|
}
|
|
setPreparedImage(prepareBackgroundImage(std::move(image)));
|
|
} else {
|
|
if (Data::IsLegacy1DefaultWallPaper(_paper)) {
|
|
image.load(qsl(":/gui/art/bg_initial.jpg"));
|
|
const auto scale = cScale() * cIntRetinaFactor();
|
|
if (scale != 100) {
|
|
image = image.scaledToWidth(
|
|
ConvertScale(image.width(), scale),
|
|
Qt::SmoothTransformation);
|
|
}
|
|
} else if (Data::IsDefaultWallPaper(_paper)
|
|
|| (!_paper.backgroundColor() && image.isNull())) {
|
|
setPaper(Data::DefaultWallPaper());
|
|
image.load(qsl(":/gui/art/bg.jpg"));
|
|
}
|
|
Local::writeBackground(
|
|
_paper,
|
|
((Data::IsDefaultWallPaper(_paper)
|
|
|| Data::IsLegacy1DefaultWallPaper(_paper))
|
|
? QImage()
|
|
: image));
|
|
if (const auto fill = _paper.backgroundColor()) {
|
|
if (_paper.isPattern() && !image.isNull()) {
|
|
setPreparedImage(Data::PreparePatternImage(
|
|
std::move(image),
|
|
*fill,
|
|
PatternColor(*fill),
|
|
_paper.patternIntensity()));
|
|
} else {
|
|
_pixmap = QPixmap();
|
|
_pixmapForTiled = QPixmap();
|
|
if (adjustPaletteRequired()) {
|
|
adjustPaletteUsingColor(*fill);
|
|
}
|
|
}
|
|
} else {
|
|
setPreparedImage(prepareBackgroundImage(std::move(image)));
|
|
}
|
|
}
|
|
Assert((!_pixmap.isNull() && !_pixmapForTiled.isNull())
|
|
|| colorForFill());
|
|
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::New, tile()));
|
|
if (needResetAdjustable) {
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::TestingTheme, tile()), true);
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::ApplyingTheme, tile()), true);
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setPreparedImage(QImage &&image) {
|
|
Expects(image.format() == QImage::Format_ARGB32_Premultiplied);
|
|
|
|
image.setDevicePixelRatio(cRetinaFactor());
|
|
|
|
if (adjustPaletteRequired()) {
|
|
adjustPaletteUsingBackground(image);
|
|
}
|
|
|
|
auto width = image.width();
|
|
auto height = image.height();
|
|
Assert(width > 0 && height > 0);
|
|
auto isSmallForTiled = (width < kMinimumTiledSize || height < kMinimumTiledSize);
|
|
if (isSmallForTiled) {
|
|
auto repeatTimesX = qCeil(kMinimumTiledSize / float64(width));
|
|
auto repeatTimesY = qCeil(kMinimumTiledSize / float64(height));
|
|
auto imageForTiled = QImage(width * repeatTimesX, height * repeatTimesY, QImage::Format_ARGB32_Premultiplied);
|
|
imageForTiled.setDevicePixelRatio(image.devicePixelRatio());
|
|
auto imageForTiledBytes = imageForTiled.bits();
|
|
auto bytesInLine = width * sizeof(uint32);
|
|
for (auto timesY = 0; timesY != repeatTimesY; ++timesY) {
|
|
auto imageBytes = image.constBits();
|
|
for (auto y = 0; y != height; ++y) {
|
|
for (auto timesX = 0; timesX != repeatTimesX; ++timesX) {
|
|
memcpy(imageForTiledBytes, imageBytes, bytesInLine);
|
|
imageForTiledBytes += bytesInLine;
|
|
}
|
|
imageBytes += image.bytesPerLine();
|
|
imageForTiledBytes += imageForTiled.bytesPerLine() - (repeatTimesX * bytesInLine);
|
|
}
|
|
}
|
|
_pixmapForTiled = App::pixmapFromImageInPlace(std::move(imageForTiled));
|
|
}
|
|
_pixmap = App::pixmapFromImageInPlace(std::move(image));
|
|
if (!isSmallForTiled) {
|
|
_pixmapForTiled = _pixmap;
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setPaper(const Data::WallPaper &paper) {
|
|
_paper = paper;
|
|
}
|
|
|
|
bool ChatBackground::adjustPaletteRequired() {
|
|
const auto usingThemeBackground = [&] {
|
|
return Data::IsThemeWallPaper(_paper)
|
|
|| Data::details::IsTestingThemeWallPaper(_paper);
|
|
};
|
|
const auto usingDefaultBackground = [&] {
|
|
return Data::IsDefaultWallPaper(_paper)
|
|
|| Data::details::IsTestingDefaultWallPaper(_paper);
|
|
};
|
|
const auto testingPalette = [&] {
|
|
const auto path = AreTestingTheme()
|
|
? GlobalApplying.pathAbsolute
|
|
: _themeAbsolutePath;
|
|
return IsPaletteTestingPath(path);
|
|
};
|
|
|
|
if (testingPalette()) {
|
|
return false;
|
|
} else if (isNonDefaultThemeOrBackground() || nightMode()) {
|
|
return !usingThemeBackground();
|
|
}
|
|
return !usingDefaultBackground();
|
|
}
|
|
|
|
void ChatBackground::adjustPaletteUsingBackground(const QImage &img) {
|
|
Assert(img.format() == QImage::Format_ARGB32_Premultiplied);
|
|
|
|
uint64 components[3] = { 0 };
|
|
uint64 componentsScroll[3] = { 0 };
|
|
auto w = img.width();
|
|
auto h = img.height();
|
|
auto size = w * h;
|
|
if (auto pix = img.constBits()) {
|
|
for (auto i = 0, l = size * 4; i != l; i += 4) {
|
|
components[2] += pix[i + 0];
|
|
components[1] += pix[i + 1];
|
|
components[0] += pix[i + 2];
|
|
}
|
|
}
|
|
|
|
if (size) {
|
|
for (auto i = 0; i != 3; ++i) {
|
|
components[i] /= size;
|
|
}
|
|
}
|
|
|
|
adjustPaletteUsingColor(
|
|
QColor(components[0], components[1], components[2]));
|
|
}
|
|
|
|
void ChatBackground::adjustPaletteUsingColor(QColor color) {
|
|
const auto hue = color.hslHueF();
|
|
const auto saturation = color.hslSaturationF();
|
|
for (const auto &color : _adjustableColors) {
|
|
adjustColor(color.item, hue, saturation);
|
|
}
|
|
}
|
|
|
|
std::optional<QColor> ChatBackground::colorForFill() const {
|
|
return _pixmap.isNull() ? _paper.backgroundColor() : std::nullopt;
|
|
}
|
|
|
|
QImage ChatBackground::createCurrentImage() const {
|
|
if (const auto fill = colorForFill()) {
|
|
auto result = QImage(
|
|
kMinimumTiledSize,
|
|
kMinimumTiledSize,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.fill(*fill);
|
|
return result;
|
|
}
|
|
return pixmap().toImage(); // #TODO patterns
|
|
}
|
|
|
|
bool ChatBackground::tile() const {
|
|
return nightMode() ? _tileNightValue : _tileDayValue;
|
|
}
|
|
|
|
bool ChatBackground::tileDay() const {
|
|
if (Data::details::IsTestingThemeWallPaper(_paper) ||
|
|
Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
if (!nightMode()) {
|
|
return _tileForRevert;
|
|
}
|
|
}
|
|
return _tileDayValue;
|
|
}
|
|
|
|
bool ChatBackground::tileNight() const {
|
|
if (Data::details::IsTestingThemeWallPaper(_paper) ||
|
|
Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
if (nightMode()) {
|
|
return _tileForRevert;
|
|
}
|
|
}
|
|
return _tileNightValue;
|
|
}
|
|
|
|
void ChatBackground::ensureStarted() {
|
|
if (_pixmap.isNull() && !_paper.backgroundColor()) {
|
|
// We should start first, otherwise the default call
|
|
// to start() will reset this value to _themeTile.
|
|
start();
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setTile(bool tile) {
|
|
ensureStarted();
|
|
const auto old = this->tile();
|
|
if (nightMode()) {
|
|
setTileNightValue(tile);
|
|
} else {
|
|
setTileDayValue(tile);
|
|
}
|
|
if (this->tile() != old) {
|
|
if (!Data::details::IsTestingThemeWallPaper(_paper)
|
|
&& !Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
Local::writeUserSettings();
|
|
}
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::Changed, tile));
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setTileDayValue(bool tile) {
|
|
ensureStarted();
|
|
_tileDayValue = tile;
|
|
}
|
|
|
|
void ChatBackground::setTileNightValue(bool tile) {
|
|
ensureStarted();
|
|
_tileNightValue = tile;
|
|
}
|
|
|
|
void ChatBackground::setThemeAbsolutePath(const QString &path) {
|
|
_themeAbsolutePath = path;
|
|
}
|
|
|
|
QString ChatBackground::themeAbsolutePath() const {
|
|
return _themeAbsolutePath;
|
|
}
|
|
|
|
void ChatBackground::reset() {
|
|
if (Data::details::IsTestingThemeWallPaper(_paper)
|
|
|| Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
if (_themeImage.isNull()) {
|
|
_paperForRevert = Data::DefaultWallPaper();
|
|
_imageForRevert = QImage();
|
|
_tileForRevert = false;
|
|
} else {
|
|
_paperForRevert = Data::ThemeWallPaper();
|
|
_imageForRevert = _themeImage;
|
|
_tileForRevert = _themeTile;
|
|
}
|
|
} else {
|
|
setImage(Data::ThemeWallPaper());
|
|
restoreAdjustableColors();
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::TestingTheme, tile()), true);
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::ApplyingTheme, tile()), true);
|
|
}
|
|
}
|
|
|
|
void ChatBackground::saveForRevert() {
|
|
ensureStarted();
|
|
if (!Data::details::IsTestingThemeWallPaper(_paper)
|
|
&& !Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
_paperForRevert = _paper;
|
|
_imageForRevert = std::move(_pixmap).toImage();
|
|
_tileForRevert = tile();
|
|
}
|
|
}
|
|
|
|
void ChatBackground::saveAdjustableColors() {
|
|
for (auto &color : _adjustableColors) {
|
|
color.original = color.item->c;
|
|
}
|
|
}
|
|
|
|
void ChatBackground::restoreAdjustableColors() {
|
|
for (const auto &color : _adjustableColors) {
|
|
const auto value = color.original;
|
|
color.item.set(value.red(), value.green(), value.blue(), value.alpha());
|
|
}
|
|
}
|
|
|
|
void ChatBackground::setTestingTheme(Instance &&theme) {
|
|
style::main_palette::apply(theme.palette);
|
|
saveAdjustableColors();
|
|
|
|
auto switchToThemeBackground = !theme.background.isNull()
|
|
|| Data::IsThemeWallPaper(_paper)
|
|
|| (Data::IsDefaultWallPaper(_paper)
|
|
&& !nightMode()
|
|
&& _themeAbsolutePath.isEmpty());
|
|
if (AreTestingTheme() && IsPaletteTestingPath(GlobalApplying.pathAbsolute)) {
|
|
// Grab current background image if it is not already custom
|
|
if (!Data::IsCustomWallPaper(_paper)) {
|
|
saveForRevert();
|
|
setImage(
|
|
Data::details::TestingEditorWallPaper(),
|
|
std::move(_pixmap).toImage());
|
|
}
|
|
} else if (switchToThemeBackground) {
|
|
saveForRevert();
|
|
setImage(
|
|
Data::details::TestingThemeWallPaper(),
|
|
std::move(theme.background));
|
|
setTile(theme.tiled);
|
|
} else {
|
|
// Apply current background image so that service bg colors are recounted.
|
|
// #TODO patterns
|
|
setImage(_paper, std::move(_pixmap).toImage());
|
|
}
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::TestingTheme, tile()), true);
|
|
}
|
|
|
|
void ChatBackground::setTestingDefaultTheme() {
|
|
style::main_palette::reset();
|
|
saveAdjustableColors();
|
|
|
|
saveForRevert();
|
|
setImage(Data::details::TestingDefaultWallPaper());
|
|
setTile(false);
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::TestingTheme, tile()), true);
|
|
}
|
|
|
|
void ChatBackground::keepApplied(const QString &path, bool write) {
|
|
setThemeAbsolutePath(path);
|
|
if (Data::details::IsTestingEditorWallPaper(_paper)) {
|
|
setPaper(Data::CustomWallPaper());
|
|
_themeImage = QImage();
|
|
_themeTile = false;
|
|
if (write) {
|
|
writeNewBackgroundSettings();
|
|
}
|
|
} else if (Data::details::IsTestingThemeWallPaper(_paper)) {
|
|
setPaper(Data::ThemeWallPaper());
|
|
_themeImage = prepareBackgroundImage(_pixmap.toImage());
|
|
_themeTile = tile();
|
|
if (write) {
|
|
writeNewBackgroundSettings();
|
|
}
|
|
} else if (Data::details::IsTestingDefaultWallPaper(_paper)) {
|
|
setPaper(Data::DefaultWallPaper());
|
|
_themeImage = QImage();
|
|
_themeTile = false;
|
|
if (write) {
|
|
writeNewBackgroundSettings();
|
|
}
|
|
}
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::ApplyingTheme, tile()), true);
|
|
}
|
|
|
|
bool ChatBackground::isNonDefaultThemeOrBackground() {
|
|
start();
|
|
return nightMode()
|
|
? (_themeAbsolutePath != NightThemePath()
|
|
|| !Data::IsThemeWallPaper(_paper))
|
|
: (!_themeAbsolutePath.isEmpty()
|
|
|| !Data::IsDefaultWallPaper(_paper));
|
|
}
|
|
|
|
bool ChatBackground::isNonDefaultBackground() {
|
|
start();
|
|
return _themeAbsolutePath.isEmpty()
|
|
? !Data::IsDefaultWallPaper(_paper)
|
|
: !Data::IsThemeWallPaper(_paper);
|
|
}
|
|
|
|
void ChatBackground::writeNewBackgroundSettings() {
|
|
if (tile() != _tileForRevert) {
|
|
Local::writeUserSettings();
|
|
}
|
|
Local::writeBackground(
|
|
_paper,
|
|
((Data::IsThemeWallPaper(_paper)
|
|
|| Data::IsDefaultWallPaper(_paper))
|
|
? QImage()
|
|
: _pixmap.toImage())); // #TODO patterns
|
|
}
|
|
|
|
void ChatBackground::revert() {
|
|
if (Data::details::IsTestingThemeWallPaper(_paper)
|
|
|| Data::details::IsTestingDefaultWallPaper(_paper)
|
|
|| Data::details::IsTestingEditorWallPaper(_paper)) {
|
|
setTile(_tileForRevert);
|
|
setImage(_paperForRevert, std::move(_imageForRevert));
|
|
} else {
|
|
// Apply current background image so that service bg colors are recounted.
|
|
// #TODO patterns
|
|
setImage(_paper, std::move(_pixmap).toImage());
|
|
}
|
|
notify(BackgroundUpdate(BackgroundUpdate::Type::RevertingTheme, tile()), true);
|
|
}
|
|
|
|
void ChatBackground::setNightModeValue(bool nightMode) {
|
|
_nightMode = nightMode;
|
|
}
|
|
|
|
bool ChatBackground::nightMode() const {
|
|
return _nightMode;
|
|
}
|
|
|
|
void ChatBackground::toggleNightMode(std::optional<QString> themePath) {
|
|
const auto settingDefault = themePath.has_value();
|
|
const auto oldNightMode = _nightMode;
|
|
const auto newNightMode = !_nightMode;
|
|
_nightMode = newNightMode;
|
|
auto read = settingDefault ? Saved() : Local::readThemeAfterSwitch();
|
|
auto path = read.pathAbsolute;
|
|
|
|
_nightMode = oldNightMode;
|
|
auto oldTileValue = (_nightMode ? _tileNightValue : _tileDayValue);
|
|
const auto alreadyOnDisk = [&] {
|
|
if (read.content.isEmpty()) {
|
|
return false;
|
|
}
|
|
auto preview = std::make_unique<Preview>();
|
|
preview->pathAbsolute = std::move(read.pathAbsolute);
|
|
preview->pathRelative = std::move(read.pathRelative);
|
|
preview->content = std::move(read.content);
|
|
preview->instance.cached = std::move(read.cache);
|
|
const auto loaded = loadTheme(
|
|
preview->content,
|
|
preview->instance.cached,
|
|
&preview->instance);
|
|
if (!loaded) {
|
|
return false;
|
|
}
|
|
Apply(std::move(preview));
|
|
return true;
|
|
}();
|
|
if (!alreadyOnDisk) {
|
|
path = themePath
|
|
? *themePath
|
|
: (newNightMode ? NightThemePath() : QString());
|
|
ApplyDefaultWithPath(path);
|
|
}
|
|
|
|
// Theme editor could have already reverted the testing of this toggle.
|
|
if (AreTestingTheme()) {
|
|
GlobalApplying.overrideKeep = [=] {
|
|
_nightMode = newNightMode;
|
|
|
|
// Restore the value, it was set inside theme testing.
|
|
(oldNightMode ? _tileNightValue : _tileDayValue) = oldTileValue;
|
|
|
|
if (!alreadyOnDisk) {
|
|
// First-time switch to default night mode should write it.
|
|
WriteAppliedTheme();
|
|
}
|
|
ClearApplying();
|
|
keepApplied(path, settingDefault);
|
|
if (tile() != _tileForRevert) {
|
|
Local::writeUserSettings();
|
|
}
|
|
Local::writeSettings();
|
|
if (!settingDefault && !Local::readBackground()) {
|
|
setImage(Data::ThemeWallPaper());
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
ChatBackground *Background() {
|
|
GlobalBackground.createIfNull();
|
|
return GlobalBackground.data();
|
|
}
|
|
|
|
bool Load(Saved &&saved) {
|
|
if (saved.content.size() < 4) {
|
|
LOG(("Theme Error: Could not load theme from '%1' (%2)"
|
|
).arg(saved.pathRelative
|
|
).arg(saved.pathAbsolute));
|
|
return false;
|
|
}
|
|
|
|
GlobalBackground.createIfNull();
|
|
if (loadThemeFromCache(saved.content, saved.cache)) {
|
|
Background()->setThemeAbsolutePath(saved.pathAbsolute);
|
|
return true;
|
|
}
|
|
|
|
if (!loadTheme(saved.content, saved.cache)) {
|
|
return false;
|
|
}
|
|
Local::writeTheme(saved);
|
|
Background()->setThemeAbsolutePath(saved.pathAbsolute);
|
|
return true;
|
|
}
|
|
|
|
void Unload() {
|
|
GlobalBackground.clear();
|
|
GlobalApplying = Applying();
|
|
}
|
|
|
|
bool Apply(const QString &filepath) {
|
|
if (auto preview = PreviewFromFile(filepath)) {
|
|
return Apply(std::move(preview));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Apply(std::unique_ptr<Preview> preview) {
|
|
GlobalApplying.pathRelative = std::move(preview->pathRelative);
|
|
GlobalApplying.pathAbsolute = std::move(preview->pathAbsolute);
|
|
GlobalApplying.content = std::move(preview->content);
|
|
GlobalApplying.cached = std::move(preview->instance.cached);
|
|
if (GlobalApplying.paletteForRevert.isEmpty()) {
|
|
GlobalApplying.paletteForRevert = style::main_palette::save();
|
|
}
|
|
Background()->setTestingTheme(std::move(preview->instance));
|
|
return true;
|
|
}
|
|
|
|
void ApplyDefaultWithPath(const QString &themePath) {
|
|
if (!themePath.isEmpty()) {
|
|
if (auto preview = PreviewFromFile(themePath)) {
|
|
Apply(std::move(preview));
|
|
}
|
|
} else {
|
|
GlobalApplying.pathRelative = QString();
|
|
GlobalApplying.pathAbsolute = QString();
|
|
GlobalApplying.content = QByteArray();
|
|
GlobalApplying.cached = Cached();
|
|
if (GlobalApplying.paletteForRevert.isEmpty()) {
|
|
GlobalApplying.paletteForRevert = style::main_palette::save();
|
|
}
|
|
Background()->setTestingDefaultTheme();
|
|
}
|
|
}
|
|
|
|
bool ApplyEditedPalette(const QString &path, const QByteArray &content) {
|
|
Instance out;
|
|
if (!loadColorScheme(content, &out)) {
|
|
return false;
|
|
}
|
|
out.cached.colors = out.palette.save();
|
|
out.cached.paletteChecksum = style::palette::Checksum();
|
|
out.cached.contentChecksum = hashCrc32(content.constData(), content.size());
|
|
|
|
GlobalApplying.pathRelative = path.isEmpty()
|
|
? QString()
|
|
: QDir().relativeFilePath(path);
|
|
GlobalApplying.pathAbsolute = path.isEmpty()
|
|
? QString()
|
|
: QFileInfo(path).absoluteFilePath();
|
|
GlobalApplying.content = content;
|
|
GlobalApplying.cached = out.cached;
|
|
if (GlobalApplying.paletteForRevert.isEmpty()) {
|
|
GlobalApplying.paletteForRevert = style::main_palette::save();
|
|
}
|
|
Background()->setTestingTheme(std::move(out));
|
|
KeepApplied();
|
|
return true;
|
|
}
|
|
|
|
void KeepApplied() {
|
|
if (!AreTestingTheme()) {
|
|
return;
|
|
} else if (GlobalApplying.overrideKeep) {
|
|
// This callback will be destroyed while running.
|
|
// And it won't be able to safely access captures after that.
|
|
// So we save it on stack for the time while it is running.
|
|
const auto onstack = base::take(GlobalApplying.overrideKeep);
|
|
onstack();
|
|
return;
|
|
}
|
|
const auto path = GlobalApplying.pathAbsolute;
|
|
WriteAppliedTheme();
|
|
ClearApplying();
|
|
Background()->keepApplied(path, true);
|
|
}
|
|
|
|
void Revert() {
|
|
if (!AreTestingTheme()) {
|
|
return;
|
|
}
|
|
style::main_palette::load(GlobalApplying.paletteForRevert);
|
|
Background()->saveAdjustableColors();
|
|
|
|
ClearApplying();
|
|
Background()->revert();
|
|
}
|
|
|
|
QString NightThemePath() {
|
|
return str_const_toString(kNightThemeFile);
|
|
}
|
|
|
|
bool IsNonDefaultBackground() {
|
|
return Background()->isNonDefaultBackground();
|
|
}
|
|
|
|
bool IsNightMode() {
|
|
return GlobalBackground ? Background()->nightMode() : false;
|
|
}
|
|
|
|
void SetNightModeValue(bool nightMode) {
|
|
if (GlobalBackground || nightMode) {
|
|
Background()->setNightModeValue(nightMode);
|
|
}
|
|
}
|
|
|
|
void ToggleNightMode() {
|
|
Background()->toggleNightMode(std::nullopt);
|
|
}
|
|
|
|
void ToggleNightMode(const QString &path) {
|
|
Background()->toggleNightMode(path);
|
|
}
|
|
|
|
bool LoadFromFile(const QString &path, Instance *out, QByteArray *outContent) {
|
|
*outContent = readThemeContent(path);
|
|
if (outContent->size() < 4) {
|
|
LOG(("Theme Error: Could not load theme from %1").arg(path));
|
|
return false;
|
|
}
|
|
|
|
return loadTheme(*outContent, out->cached, out);
|
|
}
|
|
|
|
bool IsPaletteTestingPath(const QString &path) {
|
|
if (path.endsWith(qstr(".tdesktop-palette"), Qt::CaseInsensitive)) {
|
|
return QFileInfo(path).exists();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ComputeBackgroundRects(QRect wholeFill, QSize imageSize, QRect &to, QRect &from) {
|
|
if (uint64(imageSize.width()) * wholeFill.height() > uint64(imageSize.height()) * wholeFill.width()) {
|
|
float64 pxsize = wholeFill.height() / float64(imageSize.height());
|
|
int takewidth = qCeil(wholeFill.width() / pxsize);
|
|
if (takewidth > imageSize.width()) {
|
|
takewidth = imageSize.width();
|
|
} else if ((imageSize.width() % 2) != (takewidth % 2)) {
|
|
++takewidth;
|
|
}
|
|
to = QRect(int((wholeFill.width() - takewidth * pxsize) / 2.), 0, qCeil(takewidth * pxsize), wholeFill.height());
|
|
from = QRect((imageSize.width() - takewidth) / 2, 0, takewidth, imageSize.height());
|
|
} else {
|
|
float64 pxsize = wholeFill.width() / float64(imageSize.width());
|
|
int takeheight = qCeil(wholeFill.height() / pxsize);
|
|
if (takeheight > imageSize.height()) {
|
|
takeheight = imageSize.height();
|
|
} else if ((imageSize.height() % 2) != (takeheight % 2)) {
|
|
++takeheight;
|
|
}
|
|
to = QRect(0, int((wholeFill.height() - takeheight * pxsize) / 2.), wholeFill.width(), qCeil(takeheight * pxsize));
|
|
from = QRect(0, (imageSize.height() - takeheight) / 2, imageSize.width(), takeheight);
|
|
}
|
|
}
|
|
|
|
bool CopyColorsToPalette(const QString &path, const QByteArray &themeContent) {
|
|
auto paletteContent = themeContent;
|
|
|
|
zlib::FileToRead file(themeContent);
|
|
|
|
unz_global_info globalInfo = { 0 };
|
|
file.getGlobalInfo(&globalInfo);
|
|
if (file.error() == UNZ_OK) {
|
|
paletteContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
|
|
if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
|
|
file.clearError();
|
|
paletteContent = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
|
|
}
|
|
if (file.error() != UNZ_OK) {
|
|
LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file, while copying to '%1'.").arg(path));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::WriteOnly)) {
|
|
LOG(("Theme Error: could not open file for write '%1'").arg(path));
|
|
return false;
|
|
}
|
|
|
|
if (f.write(paletteContent) != paletteContent.size()) {
|
|
LOG(("Theme Error: could not write palette to '%1'").arg(path));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ReadPaletteValues(const QByteArray &content, Fn<bool(QLatin1String name, QLatin1String value)> callback) {
|
|
if (content.size() > kThemeSchemeSizeLimit) {
|
|
LOG(("Theme Error: color scheme file too large (should be less than 1 MB, got %2)").arg(content.size()));
|
|
return false;
|
|
}
|
|
|
|
auto data = base::parse::stripComments(content);
|
|
auto from = data.constData(), end = from + data.size();
|
|
while (from != end) {
|
|
auto name = QLatin1String("");
|
|
auto value = QLatin1String("");
|
|
if (!readNameAndValue(from, end, &name, &value)) {
|
|
return false;
|
|
}
|
|
if (name.size() == 0) { // End of content reached.
|
|
return true;
|
|
}
|
|
if (!callback(name, value)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QColor PatternColor(QColor background) {
|
|
const auto hue = background.hueF();
|
|
const auto saturation = background.saturationF();
|
|
const auto value = background.valueF();
|
|
return QColor::fromHsvF(
|
|
hue,
|
|
std::min(1.0, saturation + 0.05 + 0.1 * (1. - saturation)),
|
|
(value > 0.5
|
|
? std::max(0., value * 0.65)
|
|
: std::max(0., std::min(1., 1. - value * 0.65))),
|
|
0.4
|
|
).toRgb();
|
|
}
|
|
|
|
} // namespace Theme
|
|
} // namespace Window
|