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:
63
Telegram/lib_ui/ui/widgets/box_content_divider.cpp
Normal file
63
Telegram/lib_ui/ui/widgets/box_content_divider.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
BoxContentDivider::BoxContentDivider(
|
||||
QWidget *parent,
|
||||
int height,
|
||||
const style::DividerBar &st,
|
||||
RectParts parts)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _parts(parts) {
|
||||
resize(width(), height);
|
||||
}
|
||||
|
||||
void BoxContentDivider::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
p.fillRect(e->rect(), _st.bg);
|
||||
if (_parts & RectPart::Top) {
|
||||
paintTop(p);
|
||||
}
|
||||
if (_parts & RectPart::Bottom) {
|
||||
paintBottom(p);
|
||||
}
|
||||
}
|
||||
|
||||
void BoxContentDivider::paintTop(QPainter &p, int skip) {
|
||||
const auto dividerFillTop = QRect(
|
||||
0,
|
||||
skip,
|
||||
width(),
|
||||
_st.top.height());
|
||||
_st.top.fill(p, dividerFillTop);
|
||||
}
|
||||
|
||||
void BoxContentDivider::paintBottom(QPainter &p, int skip) {
|
||||
const auto dividerFillBottom = myrtlrect(
|
||||
0,
|
||||
height() - skip - _st.bottom.height(),
|
||||
width(),
|
||||
_st.bottom.height());
|
||||
_st.bottom.fill(p, dividerFillBottom);
|
||||
}
|
||||
|
||||
const style::color &BoxContentDivider::color() const {
|
||||
return _st.bg;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
49
Telegram/lib_ui/ui/widgets/box_content_divider.h
Normal file
49
Telegram/lib_ui/ui/widgets/box_content_divider.h
Normal file
@@ -0,0 +1,49 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/rect_part.h"
|
||||
|
||||
namespace style {
|
||||
struct DividerBar;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::DividerBar &defaultDividerBar;
|
||||
extern const int &boxDividerHeight;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class BoxContentDivider : public Ui::RpWidget {
|
||||
public:
|
||||
BoxContentDivider(
|
||||
QWidget *parent,
|
||||
int height = st::boxDividerHeight,
|
||||
const style::DividerBar &st = st::defaultDividerBar,
|
||||
RectParts parts = RectPart::Top | RectPart::Bottom);
|
||||
|
||||
[[nodiscard]] const style::color &color() const;
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::Separator;
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void paintTop(QPainter &p, int skip = 0);
|
||||
void paintBottom(QPainter &p, int skip = 0);
|
||||
|
||||
private:
|
||||
const style::DividerBar &_st;
|
||||
const RectParts _parts;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1044
Telegram/lib_ui/ui/widgets/buttons.cpp
Normal file
1044
Telegram/lib_ui/ui/widgets/buttons.cpp
Normal file
File diff suppressed because it is too large
Load Diff
368
Telegram/lib_ui/ui/widgets/buttons.h
Normal file
368
Telegram/lib_ui/ui/widgets/buttons.h
Normal file
@@ -0,0 +1,368 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace st {
|
||||
extern const style::SettingsButton &defaultSettingsButton;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RippleAnimation;
|
||||
class NumbersAnimation;
|
||||
class ToggleView;
|
||||
|
||||
class LinkButton : public AbstractButton {
|
||||
public:
|
||||
LinkButton(QWidget *parent, const QString &text, const style::LinkButton &st = st::defaultLinkButton);
|
||||
|
||||
void setText(const QString &text);
|
||||
void setColorOverride(std::optional<QColor> textFg);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::Link;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _text;
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
private:
|
||||
void resizeToText();
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
const style::LinkButton &_st;
|
||||
QString _text;
|
||||
int _textWidth = 0;
|
||||
std::optional<QColor> _textFgOverride;
|
||||
|
||||
};
|
||||
|
||||
class RippleButton : public AbstractButton {
|
||||
public:
|
||||
RippleButton(QWidget *parent, const style::RippleAnimation &st);
|
||||
|
||||
void setForceRippled(
|
||||
bool rippled,
|
||||
anim::type animated = anim::type::normal);
|
||||
bool forceRippled() const {
|
||||
return _forceRippled;
|
||||
}
|
||||
|
||||
static QPoint DisabledRippleStartPosition() {
|
||||
return QPoint(-0x3FFFFFFF, -0x3FFFFFFF);
|
||||
}
|
||||
|
||||
void clearState() override;
|
||||
|
||||
void paintRipple(
|
||||
QPainter &p,
|
||||
const QPoint &point,
|
||||
const QColor *colorOverride = nullptr);
|
||||
void paintRipple(
|
||||
QPainter &p,
|
||||
int x,
|
||||
int y,
|
||||
const QColor *colorOverride = nullptr);
|
||||
|
||||
void finishAnimating();
|
||||
|
||||
~RippleButton();
|
||||
|
||||
protected:
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
virtual QImage prepareRippleMask() const;
|
||||
virtual QPoint prepareRippleStartPosition() const;
|
||||
|
||||
private:
|
||||
void ensureRipple();
|
||||
|
||||
const style::RippleAnimation &_st;
|
||||
std::unique_ptr<RippleAnimation> _ripple;
|
||||
bool _forceRippled = false;
|
||||
rpl::lifetime _forceRippledSubscription;
|
||||
|
||||
};
|
||||
|
||||
class FlatButton : public RippleButton {
|
||||
public:
|
||||
FlatButton(QWidget *parent, const QString &text, const style::FlatButton &st);
|
||||
|
||||
QString accessibilityName() override {
|
||||
return _text;
|
||||
}
|
||||
|
||||
void setText(const QString &text);
|
||||
void setWidth(int w);
|
||||
void setColorOverride(std::optional<QColor> color);
|
||||
void setTextMargins(QMargins margins);
|
||||
|
||||
int32 textWidth() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
private:
|
||||
QString _text;
|
||||
QMargins _textMargins;
|
||||
int _width = 0;
|
||||
std::optional<QColor> _colorOverride;
|
||||
|
||||
const style::FlatButton &_st;
|
||||
|
||||
};
|
||||
|
||||
class RoundButton : public RippleButton {
|
||||
public:
|
||||
RoundButton(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> text,
|
||||
const style::RoundButton &st);
|
||||
|
||||
QString accessibilityName() override {
|
||||
return _textFull.current().text;
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::RoundButton &st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
void setText(rpl::producer<QString> text);
|
||||
void setText(rpl::producer<TextWithEntities> text);
|
||||
void setContext(const Text::MarkedContext &context);
|
||||
|
||||
void setNumbersText(const QString &numbersText) {
|
||||
setNumbersText(numbersText, numbersText.toInt());
|
||||
}
|
||||
void setNumbersText(int numbers) {
|
||||
setNumbersText(QString::number(numbers), numbers);
|
||||
}
|
||||
void setWidthChangedCallback(Fn<void()> callback);
|
||||
void setBrushOverride(std::optional<QBrush> brush);
|
||||
void setPenOverride(std::optional<QPen> pen);
|
||||
void setTextFgOverride(std::optional<QColor> textFg);
|
||||
void setIconOverride(const style::icon *icon);
|
||||
void finishNumbersAnimation();
|
||||
|
||||
[[nodiscard]] int contentWidth() const;
|
||||
|
||||
void setFullWidth(int newFullWidth);
|
||||
void setFullRadius(bool enabled);
|
||||
|
||||
enum class TextTransform {
|
||||
NoTransform,
|
||||
ToUpper,
|
||||
};
|
||||
void setTextTransform(TextTransform transform);
|
||||
|
||||
~RoundButton();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
private:
|
||||
void setNumbersText(const QString &numbersText, int numbers);
|
||||
void numbersAnimationCallback();
|
||||
void resizeToText(const TextWithEntities &text);
|
||||
[[nodiscard]] int addedWidth() const;
|
||||
|
||||
rpl::variable<TextWithEntities> _textFull;
|
||||
Ui::Text::String _text;
|
||||
|
||||
std::unique_ptr<NumbersAnimation> _numbers;
|
||||
|
||||
int _fullWidthOverride = 0;
|
||||
|
||||
const style::RoundButton &_st;
|
||||
std::optional<QBrush> _brushOverride;
|
||||
std::optional<QPen> _penOverride;
|
||||
std::optional<QColor> _textFgOverride;
|
||||
const style::icon *_iconOverride = nullptr;
|
||||
RoundRect _roundRect;
|
||||
RoundRect _roundRectOver;
|
||||
Text::MarkedContext _context;
|
||||
|
||||
TextTransform _transform = TextTransform::ToUpper;
|
||||
bool _fullRadius = false;
|
||||
|
||||
};
|
||||
|
||||
class IconButton : public RippleButton {
|
||||
public:
|
||||
IconButton(QWidget *parent, const style::IconButton &st);
|
||||
|
||||
[[nodiscard]] const style::IconButton &st() const;
|
||||
|
||||
// Pass nullptr to restore the default icon.
|
||||
void setIconOverride(const style::icon *iconOverride, const style::icon *iconOverOverride = nullptr);
|
||||
void setRippleColorOverride(const style::color *colorOverride);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
[[nodiscard]] float64 iconOverOpacity() const;
|
||||
|
||||
private:
|
||||
const style::IconButton &_st;
|
||||
const style::icon *_iconOverride = nullptr;
|
||||
const style::icon *_iconOverrideOver = nullptr;
|
||||
const style::color *_rippleColorOverride = nullptr;
|
||||
|
||||
Ui::Animations::Simple _a_over;
|
||||
|
||||
};
|
||||
|
||||
class CrossButton : public RippleButton {
|
||||
public:
|
||||
CrossButton(QWidget *parent, const style::CrossButton &st);
|
||||
|
||||
void toggle(bool shown, anim::type animated);
|
||||
void show(anim::type animated) {
|
||||
return toggle(true, animated);
|
||||
}
|
||||
void hide(anim::type animated) {
|
||||
return toggle(false, animated);
|
||||
}
|
||||
void finishAnimating() {
|
||||
_showAnimation.stop();
|
||||
animationCallback();
|
||||
}
|
||||
|
||||
bool toggled() const {
|
||||
return _shown;
|
||||
}
|
||||
void setLoadingAnimation(bool enabled);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
private:
|
||||
bool loadingCallback(crl::time now);
|
||||
bool stopLoadingAnimation(crl::time now);
|
||||
void animationCallback();
|
||||
|
||||
const style::CrossButton &_st;
|
||||
|
||||
bool _shown = false;
|
||||
Ui::Animations::Simple _showAnimation;
|
||||
|
||||
crl::time _loadingStopMs = 0;
|
||||
Ui::Animations::Basic _loadingAnimation;
|
||||
|
||||
};
|
||||
|
||||
class SettingsButton : public Ui::RippleButton {
|
||||
public:
|
||||
SettingsButton(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> &&text,
|
||||
const style::SettingsButton &st = st::defaultSettingsButton);
|
||||
SettingsButton(
|
||||
QWidget *parent,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
const style::SettingsButton &st = st::defaultSettingsButton,
|
||||
const Text::MarkedContext &context = {});
|
||||
SettingsButton(
|
||||
QWidget *parent,
|
||||
std::nullptr_t,
|
||||
const style::SettingsButton &st = st::defaultSettingsButton);
|
||||
~SettingsButton();
|
||||
|
||||
QString accessibilityName() override {
|
||||
return _text.toString();
|
||||
}
|
||||
|
||||
SettingsButton *toggleOn(
|
||||
rpl::producer<bool> &&toggled,
|
||||
bool ignoreClick = false);
|
||||
bool toggled() const;
|
||||
rpl::producer<bool> toggledChanges() const;
|
||||
rpl::producer<bool> toggledValue() const;
|
||||
|
||||
void setToggleLocked(bool locked);
|
||||
void setColorOverride(std::optional<QColor> textColorOverride);
|
||||
void setPaddingOverride(style::margins padding);
|
||||
|
||||
[[nodiscard]] const style::SettingsButton &st() const;
|
||||
[[nodiscard]] int fullTextWidth() const;
|
||||
|
||||
void finishAnimating();
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void onStateChanged(
|
||||
State was,
|
||||
StateChangeSource source) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void paintBg(Painter &p, const QRect &rect, bool over) const;
|
||||
void paintText(Painter &p, bool over, int outerw) const;
|
||||
void paintToggle(Painter &p, int outerw) const;
|
||||
|
||||
[[nodiscard]] QRect maybeToggleRect() const;
|
||||
|
||||
private:
|
||||
void setText(TextWithEntities &&text);
|
||||
[[nodiscard]] QRect toggleRect() const;
|
||||
|
||||
const style::SettingsButton &_st;
|
||||
style::margins _padding;
|
||||
Ui::Text::String _text;
|
||||
std::unique_ptr<Ui::ToggleView> _toggle;
|
||||
std::optional<QColor> _textColorOverride;
|
||||
Text::MarkedContext _context;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] not_null<RippleButton*> CreateSimpleRectButton(
|
||||
QWidget *parent,
|
||||
const style::RippleAnimation &st);
|
||||
[[nodiscard]] not_null<RippleButton*> CreateSimpleSettingsButton(
|
||||
QWidget *parent,
|
||||
const style::RippleAnimation &st,
|
||||
const style::color &bg);
|
||||
[[nodiscard]] not_null<RippleButton*> CreateSimpleCircleButton(
|
||||
QWidget *parent,
|
||||
const style::RippleAnimation &st);
|
||||
[[nodiscard]] not_null<RippleButton*> CreateSimpleRoundButton(
|
||||
QWidget *parent,
|
||||
const style::RippleAnimation &st);
|
||||
|
||||
} // namespace Ui
|
||||
281
Telegram/lib_ui/ui/widgets/call_button.cpp
Normal file
281
Telegram/lib_ui/ui/widgets/call_button.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/call_button.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/qt_object_factory.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kOuterBounceDuration = crl::time(100);
|
||||
|
||||
} // namespace
|
||||
|
||||
CallButton::CallButton(
|
||||
QWidget *parent,
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo)
|
||||
: RippleButton(parent, stFrom.button.ripple)
|
||||
, _stFrom(&stFrom)
|
||||
, _stTo(stTo) {
|
||||
init();
|
||||
}
|
||||
|
||||
void CallButton::init() {
|
||||
resize(_stFrom->button.width, _stFrom->button.height);
|
||||
|
||||
const auto size = QSize(_stFrom->bgSize, _stFrom->bgSize);
|
||||
_bgMask = RippleAnimation::MaskByDrawer(size, false, [&](QPainter &p) {
|
||||
p.drawEllipse(0, 0, size.width(), size.height());
|
||||
if (_corner) {
|
||||
auto position = _corner->pos() - _stFrom->bgPosition;
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(st::transparent);
|
||||
const auto border = _stFrom->cornerButtonBorder;
|
||||
p.drawEllipse(QRect(position, _corner->size()).marginsAdded(
|
||||
{ border, border, border, border }));
|
||||
}
|
||||
});
|
||||
_bgFrom = Ui::PixmapFromImage(style::colorizeImage(_bgMask, _stFrom->bg));
|
||||
if (_stTo) {
|
||||
Assert(_stFrom->button.width == _stTo->button.width);
|
||||
Assert(_stFrom->button.height == _stTo->button.height);
|
||||
Assert(_stFrom->bgPosition == _stTo->bgPosition);
|
||||
Assert(_stFrom->bgSize == _stTo->bgSize);
|
||||
|
||||
_bg = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
_bg.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_bgTo = Ui::PixmapFromImage(style::colorizeImage(_bgMask, _stTo->bg));
|
||||
_iconMixedMask = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
_iconMixedMask.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_iconFrom = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
_iconFrom.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_iconFrom.fill(Qt::black);
|
||||
{
|
||||
QPainter p(&_iconFrom);
|
||||
p.drawImage(
|
||||
(_stFrom->bgSize
|
||||
- _stFrom->button.icon.width()) / 2,
|
||||
(_stFrom->bgSize
|
||||
- _stFrom->button.icon.height()) / 2,
|
||||
_stFrom->button.icon.instance(Qt::white));
|
||||
}
|
||||
_iconTo = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
_iconTo.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
_iconTo.fill(Qt::black);
|
||||
{
|
||||
QPainter p(&_iconTo);
|
||||
p.drawImage(
|
||||
(_stTo->bgSize
|
||||
- _stTo->button.icon.width()) / 2,
|
||||
(_stTo->bgSize
|
||||
- _stTo->button.icon.height()) / 2,
|
||||
_stTo->button.icon.instance(Qt::white));
|
||||
}
|
||||
_iconMixed = QImage(_bgMask.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
_iconMixed.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
}
|
||||
}
|
||||
|
||||
void CallButton::setOuterValue(float64 value) {
|
||||
if (_outerValue != value) {
|
||||
_outerAnimation.start([this] {
|
||||
if (_progress == 0. || _progress == 1.) {
|
||||
update();
|
||||
}
|
||||
}, _outerValue, value, kOuterBounceDuration);
|
||||
_outerValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
void CallButton::setText(rpl::producer<QString> text) {
|
||||
_label.create(this, std::move(text), _stFrom->label);
|
||||
_label->show();
|
||||
rpl::combine(
|
||||
sizeValue(),
|
||||
_label->sizeValue()
|
||||
) | rpl::on_next([=](QSize my, QSize label) {
|
||||
_label->moveToLeft(
|
||||
(my.width() - label.width()) / 2,
|
||||
my.height() - label.height(),
|
||||
my.width());
|
||||
}, _label->lifetime());
|
||||
}
|
||||
|
||||
void CallButton::setProgress(float64 progress) {
|
||||
_progress = progress;
|
||||
if (_corner) {
|
||||
_corner->setProgress(progress);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void CallButton::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
auto bgPosition = myrtlpoint(_stFrom->bgPosition);
|
||||
auto paintFrom = (_progress == 0.) || !_stTo;
|
||||
auto paintTo = !paintFrom && (_progress == 1.);
|
||||
|
||||
auto outerValue = _outerAnimation.value(_outerValue);
|
||||
if (outerValue > 0.) {
|
||||
auto outerRadius = paintFrom ? _stFrom->outerRadius : paintTo ? _stTo->outerRadius : (_stFrom->outerRadius * (1. - _progress) + _stTo->outerRadius * _progress);
|
||||
auto outerPixels = outerValue * outerRadius;
|
||||
auto outerRect = QRectF(myrtlrect(bgPosition.x(), bgPosition.y(), _stFrom->bgSize, _stFrom->bgSize));
|
||||
outerRect = outerRect.marginsAdded(QMarginsF(outerPixels, outerPixels, outerPixels, outerPixels));
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
if (paintFrom) {
|
||||
p.setBrush(_stFrom->outerBg);
|
||||
} else if (paintTo) {
|
||||
p.setBrush(_stTo->outerBg);
|
||||
} else {
|
||||
p.setBrush(anim::brush(_stFrom->outerBg, _stTo->outerBg, _progress));
|
||||
}
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(outerRect);
|
||||
}
|
||||
|
||||
if (_bgOverride) {
|
||||
Assert(!_corner); // Didn't support this case yet.
|
||||
|
||||
const auto &s = _stFrom->bgSize;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(*_bgOverride);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawEllipse(QRect(_stFrom->bgPosition, QSize(s, s)));
|
||||
} else if (paintFrom) {
|
||||
p.drawPixmap(bgPosition, _bgFrom);
|
||||
} else if (paintTo) {
|
||||
p.drawPixmap(bgPosition, _bgTo);
|
||||
} else {
|
||||
style::colorizeImage(_bgMask, anim::color(_stFrom->bg, _stTo->bg, _progress), &_bg);
|
||||
p.drawImage(bgPosition, _bg);
|
||||
}
|
||||
|
||||
auto rippleColorInterpolated = QColor();
|
||||
auto rippleColorOverride = &rippleColorInterpolated;
|
||||
if (_rippleOverride) {
|
||||
rippleColorOverride = &(*_rippleOverride);
|
||||
} else if (paintFrom) {
|
||||
rippleColorOverride = nullptr;
|
||||
} else if (paintTo) {
|
||||
rippleColorOverride = &_stTo->button.ripple.color->c;
|
||||
} else {
|
||||
rippleColorInterpolated = anim::color(_stFrom->button.ripple.color, _stTo->button.ripple.color, _progress);
|
||||
}
|
||||
paintRipple(p, _stFrom->button.rippleAreaPosition, rippleColorOverride);
|
||||
|
||||
auto positionFrom = iconPosition(_stFrom);
|
||||
if (paintFrom) {
|
||||
const auto icon = &_stFrom->button.icon;
|
||||
icon->paint(p, positionFrom, width());
|
||||
} else {
|
||||
auto positionTo = iconPosition(_stTo);
|
||||
if (paintTo) {
|
||||
_stTo->button.icon.paint(p, positionTo, width());
|
||||
} else {
|
||||
mixIconMasks();
|
||||
style::colorizeImage(_iconMixedMask, st::callIconFg->c, &_iconMixed);
|
||||
p.drawImage(myrtlpoint(_stFrom->bgPosition), _iconMixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPoint CallButton::iconPosition(not_null<const style::CallButton*> st) const {
|
||||
auto result = st->button.iconPosition;
|
||||
if (result.x() < 0) {
|
||||
result.setX((width() - st->button.icon.width()) / 2);
|
||||
}
|
||||
if (result.y() < 0) {
|
||||
result.setY((height() - st->button.icon.height()) / 2);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void CallButton::mixIconMasks() {
|
||||
_iconMixedMask.fill(Qt::black);
|
||||
|
||||
Painter p(&_iconMixedMask);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
auto paintIconMask = [this, &p](const QImage &mask, float64 angle) {
|
||||
auto skipFrom = _stFrom->bgSize / 2;
|
||||
p.translate(skipFrom, skipFrom);
|
||||
p.rotate(angle);
|
||||
p.translate(-skipFrom, -skipFrom);
|
||||
p.drawImage(0, 0, mask);
|
||||
};
|
||||
p.save();
|
||||
paintIconMask(_iconFrom, (_stFrom->angle - _stTo->angle) * _progress);
|
||||
p.restore();
|
||||
p.setOpacity(_progress);
|
||||
paintIconMask(_iconTo, (_stTo->angle - _stFrom->angle) * (1. - _progress));
|
||||
}
|
||||
|
||||
void CallButton::onStateChanged(State was, StateChangeSource source) {
|
||||
RippleButton::onStateChanged(was, source);
|
||||
|
||||
auto over = isOver();
|
||||
auto wasOver = static_cast<bool>(was & StateFlag::Over);
|
||||
if (over != wasOver) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void CallButton::setColorOverrides(rpl::producer<CallButtonColors> &&colors) {
|
||||
std::move(
|
||||
colors
|
||||
) | rpl::on_next([=](const CallButtonColors &c) {
|
||||
_bgOverride = c.bg;
|
||||
_rippleOverride = c.ripple;
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void CallButton::setStyle(
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo) {
|
||||
if (_stFrom == &stFrom && _stTo == stTo) {
|
||||
return;
|
||||
}
|
||||
_stFrom = &stFrom;
|
||||
_stTo = stTo;
|
||||
init();
|
||||
update();
|
||||
}
|
||||
|
||||
not_null<CallButton*> CallButton::addCornerButton(
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo) {
|
||||
Expects(!_corner);
|
||||
|
||||
_corner = CreateChild<CallButton>(this, stFrom, stTo);
|
||||
_corner->move(_stFrom->cornerButtonPosition);
|
||||
_corner->setProgress(_progress);
|
||||
_corner->show();
|
||||
init();
|
||||
update();
|
||||
|
||||
return _corner;
|
||||
}
|
||||
|
||||
QPoint CallButton::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos()) - _stFrom->button.rippleAreaPosition;
|
||||
}
|
||||
|
||||
QImage CallButton::prepareRippleMask() const {
|
||||
return _bgMask;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
73
Telegram/lib_ui/ui/widgets/call_button.h
Normal file
73
Telegram/lib_ui/ui/widgets/call_button.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FlatLabel;
|
||||
|
||||
struct CallButtonColors {
|
||||
std::optional<QColor> bg;
|
||||
std::optional<QColor> ripple;
|
||||
};
|
||||
|
||||
class CallButton final : public RippleButton {
|
||||
public:
|
||||
CallButton(
|
||||
QWidget *parent,
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo = nullptr);
|
||||
|
||||
void setProgress(float64 progress);
|
||||
void setOuterValue(float64 value);
|
||||
void setText(rpl::producer<QString> text);
|
||||
void setColorOverrides(rpl::producer<CallButtonColors> &&colors);
|
||||
|
||||
void setStyle(
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo = nullptr);
|
||||
|
||||
[[nodiscard]] not_null<CallButton*> addCornerButton(
|
||||
const style::CallButton &stFrom,
|
||||
const style::CallButton *stTo = nullptr);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
void init();
|
||||
QPoint iconPosition(not_null<const style::CallButton*> st) const;
|
||||
void mixIconMasks();
|
||||
|
||||
not_null<const style::CallButton*> _stFrom;
|
||||
const style::CallButton *_stTo = nullptr;
|
||||
CallButton *_corner = nullptr;
|
||||
float64 _progress = 0.;
|
||||
|
||||
object_ptr<FlatLabel> _label = { nullptr };
|
||||
|
||||
std::optional<QColor> _bgOverride;
|
||||
std::optional<QColor> _rippleOverride;
|
||||
|
||||
QImage _bgMask, _bg;
|
||||
QPixmap _bgFrom, _bgTo;
|
||||
QImage _iconMixedMask, _iconFrom, _iconTo, _iconMixed;
|
||||
|
||||
float64 _outerValue = 0.;
|
||||
Animations::Simple _outerAnimation;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1097
Telegram/lib_ui/ui/widgets/checkbox.cpp
Normal file
1097
Telegram/lib_ui/ui/widgets/checkbox.cpp
Normal file
File diff suppressed because it is too large
Load Diff
439
Telegram/lib_ui/ui/widgets/checkbox.h
Normal file
439
Telegram/lib_ui/ui/widgets/checkbox.h
Normal file
@@ -0,0 +1,439 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class QPainter;
|
||||
class Painter;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractCheckView {
|
||||
public:
|
||||
AbstractCheckView(int duration, bool checked, Fn<void()> updateCallback);
|
||||
|
||||
void setChecked(bool checked, anim::type animated);
|
||||
void finishAnimating();
|
||||
void setUpdateCallback(Fn<void()> updateCallback);
|
||||
bool checked() const {
|
||||
return _checked;
|
||||
}
|
||||
void update();
|
||||
float64 currentAnimationValue();
|
||||
bool animating() const;
|
||||
|
||||
auto checkedChanges() const {
|
||||
return _checks.events();
|
||||
}
|
||||
auto checkedValue() const {
|
||||
return _checks.events_starting_with(checked());
|
||||
}
|
||||
|
||||
virtual QSize getSize() const = 0;
|
||||
|
||||
virtual void paint(QPainter &p, int left, int top, int outerWidth) = 0;
|
||||
virtual QImage prepareRippleMask() const = 0;
|
||||
virtual bool checkRippleStartPosition(QPoint position) const = 0;
|
||||
|
||||
virtual ~AbstractCheckView() = default;
|
||||
|
||||
private:
|
||||
virtual void checkedChangedHook(anim::type animated) {
|
||||
}
|
||||
|
||||
int _duration = 0;
|
||||
bool _checked = false;
|
||||
Fn<void()> _updateCallback;
|
||||
Ui::Animations::Simple _toggleAnimation;
|
||||
|
||||
rpl::event_stream<bool> _checks;
|
||||
|
||||
};
|
||||
|
||||
class CheckView : public AbstractCheckView {
|
||||
public:
|
||||
CheckView(
|
||||
const style::Check &st,
|
||||
bool checked,
|
||||
Fn<void()> updateCallback = nullptr);
|
||||
|
||||
void setStyle(const style::Check &st);
|
||||
|
||||
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;
|
||||
|
||||
void setUntoggledOverride(
|
||||
std::optional<QColor> untoggledOverride);
|
||||
|
||||
[[nodiscard]] static Fn<void()> PrepareNonToggledError(
|
||||
not_null<CheckView*> view,
|
||||
rpl::lifetime &lifetime);
|
||||
|
||||
private:
|
||||
QSize rippleSize() const;
|
||||
|
||||
not_null<const style::Check*> _st;
|
||||
std::optional<QColor> _untoggledOverride;
|
||||
|
||||
};
|
||||
|
||||
class RadioView : public AbstractCheckView {
|
||||
public:
|
||||
RadioView(
|
||||
const style::Radio &st,
|
||||
bool checked,
|
||||
Fn<void()> updateCallback = nullptr);
|
||||
|
||||
void setStyle(const style::Radio &st);
|
||||
|
||||
void setToggledOverride(std::optional<QColor> toggledOverride);
|
||||
void setUntoggledOverride(std::optional<QColor> untoggledOverride);
|
||||
|
||||
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;
|
||||
|
||||
private:
|
||||
QSize rippleSize() const;
|
||||
|
||||
not_null<const style::Radio*> _st;
|
||||
std::optional<QColor> _toggledOverride;
|
||||
std::optional<QColor> _untoggledOverride;
|
||||
|
||||
};
|
||||
|
||||
class ToggleView : public AbstractCheckView {
|
||||
public:
|
||||
ToggleView(
|
||||
const style::Toggle &st,
|
||||
bool checked,
|
||||
Fn<void()> updateCallback = nullptr);
|
||||
|
||||
void setStyle(const style::Toggle &st);
|
||||
|
||||
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;
|
||||
void setLocked(bool locked);
|
||||
|
||||
private:
|
||||
void paintXV(QPainter &p, int left, int top, int outerWidth, float64 toggled, const QBrush &brush);
|
||||
QSize rippleSize() const;
|
||||
|
||||
not_null<const style::Toggle*> _st;
|
||||
bool _locked = false;
|
||||
|
||||
};
|
||||
|
||||
class Checkbox : public RippleButton, public ClickHandlerHost {
|
||||
public:
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
bool checked = false,
|
||||
const style::Checkbox &st = st::defaultCheckbox,
|
||||
const style::Check &checkSt = st::defaultCheck);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
const TextWithEntities &text,
|
||||
bool checked = false,
|
||||
const style::Checkbox &st = st::defaultCheckbox,
|
||||
const style::Check &checkSt = st::defaultCheck);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
bool checked,
|
||||
const style::Checkbox &st,
|
||||
const style::Toggle &toggleSt);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> &&text,
|
||||
bool checked = false,
|
||||
const style::Checkbox &st = st::defaultCheckbox,
|
||||
const style::Check &checkSt = st::defaultCheck);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> &&text,
|
||||
bool checked,
|
||||
const style::Checkbox &st,
|
||||
const style::Toggle &toggleSt);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
const style::Checkbox &st,
|
||||
std::unique_ptr<AbstractCheckView> check);
|
||||
Checkbox(
|
||||
QWidget *parent,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
const style::Checkbox &st,
|
||||
std::unique_ptr<AbstractCheckView> check);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::CheckBox;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _text.toString();
|
||||
}
|
||||
AccessibilityState accessibilityState() const override;
|
||||
void accessibilityDoAction(const QString &name) override;
|
||||
|
||||
void setText(const QString &text);
|
||||
void setCheckAlignment(style::align alignment);
|
||||
void setAllowTextLines(int lines = 0);
|
||||
void setTextBreakEverywhere(bool allow = true);
|
||||
|
||||
void setLink(uint16 index, const ClickHandlerPtr &lnk);
|
||||
void setLinksTrusted();
|
||||
|
||||
using ClickHandlerFilter = Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)>;
|
||||
void setClickHandlerFilter(ClickHandlerFilter &&filter);
|
||||
|
||||
[[nodiscard]] bool checked() const;
|
||||
[[nodiscard]] rpl::producer<bool> checkedChanges() const;
|
||||
[[nodiscard]] rpl::producer<bool> checkedValue() const;
|
||||
enum class NotifyAboutChange {
|
||||
Notify,
|
||||
DontNotify,
|
||||
};
|
||||
void setChecked(
|
||||
bool checked,
|
||||
NotifyAboutChange notify = NotifyAboutChange::Notify);
|
||||
|
||||
void finishAnimating();
|
||||
|
||||
[[nodiscard]] QMargins getMargins() const override {
|
||||
return _st.margin;
|
||||
}
|
||||
|
||||
void updateCheck() {
|
||||
rtlupdate(checkRect());
|
||||
}
|
||||
[[nodiscard]] QRect checkRect() const;
|
||||
|
||||
[[nodiscard]] not_null<AbstractCheckView*> checkView() const {
|
||||
return _check.get();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void keyReleaseEvent(QKeyEvent *e) override;
|
||||
|
||||
void onStateChanged(State was, StateChangeSource source) override;
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
QImage prepareRippleMask() const override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
|
||||
virtual void handlePress();
|
||||
|
||||
private:
|
||||
void resizeToText();
|
||||
void setMarkedText(const TextWithEntities &text);
|
||||
void updateNaturalWidth();
|
||||
QPixmap grabCheckCache() const;
|
||||
int countTextMinWidth() const;
|
||||
Text::StateResult getTextState(const QPoint &m) const;
|
||||
[[nodiscard]] bool isSubmitEvent(not_null<QKeyEvent*> e) const;
|
||||
|
||||
const style::Checkbox &_st;
|
||||
std::unique_ptr<AbstractCheckView> _check;
|
||||
rpl::event_stream<bool> _checkedChanges;
|
||||
ClickHandlerPtr _activatingHandler;
|
||||
QPixmap _checkCache;
|
||||
|
||||
ClickHandlerFilter _clickHandlerFilter;
|
||||
|
||||
style::align _checkAlignment = style::al_left;
|
||||
Text::String _text;
|
||||
int _allowTextLines = 1;
|
||||
bool _textBreakEverywhere = false;
|
||||
|
||||
};
|
||||
|
||||
class Radiobutton;
|
||||
|
||||
class RadiobuttonGroup
|
||||
: public std::enable_shared_from_this<RadiobuttonGroup> {
|
||||
public:
|
||||
RadiobuttonGroup() = default;
|
||||
RadiobuttonGroup(int value) : _value(value), _hasValue(true) {
|
||||
}
|
||||
|
||||
void setChangedCallback(Fn<void(int value)> callback) {
|
||||
_changedCallback = std::move(callback);
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> changes() const {
|
||||
return _changes.events();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<int> value() const {
|
||||
return hasValue()
|
||||
? _changes.events_starting_with_copy(_value)
|
||||
: changes();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool hasValue() const {
|
||||
return _hasValue;
|
||||
}
|
||||
[[nodiscard]] int current() const {
|
||||
return _value;
|
||||
}
|
||||
void setValue(int value);
|
||||
|
||||
private:
|
||||
friend class Radiobutton;
|
||||
void registerButton(not_null<Radiobutton*> button);
|
||||
void unregisterButton(not_null<Radiobutton*> button);
|
||||
|
||||
int _value = 0;
|
||||
bool _hasValue = false;
|
||||
Fn<void(int value)> _changedCallback;
|
||||
rpl::event_stream<int> _changes;
|
||||
std::vector<not_null<Radiobutton*>> _buttons;
|
||||
|
||||
};
|
||||
|
||||
class Radiobutton : public Checkbox {
|
||||
public:
|
||||
Radiobutton(
|
||||
QWidget *parent,
|
||||
const std::shared_ptr<RadiobuttonGroup> &group,
|
||||
int value,
|
||||
const QString &text,
|
||||
const style::Checkbox &st = st::defaultCheckbox,
|
||||
const style::Radio &radioSt = st::defaultRadio);
|
||||
Radiobutton(
|
||||
QWidget *parent,
|
||||
const std::shared_ptr<RadiobuttonGroup> &group,
|
||||
int value,
|
||||
const QString &text,
|
||||
const style::Checkbox &st,
|
||||
std::unique_ptr<AbstractCheckView> check);
|
||||
~Radiobutton();
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::RadioButton;
|
||||
}
|
||||
|
||||
protected:
|
||||
void handlePress() override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
private:
|
||||
// Hide the names from Checkbox.
|
||||
[[nodiscard]] bool checked() const;
|
||||
void checkedChanges() const;
|
||||
void checkedValue() const;
|
||||
void setChecked(bool checked, NotifyAboutChange notify);
|
||||
void trackScreenReaderState();
|
||||
|
||||
[[nodiscard]] Checkbox *checkbox() {
|
||||
return this;
|
||||
}
|
||||
[[nodiscard]] const Checkbox *checkbox() const {
|
||||
return this;
|
||||
}
|
||||
|
||||
friend class RadiobuttonGroup;
|
||||
void handleNewGroupValue(int value);
|
||||
|
||||
std::shared_ptr<RadiobuttonGroup> _group;
|
||||
int _value = 0;
|
||||
|
||||
};
|
||||
|
||||
template <typename Enum>
|
||||
class Radioenum;
|
||||
|
||||
template <typename Enum>
|
||||
class RadioenumGroup : public RadiobuttonGroup {
|
||||
using Parent = RadiobuttonGroup;
|
||||
|
||||
[[nodiscard]] static Enum Cast(int value) {
|
||||
return static_cast<Enum>(value);
|
||||
}
|
||||
|
||||
public:
|
||||
RadioenumGroup() = default;
|
||||
RadioenumGroup(Enum value) : Parent(static_cast<int>(value)) {
|
||||
}
|
||||
|
||||
template <typename Callback>
|
||||
void setChangedCallback(Callback &&callback) {
|
||||
Parent::setChangedCallback([copy = std::move(callback)](int value) {
|
||||
copy(Cast(value));
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Enum> changes() const {
|
||||
return Parent::changes() | rpl::map(Cast);
|
||||
}
|
||||
[[nodiscard]] rpl::producer<Enum> value() const {
|
||||
return Parent::value() | rpl::map(Cast);
|
||||
}
|
||||
[[nodiscard]] Enum current() const {
|
||||
return Cast(Parent::current());
|
||||
}
|
||||
void setValue(Enum value) {
|
||||
Parent::setValue(static_cast<int>(value));
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename OtherEnum>
|
||||
friend class Radioenum;
|
||||
|
||||
};
|
||||
|
||||
template <typename Enum>
|
||||
class Radioenum : public Radiobutton {
|
||||
public:
|
||||
Radioenum(
|
||||
QWidget *parent,
|
||||
const std::shared_ptr<RadioenumGroup<Enum>> &group,
|
||||
Enum value,
|
||||
const QString &text,
|
||||
const style::Checkbox &st = st::defaultCheckbox)
|
||||
: Radiobutton(
|
||||
parent,
|
||||
group->shared_from_this(),
|
||||
static_cast<int>(value),
|
||||
text,
|
||||
st) {
|
||||
}
|
||||
Radioenum(
|
||||
QWidget *parent,
|
||||
const std::shared_ptr<RadioenumGroup<Enum>> &group,
|
||||
Enum value,
|
||||
const QString &text,
|
||||
const style::Checkbox &st,
|
||||
std::unique_ptr<AbstractCheckView> check)
|
||||
: Radiobutton(
|
||||
parent,
|
||||
group->shared_from_this(),
|
||||
static_cast<int>(value),
|
||||
text,
|
||||
st,
|
||||
std::move(check)) {
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
292
Telegram/lib_ui/ui/widgets/dropdown_menu.cpp
Normal file
292
Telegram/lib_ui/ui/widgets/dropdown_menu.cpp
Normal file
@@ -0,0 +1,292 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/dropdown_menu.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
DropdownMenu::DropdownMenu(QWidget *parent, const style::DropdownMenu &st) : InnerDropdown(parent, st.wrap)
|
||||
, _st(st) {
|
||||
_menu = setOwnedWidget(object_ptr<Menu::Menu>(this, _st.menu));
|
||||
init();
|
||||
}
|
||||
|
||||
// Not ready with submenus yet.
|
||||
//DropdownMenu::DropdownMenu(QWidget *parent, QMenu *menu, const style::DropdownMenu &st) : InnerDropdown(parent, st.wrap)
|
||||
//, _st(st) {
|
||||
// _menu = setOwnedWidget(object_ptr<Menu>(this, menu, _st.menu));
|
||||
// init();
|
||||
//
|
||||
// for (auto action : actions()) {
|
||||
// if (auto submenu = action->menu()) {
|
||||
// auto it = _submenus.insert(action, new DropdownMenu(submenu, st));
|
||||
// it.value()->deleteOnHide(false);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
void DropdownMenu::init() {
|
||||
InnerDropdown::setHiddenCallback([this] { hideFinish(); });
|
||||
|
||||
_menu->resizesFromInner(
|
||||
) | rpl::on_next([=] {
|
||||
resizeToContent();
|
||||
}, _menu->lifetime());
|
||||
_menu->setActivatedCallback([this](const Menu::CallbackData &data) {
|
||||
handleActivated(data);
|
||||
});
|
||||
_menu->setTriggeredCallback([this](const Menu::CallbackData &data) {
|
||||
handleTriggered(data);
|
||||
});
|
||||
_menu->setKeyPressDelegate([this](int key) { return handleKeyPress(key); });
|
||||
_menu->setMouseMoveDelegate([this](QPoint globalPosition) { handleMouseMove(globalPosition); });
|
||||
_menu->setMousePressDelegate([this](QPoint globalPosition) { handleMousePress(globalPosition); });
|
||||
_menu->setMouseReleaseDelegate([this](QPoint globalPosition) { handleMouseRelease(globalPosition); });
|
||||
|
||||
setMouseTracking(true);
|
||||
|
||||
hide();
|
||||
}
|
||||
|
||||
not_null<QAction*> DropdownMenu::addAction(
|
||||
base::unique_qptr<Menu::ItemBase> widget) {
|
||||
return _menu->addAction(std::move(widget));
|
||||
}
|
||||
|
||||
not_null<QAction*> DropdownMenu::addAction(const QString &text, Fn<void()> callback, const style::icon *icon, const style::icon *iconOver) {
|
||||
return _menu->addAction(text, std::move(callback), icon, iconOver);
|
||||
}
|
||||
|
||||
not_null<QAction*> DropdownMenu::addSeparator(
|
||||
const style::MenuSeparator *st) {
|
||||
return _menu->addSeparator(st);
|
||||
}
|
||||
|
||||
void DropdownMenu::clearActions() {
|
||||
//for (auto submenu : base::take(_submenus)) {
|
||||
// delete submenu;
|
||||
//}
|
||||
return _menu->clearActions();
|
||||
}
|
||||
|
||||
const std::vector<not_null<QAction*>> &DropdownMenu::actions() const {
|
||||
return _menu->actions();
|
||||
}
|
||||
|
||||
bool DropdownMenu::empty() const {
|
||||
return _menu->empty();
|
||||
}
|
||||
|
||||
void DropdownMenu::handleActivated(const Menu::CallbackData &data) {
|
||||
if (data.source == TriggeredSource::Mouse) {
|
||||
if (!popupSubmenuFromAction(data)) {
|
||||
if (auto currentSubmenu = base::take(_activeSubmenu)) {
|
||||
currentSubmenu->hideMenu(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::handleTriggered(const Menu::CallbackData &data) {
|
||||
if (!popupSubmenuFromAction(data)) {
|
||||
hideMenu();
|
||||
_triggering = true;
|
||||
data.action->trigger();
|
||||
_triggering = false;
|
||||
if (_deleteLater) {
|
||||
_deleteLater = false;
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not ready with submenus yet.
|
||||
bool DropdownMenu::popupSubmenuFromAction(const Menu::CallbackData &data) {
|
||||
//if (auto submenu = _submenus.value(action)) {
|
||||
// if (_activeSubmenu == submenu) {
|
||||
// submenu->hideMenu(true);
|
||||
// } else {
|
||||
// popupSubmenu(submenu, actionTop, source);
|
||||
// }
|
||||
// return true;
|
||||
//}
|
||||
return false;
|
||||
}
|
||||
|
||||
//void DropdownMenu::popupSubmenu(SubmenuPointer submenu, int actionTop, TriggeredSource source) {
|
||||
// if (auto currentSubmenu = base::take(_activeSubmenu)) {
|
||||
// currentSubmenu->hideMenu(true);
|
||||
// }
|
||||
// if (submenu) {
|
||||
// auto menuTopLeft = mapFromGlobal(_menu->mapToGlobal(QPoint(0, 0)));
|
||||
// auto menuBottomRight = mapFromGlobal(_menu->mapToGlobal(QPoint(_menu->width(), _menu->height())));
|
||||
// QPoint p(menuTopLeft.x() + (rtl() ? (width() - menuBottomRight.x()) : menuBottomRight.x()), menuTopLeft.y() + actionTop);
|
||||
// _activeSubmenu = submenu;
|
||||
// _activeSubmenu->showMenu(geometry().topLeft() + p, this, source);
|
||||
//
|
||||
// _menu->setChildShown(true);
|
||||
// } else {
|
||||
// _menu->setChildShown(false);
|
||||
// }
|
||||
//}
|
||||
|
||||
void DropdownMenu::forwardKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!handleKeyPress(e->key())) {
|
||||
_menu->handleKeyPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
bool DropdownMenu::handleKeyPress(int key) {
|
||||
if (_activeSubmenu) {
|
||||
_activeSubmenu->handleKeyPress(key);
|
||||
return true;
|
||||
} else if (key == Qt::Key_Escape) {
|
||||
hideMenu(_parent ? true : false);
|
||||
return true;
|
||||
} else if (key == (style::RightToLeft() ? Qt::Key_Right : Qt::Key_Left)) {
|
||||
if (_parent) {
|
||||
hideMenu(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DropdownMenu::handleMouseMove(QPoint globalPosition) {
|
||||
if (_parent) {
|
||||
_parent->forwardMouseMove(globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::handleMousePress(QPoint globalPosition) {
|
||||
if (_parent) {
|
||||
_parent->forwardMousePress(globalPosition);
|
||||
} else {
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::handleMouseRelease(QPoint globalPosition) {
|
||||
if (_parent) {
|
||||
_parent->forwardMouseRelease(globalPosition);
|
||||
} else {
|
||||
hideMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::focusOutEvent(QFocusEvent *e) {
|
||||
hideMenu();
|
||||
}
|
||||
|
||||
void DropdownMenu::hideEvent(QHideEvent *e) {
|
||||
if (_deleteOnHide) {
|
||||
if (_triggering) {
|
||||
_deleteLater = true;
|
||||
} else {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::keyPressEvent(QKeyEvent *e) {
|
||||
forwardKeyPress(e);
|
||||
}
|
||||
|
||||
void DropdownMenu::mouseMoveEvent(QMouseEvent *e) {
|
||||
forwardMouseMove(e->globalPos());
|
||||
}
|
||||
|
||||
void DropdownMenu::mousePressEvent(QMouseEvent *e) {
|
||||
forwardMousePress(e->globalPos());
|
||||
}
|
||||
|
||||
void DropdownMenu::hideMenu(bool fast) {
|
||||
if (isHidden()) return;
|
||||
if (_parent && !isHiding()) {
|
||||
_parent->childHiding(this);
|
||||
}
|
||||
if (fast) {
|
||||
hideFast();
|
||||
} else {
|
||||
hideAnimated();
|
||||
if (_parent) {
|
||||
_parent->hideMenu();
|
||||
}
|
||||
}
|
||||
if (_activeSubmenu) {
|
||||
_activeSubmenu->hideMenu(fast);
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::childHiding(DropdownMenu *child) {
|
||||
if (_activeSubmenu && _activeSubmenu == child) {
|
||||
_activeSubmenu = SubmenuPointer();
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownMenu::hideFinish() {
|
||||
_menu->clearSelection();
|
||||
if (const auto onstack = _hiddenCallback) {
|
||||
onstack();
|
||||
}
|
||||
}
|
||||
|
||||
// Not ready with submenus yet.
|
||||
//void DropdownMenu::deleteOnHide(bool del) {
|
||||
// _deleteOnHide = del;
|
||||
//}
|
||||
|
||||
//void DropdownMenu::popup(const QPoint &p) {
|
||||
// showMenu(p, nullptr, TriggeredSource::Mouse);
|
||||
//}
|
||||
//
|
||||
//void DropdownMenu::showMenu(const QPoint &p, DropdownMenu *parent, TriggeredSource source) {
|
||||
// _parent = parent;
|
||||
//
|
||||
// auto menuTopLeft = mapFromGlobal(_menu->mapToGlobal(QPoint(0, 0)));
|
||||
// auto w = p - QPoint(0, menuTopLeft.y());
|
||||
// auto r = QApplication::desktop()->screenGeometry(p);
|
||||
// if (rtl()) {
|
||||
// if (w.x() - width() < r.x() - _padding.left()) {
|
||||
// if (_parent && w.x() + _parent->width() - _padding.left() - _padding.right() + width() - _padding.right() <= r.x() + r.width()) {
|
||||
// w.setX(w.x() + _parent->width() - _padding.left() - _padding.right());
|
||||
// } else {
|
||||
// w.setX(r.x() - _padding.left());
|
||||
// }
|
||||
// } else {
|
||||
// w.setX(w.x() - width());
|
||||
// }
|
||||
// } else {
|
||||
// if (w.x() + width() - _padding.right() > r.x() + r.width()) {
|
||||
// if (_parent && w.x() - _parent->width() + _padding.left() + _padding.right() - width() + _padding.right() >= r.x() - _padding.left()) {
|
||||
// w.setX(w.x() + _padding.left() + _padding.right() - _parent->width() - width() + _padding.left() + _padding.right());
|
||||
// } else {
|
||||
// w.setX(r.x() + r.width() - width() + _padding.right());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (w.y() + height() - _padding.bottom() > r.y() + r.height()) {
|
||||
// if (_parent) {
|
||||
// w.setY(r.y() + r.height() - height() + _padding.bottom());
|
||||
// } else {
|
||||
// w.setY(p.y() - height() + _padding.bottom());
|
||||
// }
|
||||
// }
|
||||
// if (w.y() < r.y()) {
|
||||
// w.setY(r.y());
|
||||
// }
|
||||
// move(w);
|
||||
//
|
||||
// _menu->setShowSource(source);
|
||||
//}
|
||||
|
||||
DropdownMenu::~DropdownMenu() {
|
||||
clearActions();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
103
Telegram/lib_ui/ui/widgets/dropdown_menu.h
Normal file
103
Telegram/lib_ui/ui/widgets/dropdown_menu.h
Normal file
@@ -0,0 +1,103 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
#include "ui/widgets/inner_dropdown.h"
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class DropdownMenu : public InnerDropdown {
|
||||
public:
|
||||
DropdownMenu(QWidget *parent, const style::DropdownMenu &st = st::defaultDropdownMenu);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::PopupMenu;
|
||||
}
|
||||
|
||||
not_null<QAction*> addAction(base::unique_qptr<Menu::ItemBase> widget);
|
||||
not_null<QAction*> addAction(const QString &text, Fn<void()> callback, const style::icon *icon = nullptr, const style::icon *iconOver = nullptr);
|
||||
not_null<QAction*> addSeparator(
|
||||
const style::MenuSeparator *st = nullptr);
|
||||
void clearActions();
|
||||
|
||||
void setHiddenCallback(Fn<void()> callback) {
|
||||
_hiddenCallback = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::vector<not_null<QAction*>> &actions() const;
|
||||
[[nodiscard]] bool empty() const;
|
||||
|
||||
[[nodiscard]] not_null<Menu::Menu*> menu() const {
|
||||
return _menu;
|
||||
}
|
||||
|
||||
~DropdownMenu();
|
||||
|
||||
protected:
|
||||
void focusOutEvent(QFocusEvent *e) override;
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
// Not ready with submenus yet.
|
||||
DropdownMenu(QWidget *parent, QMenu *menu, const style::DropdownMenu &st = st::defaultDropdownMenu);
|
||||
void deleteOnHide(bool del);
|
||||
void popup(const QPoint &p);
|
||||
void hideMenu(bool fast = false);
|
||||
|
||||
void childHiding(DropdownMenu *child);
|
||||
|
||||
void init();
|
||||
void hideFinish();
|
||||
|
||||
using TriggeredSource = Menu::TriggeredSource;
|
||||
void handleActivated(const Menu::CallbackData &data);
|
||||
void handleTriggered(const Menu::CallbackData &data);
|
||||
void forwardKeyPress(not_null<QKeyEvent*> e);
|
||||
bool handleKeyPress(int key);
|
||||
void forwardMouseMove(QPoint globalPosition) {
|
||||
_menu->handleMouseMove(globalPosition);
|
||||
}
|
||||
void handleMouseMove(QPoint globalPosition);
|
||||
void forwardMousePress(QPoint globalPosition) {
|
||||
_menu->handleMousePress(globalPosition);
|
||||
}
|
||||
void handleMousePress(QPoint globalPosition);
|
||||
void forwardMouseRelease(QPoint globalPosition) {
|
||||
_menu->handleMouseRelease(globalPosition);
|
||||
}
|
||||
void handleMouseRelease(QPoint globalPosition);
|
||||
|
||||
using SubmenuPointer = QPointer<DropdownMenu>;
|
||||
bool popupSubmenuFromAction(const Menu::CallbackData &data);
|
||||
void popupSubmenu(SubmenuPointer submenu, int actionTop, TriggeredSource source);
|
||||
void showMenu(const QPoint &p, DropdownMenu *parent, TriggeredSource source);
|
||||
|
||||
const style::DropdownMenu &_st;
|
||||
Fn<void()> _hiddenCallback;
|
||||
|
||||
QPointer<Menu::Menu> _menu;
|
||||
|
||||
// Not ready with submenus yet.
|
||||
//using Submenus = QMap<QAction*, SubmenuPointer>;
|
||||
//Submenus _submenus;
|
||||
|
||||
DropdownMenu *_parent = nullptr;
|
||||
|
||||
SubmenuPointer _activeSubmenu;
|
||||
|
||||
bool _deleteOnHide = false;
|
||||
bool _triggering = false;
|
||||
bool _deleteLater = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1339
Telegram/lib_ui/ui/widgets/elastic_scroll.cpp
Normal file
1339
Telegram/lib_ui/ui/widgets/elastic_scroll.cpp
Normal file
File diff suppressed because it is too large
Load Diff
299
Telegram/lib_ui/ui/widgets/elastic_scroll.h
Normal file
299
Telegram/lib_ui/ui/widgets/elastic_scroll.h
Normal file
@@ -0,0 +1,299 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/scroll_area.h" // For helpers, like ScrollToRequest.
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
namespace style {
|
||||
struct ScrollArea;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::ScrollArea &defaultScrollArea;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ScrollState {
|
||||
int visibleFrom = 0;
|
||||
int visibleTill = 0;
|
||||
int fullSize = 0;
|
||||
|
||||
friend inline constexpr auto operator<=>(
|
||||
const ScrollState &,
|
||||
const ScrollState &) = default;
|
||||
friend inline constexpr bool operator==(
|
||||
const ScrollState &,
|
||||
const ScrollState &) = default;
|
||||
};
|
||||
|
||||
class ElasticScrollBar final : public RpWidget {
|
||||
public:
|
||||
ElasticScrollBar(
|
||||
QWidget *parent,
|
||||
const style::ScrollArea &st,
|
||||
Qt::Orientation orientation = Qt::Vertical);
|
||||
|
||||
void updateState(ScrollState state);
|
||||
void toggle(bool shown, anim::type animated = anim::type::normal);
|
||||
|
||||
[[nodiscard]] rpl::producer<int> visibleFromDragged() const;
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
bool eventHook(QEvent *e) override;
|
||||
|
||||
[[nodiscard]] int scaleToBar(int change) const;
|
||||
[[nodiscard]] bool barHighlighted() const;
|
||||
void toggleOver(bool over, anim::type animated = anim::type::normal);
|
||||
void toggleOverBar(bool over, anim::type animated = anim::type::normal);
|
||||
void toggleDragging(
|
||||
bool dragging,
|
||||
anim::type animated = anim::type::normal);
|
||||
void startBarHighlightAnimation(bool wasHighlighted);
|
||||
void refreshGeometry();
|
||||
|
||||
const style::ScrollArea &_st;
|
||||
Ui::Animations::Simple _shownAnimation;
|
||||
Ui::Animations::Simple _overAnimation;
|
||||
Ui::Animations::Simple _barHighlightAnimation;
|
||||
base::Timer _hideTimer;
|
||||
rpl::event_stream<int> _visibleFromDragged;
|
||||
int _dragOverscrollAccumulated = 0;
|
||||
QRect _area;
|
||||
QRect _bar;
|
||||
QPoint _dragPosition;
|
||||
ScrollState _state;
|
||||
bool _shown : 1 = false;
|
||||
bool _over : 1 = false;
|
||||
bool _overBar : 1 = false;
|
||||
bool _vertical : 1 = false;
|
||||
bool _dragging : 1 = false;
|
||||
|
||||
};
|
||||
|
||||
struct ElasticScrollPosition {
|
||||
int value = 0;
|
||||
int overscroll = 0;
|
||||
|
||||
friend inline auto operator<=>(
|
||||
ElasticScrollPosition,
|
||||
ElasticScrollPosition) = default;
|
||||
friend inline bool operator==(
|
||||
ElasticScrollPosition,
|
||||
ElasticScrollPosition) = default;
|
||||
};
|
||||
|
||||
enum class ElasticScrollMovement {
|
||||
None,
|
||||
Progress,
|
||||
Momentum,
|
||||
Returning,
|
||||
};
|
||||
|
||||
class ElasticScroll final : public RpWidget {
|
||||
public:
|
||||
ElasticScroll(
|
||||
QWidget *parent,
|
||||
const style::ScrollArea &st = st::defaultScrollArea,
|
||||
Qt::Orientation orientation = Qt::Vertical);
|
||||
~ElasticScroll();
|
||||
|
||||
void setHandleTouch(bool handle);
|
||||
bool viewportEvent(QEvent *e);
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
QWidget *viewport() const; // Dummy.
|
||||
|
||||
int scrollWidth() const;
|
||||
int scrollHeight() const;
|
||||
int scrollLeftMax() const;
|
||||
int scrollTopMax() const;
|
||||
int scrollLeft() const;
|
||||
int scrollTop() const;
|
||||
|
||||
template <typename Widget>
|
||||
QPointer<Widget> setOwnedWidget(object_ptr<Widget> widget) {
|
||||
auto result = QPointer<Widget>(widget);
|
||||
doSetOwnedWidget(std::move(widget));
|
||||
return result;
|
||||
}
|
||||
template <typename Widget>
|
||||
object_ptr<Widget> takeWidget() {
|
||||
return object_ptr<Widget>::fromRaw(
|
||||
static_cast<Widget*>(doTakeWidget().release()));
|
||||
}
|
||||
|
||||
void updateBars();
|
||||
|
||||
auto scrollTopValue() const {
|
||||
return _vertical
|
||||
? _scrollValueUpdated.events_starting_with(scrollTop())
|
||||
: (rpl::single(0) | rpl::type_erased);
|
||||
}
|
||||
auto scrollTopChanges() const {
|
||||
return _vertical
|
||||
? _scrollValueUpdated.events()
|
||||
: (rpl::never<int>() | rpl::type_erased);
|
||||
}
|
||||
auto scrollLeftValue() const {
|
||||
return _vertical
|
||||
? (rpl::single(0) | rpl::type_erased)
|
||||
: _scrollValueUpdated.events_starting_with(scrollLeft());
|
||||
}
|
||||
auto scrollLeftChanges() const {
|
||||
return _vertical
|
||||
? (rpl::never<int>() | rpl::type_erased)
|
||||
: _scrollValueUpdated.events();
|
||||
}
|
||||
|
||||
void scrollTo(ScrollToRequest request);
|
||||
void scrollToWidget(not_null<QWidget*> widget);
|
||||
void scrollToY(int toTop, int toBottom = -1);
|
||||
void scrollTo(int toFrom, int toTill = -1);
|
||||
void disableScroll(bool dis);
|
||||
void innerResized();
|
||||
|
||||
void setCustomWheelProcess(Fn<bool(not_null<QWheelEvent*>)> process) {
|
||||
_customWheelProcess = std::move(process);
|
||||
}
|
||||
void setCustomTouchProcess(Fn<bool(not_null<QTouchEvent*>)> process) {
|
||||
_customTouchProcess = std::move(process);
|
||||
}
|
||||
|
||||
enum class OverscrollType : uchar {
|
||||
None,
|
||||
Virtual,
|
||||
Real,
|
||||
};
|
||||
void setOverscrollTypes(OverscrollType from, OverscrollType till);
|
||||
void setOverscrollDefaults(int from, int till, bool shift = false);
|
||||
void setOverscrollBg(QColor bg);
|
||||
|
||||
[[nodiscard]] rpl::producer<> scrolls() const;
|
||||
[[nodiscard]] rpl::producer<> innerResizes() const;
|
||||
[[nodiscard]] rpl::producer<> geometryChanged() const;
|
||||
|
||||
using Position = ElasticScrollPosition;
|
||||
[[nodiscard]] Position position() const;
|
||||
[[nodiscard]] rpl::producer<Position> positionValue() const;
|
||||
|
||||
using Movement = ElasticScrollMovement;
|
||||
[[nodiscard]] Movement movement() const;
|
||||
[[nodiscard]] rpl::producer<Movement> movementValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> touchMaybePressing() const;
|
||||
|
||||
private:
|
||||
bool eventHook(QEvent *e) override;
|
||||
bool eventFilter(QObject *obj, QEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void moveEvent(QMoveEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
bool handleWheelEvent(not_null<QWheelEvent*> e, bool touch = false);
|
||||
void handleTouchEvent(QTouchEvent *e);
|
||||
|
||||
void updateState();
|
||||
void setState(ScrollState state);
|
||||
[[nodiscard]] int willScrollTo(int position) const;
|
||||
void tryScrollTo(int position, bool synthMouseMove = true);
|
||||
void applyScrollTo(int position, bool synthMouseMove = true);
|
||||
void applyOverscroll(int overscroll);
|
||||
|
||||
void doSetOwnedWidget(object_ptr<QWidget> widget);
|
||||
object_ptr<QWidget> doTakeWidget();
|
||||
|
||||
bool filterOutTouchEvent(QEvent *e);
|
||||
void touchScrollTimer();
|
||||
void touchScrollUpdated();
|
||||
void sendWheelEvent(Qt::ScrollPhase phase, QPoint delta = {});
|
||||
|
||||
void touchResetSpeed();
|
||||
void touchUpdateSpeed();
|
||||
void touchDeaccelerate(int32 elapsed);
|
||||
|
||||
struct AccumulatedParts {
|
||||
int base = 0;
|
||||
int relative = 0;
|
||||
};
|
||||
[[nodiscard]] AccumulatedParts computeAccumulatedParts() const;
|
||||
[[nodiscard]] int currentOverscrollDefault() const;
|
||||
[[nodiscard]] int currentOverscrollDefaultAccumulated() const;
|
||||
void overscrollReturn();
|
||||
void overscrollReturnCancel();
|
||||
void overscrollCheckReturnFinish();
|
||||
bool overscrollFinish();
|
||||
void applyAccumulatedScroll();
|
||||
|
||||
const style::ScrollArea &_st;
|
||||
std::unique_ptr<ElasticScrollBar> _bar;
|
||||
ScrollState _state;
|
||||
|
||||
base::Timer _touchTimer;
|
||||
base::Timer _touchScrollTimer;
|
||||
QPoint _touchStart;
|
||||
QPoint _touchPreviousPosition;
|
||||
QPoint _touchPosition;
|
||||
QPoint _touchSpeed;
|
||||
crl::time _touchSpeedTime = 0;
|
||||
crl::time _touchAccelerationTime = 0;
|
||||
crl::time _touchTime = 0;
|
||||
crl::time _lastScroll = 0;
|
||||
rpl::variable<bool> _touchMaybePressing;
|
||||
TouchScrollState _touchScrollState = TouchScrollState::Manual;
|
||||
int _overscrollAccumulated = 0;
|
||||
int _ignoreMomentumFromOverscroll = 0;
|
||||
bool _touchDisabled : 1 = false;
|
||||
bool _touchScroll : 1 = false;
|
||||
bool _touchPress : 1 = false;
|
||||
bool _touchRightButton : 1 = false;
|
||||
bool _touchPreviousPositionValid : 1 = false;
|
||||
bool _touchWaitingAcceleration : 1 = false;
|
||||
bool _vertical : 1 = false;
|
||||
bool _widgetAcceptsTouch : 1 = false;
|
||||
bool _disabled : 1 = false;
|
||||
bool _dirtyState : 1 = false;
|
||||
bool _overscrollReturning : 1 = false;
|
||||
|
||||
Fn<bool(not_null<QWheelEvent*>)> _customWheelProcess;
|
||||
Fn<bool(not_null<QTouchEvent*>)> _customTouchProcess;
|
||||
int _overscroll = 0;
|
||||
int _overscrollDefaultFrom = 0;
|
||||
int _overscrollDefaultTill = 0;
|
||||
OverscrollType _overscrollTypeFrom = OverscrollType::Real;
|
||||
OverscrollType _overscrollTypeTill = OverscrollType::Real;
|
||||
std::optional<QColor> _overscrollBg;
|
||||
Ui::Animations::Simple _overscrollReturnAnimation;
|
||||
rpl::variable<Position> _position;
|
||||
rpl::variable<Movement> _movement;
|
||||
|
||||
object_ptr<QWidget> _widget = { nullptr };
|
||||
|
||||
rpl::event_stream<int> _scrollValueUpdated;
|
||||
rpl::event_stream<> _scrolls;
|
||||
rpl::event_stream<> _innerResizes;
|
||||
rpl::event_stream<> _geometryChanged;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] int OverscrollFromAccumulated(int accumulated);
|
||||
[[nodiscard]] int OverscrollToAccumulated(int overscroll);
|
||||
|
||||
} // namespace Ui
|
||||
304
Telegram/lib_ui/ui/widgets/fields/custom_field_object.cpp
Normal file
304
Telegram/lib_ui/ui/widgets/fields/custom_field_object.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/fields/custom_field_object.h"
|
||||
|
||||
#include "ui/effects/spoiler_mess.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_renderer.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/integration.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kSpoilerHiddenOpacity = 0.5;
|
||||
|
||||
using SpoilerRect = InputFieldSpoilerRect;
|
||||
|
||||
} // namespace
|
||||
|
||||
class FieldSpoilerOverlay final : public RpWidget {
|
||||
public:
|
||||
FieldSpoilerOverlay(
|
||||
not_null<InputField*> field,
|
||||
Fn<float64()> shown,
|
||||
Fn<bool()> paused);
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
const not_null<InputField*> _field;
|
||||
const Fn<float64()> _shown;
|
||||
const Fn<bool()> _paused;
|
||||
SpoilerAnimation _animation;
|
||||
|
||||
};
|
||||
|
||||
FieldSpoilerOverlay::FieldSpoilerOverlay(
|
||||
not_null<InputField*> field,
|
||||
Fn<float64()> shown,
|
||||
Fn<bool()> paused)
|
||||
: RpWidget(field->rawTextEdit())
|
||||
, _field(field)
|
||||
, _shown(std::move(shown))
|
||||
, _paused(std::move(paused))
|
||||
, _animation([=] { update(); }) {
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
show();
|
||||
}
|
||||
|
||||
void FieldSpoilerOverlay::paintEvent(QPaintEvent *e) {
|
||||
auto p = std::optional<QPainter>();
|
||||
auto topShift = std::optional<int>();
|
||||
auto frame = std::optional<SpoilerMessFrame>();
|
||||
auto blockquoteBg = std::optional<QColor>();
|
||||
const auto clip = e->rect();
|
||||
|
||||
const auto shown = _shown();
|
||||
const auto bgOpacity = shown;
|
||||
const auto fgOpacity = 1. * shown + kSpoilerHiddenOpacity * (1. - shown);
|
||||
for (const auto &rect : _field->_spoilerRects) {
|
||||
const auto fill = rect.geometry.intersected(clip);
|
||||
if (fill.isEmpty()) {
|
||||
continue;
|
||||
} else if (!p) {
|
||||
p.emplace(this);
|
||||
const auto paused = _paused && _paused();
|
||||
frame.emplace(
|
||||
Text::DefaultSpoilerCache()->lookup(
|
||||
st::defaultTextPalette.spoilerFg->c)->frame(
|
||||
_animation.index(crl::now(), paused)));
|
||||
topShift = -_field->rawTextEdit()->verticalScrollBar()->value();
|
||||
}
|
||||
if (bgOpacity > 0.) {
|
||||
p->setOpacity(bgOpacity);
|
||||
if (rect.blockquote && !blockquoteBg) {
|
||||
const auto bg = _field->_blockquoteBg;
|
||||
blockquoteBg = (bg.alphaF() < 1.)
|
||||
? anim::color(
|
||||
_field->_st.textBg->c,
|
||||
QColor(bg.red(), bg.green(), bg.blue()),
|
||||
bg.alphaF())
|
||||
: bg;
|
||||
}
|
||||
p->fillRect(
|
||||
fill,
|
||||
rect.blockquote ? *blockquoteBg : _field->_st.textBg->c);
|
||||
}
|
||||
p->setOpacity(fgOpacity);
|
||||
const auto shift = QPoint(0, *topShift) - rect.geometry.topLeft();
|
||||
FillSpoilerRect(*p, rect.geometry, *frame, shift);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFieldObject::CustomFieldObject(
|
||||
not_null<InputField*> field,
|
||||
Text::MarkedContext context,
|
||||
Fn<bool()> pausedEmoji,
|
||||
Fn<bool()> pausedSpoiler)
|
||||
: _field(field)
|
||||
, _context(std::move(context))
|
||||
, _pausedEmoji(std::move(pausedEmoji))
|
||||
, _pausedSpoiler(std::move(pausedSpoiler))
|
||||
, _factory(makeFactory())
|
||||
, _now(crl::now()) {
|
||||
}
|
||||
|
||||
CustomFieldObject::~CustomFieldObject() = default;
|
||||
|
||||
void *CustomFieldObject::qt_metacast(const char *iid) {
|
||||
if (QLatin1String(iid) == qobject_interface_iid<QTextObjectInterface*>()) {
|
||||
return static_cast<QTextObjectInterface*>(this);
|
||||
}
|
||||
return QObject::qt_metacast(iid);
|
||||
}
|
||||
|
||||
QSizeF CustomFieldObject::intrinsicSize(
|
||||
QTextDocument *doc,
|
||||
int posInDocument,
|
||||
const QTextFormat &format) {
|
||||
const auto line = _field->_st.style.font->height;
|
||||
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
|
||||
const auto &padding = _field->_st.style.blockquote.padding;
|
||||
const auto paddings = padding.left() + padding.right();
|
||||
const auto skip = 2 * doc->documentMargin();
|
||||
const auto height = Text::kQuoteCollapsedLines * line;
|
||||
return QSizeF(doc->pageSize().width() - paddings - skip, height);
|
||||
}
|
||||
const auto size = st::emojiSize * 1.;
|
||||
const auto width = size + st::emojiPadding * 2.;
|
||||
const auto height = std::max(line * 1., size);
|
||||
if (!_skip) {
|
||||
const auto emoji = Text::AdjustCustomEmojiSize(st::emojiSize);
|
||||
_skip = (st::emojiSize - emoji) / 2;
|
||||
}
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
void CustomFieldObject::drawObject(
|
||||
QPainter *painter,
|
||||
const QRectF &rect,
|
||||
QTextDocument *doc,
|
||||
int posInDocument,
|
||||
const QTextFormat &format) {
|
||||
if (format.objectType() == InputField::kCollapsedQuoteFormat) {
|
||||
const auto left = 0;
|
||||
const auto top = 0;
|
||||
const auto id = format.property(InputField::kQuoteId).toInt();
|
||||
if (const auto i = _quotes.find(id); i != end(_quotes)) {
|
||||
i->second.string.draw(*painter, {
|
||||
.position = QPoint(left + rect.x(), top + rect.y()),
|
||||
.outerWidth = int(base::SafeRound(doc->pageSize().width())),
|
||||
.availableWidth = int(std::floor(rect.width())),
|
||||
|
||||
.palette = nullptr,
|
||||
.spoiler = Text::DefaultSpoilerCache(),
|
||||
.now = _now,
|
||||
.pausedEmoji = _pausedEmoji(),
|
||||
.pausedSpoiler = _pausedSpoiler(),
|
||||
|
||||
.elisionLines = Text::kQuoteCollapsedLines,
|
||||
.useFullWidth = true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto id = format.property(InputField::kCustomEmojiId).toULongLong();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
auto i = _emoji.find(id);
|
||||
if (i == end(_emoji)) {
|
||||
const auto link = format.property(InputField::kCustomEmojiLink);
|
||||
const auto data = InputField::CustomEmojiEntityData(link.toString());
|
||||
if (auto emoji = _factory(data)) {
|
||||
i = _emoji.emplace(id, std::move(emoji)).first;
|
||||
}
|
||||
}
|
||||
if (i == end(_emoji)) {
|
||||
return;
|
||||
}
|
||||
i->second->paint(*painter, {
|
||||
.textColor = format.foreground().color(),
|
||||
.now = _now,
|
||||
.position = QPoint(
|
||||
int(base::SafeRound(rect.x())) + st::emojiPadding + _skip,
|
||||
int(base::SafeRound(rect.y())) + _skip),
|
||||
.paused = _pausedEmoji && _pausedEmoji(),
|
||||
});
|
||||
}
|
||||
|
||||
void CustomFieldObject::clearEmoji() {
|
||||
_emoji.clear();
|
||||
}
|
||||
|
||||
void CustomFieldObject::clearQuotes() {
|
||||
_quotes.clear();
|
||||
}
|
||||
|
||||
std::unique_ptr<RpWidget> CustomFieldObject::createSpoilerOverlay() {
|
||||
return std::make_unique<FieldSpoilerOverlay>(
|
||||
_field,
|
||||
[=] { return _spoilerOpacity.value(_spoilerHidden ? 0. : 1.); },
|
||||
_pausedSpoiler);
|
||||
}
|
||||
|
||||
void CustomFieldObject::refreshSpoilerShown(InputFieldTextRange range) {
|
||||
auto hidden = false;
|
||||
using Range = InputFieldTextRange;
|
||||
const auto intersects = [](Range a, Range b) {
|
||||
return (a.from < b.till) && (b.from < a.till);
|
||||
};
|
||||
if (range.till > range.from) {
|
||||
const auto check = [&](const std::vector<Range> &list) {
|
||||
if (!hidden) {
|
||||
for (const auto &spoiler : list) {
|
||||
if (intersects(spoiler, range)) {
|
||||
hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
check(_field->_spoilerRangesText);
|
||||
check(_field->_spoilerRangesEmoji);
|
||||
} else {
|
||||
auto touchesLeft = false;
|
||||
auto touchesRight = false;
|
||||
const auto cursor = range.from;
|
||||
const auto check = [&](const std::vector<Range> &list) {
|
||||
if (!touchesLeft || !touchesRight) {
|
||||
for (const auto &spoiler : list) {
|
||||
if (spoiler.from <= cursor && spoiler.till >= cursor) {
|
||||
if (spoiler.from < cursor) {
|
||||
touchesLeft = true;
|
||||
}
|
||||
if (spoiler.till >= cursor) {
|
||||
touchesRight = true;
|
||||
}
|
||||
if (touchesLeft && touchesRight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
check(_field->_spoilerRangesText);
|
||||
check(_field->_spoilerRangesEmoji);
|
||||
hidden = touchesLeft && touchesRight;
|
||||
}
|
||||
if (_spoilerHidden != hidden) {
|
||||
_spoilerHidden = hidden;
|
||||
_spoilerOpacity.start(
|
||||
[=] { _field->update(); },
|
||||
hidden ? 1. : 0.,
|
||||
hidden ? 0. : 1.,
|
||||
st::fadeWrapDuration);
|
||||
}
|
||||
}
|
||||
|
||||
void CustomFieldObject::setCollapsedText(int quoteId, TextWithTags text) {
|
||||
auto "e = _quotes[quoteId];
|
||||
quote.string = Text::String(_field->_st.widthMin);
|
||||
quote.string.setMarkedText(_field->_st.style, {
|
||||
text.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(text.tags),
|
||||
}, kMarkupTextOptions, makeFieldContext());
|
||||
quote.text = std::move(text);
|
||||
}
|
||||
|
||||
const TextWithTags &CustomFieldObject::collapsedText(int quoteId) const {
|
||||
if (const auto i = _quotes.find(quoteId); i != end(_quotes)) {
|
||||
return i->second.text;
|
||||
}
|
||||
static const auto kEmpty = TextWithTags();
|
||||
return kEmpty;
|
||||
}
|
||||
|
||||
Text::MarkedContext CustomFieldObject::makeFieldContext() {
|
||||
auto context = _context;
|
||||
context.repaint = [field = _field] { field->update(); };
|
||||
return context;
|
||||
}
|
||||
|
||||
CustomFieldObject::Factory CustomFieldObject::makeFactory() {
|
||||
return [context = makeFieldContext()](QStringView data) {
|
||||
return Text::MakeCustomEmoji(data, context);
|
||||
};
|
||||
}
|
||||
|
||||
void CustomFieldObject::setNow(crl::time now) {
|
||||
_now = now;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
80
Telegram/lib_ui/ui/widgets/fields/custom_field_object.h
Normal file
80
Telegram/lib_ui/ui/widgets/fields/custom_field_object.h
Normal file
@@ -0,0 +1,80 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <QtGui/QTextObjectInterface>
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class InputField;
|
||||
class RpWidget;
|
||||
struct InputFieldTextRange;
|
||||
|
||||
class CustomFieldObject : public QObject, public QTextObjectInterface {
|
||||
public:
|
||||
CustomFieldObject(
|
||||
not_null<InputField*> field,
|
||||
Text::MarkedContext context,
|
||||
Fn<bool()> pausedEmoji,
|
||||
Fn<bool()> pausedSpoiler);
|
||||
~CustomFieldObject();
|
||||
|
||||
void *qt_metacast(const char *iid) override;
|
||||
|
||||
QSizeF intrinsicSize(
|
||||
QTextDocument *doc,
|
||||
int posInDocument,
|
||||
const QTextFormat &format) override;
|
||||
void drawObject(
|
||||
QPainter *painter,
|
||||
const QRectF &rect,
|
||||
QTextDocument *doc,
|
||||
int posInDocument,
|
||||
const QTextFormat &format) override;
|
||||
|
||||
void setCollapsedText(int quoteId, TextWithTags text);
|
||||
[[nodiscard]] const TextWithTags &collapsedText(int quoteId) const;
|
||||
|
||||
void setNow(crl::time now);
|
||||
|
||||
void clearEmoji();
|
||||
void clearQuotes();
|
||||
|
||||
[[nodiscard]] std::unique_ptr<RpWidget> createSpoilerOverlay();
|
||||
void refreshSpoilerShown(InputFieldTextRange range);
|
||||
|
||||
private:
|
||||
struct Quote {
|
||||
TextWithTags text;
|
||||
Text::String string;
|
||||
};
|
||||
|
||||
using Factory = Fn<std::unique_ptr<Text::CustomEmoji>(QStringView)>;
|
||||
[[nodiscard]] Factory makeFactory();
|
||||
[[nodiscard]] Text::MarkedContext makeFieldContext();
|
||||
|
||||
const not_null<InputField*> _field;
|
||||
const Text::MarkedContext _context;
|
||||
const Fn<bool()> _pausedEmoji;
|
||||
const Fn<bool()> _pausedSpoiler;
|
||||
const Factory _factory;
|
||||
|
||||
base::flat_map<uint64, std::unique_ptr<Text::CustomEmoji>> _emoji;
|
||||
base::flat_map<int, Quote> _quotes;
|
||||
crl::time _now = 0;
|
||||
int _skip = 0;
|
||||
|
||||
Animations::Simple _spoilerOpacity;
|
||||
bool _spoilerHidden = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
5469
Telegram/lib_ui/ui/widgets/fields/input_field.cpp
Normal file
5469
Telegram/lib_ui/ui/widgets/fields/input_field.cpp
Normal file
File diff suppressed because it is too large
Load Diff
684
Telegram/lib_ui/ui/widgets/fields/input_field.h
Normal file
684
Telegram/lib_ui/ui/widgets/fields/input_field.h
Normal file
@@ -0,0 +1,684 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/flat_set.h"
|
||||
#include "base/timer.h"
|
||||
#include "ui/emoji_config.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text_entity.h"
|
||||
#include "ui/text/text_custom_emoji.h"
|
||||
|
||||
#include <rpl/variable.h>
|
||||
|
||||
#include <QtGui/QTextCursor>
|
||||
|
||||
#include <any>
|
||||
|
||||
class QMenu;
|
||||
class QShortcut;
|
||||
class QTextEdit;
|
||||
class QTouchEvent;
|
||||
class QContextMenuEvent;
|
||||
class Painter;
|
||||
|
||||
namespace Ui::Text {
|
||||
struct QuotePaintCache;
|
||||
} // namespace Ui::Text
|
||||
|
||||
namespace style {
|
||||
struct InputField;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
const auto kClearFormatSequence = QKeySequence("ctrl+shift+n");
|
||||
const auto kStrikeOutSequence = QKeySequence("ctrl+shift+x");
|
||||
const auto kBlockquoteSequence = QKeySequence("ctrl+shift+.");
|
||||
const auto kMonospaceSequence = QKeySequence("ctrl+shift+m");
|
||||
const auto kEditLinkSequence = QKeySequence("ctrl+k");
|
||||
const auto kSpoilerSequence = QKeySequence("ctrl+shift+p");
|
||||
|
||||
class PopupMenu;
|
||||
class InputField;
|
||||
|
||||
void InsertEmojiAtCursor(QTextCursor cursor, EmojiPtr emoji);
|
||||
void InsertCustomEmojiAtCursor(
|
||||
not_null<InputField*> field,
|
||||
QTextCursor cursor,
|
||||
const QString &text,
|
||||
const QString &link);
|
||||
|
||||
struct InstantReplaces {
|
||||
struct Node {
|
||||
QString text;
|
||||
std::map<QChar, Node> tail;
|
||||
};
|
||||
|
||||
void add(const QString &what, const QString &with);
|
||||
|
||||
static const InstantReplaces &Default();
|
||||
static const InstantReplaces &TextOnly();
|
||||
|
||||
int maxLength = 0;
|
||||
Node reverseMap;
|
||||
|
||||
};
|
||||
|
||||
enum class InputSubmitSettings {
|
||||
Enter,
|
||||
CtrlEnter,
|
||||
Both,
|
||||
None,
|
||||
};
|
||||
|
||||
enum class MarkdownSet {
|
||||
All,
|
||||
Notes,
|
||||
};
|
||||
|
||||
class CustomFieldObject;
|
||||
|
||||
struct MarkdownEnabled {
|
||||
base::flat_set<QString> tagsSubset;
|
||||
|
||||
friend inline bool operator==(
|
||||
const MarkdownEnabled &,
|
||||
const MarkdownEnabled &) = default;
|
||||
};
|
||||
struct MarkdownDisabled {
|
||||
friend inline bool operator==(
|
||||
const MarkdownDisabled &,
|
||||
const MarkdownDisabled &) = default;
|
||||
};
|
||||
struct MarkdownEnabledState {
|
||||
std::variant<MarkdownDisabled, MarkdownEnabled> data;
|
||||
|
||||
[[nodiscard]] bool disabled() const;
|
||||
[[nodiscard]] bool enabledForTag(QStringView tag) const;
|
||||
|
||||
friend inline bool operator==(
|
||||
const MarkdownEnabledState &,
|
||||
const MarkdownEnabledState &) = default;
|
||||
};
|
||||
struct InputFieldTextRange {
|
||||
int from = 0;
|
||||
int till = 0;
|
||||
|
||||
friend inline bool operator==(
|
||||
InputFieldTextRange,
|
||||
InputFieldTextRange) = default;
|
||||
|
||||
[[nodiscard]] bool empty() const {
|
||||
return (till <= from);
|
||||
}
|
||||
};
|
||||
struct InputFieldSpoilerRect {
|
||||
QRect geometry;
|
||||
bool blockquote = false;
|
||||
};
|
||||
|
||||
class InputField : public RpWidget {
|
||||
public:
|
||||
enum class Mode {
|
||||
SingleLine,
|
||||
NoNewlines,
|
||||
MultiLine,
|
||||
};
|
||||
using TagList = TextWithTags::Tags;
|
||||
|
||||
struct MarkdownTag {
|
||||
// With each emoji being QChar::ObjectReplacementCharacter.
|
||||
int internalStart = 0;
|
||||
int internalLength = 0;
|
||||
|
||||
// Adjusted by emoji to match _lastTextWithTags.
|
||||
int adjustedStart = 0;
|
||||
int adjustedLength = 0;
|
||||
|
||||
bool closed = false;
|
||||
QString tag;
|
||||
};
|
||||
static const QString kTagBold;
|
||||
static const QString kTagItalic;
|
||||
static const QString kTagUnderline;
|
||||
static const QString kTagStrikeOut;
|
||||
static const QString kTagCode;
|
||||
static const QString kTagPre;
|
||||
static const QString kTagSpoiler;
|
||||
static const QString kTagBlockquote;
|
||||
static const QString kTagBlockquoteCollapsed;
|
||||
static const QString kCustomEmojiTagStart;
|
||||
static const int kCollapsedQuoteFormat; // QTextFormat::ObjectTypes
|
||||
static const int kCustomEmojiFormat; // QTextFormat::ObjectTypes
|
||||
static const int kCustomEmojiId; // QTextFormat::Property
|
||||
static const int kCustomEmojiLink; // QTextFormat::Property
|
||||
static const int kQuoteId; // QTextFormat::Property
|
||||
|
||||
InputField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &value = QString());
|
||||
InputField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
Mode mode,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &value);
|
||||
InputField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
Mode mode = Mode::SingleLine,
|
||||
rpl::producer<QString> placeholder = nullptr,
|
||||
const TextWithTags &value = TextWithTags());
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::EditableText;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _placeholderFull.current();
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::InputField &st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
void showError();
|
||||
void showErrorNoFocus();
|
||||
void hideError();
|
||||
|
||||
void setMaxLength(int maxLength);
|
||||
void setMinHeight(int minHeight);
|
||||
void setMaxHeight(int maxHeight);
|
||||
void setMode(Mode mode);
|
||||
|
||||
[[nodiscard]] const TextWithTags &getTextWithTags() const {
|
||||
return _lastTextWithTags;
|
||||
}
|
||||
[[nodiscard]] const std::vector<MarkdownTag> &getMarkdownTags() const {
|
||||
return _lastMarkdownTags;
|
||||
}
|
||||
[[nodiscard]] TextWithTags getTextWithTagsPart(
|
||||
int start,
|
||||
int end = -1) const;
|
||||
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
|
||||
void insertTag(const QString &text, QString tagId = QString());
|
||||
[[nodiscard]] bool empty() const {
|
||||
return _lastTextWithTags.text.isEmpty();
|
||||
}
|
||||
enum class HistoryAction {
|
||||
NewEntry,
|
||||
MergeEntry,
|
||||
Clear,
|
||||
};
|
||||
void setTextWithTags(
|
||||
const TextWithTags &textWithTags,
|
||||
HistoryAction historyAction = HistoryAction::NewEntry);
|
||||
|
||||
// If you need to make some preparations of tags before putting them to QMimeData
|
||||
// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
|
||||
void setTagMimeProcessor(Fn<QString(QStringView)> processor);
|
||||
void setCustomTextContext(
|
||||
Text::MarkedContext context,
|
||||
Fn<bool()> pausedEmoji = nullptr,
|
||||
Fn<bool()> pausedSpoiler = nullptr);
|
||||
|
||||
struct EditLinkSelection {
|
||||
int from = 0;
|
||||
int till = 0;
|
||||
};
|
||||
enum class EditLinkAction {
|
||||
Check,
|
||||
Edit,
|
||||
};
|
||||
void setEditLinkCallback(
|
||||
Fn<bool(
|
||||
EditLinkSelection selection,
|
||||
TextWithTags text,
|
||||
QString link,
|
||||
EditLinkAction action)> callback);
|
||||
void setEditLanguageCallback(
|
||||
Fn<void(QString now, Fn<void(QString)> save)> callback);
|
||||
|
||||
struct ExtendedContextMenu {
|
||||
QMenu *menu = nullptr;
|
||||
std::shared_ptr<QContextMenuEvent> event;
|
||||
};
|
||||
|
||||
void setDocumentMargin(float64 margin);
|
||||
void setAdditionalMargin(int margin);
|
||||
void setAdditionalMargins(QMargins margins);
|
||||
|
||||
void setInstantReplaces(const InstantReplaces &replaces);
|
||||
void setInstantReplacesEnabled(rpl::producer<bool> enabled);
|
||||
void setMarkdownReplacesEnabled(bool enabled);
|
||||
void setMarkdownReplacesEnabled(rpl::producer<MarkdownEnabledState> enabled);
|
||||
void setExtendedContextMenu(rpl::producer<ExtendedContextMenu> value);
|
||||
void commitInstantReplacement(
|
||||
int from,
|
||||
int till,
|
||||
const QString &with,
|
||||
const QString &customEmojiData);
|
||||
void commitMarkdownLinkEdit(
|
||||
EditLinkSelection selection,
|
||||
const TextWithTags &textWithTags,
|
||||
const QString &link);
|
||||
[[nodiscard]] static bool IsValidMarkdownLink(QStringView link);
|
||||
[[nodiscard]] static bool IsCustomEmojiLink(QStringView link);
|
||||
[[nodiscard]] static QString CustomEmojiLink(QStringView entityData);
|
||||
[[nodiscard]] static QString CustomEmojiEntityData(QStringView link);
|
||||
|
||||
[[nodiscard]] const QString &getLastText() const {
|
||||
return _lastTextWithTags.text;
|
||||
}
|
||||
void setPlaceholder(
|
||||
rpl::producer<QString> placeholder,
|
||||
int afterSymbols = 0);
|
||||
void setPlaceholderHidden(bool forcePlaceholderHidden);
|
||||
void setDisplayFocused(bool focused);
|
||||
void finishAnimating();
|
||||
void setFocusFast() {
|
||||
setDisplayFocused(true);
|
||||
setFocus();
|
||||
}
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
bool hasText() const;
|
||||
void selectAll();
|
||||
|
||||
bool isUndoAvailable() const;
|
||||
bool isRedoAvailable() const;
|
||||
|
||||
[[nodiscard]] MarkdownEnabledState markdownEnabledState() const {
|
||||
return _markdownEnabledState;
|
||||
}
|
||||
|
||||
void setMarkdownSet(MarkdownSet set);
|
||||
|
||||
using SubmitSettings = InputSubmitSettings;
|
||||
void setSubmitSettings(SubmitSettings settings);
|
||||
static bool ShouldSubmit(
|
||||
SubmitSettings settings,
|
||||
Qt::KeyboardModifiers modifiers);
|
||||
void customUpDown(bool isCustom);
|
||||
void customTab(bool isCustom);
|
||||
int borderAnimationStart() const;
|
||||
|
||||
not_null<QTextDocument*> document();
|
||||
not_null<const QTextDocument*> document() const;
|
||||
void setTextCursor(const QTextCursor &cursor);
|
||||
void setCursorPosition(int position);
|
||||
QTextCursor textCursor() const;
|
||||
void setText(const QString &text);
|
||||
void clear();
|
||||
bool hasFocus() const;
|
||||
void setFocus();
|
||||
void clearFocus();
|
||||
void ensureCursorVisible();
|
||||
not_null<QTextEdit*> rawTextEdit();
|
||||
not_null<const QTextEdit*> rawTextEdit() const;
|
||||
|
||||
enum class MimeAction {
|
||||
Check,
|
||||
Insert,
|
||||
};
|
||||
using MimeDataHook = Fn<bool(
|
||||
not_null<const QMimeData*> data,
|
||||
MimeAction action)>;
|
||||
void setMimeDataHook(MimeDataHook hook) {
|
||||
_mimeDataHook = std::move(hook);
|
||||
}
|
||||
|
||||
const rpl::variable<int> &scrollTop() const;
|
||||
int scrollTopMax() const;
|
||||
void scrollTo(int top);
|
||||
|
||||
struct DocumentChangeInfo {
|
||||
int position = 0;
|
||||
int added = 0;
|
||||
int removed = 0;
|
||||
};
|
||||
auto documentContentsChanges() {
|
||||
return _documentContentsChanges.events();
|
||||
}
|
||||
auto markdownTagApplies() {
|
||||
return _markdownTagApplies.events();
|
||||
}
|
||||
|
||||
void setPreCache(Fn<not_null<Ui::Text::QuotePaintCache*>()> make);
|
||||
void setBlockquoteCache(Fn<not_null<Ui::Text::QuotePaintCache*>()> make);
|
||||
|
||||
[[nodiscard]] bool menuShown() const;
|
||||
[[nodiscard]] rpl::producer<bool> menuShownValue() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<> heightChanges() const;
|
||||
[[nodiscard]] rpl::producer<bool> focusedChanges() const;
|
||||
[[nodiscard]] rpl::producer<> tabbed() const;
|
||||
[[nodiscard]] rpl::producer<> cancelled() const;
|
||||
[[nodiscard]] rpl::producer<> changes() const;
|
||||
[[nodiscard]] rpl::producer<Qt::KeyboardModifiers> submits() const;
|
||||
void forceProcessContentsChanges();
|
||||
|
||||
~InputField();
|
||||
|
||||
protected:
|
||||
void startPlaceholderAnimation();
|
||||
void startBorderAnimation();
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
friend class Inner;
|
||||
friend class CustomFieldObject;
|
||||
friend class FieldSpoilerOverlay;
|
||||
using TextRange = InputFieldTextRange;
|
||||
using SpoilerRect = InputFieldSpoilerRect;
|
||||
enum class MarkdownActionType {
|
||||
ToggleTag,
|
||||
EditLink,
|
||||
};
|
||||
struct MarkdownAction {
|
||||
QKeySequence sequence;
|
||||
QString tag;
|
||||
MarkdownActionType type = MarkdownActionType::ToggleTag;
|
||||
};
|
||||
|
||||
void handleContentsChanged();
|
||||
void updateRootFrameFormat();
|
||||
bool viewportEventInner(QEvent *e);
|
||||
void handleTouchEvent(QTouchEvent *e);
|
||||
|
||||
void updatePalette();
|
||||
void refreshPlaceholder(const QString &text);
|
||||
int placeholderSkipWidth() const;
|
||||
|
||||
[[nodiscard]] static std::vector<MarkdownAction> MarkdownActions();
|
||||
[[nodiscard]] static std::vector<MarkdownAction> MarkdownActionsNotes();
|
||||
void setupMarkdownShortcuts();
|
||||
bool executeMarkdownAction(MarkdownAction action);
|
||||
|
||||
bool heightAutoupdated();
|
||||
void checkContentHeight();
|
||||
void setErrorShown(bool error);
|
||||
|
||||
void focusInEventInner(QFocusEvent *e);
|
||||
void focusOutEventInner(QFocusEvent *e);
|
||||
void setFocused(bool focused);
|
||||
void keyPressEventInner(QKeyEvent *e);
|
||||
void contextMenuEventInner(QContextMenuEvent *e, QMenu *m = nullptr);
|
||||
void dropEventInner(QDropEvent *e);
|
||||
void inputMethodEventInner(QInputMethodEvent *e);
|
||||
void paintEventInner(QPaintEvent *e);
|
||||
void paintQuotes(QPaintEvent *e);
|
||||
|
||||
void mousePressEventInner(QMouseEvent *e);
|
||||
void mouseReleaseEventInner(QMouseEvent *e);
|
||||
void mouseMoveEventInner(QMouseEvent *e);
|
||||
void leaveEventInner(QEvent *e);
|
||||
|
||||
[[nodiscard]] int lookupActionQuoteId(QPoint point) const;
|
||||
void updateCursorShape();
|
||||
|
||||
QMimeData *createMimeDataFromSelectionInner() const;
|
||||
bool canInsertFromMimeDataInner(const QMimeData *source) const;
|
||||
void insertFromMimeDataInner(const QMimeData *source);
|
||||
TextWithTags getTextWithTagsSelected() const;
|
||||
|
||||
void documentContentsChanged(
|
||||
int position,
|
||||
int charsRemoved,
|
||||
int charsAdded);
|
||||
void focusInner();
|
||||
|
||||
// "start" and "end" are in coordinates of text where emoji are replaced
|
||||
// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
|
||||
[[nodiscard]] QString getTextPart(
|
||||
int start,
|
||||
int end,
|
||||
TagList &outTagsList,
|
||||
bool &outTagsChanged,
|
||||
std::vector<MarkdownTag> *outMarkdownTags = nullptr) const;
|
||||
|
||||
// After any characters added we must postprocess them. This includes:
|
||||
// 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px.
|
||||
// 2. Replacing font family from semibold for all non-~ characters, if we used ...
|
||||
// 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics.
|
||||
// 4. Interrupting tags in which the text was inserted by any char except a letter.
|
||||
// 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text.
|
||||
// Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end).
|
||||
void processFormatting(int changedPosition, int changedEnd);
|
||||
|
||||
void chopByMaxLength(int insertPosition, int insertLength);
|
||||
|
||||
bool processMarkdownReplaces(const QString &appended);
|
||||
//bool processMarkdownReplace(const QString &tag);
|
||||
void addMarkdownActions(not_null<QMenu*> menu, QContextMenuEvent *e);
|
||||
void addMarkdownMenuAction(
|
||||
not_null<QMenu*> menu,
|
||||
not_null<QAction*> action);
|
||||
bool handleMarkdownKey(QKeyEvent *e);
|
||||
|
||||
// We don't want accidentally detach InstantReplaces map.
|
||||
// So we access it only by const reference from this method.
|
||||
const InstantReplaces &instantReplaces() const;
|
||||
void processInstantReplaces(const QString &appended);
|
||||
void applyInstantReplace(const QString &what, const QString &with);
|
||||
|
||||
struct EditLinkData {
|
||||
int from = 0;
|
||||
int till = 0;
|
||||
QString link;
|
||||
};
|
||||
EditLinkData selectionEditLinkData(EditLinkSelection selection) const;
|
||||
EditLinkSelection editLinkSelection(QContextMenuEvent *e) const;
|
||||
void editMarkdownLink(EditLinkSelection selection);
|
||||
|
||||
void commitInstantReplacement(
|
||||
int from,
|
||||
int till,
|
||||
const QString &with,
|
||||
const QString &customEmojiData,
|
||||
std::optional<QString> checkOriginal,
|
||||
bool checkIfInMonospace);
|
||||
#if 0
|
||||
bool commitMarkdownReplacement(
|
||||
int from,
|
||||
int till,
|
||||
const QString &tag,
|
||||
const QString &edge = QString());
|
||||
#endif
|
||||
TextRange insertWithTags(TextRange range, TextWithTags text);
|
||||
TextRange addMarkdownTag(TextRange range, const QString &tag);
|
||||
void removeMarkdownTag(TextRange range, const QString &tag);
|
||||
void finishMarkdownTagChange(
|
||||
TextRange range,
|
||||
const TextWithTags &textWithTags);
|
||||
void toggleSelectionMarkdown(const QString &tag);
|
||||
void clearSelectionMarkdown();
|
||||
|
||||
bool revertFormatReplace();
|
||||
bool jumpOutOfBlockByBackspace();
|
||||
|
||||
void paintSurrounding(
|
||||
QPainter &p,
|
||||
QRect clip,
|
||||
float64 errorDegree,
|
||||
float64 focusedDegree);
|
||||
void paintRoundSurrounding(
|
||||
QPainter &p,
|
||||
QRect clip,
|
||||
float64 errorDegree,
|
||||
float64 focusedDegree);
|
||||
void paintFlatSurrounding(
|
||||
QPainter &p,
|
||||
QRect clip,
|
||||
float64 errorDegree,
|
||||
float64 focusedDegree);
|
||||
void customEmojiRepaint();
|
||||
void highlightMarkdown();
|
||||
bool exitQuoteWithNewBlock(int key);
|
||||
|
||||
void blockActionClicked(int quoteId);
|
||||
void editPreLanguage(int quoteId, QStringView tag);
|
||||
void toggleBlockquoteCollapsed(
|
||||
int quoteId,
|
||||
QStringView tag,
|
||||
TextRange range);
|
||||
void trippleEnterExitBlock(QTextCursor &cursor);
|
||||
|
||||
void touchUpdate(QPoint globalPosition);
|
||||
void touchFinish();
|
||||
|
||||
const style::InputField &_st;
|
||||
Fn<not_null<Ui::Text::QuotePaintCache*>()> _preCache;
|
||||
Fn<not_null<Ui::Text::QuotePaintCache*>()> _blockquoteCache;
|
||||
|
||||
Mode _mode = Mode::SingleLine;
|
||||
int _maxLength = -1;
|
||||
int _minHeight = -1;
|
||||
int _maxHeight = -1;
|
||||
|
||||
const std::unique_ptr<Inner> _inner;
|
||||
|
||||
Fn<bool(
|
||||
EditLinkSelection selection,
|
||||
TextWithTags text,
|
||||
QString link,
|
||||
EditLinkAction action)> _editLinkCallback;
|
||||
Fn<void(QString now, Fn<void(QString)> save)> _editLanguageCallback;
|
||||
TextWithTags _lastTextWithTags;
|
||||
std::vector<MarkdownTag> _lastMarkdownTags;
|
||||
QString _lastPreEditText;
|
||||
std::optional<QString> _inputMethodCommit;
|
||||
mutable std::vector<TextRange> _spoilerRangesText;
|
||||
mutable std::vector<TextRange> _spoilerRangesEmoji;
|
||||
mutable std::vector<SpoilerRect> _spoilerRects;
|
||||
mutable QColor _blockquoteBg;
|
||||
std::unique_ptr<RpWidget> _spoilerOverlay;
|
||||
|
||||
QMargins _additionalMargins;
|
||||
QMargins _customFontMargins;
|
||||
int _placeholderCustomFontSkip = 0;
|
||||
int _requestedDocumentTopMargin = 0;
|
||||
|
||||
bool _forcePlaceholderHidden = false;
|
||||
bool _reverseMarkdownReplacement = false;
|
||||
bool _customEmojiRepaintScheduled = false;
|
||||
bool _settingDocumentMargin = false;
|
||||
|
||||
// Tags list which we should apply while setText() call or insert from mime data.
|
||||
TagList _insertedTags;
|
||||
bool _insertedTagsAreFromMime = false;
|
||||
bool _insertedTagsReplace = false;
|
||||
|
||||
// Override insert position and charsAdded from complex text editing
|
||||
// (like drag-n-drop in the same text edit field).
|
||||
int _realInsertPosition = -1;
|
||||
int _realCharsAdded = 0;
|
||||
|
||||
// Calculate the amount of emoji extra chars
|
||||
// before _documentContentsChanges fire.
|
||||
int _emojiSurrogateAmount = 0;
|
||||
|
||||
Fn<QString(QStringView)> _tagMimeProcessor;
|
||||
std::unique_ptr<CustomFieldObject> _customObject;
|
||||
std::optional<QTextCursor> _formattingCursorUpdate;
|
||||
|
||||
SubmitSettings _submitSettings = SubmitSettings::Enter;
|
||||
MarkdownEnabledState _markdownEnabledState;
|
||||
MarkdownSet _markdownSet = MarkdownSet::All;
|
||||
bool _undoAvailable = false;
|
||||
bool _redoAvailable = false;
|
||||
bool _insertedTagsDelayClear = false;
|
||||
bool _inHeightCheck = false;
|
||||
|
||||
bool _customUpDown = false;
|
||||
bool _customTab = false;
|
||||
|
||||
rpl::variable<QString> _placeholderFull;
|
||||
QString _placeholder;
|
||||
int _placeholderAfterSymbols = 0;
|
||||
Animations::Simple _a_placeholderShifted;
|
||||
bool _placeholderShifted = false;
|
||||
QPainterPath _placeholderPath;
|
||||
|
||||
Animations::Simple _a_borderShown;
|
||||
int _borderAnimationStart = 0;
|
||||
Animations::Simple _a_borderOpacity;
|
||||
bool _borderVisible = false;
|
||||
|
||||
Animations::Simple _a_focused;
|
||||
Animations::Simple _a_error;
|
||||
|
||||
bool _focused = false;
|
||||
bool _error = false;
|
||||
|
||||
base::Timer _touchTimer;
|
||||
bool _touchPress = false;
|
||||
bool _touchRightButton = false;
|
||||
bool _touchMove = false;
|
||||
bool _mousePressedInTouch = false;
|
||||
QPoint _touchStart;
|
||||
|
||||
bool _correcting = false;
|
||||
MimeDataHook _mimeDataHook;
|
||||
rpl::event_stream<bool> _menuShownChanges;
|
||||
base::unique_qptr<PopupMenu> _contextMenu;
|
||||
|
||||
QTextCharFormat _defaultCharFormat;
|
||||
|
||||
int _selectedActionQuoteId = 0;
|
||||
int _pressedActionQuoteId = -1;
|
||||
rpl::variable<int> _scrollTop;
|
||||
|
||||
InstantReplaces _mutableInstantReplaces;
|
||||
bool _instantReplacesEnabled = true;
|
||||
|
||||
rpl::event_stream<DocumentChangeInfo> _documentContentsChanges;
|
||||
rpl::event_stream<MarkdownTag> _markdownTagApplies;
|
||||
|
||||
std::vector<std::unique_ptr<QShortcut>> _markdownShortcuts;
|
||||
|
||||
rpl::event_stream<bool> _focusedChanges;
|
||||
rpl::event_stream<> _heightChanges;
|
||||
rpl::event_stream<> _tabbed;
|
||||
rpl::event_stream<> _cancelled;
|
||||
rpl::event_stream<> _changes;
|
||||
rpl::event_stream<Qt::KeyboardModifiers> _submits;
|
||||
|
||||
};
|
||||
|
||||
void PrepareFormattingOptimization(not_null<QTextDocument*> document);
|
||||
|
||||
[[nodiscard]] int ComputeRealUnicodeCharactersCount(const QString &text);
|
||||
[[nodiscard]] int ComputeFieldCharacterCount(not_null<InputField*> field);
|
||||
[[nodiscard]] bool ShouldSubmit(
|
||||
QKeyEvent *event,
|
||||
InputSubmitSettings settings);
|
||||
|
||||
struct LengthLimitLabelOptions {
|
||||
RpWidget *customParent = nullptr;
|
||||
std::optional<int> customThreshold = std::nullopt;
|
||||
Fn<QPoint(QSize parent, QSize label)> customUpdatePosition;
|
||||
Fn<int()> customCharactersCount;
|
||||
int limitLabelTop = 0;
|
||||
};
|
||||
void AddLengthLimitLabel(
|
||||
not_null<InputField*> field,
|
||||
int limit,
|
||||
LengthLimitLabelOptions options = {});
|
||||
|
||||
} // namespace Ui
|
||||
555
Telegram/lib_ui/ui/widgets/fields/masked_input_field.cpp
Normal file
555
Telegram/lib_ui/ui/widgets/fields/masked_input_field.cpp
Normal file
@@ -0,0 +1,555 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/integration.h"
|
||||
#include "styles/palette.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtWidgets/QCommonStyle>
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
class InputStyle final : public QCommonStyle {
|
||||
public:
|
||||
InputStyle() {
|
||||
setParent(QCoreApplication::instance());
|
||||
}
|
||||
|
||||
void drawPrimitive(
|
||||
PrimitiveElement element,
|
||||
const QStyleOption *option,
|
||||
QPainter *painter,
|
||||
const QWidget *widget = nullptr) const override {
|
||||
}
|
||||
|
||||
static InputStyle *instance() {
|
||||
if (!_instance) {
|
||||
if (!QGuiApplication::instance()) {
|
||||
return nullptr;
|
||||
}
|
||||
_instance = new InputStyle();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
~InputStyle() {
|
||||
_instance = nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
static InputStyle *_instance;
|
||||
|
||||
};
|
||||
|
||||
InputStyle *InputStyle::_instance = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
MaskedInputField::MaskedInputField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &val)
|
||||
: Parent(val, parent)
|
||||
, _st(st)
|
||||
, _oldtext(val)
|
||||
, _placeholderFull(std::move(placeholder)) {
|
||||
resize(_st.width, _st.heightMin);
|
||||
|
||||
setFont(_st.style.font);
|
||||
setAlignment(_st.textAlign);
|
||||
|
||||
_placeholderFull.value(
|
||||
) | rpl::on_next([=](const QString &text) {
|
||||
refreshPlaceholder(text);
|
||||
setAccessibleName(text);
|
||||
}, lifetime());
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([=] {
|
||||
updatePalette();
|
||||
}, lifetime());
|
||||
updatePalette();
|
||||
|
||||
if (_st.textBg->c.alphaF() >= 1. && !_st.borderRadius) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
connect(this, SIGNAL(textChanged(QString)), this, SLOT(onTextChange(QString)));
|
||||
connect(this, SIGNAL(cursorPositionChanged(int,int)), this, SLOT(onCursorPositionChanged(int,int)));
|
||||
|
||||
connect(this, SIGNAL(textEdited(QString)), this, SLOT(onTextEdited()));
|
||||
connect(this, &MaskedInputField::selectionChanged, [] {
|
||||
Integration::Instance().textActionsUpdated();
|
||||
});
|
||||
|
||||
setStyle(InputStyle::instance());
|
||||
QLineEdit::setTextMargins(0, 0, 0, 0);
|
||||
setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1));
|
||||
setFrame(false);
|
||||
|
||||
setAttribute(Qt::WA_AcceptTouchEvents);
|
||||
_touchTimer.setSingleShot(true);
|
||||
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
|
||||
|
||||
setTextMargins(_st.textMargins);
|
||||
|
||||
startPlaceholderAnimation();
|
||||
startBorderAnimation();
|
||||
finishAnimating();
|
||||
}
|
||||
|
||||
void MaskedInputField::updatePalette() {
|
||||
auto p = palette();
|
||||
p.setColor(QPalette::Text, _st.textFg->c);
|
||||
p.setColor(QPalette::Highlight, st::msgInBgSelected->c);
|
||||
p.setColor(QPalette::HighlightedText, st::historyTextInFgSelected->c);
|
||||
setPalette(p);
|
||||
}
|
||||
|
||||
void MaskedInputField::setCorrectedText(QString &now, int &nowCursor, const QString &newText, int newPos) {
|
||||
if (newPos < 0 || newPos > newText.size()) {
|
||||
newPos = newText.size();
|
||||
}
|
||||
auto updateText = (newText != now);
|
||||
if (updateText) {
|
||||
now = newText;
|
||||
setText(now);
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
auto updateCursorPosition = (newPos != nowCursor) || updateText;
|
||||
if (updateCursorPosition) {
|
||||
nowCursor = newPos;
|
||||
setCursorPosition(nowCursor);
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::customUpDown(bool custom) {
|
||||
_customUpDown = custom;
|
||||
}
|
||||
|
||||
int MaskedInputField::borderAnimationStart() const {
|
||||
return _borderAnimationStart;
|
||||
}
|
||||
|
||||
void MaskedInputField::setTextMargins(const QMargins &mrg) {
|
||||
_textMargins = mrg;
|
||||
setContentsMargins(_textMargins + QMargins(-2, -1, -2, -1));
|
||||
refreshPlaceholder(_placeholderFull.current());
|
||||
}
|
||||
|
||||
void MaskedInputField::onTouchTimer() {
|
||||
_touchRightButton = true;
|
||||
}
|
||||
|
||||
bool MaskedInputField::eventHook(QEvent *e) {
|
||||
auto type = e->type();
|
||||
if (type == QEvent::TouchBegin
|
||||
|| type == QEvent::TouchUpdate
|
||||
|| type == QEvent::TouchEnd
|
||||
|| type == QEvent::TouchCancel) {
|
||||
auto event = static_cast<QTouchEvent*>(e);
|
||||
if (event->device()->type() == base::TouchDevice::TouchScreen) {
|
||||
touchEvent(event);
|
||||
}
|
||||
}
|
||||
return Parent::eventHook(e);
|
||||
}
|
||||
|
||||
void MaskedInputField::touchEvent(QTouchEvent *e) {
|
||||
switch (e->type()) {
|
||||
case QEvent::TouchBegin: {
|
||||
if (_touchPress || e->touchPoints().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
_touchTimer.start(QApplication::startDragTime());
|
||||
_touchPress = true;
|
||||
_touchMove = _touchRightButton = _mousePressedInTouch = false;
|
||||
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
|
||||
} break;
|
||||
|
||||
case QEvent::TouchUpdate: {
|
||||
if (!e->touchPoints().isEmpty()) {
|
||||
touchUpdate(e->touchPoints().cbegin()->screenPos().toPoint());
|
||||
}
|
||||
} break;
|
||||
|
||||
case QEvent::TouchEnd: {
|
||||
touchFinish();
|
||||
} break;
|
||||
|
||||
case QEvent::TouchCancel: {
|
||||
_touchPress = false;
|
||||
_touchTimer.stop();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::touchUpdate(QPoint globalPosition) {
|
||||
if (_touchPress
|
||||
&& !_touchMove
|
||||
&& ((globalPosition - _touchStart).manhattanLength()
|
||||
>= QApplication::startDragDistance())) {
|
||||
_touchMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::touchFinish() {
|
||||
if (!_touchPress) {
|
||||
return;
|
||||
}
|
||||
const auto weak = base::make_weak(this);
|
||||
if (!_touchMove && window()) {
|
||||
QPoint mapped(mapFromGlobal(_touchStart));
|
||||
|
||||
if (_touchRightButton) {
|
||||
QContextMenuEvent contextEvent(
|
||||
QContextMenuEvent::Mouse,
|
||||
mapped,
|
||||
_touchStart);
|
||||
contextMenuEvent(&contextEvent);
|
||||
} else {
|
||||
QGuiApplication::inputMethod()->show();
|
||||
}
|
||||
}
|
||||
if (weak) {
|
||||
_touchTimer.stop();
|
||||
_touchPress
|
||||
= _touchMove
|
||||
= _touchRightButton
|
||||
= _mousePressedInTouch = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
auto r = rect().intersected(e->rect());
|
||||
p.fillRect(r, _st.textBg);
|
||||
if (_st.border) {
|
||||
p.fillRect(0, height() - _st.border, width(), _st.border, _st.borderFg->b);
|
||||
}
|
||||
auto errorDegree = _a_error.value(_error ? 1. : 0.);
|
||||
auto focusedDegree = _a_focused.value(_focused ? 1. : 0.);
|
||||
auto borderShownDegree = _a_borderShown.value(1.);
|
||||
auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
|
||||
if (_st.borderActive && (borderOpacity > 0.)) {
|
||||
auto borderStart = std::clamp(_borderAnimationStart, 0, width());
|
||||
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
|
||||
auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
|
||||
if (borderTo > borderFrom) {
|
||||
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
|
||||
p.setOpacity(borderOpacity);
|
||||
p.fillRect(borderFrom, height() - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
|
||||
p.setOpacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
p.setClipRect(r);
|
||||
if (_st.placeholderScale > 0. && !_placeholderPath.isEmpty()) {
|
||||
auto placeholderShiftDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
|
||||
p.save();
|
||||
p.setClipRect(r);
|
||||
|
||||
auto placeholderTop = anim::interpolate(0, _st.placeholderShift, placeholderShiftDegree);
|
||||
|
||||
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
|
||||
r.moveTop(r.top() + placeholderTop);
|
||||
if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
|
||||
|
||||
auto placeholderScale = 1. - (1. - _st.placeholderScale) * placeholderShiftDegree;
|
||||
auto placeholderFg = anim::color(_st.placeholderFg, _st.placeholderFgActive, focusedDegree);
|
||||
placeholderFg = anim::color(placeholderFg, _st.placeholderFgError, errorDegree);
|
||||
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(placeholderFg);
|
||||
p.translate(r.topLeft());
|
||||
p.scale(placeholderScale, placeholderScale);
|
||||
p.drawPath(_placeholderPath);
|
||||
|
||||
p.restore();
|
||||
} else if (!_placeholder.isEmpty()) {
|
||||
auto placeholderHiddenDegree = _a_placeholderShifted.value(_placeholderShifted ? 1. : 0.);
|
||||
if (placeholderHiddenDegree < 1.) {
|
||||
p.setOpacity(1. - placeholderHiddenDegree);
|
||||
p.save();
|
||||
p.setClipRect(r);
|
||||
|
||||
auto placeholderLeft = anim::interpolate(0, -_st.placeholderShift, placeholderHiddenDegree);
|
||||
|
||||
QRect r(rect().marginsRemoved(_textMargins + _st.placeholderMargins));
|
||||
r.moveLeft(r.left() + placeholderLeft);
|
||||
if (style::RightToLeft()) r.moveLeft(width() - r.left() - r.width());
|
||||
|
||||
p.setFont(_st.placeholderFont);
|
||||
p.setPen(anim::pen(_st.placeholderFg, _st.placeholderFgActive, focusedDegree));
|
||||
p.drawText(r, _placeholder, _st.placeholderAlign);
|
||||
|
||||
p.restore();
|
||||
p.setOpacity(1.);
|
||||
}
|
||||
}
|
||||
|
||||
paintAdditionalPlaceholder(p);
|
||||
QLineEdit::paintEvent(e);
|
||||
}
|
||||
|
||||
void MaskedInputField::mousePressEvent(QMouseEvent *e) {
|
||||
if (_touchPress && e->button() == Qt::LeftButton) {
|
||||
_mousePressedInTouch = true;
|
||||
_touchStart = e->globalPos();
|
||||
}
|
||||
return QLineEdit::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void MaskedInputField::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (_mousePressedInTouch) {
|
||||
touchFinish();
|
||||
}
|
||||
return QLineEdit::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
void MaskedInputField::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_mousePressedInTouch) {
|
||||
touchUpdate(e->globalPos());
|
||||
}
|
||||
return QLineEdit::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
QString MaskedInputField::getDisplayedText() const {
|
||||
auto result = getLastText();
|
||||
if (!_lastPreEditText.isEmpty()) {
|
||||
result = result.mid(0, _oldcursor)
|
||||
+ _lastPreEditText
|
||||
+ result.mid(_oldcursor);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void MaskedInputField::startBorderAnimation() {
|
||||
auto borderVisible = (_error || _focused);
|
||||
if (_borderVisible != borderVisible) {
|
||||
_borderVisible = borderVisible;
|
||||
if (_borderVisible) {
|
||||
if (_a_borderOpacity.animating()) {
|
||||
_a_borderOpacity.start([this] { update(); }, 0., 1., _st.duration);
|
||||
} else {
|
||||
_a_borderShown.start([this] { update(); }, 0., 1., _st.duration);
|
||||
}
|
||||
} else if (qFuzzyCompare(_a_borderShown.value(1.), 0.)) {
|
||||
_a_borderShown.stop();
|
||||
_a_borderOpacity.stop();
|
||||
} else {
|
||||
_a_borderOpacity.start([this] { update(); }, 1., 0., _st.duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::focusInEvent(QFocusEvent *e) {
|
||||
_borderAnimationStart = (e->reason() == Qt::MouseFocusReason) ? mapFromGlobal(QCursor::pos()).x() : (width() / 2);
|
||||
setFocused(true);
|
||||
QLineEdit::focusInEvent(e);
|
||||
focused();
|
||||
}
|
||||
|
||||
void MaskedInputField::focusOutEvent(QFocusEvent *e) {
|
||||
setFocused(false);
|
||||
QLineEdit::focusOutEvent(e);
|
||||
blurred();
|
||||
}
|
||||
|
||||
void MaskedInputField::setFocused(bool focused) {
|
||||
if (_focused != focused) {
|
||||
_focused = focused;
|
||||
_a_focused.start([this] { update(); }, _focused ? 0. : 1., _focused ? 1. : 0., _st.duration);
|
||||
startPlaceholderAnimation();
|
||||
startBorderAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::resizeEvent(QResizeEvent *e) {
|
||||
refreshPlaceholder(_placeholderFull.current());
|
||||
_borderAnimationStart = width() / 2;
|
||||
QLineEdit::resizeEvent(e);
|
||||
}
|
||||
|
||||
void MaskedInputField::refreshPlaceholder(const QString &text) {
|
||||
const auto availableWidth = width() - _textMargins.left() - _textMargins.right() - _st.placeholderMargins.left() - _st.placeholderMargins.right() - 1;
|
||||
if (_st.placeholderScale > 0.) {
|
||||
auto placeholderFont = _st.placeholderFont->f;
|
||||
placeholderFont.setStyleStrategy(QFont::PreferMatch);
|
||||
const auto metrics = QFontMetrics(placeholderFont);
|
||||
_placeholder = metrics.elidedText(text, Qt::ElideRight, availableWidth);
|
||||
_placeholderPath = QPainterPath();
|
||||
if (!_placeholder.isEmpty()) {
|
||||
const auto result = style::FindAdjustResult(placeholderFont);
|
||||
const auto ascent = result ? result->iascent : metrics.ascent();
|
||||
_placeholderPath.addText(0, ascent, placeholderFont, _placeholder);
|
||||
}
|
||||
} else {
|
||||
_placeholder = _st.placeholderFont->elided(text, availableWidth);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void MaskedInputField::setPlaceholder(rpl::producer<QString> placeholder) {
|
||||
_placeholderFull = std::move(placeholder);
|
||||
}
|
||||
|
||||
void MaskedInputField::contextMenuEvent(QContextMenuEvent *e) {
|
||||
if (const auto menu = createStandardContextMenu()) {
|
||||
_contextMenu = base::make_unique_q<PopupMenu>(this, menu);
|
||||
_contextMenu->popup(e->globalPos());
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::inputMethodEvent(QInputMethodEvent *e) {
|
||||
QLineEdit::inputMethodEvent(e);
|
||||
_lastPreEditText = e->preeditString();
|
||||
update();
|
||||
}
|
||||
|
||||
void MaskedInputField::showError() {
|
||||
showErrorNoFocus();
|
||||
if (!hasFocus()) {
|
||||
setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::showErrorNoFocus() {
|
||||
setErrorShown(true);
|
||||
}
|
||||
|
||||
void MaskedInputField::hideError() {
|
||||
setErrorShown(false);
|
||||
}
|
||||
|
||||
void MaskedInputField::setErrorShown(bool error) {
|
||||
if (_error != error) {
|
||||
_error = error;
|
||||
_a_error.start([this] { update(); }, _error ? 0. : 1., _error ? 1. : 0., _st.duration);
|
||||
startBorderAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
QSize MaskedInputField::sizeHint() const {
|
||||
return geometry().size();
|
||||
}
|
||||
|
||||
QSize MaskedInputField::minimumSizeHint() const {
|
||||
return geometry().size();
|
||||
}
|
||||
|
||||
void MaskedInputField::setDisplayFocused(bool focused) {
|
||||
setFocused(focused);
|
||||
finishAnimating();
|
||||
}
|
||||
|
||||
void MaskedInputField::finishAnimating() {
|
||||
_a_focused.stop();
|
||||
_a_error.stop();
|
||||
_a_placeholderShifted.stop();
|
||||
_a_borderShown.stop();
|
||||
_a_borderOpacity.stop();
|
||||
update();
|
||||
}
|
||||
|
||||
void MaskedInputField::setPlaceholderHidden(bool forcePlaceholderHidden) {
|
||||
_forcePlaceholderHidden = forcePlaceholderHidden;
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
|
||||
void MaskedInputField::startPlaceholderAnimation() {
|
||||
auto placeholderShifted = _forcePlaceholderHidden || (_focused && _st.placeholderScale > 0.) || !getLastText().isEmpty();
|
||||
if (_placeholderShifted != placeholderShifted) {
|
||||
_placeholderShifted = placeholderShifted;
|
||||
_a_placeholderShifted.start([this] { update(); }, _placeholderShifted ? 0. : 1., _placeholderShifted ? 1. : 0., _st.duration);
|
||||
}
|
||||
}
|
||||
|
||||
QRect MaskedInputField::placeholderRect() const {
|
||||
return rect().marginsRemoved(_textMargins + _st.placeholderMargins);
|
||||
}
|
||||
|
||||
style::font MaskedInputField::phFont() {
|
||||
return _st.style.font;
|
||||
}
|
||||
|
||||
void MaskedInputField::placeholderAdditionalPrepare(QPainter &p) {
|
||||
p.setFont(_st.style.font);
|
||||
p.setPen(_st.placeholderFg);
|
||||
}
|
||||
|
||||
void MaskedInputField::keyPressEvent(QKeyEvent *e) {
|
||||
QString wasText(_oldtext);
|
||||
int32 wasCursor(_oldcursor);
|
||||
|
||||
if (_customUpDown && (e->key() == Qt::Key_Up || e->key() == Qt::Key_Down || e->key() == Qt::Key_PageUp || e->key() == Qt::Key_PageDown)) {
|
||||
e->ignore();
|
||||
} else if (e == QKeySequence::DeleteStartOfWord && hasSelectedText()) {
|
||||
e->accept();
|
||||
backspace();
|
||||
} else {
|
||||
QLineEdit::keyPressEvent(e);
|
||||
}
|
||||
|
||||
auto newText = text();
|
||||
auto newCursor = cursorPosition();
|
||||
if (wasText == newText && wasCursor == newCursor) { // call correct manually
|
||||
correctValue(wasText, wasCursor, newText, newCursor);
|
||||
_oldtext = newText;
|
||||
_oldcursor = newCursor;
|
||||
if (wasText != _oldtext) changed();
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
if (e->key() == Qt::Key_Escape) {
|
||||
e->ignore();
|
||||
cancelled();
|
||||
} else if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
|
||||
submitted(e->modifiers());
|
||||
#ifdef Q_OS_MAC
|
||||
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
|
||||
auto selected = selectedText();
|
||||
if (!selected.isEmpty() && echoMode() == QLineEdit::Normal) {
|
||||
QGuiApplication::clipboard()->setText(selected, QClipboard::FindBuffer);
|
||||
}
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
}
|
||||
|
||||
void MaskedInputField::onTextEdited() {
|
||||
QString wasText(_oldtext), newText(text());
|
||||
int32 wasCursor(_oldcursor), newCursor(cursorPosition());
|
||||
|
||||
correctValue(wasText, wasCursor, newText, newCursor);
|
||||
_oldtext = newText;
|
||||
_oldcursor = newCursor;
|
||||
if (wasText != _oldtext) changed();
|
||||
startPlaceholderAnimation();
|
||||
|
||||
Integration::Instance().textActionsUpdated();
|
||||
}
|
||||
|
||||
void MaskedInputField::onTextChange(const QString &text) {
|
||||
_oldtext = QLineEdit::text();
|
||||
setErrorShown(false);
|
||||
Integration::Instance().textActionsUpdated();
|
||||
}
|
||||
|
||||
void MaskedInputField::onCursorPositionChanged(int oldPosition, int position) {
|
||||
_oldcursor = position;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
174
Telegram/lib_ui/ui/widgets/fields/masked_input_field.h
Normal file
174
Telegram/lib_ui/ui/widgets/fields/masked_input_field.h
Normal file
@@ -0,0 +1,174 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
namespace style {
|
||||
struct InputField;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PopupMenu;
|
||||
|
||||
class MaskedInputField : public RpWidgetBase<QLineEdit> {
|
||||
Q_OBJECT
|
||||
|
||||
using Parent = RpWidgetBase<QLineEdit>;
|
||||
public:
|
||||
MaskedInputField(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder = nullptr,
|
||||
const QString &val = QString());
|
||||
|
||||
void showError();
|
||||
void showErrorNoFocus();
|
||||
void hideError();
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
|
||||
void customUpDown(bool isCustom);
|
||||
int borderAnimationStart() const;
|
||||
|
||||
const QString &getLastText() const {
|
||||
return _oldtext;
|
||||
}
|
||||
void setPlaceholder(rpl::producer<QString> placeholder);
|
||||
void setPlaceholderHidden(bool forcePlaceholderHidden);
|
||||
void setDisplayFocused(bool focused);
|
||||
void finishAnimating();
|
||||
void setFocusFast() {
|
||||
setDisplayFocused(true);
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void setText(const QString &text) {
|
||||
QLineEdit::setText(text);
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
void clear() {
|
||||
QLineEdit::clear();
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
void onTextChange(const QString &text);
|
||||
void onCursorPositionChanged(int oldPosition, int position);
|
||||
|
||||
void onTextEdited();
|
||||
|
||||
void onTouchTimer();
|
||||
|
||||
Q_SIGNALS:
|
||||
void changed();
|
||||
void cancelled();
|
||||
void submitted(Qt::KeyboardModifiers);
|
||||
void focused();
|
||||
void blurred();
|
||||
|
||||
protected:
|
||||
QString getDisplayedText() const;
|
||||
void startBorderAnimation();
|
||||
void startPlaceholderAnimation();
|
||||
|
||||
bool eventHook(QEvent *e) override;
|
||||
void touchEvent(QTouchEvent *e);
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void focusOutEvent(QFocusEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
void inputMethodEvent(QInputMethodEvent *e) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
virtual void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
}
|
||||
void setCorrectedText(
|
||||
QString &now,
|
||||
int &nowCursor,
|
||||
const QString &newText,
|
||||
int newPos);
|
||||
|
||||
virtual void paintAdditionalPlaceholder(QPainter &p) {
|
||||
}
|
||||
|
||||
style::font phFont();
|
||||
|
||||
void placeholderAdditionalPrepare(QPainter &p);
|
||||
QRect placeholderRect() const;
|
||||
|
||||
void setTextMargins(const QMargins &mrg);
|
||||
const style::InputField &_st;
|
||||
|
||||
private:
|
||||
void updatePalette();
|
||||
void refreshPlaceholder(const QString &text);
|
||||
void setErrorShown(bool error);
|
||||
|
||||
void touchUpdate(QPoint globalPosition);
|
||||
void touchFinish();
|
||||
|
||||
void setFocused(bool focused);
|
||||
|
||||
int _maxLength = -1;
|
||||
bool _forcePlaceholderHidden = false;
|
||||
|
||||
QString _oldtext;
|
||||
int _oldcursor = 0;
|
||||
QString _lastPreEditText;
|
||||
|
||||
bool _undoAvailable = false;
|
||||
bool _redoAvailable = false;
|
||||
|
||||
bool _customUpDown = false;
|
||||
|
||||
rpl::variable<QString> _placeholderFull;
|
||||
QString _placeholder;
|
||||
Animations::Simple _a_placeholderShifted;
|
||||
bool _placeholderShifted = false;
|
||||
QPainterPath _placeholderPath;
|
||||
|
||||
Animations::Simple _a_borderShown;
|
||||
int _borderAnimationStart = 0;
|
||||
Animations::Simple _a_borderOpacity;
|
||||
bool _borderVisible = false;
|
||||
|
||||
Animations::Simple _a_focused;
|
||||
Animations::Simple _a_error;
|
||||
|
||||
bool _focused = false;
|
||||
bool _error = false;
|
||||
|
||||
style::margins _textMargins;
|
||||
|
||||
QTimer _touchTimer;
|
||||
bool _touchPress = false;
|
||||
bool _touchRightButton = false;
|
||||
bool _touchMove = false;
|
||||
bool _mousePressedInTouch = false;
|
||||
QPoint _touchStart;
|
||||
|
||||
base::unique_qptr<PopupMenu> _contextMenu;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
53
Telegram/lib_ui/ui/widgets/fields/number_input.cpp
Normal file
53
Telegram/lib_ui/ui/widgets/fields/number_input.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
NumberInput::NumberInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &value,
|
||||
int limit)
|
||||
: MaskedInputField(parent, st, std::move(placeholder), value)
|
||||
, _limit(limit) {
|
||||
if (!value.toInt() || (limit > 0 && value.toInt() > limit)) {
|
||||
setText(QString());
|
||||
}
|
||||
}
|
||||
|
||||
void NumberInput::changeLimit(int limit) {
|
||||
_limit = limit;
|
||||
}
|
||||
|
||||
void NumberInput::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
QString newText;
|
||||
newText.reserve(now.size());
|
||||
auto newPos = nowCursor;
|
||||
for (auto i = 0, l = int(now.size()); i < l; ++i) {
|
||||
if (now.at(i).isDigit()) {
|
||||
newText.append(now.at(i));
|
||||
} else if (i < nowCursor) {
|
||||
--newPos;
|
||||
}
|
||||
}
|
||||
if (!newText.toInt()) {
|
||||
newText = QString();
|
||||
newPos = 0;
|
||||
} else if (_limit > 0 && newText.toInt() > _limit) {
|
||||
newText = was;
|
||||
newPos = wasCursor;
|
||||
}
|
||||
setCorrectedText(now, nowCursor, newText, newPos);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
40
Telegram/lib_ui/ui/widgets/fields/number_input.h
Normal file
40
Telegram/lib_ui/ui/widgets/fields/number_input.h
Normal file
@@ -0,0 +1,40 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
|
||||
namespace style {
|
||||
struct InputField;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class NumberInput final : public MaskedInputField {
|
||||
public:
|
||||
NumberInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &value,
|
||||
int limit);
|
||||
|
||||
void changeLimit(int limit);
|
||||
|
||||
protected:
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
|
||||
private:
|
||||
int _limit = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
20
Telegram/lib_ui/ui/widgets/fields/password_input.cpp
Normal file
20
Telegram/lib_ui/ui/widgets/fields/password_input.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/fields/password_input.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
PasswordInput::PasswordInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder,
|
||||
const QString &val)
|
||||
: MaskedInputField(parent, st, std::move(placeholder), val) {
|
||||
QLineEdit::setEchoMode(QLineEdit::Password);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
27
Telegram/lib_ui/ui/widgets/fields/password_input.h
Normal file
27
Telegram/lib_ui/ui/widgets/fields/password_input.h
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
|
||||
namespace style {
|
||||
struct InputField;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PasswordInput final : public MaskedInputField {
|
||||
public:
|
||||
PasswordInput(
|
||||
QWidget *parent,
|
||||
const style::InputField &st,
|
||||
rpl::producer<QString> placeholder = nullptr,
|
||||
const QString &val = QString());
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
140
Telegram/lib_ui/ui/widgets/fields/time_part_input.cpp
Normal file
140
Telegram/lib_ui/ui/widgets/fields/time_part_input.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/fields/time_part_input.h"
|
||||
|
||||
#include "base/qt/qt_string_view.h"
|
||||
#include "ui/ui_utility.h" // WheelDirection
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
std::optional<int> TimePart::number() {
|
||||
static const auto RegExp = QRegularExpression("^\\d+$");
|
||||
const auto text = getLastText();
|
||||
auto view = QStringView(text);
|
||||
while (view.size() > 1 && view.at(0) == '0') {
|
||||
view = base::StringViewMid(view, 1);
|
||||
}
|
||||
return RegExp.match(view).hasMatch()
|
||||
? std::make_optional(view.toInt())
|
||||
: std::nullopt;
|
||||
}
|
||||
|
||||
void TimePart::setMaxValue(int value) {
|
||||
_maxValue = value;
|
||||
_maxDigits = 0;
|
||||
while (value > 0) {
|
||||
++_maxDigits;
|
||||
value /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
void TimePart::setWheelStep(int value) {
|
||||
_wheelStep = value;
|
||||
}
|
||||
|
||||
rpl::producer<> TimePart::erasePrevious() const {
|
||||
return _erasePrevious.events();
|
||||
}
|
||||
|
||||
rpl::producer<> TimePart::jumpToPrevious() const {
|
||||
return _jumpToPrevious.events();
|
||||
}
|
||||
|
||||
rpl::producer<QChar> TimePart::putNext() const {
|
||||
return _putNext.events();
|
||||
}
|
||||
|
||||
void TimePart::keyPressEvent(QKeyEvent *e) {
|
||||
const auto position = cursorPosition();
|
||||
const auto selection = hasSelectedText();
|
||||
if (!selection && !position) {
|
||||
if (e->key() == Qt::Key_Backspace) {
|
||||
_erasePrevious.fire({});
|
||||
return;
|
||||
} else if (e->key() == Qt::Key_Left) {
|
||||
_jumpToPrevious.fire({});
|
||||
return;
|
||||
}
|
||||
} else if (!selection && position == getLastText().size()) {
|
||||
if (e->key() == Qt::Key_Right) {
|
||||
_putNext.fire(QChar(0));
|
||||
return;
|
||||
}
|
||||
}
|
||||
MaskedInputField::keyPressEvent(e);
|
||||
}
|
||||
|
||||
void TimePart::wheelEvent(QWheelEvent *e) {
|
||||
const auto direction = WheelDirection(e);
|
||||
const auto now = number();
|
||||
if (!now.has_value()) {
|
||||
return;
|
||||
}
|
||||
auto time = *now + (direction * _wheelStep);
|
||||
const auto max = _maxValue + 1;
|
||||
if (time < 0) {
|
||||
time += max;
|
||||
} else if (time >= max) {
|
||||
time -= max;
|
||||
}
|
||||
setText(QString::number(time));
|
||||
Ui::MaskedInputField::changed();
|
||||
}
|
||||
|
||||
void TimePart::correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) {
|
||||
const auto oldCursor = nowCursor;
|
||||
const auto oldLength = now.size();
|
||||
auto newCursor = (oldCursor > 0) ? -1 : 0;
|
||||
auto newText = QString();
|
||||
auto accumulated = 0;
|
||||
auto limit = 0;
|
||||
for (; limit != oldLength; ++limit) {
|
||||
if (now[limit].isDigit()) {
|
||||
accumulated *= 10;
|
||||
accumulated += (now[limit].unicode() - '0');
|
||||
if (accumulated > _maxValue || limit == _maxDigits) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto i = 0; i != limit;) {
|
||||
if (now[i].isDigit()) {
|
||||
newText += now[i];
|
||||
}
|
||||
if (++i == oldCursor) {
|
||||
newCursor = newText.size();
|
||||
}
|
||||
}
|
||||
if (newCursor < 0) {
|
||||
newCursor = newText.size();
|
||||
}
|
||||
if (newText != now) {
|
||||
now = newText;
|
||||
setText(now);
|
||||
startPlaceholderAnimation();
|
||||
}
|
||||
if (newCursor != nowCursor) {
|
||||
nowCursor = newCursor;
|
||||
setCursorPosition(nowCursor);
|
||||
}
|
||||
if (accumulated > _maxValue
|
||||
|| (limit == _maxDigits && oldLength > _maxDigits)) {
|
||||
if (oldCursor > limit) {
|
||||
_putNext.fire(QChar('0' + (accumulated % 10)));
|
||||
} else {
|
||||
_putNext.fire(QChar(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
46
Telegram/lib_ui/ui/widgets/fields/time_part_input.h
Normal file
46
Telegram/lib_ui/ui/widgets/fields/time_part_input.h
Normal file
@@ -0,0 +1,46 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/fields/masked_input_field.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class TimePart : public MaskedInputField {
|
||||
public:
|
||||
using MaskedInputField::MaskedInputField;
|
||||
|
||||
void setMaxValue(int value);
|
||||
void setWheelStep(int value);
|
||||
|
||||
[[nodiscard]] rpl::producer<> erasePrevious() const;
|
||||
[[nodiscard]] rpl::producer<> jumpToPrevious() const;
|
||||
[[nodiscard]] rpl::producer<QChar> putNext() const;
|
||||
|
||||
[[nodiscard]] std::optional<int> number();
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
void correctValue(
|
||||
const QString &was,
|
||||
int wasCursor,
|
||||
QString &now,
|
||||
int &nowCursor) override;
|
||||
|
||||
private:
|
||||
int _maxValue = 0;
|
||||
int _maxDigits = 0;
|
||||
int _wheelStep = 0;
|
||||
rpl::event_stream<> _erasePrevious;
|
||||
rpl::event_stream<> _jumpToPrevious;
|
||||
rpl::event_stream<QChar> _putNext;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
44
Telegram/lib_ui/ui/widgets/icon_button_with_text.cpp
Normal file
44
Telegram/lib_ui/ui/widgets/icon_button_with_text.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/icon_button_with_text.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
IconButtonWithText::IconButtonWithText(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::IconButtonWithText &st)
|
||||
: IconButton(parent, st.iconButton)
|
||||
, _st(st) {
|
||||
}
|
||||
|
||||
void IconButtonWithText::paintEvent(QPaintEvent *e) {
|
||||
IconButton::paintEvent(e);
|
||||
|
||||
const auto r = rect() - _st.textPadding;
|
||||
const auto overIconOpacity = IconButton::iconOverOpacity();
|
||||
|
||||
auto p = QPainter(this);
|
||||
p.setFont(_st.font);
|
||||
p.setPen((overIconOpacity == 1.) ? _st.textFgOver : _st.textFg);
|
||||
p.drawText(r, _text, _st.textAlign);
|
||||
|
||||
if (overIconOpacity > 0. && overIconOpacity < 1.) {
|
||||
p.setPen(_st.textFgOver);
|
||||
p.setOpacity(overIconOpacity);
|
||||
p.drawText(r, _text, _st.textAlign);
|
||||
}
|
||||
}
|
||||
|
||||
void IconButtonWithText::setText(const QString &text) {
|
||||
if (_text != text) {
|
||||
_text = text;
|
||||
accessibilityNameChanged();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
34
Telegram/lib_ui/ui/widgets/icon_button_with_text.h
Normal file
34
Telegram/lib_ui/ui/widgets/icon_button_with_text.h
Normal file
@@ -0,0 +1,34 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class IconButtonWithText final : public Ui::IconButton {
|
||||
public:
|
||||
IconButtonWithText(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::IconButtonWithText &st);
|
||||
|
||||
void setText(const QString &text);
|
||||
|
||||
QString accessibilityName() override {
|
||||
return _text;
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::IconButtonWithText &_st;
|
||||
QString _text;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
415
Telegram/lib_ui/ui/widgets/inner_dropdown.cpp
Normal file
415
Telegram/lib_ui/ui/widgets/inner_dropdown.cpp
Normal file
@@ -0,0 +1,415 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/inner_dropdown.h"
|
||||
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/shadow.h"
|
||||
#include "ui/effects/panel_animation.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/ui_utility.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
InnerDropdown::InnerDropdown(
|
||||
QWidget *parent,
|
||||
const style::InnerDropdown &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _roundRect(ImageRoundRadius::Small, _st.bg)
|
||||
, _hideTimer([=] { hideAnimated(); })
|
||||
, _scroll(this, _st.scroll) {
|
||||
_scroll->scrolls(
|
||||
) | rpl::on_next([=] {
|
||||
scrolled();
|
||||
}, lifetime());
|
||||
|
||||
hide();
|
||||
|
||||
shownValue(
|
||||
) | rpl::filter([](bool shown) {
|
||||
return shown;
|
||||
}) | rpl::take(1) | rpl::map([=] {
|
||||
// We can't invoke this before the window is created.
|
||||
// So instead we start handling them on the first show().
|
||||
return macWindowDeactivateEvents();
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::filter([=] {
|
||||
return !isHidden();
|
||||
}) | rpl::on_next([=] {
|
||||
leaveEvent(nullptr);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
QPointer<RpWidget> InnerDropdown::doSetOwnedWidget(
|
||||
object_ptr<RpWidget> widget) {
|
||||
auto result = QPointer<RpWidget>(widget);
|
||||
widget->heightValue(
|
||||
) | rpl::skip(1) | rpl::on_next([=] {
|
||||
resizeToContent();
|
||||
}, widget->lifetime());
|
||||
auto container = _scroll->setOwnedWidget(
|
||||
object_ptr<Container>(
|
||||
_scroll,
|
||||
std::move(widget),
|
||||
_st));
|
||||
container->resizeToWidth(_scroll->width());
|
||||
container->moveToLeft(0, 0);
|
||||
container->show();
|
||||
result->show();
|
||||
return result;
|
||||
}
|
||||
|
||||
void InnerDropdown::setMaxHeight(int newMaxHeight) {
|
||||
_maxHeight = newMaxHeight;
|
||||
resizeToContent();
|
||||
}
|
||||
|
||||
void InnerDropdown::resizeToContent() {
|
||||
auto newWidth = _st.padding.left() + _st.scrollMargin.left() + _st.scrollMargin.right() + _st.padding.right();
|
||||
auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
|
||||
if (auto widget = static_cast<Container*>(_scroll->widget())) {
|
||||
widget->resizeToContent();
|
||||
newWidth += widget->width();
|
||||
newHeight += widget->height();
|
||||
}
|
||||
if (_maxHeight > 0) {
|
||||
accumulate_min(newHeight, _maxHeight);
|
||||
}
|
||||
if (newWidth != width() || newHeight != height()) {
|
||||
resize(newWidth, newHeight);
|
||||
update();
|
||||
finishAnimating();
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::resizeEvent(QResizeEvent *e) {
|
||||
_scroll->setGeometry(
|
||||
rect().marginsRemoved(_st.padding).marginsRemoved(_st.scrollMargin));
|
||||
if (auto widget = static_cast<RpWidget*>(_scroll->widget())) {
|
||||
widget->resizeToWidth(_scroll->width());
|
||||
scrolled();
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::scrolled() {
|
||||
if (auto widget = static_cast<RpWidget*>(_scroll->widget())) {
|
||||
int visibleTop = _scroll->scrollTop();
|
||||
int visibleBottom = visibleTop + _scroll->height();
|
||||
widget->setVisibleTopBottom(visibleTop, visibleBottom);
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
|
||||
if (_a_show.animating()) {
|
||||
if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
|
||||
// _a_opacity.current(ms)->opacityAnimationCallback()->_showAnimation.reset()
|
||||
if (_showAnimation) {
|
||||
_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
|
||||
}
|
||||
}
|
||||
} else if (_a_opacity.animating()) {
|
||||
p.setOpacity(_a_opacity.value(0.));
|
||||
p.drawPixmap(0, 0, _cache);
|
||||
} else if (_hiding || isHidden()) {
|
||||
hideFinished();
|
||||
} else if (_showAnimation) {
|
||||
_showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
|
||||
_showAnimation.reset();
|
||||
showChildren();
|
||||
} else {
|
||||
if (!_cache.isNull()) _cache = QPixmap();
|
||||
const auto inner = rect().marginsRemoved(_st.padding);
|
||||
Shadow::paint(p, inner, width(), _st.shadow);
|
||||
_roundRect.paint(p, inner);
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::enterEventHook(QEnterEvent *e) {
|
||||
if (_autoHiding) {
|
||||
showAnimated(_origin);
|
||||
}
|
||||
return RpWidget::enterEventHook(e);
|
||||
}
|
||||
|
||||
void InnerDropdown::leaveEventHook(QEvent *e) {
|
||||
if (_autoHiding) {
|
||||
if (_a_show.animating() || _a_opacity.animating()) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.callOnce(300);
|
||||
}
|
||||
}
|
||||
return RpWidget::leaveEventHook(e);
|
||||
}
|
||||
|
||||
void InnerDropdown::otherEnter() {
|
||||
if (_autoHiding) {
|
||||
showAnimated(_origin);
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::otherLeave() {
|
||||
if (_autoHiding) {
|
||||
if (_a_show.animating() || _a_opacity.animating()) {
|
||||
hideAnimated();
|
||||
} else {
|
||||
_hideTimer.callOnce(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::setOrigin(PanelAnimation::Origin origin) {
|
||||
_origin = origin;
|
||||
}
|
||||
|
||||
void InnerDropdown::showAnimated(PanelAnimation::Origin origin) {
|
||||
setOrigin(origin);
|
||||
showAnimated();
|
||||
}
|
||||
|
||||
void InnerDropdown::showAnimated() {
|
||||
_hideTimer.cancel();
|
||||
showStarted();
|
||||
}
|
||||
|
||||
void InnerDropdown::hideAnimated(HideOption option) {
|
||||
if (isHidden()) return;
|
||||
if (option == HideOption::IgnoreShow) {
|
||||
_ignoreShowEvents = true;
|
||||
}
|
||||
if (_hiding) return;
|
||||
|
||||
_hideTimer.cancel();
|
||||
startOpacityAnimation(true);
|
||||
}
|
||||
|
||||
void InnerDropdown::finishAnimating() {
|
||||
if (_a_show.animating()) {
|
||||
_a_show.stop();
|
||||
showAnimationCallback();
|
||||
}
|
||||
if (_showAnimation) {
|
||||
_showAnimation.reset();
|
||||
showChildren();
|
||||
}
|
||||
if (_a_opacity.animating()) {
|
||||
_a_opacity.stop();
|
||||
opacityAnimationCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::showFast() {
|
||||
_hideTimer.cancel();
|
||||
finishAnimating();
|
||||
if (isHidden()) {
|
||||
showChildren();
|
||||
show();
|
||||
}
|
||||
_hiding = false;
|
||||
}
|
||||
|
||||
void InnerDropdown::hideFast() {
|
||||
if (isHidden()) return;
|
||||
|
||||
_hideTimer.cancel();
|
||||
finishAnimating();
|
||||
_hiding = false;
|
||||
hideFinished();
|
||||
}
|
||||
|
||||
void InnerDropdown::hideFinished() {
|
||||
_a_show.stop();
|
||||
_showAnimation.reset();
|
||||
_cache = QPixmap();
|
||||
_ignoreShowEvents = false;
|
||||
if (!isHidden()) {
|
||||
const auto weak = base::make_weak(this);
|
||||
if (const auto onstack = _hiddenCallback) {
|
||||
onstack();
|
||||
}
|
||||
if (weak) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::prepareCache() {
|
||||
if (_a_opacity.animating()) return;
|
||||
|
||||
const auto animating = _a_show.animating();
|
||||
auto showAnimation = base::take(_a_show);
|
||||
auto showAnimationData = base::take(_showAnimation);
|
||||
showChildren();
|
||||
_cache = GrabWidget(this);
|
||||
if (animating) {
|
||||
hideChildren();
|
||||
}
|
||||
_showAnimation = base::take(showAnimationData);
|
||||
_a_show = base::take(showAnimation);
|
||||
}
|
||||
|
||||
void InnerDropdown::startOpacityAnimation(bool hiding) {
|
||||
const auto weak = base::make_weak(this);
|
||||
if (hiding) {
|
||||
if (const auto onstack = _hideStartCallback) {
|
||||
onstack();
|
||||
}
|
||||
} else if (const auto onstack = _showStartCallback) {
|
||||
onstack();
|
||||
}
|
||||
if (!weak) {
|
||||
return;
|
||||
}
|
||||
|
||||
_hiding = false;
|
||||
prepareCache();
|
||||
_hiding = hiding;
|
||||
hideChildren();
|
||||
_a_opacity.start(
|
||||
[=] { opacityAnimationCallback(); },
|
||||
_hiding ? 1. : 0.,
|
||||
_hiding ? 0. : 1.,
|
||||
_st.duration);
|
||||
}
|
||||
|
||||
void InnerDropdown::showStarted() {
|
||||
if (_ignoreShowEvents) return;
|
||||
if (isHidden()) {
|
||||
show();
|
||||
startShowAnimation();
|
||||
return;
|
||||
} else if (!_hiding) {
|
||||
return;
|
||||
}
|
||||
startOpacityAnimation(false);
|
||||
}
|
||||
|
||||
void InnerDropdown::startShowAnimation() {
|
||||
if (_showStartCallback) {
|
||||
_showStartCallback();
|
||||
}
|
||||
if (!_a_show.animating()) {
|
||||
auto opacityAnimation = base::take(_a_opacity);
|
||||
showChildren();
|
||||
auto cache = grabForPanelAnimation();
|
||||
_a_opacity = base::take(opacityAnimation);
|
||||
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
_showAnimation = std::make_unique<PanelAnimation>(_st.animation, _origin);
|
||||
auto inner = rect().marginsRemoved(_st.padding);
|
||||
_showAnimation->setFinalImage(std::move(cache), QRect(inner.topLeft() * pixelRatio, inner.size() * pixelRatio));
|
||||
_showAnimation->setCornerMasks(
|
||||
Images::CornersMask(ImageRoundRadius::Small));
|
||||
_showAnimation->start();
|
||||
}
|
||||
hideChildren();
|
||||
_a_show.start([this] { showAnimationCallback(); }, 0., 1., _st.showDuration);
|
||||
}
|
||||
|
||||
QImage InnerDropdown::grabForPanelAnimation() {
|
||||
SendPendingMoveResizeEvents(this);
|
||||
const auto pixelRatio = style::DevicePixelRatio();
|
||||
auto result = QImage(size() * pixelRatio, QImage::Format_ARGB32_Premultiplied);
|
||||
result.setDevicePixelRatio(pixelRatio);
|
||||
result.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&result);
|
||||
_roundRect.paint(p, rect().marginsRemoved(_st.padding));
|
||||
for (const auto child : children()) {
|
||||
if (const auto widget = qobject_cast<QWidget*>(child)) {
|
||||
RenderWidget(p, widget, widget->pos());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InnerDropdown::opacityAnimationCallback() {
|
||||
update();
|
||||
if (!_a_opacity.animating()) {
|
||||
if (_hiding) {
|
||||
_hiding = false;
|
||||
hideFinished();
|
||||
} else if (!_a_show.animating()) {
|
||||
showChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InnerDropdown::showAnimationCallback() {
|
||||
update();
|
||||
}
|
||||
|
||||
bool InnerDropdown::eventFilter(QObject *obj, QEvent *e) {
|
||||
if (e->type() == QEvent::Enter) {
|
||||
otherEnter();
|
||||
} else if (e->type() == QEvent::Leave) {
|
||||
otherLeave();
|
||||
} else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(e)->button() == Qt::LeftButton) {
|
||||
if (isHidden() || _hiding) {
|
||||
otherEnter();
|
||||
} else {
|
||||
otherLeave();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int InnerDropdown::resizeGetHeight(int newWidth) {
|
||||
auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
|
||||
if (auto widget = static_cast<RpWidget*>(_scroll->widget())) {
|
||||
auto containerWidth = newWidth - _st.padding.left() - _st.padding.right() - _st.scrollMargin.left() - _st.scrollMargin.right();
|
||||
widget->resizeToWidth(containerWidth);
|
||||
newHeight += widget->height();
|
||||
}
|
||||
if (_maxHeight > 0) {
|
||||
accumulate_min(newHeight, _maxHeight);
|
||||
}
|
||||
return newHeight;
|
||||
}
|
||||
|
||||
InnerDropdown::Container::Container(
|
||||
QWidget *parent,
|
||||
object_ptr<RpWidget> child,
|
||||
const style::InnerDropdown &st)
|
||||
: RpWidget(parent)
|
||||
, _child(std::move(child))
|
||||
, _st(st) {
|
||||
_child->setParent(this);
|
||||
_child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
|
||||
}
|
||||
|
||||
void InnerDropdown::Container::visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) {
|
||||
setChildVisibleTopBottom(_child, visibleTop, visibleBottom);
|
||||
}
|
||||
|
||||
void InnerDropdown::Container::resizeToContent() {
|
||||
auto newWidth = _st.scrollPadding.left() + _st.scrollPadding.right();
|
||||
auto newHeight = _st.scrollPadding.top() + _st.scrollPadding.bottom();
|
||||
if (auto child = static_cast<RpWidget*>(children().front())) {
|
||||
newWidth += child->width();
|
||||
newHeight += child->height();
|
||||
}
|
||||
if (newWidth != width() || newHeight != height()) {
|
||||
resize(newWidth, newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
int InnerDropdown::Container::resizeGetHeight(int newWidth) {
|
||||
auto innerWidth = newWidth - _st.scrollPadding.left() - _st.scrollPadding.right();
|
||||
auto result = _st.scrollPadding.top() + _st.scrollPadding.bottom();
|
||||
_child->resizeToWidth(innerWidth);
|
||||
_child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
|
||||
result += _child->height();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
144
Telegram/lib_ui/ui/widgets/inner_dropdown.h
Normal file
144
Telegram/lib_ui/ui/widgets/inner_dropdown.h
Normal file
@@ -0,0 +1,144 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/panel_animation.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ScrollArea;
|
||||
|
||||
class InnerDropdown : public RpWidget {
|
||||
|
||||
public:
|
||||
InnerDropdown(QWidget *parent, const style::InnerDropdown &st = st::defaultInnerDropdown);
|
||||
|
||||
template <typename Widget>
|
||||
QPointer<Widget> setOwnedWidget(object_ptr<Widget> widget) {
|
||||
auto result = doSetOwnedWidget(std::move(widget));
|
||||
return QPointer<Widget>(static_cast<Widget*>(result.data()));
|
||||
}
|
||||
|
||||
bool overlaps(const QRect &globalRect) {
|
||||
if (isHidden() || _a_show.animating() || _a_opacity.animating()) return false;
|
||||
|
||||
return rect().marginsRemoved(_st.padding).contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size()));
|
||||
}
|
||||
|
||||
void setAutoHiding(bool autoHiding) {
|
||||
_autoHiding = autoHiding;
|
||||
}
|
||||
void setMaxHeight(int newMaxHeight);
|
||||
void resizeToContent();
|
||||
|
||||
void otherEnter();
|
||||
void otherLeave();
|
||||
|
||||
void setShowStartCallback(Fn<void()> callback) {
|
||||
_showStartCallback = std::move(callback);
|
||||
}
|
||||
void setHideStartCallback(Fn<void()> callback) {
|
||||
_hideStartCallback = std::move(callback);
|
||||
}
|
||||
void setHiddenCallback(Fn<void()> callback) {
|
||||
_hiddenCallback = std::move(callback);
|
||||
}
|
||||
|
||||
bool isHiding() const {
|
||||
return _hiding && _a_opacity.animating();
|
||||
}
|
||||
|
||||
enum class HideOption {
|
||||
Default,
|
||||
IgnoreShow,
|
||||
};
|
||||
void showAnimated();
|
||||
void setOrigin(PanelAnimation::Origin origin);
|
||||
void showAnimated(PanelAnimation::Origin origin);
|
||||
void hideAnimated(HideOption option = HideOption::Default);
|
||||
void finishAnimating();
|
||||
void showFast();
|
||||
void hideFast();
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
bool eventFilter(QObject *obj, QEvent *e) override;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
|
||||
private:
|
||||
QPointer<RpWidget> doSetOwnedWidget(object_ptr<RpWidget> widget);
|
||||
QImage grabForPanelAnimation();
|
||||
void startShowAnimation();
|
||||
void startOpacityAnimation(bool hiding);
|
||||
void prepareCache();
|
||||
|
||||
class Container;
|
||||
void showAnimationCallback();
|
||||
void opacityAnimationCallback();
|
||||
|
||||
void hideFinished();
|
||||
void showStarted();
|
||||
|
||||
void updateHeight();
|
||||
|
||||
void scrolled();
|
||||
|
||||
const style::InnerDropdown &_st;
|
||||
|
||||
RoundRect _roundRect;
|
||||
PanelAnimation::Origin _origin = PanelAnimation::Origin::TopLeft;
|
||||
std::unique_ptr<PanelAnimation> _showAnimation;
|
||||
Animations::Simple _a_show;
|
||||
|
||||
bool _autoHiding = true;
|
||||
bool _hiding = false;
|
||||
QPixmap _cache;
|
||||
Animations::Simple _a_opacity;
|
||||
|
||||
base::Timer _hideTimer;
|
||||
bool _ignoreShowEvents = false;
|
||||
Fn<void()> _showStartCallback;
|
||||
Fn<void()> _hideStartCallback;
|
||||
Fn<void()> _hiddenCallback;
|
||||
|
||||
object_ptr<ScrollArea> _scroll;
|
||||
|
||||
int _maxHeight = 0;
|
||||
|
||||
};
|
||||
|
||||
class InnerDropdown::Container : public RpWidget {
|
||||
public:
|
||||
Container(QWidget *parent, object_ptr<RpWidget> child, const style::InnerDropdown &st);
|
||||
|
||||
void resizeToContent();
|
||||
|
||||
protected:
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void visibleTopBottomUpdated(
|
||||
int visibleTop,
|
||||
int visibleBottom) override;
|
||||
|
||||
private:
|
||||
object_ptr<RpWidget> _child;
|
||||
|
||||
const style::InnerDropdown &_st;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1020
Telegram/lib_ui/ui/widgets/labels.cpp
Normal file
1020
Telegram/lib_ui/ui/widgets/labels.cpp
Normal file
File diff suppressed because it is too large
Load Diff
309
Telegram/lib_ui/ui/widgets/labels.h
Normal file
309
Telegram/lib_ui/ui/widgets/labels.h
Normal file
@@ -0,0 +1,309 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/click_handler.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class QTouchEvent;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PopupMenu;
|
||||
class BoxContentDivider;
|
||||
|
||||
class CrossFadeAnimation {
|
||||
public:
|
||||
struct Data {
|
||||
QImage full;
|
||||
std::vector<int> lineWidths;
|
||||
QPoint position;
|
||||
style::align align;
|
||||
style::font font;
|
||||
style::margins margin;
|
||||
int lineHeight = 0;
|
||||
int lineAddTop = 0;
|
||||
};
|
||||
|
||||
CrossFadeAnimation(style::color bg, Data &&from, Data &&to);
|
||||
|
||||
struct Part {
|
||||
QPixmap snapshot;
|
||||
QPoint position;
|
||||
};
|
||||
void addLine(Part was, Part now);
|
||||
|
||||
void paintFrame(QPainter &p, float64 dt);
|
||||
void paintFrame(
|
||||
QPainter &p,
|
||||
float64 positionReady,
|
||||
float64 alphaWas,
|
||||
float64 alphaNow);
|
||||
|
||||
private:
|
||||
struct Line {
|
||||
Line(Part was, Part now) : was(std::move(was)), now(std::move(now)) {
|
||||
}
|
||||
Part was;
|
||||
Part now;
|
||||
};
|
||||
void paintLine(
|
||||
QPainter &p,
|
||||
const Line &line,
|
||||
float64 positionReady,
|
||||
float64 alphaWas,
|
||||
float64 alphaNow);
|
||||
|
||||
style::color _bg;
|
||||
QList<Line> _lines;
|
||||
|
||||
};
|
||||
|
||||
class LabelSimple : public RpWidget {
|
||||
public:
|
||||
LabelSimple(
|
||||
QWidget *parent,
|
||||
const style::LabelSimple &st = st::defaultLabelSimple,
|
||||
const QString &value = QString());
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::StaticText;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _fullText;
|
||||
}
|
||||
|
||||
// This method also resizes the label.
|
||||
void setText(const QString &newText, bool *outTextChanged = nullptr);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
QString _fullText;
|
||||
int _fullTextWidth;
|
||||
|
||||
QString _text;
|
||||
int _textWidth;
|
||||
|
||||
const style::LabelSimple &_st;
|
||||
|
||||
};
|
||||
|
||||
class FlatLabel : public RpWidget, public ClickHandlerHost {
|
||||
public:
|
||||
FlatLabel(
|
||||
QWidget *parent,
|
||||
const style::FlatLabel &st = st::defaultFlatLabel,
|
||||
const style::PopupMenu &stMenu = st::defaultPopupMenu);
|
||||
|
||||
FlatLabel(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
const style::FlatLabel &st = st::defaultFlatLabel,
|
||||
const style::PopupMenu &stMenu = st::defaultPopupMenu);
|
||||
|
||||
FlatLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<QString> &&text,
|
||||
const style::FlatLabel &st = st::defaultFlatLabel,
|
||||
const style::PopupMenu &stMenu = st::defaultPopupMenu);
|
||||
FlatLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
const style::FlatLabel &st = st::defaultFlatLabel,
|
||||
const style::PopupMenu &stMenu = st::defaultPopupMenu,
|
||||
const Text::MarkedContext &context = {});
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::StaticText;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _text.toString();
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::FlatLabel &st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
void setOpacity(float64 o);
|
||||
void setTextColorOverride(std::optional<QColor> color);
|
||||
|
||||
void setText(const QString &text);
|
||||
void setMarkedText(
|
||||
const TextWithEntities &textWithEntities,
|
||||
Text::MarkedContext context = {});
|
||||
void setSelectable(bool selectable);
|
||||
void setDoubleClickSelectsParagraph(bool doubleClickSelectsParagraph);
|
||||
void setContextCopyText(const QString ©Text);
|
||||
void setBreakEverywhere(bool breakEverywhere);
|
||||
void setTryMakeSimilarLines(bool tryMakeSimilarLines);
|
||||
enum class WhichAnimationsPaused {
|
||||
None,
|
||||
CustomEmoji,
|
||||
Spoiler,
|
||||
All,
|
||||
};
|
||||
void setAnimationsPausedCallback(Fn<WhichAnimationsPaused()> callback) {
|
||||
_animationsPausedCallback = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] int textMaxWidth() const;
|
||||
QMargins getMargins() const override;
|
||||
|
||||
void setLink(uint16 index, const ClickHandlerPtr &lnk);
|
||||
void setLinksTrusted();
|
||||
|
||||
using ClickHandlerFilter = Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)>;
|
||||
void setClickHandlerFilter(ClickHandlerFilter &&filter);
|
||||
void overrideLinkClickHandler(Fn<void()> handler);
|
||||
void overrideLinkClickHandler(Fn<void(QString url)> handler);
|
||||
|
||||
struct ContextMenuRequest {
|
||||
not_null<PopupMenu*> menu;
|
||||
ClickHandlerPtr link;
|
||||
TextSelection selection;
|
||||
bool uponSelection = false;
|
||||
bool fullSelection = false;
|
||||
};
|
||||
void setContextMenuHook(Fn<void(ContextMenuRequest)> hook);
|
||||
void fillContextMenu(ContextMenuRequest request);
|
||||
|
||||
// ClickHandlerHost interface
|
||||
void clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) override;
|
||||
void clickHandlerPressedChanged(const ClickHandlerPtr &action, bool pressed) override;
|
||||
|
||||
[[nodiscard]] CrossFadeAnimation::Data crossFadeData(
|
||||
style::color bg,
|
||||
QPoint basePosition = QPoint());
|
||||
|
||||
static std::unique_ptr<CrossFadeAnimation> CrossFade(
|
||||
not_null<FlatLabel*> from,
|
||||
not_null<FlatLabel*> to,
|
||||
style::color bg,
|
||||
QPoint fromPosition = QPoint(),
|
||||
QPoint toPosition = QPoint());
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void focusOutEvent(QFocusEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
bool eventHook(QEvent *e) override; // calls touchEvent when necessary
|
||||
void touchEvent(QTouchEvent *e);
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
void copySelectedText();
|
||||
void copyContextText();
|
||||
|
||||
void touchSelect();
|
||||
|
||||
void executeDrag();
|
||||
|
||||
private:
|
||||
void init();
|
||||
void textUpdated();
|
||||
|
||||
Text::StateResult dragActionUpdate();
|
||||
Text::StateResult dragActionStart(const QPoint &p, Qt::MouseButton button);
|
||||
Text::StateResult dragActionFinish(const QPoint &p, Qt::MouseButton button);
|
||||
void updateHover(const Text::StateResult &state);
|
||||
Text::StateResult getTextState(const QPoint &m) const;
|
||||
void refreshCursor(bool uponSymbol);
|
||||
|
||||
int countTextWidth(int newWidth) const;
|
||||
int countTextHeight(int textWidth);
|
||||
void refreshSize();
|
||||
|
||||
enum class ContextMenuReason {
|
||||
FromEvent,
|
||||
FromTouch,
|
||||
};
|
||||
void showContextMenu(QContextMenuEvent *e, ContextMenuReason reason);
|
||||
|
||||
Text::String _text;
|
||||
const style::FlatLabel &_st;
|
||||
const style::PopupMenu &_stMenu;
|
||||
std::optional<QColor> _textColorOverride;
|
||||
float64 _opacity = 1.;
|
||||
|
||||
int _textWidth = 0;
|
||||
int _fullTextHeight = 0;
|
||||
bool _breakEverywhere = false;
|
||||
bool _tryMakeSimilarLines = false;
|
||||
|
||||
style::cursor _cursor = style::cur_default;
|
||||
bool _selectable = false;
|
||||
TextSelection _selection, _savedSelection;
|
||||
TextSelectType _selectionType = TextSelectType::Letters;
|
||||
bool _doubleClickSelectsParagraph = false;
|
||||
|
||||
enum DragAction {
|
||||
NoDrag = 0x00,
|
||||
PrepareDrag = 0x01,
|
||||
Dragging = 0x02,
|
||||
Selecting = 0x04,
|
||||
};
|
||||
DragAction _dragAction = NoDrag;
|
||||
QPoint _dragStartPosition;
|
||||
uint16 _dragSymbol = 0;
|
||||
bool _dragWasInactive = false;
|
||||
|
||||
QPoint _lastMousePos;
|
||||
|
||||
QPoint _trippleClickPoint;
|
||||
base::Timer _trippleClickTimer;
|
||||
|
||||
base::unique_qptr<PopupMenu> _contextMenu;
|
||||
Fn<void(ContextMenuRequest)> _contextMenuHook;
|
||||
QString _contextCopyText;
|
||||
|
||||
ClickHandlerFilter _clickHandlerFilter;
|
||||
Fn<WhichAnimationsPaused()> _animationsPausedCallback;
|
||||
|
||||
// text selection and context menu by touch support (at least Windows Surface tablets)
|
||||
bool _touchSelect = false;
|
||||
bool _touchInProgress = false;
|
||||
QPoint _touchStart, _touchPrevPos, _touchPos;
|
||||
base::Timer _touchSelectTimer;
|
||||
|
||||
};
|
||||
|
||||
class DividerLabel : public PaddingWrap<> {
|
||||
public:
|
||||
DividerLabel(
|
||||
QWidget *parent,
|
||||
object_ptr<RpWidget> &&child,
|
||||
const style::margins &padding,
|
||||
const style::DividerBar &st = st::defaultDividerBar,
|
||||
RectParts parts = RectPart::Top | RectPart::Bottom);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void wrappedNaturalWidthUpdated(int width) override;
|
||||
|
||||
object_ptr<BoxContentDivider> _background;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
460
Telegram/lib_ui/ui/widgets/menu/menu.cpp
Normal file
460
Telegram/lib_ui/ui/widgets/menu/menu.cpp
Normal file
@@ -0,0 +1,460 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "ui/widgets/menu/menu_separator.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
Menu::Menu(QWidget *parent, const style::Menu &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st) {
|
||||
init();
|
||||
}
|
||||
|
||||
Menu::Menu(QWidget *parent, QMenu *menu, const style::Menu &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _wappedMenu(menu) {
|
||||
init();
|
||||
|
||||
_wappedMenu->setParent(this);
|
||||
for (auto action : _wappedMenu->actions()) {
|
||||
addAction(action);
|
||||
}
|
||||
_wappedMenu->hide();
|
||||
}
|
||||
|
||||
Menu::~Menu() = default;
|
||||
|
||||
void Menu::init() {
|
||||
resize(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
|
||||
|
||||
setMouseTracking(true);
|
||||
|
||||
if (_st.itemBg->c.alpha() == 255) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
paintRequest(
|
||||
) | rpl::on_next([=](const QRect &clip) {
|
||||
QPainter(this).fillRect(clip, _st.itemBg);
|
||||
}, lifetime());
|
||||
|
||||
positionValue(
|
||||
) | rpl::on_next([=] {
|
||||
handleMouseMove(QCursor::pos());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::addAction(
|
||||
const QString &text,
|
||||
Fn<void()> callback,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver) {
|
||||
auto action = CreateAction(this, text, std::move(callback));
|
||||
return addAction(std::move(action), icon, iconOver);
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::addAction(
|
||||
const QString &text,
|
||||
std::unique_ptr<QMenu> submenu,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver) {
|
||||
const auto action = new QAction(text, this);
|
||||
action->setMenu(submenu.release());
|
||||
return addAction(action, icon, iconOver);
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::addAction(
|
||||
not_null<QAction*> action,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver) {
|
||||
if (action->isSeparator()) {
|
||||
return addSeparator();
|
||||
}
|
||||
auto item = base::make_unique_q<Action>(
|
||||
this,
|
||||
_st,
|
||||
std::move(action),
|
||||
icon,
|
||||
iconOver ? iconOver : icon);
|
||||
return addAction(std::move(item));
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::addAction(base::unique_qptr<ItemBase> widget) {
|
||||
return insertAction(_actions.size(), std::move(widget));
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::insertAction(
|
||||
int position,
|
||||
base::unique_qptr<ItemBase> widget) {
|
||||
Expects(position >= 0 && position <= _actions.size());
|
||||
Expects(position >= 0 && position <= _actionWidgets.size());
|
||||
|
||||
const auto raw = widget.get();
|
||||
const auto action = raw->action();
|
||||
_actions.insert(begin(_actions) + position, action);
|
||||
|
||||
raw->setMenuAsParent(this);
|
||||
raw->show();
|
||||
raw->setIndex(position);
|
||||
for (auto i = position, to = int(_actionWidgets.size()); i != to; ++i) {
|
||||
_actionWidgets[i]->setIndex(i + 1);
|
||||
}
|
||||
_actionWidgets.insert(
|
||||
begin(_actionWidgets) + position,
|
||||
std::move(widget));
|
||||
|
||||
raw->selects(
|
||||
) | rpl::on_next([=](const CallbackData &data) {
|
||||
if (!data.selected) {
|
||||
if (!findSelectedAction()
|
||||
&& data.index < _actionWidgets.size()
|
||||
&& _childShownAction == data.action) {
|
||||
const auto widget = _actionWidgets[data.index].get();
|
||||
widget->setSelected(true, widget->lastTriggeredSource());
|
||||
}
|
||||
return;
|
||||
}
|
||||
_lastSelectedByMouse = (data.source == TriggeredSource::Mouse);
|
||||
for (auto i = 0; i < _actionWidgets.size(); i++) {
|
||||
if (i != data.index) {
|
||||
_actionWidgets[i]->setSelected(false);
|
||||
}
|
||||
}
|
||||
if (_activatedCallback) {
|
||||
_activatedCallback(data);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->clicks(
|
||||
) | rpl::on_next([=](const CallbackData &data) {
|
||||
if (_triggeredCallback) {
|
||||
_triggeredCallback(data);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
QObject::connect(action.get(), &QAction::changed, raw, [=] {
|
||||
// Select an item under mouse that was disabled and became enabled.
|
||||
if (_lastSelectedByMouse
|
||||
&& !findSelectedAction()
|
||||
&& action->isEnabled()) {
|
||||
updateSelected(QCursor::pos());
|
||||
}
|
||||
});
|
||||
|
||||
raw->minWidthValue(
|
||||
) | rpl::skip(1) | rpl::filter([=] {
|
||||
return !_forceWidth;
|
||||
}) | rpl::on_next([=] {
|
||||
resizeFromInner(recountWidth(), height());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->heightValue(
|
||||
) | rpl::skip(1) | rpl::on_next([=] {
|
||||
resizeFromInner(width(), recountHeight());
|
||||
}, raw->lifetime());
|
||||
|
||||
resizeFromInner(recountWidth(), recountHeight());
|
||||
|
||||
updateSelected(QCursor::pos());
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
int Menu::recountWidth() const {
|
||||
return _forceWidth
|
||||
? _forceWidth
|
||||
: std::clamp(
|
||||
(_actionWidgets.empty()
|
||||
? 0
|
||||
: (*ranges::max_element(
|
||||
_actionWidgets,
|
||||
std::less<>(),
|
||||
&ItemBase::minWidth))->minWidth()),
|
||||
_st.widthMin,
|
||||
_st.widthMax);
|
||||
}
|
||||
|
||||
int Menu::recountHeight() const {
|
||||
auto result = 0;
|
||||
for (const auto &widget : _actionWidgets) {
|
||||
if (widget->y() != result) {
|
||||
widget->move(0, result);
|
||||
}
|
||||
result += widget->height();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Menu::removeAction(int position) {
|
||||
Expects(position >= 0 && position < actions().size());
|
||||
|
||||
_actionWidgets.erase(begin(_actionWidgets) + position);
|
||||
if (_actions[position]->parent() == this) {
|
||||
delete _actions[position];
|
||||
}
|
||||
_actions.erase(begin(_actions) + position);
|
||||
resizeFromInner(width(), recountHeight());
|
||||
}
|
||||
|
||||
not_null<QAction*> Menu::addSeparator(const style::MenuSeparator *st) {
|
||||
const auto separator = new QAction(this);
|
||||
separator->setSeparator(true);
|
||||
auto item = base::make_unique_q<Separator>(
|
||||
this,
|
||||
_st,
|
||||
st ? *st : _st.separator,
|
||||
separator);
|
||||
return addAction(std::move(item));
|
||||
}
|
||||
|
||||
void Menu::clearActions() {
|
||||
_actionWidgets.clear();
|
||||
for (auto action : base::take(_actions)) {
|
||||
if (action->parent() == this) {
|
||||
delete action;
|
||||
}
|
||||
}
|
||||
resizeFromInner(_forceWidth ? _forceWidth : _st.widthMin, _st.skip * 2);
|
||||
}
|
||||
|
||||
void Menu::clearLastSeparator() {
|
||||
if (_actionWidgets.empty() || _actions.empty()) {
|
||||
return;
|
||||
}
|
||||
if (_actionWidgets.back()->action() == _actions.back()) {
|
||||
if (_actions.back()->isSeparator()) {
|
||||
resizeFromInner(
|
||||
width(),
|
||||
height() - _actionWidgets.back()->height());
|
||||
_actionWidgets.pop_back();
|
||||
if (_actions.back()->parent() == this) {
|
||||
delete _actions.back();
|
||||
_actions.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::finishAnimating() {
|
||||
for (const auto &widget : _actionWidgets) {
|
||||
widget->finishAnimating();
|
||||
}
|
||||
}
|
||||
|
||||
bool Menu::empty() const {
|
||||
return _actionWidgets.empty();
|
||||
}
|
||||
|
||||
void Menu::resizeFromInner(int w, int h) {
|
||||
if (const auto s = QSize(w, h); s != size()) {
|
||||
resize(s);
|
||||
_resizesFromInner.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<> Menu::resizesFromInner() const {
|
||||
return _resizesFromInner.events();
|
||||
}
|
||||
|
||||
rpl::producer<ScrollToRequest> Menu::scrollToRequests() const {
|
||||
return _scrollToRequests.events();
|
||||
}
|
||||
|
||||
void Menu::setShowSource(TriggeredSource source) {
|
||||
const auto mouseSelection = (source == TriggeredSource::Mouse);
|
||||
setSelected(
|
||||
(mouseSelection || _actions.empty()) ? -1 : 0,
|
||||
mouseSelection);
|
||||
}
|
||||
|
||||
const std::vector<not_null<QAction*>> &Menu::actions() const {
|
||||
return _actions;
|
||||
}
|
||||
|
||||
void Menu::setForceWidth(int forceWidth) {
|
||||
_forceWidth = forceWidth;
|
||||
resizeFromInner(_forceWidth, height());
|
||||
}
|
||||
|
||||
void Menu::updateSelected(QPoint globalPosition) {
|
||||
const auto p = mapFromGlobal(globalPosition) - QPoint(0, _st.skip);
|
||||
for (const auto &widget : _actionWidgets) {
|
||||
const auto widgetRect = QRect(widget->pos(), widget->size());
|
||||
if (widgetRect.contains(p)) {
|
||||
_lastSelectedByMouse = true;
|
||||
|
||||
// It may actually fail to become selected (if it is disabled).
|
||||
widget->setSelected(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::itemPressed(TriggeredSource source) {
|
||||
if (const auto action = findSelectedAction()) {
|
||||
if (action->lastTriggeredSource() == source) {
|
||||
action->setClicked(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::keyPressEvent(QKeyEvent *e) {
|
||||
const auto key = e->key();
|
||||
if (!_keyPressDelegate || !_keyPressDelegate(key)) {
|
||||
handleKeyPress(e);
|
||||
}
|
||||
}
|
||||
|
||||
ItemBase *Menu::findSelectedAction() const {
|
||||
const auto it = ranges::find_if(_actionWidgets, &ItemBase::isSelected);
|
||||
return (it == end(_actionWidgets)) ? nullptr : it->get();
|
||||
}
|
||||
|
||||
void Menu::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
const auto key = e->key();
|
||||
const auto selected = findSelectedAction();
|
||||
if ((key != Qt::Key_Up && key != Qt::Key_Down) || _actions.empty()) {
|
||||
if (selected) {
|
||||
selected->handleKeyPress(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto delta = (key == Qt::Key_Down ? 1 : -1);
|
||||
auto start = selected ? selected->index() : -1;
|
||||
if (start < 0 || start >= _actions.size()) {
|
||||
start = (delta > 0) ? (_actions.size() - 1) : 0;
|
||||
}
|
||||
auto newSelected = start;
|
||||
do {
|
||||
newSelected += delta;
|
||||
if (newSelected < 0) {
|
||||
newSelected += _actions.size();
|
||||
} else if (newSelected >= _actions.size()) {
|
||||
newSelected -= _actions.size();
|
||||
}
|
||||
} while (newSelected != start
|
||||
&& (!_actionWidgets[newSelected]->isEnabled()));
|
||||
|
||||
if (_actionWidgets[newSelected]->isEnabled()) {
|
||||
setSelected(newSelected, false);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::clearSelection() {
|
||||
setSelected(-1, false);
|
||||
}
|
||||
|
||||
void Menu::clearMouseSelection() {
|
||||
const auto selected = findSelectedAction();
|
||||
const auto mouseSelection = selected
|
||||
? (selected->lastTriggeredSource() == TriggeredSource::Mouse)
|
||||
: false;
|
||||
if (mouseSelection && !_childShownAction) {
|
||||
clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::setSelected(int selected, bool isMouseSelection) {
|
||||
if (selected >= _actionWidgets.size()) {
|
||||
selected = -1;
|
||||
}
|
||||
const auto source = isMouseSelection
|
||||
? TriggeredSource::Mouse
|
||||
: TriggeredSource::Keyboard;
|
||||
if (selected >= 0 && source == TriggeredSource::Keyboard) {
|
||||
const auto widget = _actionWidgets[selected].get();
|
||||
_scrollToRequests.fire({
|
||||
widget->y(),
|
||||
widget->y() + widget->height(),
|
||||
});
|
||||
}
|
||||
if (const auto selectedItem = findSelectedAction()) {
|
||||
if (selectedItem->index() == selected) {
|
||||
return;
|
||||
}
|
||||
selectedItem->setSelected(false, source);
|
||||
}
|
||||
if (selected >= 0) {
|
||||
_actionWidgets[selected].get()->setSelected(true, source);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::mouseMoveEvent(QMouseEvent *e) {
|
||||
handleMouseMove(e->globalPos());
|
||||
}
|
||||
|
||||
void Menu::handleMouseMove(QPoint globalPosition) {
|
||||
const auto margins = style::margins(0, _st.skip, 0, _st.skip);
|
||||
const auto inner = rect().marginsRemoved(margins);
|
||||
const auto localPosition = mapFromGlobal(globalPosition);
|
||||
if (inner.contains(localPosition)) {
|
||||
updateSelected(globalPosition);
|
||||
} else {
|
||||
clearMouseSelection();
|
||||
if (_mouseMoveDelegate) {
|
||||
_mouseMoveDelegate(globalPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::mousePressEvent(QMouseEvent *e) {
|
||||
handleMousePress(e->globalPos());
|
||||
}
|
||||
|
||||
void Menu::mouseReleaseEvent(QMouseEvent *e) {
|
||||
handleMouseRelease(e->globalPos());
|
||||
}
|
||||
|
||||
void Menu::handleMousePress(QPoint globalPosition) {
|
||||
handleMouseMove(globalPosition);
|
||||
const auto margins = style::margins(0, _st.skip, 0, _st.skip);
|
||||
const auto inner = rect().marginsRemoved(margins);
|
||||
const auto localPosition = mapFromGlobal(globalPosition);
|
||||
const auto pressed = (inner.contains(localPosition)
|
||||
&& _lastSelectedByMouse)
|
||||
? findSelectedAction()
|
||||
: nullptr;
|
||||
if (pressed) {
|
||||
pressed->setClicked();
|
||||
} else {
|
||||
if (_mousePressDelegate) {
|
||||
_mousePressDelegate(globalPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::handleMouseRelease(QPoint globalPosition) {
|
||||
if (_pressedOutside) {
|
||||
_pressedOutside = false;
|
||||
updateSelected(globalPosition);
|
||||
if (const auto selected = findSelectedAction()) {
|
||||
selected->setClicked(TriggeredSource::Mouse);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!rect().contains(mapFromGlobal(globalPosition))
|
||||
&& _mouseReleaseDelegate) {
|
||||
_mouseReleaseDelegate(globalPosition);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::handlePressedOutside(QPoint globalPosition) {
|
||||
_pressedOutside = true;
|
||||
updateSelected(globalPosition);
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
159
Telegram/lib_ui/ui/widgets/menu/menu.h
Normal file
159
Telegram/lib_ui/ui/widgets/menu/menu.h
Normal file
@@ -0,0 +1,159 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/widgets/menu/menu_common.h"
|
||||
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
namespace Ui {
|
||||
struct ScrollToRequest;
|
||||
} // namespace Ui
|
||||
|
||||
namespace style {
|
||||
struct Menu;
|
||||
struct MenuSeparator;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::Menu &defaultMenu;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class ItemBase;
|
||||
class RippleAnimation;
|
||||
|
||||
class Menu : public RpWidget {
|
||||
public:
|
||||
Menu(QWidget *parent, const style::Menu &st = st::defaultMenu);
|
||||
Menu(QWidget *parent, QMenu *menu, const style::Menu &st = st::defaultMenu);
|
||||
~Menu();
|
||||
|
||||
[[nodiscard]] const style::Menu &st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
not_null<QAction*> addAction(base::unique_qptr<ItemBase> widget);
|
||||
not_null<QAction*> addAction(
|
||||
const QString &text,
|
||||
Fn<void()> callback,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
not_null<QAction*> addAction(
|
||||
const QString &text,
|
||||
std::unique_ptr<QMenu> submenu,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
not_null<QAction*> addSeparator(
|
||||
const style::MenuSeparator *st = nullptr);
|
||||
not_null<QAction*> insertAction(
|
||||
int position,
|
||||
base::unique_qptr<ItemBase> widget);
|
||||
void removeAction(int position);
|
||||
void clearActions();
|
||||
void clearLastSeparator();
|
||||
void finishAnimating();
|
||||
|
||||
bool empty() const;
|
||||
|
||||
void clearSelection();
|
||||
|
||||
void setChildShownAction(QAction *action) {
|
||||
_childShownAction = action;
|
||||
}
|
||||
void setShowSource(TriggeredSource source);
|
||||
void setForceWidth(int forceWidth);
|
||||
|
||||
const std::vector<not_null<QAction*>> &actions() const;
|
||||
|
||||
void setActivatedCallback(Fn<void(const CallbackData &data)> callback) {
|
||||
_activatedCallback = std::move(callback);
|
||||
}
|
||||
void setTriggeredCallback(Fn<void(const CallbackData &data)> callback) {
|
||||
_triggeredCallback = std::move(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]] ItemBase *findSelectedAction() const;
|
||||
|
||||
void setKeyPressDelegate(Fn<bool(int key)> delegate) {
|
||||
_keyPressDelegate = std::move(delegate);
|
||||
}
|
||||
void handleKeyPress(not_null<QKeyEvent*> e);
|
||||
|
||||
void setMouseMoveDelegate(Fn<void(QPoint globalPosition)> delegate) {
|
||||
_mouseMoveDelegate = std::move(delegate);
|
||||
}
|
||||
void handleMouseMove(QPoint globalPosition);
|
||||
|
||||
void setMousePressDelegate(Fn<void(QPoint globalPosition)> delegate) {
|
||||
_mousePressDelegate = std::move(delegate);
|
||||
}
|
||||
void handleMousePress(QPoint globalPosition);
|
||||
|
||||
void setMouseReleaseDelegate(Fn<void(QPoint globalPosition)> delegate) {
|
||||
_mouseReleaseDelegate = std::move(delegate);
|
||||
}
|
||||
void handleMouseRelease(QPoint globalPosition);
|
||||
|
||||
void handlePressedOutside(QPoint globalPosition);
|
||||
|
||||
void setSelected(int selected, bool isMouseSelection);
|
||||
|
||||
[[nodiscard]] rpl::producer<> resizesFromInner() const;
|
||||
[[nodiscard]] rpl::producer<ScrollToRequest> scrollToRequests() const;
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
void updateSelected(QPoint globalPosition);
|
||||
void init();
|
||||
|
||||
not_null<QAction*> addAction(
|
||||
not_null<QAction*> action,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
|
||||
void clearMouseSelection();
|
||||
|
||||
void itemPressed(TriggeredSource source);
|
||||
|
||||
[[nodiscard]] int recountWidth() const;
|
||||
[[nodiscard]] int recountHeight() const;
|
||||
void resizeFromInner(int w, int h);
|
||||
|
||||
const style::Menu &_st;
|
||||
|
||||
Fn<void(const CallbackData &data)> _activatedCallback;
|
||||
Fn<void(const CallbackData &data)> _triggeredCallback;
|
||||
Fn<bool(int key)> _keyPressDelegate;
|
||||
Fn<void(QPoint globalPosition)> _mouseMoveDelegate;
|
||||
Fn<void(QPoint globalPosition)> _mousePressDelegate;
|
||||
Fn<void(QPoint globalPosition)> _mouseReleaseDelegate;
|
||||
|
||||
QMenu *_wappedMenu = nullptr;
|
||||
std::vector<not_null<QAction*>> _actions;
|
||||
std::vector<base::unique_qptr<ItemBase>> _actionWidgets;
|
||||
|
||||
int _forceWidth = 0;
|
||||
bool _lastSelectedByMouse = false;
|
||||
bool _pressedOutside = false;
|
||||
|
||||
QPointer<QAction> _childShownAction;
|
||||
|
||||
rpl::event_stream<> _resizesFromInner;
|
||||
rpl::event_stream<ScrollToRequest> _scrollToRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
227
Telegram/lib_ui/ui/widgets/menu/menu_action.cpp
Normal file
227
Telegram/lib_ui/ui/widgets/menu/menu_action.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui::Menu {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] TextWithEntities ParseMenuItem(const QString &text) {
|
||||
auto result = TextWithEntities();
|
||||
result.text.reserve(text.size());
|
||||
auto afterAmpersand = false;
|
||||
for (const auto &ch : text) {
|
||||
if (afterAmpersand) {
|
||||
afterAmpersand = false;
|
||||
if (ch == '&') {
|
||||
result.text.append(ch);
|
||||
} else {
|
||||
result.entities.append(EntityInText{
|
||||
EntityType::Underline,
|
||||
int(result.text.size()),
|
||||
1 });
|
||||
result.text.append(ch);
|
||||
}
|
||||
} else if (ch == '&') {
|
||||
afterAmpersand = true;
|
||||
} else {
|
||||
result.text.append(ch);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TextParseOptions MenuTextOptions = {
|
||||
TextParseLinks | TextParseMarkdown, // flags
|
||||
0, // maxw
|
||||
0, // maxh
|
||||
Qt::LayoutDirectionAuto, // dir
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Action::Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<QAction*> action,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver)
|
||||
: ItemBase(parent, st)
|
||||
, _action(action)
|
||||
, _st(st)
|
||||
, _icon(icon)
|
||||
, _iconOver(iconOver)
|
||||
, _height(_st.itemPadding.top()
|
||||
+ _st.itemStyle.font->height
|
||||
+ _st.itemPadding.bottom()) {
|
||||
setAcceptBoth(true);
|
||||
|
||||
initResizeHook(parent->sizeValue());
|
||||
processAction();
|
||||
|
||||
enableMouseSelecting();
|
||||
|
||||
connect(_action, &QAction::changed, [=] { processAction(); });
|
||||
}
|
||||
|
||||
bool Action::hasSubmenu() const {
|
||||
return _action->menu() != nullptr;
|
||||
}
|
||||
|
||||
void Action::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
paint(p);
|
||||
}
|
||||
|
||||
void Action::paintBackground(QPainter &p, bool selected) {
|
||||
if (selected && _st.itemBgOver->c.alpha() < 255) {
|
||||
p.fillRect(0, 0, width(), _height, _st.itemBg);
|
||||
}
|
||||
p.fillRect(
|
||||
QRect(0, 0, width(), _height),
|
||||
selected ? _st.itemBgOver : _st.itemBg);
|
||||
}
|
||||
|
||||
void Action::paintText(Painter &p) {
|
||||
_text.drawLeftElided(
|
||||
p,
|
||||
_st.itemPadding.left(),
|
||||
_st.itemPadding.top(),
|
||||
_textWidth,
|
||||
width());
|
||||
}
|
||||
|
||||
void Action::paint(Painter &p) {
|
||||
const auto enabled = isEnabled();
|
||||
const auto selected = isSelected();
|
||||
paintBackground(p, selected);
|
||||
if (enabled) {
|
||||
RippleButton::paintRipple(p, 0, 0);
|
||||
}
|
||||
if (const auto icon = (selected ? _iconOver : _icon)) {
|
||||
icon->paint(p, _st.itemIconPosition, width());
|
||||
}
|
||||
p.setPen(selected ? _st.itemFgOver : (enabled ? _st.itemFg : _st.itemFgDisabled));
|
||||
paintText(p);
|
||||
if (hasSubmenu()) {
|
||||
const auto skip = _st.itemRightSkip;
|
||||
const auto left = width() - skip - _st.arrow.width();
|
||||
const auto top = (_height - _st.arrow.height()) / 2;
|
||||
if (enabled) {
|
||||
_st.arrow.paint(p, left, top, width());
|
||||
} else {
|
||||
_st.arrow.paint(
|
||||
p,
|
||||
left,
|
||||
top,
|
||||
width(),
|
||||
_st.itemFgDisabled->c);
|
||||
}
|
||||
} else if (!_shortcut.isEmpty()) {
|
||||
p.setPen(selected
|
||||
? _st.itemFgShortcutOver
|
||||
: (enabled ? _st.itemFgShortcut : _st.itemFgShortcutDisabled));
|
||||
p.drawTextRight(
|
||||
_st.itemPadding.right(),
|
||||
_st.itemPadding.top(),
|
||||
width(),
|
||||
_shortcut);
|
||||
}
|
||||
}
|
||||
|
||||
void Action::processAction() {
|
||||
accessibilityNameChanged();
|
||||
|
||||
setPointerCursor(isEnabled());
|
||||
if (_action->text().isEmpty()) {
|
||||
_shortcut = QString();
|
||||
_text.clear();
|
||||
return;
|
||||
}
|
||||
const auto actionTextParts = _action->text().split('\t');
|
||||
const auto actionText = actionTextParts.empty()
|
||||
? QString()
|
||||
: actionTextParts[0];
|
||||
const auto actionShortcut = (actionTextParts.size() > 1)
|
||||
? actionTextParts[1]
|
||||
: QString();
|
||||
setMarkedText(ParseMenuItem(actionText), actionShortcut);
|
||||
}
|
||||
|
||||
void Action::setMarkedText(
|
||||
TextWithEntities text,
|
||||
QString shortcut,
|
||||
const Text::MarkedContext &context) {
|
||||
_text.setMarkedText(_st.itemStyle, text, MenuTextOptions, context);
|
||||
const auto textWidth = _text.maxWidth();
|
||||
const auto &padding = _st.itemPadding;
|
||||
|
||||
const auto additionalWidth = hasSubmenu()
|
||||
? (_st.itemRightSkip + _st.arrow.width())
|
||||
: (!shortcut.isEmpty())
|
||||
? (_st.itemRightSkip + _st.itemStyle.font->width(shortcut))
|
||||
: 0;
|
||||
const auto goodWidth = padding.left()
|
||||
+ textWidth
|
||||
+ additionalWidth
|
||||
+ padding.right();
|
||||
|
||||
const auto w = std::clamp(goodWidth, _st.widthMin, _st.widthMax);
|
||||
_textWidth = w - (goodWidth - textWidth);
|
||||
_shortcut = shortcut;
|
||||
setMinWidth(w);
|
||||
update();
|
||||
}
|
||||
|
||||
const style::Menu &Action::st() const {
|
||||
return _st;
|
||||
}
|
||||
|
||||
bool Action::isEnabled() const {
|
||||
return _action->isEnabled();
|
||||
}
|
||||
|
||||
not_null<QAction*> Action::action() const {
|
||||
return _action;
|
||||
}
|
||||
|
||||
QPoint Action::prepareRippleStartPosition() const {
|
||||
return mapFromGlobal(QCursor::pos());
|
||||
}
|
||||
|
||||
QImage Action::prepareRippleMask() const {
|
||||
return Ui::RippleAnimation::RectMask(size());
|
||||
}
|
||||
|
||||
int Action::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void Action::handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
if (!isSelected()) {
|
||||
return;
|
||||
}
|
||||
const auto key = e->key();
|
||||
if (key == Qt::Key_Enter || key == Qt::Key_Return) {
|
||||
setClicked(TriggeredSource::Keyboard);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Action::setIcon(
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver) {
|
||||
_icon = icon;
|
||||
_iconOver = iconOver ? iconOver : icon;
|
||||
update();
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
77
Telegram/lib_ui/ui/widgets/menu/menu_action.h
Normal file
77
Telegram/lib_ui/ui/widgets/menu/menu_action.h
Normal file
@@ -0,0 +1,77 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class Action : public ItemBase {
|
||||
public:
|
||||
Action(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
not_null<QAction*> action,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::MenuItem;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _action->text();
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::Menu &st() const;
|
||||
|
||||
bool isEnabled() const override;
|
||||
not_null<QAction*> action() const override;
|
||||
|
||||
void handleKeyPress(not_null<QKeyEvent*> e) override;
|
||||
|
||||
void setIcon(
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver = nullptr);
|
||||
|
||||
void setMarkedText(
|
||||
TextWithEntities text,
|
||||
QString shortcut,
|
||||
const Text::MarkedContext &context = {});
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
QPoint prepareRippleStartPosition() const override;
|
||||
QImage prepareRippleMask() const override;
|
||||
|
||||
int contentHeight() const override;
|
||||
|
||||
void paintBackground(QPainter &p, bool selected);
|
||||
void paintText(Painter &p);
|
||||
|
||||
private:
|
||||
void processAction();
|
||||
void paint(Painter &p);
|
||||
|
||||
bool hasSubmenu() const;
|
||||
|
||||
Text::String _text;
|
||||
QString _shortcut;
|
||||
const not_null<QAction*> _action;
|
||||
const style::Menu &_st;
|
||||
const style::icon *_icon;
|
||||
const style::icon *_iconOver;
|
||||
// std::unique_ptr<ToggleView> _toggle;
|
||||
int _textWidth = 0;
|
||||
const int _height;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
28
Telegram/lib_ui/ui/widgets/menu/menu_add_action_callback.cpp
Normal file
28
Telegram/lib_ui/ui/widgets/menu/menu_add_action_callback.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
MenuCallback::MenuCallback(MenuCallback::Callback callback)
|
||||
: _callback(std::move(callback)) {
|
||||
}
|
||||
|
||||
QAction *MenuCallback::operator()(Args &&args) const {
|
||||
return _callback(std::move(args));
|
||||
}
|
||||
|
||||
QAction *MenuCallback::operator()(
|
||||
const QString &text,
|
||||
Fn<void()> handler,
|
||||
const style::icon *icon) const {
|
||||
return _callback({ text, std::move(handler), icon, nullptr });
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
63
Telegram/lib_ui/ui/widgets/menu/menu_add_action_callback.h
Normal file
63
Telegram/lib_ui/ui/widgets/menu/menu_add_action_callback.h
Normal file
@@ -0,0 +1,63 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/style/style_core.h"
|
||||
|
||||
namespace anim {
|
||||
enum class type : uchar;
|
||||
} // namespace anim
|
||||
|
||||
namespace base {
|
||||
template <typename T>
|
||||
class unique_qptr;
|
||||
} // namespace base
|
||||
|
||||
namespace style {
|
||||
struct PopupMenu;
|
||||
struct MenuSeparator;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class RpWidget;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class ItemBase;
|
||||
|
||||
struct MenuCallback final {
|
||||
public:
|
||||
struct Args {
|
||||
QString text;
|
||||
Fn<void()> handler;
|
||||
const style::icon *icon;
|
||||
const style::MenuSeparator *separatorSt = nullptr;
|
||||
FnMut<void(not_null<Ui::PopupMenu*>)> fillSubmenu;
|
||||
FnMut<base::unique_qptr<ItemBase>(not_null<RpWidget*>)> make;
|
||||
const style::PopupMenu *submenuSt = nullptr;
|
||||
Fn<bool()> triggerFilter;
|
||||
rpl::producer<anim::type> hideRequests;
|
||||
int addTopShift = 0;
|
||||
bool isSeparator = false;
|
||||
bool isAttention = false;
|
||||
};
|
||||
using Callback = Fn<QAction*(Args&&)>;
|
||||
|
||||
explicit MenuCallback(Callback callback);
|
||||
|
||||
QAction *operator()(Args &&args) const;
|
||||
QAction *operator()(
|
||||
const QString &text,
|
||||
Fn<void()> handler,
|
||||
const style::icon *icon) const;
|
||||
private:
|
||||
Callback _callback;
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
@@ -0,0 +1,162 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
|
||||
#include "ui/widgets/dropdown_menu.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/qt_object_factory.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
MenuCallback CreateAddActionCallback(not_null<Ui::PopupMenu*> menu) {
|
||||
return MenuCallback([=](MenuCallback::Args a) -> QAction* {
|
||||
const auto initFilter = [&](not_null<Ui::Menu::ItemBase*> action) {
|
||||
if (const auto copy = a.triggerFilter) {
|
||||
action->setClickedCallback([=] {
|
||||
const auto weak = base::make_weak(action);
|
||||
if (copy() && weak && !action->isDisabled()) {
|
||||
action->setDisabled(true);
|
||||
crl::on_main(
|
||||
weak,
|
||||
[=] { action->setDisabled(false); });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if (a.hideRequests) {
|
||||
std::move(
|
||||
a.hideRequests
|
||||
) | rpl::on_next([=](anim::type animated) {
|
||||
menu->hideMenu(animated == anim::type::instant);
|
||||
}, menu->lifetime());
|
||||
}
|
||||
if (a.addTopShift) {
|
||||
menu->setTopShift(a.addTopShift);
|
||||
return nullptr;
|
||||
} else if (a.fillSubmenu) {
|
||||
const auto action = menu->addAction(
|
||||
a.text,
|
||||
std::move(a.handler),
|
||||
a.icon);
|
||||
// Dummy menu.
|
||||
action->setMenu(Ui::CreateChild<QMenu>(menu->menu().get()));
|
||||
a.fillSubmenu(menu->ensureSubmenu(
|
||||
action,
|
||||
a.submenuSt ? *a.submenuSt : menu->st()));
|
||||
return action;
|
||||
} else if (a.separatorSt || a.isSeparator) {
|
||||
return menu->addSeparator(a.separatorSt);
|
||||
} else if (a.isAttention) {
|
||||
auto owned = base::make_unique_q<Ui::Menu::Action>(
|
||||
menu,
|
||||
a.icon ? st::menuWithIconsAttention : st::menuAttention,
|
||||
Ui::Menu::CreateAction(
|
||||
menu->menu().get(),
|
||||
a.text,
|
||||
std::move(a.handler)),
|
||||
a.icon,
|
||||
a.icon);
|
||||
initFilter(owned.get());
|
||||
return menu->addAction(std::move(owned));
|
||||
} else if (a.triggerFilter) {
|
||||
auto owned = base::make_unique_q<Ui::Menu::Action>(
|
||||
menu,
|
||||
menu->st().menu,
|
||||
Ui::Menu::CreateAction(
|
||||
menu->menu().get(),
|
||||
a.text,
|
||||
std::move(a.handler)),
|
||||
a.icon,
|
||||
a.icon);
|
||||
initFilter(owned.get());
|
||||
return menu->addAction(std::move(owned));
|
||||
} else if (auto owned = a.make ? a.make(menu) : nullptr) {
|
||||
initFilter(owned.get());
|
||||
return menu->addAction(std::move(owned));
|
||||
}
|
||||
return menu->addAction(a.text, std::move(a.handler), a.icon);
|
||||
});
|
||||
}
|
||||
|
||||
MenuCallback CreateAddActionCallback(not_null<Ui::DropdownMenu*> menu) {
|
||||
return MenuCallback([=](MenuCallback::Args a) -> QAction* {
|
||||
const auto initFilter = [&](not_null<Ui::Menu::Action*> action) {
|
||||
if (const auto copy = a.triggerFilter) {
|
||||
action->setClickedCallback([=] {
|
||||
const auto weak = base::make_weak(action);
|
||||
if (copy() && weak && !action->isDisabled()) {
|
||||
action->setDisabled(true);
|
||||
crl::on_main(
|
||||
weak,
|
||||
[=] { action->setDisabled(false); });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if (a.hideRequests) {
|
||||
Unexpected("Dropdown menu does not support hideRequests.");
|
||||
// std::move(
|
||||
// a.hideRequests
|
||||
// ) | rpl::on_next([=](anim::type animated) {
|
||||
// menu->hideMenu(animated == anim::type::instant);
|
||||
// }, menu->lifetime());
|
||||
}
|
||||
if (a.addTopShift) {
|
||||
Unexpected("Dropdown menu does not support addTopShift.");
|
||||
// menu->setTopShift(a.addTopShift);
|
||||
// return nullptr;
|
||||
} else if (a.fillSubmenu) {
|
||||
Unexpected("Dropdown menu does not support fillSubmenu.");
|
||||
// const auto action = menu->addAction(
|
||||
// a.text,
|
||||
// std::move(a.handler),
|
||||
// a.icon);
|
||||
// // Dummy menu.
|
||||
// action->setMenu(Ui::CreateChild<QMenu>(menu->menu().get()));
|
||||
// a.fillSubmenu(menu->ensureSubmenu(action, menu->st()));
|
||||
// return action;
|
||||
} else if (a.separatorSt || a.isSeparator) {
|
||||
return menu->addSeparator(a.separatorSt);
|
||||
} else if (a.isAttention) {
|
||||
auto owned = base::make_unique_q<Ui::Menu::Action>(
|
||||
menu,
|
||||
a.icon ? st::menuWithIconsAttention : st::menuAttention,
|
||||
Ui::Menu::CreateAction(
|
||||
menu->menu().get(),
|
||||
a.text,
|
||||
std::move(a.handler)),
|
||||
a.icon,
|
||||
a.icon);
|
||||
initFilter(owned.get());
|
||||
return menu->addAction(std::move(owned));
|
||||
} else if (a.triggerFilter) {
|
||||
Unexpected("Dropdown menu does not support triggerFilter.");
|
||||
// auto owned = base::make_unique_q<Ui::Menu::Action>(
|
||||
// menu,
|
||||
// menu->st().menu,
|
||||
// Ui::Menu::CreateAction(
|
||||
// menu->menu().get(),
|
||||
// a.text,
|
||||
// std::move(a.handler)),
|
||||
// a.icon,
|
||||
// a.icon);
|
||||
// initFilter(owned.get());
|
||||
// return menu->addAction(std::move(owned));
|
||||
}
|
||||
return menu->addAction(a.text, std::move(a.handler), a.icon);
|
||||
});
|
||||
}
|
||||
|
||||
MenuCallback CreateAddActionCallback(
|
||||
const base::unique_qptr<Ui::PopupMenu> &menu) {
|
||||
return CreateAddActionCallback(menu.get());
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
@@ -0,0 +1,27 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace Ui {
|
||||
class DropdownMenu;
|
||||
class PopupMenu;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
struct MenuCallback;
|
||||
|
||||
[[nodiscard]] MenuCallback CreateAddActionCallback(
|
||||
not_null<Ui::PopupMenu*> menu);
|
||||
[[nodiscard]] MenuCallback CreateAddActionCallback(
|
||||
not_null<Ui::DropdownMenu*> menu);
|
||||
[[nodiscard]] MenuCallback CreateAddActionCallback(
|
||||
const base::unique_qptr<Ui::PopupMenu> &menu);
|
||||
|
||||
} // namespace Ui::Menu
|
||||
27
Telegram/lib_ui/ui/widgets/menu/menu_common.cpp
Normal file
27
Telegram/lib_ui/ui/widgets/menu/menu_common.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_common.h"
|
||||
|
||||
#include <QAction>
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
not_null<QAction*> CreateAction(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
Fn<void()> &&callback) {
|
||||
const auto action = new QAction(text, parent);
|
||||
parent->connect(
|
||||
action,
|
||||
&QAction::triggered,
|
||||
action,
|
||||
std::move(callback),
|
||||
Qt::QueuedConnection);
|
||||
return action;
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
29
Telegram/lib_ui/ui/widgets/menu/menu_common.h
Normal file
29
Telegram/lib_ui/ui/widgets/menu/menu_common.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
enum class TriggeredSource {
|
||||
Mouse,
|
||||
Keyboard,
|
||||
};
|
||||
|
||||
struct CallbackData {
|
||||
QAction *action;
|
||||
int actionTop = 0;
|
||||
TriggeredSource source;
|
||||
int index = 0;
|
||||
bool selected = false;
|
||||
};
|
||||
|
||||
not_null<QAction*> CreateAction(
|
||||
QWidget *parent,
|
||||
const QString &text,
|
||||
Fn<void()> &&callback);
|
||||
|
||||
} // namespace Ui::Menu
|
||||
168
Telegram/lib_ui/ui/widgets/menu/menu_item_base.cpp
Normal file
168
Telegram/lib_ui/ui/widgets/menu/menu_item_base.cpp
Normal file
@@ -0,0 +1,168 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
ItemBase::ItemBase(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st)
|
||||
: RippleButton(parent, st.ripple) {
|
||||
}
|
||||
|
||||
void ItemBase::setMenuAsParent(not_null<Menu*> menu) {
|
||||
QWidget::setParent(menu);
|
||||
_menu = menu;
|
||||
}
|
||||
|
||||
void ItemBase::setSelected(
|
||||
bool selected,
|
||||
TriggeredSource source) {
|
||||
if (selected && !isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (_selected.current() != selected) {
|
||||
setMouseTracking(!selected);
|
||||
_lastTriggeredSource = source;
|
||||
_selected = selected;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
bool ItemBase::isSelected() const {
|
||||
return _selected.current();
|
||||
}
|
||||
|
||||
rpl::producer<CallbackData> ItemBase::selects() const {
|
||||
return _selected.changes(
|
||||
) | rpl::map([=](bool selected) -> CallbackData {
|
||||
return { action(), y(), _lastTriggeredSource, _index, selected };
|
||||
});
|
||||
}
|
||||
|
||||
TriggeredSource ItemBase::lastTriggeredSource() const {
|
||||
return _lastTriggeredSource;
|
||||
}
|
||||
|
||||
int ItemBase::index() const {
|
||||
return _index;
|
||||
}
|
||||
|
||||
void ItemBase::setIndex(int index) {
|
||||
_index = index;
|
||||
}
|
||||
|
||||
void ItemBase::setClicked(TriggeredSource source) {
|
||||
if (isEnabled()) {
|
||||
_lastTriggeredSource = source;
|
||||
_clicks.fire({});
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<CallbackData> ItemBase::clicks() const {
|
||||
return rpl::merge(
|
||||
AbstractButton::clicks() | rpl::to_empty,
|
||||
_clicks.events()
|
||||
) | rpl::filter([=] {
|
||||
return isEnabled() && !AbstractButton::isDisabled();
|
||||
}) | rpl::map([=]() -> CallbackData {
|
||||
return { action(), y(), _lastTriggeredSource, _index, true };
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<int> ItemBase::minWidthValue() const {
|
||||
return _minWidth.value();
|
||||
}
|
||||
|
||||
int ItemBase::minWidth() const {
|
||||
return _minWidth.current();
|
||||
}
|
||||
|
||||
void ItemBase::initResizeHook(rpl::producer<QSize> &&size) {
|
||||
std::move(
|
||||
size
|
||||
) | rpl::on_next([=](QSize s) {
|
||||
resize(s.width(), contentHeight());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ItemBase::setMinWidth(int w) {
|
||||
_minWidth = w;
|
||||
}
|
||||
|
||||
void ItemBase::finishAnimating() {
|
||||
RippleButton::finishAnimating();
|
||||
}
|
||||
|
||||
void ItemBase::enableMouseSelecting() {
|
||||
enableMouseSelecting(this);
|
||||
}
|
||||
|
||||
void ItemBase::enableMouseSelecting(not_null<RpWidget*> widget) {
|
||||
widget->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
const auto type = e->type();
|
||||
if (((type == QEvent::Leave)
|
||||
|| (type == QEvent::Enter)
|
||||
|| (type == QEvent::MouseMove)) && action()->isEnabled()) {
|
||||
setSelected(e->type() != QEvent::Leave);
|
||||
} else if ((type == QEvent::MouseButtonRelease)
|
||||
&& isEnabled()
|
||||
&& isSelected()) {
|
||||
const auto point = mapFromGlobal(QCursor::pos());
|
||||
if (!rect().contains(point)) {
|
||||
setSelected(false);
|
||||
}
|
||||
}
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ItemBase::setClickedCallback(Fn<void()> callback) {
|
||||
Ui::AbstractButton::setClickedCallback(callback);
|
||||
_connection = QObject::connect(
|
||||
action(),
|
||||
&QAction::triggered,
|
||||
std::move(callback));
|
||||
}
|
||||
|
||||
void ItemBase::mousePressEvent(QMouseEvent *e) {
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
_mousePressed = true;
|
||||
}
|
||||
RippleButton::mousePressEvent(e);
|
||||
}
|
||||
|
||||
void ItemBase::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (_mousePressed && _menu && !rect().contains(e->pos())) {
|
||||
_menu->handlePressedOutside(e->globalPos());
|
||||
}
|
||||
RippleButton::mouseMoveEvent(e);
|
||||
}
|
||||
|
||||
void ItemBase::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto wasPressed = base::take(_mousePressed);
|
||||
#ifdef Q_OS_UNIX
|
||||
if (isEnabled() && e->button() == Qt::RightButton) {
|
||||
setClicked(TriggeredSource::Mouse);
|
||||
return;
|
||||
}
|
||||
#endif // Q_OS_UNIX
|
||||
const auto isInRect = rect().contains(e->pos());
|
||||
if (isInRect && isEnabled() && e->button() == Qt::LeftButton) {
|
||||
//
|
||||
setClicked(TriggeredSource::Mouse);
|
||||
return;
|
||||
}
|
||||
if (wasPressed && _menu && !isInRect) {
|
||||
_menu->handleMouseRelease(e->globalPos());
|
||||
}
|
||||
RippleButton::mouseReleaseEvent(e);
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
83
Telegram/lib_ui/ui/widgets/menu/menu_item_base.h
Normal file
83
Telegram/lib_ui/ui/widgets/menu/menu_item_base.h
Normal file
@@ -0,0 +1,83 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/qt_connection.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
#include "ui/widgets/menu/menu_common.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class Menu;
|
||||
|
||||
class ItemBase : public RippleButton {
|
||||
public:
|
||||
ItemBase(not_null<RpWidget*> parent, const style::Menu &st);
|
||||
|
||||
TriggeredSource lastTriggeredSource() const;
|
||||
|
||||
rpl::producer<CallbackData> selects() const;
|
||||
void setSelected(
|
||||
bool selected,
|
||||
TriggeredSource source = TriggeredSource::Mouse);
|
||||
bool isSelected() const;
|
||||
|
||||
int index() const;
|
||||
void setIndex(int index);
|
||||
|
||||
void setClicked(TriggeredSource source = TriggeredSource::Mouse);
|
||||
|
||||
rpl::producer<CallbackData> clicks() const;
|
||||
|
||||
void setClickedCallback(Fn<void()> callback);
|
||||
|
||||
rpl::producer<int> minWidthValue() const;
|
||||
int minWidth() const;
|
||||
void setMinWidth(int w);
|
||||
|
||||
virtual void handleKeyPress(not_null<QKeyEvent*> e) {
|
||||
}
|
||||
|
||||
void setMenuAsParent(not_null<Menu*> menu);
|
||||
|
||||
virtual not_null<QAction*> action() const = 0;
|
||||
virtual bool isEnabled() const = 0;
|
||||
|
||||
virtual void finishAnimating();
|
||||
|
||||
protected:
|
||||
void initResizeHook(rpl::producer<QSize> &&size);
|
||||
|
||||
void enableMouseSelecting();
|
||||
void enableMouseSelecting(not_null<RpWidget*> widget);
|
||||
|
||||
virtual int contentHeight() const = 0;
|
||||
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
bool _mousePressed = false;
|
||||
int _index = -1;
|
||||
|
||||
rpl::variable<bool> _selected = false;
|
||||
rpl::event_stream<> _clicks;
|
||||
|
||||
rpl::variable<int> _minWidth = 0;
|
||||
|
||||
TriggeredSource _lastTriggeredSource = TriggeredSource::Mouse;
|
||||
|
||||
base::qt_connection _connection;
|
||||
|
||||
Menu *_menu = nullptr;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
95
Telegram/lib_ui/ui/widgets/menu/menu_multiline_action.cpp
Normal file
95
Telegram/lib_ui/ui/widgets/menu/menu_multiline_action.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_multiline_action.h"
|
||||
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/qt_object_factory.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
MultilineAction::MultilineAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::FlatLabel &stLabel,
|
||||
QPoint labelPosition,
|
||||
TextWithEntities &&about,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver)
|
||||
: ItemBase(parent, st)
|
||||
, _st(st)
|
||||
, _icon(icon)
|
||||
, _iconOver(iconOver ? iconOver : icon)
|
||||
, _labelPosition(labelPosition)
|
||||
, _text(base::make_unique_q<Ui::FlatLabel>(
|
||||
this,
|
||||
rpl::single(std::move(about)),
|
||||
stLabel))
|
||||
, _dummyAction(Ui::CreateChild<QAction>(parent.get())) {
|
||||
ItemBase::enableMouseSelecting();
|
||||
_text->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
updateMinWidth();
|
||||
parent->widthValue() | rpl::on_next([=](int width) {
|
||||
const auto top = _labelPosition.y();
|
||||
const auto skip = _labelPosition.x();
|
||||
const auto rightSkip = _icon ? _st.itemIconPosition.x() : skip;
|
||||
_text->resizeToWidth(width - skip - rightSkip);
|
||||
_text->moveToLeft(skip, top);
|
||||
resize(width, contentHeight());
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> MultilineAction::action() const {
|
||||
return _dummyAction;
|
||||
}
|
||||
|
||||
bool MultilineAction::isEnabled() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
int MultilineAction::contentHeight() const {
|
||||
const auto skip = _labelPosition.y();
|
||||
return skip
|
||||
+ std::max(_text->height(), _icon ? _icon->height() : 0)
|
||||
+ skip;
|
||||
}
|
||||
|
||||
void MultilineAction::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
const auto selected = isSelected();
|
||||
p.fillRect(rect(), selected ? _st.itemBgOver : _st.itemBg);
|
||||
RippleButton::paintRipple(p, 0, 0);
|
||||
if (const auto icon = (selected ? _iconOver : _icon)) {
|
||||
icon->paint(p, _st.itemIconPosition, width());
|
||||
}
|
||||
}
|
||||
|
||||
void MultilineAction::updateMinWidth() {
|
||||
const auto skip = _labelPosition.x();
|
||||
const auto rightSkip = _icon ? _st.itemIconPosition.x() : skip;
|
||||
auto min = _text->textMaxWidth() / 4;
|
||||
auto max = _icon ? _st.widthMax : (_text->textMaxWidth() - skip);
|
||||
_text->resizeToWidth(max);
|
||||
const auto height = _icon
|
||||
? ((_st.itemIconPosition.y() * 2) + _icon->height())
|
||||
: _text->height();
|
||||
_text->resizeToWidth(min);
|
||||
const auto heightMax = _text->height();
|
||||
if (heightMax > height) {
|
||||
while (min + 1 < max) {
|
||||
const auto middle = (max + min) / 2;
|
||||
_text->resizeToWidth(middle);
|
||||
if (_text->height() > height) {
|
||||
min = middle;
|
||||
} else {
|
||||
max = middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
ItemBase::setMinWidth(skip + rightSkip + max);
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
51
Telegram/lib_ui/ui/widgets/menu/menu_multiline_action.h
Normal file
51
Telegram/lib_ui/ui/widgets/menu/menu_multiline_action.h
Normal file
@@ -0,0 +1,51 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
struct Menu;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
class RpWidget;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class MultilineAction final : public ItemBase {
|
||||
public:
|
||||
MultilineAction(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::FlatLabel &stLabel,
|
||||
QPoint labelPosition,
|
||||
TextWithEntities &&about,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
private:
|
||||
int contentHeight() const override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void updateMinWidth();
|
||||
|
||||
const style::Menu &_st;
|
||||
const style::icon *_icon;
|
||||
const style::icon *_iconOver;
|
||||
const QPoint _labelPosition;
|
||||
const base::unique_qptr<Ui::FlatLabel> _text;
|
||||
const not_null<QAction*> _dummyAction;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
53
Telegram/lib_ui/ui/widgets/menu/menu_separator.cpp
Normal file
53
Telegram/lib_ui/ui/widgets/menu/menu_separator.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_separator.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
Separator::Separator(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MenuSeparator &separator,
|
||||
not_null<QAction*> action)
|
||||
: ItemBase(parent, st)
|
||||
, _lineWidth(separator.width)
|
||||
, _padding(separator.padding)
|
||||
, _fg(separator.fg)
|
||||
, _bg(st.itemBg)
|
||||
, _height(_padding.top() + _lineWidth + _padding.bottom())
|
||||
, _action(action) {
|
||||
initResizeHook(parent->sizeValue());
|
||||
paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
Painter p(this);
|
||||
|
||||
p.fillRect(0, 0, width(), _height, _bg);
|
||||
p.fillRect(
|
||||
_padding.left(),
|
||||
_padding.top(),
|
||||
width() - _padding.left() - _padding.right(),
|
||||
_lineWidth,
|
||||
_fg);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
not_null<QAction*> Separator::action() const {
|
||||
return _action;
|
||||
}
|
||||
|
||||
bool Separator::isEnabled() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
int Separator::contentHeight() const {
|
||||
return _height;
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
48
Telegram/lib_ui/ui/widgets/menu/menu_separator.h
Normal file
48
Telegram/lib_ui/ui/widgets/menu/menu_separator.h
Normal file
@@ -0,0 +1,48 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/menu/menu_item_base.h"
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace style {
|
||||
struct Menu;
|
||||
struct MenuSeparator;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class Separator : public ItemBase {
|
||||
public:
|
||||
Separator(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const style::MenuSeparator &separator,
|
||||
not_null<QAction*> action);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::Separator;
|
||||
}
|
||||
|
||||
not_null<QAction*> action() const override;
|
||||
bool isEnabled() const override;
|
||||
|
||||
protected:
|
||||
int contentHeight() const override;
|
||||
|
||||
private:
|
||||
const int _lineWidth;
|
||||
const style::margins &_padding;
|
||||
const style::color &_fg;
|
||||
const style::color &_bg;
|
||||
const int _height;
|
||||
const not_null<QAction*> _action;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
77
Telegram/lib_ui/ui/widgets/menu/menu_toggle.cpp
Normal file
77
Telegram/lib_ui/ui/widgets/menu/menu_toggle.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/menu/menu_toggle.h"
|
||||
|
||||
#include "ui/widgets/checkbox.h"
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
Toggle::Toggle(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &text,
|
||||
Fn<void()> &&callback,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver)
|
||||
: Action(
|
||||
parent,
|
||||
st,
|
||||
CreateAction(parent, text, std::move(callback)),
|
||||
icon,
|
||||
iconOver)
|
||||
, _padding(st.itemPadding)
|
||||
, _toggleShift(st.itemToggleShift)
|
||||
, _itemToggle(st.itemToggle)
|
||||
, _itemToggleOver(st.itemToggleOver) {
|
||||
const auto processAction = [=] {
|
||||
if (!action()->isCheckable()) {
|
||||
_toggle.reset();
|
||||
return;
|
||||
}
|
||||
if (_toggle) {
|
||||
_toggle->setChecked(action()->isChecked(), anim::type::normal);
|
||||
} else {
|
||||
_toggle = std::make_unique<ToggleView>(
|
||||
st.itemToggle,
|
||||
action()->isChecked(),
|
||||
[=] { update(); });
|
||||
}
|
||||
};
|
||||
processAction();
|
||||
connect(action(), &QAction::changed, [=] { processAction(); });
|
||||
|
||||
selects(
|
||||
) | rpl::on_next([=](const CallbackData &data) {
|
||||
if (!_toggle) {
|
||||
return;
|
||||
}
|
||||
_toggle->setStyle(data.selected ? _itemToggleOver : _itemToggle);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
Toggle::~Toggle() = default;
|
||||
|
||||
void Toggle::paintEvent(QPaintEvent *e) {
|
||||
Action::paintEvent(e);
|
||||
if (_toggle) {
|
||||
auto p = QPainter(this);
|
||||
const auto toggleSize = _toggle->getSize();
|
||||
_toggle->paint(
|
||||
p,
|
||||
width() - _padding.right() - toggleSize.width() + _toggleShift,
|
||||
(contentHeight() - toggleSize.height()) / 2, width());
|
||||
}
|
||||
}
|
||||
|
||||
void Toggle::finishAnimating() {
|
||||
ItemBase::finishAnimating();
|
||||
if (_toggle) {
|
||||
_toggle->finishAnimating();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui::Menu
|
||||
43
Telegram/lib_ui/ui/widgets/menu/menu_toggle.h
Normal file
43
Telegram/lib_ui/ui/widgets/menu/menu_toggle.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/menu/menu_action.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Ui {
|
||||
class ToggleView;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Menu {
|
||||
|
||||
class Toggle : public Action {
|
||||
public:
|
||||
Toggle(
|
||||
not_null<RpWidget*> parent,
|
||||
const style::Menu &st,
|
||||
const QString &text,
|
||||
Fn<void()> &&callback,
|
||||
const style::icon *icon,
|
||||
const style::icon *iconOver);
|
||||
~Toggle();
|
||||
|
||||
void finishAnimating() override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::margins &_padding;
|
||||
const int _toggleShift;
|
||||
const style::Toggle &_itemToggle;
|
||||
const style::Toggle &_itemToggleOver;
|
||||
std::unique_ptr<ToggleView> _toggle;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui::Menu
|
||||
1109
Telegram/lib_ui/ui/widgets/popup_menu.cpp
Normal file
1109
Telegram/lib_ui/ui/widgets/popup_menu.cpp
Normal file
File diff suppressed because it is too large
Load Diff
238
Telegram/lib_ui/ui/widgets/popup_menu.h
Normal file
238
Telegram/lib_ui/ui/widgets/popup_menu.h
Normal file
@@ -0,0 +1,238 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "styles/style_widgets.h"
|
||||
#include "ui/widgets/menu/menu.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/panel_animation.h"
|
||||
#include "ui/round_rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/unique_qptr.h"
|
||||
|
||||
namespace style {
|
||||
struct MenuSeparator;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class ScrollArea;
|
||||
|
||||
class PopupMenu : public RpWidget {
|
||||
public:
|
||||
enum class VerticalOrigin {
|
||||
Top,
|
||||
Bottom,
|
||||
};
|
||||
|
||||
enum class AnimatePhase {
|
||||
Hidden,
|
||||
StartShow,
|
||||
Shown,
|
||||
StartHide,
|
||||
};
|
||||
|
||||
PopupMenu(QWidget *parent, const style::PopupMenu &st = st::defaultPopupMenu);
|
||||
PopupMenu(QWidget *parent, QMenu *menu, const style::PopupMenu &st = st::defaultPopupMenu);
|
||||
~PopupMenu();
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::PopupMenu;
|
||||
}
|
||||
|
||||
[[nodiscard]] const style::PopupMenu &st() const {
|
||||
return _st;
|
||||
}
|
||||
[[nodiscard]] QRect inner() const {
|
||||
return _inner;
|
||||
}
|
||||
[[nodiscard]] rpl::producer<AnimatePhase> animatePhaseValue() const {
|
||||
return _animatePhase.value();
|
||||
}
|
||||
|
||||
not_null<QAction*> addAction(base::unique_qptr<Menu::ItemBase> widget);
|
||||
not_null<QAction*> addAction(
|
||||
const QString &text,
|
||||
Fn<void()> callback,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
not_null<QAction*> addAction(
|
||||
const QString &text,
|
||||
std::unique_ptr<PopupMenu> submenu,
|
||||
const style::icon *icon = nullptr,
|
||||
const style::icon *iconOver = nullptr);
|
||||
not_null<QAction*> addSeparator(
|
||||
const style::MenuSeparator *st = nullptr);
|
||||
not_null<QAction*> insertAction(
|
||||
int position,
|
||||
base::unique_qptr<Menu::ItemBase> widget);
|
||||
void removeAction(int position);
|
||||
void clearActions();
|
||||
|
||||
[[nodiscard]] const std::vector<not_null<QAction*>> &actions() const;
|
||||
[[nodiscard]] not_null<PopupMenu*> ensureSubmenu(
|
||||
not_null<QAction*> action,
|
||||
const style::PopupMenu &st);
|
||||
void removeSubmenu(not_null<QAction*> action);
|
||||
void checkSubmenuShow();
|
||||
bool empty() const;
|
||||
|
||||
void deleteOnHide(bool del);
|
||||
void popup(const QPoint &p);
|
||||
bool prepareGeometryFor(const QPoint &p);
|
||||
void popupPrepared();
|
||||
void hideMenu(bool fast = false);
|
||||
void setTopShift(int topShift);
|
||||
void setForceWidth(int forceWidth);
|
||||
void setForcedOrigin(PanelAnimation::Origin origin);
|
||||
void setForcedVerticalOrigin(VerticalOrigin origin);
|
||||
void setAdditionalMenuPadding(QMargins padding, QMargins margins);
|
||||
|
||||
[[nodiscard]] PanelAnimation::Origin preparedOrigin() const;
|
||||
[[nodiscard]] QMargins preparedPadding() const;
|
||||
[[nodiscard]] QMargins preparedMargins() const;
|
||||
[[nodiscard]] bool useTransparency() const;
|
||||
|
||||
[[nodiscard]] int scrollTop() const;
|
||||
[[nodiscard]] rpl::producer<int> scrollTopValue() const;
|
||||
|
||||
void setDestroyedCallback(Fn<void()> callback) {
|
||||
_destroyedCallback = std::move(callback);
|
||||
}
|
||||
void discardParentReActivate() {
|
||||
_reactivateParent = false;
|
||||
}
|
||||
|
||||
[[nodiscard]] not_null<Menu::Menu*> menu() const {
|
||||
return _menu;
|
||||
}
|
||||
|
||||
struct ShowState {
|
||||
float64 opacity = 1.;
|
||||
float64 widthProgress = 1.;
|
||||
float64 heightProgress = 1.;
|
||||
int appearingWidth = 0;
|
||||
int appearingHeight = 0;
|
||||
bool appearing = false;
|
||||
bool toggling = false;
|
||||
};
|
||||
[[nodiscard]] rpl::producer<ShowState> showStateValue() const;
|
||||
|
||||
void setClearLastSeparator(bool clear);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void focusOutEvent(QFocusEvent *e) override;
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
|
||||
private:
|
||||
void paintBg(QPainter &p);
|
||||
void hideFast();
|
||||
void setOrigin(PanelAnimation::Origin origin);
|
||||
void showAnimated(PanelAnimation::Origin origin);
|
||||
void hideAnimated();
|
||||
|
||||
QImage grabForPanelAnimation();
|
||||
void startShowAnimation();
|
||||
void startOpacityAnimation(bool hiding);
|
||||
void prepareCache();
|
||||
void childHiding(PopupMenu *child);
|
||||
|
||||
void showAnimationCallback();
|
||||
void opacityAnimationCallback();
|
||||
|
||||
void init();
|
||||
|
||||
void hideFinished();
|
||||
void showStarted();
|
||||
void fireCurrentShowState();
|
||||
|
||||
using TriggeredSource = Menu::TriggeredSource;
|
||||
void validateCompositingSupport();
|
||||
void handleMenuResize();
|
||||
void handleActivated(const Menu::CallbackData &data);
|
||||
void handleTriggered(const Menu::CallbackData &data);
|
||||
void forwardKeyPress(not_null<QKeyEvent*> e);
|
||||
bool handleKeyPress(int key);
|
||||
void forwardMouseMove(QPoint globalPosition) {
|
||||
_menu->handleMouseMove(globalPosition);
|
||||
}
|
||||
void handleMouseMove(QPoint globalPosition);
|
||||
void forwardMousePress(QPoint globalPosition) {
|
||||
_menu->handleMousePress(globalPosition);
|
||||
}
|
||||
void handleMousePress(QPoint globalPosition);
|
||||
void forwardMouseRelease(QPoint globalPosition) {
|
||||
_menu->handleMouseRelease(globalPosition);
|
||||
}
|
||||
void handleMouseRelease(QPoint globalPosition);
|
||||
|
||||
bool popupSubmenuFromAction(const Menu::CallbackData &data);
|
||||
void popupSubmenu(
|
||||
not_null<QAction*> action,
|
||||
not_null<PopupMenu*> submenu,
|
||||
int actionTop,
|
||||
TriggeredSource source);
|
||||
bool prepareGeometryFor(const QPoint &p, PopupMenu *parent);
|
||||
void showPrepared(TriggeredSource source);
|
||||
void updateRoundingOverlay();
|
||||
|
||||
const style::PopupMenu &_st;
|
||||
|
||||
RoundRect _roundRect;
|
||||
object_ptr<ScrollArea> _scroll;
|
||||
not_null<Menu::Menu*> _menu;
|
||||
object_ptr<RpWidget> _roundingOverlay = { nullptr };
|
||||
|
||||
base::flat_map<
|
||||
not_null<QAction*>,
|
||||
base::unique_qptr<PopupMenu>> _submenus;
|
||||
|
||||
PopupMenu *_parent = nullptr;
|
||||
|
||||
QRect _inner;
|
||||
QMargins _padding;
|
||||
QMargins _margins;
|
||||
QMargins _additionalMenuPadding;
|
||||
QMargins _additionalMenuMargins;
|
||||
|
||||
QPointer<PopupMenu> _activeSubmenu;
|
||||
|
||||
std::optional<VerticalOrigin> _forcedVerticalOrigin;
|
||||
PanelAnimation::Origin _origin = PanelAnimation::Origin::TopLeft;
|
||||
std::optional<PanelAnimation::Origin> _forcedOrigin;
|
||||
std::unique_ptr<PanelAnimation> _showAnimation;
|
||||
Animations::Simple _a_show;
|
||||
rpl::event_stream<ShowState> _showStateChanges;
|
||||
rpl::variable<AnimatePhase> _animatePhase = AnimatePhase::Hidden;
|
||||
|
||||
bool _useTransparency = true;
|
||||
bool _hiding = false;
|
||||
QPixmap _cache;
|
||||
Animations::Simple _a_opacity;
|
||||
|
||||
bool _deleteOnHide = true;
|
||||
bool _triggering = false;
|
||||
bool _deleteLater = false;
|
||||
bool _reactivateParent = true;
|
||||
bool _grabbingForPanelAnimation = false;
|
||||
|
||||
int _topShift = 0;
|
||||
bool _clearLastSeparator = true;
|
||||
bool _keepingDelayedActivationPaused = false;
|
||||
|
||||
Fn<void()> _destroyedCallback;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
122
Telegram/lib_ui/ui/widgets/rp_window.cpp
Normal file
122
Telegram/lib_ui/ui/widgets/rp_window.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/rp_window.h"
|
||||
|
||||
#include "ui/platform/ui_platform_window.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
RpWindow::RpWindow(QWidget *parent)
|
||||
: RpWidget(parent)
|
||||
, _helper(Platform::CreateWindowHelper(this)) {
|
||||
Expects(_helper != nullptr);
|
||||
|
||||
_helper->initInWindow(this);
|
||||
hide();
|
||||
}
|
||||
|
||||
RpWindow::~RpWindow() = default;
|
||||
|
||||
not_null<RpWidget*> RpWindow::body() {
|
||||
return _helper->body();
|
||||
}
|
||||
|
||||
not_null<const RpWidget*> RpWindow::body() const {
|
||||
return _helper->body().get();
|
||||
}
|
||||
|
||||
QMargins RpWindow::frameMargins() const {
|
||||
return _helper->frameMargins();
|
||||
}
|
||||
|
||||
int RpWindow::additionalContentPadding() const {
|
||||
return _helper->additionalContentPadding();
|
||||
}
|
||||
|
||||
rpl::producer<int> RpWindow::additionalContentPaddingValue() const {
|
||||
return _helper->additionalContentPaddingValue();
|
||||
}
|
||||
|
||||
auto RpWindow::hitTestRequests() const
|
||||
-> rpl::producer<not_null<Platform::HitTestRequest*>> {
|
||||
return _helper->hitTestRequests();
|
||||
}
|
||||
|
||||
rpl::producer<Platform::HitTestResult> RpWindow::systemButtonOver() const {
|
||||
return _helper->systemButtonOver();
|
||||
}
|
||||
|
||||
rpl::producer<Platform::HitTestResult> RpWindow::systemButtonDown() const {
|
||||
return _helper->systemButtonDown();
|
||||
}
|
||||
|
||||
void RpWindow::overrideSystemButtonOver(Platform::HitTestResult button) {
|
||||
_helper->overrideSystemButtonOver(button);
|
||||
}
|
||||
|
||||
void RpWindow::overrideSystemButtonDown(Platform::HitTestResult button) {
|
||||
_helper->overrideSystemButtonDown(button);
|
||||
}
|
||||
|
||||
void RpWindow::setTitle(const QString &title) {
|
||||
_helper->setTitle(title);
|
||||
}
|
||||
|
||||
void RpWindow::setTitleStyle(const style::WindowTitle &st) {
|
||||
_helper->setTitleStyle(st);
|
||||
}
|
||||
|
||||
void RpWindow::setNativeFrame(bool enabled) {
|
||||
_helper->setNativeFrame(enabled);
|
||||
}
|
||||
|
||||
void RpWindow::setMinimumSize(QSize size) {
|
||||
_helper->setMinimumSize(size);
|
||||
}
|
||||
|
||||
void RpWindow::setFixedSize(QSize size) {
|
||||
_helper->setFixedSize(size);
|
||||
}
|
||||
|
||||
void RpWindow::setStaysOnTop(bool enabled) {
|
||||
_helper->setStaysOnTop(enabled);
|
||||
}
|
||||
|
||||
void RpWindow::setGeometry(QRect rect) {
|
||||
_helper->setGeometry(rect);
|
||||
}
|
||||
|
||||
void RpWindow::showFullScreen() {
|
||||
_helper->showFullScreen();
|
||||
}
|
||||
|
||||
void RpWindow::showNormal() {
|
||||
_helper->showNormal();
|
||||
}
|
||||
|
||||
void RpWindow::close() {
|
||||
_helper->close();
|
||||
}
|
||||
|
||||
void RpWindow::setBodyTitleArea(
|
||||
Fn<WindowTitleHitTestFlags(QPoint)> testMethod) {
|
||||
_helper->setBodyTitleArea(std::move(testMethod));
|
||||
}
|
||||
|
||||
bool RpWindow::mousePressCancelled() const {
|
||||
return _helper->mousePressCancelled();
|
||||
}
|
||||
|
||||
int RpWindow::manualRoundingRadius() const {
|
||||
return _helper->manualRoundingRadius();
|
||||
}
|
||||
|
||||
const style::TextStyle &RpWindow::titleTextStyle() const {
|
||||
return _helper->titleTextStyle();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
86
Telegram/lib_ui/ui/widgets/rp_window.h
Normal file
86
Telegram/lib_ui/ui/widgets/rp_window.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/flags.h"
|
||||
|
||||
namespace style {
|
||||
struct WindowTitle;
|
||||
struct TextStyle;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
namespace Platform {
|
||||
class BasicWindowHelper;
|
||||
struct HitTestRequest;
|
||||
enum class HitTestResult;
|
||||
} // namespace Platform
|
||||
|
||||
enum class WindowTitleHitTestFlag {
|
||||
None = 0x00,
|
||||
Move = 0x01,
|
||||
Menu = 0x02,
|
||||
Maximize = 0x04,
|
||||
FullScreen = 0x08,
|
||||
};
|
||||
inline constexpr bool is_flag_type(WindowTitleHitTestFlag) {
|
||||
return true;
|
||||
}
|
||||
using WindowTitleHitTestFlags = base::flags<WindowTitleHitTestFlag>;
|
||||
|
||||
class RpWindow : public RpWidget {
|
||||
public:
|
||||
explicit RpWindow(QWidget *parent = nullptr);
|
||||
~RpWindow();
|
||||
|
||||
[[nodiscard]] not_null<RpWidget*> body();
|
||||
[[nodiscard]] not_null<const RpWidget*> body() const;
|
||||
[[nodiscard]] QMargins frameMargins() const;
|
||||
|
||||
// In Windows 11 the window rounding shadow takes about
|
||||
// round(1px * system_scale) from the window geometry on each side.
|
||||
//
|
||||
// Top shift is made by the TitleWidget height, but the rest of the
|
||||
// side shifts are left for the RpWindow client to consider.
|
||||
[[nodiscard]] int additionalContentPadding() const;
|
||||
[[nodiscard]] rpl::producer<int> additionalContentPaddingValue() const;
|
||||
|
||||
[[nodiscard]] auto hitTestRequests() const
|
||||
-> rpl::producer<not_null<Platform::HitTestRequest*>>;
|
||||
[[nodiscard]] auto systemButtonOver() const
|
||||
-> rpl::producer<Platform::HitTestResult>;
|
||||
[[nodiscard]] auto systemButtonDown() const
|
||||
-> rpl::producer<Platform::HitTestResult>;
|
||||
void overrideSystemButtonOver(Platform::HitTestResult button);
|
||||
void overrideSystemButtonDown(Platform::HitTestResult button);
|
||||
|
||||
void setTitle(const QString &title);
|
||||
void setTitleStyle(const style::WindowTitle &st);
|
||||
void setNativeFrame(bool enabled);
|
||||
void setMinimumSize(QSize size);
|
||||
void setFixedSize(QSize size);
|
||||
void setStaysOnTop(bool enabled);
|
||||
void setGeometry(QRect rect);
|
||||
void showFullScreen();
|
||||
void showNormal();
|
||||
void close();
|
||||
[[nodiscard]] int manualRoundingRadius() const;
|
||||
void setBodyTitleArea(Fn<WindowTitleHitTestFlags(QPoint)> testMethod);
|
||||
|
||||
// Check if MouseButtonRelease was from the pressed state being
|
||||
// cancelled by startSystemMove / startSystemResize call.
|
||||
[[nodiscard]] bool mousePressCancelled() const;
|
||||
|
||||
[[nodiscard]] const style::TextStyle &titleTextStyle() const;
|
||||
|
||||
private:
|
||||
const std::unique_ptr<Platform::BasicWindowHelper> _helper;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
914
Telegram/lib_ui/ui/widgets/scroll_area.cpp
Normal file
914
Telegram/lib_ui/ui/widgets/scroll_area.cpp
Normal file
@@ -0,0 +1,914 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "base/debug_log.h"
|
||||
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QWindow>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] int ComputeScrollTo(
|
||||
int toFrom,
|
||||
int toTill,
|
||||
int toMin,
|
||||
int toMax,
|
||||
int current,
|
||||
int size) {
|
||||
if (toFrom < toMin) {
|
||||
toFrom = toMin;
|
||||
} else if (toFrom > toMax) {
|
||||
toFrom = toMax;
|
||||
}
|
||||
const auto exact = (toTill < 0);
|
||||
|
||||
const auto curBottom = current + size;
|
||||
auto scToFrom = toFrom;
|
||||
if (!exact && toFrom >= current) {
|
||||
if (toTill < toFrom) {
|
||||
toTill = toFrom;
|
||||
}
|
||||
if (toTill <= curBottom) {
|
||||
return current;
|
||||
}
|
||||
|
||||
scToFrom = toTill - size;
|
||||
if (scToFrom > toFrom) {
|
||||
scToFrom = toFrom;
|
||||
}
|
||||
if (scToFrom == current) {
|
||||
return current;
|
||||
}
|
||||
} else {
|
||||
scToFrom = toFrom;
|
||||
}
|
||||
return scToFrom;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
|
||||
|
||||
ScrollShadow::ScrollShadow(ScrollArea *parent, const style::ScrollArea *st)
|
||||
: QWidget(parent)
|
||||
, _st(st) {
|
||||
Expects(_st != nullptr);
|
||||
Expects(_st->shColor.get() != nullptr);
|
||||
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
void ScrollShadow::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), _st->shColor);
|
||||
}
|
||||
|
||||
void ScrollShadow::changeVisibility(bool shown) {
|
||||
setVisible(shown);
|
||||
}
|
||||
|
||||
ScrollBar::ScrollBar(
|
||||
ScrollArea *parent,
|
||||
bool vert,
|
||||
const style::ScrollArea *st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _vertical(vert)
|
||||
, _hiding(_st->hiding != 0)
|
||||
, _connected(vert ? parent->verticalScrollBar() : parent->horizontalScrollBar())
|
||||
, _scrollMax(_connected->maximum())
|
||||
, _hideTimer([=] { hideTimer(); }) {
|
||||
recountSize();
|
||||
|
||||
connect(_connected, &QAbstractSlider::valueChanged, [=] {
|
||||
area()->scrolled();
|
||||
updateBar();
|
||||
});
|
||||
connect(_connected, &QAbstractSlider::rangeChanged, [=] {
|
||||
area()->innerResized();
|
||||
updateBar();
|
||||
});
|
||||
|
||||
updateBar();
|
||||
}
|
||||
|
||||
void ScrollBar::recountSize() {
|
||||
setGeometry(_vertical
|
||||
? QRect(
|
||||
style::RightToLeft() ? 0 : (area()->width() - _st->width),
|
||||
_st->deltat,
|
||||
_st->width,
|
||||
area()->height() - _st->deltat - _st->deltab)
|
||||
: QRect(
|
||||
_st->deltat,
|
||||
area()->height() - _st->width,
|
||||
area()->width() - _st->deltat - _st->deltab,
|
||||
_st->width));
|
||||
}
|
||||
|
||||
void ScrollBar::updateBar(bool force) {
|
||||
QRect newBar;
|
||||
if (_connected->maximum() != _scrollMax) {
|
||||
const auto oldMax = _scrollMax;
|
||||
const auto newMax = _connected->maximum();
|
||||
_scrollMax = newMax;
|
||||
area()->rangeChanged(oldMax, newMax, _vertical);
|
||||
}
|
||||
if (_vertical) {
|
||||
const auto sh = area()->scrollHeight();
|
||||
const auto rh = height();
|
||||
auto h = sh ? int32((rh * int64(area()->height())) / sh) : 0;
|
||||
if (_st->barHidden
|
||||
|| h >= rh
|
||||
|| !area()->scrollTopMax()
|
||||
|| rh < _st->minHeight) {
|
||||
if (!isHidden()) {
|
||||
hide();
|
||||
}
|
||||
const auto newTopSh = (_st->topsh < 0);
|
||||
const auto newBottomSh = (_st->bottomsh < 0);
|
||||
if (newTopSh != _topSh || force) {
|
||||
_shadowVisibilityChanged.fire({
|
||||
.type = ScrollShadow::Type::Top,
|
||||
.visible = (_topSh = newTopSh),
|
||||
});
|
||||
}
|
||||
if (newBottomSh != _bottomSh || force) {
|
||||
_shadowVisibilityChanged.fire({
|
||||
.type = ScrollShadow::Type::Bottom,
|
||||
.visible = (_bottomSh = newBottomSh),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (h <= _st->minHeight) {
|
||||
h = _st->minHeight;
|
||||
}
|
||||
const auto stm = area()->scrollTopMax();
|
||||
const auto y = stm
|
||||
? std::min(
|
||||
int32(((rh - h) * int64(area()->scrollTop())) / stm),
|
||||
rh - h)
|
||||
: 0;
|
||||
|
||||
newBar = QRect(_st->deltax, y, width() - 2 * _st->deltax, h);
|
||||
} else {
|
||||
const auto sw = area()->scrollWidth();
|
||||
const auto rw = width();
|
||||
auto w = sw ? int32((rw * int64(area()->width())) / sw) : 0;
|
||||
if (_st->barHidden
|
||||
|| w >= rw
|
||||
|| !area()->scrollLeftMax()
|
||||
|| rw < _st->minHeight) {
|
||||
if (!isHidden()) {
|
||||
hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (w <= _st->minHeight) {
|
||||
w = _st->minHeight;
|
||||
}
|
||||
const auto slm = area()->scrollLeftMax();
|
||||
const auto x = slm
|
||||
? std::min(
|
||||
int32(((rw - w) * int64(area()->scrollLeft())) / slm),
|
||||
rw - w)
|
||||
: 0;
|
||||
|
||||
newBar = QRect(x, _st->deltax, w, height() - 2 * _st->deltax);
|
||||
}
|
||||
if (newBar != _bar) {
|
||||
_bar = newBar;
|
||||
update();
|
||||
}
|
||||
if (_vertical) {
|
||||
const auto newTopSh = (_st->topsh < 0)
|
||||
|| (area()->scrollTop() > _st->topsh);
|
||||
const auto newBottomSh = (_st->bottomsh < 0)
|
||||
|| (area()->scrollTop()
|
||||
< area()->scrollTopMax() - _st->bottomsh);
|
||||
if (newTopSh != _topSh || force) {
|
||||
_shadowVisibilityChanged.fire({
|
||||
.type = ScrollShadow::Type::Top,
|
||||
.visible = (_topSh = newTopSh),
|
||||
});
|
||||
}
|
||||
if (newBottomSh != _bottomSh || force) {
|
||||
_shadowVisibilityChanged.fire({
|
||||
.type = ScrollShadow::Type::Bottom,
|
||||
.visible = (_bottomSh = newBottomSh),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isHidden()) show();
|
||||
}
|
||||
|
||||
void ScrollBar::hideTimer() {
|
||||
if (!_hiding) {
|
||||
_hiding = true;
|
||||
_a_opacity.start([this] { update(); }, 1., 0., _st->duration);
|
||||
}
|
||||
}
|
||||
|
||||
ScrollArea *ScrollBar::area() {
|
||||
return static_cast<ScrollArea*>(parentWidget());
|
||||
}
|
||||
|
||||
void ScrollBar::setOver(bool over) {
|
||||
if (_over != over) {
|
||||
auto wasOver = (_over || _moving);
|
||||
_over = over;
|
||||
auto nowOver = (_over || _moving);
|
||||
if (wasOver != nowOver) {
|
||||
_a_over.start([this] { update(); }, nowOver ? 0. : 1., nowOver ? 1. : 0., _st->duration);
|
||||
}
|
||||
if (nowOver && _hiding) {
|
||||
_hiding = false;
|
||||
_a_opacity.start([this] { update(); }, 0., 1., _st->duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::setOverBar(bool overbar) {
|
||||
if (_overbar != overbar) {
|
||||
auto wasBarOver = (_overbar || _moving);
|
||||
_overbar = overbar;
|
||||
auto nowBarOver = (_overbar || _moving);
|
||||
if (wasBarOver != nowBarOver) {
|
||||
_a_barOver.start([this] { update(); }, nowBarOver ? 0. : 1., nowBarOver ? 1. : 0., _st->duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::setMoving(bool moving) {
|
||||
if (_moving != moving) {
|
||||
auto wasOver = (_over || _moving);
|
||||
auto wasBarOver = (_overbar || _moving);
|
||||
_moving = moving;
|
||||
auto nowBarOver = (_overbar || _moving);
|
||||
if (wasBarOver != nowBarOver) {
|
||||
_a_barOver.start([this] { update(); }, nowBarOver ? 0. : 1., nowBarOver ? 1. : 0., _st->duration);
|
||||
}
|
||||
auto nowOver = (_over || _moving);
|
||||
if (wasOver != nowOver) {
|
||||
_a_over.start([this] { update(); }, nowOver ? 0. : 1., nowOver ? 1. : 0., _st->duration);
|
||||
}
|
||||
if (!nowOver && _st->hiding && !_hiding) {
|
||||
_hideTimer.callOnce(_hideIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::paintEvent(QPaintEvent *e) {
|
||||
if (!_bar.width() && !_bar.height()) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
|
||||
if (opacity == 0.) return;
|
||||
|
||||
QPainter p(this);
|
||||
auto deltal = _vertical ? _st->deltax : 0, deltar = _vertical ? _st->deltax : 0;
|
||||
auto deltat = _vertical ? 0 : _st->deltax, deltab = _vertical ? 0 : _st->deltax;
|
||||
p.setPen(Qt::NoPen);
|
||||
auto bg = anim::color(_st->bg, _st->bgOver, _a_over.value((_over || _moving) ? 1. : 0.));
|
||||
bg.setAlpha(anim::interpolate(0, bg.alpha(), opacity));
|
||||
auto bar = anim::color(_st->barBg, _st->barBgOver, _a_barOver.value((_overbar || _moving) ? 1. : 0.));
|
||||
bar.setAlpha(anim::interpolate(0, bar.alpha(), opacity));
|
||||
const auto outer = QRect(deltal, deltat, width() - deltal - deltar, height() - deltat - deltab);
|
||||
const auto radius = (_st->round < 0)
|
||||
? (std::min(outer.width(), outer.height()) / 2.)
|
||||
: _st->round;
|
||||
if (radius) {
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.setBrush(bg);
|
||||
p.drawRoundedRect(outer, radius, radius);
|
||||
p.setBrush(bar);
|
||||
p.drawRoundedRect(_bar, radius, radius);
|
||||
} else {
|
||||
p.fillRect(outer, bg);
|
||||
p.fillRect(_bar, bar);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::hideTimeout(crl::time dt) {
|
||||
if (_hiding && dt > 0) {
|
||||
_hiding = false;
|
||||
_a_opacity.start([this] { update(); }, 0., 1., _st->duration);
|
||||
}
|
||||
_hideIn = dt;
|
||||
if (!_moving) {
|
||||
_hideTimer.callOnce(_hideIn);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::enterEventHook(QEnterEvent *e) {
|
||||
_hideTimer.cancel();
|
||||
setMouseTracking(true);
|
||||
setOver(true);
|
||||
}
|
||||
|
||||
void ScrollBar::leaveEventHook(QEvent *e) {
|
||||
if (!_moving) {
|
||||
setMouseTracking(false);
|
||||
}
|
||||
setOver(false);
|
||||
setOverBar(false);
|
||||
if (_st->hiding && !_hiding) {
|
||||
_hideTimer.callOnce(_hideIn);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::mouseMoveEvent(QMouseEvent *e) {
|
||||
setOverBar(_bar.contains(e->pos()));
|
||||
if (_moving) {
|
||||
int delta = 0, barDelta = _vertical ? (area()->height() - _bar.height()) : (area()->width() - _bar.width());
|
||||
if (barDelta > 0) {
|
||||
QPoint d = (e->globalPos() - _dragStart);
|
||||
delta = int32((_vertical ? (d.y() * int64(area()->scrollTopMax())) : (d.x() * int64(area()->scrollLeftMax()))) / barDelta);
|
||||
}
|
||||
_connected->setValue(_startFrom + delta);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::mousePressEvent(QMouseEvent *e) {
|
||||
if (!width() || !height()) return;
|
||||
|
||||
_dragStart = e->globalPos();
|
||||
area()->setMovingByScrollBar(true);
|
||||
setMoving(true);
|
||||
if (_overbar) {
|
||||
_startFrom = _connected->value();
|
||||
} else {
|
||||
int32 val = _vertical ? e->pos().y() : e->pos().x(), div = _vertical ? height() : width();
|
||||
val = (val <= _st->deltat) ? 0 : (val - _st->deltat);
|
||||
div = (div <= _st->deltat + _st->deltab) ? 1 : (div - _st->deltat - _st->deltab);
|
||||
_startFrom = _vertical ? int32((val * int64(area()->scrollTopMax())) / div) : ((val * int64(area()->scrollLeftMax())) / div);
|
||||
_connected->setValue(_startFrom);
|
||||
setOverBar(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (_moving) {
|
||||
area()->setMovingByScrollBar(false);
|
||||
setMoving(false);
|
||||
}
|
||||
if (!_over) {
|
||||
setMouseTracking(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollBar::resizeEvent(QResizeEvent *e) {
|
||||
updateBar();
|
||||
}
|
||||
|
||||
void ScrollBar::wheelEvent(QWheelEvent *e) {
|
||||
static_cast<ScrollArea*>(parentWidget())->viewportEvent(e);
|
||||
}
|
||||
|
||||
auto ScrollBar::shadowVisibilityChanged() const
|
||||
-> rpl::producer<ScrollBar::ShadowVisibility> {
|
||||
return _shadowVisibilityChanged.events();
|
||||
}
|
||||
|
||||
ScrollArea::ScrollArea(
|
||||
QWidget *parent,
|
||||
const style::ScrollArea &st,
|
||||
bool handleTouch)
|
||||
: Parent(parent)
|
||||
, _st(st)
|
||||
, _horizontalBar(this, false, &_st)
|
||||
, _verticalBar(this, true, &_st)
|
||||
, _topShadow(this, &_st)
|
||||
, _bottomShadow(this, &_st)
|
||||
, _touchEnabled(handleTouch) {
|
||||
setLayoutDirection(style::LayoutDirection());
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
_verticalBar->shadowVisibilityChanged(
|
||||
) | rpl::on_next([=](const ScrollBar::ShadowVisibility &data) {
|
||||
((data.type == ScrollShadow::Type::Top)
|
||||
? _topShadow
|
||||
: _bottomShadow)->changeVisibility(data.visible);
|
||||
}, lifetime());
|
||||
|
||||
_verticalBar->updateBar(true);
|
||||
|
||||
verticalScrollBar()->setSingleStep(style::ConvertScale(verticalScrollBar()->singleStep()));
|
||||
horizontalScrollBar()->setSingleStep(style::ConvertScale(horizontalScrollBar()->singleStep()));
|
||||
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
|
||||
viewport()->setAutoFillBackground(false);
|
||||
|
||||
_horizontalValue = horizontalScrollBar()->value();
|
||||
_verticalValue = verticalScrollBar()->value();
|
||||
|
||||
if (_touchEnabled) {
|
||||
viewport()->setAttribute(Qt::WA_AcceptTouchEvents);
|
||||
_touchTimer.setCallback([=] { _touchRightButton = true; });
|
||||
_touchScrollTimer.setCallback([=] { touchScrollTimer(); });
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::touchDeaccelerate(int32 elapsed) {
|
||||
int32 x = _touchSpeed.x();
|
||||
int32 y = _touchSpeed.y();
|
||||
_touchSpeed.setX((x == 0) ? x : (x > 0) ? qMax(0, x - elapsed) : qMin(0, x + elapsed));
|
||||
_touchSpeed.setY((y == 0) ? y : (y > 0) ? qMax(0, y - elapsed) : qMin(0, y + elapsed));
|
||||
}
|
||||
|
||||
void ScrollArea::scrolled() {
|
||||
if (const auto inner = widget()) {
|
||||
SendPendingMoveResizeEvents(inner);
|
||||
}
|
||||
|
||||
bool em = false;
|
||||
int horizontalValue = horizontalScrollBar()->value();
|
||||
int verticalValue = verticalScrollBar()->value();
|
||||
if (_horizontalValue != horizontalValue) {
|
||||
if (_disabled) {
|
||||
horizontalScrollBar()->setValue(_horizontalValue);
|
||||
} else {
|
||||
_horizontalValue = horizontalValue;
|
||||
if (_st.hiding) {
|
||||
_horizontalBar->hideTimeout(_st.hiding);
|
||||
}
|
||||
em = true;
|
||||
}
|
||||
}
|
||||
if (_verticalValue != verticalValue) {
|
||||
if (_disabled) {
|
||||
verticalScrollBar()->setValue(_verticalValue);
|
||||
} else {
|
||||
_verticalValue = verticalValue;
|
||||
if (_st.hiding) {
|
||||
_verticalBar->hideTimeout(_st.hiding);
|
||||
}
|
||||
em = true;
|
||||
_scrollTopUpdated.fire_copy(_verticalValue);
|
||||
}
|
||||
}
|
||||
if (em) {
|
||||
_scrolls.fire({});
|
||||
if (!_movingByScrollBar) {
|
||||
SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::innerResized() {
|
||||
_innerResizes.fire({});
|
||||
}
|
||||
|
||||
int ScrollArea::scrollWidth() const {
|
||||
QWidget *w(widget());
|
||||
return w ? qMax(w->width(), width()) : width();
|
||||
}
|
||||
|
||||
int ScrollArea::scrollHeight() const {
|
||||
QWidget *w(widget());
|
||||
return w ? qMax(w->height(), height()) : height();
|
||||
}
|
||||
|
||||
int ScrollArea::scrollLeftMax() const {
|
||||
return scrollWidth() - width();
|
||||
}
|
||||
|
||||
int ScrollArea::scrollTopMax() const {
|
||||
return scrollHeight() - height();
|
||||
}
|
||||
|
||||
int ScrollArea::scrollLeft() const {
|
||||
return _horizontalValue;
|
||||
}
|
||||
|
||||
int ScrollArea::scrollTop() const {
|
||||
return _verticalValue;
|
||||
}
|
||||
|
||||
void ScrollArea::touchScrollTimer() {
|
||||
auto nowTime = crl::now();
|
||||
if (_touchScrollState == TouchScrollState::Acceleration && _touchWaitingAcceleration && (nowTime - _touchAccelerationTime) > 40) {
|
||||
_touchScrollState = TouchScrollState::Manual;
|
||||
touchResetSpeed();
|
||||
} else if (_touchScrollState == TouchScrollState::Auto || _touchScrollState == TouchScrollState::Acceleration) {
|
||||
int32 elapsed = int32(nowTime - _touchTime);
|
||||
QPoint delta = _touchSpeed * elapsed / 1000;
|
||||
bool hasScrolled = touchScroll(delta);
|
||||
|
||||
if (_touchSpeed.isNull() || !hasScrolled) {
|
||||
_touchScrollState = TouchScrollState::Manual;
|
||||
_touchScroll = false;
|
||||
_touchScrollTimer.cancel();
|
||||
} else {
|
||||
_touchTime = nowTime;
|
||||
}
|
||||
touchDeaccelerate(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::touchUpdateSpeed() {
|
||||
const auto nowTime = crl::now();
|
||||
if (_touchPrevPosValid) {
|
||||
const int elapsed = nowTime - _touchSpeedTime;
|
||||
if (elapsed) {
|
||||
const QPoint newPixelDiff = (_touchPos - _touchPrevPos);
|
||||
const QPoint pixelsPerSecond = newPixelDiff * (1000 / elapsed);
|
||||
|
||||
// fingers are inacurates, we ignore small changes to avoid stopping the autoscroll because
|
||||
// of a small horizontal offset when scrolling vertically
|
||||
const int newSpeedY = (qAbs(pixelsPerSecond.y()) > kFingerAccuracyThreshold) ? pixelsPerSecond.y() : 0;
|
||||
const int newSpeedX = (qAbs(pixelsPerSecond.x()) > kFingerAccuracyThreshold) ? pixelsPerSecond.x() : 0;
|
||||
if (_touchScrollState == TouchScrollState::Auto) {
|
||||
const int oldSpeedY = _touchSpeed.y();
|
||||
const int oldSpeedX = _touchSpeed.x();
|
||||
if ((oldSpeedY <= 0 && newSpeedY <= 0) || ((oldSpeedY >= 0 && newSpeedY >= 0)
|
||||
&& (oldSpeedX <= 0 && newSpeedX <= 0)) || (oldSpeedX >= 0 && newSpeedX >= 0)) {
|
||||
_touchSpeed.setY(std::clamp((oldSpeedY + (newSpeedY / 4)), -kMaxScrollAccelerated, +kMaxScrollAccelerated));
|
||||
_touchSpeed.setX(std::clamp((oldSpeedX + (newSpeedX / 4)), -kMaxScrollAccelerated, +kMaxScrollAccelerated));
|
||||
} else {
|
||||
_touchSpeed = QPoint();
|
||||
}
|
||||
} else {
|
||||
// we average the speed to avoid strange effects with the last delta
|
||||
if (!_touchSpeed.isNull()) {
|
||||
_touchSpeed.setX(std::clamp((_touchSpeed.x() / 4) + (newSpeedX * 3 / 4), -kMaxScrollFlick, +kMaxScrollFlick));
|
||||
_touchSpeed.setY(std::clamp((_touchSpeed.y() / 4) + (newSpeedY * 3 / 4), -kMaxScrollFlick, +kMaxScrollFlick));
|
||||
} else {
|
||||
_touchSpeed = QPoint(newSpeedX, newSpeedY);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_touchPrevPosValid = true;
|
||||
}
|
||||
_touchSpeedTime = nowTime;
|
||||
_touchPrevPos = _touchPos;
|
||||
}
|
||||
|
||||
void ScrollArea::touchResetSpeed() {
|
||||
_touchSpeed = QPoint();
|
||||
_touchPrevPosValid = false;
|
||||
}
|
||||
|
||||
bool ScrollArea::eventHook(QEvent *e) {
|
||||
const auto was = (e->type() == QEvent::LayoutRequest)
|
||||
? verticalScrollBar()->minimum()
|
||||
: 0;
|
||||
const auto result = RpWidgetBase<QScrollArea>::eventHook(e);
|
||||
if (was) {
|
||||
// Because LayoutRequest resets custom-set minimum allowed value.
|
||||
verticalScrollBar()->setMinimum(was);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ScrollArea::eventFilter(QObject *obj, QEvent *e) {
|
||||
const auto result = QScrollArea::eventFilter(obj, e);
|
||||
return (obj == widget() && filterOutTouchEvent(e)) || result;
|
||||
}
|
||||
|
||||
bool ScrollArea::viewportEvent(QEvent *e) {
|
||||
if (filterOutTouchEvent(e)) {
|
||||
return true;
|
||||
} else if (e->type() == QEvent::Wheel) {
|
||||
if (_customWheelProcess
|
||||
&& _customWheelProcess(static_cast<QWheelEvent*>(e))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QScrollArea::viewportEvent(e);
|
||||
}
|
||||
|
||||
bool ScrollArea::filterOutTouchEvent(QEvent *e) {
|
||||
const auto type = e->type();
|
||||
if (type == QEvent::TouchBegin
|
||||
|| type == QEvent::TouchUpdate
|
||||
|| type == QEvent::TouchEnd
|
||||
|| type == QEvent::TouchCancel) {
|
||||
const auto ev = static_cast<QTouchEvent*>(e);
|
||||
if (ev->device()->type() == base::TouchDevice::TouchScreen) {
|
||||
if (_customTouchProcess && _customTouchProcess(ev)) {
|
||||
return true;
|
||||
} else if (_touchEnabled) {
|
||||
touchEvent(ev);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScrollArea::touchEvent(QTouchEvent *e) {
|
||||
if (!e->touchPoints().isEmpty()) {
|
||||
_touchPrevPos = _touchPos;
|
||||
_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
|
||||
}
|
||||
|
||||
switch (e->type()) {
|
||||
case QEvent::TouchBegin: {
|
||||
if (_touchPress || e->touchPoints().isEmpty()) return;
|
||||
_touchPress = true;
|
||||
if (_touchScrollState == TouchScrollState::Auto) {
|
||||
_touchScrollState = TouchScrollState::Acceleration;
|
||||
_touchWaitingAcceleration = true;
|
||||
_touchMaybePressing = false;
|
||||
_touchAccelerationTime = crl::now();
|
||||
touchUpdateSpeed();
|
||||
_touchStart = _touchPos;
|
||||
} else {
|
||||
_touchScroll = false;
|
||||
_touchMaybePressing = true;
|
||||
_touchTimer.callOnce(QApplication::startDragTime());
|
||||
}
|
||||
_touchStart = _touchPrevPos = _touchPos;
|
||||
_touchRightButton = false;
|
||||
} break;
|
||||
|
||||
case QEvent::TouchUpdate: {
|
||||
if (!_touchPress) return;
|
||||
if (!_touchScroll && (_touchPos - _touchStart).manhattanLength() >= QApplication::startDragDistance()) {
|
||||
_touchTimer.cancel();
|
||||
_touchScroll = true;
|
||||
_touchMaybePressing = false;
|
||||
touchUpdateSpeed();
|
||||
}
|
||||
if (_touchScroll) {
|
||||
if (_touchScrollState == TouchScrollState::Manual) {
|
||||
touchScrollUpdated(_touchPos);
|
||||
} else if (_touchScrollState == TouchScrollState::Acceleration) {
|
||||
touchUpdateSpeed();
|
||||
_touchAccelerationTime = crl::now();
|
||||
if (_touchSpeed.isNull()) {
|
||||
_touchScrollState = TouchScrollState::Manual;
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case QEvent::TouchEnd: {
|
||||
if (!_touchPress) return;
|
||||
_touchPress = false;
|
||||
auto weak = base::make_weak(this);
|
||||
if (_touchScroll) {
|
||||
if (_touchScrollState == TouchScrollState::Manual) {
|
||||
_touchScrollState = TouchScrollState::Auto;
|
||||
_touchPrevPosValid = false;
|
||||
_touchScrollTimer.callEach(15);
|
||||
_touchTime = crl::now();
|
||||
} else if (_touchScrollState == TouchScrollState::Auto) {
|
||||
_touchScrollState = TouchScrollState::Manual;
|
||||
_touchScroll = false;
|
||||
touchResetSpeed();
|
||||
} else if (_touchScrollState == TouchScrollState::Acceleration) {
|
||||
_touchScrollState = TouchScrollState::Auto;
|
||||
_touchWaitingAcceleration = false;
|
||||
_touchPrevPosValid = false;
|
||||
}
|
||||
} else if (window()) { // one short tap -- like left mouse click, one long tap -- like right mouse click
|
||||
Qt::MouseButton btn(_touchRightButton ? Qt::RightButton : Qt::LeftButton);
|
||||
|
||||
if (weak) SendSynteticMouseEvent(this, QEvent::MouseMove, Qt::NoButton, _touchStart);
|
||||
if (weak) SendSynteticMouseEvent(this, QEvent::MouseButtonPress, btn, _touchStart);
|
||||
if (weak) SendSynteticMouseEvent(this, QEvent::MouseButtonRelease, btn, _touchStart);
|
||||
|
||||
if (weak && _touchRightButton) {
|
||||
auto windowHandle = window()->windowHandle();
|
||||
auto localPoint = windowHandle->mapFromGlobal(_touchStart);
|
||||
QContextMenuEvent ev(QContextMenuEvent::Mouse, localPoint, _touchStart, QGuiApplication::keyboardModifiers());
|
||||
ev.setTimestamp(crl::now());
|
||||
QGuiApplication::sendEvent(windowHandle, &ev);
|
||||
}
|
||||
}
|
||||
if (weak) {
|
||||
_touchTimer.cancel();
|
||||
_touchRightButton = false;
|
||||
_touchMaybePressing = false;
|
||||
}
|
||||
} break;
|
||||
|
||||
case QEvent::TouchCancel: {
|
||||
_touchPress = false;
|
||||
_touchScroll = false;
|
||||
_touchMaybePressing = false;
|
||||
_touchScrollState = TouchScrollState::Manual;
|
||||
_touchTimer.cancel();
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::touchScrollUpdated(const QPoint &screenPos) {
|
||||
_touchPos = screenPos;
|
||||
touchScroll(_touchPos - _touchPrevPos);
|
||||
touchUpdateSpeed();
|
||||
}
|
||||
|
||||
void ScrollArea::disableScroll(bool dis) {
|
||||
_disabled = dis;
|
||||
if (_disabled && _st.hiding) {
|
||||
_horizontalBar->hideTimeout(0);
|
||||
_verticalBar->hideTimeout(0);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::scrollContentsBy(int dx, int dy) {
|
||||
if (_disabled) {
|
||||
return;
|
||||
}
|
||||
QScrollArea::scrollContentsBy(dx, dy);
|
||||
}
|
||||
|
||||
bool ScrollArea::touchScroll(const QPoint &delta) {
|
||||
const auto top = scrollTop();
|
||||
const auto topMax = scrollTopMax();
|
||||
const auto left = scrollLeft();
|
||||
const auto leftMax = scrollLeftMax();
|
||||
const auto xAbs = qAbs(delta.x());
|
||||
const auto yAbs = qAbs(delta.y());
|
||||
const auto direction = (leftMax <= 0 || yAbs > xAbs)
|
||||
? Qt::Vertical
|
||||
: Qt::Horizontal;
|
||||
const auto was = (direction == Qt::Vertical) ? top : left;
|
||||
const auto now = (direction == Qt::Vertical)
|
||||
? std::clamp(top - delta.y(), 0, topMax)
|
||||
: std::clamp(left - delta.x(), 0, leftMax);
|
||||
if (now == was) {
|
||||
return false;
|
||||
} else if (direction == Qt::Vertical) {
|
||||
scrollToY(now);
|
||||
} else {
|
||||
horizontalScrollBar()->setValue(now);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScrollArea::resizeEvent(QResizeEvent *e) {
|
||||
QScrollArea::resizeEvent(e);
|
||||
_horizontalBar->recountSize();
|
||||
_verticalBar->recountSize();
|
||||
_topShadow->setGeometry(QRect(0, 0, width(), qAbs(_st.topsh)));
|
||||
_bottomShadow->setGeometry(QRect(0, height() - qAbs(_st.bottomsh), width(), qAbs(_st.bottomsh)));
|
||||
_geometryChanged.fire({});
|
||||
}
|
||||
|
||||
void ScrollArea::moveEvent(QMoveEvent *e) {
|
||||
QScrollArea::moveEvent(e);
|
||||
_geometryChanged.fire({});
|
||||
}
|
||||
|
||||
void ScrollArea::keyPressEvent(QKeyEvent *e) {
|
||||
if ((e->key() == Qt::Key_Up || e->key() == Qt::Key_Down)
|
||||
&& (e->modifiers().testFlag(Qt::AltModifier)
|
||||
|| e->modifiers().testFlag(Qt::ControlModifier))) {
|
||||
e->ignore();
|
||||
} else if(e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
|
||||
((QObject*)widget())->event(e);
|
||||
} else {
|
||||
QScrollArea::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void ScrollArea::enterEventHook(QEnterEvent *e) {
|
||||
if (_disabled) return;
|
||||
if (_st.hiding) {
|
||||
_horizontalBar->hideTimeout(_st.hiding);
|
||||
_verticalBar->hideTimeout(_st.hiding);
|
||||
}
|
||||
return QScrollArea::enterEvent(e);
|
||||
}
|
||||
|
||||
void ScrollArea::leaveEventHook(QEvent *e) {
|
||||
if (_st.hiding) {
|
||||
_horizontalBar->hideTimeout(0);
|
||||
_verticalBar->hideTimeout(0);
|
||||
}
|
||||
return QScrollArea::leaveEvent(e);
|
||||
}
|
||||
|
||||
void ScrollArea::scrollTo(ScrollToRequest request) {
|
||||
scrollToY(request.ymin, request.ymax);
|
||||
}
|
||||
|
||||
void ScrollArea::scrollToWidget(not_null<QWidget*> widget) {
|
||||
if (auto local = this->widget()) {
|
||||
auto globalPosition = widget->mapToGlobal(QPoint(0, 0));
|
||||
auto localPosition = local->mapFromGlobal(globalPosition);
|
||||
auto localTop = localPosition.y();
|
||||
auto localBottom = localTop + widget->height();
|
||||
scrollToY(localTop, localBottom);
|
||||
}
|
||||
}
|
||||
|
||||
int ScrollArea::computeScrollToX(int toLeft, int toRight) {
|
||||
if (const auto inner = widget()) {
|
||||
SendPendingMoveResizeEvents(inner);
|
||||
}
|
||||
SendPendingMoveResizeEvents(this);
|
||||
return ComputeScrollTo(
|
||||
toLeft,
|
||||
toRight,
|
||||
0,
|
||||
scrollLeftMax(),
|
||||
scrollLeft(),
|
||||
width());
|
||||
}
|
||||
|
||||
int ScrollArea::computeScrollToY(int toTop, int toBottom) {
|
||||
if (const auto inner = widget()) {
|
||||
SendPendingMoveResizeEvents(inner);
|
||||
}
|
||||
SendPendingMoveResizeEvents(this);
|
||||
return ComputeScrollTo(
|
||||
toTop,
|
||||
toBottom,
|
||||
0,
|
||||
scrollTopMax(),
|
||||
scrollTop(),
|
||||
height());
|
||||
}
|
||||
|
||||
void ScrollArea::scrollToX(int toLeft, int toRight) {
|
||||
horizontalScrollBar()->setValue(computeScrollToX(toLeft, toRight));
|
||||
}
|
||||
|
||||
void ScrollArea::scrollToY(int toTop, int toBottom) {
|
||||
verticalScrollBar()->setValue(computeScrollToY(toTop, toBottom));
|
||||
}
|
||||
|
||||
void ScrollArea::doSetOwnedWidget(object_ptr<QWidget> w) {
|
||||
if (widget() && _touchEnabled) {
|
||||
widget()->removeEventFilter(this);
|
||||
if (!_widgetAcceptsTouch) widget()->setAttribute(Qt::WA_AcceptTouchEvents, false);
|
||||
}
|
||||
_widget = std::move(w);
|
||||
QScrollArea::setWidget(_widget);
|
||||
if (_widget) {
|
||||
_widget->setAutoFillBackground(false);
|
||||
if (_touchEnabled) {
|
||||
_widget->installEventFilter(this);
|
||||
_widgetAcceptsTouch = _widget->testAttribute(Qt::WA_AcceptTouchEvents);
|
||||
_widget->setAttribute(Qt::WA_AcceptTouchEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object_ptr<QWidget> ScrollArea::doTakeWidget() {
|
||||
QScrollArea::takeWidget();
|
||||
return std::move(_widget);
|
||||
}
|
||||
|
||||
void ScrollArea::rangeChanged(int oldMax, int newMax, bool vertical) {
|
||||
}
|
||||
|
||||
void ScrollArea::updateBars() {
|
||||
_horizontalBar->updateBar(true);
|
||||
_verticalBar->updateBar(true);
|
||||
}
|
||||
|
||||
bool ScrollArea::focusNextPrevChild(bool next) {
|
||||
if (QWidget::focusNextPrevChild(next)) {
|
||||
// if (QWidget *fw = focusWidget())
|
||||
// ensureWidgetVisible(fw);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScrollArea::setMovingByScrollBar(bool movingByScrollBar) {
|
||||
_movingByScrollBar = movingByScrollBar;
|
||||
}
|
||||
|
||||
rpl::producer<> ScrollArea::scrolls() const {
|
||||
return _scrolls.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ScrollArea::innerResizes() const {
|
||||
return _innerResizes.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ScrollArea::geometryChanged() const {
|
||||
return _geometryChanged.events();
|
||||
}
|
||||
|
||||
rpl::producer<bool> ScrollArea::touchMaybePressing() const {
|
||||
return _touchMaybePressing.value();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
256
Telegram/lib_ui/ui/widgets/scroll_area.h
Normal file
256
Telegram/lib_ui/ui/widgets/scroll_area.h
Normal file
@@ -0,0 +1,256 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "base/timer.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtWidgets/QScrollArea>
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
// Touch flick ignore 3px.
|
||||
inline constexpr auto kFingerAccuracyThreshold = 3;
|
||||
|
||||
// 4000px per second.
|
||||
inline constexpr auto kMaxScrollAccelerated = 4000;
|
||||
|
||||
// 2500px per second.
|
||||
inline constexpr auto kMaxScrollFlick = 2500;
|
||||
|
||||
enum class TouchScrollState {
|
||||
Manual, // Scrolling manually with the finger on the screen
|
||||
Auto, // Scrolling automatically
|
||||
Acceleration // Scrolling automatically but a finger is on the screen
|
||||
};
|
||||
|
||||
class ScrollArea;
|
||||
|
||||
struct ScrollToRequest {
|
||||
ScrollToRequest(int ymin, int ymax)
|
||||
: ymin(ymin)
|
||||
, ymax(ymax) {
|
||||
}
|
||||
|
||||
int ymin = 0;
|
||||
int ymax = 0;
|
||||
|
||||
};
|
||||
|
||||
class ScrollShadow final : public QWidget {
|
||||
public:
|
||||
enum class Type {
|
||||
Top,
|
||||
Bottom,
|
||||
};
|
||||
ScrollShadow(ScrollArea *parent, const style::ScrollArea *st);
|
||||
|
||||
void paintEvent(QPaintEvent *e);
|
||||
void changeVisibility(bool shown);
|
||||
|
||||
private:
|
||||
const style::ScrollArea *_st;
|
||||
|
||||
};
|
||||
|
||||
class ScrollBar : public RpWidget {
|
||||
public:
|
||||
struct ShadowVisibility {
|
||||
ScrollShadow::Type type;
|
||||
bool visible = false;
|
||||
};
|
||||
ScrollBar(ScrollArea *parent, bool vertical, const style::ScrollArea *st);
|
||||
|
||||
void recountSize();
|
||||
void updateBar(bool force = false);
|
||||
|
||||
void hideTimeout(crl::time dt);
|
||||
|
||||
[[nodiscard]] auto shadowVisibilityChanged() const
|
||||
-> rpl::producer<ShadowVisibility>;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
private:
|
||||
ScrollArea *area();
|
||||
|
||||
void setOver(bool over);
|
||||
void setOverBar(bool overbar);
|
||||
void setMoving(bool moving);
|
||||
|
||||
void hideTimer();
|
||||
|
||||
const style::ScrollArea *_st;
|
||||
|
||||
bool _vertical = true;
|
||||
bool _hiding = false;
|
||||
bool _over = false;
|
||||
bool _overbar = false;
|
||||
bool _moving = false;
|
||||
bool _topSh = false;
|
||||
bool _bottomSh = false;
|
||||
|
||||
QPoint _dragStart;
|
||||
QScrollBar *_connected;
|
||||
|
||||
int32 _startFrom, _scrollMax;
|
||||
|
||||
crl::time _hideIn = 0;
|
||||
base::Timer _hideTimer;
|
||||
|
||||
Animations::Simple _a_over;
|
||||
Animations::Simple _a_barOver;
|
||||
Animations::Simple _a_opacity;
|
||||
|
||||
QRect _bar;
|
||||
|
||||
rpl::event_stream<ShadowVisibility> _shadowVisibilityChanged;
|
||||
};
|
||||
|
||||
class ScrollArea : public RpWidgetBase<QScrollArea> {
|
||||
public:
|
||||
using Parent = RpWidgetBase<QScrollArea>;
|
||||
ScrollArea(QWidget *parent, const style::ScrollArea &st = st::defaultScrollArea, bool handleTouch = true);
|
||||
|
||||
int scrollWidth() const;
|
||||
int scrollHeight() const;
|
||||
int scrollLeftMax() const;
|
||||
int scrollTopMax() const;
|
||||
int scrollLeft() const;
|
||||
int scrollTop() const;
|
||||
|
||||
template <typename Widget>
|
||||
QPointer<Widget> setOwnedWidget(object_ptr<Widget> widget) {
|
||||
auto result = QPointer<Widget>(widget);
|
||||
doSetOwnedWidget(std::move(widget));
|
||||
return result;
|
||||
}
|
||||
template <typename Widget>
|
||||
object_ptr<Widget> takeWidget() {
|
||||
return object_ptr<Widget>::fromRaw(
|
||||
static_cast<Widget*>(doTakeWidget().release()));
|
||||
}
|
||||
|
||||
void rangeChanged(int oldMax, int newMax, bool vertical);
|
||||
|
||||
void updateBars();
|
||||
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
void setMovingByScrollBar(bool movingByScrollBar);
|
||||
|
||||
bool viewportEvent(QEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
|
||||
auto scrollTopValue() const {
|
||||
return _scrollTopUpdated.events_starting_with(scrollTop());
|
||||
}
|
||||
auto scrollTopChanges() const {
|
||||
return _scrollTopUpdated.events();
|
||||
}
|
||||
|
||||
void scrollTo(ScrollToRequest request);
|
||||
void scrollToWidget(not_null<QWidget*> widget);
|
||||
[[nodiscard]] int computeScrollToX(int toLeft, int toRight);
|
||||
[[nodiscard]] int computeScrollToY(int toTop, int toBottom);
|
||||
|
||||
void scrollToX(int toLeft, int toRight = -1);
|
||||
void scrollToY(int toTop, int toBottom = -1);
|
||||
void disableScroll(bool dis);
|
||||
void scrolled();
|
||||
void innerResized();
|
||||
|
||||
void setCustomWheelProcess(Fn<bool(not_null<QWheelEvent*>)> process) {
|
||||
_customWheelProcess = std::move(process);
|
||||
}
|
||||
void setCustomTouchProcess(Fn<bool(not_null<QTouchEvent*>)> process) {
|
||||
_customTouchProcess = std::move(process);
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<> scrolls() const;
|
||||
[[nodiscard]] rpl::producer<> innerResizes() const;
|
||||
[[nodiscard]] rpl::producer<> geometryChanged() const;
|
||||
|
||||
[[nodiscard]] rpl::producer<bool> touchMaybePressing() const;
|
||||
|
||||
protected:
|
||||
bool eventHook(QEvent *e) override;
|
||||
bool eventFilter(QObject *obj, QEvent *e) override;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void moveEvent(QMoveEvent *e) override;
|
||||
void touchEvent(QTouchEvent *e);
|
||||
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
|
||||
protected:
|
||||
void scrollContentsBy(int dx, int dy) override;
|
||||
|
||||
private:
|
||||
void doSetOwnedWidget(object_ptr<QWidget> widget);
|
||||
object_ptr<QWidget> doTakeWidget();
|
||||
|
||||
bool filterOutTouchEvent(QEvent *e);
|
||||
void touchScrollTimer();
|
||||
bool touchScroll(const QPoint &delta);
|
||||
void touchScrollUpdated(const QPoint &screenPos);
|
||||
|
||||
void touchResetSpeed();
|
||||
void touchUpdateSpeed();
|
||||
void touchDeaccelerate(int32 elapsed);
|
||||
|
||||
bool _disabled = false;
|
||||
bool _movingByScrollBar = false;
|
||||
|
||||
const style::ScrollArea &_st;
|
||||
object_ptr<ScrollBar> _horizontalBar, _verticalBar;
|
||||
object_ptr<ScrollShadow> _topShadow, _bottomShadow;
|
||||
int _horizontalValue, _verticalValue;
|
||||
|
||||
bool _touchEnabled = false;
|
||||
base::Timer _touchTimer;
|
||||
bool _touchScroll = false;
|
||||
bool _touchPress = false;
|
||||
bool _touchRightButton = false;
|
||||
QPoint _touchStart, _touchPrevPos, _touchPos;
|
||||
|
||||
TouchScrollState _touchScrollState = TouchScrollState::Manual;
|
||||
bool _touchPrevPosValid = false;
|
||||
bool _touchWaitingAcceleration = false;
|
||||
rpl::variable<bool> _touchMaybePressing;
|
||||
QPoint _touchSpeed;
|
||||
crl::time _touchSpeedTime = 0;
|
||||
crl::time _touchAccelerationTime = 0;
|
||||
crl::time _touchTime = 0;
|
||||
base::Timer _touchScrollTimer;
|
||||
|
||||
Fn<bool(not_null<QWheelEvent*>)> _customWheelProcess;
|
||||
Fn<bool(not_null<QTouchEvent*>)> _customTouchProcess;
|
||||
bool _widgetAcceptsTouch = false;
|
||||
|
||||
object_ptr<QWidget> _widget = { nullptr };
|
||||
|
||||
rpl::event_stream<int> _scrollTopUpdated;
|
||||
rpl::event_stream<> _scrolls;
|
||||
rpl::event_stream<> _innerResizes;
|
||||
rpl::event_stream<> _geometryChanged;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
1678
Telegram/lib_ui/ui/widgets/separate_panel.cpp
Normal file
1678
Telegram/lib_ui/ui/widgets/separate_panel.cpp
Normal file
File diff suppressed because it is too large
Load Diff
242
Telegram/lib_ui/ui/widgets/separate_panel.h
Normal file
242
Telegram/lib_ui/ui/widgets/separate_panel.h
Normal file
@@ -0,0 +1,242 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/flat_map.h"
|
||||
#include "base/weak_ptr.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/layers/layer_widget.h"
|
||||
#include "ui/text/text_entity.h"
|
||||
|
||||
#include <rpl/variable.h>
|
||||
|
||||
class Painter;
|
||||
|
||||
namespace style {
|
||||
struct IconButton;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui::Menu {
|
||||
struct MenuCallback;
|
||||
} // namespace Ui::Menu
|
||||
|
||||
namespace Ui::Toast {
|
||||
struct Config;
|
||||
class Instance;
|
||||
} // namespace Ui::Toast
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class Show;
|
||||
class BoxContent;
|
||||
class IconButton;
|
||||
class PopupMenu;
|
||||
class LayerStackWidget;
|
||||
class LayerWidget;
|
||||
class FlatLabel;
|
||||
class InputField;
|
||||
template <typename Widget>
|
||||
class FadeWrapScaled;
|
||||
template <typename Widget>
|
||||
class FadeWrap;
|
||||
|
||||
struct SeparatePanelArgs {
|
||||
QWidget *parent = nullptr;
|
||||
bool onAllSpaces = false;
|
||||
Fn<bool(int zorder)> animationsPaused;
|
||||
const style::PopupMenu *menuSt = nullptr;
|
||||
};
|
||||
|
||||
class SeparatePanel final : public RpWidget {
|
||||
public:
|
||||
explicit SeparatePanel(SeparatePanelArgs &&args = {});
|
||||
~SeparatePanel();
|
||||
|
||||
void setTitle(rpl::producer<QString> title);
|
||||
void setTitleHeight(int height);
|
||||
void setTitleBadge(object_ptr<RpWidget> badge);
|
||||
void setInnerSize(QSize size, bool allowResize = false);
|
||||
[[nodiscard]] QRect innerGeometry() const;
|
||||
|
||||
void toggleFullScreen(bool fullscreen);
|
||||
void allowChildFullScreenControls(bool allow);
|
||||
[[nodiscard]] rpl::producer<bool> fullScreenValue() const;
|
||||
[[nodiscard]] QMargins computePadding() const;
|
||||
|
||||
void setHideOnDeactivate(bool hideOnDeactivate);
|
||||
void showAndActivate();
|
||||
int hideGetDuration();
|
||||
|
||||
[[nodiscard]] RpWidget *inner() const;
|
||||
void showInner(base::unique_qptr<RpWidget> inner);
|
||||
void showBox(
|
||||
object_ptr<BoxContent> box,
|
||||
LayerOptions options,
|
||||
anim::type animated);
|
||||
void showLayer(
|
||||
std::unique_ptr<LayerWidget> layer,
|
||||
LayerOptions options,
|
||||
anim::type animated);
|
||||
void hideLayer(anim::type animated);
|
||||
|
||||
[[nodiscard]] rpl::producer<> backRequests() const;
|
||||
[[nodiscard]] rpl::producer<> closeRequests() const;
|
||||
[[nodiscard]] rpl::producer<> closeEvents() const;
|
||||
void setBackAllowed(bool allowed);
|
||||
|
||||
void updateBackToggled();
|
||||
|
||||
void setMenuAllowed(
|
||||
Fn<void(const Menu::MenuCallback&)> fill,
|
||||
Fn<void(not_null<RpWidget*>, bool fullscreen)> created = nullptr);
|
||||
void setSearchAllowed(
|
||||
rpl::producer<QString> placeholder,
|
||||
Fn<void(std::optional<QString>)> queryChanged);
|
||||
bool closeSearch();
|
||||
|
||||
void overrideTitleColor(std::optional<QColor> color);
|
||||
void overrideBodyColor(std::optional<QColor> color);
|
||||
void overrideBottomBarColor(std::optional<QColor> color);
|
||||
void setBottomBarHeight(int height);
|
||||
[[nodiscard]] style::palette *titleOverridePalette() const;
|
||||
|
||||
base::weak_ptr<Toast::Instance> showToast(Toast::Config &&config);
|
||||
base::weak_ptr<Toast::Instance> showToast(
|
||||
TextWithEntities &&text,
|
||||
crl::time duration = 0);
|
||||
base::weak_ptr<Toast::Instance> showToast(
|
||||
const QString &text,
|
||||
crl::time duration = 0);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<Show> uiShow();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void closeEvent(QCloseEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void focusInEvent(QFocusEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void leaveToChildEvent(QEvent *e, QWidget *child) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
bool eventHook(QEvent *e) override;
|
||||
|
||||
private:
|
||||
class ResizeEdge;
|
||||
class FullScreenButton;
|
||||
|
||||
struct BgColors {
|
||||
QColor title;
|
||||
QColor bg;
|
||||
QColor footer;
|
||||
};
|
||||
|
||||
void initControls();
|
||||
void initLayout(const SeparatePanelArgs &args);
|
||||
void initGeometry(QSize size);
|
||||
void updateGeometry(QSize size);
|
||||
void showControls();
|
||||
void updateControlsGeometry();
|
||||
void updateControlsVisibility(bool fullscreen);
|
||||
void validateBorderImage();
|
||||
[[nodiscard]] QPixmap createBorderImage(QColor color) const;
|
||||
void opacityCallback();
|
||||
void ensureLayerCreated();
|
||||
void destroyLayer();
|
||||
|
||||
void updateTitleGeometry(int newWidth) const;
|
||||
void paintShadowBorder(QPainter &p) const;
|
||||
void paintOpaqueBorder(QPainter &p) const;
|
||||
void paintBodyBg(QPainter &p, int radius = 0) const;
|
||||
|
||||
void toggleOpacityAnimation(bool visible);
|
||||
void finishAnimating();
|
||||
void finishClose();
|
||||
|
||||
void showMenu(Fn<void(const Menu::MenuCallback&)> fill);
|
||||
[[nodiscard]] bool createMenu(not_null<IconButton*> button);
|
||||
|
||||
void createFullScreenButtons();
|
||||
void initFullScreenButton(not_null<QWidget*> button);
|
||||
void updateTitleButtonColors(not_null<IconButton*> button);
|
||||
void updateTitleColors();
|
||||
|
||||
[[nodiscard]] BgColors computeBgColors() const;
|
||||
|
||||
void toggleSearch(bool shown);
|
||||
[[nodiscard]] rpl::producer<> allBackRequests() const;
|
||||
[[nodiscard]] rpl::producer<> allCloseRequests() const;
|
||||
|
||||
const style::PopupMenu &_menuSt;
|
||||
object_ptr<IconButton> _close;
|
||||
object_ptr<IconButton> _menuToggle = { nullptr };
|
||||
Fn<void(not_null<RpWidget*>, bool fullscreen)> _menuToggleCreated;
|
||||
object_ptr<FadeWrapScaled<IconButton>> _searchToggle = { nullptr };
|
||||
rpl::variable<QString> _searchPlaceholder;
|
||||
Fn<void(std::optional<QString>)> _searchQueryChanged;
|
||||
object_ptr<FadeWrap<RpWidget>> _searchWrap = { nullptr };
|
||||
InputField *_searchField = nullptr;
|
||||
object_ptr<FlatLabel> _title = { nullptr };
|
||||
object_ptr<RpWidget> _titleBadge = { nullptr };
|
||||
object_ptr<FadeWrapScaled<IconButton>> _back;
|
||||
object_ptr<RpWidget> _body;
|
||||
base::unique_qptr<RpWidget> _inner;
|
||||
base::unique_qptr<LayerStackWidget> _layer = { nullptr };
|
||||
base::unique_qptr<PopupMenu> _menu;
|
||||
std::vector<std::unique_ptr<ResizeEdge>> _resizeEdges;
|
||||
|
||||
std::unique_ptr<FullScreenButton> _fsClose;
|
||||
std::unique_ptr<FullScreenButton> _fsMenuToggle;
|
||||
std::unique_ptr<FadeWrapScaled<FullScreenButton>> _fsBack;
|
||||
bool _fsAllowChildControls = false;
|
||||
|
||||
rpl::event_stream<> _synteticBackRequests;
|
||||
rpl::event_stream<> _userCloseRequests;
|
||||
rpl::event_stream<> _closeEvents;
|
||||
|
||||
int _titleHeight = 0;
|
||||
bool _allowResize = false;
|
||||
bool _hideOnDeactivate = false;
|
||||
bool _useTransparency = true;
|
||||
bool _backAllowed = false;
|
||||
style::margins _padding;
|
||||
|
||||
bool _dragging = false;
|
||||
QPoint _dragStartMousePosition;
|
||||
QPoint _dragStartMyPosition;
|
||||
|
||||
Animations::Simple _titleLeft;
|
||||
bool _visible = false;
|
||||
rpl::variable<bool> _fullscreen = false;
|
||||
|
||||
Animations::Simple _opacityAnimation;
|
||||
QPixmap _animationCache;
|
||||
QPixmap _borderParts;
|
||||
|
||||
std::optional<QColor> _titleOverrideColor;
|
||||
QPixmap _titleOverrideBorderParts;
|
||||
std::unique_ptr<style::palette> _titleOverridePalette;
|
||||
base::flat_map<
|
||||
not_null<IconButton*>,
|
||||
std::unique_ptr<style::IconButton>> _titleOverrideStyles;
|
||||
|
||||
std::optional<QColor> _bodyOverrideColor;
|
||||
QPixmap _bodyOverrideBorderParts;
|
||||
|
||||
std::optional<QColor> _bottomBarOverrideColor;
|
||||
QPixmap _bottomBarOverrideBorderParts;
|
||||
int _bottomBarHeight = 0;
|
||||
|
||||
Fn<bool(int zorder)> _animationsPaused;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
220
Telegram/lib_ui/ui/widgets/shadow.cpp
Normal file
220
Telegram/lib_ui/ui/widgets/shadow.cpp
Normal file
@@ -0,0 +1,220 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/shadow.h"
|
||||
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/palette.h"
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
struct CustomImage {
|
||||
public:
|
||||
explicit CustomImage(const QImage &image)
|
||||
: _image(image) {
|
||||
}
|
||||
void paint(QPainter &p, int x, int y, int outerw) const {
|
||||
p.drawImage(x, y, _image);
|
||||
}
|
||||
void fill(QPainter &p, QRect rect) const {
|
||||
p.drawImage(rect, _image);
|
||||
}
|
||||
[[nodiscard]] bool empty() const {
|
||||
return _image.isNull();
|
||||
}
|
||||
[[nodiscard]] int width() const {
|
||||
return _image.width() / style::DevicePixelRatio();
|
||||
}
|
||||
[[nodiscard]] int height() const {
|
||||
return _image.height() / style::DevicePixelRatio();
|
||||
}
|
||||
|
||||
private:
|
||||
const QImage &_image;
|
||||
|
||||
};
|
||||
|
||||
struct CustomShadowCorners {
|
||||
const style::icon &left;
|
||||
CustomImage topLeft;
|
||||
const style::icon ⊤
|
||||
CustomImage topRight;
|
||||
const style::icon &right;
|
||||
CustomImage bottomRight;
|
||||
const style::icon ⊥
|
||||
CustomImage bottomLeft;
|
||||
const style::margins &extend;
|
||||
};
|
||||
|
||||
struct CustomShadow {
|
||||
CustomImage left;
|
||||
CustomImage topLeft;
|
||||
CustomImage top;
|
||||
CustomImage topRight;
|
||||
CustomImage right;
|
||||
CustomImage bottomRight;
|
||||
CustomImage bottom;
|
||||
CustomImage bottomLeft;
|
||||
const style::margins &extend;
|
||||
};
|
||||
|
||||
template <typename Shadow>
|
||||
void ShadowPaint(QPainter &p, const QRect &box, int outerWidth, const Shadow &st, RectParts sides) {
|
||||
auto left = (sides & RectPart::Left);
|
||||
auto top = (sides & RectPart::Top);
|
||||
auto right = (sides & RectPart::Right);
|
||||
auto bottom = (sides & RectPart::Bottom);
|
||||
if (left) {
|
||||
auto from = box.y();
|
||||
auto to = from + box.height();
|
||||
if (top && !st.topLeft.empty()) {
|
||||
st.topLeft.paint(p, box.x() - st.extend.left(), box.y() - st.extend.top(), outerWidth);
|
||||
from += st.topLeft.height() - st.extend.top();
|
||||
}
|
||||
if (bottom && !st.bottomLeft.empty()) {
|
||||
st.bottomLeft.paint(p, box.x() - st.extend.left(), box.y() + box.height() + st.extend.bottom() - st.bottomLeft.height(), outerWidth);
|
||||
to -= st.bottomLeft.height() - st.extend.bottom();
|
||||
}
|
||||
if (to > from && !st.left.empty()) {
|
||||
st.left.fill(p, style::rtlrect(box.x() - st.extend.left(), from, st.left.width(), to - from, outerWidth));
|
||||
}
|
||||
}
|
||||
if (right) {
|
||||
auto from = box.y();
|
||||
auto to = from + box.height();
|
||||
if (top && !st.topRight.empty()) {
|
||||
st.topRight.paint(p, box.x() + box.width() + st.extend.right() - st.topRight.width(), box.y() - st.extend.top(), outerWidth);
|
||||
from += st.topRight.height() - st.extend.top();
|
||||
}
|
||||
if (bottom && !st.bottomRight.empty()) {
|
||||
st.bottomRight.paint(p, box.x() + box.width() + st.extend.right() - st.bottomRight.width(), box.y() + box.height() + st.extend.bottom() - st.bottomRight.height(), outerWidth);
|
||||
to -= st.bottomRight.height() - st.extend.bottom();
|
||||
}
|
||||
if (to > from && !st.right.empty()) {
|
||||
st.right.fill(p, style::rtlrect(box.x() + box.width() + st.extend.right() - st.right.width(), from, st.right.width(), to - from, outerWidth));
|
||||
}
|
||||
}
|
||||
if (top && !st.top.empty()) {
|
||||
auto from = box.x();
|
||||
auto to = from + box.width();
|
||||
if (left && !st.topLeft.empty()) from += st.topLeft.width() - st.extend.left();
|
||||
if (right && !st.topRight.empty()) to -= st.topRight.width() - st.extend.right();
|
||||
if (to > from) {
|
||||
st.top.fill(p, style::rtlrect(from, box.y() - st.extend.top(), to - from, st.top.height(), outerWidth));
|
||||
}
|
||||
}
|
||||
if (bottom && !st.bottom.empty()) {
|
||||
auto from = box.x();
|
||||
auto to = from + box.width();
|
||||
if (left && !st.bottomLeft.empty()) from += st.bottomLeft.width() - st.extend.left();
|
||||
if (right && !st.bottomRight.empty()) to -= st.bottomRight.width() - st.extend.right();
|
||||
if (to > from) {
|
||||
st.bottom.fill(p, style::rtlrect(from, box.y() + box.height() + st.extend.bottom() - st.bottom.height(), to - from, st.bottom.height(), outerWidth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PlainShadow::PlainShadow(QWidget *parent)
|
||||
: PlainShadow(parent, st::shadowFg) {
|
||||
}
|
||||
|
||||
PlainShadow::PlainShadow(QWidget *parent, style::color color)
|
||||
: RpWidget(parent)
|
||||
, _color(color) {
|
||||
resize(st::lineWidth, st::lineWidth);
|
||||
}
|
||||
|
||||
void PlainShadow::paintEvent(QPaintEvent *e) {
|
||||
QPainter(this).fillRect(e->rect(), _color);
|
||||
}
|
||||
|
||||
void Shadow::paint(QPainter &p, const QRect &box, int outerWidth, const style::Shadow &st, RectParts sides) {
|
||||
ShadowPaint<style::Shadow>(p, box, outerWidth, st, sides);
|
||||
}
|
||||
|
||||
void Shadow::paint(
|
||||
QPainter &p,
|
||||
const QRect &box,
|
||||
int outerWidth,
|
||||
const style::Shadow &st,
|
||||
const std::array<QImage, 4> &corners,
|
||||
RectParts sides) {
|
||||
const auto shadow = CustomShadowCorners{
|
||||
.left = st.left,
|
||||
.topLeft = CustomImage(corners[0]),
|
||||
.top = st.top,
|
||||
.topRight = CustomImage(corners[2]),
|
||||
.right = st.right,
|
||||
.bottomRight = CustomImage(corners[3]),
|
||||
.bottom = st.bottom,
|
||||
.bottomLeft = CustomImage(corners[1]),
|
||||
.extend = st.extend,
|
||||
};
|
||||
ShadowPaint<CustomShadowCorners>(p, box, outerWidth, shadow, sides);
|
||||
}
|
||||
|
||||
void Shadow::paint(
|
||||
QPainter &p,
|
||||
const QRect &box,
|
||||
int outerWidth,
|
||||
const style::Shadow &st,
|
||||
const std::array<QImage, 4> &sides,
|
||||
const std::array<QImage, 4> &corners) {
|
||||
const auto shadow = CustomShadow{
|
||||
.left = CustomImage(sides[0]),
|
||||
.topLeft = CustomImage(corners[0]),
|
||||
.top = CustomImage(sides[1]),
|
||||
.topRight = CustomImage(corners[2]),
|
||||
.right = CustomImage(sides[2]),
|
||||
.bottomRight = CustomImage(corners[3]),
|
||||
.bottom = CustomImage(sides[3]),
|
||||
.bottomLeft = CustomImage(corners[1]),
|
||||
.extend = st.extend,
|
||||
};
|
||||
ShadowPaint<CustomShadow>(p, box, outerWidth, shadow, RectPart()
|
||||
| (sides[0].isNull() ? RectPart() : RectPart::Left)
|
||||
| (sides[1].isNull() ? RectPart() : RectPart::Top)
|
||||
| (sides[2].isNull() ? RectPart() : RectPart::Right)
|
||||
| (sides[3].isNull() ? RectPart() : RectPart::Bottom));
|
||||
}
|
||||
|
||||
QPixmap Shadow::grab(
|
||||
not_null<RpWidget*> target,
|
||||
const style::Shadow &shadow,
|
||||
RectParts sides) {
|
||||
SendPendingMoveResizeEvents(target);
|
||||
auto rect = target->rect();
|
||||
auto extend = QMargins(
|
||||
(sides & RectPart::Left) ? shadow.extend.left() : 0,
|
||||
(sides & RectPart::Top) ? shadow.extend.top() : 0,
|
||||
(sides & RectPart::Right) ? shadow.extend.right() : 0,
|
||||
(sides & RectPart::Bottom) ? shadow.extend.bottom() : 0
|
||||
);
|
||||
auto full = QRect(0, 0, extend.left() + rect.width() + extend.right(), extend.top() + rect.height() + extend.bottom());
|
||||
auto result = QPixmap(full.size() * style::DevicePixelRatio());
|
||||
result.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
result.fill(Qt::transparent);
|
||||
{
|
||||
QPainter p(&result);
|
||||
Shadow::paint(p, full.marginsRemoved(extend), full.width(), shadow);
|
||||
RenderWidget(p, target, QPoint(extend.left(), extend.top()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Shadow::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
paint(p, rect().marginsRemoved(_st.extend), width(), _st, _sides);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
79
Telegram/lib_ui/ui/widgets/shadow.h
Normal file
79
Telegram/lib_ui/ui/widgets/shadow.h
Normal file
@@ -0,0 +1,79 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/rect_part.h"
|
||||
|
||||
namespace style {
|
||||
struct Shadow;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class PlainShadow : public RpWidget {
|
||||
public:
|
||||
PlainShadow(QWidget *parent);
|
||||
PlainShadow(QWidget *parent, style::color color);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
style::color _color;
|
||||
|
||||
};
|
||||
|
||||
class Shadow : public RpWidget {
|
||||
public:
|
||||
Shadow(
|
||||
QWidget *parent,
|
||||
const style::Shadow &st,
|
||||
RectParts sides = RectPart::AllSides)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _sides(sides) {
|
||||
}
|
||||
|
||||
static void paint(
|
||||
QPainter &p,
|
||||
const QRect &box,
|
||||
int outerWidth,
|
||||
const style::Shadow &st,
|
||||
RectParts sides = RectPart::AllSides);
|
||||
|
||||
static void paint(
|
||||
QPainter &p,
|
||||
const QRect &box,
|
||||
int outerWidth,
|
||||
const style::Shadow &st,
|
||||
const std::array<QImage, 4> &corners,
|
||||
RectParts sides = RectPart::AllSides);
|
||||
|
||||
static void paint(
|
||||
QPainter &p,
|
||||
const QRect &box,
|
||||
int outerWidth,
|
||||
const style::Shadow &st,
|
||||
const std::array<QImage, 4> &sides,
|
||||
const std::array<QImage, 4> &corners);
|
||||
|
||||
static QPixmap grab(
|
||||
not_null<RpWidget*> target,
|
||||
const style::Shadow &shadow,
|
||||
RectParts sides = RectPart::AllSides);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::Shadow &_st;
|
||||
RectParts _sides;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
320
Telegram/lib_ui/ui/widgets/side_bar_button.cpp
Normal file
320
Telegram/lib_ui/ui/widgets/side_bar_button.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/side_bar_button.h"
|
||||
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QtEvents>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxLabelLines = 3;
|
||||
constexpr auto kPremiumLockedOpacity = 0.6;
|
||||
|
||||
} // namespace
|
||||
|
||||
SideBarButton::SideBarButton(
|
||||
not_null<QWidget*> parent,
|
||||
const TextWithEntities &title,
|
||||
const style::SideBarButton &st,
|
||||
Text::MarkedContext context,
|
||||
Fn<bool()> paused)
|
||||
: RippleButton(parent, st.ripple)
|
||||
, _st(st)
|
||||
, _text(_st.minTextWidth)
|
||||
, _paused(paused)
|
||||
, _context(std::move(context)) {
|
||||
_context.repaint = [this] { update(); };
|
||||
_text.setMarkedText(
|
||||
_st.style,
|
||||
title,
|
||||
kMarkupTextOptions,
|
||||
_context);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
style::PaletteChanged(
|
||||
) | rpl::on_next([this] {
|
||||
_iconCache = _iconCacheActive = QImage();
|
||||
_lock.iconCache = _lock.iconCacheActive = QImage();
|
||||
update();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void SideBarButton::setActive(bool active) {
|
||||
if (_active == active) {
|
||||
return;
|
||||
}
|
||||
_active = active;
|
||||
update();
|
||||
}
|
||||
|
||||
void SideBarButton::setBadge(const QString &badge, bool muted) {
|
||||
if (_badge.toString() == badge && _badgeMuted == muted) {
|
||||
return;
|
||||
}
|
||||
_badge.setText(_st.badgeStyle, badge);
|
||||
_badgeMuted = muted;
|
||||
const auto width = badge.isEmpty()
|
||||
? 0
|
||||
: std::max(_st.badgeHeight, _badge.maxWidth() + 2 * _st.badgeSkip);
|
||||
if (_iconCacheBadgeWidth != width) {
|
||||
_iconCacheBadgeWidth = width;
|
||||
_iconCache = _iconCacheActive = QImage();
|
||||
}
|
||||
accessibilityNameChanged();
|
||||
update();
|
||||
}
|
||||
|
||||
void SideBarButton::setIconOverride(
|
||||
const style::icon *iconOverride,
|
||||
const style::icon *iconOverrideActive) {
|
||||
_iconOverride = iconOverride;
|
||||
_iconOverrideActive = iconOverrideActive;
|
||||
update();
|
||||
}
|
||||
|
||||
void SideBarButton::setLocked(bool locked) {
|
||||
if (_lock.locked == locked) {
|
||||
return;
|
||||
}
|
||||
_lock.locked = locked;
|
||||
const auto charFiller = QChar('l');
|
||||
const auto count = std::ceil(st::sideBarButtonLockSize.width()
|
||||
/ float(_st.style.font->width(charFiller)));
|
||||
const auto filler = QString().fill(charFiller, count);
|
||||
auto result = TextWithEntities();
|
||||
if (_lock.locked) {
|
||||
result.append(filler);
|
||||
}
|
||||
const auto len = _text.length();
|
||||
result.append(_text.toTextWithEntities({
|
||||
ushort(_lock.locked ? 0 : count),
|
||||
ushort(len),
|
||||
}));
|
||||
_text.setMarkedText(_st.style, result, kMarkupTextOptions, _context);
|
||||
update();
|
||||
}
|
||||
|
||||
bool SideBarButton::locked() const {
|
||||
return _lock.locked;
|
||||
}
|
||||
|
||||
int SideBarButton::resizeGetHeight(int newWidth) {
|
||||
auto result = _st.minHeight;
|
||||
const auto text = std::min(
|
||||
_text.countHeight(newWidth - _st.textSkip * 2),
|
||||
_st.style.font->height * kMaxLabelLines);
|
||||
const auto add = text - _st.style.font->height;
|
||||
return result + std::max(add, 0);
|
||||
}
|
||||
|
||||
void SideBarButton::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
const auto clip = e->rect();
|
||||
|
||||
const auto &bg = _active ? _st.textBgActive : _st.textBg;
|
||||
p.fillRect(clip, bg);
|
||||
|
||||
RippleButton::paintRipple(p, 0, 0);
|
||||
|
||||
if (_lock.locked) {
|
||||
p.setOpacity(kPremiumLockedOpacity);
|
||||
}
|
||||
|
||||
const auto &icon = computeIcon();
|
||||
const auto x = (_st.iconPosition.x() < 0)
|
||||
? (width() - icon.width()) / 2
|
||||
: _st.iconPosition.x();
|
||||
const auto y = (_st.iconPosition.y() < 0)
|
||||
? (height() - icon.height()) / 2
|
||||
: _st.iconPosition.y();
|
||||
if (_iconCacheBadgeWidth) {
|
||||
validateIconCache();
|
||||
p.drawImage(x, y, _active ? _iconCacheActive : _iconCache);
|
||||
} else {
|
||||
icon.paint(p, x, y, width());
|
||||
}
|
||||
p.setPen(_active ? _st.textFgActive : _st.textFg);
|
||||
_text.draw(p, {
|
||||
.position = { _st.textSkip, _st.textTop },
|
||||
.availableWidth = (width() - 2 * _st.textSkip),
|
||||
.align = style::al_top,
|
||||
.pausedEmoji = _paused && _paused(),
|
||||
.elisionLines = kMaxLabelLines,
|
||||
});
|
||||
|
||||
if (_iconCacheBadgeWidth) {
|
||||
const auto desiredLeft = width() / 2 + _st.badgePosition.x();
|
||||
const auto x = std::min(
|
||||
desiredLeft,
|
||||
width() - _iconCacheBadgeWidth - st::defaultScrollArea.width);
|
||||
const auto y = _st.badgePosition.y();
|
||||
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush((_badgeMuted && !_active)
|
||||
? _st.badgeBgMuted
|
||||
: _st.badgeBg);
|
||||
const auto r = _st.badgeHeight / 2;
|
||||
p.drawRoundedRect(x, y, _iconCacheBadgeWidth, _st.badgeHeight, r, r);
|
||||
|
||||
p.setPen(_st.badgeFg);
|
||||
_badge.draw(
|
||||
p,
|
||||
x + (_iconCacheBadgeWidth - _badge.maxWidth()) / 2,
|
||||
y + (_st.badgeHeight - _st.badgeStyle.font->height) / 2,
|
||||
width());
|
||||
}
|
||||
|
||||
if (_lock.locked) {
|
||||
const auto lineWidths = _text.countLineWidths(
|
||||
width() - 2 * _st.textSkip,
|
||||
{ .reserve = kMaxLabelLines });
|
||||
if (lineWidths.empty()) {
|
||||
return;
|
||||
}
|
||||
validateLockIconCache();
|
||||
|
||||
const auto &icon = _active ? _lock.iconCacheActive : _lock.iconCache;
|
||||
const auto size = icon.size() / style::DevicePixelRatio();
|
||||
p.translate(
|
||||
(width() - lineWidths.front()) / 2.,
|
||||
_st.textTop + (_st.style.font->height - size.height()) / 2.);
|
||||
p.setOpacity(1.);
|
||||
p.fillRect(QRect(QPoint(), size), bg);
|
||||
p.setOpacity(kPremiumLockedOpacity);
|
||||
p.translate(-_st.style.font->spacew / 2., 0);
|
||||
|
||||
p.drawImage(0, 0, icon);
|
||||
}
|
||||
}
|
||||
|
||||
const style::icon &SideBarButton::computeIcon() const {
|
||||
return _active
|
||||
? (_iconOverrideActive
|
||||
? *_iconOverrideActive
|
||||
: !_st.iconActive.empty()
|
||||
? _st.iconActive
|
||||
: _iconOverride
|
||||
? *_iconOverride
|
||||
: _st.icon)
|
||||
: _iconOverride
|
||||
? *_iconOverride
|
||||
: _st.icon;
|
||||
}
|
||||
|
||||
void SideBarButton::validateIconCache() {
|
||||
Expects(_st.iconPosition.x() < 0);
|
||||
|
||||
if (!(_active ? _iconCacheActive : _iconCache).isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto &icon = computeIcon();
|
||||
auto image = QImage(
|
||||
icon.size() * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
icon.paint(p, 0, 0, icon.width());
|
||||
p.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
p.setBrush(Qt::transparent);
|
||||
auto pen = QPen(Qt::transparent);
|
||||
pen.setWidth(2 * _st.badgeStroke);
|
||||
p.setPen(pen);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto desiredLeft = (icon.width() / 2) + _st.badgePosition.x();
|
||||
const auto x = std::min(
|
||||
desiredLeft,
|
||||
(width()
|
||||
- _iconCacheBadgeWidth
|
||||
- st::defaultScrollArea.width
|
||||
- (width() / 2)
|
||||
+ (icon.width() / 2)));
|
||||
const auto top = (_st.iconPosition.y() >= 0)
|
||||
? _st.iconPosition.y()
|
||||
: (height() - icon.height()) / 2;
|
||||
const auto y = _st.badgePosition.y() - top;
|
||||
const auto r = _st.badgeHeight / 2.;
|
||||
p.drawRoundedRect(x, y, _iconCacheBadgeWidth, _st.badgeHeight, r, r);
|
||||
}
|
||||
(_active ? _iconCacheActive : _iconCache) = std::move(image);
|
||||
}
|
||||
|
||||
void SideBarButton::validateLockIconCache() {
|
||||
if (!(_active ? _lock.iconCacheActive : _lock.iconCache).isNull()) {
|
||||
return;
|
||||
}
|
||||
(_active ? _lock.iconCacheActive : _lock.iconCache)
|
||||
= SideBarLockIcon(_st.textFg);
|
||||
}
|
||||
|
||||
QImage SideBarLockIcon(const style::color &fg) {
|
||||
const auto &size = st::sideBarButtonLockSize;
|
||||
const auto arcPen = QPen(
|
||||
fg,
|
||||
// Use a divider to get 1.5.
|
||||
st::sideBarButtonLockPenWidth
|
||||
/ float64(st::sideBarButtonLockPenWidthDivider),
|
||||
Qt::SolidLine,
|
||||
Qt::SquareCap,
|
||||
Qt::RoundJoin);
|
||||
auto image = QImage(
|
||||
size * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
image.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
const auto &arcOffset = st::sideBarButtonLockArcOffset;
|
||||
const auto arcWidth = size.width() - arcOffset * 2;
|
||||
const auto &arcHeight = st::sideBarButtonLockArcHeight;
|
||||
|
||||
const auto blockRectWidth = size.width();
|
||||
const auto blockRectHeight = st::sideBarButtonLockBlockHeight;
|
||||
const auto blockRectTop = size.height() - blockRectHeight;
|
||||
|
||||
const auto blockRect = QRectF(
|
||||
(size.width() - blockRectWidth) / 2,
|
||||
blockRectTop,
|
||||
blockRectWidth,
|
||||
blockRectHeight);
|
||||
const auto lineHeight = -(blockRect.y() - arcHeight)
|
||||
+ arcPen.width() / 2.;
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(fg);
|
||||
{
|
||||
p.drawRoundedRect(blockRect, 2, 2);
|
||||
}
|
||||
|
||||
p.translate(size.width() - arcOffset, blockRect.y());
|
||||
|
||||
p.setPen(arcPen);
|
||||
const auto rLine = QLineF(0, 0, 0, lineHeight);
|
||||
const auto lLine = rLine.translated(-arcWidth, 0);
|
||||
p.drawLine(rLine);
|
||||
p.drawLine(lLine);
|
||||
|
||||
p.drawArc(
|
||||
-arcWidth,
|
||||
-arcHeight - arcPen.width() / 2.,
|
||||
arcWidth,
|
||||
arcHeight * 2,
|
||||
0,
|
||||
180 * 16);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
138
Telegram/lib_ui/ui/widgets/side_bar_button.h
Normal file
138
Telegram/lib_ui/ui/widgets/side_bar_button.h
Normal file
@@ -0,0 +1,138 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/text/text.h"
|
||||
|
||||
namespace style {
|
||||
struct SideBarButton;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RippleAnimation;
|
||||
|
||||
[[nodiscard]] QImage SideBarLockIcon(const style::color &fg);
|
||||
|
||||
class SideBarButton final : public Ui::RippleButton {
|
||||
public:
|
||||
SideBarButton(
|
||||
not_null<QWidget*> parent,
|
||||
const TextWithEntities &title,
|
||||
const style::SideBarButton &st,
|
||||
Text::MarkedContext context = {},
|
||||
Fn<bool()> paused = nullptr);
|
||||
|
||||
void setActive(bool active);
|
||||
void setBadge(const QString &badge, bool muted);
|
||||
void setIconOverride(
|
||||
const style::icon *iconOverride,
|
||||
const style::icon *iconOverrideActive = nullptr);
|
||||
void setLocked(bool locked);
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
[[nodiscard]] bool locked() const;
|
||||
|
||||
QString accessibilityName() override {
|
||||
return !_badge.isEmpty()
|
||||
? u"%1 (%2)"_q.arg(_text.toString(), _badge.toString())
|
||||
: _text.toString();
|
||||
}
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
[[nodiscard]] const style::icon &computeIcon() const;
|
||||
void validateIconCache();
|
||||
void validateLockIconCache();
|
||||
|
||||
const style::SideBarButton &_st;
|
||||
const style::icon *_iconOverride = nullptr;
|
||||
const style::icon *_iconOverrideActive = nullptr;
|
||||
Ui::Text::String _text;
|
||||
Ui::Text::String _badge;
|
||||
QImage _iconCache;
|
||||
QImage _iconCacheActive;
|
||||
int _iconCacheBadgeWidth = 0;
|
||||
bool _active = false;
|
||||
bool _badgeMuted = false;
|
||||
|
||||
Fn<bool()> _paused;
|
||||
Text::MarkedContext _context;
|
||||
|
||||
struct {
|
||||
bool locked = false;
|
||||
QImage iconCache;
|
||||
QImage iconCacheActive;
|
||||
} _lock;
|
||||
|
||||
};
|
||||
//
|
||||
//class SideBarMenu final {
|
||||
//public:
|
||||
// struct Item {
|
||||
// QString id;
|
||||
// QString title;
|
||||
// QString badge;
|
||||
// not_null<const style::icon*> icon;
|
||||
// not_null<const style::icon*> iconActive;
|
||||
// int iconTop = 0;
|
||||
// };
|
||||
//
|
||||
// SideBarMenu(not_null<QWidget*> parent, const style::SideBarMenu &st);
|
||||
// ~SideBarMenu();
|
||||
//
|
||||
// [[nodiscard]] not_null<const Ui::RpWidget*> widget() const;
|
||||
//
|
||||
// void setGeometry(QRect geometry);
|
||||
// void setItems(std::vector<Item> items);
|
||||
// void setActive(
|
||||
// const QString &id,
|
||||
// anim::type animated = anim::type::normal);
|
||||
// [[nodiscard]] rpl::producer<QString> activateRequests() const;
|
||||
//
|
||||
// [[nodiscard]] rpl::lifetime &lifetime();
|
||||
//
|
||||
//private:
|
||||
// struct MenuItem {
|
||||
// Item data;
|
||||
// Ui::Text::String text;
|
||||
// mutable std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
// int top = 0;
|
||||
// int height = 0;
|
||||
// };
|
||||
// void setup();
|
||||
// void paint(Painter &p, QRect clip) const;
|
||||
// [[nodiscard]] int countContentHeight(int width, int outerHeight);
|
||||
//
|
||||
// void mouseMove(QPoint position);
|
||||
// void mousePress(Qt::MouseButton button);
|
||||
// void mouseRelease(Qt::MouseButton button);
|
||||
//
|
||||
// void setSelected(int selected);
|
||||
// void setPressed(int pressed);
|
||||
// void addRipple(MenuItem &item, QPoint position);
|
||||
// void repaint(const QString &id);
|
||||
// [[nodiscard]] MenuItem *itemById(const QString &id);
|
||||
//
|
||||
// const style::SideBarMenu &_st;
|
||||
//
|
||||
// Ui::RpWidget _outer;
|
||||
// const not_null<Ui::ScrollArea*> _scroll;
|
||||
// const not_null<Ui::RpWidget*> _inner;
|
||||
// std::vector<MenuItem> _items;
|
||||
// int _selected = -1;
|
||||
// int _pressed = -1;
|
||||
//
|
||||
// QString _activeId;
|
||||
// rpl::event_stream<QString> _activateRequests;
|
||||
//
|
||||
//};
|
||||
|
||||
} // namespace Ui
|
||||
376
Telegram/lib_ui/ui/widgets/time_input.cpp
Normal file
376
Telegram/lib_ui/ui/widgets/time_input.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/time_input.h"
|
||||
|
||||
#include "ui/widgets/fields/time_part_input.h"
|
||||
#include "base/qt/qt_string_view.h"
|
||||
#include "base/invoke_queued.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <QTime>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
QTime ValidateTime(const QString &value) {
|
||||
static const auto RegExp = QRegularExpression(
|
||||
"^(\\d{1,2})\\:(\\d\\d)$");
|
||||
const auto match = RegExp.match(value);
|
||||
if (!match.hasMatch()) {
|
||||
return QTime();
|
||||
}
|
||||
const auto readInt = [](const QString &value) {
|
||||
auto view = QStringView(value);
|
||||
while (!view.isEmpty() && view.at(0) == '0') {
|
||||
view = base::StringViewMid(view, 1);
|
||||
}
|
||||
return view.toInt();
|
||||
};
|
||||
return QTime(readInt(match.captured(1)), readInt(match.captured(2)));
|
||||
}
|
||||
|
||||
QString GetHour(const QString &value) {
|
||||
if (const auto time = ValidateTime(value); time.isValid()) {
|
||||
return QString::number(time.hour());
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString GetMinute(const QString &value) {
|
||||
if (const auto time = ValidateTime(value); time.isValid()) {
|
||||
return QString("%1").arg(time.minute(), 2, 10, QChar('0'));
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TimeInput::TimeInput(
|
||||
QWidget *parent,
|
||||
const QString &value,
|
||||
const style::InputField &stField,
|
||||
const style::InputField &stDateField,
|
||||
const style::FlatLabel &stSeparator,
|
||||
const style::margins &stSeparatorPadding)
|
||||
: RpWidget(parent)
|
||||
, _stField(stField)
|
||||
, _stDateField(stDateField)
|
||||
, _stSeparator(stSeparator)
|
||||
, _stSeparatorPadding(stSeparatorPadding)
|
||||
, _hour(
|
||||
this,
|
||||
_stField,
|
||||
rpl::never<QString>(),
|
||||
GetHour(value))
|
||||
, _separator1(
|
||||
this,
|
||||
object_ptr<FlatLabel>(this, u":"_q, _stSeparator),
|
||||
_stSeparatorPadding)
|
||||
, _minute(
|
||||
this,
|
||||
_stField,
|
||||
rpl::never<QString>(),
|
||||
GetMinute(value))
|
||||
, _value(valueCurrent()) {
|
||||
const auto focused = [=](const object_ptr<TimePart> &field) {
|
||||
return [this, pointer = base::make_weak(field.data())]{
|
||||
_borderAnimationStart = pointer->borderAnimationStart()
|
||||
+ pointer->x()
|
||||
- _hour->x();
|
||||
setFocused(true);
|
||||
_focuses.fire({});
|
||||
};
|
||||
};
|
||||
const auto blurred = [=] {
|
||||
setFocused(false);
|
||||
};
|
||||
const auto changed = [=] {
|
||||
_value = valueCurrent();
|
||||
};
|
||||
connect(_hour, &MaskedInputField::focused, focused(_hour));
|
||||
connect(_minute, &MaskedInputField::focused, focused(_minute));
|
||||
connect(_hour, &MaskedInputField::blurred, blurred);
|
||||
connect(_minute, &MaskedInputField::blurred, blurred);
|
||||
connect(_hour, &MaskedInputField::changed, changed);
|
||||
connect(_minute, &MaskedInputField::changed, changed);
|
||||
_hour->setMaxValue(23);
|
||||
_hour->setWheelStep(1);
|
||||
_hour->putNext() | rpl::on_next([=](QChar ch) {
|
||||
putNext(_minute, ch);
|
||||
}, lifetime());
|
||||
_minute->setMaxValue(59);
|
||||
_minute->setWheelStep(10);
|
||||
_minute->erasePrevious() | rpl::on_next([=] {
|
||||
erasePrevious(_hour);
|
||||
}, lifetime());
|
||||
_minute->jumpToPrevious() | rpl::on_next([=] {
|
||||
_hour->setCursorPosition(_hour->getLastText().size());
|
||||
_hour->setFocus();
|
||||
}, lifetime());
|
||||
_separator1->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
setMouseTracking(true);
|
||||
|
||||
_value.changes(
|
||||
) | rpl::on_next([=] {
|
||||
setErrorShown(false);
|
||||
}, lifetime());
|
||||
|
||||
const auto submitHour = [=] {
|
||||
if (hour().has_value()) {
|
||||
_minute->setFocus();
|
||||
}
|
||||
};
|
||||
const auto submitMinute = [=] {
|
||||
if (minute().has_value()) {
|
||||
if (hour().has_value()) {
|
||||
_submitRequests.fire({});
|
||||
} else {
|
||||
_hour->setFocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
connect(
|
||||
_hour,
|
||||
&MaskedInputField::submitted,
|
||||
submitHour);
|
||||
connect(
|
||||
_minute,
|
||||
&MaskedInputField::submitted,
|
||||
submitMinute);
|
||||
}
|
||||
|
||||
void TimeInput::putNext(const object_ptr<TimePart> &field, QChar ch) {
|
||||
field->setCursorPosition(0);
|
||||
if (ch.unicode()) {
|
||||
field->setText(ch + field->getLastText());
|
||||
field->setCursorPosition(1);
|
||||
}
|
||||
field->onTextEdited();
|
||||
setFocusQueued(field);
|
||||
}
|
||||
|
||||
void TimeInput::erasePrevious(const object_ptr<TimePart> &field) {
|
||||
const auto text = field->getLastText();
|
||||
if (!text.isEmpty()) {
|
||||
field->setCursorPosition(text.size() - 1);
|
||||
field->setText(text.mid(0, text.size() - 1));
|
||||
}
|
||||
setFocusQueued(field);
|
||||
}
|
||||
|
||||
void TimeInput::setFocusQueued(const object_ptr<TimePart> &field) {
|
||||
// There was a "Stack Overflow" crash in some input method handling.
|
||||
//
|
||||
// See https://github.com/telegramdesktop/tdesktop/issues/25129
|
||||
//
|
||||
// The stack is something like:
|
||||
//
|
||||
// ...
|
||||
// QApplicationPrivate::sendMouseEvent
|
||||
// ----
|
||||
// QWidget::setFocus
|
||||
// QWindow::focusObjectChanged
|
||||
// QWindowsInputContext::setFocusObject
|
||||
// QWindowsInputContext::reset
|
||||
// QLineEdit::inputMethodEvent
|
||||
// QWidgetLineControl::finishChange
|
||||
// QLineEdit::textEdited
|
||||
// MaskedInputField::onTextEdited
|
||||
// TimePart::correctValue
|
||||
// TimeInput::putNext
|
||||
// ----
|
||||
// QWidget::setFocus
|
||||
// QWindow::focusObjectChanged
|
||||
// ...
|
||||
//
|
||||
// So we try to break this loop by focusing the widget async.
|
||||
const auto raw = field.data();
|
||||
InvokeQueued(raw, [raw] { raw->setFocus(); });
|
||||
}
|
||||
|
||||
bool TimeInput::setFocusFast() {
|
||||
if (hour().has_value()) {
|
||||
_minute->setFocusFast();
|
||||
} else {
|
||||
_hour->setFocusFast();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<int> TimeInput::hour() const {
|
||||
return _hour->number();
|
||||
}
|
||||
|
||||
std::optional<int> TimeInput::minute() const {
|
||||
return _minute->number();
|
||||
}
|
||||
|
||||
QString TimeInput::valueCurrent() const {
|
||||
const auto result = QString("%1:%2"
|
||||
).arg(hour().value_or(0)
|
||||
).arg(minute().value_or(0), 2, 10, QChar('0'));
|
||||
return ValidateTime(result).isValid() ? result : QString();
|
||||
}
|
||||
|
||||
rpl::producer<QString> TimeInput::value() const {
|
||||
return _value.value();
|
||||
}
|
||||
|
||||
rpl::producer<> TimeInput::submitRequests() const {
|
||||
return _submitRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> TimeInput::focuses() const {
|
||||
return _focuses.events();
|
||||
}
|
||||
|
||||
void TimeInput::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto &_st = _stDateField;
|
||||
const auto height = _st.heightMin;
|
||||
if (_st.border) {
|
||||
p.fillRect(0, height - _st.border, width(), _st.border, _st.borderFg);
|
||||
}
|
||||
auto errorDegree = _a_error.value(_error ? 1. : 0.);
|
||||
auto borderShownDegree = _a_borderShown.value(1.);
|
||||
auto borderOpacity = _a_borderOpacity.value(_borderVisible ? 1. : 0.);
|
||||
if (_st.borderActive && (borderOpacity > 0.)) {
|
||||
auto borderStart = std::clamp(_borderAnimationStart, 0, width());
|
||||
auto borderFrom = qRound(borderStart * (1. - borderShownDegree));
|
||||
auto borderTo = borderStart + qRound((width() - borderStart) * borderShownDegree);
|
||||
if (borderTo > borderFrom) {
|
||||
auto borderFg = anim::brush(_st.borderFgActive, _st.borderFgError, errorDegree);
|
||||
p.setOpacity(borderOpacity);
|
||||
p.fillRect(borderFrom, height - _st.borderActive, borderTo - borderFrom, _st.borderActive, borderFg);
|
||||
p.setOpacity(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Widget>
|
||||
bool TimeInput::insideSeparator(QPoint position, const Widget &widget) const {
|
||||
const auto x = position.x();
|
||||
const auto y = position.y();
|
||||
return (x >= widget->x() && x < widget->x() + widget->width())
|
||||
&& (y >= _hour->y() && y < _hour->y() + _hour->height());
|
||||
}
|
||||
|
||||
void TimeInput::mouseMoveEvent(QMouseEvent *e) {
|
||||
const auto cursor = insideSeparator(e->pos(), _separator1)
|
||||
? style::cur_text
|
||||
: style::cur_default;
|
||||
if (_cursor != cursor) {
|
||||
_cursor = cursor;
|
||||
setCursor(_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
void TimeInput::mousePressEvent(QMouseEvent *e) {
|
||||
const auto x = e->pos().x();
|
||||
const auto focus1 = [&] {
|
||||
if (_hour->getLastText().size() > 1) {
|
||||
_minute->setFocus();
|
||||
} else {
|
||||
_hour->setFocus();
|
||||
}
|
||||
};
|
||||
if (insideSeparator(e->pos(), _separator1)) {
|
||||
focus1();
|
||||
_borderAnimationStart = x - _hour->x();
|
||||
}
|
||||
}
|
||||
|
||||
int TimeInput::resizeGetHeight(int width) {
|
||||
const auto &_st = _stField;
|
||||
const auto &font = _st.placeholderFont;
|
||||
const auto addToWidth = _stSeparatorPadding.left();
|
||||
const auto hourWidth = _st.textMargins.left()
|
||||
+ _st.placeholderMargins.left()
|
||||
+ font->width(QString("23"))
|
||||
+ _st.placeholderMargins.right()
|
||||
+ _st.textMargins.right()
|
||||
+ addToWidth;
|
||||
const auto minuteWidth = _st.textMargins.left()
|
||||
+ _st.placeholderMargins.left()
|
||||
+ font->width(QString("59"))
|
||||
+ _st.placeholderMargins.right()
|
||||
+ _st.textMargins.right()
|
||||
+ addToWidth;
|
||||
const auto full = hourWidth
|
||||
- addToWidth
|
||||
+ _separator1->width()
|
||||
+ minuteWidth
|
||||
- addToWidth;
|
||||
auto left = (width - full) / 2;
|
||||
auto top = 0;
|
||||
_hour->setGeometry(left, top, hourWidth, _hour->height());
|
||||
left += hourWidth - addToWidth;
|
||||
_separator1->resizeToNaturalWidth(width);
|
||||
_separator1->move(left, top);
|
||||
left += _separator1->width();
|
||||
_minute->setGeometry(left, top, minuteWidth, _minute->height());
|
||||
return _stDateField.heightMin;
|
||||
}
|
||||
|
||||
void TimeInput::showError() {
|
||||
setErrorShown(true);
|
||||
if (!_focused) {
|
||||
setInnerFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void TimeInput::setInnerFocus() {
|
||||
if (hour().has_value()) {
|
||||
_minute->setFocus();
|
||||
} else {
|
||||
_hour->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void TimeInput::setErrorShown(bool error) {
|
||||
if (_error != error) {
|
||||
_error = error;
|
||||
_a_error.start(
|
||||
[=] { update(); },
|
||||
_error ? 0. : 1.,
|
||||
_error ? 1. : 0.,
|
||||
_stDateField.duration);
|
||||
startBorderAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void TimeInput::setFocused(bool focused) {
|
||||
if (_focused != focused) {
|
||||
_focused = focused;
|
||||
_a_focused.start(
|
||||
[=] { update(); },
|
||||
_focused ? 0. : 1.,
|
||||
_focused ? 1. : 0.,
|
||||
_stDateField.duration);
|
||||
startBorderAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void TimeInput::startBorderAnimation() {
|
||||
auto borderVisible = (_error || _focused);
|
||||
if (_borderVisible != borderVisible) {
|
||||
_borderVisible = borderVisible;
|
||||
const auto duration = _stDateField.duration;
|
||||
if (_borderVisible) {
|
||||
if (_a_borderOpacity.animating()) {
|
||||
_a_borderOpacity.start([=] { update(); }, 0., 1., duration);
|
||||
} else {
|
||||
_a_borderShown.start([=] { update(); }, 0., 1., duration);
|
||||
}
|
||||
} else {
|
||||
_a_borderOpacity.start([=] { update(); }, 1., 0., duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
81
Telegram/lib_ui/ui/widgets/time_input.h
Normal file
81
Telegram/lib_ui/ui/widgets/time_input.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class TimePart;
|
||||
|
||||
class TimeInput final : public RpWidget {
|
||||
public:
|
||||
TimeInput(
|
||||
QWidget *parent,
|
||||
const QString &value,
|
||||
const style::InputField &stField,
|
||||
const style::InputField &stDateField,
|
||||
const style::FlatLabel &stSeparator,
|
||||
const style::margins &stSeparatorPadding);
|
||||
|
||||
bool setFocusFast();
|
||||
[[nodiscard]] rpl::producer<QString> value() const;
|
||||
[[nodiscard]] rpl::producer<> submitRequests() const;
|
||||
[[nodiscard]] rpl::producer<> focuses() const;
|
||||
[[nodiscard]] QString valueCurrent() const;
|
||||
void showError();
|
||||
|
||||
int resizeGetHeight(int width) override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
void setInnerFocus();
|
||||
void putNext(const object_ptr<TimePart> &field, QChar ch);
|
||||
void erasePrevious(const object_ptr<TimePart> &field);
|
||||
void setFocusQueued(const object_ptr<TimePart> &field);
|
||||
void setErrorShown(bool error);
|
||||
void setFocused(bool focused);
|
||||
void startBorderAnimation();
|
||||
template <typename Widget>
|
||||
bool insideSeparator(QPoint position, const Widget &widget) const;
|
||||
|
||||
[[nodiscard]] std::optional<int> hour() const;
|
||||
[[nodiscard]] std::optional<int> minute() const;
|
||||
|
||||
const style::InputField &_stField;
|
||||
const style::InputField &_stDateField;
|
||||
const style::FlatLabel &_stSeparator;
|
||||
const style::margins &_stSeparatorPadding;
|
||||
|
||||
object_ptr<TimePart> _hour;
|
||||
object_ptr<PaddingWrap<FlatLabel>> _separator1;
|
||||
object_ptr<TimePart> _minute;
|
||||
rpl::variable<QString> _value;
|
||||
rpl::event_stream<> _submitRequests;
|
||||
rpl::event_stream<> _focuses;
|
||||
|
||||
style::cursor _cursor = style::cur_default;
|
||||
Animations::Simple _a_borderShown;
|
||||
int _borderAnimationStart = 0;
|
||||
Animations::Simple _a_borderOpacity;
|
||||
bool _borderVisible = false;
|
||||
|
||||
Animations::Simple _a_error;
|
||||
bool _error = false;
|
||||
Animations::Simple _a_focused;
|
||||
bool _focused = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
487
Telegram/lib_ui/ui/widgets/tooltip.cpp
Normal file
487
Telegram/lib_ui/ui/widgets/tooltip.cpp
Normal file
@@ -0,0 +1,487 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#include "ui/widgets/tooltip.h"
|
||||
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/platform/ui_platform_utility.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "base/invoke_queued.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include <QtGui/QScreen>
|
||||
#include <QtGui/QWindow>
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
Tooltip *TooltipInstance = nullptr;
|
||||
|
||||
const style::Tooltip *AbstractTooltipShower::tooltipSt() const {
|
||||
return &st::defaultTooltip;
|
||||
}
|
||||
|
||||
AbstractTooltipShower::~AbstractTooltipShower() {
|
||||
if (TooltipInstance && TooltipInstance->_shower == this) {
|
||||
TooltipInstance->_shower = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip::Tooltip() : RpWidget(nullptr) {
|
||||
TooltipInstance = this;
|
||||
|
||||
setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::ToolTip);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
|
||||
_showTimer.setCallback([=] { performShow(); });
|
||||
_hideByLeaveTimer.setCallback([=] { Hide(); });
|
||||
}
|
||||
|
||||
void Tooltip::performShow() {
|
||||
if (_shower) {
|
||||
auto text = _shower->tooltipWindowActive()
|
||||
? _shower->tooltipText()
|
||||
: QString();
|
||||
if (text.isEmpty()) {
|
||||
Hide();
|
||||
} else {
|
||||
TooltipInstance->popup(_shower->tooltipPos(), text, _shower->tooltipSt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Tooltip::eventFilter(QObject *o, QEvent *e) {
|
||||
if (e->type() == QEvent::Leave) {
|
||||
_hideByLeaveTimer.callOnce(10);
|
||||
} else if (e->type() == QEvent::Enter) {
|
||||
_hideByLeaveTimer.cancel();
|
||||
} else if (e->type() == QEvent::MouseMove) {
|
||||
if ((QCursor::pos() - _point).manhattanLength() > QApplication::startDragDistance()) {
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
return RpWidget::eventFilter(o, e);
|
||||
}
|
||||
|
||||
Tooltip::~Tooltip() {
|
||||
if (TooltipInstance == this) {
|
||||
TooltipInstance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip *st) {
|
||||
if (!_isEventFilter) {
|
||||
_isEventFilter = true;
|
||||
QCoreApplication::instance()->installEventFilter(this);
|
||||
}
|
||||
|
||||
_point = m;
|
||||
_st = st;
|
||||
_text = Text::String(_st->textStyle, text, kPlainTextOptions, _st->widthMax);
|
||||
accessibilityNameChanged();
|
||||
|
||||
_useTransparency = Platform::TranslucentWindowsSupported();
|
||||
setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
|
||||
|
||||
int32 addw = 2 * st::lineWidth + _st->textPadding.left() + _st->textPadding.right();
|
||||
int32 addh = 2 * st::lineWidth + _st->textPadding.top() + _st->textPadding.bottom();
|
||||
|
||||
// count tooltip size
|
||||
QSize s(addw + _text.maxWidth(), addh + _text.minHeight());
|
||||
if (s.width() > _st->widthMax) {
|
||||
s.setWidth(addw + _text.countWidth(_st->widthMax - addw));
|
||||
s.setHeight(addh + _text.countHeight(s.width() - addw));
|
||||
}
|
||||
int32 maxh = addh + (_st->linesMax * _st->textStyle.font->height);
|
||||
if (s.height() > maxh) {
|
||||
s.setHeight(maxh);
|
||||
}
|
||||
|
||||
// count tooltip position
|
||||
QPoint p(m + _st->shift);
|
||||
if (style::RightToLeft()) {
|
||||
p.setX(m.x() - s.width() - _st->shift.x());
|
||||
}
|
||||
if (s.width() < 2 * _st->shift.x()) {
|
||||
p.setX(m.x() - (s.width() / 2));
|
||||
}
|
||||
|
||||
const auto screen = QGuiApplication::screenAt(m);
|
||||
if (screen) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && !defined(Q_OS_MAC)
|
||||
setScreen(screen);
|
||||
#else // Qt >= 6.0.0
|
||||
createWinId();
|
||||
windowHandle()->setScreen(screen);
|
||||
#endif // Qt < 6.0.0
|
||||
|
||||
// adjust tooltip position
|
||||
const auto r = screen->availableGeometry();
|
||||
if (r.x() + r.width() - _st->skip < p.x() + s.width() && p.x() + s.width() > m.x()) {
|
||||
p.setX(qMax(r.x() + r.width() - int32(_st->skip) - s.width(), m.x() - s.width()));
|
||||
}
|
||||
if (r.x() + _st->skip > p.x() && p.x() < m.x()) {
|
||||
p.setX(qMin(m.x(), r.x() + int32(_st->skip)));
|
||||
}
|
||||
if (r.y() + r.height() - _st->skip < p.y() + s.height()) {
|
||||
p.setY(m.y() - s.height() - _st->skip);
|
||||
}
|
||||
if (r.y() > p.x()) {
|
||||
p.setY(qMin(m.y() + _st->shift.y(), r.y() + r.height() - s.height()));
|
||||
}
|
||||
}
|
||||
|
||||
move(p);
|
||||
setFixedSize(s);
|
||||
|
||||
_hideByLeaveTimer.cancel();
|
||||
show();
|
||||
}
|
||||
|
||||
void Tooltip::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
if (_useTransparency) {
|
||||
p.setPen(_st->textBorder);
|
||||
p.setBrush(_st->textBg);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawRoundedRect(QRectF(0.5, 0.5, width() - 1., height() - 1.), st::roundRadiusSmall, st::roundRadiusSmall);
|
||||
} else {
|
||||
p.fillRect(rect(), _st->textBg);
|
||||
|
||||
p.fillRect(QRect(0, 0, width(), st::lineWidth), _st->textBorder);
|
||||
p.fillRect(QRect(0, height() - st::lineWidth, width(), st::lineWidth), _st->textBorder);
|
||||
p.fillRect(QRect(0, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
|
||||
p.fillRect(QRect(width() - st::lineWidth, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
|
||||
}
|
||||
const auto lines = (height() - 2 * st::lineWidth - _st->textPadding.top() - _st->textPadding.bottom())
|
||||
/ _st->textStyle.font->height;
|
||||
|
||||
p.setPen(_st->textFg);
|
||||
_text.drawElided(p, st::lineWidth + _st->textPadding.left(), st::lineWidth + _st->textPadding.top(), width() - 2 * st::lineWidth - _st->textPadding.left() - _st->textPadding.right(), lines);
|
||||
}
|
||||
|
||||
void Tooltip::hideEvent(QHideEvent *e) {
|
||||
if (TooltipInstance == this) {
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
void Tooltip::Show(int32 delay, const AbstractTooltipShower *shower) {
|
||||
if (!TooltipInstance) {
|
||||
new Tooltip();
|
||||
}
|
||||
TooltipInstance->_shower = shower;
|
||||
if (delay >= 0) {
|
||||
TooltipInstance->_showTimer.callOnce(delay);
|
||||
} else {
|
||||
TooltipInstance->performShow();
|
||||
}
|
||||
}
|
||||
|
||||
void Tooltip::Hide() {
|
||||
if (auto instance = TooltipInstance) {
|
||||
TooltipInstance = nullptr;
|
||||
instance->_showTimer.cancel();
|
||||
instance->_hideByLeaveTimer.cancel();
|
||||
instance->hide();
|
||||
InvokeQueued(instance, [=] { instance->deleteLater(); });
|
||||
}
|
||||
}
|
||||
|
||||
ImportantTooltip::ImportantTooltip(
|
||||
QWidget *parent,
|
||||
object_ptr<RpWidget> content,
|
||||
const style::ImportantTooltip &st)
|
||||
: RpWidget(parent)
|
||||
, _st(st)
|
||||
, _content(std::move(content)) {
|
||||
_content->setParent(this);
|
||||
_hideTimer.setCallback([this] { toggleAnimated(false); });
|
||||
hide();
|
||||
|
||||
_content->widthValue(
|
||||
) | rpl::on_next([=] {
|
||||
resizeToContent();
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void ImportantTooltip::pointAt(
|
||||
QRect area,
|
||||
RectParts side,
|
||||
Fn<QPoint(QSize)> countPosition) {
|
||||
if (_area == area
|
||||
&& _side == side
|
||||
&& !_countPosition
|
||||
&& !countPosition) {
|
||||
return;
|
||||
}
|
||||
_countPosition = std::move(countPosition);
|
||||
_area = area;
|
||||
countApproachSide(side);
|
||||
resizeToContent();
|
||||
update();
|
||||
}
|
||||
|
||||
void ImportantTooltip::resizeToContent() {
|
||||
auto size = _content->rect().marginsAdded(_st.padding).size();
|
||||
size.setHeight(size.height() + _st.arrow);
|
||||
if (size.width() < 2 * (_st.arrowSkipMin + _st.arrow)) {
|
||||
size.setWidth(2 * (_st.arrowSkipMin + _st.arrow));
|
||||
}
|
||||
if (_side & RectPart::Right) {
|
||||
size.setWidth(size.width() + _st.arrow);
|
||||
}
|
||||
setFixedSize(size);
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void ImportantTooltip::countApproachSide(RectParts preferSide) {
|
||||
Expects(parentWidget() != nullptr);
|
||||
|
||||
auto requiredSpace = countInner().height() + _st.shift + _st.arrow;
|
||||
auto available = parentWidget()->rect();
|
||||
auto availableAbove = _area.y() - available.y();
|
||||
auto availableBelow = (available.y() + available.height()) - (_area.y() + _area.height());
|
||||
auto allowedAbove = (availableAbove >= requiredSpace + _st.margin.top());
|
||||
auto allowedBelow = (availableBelow >= requiredSpace + _st.margin.bottom());
|
||||
if ((allowedAbove && allowedBelow) || (!allowedAbove && !allowedBelow)) {
|
||||
_side = preferSide;
|
||||
} else if (preferSide & RectPart::Right) {
|
||||
_side = preferSide;
|
||||
} else {
|
||||
_side = (allowedAbove ? RectPart::Top : RectPart::Bottom)
|
||||
| (preferSide & (RectPart::Left | RectPart::Center));
|
||||
}
|
||||
auto arrow = QImage(
|
||||
QSize(_st.arrow * 2, _st.arrow) * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
arrow.fill(Qt::transparent);
|
||||
arrow.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
Painter p(&arrow);
|
||||
PainterHighQualityEnabler hq(p);
|
||||
|
||||
QPainterPath path;
|
||||
path.moveTo(0, 0);
|
||||
path.lineTo(2 * _st.arrow, 0);
|
||||
path.lineTo(_st.arrow, _st.arrow);
|
||||
path.lineTo(0, 0);
|
||||
p.fillPath(path, _st.bg);
|
||||
}
|
||||
if (_side & RectPart::Bottom) {
|
||||
arrow = std::move(arrow).transformed(QTransform(1, 0, 0, -1, 0, 0));
|
||||
} else if (_side & RectPart::Right) {
|
||||
arrow = std::move(arrow).transformed(QTransform().rotate(-90));
|
||||
}
|
||||
_arrow = PixmapFromImage(std::move(arrow));
|
||||
}
|
||||
|
||||
void ImportantTooltip::toggleAnimated(bool visible) {
|
||||
if (_visible != visible) {
|
||||
updateGeometry();
|
||||
_visible = visible;
|
||||
refreshAnimationCache();
|
||||
if (_visible) {
|
||||
show();
|
||||
} else if (isHidden()) {
|
||||
return;
|
||||
}
|
||||
hideChildren();
|
||||
_visibleAnimation.start([this] { animationCallback(); }, _visible ? 0. : 1., _visible ? 1. : 0., _st.duration, anim::easeOutCirc);
|
||||
} else if (visible && isHidden()) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
void ImportantTooltip::hideAfter(crl::time timeout) {
|
||||
_hideTimer.callOnce(timeout);
|
||||
}
|
||||
|
||||
void ImportantTooltip::animationCallback() {
|
||||
updateGeometry();
|
||||
update();
|
||||
checkAnimationFinish();
|
||||
}
|
||||
|
||||
void ImportantTooltip::refreshAnimationCache() {
|
||||
if (!_cache.isNull()) {
|
||||
return;
|
||||
}
|
||||
auto animation = base::take(_visibleAnimation);
|
||||
auto visible = std::exchange(_visible, true);
|
||||
showChildren();
|
||||
_cache = GrabWidget(this);
|
||||
_visible = base::take(visible);
|
||||
_visibleAnimation = base::take(animation);
|
||||
}
|
||||
|
||||
void ImportantTooltip::toggleFast(bool visible) {
|
||||
if (_visible == isHidden()) {
|
||||
setVisible(_visible);
|
||||
}
|
||||
if (_visibleAnimation.animating() || _visible != visible) {
|
||||
_visibleAnimation.stop();
|
||||
_visible = visible;
|
||||
checkAnimationFinish();
|
||||
}
|
||||
}
|
||||
|
||||
void ImportantTooltip::checkAnimationFinish() {
|
||||
if (!_visibleAnimation.animating()) {
|
||||
_cache = QPixmap();
|
||||
showChildren();
|
||||
setVisible(_visible);
|
||||
if (_visible) {
|
||||
update();
|
||||
} else if (_hiddenCallback) {
|
||||
_hiddenCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPoint ImportantTooltip::countPosition() const {
|
||||
Expects(parentWidget() != nullptr);
|
||||
|
||||
auto parent = parentWidget();
|
||||
auto areaMiddle = _area.x() + (_area.width() / 2);
|
||||
auto left = areaMiddle - (width() / 2);
|
||||
if (_side & RectPart::Left) {
|
||||
left = areaMiddle + _st.arrowSkip - width();
|
||||
} else if (_side & RectPart::Right) {
|
||||
left = areaMiddle - _st.arrowSkip;
|
||||
}
|
||||
accumulate_min(left, parent->width() - _st.margin.right() - width());
|
||||
accumulate_max(left, _st.margin.left());
|
||||
accumulate_max(left, areaMiddle + _st.arrow + _st.arrowSkipMin - width());
|
||||
accumulate_min(left, areaMiddle - _st.arrow - _st.arrowSkipMin);
|
||||
|
||||
const auto top = (_side & RectPart::Top)
|
||||
? (_area.y() - height())
|
||||
: (_area.y() + _area.height());
|
||||
return { left, top };
|
||||
}
|
||||
|
||||
void ImportantTooltip::updateGeometry() {
|
||||
const auto position = _countPosition
|
||||
? _countPosition(size())
|
||||
: countPosition();
|
||||
const auto isTop = (_side & RectPart::Top);
|
||||
const auto isBottom = (_side & RectPart::Bottom);
|
||||
const auto shift = anim::interpolate(
|
||||
(isTop || (_side & RectPart::Left)) ? -_st.shift : _st.shift,
|
||||
0,
|
||||
_visibleAnimation.value(_visible ? 1. : 0.));
|
||||
move(
|
||||
position.x() + (isTop || isBottom ? 0 : shift),
|
||||
position.y() + (isTop || isBottom ? shift : 0));
|
||||
}
|
||||
|
||||
void ImportantTooltip::resizeEvent(QResizeEvent *e) {
|
||||
auto contentTop = _st.padding.top();
|
||||
if (_side & RectPart::Bottom) {
|
||||
contentTop += _st.arrow;
|
||||
}
|
||||
_content->moveToLeft(_st.padding.left(), contentTop);
|
||||
}
|
||||
|
||||
QRect ImportantTooltip::countInner() const {
|
||||
return _content->geometry().marginsAdded(_st.padding);
|
||||
}
|
||||
|
||||
void ImportantTooltip::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
auto inner = countInner();
|
||||
if (!_cache.isNull()) {
|
||||
auto opacity = _visibleAnimation.value(_visible ? 1. : 0.);
|
||||
p.setOpacity(opacity);
|
||||
p.drawPixmap(0, 0, _cache);
|
||||
} else {
|
||||
if (!_visible) {
|
||||
return;
|
||||
}
|
||||
p.setBrush(_st.bg);
|
||||
p.setPen(Qt::NoPen);
|
||||
{
|
||||
PainterHighQualityEnabler hq(p);
|
||||
p.drawRoundedRect(inner, _st.radius, _st.radius);
|
||||
}
|
||||
auto areaMiddle = _area.x() + (_area.width() / 2) - x();
|
||||
auto arrowLeft = areaMiddle - _st.arrow;
|
||||
if (_side & RectPart::Top) {
|
||||
p.drawPixmapLeft(arrowLeft, inner.y() + inner.height(), width(), _arrow);
|
||||
} else if (_side & RectPart::Bottom) {
|
||||
p.drawPixmapLeft(arrowLeft, inner.y() - _st.arrow, width(), _arrow);
|
||||
} else if (_side & RectPart::Right) {
|
||||
p.drawPixmapLeft(
|
||||
inner.x() + inner.width(),
|
||||
inner.y() + (inner.height() - _st.arrow) / 2,
|
||||
width(),
|
||||
_arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] int FindNiceTooltipWidth(
|
||||
int minWidth,
|
||||
int maxWidth,
|
||||
Fn<int(int width)> heightForWidth) {
|
||||
Expects(minWidth >= 0);
|
||||
Expects(maxWidth >= minWidth);
|
||||
|
||||
const auto desired = heightForWidth(maxWidth);
|
||||
while (maxWidth - minWidth > 1) {
|
||||
const auto middle = (minWidth + maxWidth) / 2;
|
||||
if (heightForWidth(middle) > desired) {
|
||||
minWidth = middle;
|
||||
} else {
|
||||
maxWidth = middle;
|
||||
}
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
object_ptr<FlatLabel> MakeNiceTooltipLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
int maxWidth,
|
||||
const style::FlatLabel &st,
|
||||
const style::PopupMenu &stMenu,
|
||||
const Text::MarkedContext &context) {
|
||||
Expects(st.minWidth > 0);
|
||||
Expects(st.minWidth < maxWidth);
|
||||
|
||||
auto result = object_ptr<FlatLabel>(
|
||||
parent,
|
||||
rpl::duplicate(text),
|
||||
st,
|
||||
stMenu,
|
||||
context);
|
||||
const auto raw = result.data();
|
||||
std::move(text) | rpl::on_next([=, &st] {
|
||||
raw->resizeToWidth(qMin(maxWidth, raw->textMaxWidth()));
|
||||
const auto desired = raw->textMaxWidth();
|
||||
if (desired <= maxWidth) {
|
||||
raw->resizeToWidth(desired);
|
||||
return;
|
||||
}
|
||||
raw->resizeToWidth(maxWidth);
|
||||
const auto niceWidth = FindNiceTooltipWidth(
|
||||
st.minWidth,
|
||||
maxWidth,
|
||||
[&](int width) {
|
||||
raw->resizeToWidth(width);
|
||||
return raw->heightNoMargins();
|
||||
});
|
||||
raw->resizeToWidth(niceWidth);
|
||||
}, raw->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
149
Telegram/lib_ui/ui/widgets/tooltip.h
Normal file
149
Telegram/lib_ui/ui/widgets/tooltip.h
Normal file
@@ -0,0 +1,149 @@
|
||||
// This file is part of Desktop App Toolkit,
|
||||
// a set of libraries for developing nice desktop applications.
|
||||
//
|
||||
// For license and copyright information please follow this link:
|
||||
// https://github.com/desktop-app/legal/blob/master/LEGAL
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/object_ptr.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/rect_part.h"
|
||||
|
||||
namespace style {
|
||||
struct Tooltip;
|
||||
struct ImportantTooltip;
|
||||
struct FlatLabel;
|
||||
struct PopupMenu;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::FlatLabel &defaultFlatLabel;
|
||||
extern const style::PopupMenu &defaultPopupMenu;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FlatLabel;
|
||||
|
||||
class AbstractTooltipShower {
|
||||
public:
|
||||
virtual QString tooltipText() const = 0;
|
||||
virtual QPoint tooltipPos() const = 0;
|
||||
virtual bool tooltipWindowActive() const = 0;
|
||||
virtual const style::Tooltip *tooltipSt() const;
|
||||
virtual ~AbstractTooltipShower();
|
||||
|
||||
};
|
||||
|
||||
class Tooltip : public RpWidget {
|
||||
public:
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::ToolTip;
|
||||
}
|
||||
QString accessibilityName() override {
|
||||
return _text.toString();
|
||||
}
|
||||
|
||||
static void Show(int32 delay, const AbstractTooltipShower *shower);
|
||||
static void Hide();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
|
||||
private:
|
||||
void performShow();
|
||||
|
||||
Tooltip();
|
||||
~Tooltip();
|
||||
|
||||
void popup(const QPoint &p, const QString &text, const style::Tooltip *st);
|
||||
|
||||
friend class AbstractTooltipShower;
|
||||
const AbstractTooltipShower *_shower = nullptr;
|
||||
base::Timer _showTimer;
|
||||
|
||||
Text::String _text;
|
||||
QPoint _point;
|
||||
|
||||
const style::Tooltip *_st = nullptr;
|
||||
|
||||
base::Timer _hideByLeaveTimer;
|
||||
bool _isEventFilter = false;
|
||||
bool _useTransparency = true;
|
||||
|
||||
};
|
||||
|
||||
class ImportantTooltip : public RpWidget {
|
||||
public:
|
||||
ImportantTooltip(
|
||||
QWidget *parent,
|
||||
object_ptr<RpWidget> content,
|
||||
const style::ImportantTooltip &st);
|
||||
|
||||
QAccessible::Role accessibilityRole() override {
|
||||
return QAccessible::Role::ToolTip;
|
||||
}
|
||||
|
||||
void pointAt(
|
||||
QRect area,
|
||||
RectParts preferSide = RectPart::Top | RectPart::Left,
|
||||
Fn<QPoint(QSize)> countPosition = nullptr);
|
||||
|
||||
void toggleAnimated(bool visible);
|
||||
void toggleFast(bool visible);
|
||||
void hideAfter(crl::time timeout);
|
||||
void updateGeometry();
|
||||
|
||||
void setHiddenCallback(Fn<void()> callback) {
|
||||
_hiddenCallback = std::move(callback);
|
||||
}
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
void animationCallback();
|
||||
[[nodiscard]] QRect countInner() const;
|
||||
void countApproachSide(RectParts preferSide);
|
||||
void resizeToContent();
|
||||
void checkAnimationFinish();
|
||||
void refreshAnimationCache();
|
||||
[[nodiscard]] QPoint countPosition() const;
|
||||
|
||||
base::Timer _hideTimer;
|
||||
const style::ImportantTooltip &_st;
|
||||
object_ptr<RpWidget> _content;
|
||||
QRect _area;
|
||||
RectParts _side = RectPart::Top | RectPart::Left;
|
||||
QPixmap _arrow;
|
||||
|
||||
Ui::Animations::Simple _visibleAnimation;
|
||||
Fn<QPoint(QSize)> _countPosition;
|
||||
bool _visible = false;
|
||||
Fn<void()> _hiddenCallback;
|
||||
QPixmap _cache;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] int FindNiceTooltipWidth(
|
||||
int minWidth,
|
||||
int maxWidth,
|
||||
Fn<int(int width)> heightForWidth);
|
||||
|
||||
[[nodiscard]] object_ptr<FlatLabel> MakeNiceTooltipLabel(
|
||||
QWidget *parent,
|
||||
rpl::producer<TextWithEntities> &&text,
|
||||
int maxWidth,
|
||||
const style::FlatLabel &st = st::defaultFlatLabel,
|
||||
const style::PopupMenu &stMenu = st::defaultPopupMenu,
|
||||
const Text::MarkedContext &context = {});
|
||||
|
||||
} // namespace Ui
|
||||
1672
Telegram/lib_ui/ui/widgets/widgets.style
Normal file
1672
Telegram/lib_ui/ui/widgets/widgets.style
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user