Merge branch 'dev' into 250410-PluginBadge2

This commit is contained in:
Jack Ye 2025-04-12 15:25:32 +08:00 committed by GitHub
commit 02b53732c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 655 additions and 62 deletions

View file

@ -37,7 +37,7 @@ namespace Flow.Launcher.Core.Plugin
private static PluginsSettings Settings;
private static List<PluginMetadata> _metadatas;
private static List<string> _modifiedPlugins = new();
private static readonly List<string> _modifiedPlugins = new();
/// <summary>
/// Directories that will hold Flow Launcher plugin directory
@ -61,10 +61,17 @@ namespace Flow.Launcher.Core.Plugin
/// </summary>
public static void Save()
{
foreach (var plugin in AllPlugins)
foreach (var pluginPair in AllPlugins)
{
var savable = plugin.Plugin as ISavable;
savable?.Save();
var savable = pluginPair.Plugin as ISavable;
try
{
savable?.Save();
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
}
}
API.SavePluginSettings();
@ -81,14 +88,21 @@ namespace Flow.Launcher.Core.Plugin
private static async Task DisposePluginAsync(PluginPair pluginPair)
{
switch (pluginPair.Plugin)
try
{
case IDisposable disposable:
disposable.Dispose();
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
switch (pluginPair.Plugin)
{
case IDisposable disposable:
disposable.Dispose();
break;
case IAsyncDisposable asyncDisposable:
await asyncDisposable.DisposeAsync();
break;
}
}
catch (Exception e)
{
API.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
}
}
@ -292,7 +306,7 @@ namespace Flow.Launcher.Core.Plugin
{
Title = $"{metadata.Name}: Failed to respond!",
SubTitle = "Select this result for more info",
IcoPath = Flow.Launcher.Infrastructure.Constant.ErrorIcon,
IcoPath = Constant.ErrorIcon,
PluginDirectory = metadata.PluginDirectory,
ActionKeywordAssigned = query.ActionKeyword,
PluginID = metadata.ID,
@ -369,8 +383,8 @@ namespace Flow.Launcher.Core.Plugin
{
// this method is only checking for action keywords (defined as not '*') registration
// hence the actionKeyword != Query.GlobalPluginWildcardSign logic
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
return actionKeyword != Query.GlobalPluginWildcardSign
&& NonGlobalPlugins.ContainsKey(actionKeyword);
}
/// <summary>

View file

@ -22,8 +22,8 @@ namespace Flow.Launcher.Core.Resource
private const string DefaultFile = "en.xaml";
private const string Extension = ".xaml";
private readonly Settings _settings;
private readonly List<string> _languageDirectories = new List<string>();
private readonly List<ResourceDictionary> _oldResources = new List<ResourceDictionary>();
private readonly List<string> _languageDirectories = new();
private readonly List<ResourceDictionary> _oldResources = new();
private readonly string SystemLanguageCode;
public Internationalization(Settings settings)
@ -144,7 +144,7 @@ namespace Flow.Launcher.Core.Resource
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
}
private Language GetLanguageByLanguageCode(string languageCode)
private static Language GetLanguageByLanguageCode(string languageCode)
{
var lowercase = languageCode.ToLower();
var language = AvailableLanguages.GetAvailableLanguages().FirstOrDefault(o => o.LanguageCode.ToLower() == lowercase);
@ -239,7 +239,7 @@ namespace Flow.Launcher.Core.Resource
return list;
}
public string GetTranslation(string key)
public static string GetTranslation(string key)
{
var translation = Application.Current.TryFindResource(key);
if (translation is string)
@ -257,8 +257,7 @@ namespace Flow.Launcher.Core.Resource
{
foreach (var p in PluginManager.GetPluginsForInterface<IPluginI18n>())
{
var pluginI18N = p.Plugin as IPluginI18n;
if (pluginI18N == null) return;
if (p.Plugin is not IPluginI18n pluginI18N) return;
try
{
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
@ -272,11 +271,11 @@ namespace Flow.Launcher.Core.Resource
}
}
public string LanguageFile(string folder, string language)
private static string LanguageFile(string folder, string language)
{
if (Directory.Exists(folder))
{
string path = Path.Combine(folder, language);
var path = Path.Combine(folder, language);
if (File.Exists(path))
{
return path;
@ -284,7 +283,7 @@ namespace Flow.Launcher.Core.Resource
else
{
Log.Error($"|Internationalization.LanguageFile|Language path can't be found <{path}>");
string english = Path.Combine(folder, DefaultFile);
var english = Path.Combine(folder, DefaultFile);
if (File.Exists(english))
{
return english;

View file

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Threading.Tasks;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
@ -40,6 +41,16 @@ namespace Flow.Launcher.Infrastructure.Storage
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
// Let the old Program plugin get this constructor
[Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")]
public BinaryStorage(string filename, string directoryPath = null!)
{
DirectoryPath = directoryPath ?? DataLocation.CacheDirectory;
FilesFolders.ValidateDirectory(DirectoryPath);
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public async ValueTask<T> TryLoadAsync(T defaultData)
{
if (Data != null) return Data;
@ -82,8 +93,10 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
var serialized = MemoryPackSerializer.Serialize(Data);
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = MemoryPackSerializer.Serialize(Data);
File.WriteAllBytes(FilePath, serialized);
}
@ -103,6 +116,9 @@ namespace Flow.Launcher.Infrastructure.Storage
// so we need to pass it to SaveAsync
public async ValueTask SaveAsync(T data)
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
await using var stream = new FileStream(FilePath, FileMode.Create);
await MemoryPackSerializer.SerializeAsync(stream, data);
}

View file

@ -17,11 +17,11 @@ namespace Flow.Launcher.Infrastructure.Storage
public FlowLauncherJsonStorage()
{
var directoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
FilesFolders.ValidateDirectory(directoryPath);
DirectoryPath = Path.Combine(DataLocation.DataDirectory(), DirectoryName);
FilesFolders.ValidateDirectory(DirectoryPath);
var filename = typeof(T).Name;
FilePath = Path.Combine(directoryPath, $"{filename}{FileSuffix}");
FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}");
}
public new void Save()

View file

@ -183,7 +183,10 @@ namespace Flow.Launcher.Infrastructure.Storage
public void Save()
{
string serialized = JsonSerializer.Serialize(Data,
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
var serialized = JsonSerializer.Serialize(Data,
new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(TempFilePath, serialized);
@ -193,6 +196,9 @@ namespace Flow.Launcher.Infrastructure.Storage
public async Task SaveAsync()
{
// User may delete the directory, so we need to check it
FilesFolders.ValidateDirectory(DirectoryPath);
await using var tempOutput = File.OpenWrite(TempFilePath);
await JsonSerializer.SerializeAsync(tempOutput, Data,
new JsonSerializerOptions { WriteIndented = true });

View file

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Windows;
@ -517,5 +518,92 @@ namespace Flow.Launcher.Infrastructure
}
#endregion
#region Korean IME
public static bool IsWindows11()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Environment.OSVersion.Version.Build >= 22000;
}
public static bool IsKoreanIMEExist()
{
return GetLegacyKoreanIMERegistryValue() != null;
}
public static bool IsLegacyKoreanIMEEnabled()
{
object value = GetLegacyKoreanIMERegistryValue();
if (value is int intValue)
{
return intValue == 1;
}
else if (value != null && int.TryParse(value.ToString(), out int parsedValue))
{
return parsedValue == 1;
}
return false;
}
public static bool SetLegacyKoreanIMEEnabled(bool enable)
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}";
const string valueName = "NoTsf3Override5";
try
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(subKeyPath);
if (key != null)
{
int value = enable ? 1 : 0;
key.SetValue(valueName, value, RegistryValueKind.DWord);
return true;
}
}
catch (System.Exception)
{
// Ignored
}
return false;
}
public static object GetLegacyKoreanIMERegistryValue()
{
const string subKeyPath = @"Software\Microsoft\input\tsf\tsf3override\{A028AE76-01B1-46C2-99C4-ACD9858AE02F}";
const string valueName = "NoTsf3Override5";
try
{
using RegistryKey key = Registry.CurrentUser.OpenSubKey(subKeyPath);
if (key != null)
{
return key.GetValue(valueName);
}
}
catch (System.Exception)
{
// Ignored
}
return null;
}
public static void OpenImeSettings()
{
try
{
Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true });
}
catch (System.Exception)
{
// Ignored
}
}
#endregion
}
}

View file

@ -264,12 +264,12 @@ namespace Flow.Launcher.Plugin.SharedCommands
var index = path.LastIndexOf('\\');
if (index > 0 && index < (path.Length - 1))
{
string previousDirectoryPath = path.Substring(0, index + 1);
return locationExists(previousDirectoryPath) ? previousDirectoryPath : "";
string previousDirectoryPath = path[..(index + 1)];
return locationExists(previousDirectoryPath) ? previousDirectoryPath : string.Empty;
}
else
{
return "";
return string.Empty;
}
}
@ -285,7 +285,7 @@ namespace Flow.Launcher.Plugin.SharedCommands
// not full path, get previous level directory string
var indexOfSeparator = path.LastIndexOf('\\');
return path.Substring(0, indexOfSeparator + 1);
return path[..(indexOfSeparator + 1)];
}
return path;

View file

@ -153,6 +153,7 @@ namespace Flow.Launcher
RegisterAppDomainExceptions();
RegisterDispatcherUnhandledException();
RegisterTaskSchedulerUnhandledException();
var imageLoadertask = ImageLoader.InitializeAsync();
@ -284,6 +285,15 @@ namespace Flow.Launcher
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
}
/// <summary>
/// let exception throw as normal is better for Debug
/// </summary>
[Conditional("RELEASE")]
private static void RegisterTaskSchedulerUnhandledException()
{
TaskScheduler.UnobservedTaskException += ErrorReporting.TaskSchedulerUnobservedTaskException;
}
#endregion
#region IDisposable

