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_block.h"
9 
10 #include "styles/style_window.h"
11 #include "ui/effects/ripple_animation.h"
12 #include "ui/widgets/shadow.h"
13 #include "boxes/edit_color_box.h"
14 #include "lang/lang_keys.h"
15 #include "base/call_delayed.h"
16 
17 namespace Window {
18 namespace Theme {
19 namespace {
20 
21 auto SearchSplitter = QRegularExpression(qsl("[\\@\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\:\\!\\_\\;\\\"\\'\\x0\\#]"));
22 
23 } // namespace
24 
25 class EditorBlock::Row {
26 public:
27 	Row(const QString &name, const QString &copyOf, QColor value);
28 
name() const29 	QString name() const {
30 		return _name;
31 	}
32 
setCopyOf(const QString & copyOf)33 	void setCopyOf(const QString &copyOf) {
34 		_copyOf = copyOf;
35 		fillSearchIndex();
36 	}
copyOf() const37 	QString copyOf() const {
38 		return _copyOf;
39 	}
40 
41 	void setValue(QColor value);
value() const42 	const QColor &value() const {
43 		return _value;
44 	}
45 
description() const46 	QString description() const {
47 		return _description.toString();
48 	}
descriptionText() const49 	const Ui::Text::String &descriptionText() const {
50 		return _description;
51 	}
setDescription(const QString & description)52 	void setDescription(const QString &description) {
53 		_description.setText(st::defaultTextStyle, description);
54 		fillSearchIndex();
55 	}
56 
searchWords() const57 	const base::flat_set<QString> &searchWords() const {
58 		return _searchWords;
59 	}
searchWordsContain(const QString & needle) const60 	bool searchWordsContain(const QString &needle) const {
61 		for (const auto &word : _searchWords) {
62 			if (word.startsWith(needle)) {
63 				return true;
64 			}
65 		}
66 		return false;
67 	}
68 
searchStartChars() const69 	const base::flat_set<QChar> &searchStartChars() const {
70 		return _searchStartChars;
71 	}
72 
setTop(int top)73 	void setTop(int top) {
74 		_top = top;
75 	}
top() const76 	int top() const {
77 		return _top;
78 	}
79 
setHeight(int height)80 	void setHeight(int height) {
81 		_height = height;
82 	}
height() const83 	int height() const {
84 		return _height;
85 	}
86 
ripple() const87 	Ui::RippleAnimation *ripple() const {
88 		return _ripple.get();
89 	}
setRipple(std::unique_ptr<Ui::RippleAnimation> ripple) const90 	Ui::RippleAnimation *setRipple(std::unique_ptr<Ui::RippleAnimation> ripple) const {
91 		_ripple = std::move(ripple);
92 		return _ripple.get();
93 	}
resetRipple() const94 	void resetRipple() const {
95 		_ripple = nullptr;
96 	}
97 
98 private:
99 	void fillValueString();
100 	void fillSearchIndex();
101 
102 	QString _name;
103 	QString _copyOf;
104 	QColor _value;
105 	QString _valueString;
106 	Ui::Text::String _description = { st::windowMinWidth / 2 };
107 
108 	base::flat_set<QString> _searchWords;
109 	base::flat_set<QChar> _searchStartChars;
110 
111 	int _top = 0;
112 	int _height = 0;
113 
114 	mutable std::unique_ptr<Ui::RippleAnimation> _ripple;
115 
116 };
117 
Row(const QString & name,const QString & copyOf,QColor value)118 EditorBlock::Row::Row(const QString &name, const QString &copyOf, QColor value)
119 : _name(name)
120 , _copyOf(copyOf) {
121 	setValue(value);
122 }
123 
setValue(QColor value)124 void EditorBlock::Row::setValue(QColor value) {
125 	_value = value;
126 	fillValueString();
127 	fillSearchIndex();
128 }
129 
fillValueString()130 void EditorBlock::Row::fillValueString() {
131 	auto addHex = [=](int code) {
132 		if (code >= 0 && code < 10) {
133 			_valueString.append('0' + code);
134 		} else if (code >= 10 && code < 16) {
135 			_valueString.append('a' + (code - 10));
136 		}
137 	};
138 	auto addCode = [=](int code) {
139 		addHex(code / 16);
140 		addHex(code % 16);
141 	};
142 	_valueString.resize(0);
143 	_valueString.reserve(9);
144 	_valueString.append('#');
145 	addCode(_value.red());
146 	addCode(_value.green());
147 	addCode(_value.blue());
148 	if (_value.alpha() != 255) {
149 		addCode(_value.alpha());
150 	}
151 }
152 
fillSearchIndex()153 void EditorBlock::Row::fillSearchIndex() {
154 	_searchWords.clear();
155 	_searchStartChars.clear();
156 	const auto toIndex = _name
157 		+ ' ' + _copyOf
158 		+ ' ' + TextUtilities::RemoveAccents(_description.toString())
159 		+ ' ' + _valueString;
160 	const auto words = toIndex.toLower().split(
161 		SearchSplitter,
162 		Qt::SkipEmptyParts);
163 	for (const auto &word : words) {
164 		_searchWords.emplace(word);
165 		_searchStartChars.emplace(word[0]);
166 	}
167 }
168 
EditorBlock(QWidget * parent,Type type,Context * context)169 EditorBlock::EditorBlock(QWidget *parent, Type type, Context *context) : TWidget(parent)
170 , _type(type)
171 , _context(context)
172 , _transparent(style::TransparentPlaceholder()) {
173 	setMouseTracking(true);
174 	subscribe(_context->updated, [this] {
175 		if (_mouseSelection) {
176 			_lastGlobalPos = QCursor::pos();
177 			updateSelected(mapFromGlobal(_lastGlobalPos));
178 		}
179 		update();
180 	});
181 	if (_type == Type::Existing) {
182 		subscribe(_context->appended, [this](const Context::AppendData &added) {
183 			auto name = added.name;
184 			auto value = added.value;
185 			feed(name, value);
186 			feedDescription(name, added.description);
187 
188 			auto row = findRow(name);
189 			Assert(row != nullptr);
190 			auto possibleCopyOf = added.possibleCopyOf;
191 			auto copyOf = checkCopyOf(findRowIndex(row), possibleCopyOf) ? possibleCopyOf : QString();
192 			removeFromSearch(*row);
193 			row->setCopyOf(copyOf);
194 			addToSearch(*row);
195 
196 			_context->changed.notify({ QStringList(name), value }, true);
197 			_context->resized.notify();
198 			_context->pending.notify({ name, copyOf, value }, true);
199 		});
200 	} else {
201 		subscribe(_context->changed, [this](const Context::ChangeData &data) {
202 			checkCopiesChanged(0, data.names, data.value);
203 		});
204 	}
205 }
206 
feed(const QString & name,QColor value,const QString & copyOfExisting)207 void EditorBlock::feed(const QString &name, QColor value, const QString &copyOfExisting) {
208 	if (findRow(name)) {
209 		// Remove the existing row and mark all its copies as unique keys.
210 		LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name));
211 		removeRow(name);
212 	}
213 	addRow(name, copyOfExisting, value);
214 }
215 
feedCopy(const QString & name,const QString & copyOf)216 bool EditorBlock::feedCopy(const QString &name, const QString &copyOf) {
217 	if (auto row = findRow(copyOf)) {
218 		if (findRow(name)) {
219 			// Remove the existing row and mark all its copies as unique keys.
220 			LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name));
221 			removeRow(name);
222 
223 			// row was invalidated by removeRow() call.
224 			row = findRow(copyOf);
225 		}
226 		addRow(name, copyOf, row->value());
227 	} else {
228 		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, copyOf));
229 	}
230 	return true;
231 }
232 
removeRow(const QString & name,bool removeCopyReferences)233 void EditorBlock::removeRow(const QString &name, bool removeCopyReferences) {
234 	auto it = _indices.find(name);
235 	Assert(it != _indices.cend());
236 
237 	auto index = it.value();
238 	for (auto i = index + 1, count = static_cast<int>(_data.size()); i != count; ++i) {
239 		auto &row = _data[i];
240 		removeFromSearch(row);
241 		_indices[row.name()] = i - 1;
242 		if (removeCopyReferences && row.copyOf() == name) {
243 			row.setCopyOf(QString());
244 		}
245 	}
246 	removeFromSearch(_data[index]);
247 	_data.erase(_data.begin() + index);
248 	_indices.erase(it);
249 	for (auto i = index, count = static_cast<int>(_data.size()); i != count; ++i) {
250 		addToSearch(_data[i]);
251 	}
252 }
253 
addToSearch(const Row & row)254 void EditorBlock::addToSearch(const Row &row) {
255 	auto query = _searchQuery;
256 	if (!query.isEmpty()) resetSearch();
257 
258 	auto index = findRowIndex(&row);
259 	for (const auto &ch : row.searchStartChars()) {
260 		_searchIndex[ch].insert(index);
261 	}
262 
263 	if (!query.isEmpty()) searchByQuery(query);
264 }
265 
removeFromSearch(const Row & row)266 void EditorBlock::removeFromSearch(const Row &row) {
267 	auto query = _searchQuery;
268 	if (!query.isEmpty()) resetSearch();
269 
270 	auto index = findRowIndex(&row);
271 	for (const auto &ch : row.searchStartChars()) {
272 		const auto i = _searchIndex.find(ch);
273 		if (i != end(_searchIndex)) {
274 			i->second.remove(index);
275 			if (i->second.empty()) {
276 				_searchIndex.erase(i);
277 			}
278 		}
279 	}
280 
281 	if (!query.isEmpty()) searchByQuery(query);
282 }
283 
filterRows(const QString & query)284 void EditorBlock::filterRows(const QString &query) {
285 	searchByQuery(query);
286 }
287 
chooseRow()288 void EditorBlock::chooseRow() {
289 	if (_selected < 0) {
290 		return;
291 	}
292 	activateRow(rowAtIndex(_selected));
293 }
294 
activateRow(const Row & row)295 void EditorBlock::activateRow(const Row &row) {
296 	if (_context->box) {
297 		if (_type == Type::Existing) {
298 			_context->possibleCopyOf = row.name();
299 			_context->box->showColor(row.value());
300 		}
301 	} else {
302 		_editing = findRowIndex(&row);
303 		if (auto box = Ui::show(Box<EditColorBox>(row.name(), EditColorBox::Mode::RGBA, row.value()))) {
304 			box->setSaveCallback(crl::guard(this, [this](QColor value) {
305 				saveEditing(value);
306 			}));
307 			box->setCancelCallback(crl::guard(this, [this] {
308 				cancelEditing();
309 			}));
310 			_context->box = box;
311 			_context->name = row.name();
312 			_context->updated.notify();
313 		}
314 	}
315 }
316 
selectSkip(int direction)317 bool EditorBlock::selectSkip(int direction) {
318 	_mouseSelection = false;
319 
320 	auto maxSelected = size_type(isSearch()
321 		? _searchResults.size()
322 		: _data.size()) - 1;
323 	auto newSelected = _selected + direction;
324 	if (newSelected < -1 || newSelected > maxSelected) {
325 		newSelected = maxSelected;
326 	}
327 	if (newSelected != _selected) {
328 		setSelected(newSelected);
329 		scrollToSelected();
330 		return (newSelected >= 0);
331 	}
332 	return false;
333 }
334 
scrollToSelected()335 void EditorBlock::scrollToSelected() {
336 	if (_selected >= 0) {
337 		Context::ScrollData update;
338 		update.type = _type;
339 		update.position = rowAtIndex(_selected).top();
340 		update.height = rowAtIndex(_selected).height();
341 		_context->scroll.notify(update, true);
342 	}
343 }
344 
searchByQuery(QString query)345 void EditorBlock::searchByQuery(QString query) {
346 	const auto words = TextUtilities::PrepareSearchWords(
347 		query,
348 		&SearchSplitter);
349 	query = words.isEmpty() ? QString() : words.join(' ');
350 	if (_searchQuery != query) {
351 		setSelected(-1);
352 		setPressed(-1);
353 
354 		_searchQuery = query;
355 		_searchResults.clear();
356 
357 		auto toFilter = (base::flat_set<int>*)nullptr;
358 		for (const auto &word : words) {
359 			if (word.isEmpty()) continue;
360 
361 			const auto i = _searchIndex.find(word[0]);
362 			if (i == end(_searchIndex) || i->second.empty()) {
363 				toFilter = nullptr;
364 				break;
365 			} else if (!toFilter || i->second.size() < toFilter->size()) {
366 				toFilter = &i->second;
367 			}
368 		}
369 		if (toFilter) {
370 			const auto allWordsFound = [&](const Row &row) {
371 				for (const auto &word : words) {
372 					if (!row.searchWordsContain(word)) {
373 						return false;
374 					}
375 				}
376 				return true;
377 			};
378 			for (const auto index : *toFilter) {
379 				if (allWordsFound(_data[index])) {
380 					_searchResults.push_back(index);
381 				}
382 			}
383 		}
384 
385 		_context->resized.notify(true);
386 	}
387 }
388 
find(const QString & name)389 const QColor *EditorBlock::find(const QString &name) {
390 	if (auto row = findRow(name)) {
391 		return &row->value();
392 	}
393 	return nullptr;
394 }
395 
feedDescription(const QString & name,const QString & description)396 bool EditorBlock::feedDescription(const QString &name, const QString &description) {
397 	if (auto row = findRow(name)) {
398 		removeFromSearch(*row);
399 		row->setDescription(description);
400 		addToSearch(*row);
401 		return true;
402 	}
403 	return false;
404 }
405 
sortByDistance(const QColor & to)406 void EditorBlock::sortByDistance(const QColor &to) {
407 	auto toHue = int();
408 	auto toSaturation = int();
409 	auto toLightness = int();
410 	to.getHsl(&toHue, &toSaturation, &toLightness);
411 	ranges::sort(_data, ranges::less(), [&](const Row &row) {
412 		auto fromHue = int();
413 		auto fromSaturation = int();
414 		auto fromLightness = int();
415 		row.value().getHsl(&fromHue, &fromSaturation, &fromLightness);
416 		if (!row.copyOf().isEmpty()) {
417 			return 365;
418 		}
419 		const auto a = std::abs(fromHue - toHue);
420 		const auto b = 360 + fromHue - toHue;
421 		const auto c = 360 + toHue - fromHue;
422 		if (std::min(a, std::min(b, c)) > 15) {
423 			return 363;
424 		}
425 		return 255 - fromSaturation;
426 	});
427 }
428 
429 template <typename Callback>
enumerateRows(Callback callback)430 void EditorBlock::enumerateRows(Callback callback) {
431 	if (isSearch()) {
432 		for (const auto index : _searchResults) {
433 			if (!callback(_data[index])) {
434 				break;
435 			}
436 		}
437 	} else {
438 		for (auto &row : _data) {
439 			if (!callback(row)) {
440 				break;
441 			}
442 		}
443 	}
444 }
445 
446 template <typename Callback>
enumerateRows(Callback callback) const447 void EditorBlock::enumerateRows(Callback callback) const {
448 	if (isSearch()) {
449 		for (const auto index : _searchResults) {
450 			if (!callback(_data[index])) {
451 				break;
452 			}
453 		}
454 	} else {
455 		for (const auto &row : _data) {
456 			if (!callback(row)) {
457 				break;
458 			}
459 		}
460 	}
461 }
462 
463 template <typename Callback>
enumerateRowsFrom(int top,Callback callback)464 void EditorBlock::enumerateRowsFrom(int top, Callback callback) {
465 	auto started = false;
466 	auto index = 0;
467 	enumerateRows([top, callback, &started, &index](Row &row) {
468 		if (!started) {
469 			if (row.top() + row.height() <= top) {
470 				++index;
471 				return true;
472 			}
473 			started = true;
474 		}
475 		return callback(index++, row);
476 	});
477 }
478 
479 template <typename Callback>
enumerateRowsFrom(int top,Callback callback) const480 void EditorBlock::enumerateRowsFrom(int top, Callback callback) const {
481 	auto started = false;
482 	enumerateRows([top, callback, &started](const Row &row) {
483 		if (!started) {
484 			if (row.top() + row.height() <= top) {
485 				return true;
486 			}
487 			started = true;
488 		}
489 		return callback(row);
490 	});
491 }
492 
resizeGetHeight(int newWidth)493 int EditorBlock::resizeGetHeight(int newWidth) {
494 	auto result = 0;
495 	auto descriptionWidth = newWidth - st::themeEditorMargin.left() - st::themeEditorMargin.right();
496 	enumerateRows([&](Row &row) {
497 		row.setTop(result);
498 
499 		auto height = row.height();
500 		if (!height) {
501 			height = st::themeEditorMargin.top() + st::themeEditorSampleSize.height();
502 			if (!row.descriptionText().isEmpty()) {
503 				height += st::themeEditorDescriptionSkip + row.descriptionText().countHeight(descriptionWidth);
504 			}
505 			height += st::themeEditorMargin.bottom();
506 			row.setHeight(height);
507 		}
508 		result += row.height();
509 		return true;
510 	});
511 
512 	if (_type == Type::New) {
513 		setHidden(!result);
514 	}
515 	if (_type == Type::Existing && !result && !isSearch()) {
516 		return st::noContactsHeight;
517 	}
518 	return result;
519 }
520 
mousePressEvent(QMouseEvent * e)521 void EditorBlock::mousePressEvent(QMouseEvent *e) {
522 	updateSelected(e->pos());
523 	setPressed(_selected);
524 }
525 
mouseReleaseEvent(QMouseEvent * e)526 void EditorBlock::mouseReleaseEvent(QMouseEvent *e) {
527 	auto pressed = _pressed;
528 	setPressed(-1);
529 	if (pressed == _selected) {
530 		if (_context->box) {
531 			chooseRow();
532 		} else if (_selected >= 0) {
533 			base::call_delayed(st::defaultRippleAnimation.hideDuration, this, [this, index = findRowIndex(&rowAtIndex(_selected))] {
534 				if (index >= 0 && index < _data.size()) {
535 					activateRow(_data[index]);
536 				}
537 			});
538 		}
539 	}
540 }
541 
saveEditing(QColor value)542 void EditorBlock::saveEditing(QColor value) {
543 	if (_editing < 0) {
544 		return;
545 	}
546 	auto &row = _data[_editing];
547 	auto name = row.name();
548 	if (_type == Type::New) {
549 		setSelected(-1);
550 		setPressed(-1);
551 
552 		auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;
553 		auto color = value;
554 		auto description = row.description();
555 
556 		removeRow(name, false);
557 
558 		_context->appended.notify({ name, possibleCopyOf, color, description }, true);
559 	} else if (_type == Type::Existing) {
560 		removeFromSearch(row);
561 
562 		auto valueChanged = (row.value() != value);
563 		if (valueChanged) {
564 			row.setValue(value);
565 		}
566 
567 		auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf;
568 		auto copyOf = checkCopyOf(_editing, possibleCopyOf) ? possibleCopyOf : QString();
569 		auto copyOfChanged = (row.copyOf() != copyOf);
570 		if (copyOfChanged) {
571 			row.setCopyOf(copyOf);
572 		}
573 
574 		addToSearch(row);
575 
576 		if (valueChanged || copyOfChanged) {
577 			checkCopiesChanged(_editing + 1, QStringList(name), value);
578 			_context->pending.notify({ name, copyOf, value }, true);
579 		}
580 	}
581 	cancelEditing();
582 }
583 
checkCopiesChanged(int startIndex,QStringList names,QColor value)584 void EditorBlock::checkCopiesChanged(int startIndex, QStringList names, QColor value) {
585 	for (auto i = startIndex, count = static_cast<int>(_data.size()); i != count; ++i) {
586 		auto &checkIfIsCopy = _data[i];
587 		if (names.contains(checkIfIsCopy.copyOf())) {
588 			removeFromSearch(checkIfIsCopy);
589 			checkIfIsCopy.setValue(value);
590 			names.push_back(checkIfIsCopy.name());
591 			addToSearch(checkIfIsCopy);
592 		}
593 	}
594 	if (_type == Type::Existing) {
595 		_context->changed.notify({ names, value }, true);
596 	}
597 }
598 
cancelEditing()599 void EditorBlock::cancelEditing() {
600 	if (_editing >= 0) {
601 		updateRow(_data[_editing]);
602 	}
603 	_editing = -1;
604 	if (auto box = base::take(_context->box)) {
605 		box->closeBox();
606 	}
607 	_context->possibleCopyOf = QString();
608 	if (!_context->name.isEmpty()) {
609 		_context->name = QString();
610 		_context->updated.notify();
611 	}
612 }
613 
checkCopyOf(int index,const QString & possibleCopyOf)614 bool EditorBlock::checkCopyOf(int index, const QString &possibleCopyOf) {
615 	auto copyOfIndex = findRowIndex(possibleCopyOf);
616 	return (copyOfIndex >= 0
617 		&& index > copyOfIndex
618 		&& _data[copyOfIndex].value().toRgb() == _data[index].value().toRgb());
619 }
620 
mouseMoveEvent(QMouseEvent * e)621 void EditorBlock::mouseMoveEvent(QMouseEvent *e) {
622 	if (_lastGlobalPos != e->globalPos() || _mouseSelection) {
623 		_lastGlobalPos = e->globalPos();
624 		updateSelected(e->pos());
625 	}
626 }
627 
updateSelected(QPoint localPosition)628 void EditorBlock::updateSelected(QPoint localPosition) {
629 	_mouseSelection = true;
630 	auto top = localPosition.y();
631 	auto underMouseIndex = -1;
632 	enumerateRowsFrom(top, [&underMouseIndex, top](int index, const Row &row) {
633 		if (row.top() <= top) {
634 			underMouseIndex = index;
635 		}
636 		return false;
637 	});
638 	setSelected(underMouseIndex);
639 }
640 
leaveEventHook(QEvent * e)641 void EditorBlock::leaveEventHook(QEvent *e) {
642 	_mouseSelection = false;
643 	setSelected(-1);
644 }
645 
paintEvent(QPaintEvent * e)646 void EditorBlock::paintEvent(QPaintEvent *e) {
647 	Painter p(this);
648 
649 	auto clip = e->rect();
650 	if (_data.empty()) {
651 		p.fillRect(clip, st::dialogsBg);
652 		p.setFont(st::noContactsFont);
653 		p.setPen(st::noContactsColor);
654 		p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_theme_editor_no_keys(tr::now));
655 	}
656 
657 	auto cliptop = clip.y();
658 	auto clipbottom = cliptop + clip.height();
659 	enumerateRowsFrom(cliptop, [&](int index, const Row &row) {
660 		if (row.top() >= clipbottom) {
661 			return false;
662 		}
663 		paintRow(p, index, row);
664 		return true;
665 	});
666 }
667 
paintRow(Painter & p,int index,const Row & row)668 void EditorBlock::paintRow(Painter &p, int index, const Row &row) {
669 	auto rowTop = row.top() + st::themeEditorMargin.top();
670 
671 	auto rect = QRect(0, row.top(), width(), row.height());
672 	auto selected = (_pressed >= 0) ? (index == _pressed) : (index == _selected);
673 	auto active = (findRowIndex(&row) == _editing);
674 	p.fillRect(rect, active ? st::dialogsBgActive : selected ? st::dialogsBgOver : st::dialogsBg);
675 	if (auto ripple = row.ripple()) {
676 		ripple->paint(p, 0, row.top(), width(), &(active ? st::activeButtonBgRipple : st::windowBgRipple)->c);
677 		if (ripple->empty()) {
678 			row.resetRipple();
679 		}
680 	}
681 
682 	auto sample = QRect(width() - st::themeEditorMargin.right() - st::themeEditorSampleSize.width(), rowTop, st::themeEditorSampleSize.width(), st::themeEditorSampleSize.height());
683 	Ui::Shadow::paint(p, sample, width(), st::defaultRoundShadow);
684 	if (row.value().alpha() != 255) {
685 		p.fillRect(myrtlrect(sample), _transparent);
686 	}
687 	p.fillRect(myrtlrect(sample), row.value());
688 
689 	auto rowWidth = width() - st::themeEditorMargin.left() - st::themeEditorMargin.right();
690 	auto nameWidth = rowWidth - st::themeEditorSampleSize.width() - st::themeEditorDescriptionSkip;
691 
692 	p.setFont(st::themeEditorNameFont);
693 	p.setPen(active ? st::dialogsNameFgActive : selected ? st::dialogsNameFgOver : st::dialogsNameFg);
694 	p.drawTextLeft(st::themeEditorMargin.left(), rowTop, width(), st::themeEditorNameFont->elided(row.name(), nameWidth));
695 
696 	if (!row.copyOf().isEmpty()) {
697 		auto copyTop = rowTop + st::themeEditorNameFont->height;
698 		p.setFont(st::themeEditorCopyNameFont);
699 		p.drawTextLeft(st::themeEditorMargin.left(), copyTop, width(), st::themeEditorCopyNameFont->elided("= " + row.copyOf(), nameWidth));
700 	}
701 
702 	if (!row.descriptionText().isEmpty()) {
703 		auto descriptionTop = rowTop + st::themeEditorSampleSize.height() + st::themeEditorDescriptionSkip;
704 		p.setPen(active ? st::dialogsTextFgActive : selected ? st::dialogsTextFgOver : st::dialogsTextFg);
705 		row.descriptionText().drawLeft(p, st::themeEditorMargin.left(), descriptionTop, rowWidth, width());
706 	}
707 
708 	if (isEditing() && !active && (_type == Type::New || (_editing >= 0 && findRowIndex(&row) >= _editing))) {
709 		p.fillRect(rect, st::layerBg);
710 	}
711 }
712 
setSelected(int selected)713 void EditorBlock::setSelected(int selected) {
714 	if (isEditing()) {
715 		if (_type == Type::New) {
716 			selected = -1;
717 		} else if (_editing >= 0 && selected >= 0 && findRowIndex(&rowAtIndex(selected)) >= _editing) {
718 			selected = -1;
719 		}
720 	}
721 	if (_selected != selected) {
722 		if (_selected >= 0) updateRow(rowAtIndex(_selected));
723 		_selected = selected;
724 		if (_selected >= 0) updateRow(rowAtIndex(_selected));
725 		setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default);
726 	}
727 }
728 
setPressed(int pressed)729 void EditorBlock::setPressed(int pressed) {
730 	if (_pressed != pressed) {
731 		if (_pressed >= 0) {
732 			updateRow(rowAtIndex(_pressed));
733 			stopLastRipple(_pressed);
734 		}
735 		_pressed = pressed;
736 		if (_pressed >= 0) {
737 			addRowRipple(_pressed);
738 			updateRow(rowAtIndex(_pressed));
739 		}
740 	}
741 }
742 
addRowRipple(int index)743 void EditorBlock::addRowRipple(int index) {
744 	auto &row = rowAtIndex(index);
745 	auto ripple = row.ripple();
746 	if (!ripple) {
747 		auto mask = Ui::RippleAnimation::rectMask(QSize(width(), row.height()));
748 		ripple = row.setRipple(std::make_unique<Ui::RippleAnimation>(st::defaultRippleAnimation, std::move(mask), [this, index = findRowIndex(&row)] {
749 			updateRow(_data[index]);
750 		}));
751 	}
752 	auto origin = mapFromGlobal(QCursor::pos()) - QPoint(0, row.top());
753 	ripple->add(origin);
754 }
755 
stopLastRipple(int index)756 void EditorBlock::stopLastRipple(int index) {
757 	auto &row = rowAtIndex(index);
758 	if (row.ripple()) {
759 		row.ripple()->lastStop();
760 	}
761 }
762 
updateRow(const Row & row)763 void EditorBlock::updateRow(const Row &row) {
764 	update(0, row.top(), width(), row.height());
765 }
766 
addRow(const QString & name,const QString & copyOf,QColor value)767 void EditorBlock::addRow(const QString &name, const QString &copyOf, QColor value) {
768 	_data.push_back({ name, copyOf, value });
769 	_indices.insert(name, _data.size() - 1);
770 	addToSearch(_data.back());
771 }
772 
rowAtIndex(int index)773 EditorBlock::Row &EditorBlock::rowAtIndex(int index) {
774 	if (isSearch()) {
775 		return _data[_searchResults[index]];
776 	}
777 	return _data[index];
778 }
779 
findRowIndex(const QString & name) const780 int EditorBlock::findRowIndex(const QString &name) const {
781 	return _indices.value(name, -1);;
782 }
783 
findRow(const QString & name)784 EditorBlock::Row *EditorBlock::findRow(const QString &name) {
785 	auto index = findRowIndex(name);
786 	return (index >= 0) ? &_data[index] : nullptr;
787 }
788 
findRowIndex(const Row * row)789 int EditorBlock::findRowIndex(const Row *row) {
790 	return row ? (row - &_data[0]) : -1;
791 }
792 
793 } // namespace Theme
794 } // namespace Window
795