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 ©Of, QColor value);
28
name() const29 QString name() const {
30 return _name;
31 }
32
setCopyOf(const QString & copyOf)33 void setCopyOf(const QString ©Of) {
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 ©Of, 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 ©OfExisting) {
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 ©Of) {
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 ©Of, 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