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