View file

@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
@ -43,8 +44,16 @@ public class QuerySuggestionBoxConverter : IMultiValueConverter
// Check if Text will be larger than our QueryTextBox
Typeface typeface = new Typeface(queryTextBox.FontFamily, queryTextBox.FontStyle, queryTextBox.FontWeight, queryTextBox.FontStretch);
// TODO: Obsolete warning?
var ft = new FormattedText(queryTextBox.Text, CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, typeface, queryTextBox.FontSize, Brushes.Black);
var dpi = VisualTreeHelper.GetDpi(queryTextBox);
var ft = new FormattedText(
queryTextBox.Text,
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
typeface,
queryTextBox.FontSize,
Brushes.Black,
dpi.PixelsPerDip
);
var offset = queryTextBox.Padding.Right;

View file

@ -1,8 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using NLog;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Exception;
using NLog;
namespace Flow.Launcher.Helper;
@ -30,6 +32,13 @@ public static class ErrorReporting
e.Handled = true;
}
public static void TaskSchedulerUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
//handle unobserved task exceptions
Application.Current.Dispatcher.Invoke(() => Report(e.Exception));
//prevent application exit, so the user can copy the prompted error info
}
public static string RuntimeInfo()
{
var info =

View file

@ -44,6 +44,7 @@
<system:String x:Key="GameMode">Game Mode</system:String>
<system:String x:Key="GameModeToolTip">Suspend the use of Hotkeys.</system:String>
<system:String x:Key="PositionReset">Position Reset</system:String>
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
<!-- Setting General -->
@ -109,8 +110,22 @@
<system:String x:Key="shadowEffectNotAllowed">Shadow effect is not allowed while current theme has blur effect enabled</system:String>
<system:String x:Key="searchDelay">Search Delay</system:String>
<system:String x:Key="searchDelayToolTip">Adds a short delay while typing to reduce UI flicker and result load. Recommended if your typing speed is average.</system:String>
<system:String x:Key="searchDelayNumberBoxToolTip">Enter the wait time (in ms) until input is considered complete. This can only be edited if Search Delay is enabled.</system:String>
<system:String x:Key="searchDelayTime">Default Search Delay Time</system:String>
<system:String x:Key="searchDelayTimeToolTip">Wait time before showing results after typing stops. Higher values wait longer. (ms)</system:String>
<system:String x:Key="KoreanImeTitle">Information for Korean IME user</system:String>
<system:String x:Key="KoreanImeGuide">
The Korean input method used in Windows 11 may cause some issues in Flow Launcher.&#x0a;
If you experience any problems, you may need to enable "Use previous version of Korean IME".&#x0a;&#x0a;
Open Setting in Windows 11 and go to:&#x0a;
Time &amp; Language &gt; Language &amp; Region &gt; Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility,&#x0a;
and enable "Use previous version of Microsoft IME".&#x0a;&#x0a;
</system:String>
<system:String x:Key="KoreanImeOpenLink">Open Language and Region System Settings</system:String>
<system:String x:Key="KoreanImeOpenLinkToolTip">Opens the Korean IME setting location. Go to Korean &gt; Language Options &gt; Keyboard - Microsoft IME &gt; Compatibility</system:String>
<system:String x:Key="KoreanImeOpenLinkButton">Open</system:String>
<system:String x:Key="KoreanImeRegistry">Use Previous Korean IME</system:String>
<system:String x:Key="KoreanImeRegistryTooltip">You can change the Previous Korean IME settings directly from here</system:String>
<!-- Setting Plugin -->
<system:String x:Key="searchplugin">Search Plugin</system:String>

View file

@ -291,15 +291,15 @@ namespace Flow.Launcher
{
if (!CanClose)
{
CanClose = true;
_notifyIcon.Visible = false;
App.API.SaveAppAllSettings();
e.Cancel = true;
await ImageLoader.WaitSaveAsync();
await PluginManager.DisposePluginsAsync();
Notification.Uninstall();
// After plugins are all disposed, we can close the main window
CanClose = true;
// Use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
// After plugins are all disposed, we shutdown application to close app
// We use this instead of Close() to avoid InvalidOperationException when calling Close() in OnClosing event
Application.Current.Shutdown();
}
}

View file

@ -38,20 +38,23 @@ namespace Flow.Launcher
public class PublicAPIInstance : IPublicAPI, IRemovable
{
private readonly Settings _settings;
private readonly Internationalization _translater;
private readonly MainViewModel _mainVM;
// Must use getter to access Application.Current.Resources.MergedDictionaries so earlier
private Theme _theme;
private Theme Theme => _theme ??= Ioc.Default.GetRequiredService<Theme>();
// Must use getter to avoid circular dependency
private Updater _updater;
private Updater Updater => _updater ??= Ioc.Default.GetRequiredService<Updater>();
private readonly object _saveSettingsLock = new();
#region Constructor
public PublicAPIInstance(Settings settings, Internationalization translater, MainViewModel mainVM)
public PublicAPIInstance(Settings settings, MainViewModel mainVM)
{
_settings = settings;
_translater = translater;
_mainVM = mainVM;
GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback;
WebRequest.RegisterPrefix("data", new DataWebRequestFactory());
@ -100,8 +103,7 @@ namespace Flow.Launcher
remove => _mainVM.VisibilityChanged -= value;
}
// Must use Ioc.Default.GetRequiredService<Updater>() to avoid circular dependency
public void CheckForNewUpdate() => _ = Ioc.Default.GetRequiredService<Updater>().UpdateAppAsync(false);
public void CheckForNewUpdate() => _ = Updater.UpdateAppAsync(false);
public void SaveAppAllSettings()
{
@ -178,7 +180,7 @@ namespace Flow.Launcher
public void StopLoadingBar() => _mainVM.ProgressBarVisibility = Visibility.Collapsed;
public string GetTranslation(string key) => _translater.GetTranslation(key);
public string GetTranslation(string key) => Internationalization.GetTranslation(key);
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins.ToList();

View file

@ -0,0 +1,81 @@
<UserControl
x:Class="Flow.Launcher.Resources.Controls.InfoBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
d:DesignHeight="45"
d:DesignWidth="400"
mc:Ignorable="d">
<UserControl.Resources />
<Grid>
<Border
x:Name="PART_Border"
MinHeight="48"
Padding="18 18 18 18"
Background="{DynamicResource InfoBarInfoBG}"
BorderBrush="{DynamicResource Color03B}"
BorderThickness="1"
CornerRadius="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" MinWidth="24" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal">
<Border
x:Name="PART_IconBorder"
Width="16"
Height="16"
Margin="0 0 12 0"
VerticalAlignment="Top"
CornerRadius="10">
<ui:FontIcon
x:Name="PART_Icon"
Margin="1 0 0 1"
VerticalAlignment="Center"
FontFamily="Segoe MDL2 Assets"
FontSize="13"
Foreground="{DynamicResource Color01B}"
Visibility="Visible" />
</Border>
</StackPanel>
<StackPanel
x:Name="PART_StackPanel"
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
x:Name="PART_Title"
Margin="0 0 12 0"
FontWeight="SemiBold"
Foreground="{DynamicResource Color05B}"
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Title}" />
<TextBlock
x:Name="PART_Message"
Foreground="{DynamicResource Color05B}"
Text="{Binding RelativeSource={RelativeSource AncestorType=cc:InfoBar}, Path=Message}"
TextWrapping="Wrap" />
</StackPanel>
<Button
x:Name="PART_CloseButton"
Grid.Column="2"
Width="32"
Height="32"
VerticalAlignment="Center"
AutomationProperties.Name="Close InfoBar"
Click="PART_CloseButton_Click"
Content="&#xE10A;"
FontFamily="Segoe MDL2 Assets"
FontSize="12"
ToolTip="Close"
Visibility="Visible" />
</Grid>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,222 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Flow.Launcher.Resources.Controls
{
public partial class InfoBar : UserControl
{
public InfoBar()
{
InitializeComponent();
Loaded += InfoBar_Loaded;
}
private void InfoBar_Loaded(object sender, RoutedEventArgs e)
{
UpdateStyle();
UpdateTitleVisibility();
UpdateMessageVisibility();
UpdateOrientation();
UpdateIconAlignmentAndMargin();
UpdateIconVisibility();
UpdateCloseButtonVisibility();
}
public static readonly DependencyProperty TypeProperty =
DependencyProperty.Register(nameof(Type), typeof(InfoBarType), typeof(InfoBar), new PropertyMetadata(InfoBarType.Info, OnTypeChanged));
public InfoBarType Type
{
get => (InfoBarType)GetValue(TypeProperty);
set => SetValue(TypeProperty, value);
}
private static void OnTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateStyle();
}
}
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register(nameof(Message), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnMessageChanged));
public string Message
{
get => (string)GetValue(MessageProperty);
set
{
SetValue(MessageProperty, value);
}
}
private static void OnMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateMessageVisibility();
}
}
private void UpdateMessageVisibility()
{
PART_Message.Visibility = string.IsNullOrEmpty(Message) ? Visibility.Collapsed : Visibility.Visible;
}
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(nameof(Title), typeof(string), typeof(InfoBar), new PropertyMetadata(string.Empty, OnTitleChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set
{
SetValue(TitleProperty, value);
UpdateTitleVisibility(); // Visibility update when change Title
}
}
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateTitleVisibility();
}
}
private void UpdateTitleVisibility()
{
PART_Title.Visibility = string.IsNullOrEmpty(Title) ? Visibility.Collapsed : Visibility.Visible;
}
public static readonly DependencyProperty IsIconVisibleProperty =
DependencyProperty.Register(nameof(IsIconVisible), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnIsIconVisibleChanged));
public bool IsIconVisible
{
get => (bool)GetValue(IsIconVisibleProperty);
set => SetValue(IsIconVisibleProperty, value);
}
public static readonly DependencyProperty LengthProperty =
DependencyProperty.Register(nameof(Length), typeof(InfoBarLength), typeof(InfoBar), new PropertyMetadata(InfoBarLength.Short, OnLengthChanged));
public InfoBarLength Length
{
get { return (InfoBarLength)GetValue(LengthProperty); }
set { SetValue(LengthProperty, value); }
}
private static void OnLengthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is InfoBar infoBar)
{
infoBar.UpdateOrientation();
infoBar.UpdateIconAlignmentAndMargin();
}
}
private void UpdateOrientation()
{
PART_StackPanel.Orientation = Length == InfoBarLength.Long ? Orientation.Vertical : Orientation.Horizontal;
}
private void UpdateIconAlignmentAndMargin()
{
if (Length == InfoBarLength.Short)
{
PART_IconBorder.VerticalAlignment = VerticalAlignment.Center;
PART_IconBorder.Margin = new Thickness(0, 0, 12, 0);
}
else
{
PART_IconBorder.VerticalAlignment = VerticalAlignment.Top;
PART_IconBorder.Margin = new Thickness(0, 2, 12, 0);
}
}
public static readonly DependencyProperty ClosableProperty =
DependencyProperty.Register(nameof(Closable), typeof(bool), typeof(InfoBar), new PropertyMetadata(true, OnClosableChanged));
public bool Closable
{
get => (bool)GetValue(ClosableProperty);
set => SetValue(ClosableProperty, value);
}
private void PART_CloseButton_Click(object sender, RoutedEventArgs e)
{
Visibility = Visibility.Collapsed;
}
private void UpdateStyle()
{
switch (Type)
{
case InfoBarType.Info:
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
PART_Icon.Glyph = "\xF13F";
break;
case InfoBarType.Success:
PART_Border.Background = (Brush)FindResource("InfoBarSuccessBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarSuccessIcon");
PART_Icon.Glyph = "\xF13E";
break;
case InfoBarType.Warning:
PART_Border.Background = (Brush)FindResource("InfoBarWarningBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarWarningIcon");
PART_Icon.Glyph = "\xF13C";
break;
case InfoBarType.Error:
PART_Border.Background = (Brush)FindResource("InfoBarErrorBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarErrorIcon");
PART_Icon.Glyph = "\xF13D";
break;
default:
PART_Border.Background = (Brush)FindResource("InfoBarInfoBG");
PART_IconBorder.Background = (Brush)FindResource("InfoBarInfoIcon");
PART_Icon.Glyph = "\xF13F";
break;
}
}
private static void OnIsIconVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var infoBar = (InfoBar)d;
infoBar.UpdateIconVisibility();
}
private static void OnClosableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var infoBar = (InfoBar)d;
infoBar.UpdateCloseButtonVisibility();
}
private void UpdateIconVisibility()
{
PART_IconBorder.Visibility = IsIconVisible ? Visibility.Visible : Visibility.Collapsed;
}
private void UpdateCloseButtonVisibility()
{
PART_CloseButton.Visibility = Closable ? Visibility.Visible : Visibility.Collapsed;
}
}
public enum InfoBarType
{
Info,
Success,
Warning,
Error
}
public enum InfoBarLength
{
Short,
Long
}
}

