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