/* 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 "window/themes/window_themes_embedded.h" #include "window/themes/window_theme_editor.h" #include "window/window_controller.h" #include "platform/platform_specific.h" #include "mainwidget.h" #include "main/main_session.h" #include "apiwrap.h" #include "storage/localstorage.h" #include "storage/localimageloader.h" #include "storage/file_upload.h" #include "base/openssl_help.h" #include "base/parse_helper.h" #include "base/zlib_help.h" #include "base/unixtime.h" #include "base/crc32hash.h" #include "data/data_session.h" #include "main/main_account.h" // Account::local. #include "main/main_domain.h" // Domain::activeSessionValue. #include "ui/image/image.h" #include "ui/ui_utility.h" #include "boxes/confirm_box.h" #include "boxes/background_box.h" #include "core/application.h" #include "app.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" #include namespace Window { namespace Theme { namespace { constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024; constexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024; constexpr auto kNightThemeFile = ":/gui/night.tdesktop-theme"_cs; constexpr auto kMinimumTiledSize = 512; struct Applying { Saved data; QByteArray paletteForRevert; Fn overrideKeep; }; NeverFreedPointer GlobalBackground; Applying GlobalApplying; inline bool AreTestingTheme() { return !GlobalApplying.paletteForRevert.isEmpty(); } bool CalculateIsMonoColorImage(const QImage &image) { if (!image.isNull()) { const auto bits = reinterpret_cast(image.constBits()); const auto first = bits[0]; for (auto i = 0; i < image.width() * image.height(); i++) { if (first != bits[i]) { return false; } } return true; } return false; } 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, const Colorizer &colorizer, 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 (colorizer) { Colorize(name, r, g, b, colorizer); } 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, const Colorizer &colorizer, Instance *out) { auto unsupported = QMap(); return ReadPaletteValues(content, [&](QLatin1String name, QLatin1String value) { // Find the named value in the already read unsupported list. value = unsupported.value(value, value); auto result = setColorSchemeValue(name, value, colorizer, 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); } } 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, const Colorizer &colorizer, const std::optional &editedPalette, Cached *cache = nullptr, Instance *out = nullptr) { if (content.size() < 4) { LOG(("Theme Error: Bad theme content size: %1").arg(content.size())); return false; } if (cache) { *cache = Cached(); } zlib::FileToRead file(content); const auto emptyColorizer = Colorizer(); const auto &paletteColorizer = editedPalette ? emptyColorizer : colorizer; unz_global_info globalInfo = { 0 }; file.getGlobalInfo(&globalInfo); if (file.error() == UNZ_OK) { auto schemeContent = editedPalette.value_or(QByteArray()); if (schemeContent.isEmpty()) { schemeContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit); } if (schemeContent.isEmpty()) { 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, paletteColorizer, out)) { return false; } if (!out) { 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; } if (colorizer) { Colorize(background, colorizer); } if (cache) { 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), backgroundTiled, out); } } else { // Looks like it is not a .zip theme. if (!loadColorScheme(editedPalette.value_or(content), paletteColorizer, out)) { return false; } if (!out) { Background()->saveAdjustableColors(); } } if (out) { out->palette.finalize(); } if (cache) { if (out) { cache->colors = out->palette.save(); } else { cache->colors = style::main_palette::save(); } cache->paletteChecksum = style::palette::Checksum(); cache->contentChecksum = base::crc32(content.constData(), content.size()); } return true; } bool InitializeFromCache( const QByteArray &content, const Cached &cache) { if (cache.paletteChecksum != style::palette::Checksum()) { return false; } if (cache.contentChecksum != base::crc32(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; } [[nodiscard]] std::optional ReadEditingPalette() { auto file = QFile(EditingPalettePath()); return file.open(QIODevice::ReadOnly) ? std::make_optional(file.readAll()) : std::nullopt; } bool InitializeFromSaved(Saved &&saved) { if (saved.object.content.size() < 4) { LOG(("Theme Error: Could not load theme from '%1' (%2)").arg( saved.object.pathRelative, saved.object.pathAbsolute)); return false; } const auto editing = ReadEditingPalette(); GlobalBackground.createIfNull(); if (!editing && InitializeFromCache(saved.object.content, saved.cache)) { return true; } const auto colorizer = ColorizerForTheme(saved.object.pathAbsolute); if (!LoadTheme(saved.object.content, colorizer, editing, &saved.cache)) { return false; } if (editing) { Background()->setEditingTheme(ReadCloudFromText(*editing)); } else { Local::writeTheme(saved); } return true; } [[nodiscard]] QImage DitherImage(QImage image) { Expects(image.bytesPerLine() == image.width() * 4); const auto width = image.width(); const auto height = image.height(); if (width < 16 || height < 16) { return image; } const auto area = width * height; const auto shifts = std::make_unique(area); memset_rand(shifts.get(), area); // shiftx = int(shift & 0x0F) - 8; shifty = int(shift >> 4) - 8; // Clamp shifts close to edges. for (auto y = 0; y != 8; ++y) { const auto min = 8 - y; const auto shifted = (min << 4); auto shift = shifts.get() + y * width; for (const auto till = shift + width; shift != till; ++shift) { if ((*shift >> 4) < min) { *shift = shifted | (*shift & 0x0F); } } } for (auto y = height - 7; y != height; ++y) { const auto max = 8 + (height - y - 1); const auto shifted = (max << 4); auto shift = shifts.get() + y * width; for (const auto till = shift + width; shift != till; ++shift) { if ((*shift >> 4) > max) { *shift = shifted | (*shift & 0x0F); } } } for (auto shift = shifts.get(), ytill = shift + area ; shift != ytill ; shift += width - 8) { for (const auto till = shift + 8; shift != till; ++shift) { const auto min = (till - shift); if ((*shift & 0x0F) < min) { *shift = (*shift & 0xF0) | min; } } } for (auto shift = shifts.get(), ytill = shift + area; shift != ytill;) { shift += width - 7; for (const auto till = shift + 7; shift != till; ++shift) { const auto max = 8 + (till - shift - 1); if ((*shift & 0x0F) > max) { *shift = (*shift & 0xF0) | max; } } } auto result = image; result.detach(); const auto src = reinterpret_cast(image.constBits()); const auto dst = reinterpret_cast(result.bits()); for (auto index = 0; index != area; ++index) { const auto shift = shifts[index]; const auto shiftx = int(shift & 0x0F) - 8; const auto shifty = int(shift >> 4) - 8; dst[index] = src[index + (shifty * width) + shiftx]; } return result; } [[nodiscard]] QImage PostprocessBackgroundImage( QImage image, const Data::WallPaper &paper) { if (image.format() != QImage::Format_ARGB32_Premultiplied) { image = std::move(image).convertToFormat( QImage::Format_ARGB32_Premultiplied); } image.setDevicePixelRatio(cRetinaFactor()); if (Data::IsDefaultWallPaper(paper) || Data::details::IsTestingDefaultWallPaper(paper)) { return DitherImage(std::move(image)); } return image; } void ClearApplying() { GlobalApplying = Applying(); } SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) { PreparedPhotoThumbs thumbnails; QVector sizes; QByteArray jpeg; QBuffer jpegBuffer(&jpeg); image.save(&jpegBuffer, "JPG", 87); const auto scaled = [&](int size) { return image.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); }; const auto push = [&](const char *type, QImage &&image) { sizes.push_back(MTP_photoSize( MTP_string(type), MTP_int(image.width()), MTP_int(image.height()), MTP_int(0))); thumbnails.emplace( type[0], PreparedPhotoThumb{ .image = std::move(image) }); }; push("s", scaled(320)); const auto filename = qsl("wallpaper.jpg"); auto attributes = QVector( 1, MTP_documentAttributeFilename(MTP_string(filename))); attributes.push_back(MTP_documentAttributeImageSize( MTP_int(image.width()), MTP_int(image.height()))); const auto id = openssl::RandomValue(); const auto document = MTP_document( MTP_flags(0), MTP_long(id), MTP_long(0), MTP_bytes(), MTP_int(base::unixtime::now()), MTP_string("image/jpeg"), MTP_int(jpeg.size()), MTP_vector(sizes), MTPVector(), MTP_int(dcId), MTP_vector(attributes)); return SendMediaReady( SendMediaType::ThemeFile, QString(), // filepath filename, jpeg.size(), jpeg, id, 0, QString(), PeerId(), MTP_photoEmpty(MTP_long(0)), thumbnails, document, QByteArray(), 0); } void ClearEditingPalette() { QFile(EditingPalettePath()).remove(); } } // namespace ChatBackground::AdjustableColor::AdjustableColor(style::color data) : item(data) , original(data->c) { } // They're duplicated in window_theme_editor_box.cpp:ReplaceAdjustableColors. ChatBackground::ChatBackground() : _adjustableColors({ st::msgServiceBg, st::msgServiceBgSelected, st::historyScrollBg, st::historyScrollBgOver, st::historyScrollBarBg, st::historyScrollBarBgOver }) { } void ChatBackground::setThemeData(QImage &&themeImage, bool themeTile) { _themeImage = PostprocessBackgroundImage( std::move(themeImage), Data::ThemeWallPaper()); _themeTile = themeTile; } void ChatBackground::initialRead() { if (started()) { return; } else if (!Local::readBackground()) { set(Data::ThemeWallPaper()); } if (_localStoredTileDayValue) { _tileDayValue = *_localStoredTileDayValue; } if (_localStoredTileNightValue) { _tileNightValue = *_localStoredTileNightValue; } } void ChatBackground::start() { saveAdjustableColors(); _updates.events( ) | rpl::start_with_next([=](const BackgroundUpdate &update) { if (update.paletteChanged()) { style::NotifyPaletteChanged(); } }, _lifetime); initialRead(); Core::App().domain().activeSessionValue( ) | rpl::filter([=](Main::Session *session) { return session != _session; }) | rpl::start_with_next([=](Main::Session *session) { _session = session; checkUploadWallPaper(); }, _lifetime); Core::App().settings().setSystemDarkMode(Platform::IsDarkMode()); } void ChatBackground::checkUploadWallPaper() { if (!_session) { _wallPaperUploadLifetime = rpl::lifetime(); _wallPaperUploadId = FullMsgId(); _wallPaperRequestId = 0; return; } if (const auto id = base::take(_wallPaperUploadId)) { _session->uploader().cancel(id); } if (const auto id = base::take(_wallPaperRequestId)) { _session->api().request(id).cancel(); } if (!Data::IsCustomWallPaper(_paper) || _original.isNull() || _editingTheme.has_value()) { return; } const auto ready = PrepareWallPaper(_session->mainDcId(), _original); const auto documentId = ready.id; _wallPaperUploadId = FullMsgId(0, _session->data().nextLocalMessageId()); _session->uploader().uploadMedia(_wallPaperUploadId, ready); if (_wallPaperUploadLifetime) { return; } _wallPaperUploadLifetime = _session->uploader().documentReady( ) | rpl::start_with_next([=](const Storage::UploadedDocument &data) { if (data.fullId != _wallPaperUploadId) { return; } _wallPaperUploadId = FullMsgId(); _wallPaperRequestId = _session->api().request( MTPaccount_UploadWallPaper( data.file, MTP_string("image/jpeg"), _paper.mtpSettings() ) ).done([=](const MTPWallPaper &result) { result.match([&](const MTPDwallPaper &data) { _session->data().documentConvert( _session->data().document(documentId), data.vdocument()); }, [&](const MTPDwallPaperNoFile &data) { LOG(("API Error: " "Got wallPaperNoFile after account.UploadWallPaper.")); }); if (const auto paper = Data::WallPaper::Create(_session, result)) { setPaper(*paper); writeNewBackgroundSettings(); _updates.fire({ BackgroundUpdate::Type::New, tile() }); } }).send(); }); } QImage ChatBackground::postprocessBackgroundImage(QImage image) { return PostprocessBackgroundImage(std::move(image), _paper); } void ChatBackground::set(const Data::WallPaper &paper, QImage image) { image = PreprocessBackgroundImage(std::move(image)); const auto needResetAdjustable = Data::IsDefaultWallPaper(paper) && !Data::IsDefaultWallPaper(_paper) && !nightMode() && _themeObject.pathAbsolute.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(_themeImage, _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/background.jpg")); setPaper(Data::details::TestingDefaultWallPaper()); } image = postprocessBackgroundImage(std::move(image)); setPreparedImage(image, 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( style::ConvertScale(image.width(), scale), Qt::SmoothTransformation); } } else if (Data::IsDefaultWallPaper(_paper) || (!_paper.backgroundColor() && image.isNull())) { setPaper(Data::DefaultWallPaper().withParamsFrom(_paper)); image.load(qsl(":/gui/art/background.jpg")); } Local::writeBackground( _paper, ((Data::IsDefaultWallPaper(_paper) || Data::IsLegacy1DefaultWallPaper(_paper)) ? QImage() : image)); if (const auto fill = _paper.backgroundColor()) { if (_paper.isPattern() && !image.isNull()) { auto prepared = postprocessBackgroundImage( Data::PreparePatternImage( image, *fill, Data::PatternColor(*fill), _paper.patternIntensity())); setPreparedImage(std::move(image), std::move(prepared)); } else { _original = QImage(); _pixmap = QPixmap(); _pixmapForTiled = QPixmap(); if (adjustPaletteRequired()) { adjustPaletteUsingColor(*fill); } } } else { image = postprocessBackgroundImage(std::move(image)); setPreparedImage(image, image); } } Assert(colorForFill() || (!_original.isNull() && !_pixmap.isNull() && !_pixmapForTiled.isNull())); _updates.fire({ BackgroundUpdate::Type::New, tile() }); // delayed? if (needResetAdjustable) { _updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() }); _updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() }); } checkUploadWallPaper(); } void ChatBackground::setPreparedImage(QImage original, QImage prepared) { Expects(original.format() == QImage::Format_ARGB32_Premultiplied); Expects(original.width() > 0 && original.height() > 0); Expects(prepared.format() == QImage::Format_ARGB32_Premultiplied); Expects(prepared.width() > 0 && prepared.height() > 0); _original = std::move(original); if (!_paper.isPattern() && _paper.isBlurred()) { prepared = Data::PrepareBlurredBackground(std::move(prepared)); } if (adjustPaletteRequired()) { adjustPaletteUsingBackground(prepared); } preparePixmaps(std::move(prepared)); } void ChatBackground::preparePixmaps(QImage image) { const auto width = image.width(); const auto height = image.height(); const auto isSmallForTiled = (width < kMinimumTiledSize) || (height < kMinimumTiledSize); if (isSmallForTiled) { const auto repeatTimesX = qCeil(kMinimumTiledSize / (1. * width)); const auto repeatTimesY = qCeil(kMinimumTiledSize / (1. * 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 = Ui::PixmapFromImage(std::move(imageForTiled)); } _isMonoColorImage = CalculateIsMonoColorImage(image); _pixmap = Ui::PixmapFromImage(std::move(image)); if (!isSmallForTiled) { _pixmapForTiled = _pixmap; } } void ChatBackground::setPaper(const Data::WallPaper &paper) { _paper = paper.withoutImageData(); } 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); }; if (_editingTheme.has_value()) { return false; } else if (isNonDefaultThemeOrBackground() || nightMode()) { return !usingThemeBackground(); } return !usingDefaultBackground(); } std::optional ChatBackground::editingTheme() const { return _editingTheme; } void ChatBackground::setEditingTheme(const Data::CloudTheme &editing) { _editingTheme = editing; } void ChatBackground::clearEditingTheme(ClearEditing clear) { if (!_editingTheme) { return; } _editingTheme = std::nullopt; if (clear == ClearEditing::Temporary) { return; } ClearEditingPalette(); if (clear == ClearEditing::RevertChanges) { reapplyWithNightMode(std::nullopt, _nightMode); KeepApplied(); } } void ChatBackground::adjustPaletteUsingBackground(const QImage &image) { adjustPaletteUsingColor(CountAverageColor(image)); } void ChatBackground::adjustPaletteUsingColor(QColor color) { const auto prepared = color.toHsl(); for (const auto &adjustable : _adjustableColors) { const auto adjusted = AdjustedColor(adjustable.item->c, prepared); adjustable.item.set( adjusted.red(), adjusted.green(), adjusted.blue(), adjusted.alpha()); } } std::optional 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(); } 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; } bool ChatBackground::isMonoColorImage() const { return _isMonoColorImage; } void ChatBackground::setTile(bool tile) { Expects(started()); 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::writeSettings(); } _updates.fire({ BackgroundUpdate::Type::Changed, tile }); // delayed? } } void ChatBackground::setTileDayValue(bool tile) { if (started()) { _tileDayValue = tile; } else { _localStoredTileDayValue = tile; } } void ChatBackground::setTileNightValue(bool tile) { if (started()) { _tileNightValue = tile; } else { _localStoredTileNightValue = tile; } } void ChatBackground::setThemeObject(const Object &object) { _themeObject = object; _themeObject.content = QByteArray(); } const Object &ChatBackground::themeObject() const { return _themeObject; } void ChatBackground::reset() { if (Data::details::IsTestingThemeWallPaper(_paper) || Data::details::IsTestingDefaultWallPaper(_paper)) { if (_themeImage.isNull()) { _paperForRevert = Data::DefaultWallPaper(); _originalForRevert = QImage(); _tileForRevert = false; } else { _paperForRevert = Data::ThemeWallPaper(); _originalForRevert = _themeImage; _tileForRevert = _themeTile; } } else { set(Data::ThemeWallPaper()); restoreAdjustableColors(); _updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() }); _updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() }); } writeNewBackgroundSettings(); } bool ChatBackground::started() const { return !Data::details::IsUninitializedWallPaper(_paper); } void ChatBackground::saveForRevert() { Expects(started()); if (!Data::details::IsTestingThemeWallPaper(_paper) && !Data::details::IsTestingDefaultWallPaper(_paper)) { _paperForRevert = _paper; _originalForRevert = std::move(_original); _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() && _themeObject.pathAbsolute.isEmpty()); if (AreTestingTheme() && _editingTheme.has_value()) { // Grab current background image if it is not already custom // Use prepared pixmap, not original image, because we're // for sure switching to a non-pattern wall-paper (testing editor). if (!Data::IsCustomWallPaper(_paper)) { saveForRevert(); set( Data::details::TestingEditorWallPaper(), std::move(_pixmap).toImage()); } } else if (switchToThemeBackground) { saveForRevert(); set( Data::details::TestingThemeWallPaper(), std::move(theme.background)); setTile(theme.tiled); } else { // Apply current background image so that service bg colors are recounted. set(_paper, std::move(_original)); } _updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() }); } void ChatBackground::setTestingDefaultTheme() { style::main_palette::reset(); saveAdjustableColors(); saveForRevert(); set(Data::details::TestingDefaultWallPaper()); setTile(false); _updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() }); } void ChatBackground::keepApplied(const Object &object, bool write) { setThemeObject(object); 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 = postprocessBackgroundImage(base::duplicate(_original)); _themeTile = tile(); if (write) { writeNewBackgroundSettings(); } } else if (Data::details::IsTestingDefaultWallPaper(_paper)) { setPaper(Data::DefaultWallPaper()); _themeImage = QImage(); _themeTile = false; if (write) { writeNewBackgroundSettings(); } } _updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() }); } bool ChatBackground::isNonDefaultThemeOrBackground() { initialRead(); return nightMode() ? (_themeObject.pathAbsolute != NightThemePath() || !Data::IsThemeWallPaper(_paper)) : (!_themeObject.pathAbsolute.isEmpty() || !Data::IsDefaultWallPaper(_paper)); } bool ChatBackground::isNonDefaultBackground() { initialRead(); return _themeObject.pathAbsolute.isEmpty() ? !Data::IsDefaultWallPaper(_paper) : !Data::IsThemeWallPaper(_paper); } void ChatBackground::writeNewBackgroundSettings() { if (tile() != _tileForRevert) { Local::writeSettings(); } Local::writeBackground( _paper, ((Data::IsThemeWallPaper(_paper) || Data::IsDefaultWallPaper(_paper)) ? QImage() : _original)); } void ChatBackground::revert() { if (Data::details::IsTestingThemeWallPaper(_paper) || Data::details::IsTestingDefaultWallPaper(_paper) || Data::details::IsTestingEditorWallPaper(_paper)) { setTile(_tileForRevert); set(_paperForRevert, std::move(_originalForRevert)); } else { // Apply current background image so that service bg colors are recounted. set(_paper, std::move(_original)); } _updates.fire({ BackgroundUpdate::Type::RevertingTheme, tile() }); } void ChatBackground::appliedEditedPalette() { _updates.fire({ BackgroundUpdate::Type::ApplyingEdit, tile() }); } void ChatBackground::downloadingStarted(bool tile) { _updates.fire({ BackgroundUpdate::Type::Start, tile }); } void ChatBackground::setNightModeValue(bool nightMode) { _nightMode = nightMode; } bool ChatBackground::nightMode() const { return _nightMode; } void ChatBackground::reapplyWithNightMode( std::optional themePath, bool newNightMode) { if (!started()) { // We can get here from legacy passcoded state. // In this case Background() is not started yet, because // some settings and the background itself were not read. return; } else if (_nightMode != newNightMode && !nightModeChangeAllowed()) { return; } const auto settingExactTheme = themePath.has_value(); const auto nightModeChanged = (newNightMode != _nightMode); const auto oldNightMode = _nightMode; _nightMode = newNightMode; auto read = settingExactTheme ? Saved() : Local::readThemeAfterSwitch(); auto path = read.object.pathAbsolute; _nightMode = oldNightMode; auto oldTileValue = (_nightMode ? _tileNightValue : _tileDayValue); const auto alreadyOnDisk = [&] { if (read.object.content.isEmpty()) { return false; } auto preview = std::make_unique(); preview->object = std::move(read.object); preview->instance.cached = std::move(read.cache); const auto loaded = LoadTheme( preview->object.content, ColorizerForTheme(path), std::nullopt, &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 = [=] { if (nightModeChanged) { _nightMode = newNightMode; // Restore the value, it was set inside theme testing. (oldNightMode ? _tileNightValue : _tileDayValue) = oldTileValue; } const auto saved = std::move(GlobalApplying.data); if (!alreadyOnDisk) { // First-time switch to default night mode should write it. Local::writeTheme(saved); } ClearApplying(); keepApplied(saved.object, settingExactTheme); if (tile() != _tileForRevert || nightModeChanged) { Local::writeSettings(); } if (!settingExactTheme && !Local::readBackground()) { set(Data::ThemeWallPaper()); } }; } } bool ChatBackground::nightModeChangeAllowed() const { const auto &settings = Core::App().settings(); const auto allowedToBeAfterChange = settings.systemDarkModeEnabled() ? settings.systemDarkMode().value_or(!_nightMode) : !_nightMode; return (_nightMode != allowedToBeAfterChange); } void ChatBackground::toggleNightMode(std::optional themePath) { reapplyWithNightMode(themePath, !_nightMode); } ChatBackground *Background() { GlobalBackground.createIfNull(); return GlobalBackground.data(); } bool IsEmbeddedTheme(const QString &path) { return path.isEmpty() || path.startsWith(qstr(":/gui/")); } bool Initialize(Saved &&saved) { if (InitializeFromSaved(std::move(saved))) { Background()->setThemeObject(saved.object); return true; } return false; } void Uninitialize() { GlobalBackground.clear(); GlobalApplying = Applying(); } bool Apply( const QString &filepath, const Data::CloudTheme &cloud) { if (auto preview = PreviewFromFile(QByteArray(), filepath, cloud)) { return Apply(std::move(preview)); } return false; } bool Apply(std::unique_ptr preview) { GlobalApplying.data.object = std::move(preview->object); GlobalApplying.data.cache = 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(QByteArray(), themePath, {})) { Apply(std::move(preview)); } } else { GlobalApplying.data = Saved(); if (GlobalApplying.paletteForRevert.isEmpty()) { GlobalApplying.paletteForRevert = style::main_palette::save(); } Background()->setTestingDefaultTheme(); } } bool ApplyEditedPalette(const QByteArray &content) { auto out = Instance(); if (!loadColorScheme(content, Colorizer(), &out)) { return false; } style::main_palette::apply(out.palette); Background()->appliedEditedPalette(); 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 saved = std::move(GlobalApplying.data); Local::writeTheme(saved); ClearApplying(); Background()->keepApplied(saved.object, true); } void KeepFromEditor( const QByteArray &originalContent, const ParsedTheme &originalParsed, const Data::CloudTheme &cloud, const QByteArray &themeContent, const ParsedTheme &themeParsed, const QImage &background) { ClearApplying(); const auto content = themeContent.isEmpty() ? originalContent : themeContent; auto saved = Saved(); auto &cache = saved.cache; auto &object = saved.object; cache.colors = style::main_palette::save(); cache.paletteChecksum = style::palette::Checksum(); cache.contentChecksum = base::crc32(content.constData(), content.size()); cache.background = themeParsed.background; cache.tiled = themeParsed.tiled; object.cloud = cloud; object.content = themeContent.isEmpty() ? originalContent : themeContent; object.pathAbsolute = object.pathRelative = CachedThemePath( cloud.documentId); Local::writeTheme(saved); Background()->keepApplied(saved.object, true); Background()->setThemeData( base::duplicate(background), themeParsed.tiled); Background()->set(Data::ThemeWallPaper()); Background()->writeNewBackgroundSettings(); } void Revert() { if (!AreTestingTheme()) { return; } style::main_palette::load(GlobalApplying.paletteForRevert); Background()->saveAdjustableColors(); ClearApplying(); Background()->revert(); } QString NightThemePath() { return kNightThemeFile.utf16(); } 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); } void ToggleNightModeWithConfirmation( not_null window, Fn toggle) { if (Background()->nightModeChangeAllowed()) { toggle(); } else { const auto disableAndToggle = [=](Fn &&close) { Core::App().settings().setSystemDarkModeEnabled(false); Core::App().saveSettingsDelayed(); toggle(); close(); }; window->show(Box( tr::lng_settings_auto_night_warning(tr::now), tr::lng_settings_auto_night_disable(tr::now), disableAndToggle)); } } void ResetToSomeDefault() { Background()->reapplyWithNightMode( IsNightMode() ? NightThemePath() : QString(), IsNightMode()); } bool LoadFromFile( const QString &path, not_null out, Cached *outCache, not_null outContent) { *outContent = readThemeContent(path); const auto colorizer = ColorizerForTheme(path); return LoadTheme(*outContent, colorizer, std::nullopt, outCache, out); } bool LoadFromContent( const QByteArray &content, not_null out, Cached *outCache) { return LoadTheme(content, Colorizer(), std::nullopt, outCache, out); } QString EditingPalettePath() { return cWorkingDir() + "tdata/editing-theme.tdesktop-palette"; } QColor CountAverageColor(const QImage &image) { Expects(image.format() == QImage::Format_ARGB32_Premultiplied); uint64 components[3] = { 0 }; const auto w = image.width(); const auto h = image.height(); const auto size = w * h; if (const auto pix = image.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; } } return QColor(components[0], components[1], components[2]); } QColor AdjustedColor(QColor original, QColor background) { return QColor::fromHslF( background.hslHueF(), background.hslSaturationF(), original.lightnessF(), original.alphaF() ).toRgb(); } QImage PreprocessBackgroundImage(QImage image) { constexpr auto kMaxSize = 2960; if (image.format() != QImage::Format_ARGB32_Premultiplied) { image = std::move(image).convertToFormat( QImage::Format_ARGB32_Premultiplied); } if (image.width() > 40 * image.height()) { const auto width = 40 * image.height(); const auto height = image.height(); image = image.copy((image.width() - width) / 2, 0, width, height); } else if (image.height() > 40 * image.width()) { const auto width = image.width(); const auto height = 40 * image.width(); image = image.copy(0, (image.height() - height) / 2, width, height); } if (image.width() > kMaxSize || image.height() > kMaxSize) { image = image.scaled( kMaxSize, kMaxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } return image; } 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 ReadPaletteValues(const QByteArray &content, Fn 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; } } // namespace Theme } // namespace Window