init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
452
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp
Normal file
452
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.cpp
Normal file
@@ -0,0 +1,452 @@
|
||||
/*
|
||||
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 "ui/widgets/chat_filters_tabs_slider.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/widgets/side_bar_button.h"
|
||||
#include "styles/style_dialogs.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ChatsFiltersTabs::ChatsFiltersTabs(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::SettingsSlider &st)
|
||||
: Ui::SettingsSlider(parent, st)
|
||||
, _st(st)
|
||||
, _unreadSt([&] {
|
||||
auto st = Ui::UnreadBadgeStyle();
|
||||
st.align = style::al_left;
|
||||
return st;
|
||||
}())
|
||||
, _unreadMaxString(u"99+"_q)
|
||||
, _unreadSkip(st::lineWidth * 5) {
|
||||
Expects(_st.barSnapToLabel && _st.strictSkip);
|
||||
if (_st.barRadius > 0) {
|
||||
_bar.emplace(_st.barRadius, _st.barFg);
|
||||
_barActive.emplace(_st.barRadius, _st.barFgActive);
|
||||
}
|
||||
{
|
||||
const auto one = Ui::CountUnreadBadgeSize(u"9"_q, _unreadSt, 1);
|
||||
_cachedBadgeWidths = {
|
||||
one.width(),
|
||||
Ui::CountUnreadBadgeSize(u"99"_q, _unreadSt, 2).width(),
|
||||
Ui::CountUnreadBadgeSize(u"999"_q, _unreadSt, 2).width(),
|
||||
};
|
||||
_cachedBadgeHeight = one.height();
|
||||
}
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
for (auto &[index, unread] : _unreadCounts) {
|
||||
unread.cache = cacheUnreadCount(unread.count, unread.muted);
|
||||
}
|
||||
update();
|
||||
}, lifetime());
|
||||
Ui::DiscreteSlider::setSelectOnPress(false);
|
||||
}
|
||||
|
||||
bool ChatsFiltersTabs::setSectionsAndCheckChanged(
|
||||
std::vector<TextWithEntities> &§ions,
|
||||
const Text::MarkedContext &context,
|
||||
Fn<bool()> paused) {
|
||||
const auto &was = sectionsRef();
|
||||
const auto changed = [&] {
|
||||
if (was.size() != sections.size()) {
|
||||
return true;
|
||||
}
|
||||
for (auto i = 0; i < sections.size(); i++) {
|
||||
if (was[i].label.toTextWithEntities() != sections[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
if (changed) {
|
||||
Ui::DiscreteSlider::setSections(std::move(sections), context);
|
||||
}
|
||||
_emojiPaused = std::move(paused);
|
||||
return changed;
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::fitWidthToSections() {
|
||||
SettingsSlider::fitWidthToSections();
|
||||
|
||||
_lockedFromX = calculateLockedFromX();
|
||||
|
||||
{
|
||||
_sections.clear();
|
||||
enumerateSections([&](Section §ion) {
|
||||
_sections.push_back({ not_null{ §ion }, 0, false });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::setUnreadCount(int index, int unreadCount, bool mute) {
|
||||
const auto it = _unreadCounts.find(index);
|
||||
if (it == _unreadCounts.end()) {
|
||||
if (unreadCount) {
|
||||
_unreadCounts.emplace(index, Unread{
|
||||
.cache = cacheUnreadCount(unreadCount, mute),
|
||||
.count = ushort(std::clamp(
|
||||
unreadCount,
|
||||
0,
|
||||
int(std::numeric_limits<ushort>::max()))),
|
||||
.muted = mute,
|
||||
});
|
||||
update();
|
||||
}
|
||||
} else if (!unreadCount) {
|
||||
_unreadCounts.erase(it);
|
||||
update();
|
||||
} else if (it->second.count != unreadCount || it->second.muted != mute) {
|
||||
it->second.count = unreadCount;
|
||||
it->second.muted = mute;
|
||||
it->second.cache = cacheUnreadCount(unreadCount, mute);
|
||||
update();
|
||||
}
|
||||
if (unreadCount) {
|
||||
const auto widthIndex = (unreadCount < 10)
|
||||
? 0
|
||||
: (unreadCount < 100)
|
||||
? 1
|
||||
: 2;
|
||||
setAdditionalContentWidthToSection(
|
||||
index,
|
||||
_cachedBadgeWidths[widthIndex] + _unreadSkip);
|
||||
} else {
|
||||
setAdditionalContentWidthToSection(index, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int ChatsFiltersTabs::calculateLockedFromX() const {
|
||||
if (!_lockedFrom) {
|
||||
return std::numeric_limits<int>::max();
|
||||
}
|
||||
auto left = 0;
|
||||
auto index = 0;
|
||||
enumerateSections([&](const Section §ion) {
|
||||
const auto currentRight = section.left + section.width;
|
||||
if (index == _lockedFrom) {
|
||||
return false;
|
||||
}
|
||||
left = currentRight;
|
||||
index++;
|
||||
return true;
|
||||
});
|
||||
return left ? left : std::numeric_limits<int>::max();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::setLockedFrom(int index) {
|
||||
_lockedFrom = index;
|
||||
_lockedFromX = calculateLockedFromX();
|
||||
if (!index) {
|
||||
_paletteLifetime.destroy();
|
||||
return;
|
||||
}
|
||||
_paletteLifetime = style::PaletteChanged(
|
||||
) | rpl::on_next([this] {
|
||||
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
|
||||
});
|
||||
}
|
||||
|
||||
QImage ChatsFiltersTabs::cacheUnreadCount(int count, bool muted) const {
|
||||
const auto widthIndex = (count < 10) ? 0 : (count < 100) ? 1 : 2;
|
||||
auto image = QImage(
|
||||
QSize(_cachedBadgeWidths[widthIndex], _cachedBadgeHeight)
|
||||
* style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
const auto string = (count > 999)
|
||||
? _unreadMaxString
|
||||
: QString::number(count);
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
if (muted) {
|
||||
auto copy = _unreadSt;
|
||||
copy.muted = muted;
|
||||
Ui::PaintUnreadBadge(p, string, 0, 0, copy, 0);
|
||||
} else {
|
||||
Ui::PaintUnreadBadge(p, string, 0, 0, _unreadSt, 0);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto clip = e->rect();
|
||||
const auto range = getCurrentActiveRange();
|
||||
const auto activeIndex = activeSection();
|
||||
const auto now = crl::now();
|
||||
|
||||
auto index = 0;
|
||||
auto raisedIndex = -1;
|
||||
auto activeHorizontalShift = 0;
|
||||
const auto drawSection = [&](Section §ion) {
|
||||
// const auto activeWidth = _st.barSnapToLabel
|
||||
// ? section.contentWidth
|
||||
// : section.width;
|
||||
|
||||
const auto horizontalShift = _sections[index].horizontalShift;
|
||||
const auto shiftedLeft = section.left + horizontalShift;
|
||||
if (_sections[index].raise) {
|
||||
raisedIndex = index;
|
||||
}
|
||||
if (index == activeIndex) {
|
||||
activeHorizontalShift = horizontalShift;
|
||||
}
|
||||
|
||||
// const auto activeLeft = shiftedLeft
|
||||
// + (section.width - activeWidth) / 2;
|
||||
// const auto active = 1.
|
||||
// - std::clamp(
|
||||
// std::abs(range.left - activeLeft) / float64(range.width),
|
||||
// 0.,
|
||||
// 1.);
|
||||
const auto active = (index == activeIndex) ? 1. : 0.;
|
||||
if (section.ripple) {
|
||||
const auto color = anim::color(
|
||||
_st.rippleBg,
|
||||
_st.rippleBgActive,
|
||||
active);
|
||||
section.ripple->paint(p, shiftedLeft, 0, width(), &color);
|
||||
if (section.ripple->empty()) {
|
||||
section.ripple.reset();
|
||||
}
|
||||
}
|
||||
const auto labelLeft = shiftedLeft
|
||||
+ (section.width - section.contentWidth) / 2;
|
||||
const auto rect = myrtlrect(
|
||||
labelLeft,
|
||||
_st.labelTop,
|
||||
section.contentWidth,
|
||||
_st.labelStyle.font->height);
|
||||
if (rect.intersects(clip)) {
|
||||
const auto locked = (_lockedFrom && (index >= _lockedFrom));
|
||||
if (locked) {
|
||||
constexpr auto kPremiumLockedOpacity = 0.6;
|
||||
p.setOpacity(kPremiumLockedOpacity);
|
||||
}
|
||||
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
|
||||
section.label.draw(p, {
|
||||
.position = QPoint(labelLeft, _st.labelTop),
|
||||
.outerWidth = width(),
|
||||
.availableWidth = section.label.maxWidth(),
|
||||
.now = now,
|
||||
.pausedEmoji = _emojiPaused && _emojiPaused(),
|
||||
});
|
||||
{
|
||||
const auto it = _unreadCounts.find(index);
|
||||
if (it != _unreadCounts.end()) {
|
||||
p.drawImage(
|
||||
labelLeft
|
||||
+ _unreadSkip
|
||||
+ section.label.maxWidth(),
|
||||
_st.labelTop,
|
||||
it->second.cache);
|
||||
}
|
||||
}
|
||||
if (locked) {
|
||||
if (!_lockCache) {
|
||||
_lockCache.emplace(Ui::SideBarLockIcon(_st.labelFg));
|
||||
}
|
||||
const auto size = _lockCache->size()
|
||||
/ style::DevicePixelRatio();
|
||||
p.drawImage(
|
||||
labelLeft + (section.label.maxWidth() - size.width()) / 2,
|
||||
height() - size.height() - st::lineWidth,
|
||||
*_lockCache);
|
||||
p.setOpacity(1.0);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
return true;
|
||||
};
|
||||
enumerateSections(drawSection);
|
||||
if (raisedIndex >= 0) {
|
||||
index = raisedIndex;
|
||||
drawSection(*_sections[raisedIndex].section);
|
||||
}
|
||||
if (_st.barSnapToLabel) {
|
||||
const auto drawRect = [&](QRect rect, bool active) {
|
||||
const auto &bar = active ? _barActive : _bar;
|
||||
if (bar) {
|
||||
bar->paint(p, rect);
|
||||
} else {
|
||||
p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
|
||||
}
|
||||
};
|
||||
const auto add = _st.barStroke / 2;
|
||||
const auto from = std::max(range.left - add, 0);
|
||||
const auto till = std::min(range.left + range.width + add, width());
|
||||
if (from < till) {
|
||||
drawRect(
|
||||
myrtlrect(
|
||||
from,
|
||||
_st.barTop,
|
||||
till - from,
|
||||
_st.barStroke).translated(activeHorizontalShift, 0),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::mousePressEvent(QMouseEvent *e) {
|
||||
const auto mouseButton = e->button();
|
||||
if (mouseButton == Qt::MouseButton::LeftButton) {
|
||||
_lockedPressed = (e->pos().x() >= _lockedFromX);
|
||||
if (_lockedPressed) {
|
||||
Ui::RpWidget::mousePressEvent(e);
|
||||
} else {
|
||||
Ui::SettingsSlider::mousePressEvent(e);
|
||||
}
|
||||
} else {
|
||||
Ui::RpWidget::mousePressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_reordering) {
|
||||
Ui::RpWidget::mouseMoveEvent(e);
|
||||
} else {
|
||||
Ui::SettingsSlider::mouseMoveEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto mouseButton = e->button();
|
||||
if (mouseButton == Qt::MouseButton::LeftButton) {
|
||||
if (base::take(_lockedPressed)) {
|
||||
_lockedPressed = false;
|
||||
_lockedClicked.fire({});
|
||||
} else {
|
||||
if (_reordering) {
|
||||
for (const auto §ion : _sections) {
|
||||
if (section.section->ripple) {
|
||||
section.section->ripple->lastStop();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ui::SettingsSlider::mouseReleaseEvent(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ui::RpWidget::mouseReleaseEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::contextMenuEvent(QContextMenuEvent *e) {
|
||||
const auto pos = e->pos();
|
||||
if (pos.x() >= _lockedFromX) {
|
||||
return;
|
||||
}
|
||||
auto left = 0;
|
||||
auto index = 0;
|
||||
enumerateSections([&](const Section §ion) {
|
||||
const auto currentRight = section.left + section.width;
|
||||
if (pos.x() > left && pos.x() < currentRight) {
|
||||
return false;
|
||||
}
|
||||
left = currentRight;
|
||||
index++;
|
||||
return true;
|
||||
});
|
||||
_contextMenuRequested.fire_copy(index);
|
||||
}
|
||||
|
||||
rpl::producer<int> ChatsFiltersTabs::contextMenuRequested() const {
|
||||
return _contextMenuRequested.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ChatsFiltersTabs::lockedClicked() const {
|
||||
return _lockedClicked.events();
|
||||
}
|
||||
|
||||
int ChatsFiltersTabs::count() const {
|
||||
return _sections.size();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::setHorizontalShift(int index, int shift) {
|
||||
Expects(index >= 0 && index < _sections.size());
|
||||
|
||||
auto §ion = _sections[index];
|
||||
if (shift - section.horizontalShift) {
|
||||
section.horizontalShift = shift;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::setRaised(int index) {
|
||||
_sections[index].raise = true;
|
||||
update();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::reorderSections(int oldIndex, int newIndex) {
|
||||
Expects(oldIndex >= 0 && oldIndex < _sections.size());
|
||||
Expects(newIndex >= 0 && newIndex < _sections.size());
|
||||
// Expects(!_inResize);
|
||||
auto lefts = std::vector<int>();
|
||||
enumerateSections([&](Section §ion) {
|
||||
lefts.emplace_back(section.left);
|
||||
return true;
|
||||
});
|
||||
const auto wasActive = activeSection();
|
||||
|
||||
{
|
||||
auto unreadCounts = base::flat_map<Index, Unread>();
|
||||
for (auto &[index, unread] : _unreadCounts) {
|
||||
unreadCounts.emplace(
|
||||
base::reorder_index(index, oldIndex, newIndex),
|
||||
std::move(unread));
|
||||
}
|
||||
_unreadCounts = std::move(unreadCounts);
|
||||
}
|
||||
|
||||
base::reorder(sectionsRef(), oldIndex, newIndex);
|
||||
Ui::DiscreteSlider::setActiveSectionFast(
|
||||
base::reorder_index(wasActive, oldIndex, newIndex));
|
||||
Ui::DiscreteSlider::stopAnimation();
|
||||
|
||||
{
|
||||
_sections.clear();
|
||||
auto left = 0;
|
||||
enumerateSections([&](Section §ion) {
|
||||
_sections.push_back({ not_null{ §ion }, 0, false });
|
||||
section.left = left;
|
||||
left += section.width;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
not_null<Ui::DiscreteSlider::Section*> ChatsFiltersTabs::widgetAt(
|
||||
int index) const {
|
||||
Expects(index >= 0 && index < count());
|
||||
|
||||
return _sections[index].section;
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::setReordering(int value) {
|
||||
_reordering = value;
|
||||
}
|
||||
|
||||
int ChatsFiltersTabs::reordering() const {
|
||||
return _reordering;
|
||||
}
|
||||
|
||||
void ChatsFiltersTabs::stopAnimation() {
|
||||
Ui::DiscreteSlider::stopAnimation();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
101
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h
Normal file
101
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_slider.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/unread_badge_paint.h"
|
||||
#include "ui/widgets/discrete_sliders.h"
|
||||
|
||||
namespace style {
|
||||
struct SettingsSlider;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class SettingsSlider;
|
||||
|
||||
class ChatsFiltersTabsReorder;
|
||||
|
||||
class ChatsFiltersTabs final : public Ui::SettingsSlider {
|
||||
public:
|
||||
ChatsFiltersTabs(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::SettingsSlider &st);
|
||||
|
||||
bool setSectionsAndCheckChanged(
|
||||
std::vector<TextWithEntities> &§ions,
|
||||
const Text::MarkedContext &context,
|
||||
Fn<bool()> paused);
|
||||
|
||||
void fitWidthToSections() override;
|
||||
void setUnreadCount(int index, int unreadCount, bool muted);
|
||||
void setLockedFrom(int index);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> contextMenuRequested() const;
|
||||
[[nodiscard]] rpl::producer<> lockedClicked() const;
|
||||
|
||||
void setHorizontalShift(int index, int shift);
|
||||
void setRaised(int index);
|
||||
[[nodiscard]] int count() const;
|
||||
void reorderSections(int oldIndex, int newIndex);
|
||||
[[nodiscard]] not_null<DiscreteSlider::Section*> widgetAt(int i) const;
|
||||
void setReordering(int value);
|
||||
[[nodiscard]] int reordering() const;
|
||||
|
||||
void stopAnimation();
|
||||
|
||||
protected:
|
||||
struct ShiftedSection {
|
||||
not_null<Ui::DiscreteSlider::Section*> section;
|
||||
int horizontalShift = 0;
|
||||
bool raise = false;
|
||||
};
|
||||
friend class ChatsFiltersTabsReorder;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
|
||||
std::vector<ShiftedSection> _sections;
|
||||
|
||||
private:
|
||||
[[nodiscard]] QImage cacheUnreadCount(int count, bool muted) const;
|
||||
[[nodiscard]] int calculateLockedFromX() const;
|
||||
|
||||
using Index = int;
|
||||
struct Unread final {
|
||||
QImage cache;
|
||||
ushort count = 0;
|
||||
bool muted = false;
|
||||
};
|
||||
base::flat_map<Index, Unread> _unreadCounts;
|
||||
const style::SettingsSlider &_st;
|
||||
const UnreadBadgeStyle _unreadSt;
|
||||
const QString _unreadMaxString;
|
||||
const int _unreadSkip;
|
||||
std::vector<int> _cachedBadgeWidths;
|
||||
int _cachedBadgeHeight = 0;
|
||||
int _lockedFrom = 0;
|
||||
int _lockedFromX = 0;
|
||||
bool _lockedPressed = false;
|
||||
std::optional<Ui::RoundRect> _bar;
|
||||
std::optional<Ui::RoundRect> _barActive;
|
||||
std::optional<QImage> _lockCache;
|
||||
Fn<bool()> _emojiPaused;
|
||||
|
||||
int _reordering = 0;
|
||||
|
||||
rpl::lifetime _paletteLifetime;
|
||||
rpl::event_stream<int> _contextMenuRequested;
|
||||
rpl::event_stream<> _lockedClicked;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
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 "ui/widgets/chat_filters_tabs_slider_reorder.h"
|
||||
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "styles/style_basic.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QtGui/QtEvents>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kScrollFactor = 0.05;
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
|
||||
not_null<ChatsFiltersTabs*> layout,
|
||||
not_null<ScrollArea*> scroll)
|
||||
: _layout(layout)
|
||||
, _scroll(scroll)
|
||||
, _scrollAnimation([this] { updateScrollCallback(); }) {
|
||||
}
|
||||
|
||||
ChatsFiltersTabsReorder::ChatsFiltersTabsReorder(
|
||||
not_null<ChatsFiltersTabs*> layout)
|
||||
: _layout(layout) {
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::cancel() {
|
||||
if (_currentWidget) {
|
||||
cancelCurrent(indexOf(_currentWidget));
|
||||
}
|
||||
_lifetime.destroy();
|
||||
for (auto i = 0, count = _layout->count(); i != count; ++i) {
|
||||
_layout->setHorizontalShift(i, 0);
|
||||
}
|
||||
_entries.clear();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::start() {
|
||||
const auto count = _layout->count();
|
||||
if (count < 2) {
|
||||
return;
|
||||
}
|
||||
_layout->events()
|
||||
| rpl::on_next_done([this](not_null<QEvent*> e) {
|
||||
switch (e->type()) {
|
||||
case QEvent::MouseMove:
|
||||
mouseMove(static_cast<QMouseEvent*>(e.get())->globalPos());
|
||||
break;
|
||||
case QEvent::MouseButtonPress: {
|
||||
const auto m = static_cast<QMouseEvent*>(e.get());
|
||||
mousePress(m->button(), m->pos(), m->globalPos());
|
||||
break;
|
||||
}
|
||||
case QEvent::MouseButtonRelease:
|
||||
mouseRelease(static_cast<QMouseEvent*>(e.get())->button());
|
||||
break;
|
||||
}
|
||||
}, [this] {
|
||||
cancel();
|
||||
}, _lifetime);
|
||||
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto widget = _layout->widgetAt(i);
|
||||
_entries.push_back({ widget });
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::addPinnedInterval(int from, int length) {
|
||||
_pinnedIntervals.push_back({ from, length });
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::clearPinnedIntervals() {
|
||||
_pinnedIntervals.clear();
|
||||
}
|
||||
|
||||
bool ChatsFiltersTabsReorder::Interval::isIn(int index) const {
|
||||
return (index >= from) && (index < (from + length));
|
||||
}
|
||||
|
||||
bool ChatsFiltersTabsReorder::isIndexPinned(int index) const {
|
||||
return ranges::any_of(_pinnedIntervals, [&](const Interval &i) {
|
||||
return i.isIn(index);
|
||||
});
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::checkForStart(QPoint position) {
|
||||
const auto shift = position.x() - _currentStart;
|
||||
const auto delta = QApplication::startDragDistance();
|
||||
if (std::abs(shift) <= delta) {
|
||||
return;
|
||||
}
|
||||
_currentState = State::Started;
|
||||
_currentStart += (shift > 0) ? delta : -delta;
|
||||
|
||||
const auto index = indexOf(_currentWidget);
|
||||
_layout->setRaised(index);
|
||||
_currentDesiredIndex = index;
|
||||
_updates.fire({ _currentWidget, index, index, _currentState });
|
||||
|
||||
updateOrder(index, position);
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::updateOrder(int index, QPoint position) {
|
||||
if (isIndexPinned(index)) {
|
||||
return;
|
||||
}
|
||||
const auto shift = position.x() - _currentStart;
|
||||
auto ¤t = _entries[index];
|
||||
current.shiftAnimation.stop();
|
||||
current.shift = current.finalShift = shift;
|
||||
_layout->setHorizontalShift(index, shift);
|
||||
|
||||
checkForScrollAnimation();
|
||||
|
||||
const auto count = _entries.size();
|
||||
const auto currentWidth = current.widget->width;
|
||||
const auto currentMiddle = current.widget->left
|
||||
+ shift
|
||||
+ currentWidth / 2;
|
||||
_currentDesiredIndex = index;
|
||||
if (shift > 0) {
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
if (isIndexPinned(next)) {
|
||||
return;
|
||||
}
|
||||
const auto &e = _entries[next];
|
||||
if (currentMiddle < e.widget->left + e.widget->width / 2) {
|
||||
moveToShift(next, 0);
|
||||
} else {
|
||||
_currentDesiredIndex = next;
|
||||
moveToShift(next, -currentWidth);
|
||||
}
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
moveToShift(prev, 0);
|
||||
}
|
||||
} else {
|
||||
for (auto next = index + 1; next != count; ++next) {
|
||||
moveToShift(next, 0);
|
||||
}
|
||||
for (auto prev = index - 1; prev >= 0; --prev) {
|
||||
if (isIndexPinned(prev)) {
|
||||
return;
|
||||
}
|
||||
const auto &e = _entries[prev];
|
||||
if (currentMiddle >= e.widget->left + e.widget->width / 2) {
|
||||
moveToShift(prev, 0);
|
||||
} else {
|
||||
_currentDesiredIndex = prev;
|
||||
moveToShift(prev, currentWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::mousePress(
|
||||
Qt::MouseButton button,
|
||||
QPoint position,
|
||||
QPoint globalPosition) {
|
||||
if (button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
auto widget = (ChatsFiltersTabs::ShiftedSection*)(nullptr);
|
||||
for (auto i = 0; i != _layout->_sections.size(); ++i) {
|
||||
auto §ion = _layout->_sections[i];
|
||||
if ((position.x() >= section.section->left)
|
||||
&& (position.x() < (section.section->left + section.section->width))) {
|
||||
widget = §ion;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cancelCurrent();
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
_currentWidget = widget->section;
|
||||
_currentShiftedWidget = widget;
|
||||
_currentStart = globalPosition.x();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::mouseMove(QPoint position) {
|
||||
if (!_currentWidget) {
|
||||
// if (_currentWidget != widget) {
|
||||
return;
|
||||
} else if (_currentState != State::Started) {
|
||||
checkForStart(position);
|
||||
} else {
|
||||
updateOrder(indexOf(_currentWidget), position);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::mouseRelease(Qt::MouseButton button) {
|
||||
if (button != Qt::LeftButton) {
|
||||
return;
|
||||
}
|
||||
finishReordering();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::cancelCurrent() {
|
||||
if (_currentWidget) {
|
||||
cancelCurrent(indexOf(_currentWidget));
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::cancelCurrent(int index) {
|
||||
Expects(_currentWidget != nullptr);
|
||||
|
||||
if (_currentState == State::Started) {
|
||||
_currentState = State::Cancelled;
|
||||
_updates.fire({ _currentWidget, index, index, _currentState });
|
||||
}
|
||||
_currentWidget = nullptr;
|
||||
_currentShiftedWidget = nullptr;
|
||||
for (auto i = 0, count = int(_entries.size()); i != count; ++i) {
|
||||
moveToShift(i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::finishReordering() {
|
||||
if (_scroll) {
|
||||
_scrollAnimation.stop();
|
||||
}
|
||||
finishCurrent();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::finishCurrent() {
|
||||
if (!_currentWidget) {
|
||||
return;
|
||||
}
|
||||
const auto index = indexOf(_currentWidget);
|
||||
if (_currentDesiredIndex == index || _currentState != State::Started) {
|
||||
cancelCurrent(index);
|
||||
return;
|
||||
}
|
||||
const auto result = _currentDesiredIndex;
|
||||
const auto widget = _currentWidget;
|
||||
_currentState = State::Cancelled;
|
||||
_currentWidget = nullptr;
|
||||
_currentShiftedWidget = nullptr;
|
||||
|
||||
auto ¤t = _entries[index];
|
||||
const auto width = current.widget->width;
|
||||
if (index < result) {
|
||||
auto sum = 0;
|
||||
for (auto i = index; i != result; ++i) {
|
||||
auto &entry = _entries[i + 1];
|
||||
const auto widget = entry.widget;
|
||||
entry.deltaShift += width;
|
||||
updateShift(widget, i + 1);
|
||||
sum += widget->width;
|
||||
}
|
||||
current.finalShift -= sum;
|
||||
} else if (index > result) {
|
||||
auto sum = 0;
|
||||
for (auto i = result; i != index; ++i) {
|
||||
auto &entry = _entries[i];
|
||||
const auto widget = entry.widget;
|
||||
entry.deltaShift -= width;
|
||||
updateShift(widget, i);
|
||||
sum += widget->width;
|
||||
}
|
||||
current.finalShift += sum;
|
||||
}
|
||||
if (!(current.finalShift + current.deltaShift)) {
|
||||
current.shift = 0;
|
||||
_layout->setHorizontalShift(index, 0);
|
||||
}
|
||||
base::reorder(_entries, index, result);
|
||||
_layout->reorderSections(index, _currentDesiredIndex);
|
||||
for (auto i = 0; i != _layout->sectionsRef().size(); ++i) {
|
||||
_entries[i].widget = &_layout->sectionsRef()[i];
|
||||
moveToShift(i, 0);
|
||||
}
|
||||
|
||||
_updates.fire({ widget, index, result, State::Applied });
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::moveToShift(int index, int shift) {
|
||||
auto &entry = _entries[index];
|
||||
if (entry.finalShift + entry.deltaShift == shift) {
|
||||
return;
|
||||
}
|
||||
const auto widget = entry.widget;
|
||||
entry.shiftAnimation.start(
|
||||
[=, this] { updateShift(widget, index); },
|
||||
entry.finalShift,
|
||||
shift - entry.deltaShift,
|
||||
st::slideWrapDuration);
|
||||
entry.finalShift = shift - entry.deltaShift;
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::updateShift(
|
||||
not_null<Section*> widget,
|
||||
int indexHint) {
|
||||
Expects(indexHint >= 0 && indexHint < _entries.size());
|
||||
|
||||
const auto index = (_entries[indexHint].widget == widget)
|
||||
? indexHint
|
||||
: indexOf(widget);
|
||||
auto &entry = _entries[index];
|
||||
entry.shift = base::SafeRound(
|
||||
entry.shiftAnimation.value(entry.finalShift)
|
||||
) + entry.deltaShift;
|
||||
if (entry.deltaShift && !entry.shiftAnimation.animating()) {
|
||||
entry.finalShift += entry.deltaShift;
|
||||
entry.deltaShift = 0;
|
||||
}
|
||||
_layout->setHorizontalShift(index, entry.shift);
|
||||
}
|
||||
|
||||
int ChatsFiltersTabsReorder::indexOf(not_null<Section*> widget) const {
|
||||
const auto i = ranges::find(_entries, widget, &Entry::widget);
|
||||
Assert(i != end(_entries));
|
||||
return i - begin(_entries);
|
||||
}
|
||||
|
||||
auto ChatsFiltersTabsReorder::updates() const -> rpl::producer<Single> {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::updateScrollCallback() {
|
||||
if (!_scroll) {
|
||||
return;
|
||||
}
|
||||
const auto delta = deltaFromEdge();
|
||||
const auto oldLeft = _scroll->scrollLeft();
|
||||
_scroll->horizontalScrollBar()->setValue(oldLeft + delta);
|
||||
const auto newLeft = _scroll->scrollLeft();
|
||||
|
||||
_currentStart += oldLeft - newLeft;
|
||||
if (newLeft == 0 || newLeft == _scroll->scrollLeftMax()) {
|
||||
_scrollAnimation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void ChatsFiltersTabsReorder::checkForScrollAnimation() {
|
||||
if (!_scroll || !deltaFromEdge() || _scrollAnimation.animating()) {
|
||||
return;
|
||||
}
|
||||
_scrollAnimation.start();
|
||||
}
|
||||
|
||||
int ChatsFiltersTabsReorder::deltaFromEdge() {
|
||||
Expects(_currentWidget != nullptr);
|
||||
Expects(_currentShiftedWidget != nullptr);
|
||||
Expects(_scroll);
|
||||
|
||||
const auto globalPosition = _layout->mapToGlobal(
|
||||
QPoint(
|
||||
_currentWidget->left + _currentShiftedWidget->horizontalShift,
|
||||
0));
|
||||
const auto localLeft = _scroll->mapFromGlobal(globalPosition).x();
|
||||
const auto localRight = localLeft
|
||||
+ _currentWidget->width
|
||||
- _scroll->width();
|
||||
|
||||
const auto isLeftEdge = (localLeft < 0);
|
||||
const auto isRightEdge = (localRight > 0);
|
||||
if (!isLeftEdge && !isRightEdge) {
|
||||
_scrollAnimation.stop();
|
||||
return 0;
|
||||
}
|
||||
return int((isRightEdge ? localRight : localLeft) * kScrollFactor);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/chat_filters_tabs_slider.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ScrollArea;
|
||||
|
||||
class ChatsFiltersTabsReorder final {
|
||||
public:
|
||||
using Section = ChatsFiltersTabs::Section;
|
||||
enum class State : uchar {
|
||||
Started,
|
||||
Applied,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
struct Single {
|
||||
not_null<Section*> widget;
|
||||
int oldPosition = 0;
|
||||
int newPosition = 0;
|
||||
State state = State::Started;
|
||||
};
|
||||
|
||||
ChatsFiltersTabsReorder(
|
||||
not_null<ChatsFiltersTabs*> layout,
|
||||
not_null<ScrollArea*> scroll);
|
||||
ChatsFiltersTabsReorder(not_null<ChatsFiltersTabs*> layout);
|
||||
|
||||
void start();
|
||||
void cancel();
|
||||
void finishReordering();
|
||||
void addPinnedInterval(int from, int length);
|
||||
void clearPinnedIntervals();
|
||||
[[nodiscard]] rpl::producer<Single> updates() const;
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
not_null<Section*> widget;
|
||||
Ui::Animations::Simple shiftAnimation;
|
||||
int shift = 0;
|
||||
int finalShift = 0;
|
||||
int deltaShift = 0;
|
||||
};
|
||||
struct Interval {
|
||||
[[nodiscard]] bool isIn(int index) const;
|
||||
|
||||
int from = 0;
|
||||
int length = 0;
|
||||
};
|
||||
|
||||
void mousePress(Qt::MouseButton button, QPoint position, QPoint global);
|
||||
void mouseMove(QPoint position);
|
||||
void mouseRelease(Qt::MouseButton button);
|
||||
|
||||
void checkForStart(QPoint position);
|
||||
void updateOrder(int index, QPoint position);
|
||||
void cancelCurrent();
|
||||
void finishCurrent();
|
||||
void cancelCurrent(int index);
|
||||
|
||||
[[nodiscard]] int indexOf(not_null<Section*> widget) const;
|
||||
void moveToShift(int index, int shift);
|
||||
void updateShift(not_null<Section*> widget, int indexHint);
|
||||
|
||||
void updateScrollCallback();
|
||||
void checkForScrollAnimation();
|
||||
[[nodiscard]] int deltaFromEdge();
|
||||
|
||||
[[nodiscard]] bool isIndexPinned(int index) const;
|
||||
|
||||
const not_null<ChatsFiltersTabs*> _layout;
|
||||
Ui::ScrollArea *_scroll = nullptr;
|
||||
|
||||
Ui::Animations::Basic _scrollAnimation;
|
||||
|
||||
std::vector<Interval> _pinnedIntervals;
|
||||
|
||||
Section *_currentWidget = nullptr;
|
||||
ChatsFiltersTabs::ShiftedSection *_currentShiftedWidget = nullptr;
|
||||
int _currentStart = 0;
|
||||
int _currentDesiredIndex = 0;
|
||||
State _currentState = State::Cancelled;
|
||||
std::vector<Entry> _entries;
|
||||
rpl::event_stream<Single> _updates;
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
494
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp
Normal file
494
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.cpp
Normal file
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
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 "ui/widgets/chat_filters_tabs_strip.h"
|
||||
|
||||
#include "api/api_chat_filters_remove_manager.h"
|
||||
#include "boxes/filters/edit_filter_box.h"
|
||||
#include "boxes/premium_limits_box.h"
|
||||
#include "core/application.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "data/data_chat_filters.h"
|
||||
#include "data/data_peer_values.h" // Data::AmPremiumValue.
|
||||
#include "data/data_premium_limits.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_unread_value.h"
|
||||
#include "data/data_user.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "settings/settings_folders.h"
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/chat_filters_tabs_slider_reorder.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_dialogs.h" // dialogsSearchTabs
|
||||
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
struct State final {
|
||||
Ui::Animations::Simple animation;
|
||||
std::optional<FilterId> lastFilterId = std::nullopt;
|
||||
rpl::lifetime rebuildLifetime;
|
||||
rpl::lifetime reorderLifetime;
|
||||
base::unique_qptr<Ui::PopupMenu> menu;
|
||||
|
||||
Api::RemoveComplexChatFilter removeApi;
|
||||
bool waitingSuggested = false;
|
||||
|
||||
std::unique_ptr<Ui::ChatsFiltersTabsReorder> reorder;
|
||||
bool ignoreRefresh = false;
|
||||
};
|
||||
|
||||
void ShowMenu(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<State*> state,
|
||||
int index) {
|
||||
const auto session = &controller->session();
|
||||
|
||||
auto id = FilterId(0);
|
||||
{
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
if (index < 0 || index >= list.size()) {
|
||||
return;
|
||||
}
|
||||
id = list[index].id();
|
||||
}
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
const auto addAction = Ui::Menu::CreateAddActionCallback(
|
||||
state->menu.get());
|
||||
|
||||
if (id) {
|
||||
addAction(
|
||||
tr::lng_filters_context_edit(tr::now),
|
||||
[=] { EditExistingFilter(controller, id); },
|
||||
&st::menuIconEdit);
|
||||
|
||||
Window::MenuAddMarkAsReadChatListAction(
|
||||
controller,
|
||||
[=] { return session->data().chatsFilters().chatsList(id); },
|
||||
addAction);
|
||||
|
||||
auto showRemoveBox = [=] {
|
||||
state->removeApi.request(base::make_weak(parent), controller, id);
|
||||
};
|
||||
addAction({
|
||||
.text = tr::lng_filters_context_remove(tr::now),
|
||||
.handler = std::move(showRemoveBox),
|
||||
.icon = &st::menuIconDeleteAttention,
|
||||
.isAttention = true,
|
||||
});
|
||||
} else {
|
||||
auto customUnreadState = [=] {
|
||||
return Data::MainListMapUnreadState(
|
||||
session,
|
||||
session->data().chatsList()->unreadState());
|
||||
};
|
||||
Window::MenuAddMarkAsReadChatListAction(
|
||||
controller,
|
||||
[=] { return session->data().chatsList(); },
|
||||
addAction,
|
||||
std::move(customUnreadState));
|
||||
|
||||
auto openFiltersSettings = [=] {
|
||||
const auto filters = &session->data().chatsFilters();
|
||||
if (filters->suggestedLoaded()) {
|
||||
controller->showSettings(Settings::Folders::Id());
|
||||
} else if (!state->waitingSuggested) {
|
||||
state->waitingSuggested = true;
|
||||
filters->requestSuggested();
|
||||
filters->suggestedUpdated(
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
controller->showSettings(Settings::Folders::Id());
|
||||
}, parent->lifetime());
|
||||
}
|
||||
};
|
||||
addAction(
|
||||
tr::lng_filters_setup_menu(tr::now),
|
||||
std::move(openFiltersSettings),
|
||||
&st::menuIconEdit);
|
||||
}
|
||||
if (state->menu->empty()) {
|
||||
state->menu = nullptr;
|
||||
return;
|
||||
}
|
||||
state->menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void ShowFiltersListMenu(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
not_null<State*> state,
|
||||
int active,
|
||||
Fn<void(int)> changeActive) {
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
|
||||
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||
parent,
|
||||
st::popupMenuWithIcons);
|
||||
|
||||
const auto reorderAll = session->user()->isPremium();
|
||||
const auto maxLimit = (reorderAll ? 1 : 0)
|
||||
+ Data::PremiumLimits(session).dialogFiltersCurrent();
|
||||
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
|
||||
|
||||
for (auto i = 0; i < list.size(); ++i) {
|
||||
const auto title = list[i].title();
|
||||
const auto text = title.text.empty()
|
||||
? tr::lng_filters_all_short(tr::now)
|
||||
: title.text.text;
|
||||
const auto callback = [=] {
|
||||
if (i != active) {
|
||||
changeActive(i);
|
||||
}
|
||||
};
|
||||
const auto icon = (i == active)
|
||||
? &st::mediaPlayerMenuCheck
|
||||
: nullptr;
|
||||
const auto action = Ui::Menu::CreateAction(
|
||||
state->menu.get(),
|
||||
text,
|
||||
callback);
|
||||
auto item = base::make_unique_q<Ui::Menu::Action>(
|
||||
state->menu.get(),
|
||||
state->menu->st().menu,
|
||||
action,
|
||||
icon,
|
||||
icon);
|
||||
action->setEnabled(i < premiumFrom);
|
||||
if (!title.text.empty()) {
|
||||
const auto context = Core::TextContext({
|
||||
.session = session,
|
||||
.repaint = [raw = item.get()] { raw->update(); },
|
||||
.customEmojiLoopLimit = title.isStatic ? -1 : 0,
|
||||
});
|
||||
item->setMarkedText(title.text, QString(), context);
|
||||
}
|
||||
state->menu->addAction(std::move(item));
|
||||
}
|
||||
session->data().chatsFilters().changed() | rpl::on_next([=] {
|
||||
state->menu->hideMenu();
|
||||
}, state->menu->lifetime());
|
||||
|
||||
if (state->menu->empty()) {
|
||||
state->menu = nullptr;
|
||||
return;
|
||||
}
|
||||
state->menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void(FilterId)> choose,
|
||||
ChatHelpers::PauseReason pauseLevel,
|
||||
Window::SessionController *controller,
|
||||
bool trackActiveFilterAndUnreadAndReorder) {
|
||||
|
||||
const auto wrap = Ui::CreateChild<Ui::SlideWrap<Ui::RpWidget>>(
|
||||
parent,
|
||||
object_ptr<Ui::RpWidget>(parent));
|
||||
if (!controller) {
|
||||
const auto window = Core::App().findWindow(parent);
|
||||
controller = window ? window->sessionController() : nullptr;
|
||||
if (!controller) {
|
||||
return wrap;
|
||||
}
|
||||
}
|
||||
const auto container = wrap->entity();
|
||||
const auto scroll = Ui::CreateChild<Ui::ScrollArea>(
|
||||
container,
|
||||
st::dialogsTabsScroll,
|
||||
true);
|
||||
const auto slider = scroll->setOwnedWidget(
|
||||
object_ptr<Ui::ChatsFiltersTabs>(
|
||||
parent,
|
||||
trackActiveFilterAndUnreadAndReorder
|
||||
? st::dialogsSearchTabs
|
||||
: st::chatsFiltersTabs));
|
||||
const auto state = wrap->lifetime().make_state<State>();
|
||||
const auto reassignUnreadValue = [=] {
|
||||
state->reorderLifetime.destroy();
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
auto includeMuted = Data::IncludeMutedCounterFoldersValue();
|
||||
for (auto i = 0; i < list.size(); i++) {
|
||||
rpl::combine(
|
||||
Data::UnreadStateValue(session, list[i].id()),
|
||||
rpl::duplicate(includeMuted)
|
||||
) | rpl::on_next([=](
|
||||
const Dialogs::UnreadState &state,
|
||||
bool includeMuted) {
|
||||
const auto chats = state.chats;
|
||||
const auto chatsMuted = state.chatsMuted;
|
||||
const auto muted = (chatsMuted + state.marksMuted);
|
||||
const auto count = (chats + state.marks)
|
||||
- (includeMuted ? 0 : muted);
|
||||
const auto isMuted = includeMuted && (count == muted);
|
||||
slider->setUnreadCount(i, count, isMuted);
|
||||
slider->fitWidthToSections();
|
||||
}, state->reorderLifetime);
|
||||
}
|
||||
};
|
||||
if (trackActiveFilterAndUnreadAndReorder) {
|
||||
using Reorder = Ui::ChatsFiltersTabsReorder;
|
||||
state->reorder = std::make_unique<Reorder>(slider, scroll);
|
||||
const auto applyReorder = [=](
|
||||
int oldPosition,
|
||||
int newPosition) {
|
||||
if (newPosition == oldPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto filters = &session->data().chatsFilters();
|
||||
const auto &list = filters->list();
|
||||
if (!session->user()->isPremium()) {
|
||||
if (list[0].id() != FilterId()) {
|
||||
filters->moveAllToFront();
|
||||
}
|
||||
}
|
||||
Assert(oldPosition >= 0 && oldPosition < list.size());
|
||||
Assert(newPosition >= 0 && newPosition < list.size());
|
||||
|
||||
auto order = ranges::views::all(
|
||||
list
|
||||
) | ranges::views::transform(
|
||||
&Data::ChatFilter::id
|
||||
) | ranges::to_vector;
|
||||
base::reorder(order, oldPosition, newPosition);
|
||||
|
||||
state->ignoreRefresh = true;
|
||||
filters->saveOrder(order);
|
||||
state->ignoreRefresh = false;
|
||||
};
|
||||
|
||||
state->reorder->updates(
|
||||
) | rpl::on_next([=](const Reorder::Single &data) {
|
||||
if (data.state == Reorder::State::Started) {
|
||||
slider->setReordering(slider->reordering() + 1);
|
||||
} else {
|
||||
Ui::PostponeCall(slider, [=] {
|
||||
slider->setReordering(slider->reordering() - 1);
|
||||
});
|
||||
if (data.state == Reorder::State::Applied) {
|
||||
applyReorder(data.oldPosition, data.newPosition);
|
||||
reassignUnreadValue();
|
||||
}
|
||||
}
|
||||
}, slider->lifetime());
|
||||
}
|
||||
wrap->toggle(false, anim::type::instant);
|
||||
scroll->setCustomWheelProcess([=](not_null<QWheelEvent*> e) {
|
||||
const auto pixelDelta = e->pixelDelta();
|
||||
const auto angleDelta = e->angleDelta();
|
||||
if (std::abs(pixelDelta.x()) + std::abs(angleDelta.x())) {
|
||||
return false;
|
||||
}
|
||||
const auto bar = scroll->horizontalScrollBar();
|
||||
const auto y = pixelDelta.y() ? pixelDelta.y() : angleDelta.y();
|
||||
bar->setValue(bar->value() - y);
|
||||
return true;
|
||||
});
|
||||
|
||||
const auto scrollToIndex = [=](int index, anim::type type) {
|
||||
const auto to = index
|
||||
? (slider->centerOfSection(index) - scroll->width() / 2)
|
||||
: 0;
|
||||
const auto bar = scroll->horizontalScrollBar();
|
||||
state->animation.stop();
|
||||
if (type == anim::type::instant) {
|
||||
bar->setValue(to);
|
||||
} else {
|
||||
state->animation.start(
|
||||
[=](float64 v) { bar->setValue(v); },
|
||||
bar->value(),
|
||||
std::min(to, bar->maximum()),
|
||||
st::defaultTabsSlider.duration);
|
||||
}
|
||||
};
|
||||
|
||||
const auto applyFilter = [=](const Data::ChatFilter &filter) {
|
||||
if (slider->reordering()) {
|
||||
return;
|
||||
}
|
||||
choose(filter.id());
|
||||
};
|
||||
|
||||
const auto filterByIndex = [=](int index) -> const Data::ChatFilter& {
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
Assert(index >= 0 && index < list.size());
|
||||
return list[index];
|
||||
};
|
||||
|
||||
const auto rebuild = [=] {
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
if ((list.size() <= 1 && !slider->width()) || state->ignoreRefresh) {
|
||||
return;
|
||||
}
|
||||
const auto context = Core::TextContext({ .session = session });
|
||||
const auto paused = [=] {
|
||||
return On(PowerSaving::kEmojiChat)
|
||||
|| controller->isGifPausedAtLeastFor(pauseLevel);
|
||||
};
|
||||
const auto sectionsChanged = slider->setSectionsAndCheckChanged(
|
||||
ranges::views::all(
|
||||
list
|
||||
) | ranges::views::transform([](const Data::ChatFilter &filter) {
|
||||
auto title = filter.title();
|
||||
return title.text.empty()
|
||||
? TextWithEntities{ tr::lng_filters_all_short(tr::now) }
|
||||
: title.isStatic
|
||||
? Data::ForceCustomEmojiStatic(title.text)
|
||||
: title.text;
|
||||
}) | ranges::to_vector, context, paused);
|
||||
if (!sectionsChanged) {
|
||||
return;
|
||||
}
|
||||
state->rebuildLifetime.destroy();
|
||||
slider->fitWidthToSections();
|
||||
{
|
||||
const auto reorderAll = session->user()->isPremium();
|
||||
const auto maxLimit = (reorderAll ? 1 : 0)
|
||||
+ Data::PremiumLimits(session).dialogFiltersCurrent();
|
||||
const auto premiumFrom = (reorderAll ? 0 : 1) + maxLimit;
|
||||
slider->setLockedFrom((premiumFrom >= list.size())
|
||||
? 0
|
||||
: premiumFrom);
|
||||
slider->lockedClicked() | rpl::on_next([=] {
|
||||
controller->show(Box(FiltersLimitBox, session, std::nullopt));
|
||||
}, state->rebuildLifetime);
|
||||
if (state->reorder) {
|
||||
state->reorder->cancel();
|
||||
state->reorder->clearPinnedIntervals();
|
||||
if (!reorderAll) {
|
||||
state->reorder->addPinnedInterval(0, 1);
|
||||
}
|
||||
state->reorder->addPinnedInterval(
|
||||
premiumFrom,
|
||||
std::max(1, int(list.size()) - maxLimit));
|
||||
}
|
||||
}
|
||||
if (trackActiveFilterAndUnreadAndReorder) {
|
||||
reassignUnreadValue();
|
||||
}
|
||||
[&] {
|
||||
const auto lookingId = state->lastFilterId.value_or(list[0].id());
|
||||
for (auto i = 0; i < list.size(); i++) {
|
||||
const auto &filter = list[i];
|
||||
if (filter.id() == lookingId) {
|
||||
const auto wasLast = !!state->lastFilterId;
|
||||
state->lastFilterId = filter.id();
|
||||
slider->setActiveSectionFast(i);
|
||||
scrollToIndex(
|
||||
i,
|
||||
wasLast ? anim::type::normal : anim::type::instant);
|
||||
applyFilter(filter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (list.size()) {
|
||||
const auto index = 0;
|
||||
const auto &filter = filterByIndex(index);
|
||||
state->lastFilterId = filter.id();
|
||||
slider->setActiveSectionFast(index);
|
||||
scrollToIndex(index, anim::type::instant);
|
||||
applyFilter(filter);
|
||||
}
|
||||
}();
|
||||
if (trackActiveFilterAndUnreadAndReorder) {
|
||||
controller->activeChatsFilter(
|
||||
) | rpl::on_next([=](FilterId id) {
|
||||
const auto &list = session->data().chatsFilters().list();
|
||||
for (auto i = 0; i < list.size(); ++i) {
|
||||
if (list[i].id() == id) {
|
||||
slider->setActiveSection(i);
|
||||
scrollToIndex(i, anim::type::normal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
state->reorder->finishReordering();
|
||||
}, state->rebuildLifetime);
|
||||
}
|
||||
rpl::single(-1) | rpl::then(
|
||||
slider->sectionActivated()
|
||||
) | rpl::combine_previous(
|
||||
) | rpl::on_next([=](int was, int index) {
|
||||
if (slider->reordering()) {
|
||||
return;
|
||||
}
|
||||
const auto &filter = filterByIndex(index);
|
||||
if (was != index) {
|
||||
state->lastFilterId = filter.id();
|
||||
scrollToIndex(index, anim::type::normal);
|
||||
}
|
||||
applyFilter(filter);
|
||||
}, state->rebuildLifetime);
|
||||
slider->contextMenuRequested() | rpl::on_next([=](int index) {
|
||||
if (trackActiveFilterAndUnreadAndReorder) {
|
||||
ShowMenu(wrap, controller, state, index);
|
||||
} else {
|
||||
ShowFiltersListMenu(
|
||||
wrap,
|
||||
session,
|
||||
state,
|
||||
slider->activeSection(),
|
||||
[=](int i) { slider->setActiveSection(i); });
|
||||
}
|
||||
}, state->rebuildLifetime);
|
||||
wrap->toggle((list.size() > 1), anim::type::instant);
|
||||
|
||||
if (state->reorder) {
|
||||
state->reorder->start();
|
||||
}
|
||||
};
|
||||
rpl::combine(
|
||||
session->data().chatsFilters().changed(),
|
||||
Data::AmPremiumValue(session) | rpl::to_empty
|
||||
) | rpl::on_next(rebuild, wrap->lifetime());
|
||||
rebuild();
|
||||
|
||||
session->data().chatsFilters().isChatlistChanged(
|
||||
) | rpl::on_next([=](FilterId id) {
|
||||
if (!id || !state->lastFilterId || (id != state->lastFilterId)) {
|
||||
return;
|
||||
}
|
||||
for (const auto &filter : session->data().chatsFilters().list()) {
|
||||
if (filter.id() == id) {
|
||||
applyFilter(filter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, wrap->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
parent->widthValue() | rpl::filter(rpl::mappers::_1 > 0),
|
||||
slider->heightValue() | rpl::filter(rpl::mappers::_1 > 0)
|
||||
) | rpl::on_next([=](int w, int h) {
|
||||
scroll->resize(w, h);
|
||||
container->resize(w, h);
|
||||
wrap->resize(w, h);
|
||||
}, wrap->lifetime());
|
||||
|
||||
return wrap;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
36
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h
Normal file
36
Telegram/SourceFiles/ui/widgets/chat_filters_tabs_strip.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace ChatHelpers {
|
||||
enum class PauseReason;
|
||||
} // namespace ChatHelpers
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Ui {
|
||||
|
||||
not_null<Ui::RpWidget*> AddChatFiltersTabsStrip(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<Main::Session*> session,
|
||||
Fn<void(FilterId)> choose,
|
||||
ChatHelpers::PauseReason pauseLevel,
|
||||
Window::SessionController *controller = nullptr,
|
||||
bool trackActiveFilterAndUnreadAndReorder = false);
|
||||
|
||||
} // namespace Ui
|
||||
1282
Telegram/SourceFiles/ui/widgets/color_editor.cpp
Normal file
1282
Telegram/SourceFiles/ui/widgets/color_editor.cpp
Normal file
File diff suppressed because it is too large
Load Diff
106
Telegram/SourceFiles/ui/widgets/color_editor.h
Normal file
106
Telegram/SourceFiles/ui/widgets/color_editor.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
class ColorEditor : public Ui::RpWidget {
|
||||
public:
|
||||
enum class Mode {
|
||||
RGBA,
|
||||
HSL,
|
||||
};
|
||||
ColorEditor(
|
||||
QWidget *parent,
|
||||
Mode mode,
|
||||
QColor current);
|
||||
|
||||
void setLightnessLimits(int min, int max);
|
||||
|
||||
[[nodiscard]] QColor color() const;
|
||||
[[nodiscard]] rpl::producer<QColor> colorValue() const;
|
||||
[[nodiscard]] rpl::producer<> submitRequests() const;
|
||||
|
||||
void showColor(QColor color);
|
||||
void setCurrent(QColor color);
|
||||
|
||||
void setInnerFocus() const;
|
||||
|
||||
protected:
|
||||
void prepare();
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
struct HSB { // HSV or HSL depending on Mode.
|
||||
int hue = 0;
|
||||
int saturation = 0;
|
||||
int brightness = 0;
|
||||
};
|
||||
void fieldSubmitted();
|
||||
|
||||
[[nodiscard]] HSB hsbFromControls() const;
|
||||
void updateFromColor(QColor color);
|
||||
void updateControlsFromColor();
|
||||
void updateControlsFromHSB(HSB hsb);
|
||||
void updateHSBFields();
|
||||
void updateRGBFields();
|
||||
void updateResultField();
|
||||
void updateFromControls();
|
||||
void updateFromHSBFields();
|
||||
void updateFromRGBFields();
|
||||
void updateFromResultField();
|
||||
void setHSB(HSB hsb, int alpha);
|
||||
void setRGB(int red, int green, int blue, int alpha);
|
||||
[[nodiscard]] QColor applyLimits(QColor color) const;
|
||||
|
||||
int percentFromByte(int byte) {
|
||||
return std::clamp(qRound(byte * 100 / 255.), 0, 100);
|
||||
}
|
||||
int percentToByte(int percent) {
|
||||
return std::clamp(qRound(percent * 255 / 100.), 0, 255);
|
||||
}
|
||||
|
||||
class Picker;
|
||||
class Slider;
|
||||
class Field;
|
||||
class ResultField;
|
||||
|
||||
Mode _mode = Mode();
|
||||
|
||||
object_ptr<Picker> _picker;
|
||||
object_ptr<Slider> _hueSlider = { nullptr };
|
||||
object_ptr<Slider> _opacitySlider = { nullptr };
|
||||
object_ptr<Slider> _lightnessSlider = { nullptr };
|
||||
|
||||
object_ptr<Field> _hueField;
|
||||
object_ptr<Field> _saturationField;
|
||||
object_ptr<Field> _brightnessField;
|
||||
object_ptr<Field> _redField;
|
||||
object_ptr<Field> _greenField;
|
||||
object_ptr<Field> _blueField;
|
||||
object_ptr<ResultField> _result;
|
||||
|
||||
QBrush _transparent;
|
||||
QColor _current;
|
||||
QColor _new;
|
||||
|
||||
QRect _currentRect;
|
||||
QRect _newRect;
|
||||
|
||||
int _lightnessMin = 0;
|
||||
int _lightnessMax = 255;
|
||||
|
||||
rpl::event_stream<> _submitRequests;
|
||||
rpl::event_stream<QColor> _newChanges;
|
||||
|
||||
};
|
||||
498
Telegram/SourceFiles/ui/widgets/continuous_sliders.cpp
Normal file
498
Telegram/SourceFiles/ui/widgets/continuous_sliders.cpp
Normal file
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
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 "ui/widgets/continuous_sliders.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "base/timer.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kByWheelFinishedTimeout = 1000;
|
||||
|
||||
} // namespace
|
||||
|
||||
ContinuousSlider::ContinuousSlider(QWidget *parent) : RpWidget(parent) {
|
||||
setCursor(style::cur_pointer);
|
||||
}
|
||||
|
||||
void ContinuousSlider::setDisabled(bool disabled) {
|
||||
if (_disabled != disabled) {
|
||||
_disabled = disabled;
|
||||
setCursor(_disabled ? style::cur_default : style::cur_pointer);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ContinuousSlider::setMoveByWheel(bool move) {
|
||||
if (move != moveByWheel()) {
|
||||
if (move) {
|
||||
_byWheelFinished = std::make_unique<base::Timer>([=] {
|
||||
if (_changeFinishedCallback) {
|
||||
_changeFinishedCallback(getCurrentValue());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
_byWheelFinished = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QRect ContinuousSlider::getSeekRect() const {
|
||||
const auto decrease = getSeekDecreaseSize();
|
||||
return isHorizontal()
|
||||
? QRect(decrease.width() / 2, 0, width() - decrease.width(), height())
|
||||
: QRect(0, decrease.height() / 2, width(), height() - decrease.width());
|
||||
}
|
||||
|
||||
float64 ContinuousSlider::value() const {
|
||||
return getCurrentValue();
|
||||
}
|
||||
|
||||
void ContinuousSlider::setValue(float64 value) {
|
||||
setValue(value, -1);
|
||||
}
|
||||
|
||||
void ContinuousSlider::setValue(float64 value, float64 receivedTill) {
|
||||
if (_value != value || _receivedTill != receivedTill) {
|
||||
_value = value;
|
||||
_receivedTill = receivedTill;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ContinuousSlider::setFadeOpacity(float64 opacity) {
|
||||
_fadeOpacity = opacity;
|
||||
update();
|
||||
}
|
||||
|
||||
void ContinuousSlider::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_mouseDown) {
|
||||
updateDownValueFromPos(e->pos());
|
||||
}
|
||||
}
|
||||
|
||||
float64 ContinuousSlider::computeValue(const QPoint &pos) const {
|
||||
const auto seekRect = myrtlrect(getSeekRect());
|
||||
const auto result = isHorizontal() ?
|
||||
(pos.x() - seekRect.x()) / float64(seekRect.width()) :
|
||||
(1. - (pos.y() - seekRect.y()) / float64(seekRect.height()));
|
||||
const auto snapped = std::clamp(result, 0., 1.);
|
||||
return _adjustCallback ? _adjustCallback(snapped) : snapped;
|
||||
}
|
||||
|
||||
void ContinuousSlider::mousePressEvent(QMouseEvent *e) {
|
||||
_mouseDown = true;
|
||||
_downValue = computeValue(e->pos());
|
||||
update();
|
||||
if (_changeProgressCallback) {
|
||||
_changeProgressCallback(_downValue);
|
||||
}
|
||||
}
|
||||
|
||||
void ContinuousSlider::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (_mouseDown) {
|
||||
_mouseDown = false;
|
||||
if (_changeFinishedCallback) {
|
||||
_changeFinishedCallback(_downValue);
|
||||
}
|
||||
_value = _downValue;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void ContinuousSlider::wheelEvent(QWheelEvent *e) {
|
||||
if (_mouseDown || !moveByWheel()) {
|
||||
return;
|
||||
}
|
||||
constexpr auto step = static_cast<int>(QWheelEvent::DefaultDeltasPerStep);
|
||||
constexpr auto coef = 1. / (step * 10.);
|
||||
|
||||
auto deltaX = e->angleDelta().x(), deltaY = e->angleDelta().y();
|
||||
if (Platform::IsMac()) {
|
||||
deltaY *= -1;
|
||||
} else {
|
||||
deltaX *= -1;
|
||||
}
|
||||
auto delta = (qAbs(deltaX) > qAbs(deltaY)) ? deltaX : deltaY;
|
||||
auto finalValue = std::clamp(_value + delta * coef, 0., 1.);
|
||||
setValue(finalValue);
|
||||
if (_changeProgressCallback) {
|
||||
_changeProgressCallback(finalValue);
|
||||
}
|
||||
_byWheelFinished->callOnce(kByWheelFinishedTimeout);
|
||||
}
|
||||
|
||||
void ContinuousSlider::keyPressEvent(QKeyEvent *e) {
|
||||
const auto changeBy = [&](float64 step) {
|
||||
Expects(step != 0.);
|
||||
|
||||
auto steps = 0;
|
||||
while (true) {
|
||||
++steps;
|
||||
auto result = _value + (steps * step);
|
||||
const auto stopping = (result <= 0.) || (result >= 1.);
|
||||
if (_adjustCallback) {
|
||||
result = _adjustCallback(result);
|
||||
}
|
||||
result = std::clamp(result, 0., 1.);
|
||||
if (result != _value || stopping) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto newValue = [&] {
|
||||
constexpr auto kSmallStep = 0.01;
|
||||
constexpr auto kLargeStep = 0.10;
|
||||
switch (e->key()) {
|
||||
case Qt::Key_Right:
|
||||
case Qt::Key_Up: return changeBy(kSmallStep);
|
||||
case Qt::Key_Left:
|
||||
case Qt::Key_Down: return changeBy(-kSmallStep);
|
||||
case Qt::Key_PageUp: return changeBy(kLargeStep);
|
||||
case Qt::Key_PageDown: return changeBy(-kLargeStep);
|
||||
case Qt::Key_Home: return changeBy(-1.);
|
||||
case Qt::Key_End: return changeBy(1.);
|
||||
default: e->ignore();
|
||||
}
|
||||
return _value;
|
||||
}();
|
||||
|
||||
if (_value == newValue) {
|
||||
return;
|
||||
}
|
||||
setValue(newValue);
|
||||
if (_changeProgressCallback) {
|
||||
_changeProgressCallback(_value);
|
||||
}
|
||||
if (_changeFinishedCallback) {
|
||||
_changeFinishedCallback(_value);
|
||||
}
|
||||
accessibilityValueChanged();
|
||||
}
|
||||
|
||||
void ContinuousSlider::updateDownValueFromPos(const QPoint &pos) {
|
||||
_downValue = computeValue(pos);
|
||||
update();
|
||||
if (_changeProgressCallback) {
|
||||
_changeProgressCallback(_downValue);
|
||||
}
|
||||
}
|
||||
|
||||
void ContinuousSlider::enterEventHook(QEnterEvent *e) {
|
||||
setOver(true);
|
||||
}
|
||||
|
||||
void ContinuousSlider::leaveEventHook(QEvent *e) {
|
||||
setOver(false);
|
||||
}
|
||||
|
||||
void ContinuousSlider::setOver(bool over) {
|
||||
if (_over == over) return;
|
||||
|
||||
_over = over;
|
||||
auto from = _over ? 0. : 1., to = _over ? 1. : 0.;
|
||||
_overAnimation.start([=] { update(); }, from, to, getOverDuration());
|
||||
}
|
||||
|
||||
FilledSlider::FilledSlider(QWidget *parent, const style::FilledSlider &st) : ContinuousSlider(parent)
|
||||
, _st(st) {
|
||||
}
|
||||
|
||||
QSize FilledSlider::getSeekDecreaseSize() const {
|
||||
return QSize(0, 0);
|
||||
}
|
||||
|
||||
float64 FilledSlider::getOverDuration() const {
|
||||
return _st.duration;
|
||||
}
|
||||
|
||||
void FilledSlider::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
const auto masterOpacity = fadeOpacity();
|
||||
const auto disabled = isDisabled();
|
||||
const auto over = getCurrentOverFactor();
|
||||
const auto lineWidth = _st.lineWidth + ((_st.fullWidth - _st.lineWidth) * over);
|
||||
const auto lineWidthRounded = std::floor(lineWidth);
|
||||
const auto lineWidthPartial = lineWidth - lineWidthRounded;
|
||||
const auto seekRect = getSeekRect();
|
||||
const auto value = getCurrentValue();
|
||||
const auto from = seekRect.x();
|
||||
const auto mid = qRound(from + value * seekRect.width());
|
||||
const auto end = from + seekRect.width();
|
||||
if (mid > from) {
|
||||
p.setOpacity(masterOpacity);
|
||||
p.fillRect(from, height() - lineWidthRounded, (mid - from), lineWidthRounded, disabled ? _st.disabledFg : _st.activeFg);
|
||||
if (lineWidthPartial > 0.01) {
|
||||
p.setOpacity(masterOpacity * lineWidthPartial);
|
||||
p.fillRect(from, height() - lineWidthRounded - 1, (mid - from), 1, disabled ? _st.disabledFg : _st.activeFg);
|
||||
}
|
||||
}
|
||||
if (end > mid && over > 0) {
|
||||
p.setOpacity(masterOpacity * over);
|
||||
p.fillRect(mid, height() - lineWidthRounded, (end - mid), lineWidthRounded, _st.inactiveFg);
|
||||
if (lineWidthPartial > 0.01) {
|
||||
p.setOpacity(masterOpacity * over * lineWidthPartial);
|
||||
p.fillRect(mid, height() - lineWidthRounded - 1, (end - mid), 1, _st.inactiveFg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MediaSlider::MediaSlider(QWidget *parent, const style::MediaSlider &st) : ContinuousSlider(parent)
|
||||
, _st(st) {
|
||||
}
|
||||
|
||||
QSize MediaSlider::getSeekDecreaseSize() const {
|
||||
return _alwaysDisplayMarker ? _st.seekSize : QSize();
|
||||
}
|
||||
|
||||
float64 MediaSlider::getOverDuration() const {
|
||||
return _st.duration;
|
||||
}
|
||||
|
||||
void MediaSlider::disablePaint(bool disabled) {
|
||||
_paintDisabled = disabled;
|
||||
}
|
||||
|
||||
void MediaSlider::addDivider(float64 atValue, const QSize &size) {
|
||||
_dividers.push_back(Divider{ atValue, size });
|
||||
}
|
||||
|
||||
void MediaSlider::setColorOverrides(ColorOverrides overrides) {
|
||||
_overrides = std::move(overrides);
|
||||
update();
|
||||
}
|
||||
|
||||
void MediaSlider::paintEvent(QPaintEvent *e) {
|
||||
if (_paintDisabled) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(this);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setOpacity(fadeOpacity());
|
||||
|
||||
const auto horizontal = isHorizontal();
|
||||
const auto borderWidth = _st.borderWidth;
|
||||
const auto borderHalf = borderWidth / 2;
|
||||
const auto radius = _st.width / 2;
|
||||
const auto disabled = isDisabled();
|
||||
const auto over = getCurrentOverFactor();
|
||||
const auto seekRect = getSeekRect();
|
||||
|
||||
// invert colors and value for vertical
|
||||
const auto value = horizontal
|
||||
? getCurrentValue()
|
||||
: (1. - getCurrentValue());
|
||||
|
||||
// receivedTill is not supported for vertical
|
||||
const auto receivedTill = horizontal
|
||||
? getCurrentReceivedTill()
|
||||
: value;
|
||||
|
||||
const auto markerFrom = (horizontal ? seekRect.x() : seekRect.y());
|
||||
const auto markerLength = horizontal
|
||||
? seekRect.width()
|
||||
: seekRect.height();
|
||||
const auto from = 0;
|
||||
const auto length = (horizontal ? width() : height());
|
||||
const auto alwaysSeekSize = horizontal
|
||||
? _st.seekSize.width()
|
||||
: _st.seekSize.height();
|
||||
const auto mid = _alwaysDisplayMarker
|
||||
? qRound(from
|
||||
+ (alwaysSeekSize / 2.)
|
||||
+ value * (length - alwaysSeekSize))
|
||||
: qRound(from + value * length);
|
||||
const auto till = horizontal
|
||||
? std::max(mid, qRound(from + receivedTill * length))
|
||||
: mid;
|
||||
const auto end = from + length;
|
||||
const auto activeFg = disabled
|
||||
? _st.activeFgDisabled
|
||||
: _overrides.activeFg
|
||||
? QBrush(*_overrides.activeFg)
|
||||
: anim::brush(_st.activeFg, _st.activeFgOver, over);
|
||||
const auto receivedTillFg = _st.receivedTillFg;
|
||||
const auto inactiveFg = disabled
|
||||
? _st.inactiveFgDisabled
|
||||
: _overrides.inactiveFg
|
||||
? QBrush(*_overrides.inactiveFg)
|
||||
: anim::brush(_st.inactiveFg, _st.inactiveFgOver, over);
|
||||
const auto borderFg = _st.borderFg;
|
||||
if (mid > from) {
|
||||
const auto fromClipRect = horizontal
|
||||
? QRect(0, 0, mid, height())
|
||||
: QRect(0, 0, width(), mid);
|
||||
const auto till = std::min(mid + radius, end);
|
||||
const auto fromRect = horizontal
|
||||
? QRect(
|
||||
from + borderHalf,
|
||||
(height() - _st.width) / 2 + borderHalf,
|
||||
till - from - borderWidth,
|
||||
_st.width - borderWidth)
|
||||
: QRect(
|
||||
(width() - _st.width) / 2 + borderHalf,
|
||||
from + borderHalf,
|
||||
_st.width - borderWidth,
|
||||
till - from - borderWidth);
|
||||
p.setClipRect(fromClipRect);
|
||||
if (borderWidth > 0) {
|
||||
const auto borderPen = _overrides.activeBorder
|
||||
? QPen(*_overrides.activeBorder, borderWidth)
|
||||
: QPen(borderFg, borderWidth);
|
||||
const auto bgBrush = _overrides.activeBg
|
||||
? QBrush(*_overrides.activeBg)
|
||||
: (horizontal ? borderFg : inactiveFg);
|
||||
p.setPen(borderPen);
|
||||
p.setBrush(bgBrush);
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(horizontal ? activeFg : inactiveFg);
|
||||
}
|
||||
p.drawRoundedRect(fromRect, radius, radius);
|
||||
}
|
||||
if (till > mid) {
|
||||
Assert(horizontal);
|
||||
auto clipRect = QRect(mid, 0, till - mid, height());
|
||||
const auto left = std::max(mid - radius, from);
|
||||
const auto right = std::min(till + radius, end);
|
||||
const auto rect = QRect(
|
||||
left,
|
||||
(height() - _st.width) / 2,
|
||||
right - left,
|
||||
_st.width);
|
||||
p.setClipRect(clipRect);
|
||||
p.setBrush(receivedTillFg);
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
}
|
||||
if (end > till) {
|
||||
const auto endClipRect = horizontal
|
||||
? QRect(till, 0, width() - till, height())
|
||||
: QRect(0, till, width(), height() - till);
|
||||
const auto begin = std::max(till - radius, from);
|
||||
const auto endRect = horizontal
|
||||
? QRect(
|
||||
begin + borderHalf,
|
||||
(height() - _st.width) / 2 + borderHalf,
|
||||
end - begin - borderWidth,
|
||||
_st.width - borderWidth)
|
||||
: QRect(
|
||||
(width() - _st.width) / 2 + borderHalf,
|
||||
begin + borderHalf,
|
||||
_st.width - borderWidth,
|
||||
end - begin - borderWidth);
|
||||
p.setClipRect(endClipRect);
|
||||
if (borderWidth > 0) {
|
||||
const auto endBorderPen = _overrides.inactiveBorder
|
||||
? QPen(*_overrides.inactiveBorder, borderWidth)
|
||||
: QPen(borderFg, borderWidth);
|
||||
p.setPen(endBorderPen);
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
}
|
||||
p.setBrush(horizontal ? inactiveFg : activeFg);
|
||||
p.drawRoundedRect(endRect, radius, radius);
|
||||
}
|
||||
if (!_dividers.empty()) {
|
||||
p.setClipRect(rect());
|
||||
for (const auto ÷r : _dividers) {
|
||||
const auto dividerValue = horizontal
|
||||
? divider.atValue
|
||||
: (1. - divider.atValue);
|
||||
const auto dividerMid = base::SafeRound(from
|
||||
+ dividerValue * length);
|
||||
const auto &size = divider.size;
|
||||
const auto rect = horizontal
|
||||
? QRect(
|
||||
dividerMid - size.width() / 2,
|
||||
(height() - size.height()) / 2,
|
||||
size.width(),
|
||||
size.height())
|
||||
: QRect(
|
||||
(width() - size.height()) / 2,
|
||||
dividerMid - size.width() / 2,
|
||||
size.height(),
|
||||
size.width());
|
||||
p.setBrush(((value < dividerValue) == horizontal)
|
||||
? inactiveFg
|
||||
: activeFg);
|
||||
const auto dividerRadius = size.width() / 2.;
|
||||
p.drawRoundedRect(rect, dividerRadius, dividerRadius);
|
||||
}
|
||||
}
|
||||
const auto markerSizeRatio = disabled
|
||||
? 0.
|
||||
: (_alwaysDisplayMarker ? 1. : over);
|
||||
if (markerSizeRatio > 0) {
|
||||
const auto position = qRound(markerFrom + value * markerLength)
|
||||
- (horizontal
|
||||
? (_st.seekSize.width() / 2)
|
||||
: (_st.seekSize.height() / 2));
|
||||
const auto seekButton = horizontal
|
||||
? QRect(
|
||||
position,
|
||||
(height() - _st.seekSize.height()) / 2,
|
||||
_st.seekSize.width(),
|
||||
_st.seekSize.height())
|
||||
: QRect(
|
||||
(width() - _st.seekSize.width()) / 2,
|
||||
position,
|
||||
_st.seekSize.width(),
|
||||
_st.seekSize.height());
|
||||
const auto size = horizontal
|
||||
? _st.seekSize.width()
|
||||
: _st.seekSize.height();
|
||||
const auto remove = static_cast<int>(
|
||||
((1. - markerSizeRatio) * size) / 2.);
|
||||
if (remove * 2 < size) {
|
||||
p.setClipRect(rect());
|
||||
const auto seekFg = _overrides.seekFg
|
||||
? QBrush(*_overrides.seekFg)
|
||||
: activeFg;
|
||||
if (borderWidth > 0) {
|
||||
const auto seekBorderPen = _overrides.seekBorder
|
||||
? QPen(*_overrides.seekBorder, borderWidth)
|
||||
: QPen(borderFg, borderWidth);
|
||||
p.setPen(seekBorderPen);
|
||||
p.setBrush(seekFg);
|
||||
} else {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(seekFg);
|
||||
}
|
||||
const auto xshift = horizontal
|
||||
? std::max(
|
||||
seekButton.x() + seekButton.width() - remove - width(),
|
||||
0) + std::min(seekButton.x() + remove, 0)
|
||||
: 0;
|
||||
const auto yshift = horizontal
|
||||
? 0
|
||||
: std::max(
|
||||
seekButton.y() + seekButton.height() - remove - height(),
|
||||
0) + std::min(seekButton.y() + remove, 0);
|
||||
auto ellipseRect = (seekButton - Margins(remove)).translated(
|
||||
-xshift,
|
||||
-yshift);
|
||||
if (borderWidth > 0) {
|
||||
ellipseRect -= Margins(borderHalf);
|
||||
}
|
||||
p.drawEllipse(ellipseRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
274
Telegram/SourceFiles/ui/widgets/continuous_sliders.h
Normal file
274
Telegram/SourceFiles/ui/widgets/continuous_sliders.h
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace base {
|
||||
class Timer;
|
||||
} // namespace base
|
||||
|
||||
namespace style {
|
||||
struct FilledSlider;
|
||||
struct MediaSlider;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ContinuousSlider : public RpWidget {
|
||||
public:
|
||||
ContinuousSlider(QWidget *parent);
|
||||
|
||||
enum class Direction {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
};
|
||||
void setDirection(Direction direction) {
|
||||
_direction = direction;
|
||||
update();
|
||||
}
|
||||
|
||||
float64 value() const;
|
||||
void setValue(float64 value);
|
||||
void setValue(float64 value, float64 receivedTill);
|
||||
void setFadeOpacity(float64 opacity);
|
||||
void setDisabled(bool disabled);
|
||||
bool isDisabled() const {
|
||||
return _disabled;
|
||||
}
|
||||
|
||||
void setAdjustCallback(Fn<float64(float64)> callback) {
|
||||
_adjustCallback = std::move(callback);
|
||||
}
|
||||
void setChangeProgressCallback(Fn<void(float64)> callback) {
|
||||
_changeProgressCallback = std::move(callback);
|
||||
}
|
||||
void setChangeFinishedCallback(Fn<void(float64)> callback) {
|
||||
_changeFinishedCallback = std::move(callback);
|
||||
}
|
||||
bool isChanging() const {
|
||||
return _mouseDown;
|
||||
}
|
||||
|
||||
void setMoveByWheel(bool move);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::Slider;
|
||||
}
|
||||
|
||||
QString accessibilityValue() const override {
|
||||
const auto percent = std::clamp(qRound(_value * 100.), 0, 100);
|
||||
return QString::number(percent) + '%';
|
||||
}
|
||||
|
||||
protected:
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
float64 fadeOpacity() const {
|
||||
return _fadeOpacity;
|
||||
}
|
||||
float64 getCurrentValue() const {
|
||||
return _mouseDown ? _downValue : _value;
|
||||
}
|
||||
float64 getCurrentReceivedTill() const {
|
||||
return _receivedTill;
|
||||
}
|
||||
float64 getCurrentOverFactor() {
|
||||
return _disabled ? 0. : _overAnimation.value(_over ? 1. : 0.);
|
||||
}
|
||||
Direction getDirection() const {
|
||||
return _direction;
|
||||
}
|
||||
bool isHorizontal() const {
|
||||
return (_direction == Direction::Horizontal);
|
||||
}
|
||||
QRect getSeekRect() const;
|
||||
virtual QSize getSeekDecreaseSize() const = 0;
|
||||
|
||||
private:
|
||||
virtual float64 getOverDuration() const = 0;
|
||||
|
||||
bool moveByWheel() const {
|
||||
return _byWheelFinished != nullptr;
|
||||
}
|
||||
|
||||
void setOver(bool over);
|
||||
float64 computeValue(const QPoint &pos) const;
|
||||
void updateDownValueFromPos(const QPoint &pos);
|
||||
|
||||
Direction _direction = Direction::Horizontal;
|
||||
bool _disabled = false;
|
||||
|
||||
std::unique_ptr<base::Timer> _byWheelFinished;
|
||||
|
||||
Fn<float64(float64)> _adjustCallback;
|
||||
Fn<void(float64)> _changeProgressCallback;
|
||||
Fn<void(float64)> _changeFinishedCallback;
|
||||
|
||||
bool _over = false;
|
||||
Ui::Animations::Simple _overAnimation;
|
||||
|
||||
float64 _value = 0.;
|
||||
float64 _receivedTill = 0.;
|
||||
|
||||
bool _mouseDown = false;
|
||||
float64 _downValue = 0.;
|
||||
|
||||
float64 _fadeOpacity = 1.;
|
||||
|
||||
};
|
||||
|
||||
class FilledSlider : public ContinuousSlider {
|
||||
public:
|
||||
FilledSlider(QWidget *parent, const style::FilledSlider &st);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
QSize getSeekDecreaseSize() const override;
|
||||
float64 getOverDuration() const override;
|
||||
|
||||
const style::FilledSlider &_st;
|
||||
|
||||
};
|
||||
|
||||
class MediaSlider : public ContinuousSlider {
|
||||
public:
|
||||
struct ColorOverrides {
|
||||
std::optional<QColor> activeFg;
|
||||
std::optional<QColor> activeBg;
|
||||
std::optional<QColor> activeBorder;
|
||||
std::optional<QColor> seekFg;
|
||||
std::optional<QColor> seekBorder;
|
||||
std::optional<QColor> inactiveFg;
|
||||
std::optional<QColor> inactiveBorder;
|
||||
};
|
||||
|
||||
MediaSlider(QWidget *parent, const style::MediaSlider &st);
|
||||
|
||||
void setAlwaysDisplayMarker(bool alwaysDisplayMarker) {
|
||||
_alwaysDisplayMarker = alwaysDisplayMarker;
|
||||
update();
|
||||
}
|
||||
void disablePaint(bool disabled);
|
||||
|
||||
template <
|
||||
typename Value,
|
||||
typename Convert,
|
||||
typename Progress,
|
||||
typename = std::enable_if_t<
|
||||
rpl::details::is_callable_plain_v<Progress, Value>
|
||||
&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>
|
||||
void setPseudoDiscrete(
|
||||
int valuesCount,
|
||||
Convert &&convert,
|
||||
Value current,
|
||||
Progress &&progress,
|
||||
int indexMin = 0) {
|
||||
Expects(valuesCount > 1);
|
||||
|
||||
setAlwaysDisplayMarker(true);
|
||||
setDirection(Ui::ContinuousSlider::Direction::Horizontal);
|
||||
|
||||
const auto sectionsCount = (valuesCount - 1);
|
||||
setValue(1.);
|
||||
for (auto index = index_type(); index != valuesCount; ++index) {
|
||||
if (current <= convert(index)) {
|
||||
setValue(index / float64(sectionsCount));
|
||||
break;
|
||||
}
|
||||
}
|
||||
setAdjustCallback([=](float64 value) {
|
||||
return std::max(
|
||||
base::SafeRound(value * sectionsCount),
|
||||
indexMin * 1.
|
||||
) / sectionsCount;
|
||||
});
|
||||
setChangeProgressCallback([=](float64 value) {
|
||||
const auto index = std::max(
|
||||
int(base::SafeRound(value * sectionsCount)),
|
||||
indexMin);
|
||||
progress(convert(index));
|
||||
});
|
||||
}
|
||||
|
||||
template <
|
||||
typename Value,
|
||||
typename Convert,
|
||||
typename Progress,
|
||||
typename Finished,
|
||||
typename = std::enable_if_t<
|
||||
rpl::details::is_callable_plain_v<Progress, Value>
|
||||
&& rpl::details::is_callable_plain_v<Finished, Value>
|
||||
&& std::is_same_v<Value, decltype(std::declval<Convert>()(1))>>>
|
||||
void setPseudoDiscrete(
|
||||
int valuesCount,
|
||||
Convert &&convert,
|
||||
Value current,
|
||||
Progress &&progress,
|
||||
Finished &&finished,
|
||||
int indexMin = 0) {
|
||||
setPseudoDiscrete(
|
||||
valuesCount,
|
||||
std::forward<Convert>(convert),
|
||||
current,
|
||||
std::forward<Progress>(progress),
|
||||
indexMin);
|
||||
setChangeFinishedCallback([=](float64 value) {
|
||||
const auto sectionsCount = (valuesCount - 1);
|
||||
const auto index = std::max(
|
||||
int(base::SafeRound(value * sectionsCount)),
|
||||
indexMin);
|
||||
finished(convert(index));
|
||||
});
|
||||
}
|
||||
|
||||
void setColorOverrides(ColorOverrides overrides);
|
||||
void addDivider(float64 atValue, const QSize &size);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
struct Divider {
|
||||
const float64 atValue;
|
||||
const QSize size;
|
||||
};
|
||||
|
||||
QSize getSeekDecreaseSize() const override;
|
||||
float64 getOverDuration() const override;
|
||||
|
||||
const style::MediaSlider &_st;
|
||||
bool _alwaysDisplayMarker = false;
|
||||
bool _paintDisabled = false;
|
||||
|
||||
std::vector<Divider> _dividers;
|
||||
ColorOverrides _overrides;
|
||||
|
||||
};
|
||||
|
||||
class MediaSliderWheelless : public MediaSlider {
|
||||
public:
|
||||
using Ui::MediaSlider::MediaSlider;
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *e) override {
|
||||
e->ignore();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
507
Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp
Normal file
507
Telegram/SourceFiles/ui/widgets/discrete_sliders.cpp
Normal file
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
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 "ui/widgets/discrete_sliders.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
DiscreteSlider::DiscreteSlider(QWidget *parent, bool snapToLabel)
|
||||
: RpWidget(parent)
|
||||
, _snapToLabel(snapToLabel) {
|
||||
setCursor(style::cur_pointer);
|
||||
}
|
||||
|
||||
DiscreteSlider::~DiscreteSlider() = default;
|
||||
|
||||
void DiscreteSlider::setActiveSection(int index) {
|
||||
_activeIndex = index;
|
||||
activateCallback();
|
||||
setSelectedSection(index);
|
||||
}
|
||||
|
||||
void DiscreteSlider::activateCallback() {
|
||||
if (_timerId >= 0) {
|
||||
killTimer(_timerId);
|
||||
_timerId = -1;
|
||||
}
|
||||
auto ms = crl::now();
|
||||
if (ms >= _callbackAfterMs) {
|
||||
_sectionActivated.fire_copy(_activeIndex);
|
||||
} else {
|
||||
_timerId = startTimer(_callbackAfterMs - ms, Qt::PreciseTimer);
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::timerEvent(QTimerEvent *e) {
|
||||
activateCallback();
|
||||
}
|
||||
|
||||
void DiscreteSlider::setActiveSectionFast(int index) {
|
||||
setActiveSection(index);
|
||||
finishAnimating();
|
||||
}
|
||||
|
||||
void DiscreteSlider::finishAnimating() {
|
||||
_a_left.stop();
|
||||
_a_width.stop();
|
||||
update();
|
||||
_callbackAfterMs = 0;
|
||||
if (_timerId >= 0) {
|
||||
activateCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::setAdditionalContentWidthToSection(int index, int w) {
|
||||
if (index >= 0 && index < _sections.size()) {
|
||||
auto §ion = _sections[index];
|
||||
section.contentWidth = section.label.maxWidth() + w;
|
||||
}
|
||||
}
|
||||
|
||||
int DiscreteSlider::sectionsCount() const {
|
||||
return int(_sections.size());
|
||||
}
|
||||
|
||||
int DiscreteSlider::lookupSectionLeft(int index) const {
|
||||
Expects(index >= 0 && index < _sections.size());
|
||||
|
||||
return _sections[index].left;
|
||||
}
|
||||
|
||||
void DiscreteSlider::setSelectOnPress(bool selectOnPress) {
|
||||
_selectOnPress = selectOnPress;
|
||||
}
|
||||
|
||||
bool DiscreteSlider::paused() const {
|
||||
return _paused && _paused();
|
||||
}
|
||||
|
||||
std::vector<DiscreteSlider::Section> &DiscreteSlider::sectionsRef() {
|
||||
return _sections;
|
||||
}
|
||||
|
||||
void DiscreteSlider::addSection(const QString &label) {
|
||||
_sections.push_back(Section(label, getLabelStyle()));
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
void DiscreteSlider::addSection(
|
||||
const TextWithEntities &label,
|
||||
Text::MarkedContext context) {
|
||||
context.repaint = [this] { update(); };
|
||||
_sections.push_back(Section(label, getLabelStyle(), context));
|
||||
resizeToWidth(width());
|
||||
}
|
||||
|
||||
void DiscreteSlider::setSections(const std::vector<QString> &labels) {
|
||||
Assert(!labels.empty());
|
||||
|
||||
_sections.clear();
|
||||
for (const auto &label : labels) {
|
||||
_sections.push_back(Section(label, getLabelStyle()));
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void DiscreteSlider::setSections(
|
||||
const std::vector<TextWithEntities> &labels,
|
||||
Text::MarkedContext context,
|
||||
Fn<bool()> paused) {
|
||||
Assert(!labels.empty());
|
||||
|
||||
context.repaint = [this] { update(); };
|
||||
|
||||
_sections.clear();
|
||||
for (const auto &label : labels) {
|
||||
_sections.push_back(Section(label, getLabelStyle(), context));
|
||||
}
|
||||
_paused = std::move(paused);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void DiscreteSlider::refresh() {
|
||||
stopAnimation();
|
||||
if (_activeIndex >= _sections.size()) {
|
||||
_activeIndex = 0;
|
||||
}
|
||||
if (_selected >= _sections.size()) {
|
||||
_selected = 0;
|
||||
}
|
||||
resizeToWidth(width());
|
||||
update();
|
||||
}
|
||||
|
||||
DiscreteSlider::Range DiscreteSlider::getFinalActiveRange() const {
|
||||
const auto raw = (_sections.empty() || _selected < 0)
|
||||
? nullptr
|
||||
: &_sections[_selected];
|
||||
if (!raw) {
|
||||
return { 0, 0 };
|
||||
}
|
||||
const auto width = _snapToLabel
|
||||
? std::min(raw->width, raw->contentWidth)
|
||||
: raw->width;
|
||||
return { raw->left + ((raw->width - width) / 2), width };
|
||||
}
|
||||
|
||||
DiscreteSlider::Range DiscreteSlider::getCurrentActiveRange() const {
|
||||
const auto to = getFinalActiveRange();
|
||||
return {
|
||||
int(base::SafeRound(_a_left.value(to.left))),
|
||||
int(base::SafeRound(_a_width.value(to.width))),
|
||||
};
|
||||
}
|
||||
|
||||
void DiscreteSlider::enumerateSections(Fn<bool(Section&)> callback) {
|
||||
for (auto §ion : _sections) {
|
||||
if (!callback(section)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::enumerateSections(
|
||||
Fn<bool(const Section&)> callback) const {
|
||||
for (const auto §ion : _sections) {
|
||||
if (!callback(section)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::mousePressEvent(QMouseEvent *e) {
|
||||
const auto index = getIndexFromPosition(e->pos());
|
||||
if (_selectOnPress) {
|
||||
setSelectedSection(index);
|
||||
}
|
||||
startRipple(index);
|
||||
_pressed = index;
|
||||
}
|
||||
|
||||
void DiscreteSlider::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_pressed < 0) {
|
||||
return;
|
||||
}
|
||||
if (_selectOnPress) {
|
||||
setSelectedSection(getIndexFromPosition(e->pos()));
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto pressed = std::exchange(_pressed, -1);
|
||||
if (pressed < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto index = getIndexFromPosition(e->pos());
|
||||
if (pressed < _sections.size()) {
|
||||
if (_sections[pressed].ripple) {
|
||||
_sections[pressed].ripple->lastStop();
|
||||
}
|
||||
}
|
||||
if (_selectOnPress || index == pressed) {
|
||||
setActiveSection(index);
|
||||
}
|
||||
}
|
||||
|
||||
void DiscreteSlider::setSelectedSection(int index) {
|
||||
if (index >= int(_sections.size())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_selected != index) {
|
||||
const auto from = getFinalActiveRange();
|
||||
_selected = index;
|
||||
const auto to = getFinalActiveRange();
|
||||
const auto duration = getAnimationDuration();
|
||||
const auto updater = [this] { update(); };
|
||||
_a_left.start(updater, from.left, to.left, duration);
|
||||
_a_width.start(updater, from.width, to.width, duration);
|
||||
_callbackAfterMs = crl::now() + duration;
|
||||
}
|
||||
}
|
||||
|
||||
int DiscreteSlider::getIndexFromPosition(QPoint pos) {
|
||||
const auto count = _sections.size();
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
if (_sections[i].left + _sections[i].width > pos.x()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return count - 1;
|
||||
}
|
||||
|
||||
DiscreteSlider::Section::Section(
|
||||
const QString &label,
|
||||
const style::TextStyle &st)
|
||||
: label(st, label)
|
||||
, contentWidth(Section::label.maxWidth()) {
|
||||
}
|
||||
|
||||
DiscreteSlider::Section::Section(
|
||||
const TextWithEntities &label,
|
||||
const style::TextStyle &st,
|
||||
const Text::MarkedContext &context) {
|
||||
this->label.setMarkedText(st, label, kMarkupTextOptions, context);
|
||||
contentWidth = Section::label.maxWidth();
|
||||
}
|
||||
|
||||
SettingsSlider::SettingsSlider(
|
||||
QWidget *parent,
|
||||
const style::SettingsSlider &st)
|
||||
: DiscreteSlider(parent, st.barSnapToLabel)
|
||||
, _st(st) {
|
||||
if (_st.barRadius > 0) {
|
||||
_bar.emplace(_st.barRadius, _st.barFg);
|
||||
_barActive.emplace(_st.barRadius, _st.barFgActive);
|
||||
}
|
||||
setSelectOnPress(_st.ripple.showDuration == 0);
|
||||
}
|
||||
|
||||
const style::SettingsSlider &SettingsSlider::st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
int SettingsSlider::centerOfSection(int section) const {
|
||||
const auto widths = countSectionsWidths(0);
|
||||
auto result = 0;
|
||||
if (section >= 0 && section < widths.size()) {
|
||||
for (auto i = 0; i < section; i++) {
|
||||
result += widths[i];
|
||||
}
|
||||
result += widths[section] / 2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void SettingsSlider::fitWidthToSections() {
|
||||
const auto widths = countSectionsWidths(0);
|
||||
resizeToWidth(ranges::accumulate(widths, .0) + _st.padding * 2);
|
||||
}
|
||||
|
||||
void SettingsSlider::setRippleTopRoundRadius(int radius) {
|
||||
_rippleTopRoundRadius = radius;
|
||||
}
|
||||
|
||||
const style::TextStyle &SettingsSlider::getLabelStyle() const {
|
||||
return _st.labelStyle;
|
||||
}
|
||||
|
||||
int SettingsSlider::getAnimationDuration() const {
|
||||
return _st.duration;
|
||||
}
|
||||
|
||||
void SettingsSlider::resizeSections(int newWidth) {
|
||||
const auto count = getSectionsCount();
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sectionWidths = countSectionsWidths(newWidth);
|
||||
|
||||
auto skip = 0;
|
||||
auto x = _st.padding * 1.;
|
||||
auto sectionWidth = sectionWidths.begin();
|
||||
enumerateSections([&](Section §ion) {
|
||||
Expects(sectionWidth != sectionWidths.end());
|
||||
|
||||
section.left = std::floor(x) + skip;
|
||||
x += *sectionWidth;
|
||||
section.width = qRound(x) - (section.left - skip);
|
||||
skip += _st.barSkip;
|
||||
++sectionWidth;
|
||||
return true;
|
||||
});
|
||||
stopAnimation();
|
||||
}
|
||||
|
||||
std::vector<float64> SettingsSlider::countSectionsWidths(int newWidth) const {
|
||||
const auto count = getSectionsCount();
|
||||
const auto sectionsWidth = newWidth
|
||||
- 2 * _st.padding
|
||||
- (count - 1) * _st.barSkip;
|
||||
const auto sectionWidth = sectionsWidth / float64(count);
|
||||
|
||||
auto result = std::vector<float64>(count, sectionWidth);
|
||||
auto labelsWidth = 0;
|
||||
auto commonWidth = true;
|
||||
enumerateSections([&](const Section §ion) {
|
||||
labelsWidth += section.contentWidth;
|
||||
if (section.contentWidth >= sectionWidth) {
|
||||
commonWidth = false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// If labelsWidth > sectionsWidth we're screwed anyway.
|
||||
if (_st.strictSkip || (!commonWidth && labelsWidth <= sectionsWidth)) {
|
||||
auto padding = _st.strictSkip
|
||||
? (_st.strictSkip / 2.)
|
||||
: (sectionsWidth - labelsWidth) / (2. * count);
|
||||
auto currentWidth = result.begin();
|
||||
enumerateSections([&](const Section §ion) {
|
||||
Expects(currentWidth != result.end());
|
||||
|
||||
*currentWidth = padding + section.contentWidth + padding;
|
||||
++currentWidth;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int SettingsSlider::resizeGetHeight(int newWidth) {
|
||||
resizeSections(newWidth);
|
||||
return _st.height;
|
||||
}
|
||||
|
||||
void SettingsSlider::startRipple(int sectionIndex) {
|
||||
if (!_st.ripple.showDuration) {
|
||||
return;
|
||||
}
|
||||
auto index = 0;
|
||||
enumerateSections([this, &index, sectionIndex](Section §ion) {
|
||||
if (index++ == sectionIndex) {
|
||||
if (!section.ripple) {
|
||||
auto mask = prepareRippleMask(sectionIndex, section);
|
||||
section.ripple = std::make_unique<RippleAnimation>(
|
||||
_st.ripple,
|
||||
std::move(mask),
|
||||
[this] { update(); });
|
||||
}
|
||||
const auto point = mapFromGlobal(QCursor::pos());
|
||||
section.ripple->add(point - QPoint(section.left, 0));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
QImage SettingsSlider::prepareRippleMask(
|
||||
int sectionIndex,
|
||||
const Section §ion) {
|
||||
const auto size = QSize(section.width, height() - _st.rippleBottomSkip);
|
||||
if (!_rippleTopRoundRadius
|
||||
|| (sectionIndex > 0 && sectionIndex + 1 < getSectionsCount())) {
|
||||
return RippleAnimation::RectMask(size);
|
||||
}
|
||||
return RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {
|
||||
const auto plusRadius = _rippleTopRoundRadius + 1;
|
||||
p.drawRoundedRect(
|
||||
0,
|
||||
0,
|
||||
section.width,
|
||||
height() + plusRadius,
|
||||
_rippleTopRoundRadius,
|
||||
_rippleTopRoundRadius);
|
||||
if (sectionIndex > 0) {
|
||||
p.fillRect(0, 0, plusRadius, plusRadius, p.brush());
|
||||
}
|
||||
if (sectionIndex + 1 < getSectionsCount()) {
|
||||
p.fillRect(
|
||||
section.width - plusRadius,
|
||||
0,
|
||||
plusRadius,
|
||||
plusRadius, p.brush());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsSlider::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto clip = e->rect();
|
||||
const auto range = DiscreteSlider::getCurrentActiveRange();
|
||||
|
||||
const auto drawRect = [&](QRect rect, bool active = false) {
|
||||
const auto &bar = active ? _barActive : _bar;
|
||||
if (bar) {
|
||||
bar->paint(p, rect);
|
||||
} else {
|
||||
p.fillRect(rect, active ? _st.barFgActive : _st.barFg);
|
||||
}
|
||||
};
|
||||
enumerateSections([&](Section §ion) {
|
||||
const auto activeWidth = _st.barSnapToLabel
|
||||
? section.contentWidth
|
||||
: section.width;
|
||||
const auto activeLeft = section.left
|
||||
+ (section.width - activeWidth) / 2;
|
||||
const auto divider = std::max(std::min(activeWidth, range.width), 1);
|
||||
const auto active = 1.
|
||||
- std::clamp(
|
||||
std::abs(range.left - activeLeft) / float64(divider),
|
||||
0.,
|
||||
1.);
|
||||
if (section.ripple) {
|
||||
const auto color = anim::color(
|
||||
_st.rippleBg,
|
||||
_st.rippleBgActive,
|
||||
active);
|
||||
section.ripple->paint(p, section.left, 0, width(), &color);
|
||||
if (section.ripple->empty()) {
|
||||
section.ripple.reset();
|
||||
}
|
||||
}
|
||||
if (!_st.barSnapToLabel) {
|
||||
auto from = activeLeft;
|
||||
auto tofill = activeWidth;
|
||||
if (range.left > from) {
|
||||
const auto fill = std::min(tofill, range.left - from);
|
||||
drawRect(myrtlrect(from, _st.barTop, fill, _st.barStroke));
|
||||
from += fill;
|
||||
tofill -= fill;
|
||||
}
|
||||
if (range.left + activeWidth > from) {
|
||||
const auto fill = std::min(
|
||||
tofill,
|
||||
range.left + activeWidth - from);
|
||||
if (fill) {
|
||||
drawRect(
|
||||
myrtlrect(from, _st.barTop, fill, _st.barStroke),
|
||||
true);
|
||||
from += fill;
|
||||
tofill -= fill;
|
||||
}
|
||||
}
|
||||
if (tofill) {
|
||||
drawRect(myrtlrect(from, _st.barTop, tofill, _st.barStroke));
|
||||
}
|
||||
}
|
||||
const auto labelLeft = section.left
|
||||
+ (section.width - section.contentWidth) / 2;
|
||||
const auto rect = myrtlrect(
|
||||
labelLeft,
|
||||
_st.labelTop,
|
||||
section.contentWidth,
|
||||
_st.labelStyle.font->height);
|
||||
if (rect.intersects(clip)) {
|
||||
p.setPen(anim::pen(_st.labelFg, _st.labelFgActive, active));
|
||||
section.label.draw(p, {
|
||||
.position = QPoint(labelLeft, _st.labelTop),
|
||||
.outerWidth = width(),
|
||||
.availableWidth = section.label.maxWidth(),
|
||||
.paused = paused(),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (_st.barSnapToLabel) {
|
||||
const auto add = _st.barStroke / 2;
|
||||
const auto from = std::max(range.left - add, 0);
|
||||
const auto till = std::min(range.left + range.width + add, width());
|
||||
if (from < till) {
|
||||
drawRect(
|
||||
myrtlrect(from, _st.barTop, till - from, _st.barStroke),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
172
Telegram/SourceFiles/ui/widgets/discrete_sliders.h
Normal file
172
Telegram/SourceFiles/ui/widgets/discrete_sliders.h
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace style {
|
||||
struct TextStyle;
|
||||
struct SettingsSlider;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::SettingsSlider &defaultSettingsSlider;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RippleAnimation;
|
||||
|
||||
class DiscreteSlider : public RpWidget {
|
||||
public:
|
||||
DiscreteSlider(QWidget *parent, bool snapToLabel);
|
||||
~DiscreteSlider();
|
||||
|
||||
void addSection(const QString &label);
|
||||
void addSection(
|
||||
const TextWithEntities &label,
|
||||
Text::MarkedContext context = {});
|
||||
void setSections(const std::vector<QString> &labels);
|
||||
void setSections(
|
||||
const std::vector<TextWithEntities> &labels,
|
||||
Text::MarkedContext context = {},
|
||||
Fn<bool()> paused = nullptr);
|
||||
int activeSection() const {
|
||||
return _activeIndex;
|
||||
}
|
||||
void setActiveSection(int index);
|
||||
void setActiveSectionFast(int index);
|
||||
void finishAnimating();
|
||||
|
||||
void setAdditionalContentWidthToSection(int index, int width);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> sectionActivated() const {
|
||||
return _sectionActivated.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] int sectionsCount() const;
|
||||
[[nodiscard]] int lookupSectionLeft(int index) const;
|
||||
|
||||
protected:
|
||||
void timerEvent(QTimerEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
int resizeGetHeight(int newWidth) override = 0;
|
||||
|
||||
struct Section {
|
||||
Section(const QString &label, const style::TextStyle &st);
|
||||
Section(
|
||||
const TextWithEntities &label,
|
||||
const style::TextStyle &st,
|
||||
const Text::MarkedContext &context);
|
||||
|
||||
Text::String label;
|
||||
std::unique_ptr<RippleAnimation> ripple;
|
||||
int left = 0;
|
||||
int width = 0;
|
||||
int contentWidth = 0;
|
||||
};
|
||||
struct Range {
|
||||
int left = 0;
|
||||
int width = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] Range getFinalActiveRange() const;
|
||||
[[nodiscard]] Range getCurrentActiveRange() const;
|
||||
|
||||
[[nodiscard]] int getSectionsCount() const {
|
||||
return _sections.size();
|
||||
}
|
||||
|
||||
void enumerateSections(Fn<bool(Section&)> callback);
|
||||
void enumerateSections(Fn<bool(const Section&)> callback) const;
|
||||
|
||||
virtual void startRipple(int sectionIndex) {
|
||||
}
|
||||
|
||||
void stopAnimation() {
|
||||
_a_left.stop();
|
||||
_a_width.stop();
|
||||
}
|
||||
void refresh();
|
||||
|
||||
void setSelectOnPress(bool selectOnPress);
|
||||
|
||||
[[nodiscard]] std::vector<Section> §ionsRef();
|
||||
|
||||
[[nodiscard]] bool paused() const;
|
||||
|
||||
private:
|
||||
void activateCallback();
|
||||
virtual const style::TextStyle &getLabelStyle() const = 0;
|
||||
virtual int getAnimationDuration() const = 0;
|
||||
|
||||
int getIndexFromPosition(QPoint pos);
|
||||
void setSelectedSection(int index);
|
||||
|
||||
std::vector<Section> _sections;
|
||||
Fn<bool()> _paused;
|
||||
int _activeIndex = 0;
|
||||
bool _selectOnPress = true;
|
||||
bool _snapToLabel = false;
|
||||
|
||||
rpl::event_stream<int> _sectionActivated;
|
||||
|
||||
int _pressed = -1;
|
||||
int _selected = 0;
|
||||
Ui::Animations::Simple _a_left;
|
||||
Ui::Animations::Simple _a_width;
|
||||
|
||||
int _timerId = -1;
|
||||
crl::time _callbackAfterMs = 0;
|
||||
|
||||
};
|
||||
|
||||
class SettingsSlider : public DiscreteSlider {
|
||||
public:
|
||||
SettingsSlider(
|
||||
QWidget *parent,
|
||||
const style::SettingsSlider &st = st::defaultSettingsSlider);
|
||||
|
||||
[[nodiscard]] const style::SettingsSlider &st() const;
|
||||
|
||||
[[nodiscard]] int centerOfSection(int section) const;
|
||||
virtual void fitWidthToSections();
|
||||
|
||||
void setRippleTopRoundRadius(int radius);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void startRipple(int sectionIndex) override;
|
||||
|
||||
std::vector<float64> countSectionsWidths(int newWidth) const;
|
||||
|
||||
private:
|
||||
const style::TextStyle &getLabelStyle() const override;
|
||||
int getAnimationDuration() const override;
|
||||
QImage prepareRippleMask(int sectionIndex, const Section §ion);
|
||||
|
||||
void resizeSections(int newWidth);
|
||||
|
||||
const style::SettingsSlider &_st;
|
||||
std::optional<Ui::RoundRect> _bar;
|
||||
std::optional<Ui::RoundRect> _barActive;
|
||||
int _rippleTopRoundRadius = 0;
|
||||
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
237
Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp
Normal file
237
Telegram/SourceFiles/ui/widgets/expandable_peer_list.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
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 "ui/widgets/expandable_peer_list.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_text_entity.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/participants_check_view.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
class Button final : public Ui::RippleButton {
|
||||
public:
|
||||
Button(not_null<QWidget*> parent, int count);
|
||||
|
||||
[[nodiscard]] not_null<Ui::AbstractCheckView*> checkView() const;
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
std::unique_ptr<Ui::AbstractCheckView> _view;
|
||||
|
||||
};
|
||||
|
||||
Button::Button(not_null<QWidget*> parent, int count)
|
||||
: Ui::RippleButton(parent, st::defaultRippleAnimation)
|
||||
, _view(std::make_unique<Ui::ParticipantsCheckView>(
|
||||
count,
|
||||
st::slideWrapDuration,
|
||||
false,
|
||||
[=] { update(); })) {
|
||||
}
|
||||
|
||||
not_null<Ui::AbstractCheckView*> Button::checkView() const {
|
||||
return _view.get();
|
||||
}
|
||||
|
||||
QImage Button::prepareRippleMask() const {
|
||||
return _view->prepareRippleMask();
|
||||
}
|
||||
|
||||
QPoint Button::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
void Button::paintEvent(QPaintEvent *event) {
|
||||
auto p = QPainter(this);
|
||||
Ui::RippleButton::paintRipple(p, QPoint());
|
||||
_view->paint(p, 0, 0, width());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddExpandablePeerList(
|
||||
not_null<Ui::Checkbox*> checkbox,
|
||||
not_null<ExpandablePeerListController*> controller,
|
||||
not_null<Ui::VerticalLayout*> inner) {
|
||||
const auto &participants = controller->data.participants;
|
||||
const auto hideRightButton = controller->data.hideRightButton;
|
||||
const auto checkTopOnAllInner = controller->data.checkTopOnAllInner;
|
||||
const auto isSingle = controller->data.skipSingle
|
||||
? false
|
||||
: (participants.size() == 1);
|
||||
if (isSingle) {
|
||||
const auto p = participants.front();
|
||||
controller->collectRequests = [=] { return Participants{ p }; };
|
||||
return;
|
||||
}
|
||||
const auto count = int(participants.size());
|
||||
const auto button = !hideRightButton
|
||||
? Ui::CreateChild<Button>(inner, count)
|
||||
: nullptr;
|
||||
if (button) {
|
||||
button->resize(Ui::ParticipantsCheckView::ComputeSize(count));
|
||||
}
|
||||
|
||||
const auto overlay = Ui::CreateChild<Ui::AbstractButton>(inner);
|
||||
|
||||
checkbox->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
overlay->setGeometry(rect);
|
||||
overlay->raise();
|
||||
|
||||
if (button) {
|
||||
button->moveToRight(
|
||||
st::moderateBoxExpandRight,
|
||||
rect.top() + (rect.height() - button->height()) / 2,
|
||||
inner->width());
|
||||
button->raise();
|
||||
}
|
||||
}, overlay->lifetime());
|
||||
|
||||
controller->toggleRequestsFromInner.events(
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
checkbox->setChecked(toggled);
|
||||
}, checkbox->lifetime());
|
||||
if (button) {
|
||||
button->setClickedCallback([=] {
|
||||
button->checkView()->setChecked(
|
||||
!button->checkView()->checked(),
|
||||
anim::type::normal);
|
||||
controller->toggleRequestsFromTop.fire_copy(
|
||||
button->checkView()->checked());
|
||||
});
|
||||
}
|
||||
overlay->setClickedCallback([=] {
|
||||
checkbox->setChecked(!checkbox->checked());
|
||||
controller->checkAllRequests.fire_copy(checkbox->checked());
|
||||
});
|
||||
{
|
||||
const auto wrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner)));
|
||||
wrap->toggle(hideRightButton, anim::type::instant);
|
||||
|
||||
controller->toggleRequestsFromTop.events(
|
||||
) | rpl::on_next([=](bool toggled) {
|
||||
wrap->toggle(toggled, anim::type::normal);
|
||||
}, wrap->lifetime());
|
||||
|
||||
const auto container = wrap->entity();
|
||||
Ui::AddSkip(container);
|
||||
|
||||
auto &lifetime = wrap->lifetime();
|
||||
const auto clicks = lifetime.make_state<rpl::event_stream<>>();
|
||||
const auto checkboxes = ranges::views::all(
|
||||
participants
|
||||
) | ranges::views::transform([&](not_null<PeerData*> peer) {
|
||||
const auto line = container->add(
|
||||
object_ptr<Ui::AbstractButton>(container));
|
||||
const auto &st = st::moderateBoxUserpic;
|
||||
line->resize(line->width(), st.size.height());
|
||||
|
||||
using namespace Info::Profile;
|
||||
auto name = controller->data.bold
|
||||
? (NameValue(peer) | rpl::map(tr::bold) | rpl::type_erased)
|
||||
: NameValue(peer) | rpl::map(tr::marked);
|
||||
const auto userpic
|
||||
= Ui::CreateChild<Ui::UserpicButton>(line, peer, st);
|
||||
const auto checkbox = Ui::CreateChild<Ui::Checkbox>(
|
||||
line,
|
||||
controller->data.messagesCounts
|
||||
? rpl::combine(
|
||||
std::move(name),
|
||||
rpl::duplicate(controller->data.messagesCounts)
|
||||
) | rpl::map([=](const auto &richName, const auto &map) {
|
||||
const auto it = map.find(peer->id);
|
||||
return (it == map.end() || !it->second)
|
||||
? richName
|
||||
: TextWithEntities{
|
||||
(u"(%1) "_q).arg(it->second)
|
||||
}.append(richName);
|
||||
})
|
||||
: std::move(name) | rpl::type_erased,
|
||||
st::defaultBoxCheckbox,
|
||||
std::make_unique<Ui::CheckView>(
|
||||
st::defaultCheck,
|
||||
ranges::contains(controller->data.checked, peer->id)));
|
||||
checkbox->setCheckAlignment(style::al_left);
|
||||
rpl::combine(
|
||||
line->widthValue(),
|
||||
checkbox->widthValue()
|
||||
) | rpl::on_next([=](int width, int) {
|
||||
userpic->moveToLeft(
|
||||
st::boxRowPadding.left()
|
||||
+ checkbox->checkRect().width()
|
||||
+ st::defaultBoxCheckbox.textPosition.x(),
|
||||
0);
|
||||
const auto skip = st::defaultBoxCheckbox.textPosition.x();
|
||||
checkbox->resizeToWidth(width
|
||||
- rect::right(userpic)
|
||||
- skip
|
||||
- st::boxRowPadding.right());
|
||||
checkbox->moveToLeft(
|
||||
rect::right(userpic) + skip,
|
||||
((userpic->height() - checkbox->height()) / 2)
|
||||
+ st::defaultBoxCheckbox.margin.top());
|
||||
}, checkbox->lifetime());
|
||||
|
||||
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
checkbox->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
line->setClickedCallback([=] {
|
||||
checkbox->setChecked(!checkbox->checked());
|
||||
clicks->fire({});
|
||||
});
|
||||
|
||||
return checkbox;
|
||||
}) | ranges::to_vector;
|
||||
|
||||
clicks->events(
|
||||
) | rpl::on_next([=] {
|
||||
controller->toggleRequestsFromInner.fire_copy(
|
||||
checkTopOnAllInner
|
||||
? ranges::all_of(checkboxes, &Ui::Checkbox::checked)
|
||||
: ranges::any_of(checkboxes, &Ui::Checkbox::checked));
|
||||
}, container->lifetime());
|
||||
|
||||
controller->checkAllRequests.events(
|
||||
) | rpl::on_next([=](bool checked) {
|
||||
for (const auto &c : checkboxes) {
|
||||
c->setChecked(checked);
|
||||
}
|
||||
}, container->lifetime());
|
||||
|
||||
controller->collectRequests = [=] {
|
||||
auto result = Participants();
|
||||
for (auto i = 0; i < checkboxes.size(); i++) {
|
||||
if (checkboxes[i]->checked()) {
|
||||
result.push_back(participants[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
43
Telegram/SourceFiles/ui/widgets/expandable_peer_list.h
Normal file
43
Telegram/SourceFiles/ui/widgets/expandable_peer_list.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
|
||||
using Participants = std::vector<not_null<PeerData*>>;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class Checkbox;
|
||||
class VerticalLayout;
|
||||
|
||||
struct ExpandablePeerListController final {
|
||||
struct Data final {
|
||||
rpl::producer<base::flat_map<PeerId, int>> messagesCounts = nullptr;
|
||||
Participants participants;
|
||||
std::vector<PeerId> checked;
|
||||
bool skipSingle = false;
|
||||
bool hideRightButton = false;
|
||||
bool checkTopOnAllInner = false;
|
||||
bool bold = true;
|
||||
};
|
||||
ExpandablePeerListController(Data &&data) : data(std::move(data)) {
|
||||
}
|
||||
const Data data;
|
||||
rpl::event_stream<bool> toggleRequestsFromTop;
|
||||
rpl::event_stream<bool> toggleRequestsFromInner;
|
||||
rpl::event_stream<bool> checkAllRequests;
|
||||
Fn<Participants()> collectRequests;
|
||||
};
|
||||
|
||||
void AddExpandablePeerList(
|
||||
not_null<Ui::Checkbox*> checkbox,
|
||||
not_null<ExpandablePeerListController*> controller,
|
||||
not_null<Ui::VerticalLayout*> inner);
|
||||
|
||||
} // namespace Ui
|
||||
459
Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp
Normal file
459
Telegram/SourceFiles/ui/widgets/fields/special_fields.cpp
Normal file
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
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 "ui/widgets/fields/special_fields.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "countries/countries_instance.h" // Countries::ValidPhoneCode
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxUsernameLength = 32;
|
||||
|
||||
// Rest of the phone number, without country code (seen 12 at least),
|
||||
// need more for service numbers.
|
||||
constexpr auto kMaxPhoneTailLength = 32;
|
||||
|
||||
// Max length of country phone code.
|
||||
constexpr auto kMaxPhoneCodeLength = 4;
|
||||
|
||||
} // namespace
|
||||
|
||||
CountryCodeInput::CountryCodeInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st)
|
||||
: MaskedInputField(parent, st) {
|
||||
}
|
||||
|
||||
void CountryCodeInput::startErasing(QKeyEvent *e) {
|
||||
setFocus();
|
||||
keyPressEvent(e);
|
||||
}
|
||||
|
||||
void CountryCodeInput::codeSelected(const QString &code) {
|
||||
auto wasText = getLastText();
|
||||
auto wasCursor = cursorPosition();
|
||||
auto newText = '+' + code;
|
||||
auto newCursor = int(newText.size());
|
||||
setText(newText);
|
||||
_nosignal = true;
|
||||
correctValue(wasText, wasCursor, newText, newCursor);
|
||||
_nosignal = false;
|
||||
changed();
|
||||
}
|
||||
|
||||
void CountryCodeInput::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Space) {
|
||||
_spacePressed.fire({});
|
||||
} else {
|
||||
MaskedInputField::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void CountryCodeInput::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
QString newText, addToNumber;
|
||||
int oldPos(nowCursor);
|
||||
int newPos(-1);
|
||||
int oldLen(now.length());
|
||||
int start = 0;
|
||||
int digits = 5;
|
||||
newText.reserve(oldLen + 1);
|
||||
if (oldLen && now[0] == '+') {
|
||||
if (start == oldPos) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
++start;
|
||||
}
|
||||
newText += '+';
|
||||
for (int i = start; i < oldLen; ++i) {
|
||||
if (i == oldPos) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
auto ch = now[i];
|
||||
if (ch.isDigit()) {
|
||||
if (!digits || !--digits) {
|
||||
addToNumber += ch;
|
||||
} else {
|
||||
newText += ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!addToNumber.isEmpty()) {
|
||||
auto validCode = Countries::Instance().validPhoneCode(newText.mid(1));
|
||||
addToNumber = newText.mid(1 + validCode.length()) + addToNumber;
|
||||
newText = '+' + validCode;
|
||||
}
|
||||
setCorrectedText(now, nowCursor, newText, newPos);
|
||||
|
||||
if (!_nosignal && was != newText) {
|
||||
_codeChanged.fire(newText.mid(1));
|
||||
}
|
||||
if (!addToNumber.isEmpty()) {
|
||||
_addedToNumber.fire_copy(addToNumber);
|
||||
}
|
||||
}
|
||||
|
||||
PhonePartInput::PhonePartInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
PhonePartInput::GroupsCallback groupsCallback)
|
||||
: MaskedInputField(parent, st/*, tr::lng_phone_ph(tr::now)*/)
|
||||
, _groupsCallback(std::move(groupsCallback)) {
|
||||
}
|
||||
|
||||
void PhonePartInput::paintAdditionalPlaceholder(QPainter &p) {
|
||||
if (!_pattern.isEmpty()) {
|
||||
auto t = getDisplayedText();
|
||||
auto ph = _additionalPlaceholder.mid(t.size());
|
||||
if (!ph.isEmpty()) {
|
||||
p.setClipRect(rect());
|
||||
auto phRect = placeholderRect();
|
||||
int tw = phFont()->width(t);
|
||||
if (tw < phRect.width()) {
|
||||
phRect.setLeft(phRect.left() + tw);
|
||||
placeholderAdditionalPrepare(p);
|
||||
p.drawText(phRect, ph, style::al_topleft);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhonePartInput::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Backspace && cursorPosition() == 0) {
|
||||
_frontBackspaceEvent.fire_copy(e);
|
||||
} else {
|
||||
MaskedInputField::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void PhonePartInput::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
if (!now.isEmpty() && (_lastDigits != now)) {
|
||||
_lastDigits = now;
|
||||
_lastDigits.replace(TextUtilities::RegExpDigitsExclude(), QString());
|
||||
updatePattern(_groupsCallback(_code + _lastDigits));
|
||||
}
|
||||
|
||||
QString newText;
|
||||
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = 0;
|
||||
for (int i = 0; i < oldLen; ++i) {
|
||||
if (now[i].isDigit()) {
|
||||
++digitCount;
|
||||
}
|
||||
}
|
||||
if (digitCount > kMaxPhoneTailLength) {
|
||||
digitCount = kMaxPhoneTailLength;
|
||||
}
|
||||
|
||||
bool inPart = !_pattern.isEmpty();
|
||||
int curPart = -1, leftInPart = 0;
|
||||
newText.reserve(oldLen);
|
||||
for (int i = 0; i < oldLen; ++i) {
|
||||
if (i == oldPos && newPos < 0) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
|
||||
auto ch = now[i];
|
||||
if (ch.isDigit()) {
|
||||
if (!digitCount--) {
|
||||
break;
|
||||
}
|
||||
if (inPart) {
|
||||
if (leftInPart) {
|
||||
--leftInPart;
|
||||
} else {
|
||||
++curPart;
|
||||
inPart = curPart < _pattern.size();
|
||||
// Don't add an extra space to the end.
|
||||
if (inPart) {
|
||||
newText += ' ';
|
||||
}
|
||||
|
||||
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
|
||||
|
||||
++oldPos;
|
||||
}
|
||||
}
|
||||
newText += ch;
|
||||
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
|
||||
if (inPart) {
|
||||
if (leftInPart) {
|
||||
} else {
|
||||
newText += ch;
|
||||
++curPart;
|
||||
inPart = curPart < _pattern.size();
|
||||
leftInPart = inPart ? _pattern.at(curPart) : 0;
|
||||
}
|
||||
} else {
|
||||
newText += ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto newlen = newText.size();
|
||||
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
|
||||
--newlen;
|
||||
}
|
||||
if (newlen < newText.size()) {
|
||||
newText = newText.mid(0, newlen);
|
||||
}
|
||||
setCorrectedText(now, nowCursor, newText, newPos);
|
||||
}
|
||||
|
||||
void PhonePartInput::addedToNumber(const QString &added) {
|
||||
setFocus();
|
||||
auto wasText = getLastText();
|
||||
auto wasCursor = cursorPosition();
|
||||
auto newText = added + wasText;
|
||||
auto newCursor = int(newText.size());
|
||||
setText(newText);
|
||||
setCursorPosition(added.length());
|
||||
correctValue(wasText, wasCursor, newText, newCursor);
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
|
||||
void PhonePartInput::chooseCode(const QString &code) {
|
||||
_code = code;
|
||||
updatePattern(_groupsCallback(_code));
|
||||
|
||||
auto wasText = getLastText();
|
||||
auto wasCursor = cursorPosition();
|
||||
auto newText = getLastText();
|
||||
auto newCursor = int(newText.size());
|
||||
correctValue(wasText, wasCursor, newText, newCursor);
|
||||
|
||||
startPlaceholderAnimation();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void PhonePartInput::updatePattern(QVector<int> &&pattern) {
|
||||
if (_pattern == pattern) {
|
||||
return;
|
||||
}
|
||||
_pattern = std::move(pattern);
|
||||
if (!_pattern.isEmpty() && _pattern.at(0) == _code.size()) {
|
||||
_pattern.pop_front();
|
||||
} else {
|
||||
_pattern.clear();
|
||||
}
|
||||
_additionalPlaceholder = QString();
|
||||
if (!_pattern.isEmpty()) {
|
||||
_additionalPlaceholder.reserve(20);
|
||||
for (const auto &part : _pattern) {
|
||||
_additionalPlaceholder.append(' ');
|
||||
_additionalPlaceholder.append(QString(part, QChar(0x2212)));
|
||||
}
|
||||
}
|
||||
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
|
||||
}
|
||||
|
||||
UsernameInput::UsernameInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &val,
|
||||
const QString &linkPlaceholder)
|
||||
: MaskedInputField(parent, st, std::move(placeholder), val) {
|
||||
setLinkPlaceholder(linkPlaceholder);
|
||||
}
|
||||
|
||||
void UsernameInput::setLinkPlaceholder(const QString &placeholder) {
|
||||
_linkPlaceholder = placeholder;
|
||||
if (!_linkPlaceholder.isEmpty()) {
|
||||
setTextMargins(style::margins(_st.textMargins.left() + _st.style.font->width(_linkPlaceholder), _st.textMargins.top(), _st.textMargins.right(), _st.textMargins.bottom()));
|
||||
setPlaceholderHidden(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UsernameInput::paintAdditionalPlaceholder(QPainter &p) {
|
||||
if (!_linkPlaceholder.isEmpty()) {
|
||||
p.setFont(_st.style.font);
|
||||
p.setPen(_st.placeholderFg);
|
||||
p.drawText(QRect(_st.textMargins.left(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), _linkPlaceholder, style::al_topleft);
|
||||
}
|
||||
}
|
||||
|
||||
void UsernameInput::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
auto newPos = nowCursor;
|
||||
auto from = 0, len = int(now.size());
|
||||
for (; from < len; ++from) {
|
||||
if (!now.at(from).isSpace()) {
|
||||
break;
|
||||
}
|
||||
if (newPos > 0) --newPos;
|
||||
}
|
||||
len -= from;
|
||||
if (len > kMaxUsernameLength) {
|
||||
len = kMaxUsernameLength + (now.at(from) == '@' ? 1 : 0);
|
||||
}
|
||||
for (int32 to = from + len; to > from;) {
|
||||
--to;
|
||||
if (!now.at(to).isSpace()) {
|
||||
break;
|
||||
}
|
||||
--len;
|
||||
}
|
||||
setCorrectedText(now, nowCursor, now.mid(from, len), newPos);
|
||||
}
|
||||
|
||||
PhoneInput::PhoneInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &defaultValue,
|
||||
QString value,
|
||||
PhoneInput::GroupsCallback groupsCallback)
|
||||
: MaskedInputField(parent, st, std::move(placeholder), value)
|
||||
, _defaultValue(defaultValue)
|
||||
, _groupsCallback(std::move(groupsCallback)) {
|
||||
if (value.isEmpty()) {
|
||||
clearText();
|
||||
} else {
|
||||
auto pos = int(value.size());
|
||||
correctValue(QString(), 0, value, pos);
|
||||
}
|
||||
}
|
||||
|
||||
void PhoneInput::focusInEvent(QFocusEvent *e) {
|
||||
MaskedInputField::focusInEvent(e);
|
||||
setSelection(cursorPosition(), cursorPosition());
|
||||
}
|
||||
|
||||
void PhoneInput::clearText() {
|
||||
auto value = _defaultValue;
|
||||
setText(value);
|
||||
auto pos = int(value.size());
|
||||
correctValue(QString(), 0, value, pos);
|
||||
}
|
||||
|
||||
void PhoneInput::paintAdditionalPlaceholder(QPainter &p) {
|
||||
if (!_pattern.isEmpty()) {
|
||||
auto t = getDisplayedText();
|
||||
auto ph = _additionalPlaceholder.mid(t.size());
|
||||
if (!ph.isEmpty()) {
|
||||
p.setClipRect(rect());
|
||||
auto phRect = placeholderRect();
|
||||
int tw = phFont()->width(t);
|
||||
if (tw < phRect.width()) {
|
||||
phRect.setLeft(phRect.left() + tw);
|
||||
placeholderAdditionalPrepare(p);
|
||||
p.drawText(phRect, ph, style::al_topleft);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PhoneInput::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
auto digits = now;
|
||||
digits.replace(TextUtilities::RegExpDigitsExclude(), QString());
|
||||
_pattern = _groupsCallback(digits);
|
||||
|
||||
QString newPlaceholder;
|
||||
if (_pattern.isEmpty()) {
|
||||
newPlaceholder = QString();
|
||||
} else if (_pattern.size() == 1 && _pattern.at(0) == digits.size()) {
|
||||
newPlaceholder = QString(_pattern.at(0) + 2, ' ') + tr::lng_contact_phone(tr::now);
|
||||
} else {
|
||||
newPlaceholder.reserve(20);
|
||||
for (int i = 0, l = _pattern.size(); i < l; ++i) {
|
||||
if (i) {
|
||||
newPlaceholder.append(' ');
|
||||
} else {
|
||||
newPlaceholder.append('+');
|
||||
}
|
||||
newPlaceholder.append(i ? QString(_pattern.at(i), QChar(0x2212)) : digits.mid(0, _pattern.at(i)));
|
||||
}
|
||||
}
|
||||
if (_additionalPlaceholder != newPlaceholder) {
|
||||
_additionalPlaceholder = newPlaceholder;
|
||||
setPlaceholderHidden(!_additionalPlaceholder.isEmpty());
|
||||
update();
|
||||
}
|
||||
|
||||
QString newText;
|
||||
int oldPos(nowCursor), newPos(-1), oldLen(now.length()), digitCount = qMin(digits.size(), kMaxPhoneCodeLength + kMaxPhoneTailLength);
|
||||
|
||||
bool inPart = !_pattern.isEmpty(), plusFound = false;
|
||||
int curPart = 0, leftInPart = inPart ? _pattern.at(curPart) : 0;
|
||||
newText.reserve(oldLen + 1);
|
||||
newText.append('+');
|
||||
for (int i = 0; i < oldLen; ++i) {
|
||||
if (i == oldPos && newPos < 0) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
|
||||
QChar ch(now[i]);
|
||||
if (ch.isDigit()) {
|
||||
if (!digitCount--) {
|
||||
break;
|
||||
}
|
||||
if (inPart) {
|
||||
if (leftInPart) {
|
||||
--leftInPart;
|
||||
} else {
|
||||
++curPart;
|
||||
inPart = curPart < _pattern.size();
|
||||
// Don't add an extra space to the end.
|
||||
if (inPart) {
|
||||
newText += ' ';
|
||||
}
|
||||
leftInPart = inPart ? (_pattern.at(curPart) - 1) : 0;
|
||||
|
||||
++oldPos;
|
||||
}
|
||||
}
|
||||
newText += ch;
|
||||
} else if (ch == ' ' || ch == '-' || ch == '(' || ch == ')') {
|
||||
if (inPart) {
|
||||
if (leftInPart) {
|
||||
} else {
|
||||
newText += ch;
|
||||
++curPart;
|
||||
inPart = curPart < _pattern.size();
|
||||
leftInPart = inPart ? _pattern.at(curPart) : 0;
|
||||
}
|
||||
} else {
|
||||
newText += ch;
|
||||
}
|
||||
} else if (ch == '+') {
|
||||
plusFound = true;
|
||||
}
|
||||
}
|
||||
if (!plusFound && newText == u"+"_q) {
|
||||
newText = QString();
|
||||
newPos = 0;
|
||||
}
|
||||
int32 newlen = newText.size();
|
||||
while (newlen > 0 && newText.at(newlen - 1).isSpace()) {
|
||||
--newlen;
|
||||
}
|
||||
if (newlen < newText.size()) {
|
||||
newText = newText.mid(0, newlen);
|
||||
}
|
||||
setCorrectedText(now, nowCursor, newText, newPos);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
144
Telegram/SourceFiles/ui/widgets/fields/special_fields.h
Normal file
144
Telegram/SourceFiles/ui/widgets/fields/special_fields.h
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class CountryCodeInput : public MaskedInputField {
|
||||
public:
|
||||
CountryCodeInput(QWidget *parent, const style::InputField &st);
|
||||
|
||||
void startErasing(QKeyEvent *e);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> addedToNumber() const {
|
||||
return _addedToNumber.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<QString> codeChanged() const {
|
||||
return _codeChanged.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<> spacePressed() const {
|
||||
return _spacePressed.events();
|
||||
}
|
||||
|
||||
void codeSelected(const QString &code);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
|
||||
private:
|
||||
bool _nosignal = false;
|
||||
rpl::event_stream<QString> _addedToNumber;
|
||||
rpl::event_stream<QString> _codeChanged;
|
||||
rpl::event_stream<> _spacePressed;
|
||||
|
||||
};
|
||||
|
||||
class PhonePartInput : public MaskedInputField {
|
||||
public:
|
||||
using GroupsCallback = Fn<QVector<int>(const QString &)>;
|
||||
|
||||
PhonePartInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
GroupsCallback groupsCallback);
|
||||
|
||||
[[nodiscard]] auto frontBackspaceEvent() const
|
||||
-> rpl::producer<not_null<QKeyEvent*>> {
|
||||
return _frontBackspaceEvent.events();
|
||||
}
|
||||
|
||||
void addedToNumber(const QString &added);
|
||||
void chooseCode(const QString &code);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
void paintAdditionalPlaceholder(QPainter &p) override;
|
||||
|
||||
private:
|
||||
void updatePattern(QVector<int> &&pattern);
|
||||
|
||||
QString _code;
|
||||
QString _lastDigits;
|
||||
QVector<int> _pattern;
|
||||
QString _additionalPlaceholder;
|
||||
rpl::event_stream<not_null<QKeyEvent*>> _frontBackspaceEvent;
|
||||
GroupsCallback _groupsCallback;
|
||||
|
||||
};
|
||||
|
||||
class UsernameInput : public MaskedInputField {
|
||||
public:
|
||||
UsernameInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &val,
|
||||
const QString &linkPlaceholder);
|
||||
|
||||
void setLinkPlaceholder(const QString &placeholder);
|
||||
|
||||
protected:
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
void paintAdditionalPlaceholder(QPainter &p) override;
|
||||
|
||||
private:
|
||||
QString _linkPlaceholder;
|
||||
|
||||
};
|
||||
|
||||
class PhoneInput : public MaskedInputField {
|
||||
public:
|
||||
using GroupsCallback = Fn<QVector<int>(const QString &)>;
|
||||
|
||||
PhoneInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &defaultValue,
|
||||
QString value,
|
||||
GroupsCallback groupsCallback);
|
||||
|
||||
void clearText();
|
||||
|
||||
protected:
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
void paintAdditionalPlaceholder(QPainter &p) override;
|
||||
|
||||
private:
|
||||
QString _defaultValue;
|
||||
QVector<int> _pattern;
|
||||
QString _additionalPlaceholder;
|
||||
|
||||
GroupsCallback _groupsCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 "ui/widgets/fields/time_part_input_with_placeholder.h"
|
||||
|
||||
#include "lang/lang_numbers_animation.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void TimePartWithPlaceholder::setPhrase(
|
||||
const tr::phrase<lngtag_count> &phrase) {
|
||||
_phrase = phrase;
|
||||
}
|
||||
|
||||
void TimePartWithPlaceholder::paintAdditionalPlaceholder(QPainter &p) {
|
||||
maybeUpdatePlaceholder();
|
||||
|
||||
p.setClipRect(rect());
|
||||
const auto phRect = placeholderRect();
|
||||
|
||||
if (_lastPlaceholder.width < phRect.width()) {
|
||||
placeholderAdditionalPrepare(p);
|
||||
p.drawText(
|
||||
phRect.translated(-_lastPlaceholder.leftOffset, 0),
|
||||
_lastPlaceholder.text,
|
||||
style::al_left);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TimePartWithPlaceholder::maybeUpdatePlaceholder() {
|
||||
const auto displayedText = getDisplayedText();
|
||||
if (displayedText == _lastPlaceholder.displayedText) {
|
||||
return;
|
||||
}
|
||||
const auto count = displayedText.toUInt();
|
||||
const auto textWithOffset = _phrase(
|
||||
tr::now,
|
||||
lt_count,
|
||||
count,
|
||||
Ui::StringWithNumbers::FromString);
|
||||
_lastPlaceholder = {
|
||||
.width = phFont()->width(textWithOffset.text),
|
||||
.text = textWithOffset.text,
|
||||
.leftOffset = phFont()->width(
|
||||
textWithOffset.text.mid(0, textWithOffset.offset)),
|
||||
.displayedText = displayedText,
|
||||
};
|
||||
if (displayedText.size() > 1 && displayedText.startsWith(_zero)) {
|
||||
_lastPlaceholder.text.insert(textWithOffset.offset, _zero);
|
||||
}
|
||||
|
||||
const auto leftMargins = (width() - _lastPlaceholder.width) / 2
|
||||
+ _lastPlaceholder.leftOffset;
|
||||
setTextMargins({ leftMargins, 0, 0, 0 });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/fields/time_part_input.h"
|
||||
|
||||
#include "lang_auto.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class TimePartWithPlaceholder final : public TimePart {
|
||||
public:
|
||||
using Ui::TimePart::TimePart;
|
||||
|
||||
void setPhrase(const tr::phrase<lngtag_count> &phrase);
|
||||
|
||||
protected:
|
||||
void paintAdditionalPlaceholder(QPainter &p) override;
|
||||
|
||||
private:
|
||||
void maybeUpdatePlaceholder();
|
||||
|
||||
const QChar _zero = QChar('0');
|
||||
tr::phrase<lngtag_count> _phrase;
|
||||
|
||||
struct {
|
||||
int width = 0;
|
||||
QString text;
|
||||
int leftOffset = 0;
|
||||
QString displayedText;
|
||||
} _lastPlaceholder;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
94
Telegram/SourceFiles/ui/widgets/gradient_round_button.cpp
Normal file
94
Telegram/SourceFiles/ui/widgets/gradient_round_button.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
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 "ui/widgets/gradient_round_button.h"
|
||||
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
GradientButton::GradientButton(QWidget *widget, QGradientStops stops)
|
||||
: RippleButton(widget, st::defaultRippleAnimation)
|
||||
, _stops(std::move(stops)) {
|
||||
}
|
||||
|
||||
void GradientButton::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
validateBg();
|
||||
p.drawImage(0, 0, _bg);
|
||||
paintGlare(p);
|
||||
|
||||
const auto ripple = QColor(0, 0, 0, 36);
|
||||
paintRipple(p, 0, 0, &ripple);
|
||||
}
|
||||
|
||||
void GradientButton::paintGlare(QPainter &p) {
|
||||
if (!_glare.glare.birthTime) {
|
||||
return;
|
||||
}
|
||||
const auto progress = _glare.progress(crl::now());
|
||||
const auto x = (-_glare.width) + (width() + _glare.width * 2) * progress;
|
||||
const auto h = height();
|
||||
|
||||
const auto edgeWidth = _glare.width + st::roundRadiusLarge;
|
||||
if (x > edgeWidth && x < (width() - edgeWidth)) {
|
||||
p.drawTiledPixmap(x, 0, _glare.width, h, _glare.pixmap, 0, 0);
|
||||
} else {
|
||||
auto frame = QImage(
|
||||
QSize(_glare.width, h) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
frame.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
frame.fill(Qt::transparent);
|
||||
|
||||
{
|
||||
auto q = QPainter(&frame);
|
||||
q.drawTiledPixmap(0, 0, _glare.width, h, _glare.pixmap, 0, 0);
|
||||
q.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
q.drawImage(-x, 0, _bg, 0, 0);
|
||||
}
|
||||
p.drawImage(x, 0, frame);
|
||||
}
|
||||
}
|
||||
|
||||
void GradientButton::validateBg() {
|
||||
const auto factor = devicePixelRatio();
|
||||
if (!_bg.isNull()
|
||||
&& (_bg.devicePixelRatio() == factor)
|
||||
&& (_bg.size() == size() * factor)) {
|
||||
return;
|
||||
}
|
||||
_bg = QImage(size() * factor, QImage::Format_ARGB32_Premultiplied);
|
||||
_bg.setDevicePixelRatio(factor);
|
||||
|
||||
auto p = QPainter(&_bg);
|
||||
auto gradient = QLinearGradient(QPointF(0, 0), QPointF(width(), 0));
|
||||
gradient.setStops(_stops);
|
||||
p.fillRect(rect(), gradient);
|
||||
p.end();
|
||||
|
||||
_bg = Images::Round(std::move(_bg), ImageRoundRadius::Large);
|
||||
}
|
||||
|
||||
void GradientButton::setGlarePaused(bool paused) {
|
||||
_glare.paused = paused;
|
||||
}
|
||||
|
||||
void GradientButton::validateGlare() {
|
||||
_glare.validate(
|
||||
st::premiumButtonFg->c,
|
||||
[=] { update(); },
|
||||
st::gradientButtonGlareTimeout,
|
||||
st::gradientButtonGlareDuration);
|
||||
}
|
||||
|
||||
void GradientButton::startGlareAnimation() {
|
||||
validateGlare();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
35
Telegram/SourceFiles/ui/widgets/gradient_round_button.h
Normal file
35
Telegram/SourceFiles/ui/widgets/gradient_round_button.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/effects/glare.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GradientButton final : public Ui::RippleButton {
|
||||
public:
|
||||
GradientButton(QWidget *widget, QGradientStops stops);
|
||||
|
||||
void startGlareAnimation();
|
||||
void setGlarePaused(bool paused);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e);
|
||||
void paintGlare(QPainter &p);
|
||||
void validateBg();
|
||||
void validateGlare();
|
||||
|
||||
QGradientStops _stops;
|
||||
QImage _bg;
|
||||
|
||||
GlareEffect _glare;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
56
Telegram/SourceFiles/ui/widgets/horizontal_fit_container.cpp
Normal file
56
Telegram/SourceFiles/ui/widgets/horizontal_fit_container.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 "ui/widgets/horizontal_fit_container.h"
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
HorizontalFitContainer::HorizontalFitContainer(
|
||||
not_null<RpWidget*> parent,
|
||||
int spacing)
|
||||
: RpWidget(parent)
|
||||
, _spacing(spacing) {
|
||||
sizeValue() | rpl::on_next([=](QSize size) {
|
||||
updateLayout(size);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
int HorizontalFitContainer::add(not_null<AbstractButton*> button) {
|
||||
const auto id = _nextId++;
|
||||
_buttons.emplace(id, base::unique_qptr<AbstractButton>{ button.get() });
|
||||
button->setParent(this);
|
||||
button->show();
|
||||
updateLayout(size());
|
||||
return id;
|
||||
}
|
||||
|
||||
void HorizontalFitContainer::remove(int id) {
|
||||
if (const auto it = _buttons.find(id); it != _buttons.end()) {
|
||||
_buttons.erase(it);
|
||||
updateLayout(size());
|
||||
}
|
||||
}
|
||||
|
||||
void HorizontalFitContainer::updateLayout(QSize size) {
|
||||
_layoutLifetime.destroy();
|
||||
if (_buttons.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto count = int(_buttons.size());
|
||||
const auto totalSpacing = _spacing * (count - 1);
|
||||
const auto buttonWidth = (size.width() - totalSpacing) / count;
|
||||
const auto h = size.height();
|
||||
auto x = 0;
|
||||
for (const auto &[id, button] : _buttons) {
|
||||
button->setGeometry(x, 0, buttonWidth, h);
|
||||
x += buttonWidth + _spacing;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
36
Telegram/SourceFiles/ui/widgets/horizontal_fit_container.h
Normal file
36
Telegram/SourceFiles/ui/widgets/horizontal_fit_container.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractButton;
|
||||
|
||||
class HorizontalFitContainer final : public RpWidget {
|
||||
public:
|
||||
HorizontalFitContainer(
|
||||
not_null<RpWidget*> parent,
|
||||
int spacing);
|
||||
|
||||
int add(not_null<AbstractButton*> button);
|
||||
void remove(int id);
|
||||
|
||||
private:
|
||||
void updateLayout(QSize size);
|
||||
|
||||
const int _spacing;
|
||||
std::map<int, base::unique_qptr<AbstractButton>> _buttons;
|
||||
int _nextId = 0;
|
||||
rpl::lifetime _layoutLifetime;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
48
Telegram/SourceFiles/ui/widgets/level_meter.cpp
Normal file
48
Telegram/SourceFiles/ui/widgets/level_meter.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 "ui/widgets/level_meter.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
LevelMeter::LevelMeter(QWidget *parent, const style::LevelMeter &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st) {
|
||||
}
|
||||
|
||||
void LevelMeter::setValue(float value) {
|
||||
_value = value;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void LevelMeter::paintEvent(QPaintEvent* event) {
|
||||
auto p = QPainter(this);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
|
||||
const auto activeFg = _st.activeFg;
|
||||
const auto inactiveFg = _st.inactiveFg;
|
||||
const auto radius = _st.lineWidth / 2;
|
||||
const auto rect = QRect(0, 0, _st.lineWidth, height());
|
||||
p.setBrush(activeFg);
|
||||
for (auto i = 0; i < _st.lineCount; ++i) {
|
||||
const auto valueAtLine = (float64)(i + 1) / _st.lineCount;
|
||||
if (valueAtLine > _value) {
|
||||
p.setBrush(inactiveFg);
|
||||
}
|
||||
p.drawRoundedRect(
|
||||
rect.translated((_st.lineWidth + _st.lineSpacing) * i, 0),
|
||||
radius,
|
||||
radius);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
33
Telegram/SourceFiles/ui/widgets/level_meter.h
Normal file
33
Telegram/SourceFiles/ui/widgets/level_meter.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace style {
|
||||
struct LevelMeter;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class LevelMeter : public RpWidget {
|
||||
public:
|
||||
LevelMeter(QWidget *parent, const style::LevelMeter &st);
|
||||
|
||||
void setValue(float value);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::LevelMeter &_st;
|
||||
float _value = 0.0f;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1103
Telegram/SourceFiles/ui/widgets/multi_select.cpp
Normal file
1103
Telegram/SourceFiles/ui/widgets/multi_select.cpp
Normal file
File diff suppressed because it is too large
Load Diff
75
Telegram/SourceFiles/ui/widgets/multi_select.h
Normal file
75
Telegram/SourceFiles/ui/widgets/multi_select.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace style {
|
||||
struct MultiSelect;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ScrollArea;
|
||||
|
||||
class MultiSelect : public RpWidget {
|
||||
public:
|
||||
MultiSelect(
|
||||
QWidget *parent,
|
||||
const style::MultiSelect &st,
|
||||
rpl::producer<QString> placeholder = nullptr,
|
||||
const QString &query = QString());
|
||||
|
||||
[[nodiscard]] QString getQuery() const;
|
||||
void setQuery(const QString &query);
|
||||
void setInnerFocus();
|
||||
void clearQuery();
|
||||
|
||||
void setQueryChangedCallback(Fn<void(const QString &query)> callback);
|
||||
void setSubmittedCallback(Fn<void(Qt::KeyboardModifiers)> callback);
|
||||
void setCancelledCallback(Fn<void()> callback);
|
||||
void setResizedCallback(Fn<void()> callback);
|
||||
|
||||
enum class AddItemWay {
|
||||
Default,
|
||||
SkipAnimation,
|
||||
};
|
||||
using PaintRoundImage = Fn<void(Painter &p, int x, int y, int outerWidth, int size)>;
|
||||
void addItem(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage, AddItemWay way = AddItemWay::Default);
|
||||
void addItemInBunch(uint64 itemId, const QString &text, style::color color, PaintRoundImage paintRoundImage);
|
||||
void finishItemsBunch();
|
||||
void setItemText(uint64 itemId, const QString &text);
|
||||
|
||||
void setItemRemovedCallback(Fn<void(uint64 itemId)> callback);
|
||||
void removeItem(uint64 itemId);
|
||||
|
||||
int getItemsCount() const;
|
||||
QVector<uint64> getItems() const;
|
||||
bool hasItem(uint64 itemId) const;
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
|
||||
private:
|
||||
void scrollTo(int activeTop, int activeBottom);
|
||||
|
||||
const style::MultiSelect &_st;
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
|
||||
class Inner;
|
||||
QPointer<Inner> _inner;
|
||||
|
||||
Fn<void()> _resizedCallback;
|
||||
Fn<void(const QString &query)> _queryChangedCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
95
Telegram/SourceFiles/ui/widgets/participants_check_view.cpp
Normal file
95
Telegram/SourceFiles/ui/widgets/participants_check_view.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 "ui/widgets/participants_check_view.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/effects/toggle_arrow.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ParticipantsCheckView::ParticipantsCheckView(
|
||||
int count,
|
||||
int duration,
|
||||
bool checked,
|
||||
Fn<void()> updateCallback)
|
||||
: Ui::AbstractCheckView(duration, checked, std::move(updateCallback))
|
||||
, _text(QString::number(std::abs(count)))
|
||||
, _count(count) {
|
||||
}
|
||||
|
||||
QSize ParticipantsCheckView::ComputeSize(int count) {
|
||||
return QSize(
|
||||
st::moderateBoxExpandHeight
|
||||
+ st::moderateBoxExpand.width()
|
||||
+ st::moderateBoxExpandInnerSkip * 4
|
||||
+ st::moderateBoxExpandFont->width(
|
||||
QString::number(std::abs(count)))
|
||||
+ st::moderateBoxExpandToggleSize,
|
||||
st::moderateBoxExpandHeight);
|
||||
}
|
||||
|
||||
QSize ParticipantsCheckView::getSize() const {
|
||||
return ComputeSize(_count);
|
||||
}
|
||||
|
||||
void ParticipantsCheckView::paint(
|
||||
QPainter &p,
|
||||
int left,
|
||||
int top,
|
||||
int outerWidth) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto size = getSize();
|
||||
const auto radius = size.height() / 2;
|
||||
p.setPen(Qt::NoPen);
|
||||
st::moderateBoxExpand.paint(
|
||||
p,
|
||||
radius,
|
||||
left + (size.height() - st::moderateBoxExpand.height()) / 2,
|
||||
top + size.width());
|
||||
|
||||
const auto innerSkip = st::moderateBoxExpandInnerSkip;
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.setPen(st::boxTextFg);
|
||||
p.setFont(st::moderateBoxExpandFont);
|
||||
p.drawText(
|
||||
QRect(
|
||||
left + innerSkip + radius + st::moderateBoxExpand.width(),
|
||||
top,
|
||||
size.width(),
|
||||
size.height()),
|
||||
_text,
|
||||
style::al_left);
|
||||
|
||||
const auto path = Ui::ToggleUpDownArrowPath(
|
||||
left + size.width() - st::moderateBoxExpandToggleSize - radius,
|
||||
top + size.height() / 2,
|
||||
st::moderateBoxExpandToggleSize,
|
||||
st::moderateBoxExpandToggleFourStrokes,
|
||||
Ui::AbstractCheckView::currentAnimationValue());
|
||||
p.fillPath(path, st::boxTextFg);
|
||||
}
|
||||
|
||||
QImage ParticipantsCheckView::prepareRippleMask() const {
|
||||
const auto size = getSize();
|
||||
return Ui::RippleAnimation::RoundRectMask(size, size.height() / 2);
|
||||
}
|
||||
|
||||
bool ParticipantsCheckView::checkRippleStartPosition(QPoint position) const {
|
||||
return Rect(getSize()).contains(position);
|
||||
}
|
||||
|
||||
void ParticipantsCheckView::checkedChangedHook(anim::type animated) {
|
||||
}
|
||||
|
||||
ParticipantsCheckView::~ParticipantsCheckView() = default;
|
||||
|
||||
} // namespace Ui
|
||||
39
Telegram/SourceFiles/ui/widgets/participants_check_view.h
Normal file
39
Telegram/SourceFiles/ui/widgets/participants_check_view.h
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/checkbox.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ParticipantsCheckView : public Ui::AbstractCheckView {
|
||||
public:
|
||||
ParticipantsCheckView(
|
||||
int count,
|
||||
int duration,
|
||||
bool checked,
|
||||
Fn<void()> updateCallback);
|
||||
|
||||
[[nodiscard]] static QSize ComputeSize(int count);
|
||||
|
||||
QSize getSize() const override;
|
||||
|
||||
void paint(QPainter &p, int left, int top, int outerWidth) override;
|
||||
QImage prepareRippleMask() const override;
|
||||
bool checkRippleStartPosition(QPoint position) const override;
|
||||
|
||||
~ParticipantsCheckView();
|
||||
|
||||
private:
|
||||
const QString _text;
|
||||
const int _count;
|
||||
void checkedChangedHook(anim::type animated) override;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
72
Telegram/SourceFiles/ui/widgets/peer_bubble.cpp
Normal file
72
Telegram/SourceFiles/ui/widgets/peer_bubble.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 "ui/widgets/peer_bubble.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_channel_earn.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
object_ptr<Ui::RpWidget> CreatePeerBubble(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<PeerData*> peer) {
|
||||
auto owned = object_ptr<Ui::RpWidget>(parent);
|
||||
const auto peerBubble = owned.data();
|
||||
peerBubble->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
const auto left = Ui::CreateChild<Ui::UserpicButton>(
|
||||
peerBubble,
|
||||
peer,
|
||||
st::uploadUserpicButton);
|
||||
const auto right = Ui::CreateChild<Ui::FlatLabel>(
|
||||
peerBubble,
|
||||
Info::Profile::NameValue(peer),
|
||||
st::channelEarnSemiboldLabel);
|
||||
const auto padding = st::chatGiveawayPeerPadding
|
||||
+ QMargins(st::chatGiveawayPeerPadding.left(), 0, 0, 0);
|
||||
rpl::combine(
|
||||
left->sizeValue(),
|
||||
right->sizeValue()
|
||||
) | rpl::on_next([=](
|
||||
const QSize &leftSize,
|
||||
const QSize &rightSize) {
|
||||
peerBubble->setNaturalWidth(
|
||||
leftSize.width() + rightSize.width() + rect::m::sum::h(padding));
|
||||
peerBubble->resize(peerBubble->naturalWidth(), leftSize.height());
|
||||
left->moveToLeft(0, 0);
|
||||
right->moveToRight(padding.right() + st::lineWidth, padding.top());
|
||||
const auto maxRightSize = parent->width()
|
||||
- rect::m::sum::h(st::boxRowPadding)
|
||||
- rect::m::sum::h(padding)
|
||||
- leftSize.width();
|
||||
if ((rightSize.width() > maxRightSize) && (maxRightSize > 0)) {
|
||||
right->resizeToWidth(maxRightSize);
|
||||
}
|
||||
}, peerBubble->lifetime());
|
||||
peerBubble->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(peerBubble);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::windowBgOver);
|
||||
const auto rect = peerBubble->rect();
|
||||
const auto radius = rect.height() / 2;
|
||||
p.drawRoundedRect(rect, radius, radius);
|
||||
}, peerBubble->lifetime());
|
||||
|
||||
return owned;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
26
Telegram/SourceFiles/ui/widgets/peer_bubble.h
Normal file
26
Telegram/SourceFiles/ui/widgets/peer_bubble.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
template <typename Object>
|
||||
class object_ptr;
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui {
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> CreatePeerBubble(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
} // namespace Ui
|
||||
160
Telegram/SourceFiles/ui/widgets/sent_code_field.cpp
Normal file
160
Telegram/SourceFiles/ui/widgets/sent_code_field.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
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 "ui/widgets/sent_code_field.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
SentCodeField::SentCodeField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &val)
|
||||
: Ui::InputField(parent, st, std::move(placeholder), val) {
|
||||
changes() | rpl::on_next([=] { fix(); }, lifetime());
|
||||
}
|
||||
|
||||
void SentCodeField::setAutoSubmit(int length, Fn<void()> submitCallback) {
|
||||
_autoSubmitLength = length;
|
||||
_submitCallback = std::move(submitCallback);
|
||||
}
|
||||
|
||||
void SentCodeField::setChangedCallback(Fn<void()> changedCallback) {
|
||||
_changedCallback = std::move(changedCallback);
|
||||
}
|
||||
|
||||
QString SentCodeField::getDigitsOnly() const {
|
||||
return QString(
|
||||
getLastText()
|
||||
).remove(TextUtilities::RegExpDigitsExclude());
|
||||
}
|
||||
|
||||
void SentCodeField::fix() {
|
||||
if (_fixing) return;
|
||||
|
||||
_fixing = true;
|
||||
auto newText = QString();
|
||||
const auto now = getLastText();
|
||||
auto oldPos = textCursor().position();
|
||||
auto newPos = -1;
|
||||
auto oldLen = now.size();
|
||||
auto digitCount = 0;
|
||||
for (const auto &ch : now) {
|
||||
if (ch.isDigit()) {
|
||||
++digitCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (_autoSubmitLength > 0 && digitCount > _autoSubmitLength) {
|
||||
digitCount = _autoSubmitLength;
|
||||
}
|
||||
const auto strict = (_autoSubmitLength > 0)
|
||||
&& (digitCount == _autoSubmitLength);
|
||||
|
||||
newText.reserve(oldLen);
|
||||
int i = 0;
|
||||
for (const auto &ch : now) {
|
||||
if (i++ == oldPos) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
if (ch.isDigit()) {
|
||||
if (!digitCount--) {
|
||||
break;
|
||||
}
|
||||
newText += ch;
|
||||
if (strict && !digitCount) {
|
||||
break;
|
||||
}
|
||||
} else if (ch == '-') {
|
||||
newText += ch;
|
||||
}
|
||||
}
|
||||
if (newPos < 0) {
|
||||
newPos = newText.length();
|
||||
}
|
||||
if (newText != now) {
|
||||
setText(newText);
|
||||
setCursorPosition(newPos);
|
||||
}
|
||||
_fixing = false;
|
||||
|
||||
if (_changedCallback) {
|
||||
_changedCallback();
|
||||
}
|
||||
if (strict && _submitCallback) {
|
||||
_submitCallback();
|
||||
}
|
||||
}
|
||||
|
||||
SentCodeCall::SentCodeCall(
|
||||
FnMut<void()> callCallback,
|
||||
Fn<void()> updateCallback)
|
||||
: _call(std::move(callCallback))
|
||||
, _update(std::move(updateCallback)) {
|
||||
_timer.setCallback([=] {
|
||||
if (_status.state == State::Waiting) {
|
||||
if (--_status.timeout <= 0) {
|
||||
_status.state = State::Calling;
|
||||
_timer.cancel();
|
||||
if (_call) {
|
||||
_call();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_update) {
|
||||
_update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SentCodeCall::setStatus(const Status &status) {
|
||||
_status = status;
|
||||
if (_status.state == State::Waiting) {
|
||||
_timer.callEach(1000);
|
||||
}
|
||||
}
|
||||
|
||||
QString SentCodeCall::getText() const {
|
||||
switch (_status.state) {
|
||||
case State::Waiting: {
|
||||
if (_status.timeout >= 3600) {
|
||||
return tr::lng_code_call(
|
||||
tr::now,
|
||||
lt_minutes,
|
||||
(u"%1:%2"_q)
|
||||
.arg(_status.timeout / 3600)
|
||||
.arg((_status.timeout / 60) % 60, 2, 10, QChar('0')),
|
||||
lt_seconds,
|
||||
(u"%1"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));
|
||||
}
|
||||
return tr::lng_code_call(
|
||||
tr::now,
|
||||
lt_minutes,
|
||||
QString::number(_status.timeout / 60),
|
||||
lt_seconds,
|
||||
(u"%1"_q).arg(_status.timeout % 60, 2, 10, QChar('0')));
|
||||
} break;
|
||||
case State::Calling: return tr::lng_code_calling(tr::now);
|
||||
case State::Called: return tr::lng_code_called(tr::now);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void SentCodeCall::callDone() {
|
||||
if (_status.state == State::Calling) {
|
||||
_status.state = State::Called;
|
||||
if (_update) {
|
||||
_update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
74
Telegram/SourceFiles/ui/widgets/sent_code_field.h
Normal file
74
Telegram/SourceFiles/ui/widgets/sent_code_field.h
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class SentCodeField final : public Ui::InputField {
|
||||
public:
|
||||
SentCodeField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder = nullptr,
|
||||
const QString &val = QString());
|
||||
|
||||
void setAutoSubmit(int length, Fn<void()> submitCallback);
|
||||
void setChangedCallback(Fn<void()> changedCallback);
|
||||
[[nodiscard]] QString getDigitsOnly() const;
|
||||
|
||||
private:
|
||||
void fix();
|
||||
|
||||
// Flag for not calling onTextChanged() recursively.
|
||||
bool _fixing = false;
|
||||
|
||||
int _autoSubmitLength = 0;
|
||||
Fn<void()> _submitCallback;
|
||||
Fn<void()> _changedCallback;
|
||||
|
||||
};
|
||||
|
||||
class SentCodeCall final {
|
||||
public:
|
||||
SentCodeCall(
|
||||
FnMut<void()> callCallback,
|
||||
Fn<void()> updateCallback);
|
||||
|
||||
enum class State {
|
||||
Waiting,
|
||||
Calling,
|
||||
Called,
|
||||
Disabled,
|
||||
};
|
||||
struct Status {
|
||||
Status() {
|
||||
}
|
||||
Status(State state, int timeout) : state(state), timeout(timeout) {
|
||||
}
|
||||
|
||||
State state = State::Disabled;
|
||||
int timeout = 0;
|
||||
};
|
||||
void setStatus(const Status &status);
|
||||
|
||||
void callDone();
|
||||
|
||||
[[nodiscard]] QString getText() const;
|
||||
|
||||
private:
|
||||
Status _status;
|
||||
base::Timer _timer;
|
||||
FnMut<void()> _call;
|
||||
Fn<void()> _update;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
21
Telegram/SourceFiles/ui/widgets/slider_natural_width.h
Normal file
21
Telegram/SourceFiles/ui/widgets/slider_natural_width.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/discrete_sliders.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class CustomWidthSlider final : public SettingsSlider {
|
||||
public:
|
||||
using Ui::SettingsSlider::SettingsSlider;
|
||||
using SettingsSlider::setNaturalWidth;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
320
Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp
Normal file
320
Telegram/SourceFiles/ui/widgets/vertical_drum_picker.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
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 "ui/widgets/vertical_drum_picker.h"
|
||||
|
||||
#include "ui/effects/animation_value_f.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kAlmostIndex = float64(.99);
|
||||
constexpr auto kMinYScale = 0.2;
|
||||
|
||||
using PaintItemCallback = VerticalDrumPicker::PaintItemCallback;
|
||||
|
||||
} // namespace
|
||||
|
||||
PaintItemCallback VerticalDrumPicker::DefaultPaintCallback(
|
||||
const style::font &font,
|
||||
int itemHeight,
|
||||
Fn<void(QPainter&, QRectF, int)> paintContent) {
|
||||
return [=](
|
||||
QPainter &p,
|
||||
int index,
|
||||
float64 y,
|
||||
float64 distanceFromCenter,
|
||||
int outerWidth) {
|
||||
const auto r = QRectF(0, y, outerWidth, itemHeight);
|
||||
const auto progress = std::abs(distanceFromCenter);
|
||||
const auto revProgress = 1. - progress;
|
||||
p.save();
|
||||
p.translate(r.center());
|
||||
const auto yScale = kMinYScale
|
||||
+ (1. - kMinYScale) * anim::easeOutCubic(1., revProgress);
|
||||
p.scale(1., yScale);
|
||||
p.translate(-r.center());
|
||||
p.setOpacity(revProgress);
|
||||
p.setFont(font);
|
||||
p.setPen(st::defaultFlatLabel.textFg);
|
||||
paintContent(p, r, index);
|
||||
p.restore();
|
||||
};
|
||||
}
|
||||
|
||||
PickerAnimation::PickerAnimation() = default;
|
||||
|
||||
void PickerAnimation::jumpToOffset(int offset) {
|
||||
_result.from = _result.current;
|
||||
_result.to += offset;
|
||||
_animation.stop();
|
||||
auto callback = [=](float64 value) {
|
||||
const auto was = _result.current;
|
||||
_result.current = anim::interpolateF(
|
||||
_result.from,
|
||||
_result.to,
|
||||
value);
|
||||
_updates.fire(_result.current - was);
|
||||
};
|
||||
if (anim::Disabled()) {
|
||||
auto value = float64(0.);
|
||||
const auto diff = _result.to - _result.from;
|
||||
const auto step = std::min(
|
||||
kAlmostIndex,
|
||||
1. / (std::max(1. - kAlmostIndex, std::abs(diff) + 1)));
|
||||
while (true) {
|
||||
value += step;
|
||||
if (value >= 1.) {
|
||||
callback(1.);
|
||||
break;
|
||||
} else {
|
||||
callback(value);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
_animation.start(
|
||||
std::move(callback),
|
||||
0.,
|
||||
1.,
|
||||
st::fadeWrapDuration);
|
||||
}
|
||||
|
||||
void PickerAnimation::setResult(float64 from, float64 current, float64 to) {
|
||||
_result = { from, current, to };
|
||||
}
|
||||
|
||||
rpl::producer<PickerAnimation::Shift> PickerAnimation::updates() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
VerticalDrumPicker::VerticalDrumPicker(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PaintItemCallback &&paintCallback,
|
||||
int itemsCount,
|
||||
int itemHeight,
|
||||
int startIndex,
|
||||
bool looped)
|
||||
: RpWidget(parent)
|
||||
, _itemsCount(itemsCount)
|
||||
, _itemHeight(itemHeight)
|
||||
, _paintCallback(std::move(paintCallback))
|
||||
, _pendingStartIndex(startIndex)
|
||||
, _loopData({ .looped = looped }) {
|
||||
Expects(_paintCallback != nullptr);
|
||||
|
||||
sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
_itemsVisible.count = std::ceil(float64(s.height()) / _itemHeight);
|
||||
_itemsVisible.centerOffset = _itemsVisible.count / 2;
|
||||
if ((_pendingStartIndex >= 0) && _itemsVisible.count) {
|
||||
_index = normalizedIndex(_pendingStartIndex
|
||||
- _itemsVisible.centerOffset);
|
||||
_pendingStartIndex = -1;
|
||||
}
|
||||
|
||||
if (!_loopData.looped) {
|
||||
_loopData.minIndex = -_itemsVisible.centerOffset;
|
||||
_loopData.maxIndex = _itemsCount - 1 - _itemsVisible.centerOffset;
|
||||
}
|
||||
|
||||
_changes.fire({});
|
||||
}, lifetime());
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto outerWidth = width();
|
||||
const auto centerY = height() / 2.;
|
||||
const auto shiftedY = _itemHeight * _shift;
|
||||
for (auto i = -1; i < (_itemsVisible.count + 1); i++) {
|
||||
const auto index = normalizedIndex(i + _index);
|
||||
if (!isIndexInRange(index)) {
|
||||
continue;
|
||||
}
|
||||
const auto y = (_itemHeight * i + shiftedY);
|
||||
_paintCallback(
|
||||
p,
|
||||
index,
|
||||
y,
|
||||
((y + _itemHeight / 2.) - centerY) / centerY,
|
||||
outerWidth);
|
||||
}
|
||||
}, lifetime());
|
||||
|
||||
_animation.updates(
|
||||
) | rpl::on_next([=](PickerAnimation::Shift shift) {
|
||||
increaseShift(shift);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::increaseShift(float64 by) {
|
||||
{
|
||||
// Guard input.
|
||||
if (by >= 1.) {
|
||||
by = kAlmostIndex;
|
||||
} else if (by <= -1.) {
|
||||
by = -kAlmostIndex;
|
||||
}
|
||||
}
|
||||
|
||||
auto shift = _shift;
|
||||
auto index = _index;
|
||||
shift += by;
|
||||
if (shift >= 1.) {
|
||||
shift -= 1.;
|
||||
index--;
|
||||
index = normalizedIndex(index);
|
||||
} else if (shift <= -1.) {
|
||||
shift += 1.;
|
||||
index++;
|
||||
index = normalizedIndex(index);
|
||||
}
|
||||
if (_loopData.minIndex == _loopData.maxIndex) {
|
||||
_shift = 0.;
|
||||
} else if (!_loopData.looped && (index <= _loopData.minIndex)) {
|
||||
_shift = std::min(0., shift);
|
||||
_index = _loopData.minIndex;
|
||||
} else if (!_loopData.looped && (index >= _loopData.maxIndex)) {
|
||||
_shift = std::max(0., shift);
|
||||
_index = _loopData.maxIndex;
|
||||
} else {
|
||||
_shift = shift;
|
||||
_index = index;
|
||||
}
|
||||
_changes.fire({});
|
||||
update();
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::handleWheelEvent(not_null<QWheelEvent*> e) {
|
||||
const auto direction = Ui::WheelDirection(e);
|
||||
if (direction) {
|
||||
_animation.jumpToOffset(direction);
|
||||
} else {
|
||||
if (const auto delta = e->pixelDelta().y(); delta) {
|
||||
increaseShift(delta / float64(_itemHeight));
|
||||
} else if (e->phase() == Qt::ScrollEnd) {
|
||||
animationDataFromIndex();
|
||||
_animation.jumpToOffset(0);
|
||||
} else {
|
||||
constexpr auto step = int(QWheelEvent::DefaultDeltasPerStep);
|
||||
|
||||
_touch.verticalDelta += e->angleDelta().y();
|
||||
while (std::abs(_touch.verticalDelta) >= step) {
|
||||
if (_touch.verticalDelta < 0) {
|
||||
_touch.verticalDelta += step;
|
||||
_animation.jumpToOffset(1);
|
||||
} else {
|
||||
_touch.verticalDelta -= step;
|
||||
_animation.jumpToOffset(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::handleKeyEvent(not_null<QKeyEvent*> e) {
|
||||
if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Up) {
|
||||
_animation.jumpToOffset(1);
|
||||
} else if (e->key() == Qt::Key_PageUp && !e->isAutoRepeat()) {
|
||||
_animation.jumpToOffset(_itemsVisible.count);
|
||||
} else if (e->key() == Qt::Key_Right || e->key() == Qt::Key_Down) {
|
||||
_animation.jumpToOffset(-1);
|
||||
} else if (e->key() == Qt::Key_PageDown && !e->isAutoRepeat()) {
|
||||
_animation.jumpToOffset(-_itemsVisible.count);
|
||||
}
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::handleMouseEvent(not_null<QMouseEvent*> e) {
|
||||
if (e->type() == QEvent::MouseButtonPress) {
|
||||
_mouse.pressed = true;
|
||||
_mouse.lastPositionY = e->pos().y();
|
||||
} else if (e->type() == QEvent::MouseMove) {
|
||||
if (_mouse.pressed) {
|
||||
const auto was = _mouse.lastPositionY;
|
||||
_mouse.lastPositionY = e->pos().y();
|
||||
const auto diff = _mouse.lastPositionY - was;
|
||||
increaseShift(float64(diff) / _itemHeight);
|
||||
_mouse.clickDisabled = true;
|
||||
}
|
||||
} else if (e->type() == QEvent::MouseButtonRelease) {
|
||||
if (_mouse.clickDisabled) {
|
||||
animationDataFromIndex();
|
||||
_animation.jumpToOffset(0);
|
||||
} else {
|
||||
_mouse.lastPositionY = e->pos().y();
|
||||
const auto toOffset = _itemsVisible.centerOffset
|
||||
- (_mouse.lastPositionY / _itemHeight);
|
||||
_animation.jumpToOffset(toOffset);
|
||||
}
|
||||
_mouse = {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void VerticalDrumPicker::wheelEvent(QWheelEvent *e) {
|
||||
handleWheelEvent(e);
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::mousePressEvent(QMouseEvent *e) {
|
||||
handleMouseEvent(e);
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::mouseMoveEvent(QMouseEvent *e) {
|
||||
handleMouseEvent(e);
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::mouseReleaseEvent(QMouseEvent *e) {
|
||||
handleMouseEvent(e);
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::keyPressEvent(QKeyEvent *e) {
|
||||
handleKeyEvent(e);
|
||||
}
|
||||
|
||||
void VerticalDrumPicker::animationDataFromIndex() {
|
||||
_animation.setResult(
|
||||
_index,
|
||||
_index + _shift,
|
||||
std::round(_index + _shift));
|
||||
}
|
||||
|
||||
bool VerticalDrumPicker::isIndexInRange(int index) const {
|
||||
return (index >= 0) && (index < _itemsCount);
|
||||
}
|
||||
|
||||
int VerticalDrumPicker::normalizedIndex(int index) const {
|
||||
if (!_loopData.looped) {
|
||||
return index;
|
||||
}
|
||||
if (index < 0) {
|
||||
index += _itemsCount;
|
||||
} else if (index >= _itemsCount) {
|
||||
index -= _itemsCount;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
int VerticalDrumPicker::index() const {
|
||||
return normalizedIndex(_index + _itemsVisible.centerOffset);
|
||||
}
|
||||
|
||||
rpl::producer<int> VerticalDrumPicker::changes() const {
|
||||
return _changes.events() | rpl::map([=] { return index(); });
|
||||
}
|
||||
|
||||
rpl::producer<int> VerticalDrumPicker::value() const {
|
||||
return rpl::single(index())
|
||||
| rpl::then(changes())
|
||||
| rpl::distinct_until_changed();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
116
Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h
Normal file
116
Telegram/SourceFiles/ui/widgets/vertical_drum_picker.h
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PickerAnimation final {
|
||||
public:
|
||||
using Shift = float64;
|
||||
PickerAnimation();
|
||||
|
||||
void jumpToOffset(int offset);
|
||||
void setResult(float64 from, float64 current, float64 to);
|
||||
|
||||
[[nodiscard]] rpl::producer<Shift> updates() const;
|
||||
|
||||
private:
|
||||
Ui::Animations::Simple _animation;
|
||||
struct {
|
||||
float64 from = 0.;
|
||||
float64 current = 0.;
|
||||
float64 to = 0.;
|
||||
} _result;
|
||||
|
||||
rpl::event_stream<Shift> _updates;
|
||||
};
|
||||
|
||||
class VerticalDrumPicker final : public Ui::RpWidget {
|
||||
public:
|
||||
using PaintItemCallback = Fn<void(
|
||||
QPainter &p,
|
||||
int index,
|
||||
float y,
|
||||
float64 distanceFromCenter,
|
||||
int outerWidth)>;
|
||||
|
||||
VerticalDrumPicker(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
PaintItemCallback &&paintCallback,
|
||||
int itemsCount,
|
||||
int itemHeight,
|
||||
int startIndex = 0,
|
||||
bool looped = false);
|
||||
|
||||
[[nodiscard]] int index() const;
|
||||
[[nodiscard]] rpl::producer<int> changes() const;
|
||||
[[nodiscard]] rpl::producer<int> value() const;
|
||||
|
||||
void handleWheelEvent(not_null<QWheelEvent*> e);
|
||||
void handleMouseEvent(not_null<QMouseEvent*> e);
|
||||
void handleKeyEvent(not_null<QKeyEvent*> e);
|
||||
|
||||
static PaintItemCallback DefaultPaintCallback(
|
||||
const style::font &font,
|
||||
int itemHeight,
|
||||
Fn<void(QPainter&, QRectF, int)> paintContent);
|
||||
|
||||
protected:
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
void increaseShift(float64 by);
|
||||
void animationDataFromIndex();
|
||||
[[nodiscard]] int normalizedIndex(int index) const;
|
||||
[[nodiscard]] bool isIndexInRange(int index) const;
|
||||
|
||||
const int _itemsCount;
|
||||
const int _itemHeight;
|
||||
|
||||
PaintItemCallback _paintCallback;
|
||||
|
||||
int _pendingStartIndex = -1;
|
||||
|
||||
struct {
|
||||
int count = 0;
|
||||
int centerOffset = 0;
|
||||
} _itemsVisible;
|
||||
|
||||
int _index = 0;
|
||||
float64 _shift = 0.;
|
||||
rpl::event_stream<> _changes;
|
||||
|
||||
struct {
|
||||
const bool looped;
|
||||
int minIndex = 0;
|
||||
int maxIndex = 0;
|
||||
} _loopData;
|
||||
|
||||
PickerAnimation _animation;
|
||||
|
||||
struct {
|
||||
bool pressed = false;
|
||||
int lastPositionY;
|
||||
bool clickDisabled = false;
|
||||
} _mouse;
|
||||
|
||||
struct {
|
||||
int verticalDelta = 0;
|
||||
} _touch;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
Reference in New Issue
Block a user