From b2532fc88e09977b2c62d4eec3b8ec1ec0dd43d6 Mon Sep 17 00:00:00 2001 From: "pp.stabrawa" Date: Fri, 27 Dec 2024 12:43:43 +0100 Subject: [PATCH 01/57] Order search result by window title --- .../Main.cs | 30 ++++++++----- .../ProcessHelper.cs | 44 ++++++++++++++++++- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index be2a2dd66..03b563c9b 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Flow.Launcher.Infrastructure; @@ -60,30 +60,33 @@ namespace Flow.Launcher.Plugin.ProcessKiller return menuOptions; } + private record RunningProcessInfo(string ProcessName, string MainWindowTitle); + private List CreateResultsFromQuery(Query query) { string termToSearch = query.Search; - var processlist = processHelper.GetMatchingProcesses(termToSearch); - - if (!processlist.Any()) + var processList = processHelper.GetMatchingProcesses(termToSearch); + var processWithNonEmptyMainWindowTitleList = processHelper.GetProcessesWithNonEmptyWindowTitle(); + + if (!processList.Any()) { return null; } var results = new List(); - foreach (var pr in processlist) + foreach (var pr in processList) { var p = pr.Process; var path = processHelper.TryGetProcessFilename(p); results.Add(new Result() { IcoPath = path, - Title = p.ProcessName + " - " + p.Id, + Title = processWithNonEmptyMainWindowTitleList.TryGetValue(p.Id, out var mainWindowTitle) ? mainWindowTitle : p.ProcessName + " - " + p.Id, SubTitle = path, TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData, Score = pr.Score, - ContextData = p.ProcessName, + ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle), AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => { @@ -95,22 +98,25 @@ namespace Flow.Launcher.Plugin.ProcessKiller }); } - var sortedResults = results.OrderBy(x => x.Title).ToList(); + var sortedResults = results + .OrderBy(x => string.IsNullOrEmpty(((RunningProcessInfo)x.ContextData).MainWindowTitle)) + .ThenBy(x => x.Title) + .ToList(); // When there are multiple results AND all of them are instances of the same executable // add a quick option to kill them all at the top of the results. var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle)); - if (processlist.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) + if (processList.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) { sortedResults.Insert(1, new Result() { IcoPath = firstResult?.IcoPath, - Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), firstResult?.ContextData), - SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processlist.Count), + Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), ((RunningProcessInfo)firstResult?.ContextData).ProcessName), + SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processList.Count), Score = 200, Action = (c) => { - foreach (var p in processlist) + foreach (var p in processList) { processHelper.TryKill(p.Process); } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 0acc39fbb..5cc94dfdf 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -1,4 +1,4 @@ -using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using System; using System.Collections.Generic; @@ -11,6 +11,20 @@ namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + private readonly HashSet _systemProcessList = new HashSet() { "conhost", @@ -60,6 +74,34 @@ namespace Flow.Launcher.Plugin.ProcessKiller return processlist; } + /// + /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title. + /// + public Dictionary GetProcessesWithNonEmptyWindowTitle() + { + var processDict = new Dictionary(); + EnumWindows((hWnd, lParam) => + { + StringBuilder windowTitle = new StringBuilder(); + GetWindowText(hWnd, windowTitle, windowTitle.Capacity); + + if (!string.IsNullOrWhiteSpace(windowTitle.ToString()) && IsWindowVisible(hWnd)) + { + GetWindowThreadProcessId(hWnd, out var processId); + var process = Process.GetProcessById((int)processId); + + if (!processDict.ContainsKey((int)processId)) + { + processDict.Add((int)processId, windowTitle.ToString()); + } + } + + return true; + }, IntPtr.Zero); + + return processDict; + } + /// /// Returns all non-system processes whose file path matches the given processPath /// From fa465130c6141af48345a30b12cb5e11c1d4b841 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:01:02 +0800 Subject: [PATCH 02/57] Use PInvoke to replace DllImport --- .../Main.cs | 4 +- .../NativeMethods.txt | 7 ++- .../ProcessHelper.cs | 52 ++++++++++--------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 03b563c9b..098e668cb 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -6,7 +6,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { public class Main : IPlugin, IPluginI18n, IContextMenu { - private ProcessHelper processHelper = new ProcessHelper(); + private readonly ProcessHelper processHelper = new(); private static PluginInitContext _context; @@ -66,7 +66,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { string termToSearch = query.Search; var processList = processHelper.GetMatchingProcesses(termToSearch); - var processWithNonEmptyMainWindowTitleList = processHelper.GetProcessesWithNonEmptyWindowTitle(); + var processWithNonEmptyMainWindowTitleList = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); if (!processList.Any()) { diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt index 7fa794755..13bf27932 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/NativeMethods.txt @@ -1,2 +1,7 @@ QueryFullProcessImageName -OpenProcess \ No newline at end of file +OpenProcess +EnumWindows +GetWindowTextLength +GetWindowText +IsWindowVisible +GetWindowThreadProcessId \ No newline at end of file diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index d8873bc20..5f58dc6b1 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -13,21 +13,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessHelper { - [DllImport("user32.dll")] - private static extern bool EnumWindows(EnumWindowsProc enumProc, IntPtr lParam); - - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - private readonly HashSet _systemProcessList = new HashSet() + private readonly HashSet _systemProcessList = new() { "conhost", "svchost", @@ -79,22 +65,25 @@ namespace Flow.Launcher.Plugin.ProcessKiller /// /// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title. /// - public Dictionary GetProcessesWithNonEmptyWindowTitle() + public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() { var processDict = new Dictionary(); - EnumWindows((hWnd, lParam) => + PInvoke.EnumWindows((hWnd, lParam) => { - StringBuilder windowTitle = new StringBuilder(); - GetWindowText(hWnd, windowTitle, windowTitle.Capacity); - - if (!string.IsNullOrWhiteSpace(windowTitle.ToString()) && IsWindowVisible(hWnd)) + var windowTitle = GetWindowTitle(hWnd); + if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) { - GetWindowThreadProcessId(hWnd, out var processId); - var process = Process.GetProcessById((int)processId); + uint processId = 0; + var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId); + if (result == 0u || processId == 0u) + { + return false; + } + var process = Process.GetProcessById((int)processId); if (!processDict.ContainsKey((int)processId)) { - processDict.Add((int)processId, windowTitle.ToString()); + processDict.Add((int)processId, windowTitle); } } @@ -104,6 +93,21 @@ namespace Flow.Launcher.Plugin.ProcessKiller return processDict; } + private static unsafe string GetWindowTitle(HWND hwnd) + { + var capacity = PInvoke.GetWindowTextLength(hwnd) + 1; + int length; + Span buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity]; + fixed (char* pBuffer = buffer) + { + // If the window has no title bar or text, if the title bar is empty, + // or if the window or control handle is invalid, the return value is zero. + length = PInvoke.GetWindowText(hwnd, pBuffer, capacity); + } + + return buffer[..length].ToString(); + } + /// /// Returns all non-system processes whose file path matches the given processPath /// From b7694d31300a3b62a298d605e52a813e0e04f5bf Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:09:13 +0800 Subject: [PATCH 03/57] Clean up codes --- Flow.Launcher.Infrastructure/FileExplorerHelper.cs | 2 -- Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs index b97c096c3..1085cc833 100644 --- a/Flow.Launcher.Infrastructure/FileExplorerHelper.cs +++ b/Flow.Launcher.Infrastructure/FileExplorerHelper.cs @@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First; } - private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); - /// /// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1. /// diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 5f58dc6b1..c603fba09 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -68,7 +68,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller public static unsafe Dictionary GetProcessesWithNonEmptyWindowTitle() { var processDict = new Dictionary(); - PInvoke.EnumWindows((hWnd, lParam) => + PInvoke.EnumWindows((hWnd, _) => { var windowTitle = GetWindowTitle(hWnd); if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd)) From d12cde7c44dc37e54d42fd35c42c958702122e2a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 13 Mar 2025 14:16:38 +0800 Subject: [PATCH 04/57] Do not close window when killed processes --- Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 098e668cb..aec607575 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -91,9 +91,8 @@ namespace Flow.Launcher.Plugin.ProcessKiller Action = (c) => { processHelper.TryKill(p); - // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); - return true; + _context.API.ReQuery(); + return false; } }); } @@ -120,9 +119,8 @@ namespace Flow.Launcher.Plugin.ProcessKiller { processHelper.TryKill(p.Process); } - // Re-query to refresh process list - _context.API.ChangeQuery(query.RawQuery, true); - return true; + _context.API.ReQuery(); + return false; } }); } From 6bc69b17cf619d9054fea4012ada1ff42bc414ea Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 11:21:07 +0800 Subject: [PATCH 05/57] Do not allow kill FL process --- Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index c603fba09..1538f71dd 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -31,7 +31,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller "explorer" }; - private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()); + private const string FlowLauncherProcessName = "Flow.Launcher"; + + private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()) || + string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0; /// /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm From 8ee2d4852e24525f8206270a2d290cef2c3f80e4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 11:39:16 +0800 Subject: [PATCH 06/57] Use score bump & Search window title --- .../Main.cs | 29 ++++++++++++------- .../ProcessHelper.cs | 22 ++++++++++---- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index aec607575..3b88f874b 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -64,9 +64,9 @@ namespace Flow.Launcher.Plugin.ProcessKiller private List CreateResultsFromQuery(Query query) { - string termToSearch = query.Search; - var processList = processHelper.GetMatchingProcesses(termToSearch); - var processWithNonEmptyMainWindowTitleList = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + var searchTerm = query.Search; + var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + var processList = processHelper.GetMatchingProcesses(searchTerm, processWindowTitle); if (!processList.Any()) { @@ -74,18 +74,28 @@ namespace Flow.Launcher.Plugin.ProcessKiller } var results = new List(); - foreach (var pr in processList) { var p = pr.Process; var path = processHelper.TryGetProcessFilename(p); + var title = p.ProcessName + " - " + p.Id; + var score = pr.Score; + if (processWindowTitle.TryGetValue(p.Id, out var mainWindowTitle)) + { + title = mainWindowTitle; + if (string.IsNullOrWhiteSpace(searchTerm)) + { + // Add score to prioritize processes with visible windows + score += 200; + } + } results.Add(new Result() { IcoPath = path, - Title = processWithNonEmptyMainWindowTitleList.TryGetValue(p.Id, out var mainWindowTitle) ? mainWindowTitle : p.ProcessName + " - " + p.Id, + Title = title, SubTitle = path, - TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData, - Score = pr.Score, + TitleHighlightData = StringMatcher.FuzzySearch(searchTerm, p.ProcessName).MatchData, + Score = score, ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle), AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => @@ -98,14 +108,13 @@ namespace Flow.Launcher.Plugin.ProcessKiller } var sortedResults = results - .OrderBy(x => string.IsNullOrEmpty(((RunningProcessInfo)x.ContextData).MainWindowTitle)) - .ThenBy(x => x.Title) + .OrderBy(x => x.Title) .ToList(); // When there are multiple results AND all of them are instances of the same executable // add a quick option to kill them all at the top of the results. var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle)); - if (processList.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) + if (processList.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) { sortedResults.Insert(1, new Result() { diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index 1538f71dd..d4760e98d 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -39,7 +39,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller /// /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm /// - public List GetMatchingProcesses(string searchTerm) + public List GetMatchingProcesses(string searchTerm, Dictionary processWindowTitle) { var processlist = new List(); @@ -49,15 +49,27 @@ namespace Flow.Launcher.Plugin.ProcessKiller if (string.IsNullOrWhiteSpace(searchTerm)) { - // show all non-system processes + // Show all non-system processes processlist.Add(new ProcessResult(p, 0)); } else { - var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score; - if (score > 0) + // Search window title first + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) { - processlist.Add(new ProcessResult(p, score)); + var score = StringMatcher.FuzzySearch(searchTerm, windowTitle).Score; + if (score > 0) + { + processlist.Add(new ProcessResult(p, score)); + } + } + + // Search process name and process id + var score1 = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + " - " + p.Id).Score; + if (score1 > 0) + { + processlist.Add(new ProcessResult(p, score1)); + continue; } } } From f25c5b9b0530e1fbce507c58e939629026f1fcb9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 16:12:15 +0800 Subject: [PATCH 07/57] Improve code quality & Use progress name and id as title tooltip --- .../Flow.Launcher.Plugin.ProcessKiller.csproj | 1 - .../Main.cs | 110 ++++++++++++------ .../ProcessHelper.cs | 48 +++----- .../ProcessResult.cs | 14 ++- 4 files changed, 102 insertions(+), 71 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj index 4e216b7b2..c4864dc57 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Flow.Launcher.Plugin.ProcessKiller.csproj @@ -58,7 +58,6 @@ - diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs index 3b88f874b..b44ecd99c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/Main.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; -using Flow.Launcher.Infrastructure; namespace Flow.Launcher.Plugin.ProcessKiller { @@ -48,7 +48,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller { foreach (var p in similarProcesses) { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); } return true; @@ -60,73 +60,113 @@ namespace Flow.Launcher.Plugin.ProcessKiller return menuOptions; } - private record RunningProcessInfo(string ProcessName, string MainWindowTitle); - private List CreateResultsFromQuery(Query query) { - var searchTerm = query.Search; - var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); - var processList = processHelper.GetMatchingProcesses(searchTerm, processWindowTitle); - - if (!processList.Any()) + // Get all non-system processes + var allPocessList = processHelper.GetMatchingProcesses(); + if (!allPocessList.Any()) { return null; } + // Filter processes based on search term + var searchTerm = query.Search; + var processlist = new List(); + var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle(); + if (string.IsNullOrWhiteSpace(searchTerm)) + { + foreach (var p in allPocessList) + { + var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p); + + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) + { + // Add score to prioritize processes with visible windows + // And use window title for those processes + processlist.Add(new ProcessResult(p, 200, windowTitle, null, progressNameIdTitle)); + } + else + { + processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle)); + } + } + } + else + { + foreach (var p in allPocessList) + { + var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p); + + if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) + { + // Get max score from searching process name, window title and process id + var windowTitleMatch = _context.API.FuzzySearch(searchTerm, windowTitle); + var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle); + var score = Math.Max(windowTitleMatch.Score, processNameIdMatch.Score); + if (score > 0) + { + // Add score to prioritize processes with visible windows + // And use window title for those processes + score += 200; + processlist.Add(new ProcessResult(p, score, windowTitle, + score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle)); + } + } + else + { + var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle); + var score = processNameIdMatch.Score; + if (score > 0) + { + processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle)); + } + } + } + } + var results = new List(); - foreach (var pr in processList) + foreach (var pr in processlist) { var p = pr.Process; var path = processHelper.TryGetProcessFilename(p); - var title = p.ProcessName + " - " + p.Id; - var score = pr.Score; - if (processWindowTitle.TryGetValue(p.Id, out var mainWindowTitle)) - { - title = mainWindowTitle; - if (string.IsNullOrWhiteSpace(searchTerm)) - { - // Add score to prioritize processes with visible windows - score += 200; - } - } results.Add(new Result() { IcoPath = path, - Title = title, + Title = pr.Title, + TitleToolTip = pr.Tooltip, SubTitle = path, - TitleHighlightData = StringMatcher.FuzzySearch(searchTerm, p.ProcessName).MatchData, - Score = score, - ContextData = new RunningProcessInfo(p.ProcessName, mainWindowTitle), + TitleHighlightData = pr.TitleMatch?.MatchData, + Score = pr.Score, + ContextData = p.ProcessName, AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}", Action = (c) => { - processHelper.TryKill(p); + processHelper.TryKill(_context, p); _context.API.ReQuery(); return false; } }); } - var sortedResults = results - .OrderBy(x => x.Title) - .ToList(); + // Order results by process name for processes without visible windows + var sortedResults = results.OrderBy(x => x.Title).ToList(); // When there are multiple results AND all of them are instances of the same executable // add a quick option to kill them all at the top of the results. var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle)); - if (processList.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) + if (processlist.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle)) { sortedResults.Insert(1, new Result() { IcoPath = firstResult?.IcoPath, - Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), ((RunningProcessInfo)firstResult?.ContextData).ProcessName), - SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processList.Count), + Title = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all"), firstResult?.ContextData), + SubTitle = string.Format(_context.API.GetTranslation("flowlauncher_plugin_processkiller_kill_all_count"), processlist.Count), Score = 200, Action = (c) => { - foreach (var p in processList) + foreach (var p in processlist) { - processHelper.TryKill(p.Process); + processHelper.TryKill(_context, p.Process); } _context.API.ReQuery(); return false; diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index d4760e98d..b409df2af 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -1,6 +1,4 @@ -using Flow.Launcher.Infrastructure; -using Flow.Launcher.Infrastructure.Logger; -using Microsoft.Win32.SafeHandles; +using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.Diagnostics; @@ -37,41 +35,25 @@ namespace Flow.Launcher.Plugin.ProcessKiller string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0; /// - /// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm + /// Get title based on process name and id /// - public List GetMatchingProcesses(string searchTerm, Dictionary processWindowTitle) + public static string GetProcessNameIdTitle(Process p) { - var processlist = new List(); + return p.ProcessName + " - " + p.Id; + } + + /// + /// Returns a Process for evey running non-system process + /// + public List GetMatchingProcesses() + { + var processlist = new List(); foreach (var p in Process.GetProcesses()) { if (IsSystemProcess(p)) continue; - if (string.IsNullOrWhiteSpace(searchTerm)) - { - // Show all non-system processes - processlist.Add(new ProcessResult(p, 0)); - } - else - { - // Search window title first - if (processWindowTitle.TryGetValue(p.Id, out var windowTitle)) - { - var score = StringMatcher.FuzzySearch(searchTerm, windowTitle).Score; - if (score > 0) - { - processlist.Add(new ProcessResult(p, score)); - } - } - - // Search process name and process id - var score1 = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + " - " + p.Id).Score; - if (score1 > 0) - { - processlist.Add(new ProcessResult(p, score1)); - continue; - } - } + processlist.Add(p); } return processlist; @@ -131,7 +113,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath); } - public void TryKill(Process p) + public void TryKill(PluginInitContext context, Process p) { try { @@ -143,7 +125,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller } catch (Exception e) { - Log.Exception($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e); + context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e); } } diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs index 03856677e..146c9c92c 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessResult.cs @@ -1,17 +1,27 @@ using System.Diagnostics; +using Flow.Launcher.Plugin.SharedModels; namespace Flow.Launcher.Plugin.ProcessKiller { internal class ProcessResult { - public ProcessResult(Process process, int score) + public ProcessResult(Process process, int score, string title, MatchResult match, string tooltip) { Process = process; Score = score; + Title = title; + TitleMatch = match; + Tooltip = tooltip; } public Process Process { get; } public int Score { get; } + + public string Title { get; } + + public MatchResult TitleMatch { get; } + + public string Tooltip { get; } } -} \ No newline at end of file +} From aa6c9d91cf08b0adb7c3f61cd9d1d335ae755031 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 14 Mar 2025 16:42:54 +0800 Subject: [PATCH 08/57] Use string builder --- .../ProcessHelper.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs index b409df2af..4c07341ec 100644 --- a/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs +++ b/Plugins/Flow.Launcher.Plugin.ProcessKiller/ProcessHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Threading; @@ -31,15 +32,20 @@ namespace Flow.Launcher.Plugin.ProcessKiller private const string FlowLauncherProcessName = "Flow.Launcher"; - private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower()) || - string.Compare(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase) == 0; + private bool IsSystemProcessOrFlowLauncher(Process p) => + _systemProcessList.Contains(p.ProcessName.ToLower()) || + string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase); /// /// Get title based on process name and id /// public static string GetProcessNameIdTitle(Process p) { - return p.ProcessName + " - " + p.Id; + var sb = new StringBuilder(); + sb.Append(p.ProcessName); + sb.Append(" - "); + sb.Append(p.Id); + return sb.ToString(); } /// @@ -51,7 +57,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller foreach (var p in Process.GetProcesses()) { - if (IsSystemProcess(p)) continue; + if (IsSystemProcessOrFlowLauncher(p)) continue; processlist.Add(p); } @@ -110,7 +116,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller /// public IEnumerable GetSimilarProcesses(string processPath) { - return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath); + return Process.GetProcesses().Where(p => !IsSystemProcessOrFlowLauncher(p) && TryGetProcessFilename(p) == processPath); } public void TryKill(PluginInitContext context, Process p) From 1b066a572487e21be6d62d1ffebb5064abc9c99c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 16:49:22 +0800 Subject: [PATCH 09/57] Improve code quality --- Flow.Launcher/App.xaml.cs | 3 +- Flow.Launcher/Helper/SingleInstance.cs | 260 ++++++++++++------------- 2 files changed, 122 insertions(+), 141 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 23c77618f..19e932ea8 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -28,7 +28,6 @@ namespace Flow.Launcher public partial class App : IDisposable, ISingleInstanceApp { public static IPublicAPI API { get; private set; } - private const string Unique = "Flow.Launcher_Unique_Application_Mutex"; private static bool _disposed; private readonly Settings _settings; @@ -99,7 +98,7 @@ namespace Flow.Launcher [STAThread] public static void Main() { - if (SingleInstance.InitializeAsFirstInstance(Unique)) + if (SingleInstance.InitializeAsFirstInstance()) { using var application = new App(); application.InitializeComponent(); diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index e0e3075f6..76c109a39 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,155 +6,137 @@ using System.Windows; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper +namespace Flow.Launcher.Helper; + +public interface ISingleInstanceApp { - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } + void OnSecondAppStarted(); +} + +/// +/// This class checks to make sure that only one instance of +/// this application is running at a time. +/// +/// +/// Note: this class should be used with some caution, because it does no +/// security checking. For example, if one instance of an app that uses this class +/// is running as Administrator, any other instance, even if it is not +/// running as Administrator, can activate it with command line arguments. +/// For most apps, this will not be much of an issue. +/// +public static class SingleInstance where TApplication: Application, ISingleInstanceApp +{ + #region Private Fields /// - /// This class checks to make sure that only one instance of - /// this application is running at a time. + /// String delimiter used in channel names. /// - /// - /// Note: this class should be used with some caution, because it does no - /// security checking. For example, if one instance of an app that uses this class - /// is running as Administrator, any other instance, even if it is not - /// running as Administrator, can activate it with command line arguments. - /// For most apps, this will not be much of an issue. - /// - public static class SingleInstance - where TApplication: Application , ISingleInstanceApp - + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() { - #region Private Fields + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; - - /// - /// Application mutex. - /// - internal static Mutex singleInstanceMutex; - - #endregion - - #region Public Methods - - /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. - /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = uniqueName + Environment.UserName; - - string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); - if (firstInstance) - { - _ = CreateRemoteService(channelName); - return true; - } - else - { - _ = SignalFirstInstance(channelName); - return false; - } + _ = CreateRemoteServiceAsync(channelName); + return true; } - - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() + else { - singleInstanceMutex?.ReleaseMutex(); + _ = SignalFirstInstanceAsync(channelName); + return false; } - - #endregion - - #region Private Methods - - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) - { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) - { - while(true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); - } - } - } - - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstance(string channelName) - { - // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - } - - /// - /// Callback for activating first instance of the application. - /// - /// Callback argument. - /// Always null. - private static object ActivateFirstInstanceCallback(object o) - { - ActivateFirstInstance(); - return null; - } - - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) - { - return; - } - - ((TApplication)Application.Current).OnSecondAppStarted(); - } - - #endregion } + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + SingleInstanceMutex?.ReleaseMutex(); + } + + #endregion + + #region Private Methods + + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) + { + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) + { + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); + } + } + + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); + + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).OnSecondAppStarted(); + } + + #endregion } From 783cef6814c8731580ac99b691ec667ab854011b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 20:55:57 +0800 Subject: [PATCH 10/57] Gracefully shutdown all threads when exiting --- Flow.Launcher/App.xaml.cs | 133 +++++++++++++++++++---- Flow.Launcher/MainWindow.xaml | 1 + Flow.Launcher/MainWindow.xaml.cs | 61 +++++++++-- Flow.Launcher/ViewModel/MainViewModel.cs | 26 ++++- 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 19e932ea8..8f7c8aec9 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,12 +25,29 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { - public partial class App : IDisposable, ISingleInstanceApp + public partial class App : IAsyncDisposable, ISingleInstanceApp { + #region Public Properties + public static IPublicAPI API { get; private set; } + public static CancellationTokenSource NativeThreadCTS { get; private set; } + + #endregion + + #region Private Fields + private static bool _disposed; + private MainWindow _mainWindow; + private readonly MainViewModel _mainVM; private readonly Settings _settings; + // To prevent two disposals running at the same time. + private static readonly object _disposingLock = new(); + + #endregion + + #region Constructor + public App() { // Initialize settings @@ -78,34 +95,47 @@ namespace Flow.Launcher { API = Ioc.Default.GetRequiredService(); _settings.Initialize(); + _mainVM = Ioc.Default.GetRequiredService(); } catch (Exception e) { ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); return; } + + // Local function + static void ShowErrorMsgBoxAndFailFast(string message, Exception e) + { + // Firstly show users the message + MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + + // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. + Environment.FailFast(message, e); + } } - private static void ShowErrorMsgBoxAndFailFast(string message, Exception e) - { - // Firstly show users the message - MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error); + #endregion - // Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info. - Environment.FailFast(message, e); - } + #region Main [STAThread] public static void Main() { + NativeThreadCTS = new CancellationTokenSource(); + if (SingleInstance.InitializeAsFirstInstance()) { - using var application = new App(); + var application = new App(); application.InitializeComponent(); application.Run(); + application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } + #endregion + + #region App Events + #pragma warning disable VSTHRD100 // Avoid async void methods private async void OnStartup(object sender, StartupEventArgs e) @@ -136,11 +166,11 @@ namespace Flow.Launcher await PluginManager.InitializePluginsAsync(); await imageLoadertask; - var window = new MainWindow(); + _mainWindow = new MainWindow(); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); - Current.MainWindow = window; + Current.MainWindow = _mainWindow; Current.MainWindow.Title = Constant.FlowLauncher; HotKeyMapper.Initialize(); @@ -157,8 +187,7 @@ namespace Flow.Launcher AutoUpdates(); API.SaveAppAllSettings(); - Log.Info( - "|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- "); + Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------"); }); } @@ -191,7 +220,6 @@ namespace Flow.Launcher } } - //[Conditional("RELEASE")] private void AutoUpdates() { _ = Task.Run(async () => @@ -209,11 +237,30 @@ namespace Flow.Launcher }); } + #endregion + + #region Register Events + private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); - Current.Exit += (s, e) => Dispose(); - Current.SessionEnding += (s, e) => Dispose(); + AppDomain.CurrentDomain.ProcessExit += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Process Exit"); + _ = DisposeAsync(); + }; + + Current.Exit += (s, e) => + { + NativeThreadCTS.Cancel(); + Log.Info("|App.RegisterExitEvents|Application Exit"); + _ = DisposeAsync(); + }; + + Current.SessionEnding += (s, e) => + { + Log.Info("|App.RegisterExitEvents|Session Ending"); + _ = DisposeAsync(); + }; } /// @@ -234,20 +281,62 @@ namespace Flow.Launcher AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; } - public void Dispose() + #endregion + + #region IAsyncDisposable + + protected virtual async ValueTask DisposeAsync(bool disposing) { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown - if (!_disposed) + // Prevent two disposes at the same time. + lock (_disposingLock) { - API.SaveAppAllSettings(); + if (!disposing) + { + return; + } + + if (_disposed) + { + return; + } + _disposed = true; } + + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + { + Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); + + if (disposing) + { + API?.SaveAppAllSettings(); + await PluginManager.DisposePluginsAsync(); + + // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. + await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + _mainVM?.Dispose(); + } + + Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------"); + }); } + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + + #region ISingleInstanceApp + public void OnSecondAppStarted() { Ioc.Default.GetRequiredService().Show(); } + + #endregion } } diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303ac..f5f3bac84 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -17,6 +17,7 @@ AllowDrop="True" AllowsTransparency="True" Background="Transparent" + Closed="OnClosed" Closing="OnClosing" Deactivated="OnDeactivated" Icon="Images/app.png" diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 2ce3d1e95..0af617a77 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -27,7 +27,7 @@ using Screen = System.Windows.Forms.Screen; namespace Flow.Launcher { - public partial class MainWindow + public partial class MainWindow : IDisposable { #region Private Fields @@ -50,13 +50,17 @@ namespace Flow.Launcher private SoundPlayer animationSoundWPF; // Window WndProc + private HwndSource _hwndSource; 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; // 애니메이션 실행 중인지 여부 + private bool _isClockPanelAnimating = false; + + // IDisposable + private bool _disposedValue = false; #endregion @@ -85,8 +89,8 @@ namespace Flow.Launcher private void OnSourceInitialized(object sender, EventArgs e) { var handle = Win32Helper.GetWindowHandle(this, true); - var win = HwndSource.FromHwnd(handle); - win.AddHook(WndProc); + _hwndSource = HwndSource.FromHwnd(handle); + _hwndSource.AddHook(WndProc); Win32Helper.HideFromAltTab(this); Win32Helper.DisableControlBox(this); } @@ -227,20 +231,31 @@ namespace Flow.Launcher .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private async void OnClosing(object sender, CancelEventArgs e) + private void OnClosing(object sender, CancelEventArgs e) { + _viewModel.Save(); _notifyIcon.Visible = false; - App.API.SaveAppAllSettings(); - e.Cancel = true; - await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); - Environment.Exit(0); + } + + private void OnClosed(object sender, EventArgs e) + { + try + { + _hwndSource.RemoveHook(WndProc); + } + catch (Exception) + { + // Ignored + } + + _hwndSource = null; } private void OnLocationChanged(object sender, EventArgs e) { - if (_animating) - return; + if (_animating) return; + if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) { _settings.WindowLeft = Left; @@ -990,5 +1005,29 @@ namespace Flow.Launcher } #endregion + + #region IDisposable + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _hwndSource?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..18e61914d 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading; namespace Flow.Launcher.ViewModel { - public partial class MainViewModel : BaseModel, ISavable + public partial class MainViewModel : BaseModel, ISavable, IDisposable { #region Private Fields @@ -1542,5 +1542,29 @@ namespace Flow.Launcher.ViewModel } #endregion + + #region IDisposable + + private bool _disposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _updateSource?.Dispose(); + _disposed = true; + } + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion } } From b71e7226e5bbc1ff852e4a71064c72afae3f7558 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 21 Mar 2025 21:17:31 +0800 Subject: [PATCH 11/57] Improve & Fix --- Flow.Launcher/App.xaml.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 8f7c8aec9..63dcdf353 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -243,23 +243,23 @@ namespace Flow.Launcher private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += (s, e) => + AppDomain.CurrentDomain.ProcessExit += async (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.Exit += (s, e) => + Current.Exit += async (s, e) => { NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); - _ = DisposeAsync(); + await DisposeAsync(); }; - Current.SessionEnding += (s, e) => + Current.SessionEnding += async (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - _ = DisposeAsync(); + await DisposeAsync(); }; } @@ -303,6 +303,8 @@ namespace Flow.Launcher _disposed = true; } + await Task.Delay(10000); + await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -313,7 +315,7 @@ namespace Flow.Launcher await PluginManager.DisposePluginsAsync(); // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(DisposeAsync); + await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); _mainVM?.Dispose(); } From 5b29dedcbebb255a22a39932049fb8f6df53da3d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:10:52 +0800 Subject: [PATCH 12/57] Remove useless cancellation token source --- Flow.Launcher/App.xaml.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 63dcdf353..7d417e036 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -30,7 +30,6 @@ namespace Flow.Launcher #region Public Properties public static IPublicAPI API { get; private set; } - public static CancellationTokenSource NativeThreadCTS { get; private set; } #endregion @@ -121,8 +120,6 @@ namespace Flow.Launcher [STAThread] public static void Main() { - NativeThreadCTS = new CancellationTokenSource(); - if (SingleInstance.InitializeAsFirstInstance()) { var application = new App(); @@ -251,7 +248,6 @@ namespace Flow.Launcher Current.Exit += async (s, e) => { - NativeThreadCTS.Cancel(); Log.Info("|App.RegisterExitEvents|Application Exit"); await DisposeAsync(); }; From 09bc2bc48b5d0325421a6eab20856c80771dfc7d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 10:24:34 +0800 Subject: [PATCH 13/57] Cleanup & Improve --- Flow.Launcher/App.xaml.cs | 35 +++++++++++++++----------------- Flow.Launcher/MainWindow.xaml.cs | 3 +-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 7d417e036..016c2d06c 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -25,7 +25,7 @@ using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher { - public partial class App : IAsyncDisposable, ISingleInstanceApp + public partial class App : IDisposable, ISingleInstanceApp { #region Public Properties @@ -122,10 +122,9 @@ namespace Flow.Launcher { if (SingleInstance.InitializeAsFirstInstance()) { - var application = new App(); + using var application = new App(); application.InitializeComponent(); application.Run(); - application.DisposeAsync().AsTask().GetAwaiter().GetResult(); } } @@ -240,22 +239,22 @@ namespace Flow.Launcher private void RegisterExitEvents() { - AppDomain.CurrentDomain.ProcessExit += async (s, e) => + AppDomain.CurrentDomain.ProcessExit += (s, e) => { Log.Info("|App.RegisterExitEvents|Process Exit"); - await DisposeAsync(); + Dispose(); }; - Current.Exit += async (s, e) => + Current.Exit += (s, e) => { Log.Info("|App.RegisterExitEvents|Application Exit"); - await DisposeAsync(); + Dispose(); }; - Current.SessionEnding += async (s, e) => + Current.SessionEnding += (s, e) => { Log.Info("|App.RegisterExitEvents|Session Ending"); - await DisposeAsync(); + Dispose(); }; } @@ -279,9 +278,9 @@ namespace Flow.Launcher #endregion - #region IAsyncDisposable + #region IDisposable - protected virtual async ValueTask DisposeAsync(bool disposing) + protected virtual void Dispose(bool disposing) { // Prevent two disposes at the same time. lock (_disposingLock) @@ -299,9 +298,7 @@ namespace Flow.Launcher _disposed = true; } - await Task.Delay(10000); - - await Stopwatch.NormalAsync("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", async () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); @@ -310,8 +307,9 @@ namespace Flow.Launcher API?.SaveAppAllSettings(); await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, since some resources owned by the thread need to be disposed. - await _mainWindow?.Dispatcher.InvokeAsync(_mainWindow.Dispose); + // Dispose needs to be called on the main Windows thread, + // since some resources owned by the thread need to be disposed. + _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); _mainVM?.Dispose(); } @@ -319,10 +317,9 @@ namespace Flow.Launcher }); } - public async ValueTask DisposeAsync() + public void Dispose() { - // Do not change this code. Put cleanup code in 'DisposeAsync(bool disposing)' method - await DisposeAsync(disposing: true); + Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 0af617a77..e7be15081 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -42,7 +42,7 @@ namespace Flow.Launcher private readonly ContextMenu contextMenu = new(); private readonly MainViewModel _viewModel; - // Window Event : Key Event + // Window Event: Key Event private bool isArrowKeyPressed = false; // Window Sound Effects @@ -233,7 +233,6 @@ namespace Flow.Launcher private void OnClosing(object sender, CancelEventArgs e) { - _viewModel.Save(); _notifyIcon.Visible = false; Notification.Uninstall(); } From 9d81e60813be042cbf12a9a5e58bd59094728fc3 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:26:12 +0800 Subject: [PATCH 14/57] Improve dispose logic --- Flow.Launcher/App.xaml.cs | 6 +-- Flow.Launcher/MainWindow.xaml.cs | 57 ++++++++++++++---------- Flow.Launcher/ViewModel/MainViewModel.cs | 1 + 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 016c2d06c..d78c6c47b 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -298,15 +298,12 @@ namespace Flow.Launcher _disposed = true; } - Stopwatch.Normal("|App.Dispose|Dispose cost", async () => + Stopwatch.Normal("|App.Dispose|Dispose cost", () => { Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------"); if (disposing) { - API?.SaveAppAllSettings(); - await PluginManager.DisposePluginsAsync(); - // Dispose needs to be called on the main Windows thread, // since some resources owned by the thread need to be disposed. _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose); @@ -319,6 +316,7 @@ namespace Flow.Launcher public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e7be15081..adbb6f329 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -39,11 +39,13 @@ namespace Flow.Launcher private NotifyIcon _notifyIcon; // Window Context Menu - private readonly ContextMenu contextMenu = new(); + private readonly ContextMenu _contextMenu = new(); private readonly MainViewModel _viewModel; + // Window Event: Close Event + private bool _canClose = false; // Window Event: Key Event - private bool isArrowKeyPressed = false; + private bool _isArrowKeyPressed = false; // Window Sound Effects private MediaPlayer animationSoundWMP; @@ -60,7 +62,7 @@ namespace Flow.Launcher private bool _isClockPanelAnimating = false; // IDisposable - private bool _disposedValue = false; + private bool _disposed = false; #endregion @@ -231,10 +233,18 @@ namespace Flow.Launcher .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); } - private void OnClosing(object sender, CancelEventArgs e) + private async void OnClosing(object sender, CancelEventArgs e) { - _notifyIcon.Visible = false; - Notification.Uninstall(); + if (!_canClose) + { + _notifyIcon.Visible = false; + App.API.SaveAppAllSettings(); + e.Cancel = true; + await PluginManager.DisposePluginsAsync(); + Notification.Uninstall(); + _canClose = true; + Close(); + } } private void OnClosed(object sender, EventArgs e) @@ -292,12 +302,12 @@ namespace Flow.Launcher switch (e.Key) { case Key.Down: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectNextItemCommand.Execute(null); e.Handled = true; break; case Key.Up: - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _viewModel.SelectPrevItemCommand.Execute(null); e.Handled = true; break; @@ -355,13 +365,13 @@ namespace Flow.Launcher { if (e.Key == Key.Up || e.Key == Key.Down) { - isArrowKeyPressed = false; + _isArrowKeyPressed = false; } } private void OnPreviewMouseMove(object sender, MouseEventArgs e) { - if (isArrowKeyPressed) + if (_isArrowKeyPressed) { e.Handled = true; // Ignore Mouse Hover when press Arrowkeys } @@ -531,11 +541,11 @@ namespace Flow.Launcher gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); - contextMenu.Items.Add(open); - contextMenu.Items.Add(gamemode); - contextMenu.Items.Add(positionreset); - contextMenu.Items.Add(settings); - contextMenu.Items.Add(exit); + _contextMenu.Items.Add(open); + _contextMenu.Items.Add(gamemode); + _contextMenu.Items.Add(positionreset); + _contextMenu.Items.Add(settings); + _contextMenu.Items.Add(exit); _notifyIcon.MouseClick += (o, e) => { @@ -546,14 +556,14 @@ namespace Flow.Launcher break; case MouseButtons.Right: - contextMenu.IsOpen = true; + _contextMenu.IsOpen = true; // Get context menu handle and bring it to the foreground - if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource) + if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource) { Win32Helper.SetForegroundWindow(hwndSource.Handle); } - contextMenu.Focus(); + _contextMenu.Focus(); break; } }; @@ -561,7 +571,7 @@ namespace Flow.Launcher private void UpdateNotifyIconText() { - var menu = contextMenu; + var menu = _contextMenu; ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + " (" + _settings.Hotkey + ")"; ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); @@ -757,7 +767,7 @@ namespace Flow.Launcher if (_animating) return; - isArrowKeyPressed = true; + _isArrowKeyPressed = true; _animating = true; UpdatePosition(false); @@ -835,7 +845,7 @@ namespace Flow.Launcher clocksb.Completed += (_, _) => _animating = false; _settings.WindowLeft = Left; - isArrowKeyPressed = false; + _isArrowKeyPressed = false; if (QueryTextBox.Text.Length == 0) { @@ -1009,14 +1019,15 @@ namespace Flow.Launcher protected virtual void Dispose(bool disposing) { - if (!_disposedValue) + if (!_disposed) { if (disposing) { _hwndSource?.Dispose(); + _notifyIcon?.Dispose(); } - _disposedValue = true; + _disposed = true; } } diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 18e61914d..1668bac3a 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1561,6 +1561,7 @@ namespace Flow.Launcher.ViewModel public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } From 48792b6a3bcb3d2b972b00eb39247f16ddaa535e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:29:47 +0800 Subject: [PATCH 15/57] Restore style --- Flow.Launcher/Helper/SingleInstance.cs | 265 +++++++++++++------------ 1 file changed, 133 insertions(+), 132 deletions(-) diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 76c109a39..de2579b62 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -6,137 +6,138 @@ using System.Windows; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ // modified to allow single instace restart -namespace Flow.Launcher.Helper; - -public interface ISingleInstanceApp +namespace Flow.Launcher.Helper { - void OnSecondAppStarted(); -} - -/// -/// This class checks to make sure that only one instance of -/// this application is running at a time. -/// -/// -/// Note: this class should be used with some caution, because it does no -/// security checking. For example, if one instance of an app that uses this class -/// is running as Administrator, any other instance, even if it is not -/// running as Administrator, can activate it with command line arguments. -/// For most apps, this will not be much of an issue. -/// -public static class SingleInstance where TApplication: Application, ISingleInstanceApp -{ - #region Private Fields - - /// - /// String delimiter used in channel names. - /// - private const string Delimiter = ":"; - - /// - /// Suffix to the channel name. - /// - private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; - private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; - - /// - /// Application mutex. - /// - internal static Mutex SingleInstanceMutex { get; set; } - - #endregion - - #region Public Methods - - /// - /// Checks if the instance of the application attempting to start is the first instance. - /// If not, activates the first instance. - /// - /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance() - { - // Build unique application Id and the IPC channel name. - string applicationIdentifier = InstanceMutexName + Environment.UserName; - - string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); - - // Create mutex based on unique application Id to check if this is the first instance of the application. - SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); - if (firstInstance) - { - _ = CreateRemoteServiceAsync(channelName); - return true; - } - else - { - _ = SignalFirstInstanceAsync(channelName); - return false; - } - } - - /// - /// Cleans up single-instance code, clearing shared resources, mutexes, etc. - /// - public static void Cleanup() - { - SingleInstanceMutex?.ReleaseMutex(); - } - - #endregion - - #region Private Methods - - /// - /// Creates a remote server pipe for communication. - /// Once receives signal from client, will activate first instance. - /// - /// Application's IPC channel name. - private static async Task CreateRemoteServiceAsync(string channelName) - { - using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); - while (true) - { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - - // Do an asynchronous call to ActivateFirstInstance function - Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); - - // Disconect client - pipeServer.Disconnect(); - } - } - - /// - /// Creates a client pipe and sends a signal to server to launch first instance - /// - /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstanceAsync(string channelName) - { - // Create a client pipe connected to server - using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); - - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } - - /// - /// Activates the first instance of the application with arguments from a second instance. - /// - /// List of arguments to supply the first instance of the application. - private static void ActivateFirstInstance() - { - // Set main window state and process command line args - if (Application.Current == null) - { - return; - } - - ((TApplication)Application.Current).OnSecondAppStarted(); - } - - #endregion + public interface ISingleInstanceApp + { + void OnSecondAppStarted(); + } + + /// + /// This class checks to make sure that only one instance of + /// this application is running at a time. + /// + /// + /// Note: this class should be used with some caution, because it does no + /// security checking. For example, if one instance of an app that uses this class + /// is running as Administrator, any other instance, even if it is not + /// running as Administrator, can activate it with command line arguments. + /// For most apps, this will not be much of an issue. + /// + public static class SingleInstance where TApplication : Application, ISingleInstanceApp + { + #region Private Fields + + /// + /// String delimiter used in channel names. + /// + private const string Delimiter = ":"; + + /// + /// Suffix to the channel name. + /// + private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; + private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex"; + + /// + /// Application mutex. + /// + internal static Mutex SingleInstanceMutex { get; set; } + + #endregion + + #region Public Methods + + /// + /// Checks if the instance of the application attempting to start is the first instance. + /// If not, activates the first instance. + /// + /// True if this is the first instance of the application. + public static bool InitializeAsFirstInstance() + { + // Build unique application Id and the IPC channel name. + string applicationIdentifier = InstanceMutexName + Environment.UserName; + + string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); + + // Create mutex based on unique application Id to check if this is the first instance of the application. + SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); + if (firstInstance) + { + _ = CreateRemoteServiceAsync(channelName); + return true; + } + else + { + _ = SignalFirstInstanceAsync(channelName); + return false; + } + } + + /// + /// Cleans up single-instance code, clearing shared resources, mutexes, etc. + /// + public static void Cleanup() + { + SingleInstanceMutex?.ReleaseMutex(); + } + + #endregion + + #region Private Methods + + /// + /// Creates a remote server pipe for communication. + /// Once receives signal from client, will activate first instance. + /// + /// Application's IPC channel name. + private static async Task CreateRemoteServiceAsync(string channelName) + { + using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) + { + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(); + + // Do an asynchronous call to ActivateFirstInstance function + Application.Current?.Dispatcher.Invoke(ActivateFirstInstance); + + // Disconect client + pipeServer.Disconnect(); + } + } + + /// + /// Creates a client pipe and sends a signal to server to launch first instance + /// + /// Application's IPC channel name. + /// + /// Command line arguments for the second instance, passed to the first instance to take appropriate action. + /// + private static async Task SignalFirstInstanceAsync(string channelName) + { + // Create a client pipe connected to server + using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out); + + // Connect to the available pipe + await pipeClient.ConnectAsync(0); + } + + /// + /// Activates the first instance of the application with arguments from a second instance. + /// + /// List of arguments to supply the first instance of the application. + private static void ActivateFirstInstance() + { + // Set main window state and process command line args + if (Application.Current == null) + { + return; + } + + ((TApplication)Application.Current).OnSecondAppStarted(); + } + + #endregion + } } From adbef0d99438a31f3ff141654a0a968c2091b89f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:34:03 +0800 Subject: [PATCH 16/57] Improve disposable interface for mainvm --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1668bac3a..ab67b21bb 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1554,6 +1554,8 @@ namespace Flow.Launcher.ViewModel if (disposing) { _updateSource?.Dispose(); + _resultsUpdateChannelWriter?.Complete(); + _resultsViewUpdateTask?.Dispose(); _disposed = true; } } From 93ccdee54add655af0384af0a436f3f01924922a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 13:40:33 +0800 Subject: [PATCH 17/57] Improve comments --- Flow.Launcher/MainWindow.xaml.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index adbb6f329..c2b35ed12 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -219,15 +219,15 @@ namespace Flow.Launcher } }; - // ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정) + // QueryTextBox.Text change detection (modified to only work when character count is 1 or higher) QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility(); - // ✅ ContextMenu.Visibility 변경 감지 + // Detecting ContextMenu.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(ContextMenu)) .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); - // ✅ History.Visibility 변경 감지 + // Detect History.Visibility changes DependencyPropertyDescriptor .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); @@ -242,6 +242,7 @@ namespace Flow.Launcher e.Cancel = true; await PluginManager.DisposePluginsAsync(); Notification.Uninstall(); + // After plugins are all disposed, we can close the main window _canClose = true; Close(); } From 9167cba6367867b216c2440ef0f6c0393c1d1b8d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:08:47 +0800 Subject: [PATCH 18/57] Use selected item for binding preview properties & Code cleanup --- Flow.Launcher/MainWindow.xaml | 4 ++-- Flow.Launcher/ViewModel/MainViewModel.cs | 21 +++++++++++++---- Flow.Launcher/ViewModel/ResultViewModel.cs | 25 ++++++++++++++------- Flow.Launcher/ViewModel/ResultsViewModel.cs | 13 ++++++----- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 5b63303ac..d1f6e7812 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -441,7 +441,7 @@ diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 46970a6a1..f06b8aa81 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -162,6 +162,7 @@ namespace Flow.Launcher.ViewModel switch (args.PropertyName) { case nameof(Results.SelectedItem): + SelectedItem = Results.SelectedItem; UpdatePreview(); break; } @@ -786,6 +787,18 @@ namespace Flow.Launcher.ViewModel #region Preview + private ResultViewModel _selectedItem; + + public ResultViewModel SelectedItem + { + get => _selectedItem; + set + { + _selectedItem = value; + OnPropertyChanged(); + } + } + public bool InternalPreviewVisible { get @@ -884,7 +897,7 @@ namespace Flow.Launcher.ViewModel private void ShowInternalPreview() { ResultAreaColumn = ResultAreaColumnPreviewShown; - Results.SelectedItem?.LoadPreviewImage(); + SelectedItem?.LoadPreviewImage(); } private void HideInternalPreview() @@ -939,14 +952,14 @@ namespace Flow.Launcher.ViewModel case false when InternalPreviewVisible: - Results.SelectedItem?.LoadPreviewImage(); + SelectedItem?.LoadPreviewImage(); break; } } private bool CanExternalPreviewSelectedResult(out string path) { - path = Results.SelectedItem?.Result?.Preview.FilePath; + path = SelectedItem?.Result?.Preview.FilePath; return !string.IsNullOrEmpty(path); } @@ -976,7 +989,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); ContextMenu.Clear(); - var selected = Results.SelectedItem?.Result; + var selected = SelectedItem?.Result; if (selected != null) // SelectedItem returns null if selection is empty. { diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 5130e7eba..172eca502 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Drawing.Text; +using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; @@ -6,16 +9,13 @@ using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using System.IO; -using System.Drawing.Text; -using System.Collections.Generic; namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - private static PrivateFontCollection fontCollection = new(); - private static Dictionary fonts = new(); + private static readonly PrivateFontCollection fontCollection = new(); + private static readonly Dictionary fonts = new(); public ResultViewModel(Result result, Settings settings) { @@ -39,11 +39,11 @@ namespace Flow.Launcher.ViewModel fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); } - if (fonts.ContainsKey(fontFamilyPath)) + if (fonts.TryGetValue(fontFamilyPath, out var value)) { Glyph = glyph with { - FontFamily = fonts[fontFamilyPath] + FontFamily = value }; } else @@ -171,7 +171,16 @@ namespace Flow.Launcher.ViewModel public ImageSource PreviewImage { - get => previewImage; + get + { + if (!PreviewImageLoaded) + { + PreviewImageLoaded = true; + _ = LoadPreviewImageAsync(); + } + + return previewImage; + } private set => previewImage = value; } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 7d2b5bc93..512f5c150 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -1,6 +1,4 @@ using System; -using Flow.Launcher.Infrastructure.UserSettings; -using Flow.Launcher.Plugin; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -10,6 +8,8 @@ using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; namespace Flow.Launcher.ViewModel { @@ -219,7 +219,6 @@ namespace Flow.Launcher.ViewModel if (newRawResults.Count == 0) return Results; - var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); return Results.Where(r => r.Result.PluginID != resultId) @@ -241,6 +240,7 @@ namespace Flow.Launcher.ViewModel #endregion #region FormattedText Dependency Property + public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( "FormattedText", typeof(Inline), @@ -259,8 +259,7 @@ namespace Flow.Launcher.ViewModel private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - var textBlock = d as TextBlock; - if (textBlock == null) return; + if (d is not TextBlock textBlock) return; var inline = (Inline)e.NewValue; @@ -269,6 +268,7 @@ namespace Flow.Launcher.ViewModel textBlock.Inlines.Add(inline); } + #endregion public class ResultCollection : List, INotifyCollectionChanged @@ -279,7 +279,6 @@ namespace Flow.Launcher.ViewModel public event NotifyCollectionChangedEventHandler CollectionChanged; - protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); @@ -297,6 +296,7 @@ namespace Flow.Launcher.ViewModel // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + private void AddAll(List Items) { for (int i = 0; i < Items.Count; i++) @@ -308,6 +308,7 @@ namespace Flow.Launcher.ViewModel OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); } } + public void RemoveAll(int Capacity = 512) { Clear(); From b93faffdc0d0fc2141cbd0738d1606b3eff5a56a Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:12:20 +0800 Subject: [PATCH 19/57] Code cleanup --- Flow.Launcher/ViewModel/ResultViewModel.cs | 7 ++----- Flow.Launcher/ViewModel/ResultsViewModel.cs | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 172eca502..73b380394 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -21,10 +21,8 @@ namespace Flow.Launcher.ViewModel { Settings = settings; - if (result == null) - { - return; - } + if (result == null) return; + Result = result; if (Result.Glyph is { FontFamily: not null } glyph) @@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel Glyph = glyph; } } - } public Settings Settings { get; } diff --git a/Flow.Launcher/ViewModel/ResultsViewModel.cs b/Flow.Launcher/ViewModel/ResultsViewModel.cs index 512f5c150..61566b415 100644 --- a/Flow.Launcher/ViewModel/ResultsViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultsViewModel.cs @@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel Results = new ResultCollection(); BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); } + public ResultsViewModel(Settings settings) : this() { _settings = settings; From 26ab2aecef33771913af596079959d419aab550b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:19:42 +0800 Subject: [PATCH 20/57] Revert to results selected item --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index f06b8aa81..9b42ae613 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -989,7 +989,7 @@ namespace Flow.Launcher.ViewModel var query = QueryText.ToLower().Trim(); ContextMenu.Clear(); - var selected = SelectedItem?.Result; + var selected = Results.SelectedItem?.Result; if (selected != null) // SelectedItem returns null if selection is empty. { From f1bcfc1933e25901165a72c63279b9aa6ea241c9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:33:04 +0800 Subject: [PATCH 21/57] Support preview panel for history --- Flow.Launcher/MainWindow.xaml | 4 +- Flow.Launcher/ViewModel/MainViewModel.cs | 47 +++++++++++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index d1f6e7812..20e4b20d9 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -441,7 +441,7 @@ diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 9b42ae613..6b307864e 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -162,7 +162,20 @@ namespace Flow.Launcher.ViewModel switch (args.PropertyName) { case nameof(Results.SelectedItem): - SelectedItem = Results.SelectedItem; + _selectedItemFromQueryResults = true; + PreviewSelectedItem = Results.SelectedItem; + UpdatePreview(); + break; + } + }; + + History.PropertyChanged += (_, args) => + { + switch (args.PropertyName) + { + case nameof(History.SelectedItem): + _selectedItemFromQueryResults = false; + PreviewSelectedItem = History.SelectedItem; UpdatePreview(); break; } @@ -646,10 +659,12 @@ namespace Flow.Launcher.ViewModel private ResultsViewModel SelectedResults { - get { return _selectedResults; } + get => _selectedResults; set { + var isReturningFromQueryResults = SelectedIsFromQueryResults(); var isReturningFromContextMenu = ContextMenuSelected(); + var isReturningFromHistory = HistorySelected(); _selectedResults = value; if (SelectedIsFromQueryResults()) { @@ -670,12 +685,30 @@ namespace Flow.Launcher.ViewModel { ChangeQueryText(_queryTextBeforeLeaveResults); } + + // If we are returning from history and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value)) + { + PreviewSelectedItem = null; + } } else { Results.Visibility = Visibility.Collapsed; + History.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; + if(HistorySelected()) + { + // If we are returning from query results and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) + { + PreviewSelectedItem = null; + } + } + // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() @@ -787,9 +820,11 @@ namespace Flow.Launcher.ViewModel #region Preview + private bool? _selectedItemFromQueryResults; + private ResultViewModel _selectedItem; - public ResultViewModel SelectedItem + public ResultViewModel PreviewSelectedItem { get => _selectedItem; set @@ -897,7 +932,7 @@ namespace Flow.Launcher.ViewModel private void ShowInternalPreview() { ResultAreaColumn = ResultAreaColumnPreviewShown; - SelectedItem?.LoadPreviewImage(); + PreviewSelectedItem?.LoadPreviewImage(); } private void HideInternalPreview() @@ -952,14 +987,14 @@ namespace Flow.Launcher.ViewModel case false when InternalPreviewVisible: - SelectedItem?.LoadPreviewImage(); + PreviewSelectedItem?.LoadPreviewImage(); break; } } private bool CanExternalPreviewSelectedResult(out string path) { - path = SelectedItem?.Result?.Preview.FilePath; + path = PreviewSelectedItem == Results.SelectedItem ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty; return !string.IsNullOrEmpty(path); } From 3c7b8cf827dcabfad5c70b713c22d21ae25f7a08 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:37:24 +0800 Subject: [PATCH 22/57] Force preview panel height --- Flow.Launcher/MainWindow.xaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Flow.Launcher/MainWindow.xaml b/Flow.Launcher/MainWindow.xaml index 20e4b20d9..b9e5af117 100644 --- a/Flow.Launcher/MainWindow.xaml +++ b/Flow.Launcher/MainWindow.xaml @@ -432,9 +432,13 @@ + + + From 00ac32708c043cf9b1e52a56894c12f726bef8d9 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:41:20 +0800 Subject: [PATCH 23/57] Fix history visibility issue --- Flow.Launcher/ViewModel/MainViewModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 6b307864e..797e3c533 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -696,7 +696,6 @@ namespace Flow.Launcher.ViewModel else { Results.Visibility = Visibility.Collapsed; - History.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; if(HistorySelected()) From 655b017e9e5f7dbf5b50f607054c834a88b85788 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 22 Mar 2025 23:42:14 +0800 Subject: [PATCH 24/57] Code quality --- Flow.Launcher/ViewModel/MainViewModel.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 797e3c533..905855d88 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -698,16 +698,6 @@ namespace Flow.Launcher.ViewModel Results.Visibility = Visibility.Collapsed; _queryTextBeforeLeaveResults = QueryText; - if(HistorySelected()) - { - // If we are returning from query results and we have not set select item yet, - // we need to clear the preview selected item - if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) - { - PreviewSelectedItem = null; - } - } - // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() @@ -720,6 +710,16 @@ namespace Flow.Launcher.ViewModel { QueryText = string.Empty; } + + if (HistorySelected()) + { + // If we are returning from query results and we have not set select item yet, + // we need to clear the preview selected item + if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value) + { + PreviewSelectedItem = null; + } + } } _selectedResults.Visibility = Visibility.Visible; From e9f317f6a2531abaf54a73f9519ce55058ef42c7 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sun, 23 Mar 2025 11:02:23 +0800 Subject: [PATCH 25/57] Change variable names --- Flow.Launcher/ViewModel/ResultViewModel.cs | 65 ++++++++++------------ 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/Flow.Launcher/ViewModel/ResultViewModel.cs b/Flow.Launcher/ViewModel/ResultViewModel.cs index 73b380394..db124e078 100644 --- a/Flow.Launcher/ViewModel/ResultViewModel.cs +++ b/Flow.Launcher/ViewModel/ResultViewModel.cs @@ -14,8 +14,8 @@ namespace Flow.Launcher.ViewModel { public class ResultViewModel : BaseModel { - private static readonly PrivateFontCollection fontCollection = new(); - private static readonly Dictionary fonts = new(); + private static readonly PrivateFontCollection FontCollection = new(); + private static readonly Dictionary Fonts = new(); public ResultViewModel(Result result, Settings settings) { @@ -37,7 +37,7 @@ namespace Flow.Launcher.ViewModel fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); } - if (fonts.TryGetValue(fontFamilyPath, out var value)) + if (Fonts.TryGetValue(fontFamilyPath, out var value)) { Glyph = glyph with { @@ -46,11 +46,11 @@ namespace Flow.Launcher.ViewModel } else { - fontCollection.AddFontFile(fontFamilyPath); - fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}"; + FontCollection.AddFontFile(fontFamilyPath); + Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}"; Glyph = glyph with { - FontFamily = fonts[fontFamilyPath] + FontFamily = Fonts[fontFamilyPath] }; } } @@ -92,14 +92,10 @@ namespace Flow.Launcher.ViewModel get { if (PreviewImageAvailable) - { return Visibility.Visible; - } - else - { - // Fall back to icon - return ShowIcon; - } + + // Fall back to icon + return ShowIcon; } } @@ -108,9 +104,8 @@ namespace Flow.Launcher.ViewModel get { if (Result.RoundedIcon) - { return IconXY / 2; - } + return IconXY; } @@ -145,40 +140,40 @@ namespace Flow.Launcher.ViewModel ? Result.SubTitle : Result.SubTitleToolTip; - private volatile bool ImageLoaded; - private volatile bool PreviewImageLoaded; + private volatile bool _imageLoaded; + private volatile bool _previewImageLoaded; - private ImageSource image = ImageLoader.LoadingImage; - private ImageSource previewImage = ImageLoader.LoadingImage; + private ImageSource _image = ImageLoader.LoadingImage; + private ImageSource _previewImage = ImageLoader.LoadingImage; public ImageSource Image { get { - if (!ImageLoaded) + if (!_imageLoaded) { - ImageLoaded = true; + _imageLoaded = true; _ = LoadImageAsync(); } - return image; + return _image; } - private set => image = value; + private set => _image = value; } public ImageSource PreviewImage { get { - if (!PreviewImageLoaded) + if (!_previewImageLoaded) { - PreviewImageLoaded = true; + _previewImageLoaded = true; _ = LoadPreviewImageAsync(); } - return previewImage; + return _previewImage; } - private set => previewImage = value; + private set => _previewImage = value; } /// @@ -194,8 +189,7 @@ namespace Flow.Launcher.ViewModel { try { - var image = icon(); - return image; + return icon(); } catch (Exception e) { @@ -214,7 +208,7 @@ namespace Flow.Launcher.ViewModel var iconDelegate = Result.Icon; if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img)) { - image = img; + _image = img; } else { @@ -229,7 +223,7 @@ namespace Flow.Launcher.ViewModel var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img)) { - previewImage = img; + _previewImage = img; } else { @@ -240,13 +234,10 @@ namespace Flow.Launcher.ViewModel public void LoadPreviewImage() { - if (ShowDefaultPreview == Visibility.Visible) + if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible) { - if (!PreviewImageLoaded && ShowPreviewImage == Visibility.Visible) - { - PreviewImageLoaded = true; - _ = LoadPreviewImageAsync(); - } + _previewImageLoaded = true; + _ = LoadPreviewImageAsync(); } } From dfad11cbed8880c17248a2a3a689616ecc7a02a4 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 24 Mar 2025 13:22:15 +0800 Subject: [PATCH 26/57] Fix language change issue --- Flow.Launcher.Core/Plugin/PluginManager.cs | 3 - .../Resource/Internationalization.cs | 79 +++++++++++++------ Flow.Launcher/App.xaml.cs | 9 ++- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PluginManager.cs b/Flow.Launcher.Core/Plugin/PluginManager.cs index b1396e207..aab3caa40 100644 --- a/Flow.Launcher.Core/Plugin/PluginManager.cs +++ b/Flow.Launcher.Core/Plugin/PluginManager.cs @@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin } } - InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface()); - InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService().Language); - if (failedPlugins.Any()) { var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index e2a66656a..baf602096 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource return DefaultLanguageCode; } - internal void AddPluginLanguageDirectories(IEnumerable plugins) + private void AddPluginLanguageDirectories() { - foreach (var plugin in plugins) + foreach (var plugin in PluginManager.GetPluginsForInterface()) { var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location; var dir = Path.GetDirectoryName(location); @@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource _oldResources.Clear(); } + /// + /// Initialize language. Will change app language and plugin language based on settings. + /// + public async Task InitializeLanguageAsync() + { + // Get actual language + var languageCode = _settings.Language; + if (languageCode == Constant.SystemLanguageCode) + { + languageCode = SystemLanguageCode; + } + + // Get language by language code and change language + var language = GetLanguageByLanguageCode(languageCode); + + // Add plugin language directories first so that we can load language files from plugins + AddPluginLanguageDirectories(); + + // Change language + await ChangeLanguageAsync(language); + } + + /// + /// Change language during runtime. Will change app language and plugin language & save settings. + /// + /// public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); @@ -110,10 +136,33 @@ namespace Flow.Launcher.Core.Resource // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); - ChangeLanguage(language, isSystem); + + // Change language + _ = ChangeLanguageAsync(language); + + // Save settings + _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } - private Language GetLanguageByLanguageCode(string languageCode) + private async Task ChangeLanguageAsync(Language language) + { + // Remove old language files and load language + RemoveOldLanguageFiles(); + if (language != AvailableLanguages.English) + { + LoadLanguage(language); + } + + // Culture of main thread + // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's + CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); + CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; + + // Raise event for plugins after culture is set + await Task.Run(UpdatePluginMetadataTranslations); + } + + private static Language GetLanguageByLanguageCode(string languageCode) { var lowercase = languageCode.ToLower(); var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); @@ -128,28 +177,6 @@ namespace Flow.Launcher.Core.Resource } } - private void ChangeLanguage(Language language, bool isSystem) - { - language = language.NonNull(); - - RemoveOldLanguageFiles(); - if (language != AvailableLanguages.English) - { - LoadLanguage(language); - } - // Culture of main thread - // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's - CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode); - CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; - - // Raise event after culture is set - _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; - _ = Task.Run(() => - { - UpdatePluginMetadataTranslations(); - }); - } - public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 377d36c48..b102e384e 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -132,13 +132,14 @@ namespace Flow.Launcher // Register ResultsUpdated event after all plugins are loaded Ioc.Default.GetRequiredService().RegisterResultsUpdatedEvent(); - // Change language after all plugins are initialized - // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future - Ioc.Default.GetRequiredService().ChangeLanguage(_settings.Language); - Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(); + + // Change language after all plugins are initialized because we need to update plugin title based on their api + // TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future + await Ioc.Default.GetRequiredService().InitializeLanguageAsync(); + await imageLoadertask; var window = new MainWindow(); From a3193cf525c3a487bbe690e67f9b07fe05624802 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Mon, 24 Mar 2025 13:24:05 +0800 Subject: [PATCH 27/57] Move function position --- .../Resource/Internationalization.cs | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Internationalization.cs b/Flow.Launcher.Core/Resource/Internationalization.cs index baf602096..ffa17ab4d 100644 --- a/Flow.Launcher.Core/Resource/Internationalization.cs +++ b/Flow.Launcher.Core/Resource/Internationalization.cs @@ -144,6 +144,21 @@ namespace Flow.Launcher.Core.Resource _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } + private Language GetLanguageByLanguageCode(string languageCode) + { + var lowercase = languageCode.ToLower(); + var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); + if (language == null) + { + Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>"); + return AvailableLanguages.English; + } + else + { + return language; + } + } + private async Task ChangeLanguageAsync(Language language) { // Remove old language files and load language @@ -162,21 +177,6 @@ namespace Flow.Launcher.Core.Resource await Task.Run(UpdatePluginMetadataTranslations); } - private static Language GetLanguageByLanguageCode(string languageCode) - { - var lowercase = languageCode.ToLower(); - var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase); - if (language == null) - { - Log.Error($"|Internationalization.GetLanguageByLanguageCode|Language code can't be found <{languageCode}>"); - return AvailableLanguages.English; - } - else - { - return language; - } - } - public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); From ec22f596d432a0a4dc39e2a2643827989e3f6bb2 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 24 Mar 2025 16:26:07 +0900 Subject: [PATCH 28/57] Fix Non Resource situation when change theme --- Flow.Launcher.Core/Resource/Theme.cs | 41 ++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index fc5c2e4e9..25abda3a0 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -17,6 +17,7 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Microsoft.Win32; using TextBox = System.Windows.Controls.TextBox; +using System.Diagnostics; namespace Flow.Launcher.Core.Resource { @@ -56,19 +57,24 @@ namespace Flow.Launcher.Core.Resource MakeSureThemeDirectoriesExist(); var dicts = Application.Current.Resources.MergedDictionaries; - _oldResource = dicts.First(d => + _oldResource = dicts.FirstOrDefault(d => { if (d.Source == null) return false; var p = d.Source.AbsolutePath; - var dir = Path.GetDirectoryName(p).NonNull(); - var info = new DirectoryInfo(dir); - var f = info.Name; - var e = Path.GetExtension(p); - var found = f == Folder && e == Extension; - return found; + return p.Contains(Folder) && Path.GetExtension(p) == Extension; }); + + if (_oldResource != null) + { + _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); + } + else + { + Log.Error("현재 테마 리소스를 찾을 수 없습니다. 기본 테마로 초기화합니다."); + _oldTheme = Constant.DefaultTheme; + }; _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); } @@ -98,11 +104,24 @@ namespace Flow.Launcher.Core.Resource private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - var dicts = Application.Current.Resources.MergedDictionaries; - - dicts.Remove(_oldResource); - dicts.Add(dictionaryToUpdate); + // 새 테마 리소스를 먼저 추가하고 + if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) + { + Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); + } + + // 그 다음 이전 테마 리소스 제거 + if (_oldResource != null && + _oldResource != dictionaryToUpdate && + Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) + { + Application.Current.Resources.MergedDictionaries.Remove(_oldResource); + } + _oldResource = dictionaryToUpdate; + + // 리소스 변경 후 문제가 없는지 검증 + Debug.WriteLine($"테마 변경 후 리소스 딕셔너리 수: {Application.Current.Resources.MergedDictionaries.Count}"); } private ResourceDictionary GetThemeResourceDictionary(string theme) From a078777608281192c3a2835178a895d822972d42 Mon Sep 17 00:00:00 2001 From: DB p Date: Mon, 24 Mar 2025 18:10:12 +0900 Subject: [PATCH 29/57] - Fixed an issue where font settings were not applied when using the Blur theme - Removed bold style from highlight and delegated it to individual themes --- Flow.Launcher.Core/Resource/Theme.cs | 156 ++++++++++++++++-- .../ViewModels/SettingsPaneThemeViewModel.cs | 12 +- Flow.Launcher/Themes/Base.xaml | 5 - Flow.Launcher/Themes/BlurBlack Darker.xaml | 4 +- Flow.Launcher/Themes/BlurBlack.xaml | 4 +- Flow.Launcher/Themes/BlurWhite.xaml | 4 +- 6 files changed, 155 insertions(+), 30 deletions(-) diff --git a/Flow.Launcher.Core/Resource/Theme.cs b/Flow.Launcher.Core/Resource/Theme.cs index 25abda3a0..5e2e336aa 100644 --- a/Flow.Launcher.Core/Resource/Theme.cs +++ b/Flow.Launcher.Core/Resource/Theme.cs @@ -72,7 +72,7 @@ namespace Flow.Launcher.Core.Resource } else { - Log.Error("현재 테마 리소스를 찾을 수 없습니다. 기본 테마로 초기화합니다."); + Log.Error("Current theme resource not found. Initializing with default theme."); _oldTheme = Constant.DefaultTheme; }; _oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath); @@ -104,26 +104,149 @@ namespace Flow.Launcher.Core.Resource private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) { - // 새 테마 리소스를 먼저 추가하고 + // Add the new theme resource first if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate)) { Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate); } - // 그 다음 이전 테마 리소스 제거 + // Then remove the old theme resource if (_oldResource != null && _oldResource != dictionaryToUpdate && Application.Current.Resources.MergedDictionaries.Contains(_oldResource)) { Application.Current.Resources.MergedDictionaries.Remove(_oldResource); } - _oldResource = dictionaryToUpdate; - - // 리소스 변경 후 문제가 없는지 검증 - Debug.WriteLine($"테마 변경 후 리소스 딕셔너리 수: {Application.Current.Resources.MergedDictionaries.Count}"); } + /// + /// Updates only the font settings and refreshes the UI. + /// + public void UpdateFonts() + { + try + { + // Loads a ResourceDictionary for the specified theme. + var themeName = GetCurrentTheme(); + var dict = GetThemeResourceDictionary(themeName); + + // Applies font settings to the theme resource. + ApplyFontSettings(dict); + UpdateResourceDictionary(dict); + _ = RefreshFrameAsync(); + } + catch (Exception e) + { + Log.Exception("Error occurred while updating theme fonts", e); + } + } + + /// + /// Loads and applies font settings to the theme resource. + /// + private void ApplyFontSettings(ResourceDictionary dict) + { + if (dict["QueryBoxStyle"] is Style queryBoxStyle && + dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle) + { + var fontFamily = new FontFamily(_settings.QueryBoxFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); + + SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true); + SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemTitleStyle"] is Style resultItemStyle && + dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle && + dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle && + dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch); + + SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + + if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle && + dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle) + { + var fontFamily = new FontFamily(_settings.ResultSubFont); + var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle); + var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight); + var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch); + + SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false); + } + } + + /// + /// Applies font properties to a Style. + /// + private void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox) + { + // Remove existing font-related setters + if (isTextBox) + { + // First, find the setters to remove and store them in a list + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBox.FontFamilyProperty || + setter.Property == TextBox.FontStyleProperty || + setter.Property == TextBox.FontWeightProperty || + setter.Property == TextBox.FontStretchProperty) + .ToList(); + + // Remove each found setter one by one + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + // Add New font setter + style.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); + + // Set caret brush (retain existing logic) + var caretBrushPropertyValue = style.Setters.OfType().Any(x => x.Property.Name == "CaretBrush"); + var foregroundPropertyValue = style.Setters.OfType().Where(x => x.Property.Name == "Foreground") + .Select(x => x.Value).FirstOrDefault(); + if (!caretBrushPropertyValue && foregroundPropertyValue != null) + style.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue)); + } + else + { + var settersToRemove = style.Setters + .OfType() + .Where(setter => + setter.Property == TextBlock.FontFamilyProperty || + setter.Property == TextBlock.FontStyleProperty || + setter.Property == TextBlock.FontWeightProperty || + setter.Property == TextBlock.FontStretchProperty) + .ToList(); + + foreach (var setter in settersToRemove) + { + style.Setters.Remove(setter); + } + + style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily)); + style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle)); + style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight)); + style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch)); + } + } private ResourceDictionary GetThemeResourceDictionary(string theme) { var uri = GetThemePath(theme); @@ -286,9 +409,10 @@ namespace Flow.Launcher.Core.Resource 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)); + // Retrieve theme resource – always use the resource with font settings applied. + var resourceDict = GetResourceDictionary(theme); + + UpdateResourceDictionary(resourceDict); _settings.Theme = theme; @@ -299,10 +423,11 @@ namespace Flow.Launcher.Core.Resource } BlurEnabled = IsBlurTheme(); - //if (_settings.UseDropShadowEffect) - // AddDropShadowEffectToCurrentTheme(); - //Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); - _ = SetBlurForWindowAsync(); + + // 블러 및 그림자 효과 적용을 위한 비동기 처리 + _ = RefreshFrameAsync(); + + return true; } catch (DirectoryNotFoundException) { @@ -324,7 +449,6 @@ namespace Flow.Launcher.Core.Resource } return false; } - return true; } #endregion @@ -500,7 +624,7 @@ namespace Flow.Launcher.Core.Resource private void SetBlurForWindow(string theme, BackdropTypes backdropType) { - var dict = GetThemeResourceDictionary(theme); + var dict = GetResourceDictionary(theme); // GetThemeResourceDictionary 대신 GetResourceDictionary 사용 if (dict == null) return; diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs index 61d365b64..48a0aa1d1 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneThemeViewModel.cs @@ -342,7 +342,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.QueryBoxFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -364,7 +364,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.QueryBoxFontStretch = value.Stretch.ToString(); Settings.QueryBoxFontWeight = value.Weight.ToString(); Settings.QueryBoxFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -386,7 +386,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -408,7 +408,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultFontStretch = value.Stretch.ToString(); Settings.ResultFontWeight = value.Weight.ToString(); Settings.ResultFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -432,7 +432,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel set { Settings.ResultSubFont = value.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } @@ -453,7 +453,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel Settings.ResultSubFontStretch = value.Stretch.ToString(); Settings.ResultSubFontWeight = value.Weight.ToString(); Settings.ResultSubFontStyle = value.Style.ToString(); - _theme.ChangeTheme(); + _theme.UpdateFonts(); } } diff --git a/Flow.Launcher/Themes/Base.xaml b/Flow.Launcher/Themes/Base.xaml index 907ded363..4772e7768 100644 --- a/Flow.Launcher/Themes/Base.xaml +++ b/Flow.Launcher/Themes/Base.xaml @@ -27,7 +27,6 @@ - + - + - + - - + - + +