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()) if (failedPlugins.Any())
{ {
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name)); var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));

View file

@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource
return DefaultLanguageCode; 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 location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
var dir = Path.GetDirectoryName(location); var dir = Path.GetDirectoryName(location);
@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource
_oldResources.Clear(); _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) public void ChangeLanguage(string languageCode)
{ {
languageCode = languageCode.NonNull(); languageCode = languageCode.NonNull();
@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource
// Get language by language code and change language // Get language by language code and change language
var language = GetLanguageByLanguageCode(languageCode); 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 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(); RemoveOldLanguageFiles();
if (language != AvailableLanguages.English) if (language != AvailableLanguages.English)
{ {
LoadLanguage(language); LoadLanguage(language);
} }
// Culture of main thread // 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 // 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.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture; CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
// Raise event after culture is set // Raise event for plugins after culture is set
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; await Task.Run(UpdatePluginMetadataTranslations);
_ = Task.Run(() =>
{
UpdatePluginMetadataTranslations();
});
} }
public bool PromptShouldUsePinyin(string languageCodeToSet) public bool PromptShouldUsePinyin(string languageCodeToSet)

View file

@ -6,6 +6,7 @@ using System.Xml;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup; using System.Windows.Markup;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Effects; using System.Windows.Media.Effects;
@ -16,7 +17,6 @@ using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using Microsoft.Win32; using Microsoft.Win32;
using TextBox = System.Windows.Controls.TextBox;
namespace Flow.Launcher.Core.Resource namespace Flow.Launcher.Core.Resource
{ {
@ -56,20 +56,23 @@ namespace Flow.Launcher.Core.Resource
MakeSureThemeDirectoriesExist(); MakeSureThemeDirectoriesExist();
var dicts = Application.Current.Resources.MergedDictionaries; var dicts = Application.Current.Resources.MergedDictionaries;
_oldResource = dicts.First(d => _oldResource = dicts.FirstOrDefault(d =>
{ {
if (d.Source == null) if (d.Source == null) return false;
return false;
var p = d.Source.AbsolutePath; var p = d.Source.AbsolutePath;
var dir = Path.GetDirectoryName(p).NonNull(); return p.Contains(Folder) && Path.GetExtension(p) == Extension;
var info = new DirectoryInfo(dir);
var f = info.Name;
var e = Path.GetExtension(p);
var found = f == Folder && e == Extension;
return found;
}); });
_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 #endregion
@ -98,13 +101,152 @@ namespace Flow.Launcher.Core.Resource
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate) 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; _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) private ResourceDictionary GetThemeResourceDictionary(string theme)
{ {
var uri = GetThemePath(theme); var uri = GetThemePath(theme);
@ -128,22 +270,22 @@ namespace Flow.Launcher.Core.Resource
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight); var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch); var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush"); var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground") var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
.Select(x => x.Value).FirstOrDefault(); .Select(x => x.Value).FirstOrDefault();
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling 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 // Query suggestion box's font style is aligned with query box
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily)); querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle)); querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight)); querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch)); querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
} }
if (dict["ItemTitleStyle"] is Style resultItemStyle && if (dict["ItemTitleStyle"] is Style resultItemStyle &&
@ -180,7 +322,7 @@ namespace Flow.Launcher.Core.Resource
/* Ignore Theme Window Width and use setting */ /* Ignore Theme Window Width and use setting */
var windowStyle = dict["WindowStyle"] as Style; var windowStyle = dict["WindowStyle"] as Style;
var width = _settings.WindowSize; var width = _settings.WindowSize;
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width)); windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
return dict; return dict;
} }
@ -265,11 +407,12 @@ namespace Flow.Launcher.Core.Resource
try try
{ {
if (string.IsNullOrEmpty(path)) 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 // Retrieve theme resource always use the resource with font settings applied.
// to things like fonts var resourceDict = GetResourceDictionary(theme);
UpdateResourceDictionary(GetResourceDictionary(theme));
UpdateResourceDictionary(resourceDict);
_settings.Theme = theme; _settings.Theme = theme;
@ -280,10 +423,11 @@ namespace Flow.Launcher.Core.Resource
} }
BlurEnabled = IsBlurTheme(); BlurEnabled = IsBlurTheme();
//if (_settings.UseDropShadowEffect)
// AddDropShadowEffectToCurrentTheme(); // Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
//Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled); _ = RefreshFrameAsync();
_ = SetBlurForWindowAsync();
return true;
} }
catch (DirectoryNotFoundException) catch (DirectoryNotFoundException)
{ {
@ -305,7 +449,6 @@ namespace Flow.Launcher.Core.Resource
} }
return false; return false;
} }
return true;
} }
#endregion #endregion
@ -481,17 +624,14 @@ namespace Flow.Launcher.Core.Resource
private void SetBlurForWindow(string theme, BackdropTypes backdropType) private void SetBlurForWindow(string theme, BackdropTypes backdropType)
{ {
var dict = GetThemeResourceDictionary(theme); var dict = GetResourceDictionary(theme);
if (dict == null) if (dict == null) return;
return;
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null; var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
if (windowBorderStyle == null) if (windowBorderStyle == null) return;
return;
Window mainWindow = Application.Current.MainWindow; var mainWindow = Application.Current.MainWindow;
if (mainWindow == null) if (mainWindow == null) return;
return;
// Check if the theme supports blur // Check if the theme supports blur
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b; 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 MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.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 ImageIcon = Path.Combine(ImagesDirectory, "image.png");
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
public static string PythonPath; public static string PythonPath;
public static string NodePath; public static string NodePath;

View file

@ -47,3 +47,15 @@ MONITORINFOEXW
WM_ENTERSIZEMOVE 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;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using Flow.Launcher.Infrastructure.UserSettings;
using Microsoft.Win32;
using Windows.Win32; using Windows.Win32;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm; using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
using Flow.Launcher.Infrastructure.UserSettings; using Point = System.Windows.Point;
namespace Flow.Launcher.Infrastructure namespace Flow.Launcher.Infrastructure
{ {
@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure
} }
#endregion #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 public partial class App : IDisposable, ISingleInstanceApp
{ {
#region Public Properties
public static IPublicAPI API { get; private set; } 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 static bool _disposed;
private MainWindow _mainWindow;
private readonly MainViewModel _mainVM;
private readonly Settings _settings; private readonly Settings _settings;
// To prevent two disposals running at the same time.
private static readonly object _disposingLock = new();
#endregion
#region Constructor
public App() public App()
{ {
// Initialize settings // Initialize settings
@ -79,27 +94,33 @@ namespace Flow.Launcher
{ {
API = Ioc.Default.GetRequiredService<IPublicAPI>(); API = Ioc.Default.GetRequiredService<IPublicAPI>();
_settings.Initialize(); _settings.Initialize();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
} }
catch (Exception e) catch (Exception e)
{ {
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e); ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
return; 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) #endregion
{
// 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. #region Main
Environment.FailFast(message, e);
}
[STAThread] [STAThread]
public static void Main() public static void Main()
{ {
if (SingleInstance<App>.InitializeAsFirstInstance(Unique)) if (SingleInstance<App>.InitializeAsFirstInstance())
{ {
using var application = new App(); using var application = new App();
application.InitializeComponent(); application.InitializeComponent();
@ -107,6 +128,10 @@ namespace Flow.Launcher
} }
} }
#endregion
#region App Events
#pragma warning disable VSTHRD100 // Avoid async void methods #pragma warning disable VSTHRD100 // Avoid async void methods
private async void OnStartup(object sender, StartupEventArgs e) private async void OnStartup(object sender, StartupEventArgs e)
@ -127,21 +152,26 @@ namespace Flow.Launcher
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings); AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
PluginManager.LoadPlugins(_settings.PluginSettings); PluginManager.LoadPlugins(_settings.PluginSettings);
// Register ResultsUpdated event after all plugins are loaded
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
Http.Proxy = _settings.Proxy; Http.Proxy = _settings.Proxy;
await PluginManager.InitializePluginsAsync(); 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; await imageLoadertask;
var window = new MainWindow(); _mainWindow = new MainWindow();
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
Current.MainWindow = window; Current.MainWindow = _mainWindow;
Current.MainWindow.Title = Constant.FlowLauncher; Current.MainWindow.Title = Constant.FlowLauncher;
HotKeyMapper.Initialize(); HotKeyMapper.Initialize();
@ -158,8 +188,7 @@ namespace Flow.Launcher
AutoUpdates(); AutoUpdates();
API.SaveAppAllSettings(); API.SaveAppAllSettings();
Log.Info( Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
}); });
} }
@ -192,7 +221,6 @@ namespace Flow.Launcher
} }
} }
//[Conditional("RELEASE")]
private void AutoUpdates() private void AutoUpdates()
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
@ -210,11 +238,29 @@ namespace Flow.Launcher
}); });
} }
#endregion
#region Register Events
private void RegisterExitEvents() private void RegisterExitEvents()
{ {
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose(); AppDomain.CurrentDomain.ProcessExit += (s, e) =>
Current.Exit += (s, e) => Dispose(); {
Current.SessionEnding += (s, e) => Dispose(); 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> /// <summary>
@ -235,20 +281,60 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle; 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 // Prevent two disposes at the same time.
// but if sessionending is not called, exit won't be called when log off / shutdown lock (_disposingLock)
if (!_disposed)
{ {
API.SaveAppAllSettings(); if (!disposing)
{
return;
}
if (_disposed)
{
return;
}
_disposed = true; _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() public void OnSecondAppStarted()
{ {
Ioc.Default.GetRequiredService<MainViewModel>().Show(); Ioc.Default.GetRequiredService<MainViewModel>().Show();
} }
#endregion
} }
} }

View file

@ -10,7 +10,7 @@ namespace Flow.Launcher.Helper
{ {
public interface ISingleInstanceApp public interface ISingleInstanceApp
{ {
void OnSecondAppStarted(); void OnSecondAppStarted();
} }
/// <summary> /// <summary>
@ -24,9 +24,7 @@ namespace Flow.Launcher.Helper
/// running as Administrator, can activate it with command line arguments. /// running as Administrator, can activate it with command line arguments.
/// For most apps, this will not be much of an issue. /// For most apps, this will not be much of an issue.
/// </remarks> /// </remarks>
public static class SingleInstance<TApplication> public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
where TApplication: Application , ISingleInstanceApp
{ {
#region Private Fields #region Private Fields
@ -39,11 +37,12 @@ namespace Flow.Launcher.Helper
/// Suffix to the channel name. /// Suffix to the channel name.
/// </summary> /// </summary>
private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
/// <summary> /// <summary>
/// Application mutex. /// Application mutex.
/// </summary> /// </summary>
internal static Mutex singleInstanceMutex; internal static Mutex SingleInstanceMutex { get; set; }
#endregion #endregion
@ -54,24 +53,23 @@ namespace Flow.Launcher.Helper
/// If not, activates the first instance. /// If not, activates the first instance.
/// </summary> /// </summary>
/// <returns>True if this is the first instance of the application.</returns> /// <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. // 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. // 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 var firstInstance);
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
if (firstInstance) if (firstInstance)
{ {
_ = CreateRemoteService(channelName); _ = CreateRemoteServiceAsync(channelName);
return true; return true;
} }
else else
{ {
_ = SignalFirstInstance(channelName); _ = SignalFirstInstanceAsync(channelName);
return false; return false;
} }
} }
@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper
/// </summary> /// </summary>
public static void Cleanup() public static void Cleanup()
{ {
singleInstanceMutex?.ReleaseMutex(); SingleInstanceMutex?.ReleaseMutex();
} }
#endregion #endregion
@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper
/// Once receives signal from client, will activate first instance. /// Once receives signal from client, will activate first instance.
/// </summary> /// </summary>
/// <param name="channelName">Application's IPC channel name.</param> /// <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();
// Wait for connection to the pipe
await pipeServer.WaitForConnectionAsync(); // Do an asynchronous call to ActivateFirstInstance function
if (Application.Current != null) Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
{
// Do an asynchronous call to ActivateFirstInstance function // Disconect client
Application.Current.Dispatcher.Invoke(ActivateFirstInstance); pipeServer.Disconnect();
}
// Disconect client
pipeServer.Disconnect();
}
} }
} }
@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper
/// <param name="args"> /// <param name="args">
/// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// Command line arguments for the second instance, passed to the first instance to take appropriate action.
/// </param> /// </param>
private static async Task SignalFirstInstance(string channelName) private static async Task SignalFirstInstanceAsync(string channelName)
{ {
// Create a client pipe connected to server // Create a client pipe connected to server
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
{
// Connect to the available pipe
await pipeClient.ConnectAsync(0);
}
}
/// <summary> // Connect to the available pipe
/// Callback for activating first instance of the application. await pipeClient.ConnectAsync(0);
/// </summary>
/// <param name="arg">Callback argument.</param>
/// <returns>Always null.</returns>
private static object ActivateFirstInstanceCallback(object o)
{
ActivateFirstInstance();
return null;
} }
/// <summary> /// <summary>

