diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index cda125a39cb..fc5c2e4e947 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -3,21 +3,29 @@ using System.IO; using System.Linq; using System.Xml; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Effects; using System.Windows.Shell; +using System.Windows.Threading; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Microsoft.Win32; +using TextBox = System.Windows.Controls.TextBox; namespace Flow.Launcher.Core.Resource { public class Theme { + #region Properties & Fields + + public bool BlurEnabled { get; set; } + private const string ThemeMetadataNamePrefix = "Name:"; private const string ThemeMetadataIsDarkPrefix = "IsDark:"; private const string ThemeMetadataHasBlurPrefix = "HasBlur:"; @@ -31,14 +39,12 @@ public class Theme private string _oldTheme; private const string Folder = Constant.Themes; private const string Extension = ".xaml"; - private string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); - private string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); + private static string DirectoryPath => Path.Combine(Constant.ProgramDirectory, Folder); + private static string UserDirectoryPath => Path.Combine(DataLocation.DataDirectory(), Folder); - public string CurrentTheme => _settings.Theme; - - public bool BlurEnabled { get; set; } + #endregion - private double mainWindowWidth; + #region Constructor public Theme(IPublicAPI publicAPI, Settings settings) { @@ -66,6 +72,15 @@ public Theme(IPublicAPI publicAPI, Settings settings) _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } + #endregion + + #region Theme Resources + + public string GetCurrentTheme() + { + return _settings.Theme; + } + private void MakeSureThemeDirectoriesExist() { foreach (var dir in _themeDirectories.Where(dir => !Directory.Exists(dir))) @@ -81,59 +96,6 @@ private void MakeSureThemeDirectoriesExist() } } - public bool ChangeTheme(string theme) - { - const string defaultTheme = Constant.DefaultTheme; - - string path = GetThemePath(theme); - try - { - if (string.IsNullOrEmpty(path)) - throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); - - // reload all resources even if the theme itself hasn't changed in order to pickup changes - // to things like fonts - UpdateResourceDictionary(GetResourceDictionary(theme)); - - _settings.Theme = theme; - - - //always allow re-loading default theme, in case of failure of switching to a new theme from default theme - if (_oldTheme != theme || theme == defaultTheme) - { - _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); - } - - BlurEnabled = Win32Helper.IsBlurTheme(); - - if (_settings.UseDropShadowEffect && !BlurEnabled) - AddDropShadowEffectToCurrentTheme(); - - Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); - } - catch (DirectoryNotFoundException) - { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); - if (theme != defaultTheme) - { - _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_path_not_exists"), theme)); - ChangeTheme(defaultTheme); - } - return false; - } - catch (XamlParseException) - { - Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); - if (theme != defaultTheme) - { - _api.ShowMsgBox(string.Format(InternationalizationManager.Instance.GetTranslation("theme_load_failure_parse_error"), theme)); - ChangeTheme(defaultTheme); - } - return false; - } - return true; - } - private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { var dicts = Application.Current.Resources.MergedDictionaries; @@ -154,9 +116,7 @@ private ResourceDictionary GetThemeResourceDictionary(string theme) return dict; } - private ResourceDictionary CurrentThemeResourceDictionary() => GetThemeResourceDictionary(_settings.Theme); - - public ResourceDictionary GetResourceDictionary(string theme) + private ResourceDictionary GetResourceDictionary(string theme) { var dict = GetThemeResourceDictionary(theme); @@ -213,7 +173,7 @@ public ResourceDictionary GetResourceDictionary(string theme) Setter[] setters = { fontFamily, fontStyle, fontWeight, fontStretch }; Array.ForEach( - new[] { resultSubItemStyle,resultSubItemSelectedStyle}, o + new[] { resultSubItemStyle, resultSubItemSelectedStyle }, o => Array.ForEach(setters, p => o.Setters.Add(p))); } @@ -221,28 +181,12 @@ public ResourceDictionary GetResourceDictionary(string theme) var windowStyle = dict["WindowStyle"] as Style; var width = _settings.WindowSize; windowStyle.Setters.Add(new Setter(Window.WidthProperty, width)); - mainWindowWidth = (double)width; return dict; } - private ResourceDictionary GetCurrentResourceDictionary( ) + private ResourceDictionary GetCurrentResourceDictionary() { - return GetResourceDictionary(_settings.Theme); - } - - public List LoadAvailableThemes() - { - List themes = new List(); - foreach (var themeDirectory in _themeDirectories) - { - var filePaths = Directory - .GetFiles(themeDirectory) - .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) - .Select(GetThemeDataFromPath); - themes.AddRange(filePaths); - } - - return themes.OrderBy(o => o.Name).ToList(); + return GetResourceDictionary(GetCurrentTheme()); } private ThemeData GetThemeDataFromPath(string path) @@ -264,15 +208,15 @@ private ThemeData GetThemeDataFromPath(string path) { if (line.StartsWith(ThemeMetadataNamePrefix, StringComparison.OrdinalIgnoreCase)) { - name = line.Remove(0, ThemeMetadataNamePrefix.Length).Trim(); + name = line[ThemeMetadataNamePrefix.Length..].Trim(); } else if (line.StartsWith(ThemeMetadataIsDarkPrefix, StringComparison.OrdinalIgnoreCase)) { - isDark = bool.Parse(line.Remove(0, ThemeMetadataIsDarkPrefix.Length).Trim()); + isDark = bool.Parse(line[ThemeMetadataIsDarkPrefix.Length..].Trim()); } else if (line.StartsWith(ThemeMetadataHasBlurPrefix, StringComparison.OrdinalIgnoreCase)) { - hasBlur = bool.Parse(line.Remove(0, ThemeMetadataHasBlurPrefix.Length).Trim()); + hasBlur = bool.Parse(line[ThemeMetadataHasBlurPrefix.Length..].Trim()); } } @@ -293,6 +237,81 @@ private string GetThemePath(string themeName) return string.Empty; } + #endregion + + #region Load & Change + + public List LoadAvailableThemes() + { + List themes = new List(); + foreach (var themeDirectory in _themeDirectories) + { + var filePaths = Directory + .GetFiles(themeDirectory) + .Where(filePath => filePath.EndsWith(Extension) && !filePath.EndsWith("Base.xaml")) + .Select(GetThemeDataFromPath); + themes.AddRange(filePaths); + } + + return themes.OrderBy(o => o.Name).ToList(); + } + + public bool ChangeTheme(string theme = null) + { + if (string.IsNullOrEmpty(theme)) + theme = GetCurrentTheme(); + + string path = GetThemePath(theme); + try + { + if (string.IsNullOrEmpty(path)) + throw new DirectoryNotFoundException("Theme path can't be found <{path}>"); + + // reload all resources even if the theme itself hasn't changed in order to pickup changes + // to things like fonts + UpdateResourceDictionary(GetResourceDictionary(theme)); + + _settings.Theme = theme; + + //always allow re-loading default theme, in case of failure of switching to a new theme from default theme + if (_oldTheme != theme || theme == Constant.DefaultTheme) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + + BlurEnabled = IsBlurTheme(); + //if (_settings.UseDropShadowEffect) + // AddDropShadowEffectToCurrentTheme(); + //Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); + _ = SetBlurForWindowAsync(); + } + catch (DirectoryNotFoundException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> path can't be found"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_path_not_exists"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + catch (XamlParseException) + { + Log.Error($"|Theme.ChangeTheme|Theme <{theme}> fail to parse"); + if (theme != Constant.DefaultTheme) + { + _api.ShowMsgBox(string.Format(_api.GetTranslation("theme_load_failure_parse_error"), theme)); + ChangeTheme(Constant.DefaultTheme); + } + return false; + } + return true; + } + + #endregion + + #region Shadow Effect + public void AddDropShadowEffectToCurrentTheme() { var dict = GetCurrentResourceDictionary(); @@ -311,8 +330,7 @@ public void AddDropShadowEffectToCurrentTheme() } }; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - if (marginSetter == null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is not Setter marginSetter) { var margin = new Thickness(ShadowExtraMargin, 12, ShadowExtraMargin, ShadowExtraMargin); marginSetter = new Setter() @@ -347,14 +365,12 @@ public void RemoveDropShadowEffectFromCurrentTheme() var dict = GetCurrentResourceDictionary(); var windowBorderStyle = dict["WindowBorderStyle"] as Style; - var effectSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) as Setter; - var marginSetter = windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) as Setter; - - if (effectSetter != null) + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.EffectProperty) is Setter effectSetter) { windowBorderStyle.Setters.Remove(effectSetter); } - if (marginSetter != null) + + if (windowBorderStyle.Setters.FirstOrDefault(setterBase => setterBase is Setter setter && setter.Property == Border.MarginProperty) is Setter marginSetter) { var currentMargin = (Thickness)marginSetter.Value; var newMargin = new Thickness( @@ -395,6 +411,343 @@ private static void SetResizeBoarderThickness(Thickness? effectMargin) } } + #endregion + + #region Blur Handling + + /// + /// Refreshes the frame to apply the current theme settings. + /// + public async Task RefreshFrameAsync() + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, useDropShadowEffect) = GetActualValue(); + + // Remove OS minimizing/maximizing animation + // Methods.SetWindowAttribute(new WindowInteropHelper(mainWindow).Handle, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 3); + + // The timing of adding the shadow effect should vary depending on whether the theme is transparent. + if (BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + SetBlurForWindow(GetCurrentTheme(), backdropType); + + if (!BlurEnabled) + { + AutoDropShadow(useDropShadowEffect); + } + }, DispatcherPriority.Normal); + } + + /// + /// Sets the blur for a window via SetWindowCompositionAttribute + /// + public async Task SetBlurForWindowAsync() + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + // Get the actual backdrop type and drop shadow effect settings + var (backdropType, _) = GetActualValue(); + + SetBlurForWindow(GetCurrentTheme(), backdropType); + }, DispatcherPriority.Normal); + } + + /// + /// Gets the actual backdrop type and drop shadow effect settings based on the current theme status. + /// + public (BackdropTypes BackdropType, bool UseDropShadowEffect) GetActualValue() + { + var backdropType = _settings.BackdropType; + var useDropShadowEffect = _settings.UseDropShadowEffect; + + // When changed non-blur theme, change to backdrop to none + if (!BlurEnabled) + { + backdropType = BackdropTypes.None; + } + + // Dropshadow on and control disabled.(user can't change dropshadow with blur theme) + if (BlurEnabled) + { + useDropShadowEffect = true; + } + + return (backdropType, useDropShadowEffect); + } + + private void SetBlurForWindow(string theme, BackdropTypes backdropType) + { + var dict = GetThemeResourceDictionary(theme); + if (dict == null) + return; + + var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; + if (windowBorderStyle == null) + return; + + Window mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + if (BlurEnabled && hasBlur && Win32Helper.IsBackdropSupported()) + { + // If the BackdropType is Mica or MicaAlt, set the windowborderstyle's background to transparent + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)))); + } + else if (backdropType == BackdropTypes.Acrylic) + { + windowBorderStyle.Setters.Remove(windowBorderStyle.Setters.OfType().FirstOrDefault(x => x.Property.Name == "Background")); + windowBorderStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(Colors.Transparent))); + } + + // Apply the blur effect + Win32Helper.DWMSetBackdropForWindow(mainWindow, backdropType); + ColorizeWindow(theme, backdropType); + } + else + { + // Apply default style when Blur is disabled + Win32Helper.DWMSetBackdropForWindow(mainWindow, BackdropTypes.None); + ColorizeWindow(theme, backdropType); + } + + UpdateResourceDictionary(dict); + } + + private void AutoDropShadow(bool useDropShadowEffect) + { + SetWindowCornerPreference("Default"); + RemoveDropShadowEffectFromCurrentTheme(); + if (useDropShadowEffect) + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Round"); + } + else + { + SetWindowCornerPreference("Default"); + AddDropShadowEffectToCurrentTheme(); + } + } + else + { + if (BlurEnabled && Win32Helper.IsBackdropSupported()) + { + SetWindowCornerPreference("Default"); + } + else + { + RemoveDropShadowEffectFromCurrentTheme(); + } + } + } + + private static void SetWindowCornerPreference(string cornerType) + { + Window mainWindow = Application.Current.MainWindow; + if (mainWindow == null) + return; + + Win32Helper.DWMSetCornerPreferenceForWindow(mainWindow, cornerType); + } + + // Get Background Color from WindowBorderStyle when there not color for BG. + // for theme has not "LightBG" or "DarkBG" case. + private Color GetWindowBorderStyleBackground(string theme) + { + var Resources = GetThemeResourceDictionary(theme); + var windowBorderStyle = (Style)Resources["WindowBorderStyle"]; + + var backgroundSetter = windowBorderStyle.Setters + .OfType() + .FirstOrDefault(s => s.Property == Border.BackgroundProperty); + + if (backgroundSetter != null) + { + // Background's Value is DynamicColor Case + var backgroundValue = backgroundSetter.Value; + + if (backgroundValue is SolidColorBrush solidColorBrush) + { + return solidColorBrush.Color; // Return SolidColorBrush's Color + } + else if (backgroundValue is DynamicResourceExtension dynamicResource) + { + // When DynamicResource Extension it is, Key is resource's name. + var resourceKey = backgroundSetter.Value.ToString(); + + // find key in resource and return color. + if (Resources.Contains(resourceKey)) + { + var colorResource = Resources[resourceKey]; + if (colorResource is SolidColorBrush colorBrush) + { + return colorBrush.Color; + } + else if (colorResource is Color color) + { + return color; + } + } + } + } + + return Colors.Transparent; // Default is transparent + } + + private void ApplyPreviewBackground(Color? bgColor = null) + { + if (bgColor == null) return; + + // Copy the existing WindowBorderStyle + var previewStyle = new Style(typeof(Border)); + if (Application.Current.Resources.Contains("WindowBorderStyle")) + { + if (Application.Current.Resources["WindowBorderStyle"] is Style originalStyle) + { + foreach (var setter in originalStyle.Setters.OfType()) + { + previewStyle.Setters.Add(new Setter(setter.Property, setter.Value)); + } + } + } + + // Apply background color (remove transparency in color) + // WPF does not allow the use of an acrylic brush within the window's internal area, + // so transparency effects are not applied to the preview. + Color backgroundColor = Color.FromRgb(bgColor.Value.R, bgColor.Value.G, bgColor.Value.B); + previewStyle.Setters.Add(new Setter(Border.BackgroundProperty, new SolidColorBrush(backgroundColor))); + + // The blur theme keeps the corner round fixed (applying DWM code to modify it causes rendering issues). + // The non-blur theme retains the previously set WindowBorderStyle. + if (BlurEnabled) + { + previewStyle.Setters.Add(new Setter(Border.CornerRadiusProperty, new CornerRadius(5))); + previewStyle.Setters.Add(new Setter(Border.BorderThicknessProperty, new Thickness(1))); + } + Application.Current.Resources["PreviewWindowBorderStyle"] = previewStyle; + } + + private void ColorizeWindow(string theme, BackdropTypes backdropType) + { + var dict = GetThemeResourceDictionary(theme); + if (dict == null) return; + + var mainWindow = Application.Current.MainWindow; + if (mainWindow == null) return; + + // Check if the theme supports blur + bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; + + // SystemBG value check (Auto, Light, Dark) + string systemBG = dict.Contains("SystemBG") ? dict["SystemBG"] as string : "Auto"; // 기본값 Auto + + // Check the user's ColorScheme setting + string colorScheme = _settings.ColorScheme; + + // Check system dark mode setting (read AppsUseLightTheme value) + int themeValue = (int)Registry.GetValue(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme", 1); + bool isSystemDark = themeValue == 0; + + // Final decision on whether to use dark mode + bool useDarkMode = false; + + // If systemBG is not "Auto", prioritize it over ColorScheme and set the mode based on systemBG value + if (systemBG == "Dark") + { + useDarkMode = true; // Dark + } + else if (systemBG == "Light") + { + useDarkMode = false; // Light + } + else if (systemBG == "Auto") + { + // If systemBG is "Auto", decide based on ColorScheme + if (colorScheme == "Dark") + useDarkMode = true; + else if (colorScheme == "Light") + useDarkMode = false; + else + useDarkMode = isSystemDark; // Auto (based on system setting) + } + + // Apply DWM Dark Mode + Win32Helper.DWMSetDarkModeForWindow(mainWindow, useDarkMode); + + Color LightBG; + Color DarkBG; + + // Retrieve LightBG value (fallback to WindowBorderStyle background color if not found) + try + { + LightBG = dict.Contains("LightBG") ? (Color)dict["LightBG"] : GetWindowBorderStyleBackground(theme); + } + catch (Exception) + { + LightBG = GetWindowBorderStyleBackground(theme); + } + + // Retrieve DarkBG value (fallback to LightBG if not found) + try + { + DarkBG = dict.Contains("DarkBG") ? (Color)dict["DarkBG"] : LightBG; + } + catch (Exception) + { + DarkBG = LightBG; + } + + // Select background color based on ColorScheme and SystemBG + Color selectedBG = useDarkMode ? DarkBG : LightBG; + ApplyPreviewBackground(selectedBG); + + bool isBlurAvailable = hasBlur && Win32Helper.IsBackdropSupported(); // Windows 11 미만이면 hasBlur를 강제 false + + if (!isBlurAvailable) + { + mainWindow.Background = Brushes.Transparent; + } + else + { + // Only set the background to transparent if the theme supports blur + if (backdropType == BackdropTypes.Mica || backdropType == BackdropTypes.MicaAlt) + { + mainWindow.Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)); + } + else + { + mainWindow.Background = new SolidColorBrush(selectedBG); + } + } + } + + private static bool IsBlurTheme() + { + if (!Win32Helper.IsBackdropSupported()) // Windows 11 미만이면 무조건 false + return false; + + var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + + return resource is bool b && b; + } + + #endregion + + #region Classes + public record ThemeData(string FileNameWithoutExtension, string Name, bool? IsDark = null, bool? HasBlur = null); + + #endregion } } diff --git a/Flow.Launcher.Infrastructure/MonitorInfo.cs b/Flow.Launcher.Infrastructure/MonitorInfo.cs new file mode 100644 index 00000000000..3221708c135 --- /dev/null +++ b/Flow.Launcher.Infrastructure/MonitorInfo.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System; +using System.Runtime.InteropServices; +using System.Windows; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Flow.Launcher.Infrastructure; + +/// +/// Contains full information about a display monitor. +/// Codes are edited from: . +/// +internal class MonitorInfo +{ + /// + /// Gets the display monitors (including invisible pseudo-monitors associated with the mirroring drivers). + /// + /// A list of display monitors + public static unsafe IList GetDisplayMonitors() + { + var monitorCount = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CMONITORS); + var list = new List(monitorCount); + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + list.Add(new MonitorInfo(monitor, rect)); + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return list; + } + + /// + /// Gets the display monitor that is nearest to a given window. + /// + /// Window handle + /// The display monitor that is nearest to a given window, or null if no monitor is found. + public static unsafe MonitorInfo GetNearestDisplayMonitor(HWND hwnd) + { + var nearestMonitor = PInvoke.MonitorFromWindow(hwnd, MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST); + MonitorInfo nearestMonitorInfo = null; + var callback = new MONITORENUMPROC((HMONITOR monitor, HDC deviceContext, RECT* rect, LPARAM data) => + { + if (monitor == nearestMonitor) + { + nearestMonitorInfo = new MonitorInfo(monitor, rect); + return false; + } + return true; + }); + var dwData = new LPARAM(); + var hdc = new HDC(); + bool ok = PInvoke.EnumDisplayMonitors(hdc, (RECT?)null, callback, dwData); + if (!ok) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + return nearestMonitorInfo; + } + + private readonly HMONITOR _monitor; + + internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect) + { + RectMonitor = + new Rect(new Point(rect->left, rect->top), + new Point(rect->right, rect->bottom)); + _monitor = monitor; + var info = new MONITORINFOEXW() { monitorInfo = new MONITORINFO() { cbSize = (uint)sizeof(MONITORINFOEXW) } }; + GetMonitorInfo(monitor, ref info); + RectWork = + new Rect(new Point(info.monitorInfo.rcWork.left, info.monitorInfo.rcWork.top), + new Point(info.monitorInfo.rcWork.right, info.monitorInfo.rcWork.bottom)); + Name = new string(info.szDevice.AsSpan()).Replace("\0", "").Trim(); + } + + /// + /// Gets the name of the display. + /// + public string Name { get; } + + /// + /// Gets the display monitor rectangle, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectMonitor { get; } + + /// + /// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates. + /// + /// + /// If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rect RectWork { get; } + + /// + /// Gets if the monitor is the the primary display monitor. + /// + public bool IsPrimary => _monitor == PInvoke.MonitorFromWindow(new(IntPtr.Zero), MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY); + + /// + public override string ToString() => $"{Name} {RectMonitor.Width}x{RectMonitor.Height}"; + + private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi) + { + fixed (MONITORINFOEXW* lpmiLocal = &lpmi) + { + var lpmiBase = (MONITORINFO*)lpmiLocal; + var __result = PInvoke.GetMonitorInfo(hMonitor, lpmiBase); + return __result; + } + } +} diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt index f117534a1ff..f080f24de15 100644 --- a/Flow.Launcher.Infrastructure/NativeMethods.txt +++ b/Flow.Launcher.Infrastructure/NativeMethods.txt @@ -16,4 +16,34 @@ WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP -EnumWindows \ No newline at end of file +EnumWindows + +DwmSetWindowAttribute +DWM_SYSTEMBACKDROP_TYPE +DWM_WINDOW_CORNER_PREFERENCE + +MAX_PATH +SystemParametersInfo + +SetForegroundWindow + +GetWindowLong +GetForegroundWindow +GetDesktopWindow +GetShellWindow +GetWindowRect +GetClassName +FindWindowEx +WINDOW_STYLE + +SetLastError +WINDOW_EX_STYLE + +GetSystemMetrics +EnumDisplayMonitors +MonitorFromWindow +GetMonitorInfo +MONITORINFOEXW + +WM_ENTERSIZEMOVE +WM_EXITSIZEMOVE \ No newline at end of file diff --git a/Flow.Launcher.Infrastructure/PInvokeExtensions.cs b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs new file mode 100644 index 00000000000..1a72ab7a66a --- /dev/null +++ b/Flow.Launcher.Infrastructure/PInvokeExtensions.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Windows.Win32; + +// Edited from: https://github.com/files-community/Files +internal static partial class PInvoke +{ + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] + static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] + static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + + // NOTE: + // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) + { + return sizeof(nint) is 4 + ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) + : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); + } +} diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 93f6db111a2..63debfb474c 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -76,6 +76,7 @@ public string Theme } } public bool UseDropShadowEffect { get; set; } = true; + public BackdropTypes BackdropType{ get; set; } = BackdropTypes.None; /* Appearance Settings. It should be separated from the setting later.*/ public double WindowHeightSize { get; set; } = 42; @@ -430,4 +431,12 @@ public enum AnimationSpeeds Fast, Custom } + + public enum BackdropTypes + { + None, + Acrylic, + Mica, + MicaAlt + } } diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 867fef4f509..8dbe3f7e9eb 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -1,7 +1,14 @@ using System; +using System.ComponentModel; using System.Runtime.InteropServices; -using System.Windows.Interop; using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.WindowsAndMessaging; +using Flow.Launcher.Infrastructure.UserSettings; namespace Flow.Launcher.Infrastructure { @@ -9,93 +16,306 @@ public static class Win32Helper { #region Blur Handling - /* - Found on https://github.com/riverar/sample-win10-aeroglass - */ + public static bool IsBackdropSupported() + { + // Mica and Acrylic only supported Windows 11 22000+ + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.OSVersion.Version.Build >= 22000; + } + + public static unsafe bool DWMSetCloakForWindow(Window window, bool cloak) + { + var cloaked = cloak ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_CLOAK, + &cloaked, + (uint)Marshal.SizeOf()).Succeeded; + } - private enum AccentState + public static unsafe bool DWMSetBackdropForWindow(Window window, BackdropTypes backdrop) { - ACCENT_DISABLED = 0, - ACCENT_ENABLE_GRADIENT = 1, - ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, - ACCENT_ENABLE_BLURBEHIND = 3, - ACCENT_INVALID_STATE = 4 + var backdropType = backdrop switch + { + BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW, + BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW, + BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW, + _ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE, + &backdropType, + (uint)Marshal.SizeOf()).Succeeded; } - [StructLayout(LayoutKind.Sequential)] - private struct AccentPolicy + public static unsafe bool DWMSetDarkModeForWindow(Window window, bool useDarkMode) { - public AccentState AccentState; - public int AccentFlags; - public int GradientColor; - public int AnimationId; + var darkMode = useDarkMode ? 1 : 0; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &darkMode, + (uint)Marshal.SizeOf()).Succeeded; } - [StructLayout(LayoutKind.Sequential)] - private struct WindowCompositionAttributeData + /// + /// + /// + /// + /// DoNotRound, Round, RoundSmall, Default + /// + public static unsafe bool DWMSetCornerPreferenceForWindow(Window window, string cornerType) { - public WindowCompositionAttribute Attribute; - public IntPtr Data; - public int SizeOfData; + var preference = cornerType switch + { + "DoNotRound" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DONOTROUND, + "Round" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND, + "RoundSmall" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUNDSMALL, + "Default" => DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_DEFAULT, + _ => throw new InvalidOperationException("Invalid corner type") + }; + + return PInvoke.DwmSetWindowAttribute( + GetWindowHandle(window), + DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, + &preference, + (uint)Marshal.SizeOf()).Succeeded; } - private enum WindowCompositionAttribute + #endregion + + #region Wallpaper + + public static unsafe string GetWallpaperPath() { - WCA_ACCENT_POLICY = 19 + var wallpaperPtr = stackalloc char[(int)PInvoke.MAX_PATH]; + PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, PInvoke.MAX_PATH, + wallpaperPtr, + 0); + var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr); + + return wallpaper.ToString(); } - [DllImport("user32.dll")] - private static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); + #endregion + + #region Window Foreground + + public static nint GetForegroundWindow() + { + return PInvoke.GetForegroundWindow().Value; + } + + public static bool SetForegroundWindow(Window window) + { + return PInvoke.SetForegroundWindow(GetWindowHandle(window)); + } + + public static bool SetForegroundWindow(nint handle) + { + return PInvoke.SetForegroundWindow(new(handle)); + } + + #endregion + + #region Task Switching /// - /// Checks if the blur theme is enabled + /// Hide windows in the Alt+Tab window list /// - public static bool IsBlurTheme() + /// To hide a window + public static void HideFromAltTab(Window window) { - if (Environment.OSVersion.Version >= new Version(6, 2)) - { - var resource = Application.Current.TryFindResource("ThemeBlurEnabled"); + var hwnd = GetWindowHandle(window); - if (resource is bool b) - return b; + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); - return false; - } + // Add TOOLWINDOW style, remove APPWINDOW style + var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; - return false; + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); } /// - /// Sets the blur for a window via SetWindowCompositionAttribute + /// Restore window display in the Alt+Tab window list. /// - public static void SetBlurForWindow(Window w, bool blur) + /// To restore the displayed window + public static void ShowInAltTab(Window window) { - SetWindowAccent(w, blur ? AccentState.ACCENT_ENABLE_BLURBEHIND : AccentState.ACCENT_DISABLED); + var hwnd = GetWindowHandle(window); + + var exStyle = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); + + // Remove the TOOLWINDOW style and add the APPWINDOW style. + var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; + + SetWindowStyle(GetWindowHandle(window), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); } - private static void SetWindowAccent(Window w, AccentState state) + /// + /// Disable windows toolbar's control box + /// This will also disable system menu with Alt+Space hotkey + /// + public static void DisableControlBox(Window window) { - var windowHelper = new WindowInteropHelper(w); + var hwnd = GetWindowHandle(window); - windowHelper.EnsureHandle(); + var style = GetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - var accent = new AccentPolicy { AccentState = state }; - var accentStructSize = Marshal.SizeOf(accent); + style &= ~(int)WINDOW_STYLE.WS_SYSMENU; - var accentPtr = Marshal.AllocHGlobal(accentStructSize); - Marshal.StructureToPtr(accent, accentPtr, false); + SetWindowStyle(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, style); + } - var data = new WindowCompositionAttributeData + private static int GetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex) + { + var style = PInvoke.GetWindowLong(hWnd, nIndex); + if (style == 0 && Marshal.GetLastPInvokeError() != 0) { - Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, - SizeOfData = accentStructSize, - Data = accentPtr - }; + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + return style; + } + + private static nint SetWindowStyle(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) + { + PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error + + var result = PInvoke.SetWindowLongPtr(hWnd, nIndex, dwNewLong); + if (result == 0 && Marshal.GetLastPInvokeError() != 0) + { + throw new Win32Exception(Marshal.GetLastPInvokeError()); + } + + return result; + } - SetWindowCompositionAttribute(windowHelper.Handle, ref data); + #endregion + + #region Window Fullscreen + + private const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; + private const string WINDOW_CLASS_WINTAB = "Flip3D"; + private const string WINDOW_CLASS_PROGMAN = "Progman"; + private const string WINDOW_CLASS_WORKERW = "WorkerW"; + + private static HWND _hwnd_shell; + private static HWND HWND_SHELL => + _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow(); + + private static HWND _hwnd_desktop; + private static HWND HWND_DESKTOP => + _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow(); + + public static unsafe bool IsForegroundWindowFullscreen() + { + // Get current active window + var hWnd = PInvoke.GetForegroundWindow(); + if (hWnd.Equals(HWND.Null)) + { + return false; + } + + // If current active window is desktop or shell, exit early + if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)) + { + return false; + } - Marshal.FreeHGlobal(accentPtr); + string windowClass; + const int capacity = 256; + Span buffer = stackalloc char[capacity]; + int validLength; + fixed (char* pBuffer = buffer) + { + validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity); + } + + windowClass = buffer[..validLength].ToString(); + + // For Win+Tab (Flip3D) + if (windowClass == WINDOW_CLASS_WINTAB) + { + return false; + } + + PInvoke.GetWindowRect(hWnd, out var appBounds); + + // For console (ConsoleWindowClass), we have to check for negative dimensions + if (windowClass == WINDOW_CLASS_CONSOLE) + { + return appBounds.top < 0 && appBounds.bottom < 0; + } + + // For desktop (Progman or WorkerW, depends on the system), we have to check + if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW) + { + var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null); + hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView"); + if (hWndDesktop.Value != IntPtr.Zero) + { + return false; + } + } + + var monitorInfo = MonitorInfo.GetNearestDisplayMonitor(hWnd); + return (appBounds.bottom - appBounds.top) == monitorInfo.RectMonitor.Height && + (appBounds.right - appBounds.left) == monitorInfo.RectMonitor.Width; + } + + #endregion + + #region Pixel to DIP + + /// + /// Transforms pixels to Device Independent Pixels used by WPF + /// + /// current window, required to get presentation source + /// horizontal position in pixels + /// vertical position in pixels + /// point containing device independent pixels + public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) + { + Matrix matrix; + var source = PresentationSource.FromVisual(visual); + if (source is not null) + { + matrix = source.CompositionTarget.TransformFromDevice; + } + else + { + using var src = new HwndSource(new HwndSourceParameters()); + matrix = src.CompositionTarget.TransformFromDevice; + } + + return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); } + + #endregion + + #region WndProc + + public const int WM_ENTERSIZEMOVE = (int)PInvoke.WM_ENTERSIZEMOVE; + public const int WM_EXITSIZEMOVE = (int)PInvoke.WM_EXITSIZEMOVE; + + #endregion + + #region Window Handle + + internal static HWND GetWindowHandle(Window window, bool ensure = false) + { + var windowHelper = new WindowInteropHelper(window); + if (ensure) + { + windowHelper.EnsureHandle(); + } + return new(windowHelper.Handle); + } + #endregion } } diff --git a/Flow.Launcher/App.xaml b/Flow.Launcher/App.xaml index 17c0ae0d50a..565bbe3c74c 100644 --- a/Flow.Launcher/App.xaml +++ b/Flow.Launcher/App.xaml @@ -4,7 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="http://schemas.modernwpf.com/2019" ShutdownMode="OnMainWindowClose" - Startup="OnStartupAsync"> + Startup="OnStartup"> diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 550b1bdaeac..23c77618f5d 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -101,15 +101,15 @@ public static void Main() { if (SingleInstance.InitializeAsFirstInstance(Unique)) { - using (var application = new App()) - { - application.InitializeComponent(); - application.Run(); - } + using var application = new App(); + application.InitializeComponent(); + application.Run(); } } - private async void OnStartupAsync(object sender, StartupEventArgs e) +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnStartup(object sender, StartupEventArgs e) { await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => { @@ -128,7 +128,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future - InternationalizationManager.Instance.ChangeLanguage(_settings.Language); + Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); PluginManager.LoadPlugins(_settings.PluginSettings); @@ -137,8 +137,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => await PluginManager.InitializePluginsAsync(); await imageLoadertask; - var mainVM = Ioc.Default.GetRequiredService(); - var window = new MainWindow(_settings, mainVM); + var window = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); @@ -149,7 +148,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => // main windows needs initialized before theme change because of blur settings // TODO: Clean ThemeManager.Instance in future - ThemeManager.Instance.ChangeTheme(_settings.Theme); + Ioc.Default.GetRequiredService().ChangeTheme(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); @@ -164,6 +163,8 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => }); } +#pragma warning restore VSTHRD100 // Avoid async void methods + private void AutoStartup() { // we try to enable auto-startup on first launch, or reenable if it was removed @@ -186,8 +187,7 @@ private void AutoStartup() // but if it fails (permissions, etc) then don't keep retrying // this also gives the user a visual indication in the Settings widget _settings.StartFlowLauncherOnSystemStartup = false; - Notification.Show(InternationalizationManager.Instance.GetTranslation("setAutoStartFailed"), - e.Message); + API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message); } } } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 1e305d3d9cf..91cab9fa0ad 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -94,10 +94,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Flow.Launcher/Helper/DWMDropShadow.cs b/Flow.Launcher/Helper/DWMDropShadow.cs deleted file mode 100644 index 58817d70e03..00000000000 --- a/Flow.Launcher/Helper/DWMDropShadow.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Windows; -using System.Windows.Interop; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.Graphics.Dwm; -using Windows.Win32.UI.Controls; - -namespace Flow.Launcher.Helper; - -public class DwmDropShadow -{ - - /// - /// Drops a standard shadow to a WPF Window, even if the window isborderless. Only works with DWM (Vista and Seven). - /// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect, - /// as AllowsTransparency involves a huge permormance issue (hardware acceleration is turned off for all the window). - /// - /// Window to which the shadow will be applied - public static void DropShadowToWindow(Window window) - { - if (!DropShadow(window)) - { - window.SourceInitialized += window_SourceInitialized; - } - } - - private static void window_SourceInitialized(object sender, EventArgs e) //fixed typo - { - Window window = (Window)sender; - - DropShadow(window); - - window.SourceInitialized -= window_SourceInitialized; - } - - /// - /// The actual method that makes API calls to drop the shadow to the window - /// - /// Window to which the shadow will be applied - /// True if the method succeeded, false if not - private static unsafe bool DropShadow(Window window) - { - try - { - WindowInteropHelper helper = new WindowInteropHelper(window); - int val = 2; - var ret1 = PInvoke.DwmSetWindowAttribute(new (helper.Handle), DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY, &val, 4); - - if (ret1 == HRESULT.S_OK) - { - var m = new MARGINS { cyBottomHeight = 0, cxLeftWidth = 0, cxRightWidth = 0, cyTopHeight = 0 }; - var ret2 = PInvoke.DwmExtendFrameIntoClientArea(new(helper.Handle), &m); - return ret2 == HRESULT.S_OK; - } - - return false; - } - catch (Exception) - { - // Probably dwmapi.dll not found (incompatible OS) - return false; - } - } - -} diff --git a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs index a3bd83a97a6..151ce97dd83 100644 --- a/Flow.Launcher/Helper/WallpaperPathRetrieval.cs +++ b/Flow.Launcher/Helper/WallpaperPathRetrieval.cs @@ -2,19 +2,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; +using Flow.Launcher.Infrastructure; using Microsoft.Win32; -using Windows.Win32; -using Windows.Win32.UI.WindowsAndMessaging; namespace Flow.Launcher.Helper; public static class WallpaperPathRetrieval { - private static readonly int MAX_PATH = 260; private static readonly int MAX_CACHE_SIZE = 3; private static readonly Dictionary<(string, DateTime), ImageBrush> wallpaperCache = new(); @@ -29,7 +26,7 @@ public static Brush GetWallpaperBrush() try { - var wallpaperPath = GetWallpaperPath(); + var wallpaperPath = Win32Helper.GetWallpaperPath(); if (wallpaperPath is not null && File.Exists(wallpaperPath)) { // Since the wallpaper file name can be the same (TranscodedWallpaper), @@ -78,17 +75,6 @@ public static Brush GetWallpaperBrush() } } - private static unsafe string GetWallpaperPath() - { - var wallpaperPtr = stackalloc char[MAX_PATH]; - PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETDESKWALLPAPER, (uint)MAX_PATH, - wallpaperPtr, - 0); - var wallpaper = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(wallpaperPtr); - - return wallpaper.ToString(); - } - private static Color GetWallpaperColor() { RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Control Panel\Colors", true); diff --git a/Flow.Launcher/Helper/WindowsInteropHelper.cs b/Flow.Launcher/Helper/WindowsInteropHelper.cs deleted file mode 100644 index 3e57948a5f4..00000000000 --- a/Flow.Launcher/Helper/WindowsInteropHelper.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.ComponentModel; -using System.Drawing; -using System.Runtime.InteropServices; -using System.Windows; -using System.Windows.Forms; -using System.Windows.Interop; -using System.Windows.Media; -using Windows.Win32; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; -using Point = System.Windows.Point; - -namespace Flow.Launcher.Helper; - -public class WindowsInteropHelper -{ - private static HWND _hwnd_shell; - private static HWND _hwnd_desktop; - - //Accessors for shell and desktop handlers - //Will set the variables once and then will return them - private static HWND HWND_SHELL - { - get - { - return _hwnd_shell != HWND.Null ? _hwnd_shell : _hwnd_shell = PInvoke.GetShellWindow(); - } - } - - private static HWND HWND_DESKTOP - { - get - { - return _hwnd_desktop != HWND.Null ? _hwnd_desktop : _hwnd_desktop = PInvoke.GetDesktopWindow(); - } - } - - const string WINDOW_CLASS_CONSOLE = "ConsoleWindowClass"; - const string WINDOW_CLASS_WINTAB = "Flip3D"; - const string WINDOW_CLASS_PROGMAN = "Progman"; - const string WINDOW_CLASS_WORKERW = "WorkerW"; - - public unsafe static bool IsWindowFullscreen() - { - //get current active window - var hWnd = PInvoke.GetForegroundWindow(); - - if (hWnd.Equals(HWND.Null)) - { - return false; - } - - //if current active window is desktop or shell, exit early - if (hWnd.Equals(HWND_DESKTOP) || hWnd.Equals(HWND_SHELL)) - { - return false; - } - - string windowClass; - const int capacity = 256; - Span buffer = stackalloc char[capacity]; - int validLength; - fixed (char* pBuffer = buffer) - { - validLength = PInvoke.GetClassName(hWnd, pBuffer, capacity); - } - - windowClass = buffer[..validLength].ToString(); - - - //for Win+Tab (Flip3D) - if (windowClass == WINDOW_CLASS_WINTAB) - { - return false; - } - - PInvoke.GetWindowRect(hWnd, out var appBounds); - - //for console (ConsoleWindowClass), we have to check for negative dimensions - if (windowClass == WINDOW_CLASS_CONSOLE) - { - return appBounds.top < 0 && appBounds.bottom < 0; - } - - //for desktop (Progman or WorkerW, depends on the system), we have to check - if (windowClass is WINDOW_CLASS_PROGMAN or WINDOW_CLASS_WORKERW) - { - var hWndDesktop = PInvoke.FindWindowEx(hWnd, HWND.Null, "SHELLDLL_DefView", null); - hWndDesktop = PInvoke.FindWindowEx(hWndDesktop, HWND.Null, "SysListView32", "FolderView"); - if (hWndDesktop.Value != (IntPtr.Zero)) - { - return false; - } - } - - Rectangle screenBounds = Screen.FromHandle(hWnd).Bounds; - return (appBounds.bottom - appBounds.top) == screenBounds.Height && - (appBounds.right - appBounds.left) == screenBounds.Width; - } - - /// - /// disable windows toolbar's control box - /// this will also disable system menu with Alt+Space hotkey - /// - public static void DisableControlBox(Window win) - { - var hwnd = new HWND(new WindowInteropHelper(win).Handle); - - var style = PInvoke.GetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE); - - if (style == 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - - style &= ~(int)WINDOW_STYLE.WS_SYSMENU; - - var previousStyle = PInvoke.SetWindowLong(hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, - style); - - if (previousStyle == 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - } - - /// - /// Transforms pixels to Device Independent Pixels used by WPF - /// - /// current window, required to get presentation source - /// horizontal position in pixels - /// vertical position in pixels - /// point containing device independent pixels - public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY) - { - Matrix matrix; - var source = PresentationSource.FromVisual(visual); - if (source is not null) - { - matrix = source.CompositionTarget.TransformFromDevice; - } - else - { - using var src = new HwndSource(new HwndSourceParameters()); - matrix = src.CompositionTarget.TransformFromDevice; - } - - return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY)); - } - - #region Alt Tab - - private static int SetWindowLong(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, int dwNewLong) - { - PInvoke.SetLastError(WIN32_ERROR.NO_ERROR); // Clear any existing error - - var result = PInvoke.SetWindowLong(hWnd, nIndex, dwNewLong); - if (result == 0 && Marshal.GetLastPInvokeError() != 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - - return result; - } - - /// - /// Hide windows in the Alt+Tab window list - /// - /// To hide a window - public static void HideFromAltTab(Window window) - { - var exStyle = GetCurrentWindowStyle(window); - - // Add TOOLWINDOW style, remove APPWINDOW style - var newExStyle = ((uint)exStyle | (uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) & ~(uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; - - SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); - } - - /// - /// Restore window display in the Alt+Tab window list. - /// - /// To restore the displayed window - public static void ShowInAltTab(Window window) - { - var exStyle = GetCurrentWindowStyle(window); - - // Remove the TOOLWINDOW style and add the APPWINDOW style. - var newExStyle = ((uint)exStyle & ~(uint)WINDOW_EX_STYLE.WS_EX_TOOLWINDOW) | (uint)WINDOW_EX_STYLE.WS_EX_APPWINDOW; - - SetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (int)newExStyle); - } - - /// - /// To obtain the current overridden style of a window. - /// - /// To obtain the style dialog window - /// current extension style value - private static int GetCurrentWindowStyle(Window window) - { - var style = PInvoke.GetWindowLong(new(new WindowInteropHelper(window).Handle), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE); - if (style == 0 && Marshal.GetLastPInvokeError() != 0) - { - throw new Win32Exception(Marshal.GetLastPInvokeError()); - } - return style; - } - - #endregion -} diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index a3f87cd30d4..8bcfd97152f 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -104,6 +104,11 @@ Always Preview Always open preview panel when Flow activates. Press {0} to toggle preview. Shadow effect is not allowed while current theme has blur effect enabled + Backdrop Type + None + Acrylic + Mica + Mica Alt Search Plugin diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 0720501cab5..5b63303acf7 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -20,17 +20,17 @@ Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" - SourceInitialized="OnSourceInitialized" - Initialized="OnInitialized" Left="{Binding Settings.WindowLeft, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Loaded="OnLoaded" LocationChanged="OnLocationChanged" Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="OnKeyDown" PreviewKeyUp="OnKeyUp" + PreviewMouseMove="OnPreviewMouseMove" ResizeMode="CanResize" ShowInTaskbar="False" SizeToContent="Height" + SourceInitialized="OnSourceInitialized" Topmost="True" Visibility="{Binding MainWindowVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" WindowStartupLocation="Manual" @@ -240,14 +240,14 @@ FontSize="{Binding QueryBoxFontSize, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" InputMethod.PreferredImeConversionMode="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEConversionModeConverter}}" InputMethod.PreferredImeState="{Binding StartWithEnglishMode, Converter={StaticResource BoolToIMEStateConverter}}" - PreviewDragOver="OnPreviewDragOver" + PreviewDragOver="QueryTextBox_OnPreviewDragOver" PreviewKeyUp="QueryTextBox_KeyUp" Style="{DynamicResource QueryBoxStyle}" Text="{Binding QueryText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Visibility="Visible" WindowChrome.IsHitTestVisibleInChrome="True"> - + @@ -377,7 +377,7 @@ Style="{DynamicResource SeparatorStyle}" /> - + diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 1d4a2610104..2ce3d1e95e6 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1,33 +1,29 @@ using System; using System.ComponentModel; +using System.Linq; +using System.Media; using System.Threading.Tasks; using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; using System.Windows.Media.Animation; -using System.Windows.Controls; -using System.Windows.Forms; +using System.Windows.Shapes; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; -using Flow.Launcher.Helper; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.ViewModel; -using Screen = System.Windows.Forms.Screen; -using DragEventArgs = System.Windows.DragEventArgs; -using KeyEventArgs = System.Windows.Input.KeyEventArgs; -using NotifyIcon = System.Windows.Forms.NotifyIcon; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Hotkey; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; -using System.Windows.Threading; -using System.Windows.Data; +using Flow.Launcher.ViewModel; using ModernWpf.Controls; -using Key = System.Windows.Input.Key; -using System.Media; -using DataObject = System.Windows.DataObject; -using System.Windows.Media; -using System.Windows.Interop; -using Windows.Win32; -using System.Windows.Shapes; +using MouseButtons = System.Windows.Forms.MouseButtons; +using NotifyIcon = System.Windows.Forms.NotifyIcon; +using Screen = System.Windows.Forms.Screen; namespace Flow.Launcher { @@ -35,160 +31,114 @@ public partial class MainWindow { #region Private Fields - private Settings _settings; + // Dependency Injection + private readonly Settings _settings; + private readonly Theme _theme; + + // Window Notify Icon private NotifyIcon _notifyIcon; - private ContextMenu contextMenu = new ContextMenu(); - private MainViewModel _viewModel; - private bool _animating; + + // Window Context Menu + private readonly ContextMenu contextMenu = new(); + private readonly MainViewModel _viewModel; + + // Window Event : Key Event private bool isArrowKeyPressed = false; + // Window Sound Effects private MediaPlayer animationSoundWMP; private SoundPlayer animationSoundWPF; + // Window WndProc + private int _initialWidth; + private int _initialHeight; + + // Window Animation + private const double DefaultRightMargin = 66; //* this value from base.xaml + private bool _animating; + private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부 + #endregion - public MainWindow(Settings settings, MainViewModel mainVM) + #region Constructor + + public MainWindow() { - DataContext = mainVM; - _viewModel = mainVM; - _settings = settings; + _settings = Ioc.Default.GetRequiredService(); + _theme = Ioc.Default.GetRequiredService(); + _viewModel = Ioc.Default.GetRequiredService(); + DataContext = _viewModel; InitializeComponent(); - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); + UpdatePosition(true); InitSoundEffects(); - - DataObject.AddPastingHandler(QueryTextBox, OnPaste); - - Loaded += (_, _) => - { - var handle = new WindowInteropHelper(this).Handle; - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); - }; + DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste); } - private int _initialWidth; - private int _initialHeight; - - private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - if (msg == PInvoke.WM_ENTERSIZEMOVE) - { - _initialWidth = (int)Width; - _initialHeight = (int)Height; - handled = true; - } - - if (msg == PInvoke.WM_EXITSIZEMOVE) - { - if (_initialHeight != (int)Height) - { - OnResizeEnd(); - } + #endregion - if (_initialWidth != (int)Width) - { - FlowMainWindow.SizeToContent = SizeToContent.Height; - } + #region Window Event - handled = true; - } +#pragma warning disable VSTHRD100 // Avoid async void methods - return IntPtr.Zero; + private void OnSourceInitialized(object sender, EventArgs e) + { + var handle = Win32Helper.GetWindowHandle(this, true); + var win = HwndSource.FromHwnd(handle); + win.AddHook(WndProc); + Win32Helper.HideFromAltTab(this); + Win32Helper.DisableControlBox(this); } - private void OnResizeEnd() + private async void OnLoaded(object sender, RoutedEventArgs _) { - int shadowMargin = 0; - if (_settings.UseDropShadowEffect) + // Check first launch + if (_settings.FirstLaunch) { - shadowMargin = 32; + _settings.FirstLaunch = false; + App.API.SaveAppAllSettings(); + var WelcomeWindow = new WelcomeWindow(); + WelcomeWindow.Show(); } - if (!_settings.KeepMaxResults) + // Hide window if need + UpdatePosition(true); + if (_settings.HideOnStartup) { - var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; - - if (itemCount < 2) - { - _settings.MaxResultsToShow = 2; - } - else - { - _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); - } + _viewModel.Hide(); + } + else + { + _viewModel.Show(); } - FlowMainWindow.SizeToContent = SizeToContent.Height; - _viewModel.MainWindowWidth = Width; - } + // Show notify icon when flowlauncher is hidden + InitializeNotifyIcon(); - private void OnCopy(object sender, ExecutedRoutedEventArgs e) - { - var result = _viewModel.Results.SelectedItem?.Result; - if (QueryTextBox.SelectionLength == 0 && result != null) + // Initialize color scheme + if (_settings.ColorScheme == Constant.Light) { - string copyText = result.CopyText; - App.API.CopyToClipboard(copyText, directCopy: true); + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; } - else if (!string.IsNullOrEmpty(QueryTextBox.Text)) + else if (_settings.ColorScheme == Constant.Dark) { - App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); + ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; } - } - private void OnPaste(object sender, DataObjectPastingEventArgs e) - { - var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.UnicodeText, true); - if (isText) - { - var text = e.SourceDataObject.GetData(System.Windows.DataFormats.UnicodeText) as string; - text = text.Replace(Environment.NewLine, " "); - DataObject data = new DataObject(); - data.SetData(System.Windows.DataFormats.UnicodeText, text); - e.DataObject = data; - } - } + // Initialize position + InitProgressbarAnimation(); - private async void OnClosing(object sender, CancelEventArgs e) - { - _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); - Notification.Uninstall(); - Environment.Exit(0); - } + // Force update position + UpdatePosition(true); - private void OnSourceInitialized(object sender, EventArgs e) - { - WindowsInteropHelper.HideFromAltTab(this); - } + // Refresh frame + await Ioc.Default.GetRequiredService().RefreshFrameAsync(); - private void OnInitialized(object sender, EventArgs e) - { - } + // Reset preview + _viewModel.ResetPreview(); - private void OnLoaded(object sender, RoutedEventArgs _) - { - // MouseEventHandler - PreviewMouseMove += MainPreviewMouseMove; - CheckFirstLaunch(); - HideStartup(); - // show notify icon when flowlauncher is hidden - InitializeNotifyIcon(); - InitializeColorScheme(); - WindowsInteropHelper.DisableControlBox(this); - InitProgressbarAnimation(); - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); - PreviewReset(); - // since the default main window visibility is visible - // so we need set focus during startup + // Since the default main window visibility is visible, so we need set focus during startup QueryTextBox.Focus(); _viewModel.PropertyChanged += (o, e) => @@ -196,37 +146,39 @@ private void OnLoaded(object sender, RoutedEventArgs _) switch (e.PropertyName) { case nameof(MainViewModel.MainWindowVisibilityStatus): - { - Dispatcher.Invoke(() => { - if (_viewModel.MainWindowVisibilityStatus) + Dispatcher.Invoke(() => { - if (_settings.UseSound) + if (_viewModel.MainWindowVisibilityStatus) { - SoundPlay(); + if (_settings.UseSound) + { + SoundPlay(); + } + + UpdatePosition(false); + _viewModel.ResetPreview(); + Activate(); + QueryTextBox.Focus(); + _settings.ActivateTimes++; + if (!_viewModel.LastQuerySelected) + { + QueryTextBox.SelectAll(); + _viewModel.LastQuerySelected = true; + } + + if (_settings.UseAnimation) + WindowAnimation(); } - - UpdatePosition(); - PreviewReset(); - Activate(); - QueryTextBox.Focus(); - _settings.ActivateTimes++; - if (!_viewModel.LastQuerySelected) - { - QueryTextBox.SelectAll(); - _viewModel.LastQuerySelected = true; - } - - if (_settings.UseAnimation) - WindowAnimator(); - } - }); - break; - } + }); + break; + } case nameof(MainViewModel.QueryTextCursorMovedToEnd): if (_viewModel.QueryTextCursorMovedToEnd) { - MoveQueryTextToEnd(); + // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. + // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority + Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); _viewModel.QueryTextCursorMovedToEnd = false; } @@ -260,113 +212,310 @@ private void OnLoaded(object sender, RoutedEventArgs _) break; } }; + + // ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) + QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); + + // ✅ ContextMenu.Visibility 변경 감지 + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(ContextMenu)) + .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); + + // ✅ History.Visibility 변경 감지 + DependencyPropertyDescriptor + .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 + .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void InitializePosition() + private async void OnClosing(object sender, CancelEventArgs e) { - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePositionInner(); - InitializePositionInner(); - return; + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + Environment.Exit(0); + } - void InitializePositionInner() + private void OnLocationChanged(object sender, EventArgs e) + { + if (_animating) + return; + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) + _settings.WindowLeft = Left; + _settings.WindowTop = Top; + } + } + + private async void OnDeactivated(object sender, EventArgs e) + { + _settings.WindowLeft = Left; + _settings.WindowTop = Top; + ClockPanel.Opacity = 0; + SearchIcon.Opacity = 0; + //This condition stops extra hide call when animator is on, + // which causes the toggling to occasional hide instead of show. + if (_viewModel.MainWindowVisibilityStatus) + { + // Need time to initialize the main query window animation. + // This also stops the mainwindow from flickering occasionally after Settings window is opened + // and always after Settings window is closed. + if (_settings.UseAnimation) + + await Task.Delay(100); + + if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) { - Top = _settings.WindowTop; - Left = _settings.WindowLeft; + _viewModel.Hide(); } - else + } + } + + private void OnKeyDown(object sender, KeyEventArgs e) + { + var specialKeyState = GlobalHotkey.CheckModifiers(); + switch (e.Key) + { + case Key.Down: + isArrowKeyPressed = true; + _viewModel.SelectNextItemCommand.Execute(null); + e.Handled = true; + break; + case Key.Up: + isArrowKeyPressed = true; + _viewModel.SelectPrevItemCommand.Execute(null); + e.Handled = true; + break; + case Key.PageDown: + _viewModel.SelectNextPageCommand.Execute(null); + e.Handled = true; + break; + case Key.PageUp: + _viewModel.SelectPrevPageCommand.Execute(null); + e.Handled = true; + break; + case Key.Right: + if (_viewModel.SelectedIsFromQueryResults() + && QueryTextBox.CaretIndex == QueryTextBox.Text.Length + && !string.IsNullOrEmpty(QueryTextBox.Text)) + { + _viewModel.LoadContextMenuCommand.Execute(null); + e.Handled = true; + } + + break; + case Key.Left: + if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) + { + _viewModel.EscCommand.Execute(null); + e.Handled = true; + } + + break; + case Key.Back: + if (specialKeyState.CtrlPressed) + { + if (_viewModel.SelectedIsFromQueryResults() + && QueryTextBox.Text.Length > 0 + && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) + { + var queryWithoutActionKeyword = + QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + + if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) + { + _viewModel.BackspaceCommand.Execute(null); + e.Handled = true; + } + } + } + + break; + default: + break; + } + } + + private void OnKeyUp(object sender, KeyEventArgs e) + { + if (e.Key == Key.Up || e.Key == Key.Down) + { + isArrowKeyPressed = false; + } + } + + private void OnPreviewMouseMove(object sender, MouseEventArgs e) + { + if (isArrowKeyPressed) + { + e.Handled = true; // Ignore Mouse Hover when press Arrowkeys + } + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window Boarder Event + + private void OnMouseDown(object sender, MouseButtonEventArgs e) + { + if (e.ChangedButton == MouseButton.Left) DragMove(); + } + + #endregion + + #region Window Context Menu Event + +#pragma warning disable VSTHRD100 // Avoid async void methods + + private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) + { + _viewModel.Hide(); + + if (_settings.UseAnimation) + await Task.Delay(100); + + App.API.OpenSettingDialog(); + } + +#pragma warning restore VSTHRD100 // Avoid async void methods + + #endregion + + #region Window WndProc + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if (msg == Win32Helper.WM_ENTERSIZEMOVE) + { + _initialWidth = (int)Width; + _initialHeight = (int)Height; + handled = true; + } + else if (msg == Win32Helper.WM_EXITSIZEMOVE) + { + if (_initialHeight != (int)Height) { - var screen = SelectedScreen(); - switch (_settings.SearchWindowAlign) + var shadowMargin = 0; + var (_, useDropShadowEffect) = _theme.GetActualValue(); + if (useDropShadowEffect) { - case SearchWindowAligns.Center: - Left = HorizonCenter(screen); - Top = VerticalCenter(screen); - break; - case SearchWindowAligns.CenterTop: - Left = HorizonCenter(screen); - Top = 10; - break; - case SearchWindowAligns.LeftTop: - Left = HorizonLeft(screen); - Top = 10; - break; - case SearchWindowAligns.RightTop: - Left = HorizonRight(screen); - Top = 10; - break; - case SearchWindowAligns.Custom: - Left = WindowsInteropHelper.TransformPixelsToDIP(this, - screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; - Top = WindowsInteropHelper.TransformPixelsToDIP(this, 0, - screen.WorkingArea.Y + _settings.CustomWindowTop).Y; - break; + shadowMargin = 32; + } + + if (!_settings.KeepMaxResults) + { + var itemCount = (Height - (_settings.WindowHeightSize + 14) - shadowMargin) / _settings.ItemHeightSize; + + if (itemCount < 2) + { + _settings.MaxResultsToShow = 2; + } + else + { + _settings.MaxResultsToShow = Convert.ToInt32(Math.Truncate(itemCount)); + } } + + SizeToContent = SizeToContent.Height; + _viewModel.MainWindowWidth = Width; + } + + if (_initialWidth != (int)Width) + { + SizeToContent = SizeToContent.Height; } + + handled = true; } + + return IntPtr.Zero; } - private void UpdateNotifyIconText() + #endregion + + #region Window Sound Effects + + private void InitSoundEffects() { - var menu = contextMenu; - ((MenuItem)menu.Items[0]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + - " (" + _settings.Hotkey + ")"; - ((MenuItem)menu.Items[1]).Header = InternationalizationManager.Instance.GetTranslation("GameMode"); - ((MenuItem)menu.Items[2]).Header = InternationalizationManager.Instance.GetTranslation("PositionReset"); - ((MenuItem)menu.Items[3]).Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"); - ((MenuItem)menu.Items[4]).Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"); + if (_settings.WMPInstalled) + { + animationSoundWMP = new MediaPlayer(); + animationSoundWMP.Open(new Uri(AppContext.BaseDirectory + "Resources\\open.wav")); + } + else + { + animationSoundWPF = new SoundPlayer(AppContext.BaseDirectory + "Resources\\open.wav"); + } + } + + private void SoundPlay() + { + if (_settings.WMPInstalled) + { + animationSoundWMP.Position = TimeSpan.Zero; + animationSoundWMP.Volume = _settings.SoundVolume / 100.0; + animationSoundWMP.Play(); + } + else + { + animationSoundWPF.Play(); + } } + #endregion + + #region Window Notify Icon + private void InitializeNotifyIcon() { _notifyIcon = new NotifyIcon { - Text = Infrastructure.Constant.FlowLauncherFullName, + Text = Constant.FlowLauncherFullName, Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, Visible = !_settings.HideNotifyIcon }; var openIcon = new FontIcon { Glyph = "\ue71e" }; var open = new MenuItem { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayOpen") + " (" + - _settings.Hotkey + ")", + Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")", Icon = openIcon }; var gamemodeIcon = new FontIcon { Glyph = "\ue7fc" }; var gamemode = new MenuItem { - Header = InternationalizationManager.Instance.GetTranslation("GameMode"), + Header = App.API.GetTranslation("GameMode"), Icon = gamemodeIcon }; var positionresetIcon = new FontIcon { Glyph = "\ue73f" }; var positionreset = new MenuItem { - Header = InternationalizationManager.Instance.GetTranslation("PositionReset"), + Header = App.API.GetTranslation("PositionReset"), Icon = positionresetIcon }; var settingsIcon = new FontIcon { Glyph = "\ue713" }; var settings = new MenuItem { - Header = InternationalizationManager.Instance.GetTranslation("iconTraySettings"), + Header = App.API.GetTranslation("iconTraySettings"), Icon = settingsIcon }; var exitIcon = new FontIcon { Glyph = "\ue7e8" }; var exit = new MenuItem { - Header = InternationalizationManager.Instance.GetTranslation("iconTrayExit"), + Header = App.API.GetTranslation("iconTrayExit"), Icon = exitIcon }; open.Click += (o, e) => _viewModel.ToggleFlowLauncher(); gamemode.Click += (o, e) => _viewModel.ToggleGameMode(); - positionreset.Click += (o, e) => PositionReset(); + positionreset.Click += (o, e) => _ = PositionResetAsync(); settings.Click += (o, e) => App.API.OpenSettingDialog(); exit.Click += (o, e) => Close(); - gamemode.ToolTip = InternationalizationManager.Instance.GetTranslation("GameModeToolTip"); - positionreset.ToolTip = InternationalizationManager.Instance.GetTranslation("PositionResetToolTip"); + gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); + positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); contextMenu.Items.Add(open); contextMenu.Items.Add(gamemode); @@ -387,7 +536,7 @@ private void InitializeNotifyIcon() // Get context menu handle and bring it to the foreground if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) { - PInvoke.SetForegroundWindow(new(hwndSource.Handle)); + Win32Helper.SetForegroundWindow(hwndSource.Handle); } contextMenu.Focus(); @@ -396,23 +545,34 @@ private void InitializeNotifyIcon() }; } - private void CheckFirstLaunch() + private void UpdateNotifyIconText() { - if (_settings.FirstLaunch) - { - _settings.FirstLaunch = false; - App.API.SaveAppAllSettings(); - OpenWelcomeWindow(); - } + var menu = contextMenu; + ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + + " (" + _settings.Hotkey + ")"; + ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); + ((MenuItem)menu.Items[2]).Header = App.API.GetTranslation("PositionReset"); + ((MenuItem)menu.Items[3]).Header = App.API.GetTranslation("iconTraySettings"); + ((MenuItem)menu.Items[4]).Header = App.API.GetTranslation("iconTrayExit"); } - private void OpenWelcomeWindow() + #endregion + + #region Window Position + + private void UpdatePosition(bool force) { - var WelcomeWindow = new WelcomeWindow(); - WelcomeWindow.Show(); + if (_animating && !force) + { + return; + } + + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePosition(); + InitializePosition(); } - private async void PositionReset() + private async Task PositionResetAsync() { _viewModel.Show(); await Task.Delay(300); // If don't give a time, Positioning will be weird. @@ -421,6 +581,116 @@ private async void PositionReset() Top = VerticalCenter(screen); } + private void InitializePosition() + { + // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 + InitializePositionInner(); + InitializePositionInner(); + return; + + void InitializePositionInner() + { + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) + { + Top = _settings.WindowTop; + Left = _settings.WindowLeft; + } + else + { + var screen = SelectedScreen(); + switch (_settings.SearchWindowAlign) + { + case SearchWindowAligns.Center: + Left = HorizonCenter(screen); + Top = VerticalCenter(screen); + break; + case SearchWindowAligns.CenterTop: + Left = HorizonCenter(screen); + Top = 10; + break; + case SearchWindowAligns.LeftTop: + Left = HorizonLeft(screen); + Top = 10; + break; + case SearchWindowAligns.RightTop: + Left = HorizonRight(screen); + Top = 10; + break; + case SearchWindowAligns.Custom: + Left = Win32Helper.TransformPixelsToDIP(this, + screen.WorkingArea.X + _settings.CustomWindowLeft, 0).X; + Top = Win32Helper.TransformPixelsToDIP(this, 0, + screen.WorkingArea.Y + _settings.CustomWindowTop).Y; + break; + } + } + } + } + + private Screen SelectedScreen() + { + Screen screen; + switch (_settings.SearchWindowScreen) + { + case SearchWindowScreens.Cursor: + screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); + break; + case SearchWindowScreens.Primary: + screen = Screen.PrimaryScreen; + break; + case SearchWindowScreens.Focus: + var foregroundWindowHandle = Win32Helper.GetForegroundWindow(); + screen = Screen.FromHandle(foregroundWindowHandle); + break; + case SearchWindowScreens.Custom: + if (_settings.CustomScreenNumber <= Screen.AllScreens.Length) + screen = Screen.AllScreens[_settings.CustomScreenNumber - 1]; + else + screen = Screen.AllScreens[0]; + break; + default: + screen = Screen.AllScreens[0]; + break; + } + + return screen ?? Screen.AllScreens[0]; + } + + private double HorizonCenter(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip2.X - ActualWidth) / 2 + dip1.X; + return left; + } + + private double VerticalCenter(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; + return top; + } + + private double HorizonRight(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var left = (dip1.X + dip2.X - ActualWidth) - 10; + return left; + } + + private double HorizonLeft(Screen screen) + { + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var left = dip1.X + 10; + return left; + } + + #endregion + + #region Window Animation + private void InitProgressbarAnimation() { var progressBarStoryBoard = new Storyboard(); @@ -468,20 +738,21 @@ private void InitProgressbarAnimation() _viewModel.ProgressBarVisibility = Visibility.Hidden; } - public void WindowAnimator() + private void WindowAnimation() { if (_animating) return; isArrowKeyPressed = true; _animating = true; - UpdatePosition(); + UpdatePosition(false); - Storyboard windowsb = new Storyboard(); - Storyboard clocksb = new Storyboard(); - Storyboard iconsb = new Storyboard(); - CircleEase easing = new CircleEase(); - easing.EasingMode = EasingMode.EaseInOut; + ClockPanel.Opacity = 0; + SearchIcon.Opacity = 0; + + var clocksb = new Storyboard(); + var iconsb = new Storyboard(); + CircleEase easing = new CircleEase { EasingMode = EasingMode.EaseInOut }; var animationLength = _settings.AnimationSpeed switch { @@ -491,28 +762,13 @@ public void WindowAnimator() _ => _settings.CustomAnimationLength }; - var WindowOpacity = new DoubleAnimation - { - From = 0, - To = 1, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; - - var WindowMotion = new DoubleAnimation - { - From = Top + 10, - To = Top, - Duration = TimeSpan.FromMilliseconds(animationLength * 2 / 3), - FillBehavior = FillBehavior.Stop - }; var IconMotion = new DoubleAnimation { From = 12, To = 0, EasingFunction = easing, Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop + FillBehavior = FillBehavior.HoldEnd }; var ClockOpacity = new DoubleAnimation @@ -521,47 +777,50 @@ public void WindowAnimator() To = 1, EasingFunction = easing, Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop + FillBehavior = FillBehavior.HoldEnd }; - double TargetIconOpacity = SearchIcon.Opacity; // Animation Target Opacity from Style + + double TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0); + var IconOpacity = new DoubleAnimation { From = 0, To = TargetIconOpacity, EasingFunction = easing, Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop + FillBehavior = FillBehavior.HoldEnd }; + + double rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right; - double right = ClockPanel.Margin.Right; var thicknessAnimation = new ThicknessAnimation { - From = new Thickness(0, 12, right, 0), - To = new Thickness(0, 0, right, 0), + From = new Thickness(0, 12, rightMargin, 0), + To = new Thickness(0, 0, rightMargin, 0), EasingFunction = easing, Duration = TimeSpan.FromMilliseconds(animationLength), - FillBehavior = FillBehavior.Stop + FillBehavior = FillBehavior.HoldEnd }; Storyboard.SetTargetProperty(ClockOpacity, new PropertyPath(OpacityProperty)); + Storyboard.SetTarget(ClockOpacity, ClockPanel); + Storyboard.SetTargetName(thicknessAnimation, "ClockPanel"); Storyboard.SetTargetProperty(thicknessAnimation, new PropertyPath(MarginProperty)); - Storyboard.SetTarget(WindowOpacity, this); - Storyboard.SetTargetProperty(WindowOpacity, new PropertyPath(Window.OpacityProperty)); - Storyboard.SetTargetProperty(WindowMotion, new PropertyPath(Window.TopProperty)); + + Storyboard.SetTarget(IconMotion, SearchIcon); Storyboard.SetTargetProperty(IconMotion, new PropertyPath(TopProperty)); + + Storyboard.SetTarget(IconOpacity, SearchIcon); Storyboard.SetTargetProperty(IconOpacity, new PropertyPath(OpacityProperty)); clocksb.Children.Add(thicknessAnimation); clocksb.Children.Add(ClockOpacity); - windowsb.Children.Add(WindowOpacity); - windowsb.Children.Add(WindowMotion); iconsb.Children.Add(IconMotion); iconsb.Children.Add(IconOpacity); - windowsb.Completed += (_, _) => _animating = false; + clocksb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; - _settings.WindowTop = Top; isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) @@ -570,277 +829,149 @@ public void WindowAnimator() } iconsb.Begin(SearchIcon); - windowsb.Begin(FlowMainWindow); } - private void InitSoundEffects() + private void UpdateClockPanelVisibility() { - if (_settings.WMPInstalled) - { - animationSoundWMP = new MediaPlayer(); - animationSoundWMP.Open(new Uri(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav")); - } - else + if (QueryTextBox == null || ContextMenu == null || History == null || ClockPanel == null) + return; + + var animationLength = _settings.AnimationSpeed switch { - animationSoundWPF = new SoundPlayer(AppDomain.CurrentDomain.BaseDirectory + "Resources\\open.wav"); - } - } + AnimationSpeeds.Slow => 560, + AnimationSpeeds.Medium => 360, + AnimationSpeeds.Fast => 160, + _ => _settings.CustomAnimationLength + }; - private void SoundPlay() - { - if (_settings.WMPInstalled) + var animationDuration = TimeSpan.FromMilliseconds(animationLength * 2 / 3); + + // ✅ Conditions for showing ClockPanel (No query input & ContextMenu, History are closed) + bool shouldShowClock = QueryTextBox.Text.Length == 0 && + ContextMenu.Visibility != Visibility.Visible && + History.Visibility != Visibility.Visible; + + // ✅ 1. When ContextMenu opens, immediately set Visibility.Hidden (force hide without animation) + if (ContextMenu.Visibility == Visibility.Visible) { - animationSoundWMP.Position = TimeSpan.Zero; - animationSoundWMP.Volume = _settings.SoundVolume / 100.0; - animationSoundWMP.Play(); + ClockPanel.Visibility = Visibility.Hidden; + ClockPanel.Opacity = 0.0; // Set to 0 in case Opacity animation affects it + return; } - else + + // ✅ 2. When ContextMenu is closed, keep it Hidden if there's text in the query (remember previous state) + if (ContextMenu.Visibility != Visibility.Visible && QueryTextBox.Text.Length > 0) { - animationSoundWPF.Play(); + ClockPanel.Visibility = Visibility.Hidden; + ClockPanel.Opacity = 0.0; + return; } - } - private void OnMouseDown(object sender, MouseButtonEventArgs e) - { - if (e.ChangedButton == MouseButton.Left) DragMove(); - } - - private void OnPreviewDragOver(object sender, DragEventArgs e) - { - e.Handled = true; - } - - private async void OnContextMenusForSettingsClick(object sender, RoutedEventArgs e) - { - _viewModel.Hide(); + // ✅ 3. When hiding ClockPanel (apply fade-out animation) + if ((!shouldShowClock) && ClockPanel.Visibility == Visibility.Visible && !_isClockPanelAnimating) + { + _isClockPanelAnimating = true; - if (_settings.UseAnimation) - await Task.Delay(100); + var fadeOut = new DoubleAnimation + { + From = 1.0, + To = 0.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; - App.API.OpenSettingDialog(); - } + fadeOut.Completed += (s, e) => + { + ClockPanel.Visibility = Visibility.Hidden; // ✅ Completely hide after animation + _isClockPanelAnimating = false; + }; - private async void OnDeactivated(object sender, EventArgs e) - { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; - //This condition stops extra hide call when animator is on, - // which causes the toggling to occasional hide instead of show. - if (_viewModel.MainWindowVisibilityStatus) + ClockPanel.BeginAnimation(OpacityProperty, fadeOut); + } + // ✅ 4. When showing ClockPanel (apply fade-in animation) + else if (shouldShowClock && ClockPanel.Visibility != Visibility.Visible && !_isClockPanelAnimating) { - // Need time to initialize the main query window animation. - // This also stops the mainwindow from flickering occasionally after Settings window is opened - // and always after Settings window is closed. - if (_settings.UseAnimation) - await Task.Delay(100); + _isClockPanelAnimating = true; - if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible) + Application.Current.Dispatcher.Invoke(() => { - _viewModel.Hide(); - } - } - } - - private void UpdatePosition() - { - if (_animating) - return; - - // Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910 - InitializePosition(); - InitializePosition(); - } + ClockPanel.Visibility = Visibility.Visible; // ✅ Set Visibility to Visible first - private void OnLocationChanged(object sender, EventArgs e) - { - if (_animating) - return; - if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) - { - _settings.WindowLeft = Left; - _settings.WindowTop = Top; + var fadeIn = new DoubleAnimation + { + From = 0.0, + To = 1.0, + Duration = animationDuration, + FillBehavior = FillBehavior.HoldEnd + }; + + fadeIn.Completed += (s, e) => _isClockPanelAnimating = false; + ClockPanel.BeginAnimation(OpacityProperty, fadeIn); + }, DispatcherPriority.Render); } } - public void HideStartup() - { - UpdatePosition(); - if (_settings.HideOnStartup) - { - _viewModel.Hide(); - } - else - { - _viewModel.Show(); - } - } - public Screen SelectedScreen() + private static double GetOpacityFromStyle(Style style, double defaultOpacity = 1.0) { - Screen screen = null; - switch (_settings.SearchWindowScreen) + if (style == null) + return defaultOpacity; + + foreach (Setter setter in style.Setters.Cast()) { - case SearchWindowScreens.Cursor: - screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - break; - case SearchWindowScreens.Primary: - screen = Screen.PrimaryScreen; - break; - case SearchWindowScreens.Focus: - var foregroundWindowHandle = PInvoke.GetForegroundWindow().Value; - screen = Screen.FromHandle(foregroundWindowHandle); - break; - case SearchWindowScreens.Custom: - if (_settings.CustomScreenNumber <= Screen.AllScreens.Length) - screen = Screen.AllScreens[_settings.CustomScreenNumber - 1]; - else - screen = Screen.AllScreens[0]; - break; - default: - screen = Screen.AllScreens[0]; - break; + if (setter.Property == OpacityProperty) + { + return setter.Value is double opacity ? opacity : defaultOpacity; + } } - return screen ?? Screen.AllScreens[0]; - } - - public double HorizonCenter(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip2.X - ActualWidth) / 2 + dip1.X; - return left; - } - - public double VerticalCenter(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); - var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y; - return top; - } - - public double HorizonRight(Screen screen) - { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); - var left = (dip1.X + dip2.X - ActualWidth) - 10; - return left; + return defaultOpacity; } - public double HorizonLeft(Screen screen) + private static Thickness GetThicknessFromStyle(Style style, Thickness defaultThickness) { - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var left = dip1.X + 10; - return left; - } + if (style == null) + return defaultThickness; - /// - /// Register up and down key - /// todo: any way to put this in xaml ? - /// - private void OnKeyDown(object sender, KeyEventArgs e) - { - var specialKeyState = GlobalHotkey.CheckModifiers(); - switch (e.Key) + foreach (Setter setter in style.Setters.Cast()) { - case Key.Down: - isArrowKeyPressed = true; - _viewModel.SelectNextItemCommand.Execute(null); - e.Handled = true; - break; - case Key.Up: - isArrowKeyPressed = true; - _viewModel.SelectPrevItemCommand.Execute(null); - e.Handled = true; - break; - case Key.PageDown: - _viewModel.SelectNextPageCommand.Execute(null); - e.Handled = true; - break; - case Key.PageUp: - _viewModel.SelectPrevPageCommand.Execute(null); - e.Handled = true; - break; - case Key.Right: - if (_viewModel.SelectedIsFromQueryResults() - && QueryTextBox.CaretIndex == QueryTextBox.Text.Length - && !string.IsNullOrEmpty(QueryTextBox.Text)) - { - _viewModel.LoadContextMenuCommand.Execute(null); - e.Handled = true; - } - - break; - case Key.Left: - if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) - { - _viewModel.EscCommand.Execute(null); - e.Handled = true; - } + if (setter.Property == MarginProperty) + { + return setter.Value is Thickness thickness ? thickness : defaultThickness; + } + } - break; - case Key.Back: - if (specialKeyState.CtrlPressed) - { - if (_viewModel.SelectedIsFromQueryResults() - && QueryTextBox.Text.Length > 0 - && QueryTextBox.CaretIndex == QueryTextBox.Text.Length) - { - var queryWithoutActionKeyword = - QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search; + return defaultThickness; + } - if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword)) - { - _viewModel.BackspaceCommand.Execute(null); - e.Handled = true; - } - } - } + #endregion - break; - default: - break; - } - } + #region QueryTextBox Event - private void OnKeyUp(object sender, KeyEventArgs e) + private void QueryTextBox_OnCopy(object sender, ExecutedRoutedEventArgs e) { - if (e.Key == Key.Up || e.Key == Key.Down) + var result = _viewModel.Results.SelectedItem?.Result; + if (QueryTextBox.SelectionLength == 0 && result != null) { - isArrowKeyPressed = false; + string copyText = result.CopyText; + App.API.CopyToClipboard(copyText, directCopy: true); } - } - - private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) - { - if (isArrowKeyPressed) + else if (!string.IsNullOrEmpty(QueryTextBox.Text)) { - e.Handled = true; // Ignore Mouse Hover when press Arrowkeys + App.API.CopyToClipboard(QueryTextBox.SelectedText, showDefaultNotification: false); } } - public void PreviewReset() - { - _viewModel.ResetPreview(); - } - - private void MoveQueryTextToEnd() - { - // QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle. - // To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority - Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length); - } - - public void InitializeColorScheme() + private void QueryTextBox_OnPaste(object sender, DataObjectPastingEventArgs e) { - if (_settings.ColorScheme == Constant.Light) - { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Light; - } - else if (_settings.ColorScheme == Constant.Dark) + var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true); + if (isText) { - ModernWpf.ThemeManager.Current.ApplicationTheme = ModernWpf.ApplicationTheme.Dark; + var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string; + text = text.Replace(Environment.NewLine, " "); + DataObject data = new DataObject(); + data.SetData(DataFormats.UnicodeText, text); + e.DataObject = data; } } @@ -848,9 +979,16 @@ private void QueryTextBox_KeyUp(object sender, KeyEventArgs e) { if (_viewModel.QueryText != QueryTextBox.Text) { - BindingExpression be = QueryTextBox.GetBindingExpression(System.Windows.Controls.TextBox.TextProperty); + BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); be.UpdateSource(); } } + + private void QueryTextBox_OnPreviewDragOver(object sender, DragEventArgs e) + { + e.Handled = true; + } + + #endregion } } diff --git a/Flow.Launcher/Msg.xaml.cs b/Flow.Launcher/Msg.xaml.cs index 0bb02bbc51f..94184ff6377 100644 --- a/Flow.Launcher/Msg.xaml.cs +++ b/Flow.Launcher/Msg.xaml.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.IO; using System.Windows; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Media.Animation; -using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Image; @@ -12,14 +11,14 @@ namespace Flow.Launcher { public partial class Msg : Window { - Storyboard fadeOutStoryboard = new Storyboard(); + private readonly Storyboard fadeOutStoryboard = new(); private bool closing; public Msg() { InitializeComponent(); var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dipWorkingArea = WindowsInteropHelper.TransformPixelsToDIP(this, + var dipWorkingArea = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, screen.WorkingArea.Height); Left = dipWorkingArea.X - Width; @@ -82,13 +81,13 @@ public async void Show(string title, string subTitle, string iconPath) Show(); await Dispatcher.InvokeAsync(async () => - { - if (!closing) - { - closing = true; - await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); - } - }); + { + if (!closing) + { + closing = true; + await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin); + } + }); } } } diff --git a/Flow.Launcher/NativeMethods.txt b/Flow.Launcher/NativeMethods.txt deleted file mode 100644 index 88eeeca6e5d..00000000000 --- a/Flow.Launcher/NativeMethods.txt +++ /dev/null @@ -1,20 +0,0 @@ -DwmSetWindowAttribute -DwmExtendFrameIntoClientArea -SystemParametersInfo -SetForegroundWindow - -GetWindowLong -SetWindowLong -GetForegroundWindow -GetDesktopWindow -GetShellWindow -GetWindowRect -GetClassName -FindWindowEx -WINDOW_STYLE - -WM_ENTERSIZEMOVE -WM_EXITSIZEMOVE - -SetLastError -WINDOW_EX_STYLE \ No newline at end of file diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index ac22170aeeb..f367ac921d7 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -1,46 +1,48 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using System.Windows; +using CommunityToolkit.Mvvm.DependencyInjection; using Squirrel; +using Flow.Launcher.Core; using Flow.Launcher.Core.Plugin; -using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.Hotkey; using Flow.Launcher.Infrastructure.Image; +using Flow.Launcher.Infrastructure.Logger; +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using Flow.Launcher.ViewModel; using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedCommands; -using System.Threading; -using System.IO; -using Flow.Launcher.Infrastructure.Http; +using Flow.Launcher.ViewModel; using JetBrains.Annotations; -using System.Runtime.CompilerServices; -using Flow.Launcher.Infrastructure.Logger; -using Flow.Launcher.Infrastructure.Storage; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Collections.Specialized; -using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Core; -using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Core.Resource; namespace Flow.Launcher { public class PublicAPIInstance : IPublicAPI { private readonly Settings _settings; + private readonly Internationalization _translater; private readonly MainViewModel _mainVM; #region Constructor - public PublicAPIInstance(Settings settings, MainViewModel mainVM) + public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM) { _settings = settings; + _translater = translater; _mainVM = mainVM; GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); @@ -153,17 +155,17 @@ public void CopyToClipboard(string stringToCopy, bool directCopy = false, bool s public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed; - public string GetTranslation(string key) => InternationalizationManager.Instance.GetTranslation(key); + public string GetTranslation(string key) => _translater.GetTranslation(key); public List GetAllPlugins() => PluginManager.AllPlugins.ToList(); public MatchResult FuzzySearch(string query, string stringToCompare) => StringMatcher.FuzzySearch(query, stringToCompare); - public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url); + public Task HttpGetStringAsync(string url, CancellationToken token = default) => Http.GetAsync(url, token); public Task HttpGetStreamAsync(string url, CancellationToken token = default) => - Http.GetStreamAsync(url); + Http.GetStreamAsync(url, token); public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, Action reportProgress = null, CancellationToken token = default) => Http.DownloadAsync(url, filePath, reportProgress, token); diff --git a/Flow.Launcher/Resources/Dark.xaml b/Flow.Launcher/Resources/Dark.xaml index ed031c939e0..25cc8e878e3 100644 --- a/Flow.Launcher/Resources/Dark.xaml +++ b/Flow.Launcher/Resources/Dark.xaml @@ -7,17 +7,17 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - - - + + + + - - - + + + #198F8F8F diff --git a/Flow.Launcher/Resources/Light.xaml b/Flow.Launcher/Resources/Light.xaml index 8fe84588f5e..b9f65ce9650 100644 --- a/Flow.Launcher/Resources/Light.xaml +++ b/Flow.Launcher/Resources/Light.xaml @@ -7,18 +7,18 @@ xmlns:sys="clr-namespace:System;assembly=mscorlib"> - + - - + + - - - - #198F8F8F + + + + #0C000000 diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs index ed933678d46..61d365b649c 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Windows.Media; +using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -13,7 +14,6 @@ using Flow.Launcher.Plugin; using Flow.Launcher.ViewModel; using ModernWpf; -using ThemeManager = Flow.Launcher.Core.Resource.ThemeManager; using ThemeManagerForColorSchemeSwitch = ModernWpf.ThemeManager; namespace Flow.Launcher.SettingPages.ViewModels; @@ -22,45 +22,68 @@ public partial class SettingsPaneThemeViewModel : BaseModel { private const string DefaultFont = "Segoe UI"; public Settings Settings { get; } + private readonly Theme _theme = Ioc.Default.GetRequiredService(); public static string LinkHowToCreateTheme => @"https://flowlauncher.com/docs/#/how-to-create-a-theme"; public static string LinkThemeGallery => "https://github.com/Flow-Launcher/Flow.Launcher/discussions/1438"; + private List _themes; + public List Themes => _themes ??= _theme.LoadAvailableThemes(); + private Theme.ThemeData _selectedTheme; public Theme.ThemeData SelectedTheme { - get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == Settings.Theme); + get => _selectedTheme ??= Themes.Find(v => v.FileNameWithoutExtension == _theme.GetCurrentTheme()); set { _selectedTheme = value; - ThemeManager.Instance.ChangeTheme(value.FileNameWithoutExtension); + _theme.ChangeTheme(value.FileNameWithoutExtension); + + // Update UI state + OnPropertyChanged(nameof(BackdropType)); + OnPropertyChanged(nameof(IsBackdropEnabled)); + OnPropertyChanged(nameof(IsDropShadowEnabled)); + OnPropertyChanged(nameof(DropShadowEffect)); + + _ = _theme.RefreshFrameAsync(); + } + } - if (ThemeManager.Instance.BlurEnabled && Settings.UseDropShadowEffect) - DropShadowEffect = false; + public bool IsBackdropEnabled + { + get + { + if (!Win32Helper.IsBackdropSupported()) return false; + return SelectedTheme?.HasBlur ?? false; } } + public bool IsDropShadowEnabled => !_theme.BlurEnabled; + public bool DropShadowEffect { get => Settings.UseDropShadowEffect; set { - if (ThemeManager.Instance.BlurEnabled && value) + if (_theme.BlurEnabled) { - App.API.ShowMsgBox(InternationalizationManager.Instance.GetTranslation("shadowEffectNotAllowed")); + // Always DropShadowEffect = true with blur theme + Settings.UseDropShadowEffect = true; return; } + // User can change shadow with non-blur theme. if (value) { - ThemeManager.Instance.AddDropShadowEffectToCurrentTheme(); + _theme.AddDropShadowEffectToCurrentTheme(); } else { - ThemeManager.Instance.RemoveDropShadowEffectFromCurrentTheme(); + _theme.RemoveDropShadowEffectFromCurrentTheme(); } Settings.UseDropShadowEffect = value; + OnPropertyChanged(nameof(DropShadowEffect)); } } @@ -94,12 +117,25 @@ public double ResultSubItemFontSize set => Settings.ResultSubItemFontSize = value; } - private List _themes; - public List Themes => _themes ??= ThemeManager.Instance.LoadAvailableThemes(); - public class ColorSchemeData : DropdownDataGeneric { } public List ColorSchemes { get; } = DropdownDataGeneric.GetValues("ColorScheme"); + public string ColorScheme + { + get => Settings.ColorScheme; + set + { + ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = value switch + { + Constant.Light => ApplicationTheme.Light, + Constant.Dark => ApplicationTheme.Dark, + Constant.System => null, + _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme + }; + Settings.ColorScheme = value; + _ = _theme.RefreshFrameAsync(); + } + } public List TimeFormatList { get; } = new() { @@ -174,6 +210,31 @@ public bool UseAnimation public class AnimationSpeedData : DropdownDataGeneric { } public List AnimationSpeeds { get; } = DropdownDataGeneric.GetValues("AnimationSpeed"); + public class BackdropTypeData : DropdownDataGeneric { } + + public List BackdropTypesList { get; } = + DropdownDataGeneric.GetValues("BackdropTypes"); + + public BackdropTypes BackdropType + { + get => Enum.IsDefined(typeof(BackdropTypes), Settings.BackdropType) + ? Settings.BackdropType + : BackdropTypes.None; + set + { + if (!Enum.IsDefined(typeof(BackdropTypes), value)) + { + value = BackdropTypes.None; + } + + Settings.BackdropType = value; + + _ = _theme.SetBlurForWindowAsync(); + + OnPropertyChanged(nameof(IsDropShadowEnabled)); + } + } + public bool UseSound { get => Settings.UseSound; @@ -219,37 +280,37 @@ public ResultsViewModel PreviewResults { var results = new List { - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleExplorer"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleExplorer"), + Title = App.API.GetTranslation("SampleTitleExplorer"), + SubTitle = App.API.GetTranslation("SampleSubTitleExplorer"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Explorer\Images\explorer.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleWebSearch"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleWebSearch"), + Title = App.API.GetTranslation("SampleTitleWebSearch"), + SubTitle = App.API.GetTranslation("SampleSubTitleWebSearch"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.WebSearch\Images\web_search.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProgram"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProgram"), + Title = App.API.GetTranslation("SampleTitleProgram"), + SubTitle = App.API.GetTranslation("SampleSubTitleProgram"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.Program\Images\program.png" ) }, - new Result + new() { - Title = InternationalizationManager.Instance.GetTranslation("SampleTitleProcessKiller"), - SubTitle = InternationalizationManager.Instance.GetTranslation("SampleSubTitleProcessKiller"), + Title = App.API.GetTranslation("SampleTitleProcessKiller"), + SubTitle = App.API.GetTranslation("SampleSubTitleProcessKiller"), IcoPath = Path.Combine( Constant.ProgramDirectory, @"Plugins\Flow.Launcher.Plugin.ProcessKiller\Images\app.png" @@ -281,7 +342,7 @@ public FontFamily SelectedQueryBoxFont set { Settings.QueryBoxFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } @@ -303,7 +364,7 @@ public FamilyTypeface SelectedQueryBoxFontFaces Settings.QueryBoxFontStretch = value.Stretch.ToString(); Settings.QueryBoxFontWeight = value.Weight.ToString(); Settings.QueryBoxFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } @@ -325,7 +386,7 @@ public FontFamily SelectedResultFont set { Settings.ResultFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } @@ -347,7 +408,7 @@ public FamilyTypeface SelectedResultFontFaces Settings.ResultFontStretch = value.Stretch.ToString(); Settings.ResultFontWeight = value.Weight.ToString(); Settings.ResultFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } @@ -355,9 +416,9 @@ public FontFamily SelectedResultSubFont { get { - if (Fonts.SystemFontFamilies.Count(o => + if (Fonts.SystemFontFamilies.Any(o => o.FamilyNames.Values != null && - o.FamilyNames.Values.Contains(Settings.ResultSubFont)) > 0) + o.FamilyNames.Values.Contains(Settings.ResultSubFont))) { var font = new FontFamily(Settings.ResultSubFont); return font; @@ -371,7 +432,7 @@ public FontFamily SelectedResultSubFont set { Settings.ResultSubFont = value.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } @@ -392,32 +453,21 @@ public FamilyTypeface SelectedResultSubFontFaces Settings.ResultSubFontStretch = value.Stretch.ToString(); Settings.ResultSubFontWeight = value.Weight.ToString(); Settings.ResultSubFontStyle = value.Style.ToString(); - ThemeManager.Instance.ChangeTheme(Settings.Theme); + _theme.ChangeTheme(); } } public string ThemeImage => Constant.QueryTextBoxIconImagePath; - [RelayCommand] - private void OpenThemesFolder() - { - App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); - } - - public void UpdateColorScheme() + public SettingsPaneThemeViewModel(Settings settings) { - ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme = Settings.ColorScheme switch - { - Constant.Light => ApplicationTheme.Light, - Constant.Dark => ApplicationTheme.Dark, - Constant.System => null, - _ => ThemeManagerForColorSchemeSwitch.Current.ApplicationTheme - }; + Settings = settings; } - public SettingsPaneThemeViewModel(Settings settings) + [RelayCommand] + private void OpenThemesFolder() { - Settings = settings; + App.API.OpenDirectory(Path.Combine(DataLocation.DataDirectory(), Constant.Themes)); } [RelayCommand] diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml index 18835259bdc..6142371469d 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml @@ -303,7 +303,7 @@ Width="400" Margin="40 30 0 30" SnapsToDevicePixels="True" - Style="{DynamicResource WindowBorderStyle}"> + Style="{DynamicResource PreviewWindowBorderStyle}"> @@ -370,17 +370,6 @@ - - - - + + + + + + + + + + + + + + + + + SelectedValue="{Binding ColorScheme, Mode=TwoWay}" + SelectedValuePath="Value" /> diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs index bf7502e1927..22de4fcc082 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneTheme.xaml.cs @@ -1,4 +1,3 @@ -using System.Windows.Controls; using System.Windows.Navigation; using CommunityToolkit.Mvvm.DependencyInjection; using Flow.Launcher.SettingPages.ViewModels; @@ -23,9 +22,4 @@ protected override void OnNavigatedTo(NavigationEventArgs e) base.OnNavigatedTo(e); } - - private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e) - { - _viewModel.UpdateColorScheme(); - } } diff --git a/Flow.Launcher/SettingWindow.xaml.cs b/Flow.Launcher/SettingWindow.xaml.cs index 30b51f992b0..28140f0245c 100644 --- a/Flow.Launcher/SettingWindow.xaml.cs +++ b/Flow.Launcher/SettingWindow.xaml.cs @@ -4,7 +4,7 @@ using System.Windows.Input; using System.Windows.Interop; using CommunityToolkit.Mvvm.DependencyInjection; -using Flow.Launcher.Helper; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.SettingPages.Views; @@ -92,13 +92,13 @@ private void RefreshMaximizeRestoreButton() { if (WindowState == WindowState.Maximized) { - MaximizeButton.Visibility = Visibility.Collapsed; + MaximizeButton.Visibility = Visibility.Hidden; RestoreButton.Visibility = Visibility.Visible; } else { MaximizeButton.Visibility = Visibility.Visible; - RestoreButton.Visibility = Visibility.Collapsed; + RestoreButton.Visibility = Visibility.Hidden; } } @@ -143,8 +143,8 @@ private static bool IsPositionValid(double top, double left) private double WindowLeft() { var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); + var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0); + var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0); var left = (dip2.X - ActualWidth) / 2 + dip1.X; return left; } @@ -152,8 +152,8 @@ private double WindowLeft() private double WindowTop() { var screen = Screen.FromPoint(System.Windows.Forms.Cursor.Position); - var dip1 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); - var dip2 = WindowsInteropHelper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); + var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y); + var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height); var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20; return top; } diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index b96cb661e5e..907ded363dd 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -107,7 +107,7 @@ @@ -557,6 +534,14 @@ + @@ -135,8 +138,8 @@ BasedOn="{StaticResource BaseSearchIconStyle}" TargetType="{x:Type Path}"> - - + + + @@ -33,7 +43,9 @@ + Opacity="0.6" + Color="#33000000" /> @@ -112,7 +124,7 @@ - - + + @@ -75,7 +79,7 @@ x:Key="SeparatorStyle" BasedOn="{StaticResource BaseSeparatorStyle}" TargetType="{x:Type Rectangle}"> - + diff --git a/Flow.Launcher/Themes/Darker.xaml b/Flow.Launcher/Themes/Darker.xaml index d1abbe9789f..7d2614be897 100644 --- a/Flow.Launcher/Themes/Darker.xaml +++ b/Flow.Launcher/Themes/Darker.xaml @@ -29,7 +29,9 @@ - #0e172c + #cc1081 \ No newline at end of file diff --git a/Flow.Launcher/Themes/Ubuntu.xaml b/Flow.Launcher/Themes/Ubuntu.xaml index 3c9e0a1255b..4007370f9a1 100644 --- a/Flow.Launcher/Themes/Ubuntu.xaml +++ b/Flow.Launcher/Themes/Ubuntu.xaml @@ -32,7 +32,7 @@ x:Key="QueryBoxStyle" BasedOn="{StaticResource BaseQueryBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -43,7 +43,7 @@ x:Key="QuerySuggestionBoxStyle" BasedOn="{StaticResource BaseQuerySuggestionBoxStyle}" TargetType="{x:Type TextBox}"> - + @@ -56,15 +56,7 @@ TargetType="{x:Type Border}"> - - - - - - - - - + @@ -96,7 +88,7 @@ TargetType="{x:Type Rectangle}"> - + @@ -174,7 +166,7 @@ x:Key="ClockPanel" BasedOn="{StaticResource ClockPanel}" TargetType="{x:Type StackPanel}"> - +