mirror of https://github.com/procxx/kepka.git
Optimize connecting widget.
This commit is contained in:
parent
9dd93a77a0
commit
26f1ade5ba
|
@ -137,10 +137,27 @@ int Sandbox::start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sandbox::launchApplication() {
|
void Sandbox::launchApplication() {
|
||||||
if (_application) {
|
InvokeQueued(this, [=] {
|
||||||
return;
|
if (App::quitting()) {
|
||||||
}
|
quit();
|
||||||
|
} else if (_application) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setupScreenScale();
|
||||||
|
|
||||||
|
_application = std::make_unique<Application>(_launcher);
|
||||||
|
|
||||||
|
// Ideally this should go to constructor.
|
||||||
|
// But we want to catch all native events and Application installs
|
||||||
|
// its own filter that can filter out some of them. So we install
|
||||||
|
// our filter after the Application constructor installs his.
|
||||||
|
installNativeEventFilter(this);
|
||||||
|
|
||||||
|
_application->run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sandbox::setupScreenScale() {
|
||||||
const auto dpi = Sandbox::primaryScreen()->logicalDotsPerInch();
|
const auto dpi = Sandbox::primaryScreen()->logicalDotsPerInch();
|
||||||
LOG(("Primary screen DPI: %1").arg(dpi));
|
LOG(("Primary screen DPI: %1").arg(dpi));
|
||||||
if (dpi <= 108) {
|
if (dpi <= 108) {
|
||||||
|
@ -170,8 +187,6 @@ void Sandbox::launchApplication() {
|
||||||
cSetIntRetinaFactor(int32(ratio));
|
cSetIntRetinaFactor(int32(ratio));
|
||||||
cSetScreenScale(kInterfaceScaleDefault);
|
cSetScreenScale(kInterfaceScaleDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
runApplication();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Sandbox::~Sandbox() = default;
|
Sandbox::~Sandbox() = default;
|
||||||
|
@ -409,20 +424,6 @@ void Sandbox::checkForQuit() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Sandbox::runApplication() {
|
|
||||||
Expects(!App::quitting());
|
|
||||||
|
|
||||||
_application = std::make_unique<Application>(_launcher);
|
|
||||||
|
|
||||||
// Ideally this should go to constructor.
|
|
||||||
// But we want to catch all native events and Application installs
|
|
||||||
// its own filter that can filter out some of them. So we install
|
|
||||||
// our filter after the Application constructor installs his.
|
|
||||||
installNativeEventFilter(this);
|
|
||||||
|
|
||||||
_application->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Sandbox::refreshGlobalProxy() {
|
void Sandbox::refreshGlobalProxy() {
|
||||||
#ifndef TDESKTOP_DISABLE_NETWORK_PROXY
|
#ifndef TDESKTOP_DISABLE_NETWORK_PROXY
|
||||||
const auto proxy = !Global::started()
|
const auto proxy = !Global::started()
|
||||||
|
|
|
@ -81,7 +81,7 @@ private:
|
||||||
void processPostponedCalls(int level);
|
void processPostponedCalls(int level);
|
||||||
void singleInstanceChecked();
|
void singleInstanceChecked();
|
||||||
void launchApplication();
|
void launchApplication();
|
||||||
void runApplication();
|
void setupScreenScale();
|
||||||
void execExternal(const QString &cmd);
|
void execExternal(const QString &cmd);
|
||||||
|
|
||||||
// Single instance application
|
// Single instance application
|
||||||
|
|
|
@ -246,7 +246,7 @@ DialogsWidget::DialogsWidget(QWidget *parent, not_null<Window::Controller*> cont
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogsWidget::setupConnectingWidget() {
|
void DialogsWidget::setupConnectingWidget() {
|
||||||
_connecting = Window::ConnectingWidget::CreateDefaultWidget(
|
_connecting = std::make_unique<Window::ConnectionState>(
|
||||||
this,
|
this,
|
||||||
Window::AdaptiveIsOneColumn());
|
Window::AdaptiveIsOneColumn());
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class FadeWrapScaled;
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
class Controller;
|
class Controller;
|
||||||
class ConnectingWidget;
|
class ConnectionState;
|
||||||
} // namespace Window
|
} // namespace Window
|
||||||
|
|
||||||
enum DialogsSearchRequestType {
|
enum DialogsSearchRequestType {
|
||||||
|
@ -195,7 +195,7 @@ private:
|
||||||
class BottomButton;
|
class BottomButton;
|
||||||
object_ptr<BottomButton> _updateTelegram = { nullptr };
|
object_ptr<BottomButton> _updateTelegram = { nullptr };
|
||||||
object_ptr<BottomButton> _loadMoreChats = { nullptr };
|
object_ptr<BottomButton> _loadMoreChats = { nullptr };
|
||||||
base::unique_qptr<Window::ConnectingWidget> _connecting;
|
std::unique_ptr<Window::ConnectionState> _connecting;
|
||||||
|
|
||||||
Animation _a_show;
|
Animation _a_show;
|
||||||
Window::SlideDirection _showDirection;
|
Window::SlideDirection _showDirection;
|
||||||
|
|
|
@ -37,6 +37,8 @@ SectionWidget::SectionWidget(
|
||||||
}
|
}
|
||||||
|
|
||||||
void SectionWidget::init() {
|
void SectionWidget::init() {
|
||||||
|
Expects(_connecting == nullptr);
|
||||||
|
|
||||||
sizeValue(
|
sizeValue(
|
||||||
) | rpl::start_with_next([wrap = _content.data()](QSize size) {
|
) | rpl::start_with_next([wrap = _content.data()](QSize size) {
|
||||||
auto wrapGeometry = QRect{ { 0, 0 }, size };
|
auto wrapGeometry = QRect{ { 0, 0 }, size };
|
||||||
|
@ -44,14 +46,14 @@ void SectionWidget::init() {
|
||||||
wrap->updateGeometry(wrapGeometry, additionalScroll);
|
wrap->updateGeometry(wrapGeometry, additionalScroll);
|
||||||
}, _content->lifetime());
|
}, _content->lifetime());
|
||||||
|
|
||||||
_connecting = Window::ConnectingWidget::CreateDefaultWidget(
|
_connecting = std::make_unique<Window::ConnectionState>(
|
||||||
_content.data(),
|
_content.data(),
|
||||||
Window::AdaptiveIsOneColumn());
|
Window::AdaptiveIsOneColumn());
|
||||||
|
|
||||||
_content->contentChanged(
|
_content->contentChanged(
|
||||||
) | rpl::start_with_next([=] {
|
) | rpl::start_with_next([=] {
|
||||||
_connecting->raise();
|
_connecting->raise();
|
||||||
}, _connecting->lifetime());
|
}, _content->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialogs::RowDescriptor SectionWidget::activeChat() const {
|
Dialogs::RowDescriptor SectionWidget::activeChat() const {
|
||||||
|
|
|
@ -15,7 +15,7 @@ class SettingsSlider;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
class ConnectingWidget;
|
class ConnectionState;
|
||||||
} // namespace Window
|
} // namespace Window
|
||||||
|
|
||||||
namespace Info {
|
namespace Info {
|
||||||
|
@ -69,7 +69,7 @@ private:
|
||||||
|
|
||||||
object_ptr<WrapWidget> _content;
|
object_ptr<WrapWidget> _content;
|
||||||
object_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };
|
object_ptr<Ui::RpWidget> _topBarSurrogate = { nullptr };
|
||||||
base::unique_qptr<Window::ConnectingWidget> _connecting;
|
std::unique_ptr<Window::ConnectionState> _connecting;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ Widget::Widget(QWidget *parent) : RpWidget(parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
void Widget::setupConnectingWidget() {
|
void Widget::setupConnectingWidget() {
|
||||||
_connecting = Window::ConnectingWidget::CreateDefaultWidget(
|
_connecting = std::make_unique<Window::ConnectionState>(
|
||||||
this,
|
this,
|
||||||
rpl::single(true));
|
rpl::single(true));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class FadeWrap;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
class ConnectingWidget;
|
class ConnectionState;
|
||||||
} // namespace Window
|
} // namespace Window
|
||||||
|
|
||||||
namespace Intro {
|
namespace Intro {
|
||||||
|
@ -281,7 +281,7 @@ private:
|
||||||
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _resetAccount = { nullptr };
|
object_ptr<Ui::FadeWrap<Ui::RoundButton>> _resetAccount = { nullptr };
|
||||||
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _terms = { nullptr };
|
object_ptr<Ui::FadeWrap<Ui::FlatLabel>> _terms = { nullptr };
|
||||||
|
|
||||||
base::unique_qptr<Window::ConnectingWidget> _connecting;
|
std::unique_ptr<Window::ConnectionState> _connecting;
|
||||||
|
|
||||||
mtpRequestId _resetRequest = 0;
|
mtpRequestId _resetRequest = 0;
|
||||||
|
|
||||||
|
|
|
@ -501,7 +501,7 @@ AuthSession &MainWidget::session() const {
|
||||||
|
|
||||||
void MainWidget::setupConnectingWidget() {
|
void MainWidget::setupConnectingWidget() {
|
||||||
using namespace rpl::mappers;
|
using namespace rpl::mappers;
|
||||||
_connecting = Window::ConnectingWidget::CreateDefaultWidget(
|
_connecting = std::make_unique<Window::ConnectionState>(
|
||||||
this,
|
this,
|
||||||
Window::AdaptiveIsOneColumn() | rpl::map(!_1));
|
Window::AdaptiveIsOneColumn() | rpl::map(!_1));
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ class TopBarWrapWidget;
|
||||||
class SectionMemento;
|
class SectionMemento;
|
||||||
class SectionWidget;
|
class SectionWidget;
|
||||||
class AbstractSectionWidget;
|
class AbstractSectionWidget;
|
||||||
class ConnectingWidget;
|
class ConnectionState;
|
||||||
struct SectionSlideParams;
|
struct SectionSlideParams;
|
||||||
struct SectionShow;
|
struct SectionShow;
|
||||||
enum class Column;
|
enum class Column;
|
||||||
|
@ -486,7 +486,7 @@ private:
|
||||||
object_ptr<Window::SectionWidget> _mainSection = { nullptr };
|
object_ptr<Window::SectionWidget> _mainSection = { nullptr };
|
||||||
object_ptr<Window::SectionWidget> _thirdSection = { nullptr };
|
object_ptr<Window::SectionWidget> _thirdSection = { nullptr };
|
||||||
std::unique_ptr<Window::SectionMemento> _thirdSectionFromStack;
|
std::unique_ptr<Window::SectionMemento> _thirdSectionFromStack;
|
||||||
base::unique_qptr<Window::ConnectingWidget> _connecting;
|
std::unique_ptr<Window::ConnectionState> _connecting;
|
||||||
|
|
||||||
base::weak_ptr<Calls::Call> _currentCall;
|
base::weak_ptr<Calls::Call> _currentCall;
|
||||||
object_ptr<Ui::SlideWrap<Calls::TopBar>> _callTopBar = { nullptr };
|
object_ptr<Ui::SlideWrap<Calls::TopBar>> _callTopBar = { nullptr };
|
||||||
|
|
|
@ -116,10 +116,10 @@ InfiniteRadialAnimation::InfiniteRadialAnimation(
|
||||||
, _animation(std::move(callbacks)) {
|
, _animation(std::move(callbacks)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void InfiniteRadialAnimation::start() {
|
void InfiniteRadialAnimation::start(TimeMs skip) {
|
||||||
const auto now = getms();
|
const auto now = getms();
|
||||||
if (_workFinished <= now && (_workFinished || !_workStarted)) {
|
if (_workFinished <= now && (_workFinished || !_workStarted)) {
|
||||||
_workStarted = now + _st.sineDuration;
|
_workStarted = std::max(now + _st.sineDuration - skip, TimeMs(1));
|
||||||
_workFinished = 0;
|
_workFinished = 0;
|
||||||
}
|
}
|
||||||
if (!_animation.animating()) {
|
if (!_animation.animating()) {
|
||||||
|
|
|
@ -69,7 +69,7 @@ public:
|
||||||
return _animation.animating();
|
return _animation.animating();
|
||||||
}
|
}
|
||||||
|
|
||||||
void start();
|
void start(TimeMs skip = 0);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
void step(TimeMs ms);
|
void step(TimeMs ms);
|
||||||
|
|
|
@ -42,7 +42,7 @@ Progress::Progress(QWidget *parent)
|
||||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
setAttribute(Qt::WA_TransparentForMouseEvents);
|
setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
resize(st::connectingRadial.size);
|
resize(st::connectingRadial.size);
|
||||||
_animation.start();
|
_animation.start(st::connectingRadial.sineDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Progress::paintEvent(QPaintEvent *e) {
|
void Progress::paintEvent(QPaintEvent *e) {
|
||||||
|
@ -66,7 +66,41 @@ void Progress::step(TimeMs ms, bool timer) {
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class ConnectingWidget::ProxyIcon
|
class ConnectionState::Widget : public Ui::AbstractButton {
|
||||||
|
public:
|
||||||
|
Widget(QWidget *parent, const Layout &layout);
|
||||||
|
|
||||||
|
void refreshRetryLink(bool hasRetry);
|
||||||
|
void setLayout(const Layout &layout);
|
||||||
|
void setProgressVisibility(bool visible);
|
||||||
|
|
||||||
|
rpl::producer<> refreshStateRequests() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
|
||||||
|
void onStateChanged(State was, StateChangeSource source) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
class ProxyIcon;
|
||||||
|
using State = ConnectionState::State;
|
||||||
|
using Layout = ConnectionState::Layout;
|
||||||
|
|
||||||
|
void updateRetryGeometry();
|
||||||
|
QRect innerRect() const;
|
||||||
|
QRect contentRect() const;
|
||||||
|
QRect textRect() const;
|
||||||
|
|
||||||
|
Layout _currentLayout;
|
||||||
|
base::unique_qptr<Ui::LinkButton> _retry;
|
||||||
|
QPointer<Ui::RpWidget> _progress;
|
||||||
|
QPointer<ProxyIcon> _proxyIcon;
|
||||||
|
rpl::event_stream<> _refreshStateRequests;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConnectionState::Widget::ProxyIcon
|
||||||
: public Ui::RpWidget
|
: public Ui::RpWidget
|
||||||
, private base::Subscriber {
|
, private base::Subscriber {
|
||||||
public:
|
public:
|
||||||
|
@ -88,7 +122,7 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ConnectingWidget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {
|
ConnectionState::Widget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {
|
||||||
resize(
|
resize(
|
||||||
std::max(
|
std::max(
|
||||||
st::connectingRadial.size.width(),
|
st::connectingRadial.size.width(),
|
||||||
|
@ -107,7 +141,7 @@ ConnectingWidget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {
|
||||||
refreshCacheImages();
|
refreshCacheImages();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::ProxyIcon::refreshCacheImages() {
|
void ConnectionState::Widget::ProxyIcon::refreshCacheImages() {
|
||||||
const auto prepareCache = [&](const style::icon &icon) {
|
const auto prepareCache = [&](const style::icon &icon) {
|
||||||
auto image = QImage(
|
auto image = QImage(
|
||||||
size() * cIntRetinaFactor(),
|
size() * cIntRetinaFactor(),
|
||||||
|
@ -128,14 +162,14 @@ void ConnectingWidget::ProxyIcon::refreshCacheImages() {
|
||||||
_cacheOff = prepareCache(st::connectingProxyOff);
|
_cacheOff = prepareCache(st::connectingProxyOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::ProxyIcon::setToggled(bool toggled) {
|
void ConnectionState::Widget::ProxyIcon::setToggled(bool toggled) {
|
||||||
if (_toggled != toggled) {
|
if (_toggled != toggled) {
|
||||||
_toggled = toggled;
|
_toggled = toggled;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::ProxyIcon::setOpacity(float64 opacity) {
|
void ConnectionState::Widget::ProxyIcon::setOpacity(float64 opacity) {
|
||||||
_opacity = opacity;
|
_opacity = opacity;
|
||||||
if (_opacity == 0.) {
|
if (_opacity == 0.) {
|
||||||
hide();
|
hide();
|
||||||
|
@ -145,29 +179,35 @@ void ConnectingWidget::ProxyIcon::setOpacity(float64 opacity) {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::ProxyIcon::paintEvent(QPaintEvent *e) {
|
void ConnectionState::Widget::ProxyIcon::paintEvent(QPaintEvent *e) {
|
||||||
Painter p(this);
|
Painter p(this);
|
||||||
p.setOpacity(_opacity);
|
p.setOpacity(_opacity);
|
||||||
p.drawPixmap(0, 0, _toggled ? _cacheOn : _cacheOff);
|
p.drawPixmap(0, 0, _toggled ? _cacheOn : _cacheOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConnectingWidget::State::operator==(const State &other) const {
|
bool ConnectionState::Widget::State::operator==(const State &other) const {
|
||||||
return (type == other.type)
|
return (type == other.type)
|
||||||
&& (useProxy == other.useProxy)
|
&& (useProxy == other.useProxy)
|
||||||
&& (underCursor == other.underCursor)
|
&& (underCursor == other.underCursor)
|
||||||
&& (waitTillRetry == other.waitTillRetry);
|
&& (waitTillRetry == other.waitTillRetry);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectingWidget::ConnectingWidget(QWidget *parent)
|
ConnectionState::ConnectionState(
|
||||||
: AbstractButton(parent)
|
not_null<Ui::RpWidget*> parent,
|
||||||
|
rpl::producer<bool> shown)
|
||||||
|
: _parent(parent)
|
||||||
, _refreshTimer([=] { refreshState(); })
|
, _refreshTimer([=] { refreshState(); })
|
||||||
, _currentLayout(computeLayout(_state)) {
|
, _currentLayout(computeLayout(_state)) {
|
||||||
_proxyIcon = Ui::CreateChild<ProxyIcon>(this);
|
rpl::combine(
|
||||||
_progress = Ui::CreateChild<Progress>(this);
|
std::move(shown),
|
||||||
|
visibility()
|
||||||
addClickHandler([=] {
|
) | rpl::start_with_next([=](bool shown, float64 visible) {
|
||||||
Ui::show(ProxiesBoxController::CreateOwningBox());
|
if (!shown || visible == 0.) {
|
||||||
});
|
_widget = nullptr;
|
||||||
|
} else if (!_widget) {
|
||||||
|
createWidget();
|
||||||
|
}
|
||||||
|
}, _lifetime);
|
||||||
|
|
||||||
subscribe(Global::RefConnectionTypeChanged(), [=] {
|
subscribe(Global::RefConnectionTypeChanged(), [=] {
|
||||||
refreshState();
|
refreshState();
|
||||||
|
@ -175,11 +215,35 @@ ConnectingWidget::ConnectingWidget(QWidget *parent)
|
||||||
refreshState();
|
refreshState();
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<float64> ConnectingWidget::visibility() const {
|
void ConnectionState::createWidget() {
|
||||||
return _visibilityValues.events_starting_with(currentVisibility());
|
_widget = base::make_unique_q<Widget>(_parent, _currentLayout);
|
||||||
|
_widget->setVisible(!_forceHidden);
|
||||||
|
|
||||||
|
updateWidth();
|
||||||
|
|
||||||
|
rpl::combine(
|
||||||
|
visibility(),
|
||||||
|
_parent->heightValue()
|
||||||
|
) | rpl::start_with_next([=](float64 visible, int height) {
|
||||||
|
_widget->moveToLeft(0, anim::interpolate(
|
||||||
|
height - st::connectingMargin.top(),
|
||||||
|
height - _widget->height(),
|
||||||
|
visible));
|
||||||
|
}, _widget->lifetime());
|
||||||
|
|
||||||
|
_widget->refreshStateRequests(
|
||||||
|
) | rpl::start_with_next([=] {
|
||||||
|
refreshState();
|
||||||
|
}, _widget->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::finishAnimating() {
|
void ConnectionState::raise() {
|
||||||
|
if (_widget) {
|
||||||
|
_widget->raise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::finishAnimating() {
|
||||||
if (_contentWidth.animating()) {
|
if (_contentWidth.animating()) {
|
||||||
_contentWidth.finish();
|
_contentWidth.finish();
|
||||||
updateWidth();
|
updateWidth();
|
||||||
|
@ -190,160 +254,16 @@ void ConnectingWidget::finishAnimating() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::setForceHidden(bool hidden) {
|
void ConnectionState::setForceHidden(bool hidden) {
|
||||||
if (_forceHidden == hidden) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hidden) {
|
|
||||||
const auto real = isHidden();
|
|
||||||
if (!real) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
_realHidden = real;
|
|
||||||
}
|
|
||||||
_forceHidden = hidden;
|
_forceHidden = hidden;
|
||||||
if (!hidden && isHidden() != _realHidden) {
|
if (_widget) {
|
||||||
setVisible(!_realHidden);
|
_widget->setVisible(!hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::setVisibleHook(bool visible) {
|
void ConnectionState::refreshState() {
|
||||||
if (_forceHidden) {
|
|
||||||
_realHidden = !visible;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
QWidget::setVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
base::unique_qptr<ConnectingWidget> ConnectingWidget::CreateDefaultWidget(
|
|
||||||
Ui::RpWidget *parent,
|
|
||||||
rpl::producer<bool> shown) {
|
|
||||||
auto result = base::make_unique_q<Window::ConnectingWidget>(parent);
|
|
||||||
const auto weak = result.get();
|
|
||||||
rpl::combine(
|
|
||||||
result->visibility(),
|
|
||||||
parent->heightValue()
|
|
||||||
) | rpl::start_with_next([=](float64 visible, int height) {
|
|
||||||
const auto hidden = (visible == 0.);
|
|
||||||
if (weak->isHidden() != hidden) {
|
|
||||||
weak->setVisible(!hidden);
|
|
||||||
}
|
|
||||||
const auto size = weak->size();
|
|
||||||
weak->moveToLeft(0, anim::interpolate(
|
|
||||||
height - st::connectingMargin.top(),
|
|
||||||
height - weak->height(),
|
|
||||||
visible));
|
|
||||||
}, weak->lifetime());
|
|
||||||
std::move(
|
|
||||||
shown
|
|
||||||
) | rpl::start_with_next([=](bool shown) {
|
|
||||||
weak->setForceHidden(!shown);
|
|
||||||
}, weak->lifetime());
|
|
||||||
result->finishAnimating();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConnectingWidget::onStateChanged(
|
|
||||||
AbstractButton::State was,
|
|
||||||
StateChangeSource source) {
|
|
||||||
crl::on_main(this, [=] { refreshState(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConnectingWidget::paintEvent(QPaintEvent *e) {
|
|
||||||
Painter p(this);
|
|
||||||
PainterHighQualityEnabler hq(p);
|
|
||||||
|
|
||||||
p.setPen(Qt::NoPen);
|
|
||||||
p.setBrush(st::windowBg);
|
|
||||||
const auto inner = innerRect();
|
|
||||||
const auto content = contentRect();
|
|
||||||
const auto text = textRect();
|
|
||||||
const auto left = inner.topLeft();
|
|
||||||
const auto right = content.topLeft() + QPoint(content.width(), 0);
|
|
||||||
st::connectingLeftShadow.paint(p, left, width());
|
|
||||||
st::connectingLeft.paint(p, left, width());
|
|
||||||
st::connectingRightShadow.paint(p, right, width());
|
|
||||||
st::connectingRight.paint(p, right, width());
|
|
||||||
st::connectingBodyShadow.fill(p, content);
|
|
||||||
st::connectingBody.fill(p, content);
|
|
||||||
|
|
||||||
const auto available = text.width();
|
|
||||||
if (available > 0 && !_currentLayout.text.isEmpty()) {
|
|
||||||
p.setFont(st::normalFont);
|
|
||||||
p.setPen(st::windowSubTextFg);
|
|
||||||
if (available >= _currentLayout.textWidth) {
|
|
||||||
p.drawTextLeft(
|
|
||||||
text.x(),
|
|
||||||
text.y(),
|
|
||||||
width(),
|
|
||||||
_currentLayout.text,
|
|
||||||
_currentLayout.textWidth);
|
|
||||||
} else {
|
|
||||||
p.drawTextLeft(
|
|
||||||
text.x(),
|
|
||||||
text.y(),
|
|
||||||
width(),
|
|
||||||
st::normalFont->elided(_currentLayout.text, available));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect ConnectingWidget::innerRect() const {
|
|
||||||
return rect().marginsRemoved(
|
|
||||||
st::connectingMargin
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect ConnectingWidget::contentRect() const {
|
|
||||||
return innerRect().marginsRemoved(style::margins(
|
|
||||||
st::connectingLeft.width(),
|
|
||||||
0,
|
|
||||||
st::connectingRight.width(),
|
|
||||||
0));
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect ConnectingWidget::textRect() const {
|
|
||||||
return contentRect().marginsRemoved(
|
|
||||||
st::connectingTextPadding
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConnectingWidget::resizeEvent(QResizeEvent *e) {
|
|
||||||
{
|
|
||||||
const auto xShift = (height() - _progress->width()) / 2;
|
|
||||||
const auto yShift = (height() - _progress->height()) / 2;
|
|
||||||
_progress->moveToLeft(xShift, yShift);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const auto xShift = (height() - _proxyIcon->width()) / 2;
|
|
||||||
const auto yShift = (height() - _proxyIcon->height()) / 2;
|
|
||||||
_proxyIcon->moveToRight(xShift, yShift);
|
|
||||||
}
|
|
||||||
updateRetryGeometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConnectingWidget::updateRetryGeometry() {
|
|
||||||
if (!_retry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto text = textRect();
|
|
||||||
const auto available = text.width() - _currentLayout.textWidth;
|
|
||||||
if (available <= 0) {
|
|
||||||
_retry->hide();
|
|
||||||
} else {
|
|
||||||
_retry->show();
|
|
||||||
_retry->resize(
|
|
||||||
std::min(available, _retry->naturalWidth()),
|
|
||||||
innerRect().height());
|
|
||||||
_retry->moveToLeft(
|
|
||||||
text.x() + text.width() - _retry->width(),
|
|
||||||
st::connectingMargin.top());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ConnectingWidget::refreshState() {
|
|
||||||
const auto state = [&]() -> State {
|
const auto state = [&]() -> State {
|
||||||
const auto under = isOver();
|
const auto under = _widget && _widget->isOver();
|
||||||
const auto mtp = MTP::dcstate();
|
const auto mtp = MTP::dcstate();
|
||||||
const auto throughProxy
|
const auto throughProxy
|
||||||
= (Global::ProxySettings() == ProxyData::Settings::Enabled);
|
= (Global::ProxySettings() == ProxyData::Settings::Enabled);
|
||||||
|
@ -385,12 +305,9 @@ void ConnectingWidget::refreshState() {
|
||||||
applyState(state);
|
applyState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::applyState(const State &state) {
|
void ConnectionState::applyState(const State &state) {
|
||||||
const auto newLayout = computeLayout(state);
|
const auto newLayout = computeLayout(state);
|
||||||
const auto guard = gsl::finally([&] {
|
const auto guard = gsl::finally([&] { updateWidth(); });
|
||||||
updateWidth();
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
|
|
||||||
_state = state;
|
_state = state;
|
||||||
if (_currentLayout.visible != newLayout.visible) {
|
if (_currentLayout.visible != newLayout.visible) {
|
||||||
|
@ -416,10 +333,9 @@ void ConnectingWidget::applyState(const State &state) {
|
||||||
_currentLayout.text = saved.text;
|
_currentLayout.text = saved.text;
|
||||||
_currentLayout.textWidth = saved.textWidth;
|
_currentLayout.textWidth = saved.textWidth;
|
||||||
}
|
}
|
||||||
refreshRetryLink(_currentLayout.hasRetry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::changeVisibilityWithLayout(const Layout &layout) {
|
void ConnectionState::changeVisibilityWithLayout(const Layout &layout) {
|
||||||
Expects(_currentLayout.visible != layout.visible);
|
Expects(_currentLayout.visible != layout.visible);
|
||||||
|
|
||||||
const auto changeLayout = !_currentLayout.visible;
|
const auto changeLayout = !_currentLayout.visible;
|
||||||
|
@ -442,29 +358,22 @@ void ConnectingWidget::changeVisibilityWithLayout(const Layout &layout) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::setLayout(const Layout &layout) {
|
void ConnectionState::setLayout(const Layout &layout) {
|
||||||
_currentLayout = layout;
|
_currentLayout = layout;
|
||||||
_proxyIcon->setToggled(_currentLayout.proxyEnabled);
|
if (_widget) {
|
||||||
_progress->setVisible(_contentWidth.animating()
|
_widget->setLayout(layout);
|
||||||
|| _currentLayout.progressShown);
|
}
|
||||||
|
refreshProgressVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::refreshRetryLink(bool hasRetry) {
|
void ConnectionState::refreshProgressVisibility() {
|
||||||
if (hasRetry && !_retry) {
|
if (_widget) {
|
||||||
_retry = base::make_unique_q<Ui::LinkButton>(
|
_widget->setProgressVisibility(_contentWidth.animating()
|
||||||
this,
|
|| _currentLayout.progressShown);
|
||||||
lang(lng_reconnecting_try_now),
|
|
||||||
st::connectingRetryLink);
|
|
||||||
_retry->addClickHandler([=] {
|
|
||||||
MTP::restart();
|
|
||||||
});
|
|
||||||
updateRetryGeometry();
|
|
||||||
} else if (!hasRetry) {
|
|
||||||
_retry = nullptr;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::updateVisibility() {
|
void ConnectionState::updateVisibility() {
|
||||||
const auto value = currentVisibility();
|
const auto value = currentVisibility();
|
||||||
if (value == 0. && _contentWidth.animating()) {
|
if (value == 0. && _contentWidth.animating()) {
|
||||||
_contentWidth.finish();
|
_contentWidth.finish();
|
||||||
|
@ -473,11 +382,15 @@ void ConnectingWidget::updateVisibility() {
|
||||||
_visibilityValues.fire_copy(value);
|
_visibilityValues.fire_copy(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
float64 ConnectingWidget::currentVisibility() const {
|
float64 ConnectionState::currentVisibility() const {
|
||||||
return _visibility.current(_currentLayout.visible ? 1. : 0.);
|
return _visibility.current(_currentLayout.visible ? 1. : 0.);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ConnectingWidget::computeLayout(const State &state) const -> Layout {
|
rpl::producer<float64> ConnectionState::visibility() const {
|
||||||
|
return _visibilityValues.events_starting_with(currentVisibility());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ConnectionState::computeLayout(const State &state) const -> Layout {
|
||||||
auto result = Layout();
|
auto result = Layout();
|
||||||
result.proxyEnabled = state.useProxy;
|
result.proxyEnabled = state.useProxy;
|
||||||
result.progressShown = (state.type != State::Type::Connected);
|
result.progressShown = (state.type != State::Type::Connected);
|
||||||
|
@ -513,7 +426,7 @@ auto ConnectingWidget::computeLayout(const State &state) const -> Layout {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectingWidget::updateWidth() {
|
void ConnectionState::updateWidth() {
|
||||||
const auto current = _contentWidth.current(_currentLayout.contentWidth);
|
const auto current = _contentWidth.current(_currentLayout.contentWidth);
|
||||||
const auto height = st::connectingLeft.height();
|
const auto height = st::connectingLeft.height();
|
||||||
const auto desired = QRect(0, 0, current, height).marginsAdded(
|
const auto desired = QRect(0, 0, current, height).marginsAdded(
|
||||||
|
@ -525,11 +438,153 @@ void ConnectingWidget::updateWidth() {
|
||||||
).marginsAdded(
|
).marginsAdded(
|
||||||
st::connectingMargin
|
st::connectingMargin
|
||||||
);
|
);
|
||||||
resize(desired.size());
|
if (_widget) {
|
||||||
if (!_contentWidth.animating()) {
|
_widget->resize(desired.size());
|
||||||
_progress->setVisible(_currentLayout.progressShown);
|
_widget->update();
|
||||||
|
}
|
||||||
|
refreshProgressVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState::Widget::Widget(QWidget *parent, const Layout &layout)
|
||||||
|
: AbstractButton(parent)
|
||||||
|
, _currentLayout(layout) {
|
||||||
|
_proxyIcon = Ui::CreateChild<ProxyIcon>(this);
|
||||||
|
_progress = Ui::CreateChild<Progress>(this);
|
||||||
|
|
||||||
|
addClickHandler([=] {
|
||||||
|
Ui::show(ProxiesBoxController::CreateOwningBox());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::onStateChanged(
|
||||||
|
AbstractButton::State was,
|
||||||
|
StateChangeSource source) {
|
||||||
|
Ui::PostponeCall(crl::guard(this, [=] {
|
||||||
|
_refreshStateRequests.fire({});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> ConnectionState::Widget::refreshStateRequests() const {
|
||||||
|
return _refreshStateRequests.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::paintEvent(QPaintEvent *e) {
|
||||||
|
Painter p(this);
|
||||||
|
PainterHighQualityEnabler hq(p);
|
||||||
|
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::windowBg);
|
||||||
|
const auto inner = innerRect();
|
||||||
|
const auto content = contentRect();
|
||||||
|
const auto text = textRect();
|
||||||
|
const auto left = inner.topLeft();
|
||||||
|
const auto right = content.topLeft() + QPoint(content.width(), 0);
|
||||||
|
st::connectingLeftShadow.paint(p, left, width());
|
||||||
|
st::connectingLeft.paint(p, left, width());
|
||||||
|
st::connectingRightShadow.paint(p, right, width());
|
||||||
|
st::connectingRight.paint(p, right, width());
|
||||||
|
st::connectingBodyShadow.fill(p, content);
|
||||||
|
st::connectingBody.fill(p, content);
|
||||||
|
|
||||||
|
const auto available = text.width();
|
||||||
|
if (available > 0 && !_currentLayout.text.isEmpty()) {
|
||||||
|
p.setFont(st::normalFont);
|
||||||
|
p.setPen(st::windowSubTextFg);
|
||||||
|
if (available >= _currentLayout.textWidth) {
|
||||||
|
p.drawTextLeft(
|
||||||
|
text.x(),
|
||||||
|
text.y(),
|
||||||
|
width(),
|
||||||
|
_currentLayout.text,
|
||||||
|
_currentLayout.textWidth);
|
||||||
|
} else {
|
||||||
|
p.drawTextLeft(
|
||||||
|
text.x(),
|
||||||
|
text.y(),
|
||||||
|
width(),
|
||||||
|
st::normalFont->elided(_currentLayout.text, available));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect ConnectionState::Widget::innerRect() const {
|
||||||
|
return rect().marginsRemoved(
|
||||||
|
st::connectingMargin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect ConnectionState::Widget::contentRect() const {
|
||||||
|
return innerRect().marginsRemoved(style::margins(
|
||||||
|
st::connectingLeft.width(),
|
||||||
|
0,
|
||||||
|
st::connectingRight.width(),
|
||||||
|
0));
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect ConnectionState::Widget::textRect() const {
|
||||||
|
return contentRect().marginsRemoved(
|
||||||
|
st::connectingTextPadding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::resizeEvent(QResizeEvent *e) {
|
||||||
|
{
|
||||||
|
const auto xShift = (height() - _progress->width()) / 2;
|
||||||
|
const auto yShift = (height() - _progress->height()) / 2;
|
||||||
|
_progress->moveToLeft(xShift, yShift);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto xShift = (height() - _proxyIcon->width()) / 2;
|
||||||
|
const auto yShift = (height() - _proxyIcon->height()) / 2;
|
||||||
|
_proxyIcon->moveToRight(xShift, yShift);
|
||||||
|
}
|
||||||
|
updateRetryGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::updateRetryGeometry() {
|
||||||
|
if (!_retry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto text = textRect();
|
||||||
|
const auto available = text.width() - _currentLayout.textWidth;
|
||||||
|
if (available <= 0) {
|
||||||
|
_retry->hide();
|
||||||
|
} else {
|
||||||
|
_retry->show();
|
||||||
|
_retry->resize(
|
||||||
|
std::min(available, _retry->naturalWidth()),
|
||||||
|
innerRect().height());
|
||||||
|
_retry->moveToLeft(
|
||||||
|
text.x() + text.width() - _retry->width(),
|
||||||
|
st::connectingMargin.top());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::setLayout(const Layout &layout) {
|
||||||
|
_currentLayout = layout;
|
||||||
|
_proxyIcon->setToggled(_currentLayout.proxyEnabled);
|
||||||
|
refreshRetryLink(_currentLayout.hasRetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::setProgressVisibility(bool visible) {
|
||||||
|
if (_progress->isHidden() == visible) {
|
||||||
|
_progress->setVisible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectionState::Widget::refreshRetryLink(bool hasRetry) {
|
||||||
|
if (hasRetry && !_retry) {
|
||||||
|
_retry = base::make_unique_q<Ui::LinkButton>(
|
||||||
|
this,
|
||||||
|
lang(lng_reconnecting_try_now),
|
||||||
|
st::connectingRetryLink);
|
||||||
|
_retry->addClickHandler([=] {
|
||||||
|
MTP::restart();
|
||||||
|
});
|
||||||
|
updateRetryGeometry();
|
||||||
|
} else if (!hasRetry) {
|
||||||
|
_retry = nullptr;
|
||||||
}
|
}
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<bool> AdaptiveIsOneColumn() {
|
rpl::producer<bool> AdaptiveIsOneColumn() {
|
||||||
|
|
|
@ -7,39 +7,30 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/abstract_button.h"
|
|
||||||
#include "base/timer.h"
|
#include "base/timer.h"
|
||||||
|
#include "base/unique_qptr.h"
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class LinkButton;
|
class RpWidget;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
|
|
||||||
class ConnectingWidget
|
class ConnectionState : private base::Subscriber {
|
||||||
: public Ui::AbstractButton
|
|
||||||
, private base::Subscriber {
|
|
||||||
public:
|
public:
|
||||||
ConnectingWidget(QWidget *parent);
|
ConnectionState(
|
||||||
|
not_null<Ui::RpWidget*> parent,
|
||||||
rpl::producer<float64> visibility() const;
|
|
||||||
|
|
||||||
void finishAnimating();
|
|
||||||
void setForceHidden(bool hidden);
|
|
||||||
void setVisibleHook(bool visible) override;
|
|
||||||
|
|
||||||
static base::unique_qptr<ConnectingWidget> CreateDefaultWidget(
|
|
||||||
Ui::RpWidget *parent,
|
|
||||||
rpl::producer<bool> shown);
|
rpl::producer<bool> shown);
|
||||||
|
|
||||||
protected:
|
void raise();
|
||||||
void resizeEvent(QResizeEvent *e) override;
|
void setForceHidden(bool hidden);
|
||||||
void paintEvent(QPaintEvent *e) override;
|
|
||||||
|
|
||||||
void onStateChanged(State was, StateChangeSource source) override;
|
rpl::lifetime &lifetime() {
|
||||||
|
return _lifetime;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class ProxyIcon;
|
class Widget;
|
||||||
struct State {
|
struct State {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
Connected,
|
Connected,
|
||||||
|
@ -64,34 +55,32 @@ private:
|
||||||
int textWidth = 0;
|
int textWidth = 0;
|
||||||
|
|
||||||
};
|
};
|
||||||
void updateRetryGeometry();
|
|
||||||
void updateWidth();
|
void createWidget();
|
||||||
void updateVisibility();
|
void finishAnimating();
|
||||||
void refreshState();
|
void refreshState();
|
||||||
void applyState(const State &state);
|
void applyState(const State &state);
|
||||||
void changeVisibilityWithLayout(const Layout &layout);
|
void changeVisibilityWithLayout(const Layout &layout);
|
||||||
void refreshRetryLink(bool hasRetry);
|
|
||||||
Layout computeLayout(const State &state) const;
|
Layout computeLayout(const State &state) const;
|
||||||
void setLayout(const Layout &layout);
|
void setLayout(const Layout &layout);
|
||||||
float64 currentVisibility() const;
|
float64 currentVisibility() const;
|
||||||
|
rpl::producer<float64> visibility() const;
|
||||||
|
void updateWidth();
|
||||||
|
void updateVisibility();
|
||||||
|
void refreshProgressVisibility();
|
||||||
|
|
||||||
QRect innerRect() const;
|
not_null<Ui::RpWidget*> _parent;
|
||||||
QRect contentRect() const;
|
base::unique_qptr<Widget> _widget;
|
||||||
QRect textRect() const;
|
bool _forceHidden = false;
|
||||||
|
|
||||||
base::Timer _refreshTimer;
|
base::Timer _refreshTimer;
|
||||||
State _state;
|
State _state;
|
||||||
Layout _currentLayout;
|
Layout _currentLayout;
|
||||||
TimeMs _connectingStartedAt = 0;
|
TimeMs _connectingStartedAt = 0;
|
||||||
Animation _contentWidth;
|
Animation _contentWidth;
|
||||||
Animation _visibility;
|
Animation _visibility;
|
||||||
base::unique_qptr<Ui::LinkButton> _retry;
|
|
||||||
QPointer<Ui::RpWidget> _progress;
|
|
||||||
QPointer<ProxyIcon> _proxyIcon;
|
|
||||||
bool _forceHidden = false;
|
|
||||||
bool _realHidden = false;
|
|
||||||
|
|
||||||
rpl::event_stream<float64> _visibilityValues;
|
rpl::event_stream<float64> _visibilityValues;
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue