/* 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 "info/info_top_bar.h" #include #include #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" #include "lang/lang_keys.h" #include "lang/lang_numbers_animation.h" #include "info/info_wrap_widget.h" #include "info/info_controller.h" #include "info/profile/info_profile_values.h" #include "storage/storage_shared_media.h" #include "boxes/delete_messages_box.h" #include "boxes/peer_list_controllers.h" #include "mainwidget.h" #include "main/main_session.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/shadow.h" #include "ui/wrap/fade_wrap.h" #include "ui/wrap/padding_wrap.h" #include "ui/search_field_controller.h" #include "window/window_peer_menu.h" #include "data/data_session.h" #include "data/data_channel.h" #include "data/data_user.h" #include "styles/style_dialogs.h" #include "styles/style_info.h" namespace Info { TopBar::TopBar( QWidget *parent, not_null navigation, const style::InfoTopBar &st, SelectedItems &&selectedItems) : RpWidget(parent) , _navigation(navigation) , _st(st) , _selectedItems(Section::MediaType::kCount) { if (_st.radius) { _roundRect.emplace(_st.radius, _st.bg); } setAttribute(Qt::WA_OpaquePaintEvent, !_roundRect); setSelectedItems(std::move(selectedItems)); updateControlsVisibility(anim::type::instant); } template void TopBar::registerUpdateControlCallback( QObject *guard, Callback &&callback) { _updateControlCallbacks[guard] =[ weak = Ui::MakeWeak(guard), callback = std::forward(callback) ](anim::type animated) { if (!weak) { return false; } callback(animated); return true; }; } template void TopBar::registerToggleControlCallback( Widget *widget, IsVisible &&callback) { registerUpdateControlCallback(widget, [ widget, isVisible = std::forward(callback) ](anim::type animated) { widget->toggle(isVisible(), animated); }); } void TopBar::setTitle(TitleDescriptor descriptor) { if (_title) { delete _title; } if (_subtitle) { delete _subtitle; } const auto withSubtitle = !!descriptor.subtitle; if (withSubtitle) { _subtitle = Ui::CreateChild>( this, object_ptr( this, std::move(descriptor.subtitle), _st.subtitle), st::infoTopBarScale); _subtitle->setDuration(st::infoTopBarDuration); _subtitle->toggle( !selectionMode() && !storiesTitle(), anim::type::instant); registerToggleControlCallback(_subtitle.data(), [=] { return !selectionMode() && !storiesTitle() && !searchMode(); }); } _title = Ui::CreateChild>( this, object_ptr( this, std::move(descriptor.title), withSubtitle ? _st.titleWithSubtitle : _st.title), st::infoTopBarScale); _title->setDuration(st::infoTopBarDuration); _title->toggle( !selectionMode() && !storiesTitle(), anim::type::instant); registerToggleControlCallback(_title.data(), [=] { return !selectionMode() && !storiesTitle() && !searchMode(); }); if (_back) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); if (_subtitle) { _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); } } updateControlsGeometry(width()); } void TopBar::enableBackButton() { if (_back) { return; } _back = Ui::CreateChild>( this, object_ptr(this, _st.back), st::infoTopBarScale); _back->setDuration(st::infoTopBarDuration); _back->toggle(!selectionMode(), anim::type::instant); _back->entity()->clicks( ) | rpl::to_empty | rpl::start_to_stream(_backClicks, _back->lifetime()); registerToggleControlCallback(_back.data(), [=] { return !selectionMode(); }); if (_title) { _title->setAttribute(Qt::WA_TransparentForMouseEvents); } if (_subtitle) { _subtitle->setAttribute(Qt::WA_TransparentForMouseEvents); } if (_storiesWrap) { _storiesWrap->raise(); } updateControlsGeometry(width()); } void TopBar::createSearchView( not_null controller, rpl::producer &&shown, bool startsFocused) { setSearchField( controller->createField(this, _st.searchRow.field), std::move(shown), startsFocused); } bool TopBar::focusSearchField() { if (_searchField && _searchField->isVisible()) { _searchField->setFocus(); return true; } return false; } Ui::FadeWrap *TopBar::pushButton( base::unique_qptr button) { auto wrapped = base::make_unique_q>( this, object_ptr::fromRaw(button.release()), st::infoTopBarScale); auto weak = wrapped.get(); _buttons.push_back(std::move(wrapped)); weak->setDuration(st::infoTopBarDuration); registerToggleControlCallback(weak, [=] { return !selectionMode() && !_searchModeEnabled; }); weak->toggle( !selectionMode() && !_searchModeEnabled, anim::type::instant); weak->widthValue( ) | rpl::start_with_next([this] { updateControlsGeometry(width()); }, lifetime()); return weak; } void TopBar::forceButtonVisibility( Ui::FadeWrap *button, rpl::producer shown) { _updateControlCallbacks.erase(button); button->toggleOn(std::move(shown)); } void TopBar::setSearchField( base::unique_qptr field, rpl::producer &&shown, bool startsFocused) { Expects(field != nullptr); createSearchView(field.release(), std::move(shown), startsFocused); } void TopBar::clearSearchField() { _searchView = nullptr; } void TopBar::createSearchView( not_null field, rpl::producer &&shown, bool startsFocused) { _searchView = base::make_unique_q( this, _st.searchRow.height); auto wrap = _searchView.get(); registerUpdateControlCallback(wrap, [=](anim::type) { wrap->setVisible(!selectionMode() && _searchModeAvailable); }); _searchField = field; auto fieldWrap = Ui::CreateChild>( wrap, object_ptr::fromRaw(field), st::infoTopBarScale); fieldWrap->setDuration(st::infoTopBarDuration); auto focusLifetime = field->lifetime().make_state(); registerUpdateControlCallback(fieldWrap, [=](anim::type animated) { auto fieldShown = !selectionMode() && searchMode(); if (!fieldShown && field->hasFocus()) { setFocus(); } fieldWrap->toggle(fieldShown, animated); if (fieldShown) { *focusLifetime = field->shownValue() | rpl::filter([](bool shown) { return shown; }) | rpl::take(1) | rpl::start_with_next([=] { field->setFocus(); }); } else { focusLifetime->destroy(); } }); auto button = base::make_unique_q(this, _st.search); auto search = button.get(); search->addClickHandler([=] { showSearch(); }); auto searchWrap = pushButton(std::move(button)); registerToggleControlCallback(searchWrap, [=] { return !selectionMode() && _searchModeAvailable && !_searchModeEnabled; }); auto cancel = Ui::CreateChild( wrap, _st.searchRow.fieldCancel); registerToggleControlCallback(cancel, [=] { return !selectionMode() && searchMode(); }); auto cancelSearch = [=] { if (!field->getLastText().isEmpty()) { field->setText(QString()); } else { _searchModeEnabled = false; updateControlsVisibility(anim::type::normal); } }; cancel->addClickHandler(cancelSearch); field->cancelled( ) | rpl::start_with_next(cancelSearch, field->lifetime()); wrap->widthValue( ) | rpl::start_with_next([=](int newWidth) { auto availableWidth = newWidth - _st.searchRow.fieldCancelSkip; fieldWrap->resizeToWidth(availableWidth); fieldWrap->moveToLeft( _st.searchRow.padding.left(), _st.searchRow.padding.top()); cancel->moveToRight(0, 0); }, wrap->lifetime()); widthValue( ) | rpl::start_with_next([=](int newWidth) { auto left = _back ? _st.back.width : _st.titlePosition.x(); wrap->setGeometryToLeft( left, 0, newWidth - left, wrap->height(), newWidth); }, wrap->lifetime()); field->alive( ) | rpl::start_with_done([=] { field->setParent(nullptr); removeButton(search); clearSearchField(); }, _searchView->lifetime()); _searchModeEnabled = !field->getLastText().isEmpty() || startsFocused; updateControlsVisibility(anim::type::instant); std::move( shown ) | rpl::start_with_next([=](bool visible) { auto alreadyInSearch = !field->getLastText().isEmpty(); _searchModeAvailable = visible || alreadyInSearch; updateControlsVisibility(anim::type::instant); }, wrap->lifetime()); } void TopBar::showSearch() { _searchModeEnabled = true; updateControlsVisibility(anim::type::normal); } void TopBar::removeButton(not_null button) { _buttons.erase( std::remove(_buttons.begin(), _buttons.end(), button), _buttons.end()); } int TopBar::resizeGetHeight(int newWidth) { updateControlsGeometry(newWidth); return _st.height; } void TopBar::updateControlsGeometry(int newWidth) { updateDefaultControlsGeometry(newWidth); updateSelectionControlsGeometry(newWidth); updateStoriesGeometry(newWidth); } void TopBar::updateDefaultControlsGeometry(int newWidth) { auto right = 0; for (auto &button : _buttons) { if (!button) { continue; } button->moveToRight(right, 0, newWidth); right += button->width(); } if (_back) { _back->setGeometryToLeft( 0, 0, newWidth - right, _back->height(), newWidth); } if (_title) { const auto x = _back ? _st.back.width : _subtitle ? _st.titleWithSubtitlePosition.x() : _st.titlePosition.x(); const auto y = _subtitle ? _st.titleWithSubtitlePosition.y() : _st.titlePosition.y(); _title->moveToLeft(x, y, newWidth); if (_subtitle) { _subtitle->moveToLeft( _back ? _st.back.width : _st.subtitlePosition.x(), _st.subtitlePosition.y(), newWidth); } } } void TopBar::updateSelectionControlsGeometry(int newWidth) { if (!_selectionText) { return; } auto right = _st.mediaActionsSkip; if (_canDelete) { _delete->moveToRight(right, 0, newWidth); right += _delete->width(); } if (_canToggleStoryPin) { _toggleStoryPin->moveToRight(right, 0, newWidth); right += _toggleStoryPin->width(); } if (_canForward) { _forward->moveToRight(right, 0, newWidth); right += _forward->width(); } auto left = 0; _cancelSelection->moveToLeft(left, 0); left += _cancelSelection->width(); const auto top = 0; const auto availableWidth = newWidth - left - right; _selectionText->resizeToNaturalWidth(availableWidth); _selectionText->moveToLeft( left, top, newWidth); } void TopBar::updateStoriesGeometry(int newWidth) { if (!_stories) { return; } auto right = 0; for (auto &button : _buttons) { if (!button) { continue; } button->moveToRight(right, 0, newWidth); right += button->width(); } const auto &small = st::dialogsStories; const auto wrapLeft = (_back ? _st.back.width : 0); const auto left = _back ? 0 : (_st.titlePosition.x() - small.left - small.photoLeft); const auto height = small.photo + 2 * small.photoTop; const auto top = _st.titlePosition.y() + (_st.title.style.font->height - height) / 2; _stories->setLayoutConstraints({ left, top }, style::al_left); _storiesWrap->move(wrapLeft, 0); } void TopBar::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto highlight = _a_highlight.value(_highlight ? 1. : 0.); if (_highlight && !_a_highlight.animating()) { _highlight = false; startHighlightAnimation(); } if (!_roundRect) { const auto brush = anim::brush(_st.bg, _st.highlightBg, highlight); p.fillRect(e->rect(), brush); } else if (highlight > 0.) { p.setPen(Qt::NoPen); p.setBrush(anim::brush(_st.bg, _st.highlightBg, highlight)); p.drawRoundedRect( rect() + style::margins(0, 0, 0, _st.radius * 2), _st.radius, _st.radius); } else { _roundRect->paintSomeRounded( p, rect(), RectPart::TopLeft | RectPart::TopRight); } } void TopBar::highlight() { _highlight = true; startHighlightAnimation(); } void TopBar::startHighlightAnimation() { _a_highlight.start( [this] { update(); }, _highlight ? 0. : 1., _highlight ? 1. : 0., _st.highlightDuration); } void TopBar::updateControlsVisibility(anim::type animated) { for (auto i = _updateControlCallbacks.begin(); i != _updateControlCallbacks.end();) { auto &&[widget, callback] = *i; if (!callback(animated)) { i = _updateControlCallbacks.erase(i); } else { ++i; } } } void TopBar::setStories(rpl::producer content) { _storiesLifetime.destroy(); delete _storiesWrap.data(); if (content) { using namespace Dialogs::Stories; auto last = std::move( content ) | rpl::start_spawning(_storiesLifetime); _storiesWrap = _storiesLifetime.make_state< Ui::FadeWrap >(this, object_ptr(this), st::infoTopBarScale); registerToggleControlCallback( _storiesWrap.data(), [this] { return _storiesCount > 0; }); _storiesWrap->toggle(false, anim::type::instant); _storiesWrap->setDuration(st::infoTopBarDuration); const auto button = _storiesWrap->entity(); const auto stories = Ui::CreateChild( button, st::dialogsStoriesListInfo, rpl::duplicate( last ) | rpl::filter([](const Content &content) { return !content.elements.empty(); })); const auto label = Ui::CreateChild( button, QString(), _st.title); stories->setAttribute(Qt::WA_TransparentForMouseEvents); label->setAttribute(Qt::WA_TransparentForMouseEvents); stories->geometryValue( ) | rpl::start_with_next([=](QRect geometry) { const auto skip = _st.title.style.font->spacew; label->move( geometry.x() + geometry.width() + skip, _st.titlePosition.y()); }, label->lifetime()); rpl::combine( _storiesWrap->positionValue(), label->geometryValue() ) | rpl::start_with_next([=] { button->resize( label->x() + label->width() + _st.titlePosition.x(), _st.height); }, button->lifetime()); _stories = stories; _stories->clicks( ) | rpl::start_to_stream(_storyClicks, _stories->lifetime()); button->setClickedCallback([=] { _storyClicks.fire({}); }); rpl::duplicate( last ) | rpl::start_with_next([=](const Content &content) { const auto count = content.total; if (_storiesCount != count) { const auto was = (_storiesCount > 0); _storiesCount = count; const auto now = (_storiesCount > 0); if (was != now) { updateControlsVisibility(anim::type::normal); } if (now) { label->setText( tr::lng_contacts_stories_status( tr::now, lt_count, _storiesCount)); } updateControlsGeometry(width()); } }, _storiesLifetime); _storiesLifetime.add([weak = QPointer(label)] { delete weak.data(); }); } else { _storiesCount = 0; } updateControlsVisibility(anim::type::instant); } void TopBar::setStoriesArchive(bool archive) { _storiesArchive = archive; } void TopBar::setSelectedItems(SelectedItems &&items) { auto wasSelectionMode = selectionMode(); _selectedItems = std::move(items); if (selectionMode()) { if (_selectionText) { updateSelectionState(); if (!wasSelectionMode) { _selectionText->entity()->finishAnimating(); } } else { createSelectionControls(); } } updateControlsVisibility(anim::type::normal); } SelectedItems TopBar::takeSelectedItems() { _canDelete = false; _canForward = false; return std::move(_selectedItems); } rpl::producer TopBar::selectionActionRequests() const { return _selectionActionRequests.events(); } void TopBar::updateSelectionState() { Expects(_selectionText && _delete && _forward && _toggleStoryPin); _canDelete = computeCanDelete(); _canForward = computeCanForward(); _selectionText->entity()->setValue(generateSelectedText()); _delete->toggle(_canDelete, anim::type::instant); _forward->toggle(_canForward, anim::type::instant); _toggleStoryPin->toggle(_canToggleStoryPin, anim::type::instant); updateSelectionControlsGeometry(width()); } void TopBar::createSelectionControls() { auto wrap = [&](auto created) { registerToggleControlCallback( created, [this] { return selectionMode(); }); created->toggle(false, anim::type::instant); return created; }; _canDelete = computeCanDelete(); _canForward = computeCanForward(); _canToggleStoryPin = computeCanToggleStoryPin(); _cancelSelection = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaCancel), st::infoTopBarScale)); _cancelSelection->setDuration(st::infoTopBarDuration); _cancelSelection->entity()->clicks( ) | rpl::map_to( SelectionAction::Clear ) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); _selectionText = wrap(Ui::CreateChild>( this, object_ptr( this, _st.title, _st.titlePosition.y(), generateSelectedText()), st::infoTopBarScale)); _selectionText->setDuration(st::infoTopBarDuration); _selectionText->entity()->resize(0, _st.height); _forward = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaForward), st::infoTopBarScale)); registerToggleControlCallback( _forward.data(), [this] { return selectionMode() && _canForward; }); _forward->setDuration(st::infoTopBarDuration); _forward->entity()->clicks( ) | rpl::map_to( SelectionAction::Forward ) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); _forward->entity()->setVisible(_canForward); _delete = wrap(Ui::CreateChild>( this, object_ptr(this, _st.mediaDelete), st::infoTopBarScale)); registerToggleControlCallback( _delete.data(), [this] { return selectionMode() && _canDelete; }); _delete->setDuration(st::infoTopBarDuration); _delete->entity()->clicks( ) | rpl::map_to( SelectionAction::Delete ) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); _delete->entity()->setVisible(_canDelete); const auto archive = _toggleStoryPin = wrap(Ui::CreateChild>( this, object_ptr( this, _storiesArchive ? _st.storiesSave : _st.storiesArchive), st::infoTopBarScale)); registerToggleControlCallback( _toggleStoryPin.data(), [this] { return selectionMode() && _canToggleStoryPin; }); _toggleStoryPin->setDuration(st::infoTopBarDuration); _toggleStoryPin->entity()->clicks( ) | rpl::map_to( SelectionAction::ToggleStoryPin ) | rpl::start_to_stream( _selectionActionRequests, _cancelSelection->lifetime()); _toggleStoryPin->entity()->setVisible(_canToggleStoryPin); updateControlsGeometry(width()); } bool TopBar::computeCanDelete() const { return ranges::all_of(_selectedItems.list, &SelectedItem::canDelete); } bool TopBar::computeCanForward() const { return ranges::all_of(_selectedItems.list, &SelectedItem::canForward); } bool TopBar::computeCanToggleStoryPin() const { return ranges::all_of( _selectedItems.list, &SelectedItem::canToggleStoryPin); } Ui::StringWithNumbers TopBar::generateSelectedText() const { using Type = Storage::SharedMediaType; const auto phrase = [&] { switch (_selectedItems.type) { case Type::Photo: return tr::lng_media_selected_photo; case Type::GIF: return tr::lng_media_selected_gif; case Type::Video: return tr::lng_media_selected_video; case Type::File: return tr::lng_media_selected_file; case Type::MusicFile: return tr::lng_media_selected_song; case Type::Link: return tr::lng_media_selected_link; case Type::RoundVoiceFile: return tr::lng_media_selected_audio; case Type::PhotoVideo: return tr::lng_stories_row_count; } Unexpected("Type in TopBar::generateSelectedText()"); }(); return phrase( tr::now, lt_count, _selectedItems.list.size(), Ui::StringWithNumbers::FromString); } bool TopBar::selectionMode() const { return !_selectedItems.list.empty(); } bool TopBar::storiesTitle() const { return _storiesCount > 0; } bool TopBar::searchMode() const { return _searchModeAvailable && _searchModeEnabled; } void TopBar::performForward() { _selectionActionRequests.fire(SelectionAction::Forward); } void TopBar::performDelete() { _selectionActionRequests.fire(SelectionAction::Delete); } } // namespace Info