View file

@ -96,8 +96,11 @@
PlaceholderText="{Binding DefaultSearchDelay}"
SmallChange="10"
SpinButtonPlacementMode="Compact"
ToolTip="{DynamicResource searchDelayToolTip}"
ToolTip="{DynamicResource searchDelayNumberBoxToolTip}"
ToolTipService.InitialShowDelay="0"
ToolTipService.ShowOnDisabled="True"
Value="{Binding PluginSearchDelayTime, Mode=TwoWay}" />
</StackPanel>
<!-- Put OnOffControl after PriorityControl & SearchDelayControl so that it can display correctly -->

View file

@ -1285,7 +1285,6 @@
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
<Setter Property="Padding" Value="12 0 0 0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="ui:ControlHelper.CornerRadius" Value="0" />
@ -2784,9 +2783,12 @@
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="HeaderContentPresenter" Property="Foreground" Value="{DynamicResource TextControlHeaderForegroundDisabled}" />
<Setter Property="Background" Value="{DynamicResource TextControlBackgroundDisabled}" />
<Setter Property="Background" Value="{DynamicResource CustomNumberBoxBGDisabled}" />
<Setter TargetName="BorderElementInline" Property="Background" Value="{DynamicResource TextControlBackgroundDisabled}" />
<Setter TargetName="BorderElement" Property="BorderBrush" Value="{DynamicResource TextControlBorderBrushDisabled}" />
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}" />
<Setter TargetName="BorderElement" Property="BorderThickness" Value="0 0 0 0" />
<Setter TargetName="BorderElementInline" Property="BorderThickness" Value="0 0 0 0" />
<Setter Property="Foreground" Value="{DynamicResource TextControlPlaceholderForegroundDisabled}" />
<Setter TargetName="PlaceholderTextContentPresenter" Property="Foreground" Value="{DynamicResource TextControlPlaceholderForegroundDisabled}" />
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
@ -2801,7 +2803,7 @@
<Setter TargetName="BorderElementInline" Property="BorderBrush" Value="{DynamicResource TextControlBorderBrushFocused}" />
<Setter TargetName="BorderElementInline" Property="BorderThickness" Value="0 0 0 2" />
<Setter TargetName="BorderElement" Property="BorderBrush" Value="{DynamicResource CustomTextBoxOutline}" />
<Setter TargetName="BorderElement" Property="BorderThickness" Value="{DynamicResource 2 2 2 0}" />
<Setter TargetName="BorderElement" Property="BorderThickness" Value="2 2 2 0" />
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundFocused}" />
</Trigger>
<MultiTrigger>
@ -2872,7 +2874,6 @@
FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{DynamicResource Color05B}"
InputScope="{TemplateBinding InputScope}"
SelectionBrush="{TemplateBinding SelectionBrush}"
TextAlignment="{TemplateBinding TextAlignment}" />

