tray_mac.mm 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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/mac/tray_mac.h"
  8. #include "base/platform/mac/base_utilities_mac.h"
  9. #include "core/application.h"
  10. #include "core/sandbox.h"
  11. #include "window/window_controller.h"
  12. #include "window/window_session_controller.h"
  13. #include "ui/painter.h"
  14. #include "styles/style_window.h"
  15. #include <QtWidgets/QMenu>
  16. #import <AppKit/NSMenu.h>
  17. #import <AppKit/NSStatusItem.h>
  18. @interface CommonDelegate : NSObject<NSMenuDelegate> {
  19. }
  20. - (void) menuDidClose:(NSMenu *)menu;
  21. - (void) menuWillOpen:(NSMenu *)menu;
  22. - (void) observeValueForKeyPath:(NSString *)keyPath
  23. ofObject:(id)object
  24. change:(NSDictionary<NSKeyValueChangeKey, id> *)change
  25. context:(void *)context;
  26. - (rpl::producer<>) closes;
  27. - (rpl::producer<>) aboutToShowRequests;
  28. - (rpl::producer<>) appearanceChanges;
  29. @end // @interface CommonDelegate
  30. @implementation CommonDelegate {
  31. rpl::event_stream<> _closes;
  32. rpl::event_stream<> _aboutToShowRequests;
  33. rpl::event_stream<> _appearanceChanges;
  34. }
  35. - (void) menuDidClose:(NSMenu *)menu {
  36. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  37. _closes.fire({});
  38. });
  39. }
  40. - (void) menuWillOpen:(NSMenu *)menu {
  41. Core::Sandbox::Instance().customEnterFromEventLoop([&] {
  42. _aboutToShowRequests.fire({});
  43. });
  44. }
  45. // Thanks https://stackoverflow.com/a/64525038
  46. - (void) observeValueForKeyPath:(NSString *)keyPath
  47. ofObject:(id)object
  48. change:(NSDictionary<NSKeyValueChangeKey, id> *)change
  49. context:(void *)context {
  50. if ([keyPath isEqualToString:@"button.effectiveAppearance"]) {
  51. _appearanceChanges.fire({});
  52. }
  53. }
  54. - (rpl::producer<>) closes {
  55. return _closes.events();
  56. }
  57. - (rpl::producer<>) aboutToShowRequests {
  58. return _aboutToShowRequests.events();
  59. }
  60. - (rpl::producer<>) appearanceChanges {
  61. return _appearanceChanges.events();
  62. }
  63. @end // @implementation MenuDelegate
  64. namespace Platform {
  65. namespace {
  66. [[nodiscard]] bool IsAnyActiveForTrayMenu() {
  67. for (const NSWindow *w in [[NSApplication sharedApplication] windows]) {
  68. if (w.isKeyWindow) {
  69. return true;
  70. }
  71. }
  72. return false;
  73. }
  74. [[nodiscard]] QImage TrayIconBack(bool darkMode) {
  75. static const auto WithColor = [](QColor color) {
  76. return st::macTrayIcon.instance(color, 100);
  77. };
  78. static const auto DarkModeResult = WithColor({ 255, 255, 255 });
  79. static const auto LightModeResult = WithColor({ 0, 0, 0, 180 });
  80. auto result = darkMode ? DarkModeResult : LightModeResult;
  81. result.detach();
  82. return result;
  83. }
  84. void PlaceCounter(
  85. QImage &img,
  86. int size,
  87. int count,
  88. style::color bg,
  89. style::color color) {
  90. if (!count) {
  91. return;
  92. }
  93. const auto savedRatio = img.devicePixelRatio();
  94. img.setDevicePixelRatio(1.);
  95. {
  96. Painter p(&img);
  97. PainterHighQualityEnabler hq(p);
  98. const auto cnt = (count < 100)
  99. ? QString("%1").arg(count)
  100. : QString("..%1").arg(count % 100, 2, 10, QChar('0'));
  101. const auto cntSize = cnt.size();
  102. p.setBrush(bg);
  103. p.setPen(Qt::NoPen);
  104. int32 fontSize, skip;
  105. if (size == 22) {
  106. skip = 1;
  107. fontSize = 8;
  108. } else {
  109. skip = 2;
  110. fontSize = 16;
  111. }
  112. style::font f(fontSize, 0, 0);
  113. int32 w = f->width(cnt), d, r;
  114. if (size == 22) {
  115. d = (cntSize < 2) ? 3 : 2;
  116. r = (cntSize < 2) ? 6 : 5;
  117. } else {
  118. d = (cntSize < 2) ? 6 : 5;
  119. r = (cntSize < 2) ? 9 : 11;
  120. }
  121. p.drawRoundedRect(
  122. QRect(
  123. size - w - d * 2 - skip,
  124. size - f->height - skip,
  125. w + d * 2,
  126. f->height),
  127. r,
  128. r);
  129. p.setCompositionMode(QPainter::CompositionMode_Source);
  130. p.setFont(f);
  131. p.setPen(color);
  132. p.drawText(
  133. size - w - d - skip,
  134. size - f->height + f->ascent - skip,
  135. cnt);
  136. }
  137. img.setDevicePixelRatio(savedRatio);
  138. }
  139. void UpdateIcon(const NSStatusItem *status) {
  140. if (!status) {
  141. return;
  142. }
  143. const auto appearance = status.button.effectiveAppearance;
  144. const auto darkMode = [[appearance.name lowercaseString]
  145. containsString:@"dark"];
  146. // The recommended maximum title bar icon height is 18 points
  147. // (device independent pixels). The menu height on past and
  148. // current OS X versions is 22 points. Provide some future-proofing
  149. // by deriving the icon height from the menu height.
  150. const int padding = 0;
  151. const int menuHeight = NSStatusBar.systemStatusBar.thickness;
  152. // [[status.button window] backingScaleFactor];
  153. const int maxImageHeight = (menuHeight - padding)
  154. * style::DevicePixelRatio();
  155. // Select pixmap based on the device pixel height. Ideally we would use
  156. // the devicePixelRatio of the target screen, but that value is not
  157. // known until draw time. Use qApp->devicePixelRatio, which returns the
  158. // devicePixelRatio for the "best" screen on the system.
  159. const auto side = 22 * style::DevicePixelRatio();
  160. const auto selectedSize = QSize(side, side);
  161. auto result = TrayIconBack(darkMode);
  162. auto resultActive = result;
  163. resultActive.detach();
  164. const auto counter = Core::App().unreadBadge();
  165. const auto muted = Core::App().unreadBadgeMuted();
  166. const auto &bg = (muted ? st::trayCounterBgMute : st::trayCounterBg);
  167. const auto &fg = st::trayCounterFg;
  168. const auto &fgInvert = st::trayCounterFgMacInvert;
  169. const auto &bgInvert = st::trayCounterBgMacInvert;
  170. const auto &resultFg = !darkMode ? fg : muted ? fgInvert : fg;
  171. PlaceCounter(result, side, counter, bg, resultFg);
  172. PlaceCounter(resultActive, side, counter, bgInvert, fgInvert);
  173. // Scale large pixmaps to fit the available menu bar area.
  174. if (result.height() > maxImageHeight) {
  175. result = result.scaledToHeight(
  176. maxImageHeight,
  177. Qt::SmoothTransformation);
  178. }
  179. if (resultActive.height() > maxImageHeight) {
  180. resultActive = resultActive.scaledToHeight(
  181. maxImageHeight,
  182. Qt::SmoothTransformation);
  183. }
  184. status.button.image = Q2NSImage(result);
  185. status.button.alternateImage = Q2NSImage(resultActive);
  186. status.button.imageScaling = NSImageScaleProportionallyDown;
  187. }
  188. } // namespace
  189. class NativeIcon final {
  190. public:
  191. NativeIcon();
  192. ~NativeIcon();
  193. void updateIcon();
  194. void showMenu(not_null<QMenu*> menu);
  195. void deactivateButton();
  196. [[nodiscard]] rpl::producer<> clicks() const;
  197. [[nodiscard]] rpl::producer<> aboutToShowRequests() const;
  198. private:
  199. CommonDelegate *_delegate;
  200. NSStatusItem *_status;
  201. rpl::event_stream<> _clicks;
  202. rpl::lifetime _lifetime;
  203. };
  204. NativeIcon::NativeIcon()
  205. : _delegate([[CommonDelegate alloc] init])
  206. , _status([
  207. [NSStatusBar.systemStatusBar
  208. statusItemWithLength:NSSquareStatusItemLength] retain]) {
  209. [_status
  210. addObserver:_delegate
  211. forKeyPath:@"button.effectiveAppearance"
  212. options:0
  213. | NSKeyValueObservingOptionNew
  214. | NSKeyValueObservingOptionInitial
  215. context:nil];
  216. [_delegate closes] | rpl::start_with_next([=] {
  217. _status.menu = nil;
  218. }, _lifetime);
  219. [_delegate appearanceChanges] | rpl::start_with_next([=] {
  220. updateIcon();
  221. }, _lifetime);
  222. const auto masks = NSEventMaskLeftMouseDown
  223. | NSEventMaskLeftMouseUp
  224. | NSEventMaskRightMouseDown
  225. | NSEventMaskRightMouseUp
  226. | NSEventMaskOtherMouseUp;
  227. [_status.button sendActionOn:masks];
  228. id buttonCallback = [^{
  229. const auto type = NSApp.currentEvent.type;
  230. if ((type == NSEventTypeLeftMouseDown)
  231. || (type == NSEventTypeRightMouseDown)) {
  232. Core::Sandbox::Instance().customEnterFromEventLoop([=] {
  233. _clicks.fire({});
  234. });
  235. }
  236. } copy];
  237. _lifetime.add([=] {
  238. [buttonCallback release];
  239. });
  240. _status.button.target = buttonCallback;
  241. _status.button.action = @selector(invoke);
  242. _status.button.toolTip = Q2NSString(AppName.utf16());
  243. }
  244. NativeIcon::~NativeIcon() {
  245. [_status
  246. removeObserver:_delegate
  247. forKeyPath:@"button.effectiveAppearance"];
  248. [NSStatusBar.systemStatusBar removeStatusItem:_status];
  249. [_status release];
  250. [_delegate release];
  251. }
  252. void NativeIcon::updateIcon() {
  253. UpdateIcon(_status);
  254. }
  255. void NativeIcon::showMenu(not_null<QMenu*> menu) {
  256. _status.menu = menu->toNSMenu();
  257. _status.menu.delegate = _delegate;
  258. [_status.button performClick:nil];
  259. }
  260. void NativeIcon::deactivateButton() {
  261. [_status.button highlight:false];
  262. }
  263. rpl::producer<> NativeIcon::clicks() const {
  264. return _clicks.events();
  265. }
  266. rpl::producer<> NativeIcon::aboutToShowRequests() const {
  267. return [_delegate aboutToShowRequests];
  268. }
  269. Tray::Tray() {
  270. }
  271. void Tray::createIcon() {
  272. if (!_nativeIcon) {
  273. _nativeIcon = std::make_unique<NativeIcon>();
  274. // On macOS we are activating the window on click
  275. // instead of showing the menu, when the window is not activated.
  276. _nativeIcon->clicks(
  277. ) | rpl::start_with_next([=] {
  278. if (IsAnyActiveForTrayMenu()) {
  279. _nativeIcon->showMenu(_menu.get());
  280. } else {
  281. _nativeIcon->deactivateButton();
  282. _showFromTrayRequests.fire({});
  283. }
  284. }, _lifetime);
  285. }
  286. updateIcon();
  287. }
  288. void Tray::destroyIcon() {
  289. _nativeIcon = nullptr;
  290. }
  291. void Tray::updateIcon() {
  292. if (_nativeIcon) {
  293. _nativeIcon->updateIcon();
  294. }
  295. }
  296. void Tray::createMenu() {
  297. if (!_menu) {
  298. _menu = base::make_unique_q<QMenu>(nullptr);
  299. }
  300. }
  301. void Tray::destroyMenu() {
  302. if (_menu) {
  303. _menu->clear();
  304. }
  305. _actionsLifetime.destroy();
  306. }
  307. void Tray::addAction(rpl::producer<QString> text, Fn<void()> &&callback) {
  308. if (!_menu) {
  309. return;
  310. }
  311. const auto action = _menu->addAction(QString(), std::move(callback));
  312. std::move(
  313. text
  314. ) | rpl::start_with_next([=](const QString &text) {
  315. action->setText(text);
  316. }, _actionsLifetime);
  317. }
  318. void Tray::showTrayMessage() const {
  319. }
  320. bool Tray::hasTrayMessageSupport() const {
  321. return false;
  322. }
  323. rpl::producer<> Tray::aboutToShowRequests() const {
  324. return _nativeIcon
  325. ? _nativeIcon->aboutToShowRequests()
  326. : rpl::never<>();
  327. }
  328. rpl::producer<> Tray::showFromTrayRequests() const {
  329. return _showFromTrayRequests.events();
  330. }
  331. rpl::producer<> Tray::hideToTrayRequests() const {
  332. return rpl::never<>();
  333. }
  334. rpl::producer<> Tray::iconClicks() const {
  335. return rpl::never<>();
  336. }
  337. bool Tray::hasIcon() const {
  338. return _nativeIcon != nullptr;
  339. }
  340. rpl::lifetime &Tray::lifetime() {
  341. return _lifetime;
  342. }
  343. Tray::~Tray() = default;
  344. } // namespace Platform