2022-03-23 15:59:53 +00:00
|
|
|
/*
|
|
|
|
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 "platform/win/tray_win.h"
|
|
|
|
|
2022-04-18 13:34:59 +00:00
|
|
|
#include "base/invoke_queued.h"
|
|
|
|
#include "base/qt_signal_producer.h"
|
|
|
|
#include "core/application.h"
|
2023-11-15 10:47:52 +00:00
|
|
|
#include "lang/lang_keys.h"
|
2022-04-18 13:34:59 +00:00
|
|
|
#include "main/main_session.h"
|
|
|
|
#include "storage/localstorage.h"
|
2023-11-04 20:03:44 +00:00
|
|
|
#include "ui/painter.h"
|
2022-04-18 13:34:59 +00:00
|
|
|
#include "ui/ui_utility.h"
|
|
|
|
#include "ui/widgets/popup_menu.h"
|
|
|
|
#include "window/window_controller.h"
|
|
|
|
#include "window/window_session_controller.h"
|
|
|
|
#include "styles/style_window.h"
|
|
|
|
|
2023-08-15 10:11:45 +00:00
|
|
|
#include <qpa/qplatformscreen.h>
|
|
|
|
#include <qpa/qplatformsystemtrayicon.h>
|
|
|
|
#include <qpa/qplatformtheme.h>
|
|
|
|
#include <private/qguiapplication_p.h>
|
|
|
|
#include <private/qhighdpiscaling_p.h>
|
2023-11-04 20:03:44 +00:00
|
|
|
#include <QSvgRenderer>
|
2024-01-03 18:14:34 +00:00
|
|
|
#include <QBuffer>
|
2022-04-18 13:34:59 +00:00
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
namespace Platform {
|
|
|
|
|
2022-04-18 13:34:59 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
constexpr auto kTooltipDelay = crl::time(10000);
|
|
|
|
|
2024-01-03 18:14:34 +00:00
|
|
|
std::optional<bool> DarkTaskbar;
|
|
|
|
bool DarkTasbarValueValid/* = false*/;
|
2023-10-19 07:34:28 +00:00
|
|
|
|
2024-01-03 18:14:34 +00:00
|
|
|
[[nodiscard]] std::optional<bool> ReadDarkTaskbarValue() {
|
2023-10-19 07:34:28 +00:00
|
|
|
const auto keyName = L""
|
|
|
|
"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
|
|
|
const auto valueName = L"SystemUsesLightTheme";
|
|
|
|
auto key = HKEY();
|
|
|
|
auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);
|
|
|
|
if (result != ERROR_SUCCESS) {
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
|
|
|
DWORD value = 0, type = 0, size = sizeof(value);
|
|
|
|
result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);
|
|
|
|
RegCloseKey(key);
|
|
|
|
if (result != ERROR_SUCCESS) {
|
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (value == 0);
|
|
|
|
}
|
|
|
|
|
2024-01-03 18:14:34 +00:00
|
|
|
[[nodiscard]] std::optional<bool> IsDarkTaskbar() {
|
|
|
|
static const auto kSystemVersion = QOperatingSystemVersion::current();
|
|
|
|
static const auto kDarkModeAddedVersion = QOperatingSystemVersion(
|
|
|
|
QOperatingSystemVersion::Windows,
|
|
|
|
10,
|
|
|
|
0,
|
|
|
|
18282);
|
|
|
|
static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);
|
|
|
|
if (!kSupported) {
|
|
|
|
return std::nullopt;
|
|
|
|
} else if (!DarkTasbarValueValid) {
|
|
|
|
DarkTasbarValueValid = true;
|
|
|
|
DarkTaskbar = ReadDarkTaskbarValue();
|
|
|
|
}
|
|
|
|
return DarkTaskbar;
|
|
|
|
}
|
|
|
|
|
2023-11-04 20:03:44 +00:00
|
|
|
[[nodiscard]] QImage MonochromeIconFor(int size, bool darkMode) {
|
|
|
|
Expects(size > 0);
|
|
|
|
|
|
|
|
static const auto Content = [&] {
|
|
|
|
auto f = QFile(u":/gui/icons/tray/monochrome.svg"_q);
|
|
|
|
f.open(QIODevice::ReadOnly);
|
|
|
|
return f.readAll();
|
|
|
|
}();
|
|
|
|
static auto Mask = QImage();
|
|
|
|
static auto Size = 0;
|
|
|
|
if (Mask.isNull() || Size != size) {
|
|
|
|
Size = size;
|
|
|
|
Mask = QImage(size, size, QImage::Format_ARGB32_Premultiplied);
|
|
|
|
Mask.fill(Qt::transparent);
|
|
|
|
auto p = QPainter(&Mask);
|
|
|
|
QSvgRenderer(Content).render(&p, QRectF(0, 0, size, size));
|
|
|
|
}
|
|
|
|
static auto Colored = QImage();
|
|
|
|
static auto ColoredDark = QImage();
|
|
|
|
auto &use = darkMode ? ColoredDark : Colored;
|
|
|
|
if (use.size() != Mask.size()) {
|
|
|
|
const auto color = darkMode ? 255 : 0;
|
|
|
|
const auto alpha = darkMode ? 255 : 228;
|
|
|
|
use = style::colorizeImage(Mask, { color, color, color, alpha });
|
|
|
|
}
|
|
|
|
return use;
|
|
|
|
}
|
|
|
|
|
|
|
|
[[nodiscard]] QImage MonochromeWithDot(QImage image, style::color color) {
|
|
|
|
auto p = QPainter(&image);
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
|
|
const auto xm = image.width() / 16.;
|
|
|
|
const auto ym = image.height() / 16.;
|
|
|
|
p.setBrush(color);
|
|
|
|
p.setPen(Qt::NoPen);
|
|
|
|
p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2
|
|
|
|
1.7 * xm,
|
|
|
|
10.5 * ym,
|
|
|
|
4.4 * xm,
|
|
|
|
4.4 * ym));
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
|
2022-04-21 11:59:26 +00:00
|
|
|
[[nodiscard]] QImage ImageIconWithCounter(
|
2022-04-18 13:34:59 +00:00
|
|
|
Window::CounterLayerArgs &&args,
|
|
|
|
bool supportMode,
|
2023-10-19 07:34:28 +00:00
|
|
|
bool smallIcon,
|
|
|
|
bool monochrome) {
|
2023-10-30 19:06:35 +00:00
|
|
|
static auto ScaledLogo = base::flat_map<int, QImage>();
|
|
|
|
static auto ScaledLogoNoMargin = base::flat_map<int, QImage>();
|
|
|
|
static auto ScaledLogoDark = base::flat_map<int, QImage>();
|
|
|
|
static auto ScaledLogoLight = base::flat_map<int, QImage>();
|
2022-04-18 13:34:59 +00:00
|
|
|
|
2023-10-19 07:34:28 +00:00
|
|
|
const auto darkMode = IsDarkTaskbar();
|
|
|
|
auto &scaled = (monochrome && darkMode)
|
|
|
|
? (*darkMode
|
|
|
|
? ScaledLogoDark
|
|
|
|
: ScaledLogoLight)
|
|
|
|
: smallIcon
|
|
|
|
? ScaledLogoNoMargin
|
|
|
|
: ScaledLogo;
|
|
|
|
|
2022-04-18 13:34:59 +00:00
|
|
|
auto result = [&] {
|
2023-10-30 19:06:35 +00:00
|
|
|
if (const auto it = scaled.find(args.size); it != scaled.end()) {
|
|
|
|
return it->second;
|
2023-11-04 20:03:44 +00:00
|
|
|
} else if (monochrome && darkMode) {
|
|
|
|
return MonochromeIconFor(args.size, *darkMode);
|
2022-04-18 13:34:59 +00:00
|
|
|
}
|
2023-11-04 20:03:44 +00:00
|
|
|
return scaled.emplace(
|
|
|
|
args.size,
|
|
|
|
(smallIcon
|
|
|
|
? Window::LogoNoMargin()
|
|
|
|
: Window::Logo()
|
|
|
|
).scaledToWidth(args.size, Qt::SmoothTransformation)
|
|
|
|
).first->second;
|
2022-04-18 13:34:59 +00:00
|
|
|
}();
|
2023-10-19 07:34:28 +00:00
|
|
|
if ((!monochrome || !darkMode) && supportMode) {
|
2022-04-18 13:34:59 +00:00
|
|
|
Window::ConvertIconToBlack(result);
|
|
|
|
}
|
|
|
|
if (!args.count) {
|
|
|
|
return result;
|
|
|
|
} else if (smallIcon) {
|
2023-11-04 20:03:44 +00:00
|
|
|
if (monochrome && darkMode) {
|
|
|
|
return MonochromeWithDot(std::move(result), args.bg);
|
|
|
|
}
|
2022-04-18 13:34:59 +00:00
|
|
|
return Window::WithSmallCounter(std::move(result), std::move(args));
|
|
|
|
}
|
|
|
|
QPainter p(&result);
|
2023-10-30 19:06:35 +00:00
|
|
|
const auto half = args.size / 2;
|
2022-04-18 13:34:59 +00:00
|
|
|
args.size = half;
|
|
|
|
p.drawPixmap(
|
|
|
|
half,
|
|
|
|
half,
|
|
|
|
Ui::PixmapFromImage(Window::GenerateCounterLayer(std::move(args))));
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
Tray::Tray() {
|
|
|
|
}
|
|
|
|
|
2022-04-18 13:34:59 +00:00
|
|
|
void Tray::createIcon() {
|
|
|
|
if (!_icon) {
|
2023-08-15 10:11:45 +00:00
|
|
|
if (const auto theme = QGuiApplicationPrivate::platformTheme()) {
|
|
|
|
_icon.reset(theme->createPlatformSystemTrayIcon());
|
|
|
|
}
|
|
|
|
if (!_icon) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_icon->init();
|
2022-04-18 13:34:59 +00:00
|
|
|
updateIcon();
|
2023-08-15 10:11:45 +00:00
|
|
|
_icon->updateToolTip(AppName.utf16());
|
|
|
|
|
|
|
|
using Reason = QPlatformSystemTrayIcon::ActivationReason;
|
2022-04-18 13:34:59 +00:00
|
|
|
base::qt_signal_producer(
|
|
|
|
_icon.get(),
|
2023-08-15 10:11:45 +00:00
|
|
|
&QPlatformSystemTrayIcon::activated
|
|
|
|
) | rpl::filter(
|
|
|
|
rpl::mappers::_1 != Reason::Context
|
|
|
|
) | rpl::map_to(
|
|
|
|
rpl::empty
|
|
|
|
) | rpl::start_to_stream(_iconClicks, _lifetime);
|
|
|
|
|
|
|
|
base::qt_signal_producer(
|
|
|
|
_icon.get(),
|
|
|
|
&QPlatformSystemTrayIcon::contextMenuRequested
|
|
|
|
) | rpl::filter([=] {
|
|
|
|
return _menu != nullptr;
|
|
|
|
}) | rpl::start_with_next([=](
|
|
|
|
QPoint globalNativePosition,
|
|
|
|
const QPlatformScreen *screen) {
|
|
|
|
_aboutToShowRequests.fire({});
|
|
|
|
const auto position = QHighDpi::fromNativePixels(
|
|
|
|
globalNativePosition,
|
|
|
|
screen ? screen->screen() : nullptr);
|
|
|
|
InvokeQueued(_menu.get(), [=] {
|
|
|
|
_menu->popup(position);
|
|
|
|
});
|
2022-04-18 13:34:59 +00:00
|
|
|
}, _lifetime);
|
|
|
|
} else {
|
|
|
|
updateIcon();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::destroyIcon() {
|
|
|
|
_icon = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::updateIcon() {
|
|
|
|
if (!_icon) {
|
|
|
|
return;
|
|
|
|
}
|
2023-01-17 15:47:58 +00:00
|
|
|
const auto controller = Core::App().activePrimaryWindow();
|
2022-04-18 13:34:59 +00:00
|
|
|
const auto session = !controller
|
|
|
|
? nullptr
|
|
|
|
: !controller->sessionController()
|
|
|
|
? nullptr
|
|
|
|
: &controller->sessionController()->session();
|
|
|
|
|
|
|
|
// Force Qt to use right icon size, not the larger one.
|
|
|
|
QIcon forTrayIcon;
|
2023-10-30 19:06:35 +00:00
|
|
|
forTrayIcon.addPixmap(
|
|
|
|
Tray::IconWithCounter(
|
|
|
|
CounterLayerArgs(
|
|
|
|
GetSystemMetrics(SM_CXSMICON),
|
|
|
|
Core::App().unreadBadge(),
|
|
|
|
Core::App().unreadBadgeMuted()),
|
|
|
|
true,
|
|
|
|
Core::App().settings().trayIconMonochrome(),
|
|
|
|
session && session->supportMode()));
|
2023-08-15 10:11:45 +00:00
|
|
|
_icon->updateIcon(forTrayIcon);
|
2022-04-18 13:34:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::createMenu() {
|
|
|
|
if (!_menu) {
|
|
|
|
_menu = base::make_unique_q<Ui::PopupMenu>(nullptr);
|
|
|
|
_menu->deleteOnHide(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::destroyMenu() {
|
|
|
|
_menu = nullptr;
|
|
|
|
_actionsLifetime.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
|
|
|
|
if (!_menu) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-21 11:59:26 +00:00
|
|
|
// If we try to activate() window before the _menu is hidden,
|
|
|
|
// then the window will be shown in semi-active state (Qt bug).
|
|
|
|
// It will receive input events, but it will be rendered as inactive.
|
|
|
|
auto callbackLater = crl::guard(_menu.get(), [=] {
|
|
|
|
using namespace rpl::mappers;
|
|
|
|
_callbackFromTrayLifetime = _menu->shownValue(
|
|
|
|
) | rpl::filter(!_1) | rpl::take(1) | rpl::start_with_next([=] {
|
|
|
|
callback();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
const auto action = _menu->addAction(QString(), std::move(callbackLater));
|
2022-04-18 13:34:59 +00:00
|
|
|
std::move(
|
|
|
|
text
|
|
|
|
) | rpl::start_with_next([=](const QString &text) {
|
|
|
|
action->setText(text);
|
|
|
|
}, _actionsLifetime);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Tray::showTrayMessage() const {
|
|
|
|
if (!cSeenTrayTooltip() && _icon) {
|
|
|
|
_icon->showMessage(
|
|
|
|
AppName.utf16(),
|
|
|
|
tr::lng_tray_icon_text(tr::now),
|
2023-08-15 10:11:45 +00:00
|
|
|
QIcon(),
|
|
|
|
QPlatformSystemTrayIcon::Information,
|
2022-04-18 13:34:59 +00:00
|
|
|
kTooltipDelay);
|
|
|
|
cSetSeenTrayTooltip(true);
|
|
|
|
Local::writeSettings();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Tray::hasTrayMessageSupport() const {
|
|
|
|
return !cSeenTrayTooltip();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::aboutToShowRequests() const {
|
|
|
|
return _aboutToShowRequests.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::showFromTrayRequests() const {
|
|
|
|
return rpl::never<>();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::hideToTrayRequests() const {
|
|
|
|
return rpl::never<>();
|
|
|
|
}
|
|
|
|
|
|
|
|
rpl::producer<> Tray::iconClicks() const {
|
|
|
|
return _iconClicks.events();
|
|
|
|
}
|
|
|
|
|
2022-06-23 05:29:11 +00:00
|
|
|
bool Tray::hasIcon() const {
|
|
|
|
return _icon;
|
|
|
|
}
|
|
|
|
|
2022-04-18 13:34:59 +00:00
|
|
|
rpl::lifetime &Tray::lifetime() {
|
|
|
|
return _lifetime;
|
|
|
|
}
|
|
|
|
|
2022-04-21 11:59:26 +00:00
|
|
|
Window::CounterLayerArgs Tray::CounterLayerArgs(
|
|
|
|
int size,
|
|
|
|
int counter,
|
|
|
|
bool muted) {
|
|
|
|
return Window::CounterLayerArgs{
|
|
|
|
.size = size,
|
|
|
|
.count = counter,
|
|
|
|
.bg = muted ? st::trayCounterBgMute : st::trayCounterBg,
|
|
|
|
.fg = st::trayCounterFg,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
QPixmap Tray::IconWithCounter(
|
|
|
|
Window::CounterLayerArgs &&args,
|
|
|
|
bool smallIcon,
|
2023-10-19 07:34:28 +00:00
|
|
|
bool monochrome,
|
2022-04-21 11:59:26 +00:00
|
|
|
bool supportMode) {
|
2023-10-30 07:16:17 +00:00
|
|
|
return Ui::PixmapFromImage(ImageIconWithCounter(
|
|
|
|
std::move(args),
|
|
|
|
supportMode,
|
|
|
|
smallIcon,
|
|
|
|
monochrome));
|
|
|
|
}
|
|
|
|
|
2024-01-03 18:14:34 +00:00
|
|
|
void WriteIco(const QString &path, std::vector<QImage> images) {
|
|
|
|
Expects(!images.empty());
|
|
|
|
|
|
|
|
auto buffer = QByteArray();
|
|
|
|
const auto write = [&](auto value) {
|
|
|
|
buffer.append(reinterpret_cast<const char*>(&value), sizeof(value));
|
|
|
|
};
|
|
|
|
|
|
|
|
const auto count = int(images.size());
|
|
|
|
|
|
|
|
auto full = 0;
|
|
|
|
auto pngs = std::vector<QByteArray>();
|
|
|
|
pngs.reserve(count);
|
|
|
|
for (const auto &image : images) {
|
|
|
|
pngs.emplace_back();
|
|
|
|
{
|
|
|
|
auto buffer = QBuffer(&pngs.back());
|
|
|
|
image.save(&buffer, "PNG");
|
|
|
|
}
|
|
|
|
full += pngs.back().size();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Images directory
|
|
|
|
constexpr auto entry = sizeof(int8)
|
|
|
|
+ sizeof(int8)
|
|
|
|
+ sizeof(int8)
|
|
|
|
+ sizeof(int8)
|
|
|
|
+ sizeof(int16)
|
|
|
|
+ sizeof(int16)
|
|
|
|
+ sizeof(uint32)
|
|
|
|
+ sizeof(uint32);
|
|
|
|
static_assert(entry == 16);
|
|
|
|
|
|
|
|
auto offset = 3 * sizeof(int16) + count * entry;
|
|
|
|
full += offset;
|
|
|
|
|
|
|
|
buffer.reserve(full);
|
|
|
|
|
|
|
|
// Thanks https://stackoverflow.com/a/54289564/6509833
|
|
|
|
write(int16(0));
|
|
|
|
write(int16(1));
|
|
|
|
write(int16(count));
|
|
|
|
|
|
|
|
for (auto i = 0; i != count; ++i) {
|
|
|
|
const auto &image = images[i];
|
|
|
|
Assert(image.width() <= 256 && image.height() <= 256);
|
|
|
|
|
|
|
|
write(int8(image.width() == 256 ? 0 : image.width()));
|
|
|
|
write(int8(image.height() == 256 ? 0 : image.height()));
|
|
|
|
write(int8(0)); // palette size
|
|
|
|
write(int8(0)); // reserved
|
|
|
|
write(int16(1)); // color planes
|
|
|
|
write(int16(image.depth())); // bits-per-pixel
|
|
|
|
write(uint32(pngs[i].size())); // size of image in bytes
|
|
|
|
write(uint32(offset)); // offset
|
|
|
|
offset += pngs[i].size();
|
|
|
|
}
|
|
|
|
for (auto i = 0; i != count; ++i) {
|
|
|
|
buffer.append(pngs[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
auto f = QFile(path);
|
|
|
|
if (f.open(QIODevice::WriteOnly)) {
|
|
|
|
f.write(buffer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString Tray::QuitJumpListIconPath() {
|
|
|
|
const auto dark = IsDarkTaskbar();
|
|
|
|
const auto key = !dark ? 0 : *dark ? 1 : 2;
|
|
|
|
const auto path = cWorkingDir() + u"tdata/temp/quit_%1.ico"_q.arg(key);
|
|
|
|
if (QFile::exists(path)) {
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
const auto color = !dark
|
|
|
|
? st::trayCounterBg->c
|
|
|
|
: *dark
|
|
|
|
? QColor(255, 255, 255)
|
|
|
|
: QColor(0, 0, 0, 228);
|
|
|
|
WriteIco(path, {
|
|
|
|
st::winQuitIcon.instance(color, 100, true),
|
|
|
|
st::winQuitIcon.instance(color, 200, true),
|
|
|
|
st::winQuitIcon.instance(color, 300, true),
|
|
|
|
});
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2023-10-30 07:16:17 +00:00
|
|
|
bool HasMonochromeSetting() {
|
|
|
|
return IsDarkTaskbar().has_value();
|
2022-04-21 11:59:26 +00:00
|
|
|
}
|
|
|
|
|
2024-01-03 18:14:34 +00:00
|
|
|
void RefreshTaskbarThemeValue() {
|
|
|
|
DarkTasbarValueValid = false;
|
|
|
|
}
|
|
|
|
|
2022-03-23 15:59:53 +00:00
|
|
|
} // namespace Platform
|