1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "platform/win/main_window_win.h"
9
10 #include "styles/style_window.h"
11 #include "platform/platform_specific.h"
12 #include "platform/platform_notifications_manager.h"
13 #include "platform/win/windows_dlls.h"
14 #include "platform/win/windows_event_filter.h"
15 #include "window/notifications_manager.h"
16 #include "window/window_session_controller.h"
17 #include "mainwindow.h"
18 #include "main/main_session.h"
19 #include "base/crc32hash.h"
20 #include "base/platform/win/base_windows_wrl.h"
21 #include "base/platform/base_platform_info.h"
22 #include "core/application.h"
23 #include "lang/lang_keys.h"
24 #include "storage/localstorage.h"
25 #include "ui/widgets/popup_menu.h"
26 #include "ui/ui_utility.h"
27 #include "window/themes/window_theme.h"
28 #include "history/history.h"
29
30 #include <QtWidgets/QDesktopWidget>
31 #include <QtWidgets/QStyleFactory>
32 #include <QtWidgets/QApplication>
33 #include <QtGui/QWindow>
34 #include <QtGui/QScreen>
35 #include <qpa/qplatformnativeinterface.h>
36
37 #include <Shobjidl.h>
38 #include <shellapi.h>
39 #include <WtsApi32.h>
40 #include <dwmapi.h>
41
42 #include <windows.ui.viewmanagement.h>
43 #include <UIViewSettingsInterop.h>
44
45 #include <Windowsx.h>
46 #include <VersionHelpers.h>
47
48 HICON qt_pixmapToWinHICON(const QPixmap &);
49
50 namespace ViewManagement = ABI::Windows::UI::ViewManagement;
51
52 namespace Platform {
53 namespace {
54
55 // Mouse down on tray icon deactivates the application.
56 // So there is no way to know for sure if the tray icon was clicked from
57 // active application or from inactive application. So we assume that
58 // if the application was deactivated less than 0.5s ago, then the tray
59 // icon click (both left or right button) was made from the active app.
60 constexpr auto kKeepActiveForTrayIcon = crl::time(500);
61
62 using namespace Microsoft::WRL;
63
NativeIcon(const QIcon & icon,QSize size)64 [[nodiscard]] HICON NativeIcon(const QIcon &icon, QSize size) {
65 if (!icon.isNull()) {
66 const auto pixmap = icon.pixmap(icon.actualSize(size));
67 if (!pixmap.isNull()) {
68 return qt_pixmapToWinHICON(pixmap);
69 }
70 }
71 return nullptr;
72 }
73
IconWithCounter(Window::CounterLayerArgs && args,Main::Session * session,bool smallIcon)74 [[nodiscard]] QImage IconWithCounter(
75 Window::CounterLayerArgs &&args,
76 Main::Session *session,
77 bool smallIcon) {
78 static constexpr auto kCount = 3;
79 static auto ScaledLogo = std::array<QImage, kCount>();
80 static auto ScaledLogoNoMargin = std::array<QImage, kCount>();
81
82 struct Dimensions {
83 int index = 0;
84 int size = 0;
85 };
86 const auto d = [&]() -> Dimensions {
87 switch (args.size) {
88 case 16:
89 return {
90 .index = 0,
91 .size = 16,
92 };
93 case 32:
94 return {
95 .index = 1,
96 .size = 32,
97 };
98 default:
99 return {
100 .index = 2,
101 .size = 64,
102 };
103 }
104 }();
105 Assert(d.index < kCount);
106
107 auto &scaled = smallIcon ? ScaledLogoNoMargin : ScaledLogo;
108 auto result = [&] {
109 auto &image = scaled[d.index];
110 if (image.isNull()) {
111 image = (smallIcon
112 ? Window::LogoNoMargin()
113 : Window::Logo()).scaledToWidth(
114 d.size,
115 Qt::SmoothTransformation);
116 }
117 return image;
118 }();
119 if (session && session->supportMode()) {
120 Window::ConvertIconToBlack(result);
121 }
122 if (!args.count) {
123 return result;
124 } else if (smallIcon) {
125 return Window::WithSmallCounter(std::move(result), std::move(args));
126 }
127 QPainter p(&result);
128 const auto half = d.size / 2;
129 args.size = half;
130 p.drawPixmap(
131 half,
132 half,
133 Ui::PixmapFromImage(Window::GenerateCounterLayer(std::move(args))));
134 return result;
135 }
136
137 ComPtr<ITaskbarList3> taskbarList;
138 bool handleSessionNotification = false;
139 uint32 kTaskbarCreatedMsgId = 0;
140
141 } // namespace
142
143 struct MainWindow::Private {
144 ComPtr<ViewManagement::IUIViewSettings> viewSettings;
145 };
146
MainWindow(not_null<Window::Controller * > controller)147 MainWindow::MainWindow(not_null<Window::Controller*> controller)
148 : Window::MainWindow(controller)
149 , _private(std::make_unique<Private>())
150 , _taskbarHiderWindow(std::make_unique<QWindow>()) {
151 QCoreApplication::instance()->installNativeEventFilter(
152 EventFilter::CreateInstance(this));
153
154 if (!kTaskbarCreatedMsgId) {
155 kTaskbarCreatedMsgId = RegisterWindowMessage(L"TaskbarButtonCreated");
156 }
157 setupNativeWindowFrame();
158
159 using namespace rpl::mappers;
160 Core::App().appDeactivatedValue(
161 ) | rpl::distinct_until_changed(
162 ) | rpl::filter(_1) | rpl::start_with_next([=] {
163 _lastDeactivateTime = crl::now();
164 }, lifetime());
165 }
166
setupNativeWindowFrame()167 void MainWindow::setupNativeWindowFrame() {
168 auto nativeFrame = rpl::single(
169 Core::App().settings().nativeWindowFrame()
170 ) | rpl::then(
171 Core::App().settings().nativeWindowFrameChanges()
172 );
173
174 rpl::combine(
175 std::move(nativeFrame),
176 Window::Theme::IsNightModeValue()
177 ) | rpl::skip(1) | rpl::start_with_next([=](bool native, bool night) {
178 validateWindowTheme(native, night);
179 }, lifetime());
180 }
181
TaskbarCreatedMsgId()182 uint32 MainWindow::TaskbarCreatedMsgId() {
183 return kTaskbarCreatedMsgId;
184 }
185
TaskbarCreated()186 void MainWindow::TaskbarCreated() {
187 HRESULT hr = CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&taskbarList));
188 if (!SUCCEEDED(hr)) {
189 taskbarList.Reset();
190 }
191 }
192
shadowsActivate()193 void MainWindow::shadowsActivate() {
194 _hasActiveFrame = true;
195 }
196
shadowsDeactivate()197 void MainWindow::shadowsDeactivate() {
198 _hasActiveFrame = false;
199 }
200
psShowTrayMenu()201 void MainWindow::psShowTrayMenu() {
202 trayIconMenu->popup(QCursor::pos());
203 }
204
screenNameChecksum(const QString & name) const205 int32 MainWindow::screenNameChecksum(const QString &name) const {
206 constexpr int DeviceNameSize = base::array_size(MONITORINFOEX().szDevice);
207 wchar_t buffer[DeviceNameSize] = { 0 };
208 if (name.size() < DeviceNameSize) {
209 name.toWCharArray(buffer);
210 } else {
211 memcpy(buffer, name.toStdWString().data(), sizeof(buffer));
212 }
213 return base::crc32(buffer, sizeof(buffer));
214 }
215
psRefreshTaskbarIcon()216 void MainWindow::psRefreshTaskbarIcon() {
217 const auto refresher = std::make_unique<QWidget>(this);
218 refresher->setWindowFlags(static_cast<Qt::WindowFlags>(Qt::Tool) | Qt::FramelessWindowHint);
219 refresher->setGeometry(x() + 1, y() + 1, 1, 1);
220 auto palette = refresher->palette();
221 palette.setColor(QPalette::Window, (isActiveWindow() ? st::titleBgActive : st::titleBg)->c);
222 refresher->setPalette(palette);
223 refresher->show();
224 refresher->raise();
225 refresher->activateWindow();
226
227 updateIconCounters();
228 }
229
psTrayMenuUpdated()230 void MainWindow::psTrayMenuUpdated() {
231 }
232
psSetupTrayIcon()233 void MainWindow::psSetupTrayIcon() {
234 if (!trayIcon) {
235 trayIcon = new QSystemTrayIcon(this);
236
237 const auto icon = QIcon(Ui::PixmapFromImage(
238 QImage(Window::LogoNoMargin())));
239
240 trayIcon->setIcon(icon);
241 connect(
242 trayIcon,
243 &QSystemTrayIcon::messageClicked,
244 this,
245 [=] { showFromTray(); });
246 attachToTrayIcon(trayIcon);
247 }
248 updateIconCounters();
249
250 trayIcon->show();
251 }
252
showTrayTooltip()253 void MainWindow::showTrayTooltip() {
254 if (trayIcon && !cSeenTrayTooltip()) {
255 trayIcon->showMessage(
256 AppName.utf16(),
257 tr::lng_tray_icon_text(tr::now),
258 QSystemTrayIcon::Information,
259 10000);
260 cSetSeenTrayTooltip(true);
261 Local::writeSettings();
262 }
263 }
264
workmodeUpdated(Core::Settings::WorkMode mode)265 void MainWindow::workmodeUpdated(Core::Settings::WorkMode mode) {
266 using WorkMode = Core::Settings::WorkMode;
267
268 switch (mode) {
269 case WorkMode::WindowAndTray: {
270 psSetupTrayIcon();
271 HWND psOwner = (HWND)GetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT);
272 if (psOwner) {
273 SetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT, 0);
274 windowHandle()->setTransientParent(nullptr);
275 psRefreshTaskbarIcon();
276 }
277 } break;
278
279 case WorkMode::TrayOnly: {
280 psSetupTrayIcon();
281 HWND psOwner = (HWND)GetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT);
282 if (!psOwner) {
283 const auto hwnd = _taskbarHiderWindow->winId();
284 SetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT, (LONG_PTR)hwnd);
285 windowHandle()->setTransientParent(_taskbarHiderWindow.get());
286 }
287 } break;
288
289 case WorkMode::WindowOnly: {
290 if (trayIcon) {
291 trayIcon->setContextMenu(0);
292 trayIcon->deleteLater();
293 }
294 trayIcon = 0;
295
296 HWND psOwner = (HWND)GetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT);
297 if (psOwner) {
298 SetWindowLongPtr(ps_hWnd, GWLP_HWNDPARENT, 0);
299 windowHandle()->setTransientParent(nullptr);
300 psRefreshTaskbarIcon();
301 }
302 } break;
303 }
304 }
305
hasTabletView() const306 bool MainWindow::hasTabletView() const {
307 if (!_private->viewSettings) {
308 return false;
309 }
310 auto mode = ViewManagement::UserInteractionMode();
311 _private->viewSettings->get_UserInteractionMode(&mode);
312 return (mode == ViewManagement::UserInteractionMode_Touch);
313 }
314
initGeometryFromSystem()315 bool MainWindow::initGeometryFromSystem() {
316 if (!hasTabletView()) {
317 return false;
318 }
319 const auto screen = [&] {
320 if (const auto result = windowHandle()->screen()) {
321 return result;
322 }
323 return QGuiApplication::primaryScreen();
324 }();
325 if (!screen) {
326 return false;
327 }
328 Ui::RpWidget::setGeometry(screen->availableGeometry());
329 return true;
330 }
331
computeDesktopRect() const332 QRect MainWindow::computeDesktopRect() const {
333 const auto flags = MONITOR_DEFAULTTONEAREST;
334 if (const auto monitor = MonitorFromWindow(psHwnd(), flags)) {
335 MONITORINFOEX info;
336 info.cbSize = sizeof(info);
337 GetMonitorInfo(monitor, &info);
338 return QRect(
339 info.rcWork.left,
340 info.rcWork.top,
341 info.rcWork.right - info.rcWork.left,
342 info.rcWork.bottom - info.rcWork.top);
343 }
344 return Window::MainWindow::computeDesktopRect();
345 }
346
updateWindowIcon()347 void MainWindow::updateWindowIcon() {
348 updateIconCounters();
349 }
350
isActiveForTrayMenu()351 bool MainWindow::isActiveForTrayMenu() {
352 return !_lastDeactivateTime
353 || (_lastDeactivateTime + kKeepActiveForTrayIcon >= crl::now());
354 }
355
unreadCounterChangedHook()356 void MainWindow::unreadCounterChangedHook() {
357 updateIconCounters();
358 }
359
updateIconCounters()360 void MainWindow::updateIconCounters() {
361 const auto counter = Core::App().unreadBadge();
362 const auto muted = Core::App().unreadBadgeMuted();
363 const auto controller = sessionController();
364 const auto session = controller ? &controller->session() : nullptr;
365
366 const auto iconSizeSmall = QSize(
367 GetSystemMetrics(SM_CXSMICON),
368 GetSystemMetrics(SM_CYSMICON));
369 const auto iconSizeBig = QSize(
370 GetSystemMetrics(SM_CXICON),
371 GetSystemMetrics(SM_CYICON));
372
373 const auto &bg = muted ? st::trayCounterBgMute : st::trayCounterBg;
374 const auto &fg = st::trayCounterFg;
375 const auto counterArgs = [&](int size, int counter) {
376 return Window::CounterLayerArgs{
377 .size = size,
378 .count = counter,
379 .bg = bg,
380 .fg = fg,
381 };
382 };
383 const auto iconWithCounter = [&](int size, int counter, bool smallIcon) {
384 return Ui::PixmapFromImage(IconWithCounter(
385 counterArgs(size, counter),
386 session,
387 smallIcon));
388 };
389
390 auto iconSmallPixmap16 = iconWithCounter(16, counter, true);
391 auto iconSmallPixmap32 = iconWithCounter(32, counter, true);
392 QIcon iconSmall, iconBig;
393 iconSmall.addPixmap(iconSmallPixmap16);
394 iconSmall.addPixmap(iconSmallPixmap32);
395 const auto bigCounter = taskbarList.Get() ? 0 : counter;
396 iconBig.addPixmap(iconWithCounter(32, bigCounter, false));
397 iconBig.addPixmap(iconWithCounter(64, bigCounter, false));
398 if (trayIcon) {
399 // Force Qt to use right icon size, not the larger one.
400 QIcon forTrayIcon;
401 forTrayIcon.addPixmap(iconSizeSmall.width() >= 20
402 ? iconSmallPixmap32
403 : iconSmallPixmap16);
404 trayIcon->setIcon(forTrayIcon);
405 }
406
407 psDestroyIcons();
408 ps_iconSmall = NativeIcon(iconSmall, iconSizeSmall);
409 ps_iconBig = NativeIcon(iconBig, iconSizeBig);
410 SendMessage(ps_hWnd, WM_SETICON, ICON_SMALL, (LPARAM)ps_iconSmall);
411 SendMessage(ps_hWnd, WM_SETICON, ICON_BIG, (LPARAM)(ps_iconBig ? ps_iconBig : ps_iconSmall));
412 if (taskbarList) {
413 if (counter > 0) {
414 const auto pixmap = [&](int size) {
415 return Ui::PixmapFromImage(Window::GenerateCounterLayer(
416 counterArgs(size, counter)));
417 };
418 QIcon iconOverlay;
419 iconOverlay.addPixmap(pixmap(16));
420 iconOverlay.addPixmap(pixmap(32));
421 ps_iconOverlay = NativeIcon(iconOverlay, iconSizeSmall);
422 }
423 const auto description = (counter > 0)
424 ? tr::lng_unread_bar(tr::now, lt_count, counter).toStdWString()
425 : std::wstring();
426 taskbarList->SetOverlayIcon(ps_hWnd, ps_iconOverlay, description.c_str());
427 }
428 SetWindowPos(ps_hWnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
429 }
430
initHook()431 void MainWindow::initHook() {
432 if (const auto native = QGuiApplication::platformNativeInterface()) {
433 ps_hWnd = static_cast<HWND>(native->nativeResourceForWindow(
434 QByteArrayLiteral("handle"),
435 windowHandle()));
436 }
437 if (!ps_hWnd) {
438 return;
439 }
440
441 handleSessionNotification = (Dlls::WTSRegisterSessionNotification != nullptr)
442 && (Dlls::WTSUnRegisterSessionNotification != nullptr);
443 if (handleSessionNotification) {
444 Dlls::WTSRegisterSessionNotification(ps_hWnd, NOTIFY_FOR_THIS_SESSION);
445 }
446
447 using namespace base::Platform;
448 auto factory = ComPtr<IUIViewSettingsInterop>();
449 if (SupportsWRL()) {
450 ABI::Windows::Foundation::GetActivationFactory(
451 StringReferenceWrapper(
452 RuntimeClass_Windows_UI_ViewManagement_UIViewSettings).Get(),
453 &factory);
454 if (factory) {
455 // NB! No such method (or IUIViewSettingsInterop) in C++/WinRT :(
456 factory->GetForWindow(
457 ps_hWnd,
458 IID_PPV_ARGS(&_private->viewSettings));
459 }
460 }
461
462 validateWindowTheme(
463 Core::App().settings().nativeWindowFrame(),
464 Window::Theme::IsNightMode());
465 }
466
validateWindowTheme(bool native,bool night)467 void MainWindow::validateWindowTheme(bool native, bool night) {
468 if (!IsWindows8OrGreater()) {
469 const auto empty = native ? nullptr : L" ";
470 SetWindowTheme(ps_hWnd, empty, empty);
471 QApplication::setStyle(QStyleFactory::create(u"Windows"_q));
472 #if 0
473 } else if (!Platform::IsDarkModeSupported()/*
474 || (!Dlls::AllowDarkModeForApp && !Dlls::SetPreferredAppMode)
475 || !Dlls::AllowDarkModeForWindow
476 || !Dlls::RefreshImmersiveColorPolicyState
477 || !Dlls::FlushMenuThemes*/) {
478 return;
479 #endif
480 } else if (!native) {
481 SetWindowTheme(ps_hWnd, nullptr, nullptr);
482 return;
483 }
484
485 // See "https://github.com/microsoft/terminal/blob/"
486 // "eb480b6bbbd83a2aafbe62992d360838e0ab9da5/"
487 // "src/interactivity/win32/windowtheme.cpp#L43-L63"
488
489 auto darkValue = BOOL(night ? TRUE : FALSE);
490
491 const auto updateStyle = [&] {
492 static const auto kSystemVersion = QOperatingSystemVersion::current();
493 if (kSystemVersion.microVersion() >= 18875 && Dlls::SetWindowCompositionAttribute) {
494 Dlls::WINDOWCOMPOSITIONATTRIBDATA data = {
495 Dlls::WINDOWCOMPOSITIONATTRIB::WCA_USEDARKMODECOLORS,
496 &darkValue,
497 sizeof(darkValue)
498 };
499 Dlls::SetWindowCompositionAttribute(ps_hWnd, &data);
500 } else if (kSystemVersion.microVersion() >= 17763) {
501 static const auto kDWMWA_USE_IMMERSIVE_DARK_MODE = (kSystemVersion.microVersion() >= 18985)
502 ? DWORD(20)
503 : DWORD(19);
504 DwmSetWindowAttribute(
505 ps_hWnd,
506 kDWMWA_USE_IMMERSIVE_DARK_MODE,
507 &darkValue,
508 sizeof(darkValue));
509 }
510 };
511
512 updateStyle();
513
514 // See "https://osdn.net/projects/tortoisesvn/scm/svn/blobs/28812/"
515 // "trunk/src/TortoiseIDiff/MainWindow.cpp"
516 //
517 // But for now it works event with a small part of that.
518 //
519
520 //const auto updateWindowTheme = [&] {
521 // const auto set = [&](LPCWSTR name) {
522 // return SetWindowTheme(ps_hWnd, name, nullptr);
523 // };
524 // if (!night || FAILED(set(L"DarkMode_Explorer"))) {
525 // set(L"Explorer");
526 // }
527 //};
528 //
529 //if (night) {
530 // if (Dlls::SetPreferredAppMode) {
531 // Dlls::SetPreferredAppMode(Dlls::PreferredAppMode::AllowDark);
532 // } else {
533 // Dlls::AllowDarkModeForApp(TRUE);
534 // }
535 // Dlls::AllowDarkModeForWindow(ps_hWnd, TRUE);
536 // updateWindowTheme();
537 // updateStyle();
538 // Dlls::FlushMenuThemes();
539 // Dlls::RefreshImmersiveColorPolicyState();
540 //} else {
541 // updateWindowTheme();
542 // Dlls::AllowDarkModeForWindow(ps_hWnd, FALSE);
543 // updateStyle();
544 // Dlls::FlushMenuThemes();
545 // Dlls::RefreshImmersiveColorPolicyState();
546 // if (Dlls::SetPreferredAppMode) {
547 // Dlls::SetPreferredAppMode(Dlls::PreferredAppMode::Default);
548 // } else {
549 // Dlls::AllowDarkModeForApp(FALSE);
550 // }
551 //}
552
553 // Didn't find any other way to definitely repaint with the new style.
554 SendMessage(ps_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 0 : 1, 0);
555 SendMessage(ps_hWnd, WM_NCACTIVATE, _hasActiveFrame ? 1 : 0, 0);
556 }
557
showFromTrayMenu()558 void MainWindow::showFromTrayMenu() {
559 // If we try to activate() window before the trayIconMenu is hidden,
560 // then the window will be shown in semi-active state (Qt bug).
561 // It will receive input events, but it will be rendered as inactive.
562 using namespace rpl::mappers;
563 _showFromTrayLifetime = trayIconMenu->shownValue(
564 ) | rpl::filter(_1) | rpl::take(1) | rpl::start_with_next([=] {
565 showFromTray();
566 });
567 }
568
psHwnd() const569 HWND MainWindow::psHwnd() const {
570 return ps_hWnd;
571 }
572
psDestroyIcons()573 void MainWindow::psDestroyIcons() {
574 if (ps_iconBig) {
575 DestroyIcon(ps_iconBig);
576 ps_iconBig = 0;
577 }
578 if (ps_iconSmall) {
579 DestroyIcon(ps_iconSmall);
580 ps_iconSmall = 0;
581 }
582 if (ps_iconOverlay) {
583 DestroyIcon(ps_iconOverlay);
584 ps_iconOverlay = 0;
585 }
586 }
587
~MainWindow()588 MainWindow::~MainWindow() {
589 if (handleSessionNotification) {
590 Dlls::WTSUnRegisterSessionNotification(ps_hWnd);
591 }
592 _private->viewSettings.Reset();
593 if (taskbarList) {
594 taskbarList.Reset();
595 }
596
597 psDestroyIcons();
598
599 EventFilter::Destroy();
600 }
601
602 } // namespace Platform
603