diff --git a/Telegram/SourceFiles/core/application.cpp b/Telegram/SourceFiles/core/application.cpp index fc8177ccc4..fa91af4c00 100644 --- a/Telegram/SourceFiles/core/application.cpp +++ b/Telegram/SourceFiles/core/application.cpp @@ -414,6 +414,35 @@ void Application::startSystemDarkModeViewer() { } void Application::startTray() { + using WindowRaw = not_null; + const auto enumerate = [=](Fn c) { + if (_primaryWindow) { + c(_primaryWindow.get()); + } + for (const auto &window : ranges::views::values(_secondaryWindows)) { + c(window.get()); + } + }; + _tray->create(); + _tray->aboutToShowRequests( + ) | rpl::start_with_next([=] { + enumerate([&](WindowRaw w) { w->updateIsActive(); }); + _tray->updateMenuText(); + }, _primaryWindow->widget()->lifetime()); + + _tray->showFromTrayRequests( + ) | rpl::start_with_next([=] { + const auto last = _lastActiveWindow; + enumerate([&](WindowRaw w) { w->widget()->showFromTrayMenu(); }); + if (last) { + last->widget()->showFromTrayMenu(); + } + }, _primaryWindow->widget()->lifetime()); + + _tray->hideToTrayRequests( + ) | rpl::start_with_next([=] { + enumerate([&](WindowRaw w) { w->widget()->minimizeToTray(); }); + }, _primaryWindow->widget()->lifetime()); } auto Application::prepareEmojiSourceImages() diff --git a/Telegram/SourceFiles/core/application.h b/Telegram/SourceFiles/core/application.h index ae3240cd9e..056ada5bdf 100644 --- a/Telegram/SourceFiles/core/application.h +++ b/Telegram/SourceFiles/core/application.h @@ -148,6 +148,9 @@ public: [[nodiscard]] Data::DownloadManager &downloadManager() const { return *_downloadManager; } + [[nodiscard]] Tray &tray() const { + return *_tray; + } // Windows interface. bool hasActiveWindow(not_null session) const; diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.h b/Telegram/SourceFiles/platform/mac/tray_mac.h index c2345faa79..584f811247 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.h +++ b/Telegram/SourceFiles/platform/mac/tray_mac.h @@ -9,6 +9,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/platform_tray.h" +#include "base/unique_qptr.h" + +class QMenu; +class QSystemTrayIcon; + namespace Platform { class Tray final { @@ -36,6 +41,11 @@ public: [[nodiscard]] rpl::lifetime &lifetime(); private: + base::unique_qptr _icon; + base::unique_qptr _menu; + + rpl::lifetime _actionsLifetime; + rpl::lifetime _lifetime; }; diff --git a/Telegram/SourceFiles/platform/mac/tray_mac.mm b/Telegram/SourceFiles/platform/mac/tray_mac.mm index 4f800239d2..90996f32d0 100644 --- a/Telegram/SourceFiles/platform/mac/tray_mac.mm +++ b/Telegram/SourceFiles/platform/mac/tray_mac.mm @@ -7,9 +7,222 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "platform/mac/tray_mac.h" +#include "base/qt_signal_producer.h" +#include "core/application.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "ui/ui_utility.h" +#include "styles/style_window.h" + +#include +#include + namespace Platform { +namespace { + +[[nodiscard]] QImage TrayIconBack(bool darkMode) { + static const auto WithColor = [](QColor color) { + return st::macTrayIcon.instance(color, 100); + }; + static const auto DarkModeResult = WithColor({ 255, 255, 255 }); + static const auto LightModeResult = WithColor({ 0, 0, 0, 180 }); + auto result = darkMode ? DarkModeResult : LightModeResult; + result.detach(); + return result; +} + +void PlaceCounter( + QImage &img, + int size, + int count, + style::color bg, + style::color color) { + if (!count) { + return; + } + const auto savedRatio = img.devicePixelRatio(); + img.setDevicePixelRatio(1.); + + { + Painter p(&img); + PainterHighQualityEnabler hq(p); + + const auto cnt = (count < 100) + ? QString("%1").arg(count) + : QString("..%1").arg(count % 100, 2, 10, QChar('0')); + const auto cntSize = cnt.size(); + + p.setBrush(bg); + p.setPen(Qt::NoPen); + int32 fontSize, skip; + if (size == 22) { + skip = 1; + fontSize = 8; + } else { + skip = 2; + fontSize = 16; + } + style::font f(fontSize, 0, 0); + int32 w = f->width(cnt), d, r; + if (size == 22) { + d = (cntSize < 2) ? 3 : 2; + r = (cntSize < 2) ? 6 : 5; + } else { + d = (cntSize < 2) ? 6 : 5; + r = (cntSize < 2) ? 9 : 11; + } + p.drawRoundedRect( + QRect( + size - w - d * 2 - skip, + size - f->height - skip, + w + d * 2, + f->height), + r, + r); + + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setFont(f); + p.setPen(color); + p.drawText( + size - w - d - skip, + size - f->height + f->ascent - skip, + cnt); + } + img.setDevicePixelRatio(savedRatio); +} + +[[nodiscard]] QIcon GenerateIconForTray(int counter, bool muted) { + auto result = QIcon(); + auto lightMode = TrayIconBack(false); + auto darkMode = TrayIconBack(true); + auto lightModeActive = darkMode; + auto darkModeActive = darkMode; + lightModeActive.detach(); + darkModeActive.detach(); + const auto size = 22 * cIntRetinaFactor(); + const auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg); + + const auto &fg = st::trayCounterFg; + const auto &fgInvert = st::trayCounterFgMacInvert; + const auto &bgInvert = st::trayCounterBgMacInvert; + + PlaceCounter(lightMode, size, counter, bg, fg); + PlaceCounter(darkMode, size, counter, bg, muted ? fgInvert : fg); + PlaceCounter(lightModeActive, size, counter, bgInvert, fgInvert); + PlaceCounter(darkModeActive, size, counter, bgInvert, fgInvert); + result.addPixmap(Ui::PixmapFromImage( + std::move(lightMode)), + QIcon::Normal, + QIcon::Off); + result.addPixmap(Ui::PixmapFromImage( + std::move(darkMode)), + QIcon::Normal, + QIcon::On); + result.addPixmap(Ui::PixmapFromImage( + std::move(lightModeActive)), + QIcon::Active, + QIcon::Off); + result.addPixmap(Ui::PixmapFromImage( + std::move(darkModeActive)), + QIcon::Active, + QIcon::On); + return result; +} + +[[nodiscard]] QWidget *Parent() { + Expects(Core::App().primaryWindow() != nullptr); + return Core::App().primaryWindow()->widget(); +} + +} // namespace + Tray::Tray() { } +void Tray::createIcon() { + if (!_icon) { + _icon = base::make_unique_q(Parent()); + updateIcon(); + if (Core::App().isActiveForTrayMenu()) { + _icon->setContextMenu(_menu.get()); + } else { + _icon->setContextMenu(nullptr); + } + _icon->setContextMenu(_menu.get()); // Todo. + // attachToTrayIcon(_icon); + } else { + updateIcon(); + } + + _icon->show(); +} + +void Tray::destroyIcon() { + _icon = nullptr; +} + +void Tray::updateIcon() { + if (_icon) { + const auto counter = Core::App().unreadBadge(); + const auto muted = Core::App().unreadBadgeMuted(); + _icon->setIcon(GenerateIconForTray(counter, muted)); + } +} + +void Tray::createMenu() { + if (!_menu) { + _menu = base::make_unique_q(Parent()); + } +} + +void Tray::destroyMenu() { + if (_menu) { + _menu->clear(); + } + _actionsLifetime.destroy(); +} + +void Tray::addAction(rpl::producer text, Fn &&callback) { + if (!_menu) { + return; + } + + const auto action = _menu->addAction(QString(), std::move(callback)); + std::move( + text + ) | rpl::start_with_next([=](const QString &text) { + action->setText(text); + }, _actionsLifetime); +} + +void Tray::showTrayMessage() const { +} + +bool Tray::hasTrayMessageSupport() const { + return false; +} + +rpl::producer<> Tray::aboutToShowRequests() const { + return _menu + ? base::qt_signal_producer(_menu.get(), &QMenu::aboutToShow) + : rpl::never<>(); +} + +rpl::producer<> Tray::showFromTrayRequests() const { + return rpl::never<>(); +} + +rpl::producer<> Tray::hideToTrayRequests() const { + return rpl::never<>(); +} + +rpl::producer<> Tray::iconClicks() const { + return rpl::never<>(); +} + +rpl::lifetime &Tray::lifetime() { + return _lifetime; +} + } // namespace Platform diff --git a/Telegram/SourceFiles/tray.cpp b/Telegram/SourceFiles/tray.cpp index f608a9ab49..0943cac921 100644 --- a/Telegram/SourceFiles/tray.cpp +++ b/Telegram/SourceFiles/tray.cpp @@ -7,9 +7,151 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "tray.h" +#include "core/application.h" + namespace Core { Tray::Tray() { } +void Tray::create() { + rebuildMenu(); + if (Core::App().settings().workMode() != Settings::WorkMode::WindowOnly) { + _tray.createIcon(); + } + + Core::App().settings().workModeValue( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=]( + Settings::WorkMode previous, + Settings::WorkMode state) { + const auto wasHasIcon = (previous != Settings::WorkMode::WindowOnly); + const auto nowHasIcon = (state != Settings::WorkMode::WindowOnly); + if (wasHasIcon != nowHasIcon) { + if (nowHasIcon) { + _tray.createIcon(); + } else { + _tray.destroyIcon(); + } + } + }, _tray.lifetime()); + + Core::App().passcodeLockChanges( + ) | rpl::start_with_next([=] { + rebuildMenu(); + }, _tray.lifetime()); +} + +void Tray::rebuildMenu() { + _tray.destroyMenu(); + _tray.createMenu(); + + { + auto minimizeText = _textUpdates.events( + ) | rpl::map([=] { + _activeForTrayIconAction = Core::App().isActiveForTrayMenu(); + return _activeForTrayIconAction + ? tr::lng_minimize_to_tray(tr::now) + : tr::lng_open_from_tray(tr::now); + }); + + _tray.addAction( + std::move(minimizeText), + [=] { _minimizeMenuItemClicks.fire({}); }); + } + + if (!Core::App().passcodeLocked()) { + auto notificationsText = _textUpdates.events( + ) | rpl::map([=] { + return Core::App().settings().desktopNotify() + ? tr::lng_disable_notifications_from_tray(tr::now) + : tr::lng_enable_notifications_from_tray(tr::now); + }); + + _tray.addAction( + std::move(notificationsText), + [=] { toggleSoundNotifications(); }); + } + + _tray.addAction(tr::lng_quit_from_tray(), [] { Core::Quit(); }); + + updateMenuText(); +} + +void Tray::updateMenuText() { + _textUpdates.fire({}); +} + +void Tray::updateIconCounters() { + _tray.updateIcon(); +} + +rpl::producer<> Tray::aboutToShowRequests() const { + return _tray.aboutToShowRequests(); +} + +rpl::producer<> Tray::showFromTrayRequests() const { + return rpl::merge( + _tray.showFromTrayRequests(), + _minimizeMenuItemClicks.events() | rpl::filter([=] { + return !_activeForTrayIconAction; + }) + ); +} + +rpl::producer<> Tray::hideToTrayRequests() const { + return rpl::merge( + _tray.hideToTrayRequests(), + _minimizeMenuItemClicks.events() | rpl::filter([=] { + return _activeForTrayIconAction; + }) + ); +} + +void Tray::toggleSoundNotifications() { + auto soundNotifyChanged = false; + auto flashBounceNotifyChanged = false; + auto &settings = Core::App().settings(); + settings.setDesktopNotify(!settings.desktopNotify()); + if (settings.desktopNotify()) { + if (settings.rememberedSoundNotifyFromTray() + && !settings.soundNotify()) { + settings.setSoundNotify(true); + settings.setRememberedSoundNotifyFromTray(false); + soundNotifyChanged = true; + } + if (settings.rememberedFlashBounceNotifyFromTray() + && !settings.flashBounceNotify()) { + settings.setFlashBounceNotify(true); + settings.setRememberedFlashBounceNotifyFromTray(false); + flashBounceNotifyChanged = true; + } + } else { + if (settings.soundNotify()) { + settings.setSoundNotify(false); + settings.setRememberedSoundNotifyFromTray(true); + soundNotifyChanged = true; + } else { + settings.setRememberedSoundNotifyFromTray(false); + } + if (settings.flashBounceNotify()) { + settings.setFlashBounceNotify(false); + settings.setRememberedFlashBounceNotifyFromTray(true); + flashBounceNotifyChanged = true; + } else { + settings.setRememberedFlashBounceNotifyFromTray(false); + } + } + Core::App().saveSettingsDelayed(); + using Change = Window::Notifications::ChangeType; + auto ¬ifications = Core::App().notifications(); + notifications.notifySettingsChanged(Change::DesktopEnabled); + if (soundNotifyChanged) { + notifications.notifySettingsChanged(Change::SoundEnabled); + } + if (flashBounceNotifyChanged) { + notifications.notifySettingsChanged(Change::FlashBounceEnabled); + } +} + } // namespace Core diff --git a/Telegram/SourceFiles/tray.h b/Telegram/SourceFiles/tray.h index 3e5a0c9d60..7b279494df 100644 --- a/Telegram/SourceFiles/tray.h +++ b/Telegram/SourceFiles/tray.h @@ -15,9 +15,24 @@ class Tray final { public: Tray(); + void create(); + void updateMenuText(); + void updateIconCounters(); + + [[nodiscard]] rpl::producer<> aboutToShowRequests() const; + [[nodiscard]] rpl::producer<> showFromTrayRequests() const; + [[nodiscard]] rpl::producer<> hideToTrayRequests() const; + private: + void rebuildMenu(); + void toggleSoundNotifications(); + Platform::Tray _tray; + bool _activeForTrayIconAction = false; + rpl::event_stream<> _textUpdates; + rpl::event_stream<> _minimizeMenuItemClicks; + }; } // namespace Core diff --git a/Telegram/SourceFiles/window/main_window.cpp b/Telegram/SourceFiles/window/main_window.cpp index 37d9d704bb..9ec29ed7dd 100644 --- a/Telegram/SourceFiles/window/main_window.cpp +++ b/Telegram/SourceFiles/window/main_window.cpp @@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mainwindow.h" #include "mainwidget.h" // session->content()->windowShown(). #include "facades.h" +#include "tray.h" #include "styles/style_widgets.h" #include "styles/style_window.h" @@ -811,6 +812,7 @@ void MainWindow::updateUnreadCounter() { const auto counter = Core::App().unreadBadge(); setTitle((counter > 0) ? qsl("Telegram (%1)").arg(counter) : qsl("Telegram")); + Core::App().tray().updateIconCounters(); unreadCounterChangedHook(); }