/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "export/view/export_view_progress.h" #include "ui/effects/animations.h" #include "ui/widgets/labels.h" #include "ui/widgets/buttons.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/vertical_layout.h" #include "lang/lang_keys.h" #include "styles/style_boxes.h" #include "styles/style_export.h" namespace Export { namespace View { namespace { constexpr auto kShowSkipFileTimeout = 0.5 * crl::time(1000); } // namespace class ProgressWidget::Row : public Ui::RpWidget { public: Row(QWidget *parent, Content::Row &&data); void updateData(Content::Row &&data); protected: int resizeGetHeight(int newWidth) override; void paintEvent(QPaintEvent *e) override; private: struct Instance { base::unique_qptr> label; base::unique_qptr> info; float64 value = 0.; Ui::Animations::Simple progress; bool hiding = true; Ui::Animations::Simple opacity; }; void fillCurrentInstance(); void hideCurrentInstance(); void setInstanceProgress(Instance &instance, float64 progress); void toggleInstance(Instance &data, bool shown); void instanceOpacityCallback(QPointer label); void removeOldInstance(QPointer label); void paintInstance(Painter &p, const Instance &data); void updateControlsGeometry(int newWidth); void updateInstanceGeometry(const Instance &instance, int newWidth); Content::Row _data; Instance _current; std::vector _old; }; ProgressWidget::Row::Row(QWidget *parent, Content::Row &&data) : RpWidget(parent) , _data(std::move(data)) { fillCurrentInstance(); } void ProgressWidget::Row::updateData(Content::Row &&data) { const auto wasId = _data.id; const auto nowId = data.id; _data = std::move(data); if (nowId.isEmpty()) { hideCurrentInstance(); } else if (wasId.isEmpty()) { fillCurrentInstance(); } else { _current.label->entity()->setText(_data.label); _current.info->entity()->setText(_data.info); setInstanceProgress(_current, _data.progress); if (nowId != wasId) { _current.progress.stop(); } } updateControlsGeometry(width()); update(); } void ProgressWidget::Row::fillCurrentInstance() { _current.label = base::make_unique_q>( this, object_ptr( this, _data.label, st::exportProgressLabel)); _current.info = base::make_unique_q>( this, object_ptr( this, _data.info, st::exportProgressInfoLabel)); _current.label->hide(anim::type::instant); _current.info->hide(anim::type::instant); setInstanceProgress(_current, _data.progress); toggleInstance(_current, true); if (_data.id == "main") { _current.opacity.stop(); _current.label->finishAnimating(); _current.info->finishAnimating(); } } void ProgressWidget::Row::hideCurrentInstance() { if (!_current.label) { return; } setInstanceProgress(_current, 1.); toggleInstance(_current, false); _old.push_back(std::move(_current)); } void ProgressWidget::Row::setInstanceProgress( Instance &instance, float64 progress) { if (_current.value < progress) { _current.progress.start( [=] { update(); }, _current.value, progress, st::exportProgressDuration, anim::sineInOut); } else if (_current.value > progress) { _current.progress.stop(); } _current.value = progress; } void ProgressWidget::Row::toggleInstance(Instance &instance, bool shown) { Expects(instance.label != nullptr); if (instance.hiding != shown) { return; } const auto label = Ui::MakeWeak(instance.label->entity()); instance.opacity.start( [=] { instanceOpacityCallback(label); }, shown ? 0. : 1., shown ? 1. : 0., st::exportProgressDuration); instance.hiding = !shown; _current.label->toggle(shown, anim::type::normal); _current.info->toggle(shown, anim::type::normal); } void ProgressWidget::Row::instanceOpacityCallback( QPointer label) { update(); const auto i = ranges::find(_old, label, [](const Instance &instance) { return Ui::MakeWeak(instance.label->entity()); }); if (i != end(_old) && i->hiding && !i->opacity.animating()) { crl::on_main(this, [=] { removeOldInstance(label); }); } } void ProgressWidget::Row::removeOldInstance(QPointer label) { const auto i = ranges::find(_old, label, [](const Instance &instance) { return Ui::MakeWeak(instance.label->entity()); }); if (i != end(_old)) { _old.erase(i); } } int ProgressWidget::Row::resizeGetHeight(int newWidth) { updateControlsGeometry(newWidth); return st::exportProgressRowHeight; } void ProgressWidget::Row::paintEvent(QPaintEvent *e) { Painter p(this); const auto thickness = st::exportProgressWidth; const auto top = height() - thickness; p.fillRect(0, top, width(), thickness, st::shadowFg); for (const auto &instance : _old) { paintInstance(p, instance); } paintInstance(p, _current); } void ProgressWidget::Row::paintInstance(Painter &p, const Instance &data) { const auto opacity = data.opacity.value(data.hiding ? 0. : 1.); if (!opacity) { return; } p.setOpacity(opacity); const auto thickness = st::exportProgressWidth; const auto top = height() - thickness; const auto till = qRound(data.progress.value(data.value) * width()); if (till > 0) { p.fillRect(0, top, till, thickness, st::exportProgressFg); } if (till < width()) { const auto left = width() - till; p.fillRect(till, top, left, thickness, st::exportProgressBg); } } void ProgressWidget::Row::updateControlsGeometry(int newWidth) { updateInstanceGeometry(_current, newWidth); for (const auto &instance : _old) { updateInstanceGeometry(instance, newWidth); } } void ProgressWidget::Row::updateInstanceGeometry( const Instance &instance, int newWidth) { if (!instance.label) { return; } instance.info->resizeToNaturalWidth(newWidth); instance.label->resizeToWidth(newWidth - instance.info->width()); instance.info->moveToRight(0, 0, newWidth); instance.label->moveToLeft(0, 0, newWidth); } ProgressWidget::ProgressWidget( QWidget *parent, rpl::producer content) : RpWidget(parent) , _body(this) , _skipChat(base::make_unique_q>( _body->add(object_ptr( _body.data(), st::defaultLinkButton.font->height + st::exportProgressRowSkip)), object_ptr( this, tr::lng_export_skip_chat(tr::now), st::defaultLinkButton))) , _fileShowSkipTimer([=] { _skipFile->show(anim::type::normal); }) { widthValue( ) | rpl::start_with_next([=](int width) { _body->resizeToWidth(width); _body->moveToLeft(0, 0); }, _body->lifetime()); auto skipFileWrap = _body->add(object_ptr( _body.data(), st::defaultLinkButton.font->height + st::exportProgressRowSkip)); _skipFile = base::make_unique_q>( skipFileWrap, object_ptr( this, tr::lng_export_skip_file(tr::now), st::defaultLinkButton)); _skipFile->hide(anim::type::instant); _skipFile->moveToLeft(st::exportProgressRowPadding.left(), 0); _about = _body->add( object_ptr( this, tr::lng_export_progress(tr::now), st::exportAboutLabel), st::exportAboutPadding); std::move( content ) | rpl::start_with_next([=](Content &&content) { updateState(std::move(content)); }, lifetime()); _cancel = base::make_unique_q( this, tr::lng_export_stop(), st::exportCancelButton); setupBottomButton(_cancel.get()); } rpl::producer ProgressWidget::skipChatClicks() const { return _skipChat->entity()->clicks( ) | rpl::map([=] { return _chatRandomId; }); } rpl::producer ProgressWidget::skipFileClicks() const { return _skipFile->entity()->clicks( ) | rpl::map([=] { return _fileRandomId; }); } rpl::producer<> ProgressWidget::cancelClicks() const { return _cancel ? (_cancel->clicks() | rpl::to_empty) : (rpl::never<>() | rpl::type_erased()); } rpl::producer<> ProgressWidget::doneClicks() const { return _doneClicks.events(); } void ProgressWidget::setupBottomButton(not_null button) { button->show(); sizeValue( ) | rpl::start_with_next([=](QSize size) { button->move( (size.width() - button->width()) / 2, (size.height() - st::exportCancelBottom - button->height())); }, button->lifetime()); } void ProgressWidget::updateState(Content &&content) { if (!content.rows.empty() && content.rows[0].id == Content::kDoneId) { showDone(); } const auto wasCount = _rows.size(); auto index = 0; for (auto &row : content.rows) { if (index < _rows.size()) { _rows[index]->updateData(std::move(row)); } else { if (index > 0) { _body->insert( index * 2 - 1, object_ptr( this, st::exportProgressRowSkip)); } _rows.push_back(_body->insert( index * 2, object_ptr(this, std::move(row)), st::exportProgressRowPadding)); _rows.back()->show(); } ++index; } const auto fileRandomId = !content.rows.empty() ? content.rows.back().randomId : uint64(0); if (_fileRandomId != fileRandomId) { _fileShowSkipTimer.cancel(); _skipFile->hide(anim::type::normal); _fileRandomId = fileRandomId; if (_fileRandomId) { _fileShowSkipTimer.callOnce(kShowSkipFileTimeout); } } for (const auto count = _rows.size(); index != count; ++index) { _rows[index]->updateData(Content::Row()); } if (_rows.size() != wasCount) { _body->resizeToWidth(width()); } } void ProgressWidget::showDone() { _cancel = nullptr; _skipFile->hide(anim::type::instant); _fileShowSkipTimer.cancel(); _about->setText(tr::lng_export_about_done(tr::now)); _done = base::make_unique_q( this, tr::lng_export_done(), st::exportDoneButton); const auto desired = std::min( st::exportDoneButton.font->width(tr::lng_export_done(tr::now).toUpper()) + st::exportDoneButton.height - st::exportDoneButton.font->height, st::exportPanelSize.width() - 2 * st::exportCancelBottom); if (_done->width() < desired) { _done->setFullWidth(desired); } _done->clicks( ) | rpl::to_empty | rpl::start_to_stream(_doneClicks, _done->lifetime()); setupBottomButton(_done.get()); } ProgressWidget::~ProgressWidget() = default; } // namespace View } // namespace Export