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