View file

@ -106,7 +106,6 @@
<Color x:Key="NumberBoxColor24">#f5f5f5</Color>
<Color x:Key="NumberBoxColor25">#464646</Color>
<Color x:Key="NumberBoxColor26">#ffffff</Color>
<SolidColorBrush x:Key="NumberBoxPlaceHolder" Color="#565656" />
<Color x:Key="HoverStoreGrid">#272727</Color>
<!-- Resources for HotkeyControl -->
@ -115,10 +114,19 @@
<SolidColorBrush x:Key="CustomExpanderHover" Color="#323232" />
<!-- Resource for ContentDialog -->
<SolidColorBrush x:Key="ContentDialogOverlayBG" Color="#58000000" />
<!-- Infobar Warning -->
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#FCE100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<!-- DIY Infobar -->
<SolidColorBrush x:Key="InfoBarBD" Color="#19000000" />
<SolidColorBrush
x:Key="InfoBarInfoIcon"
Opacity="0.9"
Color="{m:DynamicColor SystemAccentColor}" />
<SolidColorBrush x:Key="InfoBarInfoBG" Color="#272727" />
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#fce100" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
<SolidColorBrush x:Key="InfoBarSuccessIcon" Color="#6ccb5f" />
<SolidColorBrush x:Key="InfoBarSuccessBG" Color="#393d1b" />
<SolidColorBrush x:Key="InfoBarErrorIcon" Color="#ff99a4" />
<SolidColorBrush x:Key="InfoBarErrorBG" Color="#442726" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />

View file

@ -108,10 +108,20 @@
<SolidColorBrush x:Key="CustomExpanderHover" Color="#f6f6f6" />
<!-- Resource for ContentDialog -->
<SolidColorBrush x:Key="ContentDialogOverlayBG" Color="#4D000000" />
<!-- Infobar Warning -->
<!-- DIY Infobar -->
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush
x:Key="InfoBarInfoIcon"
Opacity="0.8"
Color="{m:DynamicColor SystemAccentColor}" />
<SolidColorBrush x:Key="InfoBarInfoBG" Color="{m:DynamicColor Color01}" />
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#9D5D00" />
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#FFF4CE" />
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
<SolidColorBrush x:Key="InfoBarSuccessIcon" Color="#3f973e" />
<SolidColorBrush x:Key="InfoBarSuccessBG" Color="#dff6dd" />
<SolidColorBrush x:Key="InfoBarErrorIcon" Color="#c42b1c" />
<SolidColorBrush x:Key="InfoBarErrorBG" Color="#fde7e9" />
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />

View file

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
using Flow.Launcher.Core.Configuration;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
@ -179,6 +181,70 @@ public partial class SettingsPaneGeneralViewModel : BaseModel
}
}
#region Korean IME
// The new Korean IME used in Windows 11 has compatibility issues with WPF. This issue is difficult to resolve within
// WPF itself, but it can be avoided by having the user switch to the legacy IME at the system level. Therefore,
// we provide guidance and a direct button for users to make this change themselves. If the relevant registry key does
// not exist (i.e., the Korean IME is not installed), this setting will not be shown at all.
public bool LegacyKoreanIMEEnabled
{
get => Win32Helper.IsLegacyKoreanIMEEnabled();
set
{
if (Win32Helper.SetLegacyKoreanIMEEnabled(value))
{
OnPropertyChanged();
OnPropertyChanged(nameof(KoreanIMERegistryValueIsZero));
}
else
{
//Since this is rarely seen text, language support is not provided.
App.API.ShowMsg("Failed to change Korean IME setting", "Please check your system registry access or contact support.");
}
}
}
public bool KoreanIMERegistryKeyExists
{
get
{
var registryKeyExists = Win32Helper.IsKoreanIMEExist();
var koreanLanguageInstalled = InputLanguage.InstalledInputLanguages.Cast<InputLanguage>().Any(lang => lang.Culture.Name.StartsWith("ko"));
var isWindows11 = Win32Helper.IsWindows11();
// Return true if Windows 11 with Korean IME installed, or if the registry key exists
return (isWindows11 && koreanLanguageInstalled) || registryKeyExists;
}
}
public bool KoreanIMERegistryValueIsZero
{
get
{
var value = Win32Helper.GetLegacyKoreanIMERegistryValue();
if (value is int intValue)
{
return intValue == 0;
}
else if (value != null && int.TryParse(value.ToString(), out var parsedValue))
{
return parsedValue == 0;
}
return false;
}
}
[RelayCommand]
private void OpenImeSettings()
{
Win32Helper.OpenImeSettings();
}
#endregion
public bool ShouldUsePinyin
{
get => Settings.ShouldUsePinyin;

View file

@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Flow.Launcher.Resources.Controls"
xmlns:converters="clr-namespace:Flow.Launcher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ext="clr-namespace:Flow.Launcher.Resources.MarkupExtensions"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@ -14,6 +15,9 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<ui:Page.Resources>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ui:Page.Resources>
<ScrollViewer
Margin="0"
CanContentScroll="False"
@ -28,6 +32,7 @@
Style="{StaticResource PageTitle}"
Text="{DynamicResource general}"
TextAlignment="left" />
<cc:ExCard
Title="{DynamicResource startFlowLauncherOnSystemStartup}"
Margin="0 8 0 0"
@ -215,12 +220,12 @@
<ui:NumberBox
Width="120"
Margin="0 0 0 0"
Minimum="0"
Maximum="1000"
Minimum="0"
SmallChange="10"
SpinButtonPlacementMode="Compact"
Value="{Binding SearchDelayTimeValue}"
ValidationMode="InvalidInputOverwritten" />
ValidationMode="InvalidInputOverwritten"
Value="{Binding SearchDelayTimeValue}" />
</StackPanel>
</cc:Card>
</cc:ExCard>
@ -312,6 +317,35 @@
SelectedValue="{Binding Language}"
SelectedValuePath="LanguageCode" />
</cc:Card>
<Border Visibility="{Binding KoreanIMERegistryKeyExists, Converter={StaticResource BoolToVisibilityConverter}}">
<cc:InfoBar
Title="{DynamicResource KoreanImeTitle}"
Margin="0 14 0 0"
Closable="False"
DataContext="{Binding RelativeSource={RelativeSource AncestorType=Border}, Path=DataContext}"
IsIconVisible="True"
Length="Long"
Message="{DynamicResource KoreanImeGuide}"
Type="Warning"
Visibility="{Binding LegacyKoreanIMEEnabled, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverted, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" />
</Border>
<cc:CardGroup Margin="0 14 0 0" Visibility="{Binding KoreanIMERegistryKeyExists, Converter={StaticResource BoolToVisibilityConverter}}">
<cc:Card
Title="{DynamicResource KoreanImeRegistry}"
Icon="&#xe88b;"
Sub="{DynamicResource KoreanImeRegistryTooltip}">
<ui:ToggleSwitch
IsOn="{Binding LegacyKoreanIMEEnabled}"
OffContent="{DynamicResource disable}"
OnContent="{DynamicResource enable}" />
</cc:Card>
<cc:Card
Title="{DynamicResource KoreanImeOpenLink}"
Icon="&#xF210;"
Sub="{DynamicResource KoreanImeOpenLinkToolTip}">
<Button Command="{Binding OpenImeSettingsCommand}" Content="{DynamicResource KoreanImeOpenLinkButton}" />
</cc:Card>
</cc:CardGroup>
</VirtualizingStackPanel>
</ScrollViewer>
</ui:Page>

View file

@ -598,7 +598,7 @@
<ComboBox
Grid.Row="2"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
VerticalAlignment="Center"
ItemsSource="{Binding Settings.SortOptions, Mode=OneWay}"
SelectedItem="{Binding Settings.SortOption}"
@ -623,7 +623,7 @@
Grid.Row="3"
Grid.Column="1"
Width="{StaticResource SettingPanelPathTextBoxWidth}"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Margin="{StaticResource SettingPanelItemLeftTopBottomMargin}"
HorizontalAlignment="Left"
Text="{Binding EverythingInstalledPath}" />