Merge branch 'dev' into search_delay

This commit is contained in:
Jack Ye 2025-03-26 15:54:55 +08:00 committed by GitHub
commit 5a88a1fc41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 883 additions and 301 deletions

View file

@ -205,9 +205,6 @@ namespace Flow.Launcher.Core.Plugin
}
}
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
if (failedPlugins.Any())
{
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));

View file

@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource
return DefaultLanguageCode;
}
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
private void AddPluginLanguageDirectories()
{
foreach (var plugin in plugins)
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location);
@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource
_oldResources.Clear();
}
/// <summary>
/// Initialize language. Will change app language and plugin language based on settings.
/// </summary>
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);
}
/// <summary>
/// Change language during runtime. Will change app language and plugin language & save settings.
/// </summary>
/// <param name="languageCode"></param>
public void ChangeLanguage(string languageCode)
{
languageCode = languageCode.NonNull();
@ -110,7 +136,12 @@ 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)
@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource
}
}
private void ChangeLanguage(Language language, bool isSystem)
private async Task ChangeLanguageAsync(Language language)
{
language = language.NonNull();
// 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 after culture is set
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
_ = Task.Run(() =>
{
UpdatePluginMetadataTranslations();
});
// Raise event for plugins after culture is set
await Task.Run(UpdatePluginMetadataTranslations);
}
public bool PromptShouldUsePinyin(string languageCodeToSet)

View file

