/* This file is part of Telegram Desktop, the official desktop version of Telegram messaging app, see https://telegram.org Telegram Desktop is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. In addition, as a special exception, the copyright holders give permission to link the code of portions of this program with the OpenSSL library. Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "stdafx.h" #include "window/themes/window_theme_editor_block.h" #include "styles/style_window.h" #include "ui/effects/ripple_animation.h" #include "boxes/editcolorbox.h" #include "lang.h" namespace Window { namespace Theme { namespace { auto SearchSplitter = QRegularExpression(qsl("[\\@\\s\\-\\+\\(\\)\\[\\]\\{\\}\\<\\>\\,\\.\\:\\!\\_\\;\\\"\\'\\x0\\#]")); } // namespace class EditorBlock::Row { public: Row(const QString &name, const QString ©Of, QColor value); QString name() const { return _name; } void setCopyOf(const QString ©Of) { _copyOf = copyOf; fillSearchIndex(); } QString copyOf() const { return _copyOf; } void setValue(QColor value); const QColor &value() const { return _value; } QString description() const { return _description.originalText(); } const Text &descriptionText() const { return _description; } void setDescription(const QString &description) { _description.setText(st::defaultTextStyle, description); fillSearchIndex(); } const OrderedSet &searchWords() const { return _searchWords; } bool searchWordsContain(const QString &needle) const { for_const (auto &word, _searchWords) { if (word.startsWith(needle)) { return true; } } return false; } const OrderedSet &searchStartChars() const { return _searchStartChars; } void setTop(int top) { _top = top; } int top() const { return _top; } void setHeight(int height) { _height = height; } int height() const { return _height; } Ui::RippleAnimation *ripple() const { return _ripple.get(); } Ui::RippleAnimation *setRipple(std_::unique_ptr ripple) const { _ripple = std_::move(ripple); return _ripple.get(); } void resetRipple() const { _ripple = nullptr; } private: void fillValueString(); void fillSearchIndex(); QString _name; QString _copyOf; QColor _value; QString _valueString; Text _description = { st::windowMinWidth / 2 }; OrderedSet _searchWords; OrderedSet _searchStartChars; int _top = 0; int _height = 0; mutable std_::unique_ptr _ripple; }; EditorBlock::Row::Row(const QString &name, const QString ©Of, QColor value) : _name(name) , _copyOf(copyOf) { setValue(value); } void EditorBlock::Row::setValue(QColor value) { _value = value; fillValueString(); fillSearchIndex(); } void EditorBlock::Row::fillValueString() { auto addHex = [this](int code) { if (code >= 0 && code < 10) { _valueString.append('0' + code); } else if (code >= 10 && code < 16) { _valueString.append('a' + (code - 10)); } }; auto addCode = [this, addHex](int code) { addHex(code / 16); addHex(code % 16); }; _valueString.resize(0); _valueString.reserve(9); _valueString.append('#'); addCode(_value.red()); addCode(_value.green()); addCode(_value.blue()); if (_value.alpha() != 255) { addCode(_value.alpha()); } } void EditorBlock::Row::fillSearchIndex() { _searchWords.clear(); _searchStartChars.clear(); auto toIndex = _name + ' ' + _copyOf + ' ' + textAccentFold(_description.originalText()) + ' ' + _valueString; auto words = toIndex.toLower().split(SearchSplitter, QString::SkipEmptyParts); for_const (auto &word, words) { _searchWords.insert(word); _searchStartChars.insert(word[0]); } } EditorBlock::EditorBlock(QWidget *parent, Type type, Context *context) : TWidget(parent) , _type(type) , _context(context) , _transparent(style::transparentPlaceholderBrush()) { setMouseTracking(true); subscribe(_context->updated, [this] { if (_mouseSelection) { _lastGlobalPos = QCursor::pos(); updateSelected(mapFromGlobal(_lastGlobalPos)); } update(); }); if (_type == Type::Existing) { subscribe(_context->appended, [this](const Context::AppendData &added) { auto name = added.name; auto value = added.value; feed(name, value); feedDescription(name, added.description); auto row = findRow(name); t_assert(row != nullptr); auto possibleCopyOf = added.possibleCopyOf; auto copyOf = checkCopyOf(findRowIndex(row), possibleCopyOf) ? possibleCopyOf : QString(); removeFromSearch(*row); row->setCopyOf(copyOf); addToSearch(*row); _context->changed.notify({ QStringList(name), value }, true); _context->resized.notify(); _context->pending.notify({ name, copyOf, value }, true); }); } else { subscribe(_context->changed, [this](const Context::ChangeData &data) { checkCopiesChanged(0, data.names, data.value); }); } } void EditorBlock::feed(const QString &name, QColor value, const QString ©OfExisting) { if (findRow(name)) { // Remove the existing row and mark all its copies as unique keys. LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name)); removeRow(name); } addRow(name, copyOfExisting, value); } bool EditorBlock::feedCopy(const QString &name, const QString ©Of) { if (auto row = findRow(copyOf)) { if (findRow(name)) { // Remove the existing row and mark all its copies as unique keys. LOG(("Theme Warning: Color value '%1' appears more than once in the color scheme.").arg(name)); removeRow(name); // row was invalidated by removeRow() call. row = findRow(copyOf); } addRow(name, copyOf, row->value()); } else { 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(copyOf)); } return true; } void EditorBlock::removeRow(const QString &name, bool removeCopyReferences) { auto it = _indices.find(name); t_assert(it != _indices.cend()); auto index = it.value(); for (auto i = index + 1, count = _data.size(); i != count; ++i) { auto &row = _data[i]; removeFromSearch(row); _indices[row.name()] = i - 1; if (removeCopyReferences && row.copyOf() == name) { row.setCopyOf(QString()); } } _data.erase(_data.begin() + index); _indices.erase(it); for (auto i = index, count = _data.size(); i != count; ++i) { addToSearch(_data[i]); } } void EditorBlock::addToSearch(const Row &row) { auto query = _searchQuery; if (!query.isEmpty()) resetSearch(); auto index = findRowIndex(&row); for_const (auto ch, row.searchStartChars()) { _searchIndex[ch].insert(index); } if (!query.isEmpty()) searchByQuery(query); } void EditorBlock::removeFromSearch(const Row &row) { auto query = _searchQuery; if (!query.isEmpty()) resetSearch(); auto index = findRowIndex(&row); for_const (auto ch, row.searchStartChars()) { auto it = _searchIndex.find(ch); if (it != _searchIndex.cend()) { it->remove(index); if (it->isEmpty()) { _searchIndex.erase(it); } } } if (!query.isEmpty()) searchByQuery(query); } void EditorBlock::filterRows(const QString &query) { searchByQuery(query); } void EditorBlock::chooseRow() { if (_selected < 0) { return; } activateRow(rowAtIndex(_selected)); } void EditorBlock::activateRow(const Row &row) { if (_context->box) { if (_type == Type::Existing) { _context->possibleCopyOf = row.name(); _context->box->showColor(row.value()); } } else { _editing = findRowIndex(&row); if (auto box = Ui::show(Box(row.name(), row.value()))) { box->setSaveCallback(base::lambda_guarded(this, [this](QColor value) { saveEditing(value); })); box->setCancelCallback(base::lambda_guarded(this, [this] { cancelEditing(); })); _context->box = box; _context->name = row.name(); _context->updated.notify(); } } } bool EditorBlock::selectSkip(int direction) { _mouseSelection = false; auto maxSelected = (isSearch() ? _searchResults.size() : _data.size()) - 1; auto newSelected = _selected + direction; if (newSelected < -1 || newSelected > maxSelected) { newSelected = maxSelected; } if (auto changed = (newSelected != _selected)) { setSelected(newSelected); scrollToSelected(); return (newSelected >= 0); } return false; } void EditorBlock::scrollToSelected() { if (_selected >= 0) { Context::ScrollData update; update.type = _type; update.position = rowAtIndex(_selected).top(); update.height = rowAtIndex(_selected).height(); _context->scroll.notify(update, true); } } void EditorBlock::searchByQuery(QString query) { auto searchWords = QStringList(); if (!query.isEmpty()) { searchWords = textAccentFold(query.trimmed().toLower()).split(SearchSplitter, QString::SkipEmptyParts); query = searchWords.join(' '); } if (_searchQuery != query) { setSelected(-1); setPressed(-1); _searchQuery = query; _searchResults.clear(); auto toFilter = OrderedSet(); for_const (auto &word, searchWords) { if (word.isEmpty()) continue; auto testToFilter = _searchIndex.value(word[0]); if (testToFilter.isEmpty()) { toFilter.clear(); break; } else if (toFilter.isEmpty() || testToFilter.size() < toFilter.size()) { toFilter = testToFilter; } } if (!toFilter.isEmpty()) { auto allWordsFound = [&searchWords](const Row &row) { for_const (auto &word, searchWords) { if (!row.searchWordsContain(word)) { return false; } } return true; }; for_const (auto index, toFilter) { if (allWordsFound(_data[index])) { _searchResults.push_back(index); } } } _context->resized.notify(true); } } const QColor *EditorBlock::find(const QString &name) { if (auto row = findRow(name)) { return &row->value(); } return nullptr; } bool EditorBlock::feedDescription(const QString &name, const QString &description) { if (auto row = findRow(name)) { removeFromSearch(*row); row->setDescription(description); addToSearch(*row); return true; } return false; } template void EditorBlock::enumerateRows(Callback callback) { if (isSearch()) { for_const (auto index, _searchResults) { if (!callback(_data[index])) { break; } } } else { for_const (auto &row, _data) { if (!callback(row)) { break; } } } } template void EditorBlock::enumerateRows(Callback callback) const { if (isSearch()) { for_const (auto index, _searchResults) { if (!callback(_data[index])) { break; } } } else { for_const (auto &row, _data) { if (!callback(row)) { break; } } } } template void EditorBlock::enumerateRowsFrom(int top, Callback callback) { auto started = false; auto index = 0; enumerateRows([top, callback, &started, &index](Row &row) { if (!started) { if (row.top() + row.height() <= top) { ++index; return true; } started = true; } return callback(index++, row); }); } template void EditorBlock::enumerateRowsFrom(int top, Callback callback) const { auto started = false; enumerateRows([top, callback, &started](const Row &row) { if (!started) { if (row.top() + row.height() <= top) { return true; } started = true; } return callback(row); }); } int EditorBlock::resizeGetHeight(int newWidth) { auto result = 0; auto descriptionWidth = newWidth - st::themeEditorMargin.left() - st::themeEditorMargin.right(); enumerateRows([this, &result, descriptionWidth](Row &row) { row.setTop(result); auto height = row.height(); if (!height) { height = st::themeEditorMargin.top() + st::themeEditorSampleSize.height(); if (!row.descriptionText().isEmpty()) { height += st::themeEditorDescriptionSkip + row.descriptionText().countHeight(descriptionWidth); } height += st::themeEditorMargin.bottom(); row.setHeight(height); } result += row.height(); return true; }); if (_type == Type::New) { setHidden(!result); } if (_type == Type::Existing && !result && !isSearch()) { return st::noContactsHeight; } return result; } void EditorBlock::mousePressEvent(QMouseEvent *e) { updateSelected(e->pos()); setPressed(_selected); } void EditorBlock::mouseReleaseEvent(QMouseEvent *e) { auto pressed = _pressed; setPressed(-1); if (pressed == _selected) { if (_context->box) { chooseRow(); } else if (_selected >= 0) { App::CallDelayed(st::defaultRippleAnimation.hideDuration, this, [this, index = findRowIndex(&rowAtIndex(_selected))] { if (index >= 0 && index < _data.size()) { activateRow(_data[index]); } }); } } } void EditorBlock::saveEditing(QColor value) { if (_editing < 0) { return; } auto &row = _data[_editing]; auto name = row.name(); if (_type == Type::New) { auto removing = base::take(_editing, -1); setSelected(-1); setPressed(-1); auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf; auto color = value; auto description = row.description(); removeRow(name, false); _context->appended.notify({ name, possibleCopyOf, color, description }, true); } else if (_type == Type::Existing) { removeFromSearch(row); auto valueChanged = (row.value() != value); if (valueChanged) { row.setValue(value); } auto possibleCopyOf = _context->possibleCopyOf.isEmpty() ? row.copyOf() : _context->possibleCopyOf; auto copyOf = checkCopyOf(_editing, possibleCopyOf) ? possibleCopyOf : QString(); auto copyOfChanged = (row.copyOf() != copyOf); if (copyOfChanged) { row.setCopyOf(copyOf); } addToSearch(row); if (valueChanged || copyOfChanged) { checkCopiesChanged(_editing + 1, QStringList(name), value); _context->pending.notify({ name, copyOf, value }, true); } } cancelEditing(); } void EditorBlock::checkCopiesChanged(int startIndex, QStringList names, QColor value) { for (auto i = startIndex, count = _data.size(); i != count; ++i) { auto &checkIfIsCopy = _data[i]; if (names.contains(checkIfIsCopy.copyOf())) { removeFromSearch(checkIfIsCopy); checkIfIsCopy.setValue(value); names.push_back(checkIfIsCopy.name()); addToSearch(checkIfIsCopy); } } if (_type == Type::Existing) { _context->changed.notify({ names, value }, true); } } void EditorBlock::cancelEditing() { if (_editing >= 0) { updateRow(_data[_editing]); } _editing = -1; if (auto box = base::take(_context->box)) { box->closeBox(); } _context->possibleCopyOf = QString(); if (!_context->name.isEmpty()) { _context->name = QString(); _context->updated.notify(); } } bool EditorBlock::checkCopyOf(int index, const QString &possibleCopyOf) { auto copyOfIndex = findRowIndex(possibleCopyOf); return (copyOfIndex >= 0 && index > copyOfIndex && _data[copyOfIndex].value().toRgb() == _data[index].value().toRgb()); } void EditorBlock::mouseMoveEvent(QMouseEvent *e) { if (_lastGlobalPos != e->globalPos() || _mouseSelection) { _lastGlobalPos = e->globalPos(); updateSelected(e->pos()); } } void EditorBlock::updateSelected(QPoint localPosition) { _mouseSelection = true; auto top = localPosition.y(); auto underMouseIndex = -1; enumerateRowsFrom(top, [&underMouseIndex, top](int index, const Row &row) { if (row.top() <= top) { underMouseIndex = index; } return false; }); setSelected(underMouseIndex); } void EditorBlock::leaveEventHook(QEvent *e) { _mouseSelection = false; setSelected(-1); } void EditorBlock::paintEvent(QPaintEvent *e) { Painter p(this); auto clip = e->rect(); if (_data.isEmpty()) { p.fillRect(clip, st::dialogsBg); p.setFont(st::noContactsFont); p.setPen(st::noContactsColor); p.drawText(QRect(0, 0, width(), st::noContactsHeight), lang(lng_theme_editor_no_keys)); } auto ms = getms(); auto cliptop = clip.y(); auto clipbottom = cliptop + clip.height(); enumerateRowsFrom(cliptop, [this, &p, clipbottom, ms](int index, const Row &row) { if (row.top() >= clipbottom) { return false; } paintRow(p, index, row, ms); return true; }); } void EditorBlock::paintRow(Painter &p, int index, const Row &row, TimeMs ms) { auto rowTop = row.top() + st::themeEditorMargin.top(); auto rect = QRect(0, row.top(), width(), row.height()); auto selected = (_pressed >= 0) ? (index == _pressed) : (index == _selected); auto active = (findRowIndex(&row) == _editing); p.fillRect(rect, active ? st::dialogsBgActive : selected ? st::dialogsBgOver : st::dialogsBg); if (auto ripple = row.ripple()) { ripple->paint(p, 0, row.top(), width(), ms, &(active ? st::activeButtonBgRipple : st::windowBgRipple)->c); if (ripple->empty()) { row.resetRipple(); } } auto sample = QRect(width() - st::themeEditorMargin.right() - st::themeEditorSampleSize.width(), rowTop, st::themeEditorSampleSize.width(), st::themeEditorSampleSize.height()); Ui::Shadow::paint(p, sample, width(), st::defaultRoundShadow); if (row.value().alpha() != 255) { p.fillRect(myrtlrect(sample), _transparent); } p.fillRect(myrtlrect(sample), row.value()); auto rowWidth = width() - st::themeEditorMargin.left() - st::themeEditorMargin.right(); auto nameWidth = rowWidth - st::themeEditorSampleSize.width() - st::themeEditorDescriptionSkip; p.setFont(st::themeEditorNameFont); p.setPen(active ? st::dialogsNameFgActive : selected ? st::dialogsNameFgOver : st::dialogsNameFg); p.drawTextLeft(st::themeEditorMargin.left(), rowTop, width(), st::themeEditorNameFont->elided(row.name(), nameWidth)); if (!row.copyOf().isEmpty()) { auto copyTop = rowTop + st::themeEditorNameFont->height; p.setFont(st::themeEditorCopyNameFont); p.drawTextLeft(st::themeEditorMargin.left(), copyTop, width(), st::themeEditorCopyNameFont->elided("= " + row.copyOf(), nameWidth)); } if (!row.descriptionText().isEmpty()) { auto descriptionTop = rowTop + st::themeEditorSampleSize.height() + st::themeEditorDescriptionSkip; p.setPen(active ? st::dialogsTextFgActive : selected ? st::dialogsTextFgOver : st::dialogsTextFg); row.descriptionText().drawLeft(p, st::themeEditorMargin.left(), descriptionTop, rowWidth, width()); } if (isEditing() && !active && (_type == Type::New || (_editing >= 0 && findRowIndex(&row) >= _editing))) { p.fillRect(rect, st::layerBg); } } void EditorBlock::setSelected(int selected) { if (isEditing()) { if (_type == Type::New) { selected = -1; } else if (_editing >= 0 && selected >= 0 && findRowIndex(&rowAtIndex(selected)) >= _editing) { selected = -1; } } if (_selected != selected) { if (_selected >= 0) updateRow(rowAtIndex(_selected)); _selected = selected; if (_selected >= 0) updateRow(rowAtIndex(_selected)); setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); } } void EditorBlock::setPressed(int pressed) { if (_pressed != pressed) { if (_pressed >= 0) { updateRow(rowAtIndex(_pressed)); stopLastRipple(_pressed); } _pressed = pressed; if (_pressed >= 0) { addRowRipple(_pressed); updateRow(rowAtIndex(_pressed)); } } } void EditorBlock::addRowRipple(int index) { auto &row = rowAtIndex(index); auto ripple = row.ripple(); if (!ripple) { auto mask = Ui::RippleAnimation::rectMask(QSize(width(), row.height())); ripple = row.setRipple(std_::make_unique(st::defaultRippleAnimation, std_::move(mask), [this, index = findRowIndex(&row)] { updateRow(_data[index]); })); } auto origin = mapFromGlobal(QCursor::pos()) - QPoint(0, row.top()); ripple->add(origin); } void EditorBlock::stopLastRipple(int index) { auto &row = rowAtIndex(index); if (row.ripple()) { row.ripple()->lastStop(); } } void EditorBlock::updateRow(const Row &row) { update(0, row.top(), width(), row.height()); } void EditorBlock::addRow(const QString &name, const QString ©Of, QColor value) { _data.push_back({ name, copyOf, value }); _indices.insert(name, _data.size() - 1); addToSearch(_data.back()); } EditorBlock::Row &EditorBlock::rowAtIndex(int index) { if (isSearch()) { return _data[_searchResults[index]]; } return _data[index]; } int EditorBlock::findRowIndex(const QString &name) const { return _indices.value(name, -1);; } EditorBlock::Row *EditorBlock::findRow(const QString &name) { auto index = findRowIndex(name); return (index >= 0) ? &_data[index] : nullptr; } int EditorBlock::findRowIndex(const Row *row) { return row ? (row - &_data[0]) : -1; } } // namespace Theme } // namespace Window