window_connecting_widget.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "window/window_connecting_widget.h"
  8. #include "ui/widgets/buttons.h"
  9. #include "ui/effects/radial_animation.h"
  10. #include "ui/painter.h"
  11. #include "ui/ui_utility.h"
  12. #include "mtproto/mtp_instance.h"
  13. #include "mtproto/facade.h"
  14. #include "main/main_account.h"
  15. #include "core/application.h"
  16. #include "core/core_settings.h"
  17. #include "core/update_checker.h"
  18. #include "boxes/connection_box.h"
  19. #include "boxes/abstract_box.h"
  20. #include "lang/lang_keys.h"
  21. #include "styles/style_window.h"
  22. #include <QtGui/QWindow>
  23. namespace Window {
  24. namespace {
  25. constexpr auto kIgnoreStartConnectingFor = crl::time(3000);
  26. constexpr auto kConnectingStateDelay = crl::time(1000);
  27. constexpr auto kRefreshTimeout = crl::time(200);
  28. constexpr auto kMinimalWaitingStateDuration = crl::time(4000);
  29. class Progress : public Ui::RpWidget {
  30. public:
  31. Progress(QWidget *parent);
  32. rpl::producer<> animationStepRequests() const;
  33. protected:
  34. void paintEvent(QPaintEvent *e) override;
  35. private:
  36. void animationStep();
  37. Ui::InfiniteRadialAnimation _animation;
  38. rpl::event_stream<> _animationStepRequests;
  39. };
  40. Progress::Progress(QWidget *parent)
  41. : RpWidget(parent)
  42. , _animation([=] { animationStep(); }, st::connectingRadial) {
  43. setAttribute(Qt::WA_OpaquePaintEvent);
  44. setAttribute(Qt::WA_TransparentForMouseEvents);
  45. resize(st::connectingRadial.size);
  46. _animation.start(st::connectingRadial.sineDuration);
  47. }
  48. void Progress::paintEvent(QPaintEvent *e) {
  49. auto p = QPainter(this);
  50. p.fillRect(e->rect(), st::windowBg);
  51. const auto &st = st::connectingRadial;
  52. const auto shift = st.thickness - (st.thickness / 2);
  53. _animation.draw(
  54. p,
  55. { shift, shift },
  56. QSize(st.size.width() - 2 * shift, st.size.height() - 2 * shift),
  57. width());
  58. }
  59. void Progress::animationStep() {
  60. if (!anim::Disabled()) {
  61. _animationStepRequests.fire({});
  62. update();
  63. }
  64. }
  65. rpl::producer<> Progress::animationStepRequests() const {
  66. return _animationStepRequests.events();
  67. }
  68. } // namespace
  69. class ConnectionState::Widget : public Ui::AbstractButton {
  70. public:
  71. Widget(
  72. QWidget *parent,
  73. not_null<Main::Account*> account,
  74. const Layout &layout);
  75. void refreshRetryLink(bool hasRetry);
  76. void setLayout(const Layout &layout);
  77. void setProgressVisibility(bool visible);
  78. rpl::producer<> refreshStateRequests() const;
  79. protected:
  80. void resizeEvent(QResizeEvent *e) override;
  81. void paintEvent(QPaintEvent *e) override;
  82. void onStateChanged(State was, StateChangeSource source) override;
  83. private:
  84. class ProxyIcon;
  85. using State = ConnectionState::State;
  86. using Layout = ConnectionState::Layout;
  87. void updateRetryGeometry();
  88. QRect innerRect() const;
  89. QRect contentRect() const;
  90. QRect textRect() const;
  91. const not_null<Main::Account*> _account;
  92. Layout _currentLayout;
  93. base::unique_qptr<Ui::LinkButton> _retry;
  94. QPointer<Progress> _progress;
  95. QPointer<ProxyIcon> _proxyIcon;
  96. rpl::event_stream<> _refreshStateRequests;
  97. };
  98. class ConnectionState::Widget::ProxyIcon final : public Ui::RpWidget {
  99. public:
  100. ProxyIcon(QWidget *parent);
  101. void setToggled(bool toggled);
  102. void setOpacity(float64 opacity);
  103. protected:
  104. void paintEvent(QPaintEvent *e) override;
  105. private:
  106. void refreshCacheImages();
  107. float64 _opacity = 1.;
  108. QPixmap _cacheOn;
  109. QPixmap _cacheOff;
  110. bool _toggled = true;
  111. };
  112. ConnectionState::Widget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {
  113. resize(
  114. std::max(
  115. st::connectingRadial.size.width(),
  116. st::connectingProxyOn.width()),
  117. std::max(
  118. st::connectingRadial.size.height(),
  119. st::connectingProxyOn.height()));
  120. style::PaletteChanged(
  121. ) | rpl::start_with_next([=] {
  122. refreshCacheImages();
  123. }, lifetime());
  124. refreshCacheImages();
  125. }
  126. void ConnectionState::Widget::ProxyIcon::refreshCacheImages() {
  127. const auto prepareCache = [&](const style::icon &icon) {
  128. auto image = QImage(
  129. size() * style::DevicePixelRatio(),
  130. QImage::Format_ARGB32_Premultiplied);
  131. image.setDevicePixelRatio(style::DevicePixelRatio());
  132. image.fill(st::windowBg->c);
  133. {
  134. auto p = QPainter(&image);
  135. icon.paint(
  136. p,
  137. (width() - icon.width()) / 2,
  138. (height() - icon.height()) / 2,
  139. width());
  140. }
  141. return Ui::PixmapFromImage(std::move(image));
  142. };
  143. _cacheOn = prepareCache(st::connectingProxyOn);
  144. _cacheOff = prepareCache(st::connectingProxyOff);
  145. }
  146. void ConnectionState::Widget::ProxyIcon::setToggled(bool toggled) {
  147. if (_toggled != toggled) {
  148. _toggled = toggled;
  149. update();
  150. }
  151. }
  152. void ConnectionState::Widget::ProxyIcon::setOpacity(float64 opacity) {
  153. _opacity = opacity;
  154. if (_opacity == 0.) {
  155. hide();
  156. } else if (isHidden()) {
  157. show();
  158. }
  159. update();
  160. }
  161. void ConnectionState::Widget::ProxyIcon::paintEvent(QPaintEvent *e) {
  162. auto p = QPainter(this);
  163. p.setOpacity(_opacity);
  164. p.drawPixmap(0, 0, _toggled ? _cacheOn : _cacheOff);
  165. }
  166. bool ConnectionState::State::operator==(const State &other) const {
  167. return (type == other.type)
  168. && (useProxy == other.useProxy)
  169. && (underCursor == other.underCursor)
  170. && (updateReady == other.updateReady)
  171. && (waitTillRetry == other.waitTillRetry);
  172. }
  173. ConnectionState::ConnectionState(
  174. not_null<Ui::RpWidget*> parent,
  175. not_null<Main::Account*> account,
  176. rpl::producer<bool> shown)
  177. : _account(account)
  178. , _parent(parent)
  179. , _refreshTimer([=] { refreshState(); })
  180. , _currentLayout(computeLayout(_state)) {
  181. rpl::combine(
  182. std::move(shown),
  183. visibility()
  184. ) | rpl::start_with_next([=](bool shown, float64 visible) {
  185. if (!shown || visible == 0.) {
  186. _widget = nullptr;
  187. } else if (!_widget) {
  188. createWidget();
  189. }
  190. }, _lifetime);
  191. if (!Core::UpdaterDisabled()) {
  192. Core::UpdateChecker checker;
  193. rpl::merge(
  194. rpl::single(rpl::empty),
  195. checker.ready()
  196. ) | rpl::start_with_next([=] {
  197. refreshState();
  198. }, _lifetime);
  199. }
  200. rpl::combine(
  201. Core::App().settings().proxy().connectionTypeValue(),
  202. rpl::single(QRect()) | rpl::then(_parent->paintRequest())
  203. ) | rpl::start_with_next([=] {
  204. refreshState();
  205. }, _lifetime);
  206. }
  207. void ConnectionState::createWidget() {
  208. _widget = base::make_unique_q<Widget>(_parent, _account, _currentLayout);
  209. _widget->setVisible(!_forceHidden);
  210. updateWidth();
  211. rpl::combine(
  212. visibility(),
  213. _parent->heightValue(),
  214. _bottomSkip.value()
  215. ) | rpl::start_with_next([=](float64 visible, int height, int skip) {
  216. _widget->moveToLeft(0, anim::interpolate(
  217. height - st::connectingMargin.top(),
  218. height - _widget->height() - skip,
  219. visible));
  220. }, _widget->lifetime());
  221. _widget->refreshStateRequests(
  222. ) | rpl::start_with_next([=] {
  223. refreshState();
  224. }, _widget->lifetime());
  225. }
  226. void ConnectionState::raise() {
  227. if (_widget) {
  228. _widget->raise();
  229. }
  230. }
  231. void ConnectionState::finishAnimating() {
  232. if (_contentWidth.animating()) {
  233. _contentWidth.stop();
  234. updateWidth();
  235. }
  236. if (_visibility.animating()) {
  237. _visibility.stop();
  238. updateVisibility();
  239. }
  240. }
  241. void ConnectionState::setForceHidden(bool hidden) {
  242. _forceHidden = hidden;
  243. if (_widget) {
  244. _widget->setVisible(!hidden);
  245. }
  246. }
  247. void ConnectionState::setBottomSkip(int skip) {
  248. _bottomSkip = skip;
  249. }
  250. void ConnectionState::refreshState() {
  251. using Checker = Core::UpdateChecker;
  252. const auto state = [&]() -> State {
  253. const auto exposed = _parent->window()->windowHandle()
  254. && _parent->window()->windowHandle()->isExposed();
  255. const auto under = _widget && _widget->isOver();
  256. const auto ready = (Checker().state() == Checker::State::Ready);
  257. const auto state = _account->mtp().dcstate();
  258. const auto proxy = Core::App().settings().proxy().isEnabled();
  259. if (state == MTP::ConnectingState
  260. || state == MTP::DisconnectedState
  261. || (state < 0 && state > -600)) {
  262. return { State::Type::Connecting, proxy, exposed, under, ready };
  263. } else if (state < 0
  264. && state >= -kMinimalWaitingStateDuration
  265. && _state.type != State::Type::Waiting) {
  266. return { State::Type::Connecting, proxy, exposed, under, ready };
  267. } else if (state < 0) {
  268. const auto wait = ((-state) / 1000) + 1;
  269. return { State::Type::Waiting, proxy, exposed, under, ready, wait };
  270. }
  271. return { State::Type::Connected, proxy, exposed, under, ready };
  272. }();
  273. if (state.exposed && state.waitTillRetry > 0) {
  274. _refreshTimer.callOnce(kRefreshTimeout);
  275. }
  276. if (state == _state) {
  277. return;
  278. } else if (state.type == State::Type::Connecting
  279. && _state.type == State::Type::Connected) {
  280. const auto now = crl::now();
  281. if (!_connectingStartedAt) {
  282. _connectingStartedAt = now;
  283. _refreshTimer.callOnce(kConnectingStateDelay);
  284. return;
  285. }
  286. const auto applyConnectingAt = std::max(
  287. _connectingStartedAt + kConnectingStateDelay,
  288. kIgnoreStartConnectingFor);
  289. if (now < applyConnectingAt) {
  290. _refreshTimer.callOnce(applyConnectingAt - now);
  291. return;
  292. }
  293. }
  294. applyState(state);
  295. }
  296. void ConnectionState::applyState(const State &state) {
  297. const auto newLayout = computeLayout(state);
  298. const auto guard = gsl::finally([&] { updateWidth(); });
  299. _state = state;
  300. if (_currentLayout.visible != newLayout.visible) {
  301. changeVisibilityWithLayout(newLayout);
  302. return;
  303. }
  304. if (_currentLayout.contentWidth != newLayout.contentWidth) {
  305. if (!_currentLayout.contentWidth
  306. || !newLayout.contentWidth
  307. || _contentWidth.animating()) {
  308. _contentWidth.start(
  309. [=] { updateWidth(); },
  310. _currentLayout.contentWidth,
  311. newLayout.contentWidth,
  312. st::connectingDuration);
  313. }
  314. }
  315. const auto saved = _currentLayout;
  316. setLayout(newLayout);
  317. if (_currentLayout.text.isEmpty()
  318. && !saved.text.isEmpty()
  319. && _contentWidth.animating()) {
  320. _currentLayout.text = saved.text;
  321. _currentLayout.textWidth = saved.textWidth;
  322. }
  323. }
  324. void ConnectionState::changeVisibilityWithLayout(const Layout &layout) {
  325. Expects(_currentLayout.visible != layout.visible);
  326. const auto changeLayout = !_currentLayout.visible;
  327. _visibility.start(
  328. [=] { updateVisibility(); },
  329. layout.visible ? 0. : 1.,
  330. layout.visible ? 1. : 0.,
  331. st::connectingDuration);
  332. if (_contentWidth.animating()) {
  333. _contentWidth.start(
  334. [=] { updateWidth(); },
  335. _currentLayout.contentWidth,
  336. (changeLayout ? layout : _currentLayout).contentWidth,
  337. st::connectingDuration);
  338. }
  339. if (changeLayout) {
  340. setLayout(layout);
  341. } else {
  342. _currentLayout.visible = layout.visible;
  343. }
  344. }
  345. void ConnectionState::setLayout(const Layout &layout) {
  346. _currentLayout = layout;
  347. if (_widget) {
  348. _widget->setLayout(layout);
  349. }
  350. refreshProgressVisibility();
  351. }
  352. void ConnectionState::refreshProgressVisibility() {
  353. if (_widget) {
  354. _widget->setProgressVisibility(_contentWidth.animating()
  355. || _currentLayout.progressShown);
  356. }
  357. }
  358. void ConnectionState::updateVisibility() {
  359. const auto value = currentVisibility();
  360. if (value == 0. && _contentWidth.animating()) {
  361. _contentWidth.stop();
  362. updateWidth();
  363. }
  364. _visibilityValues.fire_copy(value);
  365. }
  366. float64 ConnectionState::currentVisibility() const {
  367. return _visibility.value(_currentLayout.visible ? 1. : 0.);
  368. }
  369. rpl::producer<float64> ConnectionState::visibility() const {
  370. return _visibilityValues.events_starting_with(currentVisibility());
  371. }
  372. auto ConnectionState::computeLayout(const State &state) const -> Layout {
  373. auto result = Layout();
  374. result.proxyEnabled = state.useProxy;
  375. result.progressShown = (state.type != State::Type::Connected);
  376. result.visible = state.exposed
  377. && !state.updateReady
  378. && (state.useProxy
  379. || state.type == State::Type::Connecting
  380. || state.type == State::Type::Waiting);
  381. switch (state.type) {
  382. case State::Type::Connecting:
  383. result.text = state.underCursor
  384. ? tr::lng_connecting(tr::now)
  385. : QString();
  386. break;
  387. case State::Type::Waiting:
  388. Assert(state.waitTillRetry > 0);
  389. result.text = tr::lng_reconnecting(
  390. tr::now,
  391. lt_count,
  392. state.waitTillRetry);
  393. break;
  394. }
  395. result.textWidth = st::normalFont->width(result.text);
  396. result.contentWidth = (result.textWidth > 0)
  397. ? (st::connectingTextPadding.left()
  398. + result.textWidth
  399. + st::connectingTextPadding.right())
  400. : 0;
  401. if (state.type == State::Type::Waiting) {
  402. result.contentWidth += st::connectingRetryLink.padding.left()
  403. + st::connectingRetryLink.font->width(
  404. tr::lng_reconnecting_try_now(tr::now))
  405. + st::connectingRetryLink.padding.right();
  406. }
  407. result.hasRetry = (state.type == State::Type::Waiting);
  408. return result;
  409. }
  410. void ConnectionState::updateWidth() {
  411. const auto current = _contentWidth.value(_currentLayout.contentWidth);
  412. const auto height = st::connectingLeft.height();
  413. const auto desired = QRect(0, 0, current, height).marginsAdded(
  414. style::margins(
  415. st::connectingLeft.width(),
  416. 0,
  417. st::connectingRight.width(),
  418. 0)
  419. ).marginsAdded(
  420. st::connectingMargin
  421. );
  422. if (_widget) {
  423. _widget->resize(desired.size());
  424. _widget->update();
  425. }
  426. refreshProgressVisibility();
  427. }
  428. ConnectionState::Widget::Widget(
  429. QWidget *parent,
  430. not_null<Main::Account*> account,
  431. const Layout &layout)
  432. : AbstractButton(parent)
  433. , _account(account)
  434. , _currentLayout(layout) {
  435. _proxyIcon = Ui::CreateChild<ProxyIcon>(this);
  436. _progress = Ui::CreateChild<Progress>(this);
  437. addClickHandler([=] {
  438. Ui::show(ProxiesBoxController::CreateOwningBox(account));
  439. });
  440. _progress->animationStepRequests(
  441. ) | rpl::start_with_next([=] {
  442. _refreshStateRequests.fire({});
  443. }, _progress->lifetime());
  444. }
  445. void ConnectionState::Widget::onStateChanged(
  446. AbstractButton::State was,
  447. StateChangeSource source) {
  448. Ui::PostponeCall(crl::guard(this, [=] {
  449. _refreshStateRequests.fire({});
  450. }));
  451. }
  452. rpl::producer<> ConnectionState::Widget::refreshStateRequests() const {
  453. return _refreshStateRequests.events();
  454. }
  455. void ConnectionState::Widget::paintEvent(QPaintEvent *e) {
  456. Painter p(this);
  457. PainterHighQualityEnabler hq(p);
  458. p.setPen(Qt::NoPen);
  459. p.setBrush(st::windowBg);
  460. const auto inner = innerRect();
  461. const auto content = contentRect();
  462. const auto text = textRect();
  463. const auto left = inner.topLeft();
  464. const auto right = content.topLeft() + QPoint(content.width(), 0);
  465. st::connectingLeftShadow.paint(p, left, width());
  466. st::connectingLeft.paint(p, left, width());
  467. st::connectingRightShadow.paint(p, right, width());
  468. st::connectingRight.paint(p, right, width());
  469. st::connectingBodyShadow.fill(p, content);
  470. st::connectingBody.fill(p, content);
  471. const auto available = text.width();
  472. if (available > 0 && !_currentLayout.text.isEmpty()) {
  473. p.setFont(st::normalFont);
  474. p.setPen(st::windowSubTextFg);
  475. if (available >= _currentLayout.textWidth) {
  476. p.drawTextLeft(
  477. text.x(),
  478. text.y(),
  479. width(),
  480. _currentLayout.text,
  481. _currentLayout.textWidth);
  482. } else {
  483. p.drawTextLeft(
  484. text.x(),
  485. text.y(),
  486. width(),
  487. st::normalFont->elided(_currentLayout.text, available));
  488. }
  489. }
  490. }
  491. QRect ConnectionState::Widget::innerRect() const {
  492. return rect().marginsRemoved(
  493. st::connectingMargin
  494. );
  495. }
  496. QRect ConnectionState::Widget::contentRect() const {
  497. return innerRect().marginsRemoved(style::margins(
  498. st::connectingLeft.width(),
  499. 0,
  500. st::connectingRight.width(),
  501. 0));
  502. }
  503. QRect ConnectionState::Widget::textRect() const {
  504. return contentRect().marginsRemoved(
  505. st::connectingTextPadding
  506. );
  507. }
  508. void ConnectionState::Widget::resizeEvent(QResizeEvent *e) {
  509. {
  510. const auto xShift = (height() - _progress->width()) / 2;
  511. const auto yShift = (height() - _progress->height()) / 2;
  512. _progress->moveToLeft(xShift, yShift);
  513. }
  514. {
  515. const auto xShift = (height() - _proxyIcon->width()) / 2;
  516. const auto yShift = (height() - _proxyIcon->height()) / 2;
  517. _proxyIcon->moveToRight(xShift, yShift);
  518. }
  519. updateRetryGeometry();
  520. }
  521. void ConnectionState::Widget::updateRetryGeometry() {
  522. if (!_retry) {
  523. return;
  524. }
  525. const auto text = textRect();
  526. const auto available = text.width() - _currentLayout.textWidth;
  527. if (available <= 0) {
  528. _retry->hide();
  529. } else {
  530. _retry->show();
  531. _retry->resize(
  532. std::min(available, _retry->naturalWidth()),
  533. innerRect().height());
  534. _retry->moveToLeft(
  535. text.x() + text.width() - _retry->width(),
  536. st::connectingMargin.top());
  537. }
  538. }
  539. void ConnectionState::Widget::setLayout(const Layout &layout) {
  540. _currentLayout = layout;
  541. _proxyIcon->setToggled(_currentLayout.proxyEnabled);
  542. refreshRetryLink(_currentLayout.hasRetry);
  543. }
  544. void ConnectionState::Widget::setProgressVisibility(bool visible) {
  545. if (_progress->isHidden() == visible) {
  546. _progress->setVisible(visible);
  547. }
  548. }
  549. void ConnectionState::Widget::refreshRetryLink(bool hasRetry) {
  550. if (hasRetry && !_retry) {
  551. _retry = base::make_unique_q<Ui::LinkButton>(
  552. this,
  553. tr::lng_reconnecting_try_now(tr::now),
  554. st::connectingRetryLink);
  555. _retry->addClickHandler([=] {
  556. _account->mtp().restart();
  557. });
  558. updateRetryGeometry();
  559. } else if (!hasRetry) {
  560. _retry = nullptr;
  561. }
  562. }
  563. } // namespace Window