window_theme_editor.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938
  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_editor.h"
  8. #include "window/themes/window_theme.h"
  9. #include "window/themes/window_theme_editor_block.h"
  10. #include "window/themes/window_theme_editor_box.h"
  11. #include "window/themes/window_themes_embedded.h"
  12. #include "window/window_controller.h"
  13. #include "main/main_account.h"
  14. #include "mainwindow.h"
  15. #include "storage/localstorage.h"
  16. #include "ui/boxes/confirm_box.h"
  17. #include "ui/widgets/color_editor.h"
  18. #include "ui/widgets/scroll_area.h"
  19. #include "ui/widgets/shadow.h"
  20. #include "ui/widgets/buttons.h"
  21. #include "ui/widgets/multi_select.h"
  22. #include "ui/widgets/dropdown_menu.h"
  23. #include "ui/toast/toast.h"
  24. #include "ui/style/style_palette_colorizer.h"
  25. #include "ui/image/image_prepare.h"
  26. #include "ui/painter.h"
  27. #include "ui/ui_utility.h"
  28. #include "boxes/abstract_box.h"
  29. #include "base/parse_helper.h"
  30. #include "base/zlib_help.h"
  31. #include "base/call_delayed.h"
  32. #include "core/file_utilities.h"
  33. #include "core/application.h"
  34. #include "lang/lang_keys.h"
  35. #include "styles/style_window.h"
  36. #include "styles/style_dialogs.h"
  37. #include "styles/style_layers.h"
  38. #include "styles/style_boxes.h"
  39. #include "styles/style_menu_icons.h"
  40. namespace Window {
  41. namespace Theme {
  42. namespace {
  43. template <size_t Size>
  44. QByteArray qba(const char(&string)[Size]) {
  45. return QByteArray::fromRawData(string, Size - 1);
  46. }
  47. const auto kCloudInTextStart = qba("// THEME EDITOR SERVICE INFO START\n");
  48. const auto kCloudInTextEnd = qba("// THEME EDITOR SERVICE INFO END\n\n");
  49. struct ReadColorResult {
  50. ReadColorResult(QColor color, bool error = false) : color(color), error(error) {
  51. }
  52. QColor color;
  53. bool error = false;
  54. };
  55. ReadColorResult colorError(const QString &name) {
  56. return { QColor(), true };
  57. }
  58. ReadColorResult readColor(const QString &name, const char *data, int size) {
  59. if (size != 6 && size != 8) {
  60. return colorError(name);
  61. }
  62. auto readHex = [](char ch) {
  63. if (ch >= '0' && ch <= '9') {
  64. return (ch - '0');
  65. } else if (ch >= 'a' && ch <= 'f') {
  66. return (ch - 'a' + 10);
  67. } else if (ch >= 'A' && ch <= 'F') {
  68. return (ch - 'A' + 10);
  69. }
  70. return -1;
  71. };
  72. auto readValue = [readHex](const char *data) {
  73. auto high = readHex(data[0]);
  74. auto low = readHex(data[1]);
  75. return (high >= 0 && low >= 0) ? (high * 0x10 + low) : -1;
  76. };
  77. auto r = readValue(data);
  78. auto g = readValue(data + 2);
  79. auto b = readValue(data + 4);
  80. auto a = (size == 8) ? readValue(data + 6) : 255;
  81. if (r < 0 || g < 0 || b < 0 || a < 0) {
  82. return colorError(name);
  83. }
  84. return { QColor(r, g, b, a) };
  85. }
  86. bool skipComment(const char *&data, const char *end) {
  87. if (data == end) return false;
  88. if (*data == '/' && data + 1 != end) {
  89. if (*(data + 1) == '/') {
  90. data += 2;
  91. while (data != end && *data != '\n') {
  92. ++data;
  93. }
  94. return true;
  95. } else if (*(data + 1) == '*') {
  96. data += 2;
  97. while (true) {
  98. while (data != end && *data != '*') {
  99. ++data;
  100. }
  101. if (data != end) {
  102. ++data;
  103. if (data != end && *data == '/') {
  104. ++data;
  105. break;
  106. }
  107. }
  108. if (data == end) {
  109. break;
  110. }
  111. }
  112. return true;
  113. }
  114. }
  115. return false;
  116. }
  117. void skipWhitespacesAndComments(const char *&data, const char *end) {
  118. while (data != end) {
  119. if (!base::parse::skipWhitespaces(data, end)) return;
  120. if (!skipComment(data, end)) return;
  121. }
  122. }
  123. QLatin1String readValue(const char *&data, const char *end) {
  124. auto start = data;
  125. if (data != end && *data == '#') {
  126. ++data;
  127. }
  128. base::parse::readName(data, end);
  129. return QLatin1String(start, data - start);
  130. }
  131. bool isValidColorValue(QLatin1String value) {
  132. auto isValidHexChar = [](char ch) {
  133. return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f');
  134. };
  135. auto data = value.data();
  136. auto size = value.size();
  137. if ((size != 7 && size != 9) || data[0] != '#') {
  138. return false;
  139. }
  140. for (auto i = 1; i != size; ++i) {
  141. if (!isValidHexChar(data[i])) {
  142. return false;
  143. }
  144. }
  145. return true;
  146. }
  147. [[nodiscard]] QByteArray ColorizeInContent(
  148. QByteArray content,
  149. const style::colorizer &colorizer) {
  150. auto validNames = OrderedSet<QLatin1String>();
  151. content.detach();
  152. auto start = content.constBegin(), data = start, end = data + content.size();
  153. while (data != end) {
  154. skipWhitespacesAndComments(data, end);
  155. if (data == end) break;
  156. [[maybe_unused]] auto foundName = base::parse::readName(data, end);
  157. skipWhitespacesAndComments(data, end);
  158. if (data == end || *data != ':') {
  159. return "error";
  160. }
  161. ++data;
  162. skipWhitespacesAndComments(data, end);
  163. auto value = readValue(data, end);
  164. if (value.size() == 0) {
  165. return "error";
  166. }
  167. if (isValidColorValue(value)) {
  168. const auto colorized = style::colorize(value, colorizer);
  169. Assert(colorized.size() == value.size());
  170. memcpy(
  171. content.data() + (data - start) - value.size(),
  172. colorized.data(),
  173. value.size());
  174. }
  175. skipWhitespacesAndComments(data, end);
  176. if (data == end || *data != ';') {
  177. return "error";
  178. }
  179. ++data;
  180. }
  181. return content;
  182. }
  183. QString bytesToUtf8(QLatin1String bytes) {
  184. return QString::fromUtf8(bytes.data(), bytes.size());
  185. }
  186. } // namespace
  187. class Editor::Inner final : public Ui::RpWidget {
  188. public:
  189. Inner(QWidget *parent, const QString &path);
  190. void setErrorCallback(Fn<void()> callback) {
  191. _errorCallback = std::move(callback);
  192. }
  193. void setFocusCallback(Fn<void()> callback) {
  194. _focusCallback = std::move(callback);
  195. }
  196. void setScrollCallback(Fn<void(int top, int bottom)> callback) {
  197. _scrollCallback = std::move(callback);
  198. }
  199. void prepare();
  200. [[nodiscard]] QByteArray paletteContent() const {
  201. return _paletteContent;
  202. }
  203. void filterRows(const QString &query);
  204. void chooseRow();
  205. void selectSkip(int direction);
  206. void selectSkipPage(int delta, int direction);
  207. void applyNewPalette(const QByteArray &newContent);
  208. void recreateRows();
  209. ~Inner() {
  210. if (_context.colorEditor.box) {
  211. _context.colorEditor.box->closeBox();
  212. }
  213. }
  214. protected:
  215. void paintEvent(QPaintEvent *e) override;
  216. int resizeGetHeight(int newWidth) override;
  217. private:
  218. bool readData();
  219. bool readExistingRows();
  220. bool feedExistingRow(const QString &name, QLatin1String value);
  221. void error() {
  222. if (_errorCallback) {
  223. _errorCallback();
  224. }
  225. }
  226. void applyEditing(const QString &name, const QString &copyOf, QColor value);
  227. void sortByAccentDistance();
  228. EditorBlock::Context _context;
  229. QString _path;
  230. QByteArray _paletteContent;
  231. Fn<void()> _errorCallback;
  232. Fn<void()> _focusCallback;
  233. Fn<void(int top, int bottom)> _scrollCallback;
  234. object_ptr<EditorBlock> _existingRows;
  235. object_ptr<EditorBlock> _newRows;
  236. bool _applyingUpdate = false;
  237. };
  238. QByteArray ColorHexString(const QColor &color) {
  239. auto result = QByteArray();
  240. result.reserve(9);
  241. result.append('#');
  242. const auto addHex = [&](int code) {
  243. if (code >= 0 && code < 10) {
  244. result.append('0' + code);
  245. } else if (code >= 10 && code < 16) {
  246. result.append('a' + (code - 10));
  247. }
  248. };
  249. const auto addValue = [&](int code) {
  250. addHex(code / 16);
  251. addHex(code % 16);
  252. };
  253. addValue(color.red());
  254. addValue(color.green());
  255. addValue(color.blue());
  256. if (color.alpha() != 255) {
  257. addValue(color.alpha());
  258. }
  259. return result;
  260. }
  261. QByteArray ReplaceValueInPaletteContent(
  262. const QByteArray &content,
  263. const QByteArray &name,
  264. const QByteArray &value) {
  265. auto validNames = OrderedSet<QLatin1String>();
  266. auto start = content.constBegin(), data = start, end = data + content.size();
  267. auto lastValidValueStart = end, lastValidValueEnd = end;
  268. while (data != end) {
  269. skipWhitespacesAndComments(data, end);
  270. if (data == end) break;
  271. auto foundName = base::parse::readName(data, end);
  272. skipWhitespacesAndComments(data, end);
  273. if (data == end || *data != ':') {
  274. return "error";
  275. }
  276. ++data;
  277. skipWhitespacesAndComments(data, end);
  278. auto valueStart = data;
  279. auto value = readValue(data, end);
  280. auto valueEnd = data;
  281. if (value.size() == 0) {
  282. return "error";
  283. }
  284. auto validValue = validNames.contains(value) || isValidColorValue(value);
  285. if (validValue) {
  286. validNames.insert(foundName);
  287. if (foundName == name) {
  288. lastValidValueStart = valueStart;
  289. lastValidValueEnd = valueEnd;
  290. }
  291. }
  292. skipWhitespacesAndComments(data, end);
  293. if (data == end || *data != ';') {
  294. return "error";
  295. }
  296. ++data;
  297. }
  298. if (lastValidValueStart != end) {
  299. auto result = QByteArray();
  300. result.reserve((lastValidValueStart - start) + value.size() + (end - lastValidValueEnd));
  301. result.append(start, lastValidValueStart - start);
  302. result.append(value);
  303. if (end - lastValidValueEnd > 0) result.append(lastValidValueEnd, end - lastValidValueEnd);
  304. return result;
  305. }
  306. auto newline = (content.indexOf("\r\n") >= 0 ? "\r\n" : "\n");
  307. auto addedline = (content.endsWith('\n') ? "" : newline);
  308. return content + addedline + name + ": " + value + ";" + newline;
  309. }
  310. [[nodiscard]] QByteArray WriteCloudToText(const Data::CloudTheme &cloud) {
  311. auto result = QByteArray();
  312. const auto add = [&](const QByteArray &key, const QString &value) {
  313. result.append("// " + key + ": " + value.toLatin1() + "\n");
  314. };
  315. result.append(kCloudInTextStart);
  316. add("ID", QString::number(cloud.id));
  317. add("ACCESS", QString::number(cloud.accessHash));
  318. result.append(kCloudInTextEnd);
  319. return result;
  320. }
  321. [[nodiscard]] Data::CloudTheme ReadCloudFromText(const QByteArray &text) {
  322. const auto index = text.indexOf(kCloudInTextEnd);
  323. if (index <= 1) {
  324. return Data::CloudTheme();
  325. }
  326. auto result = Data::CloudTheme();
  327. const auto list = text.mid(0, index - 1).split('\n');
  328. const auto take = [&](uint64 &value, int index) {
  329. if (list.size() <= index) {
  330. return false;
  331. }
  332. const auto &entry = list[index];
  333. const auto position = entry.indexOf(": ");
  334. if (position < 0) {
  335. return false;
  336. }
  337. value = QString::fromLatin1(entry.mid(position + 2)).toULongLong();
  338. return true;
  339. };
  340. if (!take(result.id, 1) || !take(result.accessHash, 2)) {
  341. return Data::CloudTheme();
  342. }
  343. return result;
  344. }
  345. QByteArray StripCloudTextFields(const QByteArray &text) {
  346. const auto firstValue = text.indexOf(": #");
  347. auto start = 0;
  348. while (true) {
  349. const auto index = text.indexOf(kCloudInTextEnd, start);
  350. if (index < 0 || index > firstValue) {
  351. break;
  352. }
  353. start = index + kCloudInTextEnd.size();
  354. }
  355. return (start > 0) ? text.mid(start) : text;
  356. }
  357. Editor::Inner::Inner(QWidget *parent, const QString &path)
  358. : RpWidget(parent)
  359. , _path(path)
  360. , _existingRows(this, EditorBlock::Type::Existing, &_context)
  361. , _newRows(this, EditorBlock::Type::New, &_context) {
  362. resize(st::windowMinWidth, st::windowMinHeight);
  363. _context.resized.events(
  364. ) | rpl::start_with_next([=] {
  365. resizeToWidth(width());
  366. }, lifetime());
  367. using Context = EditorBlock::Context;
  368. _context.pending.events(
  369. ) | rpl::start_with_next([=](const Context::EditionData &data) {
  370. applyEditing(data.name, data.copyOf, data.value);
  371. }, lifetime());
  372. _context.updated.events(
  373. ) | rpl::start_with_next([=] {
  374. if (_context.name.isEmpty() && _focusCallback) {
  375. _focusCallback();
  376. }
  377. }, lifetime());
  378. _context.scroll.events(
  379. ) | rpl::start_with_next([=](const Context::ScrollData &data) {
  380. if (_scrollCallback) {
  381. auto top = (data.type == EditorBlock::Type::Existing
  382. ? _existingRows
  383. : _newRows)->y();
  384. top += data.position;
  385. _scrollCallback(top, top + data.height);
  386. }
  387. }, lifetime());
  388. Background()->updates(
  389. ) | rpl::start_with_next([=](const BackgroundUpdate &update) {
  390. if (_applyingUpdate || !Background()->editingTheme()) {
  391. return;
  392. }
  393. if (update.type == BackgroundUpdate::Type::TestingTheme) {
  394. Revert();
  395. base::call_delayed(st::slideDuration, this, [] {
  396. Ui::show(Ui::MakeInformBox(
  397. tr::lng_theme_editor_cant_change_theme()));
  398. });
  399. }
  400. }, lifetime());
  401. }
  402. void Editor::Inner::recreateRows() {
  403. _existingRows.create(this, EditorBlock::Type::Existing, &_context);
  404. _existingRows->show();
  405. _newRows.create(this, EditorBlock::Type::New, &_context);
  406. _newRows->show();
  407. if (!readData()) {
  408. error();
  409. }
  410. }
  411. void Editor::Inner::prepare() {
  412. QFile f(_path);
  413. if (!f.open(QIODevice::ReadOnly)) {
  414. LOG(("Theme Error: could not open color palette file '%1'").arg(_path));
  415. error();
  416. return;
  417. }
  418. _paletteContent = f.readAll();
  419. if (f.error() != QFileDevice::NoError) {
  420. LOG(("Theme Error: could not read content from palette file '%1'").arg(_path));
  421. error();
  422. return;
  423. }
  424. f.close();
  425. if (!readData()) {
  426. error();
  427. }
  428. }
  429. void Editor::Inner::filterRows(const QString &query) {
  430. if (query == ":sort-for-accent") {
  431. sortByAccentDistance();
  432. filterRows(QString());
  433. return;
  434. }
  435. _existingRows->filterRows(query);
  436. _newRows->filterRows(query);
  437. }
  438. void Editor::Inner::chooseRow() {
  439. if (!_existingRows->hasSelected() && !_newRows->hasSelected()) {
  440. selectSkip(1);
  441. }
  442. if (_existingRows->hasSelected()) {
  443. _existingRows->chooseRow();
  444. } else if (_newRows->hasSelected()) {
  445. _newRows->chooseRow();
  446. }
  447. }
  448. // Block::selectSkip(-1) removes the selection if it can't select anything
  449. // Block::selectSkip(1) leaves the selection if it can't select anything
  450. void Editor::Inner::selectSkip(int direction) {
  451. if (direction > 0) {
  452. if (_newRows->hasSelected()) {
  453. _existingRows->clearSelected();
  454. _newRows->selectSkip(direction);
  455. } else if (_existingRows->hasSelected()) {
  456. if (!_existingRows->selectSkip(direction)) {
  457. if (_newRows->selectSkip(direction)) {
  458. _existingRows->clearSelected();
  459. }
  460. }
  461. } else {
  462. if (!_existingRows->selectSkip(direction)) {
  463. _newRows->selectSkip(direction);
  464. }
  465. }
  466. } else {
  467. if (_existingRows->hasSelected()) {
  468. _newRows->clearSelected();
  469. _existingRows->selectSkip(direction);
  470. } else if (_newRows->hasSelected()) {
  471. if (!_newRows->selectSkip(direction)) {
  472. _existingRows->selectSkip(direction);
  473. }
  474. }
  475. }
  476. }
  477. void Editor::Inner::selectSkipPage(int delta, int direction) {
  478. auto defaultRowHeight = st::themeEditorMargin.top()
  479. + st::themeEditorSampleSize.height()
  480. + st::themeEditorDescriptionSkip
  481. + st::defaultTextStyle.font->height
  482. + st::themeEditorMargin.bottom();
  483. for (auto i = 0, count = ceilclamp(delta, defaultRowHeight, 1, delta); i != count; ++i) {
  484. selectSkip(direction);
  485. }
  486. }
  487. void Editor::Inner::paintEvent(QPaintEvent *e) {
  488. Painter p(this);
  489. p.setFont(st::boxTitleFont);
  490. p.setPen(st::windowFg);
  491. if (!_newRows->isHidden()) {
  492. p.drawTextLeft(st::themeEditorMargin.left(), _existingRows->y() + _existingRows->height() + st::boxTitlePosition.y(), width(), tr::lng_theme_editor_new_keys(tr::now));
  493. }
  494. }
  495. int Editor::Inner::resizeGetHeight(int newWidth) {
  496. auto rowsWidth = newWidth;
  497. _existingRows->resizeToWidth(rowsWidth);
  498. _newRows->resizeToWidth(rowsWidth);
  499. _existingRows->moveToLeft(0, 0);
  500. _newRows->moveToLeft(0, _existingRows->height() + st::boxTitleHeight);
  501. auto lowest = (_newRows->isHidden() ? _existingRows : _newRows).data();
  502. return lowest->y() + lowest->height();
  503. }
  504. bool Editor::Inner::readData() {
  505. if (!readExistingRows()) {
  506. return false;
  507. }
  508. const auto rows = style::main_palette::data();
  509. for (const auto &row : rows) {
  510. auto name = bytesToUtf8(row.name);
  511. auto description = bytesToUtf8(row.description);
  512. if (!_existingRows->feedDescription(name, description)) {
  513. if (row.value.data()[0] == '#') {
  514. auto result = readColor(name, row.value.data() + 1, row.value.size() - 1);
  515. Assert(!result.error);
  516. _newRows->feed(name, result.color);
  517. //if (!_newRows->feedFallbackName(name, row.fallback.utf16())) {
  518. // Unexpected("Row for fallback not found");
  519. //}
  520. } else {
  521. auto copyOf = bytesToUtf8(row.value);
  522. if (auto result = _existingRows->find(copyOf)) {
  523. _newRows->feed(name, *result, copyOf);
  524. } else if (!_newRows->feedCopy(name, copyOf)) {
  525. Unexpected("Copy of unknown value in the default palette");
  526. }
  527. Assert(row.fallback.size() == 0);
  528. }
  529. if (!_newRows->feedDescription(name, description)) {
  530. Unexpected("Row for description not found");
  531. }
  532. }
  533. }
  534. return true;
  535. }
  536. void Editor::Inner::sortByAccentDistance() {
  537. const auto accent = *_existingRows->find("windowBgActive");
  538. _existingRows->sortByDistance(accent);
  539. _newRows->sortByDistance(accent);
  540. }
  541. bool Editor::Inner::readExistingRows() {
  542. return ReadPaletteValues(_paletteContent, [this](QLatin1String name, QLatin1String value) {
  543. return feedExistingRow(name, value);
  544. });
  545. }
  546. bool Editor::Inner::feedExistingRow(const QString &name, QLatin1String value) {
  547. auto data = value.data();
  548. auto size = value.size();
  549. if (data[0] != '#') {
  550. return _existingRows->feedCopy(name, QString(value));
  551. }
  552. auto result = readColor(name, data + 1, size - 1);
  553. if (result.error) {
  554. 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));
  555. } else {
  556. _existingRows->feed(name, result.color);
  557. }
  558. return true;
  559. }
  560. void Editor::Inner::applyEditing(const QString &name, const QString &copyOf, QColor value) {
  561. auto plainName = name.toLatin1();
  562. auto plainValue = copyOf.isEmpty() ? ColorHexString(value) : copyOf.toLatin1();
  563. auto newContent = ReplaceValueInPaletteContent(_paletteContent, plainName, plainValue);
  564. if (newContent == "error") {
  565. LOG(("Theme Error: could not replace '%1: %2' in content").arg(name, copyOf.isEmpty() ? QString::fromLatin1(ColorHexString(value)) : copyOf));
  566. error();
  567. return;
  568. }
  569. applyNewPalette(newContent);
  570. }
  571. void Editor::Inner::applyNewPalette(const QByteArray &newContent) {
  572. QFile f(_path);
  573. if (!f.open(QIODevice::WriteOnly)) {
  574. LOG(("Theme Error: could not open '%1' for writing a palette update.").arg(_path));
  575. error();
  576. return;
  577. }
  578. if (f.write(newContent) != newContent.size()) {
  579. LOG(("Theme Error: could not write all content to '%1' while writing a palette update.").arg(_path));
  580. error();
  581. return;
  582. }
  583. f.close();
  584. _applyingUpdate = true;
  585. if (!ApplyEditedPalette(newContent)) {
  586. LOG(("Theme Error: could not apply newly composed content :("));
  587. error();
  588. return;
  589. }
  590. _applyingUpdate = false;
  591. _paletteContent = newContent;
  592. }
  593. Editor::Editor(
  594. QWidget*,
  595. not_null<Window::Controller*> window,
  596. const Data::CloudTheme &cloud)
  597. : _window(window)
  598. , _cloud(cloud)
  599. , _scroll(this)
  600. , _close(this, st::defaultMultiSelect.fieldCancel)
  601. , _menuToggle(this, st::themesMenuToggle)
  602. , _select(this, st::defaultMultiSelect, tr::lng_country_ph())
  603. , _leftShadow(this)
  604. , _topShadow(this)
  605. , _save(
  606. this,
  607. tr::lng_theme_editor_save_button(tr::now),
  608. st::dialogsUpdateButton) {
  609. const auto path = EditingPalettePath();
  610. _inner = _scroll->setOwnedWidget(object_ptr<Inner>(this, path));
  611. _save->setClickedCallback(base::fn_delayed(
  612. st::defaultRippleAnimation.hideDuration,
  613. this,
  614. [=] { save(); }));
  615. _inner->setErrorCallback([=] {
  616. window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
  617. // This could be from inner->_context observable notification.
  618. // We should not destroy it while iterating in subscribers.
  619. crl::on_main(this, [=] {
  620. closeEditor();
  621. });
  622. });
  623. _inner->setFocusCallback([this] {
  624. base::call_delayed(2 * st::boxDuration, this, [this] {
  625. _select->setInnerFocus();
  626. });
  627. });
  628. _inner->setScrollCallback([this](int top, int bottom) {
  629. _scroll->scrollToY(top, bottom);
  630. });
  631. _menuToggle->setClickedCallback([=] {
  632. showMenu();
  633. });
  634. _close->setClickedCallback([=] {
  635. closeWithConfirmation();
  636. });
  637. _close->show(anim::type::instant);
  638. _select->resizeToWidth(st::windowMinWidth);
  639. _select->setQueryChangedCallback([this](const QString &query) { _inner->filterRows(query); _scroll->scrollToY(0); });
  640. _select->setSubmittedCallback([this](Qt::KeyboardModifiers) { _inner->chooseRow(); });
  641. _inner->prepare();
  642. resizeToWidth(st::windowMinWidth);
  643. }
  644. void Editor::showMenu() {
  645. if (_menu) {
  646. return;
  647. }
  648. _menu = base::make_unique_q<Ui::DropdownMenu>(
  649. this,
  650. st::dropdownMenuWithIcons);
  651. _menu->setHiddenCallback([weak = Ui::MakeWeak(this), menu = _menu.get()]{
  652. menu->deleteLater();
  653. if (weak && weak->_menu == menu) {
  654. weak->_menu = nullptr;
  655. weak->_menuToggle->setForceRippled(false);
  656. }
  657. });
  658. _menu->setShowStartCallback(crl::guard(this, [this, menu = _menu.get()]{
  659. if (_menu == menu) {
  660. _menuToggle->setForceRippled(true);
  661. }
  662. }));
  663. _menu->setHideStartCallback(crl::guard(this, [this, menu = _menu.get()]{
  664. if (_menu == menu) {
  665. _menuToggle->setForceRippled(false);
  666. }
  667. }));
  668. _menuToggle->installEventFilter(_menu);
  669. _menu->addAction(tr::lng_theme_editor_menu_export(tr::now), [=] {
  670. base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {
  671. exportTheme();
  672. });
  673. }, &st::menuIconExportTheme);
  674. _menu->addAction(tr::lng_theme_editor_menu_import(tr::now), [=] {
  675. base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [=] {
  676. importTheme();
  677. });
  678. }, &st::menuIconImportTheme);
  679. _menu->addAction(tr::lng_theme_editor_menu_show(tr::now), [=] {
  680. File::ShowInFolder(EditingPalettePath());
  681. }, &st::menuIconPalette);
  682. _menu->moveToRight(st::themesMenuPosition.x(), st::themesMenuPosition.y());
  683. _menu->showAnimated(Ui::PanelAnimation::Origin::TopRight);
  684. }
  685. void Editor::exportTheme() {
  686. auto caption = tr::lng_theme_editor_choose_name(tr::now);
  687. auto filter = "Themes (*.tdesktop-theme)";
  688. auto name = "awesome.tdesktop-theme";
  689. FileDialog::GetWritePath(this, caption, filter, name, crl::guard(this, [=](const QString &path) {
  690. const auto result = CollectForExport(_inner->paletteContent());
  691. QFile f(path);
  692. if (!f.open(QIODevice::WriteOnly)) {
  693. LOG(("Theme Error: could not open zip-ed theme file '%1' for writing").arg(path));
  694. _window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
  695. return;
  696. }
  697. if (f.write(result) != result.size()) {
  698. LOG(("Theme Error: could not write zip-ed theme to file '%1'").arg(path));
  699. _window->show(Ui::MakeInformBox(tr::lng_theme_editor_error()));
  700. return;
  701. }
  702. _window->showToast(tr::lng_theme_editor_done(tr::now));
  703. }));
  704. }
  705. void Editor::importTheme() {
  706. auto filters = QStringList(
  707. u"Theme files (*.tdesktop-theme *.tdesktop-palette)"_q);
  708. filters.push_back(FileDialog::AllFilesFilter());
  709. const auto callback = crl::guard(this, [=](
  710. const FileDialog::OpenResult &result) {
  711. const auto path = result.paths.isEmpty()
  712. ? QString()
  713. : result.paths.front();
  714. if (path.isEmpty()) {
  715. return;
  716. }
  717. auto f = QFile(path);
  718. if (!f.open(QIODevice::ReadOnly)) {
  719. return;
  720. }
  721. auto object = Object();
  722. object.pathAbsolute = QFileInfo(path).absoluteFilePath();
  723. object.pathRelative = QDir().relativeFilePath(path);
  724. object.content = f.readAll();
  725. if (object.content.isEmpty()) {
  726. return;
  727. }
  728. _select->clearQuery();
  729. const auto parsed = ParseTheme(object, false, false);
  730. _inner->applyNewPalette(parsed.palette);
  731. _inner->recreateRows();
  732. updateControlsGeometry();
  733. auto image = Images::Read({
  734. .content = parsed.background,
  735. .forceOpaque = true,
  736. }).image;
  737. if (!image.isNull() && !image.size().isEmpty()) {
  738. Background()->set(Data::CustomWallPaper(), std::move(image));
  739. Background()->setTile(parsed.tiled);
  740. Ui::ForceFullRepaint(_window->widget());
  741. }
  742. });
  743. FileDialog::GetOpenPath(
  744. this,
  745. tr::lng_theme_editor_menu_import(tr::now),
  746. filters.join(u";;"_q),
  747. crl::guard(this, callback));
  748. }
  749. QByteArray Editor::ColorizeInContent(
  750. QByteArray content,
  751. const style::colorizer &colorizer) {
  752. return Window::Theme::ColorizeInContent(content, colorizer);
  753. }
  754. void Editor::save() {
  755. if (Core::App().passcodeLocked()) {
  756. _window->showToast(tr::lng_theme_editor_need_unlock(tr::now));
  757. return;
  758. } else if (!_window->account().sessionExists()) {
  759. _window->showToast(tr::lng_theme_editor_need_auth(tr::now));
  760. return;
  761. } else if (_saving) {
  762. return;
  763. }
  764. _saving = true;
  765. const auto unlock = crl::guard(this, [=] { _saving = false; });
  766. SaveTheme(_window, _cloud, _inner->paletteContent(), unlock);
  767. }
  768. void Editor::resizeEvent(QResizeEvent *e) {
  769. updateControlsGeometry();
  770. }
  771. void Editor::updateControlsGeometry() {
  772. _save->resizeToWidth(width());
  773. _close->moveToRight(0, 0);
  774. _menuToggle->moveToRight(_close->width(), 0);
  775. _select->resizeToWidth(width());
  776. _select->moveToLeft(0, _close->height());
  777. auto shadowTop = _select->y() + _select->height();
  778. _topShadow->resize(width() - st::lineWidth, st::lineWidth);
  779. _topShadow->moveToLeft(st::lineWidth, shadowTop);
  780. _leftShadow->resize(st::lineWidth, height());
  781. _leftShadow->moveToLeft(0, 0);
  782. auto scrollSize = QSize(width(), height() - shadowTop - _save->height());
  783. if (_scroll->size() != scrollSize) {
  784. _scroll->resize(scrollSize);
  785. }
  786. _inner->resizeToWidth(width());
  787. _scroll->moveToLeft(0, shadowTop);
  788. if (!_scroll->isHidden()) {
  789. auto scrollTop = _scroll->scrollTop();
  790. _inner->setVisibleTopBottom(scrollTop, scrollTop + _scroll->height());
  791. }
  792. _save->moveToLeft(0, _scroll->y() + _scroll->height());
  793. }
  794. void Editor::keyPressEvent(QKeyEvent *e) {
  795. if (e->key() == Qt::Key_Escape) {
  796. if (!_select->getQuery().isEmpty()) {
  797. _select->clearQuery();
  798. } else {
  799. _window->widget()->setInnerFocus();
  800. }
  801. } else if (e->key() == Qt::Key_Down) {
  802. _inner->selectSkip(1);
  803. } else if (e->key() == Qt::Key_Up) {
  804. _inner->selectSkip(-1);
  805. } else if (e->key() == Qt::Key_PageDown) {
  806. _inner->selectSkipPage(_scroll->height(), 1);
  807. } else if (e->key() == Qt::Key_PageUp) {
  808. _inner->selectSkipPage(_scroll->height(), -1);
  809. }
  810. }
  811. void Editor::focusInEvent(QFocusEvent *e) {
  812. _select->setInnerFocus();
  813. }
  814. void Editor::paintEvent(QPaintEvent *e) {
  815. Painter p(this);
  816. p.fillRect(e->rect(), st::dialogsBg);
  817. p.setFont(st::boxTitleFont);
  818. p.setPen(st::windowFg);
  819. p.drawTextLeft(st::themeEditorMargin.left(), st::themeEditorMargin.top(), width(), tr::lng_theme_editor_title(tr::now));
  820. }
  821. void Editor::closeWithConfirmation() {
  822. if (!PaletteChanged(_inner->paletteContent(), _cloud)) {
  823. Background()->clearEditingTheme(ClearEditing::KeepChanges);
  824. closeEditor();
  825. return;
  826. }
  827. const auto close = crl::guard(this, [=](Fn<void()> &&close) {
  828. Background()->clearEditingTheme(ClearEditing::RevertChanges);
  829. closeEditor();
  830. close();
  831. });
  832. _window->show(Ui::MakeConfirmBox({
  833. .text = tr::lng_theme_editor_sure_close(),
  834. .confirmed = close,
  835. .confirmText = tr::lng_close(),
  836. }));
  837. }
  838. void Editor::closeEditor() {
  839. _window->widget()->showRightColumn(nullptr);
  840. Background()->clearEditingTheme();
  841. }
  842. } // namespace Theme
  843. } // namespace Window