tray_win.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "platform/win/tray_win.h"
  8. #include "base/invoke_queued.h"
  9. #include "base/qt_signal_producer.h"
  10. #include "core/application.h"
  11. #include "lang/lang_keys.h"
  12. #include "main/main_session.h"
  13. #include "storage/localstorage.h"
  14. #include "ui/painter.h"
  15. #include "ui/ui_utility.h"
  16. #include "ui/widgets/popup_menu.h"
  17. #include "window/window_controller.h"
  18. #include "window/window_session_controller.h"
  19. #include "styles/style_window.h"
  20. #include <qpa/qplatformscreen.h>
  21. #include <qpa/qplatformsystemtrayicon.h>
  22. #include <qpa/qplatformtheme.h>
  23. #include <private/qguiapplication_p.h>
  24. #include <private/qhighdpiscaling_p.h>
  25. #include <QSvgRenderer>
  26. #include <QBuffer>
  27. namespace Platform {
  28. namespace {
  29. constexpr auto kTooltipDelay = crl::time(10000);
  30. std::optional<bool> DarkTaskbar;
  31. bool DarkTasbarValueValid/* = false*/;
  32. [[nodiscard]] std::optional<bool> ReadDarkTaskbarValue() {
  33. const auto keyName = L""
  34. "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
  35. const auto valueName = L"SystemUsesLightTheme";
  36. auto key = HKEY();
  37. auto result = RegOpenKeyEx(HKEY_CURRENT_USER, keyName, 0, KEY_READ, &key);
  38. if (result != ERROR_SUCCESS) {
  39. return std::nullopt;
  40. }
  41. DWORD value = 0, type = 0, size = sizeof(value);
  42. result = RegQueryValueEx(key, valueName, 0, &type, (LPBYTE)&value, &size);
  43. RegCloseKey(key);
  44. if (result != ERROR_SUCCESS) {
  45. return std::nullopt;
  46. }
  47. return (value == 0);
  48. }
  49. [[nodiscard]] std::optional<bool> IsDarkTaskbar() {
  50. static const auto kSystemVersion = QOperatingSystemVersion::current();
  51. static const auto kDarkModeAddedVersion = QOperatingSystemVersion(
  52. QOperatingSystemVersion::Windows,
  53. 10,
  54. 0,
  55. 18282);
  56. static const auto kSupported = (kSystemVersion >= kDarkModeAddedVersion);
  57. if (!kSupported) {
  58. return std::nullopt;
  59. } else if (!DarkTasbarValueValid) {
  60. DarkTasbarValueValid = true;
  61. DarkTaskbar = ReadDarkTaskbarValue();
  62. }
  63. return DarkTaskbar;
  64. }
  65. [[nodiscard]] QImage MonochromeIconFor(int size, bool darkMode) {
  66. Expects(size > 0);
  67. static const auto Content = [&] {
  68. auto f = QFile(u":/gui/icons/tray/monochrome.svg"_q);
  69. f.open(QIODevice::ReadOnly);
  70. return f.readAll();
  71. }();
  72. static auto Mask = QImage();
  73. static auto Size = 0;
  74. if (Mask.isNull() || Size != size) {
  75. Size = size;
  76. Mask = QImage(size, size, QImage::Format_ARGB32_Premultiplied);
  77. Mask.fill(Qt::transparent);
  78. auto p = QPainter(&Mask);
  79. QSvgRenderer(Content).render(&p, QRectF(0, 0, size, size));
  80. }
  81. static auto Colored = QImage();
  82. static auto ColoredDark = QImage();
  83. auto &use = darkMode ? ColoredDark : Colored;
  84. if (use.size() != Mask.size()) {
  85. const auto color = darkMode ? 255 : 0;
  86. const auto alpha = darkMode ? 255 : 228;
  87. use = style::colorizeImage(Mask, { color, color, color, alpha });
  88. }
  89. return use;
  90. }
  91. [[nodiscard]] QImage MonochromeWithDot(QImage image, style::color color) {
  92. auto p = QPainter(&image);
  93. auto hq = PainterHighQualityEnabler(p);
  94. const auto xm = image.width() / 16.;
  95. const auto ym = image.height() / 16.;
  96. p.setBrush(color);
  97. p.setPen(Qt::NoPen);
  98. p.drawEllipse(QRectF( // cx=3.9, cy=12.7, r=2.2
  99. 1.7 * xm,
  100. 10.5 * ym,
  101. 4.4 * xm,
  102. 4.4 * ym));
  103. return image;
  104. }
  105. [[nodiscard]] QImage ImageIconWithCounter(
  106. Window::CounterLayerArgs &&args,
  107. bool supportMode,
  108. bool smallIcon,
  109. bool monochrome) {
  110. static auto ScaledLogo = base::flat_map<int, QImage>();
  111. static auto ScaledLogoNoMargin = base::flat_map<int, QImage>();
  112. static auto ScaledLogoDark = base::flat_map<int, QImage>();
  113. static auto ScaledLogoLight = base::flat_map<int, QImage>();
  114. const auto darkMode = IsDarkTaskbar();
  115. auto &scaled = (monochrome && darkMode)
  116. ? (*darkMode
  117. ? ScaledLogoDark
  118. : ScaledLogoLight)
  119. : smallIcon
  120. ? ScaledLogoNoMargin
  121. : ScaledLogo;
  122. auto result = [&] {
  123. if (const auto it = scaled.find(args.size); it != scaled.end()) {
  124. return it->second;
  125. } else if (monochrome && darkMode) {
  126. return MonochromeIconFor(args.size, *darkMode);
  127. }
  128. return scaled.emplace(
  129. args.size,
  130. (smallIcon
  131. ? Window::LogoNoMargin()
  132. : Window::Logo()
  133. ).scaledToWidth(args.size, Qt::SmoothTransformation)
  134. ).first->second;
  135. }();
  136. if ((!monochrome || !darkMode) && supportMode) {
  137. Window::ConvertIconToBlack(result);
  138. }
  139. if (!args.count) {
  140. return result;
  141. } else if (smallIcon) {
  142. if (monochrome && darkMode) {
  143. return MonochromeWithDot(std::move(result), args.bg);
  144. }
  145. return Window::WithSmallCounter(std::move(result), std::move(args));
  146. }
  147. QPainter p(&result);
  148. const auto half = args.size / 2;
  149. args.size = half;
  150. p.drawPixmap(
  151. half,
  152. half,
  153. Ui::PixmapFromImage(Window::GenerateCounterLayer(std::move(args))));
  154. return result;
  155. }
  156. } // namespace
  157. Tray::Tray() {
  158. }
  159. void Tray::createIcon() {
  160. if (!_icon) {
  161. if (const auto theme = QGuiApplicationPrivate::platformTheme()) {
  162. _icon.reset(theme->createPlatformSystemTrayIcon());
  163. }
  164. if (!_icon) {
  165. return;
  166. }
  167. _icon->init();
  168. updateIcon();
  169. _icon->updateToolTip(AppName.utf16());
  170. using Reason = QPlatformSystemTrayIcon::ActivationReason;
  171. base::qt_signal_producer(
  172. _icon.get(),
  173. &QPlatformSystemTrayIcon::activated
  174. ) | rpl::filter(
  175. rpl::mappers::_1 != Reason::Context
  176. ) | rpl::map_to(
  177. rpl::empty
  178. ) | rpl::start_to_stream(_iconClicks, _lifetime);
  179. base::qt_signal_producer(
  180. _icon.get(),
  181. &QPlatformSystemTrayIcon::contextMenuRequested
  182. ) | rpl::filter([=] {
  183. return _menu != nullptr;
  184. }) | rpl::start_with_next([=](
  185. QPoint globalNativePosition,
  186. const QPlatformScreen *screen) {
  187. _aboutToShowRequests.fire({});
  188. const auto position = QHighDpi::fromNativePixels(
  189. globalNativePosition,
  190. screen ? screen->screen() : nullptr);
  191. InvokeQueued(_menu.get(), [=] {
  192. _menu->popup(position);
  193. });
  194. }, _lifetime);
  195. } else {
  196. updateIcon();
  197. }
  198. }
  199. void Tray::destroyIcon() {
  200. _icon = nullptr;
  201. }
  202. void Tray::updateIcon() {
  203. if (!_icon) {
  204. return;
  205. }
  206. const auto controller = Core::App().activePrimaryWindow();
  207. const auto session = !controller
  208. ? nullptr
  209. : !controller->sessionController()
  210. ? nullptr
  211. : &controller->sessionController()->session();
  212. // Force Qt to use right icon size, not the larger one.
  213. QIcon forTrayIcon;
  214. forTrayIcon.addPixmap(
  215. Tray::IconWithCounter(
  216. CounterLayerArgs(
  217. GetSystemMetrics(SM_CXSMICON),
  218. Core::App().unreadBadge(),
  219. Core::App().unreadBadgeMuted()),
  220. true,
  221. Core::App().settings().trayIconMonochrome(),
  222. session && session->supportMode()));
  223. _icon->updateIcon(forTrayIcon);
  224. }
  225. void Tray::createMenu() {
  226. if (!_menu) {
  227. _menu = base::make_unique_q<Ui::PopupMenu>(nullptr);
  228. _menu->deleteOnHide(false);
  229. }
  230. }
  231. void Tray::destroyMenu() {
  232. _menu = nullptr;
  233. _actionsLifetime.destroy();
  234. }
  235. void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
  236. if (!_menu) {
  237. return;
  238. }
  239. // If we try to activate() window before the _menu is hidden,
  240. // then the window will be shown in semi-active state (Qt bug).
  241. // It will receive input events, but it will be rendered as inactive.
  242. auto callbackLater = crl::guard(_menu.get(), [=] {
  243. using namespace rpl::mappers;
  244. _callbackFromTrayLifetime = _menu->shownValue(
  245. ) | rpl::filter(!_1) | rpl::take(1) | rpl::start_with_next([=] {
  246. callback();
  247. });
  248. });
  249. const auto action = _menu->addAction(QString(), std::move(callbackLater));
  250. std::move(
  251. text
  252. ) | rpl::start_with_next([=](const QString &text) {
  253. action->setText(text);
  254. }, _actionsLifetime);
  255. }
  256. void Tray::showTrayMessage() const {
  257. if (!cSeenTrayTooltip() && _icon) {
  258. _icon->showMessage(
  259. AppName.utf16(),
  260. tr::lng_tray_icon_text(tr::now),
  261. QIcon(),
  262. QPlatformSystemTrayIcon::Information,
  263. kTooltipDelay);
  264. cSetSeenTrayTooltip(true);
  265. Local::writeSettings();
  266. }
  267. }
  268. bool Tray::hasTrayMessageSupport() const {
  269. return !cSeenTrayTooltip();
  270. }
  271. rpl::producer<> Tray::aboutToShowRequests() const {
  272. return _aboutToShowRequests.events();
  273. }
  274. rpl::producer<> Tray::showFromTrayRequests() const {
  275. return rpl::never<>();
  276. }
  277. rpl::producer<> Tray::hideToTrayRequests() const {
  278. return rpl::never<>();
  279. }
  280. rpl::producer<> Tray::iconClicks() const {
  281. return _iconClicks.events();
  282. }
  283. bool Tray::hasIcon() const {
  284. return _icon;
  285. }
  286. rpl::lifetime &Tray::lifetime() {
  287. return _lifetime;
  288. }
  289. Window::CounterLayerArgs Tray::CounterLayerArgs(
  290. int size,
  291. int counter,
  292. bool muted) {
  293. return Window::CounterLayerArgs{
  294. .size = size,
  295. .count = counter,
  296. .bg = muted ? st::trayCounterBgMute : st::trayCounterBg,
  297. .fg = st::trayCounterFg,
  298. };
  299. }
  300. QPixmap Tray::IconWithCounter(
  301. Window::CounterLayerArgs &&args,
  302. bool smallIcon,
  303. bool monochrome,
  304. bool supportMode) {
  305. return Ui::PixmapFromImage(ImageIconWithCounter(
  306. std::move(args),
  307. supportMode,
  308. smallIcon,
  309. monochrome));
  310. }
  311. void WriteIco(const QString &path, std::vector<QImage> images) {
  312. Expects(!images.empty());
  313. auto buffer = QByteArray();
  314. const auto write = [&](auto value) {
  315. buffer.append(reinterpret_cast<const char*>(&value), sizeof(value));
  316. };
  317. const auto count = int(images.size());
  318. auto full = 0;
  319. auto pngs = std::vector<QByteArray>();
  320. pngs.reserve(count);
  321. for (const auto &image : images) {
  322. pngs.emplace_back();
  323. {
  324. auto buffer = QBuffer(&pngs.back());
  325. image.save(&buffer, "PNG");
  326. }
  327. full += pngs.back().size();
  328. }
  329. // Images directory
  330. constexpr auto entry = sizeof(int8)
  331. + sizeof(int8)
  332. + sizeof(int8)
  333. + sizeof(int8)
  334. + sizeof(int16)
  335. + sizeof(int16)
  336. + sizeof(uint32)
  337. + sizeof(uint32);
  338. static_assert(entry == 16);
  339. auto offset = 3 * sizeof(int16) + count * entry;
  340. full += offset;
  341. buffer.reserve(full);
  342. // Thanks https://stackoverflow.com/a/54289564/6509833
  343. write(int16(0));
  344. write(int16(1));
  345. write(int16(count));
  346. for (auto i = 0; i != count; ++i) {
  347. const auto &image = images[i];
  348. Assert(image.width() <= 256 && image.height() <= 256);
  349. write(int8(image.width() == 256 ? 0 : image.width()));
  350. write(int8(image.height() == 256 ? 0 : image.height()));
  351. write(int8(0)); // palette size
  352. write(int8(0)); // reserved
  353. write(int16(1)); // color planes
  354. write(int16(image.depth())); // bits-per-pixel
  355. write(uint32(pngs[i].size())); // size of image in bytes
  356. write(uint32(offset)); // offset
  357. offset += pngs[i].size();
  358. }
  359. for (auto i = 0; i != count; ++i) {
  360. buffer.append(pngs[i]);
  361. }
  362. const auto dir = QFileInfo(path).dir();
  363. dir.mkpath(dir.absolutePath());
  364. auto f = QFile(path);
  365. if (f.open(QIODevice::WriteOnly)) {
  366. f.write(buffer);
  367. }
  368. }
  369. QString Tray::QuitJumpListIconPath() {
  370. const auto dark = IsDarkTaskbar();
  371. const auto key = !dark ? 0 : *dark ? 1 : 2;
  372. const auto path = cWorkingDir() + u"tdata/temp/quit_%1.ico"_q.arg(key);
  373. if (QFile::exists(path)) {
  374. return path;
  375. }
  376. const auto color = !dark
  377. ? st::trayCounterBg->c
  378. : *dark
  379. ? QColor(255, 255, 255)
  380. : QColor(0, 0, 0, 228);
  381. WriteIco(path, {
  382. st::winQuitIcon.instance(color, 100, true),
  383. st::winQuitIcon.instance(color, 200, true),
  384. st::winQuitIcon.instance(color, 300, true),
  385. });
  386. return path;
  387. }
  388. bool HasMonochromeSetting() {
  389. return IsDarkTaskbar().has_value();
  390. }
  391. void RefreshTaskbarThemeValue() {
  392. DarkTasbarValueValid = false;
  393. }
  394. } // namespace Platform