@ -6,6 +6,7 @@ using System.Xml;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Effects;
@ -16,7 +17,6 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Microsoft.Win32;
using TextBox = System.Windows.Controls.TextBox;
namespace Flow.Launcher.Core.Resource
{
@ -56,20 +56,23 @@ 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;
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;
});
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
if (_oldResource != null)
{
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
}
else
{
Log.Error("Current theme resource not found. Initializing with default theme.");
_oldTheme = Constant.DefaultTheme;
};
}
#endregion
@ -98,13 +101,152 @@ namespace Flow.Launcher.Core.Resource
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
{
var dicts = Application.Current.Resources.MergedDictionaries;
// Add new resources
if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate))
{
Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate);
}
// Remove old resources
if (_oldResource != null && _oldResource != dictionaryToUpdate &&
Application.Current.Resources.MergedDictionaries.Contains(_oldResource))
{
Application.Current.Resources.MergedDictionaries.Remove(_oldResource);
}
dicts.Remove(_oldResource);
dicts.Add(dictionaryToUpdate);
_oldResource = dictionaryToUpdate;
}
/// <summary>
/// Updates only the font settings and refreshes the UI.
/// </summary>
public void UpdateFonts()
{
try
{
// Load a ResourceDictionary for the specified theme.
var themeName = GetCurrentTheme();
var dict = GetThemeResourceDictionary(themeName);
// Apply font settings to the theme resource.
ApplyFontSettings(dict);
UpdateResourceDictionary(dict);
// Must apply blur and drop shadow effects
_ = RefreshFrameAsync();
}
catch (Exception e)
{
Log.Exception("Error occurred while updating theme fonts", e);
}
}
/// <summary>
/// Loads and applies font settings to the theme resource.
/// </summary>
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);
}
}
/// <summary>
/// Applies font properties to a Style.
/// </summary>
private static 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<Setter>()
.Where(setter =>
setter.Property == Control.FontFamilyProperty ||
setter.Property == Control.FontStyleProperty ||
setter.Property == Control.FontWeightProperty ||
setter.Property == Control.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(Control.FontFamilyProperty, fontFamily));
style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
// Set caret brush (retain existing logic)
var caretBrushPropertyValue = style.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
var foregroundPropertyValue = style.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
.Select(x => x.Value).FirstOrDefault();
if (!caretBrushPropertyValue && foregroundPropertyValue != null)
style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
}
else
{
var settersToRemove = style.Setters
.OfType<Setter>()
.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);
@ -128,22 +270,22 @@ namespace Flow.Launcher.Core.Resource
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
.Select(x => x.Value).FirstOrDefault();
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling
queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue));
queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
// Query suggestion box's font style is aligned with query box
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
}
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
@ -180,7 +322,7 @@ namespace Flow.Launcher.Core.Resource
/* Ignore Theme Window Width and use setting */
var windowStyle = dict["WindowStyle"] as Style;
var width = _settings.WindowSize;
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
return dict;
}
@ -265,11 +407,12 @@ namespace Flow.Launcher.Core.Resource
try
{
if (string.IsNullOrEmpty(path))
throw new DirectoryNotFoundException("Theme path can't be found <{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;
@ -280,10 +423,11 @@ namespace Flow.Launcher.Core.Resource
}
BlurEnabled = IsBlurTheme();
//if (_settings.UseDropShadowEffect)
// AddDropShadowEffectToCurrentTheme();
//Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
_ = SetBlurForWindowAsync();
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
_ = RefreshFrameAsync();
return true;
}
catch (DirectoryNotFoundException)
{
@ -305,7 +449,6 @@ namespace Flow.Launcher.Core.Resource
}
return false;
}
return true;
}
#endregion
@ -481,17 +624,14 @@ namespace Flow.Launcher.Core.Resource
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
{
var dict = GetThemeResourceDictionary(theme);
if (dict == null)
return;
var dict = GetResourceDictionary(theme);
if (dict == null) return;
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
if (windowBorderStyle == null)
return;
if (windowBorderStyle == null) return;
Window mainWindow = Application.Current.MainWindow;
if (mainWindow == null)
return;
var mainWindow = Application.Current.MainWindow;
if (mainWindow == null) return;
// Check if the theme supports blur
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;

View file

@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
public static string PythonPath;
public static string NodePath;

View file

@ -46,4 +46,16 @@ GetMonitorInfo
MONITORINFOEXW
WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
WM_EXITSIZEMOVE
GetKeyboardLayout
GetWindowThreadProcessId
ActivateKeyboardLayout
GetKeyboardLayoutList
PostMessage
WM_INPUTLANGCHANGEREQUEST
INPUTLANGCHANGE_FORWARD
LOCALE_TRANSIENT_KEYBOARD1
LOCALE_TRANSIENT_KEYBOARD2
LOCALE_TRANSIENT_KEYBOARD3
LOCALE_TRANSIENT_KEYBOARD4

View file

@ -1,14 +1,18 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings;
using Microsoft.Win32;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using Flow.Launcher.Infrastructure.UserSettings;
using Point = System.Windows.Point;
namespace Flow.Launcher.Infrastructure
{
@ -63,7 +67,7 @@ namespace Flow.Launcher.Infrastructure
}
/// <summary>
///
///
/// </summary>
/// <param name="window"></param>
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
#region Keyboard Layout
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
private const string EnglishLanguageTag = "en";
private static readonly string[] ImeLanguageTags =
{
"zh", // Chinese
"ja", // Japanese
"ko", // Korean
};
private const uint KeyboardLayoutLoWord = 0xFFFF;
// Store the previous keyboard layout
private static HKL _previousLayout;
/// <summary>
/// Switches the keyboard layout to English if available.
/// </summary>
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
{
// Find an installed English layout
var enHKL = FindEnglishKeyboardLayout();
// No installed English layout found
if (enHKL == HKL.Null) return;
// Get the current foreground window
var hwnd = PInvoke.GetForegroundWindow();
if (hwnd == HWND.Null) return;
// Get the current foreground window thread ID
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
// If the current layout has an IME mode, disable it without switching to another layout.
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
// the IME mode instead of switching to another layout.
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
foreach (var langTag in ImeLanguageTags)
{
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
{
return;
}
}
// Backup current keyboard layout
if (backupPrevious) _previousLayout = currentLayout;
// Switch to English layout
PInvoke.ActivateKeyboardLayout(enHKL, 0);
}
/// <summary>
/// Restores the previously backed-up keyboard layout.
/// If it wasn't backed up or has already been restored, this method does nothing.
/// </summary>
public static void RestorePreviousKeyboardLayout()
{
if (_previousLayout == HKL.Null) return;
var hwnd = PInvoke.GetForegroundWindow();
if (hwnd == HWND.Null) return;
PInvoke.PostMessage(
hwnd,
PInvoke.WM_INPUTLANGCHANGEREQUEST,
PInvoke.INPUTLANGCHANGE_FORWARD,
_previousLayout.Value
);
_previousLayout = HKL.Null;
}
/// <summary>
/// Finds an installed English keyboard layout.
/// </summary>
/// <returns></returns>
/// <exception cref="Win32Exception"></exception>
private static unsafe HKL FindEnglishKeyboardLayout()
{
// Get the number of keyboard layouts
int count = PInvoke.GetKeyboardLayoutList(0, null);
if (count <= 0) return HKL.Null;
// Get all keyboard layouts
var handles = new HKL[count];
fixed (HKL* h = handles)
{
var result = PInvoke.GetKeyboardLayoutList(count, h);
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
}
// Look for any English keyboard layout
foreach (var hkl in handles)
{
// The lower word contains the language identifier
var langId = (uint)hkl.Value & KeyboardLayoutLoWord;
var langTag = GetLanguageTag(langId);
// Check if it's an English layout
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
{
return hkl;
}
}
return HKL.Null;
}
/// <summary>
/// Returns the
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
/// BCP 47 language tag
/// </see>
/// of the current input language.
/// </summary>
/// <remarks>
/// Edited from: https://github.com/dotnet/winforms
/// </remarks>
private static string GetLanguageTag(uint langId)
{
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
// transient value.
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
//
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
//
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
//
// NOTE: this logic may break in future versions of Windows since it is not documented.
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
{
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
if (key?.GetValue("Languages") is string[] languages)
{
foreach (string language in languages)
{
using var subKey = key.OpenSubKey(language);
if (subKey?.GetValue("TransientLangId") is int transientLangId
&& transientLangId == langId)
{
return language;
}
}
}
}
return CultureInfo.GetCultureInfo((int)langId).Name;
}
#endregion
}
}

