1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "window/themes/window_theme.h"
9 
10 #include "window/themes/window_theme_preview.h"
11 #include "window/themes/window_themes_embedded.h"
12 #include "window/themes/window_theme_editor.h"
13 #include "window/window_controller.h"
14 #include "platform/platform_specific.h"
15 #include "mainwidget.h"
16 #include "main/main_session.h"
17 #include "apiwrap.h"
18 #include "storage/localstorage.h"
19 #include "storage/localimageloader.h"
20 #include "storage/file_upload.h"
21 #include "base/random.h"
22 #include "base/parse_helper.h"
23 #include "base/zlib_help.h"
24 #include "base/unixtime.h"
25 #include "base/crc32hash.h"
26 #include "data/data_session.h"
27 #include "data/data_document_resolver.h"
28 #include "main/main_account.h" // Account::local.
29 #include "main/main_domain.h" // Domain::activeSessionValue.
30 #include "ui/chat/chat_theme.h"
31 #include "ui/image/image.h"
32 #include "ui/style/style_palette_colorizer.h"
33 #include "ui/ui_utility.h"
34 #include "ui/boxes/confirm_box.h"
35 #include "boxes/background_box.h"
36 #include "core/application.h"
37 #include "styles/style_widgets.h"
38 #include "styles/style_chat.h"
39 
40 #include <QtCore/QBuffer>
41 
42 namespace Window {
43 namespace Theme {
44 namespace {
45 
46 constexpr auto kThemeFileSizeLimit = 5 * 1024 * 1024;
47 constexpr auto kBackgroundSizeLimit = 25 * 1024 * 1024;
48 constexpr auto kNightThemeFile = ":/gui/night.tdesktop-theme"_cs;
49 constexpr auto kDarkValueThreshold = 0.5;
50 
51 struct Applying {
52 	Saved data;
53 	QByteArray paletteForRevert;
54 	Fn<void()> overrideKeep;
55 };
56 
57 NeverFreedPointer<ChatBackground> GlobalBackground;
58 Applying GlobalApplying;
59 
AreTestingTheme()60 inline bool AreTestingTheme() {
61 	return !GlobalApplying.paletteForRevert.isEmpty();
62 }
63 
ReadDefaultImage()64 [[nodiscard]] QImage ReadDefaultImage() {
65 	return Ui::ReadBackgroundImage(
66 		u":/gui/art/background.tgv"_q,
67 		QByteArray(),
68 		true);
69 }
70 
GoodImageFormatAndSize(const QImage & image)71 [[nodiscard]] bool GoodImageFormatAndSize(const QImage &image) {
72 	return !image.size().isEmpty()
73 		&& (image.format() == QImage::Format_ARGB32_Premultiplied
74 			|| image.format() == QImage::Format_RGB32);
75 }
76 
readThemeContent(const QString & path)77 QByteArray readThemeContent(const QString &path) {
78 	QFile file(path);
79 	if (!file.exists()) {
80 		LOG(("Theme Error: theme file not found: %1").arg(path));
81 		return QByteArray();
82 	}
83 
84 	if (file.size() > kThemeFileSizeLimit) {
85 		LOG(("Theme Error: theme file too large: %1 (should be less than 5 MB, got %2)").arg(path).arg(file.size()));
86 		return QByteArray();
87 	}
88 	if (!file.open(QIODevice::ReadOnly)) {
89 		LOG(("Theme Error: could not open theme file: %1").arg(path));
90 		return QByteArray();
91 	}
92 
93 	return file.readAll();
94 }
95 
readHexUchar(char code,bool & error)96 inline uchar readHexUchar(char code, bool &error) {
97 	if (code >= '0' && code <= '9') {
98 		return ((code - '0') & 0xFF);
99 	} else if (code >= 'a' && code <= 'f') {
100 		return ((code + 10 - 'a') & 0xFF);
101 	} else if (code >= 'A' && code <= 'F') {
102 		return ((code + 10 - 'A') & 0xFF);
103 	}
104 	error = true;
105 	return 0xFF;
106 }
107 
readHexUchar(char char1,char char2,bool & error)108 inline uchar readHexUchar(char char1, char char2, bool &error) {
109 	return ((readHexUchar(char1, error) & 0x0F) << 4) | (readHexUchar(char2, error) & 0x0F);
110 }
111 
readNameAndValue(const char * & from,const char * end,QLatin1String * outName,QLatin1String * outValue)112 bool readNameAndValue(const char *&from, const char *end, QLatin1String *outName, QLatin1String *outValue) {
113 	using base::parse::skipWhitespaces;
114 	using base::parse::readName;
115 
116 	if (!skipWhitespaces(from, end)) return true;
117 
118 	*outName = readName(from, end);
119 	if (outName->size() == 0) {
120 		LOG(("Theme Error: Could not read name in the color scheme."));
121 		return false;
122 	}
123 	if (!skipWhitespaces(from, end)) {
124 		LOG(("Theme Error: Unexpected end of the color scheme."));
125 		return false;
126 	}
127 	if (*from != ':') {
128 		LOG(("Theme Error: Expected ':' between each name and value in the color scheme (while reading key '%1')").arg(*outName));
129 		return false;
130 	}
131 	if (!skipWhitespaces(++from, end)) {
132 		LOG(("Theme Error: Unexpected end of the color scheme."));
133 		return false;
134 	}
135 	auto valueStart = from;
136 	if (*from == '#') ++from;
137 
138 	if (readName(from, end).size() == 0) {
139 		LOG(("Theme Error: Expected a color value in #rrggbb or #rrggbbaa format in the color scheme (while reading key '%1')").arg(*outName));
140 		return false;
141 	}
142 	*outValue = QLatin1String(valueStart, from - valueStart);
143 
144 	if (!skipWhitespaces(from, end)) {
145 		LOG(("Theme Error: Unexpected end of the color scheme."));
146 		return false;
147 	}
148 	if (*from != ';') {
149 		LOG(("Theme Error: Expected ';' after each value in the color scheme (while reading key '%1')").arg(*outName));
150 		return false;
151 	}
152 	++from;
153 	return true;
154 }
155 
156 enum class SetResult {
157 	Ok,
158 	NotFound,
159 };
setColorSchemeValue(QLatin1String name,QLatin1String value,const style::colorizer & colorizer,Instance * out)160 SetResult setColorSchemeValue(
161 		QLatin1String name,
162 		QLatin1String value,
163 		const style::colorizer &colorizer,
164 		Instance *out) {
165 	auto result = style::palette::SetResult::Ok;
166 	auto size = value.size();
167 	auto data = value.data();
168 	if (data[0] == '#' && (size == 7 || size == 9)) {
169 		auto error = false;
170 		auto r = readHexUchar(data[1], data[2], error);
171 		auto g = readHexUchar(data[3], data[4], error);
172 		auto b = readHexUchar(data[5], data[6], error);
173 		auto a = (size == 9) ? readHexUchar(data[7], data[8], error) : uchar(255);
174 		if (colorizer) {
175 			style::colorize(name, r, g, b, colorizer);
176 		}
177 		if (error) {
178 			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));
179 			return SetResult::Ok;
180 		} else if (out) {
181 			result = out->palette.setColor(name, r, g, b, a);
182 		} else {
183 			result = style::main_palette::setColor(name, r, g, b, a);
184 		}
185 	} else {
186 		if (out) {
187 			result = out->palette.setColor(name, value);
188 		} else {
189 			result = style::main_palette::setColor(name, value);
190 		}
191 	}
192 	if (result == style::palette::SetResult::Ok) {
193 		return SetResult::Ok;
194 	} else if (result == style::palette::SetResult::KeyNotFound) {
195 		return SetResult::NotFound;
196 	} else if (result == style::palette::SetResult::ValueNotFound) {
197 		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));
198 		return SetResult::Ok;
199 	} else if (result == style::palette::SetResult::Duplicate) {
200 		LOG(("Theme Warning: Color value appears more than once in the color scheme (while applying '%1: %2')").arg(name).arg(value));
201 		return SetResult::Ok;
202 	} else {
203 		LOG(("Theme Error: Unexpected internal error."));
204 	}
205 	Unexpected("Value after palette.setColor().");
206 }
207 
loadColorScheme(const QByteArray & content,const style::colorizer & colorizer,Instance * out)208 bool loadColorScheme(
209 		const QByteArray &content,
210 		const style::colorizer &colorizer,
211 		Instance *out) {
212 	auto unsupported = QMap<QLatin1String, QLatin1String>();
213 	return ReadPaletteValues(content, [&](QLatin1String name, QLatin1String value) {
214 		// Find the named value in the already read unsupported list.
215 		value = unsupported.value(value, value);
216 
217 		auto result = setColorSchemeValue(name, value, colorizer, out);
218 		if (result == SetResult::NotFound) {
219 			unsupported.insert(name, value);
220 		}
221 		return true;
222 	});
223 }
224 
applyBackground(QImage && background,bool tiled,Instance * out)225 void applyBackground(QImage &&background, bool tiled, Instance *out) {
226 	if (out) {
227 		out->background = std::move(background);
228 		out->tiled = tiled;
229 	} else {
230 		Background()->setThemeData(std::move(background), tiled);
231 	}
232 }
233 
234 enum class LoadResult {
235 	Loaded,
236 	Failed,
237 	NotFound,
238 };
239 
loadBackgroundFromFile(zlib::FileToRead & file,const char * filename,QByteArray * outBackground)240 LoadResult loadBackgroundFromFile(zlib::FileToRead &file, const char *filename, QByteArray *outBackground) {
241 	*outBackground = file.readFileContent(filename, zlib::kCaseInsensitive, kThemeBackgroundSizeLimit);
242 	if (file.error() == UNZ_OK) {
243 		return LoadResult::Loaded;
244 	} else if (file.error() == UNZ_END_OF_LIST_OF_FILE) {
245 		file.clearError();
246 		return LoadResult::NotFound;
247 	}
248 	LOG(("Theme Error: could not read '%1' in the theme file.").arg(filename));
249 	return LoadResult::Failed;
250 }
251 
loadBackground(zlib::FileToRead & file,QByteArray * outBackground,bool * outTiled)252 bool loadBackground(zlib::FileToRead &file, QByteArray *outBackground, bool *outTiled) {
253 	auto result = loadBackgroundFromFile(file, "background.jpg", outBackground);
254 	if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
255 
256 	result = loadBackgroundFromFile(file, "background.png", outBackground);
257 	if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
258 
259 	*outTiled = true;
260 	result = loadBackgroundFromFile(file, "tiled.jpg", outBackground);
261 	if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
262 
263 	result = loadBackgroundFromFile(file, "tiled.png", outBackground);
264 	if (result != LoadResult::NotFound) return (result == LoadResult::Loaded);
265 	return true;
266 }
267 
LoadTheme(const QByteArray & content,const style::colorizer & colorizer,const std::optional<QByteArray> & editedPalette,Cached * cache=nullptr,Instance * out=nullptr)268 bool LoadTheme(
269 		const QByteArray &content,
270 		const style::colorizer &colorizer,
271 		const std::optional<QByteArray> &editedPalette,
272 		Cached *cache = nullptr,
273 		Instance *out = nullptr) {
274 	if (content.size() < 4) {
275 		LOG(("Theme Error: Bad theme content size: %1").arg(content.size()));
276 		return false;
277 	}
278 
279 	if (cache) {
280 		*cache = Cached();
281 	}
282 	zlib::FileToRead file(content);
283 
284 	const auto emptyColorizer = style::colorizer();
285 	const auto &paletteColorizer = editedPalette ? emptyColorizer : colorizer;
286 
287 	unz_global_info globalInfo = { 0 };
288 	file.getGlobalInfo(&globalInfo);
289 	if (file.error() == UNZ_OK) {
290 		auto schemeContent = editedPalette.value_or(QByteArray());
291 		if (schemeContent.isEmpty()) {
292 			schemeContent = file.readFileContent("colors.tdesktop-theme", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
293 		}
294 		if (schemeContent.isEmpty()) {
295 			file.clearError();
296 			schemeContent = file.readFileContent("colors.tdesktop-palette", zlib::kCaseInsensitive, kThemeSchemeSizeLimit);
297 		}
298 		if (file.error() != UNZ_OK) {
299 			LOG(("Theme Error: could not read 'colors.tdesktop-theme' or 'colors.tdesktop-palette' in the theme file."));
300 			return false;
301 		}
302 		if (!loadColorScheme(schemeContent, paletteColorizer, out)) {
303 			DEBUG_LOG(("Theme: Could not loadColorScheme."));
304 			return false;
305 		}
306 		if (!out) {
307 			Background()->saveAdjustableColors();
308 		}
309 
310 		auto backgroundTiled = false;
311 		auto backgroundContent = QByteArray();
312 		if (!loadBackground(file, &backgroundContent, &backgroundTiled)) {
313 			DEBUG_LOG(("Theme: Could not loadBackground."));
314 			return false;
315 		}
316 
317 		if (!backgroundContent.isEmpty()) {
318 			auto check = QBuffer(&backgroundContent);
319 			auto reader = QImageReader(&check);
320 			const auto size = reader.size();
321 			if (size.isEmpty()
322 				|| (size.width() * size.height() > kBackgroundSizeLimit)) {
323 				LOG(("Theme Error: bad background image size in the theme file."));
324 				return false;
325 			}
326 			auto background = Images::Read({
327 				.content = backgroundContent,
328 				.forceOpaque = true,
329 			}).image;
330 			if (background.isNull()) {
331 				LOG(("Theme Error: could not read background image in the theme file."));
332 				return false;
333 			}
334 			if (colorizer) {
335 				style::colorize(background, colorizer);
336 			}
337 			if (cache) {
338 				auto buffer = QBuffer(&cache->background);
339 				if (!background.save(&buffer, "BMP")) {
340 					LOG(("Theme Error: could not write background image as a BMP to cache."));
341 					return false;
342 				}
343 				cache->tiled = backgroundTiled;
344 			}
345 			applyBackground(std::move(background), backgroundTiled, out);
346 		}
347 	} else {
348 		// Looks like it is not a .zip theme.
349 		if (!loadColorScheme(editedPalette.value_or(content), paletteColorizer, out)) {
350 			DEBUG_LOG(("Theme: Could not loadColorScheme from non-zip."));
351 			return false;
352 		}
353 		if (!out) {
354 			Background()->saveAdjustableColors();
355 		}
356 	}
357 	if (out) {
358 		out->palette.finalize(paletteColorizer);
359 	}
360 	if (cache) {
361 		if (out) {
362 			cache->colors = out->palette.save();
363 		} else {
364 			cache->colors = style::main_palette::save();
365 		}
366 		cache->paletteChecksum = style::palette::Checksum();
367 		cache->contentChecksum = base::crc32(content.constData(), content.size());
368 	}
369 	return true;
370 }
371 
InitializeFromCache(const QByteArray & content,const Cached & cache)372 bool InitializeFromCache(
373 		const QByteArray &content,
374 		const Cached &cache) {
375 	if (cache.paletteChecksum != style::palette::Checksum()) {
376 		return false;
377 	}
378 	if (cache.contentChecksum != base::crc32(content.constData(), content.size())) {
379 		return false;
380 	}
381 
382 	QImage background;
383 	if (!cache.background.isEmpty()) {
384 		QDataStream stream(cache.background);
385 		QImageReader reader(stream.device());
386 		reader.setAutoTransform(true);
387 		if (!reader.read(&background) || background.isNull()) {
388 			return false;
389 		}
390 	}
391 
392 	if (!style::main_palette::load(cache.colors)) {
393 		return false;
394 	}
395 	Background()->saveAdjustableColors();
396 	if (!background.isNull()) {
397 		applyBackground(std::move(background), cache.tiled, nullptr);
398 	}
399 
400 	return true;
401 }
402 
ReadEditingPalette()403 [[nodiscard]] std::optional<QByteArray> ReadEditingPalette() {
404 	auto file = QFile(EditingPalettePath());
405 	return file.open(QIODevice::ReadOnly)
406 		? std::make_optional(file.readAll())
407 		: std::nullopt;
408 }
409 
InitializeFromSaved(Saved && saved)410 bool InitializeFromSaved(Saved &&saved) {
411 	if (saved.object.content.size() < 4) {
412 		LOG(("Theme Error: Could not load theme from '%1' (%2)").arg(
413 			saved.object.pathRelative,
414 			saved.object.pathAbsolute));
415 		return false;
416 	}
417 
418 	const auto editing = ReadEditingPalette();
419 	GlobalBackground.createIfNull();
420 	if (!editing && InitializeFromCache(saved.object.content, saved.cache)) {
421 		return true;
422 	}
423 
424 	const auto colorizer = ColorizerForTheme(saved.object.pathAbsolute);
425 	if (!LoadTheme(saved.object.content, colorizer, editing, &saved.cache)) {
426 		DEBUG_LOG(("Theme: Could not load from saved."));
427 		return false;
428 	}
429 	if (editing) {
430 		Background()->setEditingTheme(ReadCloudFromText(*editing));
431 	} else {
432 		Local::writeTheme(saved);
433 	}
434 	return true;
435 }
436 
PostprocessBackgroundImage(QImage image,const Data::WallPaper & paper)437 [[nodiscard]] QImage PostprocessBackgroundImage(
438 		QImage image,
439 		const Data::WallPaper &paper) {
440 	if (image.format() != QImage::Format_ARGB32_Premultiplied) {
441 		image = std::move(image).convertToFormat(
442 			QImage::Format_ARGB32_Premultiplied);
443 	}
444 	image.setDevicePixelRatio(cRetinaFactor());
445 	if (Data::IsLegacy3DefaultWallPaper(paper)) {
446 		return Images::DitherImage(std::move(image));
447 	}
448 	return image;
449 }
450 
ClearApplying()451 void ClearApplying() {
452 	GlobalApplying = Applying();
453 }
454 
PrepareWallPaper(MTP::DcId dcId,const QImage & image)455 SendMediaReady PrepareWallPaper(MTP::DcId dcId, const QImage &image) {
456 	PreparedPhotoThumbs thumbnails;
457 	QVector<MTPPhotoSize> sizes;
458 
459 	QByteArray jpeg;
460 	QBuffer jpegBuffer(&jpeg);
461 	image.save(&jpegBuffer, "JPG", 87);
462 
463 	const auto scaled = [&](int size) {
464 		return image.scaled(
465 			size,
466 			size,
467 			Qt::KeepAspectRatio,
468 			Qt::SmoothTransformation);
469 	};
470 	const auto push = [&](const char *type, QImage &&image) {
471 		sizes.push_back(MTP_photoSize(
472 			MTP_string(type),
473 			MTP_int(image.width()),
474 			MTP_int(image.height()), MTP_int(0)));
475 		thumbnails.emplace(
476 			type[0],
477 			PreparedPhotoThumb{ .image = std::move(image) });
478 	};
479 	push("s", scaled(320));
480 
481 	const auto filename = qsl("wallpaper.jpg");
482 	auto attributes = QVector<MTPDocumentAttribute>(
483 		1,
484 		MTP_documentAttributeFilename(MTP_string(filename)));
485 	attributes.push_back(MTP_documentAttributeImageSize(
486 		MTP_int(image.width()),
487 		MTP_int(image.height())));
488 	const auto id = base::RandomValue<DocumentId>();
489 	const auto document = MTP_document(
490 		MTP_flags(0),
491 		MTP_long(id),
492 		MTP_long(0),
493 		MTP_bytes(),
494 		MTP_int(base::unixtime::now()),
495 		MTP_string("image/jpeg"),
496 		MTP_int(jpeg.size()),
497 		MTP_vector<MTPPhotoSize>(sizes),
498 		MTPVector<MTPVideoSize>(),
499 		MTP_int(dcId),
500 		MTP_vector<MTPDocumentAttribute>(attributes));
501 
502 	return SendMediaReady(
503 		SendMediaType::ThemeFile,
504 		QString(), // filepath
505 		filename,
506 		jpeg.size(),
507 		jpeg,
508 		id,
509 		0,
510 		QString(),
511 		PeerId(),
512 		MTP_photoEmpty(MTP_long(0)),
513 		thumbnails,
514 		document,
515 		QByteArray(),
516 		0);
517 }
518 
ClearEditingPalette()519 void ClearEditingPalette() {
520 	QFile(EditingPalettePath()).remove();
521 }
522 
523 } // namespace
524 
AdjustableColor(style::color data)525 ChatBackground::AdjustableColor::AdjustableColor(style::color data)
526 : item(data)
527 , original(data->c) {
528 }
529 
530 // They're duplicated in window_theme_editor_box.cpp:ReplaceAdjustableColors.
ChatBackground()531 ChatBackground::ChatBackground() : _adjustableColors({
532 		st::msgServiceBg,
533 		st::msgServiceBgSelected,
534 		st::historyScrollBg,
535 		st::historyScrollBgOver,
536 		st::historyScrollBarBg,
537 		st::historyScrollBarBgOver }) {
538 }
539 
setThemeData(QImage && themeImage,bool themeTile)540 void ChatBackground::setThemeData(QImage &&themeImage, bool themeTile) {
541 	_themeImage = PostprocessBackgroundImage(
542 		std::move(themeImage),
543 		Data::ThemeWallPaper());
544 	_themeTile = themeTile;
545 }
546 
initialRead()547 void ChatBackground::initialRead() {
548 	if (started()) {
549 		return;
550 	} else if (!Local::readBackground()) {
551 		set(Data::ThemeWallPaper());
552 	}
553 	if (_localStoredTileDayValue) {
554 		_tileDayValue = *_localStoredTileDayValue;
555 	}
556 	if (_localStoredTileNightValue) {
557 		_tileNightValue = *_localStoredTileNightValue;
558 	}
559 }
560 
start()561 void ChatBackground::start() {
562 	saveAdjustableColors();
563 
564 	_updates.events(
565 	) | rpl::start_with_next([=](const BackgroundUpdate &update) {
566 		if (update.paletteChanged()) {
567 			style::NotifyPaletteChanged();
568 		}
569 	}, _lifetime);
570 
571 	initialRead();
572 
573 	Core::App().domain().activeSessionValue(
574 	) | rpl::filter([=](Main::Session *session) {
575 		return session != _session;
576 	}) | rpl::start_with_next([=](Main::Session *session) {
577 		_session = session;
578 		checkUploadWallPaper();
579 	}, _lifetime);
580 
581 	Core::App().settings().setSystemDarkMode(Platform::IsDarkMode());
582 }
583 
checkUploadWallPaper()584 void ChatBackground::checkUploadWallPaper() {
585 	if (!_session) {
586 		_wallPaperUploadLifetime = rpl::lifetime();
587 		_wallPaperUploadId = FullMsgId();
588 		_wallPaperRequestId = 0;
589 		return;
590 	}
591 	if (const auto id = base::take(_wallPaperUploadId)) {
592 		_session->uploader().cancel(id);
593 	}
594 	if (const auto id = base::take(_wallPaperRequestId)) {
595 		_session->api().request(id).cancel();
596 	}
597 	if (!Data::IsCustomWallPaper(_paper)
598 		|| _original.isNull()
599 		|| _editingTheme.has_value()) {
600 		return;
601 	}
602 
603 	const auto ready = PrepareWallPaper(_session->mainDcId(), _original);
604 	const auto documentId = ready.id;
605 	_wallPaperUploadId = FullMsgId(0, _session->data().nextLocalMessageId());
606 	_session->uploader().uploadMedia(_wallPaperUploadId, ready);
607 	if (_wallPaperUploadLifetime) {
608 		return;
609 	}
610 	_wallPaperUploadLifetime = _session->uploader().documentReady(
611 	) | rpl::start_with_next([=](const Storage::UploadedDocument &data) {
612 		if (data.fullId != _wallPaperUploadId) {
613 			return;
614 		}
615 		_wallPaperUploadId = FullMsgId();
616 		_wallPaperRequestId = _session->api().request(
617 			MTPaccount_UploadWallPaper(
618 				data.file,
619 				MTP_string("image/jpeg"),
620 				_paper.mtpSettings()
621 			)
622 		).done([=](const MTPWallPaper &result) {
623 			result.match([&](const MTPDwallPaper &data) {
624 				_session->data().documentConvert(
625 					_session->data().document(documentId),
626 					data.vdocument());
627 			}, [&](const MTPDwallPaperNoFile &data) {
628 				LOG(("API Error: "
629 					"Got wallPaperNoFile after account.UploadWallPaper."));
630 			});
631 			if (const auto paper = Data::WallPaper::Create(_session, result)) {
632 				setPaper(*paper);
633 				writeNewBackgroundSettings();
634 				_updates.fire({ BackgroundUpdate::Type::New, tile() });
635 			}
636 		}).send();
637 	});
638 }
639 
postprocessBackgroundImage(QImage image)640 QImage ChatBackground::postprocessBackgroundImage(QImage image) {
641 	return PostprocessBackgroundImage(std::move(image), _paper);
642 }
643 
set(const Data::WallPaper & paper,QImage image)644 void ChatBackground::set(const Data::WallPaper &paper, QImage image) {
645 	image = Ui::PreprocessBackgroundImage(std::move(image));
646 
647 	const auto needResetAdjustable = Data::IsDefaultWallPaper(paper)
648 		&& !Data::IsDefaultWallPaper(_paper)
649 		&& !nightMode()
650 		&& _themeObject.pathAbsolute.isEmpty();
651 	if (Data::IsThemeWallPaper(paper) && _themeImage.isNull()) {
652 		setPaper(Data::DefaultWallPaper());
653 	} else {
654 		setPaper(paper);
655 		if (needResetAdjustable) {
656 			// If we had a default color theme with non-default background,
657 			// and we switch to default background we must somehow switch from
658 			// adjusted service colors to default (non-adjusted) service colors.
659 			// The only way to do that right now is through full palette reset.
660 			restoreAdjustableColors();
661 		}
662 	}
663 	if (Data::IsThemeWallPaper(_paper)) {
664 		(nightMode() ? _tileNightValue : _tileDayValue) = _themeTile;
665 		setPrepared(_themeImage, _themeImage, QImage());
666 	} else if (Data::details::IsTestingThemeWallPaper(_paper)
667 		|| Data::details::IsTestingDefaultWallPaper(_paper)
668 		|| Data::details::IsTestingEditorWallPaper(_paper)) {
669 		if (Data::details::IsTestingDefaultWallPaper(_paper)
670 			|| image.isNull()) {
671 			image = ReadDefaultImage();
672 			setPaper(Data::details::TestingDefaultWallPaper());
673 		}
674 		setPreparedAfterPaper(std::move(image));
675 	} else {
676 		if (Data::IsLegacy1DefaultWallPaper(_paper)) {
677 			image.load(qsl(":/gui/art/bg_initial.jpg"));
678 			const auto scale = cScale() * cIntRetinaFactor();
679 			if (scale != 100) {
680 				image = image.scaledToWidth(
681 					style::ConvertScale(image.width(), scale),
682 					Qt::SmoothTransformation);
683 			}
684 		} else if (Data::IsDefaultWallPaper(_paper)
685 			|| (_paper.backgroundColors().empty() && image.isNull())) {
686 			setPaper(Data::DefaultWallPaper().withParamsFrom(_paper));
687 			image = ReadDefaultImage();
688 		}
689 		Local::writeBackground(
690 			_paper,
691 			((Data::IsDefaultWallPaper(_paper)
692 				|| Data::IsLegacy1DefaultWallPaper(_paper))
693 				? QImage()
694 				: image));
695 		setPreparedAfterPaper(std::move(image));
696 	}
697 	Assert(colorForFill()
698 		|| !_gradient.isNull()
699 		|| (!_original.isNull()
700 			&& !_prepared.isNull()
701 			&& !_preparedForTiled.isNull()));
702 
703 	_updates.fire({ BackgroundUpdate::Type::New, tile() }); // delayed?
704 	if (needResetAdjustable) {
705 		_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });
706 		_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });
707 	}
708 	checkUploadWallPaper();
709 }
710 
setPreparedAfterPaper(QImage image)711 void ChatBackground::setPreparedAfterPaper(QImage image) {
712 	const auto &bgColors = _paper.backgroundColors();
713 	if (_paper.isPattern() && !image.isNull()) {
714 		if (bgColors.size() < 2) {
715 			auto prepared = postprocessBackgroundImage(
716 				Ui::PreparePatternImage(
717 					image,
718 					bgColors,
719 					_paper.gradientRotation(),
720 					_paper.patternOpacity()));
721 			setPrepared(
722 				std::move(image),
723 				std::move(prepared),
724 				QImage());
725 		} else {
726 			image = postprocessBackgroundImage(std::move(image));
727 			if (Ui::IsPatternInverted(bgColors, _paper.patternOpacity())) {
728 				image = Ui::InvertPatternImage(std::move(image));
729 			}
730 			setPrepared(
731 				image,
732 				image,
733 				Data::GenerateDitheredGradient(_paper));
734 		}
735 	} else if (bgColors.size() == 1) {
736 		setPrepared(QImage(), QImage(), QImage());
737 	} else if (!bgColors.empty()) {
738 		setPrepared(
739 			QImage(),
740 			QImage(),
741 			Data::GenerateDitheredGradient(_paper));
742 	} else {
743 		image = postprocessBackgroundImage(std::move(image));
744 		setPrepared(image, image, QImage());
745 	}
746 }
747 
setPrepared(QImage original,QImage prepared,QImage gradient)748 void ChatBackground::setPrepared(
749 		QImage original,
750 		QImage prepared,
751 		QImage gradient) {
752 	Expects(original.isNull() || GoodImageFormatAndSize(original));
753 	Expects(prepared.isNull() || GoodImageFormatAndSize(prepared));
754 	Expects(gradient.isNull() || GoodImageFormatAndSize(gradient));
755 
756 	if (!prepared.isNull() && !_paper.isPattern() && _paper.isBlurred()) {
757 		prepared = Ui::PrepareBlurredBackground(std::move(prepared));
758 	}
759 	if (adjustPaletteRequired()) {
760 		if ((prepared.isNull() || _paper.isPattern())
761 			&& !_paper.backgroundColors().empty()) {
762 			adjustPaletteUsingColors(_paper.backgroundColors());
763 		} else if (!prepared.isNull()) {
764 			adjustPaletteUsingBackground(prepared);
765 		}
766 	}
767 
768 	_original = std::move(original);
769 	_prepared = std::move(prepared);
770 	_gradient = std::move(gradient);
771 	_imageMonoColor = _gradient.isNull()
772 		? Ui::CalculateImageMonoColor(_prepared)
773 		: std::nullopt;
774 	_preparedForTiled = Ui::PrepareImageForTiled(_prepared);
775 }
776 
setPaper(const Data::WallPaper & paper)777 void ChatBackground::setPaper(const Data::WallPaper &paper) {
778 	_paper = paper.withoutImageData();
779 }
780 
adjustPaletteRequired()781 bool ChatBackground::adjustPaletteRequired() {
782 	const auto usingThemeBackground = [&] {
783 		return Data::IsThemeWallPaper(_paper)
784 			|| Data::details::IsTestingThemeWallPaper(_paper);
785 	};
786 	const auto usingDefaultBackground = [&] {
787 		return Data::IsDefaultWallPaper(_paper)
788 			|| Data::details::IsTestingDefaultWallPaper(_paper);
789 	};
790 
791 	if (_editingTheme.has_value()) {
792 		return false;
793 	} else if (isNonDefaultThemeOrBackground() || nightMode()) {
794 		return !usingThemeBackground();
795 	}
796 	return !usingDefaultBackground();
797 }
798 
editingTheme() const799 std::optional<Data::CloudTheme> ChatBackground::editingTheme() const {
800 	return _editingTheme;
801 }
802 
setEditingTheme(const Data::CloudTheme & editing)803 void ChatBackground::setEditingTheme(const Data::CloudTheme &editing) {
804 	_editingTheme = editing;
805 }
806 
clearEditingTheme(ClearEditing clear)807 void ChatBackground::clearEditingTheme(ClearEditing clear) {
808 	if (!_editingTheme) {
809 		return;
810 	}
811 	_editingTheme = std::nullopt;
812 	if (clear == ClearEditing::Temporary) {
813 		return;
814 	}
815 	ClearEditingPalette();
816 	if (clear == ClearEditing::RevertChanges) {
817 		reapplyWithNightMode(std::nullopt, _nightMode);
818 		KeepApplied();
819 	}
820 }
821 
adjustPaletteUsingBackground(const QImage & image)822 void ChatBackground::adjustPaletteUsingBackground(const QImage &image) {
823 	adjustPaletteUsingColor(Ui::CountAverageColor(image));
824 }
825 
adjustPaletteUsingColors(const std::vector<QColor> & colors)826 void ChatBackground::adjustPaletteUsingColors(
827 		const std::vector<QColor> &colors) {
828 	adjustPaletteUsingColor(Ui::CountAverageColor(colors));
829 }
830 
adjustPaletteUsingColor(QColor color)831 void ChatBackground::adjustPaletteUsingColor(QColor color) {
832 	const auto prepared = color.toHsl();
833 	for (const auto &adjustable : _adjustableColors) {
834 		const auto adjusted = Ui::ThemeAdjustedColor(adjustable.item->c, prepared);
835 		adjustable.item.set(
836 			adjusted.red(),
837 			adjusted.green(),
838 			adjusted.blue(),
839 			adjusted.alpha());
840 	}
841 }
842 
colorForFill() const843 std::optional<QColor> ChatBackground::colorForFill() const {
844 	return !_prepared.isNull()
845 		? imageMonoColor()
846 		: (!_gradient.isNull() || _paper.backgroundColors().empty())
847 		? std::nullopt
848 		: std::make_optional(_paper.backgroundColors().front());
849 }
850 
gradientForFill() const851 QImage ChatBackground::gradientForFill() const {
852 	return _gradient;
853 }
854 
recacheGradientForFill(QImage gradient)855 void ChatBackground::recacheGradientForFill(QImage gradient) {
856 	if (_gradient.size() == gradient.size()) {
857 		_gradient = std::move(gradient);
858 	}
859 }
860 
createCurrentImage() const861 QImage ChatBackground::createCurrentImage() const {
862 	if (const auto fill = colorForFill()) {
863 		auto result = QImage(512, 512, QImage::Format_ARGB32_Premultiplied);
864 		result.fill(*fill);
865 		return result;
866 	} else if (_gradient.isNull()) {
867 		return _prepared;
868 	} else if (_prepared.isNull()) {
869 		return _gradient;
870 	}
871 	auto result = _gradient.scaled(
872 		_prepared.size(),
873 		Qt::IgnoreAspectRatio,
874 		Qt::SmoothTransformation);
875 	result.setDevicePixelRatio(1.);
876 	{
877 		auto p = QPainter(&result);
878 		const auto patternOpacity = paper().patternOpacity();
879 		if (patternOpacity >= 0.) {
880 			p.setCompositionMode(QPainter::CompositionMode_SoftLight);
881 			p.setOpacity(patternOpacity);
882 		} else {
883 			p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
884 		}
885 		p.drawImage(QRect(QPoint(), _prepared.size()), _prepared);
886 		if (patternOpacity < 0. && patternOpacity > -1.) {
887 			p.setCompositionMode(QPainter::CompositionMode_SourceOver);
888 			p.setOpacity(1. + patternOpacity);
889 			p.fillRect(QRect(QPoint(), _prepared.size()), Qt::black);
890 		}
891 	}
892 	return result;
893 }
894 
tile() const895 bool ChatBackground::tile() const {
896 	return nightMode() ? _tileNightValue : _tileDayValue;
897 }
898 
tileDay() const899 bool ChatBackground::tileDay() const {
900 	if (Data::details::IsTestingThemeWallPaper(_paper) ||
901 		Data::details::IsTestingDefaultWallPaper(_paper)) {
902 		if (!nightMode()) {
903 			return _tileForRevert;
904 		}
905 	}
906 	return _tileDayValue;
907 }
908 
tileNight() const909 bool ChatBackground::tileNight() const {
910 	if (Data::details::IsTestingThemeWallPaper(_paper) ||
911 		Data::details::IsTestingDefaultWallPaper(_paper)) {
912 		if (nightMode()) {
913 			return _tileForRevert;
914 		}
915 	}
916 	return _tileNightValue;
917 }
918 
imageMonoColor() const919 std::optional<QColor> ChatBackground::imageMonoColor() const {
920 	return _imageMonoColor;
921 }
922 
setTile(bool tile)923 void ChatBackground::setTile(bool tile) {
924 	Expects(started());
925 
926 	const auto old = this->tile();
927 	if (nightMode()) {
928 		setTileNightValue(tile);
929 	} else {
930 		setTileDayValue(tile);
931 	}
932 	if (this->tile() != old) {
933 		if (!Data::details::IsTestingThemeWallPaper(_paper)
934 			&& !Data::details::IsTestingDefaultWallPaper(_paper)) {
935 			Local::writeSettings();
936 		}
937 		_updates.fire({ BackgroundUpdate::Type::Changed, tile }); // delayed?
938 	}
939 }
940 
setTileDayValue(bool tile)941 void ChatBackground::setTileDayValue(bool tile) {
942 	if (started()) {
943 		_tileDayValue = tile;
944 	} else {
945 		_localStoredTileDayValue = tile;
946 	}
947 }
948 
setTileNightValue(bool tile)949 void ChatBackground::setTileNightValue(bool tile) {
950 	if (started()) {
951 		_tileNightValue = tile;
952 	} else {
953 		_localStoredTileNightValue = tile;
954 	}
955 }
956 
setThemeObject(const Object & object)957 void ChatBackground::setThemeObject(const Object &object) {
958 	_themeObject = object;
959 	_themeObject.content = QByteArray();
960 }
961 
themeObject() const962 const Object &ChatBackground::themeObject() const {
963 	return _themeObject;
964 }
965 
reset()966 void ChatBackground::reset() {
967 	if (Data::details::IsTestingThemeWallPaper(_paper)
968 		|| Data::details::IsTestingDefaultWallPaper(_paper)) {
969 		if (_themeImage.isNull()) {
970 			_paperForRevert = Data::DefaultWallPaper();
971 			_originalForRevert = QImage();
972 			_tileForRevert = false;
973 		} else {
974 			_paperForRevert = Data::ThemeWallPaper();
975 			_originalForRevert = _themeImage;
976 			_tileForRevert = _themeTile;
977 		}
978 	} else {
979 		set(Data::ThemeWallPaper());
980 		restoreAdjustableColors();
981 		_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });
982 		_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });
983 	}
984 	writeNewBackgroundSettings();
985 }
986 
started() const987 bool ChatBackground::started() const {
988 	return !Data::details::IsUninitializedWallPaper(_paper);
989 }
990 
saveForRevert()991 void ChatBackground::saveForRevert() {
992 	Expects(started());
993 
994 	if (!Data::details::IsTestingThemeWallPaper(_paper)
995 		&& !Data::details::IsTestingDefaultWallPaper(_paper)) {
996 		_paperForRevert = _paper;
997 		_originalForRevert = std::move(_original);
998 		_tileForRevert = tile();
999 	}
1000 }
1001 
saveAdjustableColors()1002 void ChatBackground::saveAdjustableColors() {
1003 	for (auto &color : _adjustableColors) {
1004 		color.original = color.item->c;
1005 	}
1006 }
1007 
restoreAdjustableColors()1008 void ChatBackground::restoreAdjustableColors() {
1009 	for (const auto &color : _adjustableColors) {
1010 		const auto value = color.original;
1011 		color.item.set(value.red(), value.green(), value.blue(), value.alpha());
1012 	}
1013 }
1014 
setTestingTheme(Instance && theme)1015 void ChatBackground::setTestingTheme(Instance &&theme) {
1016 	style::main_palette::apply(theme.palette);
1017 	saveAdjustableColors();
1018 
1019 	auto switchToThemeBackground = !theme.background.isNull()
1020 		|| Data::IsThemeWallPaper(_paper)
1021 		|| (Data::IsDefaultWallPaper(_paper)
1022 			&& !nightMode()
1023 			&& _themeObject.pathAbsolute.isEmpty());
1024 	if (AreTestingTheme() && _editingTheme.has_value()) {
1025 		// Grab current background image if it is not already custom
1026 		// Use prepared pixmap, not original image, because we're
1027 		// for sure switching to a non-pattern wall-paper (testing editor).
1028 		if (!Data::IsCustomWallPaper(_paper)) {
1029 			saveForRevert();
1030 			set(
1031 				Data::details::TestingEditorWallPaper(),
1032 				base::take(_prepared));
1033 		}
1034 	} else if (switchToThemeBackground) {
1035 		saveForRevert();
1036 		set(
1037 			Data::details::TestingThemeWallPaper(),
1038 			std::move(theme.background));
1039 		setTile(theme.tiled);
1040 	} else {
1041 		// Apply current background image so that service bg colors are recounted.
1042 		set(_paper, std::move(_original));
1043 	}
1044 	_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });
1045 }
1046 
setTestingDefaultTheme()1047 void ChatBackground::setTestingDefaultTheme() {
1048 	style::main_palette::reset(ColorizerForTheme(QString()));
1049 	saveAdjustableColors();
1050 
1051 	saveForRevert();
1052 	set(Data::details::TestingDefaultWallPaper());
1053 	setTile(false);
1054 	_updates.fire({ BackgroundUpdate::Type::TestingTheme, tile() });
1055 }
1056 
keepApplied(const Object & object,bool write)1057 void ChatBackground::keepApplied(const Object &object, bool write) {
1058 	setThemeObject(object);
1059 	if (Data::details::IsTestingEditorWallPaper(_paper)) {
1060 		setPaper(Data::CustomWallPaper());
1061 		_themeImage = QImage();
1062 		_themeTile = false;
1063 		if (write) {
1064 			writeNewBackgroundSettings();
1065 		}
1066 	} else if (Data::details::IsTestingThemeWallPaper(_paper)) {
1067 		setPaper(Data::ThemeWallPaper());
1068 		_themeImage = postprocessBackgroundImage(base::duplicate(_original));
1069 		_themeTile = tile();
1070 		if (write) {
1071 			writeNewBackgroundSettings();
1072 		}
1073 	} else if (Data::details::IsTestingDefaultWallPaper(_paper)) {
1074 		setPaper(Data::DefaultWallPaper());
1075 		_themeImage = QImage();
1076 		_themeTile = false;
1077 		if (write) {
1078 			writeNewBackgroundSettings();
1079 		}
1080 	}
1081 	_updates.fire({ BackgroundUpdate::Type::ApplyingTheme, tile() });
1082 }
1083 
isNonDefaultThemeOrBackground()1084 bool ChatBackground::isNonDefaultThemeOrBackground() {
1085 	initialRead();
1086 	return nightMode()
1087 		? (_themeObject.pathAbsolute != NightThemePath()
1088 			|| !Data::IsThemeWallPaper(_paper))
1089 		: (!_themeObject.pathAbsolute.isEmpty()
1090 			|| !Data::IsDefaultWallPaper(_paper));
1091 }
1092 
isNonDefaultBackground()1093 bool ChatBackground::isNonDefaultBackground() {
1094 	initialRead();
1095 	return _themeObject.pathAbsolute.isEmpty()
1096 		? !Data::IsDefaultWallPaper(_paper)
1097 		: !Data::IsThemeWallPaper(_paper);
1098 }
1099 
writeNewBackgroundSettings()1100 void ChatBackground::writeNewBackgroundSettings() {
1101 	if (tile() != _tileForRevert) {
1102 		Local::writeSettings();
1103 	}
1104 	Local::writeBackground(
1105 		_paper,
1106 		((Data::IsThemeWallPaper(_paper)
1107 			|| Data::IsDefaultWallPaper(_paper))
1108 			? QImage()
1109 			: _original));
1110 }
1111 
revert()1112 void ChatBackground::revert() {
1113 	if (Data::details::IsTestingThemeWallPaper(_paper)
1114 		|| Data::details::IsTestingDefaultWallPaper(_paper)
1115 		|| Data::details::IsTestingEditorWallPaper(_paper)) {
1116 		setTile(_tileForRevert);
1117 		set(_paperForRevert, std::move(_originalForRevert));
1118 	} else {
1119 		// Apply current background image so that service bg colors are recounted.
1120 		set(_paper, std::move(_original));
1121 	}
1122 	_updates.fire({ BackgroundUpdate::Type::RevertingTheme, tile() });
1123 }
1124 
appliedEditedPalette()1125 void ChatBackground::appliedEditedPalette() {
1126 	_updates.fire({ BackgroundUpdate::Type::ApplyingEdit, tile() });
1127 }
1128 
downloadingStarted(bool tile)1129 void ChatBackground::downloadingStarted(bool tile) {
1130 	_updates.fire({ BackgroundUpdate::Type::Start, tile });
1131 }
1132 
setNightModeValue(bool nightMode)1133 void ChatBackground::setNightModeValue(bool nightMode) {
1134 	_nightMode = nightMode;
1135 }
1136 
nightMode() const1137 bool ChatBackground::nightMode() const {
1138 	return _nightMode;
1139 }
1140 
reapplyWithNightMode(std::optional<QString> themePath,bool newNightMode)1141 void ChatBackground::reapplyWithNightMode(
1142 		std::optional<QString> themePath,
1143 		bool newNightMode) {
1144 	if (!started()) {
1145 		// We can get here from legacy passcoded state.
1146 		// In this case Background() is not started yet, because
1147 		// some settings and the background itself were not read.
1148 		return;
1149 	} else if (_nightMode != newNightMode && !nightModeChangeAllowed()) {
1150 		return;
1151 	}
1152 	const auto settingExactTheme = themePath.has_value();
1153 	const auto nightModeChanged = (newNightMode != _nightMode);
1154 	const auto oldNightMode = _nightMode;
1155 	_nightMode = newNightMode;
1156 	auto read = settingExactTheme ? Saved() : Local::readThemeAfterSwitch();
1157 	auto path = read.object.pathAbsolute;
1158 
1159 	_nightMode = oldNightMode;
1160 	auto oldTileValue = (_nightMode ? _tileNightValue : _tileDayValue);
1161 	const auto alreadyOnDisk = [&] {
1162 		if (read.object.content.isEmpty()) {
1163 			return false;
1164 		}
1165 		auto preview = std::make_unique<Preview>();
1166 		preview->object = std::move(read.object);
1167 		preview->instance.cached = std::move(read.cache);
1168 		const auto loaded = LoadTheme(
1169 			preview->object.content,
1170 			ColorizerForTheme(path),
1171 			std::nullopt,
1172 			&preview->instance.cached,
1173 			&preview->instance);
1174 		if (!loaded) {
1175 			return false;
1176 		}
1177 		Apply(std::move(preview));
1178 		return true;
1179 	}();
1180 	if (!alreadyOnDisk) {
1181 		path = themePath
1182 			? *themePath
1183 			: (newNightMode ? NightThemePath() : QString());
1184 		ApplyDefaultWithPath(path);
1185 	}
1186 
1187 	// Theme editor could have already reverted the testing of this toggle.
1188 	if (AreTestingTheme()) {
1189 		GlobalApplying.overrideKeep = [=] {
1190 			if (nightModeChanged) {
1191 				_nightMode = newNightMode;
1192 
1193 				// Restore the value, it was set inside theme testing.
1194 				(oldNightMode ? _tileNightValue : _tileDayValue) = oldTileValue;
1195 			}
1196 
1197 			const auto saved = std::move(GlobalApplying.data);
1198 			if (!alreadyOnDisk) {
1199 				// First-time switch to default night mode should write it.
1200 				Local::writeTheme(saved);
1201 			}
1202 			ClearApplying();
1203 			keepApplied(saved.object, settingExactTheme);
1204 			if (tile() != _tileForRevert || nightModeChanged) {
1205 				Local::writeSettings();
1206 			}
1207 			if (!settingExactTheme && !Local::readBackground()) {
1208 				set(Data::ThemeWallPaper());
1209 			}
1210 		};
1211 	}
1212 }
1213 
nightModeChangeAllowed() const1214 bool ChatBackground::nightModeChangeAllowed() const {
1215 	const auto &settings = Core::App().settings();
1216 	const auto allowedToBeAfterChange = settings.systemDarkModeEnabled()
1217 		? settings.systemDarkMode().value_or(!_nightMode)
1218 		: !_nightMode;
1219 	return (_nightMode != allowedToBeAfterChange);
1220 }
1221 
toggleNightMode(std::optional<QString> themePath)1222 void ChatBackground::toggleNightMode(std::optional<QString> themePath) {
1223 	reapplyWithNightMode(themePath, !_nightMode);
1224 }
1225 
Background()1226 ChatBackground *Background() {
1227 	GlobalBackground.createIfNull();
1228 	return GlobalBackground.data();
1229 }
1230 
IsEmbeddedTheme(const QString & path)1231 bool IsEmbeddedTheme(const QString &path) {
1232 	return path.isEmpty() || path.startsWith(qstr(":/gui/"));
1233 }
1234 
Initialize(Saved && saved)1235 bool Initialize(Saved &&saved) {
1236 	if (InitializeFromSaved(std::move(saved))) {
1237 		Background()->setThemeObject(saved.object);
1238 		return true;
1239 	}
1240 	DEBUG_LOG(("Theme: Could not initialize from saved."));
1241 	return false;
1242 }
1243 
Uninitialize()1244 void Uninitialize() {
1245 	GlobalBackground.clear();
1246 	GlobalApplying = Applying();
1247 }
1248 
Apply(const QString & filepath,const Data::CloudTheme & cloud)1249 bool Apply(
1250 		const QString &filepath,
1251 		const Data::CloudTheme &cloud) {
1252 	if (auto preview = PreviewFromFile(QByteArray(), filepath, cloud)) {
1253 		return Apply(std::move(preview));
1254 	}
1255 	return false;
1256 }
1257 
Apply(std::unique_ptr<Preview> preview)1258 bool Apply(std::unique_ptr<Preview> preview) {
1259 	GlobalApplying.data.object = std::move(preview->object);
1260 	GlobalApplying.data.cache = std::move(preview->instance.cached);
1261 	if (GlobalApplying.paletteForRevert.isEmpty()) {
1262 		GlobalApplying.paletteForRevert = style::main_palette::save();
1263 	}
1264 	Background()->setTestingTheme(std::move(preview->instance));
1265 	return true;
1266 }
1267 
ApplyDefaultWithPath(const QString & themePath)1268 void ApplyDefaultWithPath(const QString &themePath) {
1269 	if (!themePath.isEmpty()) {
1270 		if (auto preview = PreviewFromFile(QByteArray(), themePath, {})) {
1271 			Apply(std::move(preview));
1272 		}
1273 	} else {
1274 		GlobalApplying.data = Saved();
1275 		if (GlobalApplying.paletteForRevert.isEmpty()) {
1276 			GlobalApplying.paletteForRevert = style::main_palette::save();
1277 		}
1278 		Background()->setTestingDefaultTheme();
1279 	}
1280 }
1281 
ApplyEditedPalette(const QByteArray & content)1282 bool ApplyEditedPalette(const QByteArray &content) {
1283 	auto out = Instance();
1284 	if (!loadColorScheme(content, style::colorizer(), &out)) {
1285 		return false;
1286 	}
1287 	style::main_palette::apply(out.palette);
1288 	Background()->appliedEditedPalette();
1289 	return true;
1290 }
1291 
KeepApplied()1292 void KeepApplied() {
1293 	if (!AreTestingTheme()) {
1294 		return;
1295 	} else if (GlobalApplying.overrideKeep) {
1296 		// This callback will be destroyed while running.
1297 		// And it won't be able to safely access captures after that.
1298 		// So we save it on stack for the time while it is running.
1299 		const auto onstack = base::take(GlobalApplying.overrideKeep);
1300 		onstack();
1301 		return;
1302 	}
1303 	const auto saved = std::move(GlobalApplying.data);
1304 	Local::writeTheme(saved);
1305 	ClearApplying();
1306 	Background()->keepApplied(saved.object, true);
1307 }
1308 
KeepFromEditor(const QByteArray & originalContent,const ParsedTheme & originalParsed,const Data::CloudTheme & cloud,const QByteArray & themeContent,const ParsedTheme & themeParsed,const QImage & background)1309 void KeepFromEditor(
1310 		const QByteArray &originalContent,
1311 		const ParsedTheme &originalParsed,
1312 		const Data::CloudTheme &cloud,
1313 		const QByteArray &themeContent,
1314 		const ParsedTheme &themeParsed,
1315 		const QImage &background) {
1316 	ClearApplying();
1317 	const auto content = themeContent.isEmpty()
1318 		? originalContent
1319 		: themeContent;
1320 	auto saved = Saved();
1321 	auto &cache = saved.cache;
1322 	auto &object = saved.object;
1323 	cache.colors = style::main_palette::save();
1324 	cache.paletteChecksum = style::palette::Checksum();
1325 	cache.contentChecksum = base::crc32(content.constData(), content.size());
1326 	cache.background = themeParsed.background;
1327 	cache.tiled = themeParsed.tiled;
1328 	object.cloud = cloud;
1329 	object.content = themeContent.isEmpty()
1330 		? originalContent
1331 		: themeContent;
1332 	object.pathAbsolute = object.pathRelative = CachedThemePath(
1333 		cloud.documentId);
1334 	Local::writeTheme(saved);
1335 	Background()->keepApplied(saved.object, true);
1336 	Background()->setThemeData(
1337 		base::duplicate(background),
1338 		themeParsed.tiled);
1339 	Background()->set(Data::ThemeWallPaper());
1340 	Background()->writeNewBackgroundSettings();
1341 }
1342 
Revert()1343 void Revert() {
1344 	if (!AreTestingTheme()) {
1345 		return;
1346 	}
1347 	style::main_palette::load(GlobalApplying.paletteForRevert);
1348 	Background()->saveAdjustableColors();
1349 
1350 	ClearApplying();
1351 	Background()->revert();
1352 }
1353 
NightThemePath()1354 QString NightThemePath() {
1355 	return kNightThemeFile.utf16();
1356 }
1357 
IsNonDefaultBackground()1358 bool IsNonDefaultBackground() {
1359 	return Background()->isNonDefaultBackground();
1360 }
1361 
IsNightMode()1362 bool IsNightMode() {
1363 	return GlobalBackground ? Background()->nightMode() : false;
1364 }
1365 
IsNightModeValue()1366 rpl::producer<bool> IsNightModeValue() {
1367 	auto changes = Background()->updates(
1368 	) | rpl::filter([=](const BackgroundUpdate &update) {
1369 		return update.type == BackgroundUpdate::Type::ApplyingTheme;
1370 	}) | rpl::to_empty;
1371 
1372 	return rpl::single(
1373 		rpl::empty_value()
1374 	) | rpl::then(
1375 		std::move(changes)
1376 	) | rpl::map([=] {
1377 		return IsNightMode();
1378 	}) | rpl::distinct_until_changed();
1379 }
1380 
SetNightModeValue(bool nightMode)1381 void SetNightModeValue(bool nightMode) {
1382 	if (GlobalBackground || nightMode) {
1383 		Background()->setNightModeValue(nightMode);
1384 	}
1385 }
1386 
ToggleNightMode()1387 void ToggleNightMode() {
1388 	Background()->toggleNightMode(std::nullopt);
1389 }
1390 
ToggleNightMode(const QString & path)1391 void ToggleNightMode(const QString &path) {
1392 	Background()->toggleNightMode(path);
1393 }
1394 
ToggleNightModeWithConfirmation(not_null<Controller * > window,Fn<void ()> toggle)1395 void ToggleNightModeWithConfirmation(
1396 		not_null<Controller*> window,
1397 		Fn<void()> toggle) {
1398 	if (Background()->nightModeChangeAllowed()) {
1399 		toggle();
1400 	} else {
1401 		const auto disableAndToggle = [=](Fn<void()> &&close) {
1402 			Core::App().settings().setSystemDarkModeEnabled(false);
1403 			Core::App().saveSettingsDelayed();
1404 			toggle();
1405 			close();
1406 		};
1407 		window->show(Box<Ui::ConfirmBox>(
1408 			tr::lng_settings_auto_night_warning(tr::now),
1409 			tr::lng_settings_auto_night_disable(tr::now),
1410 			disableAndToggle));
1411 	}
1412 }
1413 
ResetToSomeDefault()1414 void ResetToSomeDefault() {
1415 	Background()->reapplyWithNightMode(
1416 		IsNightMode() ? NightThemePath() : QString(),
1417 		IsNightMode());
1418 }
1419 
LoadFromFile(const QString & path,not_null<Instance * > out,Cached * outCache,QByteArray * outContent)1420 bool LoadFromFile(
1421 		const QString &path,
1422 		not_null<Instance*> out,
1423 		Cached *outCache,
1424 		QByteArray *outContent) {
1425 	const auto colorizer = ColorizerForTheme(path);
1426 	return LoadFromFile(path, out, outCache, outContent, colorizer);
1427 }
1428 
LoadFromFile(const QString & path,not_null<Instance * > out,Cached * outCache,QByteArray * outContent,const style::colorizer & colorizer)1429 bool LoadFromFile(
1430 		const QString &path,
1431 		not_null<Instance*> out,
1432 		Cached *outCache,
1433 		QByteArray *outContent,
1434 		const style::colorizer &colorizer) {
1435 	const auto content = readThemeContent(path);
1436 	if (outContent) {
1437 		*outContent = content;
1438 	}
1439 	return LoadTheme(content, colorizer, std::nullopt, outCache, out);
1440 }
1441 
LoadFromContent(const QByteArray & content,not_null<Instance * > out,Cached * outCache)1442 bool LoadFromContent(
1443 		const QByteArray &content,
1444 		not_null<Instance*> out,
1445 		Cached *outCache) {
1446 	return LoadTheme(
1447 		content,
1448 		style::colorizer(),
1449 		std::nullopt,
1450 		outCache,
1451 		out);
1452 }
1453 
IsThemeDarkValue()1454 rpl::producer<bool> IsThemeDarkValue() {
1455 	return rpl::single(
1456 		rpl::empty_value()
1457 	) | rpl::then(
1458 		style::PaletteChanged()
1459 	) | rpl::map([] {
1460 		return (st::dialogsBg->c.valueF() < kDarkValueThreshold);
1461 	});
1462 }
1463 
EditingPalettePath()1464 QString EditingPalettePath() {
1465 	return cWorkingDir() + "tdata/editing-theme.tdesktop-palette";
1466 }
1467 
ReadPaletteValues(const QByteArray & content,Fn<bool (QLatin1String name,QLatin1String value)> callback)1468 bool ReadPaletteValues(const QByteArray &content, Fn<bool(QLatin1String name, QLatin1String value)> callback) {
1469 	if (content.size() > kThemeSchemeSizeLimit) {
1470 		LOG(("Theme Error: color scheme file too large (should be less than 1 MB, got %2)").arg(content.size()));
1471 		return false;
1472 	}
1473 
1474 	auto data = base::parse::stripComments(content);
1475 	auto from = data.constData(), end = from + data.size();
1476 	while (from != end) {
1477 		auto name = QLatin1String("");
1478 		auto value = QLatin1String("");
1479 		if (!readNameAndValue(from, end, &name, &value)) {
1480 			DEBUG_LOG(("Theme: Could not readNameAndValue."));
1481 			return false;
1482 		}
1483 		if (name.size() == 0) { // End of content reached.
1484 			return true;
1485 		}
1486 		if (!callback(name, value)) {
1487 			return false;
1488 		}
1489 	}
1490 	return true;
1491 }
1492 
1493 } // namespace Theme
1494 } // namespace Window
1495