/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Input; using ProtonVPN.Client.Common.Dispatching; using ProtonVPN.Client.Common.UI.Controls.Custom; using ProtonVPN.Client.Common.UI.Keyboards; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Extensions; using ProtonVPN.Client.Core.Services.Selection; using ProtonVPN.Client.Localization.Contracts; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; using Windows.System; using WinUIEx; namespace ProtonVPN.Client.Core.Services.Activation.Bases; public abstract class WindowActivatorBase : WindowHostActivatorBase, IWindowActivator where TWindow : BaseWindow { protected readonly ILocalizationProvider Localizer; protected readonly IApplicationIconSelector IconSelector; private uint _currentHostDpi; public abstract string WindowTitle { get; } protected bool HandleClosedEvent { get; private set; } protected WindowState CurrentWindowState { get; private set; } protected WindowActivationState CurrentActivationState { get; private set; } = WindowActivationState.Deactivated; public bool IsWindowVisible { get; private set; } public bool EnableExitOnEsc { get; protected set; } public bool IsWindowFocused { get; private set; } private bool _isActivationPending; public event EventHandler? HostDpiChanged; public event EventHandler? HostSizeChanged; protected virtual bool ShouldCreateInstanceIfMissing => true; protected WindowActivatorBase( ILogger logger, IUIThreadDispatcher uiThreadDispatcher, IApplicationThemeSelector themeSelector, ISettings settings, ILocalizationService localizationService, ILocalizationProvider localizer, IApplicationIconSelector iconSelector) : base(logger, uiThreadDispatcher, themeSelector, settings, localizationService) { Localizer = localizer; IconSelector = iconSelector; HandleClosedEvent = true; } private void CreateWindowInstanceAndSetAutoActivation() { if (ShouldCreateInstanceIfMissing) { Logger.Info($"Creating instance of {HostTypeName}."); _isActivationPending = true; Activator.CreateInstance(); } else { Logger.Warn($"Cannot activate {HostTypeName} because no instance exists and automatic creation is disabled."); } } public void Activate() { if (Host == null) { CreateWindowInstanceAndSetAutoActivation(); return; } Logger.Info($"Activating {HostTypeName}."); Host.Activate(); Host.SetForegroundWindow(); } public void Hide() { if (Host == null) { return; } Logger.Info($"Hiding {HostTypeName}."); Host.Hide(); } public void Exit() { if (Host == null) { return; } Logger.Info($"Exiting {HostTypeName}."); DisableHandleClosedEvent(); Host.Close(); } public void DisableHandleClosedEvent() { HandleClosedEvent = false; } protected override void RegisterToHostEvents() { base.RegisterToHostEvents(); if (Host != null) { _currentHostDpi = Host.GetDpiForWindow(); Host.Closed += OnWindowClosed; Host.WindowStateChanged += OnWindowStateChanged; Host.Activated += OnWindowActivationStateChanged; Host.SizeChanged += OnWindowSizeChanged; if (Host.Content is FrameworkElement content && EnableExitOnEsc) { content.KeyboardAccelerators.Add(KeyboardAcceleratorBuilder.Build(OnEscapeAccelerator, VirtualKey.Escape)); content.KeyboardAcceleratorPlacementMode = KeyboardAcceleratorPlacementMode.Hidden; } } } protected virtual void OnEscapeAccelerator(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) { Logger.Info($"Escape accelerator invoked in '{typeof(TWindow)?.Name}'."); args.Handled = true; Host?.Close(); } protected override void UnregisterFromHostEvents() { base.UnregisterFromHostEvents(); if (Host != null) { Host.Closed -= OnWindowClosed; Host.WindowStateChanged -= OnWindowStateChanged; Host.Activated -= OnWindowActivationStateChanged; Host.SizeChanged -= OnWindowSizeChanged; if (Host.Content is FrameworkElement content && EnableExitOnEsc) { content.KeyboardAccelerators.Clear(); } } } protected override void OnInitialized() { base.OnInitialized(); if (Host != null) { Host.ExtendsContentIntoTitleBar = true; InvalidateWindowIcon(); InvalidateWindowTitle(); InvalidateWindowPosition(); InvalidateWindowState(); } } protected override void OnWindowLoaded() { base.OnWindowLoaded(); if (_isActivationPending) { _isActivationPending = false; Activate(); } InvalidateWindowFocusState(); } protected virtual void OnWindowOpened() { Logger.Info($"Window '{HostTypeName}' is opened."); InvalidateWindowState(); } protected virtual void OnWindowFocused() { Logger.Info($"Window '{HostTypeName}' is focused."); } protected virtual void OnWindowUnfocused() { Logger.Info($"Window '{HostTypeName}' is unfocused."); } protected virtual void OnWindowHidden() { Logger.Info($"Window '{HostTypeName}' is hidden."); } protected virtual void OnWindowClosing(WindowEventArgs e) { Logger.Info($"Closing window '{HostTypeName}' requested."); } protected virtual void OnWindowCloseAborted() { Logger.Info($"Closing window '{HostTypeName}' aborted."); } protected virtual void OnWindowClosed() { Logger.Info($"Window '{HostTypeName}' is closed."); // Current window instance is closed. Reset handle closed event flag for the next instance. HandleClosedEvent = true; } protected virtual void OnWindowStateChanged() { Logger.Info($"Window '{HostTypeName}' state has changed to {CurrentWindowState}."); } protected override void OnLanguageChanged() { base.OnLanguageChanged(); InvalidateWindowTitle(); } protected override void OnFlowDirectionChanged() { base.OnFlowDirectionChanged(); Host?.ApplyFlowDirection(CurrentFlowDirection); } protected override void OnAppThemeChanged() { base.OnAppThemeChanged(); Host?.ApplyTheme(CurrentAppTheme); } protected virtual void InvalidateWindowIcon() { Host?.AppWindow?.SetIcon(IconSelector.GetAppIconPath()); } protected virtual void InvalidateWindowPosition() { Host?.CenterOnScreen(); } protected virtual void InvalidateWindowState() { if (Host != null) { Host.WindowState = WindowState.Normal; } } protected virtual void OnDpiChanged() { HostDpiChanged?.Invoke(this, EventArgs.Empty); } protected virtual void OnSizeChanged() { HostSizeChanged?.Invoke(this, EventArgs.Empty); } private void InvalidateWindowTitle() { if (Host != null) { Host.Title = WindowTitle; } } private void InvalidateWindowVisibilityState() { bool isWindowVisible = Host != null && Host.Visible && CurrentWindowState is not WindowState.Minimized; if (IsWindowVisible != isWindowVisible) { IsWindowVisible = isWindowVisible; if (IsWindowVisible) { OnWindowOpened(); } else { OnWindowHidden(); } } } private void InvalidateWindowFocusState() { bool isWindowVisible = Host != null && Host.Visible && CurrentWindowState is not WindowState.Minimized; bool isWindowFocused = isWindowVisible && CurrentActivationState is not WindowActivationState.Deactivated; if (IsWindowFocused != isWindowFocused) { IsWindowFocused = isWindowFocused; if (IsWindowFocused) { InvalidateWindowVisibilityState(); if (IsWindowVisible) { OnWindowFocused(); } } else { if (IsWindowVisible) { OnWindowUnfocused(); } InvalidateWindowVisibilityState(); } } } private void OnWindowClosed(object sender, WindowEventArgs e) { if (HandleClosedEvent) { OnWindowClosing(e); } if (e.Handled) { OnWindowCloseAborted(); InvalidateWindowVisibilityState(); } else { OnWindowClosed(); Reset(); } } private void OnWindowStateChanged(object? sender, WindowState windowState) { CurrentWindowState = windowState; if (IsWindowLoaded) { OnWindowStateChanged(); InvalidateWindowFocusState(); } } private void OnWindowActivationStateChanged(object sender, WindowActivatedEventArgs e) { CurrentActivationState = e.WindowActivationState; if (IsWindowLoaded) { InvalidateWindowFocusState(); if (Host is IFocusAware focusAware) { focusAware.OnFocusChanged(); } } } private void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs e) { uint hostDpi = Host?.GetDpiForWindow() ?? default; if (_currentHostDpi != default && _currentHostDpi != hostDpi) { OnDpiChanged(); } _currentHostDpi = hostDpi; OnSizeChanged(); } }