/* 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 "window/window_main_menu.h" #include "window/themes/window_theme.h" #include "window/window_peer_menu.h" #include "window/window_session_controller.h" #include "window/window_controller.h" #include "ui/chat/chat_theme.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout_reorder.h" #include "ui/text/format_values.h" // Ui::FormatPhone #include "ui/text/text_utilities.h" #include "ui/special_buttons.h" #include "ui/empty_userpic.h" #include "dialogs/ui/dialogs_layout.h" #include "base/call_delayed.h" #include "mainwindow.h" #include "storage/localstorage.h" #include "storage/storage_account.h" #include "support/support_templates.h" #include "settings/settings_common.h" #include "settings/settings_calls.h" #include "settings/settings_information.h" #include "base/qt_signal_producer.h" #include "boxes/about_box.h" #include "ui/boxes/confirm_box.h" #include "boxes/peer_list_controllers.h" #include "calls/calls_box_controller.h" #include "lang/lang_keys.h" #include "core/click_handler_types.h" #include "core/core_settings.h" #include "core/application.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" #include "main/main_domain.h" #include "mtproto/mtp_instance.h" #include "mtproto/mtproto_config.h" #include "data/data_folder.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/data_changes.h" #include "mainwidget.h" #include "styles/style_window.h" #include "styles/style_widgets.h" #include "styles/style_dialogs.h" #include "styles/style_settings.h" #include "styles/style_boxes.h" #include "styles/style_info.h" // infoTopBarMenu #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include #include #include #include namespace Window { namespace { void ShowCallsBox(not_null window) { auto controller = std::make_unique(window); const auto initBox = [ window, controller = controller.get() ](not_null box) { box->addButton(tr::lng_close(), [=] { box->closeBox(); }); using MenuPointer = base::unique_qptr; const auto menu = std::make_shared(); const auto menuButton = box->addTopButton(st::infoTopBarMenu); menuButton->setClickedCallback([=] { *menu = base::make_unique_q( menuButton, st::popupMenuWithIcons); const auto showSettings = [=] { window->showSettings( Settings::Calls::Id(), Window::SectionShow(anim::type::instant)); }; const auto clearAll = crl::guard(box, [=] { box->getDelegate()->show(Box(Calls::ClearCallsBox, window)); }); (*menu)->addAction( tr::lng_settings_section_call_settings(tr::now), showSettings, &st::menuIconSettings); if (controller->delegate()->peerListFullRowsCount() > 0) { (*menu)->addAction( tr::lng_call_box_clear_all(tr::now), clearAll, &st::menuIconDelete); } (*menu)->popup(QCursor::pos()); return true; }); }; window->show(Box(std::move(controller), initBox)); } } // namespace class MainMenu::ToggleAccountsButton final : public Ui::AbstractButton { public: explicit ToggleAccountsButton(QWidget *parent); [[nodiscard]] int rightSkip() const { return _rightSkip; } private: void paintEvent(QPaintEvent *e) override; void paintUnreadBadge(Painter &p); void validateUnreadBadge(); [[nodiscard]] QString computeUnreadBadge() const; int _rightSkip = 0; Ui::Animations::Simple _toggledAnimation; bool _toggled = false; QString _unreadBadge; bool _unreadBadgeStale = false; }; class MainMenu::ResetScaleButton final : public Ui::AbstractButton { public: ResetScaleButton(QWidget *parent); protected: void paintEvent(QPaintEvent *e) override; static constexpr auto kText = "100%"; }; MainMenu::ToggleAccountsButton::ToggleAccountsButton(QWidget *parent) : AbstractButton(parent) { rpl::single(rpl::empty) | rpl::then( Core::App().unreadBadgeChanges() ) | rpl::start_with_next([=] { _unreadBadgeStale = true; if (!_toggled) { validateUnreadBadge(); update(); } }, lifetime()); auto &settings = Core::App().settings(); if (Core::App().domain().accounts().size() < 2 && settings.mainMenuAccountsShown()) { settings.setMainMenuAccountsShown(false); } settings.mainMenuAccountsShownValue( ) | rpl::filter([=](bool value) { return (_toggled != value); }) | rpl::start_with_next([=](bool value) { _toggled = value; _toggledAnimation.start( [=] { update(); }, _toggled ? 0. : 1., _toggled ? 1. : 0., st::slideWrapDuration); validateUnreadBadge(); }, lifetime()); _toggledAnimation.stop(); } void MainMenu::ToggleAccountsButton::paintEvent(QPaintEvent *e) { auto p = Painter(this); const auto toggled = _toggledAnimation.value(_toggled ? 1. : 0.); const auto x = 0. + width() - st::mainMenuTogglePosition.x(); const auto y = 0. + height() - st::mainMenuTogglePosition.y(); const auto size = st::mainMenuToggleSize; const auto size2 = size / 2.; const auto sqrt2 = sqrt(2.); const auto stroke = (st::mainMenuToggleFourStrokes / 4.) / sqrt2; const auto left = x - size; const auto right = x + size; const auto bottom = y + size2; constexpr auto kPointCount = 6; std::array points = { { { left - stroke, bottom - stroke }, { x, bottom - stroke - size - stroke }, { right + stroke, bottom - stroke }, { right - stroke, bottom + stroke }, { x, bottom + stroke - size + stroke }, { left + stroke, bottom + stroke } } }; const auto alpha = (toggled - 1.) * M_PI; const auto cosalpha = cos(alpha); const auto sinalpha = sin(alpha); for (auto &point : points) { auto px = point.x() - x; auto py = point.y() - y; point.setX(x + px * cosalpha - py * sinalpha); point.setY(y + py * cosalpha + px * sinalpha); } QPainterPath path; path.moveTo(points[0]); for (int i = 1; i != kPointCount; ++i) { path.lineTo(points[i]); } path.lineTo(points[0]); auto hq = PainterHighQualityEnabler(p); p.fillPath(path, st::windowSubTextFg); paintUnreadBadge(p); } void MainMenu::ToggleAccountsButton::paintUnreadBadge(Painter &p) { const auto progress = 1. - _toggledAnimation.value(_toggled ? 1. : 0.); if (!progress) { return; } validateUnreadBadge(); if (_unreadBadge.isEmpty()) { return; } auto st = Settings::BadgeStyle(); const auto right = width() - st::mainMenuTogglePosition.x() - st::mainMenuToggleSize * 3; const auto top = height() - st::mainMenuTogglePosition.y() - st::mainMenuBadgeSize / 2; p.setOpacity(progress); Dialogs::Ui::PaintUnreadBadge(p, _unreadBadge, right, top, st); } void MainMenu::ToggleAccountsButton::validateUnreadBadge() { const auto base = st::mainMenuTogglePosition.x() + 2 * st::mainMenuToggleSize; if (_toggled) { _rightSkip = base; return; } else if (!_unreadBadgeStale) { return; } _unreadBadge = computeUnreadBadge(); _rightSkip = base; if (!_unreadBadge.isEmpty()) { const auto st = Settings::BadgeStyle(); _rightSkip += 2 * st::mainMenuToggleSize + Dialogs::Ui::CountUnreadBadgeSize(_unreadBadge, st).width(); } } QString MainMenu::ToggleAccountsButton::computeUnreadBadge() const { const auto state = OtherAccountsUnreadStateCurrent(); return state.allMuted ? QString() : (state.count > 0) ? Lang::FormatCountToShort(state.count).string : QString(); } MainMenu::ResetScaleButton::ResetScaleButton(QWidget *parent) : AbstractButton(parent) { const auto margin = st::mainMenuCloudButton.height - st::mainMenuCloudSize; const auto textWidth = st::mainMenuResetScaleFont->width(kText); const auto innerWidth = st::mainMenuResetScaleLeft + textWidth + st::mainMenuResetScaleRight; const auto width = margin + innerWidth; resize(width, st::mainMenuCloudButton.height); } void MainMenu::ResetScaleButton::paintEvent(QPaintEvent *e) { Painter p(this); const auto innerHeight = st::mainMenuCloudSize; const auto radius = innerHeight / 2; const auto margin = st::mainMenuCloudButton.height - st::mainMenuCloudSize; const auto textWidth = st::mainMenuResetScaleFont->width(kText); const auto innerWidth = st::mainMenuResetScaleLeft + textWidth + st::mainMenuResetScaleRight; const auto left = margin / 2; const auto top = margin / 2; p.setPen(Qt::NoPen); p.setBrush(st::mainMenuCloudBg); p.drawRoundedRect(left, top, innerWidth, innerHeight, radius, radius); st::settingsIconInterfaceScale.paint( p, left + st::mainMenuResetScaleIconLeft, top + ((innerHeight - st::settingsIconInterfaceScale.height()) / 2), width(), st::mainMenuCloudFg->c); p.setFont(st::mainMenuResetScaleFont); p.setPen(st::mainMenuCloudFg); p.drawText( left + st::mainMenuResetScaleLeft, top + st::mainMenuResetScaleTop + st::mainMenuResetScaleFont->ascent, kText); } MainMenu::MainMenu( QWidget *parent, not_null controller) : LayerWidget(parent) , _controller(controller) , _userpicButton( this, _controller, _controller->session().user(), Ui::UserpicButton::Role::Custom, st::mainMenuUserpic) , _toggleAccounts(this) , _scroll(this, st::defaultSolidScroll) , _inner(_scroll->setOwnedWidget( object_ptr(_scroll.data()))) , _topShadowSkip(_inner->add( object_ptr(_inner.get(), st::lineWidth))) , _accounts(_inner->add(object_ptr>( _inner.get(), object_ptr(_inner.get())))) , _shadow(_inner->add(object_ptr>( _inner.get(), object_ptr(_inner.get())))) , _menu(_inner->add( object_ptr(_inner.get()), { 0, st::mainMenuSkip, 0, 0 })) , _footer(_inner->add(object_ptr(_inner.get()))) , _telegram( Ui::CreateChild(_footer.get(), st::mainMenuTelegramLabel)) , _version( Ui::CreateChild( _footer.get(), st::mainMenuVersionLabel)) { setAttribute(Qt::WA_OpaquePaintEvent); setupUserpicButton(); setupAccountsToggle(); setupAccounts(); setupArchive(); setupMenu(); const auto shadow = Ui::CreateChild(this); widthValue( ) | rpl::start_with_next([=](int width) { const auto line = st::lineWidth; shadow->setGeometry(0, st::mainMenuCoverHeight - line, width, line); }, shadow->lifetime()); _nightThemeSwitch.setCallback([this] { Expects(_nightThemeToggle != nullptr); const auto nightMode = Window::Theme::IsNightMode(); if (_nightThemeToggle->toggled() != nightMode) { Window::Theme::ToggleNightMode(); Window::Theme::KeepApplied(); } }); _footer->heightValue( ) | rpl::start_with_next([=] { _telegram->moveToLeft(st::mainMenuFooterLeft, _footer->height() - st::mainMenuTelegramBottom - _telegram->height()); _version->moveToLeft(st::mainMenuFooterLeft, _footer->height() - st::mainMenuVersionBottom - _version->height()); }, _footer->lifetime()); rpl::combine( heightValue(), _inner->heightValue() ) | rpl::start_with_next([=] { updateInnerControlsGeometry(); }, _inner->lifetime()); parentResized(); _telegram->setMarkedText(Ui::Text::Link( qsl("Telegram Desktop"), qsl("https://desktop.telegram.org"))); _telegram->setLinksTrusted(); _version->setMarkedText( Ui::Text::Link( tr::lng_settings_current_version( tr::now, lt_version, currentVersionText()), 1) // Link 1. .append(QChar(' ')) .append(QChar(8211)) .append(QChar(' ')) .append(Ui::Text::Link(tr::lng_menu_about(tr::now), 2))); // Link 2. _version->setLink( 1, std::make_shared(Core::App().changelogLink())); _version->setLink( 2, std::make_shared([=] { controller->show(Box()); })); _controller->session().downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); }, lifetime()); _controller->session().changes().peerUpdates( _controller->session().user(), Data::PeerUpdate::Flag::PhoneNumber ) | rpl::start_with_next([=] { updatePhone(); }, lifetime()); updatePhone(); initResetScaleButton(); } void MainMenu::setupArchive() { using namespace Settings; const auto controller = _controller; const auto folder = [=] { return controller->session().data().folderLoaded(Data::Folder::kId); }; const auto showArchive = [=] { if (const auto f = folder()) { controller->openFolder(f); controller->window().hideSettingsAndLayer(); } }; const auto checkArchive = [=] { const auto f = folder(); return f && !f->chatsList()->empty() && controller->session().settings().archiveInMainMenu(); }; const auto wrap = _menu->add( object_ptr>( _menu, object_ptr(_menu))); const auto inner = wrap->entity(); wrap->toggle(checkArchive(), anim::type::instant); const auto button = AddButton( inner, tr::lng_archived_name(), st::mainMenuButton, { &st::settingsIconArchive, kIconGray }); inner->add( object_ptr(inner), { 0, st::mainMenuSkip, 0, st::mainMenuSkip }); button->setAcceptBoth(true); button->clicks( ) | rpl::start_with_next([=](Qt::MouseButton which) { if (which == Qt::LeftButton) { showArchive(); return; } else if (which != Qt::RightButton) { return; } _contextMenu = base::make_unique_q( this, st::popupMenuWithIcons); const auto addAction = PeerMenuCallback([&]( PeerMenuCallback::Args a) { return _contextMenu->addAction( a.text, std::move(a.handler), a.icon); }); const auto hide = [=] { controller->session().settings().setArchiveInMainMenu(false); controller->session().saveSettingsDelayed(); controller->window().hideSettingsAndLayer(); }; addAction( tr::lng_context_archive_to_list(tr::now), std::move(hide), &st::menuIconFromMainMenu); MenuAddMarkAsReadChatListAction( controller, [f = folder()] { return f->chatsList(); }, addAction); _contextMenu->popup(QCursor::pos()); }, button->lifetime()); const auto now = folder(); auto folderValue = now ? (rpl::single(now) | rpl::type_erased()) : controller->session().data().chatsListChanges( ) | rpl::filter([](Data::Folder *folder) { return folder && (folder->id() == Data::Folder::kId); }) | rpl::take(1); using namespace Settings; AddUnreadBadge(button, rpl::single(rpl::empty) | rpl::then(std::move( folderValue ) | rpl::map([=](not_null folder) { return folder->owner().chatsList(folder)->unreadStateChanges(); }) | rpl::flatten_latest() | rpl::to_empty) | rpl::map([=] { const auto loaded = folder(); return UnreadBadge{ loaded ? loaded->chatListUnreadCount() : 0, true, }; })); controller->session().data().chatsListChanges( ) | rpl::filter([](Data::Folder *folder) { return folder && (folder->id() == Data::Folder::kId); }) | rpl::start_with_next([=] { const auto isArchiveVisible = checkArchive(); wrap->toggle(isArchiveVisible, anim::type::normal); if (!isArchiveVisible) { _contextMenu = nullptr; } update(); }, lifetime()); } void MainMenu::setupUserpicButton() { _userpicButton->setClickedCallback([=] { toggleAccounts(); }); _userpicButton->show(); } void MainMenu::toggleAccounts() { auto &settings = Core::App().settings(); const auto shown = !settings.mainMenuAccountsShown(); settings.setMainMenuAccountsShown(shown); Core::App().saveSettingsDelayed(); } void MainMenu::setupAccounts() { const auto inner = _accounts->entity(); inner->add(object_ptr(inner, st::mainMenuSkip)); auto events = Settings::SetupAccounts(inner, _controller); inner->add(object_ptr(inner, st::mainMenuSkip)); std::move( events.currentAccountActivations ) | rpl::start_with_next([=] { closeLayer(); }, inner->lifetime()); _accounts->toggleOn(Core::App().settings().mainMenuAccountsShownValue()); _accounts->finishAnimating(); _shadow->setDuration(0)->toggleOn(_accounts->shownValue()); } void MainMenu::setupAccountsToggle() { _toggleAccounts->show(); _toggleAccounts->setAcceptBoth(); _toggleAccounts->addClickHandler([=](Qt::MouseButton button) { if (button == Qt::LeftButton) { toggleAccounts(); } else if (button == Qt::RightButton) { const auto menu = Ui::CreateChild( _toggleAccounts.data()); menu->addAction(tr::lng_profile_copy_phone(tr::now), [=] { QGuiApplication::clipboard()->setText(_phoneText); }); menu->popup(QCursor::pos()); } }); } void MainMenu::parentResized() { resize(st::mainMenuWidth, parentWidget()->height()); } void MainMenu::setupMenu() { using namespace Settings; const auto controller = _controller; const auto addAction = [&]( rpl::producer text, IconDescriptor &&descriptor) { return AddButton( _menu, std::move(text), st::mainMenuButton, std::move(descriptor)); }; if (!_controller->session().supportMode()) { addAction( tr::lng_create_group_title(), { &st::settingsIconGroup, kIconLightBlue } )->setClickedCallback([=] { controller->showNewGroup(); }); addAction( tr::lng_create_channel_title(), { &st::settingsIconChannel, kIconLightOrange } )->setClickedCallback([=] { controller->showNewChannel(); }); addAction( tr::lng_menu_contacts(), { &st::settingsIconUser, kIconRed } )->setClickedCallback([=] { controller->show(PrepareContactsBox(controller)); }); addAction( tr::lng_menu_calls(), { &st::settingsIconCalls, kIconGreen } )->setClickedCallback([=] { ShowCallsBox(controller); }); addAction( tr::lng_saved_messages(), { &st::settingsIconSavedMessages, kIconLightBlue } )->setClickedCallback([=] { controller->content()->choosePeer( controller->session().userPeerId(), ShowAtUnreadMsgId); }); } else { addAction( tr::lng_profile_add_contact(), { &st::settingsIconUser, kIconRed } )->setClickedCallback([=] { controller->showAddContact(); }); addAction( rpl::single(u"Fix chats order"_q), { &st::settingsIconPin, kIconGreen } )->toggleOn(rpl::single( _controller->session().settings().supportFixChatsOrder() ))->toggledChanges( ) | rpl::start_with_next([=](bool fix) { _controller->session().settings().setSupportFixChatsOrder(fix); _controller->session().saveSettings(); }, _menu->lifetime()); addAction( rpl::single(u"Reload templates"_q), { &st::settingsIconReload, kIconLightBlue } )->setClickedCallback([=] { _controller->session().supportTemplates().reload(); }); } addAction( tr::lng_menu_settings(), { &st::settingsIconSettings, kIconPurple } )->setClickedCallback([=] { controller->showSettings(); }); _nightThemeToggle = addAction( tr::lng_menu_night_mode(), { &st::settingsIconNight, kIconDarkBlue } )->toggleOn(_nightThemeSwitches.events_starting_with( Window::Theme::IsNightMode() )); _nightThemeToggle->toggledChanges( ) | rpl::filter([=](bool night) { return (night != Window::Theme::IsNightMode()); }) | rpl::start_with_next([=](bool night) { if (Window::Theme::Background()->editingTheme()) { _nightThemeSwitches.fire(!night); controller->show(Ui::MakeInformBox( tr::lng_theme_editor_cant_change_theme())); return; } const auto weak = MakeWeak(this); const auto toggle = [=] { if (!weak) { Window::Theme::ToggleNightMode(); Window::Theme::KeepApplied(); } else { _nightThemeSwitch.callOnce(st::mainMenu.itemToggle.duration); } }; Window::Theme::ToggleNightModeWithConfirmation( &_controller->window(), toggle); }, _nightThemeToggle->lifetime()); Core::App().settings().systemDarkModeValue( ) | rpl::start_with_next([=](std::optional darkMode) { const auto darkModeEnabled = Core::App().settings().systemDarkModeEnabled(); if (darkModeEnabled && darkMode.has_value()) { _nightThemeSwitches.fire_copy(*darkMode); } }, _nightThemeToggle->lifetime()); updatePhone(); } void MainMenu::resizeEvent(QResizeEvent *e) { _inner->resizeToWidth(width()); updateControlsGeometry(); } void MainMenu::updateControlsGeometry() { _userpicButton->moveToLeft( st::mainMenuUserpicLeft, st::mainMenuUserpicTop); if (_resetScaleButton) { _resetScaleButton->moveToRight(0, 0); } _toggleAccounts->setGeometry( 0, st::mainMenuCoverNameTop, width(), st::mainMenuCoverHeight - st::mainMenuCoverNameTop); // Allow cover shadow over the scrolled content. const auto top = st::mainMenuCoverHeight - st::lineWidth; _scroll->setGeometry(0, top, width(), height() - top); updateInnerControlsGeometry(); } void MainMenu::updateInnerControlsGeometry() { const auto contentHeight = _accounts->height() + _shadow->height() + st::mainMenuSkip + _menu->height(); const auto available = height() - st::mainMenuCoverHeight - contentHeight; const auto footerHeight = std::max( available, st::mainMenuFooterHeightMin); if (_footer->height() != footerHeight) { _footer->resize(_footer->width(), footerHeight); } } void MainMenu::updatePhone() { _phoneText = Ui::FormatPhone(_controller->session().user()->phone()); update(); } void MainMenu::paintEvent(QPaintEvent *e) { Painter p(this); const auto clip = e->rect(); const auto cover = QRect(0, 0, width(), st::mainMenuCoverHeight); p.fillRect(clip, st::mainMenuBg); if (cover.intersects(clip)) { const auto widthText = width() - st::mainMenuCoverNameLeft - _toggleAccounts->rightSkip(); p.setFont(st::semiboldFont); p.setPen(st::windowBoldFg); _controller->session().user()->nameText().drawLeftElided( p, st::mainMenuCoverNameLeft, st::mainMenuCoverNameTop, widthText, width()); p.setFont(st::mainMenuPhoneFont); p.setPen(st::windowSubTextFg); p.drawTextLeft( st::mainMenuCoverStatusLeft, st::mainMenuCoverStatusTop, width(), _phoneText); } } void MainMenu::initResetScaleButton() { if (!window() || !window()->windowHandle()) { return; } const auto handle = window()->windowHandle(); rpl::single( handle->screen() ) | rpl::then( base::qt_signal_producer(handle, &QWindow::screenChanged) ) | rpl::filter([](QScreen *screen) { return screen != nullptr; }) | rpl::map([](QScreen * screen) { return rpl::single( screen->availableGeometry() ) | rpl::then( base::qt_signal_producer(screen, &QScreen::availableGeometryChanged) ); }) | rpl::flatten_latest( ) | rpl::map([](QRect available) { return (available.width() >= st::windowMinWidth) && (available.height() >= st::windowMinHeight); }) | rpl::distinct_until_changed( ) | rpl::start_with_next([=](bool good) { if (good) { _resetScaleButton.destroy(); } else { _resetScaleButton.create(this); _resetScaleButton->addClickHandler([] { cSetConfigScale(style::kScaleDefault); Local::writeSettings(); Core::Restart(); }); _resetScaleButton->show(); updateControlsGeometry(); } }, lifetime()); } OthersUnreadState OtherAccountsUnreadStateCurrent() { auto &app = Core::App(); const auto active = &app.activeAccount(); auto allMuted = true; for (const auto &[index, account] : app.domain().accounts()) { if (account.get() == active) { continue; } else if (const auto session = account->maybeSession()) { if (!session->data().unreadBadgeMuted()) { allMuted = false; break; } } } return { .count = (app.unreadBadge() - active->session().data().unreadBadge()), .allMuted = allMuted, }; } rpl::producer OtherAccountsUnreadState() { return rpl::single(rpl::empty) | rpl::then( Core::App().unreadBadgeChanges() ) | rpl::map(OtherAccountsUnreadStateCurrent); } } // namespace Window