View file

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

View file

@ -30,7 +30,7 @@ using Screen = System.Windows.Forms.Screen;
namespace Flow.Launcher namespace Flow.Launcher
{ {
public partial class MainWindow public partial class MainWindow : IDisposable
{ {
#region Private Fields #region Private Fields
@ -42,17 +42,20 @@ namespace Flow.Launcher
private NotifyIcon _notifyIcon; private NotifyIcon _notifyIcon;
// Window Context Menu // Window Context Menu
private readonly ContextMenu contextMenu = new(); private readonly ContextMenu _contextMenu = new();
private readonly MainViewModel _viewModel; private readonly MainViewModel _viewModel;
// Window Event : Key Event // Window Event: Close Event
private bool isArrowKeyPressed = false; private bool _canClose = false;
// Window Event: Key Event
private bool _isArrowKeyPressed = false;
// Window Sound Effects // Window Sound Effects
private MediaPlayer animationSoundWMP; private MediaPlayer animationSoundWMP;
private SoundPlayer animationSoundWPF; private SoundPlayer animationSoundWPF;
// Window WndProc // Window WndProc
private HwndSource _hwndSource;
private int _initialWidth; private int _initialWidth;
private int _initialHeight; private int _initialHeight;
@ -64,6 +67,9 @@ namespace Flow.Launcher
// Search Delay // Search Delay
private IDisposable _reactiveSubscription; private IDisposable _reactiveSubscription;
// IDisposable
private bool _disposed = false;
#endregion #endregion
#region Constructor #region Constructor
@ -91,8 +97,8 @@ namespace Flow.Launcher
private void OnSourceInitialized(object sender, EventArgs e) private void OnSourceInitialized(object sender, EventArgs e)
{ {
var handle = Win32Helper.GetWindowHandle(this, true); var handle = Win32Helper.GetWindowHandle(this, true);
var win = HwndSource.FromHwnd(handle); _hwndSource = HwndSource.FromHwnd(handle);
win.AddHook(WndProc); _hwndSource.AddHook(WndProc);
Win32Helper.HideFromAltTab(this); Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this); Win32Helper.DisableControlBox(this);
} }
@ -107,6 +113,9 @@ namespace Flow.Launcher
{ {
_settings.FirstLaunch = false; _settings.FirstLaunch = false;
App.API.SaveAppAllSettings(); 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(); var WelcomeWindow = new WelcomeWindow();
WelcomeWindow.Show(); WelcomeWindow.Show();
} }
@ -150,6 +159,10 @@ namespace Flow.Launcher
// Since the default main window visibility is visible, so we need set focus during startup // Since the default main window visibility is visible, so we need set focus during startup
QueryTextBox.Focus(); 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 // View model property changed event
_viewModel.PropertyChanged += (o, e) => _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(); QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility();
// ✅ ContextMenu.Visibility 변경 감지 // Detecting ContextMenu.Visibility changes
DependencyPropertyDescriptor DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(ContextMenu)) .FromProperty(VisibilityProperty, typeof(ContextMenu))
.AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility()); .AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility());
// ✅ History.Visibility 변경 감지 // Detect History.Visibility changes
DependencyPropertyDescriptor DependencyPropertyDescriptor
.FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정 .FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility()); .AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
@ -243,18 +256,37 @@ namespace Flow.Launcher
private async void OnClosing(object sender, CancelEventArgs e) private async void OnClosing(object sender, CancelEventArgs e)
{ {
_notifyIcon.Visible = false; if (!_canClose)
App.API.SaveAppAllSettings(); {
e.Cancel = true; _notifyIcon.Visible = false;
await PluginManager.DisposePluginsAsync(); App.API.SaveAppAllSettings();
Notification.Uninstall(); e.Cancel = true;
Environment.Exit(0); 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) private void OnLocationChanged(object sender, EventArgs e)
{ {
if (_animating) if (_animating) return;
return;
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation) if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
{ {
_settings.WindowLeft = Left; _settings.WindowLeft = Left;
@ -292,12 +324,12 @@ namespace Flow.Launcher
switch (e.Key) switch (e.Key)
{ {
case Key.Down: case Key.Down:
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_viewModel.SelectNextItemCommand.Execute(null); _viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
case Key.Up: case Key.Up:
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_viewModel.SelectPrevItemCommand.Execute(null); _viewModel.SelectPrevItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
@ -310,7 +342,7 @@ namespace Flow.Launcher
e.Handled = true; e.Handled = true;
break; break;
case Key.Right: case Key.Right:
if (_viewModel.SelectedIsFromQueryResults() if (_viewModel.QueryResultsSelected()
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length && QueryTextBox.CaretIndex == QueryTextBox.Text.Length
&& !string.IsNullOrEmpty(QueryTextBox.Text)) && !string.IsNullOrEmpty(QueryTextBox.Text))
{ {
@ -320,7 +352,7 @@ namespace Flow.Launcher
break; break;
case Key.Left: case Key.Left:
if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0) if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
{ {
_viewModel.EscCommand.Execute(null); _viewModel.EscCommand.Execute(null);
e.Handled = true; e.Handled = true;
@ -330,7 +362,7 @@ namespace Flow.Launcher
case Key.Back: case Key.Back:
if (specialKeyState.CtrlPressed) if (specialKeyState.CtrlPressed)
{ {
if (_viewModel.SelectedIsFromQueryResults() if (_viewModel.QueryResultsSelected()
&& QueryTextBox.Text.Length > 0 && QueryTextBox.Text.Length > 0
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length) && QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
{ {
@ -355,13 +387,13 @@ namespace Flow.Launcher
{ {
if (e.Key == Key.Up || e.Key == Key.Down) if (e.Key == Key.Up || e.Key == Key.Down)
{ {
isArrowKeyPressed = false; _isArrowKeyPressed = false;
} }
} }
private void OnPreviewMouseMove(object sender, MouseEventArgs e) private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{ {
if (isArrowKeyPressed) if (_isArrowKeyPressed)
{ {
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
} }
@ -531,11 +563,11 @@ namespace Flow.Launcher
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip"); gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip"); positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
contextMenu.Items.Add(open); _contextMenu.Items.Add(open);
contextMenu.Items.Add(gamemode); _contextMenu.Items.Add(gamemode);
contextMenu.Items.Add(positionreset); _contextMenu.Items.Add(positionreset);
contextMenu.Items.Add(settings); _contextMenu.Items.Add(settings);
contextMenu.Items.Add(exit); _contextMenu.Items.Add(exit);
_notifyIcon.MouseClick += (o, e) => _notifyIcon.MouseClick += (o, e) =>
{ {
@ -546,14 +578,14 @@ namespace Flow.Launcher
break; break;
case MouseButtons.Right: case MouseButtons.Right:
contextMenu.IsOpen = true; _contextMenu.IsOpen = true;
// Get context menu handle and bring it to the foreground // 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); Win32Helper.SetForegroundWindow(hwndSource.Handle);
} }
contextMenu.Focus(); _contextMenu.Focus();
break; break;
} }
}; };
@ -561,7 +593,7 @@ namespace Flow.Launcher
private void UpdateNotifyIconText() private void UpdateNotifyIconText()
{ {
var menu = contextMenu; var menu = _contextMenu;
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") + ((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
" (" + _settings.Hotkey + ")"; " (" + _settings.Hotkey + ")";
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode"); ((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
@ -757,7 +789,7 @@ namespace Flow.Launcher
if (_animating) if (_animating)
return; return;
isArrowKeyPressed = true; _isArrowKeyPressed = true;
_animating = true; _animating = true;
UpdatePosition(false); UpdatePosition(false);
@ -835,7 +867,7 @@ namespace Flow.Launcher
clocksb.Completed += (_, _) => _animating = false; clocksb.Completed += (_, _) => _animating = false;
_settings.WindowLeft = Left; _settings.WindowLeft = Left;
isArrowKeyPressed = false; _isArrowKeyPressed = false;
if (QueryTextBox.Text.Length == 0) if (QueryTextBox.Text.Length == 0)
{ {
@ -1048,5 +1080,31 @@ namespace Flow.Launcher
} }
#endregion #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; 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(); _ = _theme.SetBlurForWindowAsync();
OnPropertyChanged(nameof(IsDropShadowEnabled)); OnPropertyChanged(nameof(IsDropShadowEnabled));
@ -342,7 +344,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set set
{ {
Settings.QueryBoxFont = value.ToString(); Settings.QueryBoxFont = value.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }
@ -364,7 +366,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.QueryBoxFontStretch = value.Stretch.ToString(); Settings.QueryBoxFontStretch = value.Stretch.ToString();
Settings.QueryBoxFontWeight = value.Weight.ToString(); Settings.QueryBoxFontWeight = value.Weight.ToString();
Settings.QueryBoxFontStyle = value.Style.ToString(); Settings.QueryBoxFontStyle = value.Style.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }
@ -386,7 +388,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set set
{ {
Settings.ResultFont = value.ToString(); Settings.ResultFont = value.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }
@ -408,7 +410,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.ResultFontStretch = value.Stretch.ToString(); Settings.ResultFontStretch = value.Stretch.ToString();
Settings.ResultFontWeight = value.Weight.ToString(); Settings.ResultFontWeight = value.Weight.ToString();
Settings.ResultFontStyle = value.Style.ToString(); Settings.ResultFontStyle = value.Style.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }
@ -432,7 +434,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
set set
{ {
Settings.ResultSubFont = value.ToString(); Settings.ResultSubFont = value.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }
@ -453,7 +455,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
Settings.ResultSubFontStretch = value.Stretch.ToString(); Settings.ResultSubFontStretch = value.Stretch.ToString();
Settings.ResultSubFontWeight = value.Weight.ToString(); Settings.ResultSubFontWeight = value.Weight.ToString();
Settings.ResultSubFontStyle = value.Style.ToString(); Settings.ResultSubFontStyle = value.Style.ToString();
_theme.ChangeTheme(); _theme.UpdateFonts();
} }
} }

View file

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

View file

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

View file

@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public partial class MainViewModel : BaseModel, ISavable public partial class MainViewModel : BaseModel, ISavable, IDisposable
{ {
#region Private Fields #region Private Fields
@ -49,6 +49,8 @@ namespace Flow.Launcher.ViewModel
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter; private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
private Task _resultsViewUpdateTask; private Task _resultsViewUpdateTask;
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
#endregion #endregion
#region Constructor #region Constructor
@ -162,13 +164,26 @@ namespace Flow.Launcher.ViewModel
switch (args.PropertyName) switch (args.PropertyName)
{ {
case nameof(Results.SelectedItem): 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; break;
} }
}; };
RegisterViewUpdate(); RegisterViewUpdate();
RegisterResultsUpdatedEvent();
_ = RegisterClockAndDateUpdateAsync(); _ = RegisterClockAndDateUpdateAsync();
} }
@ -213,7 +228,7 @@ namespace Flow.Launcher.ViewModel
} }
} }
private void RegisterResultsUpdatedEvent() public void RegisterResultsUpdatedEvent()
{ {
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>()) foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
{ {
@ -266,7 +281,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void LoadHistory() private void LoadHistory()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
SelectedResults = History; SelectedResults = History;
History.SelectedIndex = _history.Items.Count - 1; History.SelectedIndex = _history.Items.Count - 1;
@ -280,7 +295,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
public void ReQuery() public void ReQuery()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
_ = QueryResultsAsync(false, isReQuery: true); _ = QueryResultsAsync(false, isReQuery: true);
} }
@ -321,7 +336,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void LoadContextMenu() private void LoadContextMenu()
{ {
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing // 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 // i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
@ -351,7 +366,7 @@ namespace Flow.Launcher.ViewModel
private void AutocompleteQuery() private void AutocompleteQuery()
{ {
var result = SelectedResults.SelectedItem?.Result; 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; var autoCompleteText = result.Title;
@ -403,7 +418,7 @@ namespace Flow.Launcher.ViewModel
}) })
.ConfigureAwait(false); .ConfigureAwait(false);
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
_userSelectedRecord.Add(result); _userSelectedRecord.Add(result);
// origin query is null when user select the context menu item directly of one item from query list // 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 if (_history.Items.Count > 0
&& QueryText == string.Empty && QueryText == string.Empty
&& SelectedIsFromQueryResults()) && QueryResultsSelected())
{ {
lastHistoryIndex = 1; lastHistoryIndex = 1;
ReverseHistory(); ReverseHistory();
@ -502,7 +517,7 @@ namespace Flow.Launcher.ViewModel
[RelayCommand] [RelayCommand]
private void Esc() private void Esc()
{ {
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -514,7 +529,7 @@ namespace Flow.Launcher.ViewModel
public void BackToQueryResults() public void BackToQueryResults()
{ {
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -645,13 +660,16 @@ namespace Flow.Launcher.ViewModel
private ResultsViewModel SelectedResults private ResultsViewModel SelectedResults
{ {
get { return _selectedResults; } get => _selectedResults;
set set
{ {
var isReturningFromQueryResults = QueryResultsSelected();
var isReturningFromContextMenu = ContextMenuSelected(); var isReturningFromContextMenu = ContextMenuSelected();
var isReturningFromHistory = HistorySelected();
_selectedResults = value; _selectedResults = value;
if (SelectedIsFromQueryResults()) if (QueryResultsSelected())
{ {
Results.Visibility = Visibility.Visible;
ContextMenu.Visibility = Visibility.Collapsed; ContextMenu.Visibility = Visibility.Collapsed;
History.Visibility = Visibility.Collapsed; History.Visibility = Visibility.Collapsed;
@ -669,10 +687,27 @@ namespace Flow.Launcher.ViewModel
{ {
ChangeQueryText(_queryTextBeforeLeaveResults); 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 else
{ {
Results.Visibility = Visibility.Collapsed; 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; _queryTextBeforeLeaveResults = QueryText;
// Because of Fody's optimization // Because of Fody's optimization
@ -681,6 +716,16 @@ namespace Flow.Launcher.ViewModel
// http://stackoverflow.com/posts/25895769/revisions // http://stackoverflow.com/posts/25895769/revisions
QueryText = string.Empty; QueryText = string.Empty;
Query(false); 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; _selectedResults.Visibility = Visibility.Visible;
@ -780,6 +825,22 @@ namespace Flow.Launcher.ViewModel
#region Preview #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 public bool InternalPreviewVisible
{ {
get 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; public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
// This is not a reliable indicator of whether external preview is visible due to the // 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 // ability of manually closing/exiting the external preview program which, does not inform flow that
// preview is no longer available. // 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(); var useExternalPreview = PluginManager.UseExternalPreview();
@ -820,13 +877,15 @@ namespace Flow.Launcher.ViewModel
// Internal preview may still be on when user switches to external // Internal preview may still be on when user switches to external
if (InternalPreviewVisible) if (InternalPreviewVisible)
HideInternalPreview(); HideInternalPreview();
OpenExternalPreview(path);
_ = OpenExternalPreviewAsync(path);
break; break;
case true case true
when !CanExternalPreviewSelectedResult(out var _): when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
CloseExternalPreview(); await CloseExternalPreviewAsync();
ShowInternalPreview(); ShowInternalPreview();
break; break;
@ -839,7 +898,7 @@ namespace Flow.Launcher.ViewModel
private void HidePreview() private void HidePreview()
{ {
if (PluginManager.UseExternalPreview()) if (PluginManager.UseExternalPreview())
CloseExternalPreview(); _ = CloseExternalPreviewAsync();
if (InternalPreviewVisible) if (InternalPreviewVisible)
HideInternalPreview(); HideInternalPreview();
@ -854,31 +913,31 @@ namespace Flow.Launcher.ViewModel
} }
else 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; ExternalPreviewVisible = true;
} }
private void CloseExternalPreview() private async Task CloseExternalPreviewAsync()
{ {
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false); await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
ExternalPreviewVisible = 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() private void ShowInternalPreview()
{ {
ResultAreaColumn = ResultAreaColumnPreviewShown; ResultAreaColumn = ResultAreaColumnPreviewShown;
Results.SelectedItem?.LoadPreviewImage(); PreviewSelectedItem?.LoadPreviewImage();
} }
private void HideInternalPreview() private void HideInternalPreview()
@ -892,20 +951,18 @@ namespace Flow.Launcher.ViewModel
{ {
case true case true
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path): when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
OpenExternalPreview(path); _ = OpenExternalPreviewAsync(path);
break; break;
case true: case true:
ShowInternalPreview(); ShowInternalPreview();
break; break;
case false: case false:
HidePreview(); HidePreview();
break; break;
} }
} }
private void UpdatePreview() private async Task UpdatePreviewAsync()
{ {
switch (PluginManager.UseExternalPreview()) switch (PluginManager.UseExternalPreview())
{ {
@ -913,44 +970,48 @@ namespace Flow.Launcher.ViewModel
when CanExternalPreviewSelectedResult(out var path): when CanExternalPreviewSelectedResult(out var path):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
SwitchExternalPreview(path, false); _ = SwitchExternalPreviewAsync(path, false);
} }
else if (InternalPreviewVisible) else if (InternalPreviewVisible)
{ {
HideInternalPreview(); HideInternalPreview();
OpenExternalPreview(path); _ = OpenExternalPreviewAsync(path);
} }
break; break;
case true case true
when !CanExternalPreviewSelectedResult(out var _): when !CanExternalPreviewSelectedResult(out var _):
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
CloseExternalPreview(); await CloseExternalPreviewAsync();
ShowInternalPreview(); ShowInternalPreview();
} }
break; break;
case false case false
when InternalPreviewVisible: when InternalPreviewVisible:
Results.SelectedItem?.LoadPreviewImage(); PreviewSelectedItem?.LoadPreviewImage();
break; break;
} }
} }
private bool CanExternalPreviewSelectedResult(out string path) 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); return !string.IsNullOrEmpty(path);
} }
private bool QueryResultsPreviewed()
{
var previewed = PreviewSelectedItem == Results.SelectedItem;
return previewed;
}
#endregion #endregion
#region Query #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); _ = QueryResultsAsync(searchDelay, isReQuery);
} }
@ -1022,6 +1083,11 @@ namespace Flow.Launcher.ViewModel
Title = string.Format(title, h.Query), Title = string.Format(title, h.Query),
SubTitle = string.Format(time, h.ExecutedDateTime), SubTitle = string.Format(time, h.ExecutedDateTime),
IcoPath = "Images\\history.png", IcoPath = "Images\\history.png",
Preview = new Result.PreviewInfo
{
PreviewImagePath = Constant.HistoryIcon,
Description = string.Format(time, h.ExecutedDateTime)
},
OriginQuery = new Query { RawQuery = h.Query }, OriginQuery = new Query { RawQuery = h.Query },
Action = _ => 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) private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, bool reSelect = true)
{ {
// TODO: Remove debug codes. // TODO: Remove debug codes.
@ -1347,7 +1411,7 @@ namespace Flow.Launcher.ViewModel
return menu; return menu;
} }
internal bool SelectedIsFromQueryResults() internal bool QueryResultsSelected()
{ {
var selected = SelectedResults == Results; var selected = SelectedResults == Results;
return selected; 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() public void Show()
{ {
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
@ -1401,6 +1477,11 @@ namespace Flow.Launcher.ViewModel
MainWindowOpacity = 1; MainWindowOpacity = 1;
MainWindowVisibilityStatus = true; MainWindowVisibilityStatus = true;
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true }); VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
if (StartWithEnglishMode)
{
Win32Helper.SwitchToEnglishKeyboardLayout(true);
}
}); });
} }
@ -1412,10 +1493,10 @@ namespace Flow.Launcher.ViewModel
if (ExternalPreviewVisible) if (ExternalPreviewVisible)
{ {
CloseExternalPreview(); await CloseExternalPreviewAsync();
} }
if (!SelectedIsFromQueryResults()) if (!QueryResultsSelected())
{ {
SelectedResults = Results; SelectedResults = Results;
} }
@ -1425,10 +1506,7 @@ namespace Flow.Launcher.ViewModel
{ {
ChangeQueryText(string.Empty); ChangeQueryText(string.Empty);
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(Application.Current.MainWindow.UpdateLayout); // Force UI update
{
Application.Current.MainWindow.UpdateLayout(); // Force UI update
});
} }
switch (Settings.LastQueryMode) switch (Settings.LastQueryMode)
@ -1470,6 +1548,11 @@ namespace Flow.Launcher.ViewModel
Win32Helper.DWMSetCloakForWindow(mainWindow, true); Win32Helper.DWMSetCloakForWindow(mainWindow, true);
} }
if (StartWithEnglishMode)
{
Win32Helper.RestorePreviousKeyboardLayout();
}
await Task.Delay(50); await Task.Delay(50);
// Update WPF properties // Update WPF properties
@ -1482,17 +1565,8 @@ namespace Flow.Launcher.ViewModel
#pragma warning restore VSTHRD100 // Avoid async void methods #pragma warning restore VSTHRD100 // Avoid async void methods
/// <summary> /// <summary>
/// Checks if Flow Launcher should ignore any hotkeys /// Save history, user selected records and top most records
/// </summary> /// </summary>
public bool ShouldIgnoreHotkeys()
{
return Settings.IgnoreHotkeysOnFullscreen && Win32Helper.IsForegroundWindowFullscreen() || GameModeStatus;
}
#endregion
#region Public Methods
public void Save() public void Save()
{ {
_historyItemsStorage.Save(); _historyItemsStorage.Save();
@ -1570,5 +1644,35 @@ namespace Flow.Launcher.ViewModel
} }
#endregion #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;
using System.Collections.Generic;
using System.Drawing.Text;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
@ -6,25 +9,20 @@ using Flow.Launcher.Infrastructure.Image;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin; using Flow.Launcher.Plugin;
using System.IO;
using System.Drawing.Text;
using System.Collections.Generic;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
public class ResultViewModel : BaseModel public class ResultViewModel : BaseModel
{ {
private static PrivateFontCollection fontCollection = new(); private static readonly PrivateFontCollection FontCollection = new();
private static Dictionary<string, string> fonts = new(); private static readonly Dictionary<string, string> Fonts = new();
public ResultViewModel(Result result, Settings settings) public ResultViewModel(Result result, Settings settings)
{ {
Settings = settings; Settings = settings;
if (result == null) if (result == null) return;
{
return;
}
Result = result; Result = result;
if (Result.Glyph is { FontFamily: not null } glyph) if (Result.Glyph is { FontFamily: not null } glyph)
@ -39,20 +37,20 @@ namespace Flow.Launcher.ViewModel
fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath); fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath);
} }
if (fonts.ContainsKey(fontFamilyPath)) if (Fonts.TryGetValue(fontFamilyPath, out var value))
{ {
Glyph = glyph with Glyph = glyph with
{ {
FontFamily = fonts[fontFamilyPath] FontFamily = value
}; };
} }
else else
{ {
fontCollection.AddFontFile(fontFamilyPath); FontCollection.AddFontFile(fontFamilyPath);
fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}"; Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}";
Glyph = glyph with Glyph = glyph with
{ {
FontFamily = fonts[fontFamilyPath] FontFamily = Fonts[fontFamilyPath]
}; };
} }
} }
@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel
Glyph = glyph; Glyph = glyph;
} }
} }
} }
public Settings Settings { get; } public Settings Settings { get; }
@ -95,14 +92,10 @@ namespace Flow.Launcher.ViewModel
get get
{ {
if (PreviewImageAvailable) if (PreviewImageAvailable)
{
return Visibility.Visible; 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 get
{ {
if (Result.RoundedIcon) if (Result.RoundedIcon)
{
return IconXY / 2; return IconXY / 2;
}
return IconXY; return IconXY;
} }
@ -148,31 +140,40 @@ namespace Flow.Launcher.ViewModel
? Result.SubTitle ? Result.SubTitle
: Result.SubTitleToolTip; : Result.SubTitleToolTip;
private volatile bool ImageLoaded; private volatile bool _imageLoaded;
private volatile bool PreviewImageLoaded; private volatile bool _previewImageLoaded;
private ImageSource image = ImageLoader.LoadingImage; private ImageSource _image = ImageLoader.LoadingImage;
private ImageSource previewImage = ImageLoader.LoadingImage; private ImageSource _previewImage = ImageLoader.LoadingImage;
public ImageSource Image public ImageSource Image
{ {
get get
{ {
if (!ImageLoaded) if (!_imageLoaded)
{ {
ImageLoaded = true; _imageLoaded = true;
_ = LoadImageAsync(); _ = LoadImageAsync();
} }
return image; return _image;
} }
private set => image = value; private set => _image = value;
} }
public ImageSource PreviewImage public ImageSource PreviewImage
{ {
get => previewImage; get
private set => previewImage = value; {
if (!_previewImageLoaded)
{
_previewImageLoaded = true;
_ = LoadPreviewImageAsync();
}
return _previewImage;
}
private set => _previewImage = value;
} }
/// <summary> /// <summary>
@ -188,8 +189,7 @@ namespace Flow.Launcher.ViewModel
{ {
try try
{ {
var image = icon(); return icon();
return image;
} }
catch (Exception e) catch (Exception e)
{ {
@ -208,7 +208,7 @@ namespace Flow.Launcher.ViewModel
var iconDelegate = Result.Icon; var iconDelegate = Result.Icon;
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img)) if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
{ {
image = img; _image = img;
} }
else else
{ {
@ -223,7 +223,7 @@ namespace Flow.Launcher.ViewModel
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon; var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img)) if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
{ {
previewImage = img; _previewImage = img;
} }
else else
{ {
@ -234,13 +234,10 @@ namespace Flow.Launcher.ViewModel
public void LoadPreviewImage() 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 System;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
@ -10,6 +8,8 @@ using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Input; using System.Windows.Input;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
namespace Flow.Launcher.ViewModel namespace Flow.Launcher.ViewModel
{ {
@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel
Results = new ResultCollection(); Results = new ResultCollection();
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock); BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
} }
public ResultsViewModel(Settings settings) : this() public ResultsViewModel(Settings settings) : this()
{ {
_settings = settings; _settings = settings;
@ -219,7 +220,6 @@ namespace Flow.Launcher.ViewModel
if (newRawResults.Count == 0) if (newRawResults.Count == 0)
return Results; return Results;
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings)); var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
return Results.Where(r => r.Result.PluginID != resultId) return Results.Where(r => r.Result.PluginID != resultId)
@ -241,6 +241,7 @@ namespace Flow.Launcher.ViewModel
#endregion #endregion
#region FormattedText Dependency Property #region FormattedText Dependency Property
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached( public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
"FormattedText", "FormattedText",
typeof(Inline), typeof(Inline),
@ -259,8 +260,7 @@ namespace Flow.Launcher.ViewModel
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ {
var textBlock = d as TextBlock; if (d is not TextBlock textBlock) return;
if (textBlock == null) return;
var inline = (Inline)e.NewValue; var inline = (Inline)e.NewValue;
@ -269,6 +269,7 @@ namespace Flow.Launcher.ViewModel
textBlock.Inlines.Add(inline); textBlock.Inlines.Add(inline);
} }
#endregion #endregion
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
@ -279,7 +280,6 @@ namespace Flow.Launcher.ViewModel
public event NotifyCollectionChangedEventHandler CollectionChanged; public event NotifyCollectionChangedEventHandler CollectionChanged;
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e) protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{ {
CollectionChanged?.Invoke(this, 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 // wpf use DirectX / double buffered already, so just reset all won't cause ui flickering
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
} }
private void AddAll(List<ResultViewModel> Items) private void AddAll(List<ResultViewModel> Items)
{ {
for (int i = 0; i < Items.Count; i++) for (int i = 0; i < Items.Count; i++)
@ -308,6 +309,7 @@ namespace Flow.Launcher.ViewModel
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i)); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
} }
} }
public void RemoveAll(int Capacity = 512) public void RemoveAll(int Capacity = 512)
{ {
Clear(); Clear();

View file

@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
## 📦 Plugins ## 📦 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: - Publish your own plugin to flow! Create plugins in:
<p align="center"> <p align="center">