View file

@ -27,11 +27,26 @@ namespace Flow.Launcher
{
public partial class App : IDisposable, ISingleInstanceApp
{
#region Public Properties
public static IPublicAPI API { get; private set; }
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
#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
@ -79,27 +94,33 @@ namespace Flow.Launcher
{
API = Ioc.Default.GetRequiredService<IPublicAPI>();
_settings.Initialize();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
}
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()
{
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
if (SingleInstance<App>.InitializeAsFirstInstance())
{
using var application = new App();
application.InitializeComponent();
@ -107,6 +128,10 @@ namespace Flow.Launcher
}
}
#endregion
#region App Events
#pragma warning disable VSTHRD100 // Avoid async void methods
private async void OnStartup(object sender, StartupEventArgs e)
@ -127,21 +152,26 @@ namespace Flow.Launcher
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
PluginManager.LoadPlugins(_settings.PluginSettings);
// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
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<Internationalization>().InitializeLanguageAsync();
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();
@ -158,8 +188,7 @@ namespace Flow.Launcher
AutoUpdates();
API.SaveAppAllSettings();
Log.Info(
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
});
}
@ -192,7 +221,6 @@ namespace Flow.Launcher
}
}
//[Conditional("RELEASE")]
private void AutoUpdates()
{
_ = Task.Run(async () =>
@ -210,11 +238,29 @@ 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");
Dispose();
};
Current.Exit += (s, e) =>
{
Log.Info("|App.RegisterExitEvents|Application Exit");
Dispose();
};
Current.SessionEnding += (s, e) =>
{
Log.Info("|App.RegisterExitEvents|Session Ending");
Dispose();
};
}
/// <summary>
@ -235,20 +281,60 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
}
public void Dispose()
#endregion
#region IDisposable
protected virtual void Dispose(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;
}
Stopwatch.Normal("|App.Dispose|Dispose cost", () =>
{
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
if (disposing)
{
// 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();
}
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
});
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
#region ISingleInstanceApp
public void OnSecondAppStarted()
{
Ioc.Default.GetRequiredService<MainViewModel>().Show();
}
#endregion
}
}

View file

@ -8,10 +8,10 @@ using System.Windows;
// modified to allow single instace restart
namespace Flow.Launcher.Helper
{
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
}
public interface ISingleInstanceApp
{
void OnSecondAppStarted();
}
/// <summary>
/// This class checks to make sure that only one instance of
@ -24,9 +24,7 @@ namespace Flow.Launcher.Helper
/// running as Administrator, can activate it with command line arguments.
/// For most apps, this will not be much of an issue.
/// </remarks>
public static class SingleInstance<TApplication>
where TApplication: Application , ISingleInstanceApp
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
{
#region Private Fields
@ -39,11 +37,12 @@ namespace Flow.Launcher.Helper
/// Suffix to the channel name.
/// </summary>
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
/// <summary>
/// Application mutex.
/// </summary>
internal static Mutex singleInstanceMutex;
internal static Mutex SingleInstanceMutex { get; set; }
#endregion
@ -54,24 +53,23 @@ namespace Flow.Launcher.Helper
/// If not, activates the first instance.
/// </summary>
/// <returns>True if this is the first instance of the application.</returns>
public static bool InitializeAsFirstInstance( string uniqueName )
public static bool InitializeAsFirstInstance()
{
// Build unique application Id and the IPC channel name.
string applicationIdentifier = uniqueName + Environment.UserName;
string applicationIdentifier = InstanceMutexName + Environment.UserName;
string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
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);
SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance);
if (firstInstance)
{
_ = CreateRemoteService(channelName);
_ = CreateRemoteServiceAsync(channelName);
return true;
}
else
{
_ = SignalFirstInstance(channelName);
_ = SignalFirstInstanceAsync(channelName);
return false;
}
}
@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper
/// </summary>
public static void Cleanup()
{
singleInstanceMutex?.ReleaseMutex();
SingleInstanceMutex?.ReleaseMutex();
}
#endregion
@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper
/// Once receives signal from client, will activate first instance.
/// </summary>
/// <param name="channelName">Application's IPC channel name.</param>
private static async Task CreateRemoteService(string channelName)
private static async Task CreateRemoteServiceAsync(string channelName)
{
using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In))
using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In);
while (true)
{
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();
}
// 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();
}
}
@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper
/// <param name="args">
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
/// </param>
private static async Task SignalFirstInstance(string channelName)
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);
}
}
using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
/// <summary>
/// Callback for activating first instance of the application.
/// </summary>
/// <param name="arg">Callback argument.</param>
/// <returns>Always null.</returns>
private static object ActivateFirstInstanceCallback(object o)
{
ActivateFirstInstance();
return null;
// Connect to the available pipe
await pipeClient.ConnectAsync(0);
}
/// <summary>

View file

@ -17,6 +17,7 @@
AllowDrop="True"
AllowsTransparency="True"
Background="Transparent"
Closed="OnClosed"
Closing="OnClosing"
Deactivated="OnDeactivated"
Icon="Images/app.png"
@ -215,7 +216,7 @@
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
<StackPanel Orientation="Vertical">
<Grid>
<Grid x:Name="QueryBoxArea">
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
<Grid>
<TextBox
@ -338,7 +339,7 @@
Y2="0" />
</Grid>
<Grid ClipToBounds="True">
<Grid x:Name="MiddleSeparatorArea" ClipToBounds="True">
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
@ -378,7 +379,7 @@
</ContentControl>
</Grid>
<Border Style="{DynamicResource WindowRadius}">
<Border x:Name="ResultPreviewAreaBoarder" Style="{DynamicResource WindowRadius}">
<Border.Clip>
<MultiBinding Converter="{StaticResource BorderClipConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
@ -386,12 +387,14 @@
<Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</Border.Clip>
<Grid>
<Grid x:Name="ResultPreviewArea">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="80" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="0.85*" MinWidth="244" />
</Grid.ColumnDefinitions>
<StackPanel
x:Name="ResultArea"
Grid.Column="0"
@ -418,7 +421,9 @@
RightClickResultCommand="{Binding RightClickResultCommand}" />
</ContentControl>
</StackPanel>
<GridSplitter
x:Name="PreviewMiddleSeparator"
Grid.Column="1"
Margin="0"
HorizontalAlignment="Center"
@ -432,6 +437,7 @@
</ControlTemplate>
</GridSplitter.Template>
</GridSplitter>
<Grid
x:Name="Preview"
Grid.Column="2"
@ -441,7 +447,7 @@
<Border
MinHeight="380"
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
Visibility="{Binding ShowDefaultPreview}">
<Grid
Margin="0 0 10 5"
@ -518,7 +524,7 @@
MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}"
Padding="0 0 10 10"
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
Visibility="{Binding ShowCustomizedPreview}">
<ContentControl Content="{Binding Result.PreviewPanel.Value}" />
</Border>

View file

@ -30,7 +30,7 @@ using Screen = System.Windows.Forms.Screen;
namespace Flow.Launcher
{
public partial class MainWindow
public partial class MainWindow : IDisposable
{
#region Private Fields
@ -42,17 +42,20 @@ 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 : Key Event
private bool isArrowKeyPressed = false;
// Window Event: Close Event
private bool _canClose = false;
// Window Event: Key Event
private bool _isArrowKeyPressed = false;
// Window Sound Effects
private MediaPlayer animationSoundWMP;
private SoundPlayer animationSoundWPF;
// Window WndProc
private HwndSource _hwndSource;
private int _initialWidth;
private int _initialHeight;
@ -64,6 +67,9 @@ namespace Flow.Launcher
// Search Delay
private IDisposable _reactiveSubscription;
// IDisposable
private bool _disposed = false;
#endregion
#region Constructor
@ -91,8 +97,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);
}
@ -107,6 +113,9 @@ namespace Flow.Launcher
{
_settings.FirstLaunch = false;
App.API.SaveAppAllSettings();
/* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
_settings.BackdropType = BackdropTypes.Acrylic;
var WelcomeWindow = new WelcomeWindow();
WelcomeWindow.Show();
}
@ -149,7 +158,11 @@ namespace Flow.Launcher
// Since the default main window visibility is visible, so we need set focus during startup
QueryTextBox.Focus();
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
_viewModel.QueryTextCursorMovedToEnd = false;
// View model property changed event
_viewModel.PropertyChanged += (o, e) =>
{
@ -227,15 +240,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());
@ -243,18 +256,37 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e)
{
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
Environment.Exit(0);
if (!_canClose)
{
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we can close the main window
_canClose = true;
Close();
}
}
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;
@ -292,12 +324,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;
@ -310,7 +342,7 @@ namespace Flow.Launcher
e.Handled = true;
break;
case Key.Right:
if (_viewModel.SelectedIsFromQueryResults()
if (_viewModel.QueryResultsSelected()
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
&& !string.IsNullOrEmpty(QueryTextBox.Text))
{
@ -320,7 +352,7 @@ namespace Flow.Launcher
break;
case Key.Left:
if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0)
if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
{
_viewModel.EscCommand.Execute(null);
e.Handled = true;
@ -330,7 +362,7 @@ namespace Flow.Launcher
case Key.Back:
if (specialKeyState.CtrlPressed)
{
if (_viewModel.SelectedIsFromQueryResults()
if (_viewModel.QueryResultsSelected()
&& QueryTextBox.Text.Length > 0
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{
@ -355,13 +387,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 +563,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 +578,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 +593,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 +789,7 @@ namespace Flow.Launcher
if (_animating)
return;
isArrowKeyPressed = true;
_isArrowKeyPressed = true;
_animating = true;
UpdatePosition(false);
@ -835,7 +867,7 @@ namespace Flow.Launcher
clocksb.Completed += (_, _) => _animating = false;
_settings.WindowLeft = Left;
isArrowKeyPressed = false;
_isArrowKeyPressed = false;
if (QueryTextBox.Text.Length == 0)
{
@ -1048,5 +1080,31 @@ namespace Flow.Launcher
}
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hwndSource?.Dispose();
_notifyIcon?.Dispose();
_reactiveSubscription?.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View file

@ -229,6 +229,8 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.BackdropType = value;
// Can only apply blur because drop shadow effect is not supported with backdrop
// So drop shadow effect has been disabled
_ = _theme.SetBlurForWindowAsync();
OnPropertyChanged(nameof(IsDropShadowEnabled));
@ -342,7 +344,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.QueryBoxFont = value.ToString();
_theme.ChangeTheme();
_theme.UpdateFonts();
}
}
@ -364,7 +366,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 +388,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.ResultFont = value.ToString();
_theme.ChangeTheme();
_theme.UpdateFonts();
}
}
@ -408,7 +410,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 +434,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set
{
Settings.ResultSubFont = value.ToString();
_theme.ChangeTheme();
_theme.UpdateFonts();
}
}
@ -453,7 +455,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();
}
}

View file

@ -27,7 +27,6 @@
<Style x:Key="BaseQueryBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontSize" Value="28" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="Margin" Value="16 7 0 7" />
<Setter Property="Padding" Value="0 0 68 0" />
<Setter Property="Background" Value="Transparent" />
@ -181,12 +180,10 @@
<Style x:Key="BaseItemTitleStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#FFFFF8" />
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Medium" />
</Style>
<Style x:Key="BaseItemSubTitleStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#D9D9D4" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=SubTitle, UpdateSourceTrigger=PropertyChanged, Path=Text.Length}" Value="0">
<Setter Property="Height" Value="0" />
@ -218,7 +215,6 @@
<Style x:Key="BaseItemTitleSelectedStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#FFFFF8" />
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Normal" />
</Style>
<Style x:Key="BaseItemSubTitleSelectedStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="#D9D9D4" />
@ -394,6 +390,7 @@
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
@ -435,12 +432,12 @@
</DataTrigger>
</Style.Triggers>
</Style>
<Style
x:Key="PreviewBorderStyle"
BasedOn="{StaticResource BasePreviewBorderStyle}"
TargetType="{x:Type Border}">
<Setter Property="BorderBrush" Value="Gray" />
</Style>
<Style x:Key="PreviewArea" TargetType="{x:Type Grid}">
@ -450,8 +447,8 @@
<MultiDataTrigger.Conditions>
<!--
<Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />-->
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />-->
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>

View file

@ -54,7 +54,6 @@
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SeparatorStyle"
BasedOn="{StaticResource BaseSeparatorStyle}"

View file

@ -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
@ -49,6 +49,8 @@ namespace Flow.Launcher.ViewModel
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
private Task _resultsViewUpdateTask;
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
#endregion
#region Constructor
@ -162,13 +164,26 @@ namespace Flow.Launcher.ViewModel
switch (args.PropertyName)
{
case nameof(Results.SelectedItem):
UpdatePreview();
_selectedItemFromQueryResults = true;
PreviewSelectedItem = Results.SelectedItem;
_ = UpdatePreviewAsync();
break;
}
};
History.PropertyChanged += (_, args) =>
{
switch (args.PropertyName)
{
case nameof(History.SelectedItem):
_selectedItemFromQueryResults = false;
PreviewSelectedItem = History.SelectedItem;
_ = UpdatePreviewAsync();
break;
}
};
RegisterViewUpdate();
RegisterResultsUpdatedEvent();
_ = RegisterClockAndDateUpdateAsync();
}
@ -213,7 +228,7 @@ namespace Flow.Launcher.ViewModel
}
}
private void RegisterResultsUpdatedEvent()
public void RegisterResultsUpdatedEvent()
{
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
{
@ -266,7 +281,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void LoadHistory()
{
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1;
@ -280,7 +295,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
public void ReQuery()
{
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
_ = QueryResultsAsync(false, isReQuery: true);
}
@ -321,7 +336,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void LoadContextMenu()
{
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
@ -351,7 +366,7 @@ namespace Flow.Launcher.ViewModel
private void AutocompleteQuery()
{
var result = SelectedResults.SelectedItem?.Result;
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty.
if (result != null && QueryResultsSelected()) // SelectedItem returns null if selection is empty.
{
var autoCompleteText = result.Title;
@ -403,7 +418,7 @@ namespace Flow.Launcher.ViewModel
})
.ConfigureAwait(false);
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
_userSelectedRecord.Add(result);
// origin query is null when user select the context menu item directly of one item from query list
@ -482,7 +497,7 @@ namespace Flow.Launcher.ViewModel
{
if (_history.Items.Count > 0
&& QueryText == string.Empty
&& SelectedIsFromQueryResults())
&& QueryResultsSelected())
{
lastHistoryIndex = 1;
ReverseHistory();
@ -502,7 +517,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand]
private void Esc()
{
if (!SelectedIsFromQueryResults())
if (!QueryResultsSelected())
{
SelectedResults = Results;
}
@ -514,7 +529,7 @@ namespace Flow.Launcher.ViewModel
public void BackToQueryResults()
{
if (!SelectedIsFromQueryResults())
if (!QueryResultsSelected())
{
SelectedResults = Results;
}
@ -645,13 +660,16 @@ namespace Flow.Launcher.ViewModel
private ResultsViewModel SelectedResults
{
get { return _selectedResults; }
get => _selectedResults;
set
{
var isReturningFromQueryResults = QueryResultsSelected();
var isReturningFromContextMenu = ContextMenuSelected();
var isReturningFromHistory = HistorySelected();
_selectedResults = value;
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
Results.Visibility = Visibility.Visible;
ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Collapsed;
@ -669,10 +687,27 @@ 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;
if (HistorySelected())
{
ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Visible;
}
else
{
ContextMenu.Visibility = Visibility.Visible;
History.Visibility = Visibility.Collapsed;
}
_queryTextBeforeLeaveResults = QueryText;
// Because of Fody's optimization
@ -681,6 +716,16 @@ namespace Flow.Launcher.ViewModel
// http://stackoverflow.com/posts/25895769/revisions
QueryText = string.Empty;
Query(false);
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;
@ -780,6 +825,22 @@ namespace Flow.Launcher.ViewModel
#region Preview
private static readonly int ResultAreaColumnPreviewShown = 1;
private static readonly int ResultAreaColumnPreviewHidden = 3;
private bool? _selectedItemFromQueryResults;
private ResultViewModel _previewSelectedItem;
public ResultViewModel PreviewSelectedItem
{
get => _previewSelectedItem;
set
{
_previewSelectedItem = value;
OnPropertyChanged();
}
}
public bool InternalPreviewVisible
{
get
@ -798,18 +859,14 @@ namespace Flow.Launcher.ViewModel
}
}
private static readonly int ResultAreaColumnPreviewShown = 1;
private static readonly int ResultAreaColumnPreviewHidden = 3;
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
// This is not a reliable indicator of whether external preview is visible due to the
// ability of manually closing/exiting the external preview program which, does not inform flow that
// preview is no longer available.
public bool ExternalPreviewVisible { get; set; } = false;
public bool ExternalPreviewVisible { get; private set; }
private void ShowPreview()
private async Task ShowPreviewAsync()
{
var useExternalPreview = PluginManager.UseExternalPreview();
@ -820,13 +877,15 @@ namespace Flow.Launcher.ViewModel
// Internal preview may still be on when user switches to external
if (InternalPreviewVisible)
HideInternalPreview();
OpenExternalPreview(path);
_ = OpenExternalPreviewAsync(path);
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible)
CloseExternalPreview();
await CloseExternalPreviewAsync();
ShowInternalPreview();
break;
@ -839,7 +898,7 @@ namespace Flow.Launcher.ViewModel
private void HidePreview()
{
if (PluginManager.UseExternalPreview())
CloseExternalPreview();
_ = CloseExternalPreviewAsync();
if (InternalPreviewVisible)
HideInternalPreview();
@ -854,31 +913,31 @@ namespace Flow.Launcher.ViewModel
}
else
{
ShowPreview();
_ = ShowPreviewAsync();
}
}
private void OpenExternalPreview(string path, bool sendFailToast = true)
private async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
{
_ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
await PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
ExternalPreviewVisible = true;
}
private void CloseExternalPreview()
private async Task CloseExternalPreviewAsync()
{
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
ExternalPreviewVisible = false;
}
private static void SwitchExternalPreview(string path, bool sendFailToast = true)
private static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
{
_ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false);
await PluginManager.SwitchExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
}
private void ShowInternalPreview()
{
ResultAreaColumn = ResultAreaColumnPreviewShown;
Results.SelectedItem?.LoadPreviewImage();
PreviewSelectedItem?.LoadPreviewImage();
}
private void HideInternalPreview()
@ -892,20 +951,18 @@ namespace Flow.Launcher.ViewModel
{
case true
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
OpenExternalPreview(path);
_ = OpenExternalPreviewAsync(path);
break;
case true:
ShowInternalPreview();
break;
case false:
HidePreview();
break;
}
}
private void UpdatePreview()
private async Task UpdatePreviewAsync()
{
switch (PluginManager.UseExternalPreview())
{
@ -913,44 +970,48 @@ namespace Flow.Launcher.ViewModel
when CanExternalPreviewSelectedResult(out var path):
if (ExternalPreviewVisible)
{
SwitchExternalPreview(path, false);
_ = SwitchExternalPreviewAsync(path, false);
}
else if (InternalPreviewVisible)
{
HideInternalPreview();
OpenExternalPreview(path);
_ = OpenExternalPreviewAsync(path);
}
break;
case true
when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible)
{
CloseExternalPreview();
await CloseExternalPreviewAsync();
ShowInternalPreview();
}
break;
case false
when InternalPreviewVisible:
Results.SelectedItem?.LoadPreviewImage();
PreviewSelectedItem?.LoadPreviewImage();
break;
}
}
private bool CanExternalPreviewSelectedResult(out string path)
{
path = Results.SelectedItem?.Result?.Preview.FilePath;
path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty;
return !string.IsNullOrEmpty(path);
}
private bool QueryResultsPreviewed()
{
var previewed = PreviewSelectedItem == Results.SelectedItem;
return previewed;
}
#endregion
#region Query
public void Query(bool searchDelay, bool isReQuery = false)
private void Query(bool searchDelay, bool isReQuery = false)
{
if (SelectedIsFromQueryResults())
if (QueryResultsSelected())
{
_ = QueryResultsAsync(searchDelay, isReQuery);
}
@ -1022,6 +1083,11 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png",
Preview = new Result.PreviewInfo
{
PreviewImagePath = Constant.HistoryIcon,
Description = string.Format(time, h.ExecutedDateTime)
},
OriginQuery = new Query { RawQuery = h.Query },
Action = _ =>
{
@ -1048,8 +1114,6 @@ namespace Flow.Launcher.ViewModel
}
}
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{
// TODO: Remove debug codes.
@ -1347,7 +1411,7 @@ namespace Flow.Launcher.ViewModel
return menu;
}
internal bool SelectedIsFromQueryResults()
internal bool QueryResultsSelected()
{
var selected = SelectedResults == Results;
return selected;
@ -1381,6 +1445,18 @@ namespace Flow.Launcher.ViewModel
}
}
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// </summary>
public bool ShouldIgnoreHotkeys()
{
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
}
#endregion
#region Public Methods
public void Show()
{
Application.Current.Dispatcher.Invoke(() =>
@ -1401,6 +1477,11 @@ namespace Flow.Launcher.ViewModel
MainWindowOpacity = 1;
MainWindowVisibilityStatus = true;
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
if (StartWithEnglishMode)
{
Win32Helper.SwitchToEnglishKeyboardLayout(true);
}
});
}
@ -1412,10 +1493,10 @@ namespace Flow.Launcher.ViewModel
if (ExternalPreviewVisible)
{
CloseExternalPreview();
await CloseExternalPreviewAsync();
}
if (!SelectedIsFromQueryResults())
if (!QueryResultsSelected())
{
SelectedResults = Results;
}
@ -1425,10 +1506,7 @@ namespace Flow.Launcher.ViewModel
{
ChangeQueryText(string.Empty);
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
Application.Current.Dispatcher.Invoke(() =>
{
Application.Current.MainWindow.UpdateLayout(); // Force UI update
});
Application.Current.Dispatcher.Invoke(Application.Current.MainWindow.UpdateLayout); // Force UI update
}
switch (Settings.LastQueryMode)
@ -1469,7 +1547,12 @@ namespace Flow.Launcher.ViewModel
// 📌 Apply DWM Cloak (Completely hide the window)
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
}
if (StartWithEnglishMode)
{
Win32Helper.RestorePreviousKeyboardLayout();
}
await Task.Delay(50);
// Update WPF properties
@ -1482,17 +1565,8 @@ namespace Flow.Launcher.ViewModel
#pragma warning restore VSTHRD100 // Avoid async void methods
/// <summary>
/// Checks if Flow Launcher should ignore any hotkeys
/// Save history, user selected records and top most records
/// </summary>
public bool ShouldIgnoreHotkeys()
{
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
}
#endregion
#region Public Methods
public void Save()
{
_historyItemsStorage.Save();
@ -1570,5 +1644,35 @@ namespace Flow.Launcher.ViewModel
}
#endregion
#region IDisposable
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_updateSource?.Dispose();
_resultsUpdateChannelWriter?.Complete();
if (_resultsViewUpdateTask?.IsCompleted == true)
{
_resultsViewUpdateTask.Dispose();
}
_disposed = true;
}
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
}

View file

@ -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,25 +9,20 @@ 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<string, string> fonts = new();
private static readonly PrivateFontCollection FontCollection = new();
private static readonly Dictionary<string, string> Fonts = new();
public ResultViewModel(Result result, Settings settings)
{
Settings = settings;
if (result == null)
{
return;
}
if (result == null) return;
Result = result;
if (Result.Glyph is { FontFamily: not null } glyph)
@ -39,20 +37,20 @@ 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
{
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]
};
}
}
@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel
Glyph = glyph;
}
}
}
public Settings Settings { get; }
@ -95,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;
}
}
@ -111,9 +104,8 @@ namespace Flow.Launcher.ViewModel
get
{
if (Result.RoundedIcon)
{
return IconXY / 2;
}
return IconXY;
}
@ -148,31 +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 => previewImage;
private set => previewImage = value;
get
{
if (!_previewImageLoaded)
{
_previewImageLoaded = true;
_ = LoadPreviewImageAsync();
}
return _previewImage;
}
private set => _previewImage = value;
}
/// <summary>
@ -188,8 +189,7 @@ namespace Flow.Launcher.ViewModel
{
try
{
var image = icon();
return image;
return icon();
}
catch (Exception e)
{
@ -208,7 +208,7 @@ namespace Flow.Launcher.ViewModel
var iconDelegate = Result.Icon;
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
{
image = img;
_image = img;
}
else
{
@ -223,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
{
@ -234,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();
}
}

View file

@ -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
{
@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel
Results = new ResultCollection();
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
}
public ResultsViewModel(Settings settings) : this()
{
_settings = settings;
@ -219,7 +220,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 +241,7 @@ namespace Flow.Launcher.ViewModel
#endregion
#region FormattedText Dependency Property
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText",
typeof(Inline),
@ -259,8 +260,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 +269,7 @@ namespace Flow.Launcher.ViewModel
textBlock.Inlines.Add(inline);
}
#endregion
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
@ -279,7 +280,6 @@ namespace Flow.Launcher.ViewModel
public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
@ -297,6 +297,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<ResultViewModel> Items)
{
for (int i = 0; i < Items.Count; i++)
@ -308,6 +309,7 @@ namespace Flow.Launcher.ViewModel
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
}
}
public void RemoveAll(int Capacity = 512)
{
Clear();

View file

@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
## 📦 Plugins
- Support wide range of plugins. Visit [here](https://flowlauncher.com/docs/#/plugins) for our plugin portfolio.
- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio.
- Publish your own plugin to flow! Create plugins in:
<p align="center">