/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/settings_key_navigation.h" #include "base/invoke_queued.h" #include "settings/settings_common.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" #include "ui/painter.h" #include "ui/rect.h" #include "ui/ui_utility.h" #include "styles/style_settings.h" namespace Settings { namespace { constexpr auto kPageSkip = 5; [[nodiscard]] bool SameBand(const QRect &a, const QRect &b) { const auto delta = std::abs(rect::center(a).y() - rect::center(b).y()); return (delta * 2) < std::min(a.height(), b.height()); } } // namespace KeyNavigation::KeyNavigation(not_null inner) : _inner(inner) { } void KeyNavigation::anchorTo(not_null widget) { const auto raw = widget.get(); const auto entries = list(); for (auto i = 0; i != int(entries.size()); ++i) { if (entries[i].widget.get() == raw) { select(entries, i); return; } } _anchor = raw; } auto KeyNavigation::list() const -> std::vector { auto result = std::vector(); const auto widgets = _inner->findChildren(); for (const auto widget : widgets) { auto fullWidth = true; auto ownBackground = false; const auto button = [&]() -> Ui::AbstractButton* { if (const auto row = dynamic_cast(widget)) { ownBackground = true; return row; } else if (const auto check = dynamic_cast(widget)) { return check; } fullWidth = false; if (const auto link = dynamic_cast(widget)) { return link; } const auto abstract = dynamic_cast(widget); return (abstract && abstract->isListItem()) ? abstract : nullptr; }(); const auto slider = button ? nullptr : dynamic_cast(widget); if (slider) { fullWidth = true; } const auto raw = button ? static_cast(button) : static_cast(slider); if (!raw || !raw->isVisibleTo(_inner) || (button ? button->isDisabled() : slider->isDisabled())) { continue; } const auto geometry = QRect( raw->mapTo(_inner, QPoint()), raw->size()); if (!geometry.isEmpty()) { result.push_back({ raw, button, slider, geometry, fullWidth, ownBackground, }); } } ranges::sort(result, ranges::less(), [](const Entry &entry) { return std::pair(entry.geometry.y(), entry.geometry.x()); }); return result; } bool KeyNavigation::handle(not_null e) { const auto key = e->key(); const auto modifiers = e->modifiers() & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier); const auto navigationKey = (key == Qt::Key_Up) || (key == Qt::Key_Down) || (key == Qt::Key_PageUp) || (key == Qt::Key_PageDown); const auto horizontalKey = (key == Qt::Key_Left) || (key == Qt::Key_Right); const auto submitKey = (key == Qt::Key_Return) || (key == Qt::Key_Enter) || (key == Qt::Key_Space); if ((modifiers != Qt::NoModifier) || (!navigationKey && !horizontalKey && !submitKey)) { return false; } const auto entries = list(); if (entries.empty()) { return false; } const auto selectedWidget = _selected.get(); auto selected = -1; auto hovered = -1; for (auto i = 0; i != int(entries.size()); ++i) { if (entries[i].widget == selectedWidget) { selected = i; } else if ((hovered < 0) && entries[i].widget->underMouse()) { hovered = i; } } if (submitKey) { if (selected < 0) { return false; } else if (const auto button = entries[selected].button) { if (!e->isAutoRepeat()) { activate(button, e->modifiers()); } } return true; } const auto count = int(entries.size()); const auto anchorIndex = [&] { const auto anchor = _anchor.get(); if (!anchor || !anchor->isVisibleTo(_inner)) { return -1; } const auto center = rect::center(QRect( anchor->mapTo(_inner, QPoint()), anchor->size())).y(); for (auto i = 0; i != count; ++i) { if (rect::center(entries[i].geometry).y() >= center) { return i - 1; } } return count - 1; }; const auto start = (selected >= 0) ? selected : (hovered >= 0) ? hovered : anchorIndex(); if (horizontalKey) { if (start < 0) { return false; } const auto ¤t = entries[start]; if (current.slider) { QCoreApplication::sendEvent(current.slider, e); return true; } const auto next = start + ((key == Qt::Key_Right) ? 1 : -1); if (next >= 0 && next < count && SameBand(current.geometry, entries[next].geometry)) { select(entries, next); } return true; } const auto visible = _inner->visibleRegion().boundingRect(); const auto firstVisible = [&] { for (auto i = 0; i != count; ++i) { if (rect::bottom(entries[i].geometry) > visible.y()) { return i; } } return 0; }; const auto lastVisible = [&] { for (auto i = count - 1; i >= 0; --i) { if (entries[i].geometry.y() < rect::bottom(visible)) { return i; } } return count - 1; }; const auto bandStart = [&](int index) { while (index > 0 && SameBand( entries[index - 1].geometry, entries[index].geometry)) { --index; } return index; }; if (key == Qt::Key_Down) { if (start < 0) { select(entries, firstVisible()); } else { auto next = start + 1; while (next < count && SameBand( entries[next].geometry, entries[start].geometry)) { ++next; } select(entries, (next < count) ? next : 0); } } else if (key == Qt::Key_Up) { if (start < 0) { select(entries, bandStart(lastVisible())); } else { auto prev = start - 1; while (prev >= 0 && SameBand( entries[prev].geometry, entries[start].geometry)) { --prev; } select(entries, bandStart((prev >= 0) ? prev : (count - 1))); } } else if ((key == Qt::Key_PageDown) || (key == Qt::Key_PageUp)) { if (start < 0) { return false; } const auto delta = (key == Qt::Key_PageDown) ? kPageSkip : -kPageSkip; select(entries, start + delta); } return true; } void KeyNavigation::activate( not_null button, Qt::KeyboardModifiers modifiers) { if (const auto checkbox = dynamic_cast(button.get())) { const auto radio = dynamic_cast(button.get()); if (!radio) { checkbox->setChecked(!checkbox->checked()); } else if (!checkbox->checked()) { checkbox->setChecked(true); } } button->clicked(modifiers, Qt::LeftButton); } void KeyNavigation::select(const std::vector &entries, int index) { if (entries.empty()) { return; } index = std::clamp(index, 0, int(entries.size()) - 1); const auto widget = entries[index].widget; _anchor = nullptr; if (_selected.get() == widget.get()) { return; } for (const auto &entry : entries) { track(entry.widget); } const auto button = entries[index].button; _selectedGeometry = entries[index].geometry; _selectedLifetime.destroy(); widget->events( ) | rpl::filter([](not_null e) { const auto type = e->type(); return (type == QEvent::Hide) || (type == QEvent::HideToParent); }) | rpl::on_next([=] { InvokeQueued(_inner, [=] { reselectFromHidden(); }); }, _selectedLifetime); const auto apply = [&] { for (const auto &entry : entries) { const auto other = entry.button; if (other && other != button && other->isOver()) { other->setSynteticOver(false); } } _selected = widget; if (button) { button->setSynteticOver(true); } updateHighlight(); }; apply(); RevealWidget(widget, st::settingsNavigationMargin); apply(); } void KeyNavigation::reselectFromHidden() { const auto widget = _selected.get(); if (!widget) { return; } else if (widget->isVisibleTo(_inner)) { updateHighlight(); return; } const auto center = rect::center(_selectedGeometry).y(); clearSelection(); const auto entries = list(); if (entries.empty()) { return; } auto index = int(entries.size()) - 1; for (auto i = 0; i != int(entries.size()); ++i) { if (rect::center(entries[i].geometry).y() >= center) { index = i; break; } } select(entries, index); } void KeyNavigation::updateHighlight() { const auto selected = _selected.get(); if (!selected) { if (_highlight) { _highlight->hide(); } return; } if (!_highlight) { _highlight = Ui::CreateChild(_inner.get()); const auto raw = _highlight; raw->setAttribute(Qt::WA_TransparentForMouseEvents); raw->paintRequest( ) | rpl::on_next([=](QRect clip) { auto p = QPainter(raw); if (_highlightRounded) { auto hq = PainterHighQualityEnabler(p); const auto radius = st::roundRadiusLarge; p.setPen(Qt::NoPen); p.setBrush(st::windowBgOver); p.drawRoundedRect(raw->rect(), radius, radius); } else { p.fillRect(clip, st::windowBgOver); } }, raw->lifetime()); raw->lower(); _inner->sizeValue( ) | rpl::on_next([=] { updateHighlight(); }, _lifetime); } const auto entries = list(); const auto i = ranges::find(entries, selected, [](const Entry &entry) { return entry.widget.get(); }); if (i == end(entries) || i->ownBackground) { _highlight->hide(); return; } const auto &geometry = i->geometry; const auto banded = ranges::any_of(entries, [&](const Entry &entry) { return (entry.widget.get() != selected) && SameBand(entry.geometry, geometry); }); const auto skip = st::settingsNavigationBandSkip; _highlightRounded = banded || !i->fullWidth; _highlight->setGeometry(_highlightRounded ? geometry.marginsAdded(Margins(skip)) : QRect(0, geometry.y(), _inner->width(), geometry.height())); _highlight->update(); _highlight->show(); } void KeyNavigation::clearSelection() { if (const auto widget = _selected.get()) { if (const auto button = dynamic_cast(widget)) { button->setSynteticOver(false); } } _selected = nullptr; _selectedLifetime.destroy(); if (_highlight) { _highlight->hide(); } } void KeyNavigation::track(not_null widget) { if (!_tracked.emplace(widget).second) { return; } widget->events( ) | rpl::filter([](not_null e) { return (e->type() == QEvent::Enter); }) | rpl::on_next([=] { if (_selected.get()) { clearSelection(); } }, _lifetime); } } // namespace Settings