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

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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 &quote = _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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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 &copyText);
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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View 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 "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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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 &top;
CustomImage topRight;
const style::icon &right;
CustomImage bottomRight;
const style::icon &bottom;
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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff