window_theme.cpp 47 KB

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