window_theme_editor_block.cpp 22 KB

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