mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Merge branch 'dev' into plugin_settings_cache_path
This commit is contained in:
commit
fd10addba4
32 changed files with 1278 additions and 483 deletions
|
|
@ -229,9 +229,6 @@ namespace Flow.Launcher.Core.Plugin
|
|||
}
|
||||
}
|
||||
|
||||
InternationalizationManager.Instance.AddPluginLanguageDirectories(GetPluginsForInterface<IPluginI18n>());
|
||||
InternationalizationManager.Instance.ChangeLanguage(Ioc.Default.GetRequiredService<Settings>().Language);
|
||||
|
||||
if (failedPlugins.Any())
|
||||
{
|
||||
var failed = string.Join(",", failedPlugins.Select(x => x.Metadata.Name));
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ namespace Flow.Launcher.Core.Resource
|
|||
return DefaultLanguageCode;
|
||||
}
|
||||
|
||||
internal void AddPluginLanguageDirectories(IEnumerable<PluginPair> plugins)
|
||||
private void AddPluginLanguageDirectories()
|
||||
{
|
||||
foreach (var plugin in plugins)
|
||||
foreach (var plugin in PluginManager.GetPluginsForInterface<IPluginI18n>())
|
||||
{
|
||||
var location = Assembly.GetAssembly(plugin.Plugin.GetType()).Location;
|
||||
var dir = Path.GetDirectoryName(location);
|
||||
|
|
@ -96,6 +96,32 @@ namespace Flow.Launcher.Core.Resource
|
|||
_oldResources.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize language. Will change app language and plugin language based on settings.
|
||||
/// </summary>
|
||||
public async Task InitializeLanguageAsync()
|
||||
{
|
||||
// Get actual language
|
||||
var languageCode = _settings.Language;
|
||||
if (languageCode == Constant.SystemLanguageCode)
|
||||
{
|
||||
languageCode = SystemLanguageCode;
|
||||
}
|
||||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
|
||||
// Add plugin language directories first so that we can load language files from plugins
|
||||
AddPluginLanguageDirectories();
|
||||
|
||||
// Change language
|
||||
await ChangeLanguageAsync(language);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change language during runtime. Will change app language and plugin language & save settings.
|
||||
/// </summary>
|
||||
/// <param name="languageCode"></param>
|
||||
public void ChangeLanguage(string languageCode)
|
||||
{
|
||||
languageCode = languageCode.NonNull();
|
||||
|
|
@ -110,7 +136,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
// Get language by language code and change language
|
||||
var language = GetLanguageByLanguageCode(languageCode);
|
||||
ChangeLanguage(language, isSystem);
|
||||
|
||||
// Change language
|
||||
_ = ChangeLanguageAsync(language);
|
||||
|
||||
// Save settings
|
||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||
}
|
||||
|
||||
private Language GetLanguageByLanguageCode(string languageCode)
|
||||
|
|
@ -128,26 +159,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
}
|
||||
|
||||
private void ChangeLanguage(Language language, bool isSystem)
|
||||
private async Task ChangeLanguageAsync(Language language)
|
||||
{
|
||||
language = language.NonNull();
|
||||
|
||||
// Remove old language files and load language
|
||||
RemoveOldLanguageFiles();
|
||||
if (language != AvailableLanguages.English)
|
||||
{
|
||||
LoadLanguage(language);
|
||||
}
|
||||
|
||||
// Culture of main thread
|
||||
// Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's
|
||||
CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture(language.LanguageCode);
|
||||
CultureInfo.CurrentUICulture = CultureInfo.CurrentCulture;
|
||||
|
||||
// Raise event after culture is set
|
||||
_settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
UpdatePluginMetadataTranslations();
|
||||
});
|
||||
// Raise event for plugins after culture is set
|
||||
await Task.Run(UpdatePluginMetadataTranslations);
|
||||
}
|
||||
|
||||
public bool PromptShouldUsePinyin(string languageCodeToSet)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Xml;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
|
|
@ -16,7 +17,6 @@ using Flow.Launcher.Infrastructure.Logger;
|
|||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using Microsoft.Win32;
|
||||
using TextBox = System.Windows.Controls.TextBox;
|
||||
|
||||
namespace Flow.Launcher.Core.Resource
|
||||
{
|
||||
|
|
@ -56,20 +56,23 @@ namespace Flow.Launcher.Core.Resource
|
|||
MakeSureThemeDirectoriesExist();
|
||||
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
_oldResource = dicts.First(d =>
|
||||
_oldResource = dicts.FirstOrDefault(d =>
|
||||
{
|
||||
if (d.Source == null)
|
||||
return false;
|
||||
if (d.Source == null) return false;
|
||||
|
||||
var p = d.Source.AbsolutePath;
|
||||
var dir = Path.GetDirectoryName(p).NonNull();
|
||||
var info = new DirectoryInfo(dir);
|
||||
var f = info.Name;
|
||||
var e = Path.GetExtension(p);
|
||||
var found = f == Folder && e == Extension;
|
||||
return found;
|
||||
return p.Contains(Folder) && Path.GetExtension(p) == Extension;
|
||||
});
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
|
||||
if (_oldResource != null)
|
||||
{
|
||||
_oldTheme = Path.GetFileNameWithoutExtension(_oldResource.Source.AbsolutePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Current theme resource not found. Initializing with default theme.");
|
||||
_oldTheme = Constant.DefaultTheme;
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -98,13 +101,152 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void UpdateResourceDictionary(ResourceDictionary dictionaryToUpdate)
|
||||
{
|
||||
var dicts = Application.Current.Resources.MergedDictionaries;
|
||||
// Add new resources
|
||||
if (!Application.Current.Resources.MergedDictionaries.Contains(dictionaryToUpdate))
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Add(dictionaryToUpdate);
|
||||
}
|
||||
|
||||
// Remove old resources
|
||||
if (_oldResource != null && _oldResource != dictionaryToUpdate &&
|
||||
Application.Current.Resources.MergedDictionaries.Contains(_oldResource))
|
||||
{
|
||||
Application.Current.Resources.MergedDictionaries.Remove(_oldResource);
|
||||
}
|
||||
|
||||
dicts.Remove(_oldResource);
|
||||
dicts.Add(dictionaryToUpdate);
|
||||
_oldResource = dictionaryToUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates only the font settings and refreshes the UI.
|
||||
/// </summary>
|
||||
public void UpdateFonts()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load a ResourceDictionary for the specified theme.
|
||||
var themeName = GetCurrentTheme();
|
||||
var dict = GetThemeResourceDictionary(themeName);
|
||||
|
||||
// Apply font settings to the theme resource.
|
||||
ApplyFontSettings(dict);
|
||||
UpdateResourceDictionary(dict);
|
||||
|
||||
// Must apply blur and drop shadow effects
|
||||
_ = RefreshFrameAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception("Error occurred while updating theme fonts", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads and applies font settings to the theme resource.
|
||||
/// </summary>
|
||||
private void ApplyFontSettings(ResourceDictionary dict)
|
||||
{
|
||||
if (dict["QueryBoxStyle"] is Style queryBoxStyle &&
|
||||
dict["QuerySuggestionBoxStyle"] is Style querySuggestionBoxStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.QueryBoxFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.QueryBoxFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
SetFontProperties(queryBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, true);
|
||||
SetFontProperties(querySuggestionBoxStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
dict["ItemTitleSelectedStyle"] is Style resultItemSelectedStyle &&
|
||||
dict["ItemHotkeyStyle"] is Style resultHotkeyItemStyle &&
|
||||
dict["ItemHotkeySelectedStyle"] is Style resultHotkeyItemSelectedStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.ResultFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultFontStretch);
|
||||
|
||||
SetFontProperties(resultItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultHotkeyItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultHotkeyItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
|
||||
if (dict["ItemSubTitleStyle"] is Style resultSubItemStyle &&
|
||||
dict["ItemSubTitleSelectedStyle"] is Style resultSubItemSelectedStyle)
|
||||
{
|
||||
var fontFamily = new FontFamily(_settings.ResultSubFont);
|
||||
var fontStyle = FontHelper.GetFontStyleFromInvariantStringOrNormal(_settings.ResultSubFontStyle);
|
||||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.ResultSubFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.ResultSubFontStretch);
|
||||
|
||||
SetFontProperties(resultSubItemStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
SetFontProperties(resultSubItemSelectedStyle, fontFamily, fontStyle, fontWeight, fontStretch, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies font properties to a Style.
|
||||
/// </summary>
|
||||
private static void SetFontProperties(Style style, FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, bool isTextBox)
|
||||
{
|
||||
// Remove existing font-related setters
|
||||
if (isTextBox)
|
||||
{
|
||||
// First, find the setters to remove and store them in a list
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<Setter>()
|
||||
.Where(setter =>
|
||||
setter.Property == Control.FontFamilyProperty ||
|
||||
setter.Property == Control.FontStyleProperty ||
|
||||
setter.Property == Control.FontWeightProperty ||
|
||||
setter.Property == Control.FontStretchProperty)
|
||||
.ToList();
|
||||
|
||||
// Remove each found setter one by one
|
||||
foreach (var setter in settersToRemove)
|
||||
{
|
||||
style.Setters.Remove(setter);
|
||||
}
|
||||
|
||||
// Add New font setter
|
||||
style.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
style.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
style.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
style.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
|
||||
// Set caret brush (retain existing logic)
|
||||
var caretBrushPropertyValue = style.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||
var foregroundPropertyValue = style.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||
.Select(x => x.Value).FirstOrDefault();
|
||||
if (!caretBrushPropertyValue && foregroundPropertyValue != null)
|
||||
style.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
var settersToRemove = style.Setters
|
||||
.OfType<Setter>()
|
||||
.Where(setter =>
|
||||
setter.Property == TextBlock.FontFamilyProperty ||
|
||||
setter.Property == TextBlock.FontStyleProperty ||
|
||||
setter.Property == TextBlock.FontWeightProperty ||
|
||||
setter.Property == TextBlock.FontStretchProperty)
|
||||
.ToList();
|
||||
|
||||
foreach (var setter in settersToRemove)
|
||||
{
|
||||
style.Setters.Remove(setter);
|
||||
}
|
||||
|
||||
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, fontFamily));
|
||||
style.Setters.Add(new Setter(TextBlock.FontStyleProperty, fontStyle));
|
||||
style.Setters.Add(new Setter(TextBlock.FontWeightProperty, fontWeight));
|
||||
style.Setters.Add(new Setter(TextBlock.FontStretchProperty, fontStretch));
|
||||
}
|
||||
}
|
||||
|
||||
private ResourceDictionary GetThemeResourceDictionary(string theme)
|
||||
{
|
||||
var uri = GetThemePath(theme);
|
||||
|
|
@ -128,22 +270,22 @@ namespace Flow.Launcher.Core.Resource
|
|||
var fontWeight = FontHelper.GetFontWeightFromInvariantStringOrNormal(_settings.QueryBoxFontWeight);
|
||||
var fontStretch = FontHelper.GetFontStretchFromInvariantStringOrNormal(_settings.QueryBoxFontStretch);
|
||||
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
queryBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
|
||||
var caretBrushPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Any(x => x.Property.Name == "CaretBrush");
|
||||
var foregroundPropertyValue = queryBoxStyle.Setters.OfType<Setter>().Where(x => x.Property.Name == "Foreground")
|
||||
.Select(x => x.Value).FirstOrDefault();
|
||||
if (!caretBrushPropertyValue && foregroundPropertyValue != null) //otherwise BaseQueryBoxStyle will handle styling
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBox.CaretBrushProperty, foregroundPropertyValue));
|
||||
queryBoxStyle.Setters.Add(new Setter(TextBoxBase.CaretBrushProperty, foregroundPropertyValue));
|
||||
|
||||
// Query suggestion box's font style is aligned with query box
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontFamilyProperty, fontFamily));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStyleProperty, fontStyle));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontWeightProperty, fontWeight));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(TextBox.FontStretchProperty, fontStretch));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontFamilyProperty, fontFamily));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStyleProperty, fontStyle));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontWeightProperty, fontWeight));
|
||||
querySuggestionBoxStyle.Setters.Add(new Setter(Control.FontStretchProperty, fontStretch));
|
||||
}
|
||||
|
||||
if (dict["ItemTitleStyle"] is Style resultItemStyle &&
|
||||
|
|
@ -180,7 +322,7 @@ namespace Flow.Launcher.Core.Resource
|
|||
/* Ignore Theme Window Width and use setting */
|
||||
var windowStyle = dict["WindowStyle"] as Style;
|
||||
var width = _settings.WindowSize;
|
||||
windowStyle.Setters.Add(new Setter(Window.WidthProperty, width));
|
||||
windowStyle.Setters.Add(new Setter(FrameworkElement.WidthProperty, width));
|
||||
return dict;
|
||||
}
|
||||
|
||||
|
|
@ -265,11 +407,12 @@ namespace Flow.Launcher.Core.Resource
|
|||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new DirectoryNotFoundException("Theme path can't be found <{path}>");
|
||||
throw new DirectoryNotFoundException($"Theme path can't be found <{path}>");
|
||||
|
||||
// reload all resources even if the theme itself hasn't changed in order to pickup changes
|
||||
// to things like fonts
|
||||
UpdateResourceDictionary(GetResourceDictionary(theme));
|
||||
// Retrieve theme resource – always use the resource with font settings applied.
|
||||
var resourceDict = GetResourceDictionary(theme);
|
||||
|
||||
UpdateResourceDictionary(resourceDict);
|
||||
|
||||
_settings.Theme = theme;
|
||||
|
||||
|
|
@ -280,10 +423,11 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
|
||||
BlurEnabled = IsBlurTheme();
|
||||
//if (_settings.UseDropShadowEffect)
|
||||
// AddDropShadowEffectToCurrentTheme();
|
||||
//Win32Helper.SetBlurForWindow(Application.Current.MainWindow, BlurEnabled);
|
||||
_ = SetBlurForWindowAsync();
|
||||
|
||||
// Can only apply blur but here also apply drop shadow effect to avoid possible drop shadow effect issues
|
||||
_ = RefreshFrameAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
|
|
@ -305,7 +449,6 @@ namespace Flow.Launcher.Core.Resource
|
|||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -481,17 +624,14 @@ namespace Flow.Launcher.Core.Resource
|
|||
|
||||
private void SetBlurForWindow(string theme, BackdropTypes backdropType)
|
||||
{
|
||||
var dict = GetThemeResourceDictionary(theme);
|
||||
if (dict == null)
|
||||
return;
|
||||
var dict = GetResourceDictionary(theme);
|
||||
if (dict == null) return;
|
||||
|
||||
var windowBorderStyle = dict.Contains("WindowBorderStyle") ? dict["WindowBorderStyle"] as Style : null;
|
||||
if (windowBorderStyle == null)
|
||||
return;
|
||||
if (windowBorderStyle == null) return;
|
||||
|
||||
Window mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null)
|
||||
return;
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow == null) return;
|
||||
|
||||
// Check if the theme supports blur
|
||||
bool hasBlur = dict.Contains("ThemeBlurEnabled") && dict["ThemeBlurEnabled"] is bool b && b;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
public static readonly string MissingImgIcon = Path.Combine(ImagesDirectory, "app_missing_img.png");
|
||||
public static readonly string LoadingImgIcon = Path.Combine(ImagesDirectory, "loading.png");
|
||||
public static readonly string ImageIcon = Path.Combine(ImagesDirectory, "image.png");
|
||||
public static readonly string HistoryIcon = Path.Combine(ImagesDirectory, "history.png");
|
||||
|
||||
public static string PythonPath;
|
||||
public static string NodePath;
|
||||
|
|
|
|||
|
|
@ -67,8 +67,6 @@ namespace Flow.Launcher.Infrastructure
|
|||
return explorerWindows.Zip(zOrders).MinBy(x => x.Second).First;
|
||||
}
|
||||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the z-order for one or more windows atomically with respect to each other. In Windows, smaller z-order is higher. If the window is not top level, the z order is returned as -1.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -46,4 +46,16 @@ GetMonitorInfo
|
|||
MONITORINFOEXW
|
||||
|
||||
WM_ENTERSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
WM_EXITSIZEMOVE
|
||||
|
||||
GetKeyboardLayout
|
||||
GetWindowThreadProcessId
|
||||
ActivateKeyboardLayout
|
||||
GetKeyboardLayoutList
|
||||
PostMessage
|
||||
WM_INPUTLANGCHANGEREQUEST
|
||||
INPUTLANGCHANGE_FORWARD
|
||||
LOCALE_TRANSIENT_KEYBOARD1
|
||||
LOCALE_TRANSIENT_KEYBOARD2
|
||||
LOCALE_TRANSIENT_KEYBOARD3
|
||||
LOCALE_TRANSIENT_KEYBOARD4
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Microsoft.Win32;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Point = System.Windows.Point;
|
||||
|
||||
namespace Flow.Launcher.Infrastructure
|
||||
{
|
||||
|
|
@ -63,7 +67,7 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="window"></param>
|
||||
/// <param name="cornerType">DoNotRound, Round, RoundSmall, Default</param>
|
||||
|
|
@ -317,5 +321,172 @@ namespace Flow.Launcher.Infrastructure
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyboard Layout
|
||||
|
||||
private const string UserProfileRegistryPath = @"Control Panel\International\User Profile";
|
||||
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/70feba9f-294e-491e-b6eb-56532684c37f
|
||||
private const string EnglishLanguageTag = "en";
|
||||
|
||||
private static readonly string[] ImeLanguageTags =
|
||||
{
|
||||
"zh", // Chinese
|
||||
"ja", // Japanese
|
||||
"ko", // Korean
|
||||
};
|
||||
|
||||
private const uint KeyboardLayoutLoWord = 0xFFFF;
|
||||
|
||||
// Store the previous keyboard layout
|
||||
private static HKL _previousLayout;
|
||||
|
||||
/// <summary>
|
||||
/// Switches the keyboard layout to English if available.
|
||||
/// </summary>
|
||||
/// <param name="backupPrevious">If true, the current keyboard layout will be stored for later restoration.</param>
|
||||
/// <exception cref="Win32Exception">Thrown when there's an error getting the window thread process ID.</exception>
|
||||
public static unsafe void SwitchToEnglishKeyboardLayout(bool backupPrevious)
|
||||
{
|
||||
// Find an installed English layout
|
||||
var enHKL = FindEnglishKeyboardLayout();
|
||||
|
||||
// No installed English layout found
|
||||
if (enHKL == HKL.Null) return;
|
||||
|
||||
// Get the current foreground window
|
||||
var hwnd = PInvoke.GetForegroundWindow();
|
||||
if (hwnd == HWND.Null) return;
|
||||
|
||||
// Get the current foreground window thread ID
|
||||
var threadId = PInvoke.GetWindowThreadProcessId(hwnd);
|
||||
if (threadId == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
|
||||
// If the current layout has an IME mode, disable it without switching to another layout.
|
||||
// This is needed because for languages with IME mode, Flow Launcher just temporarily disables
|
||||
// the IME mode instead of switching to another layout.
|
||||
var currentLayout = PInvoke.GetKeyboardLayout(threadId);
|
||||
var currentLangId = (uint)currentLayout.Value & KeyboardLayoutLoWord;
|
||||
foreach (var langTag in ImeLanguageTags)
|
||||
{
|
||||
if (GetLanguageTag(currentLangId).StartsWith(langTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup current keyboard layout
|
||||
if (backupPrevious) _previousLayout = currentLayout;
|
||||
|
||||
// Switch to English layout
|
||||
PInvoke.ActivateKeyboardLayout(enHKL, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the previously backed-up keyboard layout.
|
||||
/// If it wasn't backed up or has already been restored, this method does nothing.
|
||||
/// </summary>
|
||||
public static void RestorePreviousKeyboardLayout()
|
||||
{
|
||||
if (_previousLayout == HKL.Null) return;
|
||||
|
||||
var hwnd = PInvoke.GetForegroundWindow();
|
||||
if (hwnd == HWND.Null) return;
|
||||
|
||||
PInvoke.PostMessage(
|
||||
hwnd,
|
||||
PInvoke.WM_INPUTLANGCHANGEREQUEST,
|
||||
PInvoke.INPUTLANGCHANGE_FORWARD,
|
||||
_previousLayout.Value
|
||||
);
|
||||
|
||||
_previousLayout = HKL.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an installed English keyboard layout.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="Win32Exception"></exception>
|
||||
private static unsafe HKL FindEnglishKeyboardLayout()
|
||||
{
|
||||
// Get the number of keyboard layouts
|
||||
int count = PInvoke.GetKeyboardLayoutList(0, null);
|
||||
if (count <= 0) return HKL.Null;
|
||||
|
||||
// Get all keyboard layouts
|
||||
var handles = new HKL[count];
|
||||
fixed (HKL* h = handles)
|
||||
{
|
||||
var result = PInvoke.GetKeyboardLayoutList(count, h);
|
||||
if (result == 0) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
// Look for any English keyboard layout
|
||||
foreach (var hkl in handles)
|
||||
{
|
||||
// The lower word contains the language identifier
|
||||
var langId = (uint)hkl.Value & KeyboardLayoutLoWord;
|
||||
var langTag = GetLanguageTag(langId);
|
||||
|
||||
// Check if it's an English layout
|
||||
if (langTag.StartsWith(EnglishLanguageTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return hkl;
|
||||
}
|
||||
}
|
||||
|
||||
return HKL.Null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the
|
||||
/// <see href="https://learn.microsoft.com/globalization/locale/standard-locale-names">
|
||||
/// BCP 47 language tag
|
||||
/// </see>
|
||||
/// of the current input language.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Edited from: https://github.com/dotnet/winforms
|
||||
/// </remarks>
|
||||
private static string GetLanguageTag(uint langId)
|
||||
{
|
||||
// We need to convert the language identifier to a language tag, because they are deprecated and may have a
|
||||
// transient value.
|
||||
// https://learn.microsoft.com/globalization/locale/other-locale-names#lcid
|
||||
// https://learn.microsoft.com/windows/win32/winmsg/wm-inputlangchange#remarks
|
||||
//
|
||||
// It turns out that the LCIDToLocaleName API, which is used inside CultureInfo, may return incorrect
|
||||
// language tags for transient language identifiers. For example, it returns "nqo-GN" and "jv-Java-ID"
|
||||
// instead of the "nqo" and "jv-Java" (as seen in the Get-WinUserLanguageList PowerShell cmdlet).
|
||||
//
|
||||
// Try to extract proper language tag from registry as a workaround approved by a Windows team.
|
||||
// https://github.com/dotnet/winforms/pull/8573#issuecomment-1542600949
|
||||
//
|
||||
// NOTE: this logic may break in future versions of Windows since it is not documented.
|
||||
if (langId is PInvoke.LOCALE_TRANSIENT_KEYBOARD1
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD2
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD3
|
||||
or PInvoke.LOCALE_TRANSIENT_KEYBOARD4)
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(UserProfileRegistryPath);
|
||||
if (key?.GetValue("Languages") is string[] languages)
|
||||
{
|
||||
foreach (string language in languages)
|
||||
{
|
||||
using var subKey = key.OpenSubKey(language);
|
||||
if (subKey?.GetValue("TransientLangId") is int transientLangId
|
||||
&& transientLangId == langId)
|
||||
{
|
||||
return language;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CultureInfo.GetCultureInfo((int)langId).Name;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,26 @@ namespace Flow.Launcher
|
|||
{
|
||||
public partial class App : IDisposable, ISingleInstanceApp
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
public static IPublicAPI API { get; private set; }
|
||||
private const string Unique = "Flow.Launcher_Unique_Application_Mutex";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private static bool _disposed;
|
||||
private MainWindow _mainWindow;
|
||||
private readonly MainViewModel _mainVM;
|
||||
private readonly Settings _settings;
|
||||
|
||||
// To prevent two disposals running at the same time.
|
||||
private static readonly object _disposingLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public App()
|
||||
{
|
||||
// Initialize settings
|
||||
|
|
@ -79,27 +94,33 @@ namespace Flow.Launcher
|
|||
{
|
||||
API = Ioc.Default.GetRequiredService<IPublicAPI>();
|
||||
_settings.Initialize();
|
||||
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowErrorMsgBoxAndFailFast("Cannot initialize api and settings, please open new issue in Flow.Launcher", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Local function
|
||||
static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowErrorMsgBoxAndFailFast(string message, Exception e)
|
||||
{
|
||||
// Firstly show users the message
|
||||
MessageBox.Show(e.ToString(), message, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
#endregion
|
||||
|
||||
// Flow cannot construct its App instance, so ensure Flow crashes w/ the exception info.
|
||||
Environment.FailFast(message, e);
|
||||
}
|
||||
#region Main
|
||||
|
||||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance(Unique))
|
||||
if (SingleInstance<App>.InitializeAsFirstInstance())
|
||||
{
|
||||
using var application = new App();
|
||||
application.InitializeComponent();
|
||||
|
|
@ -107,6 +128,10 @@ namespace Flow.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region App Events
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
private async void OnStartup(object sender, StartupEventArgs e)
|
||||
|
|
@ -127,21 +152,26 @@ namespace Flow.Launcher
|
|||
|
||||
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
|
||||
|
||||
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||
Ioc.Default.GetRequiredService<Internationalization>().ChangeLanguage(_settings.Language);
|
||||
|
||||
PluginManager.LoadPlugins(_settings.PluginSettings);
|
||||
|
||||
// Register ResultsUpdated event after all plugins are loaded
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
|
||||
|
||||
Http.Proxy = _settings.Proxy;
|
||||
|
||||
await PluginManager.InitializePluginsAsync();
|
||||
|
||||
// Change language after all plugins are initialized because we need to update plugin title based on their api
|
||||
// TODO: Clean InternationalizationManager.Instance and InternationalizationManager.Instance.GetTranslation in future
|
||||
await Ioc.Default.GetRequiredService<Internationalization>().InitializeLanguageAsync();
|
||||
|
||||
await imageLoadertask;
|
||||
|
||||
var window = new MainWindow();
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}");
|
||||
|
||||
Current.MainWindow = window;
|
||||
Current.MainWindow = _mainWindow;
|
||||
Current.MainWindow.Title = Constant.FlowLauncher;
|
||||
|
||||
HotKeyMapper.Initialize();
|
||||
|
|
@ -158,8 +188,7 @@ namespace Flow.Launcher
|
|||
AutoUpdates();
|
||||
|
||||
API.SaveAppAllSettings();
|
||||
Log.Info(
|
||||
"|App.OnStartup|End Flow Launcher startup ---------------------------------------------------- ");
|
||||
Log.Info("|App.OnStartup|End Flow Launcher startup ----------------------------------------------------");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +221,6 @@ namespace Flow.Launcher
|
|||
}
|
||||
}
|
||||
|
||||
//[Conditional("RELEASE")]
|
||||
private void AutoUpdates()
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
|
|
@ -210,11 +238,29 @@ namespace Flow.Launcher
|
|||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Register Events
|
||||
|
||||
private void RegisterExitEvents()
|
||||
{
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
|
||||
Current.Exit += (s, e) => Dispose();
|
||||
Current.SessionEnding += (s, e) => Dispose();
|
||||
AppDomain.CurrentDomain.ProcessExit += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Process Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.Exit += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Application Exit");
|
||||
Dispose();
|
||||
};
|
||||
|
||||
Current.SessionEnding += (s, e) =>
|
||||
{
|
||||
Log.Info("|App.RegisterExitEvents|Session Ending");
|
||||
Dispose();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -235,20 +281,60 @@ namespace Flow.Launcher
|
|||
AppDomain.CurrentDomain.UnhandledException += ErrorReporting.UnhandledExceptionHandle;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
// if sessionending is called, exit proverbially be called when log off / shutdown
|
||||
// but if sessionending is not called, exit won't be called when log off / shutdown
|
||||
if (!_disposed)
|
||||
// Prevent two disposes at the same time.
|
||||
lock (_disposingLock)
|
||||
{
|
||||
API.SaveAppAllSettings();
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
Stopwatch.Normal("|App.Dispose|Dispose cost", () =>
|
||||
{
|
||||
Log.Info("|App.Dispose|Begin Flow Launcher dispose ----------------------------------------------------");
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Dispose needs to be called on the main Windows thread,
|
||||
// since some resources owned by the thread need to be disposed.
|
||||
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
|
||||
_mainVM?.Dispose();
|
||||
}
|
||||
|
||||
Log.Info("|App.Dispose|End Flow Launcher dispose ----------------------------------------------------");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISingleInstanceApp
|
||||
|
||||
public void OnSecondAppStarted()
|
||||
{
|
||||
Ioc.Default.GetRequiredService<MainViewModel>().Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ using System.Windows;
|
|||
// modified to allow single instace restart
|
||||
namespace Flow.Launcher.Helper
|
||||
{
|
||||
public interface ISingleInstanceApp
|
||||
{
|
||||
void OnSecondAppStarted();
|
||||
}
|
||||
public interface ISingleInstanceApp
|
||||
{
|
||||
void OnSecondAppStarted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class checks to make sure that only one instance of
|
||||
|
|
@ -24,9 +24,7 @@ namespace Flow.Launcher.Helper
|
|||
/// running as Administrator, can activate it with command line arguments.
|
||||
/// For most apps, this will not be much of an issue.
|
||||
/// </remarks>
|
||||
public static class SingleInstance<TApplication>
|
||||
where TApplication: Application , ISingleInstanceApp
|
||||
|
||||
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -39,11 +37,12 @@ namespace Flow.Launcher.Helper
|
|||
/// Suffix to the channel name.
|
||||
/// </summary>
|
||||
private const string ChannelNameSuffix = "SingeInstanceIPCChannel";
|
||||
private const string InstanceMutexName = "Flow.Launcher_Unique_Application_Mutex";
|
||||
|
||||
/// <summary>
|
||||
/// Application mutex.
|
||||
/// </summary>
|
||||
internal static Mutex singleInstanceMutex;
|
||||
internal static Mutex SingleInstanceMutex { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -54,24 +53,23 @@ namespace Flow.Launcher.Helper
|
|||
/// If not, activates the first instance.
|
||||
/// </summary>
|
||||
/// <returns>True if this is the first instance of the application.</returns>
|
||||
public static bool InitializeAsFirstInstance( string uniqueName )
|
||||
public static bool InitializeAsFirstInstance()
|
||||
{
|
||||
// Build unique application Id and the IPC channel name.
|
||||
string applicationIdentifier = uniqueName + Environment.UserName;
|
||||
string applicationIdentifier = InstanceMutexName + Environment.UserName;
|
||||
|
||||
string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||
string channelName = string.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix);
|
||||
|
||||
// Create mutex based on unique application Id to check if this is the first instance of the application.
|
||||
bool firstInstance;
|
||||
singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance);
|
||||
SingleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance);
|
||||
if (firstInstance)
|
||||
{
|
||||
_ = CreateRemoteService(channelName);
|
||||
_ = CreateRemoteServiceAsync(channelName);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = SignalFirstInstance(channelName);
|
||||
_ = SignalFirstInstanceAsync(channelName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +79,7 @@ namespace Flow.Launcher.Helper
|
|||
/// </summary>
|
||||
public static void Cleanup()
|
||||
{
|
||||
singleInstanceMutex?.ReleaseMutex();
|
||||
SingleInstanceMutex?.ReleaseMutex();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -93,22 +91,19 @@ namespace Flow.Launcher.Helper
|
|||
/// Once receives signal from client, will activate first instance.
|
||||
/// </summary>
|
||||
/// <param name="channelName">Application's IPC channel name.</param>
|
||||
private static async Task CreateRemoteService(string channelName)
|
||||
private static async Task CreateRemoteServiceAsync(string channelName)
|
||||
{
|
||||
using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In))
|
||||
using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In);
|
||||
while (true)
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
// Wait for connection to the pipe
|
||||
await pipeServer.WaitForConnectionAsync();
|
||||
if (Application.Current != null)
|
||||
{
|
||||
// Do an asynchronous call to ActivateFirstInstance function
|
||||
Application.Current.Dispatcher.Invoke(ActivateFirstInstance);
|
||||
}
|
||||
// Disconect client
|
||||
pipeServer.Disconnect();
|
||||
}
|
||||
// Wait for connection to the pipe
|
||||
await pipeServer.WaitForConnectionAsync();
|
||||
|
||||
// Do an asynchronous call to ActivateFirstInstance function
|
||||
Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
|
||||
|
||||
// Disconect client
|
||||
pipeServer.Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,25 +114,13 @@ namespace Flow.Launcher.Helper
|
|||
/// <param name="args">
|
||||
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
||||
/// </param>
|
||||
private static async Task SignalFirstInstance(string channelName)
|
||||
private static async Task SignalFirstInstanceAsync(string channelName)
|
||||
{
|
||||
// Create a client pipe connected to server
|
||||
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out))
|
||||
{
|
||||
// Connect to the available pipe
|
||||
await pipeClient.ConnectAsync(0);
|
||||
}
|
||||
}
|
||||
using NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out);
|
||||
|
||||
/// <summary>
|
||||
/// Callback for activating first instance of the application.
|
||||
/// </summary>
|
||||
/// <param name="arg">Callback argument.</param>
|
||||
/// <returns>Always null.</returns>
|
||||
private static object ActivateFirstInstanceCallback(object o)
|
||||
{
|
||||
ActivateFirstInstance();
|
||||
return null;
|
||||
// Connect to the available pipe
|
||||
await pipeClient.ConnectAsync(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
AllowDrop="True"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Closed="OnClosed"
|
||||
Closing="OnClosing"
|
||||
Deactivated="OnDeactivated"
|
||||
Icon="Images/app.png"
|
||||
|
|
@ -215,7 +216,7 @@
|
|||
|
||||
<Border MouseDown="OnMouseDown" Style="{DynamicResource WindowBorderStyle}">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<Grid>
|
||||
<Grid x:Name="QueryBoxArea">
|
||||
<Border MinHeight="30" Style="{DynamicResource QueryBoxBgStyle}">
|
||||
<Grid>
|
||||
<TextBox
|
||||
|
|
@ -338,7 +339,7 @@
|
|||
Y2="0" />
|
||||
</Grid>
|
||||
|
||||
<Grid ClipToBounds="True">
|
||||
<Grid x:Name="MiddleSeparatorArea" ClipToBounds="True">
|
||||
<ContentControl>
|
||||
<ContentControl.Style>
|
||||
<Style TargetType="ContentControl">
|
||||
|
|
@ -378,7 +379,7 @@
|
|||
</ContentControl>
|
||||
</Grid>
|
||||
|
||||
<Border Style="{DynamicResource WindowRadius}">
|
||||
<Border x:Name="ResultPreviewAreaBoarder" Style="{DynamicResource WindowRadius}">
|
||||
<Border.Clip>
|
||||
<MultiBinding Converter="{StaticResource BorderClipConverter}">
|
||||
<Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
|
||||
|
|
@ -386,12 +387,14 @@
|
|||
<Binding Path="CornerRadius" RelativeSource="{RelativeSource Self}" />
|
||||
</MultiBinding>
|
||||
</Border.Clip>
|
||||
<Grid>
|
||||
|
||||
<Grid x:Name="ResultPreviewArea">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" MinWidth="80" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="0.85*" MinWidth="244" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel
|
||||
x:Name="ResultArea"
|
||||
Grid.Column="0"
|
||||
|
|
@ -418,7 +421,9 @@
|
|||
RightClickResultCommand="{Binding RightClickResultCommand}" />
|
||||
</ContentControl>
|
||||
</StackPanel>
|
||||
|
||||
<GridSplitter
|
||||
x:Name="PreviewMiddleSeparator"
|
||||
Grid.Column="1"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center"
|
||||
|
|
@ -432,6 +437,7 @@
|
|||
</ControlTemplate>
|
||||
</GridSplitter.Template>
|
||||
</GridSplitter>
|
||||
|
||||
<Grid
|
||||
x:Name="Preview"
|
||||
Grid.Column="2"
|
||||
|
|
@ -441,7 +447,7 @@
|
|||
<Border
|
||||
MinHeight="380"
|
||||
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
||||
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
|
||||
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
|
||||
Visibility="{Binding ShowDefaultPreview}">
|
||||
<Grid
|
||||
Margin="0 0 10 5"
|
||||
|
|
@ -518,7 +524,7 @@
|
|||
MaxHeight="{Binding ElementName=ResultListBox, Path=ActualHeight}"
|
||||
Padding="0 0 10 10"
|
||||
d:DataContext="{d:DesignInstance vm:ResultViewModel}"
|
||||
DataContext="{Binding SelectedItem, ElementName=ResultListBox}"
|
||||
DataContext="{Binding PreviewSelectedItem, Mode=OneWay}"
|
||||
Visibility="{Binding ShowCustomizedPreview}">
|
||||
<ContentControl Content="{Binding Result.PreviewPanel.Value}" />
|
||||
</Border>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ using Screen = System.Windows.Forms.Screen;
|
|||
|
||||
namespace Flow.Launcher
|
||||
{
|
||||
public partial class MainWindow
|
||||
public partial class MainWindow : IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -39,24 +39,29 @@ namespace Flow.Launcher
|
|||
private NotifyIcon _notifyIcon;
|
||||
|
||||
// Window Context Menu
|
||||
private readonly ContextMenu contextMenu = new();
|
||||
private readonly ContextMenu _contextMenu = new();
|
||||
private readonly MainViewModel _viewModel;
|
||||
|
||||
// Window Event : Key Event
|
||||
private bool isArrowKeyPressed = false;
|
||||
// Window Event: Close Event
|
||||
private bool _canClose = false;
|
||||
// Window Event: Key Event
|
||||
private bool _isArrowKeyPressed = false;
|
||||
|
||||
// Window Sound Effects
|
||||
private MediaPlayer animationSoundWMP;
|
||||
private SoundPlayer animationSoundWPF;
|
||||
|
||||
// Window WndProc
|
||||
private HwndSource _hwndSource;
|
||||
private int _initialWidth;
|
||||
private int _initialHeight;
|
||||
|
||||
// Window Animation
|
||||
private const double DefaultRightMargin = 66; //* this value from base.xaml
|
||||
private bool _animating;
|
||||
private bool _isClockPanelAnimating = false; // 애니메이션 실행 중인지 여부
|
||||
private bool _isClockPanelAnimating = false;
|
||||
|
||||
// IDisposable
|
||||
private bool _disposed = false;
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -70,7 +75,7 @@ namespace Flow.Launcher
|
|||
DataContext = _viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
UpdatePosition(true);
|
||||
UpdatePosition();
|
||||
|
||||
InitSoundEffects();
|
||||
DataObject.AddPastingHandler(QueryTextBox, QueryTextBox_OnPaste);
|
||||
|
|
@ -85,8 +90,8 @@ namespace Flow.Launcher
|
|||
private void OnSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
var handle = Win32Helper.GetWindowHandle(this, true);
|
||||
var win = HwndSource.FromHwnd(handle);
|
||||
win.AddHook(WndProc);
|
||||
_hwndSource = HwndSource.FromHwnd(handle);
|
||||
_hwndSource.AddHook(WndProc);
|
||||
Win32Helper.HideFromAltTab(this);
|
||||
Win32Helper.DisableControlBox(this);
|
||||
}
|
||||
|
|
@ -98,12 +103,15 @@ namespace Flow.Launcher
|
|||
{
|
||||
_settings.FirstLaunch = false;
|
||||
App.API.SaveAppAllSettings();
|
||||
/* Set Backdrop Type to Acrylic for Windows 11 when First Launch. Default is None. */
|
||||
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
|
||||
_settings.BackdropType = BackdropTypes.Acrylic;
|
||||
var WelcomeWindow = new WelcomeWindow();
|
||||
WelcomeWindow.Show();
|
||||
}
|
||||
|
||||
// Hide window if need
|
||||
UpdatePosition(true);
|
||||
UpdatePosition();
|
||||
if (_settings.HideOnStartup)
|
||||
{
|
||||
_viewModel.Hide();
|
||||
|
|
@ -130,7 +138,7 @@ namespace Flow.Launcher
|
|||
InitProgressbarAnimation();
|
||||
|
||||
// Force update position
|
||||
UpdatePosition(true);
|
||||
UpdatePosition();
|
||||
|
||||
// Refresh frame
|
||||
await Ioc.Default.GetRequiredService<Theme>().RefreshFrameAsync();
|
||||
|
|
@ -140,7 +148,9 @@ namespace Flow.Launcher
|
|||
|
||||
// Since the default main window visibility is visible, so we need set focus during startup
|
||||
QueryTextBox.Focus();
|
||||
|
||||
// Set the initial state of the QueryTextBoxCursorMovedToEnd property
|
||||
// Without this part, when shown for the first time, switching the context menu does not move the cursor to the end.
|
||||
_viewModel.QueryTextCursorMovedToEnd = false;
|
||||
_viewModel.PropertyChanged += (o, e) =>
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
|
|
@ -156,7 +166,7 @@ namespace Flow.Launcher
|
|||
SoundPlay();
|
||||
}
|
||||
|
||||
UpdatePosition(false);
|
||||
UpdatePosition();
|
||||
_viewModel.ResetPreview();
|
||||
Activate();
|
||||
QueryTextBox.Focus();
|
||||
|
|
@ -213,34 +223,51 @@ namespace Flow.Launcher
|
|||
}
|
||||
};
|
||||
|
||||
// ✅ QueryTextBox.Text 변경 감지 (글자 수 1 이상일 때만 동작하도록 수정)
|
||||
// QueryTextBox.Text change detection (modified to only work when character count is 1 or higher)
|
||||
QueryTextBox.TextChanged += (sender, e) => UpdateClockPanelVisibility();
|
||||
|
||||
// ✅ ContextMenu.Visibility 변경 감지
|
||||
// Detecting ContextMenu.Visibility changes
|
||||
DependencyPropertyDescriptor
|
||||
.FromProperty(VisibilityProperty, typeof(ContextMenu))
|
||||
.AddValueChanged(ContextMenu, (s, e) => UpdateClockPanelVisibility());
|
||||
|
||||
// ✅ History.Visibility 변경 감지
|
||||
// Detect History.Visibility changes
|
||||
DependencyPropertyDescriptor
|
||||
.FromProperty(VisibilityProperty, typeof(StackPanel)) // History는 StackPanel이라고 가정
|
||||
.FromProperty(VisibilityProperty, typeof(StackPanel))
|
||||
.AddValueChanged(History, (s, e) => UpdateClockPanelVisibility());
|
||||
}
|
||||
|
||||
private async void OnClosing(object sender, CancelEventArgs e)
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
App.API.SaveAppAllSettings();
|
||||
e.Cancel = true;
|
||||
await PluginManager.DisposePluginsAsync();
|
||||
Notification.Uninstall();
|
||||
Environment.Exit(0);
|
||||
if (!_canClose)
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
App.API.SaveAppAllSettings();
|
||||
e.Cancel = true;
|
||||
await PluginManager.DisposePluginsAsync();
|
||||
Notification.Uninstall();
|
||||
// After plugins are all disposed, we can close the main window
|
||||
_canClose = true;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClosed(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_hwndSource.RemoveHook(WndProc);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignored
|
||||
}
|
||||
|
||||
_hwndSource = null;
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (_animating)
|
||||
return;
|
||||
if (_settings.SearchWindowScreen == SearchWindowScreens.RememberLastLaunchLocation)
|
||||
{
|
||||
_settings.WindowLeft = Left;
|
||||
|
|
@ -252,9 +279,11 @@ namespace Flow.Launcher
|
|||
{
|
||||
_settings.WindowLeft = Left;
|
||||
_settings.WindowTop = Top;
|
||||
|
||||
ClockPanel.Opacity = 0;
|
||||
SearchIcon.Opacity = 0;
|
||||
//This condition stops extra hide call when animator is on,
|
||||
|
||||
// This condition stops extra hide call when animator is on,
|
||||
// which causes the toggling to occasional hide instead of show.
|
||||
if (_viewModel.MainWindowVisibilityStatus)
|
||||
{
|
||||
|
|
@ -262,7 +291,6 @@ namespace Flow.Launcher
|
|||
// This also stops the mainwindow from flickering occasionally after Settings window is opened
|
||||
// and always after Settings window is closed.
|
||||
if (_settings.UseAnimation)
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
if (_settings.HideWhenDeactivated && !_viewModel.ExternalPreviewVisible)
|
||||
|
|
@ -278,12 +306,12 @@ namespace Flow.Launcher
|
|||
switch (e.Key)
|
||||
{
|
||||
case Key.Down:
|
||||
isArrowKeyPressed = true;
|
||||
_isArrowKeyPressed = true;
|
||||
_viewModel.SelectNextItemCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Up:
|
||||
isArrowKeyPressed = true;
|
||||
_isArrowKeyPressed = true;
|
||||
_viewModel.SelectPrevItemCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
|
@ -296,7 +324,7 @@ namespace Flow.Launcher
|
|||
e.Handled = true;
|
||||
break;
|
||||
case Key.Right:
|
||||
if (_viewModel.SelectedIsFromQueryResults()
|
||||
if (_viewModel.QueryResultsSelected()
|
||||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length
|
||||
&& !string.IsNullOrEmpty(QueryTextBox.Text))
|
||||
{
|
||||
|
|
@ -306,7 +334,7 @@ namespace Flow.Launcher
|
|||
|
||||
break;
|
||||
case Key.Left:
|
||||
if (!_viewModel.SelectedIsFromQueryResults() && QueryTextBox.CaretIndex == 0)
|
||||
if (!_viewModel.QueryResultsSelected() && QueryTextBox.CaretIndex == 0)
|
||||
{
|
||||
_viewModel.EscCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
|
|
@ -316,7 +344,7 @@ namespace Flow.Launcher
|
|||
case Key.Back:
|
||||
if (specialKeyState.CtrlPressed)
|
||||
{
|
||||
if (_viewModel.SelectedIsFromQueryResults()
|
||||
if (_viewModel.QueryResultsSelected()
|
||||
&& QueryTextBox.Text.Length > 0
|
||||
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
|
||||
{
|
||||
|
|
@ -341,13 +369,13 @@ namespace Flow.Launcher
|
|||
{
|
||||
if (e.Key == Key.Up || e.Key == Key.Down)
|
||||
{
|
||||
isArrowKeyPressed = false;
|
||||
_isArrowKeyPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (isArrowKeyPressed)
|
||||
if (_isArrowKeyPressed)
|
||||
{
|
||||
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
|
||||
}
|
||||
|
|
@ -517,11 +545,11 @@ namespace Flow.Launcher
|
|||
gamemode.ToolTip = App.API.GetTranslation("GameModeToolTip");
|
||||
positionreset.ToolTip = App.API.GetTranslation("PositionResetToolTip");
|
||||
|
||||
contextMenu.Items.Add(open);
|
||||
contextMenu.Items.Add(gamemode);
|
||||
contextMenu.Items.Add(positionreset);
|
||||
contextMenu.Items.Add(settings);
|
||||
contextMenu.Items.Add(exit);
|
||||
_contextMenu.Items.Add(open);
|
||||
_contextMenu.Items.Add(gamemode);
|
||||
_contextMenu.Items.Add(positionreset);
|
||||
_contextMenu.Items.Add(settings);
|
||||
_contextMenu.Items.Add(exit);
|
||||
|
||||
_notifyIcon.MouseClick += (o, e) =>
|
||||
{
|
||||
|
|
@ -532,14 +560,14 @@ namespace Flow.Launcher
|
|||
break;
|
||||
case MouseButtons.Right:
|
||||
|
||||
contextMenu.IsOpen = true;
|
||||
_contextMenu.IsOpen = true;
|
||||
// Get context menu handle and bring it to the foreground
|
||||
if (PresentationSource.FromVisual(contextMenu) is HwndSource hwndSource)
|
||||
if (PresentationSource.FromVisual(_contextMenu) is HwndSource hwndSource)
|
||||
{
|
||||
Win32Helper.SetForegroundWindow(hwndSource.Handle);
|
||||
}
|
||||
|
||||
contextMenu.Focus();
|
||||
_contextMenu.Focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
@ -547,7 +575,7 @@ namespace Flow.Launcher
|
|||
|
||||
private void UpdateNotifyIconText()
|
||||
{
|
||||
var menu = contextMenu;
|
||||
var menu = _contextMenu;
|
||||
((MenuItem)menu.Items[0]).Header = App.API.GetTranslation("iconTrayOpen") +
|
||||
" (" + _settings.Hotkey + ")";
|
||||
((MenuItem)menu.Items[1]).Header = App.API.GetTranslation("GameMode");
|
||||
|
|
@ -560,13 +588,8 @@ namespace Flow.Launcher
|
|||
|
||||
#region Window Position
|
||||
|
||||
private void UpdatePosition(bool force)
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (_animating && !force)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize call twice to work around multi-display alignment issue- https://github.com/Flow-Launcher/Flow.Launcher/issues/2910
|
||||
InitializePosition();
|
||||
InitializePosition();
|
||||
|
|
@ -740,19 +763,17 @@ namespace Flow.Launcher
|
|||
|
||||
private void WindowAnimation()
|
||||
{
|
||||
if (_animating)
|
||||
return;
|
||||
_isArrowKeyPressed = true;
|
||||
|
||||
isArrowKeyPressed = true;
|
||||
_animating = true;
|
||||
UpdatePosition(false);
|
||||
UpdatePosition();
|
||||
|
||||
var opacity = _settings.UseAnimation ? 0.0 : 1.0;
|
||||
ClockPanel.Opacity = opacity;
|
||||
SearchIcon.Opacity = opacity;
|
||||
|
||||
ClockPanel.Opacity = 0;
|
||||
SearchIcon.Opacity = 0;
|
||||
|
||||
var clocksb = new Storyboard();
|
||||
var iconsb = new Storyboard();
|
||||
CircleEase easing = new CircleEase { EasingMode = EasingMode.EaseInOut };
|
||||
var easing = new CircleEase { EasingMode = EasingMode.EaseInOut };
|
||||
|
||||
var animationLength = _settings.AnimationSpeed switch
|
||||
{
|
||||
|
|
@ -780,7 +801,7 @@ namespace Flow.Launcher
|
|||
FillBehavior = FillBehavior.HoldEnd
|
||||
};
|
||||
|
||||
double TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0);
|
||||
var TargetIconOpacity = GetOpacityFromStyle(SearchIcon.Style, 1.0);
|
||||
|
||||
var IconOpacity = new DoubleAnimation
|
||||
{
|
||||
|
|
@ -791,7 +812,7 @@ namespace Flow.Launcher
|
|||
FillBehavior = FillBehavior.HoldEnd
|
||||
};
|
||||
|
||||
double rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right;
|
||||
var rightMargin = GetThicknessFromStyle(ClockPanel.Style, new Thickness(0, 0, DefaultRightMargin, 0)).Right;
|
||||
|
||||
var thicknessAnimation = new ThicknessAnimation
|
||||
{
|
||||
|
|
@ -818,16 +839,11 @@ namespace Flow.Launcher
|
|||
clocksb.Children.Add(ClockOpacity);
|
||||
iconsb.Children.Add(IconMotion);
|
||||
iconsb.Children.Add(IconOpacity);
|
||||
|
||||
clocksb.Completed += (_, _) => _animating = false;
|
||||
|
||||
_settings.WindowLeft = Left;
|
||||
isArrowKeyPressed = false;
|
||||
|
||||
if (QueryTextBox.Text.Length == 0)
|
||||
{
|
||||
clocksb.Begin(ClockPanel);
|
||||
}
|
||||
|
||||
_isArrowKeyPressed = false;
|
||||
|
||||
clocksb.Begin(ClockPanel);
|
||||
iconsb.Begin(SearchIcon);
|
||||
}
|
||||
|
||||
|
|
@ -990,5 +1006,30 @@ namespace Flow.Launcher
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_hwndSource?.Dispose();
|
||||
_notifyIcon?.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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@
|
|||
<SolidColorBrush x:Key="InfoBarWarningIcon" Color="#FCE100" />
|
||||
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#433519" />
|
||||
<SolidColorBrush x:Key="InfoBarBD" Color="#19000000" />
|
||||
|
||||
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
|
||||
|
||||
<SolidColorBrush x:Key="ButtonOutBorder" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#3f3f3f" />
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
<SolidColorBrush x:Key="InfoBarWarningBG" Color="#FFF4CE" />
|
||||
<SolidColorBrush x:Key="InfoBarBD" Color="#0F000000" />
|
||||
|
||||
<SolidColorBrush x:Key="MouseOverWindowCloseButtonForegroundBrush" Color="#ffffff" />
|
||||
|
||||
<SolidColorBrush x:Key="ButtonOutBorder" Color="#e5e5e5" />
|
||||
<SolidColorBrush x:Key="ButtonInsideBorder" Color="#d3d3d3" />
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
|
||||
Settings.BackdropType = value;
|
||||
|
||||
// Can only apply blur because drop shadow effect is not supported with backdrop
|
||||
// So drop shadow effect has been disabled
|
||||
_ = _theme.SetBlurForWindowAsync();
|
||||
|
||||
OnPropertyChanged(nameof(IsDropShadowEnabled));
|
||||
|
|
@ -342,7 +344,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
set
|
||||
{
|
||||
Settings.QueryBoxFont = value.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -364,7 +366,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
Settings.QueryBoxFontStretch = value.Stretch.ToString();
|
||||
Settings.QueryBoxFontWeight = value.Weight.ToString();
|
||||
Settings.QueryBoxFontStyle = value.Style.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +388,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
set
|
||||
{
|
||||
Settings.ResultFont = value.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +410,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
Settings.ResultFontStretch = value.Stretch.ToString();
|
||||
Settings.ResultFontWeight = value.Weight.ToString();
|
||||
Settings.ResultFontStyle = value.Style.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -432,7 +434,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
set
|
||||
{
|
||||
Settings.ResultSubFont = value.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -453,7 +455,7 @@ public partial class SettingsPaneThemeViewModel : BaseModel
|
|||
Settings.ResultSubFontStretch = value.Stretch.ToString();
|
||||
Settings.ResultSubFontWeight = value.Weight.ToString();
|
||||
Settings.ResultSubFontStyle = value.Style.ToString();
|
||||
_theme.ChangeTheme();
|
||||
_theme.UpdateFonts();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
Loaded="OnLoaded"
|
||||
MouseDown="window_MouseDown"
|
||||
ResizeMode="CanResize"
|
||||
SnapsToDevicePixels="True"
|
||||
UseLayoutRounding="True"
|
||||
StateChanged="Window_StateChanged"
|
||||
Top="{Binding SettingWindowTop, Mode=TwoWay}"
|
||||
WindowStartupLocation="Manual"
|
||||
|
|
@ -54,6 +56,7 @@
|
|||
Width="16"
|
||||
Height="16"
|
||||
Margin="10 4 4 4"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="/Images/app.png" />
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
<Style x:Key="BaseQueryBoxStyle" TargetType="{x:Type TextBox}">
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="FontSize" Value="28" />
|
||||
<Setter Property="FontWeight" Value="Regular" />
|
||||
<Setter Property="Margin" Value="16 7 0 7" />
|
||||
<Setter Property="Padding" Value="0 0 68 0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
|
|
@ -181,12 +180,10 @@
|
|||
<Style x:Key="BaseItemTitleStyle" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="#FFFFF8" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="Medium" />
|
||||
</Style>
|
||||
<Style x:Key="BaseItemSubTitleStyle" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="#D9D9D4" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ElementName=SubTitle, UpdateSourceTrigger=PropertyChanged, Path=Text.Length}" Value="0">
|
||||
<Setter Property="Height" Value="0" />
|
||||
|
|
@ -218,7 +215,6 @@
|
|||
<Style x:Key="BaseItemTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="#FFFFF8" />
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
</Style>
|
||||
<Style x:Key="BaseItemSubTitleSelectedStyle" TargetType="{x:Type TextBlock}">
|
||||
<Setter Property="Foreground" Value="#D9D9D4" />
|
||||
|
|
@ -394,6 +390,7 @@
|
|||
<Style.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
|
||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<MultiDataTrigger.Setters>
|
||||
|
|
@ -435,12 +432,12 @@
|
|||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="PreviewBorderStyle"
|
||||
BasedOn="{StaticResource BasePreviewBorderStyle}"
|
||||
TargetType="{x:Type Border}">
|
||||
<Setter Property="BorderBrush" Value="Gray" />
|
||||
|
||||
</Style>
|
||||
|
||||
<Style x:Key="PreviewArea" TargetType="{x:Type Grid}">
|
||||
|
|
@ -450,8 +447,8 @@
|
|||
<MultiDataTrigger.Conditions>
|
||||
<!--
|
||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Visibility}" Value="Collapsed" />
|
||||
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />
|
||||
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />-->
|
||||
<Condition Binding="{Binding ElementName=ContextMenu, Path=Visibility}" Value="Collapsed" />-->
|
||||
<Condition Binding="{Binding ElementName=History, Path=Visibility}" Value="Collapsed" />
|
||||
<Condition Binding="{Binding ElementName=ResultListBox, Path=Items.Count}" Value="0" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<MultiDataTrigger.Setters>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@
|
|||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="SeparatorStyle"
|
||||
BasedOn="{StaticResource BaseSeparatorStyle}"
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Windows.Input;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using CommunityToolkit.Mvvm.DependencyInjection;
|
||||
|
|
@ -27,7 +27,7 @@ using Microsoft.VisualStudio.Threading;
|
|||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
public partial class MainViewModel : BaseModel, ISavable
|
||||
public partial class MainViewModel : BaseModel, ISavable, IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -49,6 +49,8 @@ namespace Flow.Launcher.ViewModel
|
|||
private ChannelWriter<ResultsForUpdate> _resultsUpdateChannelWriter;
|
||||
private Task _resultsViewUpdateTask;
|
||||
|
||||
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
|
@ -162,13 +164,26 @@ namespace Flow.Launcher.ViewModel
|
|||
switch (args.PropertyName)
|
||||
{
|
||||
case nameof(Results.SelectedItem):
|
||||
UpdatePreview();
|
||||
_selectedItemFromQueryResults = true;
|
||||
PreviewSelectedItem = Results.SelectedItem;
|
||||
_ = UpdatePreviewAsync();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
History.PropertyChanged += (_, args) =>
|
||||
{
|
||||
switch (args.PropertyName)
|
||||
{
|
||||
case nameof(History.SelectedItem):
|
||||
_selectedItemFromQueryResults = false;
|
||||
PreviewSelectedItem = History.SelectedItem;
|
||||
_ = UpdatePreviewAsync();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
RegisterViewUpdate();
|
||||
RegisterResultsUpdatedEvent();
|
||||
_ = RegisterClockAndDateUpdateAsync();
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +228,7 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private void RegisterResultsUpdatedEvent()
|
||||
public void RegisterResultsUpdatedEvent()
|
||||
{
|
||||
foreach (var pair in PluginManager.GetPluginsForInterface<IResultUpdated>())
|
||||
{
|
||||
|
|
@ -266,7 +281,7 @@ namespace Flow.Launcher.ViewModel
|
|||
[RelayCommand]
|
||||
private void LoadHistory()
|
||||
{
|
||||
if (SelectedIsFromQueryResults())
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
SelectedResults = History;
|
||||
History.SelectedIndex = _history.Items.Count - 1;
|
||||
|
|
@ -280,7 +295,7 @@ namespace Flow.Launcher.ViewModel
|
|||
[RelayCommand]
|
||||
public void ReQuery()
|
||||
{
|
||||
if (SelectedIsFromQueryResults())
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
_ = QueryResultsAsync(isReQuery: true);
|
||||
}
|
||||
|
|
@ -321,7 +336,7 @@ namespace Flow.Launcher.ViewModel
|
|||
[RelayCommand]
|
||||
private void LoadContextMenu()
|
||||
{
|
||||
if (SelectedIsFromQueryResults())
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
// When switch to ContextMenu from QueryResults, but no item being chosen, should do nothing
|
||||
// i.e. Shift+Enter/Ctrl+O right after Alt + Space should do nothing
|
||||
|
|
@ -351,7 +366,7 @@ namespace Flow.Launcher.ViewModel
|
|||
private void AutocompleteQuery()
|
||||
{
|
||||
var result = SelectedResults.SelectedItem?.Result;
|
||||
if (result != null && SelectedIsFromQueryResults()) // SelectedItem returns null if selection is empty.
|
||||
if (result != null && QueryResultsSelected()) // SelectedItem returns null if selection is empty.
|
||||
{
|
||||
var autoCompleteText = result.Title;
|
||||
|
||||
|
|
@ -403,7 +418,7 @@ namespace Flow.Launcher.ViewModel
|
|||
})
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (SelectedIsFromQueryResults())
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
_userSelectedRecord.Add(result);
|
||||
// origin query is null when user select the context menu item directly of one item from query list
|
||||
|
|
@ -482,7 +497,7 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
if (_history.Items.Count > 0
|
||||
&& QueryText == string.Empty
|
||||
&& SelectedIsFromQueryResults())
|
||||
&& QueryResultsSelected())
|
||||
{
|
||||
lastHistoryIndex = 1;
|
||||
ReverseHistory();
|
||||
|
|
@ -502,7 +517,7 @@ namespace Flow.Launcher.ViewModel
|
|||
[RelayCommand]
|
||||
private void Esc()
|
||||
{
|
||||
if (!SelectedIsFromQueryResults())
|
||||
if (!QueryResultsSelected())
|
||||
{
|
||||
SelectedResults = Results;
|
||||
}
|
||||
|
|
@ -514,7 +529,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public void BackToQueryResults()
|
||||
{
|
||||
if (!SelectedIsFromQueryResults())
|
||||
if (!QueryResultsSelected())
|
||||
{
|
||||
SelectedResults = Results;
|
||||
}
|
||||
|
|
@ -615,7 +630,15 @@ namespace Flow.Launcher.ViewModel
|
|||
/// <param name="isReQuery">Force query even when Query Text doesn't change</param>
|
||||
public void ChangeQueryText(string queryText, bool isReQuery = false)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
_ = ChangeQueryTextAsync(queryText, isReQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async version of <see cref="ChangeQueryText"/>
|
||||
/// </summary>
|
||||
private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false)
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
BackToQueryResults();
|
||||
|
||||
|
|
@ -629,7 +652,7 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
else if (isReQuery)
|
||||
{
|
||||
Query(isReQuery: true);
|
||||
await QueryAsync(isReQuery: true);
|
||||
}
|
||||
|
||||
QueryTextCursorMovedToEnd = true;
|
||||
|
|
@ -645,13 +668,16 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
private ResultsViewModel SelectedResults
|
||||
{
|
||||
get { return _selectedResults; }
|
||||
get => _selectedResults;
|
||||
set
|
||||
{
|
||||
var isReturningFromQueryResults = QueryResultsSelected();
|
||||
var isReturningFromContextMenu = ContextMenuSelected();
|
||||
var isReturningFromHistory = HistorySelected();
|
||||
_selectedResults = value;
|
||||
if (SelectedIsFromQueryResults())
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
Results.Visibility = Visibility.Visible;
|
||||
ContextMenu.Visibility = Visibility.Collapsed;
|
||||
History.Visibility = Visibility.Collapsed;
|
||||
|
||||
|
|
@ -669,10 +695,27 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
ChangeQueryText(_queryTextBeforeLeaveResults);
|
||||
}
|
||||
|
||||
// If we are returning from history and we have not set select item yet,
|
||||
// we need to clear the preview selected item
|
||||
if (isReturningFromHistory && _selectedItemFromQueryResults.HasValue && (!_selectedItemFromQueryResults.Value))
|
||||
{
|
||||
PreviewSelectedItem = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Results.Visibility = Visibility.Collapsed;
|
||||
if (HistorySelected())
|
||||
{
|
||||
ContextMenu.Visibility = Visibility.Collapsed;
|
||||
History.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
ContextMenu.Visibility = Visibility.Visible;
|
||||
History.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
_queryTextBeforeLeaveResults = QueryText;
|
||||
|
||||
// Because of Fody's optimization
|
||||
|
|
@ -687,6 +730,16 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
QueryText = string.Empty;
|
||||
}
|
||||
|
||||
if (HistorySelected())
|
||||
{
|
||||
// If we are returning from query results and we have not set select item yet,
|
||||
// we need to clear the preview selected item
|
||||
if (isReturningFromQueryResults && _selectedItemFromQueryResults.HasValue && _selectedItemFromQueryResults.Value)
|
||||
{
|
||||
PreviewSelectedItem = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_selectedResults.Visibility = Visibility.Visible;
|
||||
|
|
@ -786,6 +839,22 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Preview
|
||||
|
||||
private static readonly int ResultAreaColumnPreviewShown = 1;
|
||||
private static readonly int ResultAreaColumnPreviewHidden = 3;
|
||||
|
||||
private bool? _selectedItemFromQueryResults;
|
||||
|
||||
private ResultViewModel _previewSelectedItem;
|
||||
public ResultViewModel PreviewSelectedItem
|
||||
{
|
||||
get => _previewSelectedItem;
|
||||
set
|
||||
{
|
||||
_previewSelectedItem = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool InternalPreviewVisible
|
||||
{
|
||||
get
|
||||
|
|
@ -804,18 +873,14 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly int ResultAreaColumnPreviewShown = 1;
|
||||
|
||||
private static readonly int ResultAreaColumnPreviewHidden = 3;
|
||||
|
||||
public int ResultAreaColumn { get; set; } = ResultAreaColumnPreviewShown;
|
||||
|
||||
// This is not a reliable indicator of whether external preview is visible due to the
|
||||
// ability of manually closing/exiting the external preview program which, does not inform flow that
|
||||
// preview is no longer available.
|
||||
public bool ExternalPreviewVisible { get; set; } = false;
|
||||
public bool ExternalPreviewVisible { get; private set; }
|
||||
|
||||
private void ShowPreview()
|
||||
private async Task ShowPreviewAsync()
|
||||
{
|
||||
var useExternalPreview = PluginManager.UseExternalPreview();
|
||||
|
||||
|
|
@ -826,13 +891,15 @@ namespace Flow.Launcher.ViewModel
|
|||
// Internal preview may still be on when user switches to external
|
||||
if (InternalPreviewVisible)
|
||||
HideInternalPreview();
|
||||
OpenExternalPreview(path);
|
||||
|
||||
_ = OpenExternalPreviewAsync(path);
|
||||
break;
|
||||
|
||||
case true
|
||||
when !CanExternalPreviewSelectedResult(out var _):
|
||||
if (ExternalPreviewVisible)
|
||||
CloseExternalPreview();
|
||||
await CloseExternalPreviewAsync();
|
||||
|
||||
ShowInternalPreview();
|
||||
break;
|
||||
|
||||
|
|
@ -845,7 +912,7 @@ namespace Flow.Launcher.ViewModel
|
|||
private void HidePreview()
|
||||
{
|
||||
if (PluginManager.UseExternalPreview())
|
||||
CloseExternalPreview();
|
||||
_ = CloseExternalPreviewAsync();
|
||||
|
||||
if (InternalPreviewVisible)
|
||||
HideInternalPreview();
|
||||
|
|
@ -860,31 +927,31 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
else
|
||||
{
|
||||
ShowPreview();
|
||||
_ = ShowPreviewAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenExternalPreview(string path, bool sendFailToast = true)
|
||||
private async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
_ = PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
||||
await PluginManager.OpenExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
||||
ExternalPreviewVisible = true;
|
||||
}
|
||||
|
||||
private void CloseExternalPreview()
|
||||
private async Task CloseExternalPreviewAsync()
|
||||
{
|
||||
_ = PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
|
||||
await PluginManager.CloseExternalPreviewAsync().ConfigureAwait(false);
|
||||
ExternalPreviewVisible = false;
|
||||
}
|
||||
|
||||
private static void SwitchExternalPreview(string path, bool sendFailToast = true)
|
||||
private static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
|
||||
{
|
||||
_ = PluginManager.SwitchExternalPreviewAsync(path,sendFailToast).ConfigureAwait(false);
|
||||
await PluginManager.SwitchExternalPreviewAsync(path, sendFailToast).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void ShowInternalPreview()
|
||||
{
|
||||
ResultAreaColumn = ResultAreaColumnPreviewShown;
|
||||
Results.SelectedItem?.LoadPreviewImage();
|
||||
PreviewSelectedItem?.LoadPreviewImage();
|
||||
}
|
||||
|
||||
private void HideInternalPreview()
|
||||
|
|
@ -898,20 +965,18 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
case true
|
||||
when PluginManager.AllowAlwaysPreview() && CanExternalPreviewSelectedResult(out var path):
|
||||
OpenExternalPreview(path);
|
||||
_ = OpenExternalPreviewAsync(path);
|
||||
break;
|
||||
|
||||
case true:
|
||||
ShowInternalPreview();
|
||||
break;
|
||||
|
||||
case false:
|
||||
HidePreview();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreview()
|
||||
private async Task UpdatePreviewAsync()
|
||||
{
|
||||
switch (PluginManager.UseExternalPreview())
|
||||
{
|
||||
|
|
@ -919,46 +984,55 @@ namespace Flow.Launcher.ViewModel
|
|||
when CanExternalPreviewSelectedResult(out var path):
|
||||
if (ExternalPreviewVisible)
|
||||
{
|
||||
SwitchExternalPreview(path, false);
|
||||
_ = SwitchExternalPreviewAsync(path, false);
|
||||
}
|
||||
else if (InternalPreviewVisible)
|
||||
{
|
||||
HideInternalPreview();
|
||||
OpenExternalPreview(path);
|
||||
_ = OpenExternalPreviewAsync(path);
|
||||
}
|
||||
break;
|
||||
|
||||
case true
|
||||
when !CanExternalPreviewSelectedResult(out var _):
|
||||
if (ExternalPreviewVisible)
|
||||
{
|
||||
CloseExternalPreview();
|
||||
await CloseExternalPreviewAsync();
|
||||
ShowInternalPreview();
|
||||
}
|
||||
break;
|
||||
|
||||
case false
|
||||
when InternalPreviewVisible:
|
||||
Results.SelectedItem?.LoadPreviewImage();
|
||||
PreviewSelectedItem?.LoadPreviewImage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanExternalPreviewSelectedResult(out string path)
|
||||
{
|
||||
path = Results.SelectedItem?.Result?.Preview.FilePath;
|
||||
path = QueryResultsPreviewed() ? Results.SelectedItem?.Result?.Preview.FilePath : string.Empty;
|
||||
return !string.IsNullOrEmpty(path);
|
||||
}
|
||||
|
||||
private bool QueryResultsPreviewed()
|
||||
{
|
||||
var previewed = PreviewSelectedItem == Results.SelectedItem;
|
||||
return previewed;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query
|
||||
|
||||
public void Query(bool isReQuery = false)
|
||||
private void Query(bool isReQuery = false)
|
||||
{
|
||||
if (SelectedIsFromQueryResults())
|
||||
_ = QueryAsync(isReQuery);
|
||||
}
|
||||
|
||||
private async Task QueryAsync(bool isReQuery = false)
|
||||
{
|
||||
if (QueryResultsSelected())
|
||||
{
|
||||
_ = QueryResultsAsync(isReQuery);
|
||||
await QueryResultsAsync(isReQuery);
|
||||
}
|
||||
else if (ContextMenuSelected())
|
||||
{
|
||||
|
|
@ -992,10 +1066,10 @@ namespace Flow.Launcher.ViewModel
|
|||
(
|
||||
r =>
|
||||
{
|
||||
var match = StringMatcher.FuzzySearch(query, r.Title);
|
||||
var match = App.API.FuzzySearch(query, r.Title);
|
||||
if (!match.IsSearchPrecisionScoreMet())
|
||||
{
|
||||
match = StringMatcher.FuzzySearch(query, r.SubTitle);
|
||||
match = App.API.FuzzySearch(query, r.SubTitle);
|
||||
}
|
||||
|
||||
if (!match.IsSearchPrecisionScoreMet()) return false;
|
||||
|
|
@ -1028,11 +1102,16 @@ namespace Flow.Launcher.ViewModel
|
|||
Title = string.Format(title, h.Query),
|
||||
SubTitle = string.Format(time, h.ExecutedDateTime),
|
||||
IcoPath = "Images\\history.png",
|
||||
Preview = new Result.PreviewInfo
|
||||
{
|
||||
PreviewImagePath = Constant.HistoryIcon,
|
||||
Description = string.Format(time, h.ExecutedDateTime)
|
||||
},
|
||||
OriginQuery = new Query { RawQuery = h.Query },
|
||||
Action = _ =>
|
||||
{
|
||||
SelectedResults = Results;
|
||||
ChangeQueryText(h.Query);
|
||||
App.API.ChangeQuery(h.Query);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -1043,8 +1122,8 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
var filtered = results.Where
|
||||
(
|
||||
r => StringMatcher.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() ||
|
||||
StringMatcher.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet()
|
||||
r => App.API.FuzzySearch(query, r.Title).IsSearchPrecisionScoreMet() ||
|
||||
App.API.FuzzySearch(query, r.SubTitle).IsSearchPrecisionScoreMet()
|
||||
).ToList();
|
||||
History.AddResults(filtered, id);
|
||||
}
|
||||
|
|
@ -1054,8 +1133,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<Result> _emptyResult = new List<Result>();
|
||||
|
||||
private async Task QueryResultsAsync(bool isReQuery = false, bool reSelect = true)
|
||||
{
|
||||
_updateSource?.Cancel();
|
||||
|
|
@ -1319,7 +1396,7 @@ namespace Flow.Launcher.ViewModel
|
|||
return menu;
|
||||
}
|
||||
|
||||
internal bool SelectedIsFromQueryResults()
|
||||
internal bool QueryResultsSelected()
|
||||
{
|
||||
var selected = SelectedResults == Results;
|
||||
return selected;
|
||||
|
|
@ -1353,106 +1430,6 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 📌 Remove DWM Cloak (Make the window visible normally)
|
||||
Win32Helper.DWMSetCloakForWindow(mainWindow, false);
|
||||
|
||||
// 📌 Restore UI elements
|
||||
mainWindow.ClockPanel.Visibility = Visibility.Visible;
|
||||
//mainWindow.SearchIcon.Visibility = Visibility.Visible;
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// Update WPF properties
|
||||
MainWindowVisibility = Visibility.Visible;
|
||||
MainWindowOpacity = 1;
|
||||
MainWindowVisibilityStatus = true;
|
||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
public async void Hide()
|
||||
{
|
||||
lastHistoryIndex = 1;
|
||||
|
||||
if (ExternalPreviewVisible)
|
||||
{
|
||||
CloseExternalPreview();
|
||||
}
|
||||
|
||||
if (!SelectedIsFromQueryResults())
|
||||
{
|
||||
SelectedResults = Results;
|
||||
}
|
||||
|
||||
// 📌 Immediately apply text reset + force UI update
|
||||
if (Settings.LastQueryMode == LastQueryMode.Empty)
|
||||
{
|
||||
ChangeQueryText(string.Empty);
|
||||
await Task.Delay(1); // Wait for one frame to ensure UI reflects changes
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Application.Current.MainWindow.UpdateLayout(); // Force UI update
|
||||
});
|
||||
}
|
||||
|
||||
switch (Settings.LastQueryMode)
|
||||
{
|
||||
case LastQueryMode.Preserved:
|
||||
case LastQueryMode.Selected:
|
||||
LastQuerySelected = (Settings.LastQueryMode == LastQueryMode.Preserved);
|
||||
break;
|
||||
|
||||
case LastQueryMode.ActionKeywordPreserved:
|
||||
case LastQueryMode.ActionKeywordSelected:
|
||||
var newQuery = _lastQuery.ActionKeyword;
|
||||
if (!string.IsNullOrEmpty(newQuery))
|
||||
newQuery += " ";
|
||||
ChangeQueryText(newQuery);
|
||||
|
||||
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
|
||||
LastQuerySelected = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 📌 Set Opacity of icon and clock to 0 and apply Visibility.Hidden
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
mainWindow.ClockPanel.Opacity = 0;
|
||||
mainWindow.SearchIcon.Opacity = 0;
|
||||
mainWindow.ClockPanel.Visibility = Visibility.Hidden;
|
||||
//mainWindow.SearchIcon.Visibility = Visibility.Hidden;
|
||||
SearchIconVisibility = Visibility.Hidden;
|
||||
|
||||
// Force UI update
|
||||
mainWindow.ClockPanel.UpdateLayout();
|
||||
mainWindow.SearchIcon.UpdateLayout();
|
||||
}, DispatcherPriority.Render);
|
||||
|
||||
// 📌 Apply DWM Cloak (Completely hide the window)
|
||||
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
||||
}
|
||||
|
||||
await Task.Delay(50);
|
||||
|
||||
// Update WPF properties
|
||||
//MainWindowOpacity = 0;
|
||||
MainWindowVisibilityStatus = false;
|
||||
MainWindowVisibility = Visibility.Collapsed;
|
||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false });
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
/// <summary>
|
||||
/// Checks if Flow Launcher should ignore any hotkeys
|
||||
/// </summary>
|
||||
|
|
@ -1465,6 +1442,131 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
#region Public Methods
|
||||
|
||||
#pragma warning disable VSTHRD100 // Avoid async void methods
|
||||
|
||||
public async void Show()
|
||||
{
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 📌 Remove DWM Cloak (Make the window visible normally)
|
||||
Win32Helper.DWMSetCloakForWindow(mainWindow, false);
|
||||
|
||||
// Clock and SearchIcon hide when show situation
|
||||
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
|
||||
mainWindow.ClockPanel.Opacity = opacity;
|
||||
mainWindow.SearchIcon.Opacity = opacity;
|
||||
|
||||
// QueryText sometimes is null when it is just initialized
|
||||
if (QueryText != null && QueryText.Length != 0)
|
||||
{
|
||||
mainWindow.ClockPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
mainWindow.ClockPanel.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
if (PluginIconSource != null)
|
||||
{
|
||||
mainWindow.SearchIcon.Opacity = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
SearchIconVisibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// 📌 Restore UI elements
|
||||
//mainWindow.SearchIcon.Visibility = Visibility.Visible;
|
||||
}
|
||||
}, DispatcherPriority.Render);
|
||||
|
||||
// Update WPF properties
|
||||
MainWindowVisibility = Visibility.Visible;
|
||||
MainWindowOpacity = 1;
|
||||
MainWindowVisibilityStatus = true;
|
||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = true });
|
||||
|
||||
if (StartWithEnglishMode)
|
||||
{
|
||||
Win32Helper.SwitchToEnglishKeyboardLayout(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Hide()
|
||||
{
|
||||
lastHistoryIndex = 1;
|
||||
|
||||
if (ExternalPreviewVisible)
|
||||
{
|
||||
await CloseExternalPreviewAsync();
|
||||
}
|
||||
|
||||
if (!QueryResultsSelected())
|
||||
{
|
||||
SelectedResults = Results;
|
||||
}
|
||||
|
||||
switch (Settings.LastQueryMode)
|
||||
{
|
||||
case LastQueryMode.Empty:
|
||||
await ChangeQueryTextAsync(string.Empty);
|
||||
break;
|
||||
case LastQueryMode.Preserved:
|
||||
case LastQueryMode.Selected:
|
||||
LastQuerySelected = Settings.LastQueryMode == LastQueryMode.Preserved;
|
||||
break;
|
||||
case LastQueryMode.ActionKeywordPreserved:
|
||||
case LastQueryMode.ActionKeywordSelected:
|
||||
var newQuery = _lastQuery.ActionKeyword;
|
||||
|
||||
if (!string.IsNullOrEmpty(newQuery))
|
||||
newQuery += " ";
|
||||
await ChangeQueryTextAsync(newQuery);
|
||||
|
||||
if (Settings.LastQueryMode == LastQueryMode.ActionKeywordSelected)
|
||||
LastQuerySelected = false;
|
||||
break;
|
||||
}
|
||||
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (Application.Current.MainWindow is MainWindow mainWindow)
|
||||
{
|
||||
// 📌 Set Opacity of icon and clock to 0 and apply Visibility.Hidden
|
||||
var opacity = Settings.UseAnimation ? 0.0 : 1.0;
|
||||
mainWindow.ClockPanel.Opacity = opacity;
|
||||
mainWindow.SearchIcon.Opacity = opacity;
|
||||
mainWindow.ClockPanel.Visibility = Visibility.Hidden;
|
||||
SearchIconVisibility = Visibility.Hidden;
|
||||
|
||||
// Force UI update
|
||||
mainWindow.ClockPanel.UpdateLayout();
|
||||
mainWindow.SearchIcon.UpdateLayout();
|
||||
|
||||
// 📌 Apply DWM Cloak (Completely hide the window)
|
||||
Win32Helper.DWMSetCloakForWindow(mainWindow, true);
|
||||
}
|
||||
});
|
||||
|
||||
if (StartWithEnglishMode)
|
||||
{
|
||||
Win32Helper.RestorePreviousKeyboardLayout();
|
||||
}
|
||||
|
||||
// Update WPF properties
|
||||
//MainWindowOpacity = 0;
|
||||
MainWindowVisibilityStatus = false;
|
||||
MainWindowVisibility = Visibility.Collapsed;
|
||||
VisibilityChanged?.Invoke(this, new VisibilityChangedEventArgs { IsVisible = false });
|
||||
}
|
||||
|
||||
#pragma warning restore VSTHRD100 // Avoid async void methods
|
||||
|
||||
/// <summary>
|
||||
/// Save history, user selected records and top most records
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
_historyItemsStorage.Save();
|
||||
|
|
@ -1542,5 +1644,35 @@ namespace Flow.Launcher.ViewModel
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_updateSource?.Dispose();
|
||||
_resultsUpdateChannelWriter?.Complete();
|
||||
if (_resultsViewUpdateTask?.IsCompleted == true)
|
||||
{
|
||||
_resultsViewUpdateTask.Dispose();
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing.Text;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
|
@ -6,25 +9,20 @@ using Flow.Launcher.Infrastructure.Image;
|
|||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.IO;
|
||||
using System.Drawing.Text;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
public class ResultViewModel : BaseModel
|
||||
{
|
||||
private static PrivateFontCollection fontCollection = new();
|
||||
private static Dictionary<string, string> fonts = new();
|
||||
private static readonly PrivateFontCollection FontCollection = new();
|
||||
private static readonly Dictionary<string, string> Fonts = new();
|
||||
|
||||
public ResultViewModel(Result result, Settings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (result == null) return;
|
||||
|
||||
Result = result;
|
||||
|
||||
if (Result.Glyph is { FontFamily: not null } glyph)
|
||||
|
|
@ -39,20 +37,20 @@ namespace Flow.Launcher.ViewModel
|
|||
fontFamilyPath = Path.Combine(Result.PluginDirectory, fontFamilyPath);
|
||||
}
|
||||
|
||||
if (fonts.ContainsKey(fontFamilyPath))
|
||||
if (Fonts.TryGetValue(fontFamilyPath, out var value))
|
||||
{
|
||||
Glyph = glyph with
|
||||
{
|
||||
FontFamily = fonts[fontFamilyPath]
|
||||
FontFamily = value
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
fontCollection.AddFontFile(fontFamilyPath);
|
||||
fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{fontCollection.Families[^1].Name}";
|
||||
FontCollection.AddFontFile(fontFamilyPath);
|
||||
Fonts[fontFamilyPath] = $"{Path.GetDirectoryName(fontFamilyPath)}/#{FontCollection.Families[^1].Name}";
|
||||
Glyph = glyph with
|
||||
{
|
||||
FontFamily = fonts[fontFamilyPath]
|
||||
FontFamily = Fonts[fontFamilyPath]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +59,6 @@ namespace Flow.Launcher.ViewModel
|
|||
Glyph = glyph;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
|
@ -95,14 +92,10 @@ namespace Flow.Launcher.ViewModel
|
|||
get
|
||||
{
|
||||
if (PreviewImageAvailable)
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fall back to icon
|
||||
return ShowIcon;
|
||||
}
|
||||
|
||||
// Fall back to icon
|
||||
return ShowIcon;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,9 +104,8 @@ namespace Flow.Launcher.ViewModel
|
|||
get
|
||||
{
|
||||
if (Result.RoundedIcon)
|
||||
{
|
||||
return IconXY / 2;
|
||||
}
|
||||
|
||||
return IconXY;
|
||||
}
|
||||
|
||||
|
|
@ -148,31 +140,40 @@ namespace Flow.Launcher.ViewModel
|
|||
? Result.SubTitle
|
||||
: Result.SubTitleToolTip;
|
||||
|
||||
private volatile bool ImageLoaded;
|
||||
private volatile bool PreviewImageLoaded;
|
||||
private volatile bool _imageLoaded;
|
||||
private volatile bool _previewImageLoaded;
|
||||
|
||||
private ImageSource image = ImageLoader.LoadingImage;
|
||||
private ImageSource previewImage = ImageLoader.LoadingImage;
|
||||
private ImageSource _image = ImageLoader.LoadingImage;
|
||||
private ImageSource _previewImage = ImageLoader.LoadingImage;
|
||||
|
||||
public ImageSource Image
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!ImageLoaded)
|
||||
if (!_imageLoaded)
|
||||
{
|
||||
ImageLoaded = true;
|
||||
_imageLoaded = true;
|
||||
_ = LoadImageAsync();
|
||||
}
|
||||
|
||||
return image;
|
||||
return _image;
|
||||
}
|
||||
private set => image = value;
|
||||
private set => _image = value;
|
||||
}
|
||||
|
||||
public ImageSource PreviewImage
|
||||
{
|
||||
get => previewImage;
|
||||
private set => previewImage = value;
|
||||
get
|
||||
{
|
||||
if (!_previewImageLoaded)
|
||||
{
|
||||
_previewImageLoaded = true;
|
||||
_ = LoadPreviewImageAsync();
|
||||
}
|
||||
|
||||
return _previewImage;
|
||||
}
|
||||
private set => _previewImage = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -188,8 +189,7 @@ namespace Flow.Launcher.ViewModel
|
|||
{
|
||||
try
|
||||
{
|
||||
var image = icon();
|
||||
return image;
|
||||
return icon();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -208,7 +208,7 @@ namespace Flow.Launcher.ViewModel
|
|||
var iconDelegate = Result.Icon;
|
||||
if (ImageLoader.TryGetValue(imagePath, false, out ImageSource img))
|
||||
{
|
||||
image = img;
|
||||
_image = img;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -223,7 +223,7 @@ namespace Flow.Launcher.ViewModel
|
|||
var iconDelegate = Result.Preview.PreviewDelegate ?? Result.Icon;
|
||||
if (ImageLoader.TryGetValue(imagePath, true, out ImageSource img))
|
||||
{
|
||||
previewImage = img;
|
||||
_previewImage = img;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -234,13 +234,10 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public void LoadPreviewImage()
|
||||
{
|
||||
if (ShowDefaultPreview == Visibility.Visible)
|
||||
if (ShowDefaultPreview == Visibility.Visible && !_previewImageLoaded && ShowPreviewImage == Visibility.Visible)
|
||||
{
|
||||
if (!PreviewImageLoaded && ShowPreviewImage == Visibility.Visible)
|
||||
{
|
||||
PreviewImageLoaded = true;
|
||||
_ = LoadPreviewImageAsync();
|
||||
}
|
||||
_previewImageLoaded = true;
|
||||
_ = LoadPreviewImageAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
using System;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
|
|
@ -10,6 +8,8 @@ using System.Windows.Controls;
|
|||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using Flow.Launcher.Infrastructure.UserSettings;
|
||||
using Flow.Launcher.Plugin;
|
||||
|
||||
namespace Flow.Launcher.ViewModel
|
||||
{
|
||||
|
|
@ -28,6 +28,7 @@ namespace Flow.Launcher.ViewModel
|
|||
Results = new ResultCollection();
|
||||
BindingOperations.EnableCollectionSynchronization(Results, _collectionLock);
|
||||
}
|
||||
|
||||
public ResultsViewModel(Settings settings) : this()
|
||||
{
|
||||
_settings = settings;
|
||||
|
|
@ -219,7 +220,6 @@ namespace Flow.Launcher.ViewModel
|
|||
if (newRawResults.Count == 0)
|
||||
return Results;
|
||||
|
||||
|
||||
var newResults = newRawResults.Select(r => new ResultViewModel(r, _settings));
|
||||
|
||||
return Results.Where(r => r.Result.PluginID != resultId)
|
||||
|
|
@ -241,6 +241,7 @@ namespace Flow.Launcher.ViewModel
|
|||
#endregion
|
||||
|
||||
#region FormattedText Dependency Property
|
||||
|
||||
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
|
||||
"FormattedText",
|
||||
typeof(Inline),
|
||||
|
|
@ -259,8 +260,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var textBlock = d as TextBlock;
|
||||
if (textBlock == null) return;
|
||||
if (d is not TextBlock textBlock) return;
|
||||
|
||||
var inline = (Inline)e.NewValue;
|
||||
|
||||
|
|
@ -269,6 +269,7 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
textBlock.Inlines.Add(inline);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public class ResultCollection : List<ResultViewModel>, INotifyCollectionChanged
|
||||
|
|
@ -279,7 +280,6 @@ namespace Flow.Launcher.ViewModel
|
|||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
|
||||
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
CollectionChanged?.Invoke(this, e);
|
||||
|
|
@ -297,6 +297,7 @@ namespace Flow.Launcher.ViewModel
|
|||
// wpf use DirectX / double buffered already, so just reset all won't cause ui flickering
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
private void AddAll(List<ResultViewModel> Items)
|
||||
{
|
||||
for (int i = 0; i < Items.Count; i++)
|
||||
|
|
@ -308,6 +309,7 @@ namespace Flow.Launcher.ViewModel
|
|||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, i));
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveAll(int Capacity = 512)
|
||||
{
|
||||
Clear();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
|
||||
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
|
||||
using Flow.Launcher.Plugin.PluginsManager.ViewModels;
|
||||
|
||||
namespace Flow.Launcher.Plugin.PluginsManager.Views
|
||||
{
|
||||
|
|
@ -8,15 +7,11 @@ namespace Flow.Launcher.Plugin.PluginsManager.Views
|
|||
/// </summary>
|
||||
public partial class PluginsManagerSettings
|
||||
{
|
||||
private readonly SettingsViewModel viewModel;
|
||||
|
||||
internal PluginsManagerSettings(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.viewModel = viewModel;
|
||||
|
||||
this.DataContext = viewModel;
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
|
|
@ -58,7 +59,6 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Infrastructure\Flow.Launcher.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\Flow.Launcher.Plugin\Flow.Launcher.Plugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_name">Process Killer</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_plugin_description">Kill running processes from Flow Launcher</system:String>
|
||||
|
|
@ -9,4 +10,6 @@
|
|||
<system:String x:Key="flowlauncher_plugin_processkiller_kill_all_count">kill {0} processes</system:String>
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_kill_instances">kill all instances</system:String>
|
||||
|
||||
<system:String x:Key="flowlauncher_plugin_processkiller_put_visible_window_process_top">Put processes with visible windows on the top</system:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
|
@ -1,18 +1,27 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Flow.Launcher.Infrastructure;
|
||||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
|
||||
using Flow.Launcher.Plugin.ProcessKiller.Views;
|
||||
|
||||
namespace Flow.Launcher.Plugin.ProcessKiller
|
||||
{
|
||||
public class Main : IPlugin, IPluginI18n, IContextMenu
|
||||
public class Main : IPlugin, IPluginI18n, IContextMenu, ISettingProvider
|
||||
{
|
||||
private ProcessHelper processHelper = new ProcessHelper();
|
||||
private readonly ProcessHelper processHelper = new();
|
||||
|
||||
private static PluginInitContext _context;
|
||||
|
||||
internal Settings Settings;
|
||||
|
||||
private SettingsViewModel _viewModel;
|
||||
|
||||
public void Init(PluginInitContext context)
|
||||
{
|
||||
_context = context;
|
||||
Settings = context.API.LoadSettingJsonStorage<Settings>();
|
||||
_viewModel = new SettingsViewModel(Settings);
|
||||
}
|
||||
|
||||
public List<Result> Query(Query query)
|
||||
|
|
@ -48,7 +57,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
{
|
||||
foreach (var p in similarProcesses)
|
||||
{
|
||||
processHelper.TryKill(p);
|
||||
processHelper.TryKill(_context, p);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -62,16 +71,72 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
|
||||
private List<Result> CreateResultsFromQuery(Query query)
|
||||
{
|
||||
string termToSearch = query.Search;
|
||||
var processlist = processHelper.GetMatchingProcesses(termToSearch);
|
||||
|
||||
if (!processlist.Any())
|
||||
// Get all non-system processes
|
||||
var allPocessList = processHelper.GetMatchingProcesses();
|
||||
if (!allPocessList.Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
// Filter processes based on search term
|
||||
var searchTerm = query.Search;
|
||||
var processlist = new List<ProcessResult>();
|
||||
var processWindowTitle = ProcessHelper.GetProcessesWithNonEmptyWindowTitle();
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
foreach (var p in allPocessList)
|
||||
{
|
||||
var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
|
||||
|
||||
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
|
||||
{
|
||||
// Add score to prioritize processes with visible windows
|
||||
// And use window title for those processes
|
||||
processlist.Add(new ProcessResult(p, Settings.PutVisibleWindowProcessesTop ? 200 : 0, windowTitle, null, progressNameIdTitle));
|
||||
}
|
||||
else
|
||||
{
|
||||
processlist.Add(new ProcessResult(p, 0, progressNameIdTitle, null, progressNameIdTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var p in allPocessList)
|
||||
{
|
||||
var progressNameIdTitle = ProcessHelper.GetProcessNameIdTitle(p);
|
||||
|
||||
if (processWindowTitle.TryGetValue(p.Id, out var windowTitle))
|
||||
{
|
||||
// Get max score from searching process name, window title and process id
|
||||
var windowTitleMatch = _context.API.FuzzySearch(searchTerm, windowTitle);
|
||||
var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
|
||||
var score = Math.Max(windowTitleMatch.Score, processNameIdMatch.Score);
|
||||
if (score > 0)
|
||||
{
|
||||
// Add score to prioritize processes with visible windows
|
||||
// And use window title for those processes
|
||||
if (Settings.PutVisibleWindowProcessesTop)
|
||||
{
|
||||
score += 200;
|
||||
}
|
||||
processlist.Add(new ProcessResult(p, score, windowTitle,
|
||||
score == windowTitleMatch.Score ? windowTitleMatch : null, progressNameIdTitle));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var processNameIdMatch = _context.API.FuzzySearch(searchTerm, progressNameIdTitle);
|
||||
var score = processNameIdMatch.Score;
|
||||
if (score > 0)
|
||||
{
|
||||
processlist.Add(new ProcessResult(p, score, progressNameIdTitle, processNameIdMatch, progressNameIdTitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<Result>();
|
||||
foreach (var pr in processlist)
|
||||
{
|
||||
var p = pr.Process;
|
||||
|
|
@ -79,28 +144,30 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
results.Add(new Result()
|
||||
{
|
||||
IcoPath = path,
|
||||
Title = p.ProcessName + " - " + p.Id,
|
||||
Title = pr.Title,
|
||||
TitleToolTip = pr.Tooltip,
|
||||
SubTitle = path,
|
||||
TitleHighlightData = StringMatcher.FuzzySearch(termToSearch, p.ProcessName).MatchData,
|
||||
TitleHighlightData = pr.TitleMatch?.MatchData,
|
||||
Score = pr.Score,
|
||||
ContextData = p.ProcessName,
|
||||
AutoCompleteText = $"{_context.CurrentPluginMetadata.ActionKeyword}{Plugin.Query.TermSeparator}{p.ProcessName}",
|
||||
Action = (c) =>
|
||||
{
|
||||
processHelper.TryKill(p);
|
||||
processHelper.TryKill(_context, p);
|
||||
// Re-query to refresh process list
|
||||
_context.API.ChangeQuery(query.RawQuery, true);
|
||||
_context.API.ReQuery();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Order results by process name for processes without visible windows
|
||||
var sortedResults = results.OrderBy(x => x.Title).ToList();
|
||||
|
||||
// When there are multiple results AND all of them are instances of the same executable
|
||||
// add a quick option to kill them all at the top of the results.
|
||||
var firstResult = sortedResults.FirstOrDefault(x => !string.IsNullOrEmpty(x.SubTitle));
|
||||
if (processlist.Count > 1 && !string.IsNullOrEmpty(termToSearch) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
|
||||
if (processlist.Count > 1 && !string.IsNullOrEmpty(searchTerm) && sortedResults.All(r => r.SubTitle == firstResult?.SubTitle))
|
||||
{
|
||||
sortedResults.Insert(1, new Result()
|
||||
{
|
||||
|
|
@ -112,10 +179,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
{
|
||||
foreach (var p in processlist)
|
||||
{
|
||||
processHelper.TryKill(p.Process);
|
||||
processHelper.TryKill(_context, p.Process);
|
||||
}
|
||||
// Re-query to refresh process list
|
||||
_context.API.ChangeQuery(query.RawQuery, true);
|
||||
_context.API.ReQuery();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
@ -123,5 +190,10 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
|
||||
return sortedResults;
|
||||
}
|
||||
|
||||
public Control CreateSettingPanel()
|
||||
{
|
||||
return new SettingsControl(_viewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
QueryFullProcessImageName
|
||||
OpenProcess
|
||||
OpenProcess
|
||||
EnumWindows
|
||||
GetWindowTextLength
|
||||
GetWindowText
|
||||
IsWindowVisible
|
||||
GetWindowThreadProcessId
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
using Flow.Launcher.Infrastructure;
|
||||
using Flow.Launcher.Infrastructure.Logger;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Threading;
|
||||
|
|
@ -13,7 +12,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
{
|
||||
internal class ProcessHelper
|
||||
{
|
||||
private readonly HashSet<string> _systemProcessList = new HashSet<string>()
|
||||
private readonly HashSet<string> _systemProcessList = new()
|
||||
{
|
||||
"conhost",
|
||||
"svchost",
|
||||
|
|
@ -31,46 +30,96 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
"explorer"
|
||||
};
|
||||
|
||||
private bool IsSystemProcess(Process p) => _systemProcessList.Contains(p.ProcessName.ToLower());
|
||||
private const string FlowLauncherProcessName = "Flow.Launcher";
|
||||
|
||||
private bool IsSystemProcessOrFlowLauncher(Process p) =>
|
||||
_systemProcessList.Contains(p.ProcessName.ToLower()) ||
|
||||
string.Equals(p.ProcessName, FlowLauncherProcessName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a ProcessResult for evey running non-system process whose name matches the given searchTerm
|
||||
/// Get title based on process name and id
|
||||
/// </summary>
|
||||
public List<ProcessResult> GetMatchingProcesses(string searchTerm)
|
||||
public static string GetProcessNameIdTitle(Process p)
|
||||
{
|
||||
var processlist = new List<ProcessResult>();
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(p.ProcessName);
|
||||
sb.Append(" - ");
|
||||
sb.Append(p.Id);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a Process for evey running non-system process
|
||||
/// </summary>
|
||||
public List<Process> GetMatchingProcesses()
|
||||
{
|
||||
var processlist = new List<Process>();
|
||||
|
||||
foreach (var p in Process.GetProcesses())
|
||||
{
|
||||
if (IsSystemProcess(p)) continue;
|
||||
if (IsSystemProcessOrFlowLauncher(p)) continue;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
// show all non-system processes
|
||||
processlist.Add(new ProcessResult(p, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
var score = StringMatcher.FuzzySearch(searchTerm, p.ProcessName + p.Id).Score;
|
||||
if (score > 0)
|
||||
{
|
||||
processlist.Add(new ProcessResult(p, score));
|
||||
}
|
||||
}
|
||||
processlist.Add(p);
|
||||
}
|
||||
|
||||
return processlist;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of process IDs and their window titles for processes that have a visible main window with a non-empty title.
|
||||
/// </summary>
|
||||
public static unsafe Dictionary<int, string> GetProcessesWithNonEmptyWindowTitle()
|
||||
{
|
||||
var processDict = new Dictionary<int, string>();
|
||||
PInvoke.EnumWindows((hWnd, _) =>
|
||||
{
|
||||
var windowTitle = GetWindowTitle(hWnd);
|
||||
if (!string.IsNullOrWhiteSpace(windowTitle) && PInvoke.IsWindowVisible(hWnd))
|
||||
{
|
||||
uint processId = 0;
|
||||
var result = PInvoke.GetWindowThreadProcessId(hWnd, &processId);
|
||||
if (result == 0u || processId == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var process = Process.GetProcessById((int)processId);
|
||||
if (!processDict.ContainsKey((int)processId))
|
||||
{
|
||||
processDict.Add((int)processId, windowTitle);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
|
||||
return processDict;
|
||||
}
|
||||
|
||||
private static unsafe string GetWindowTitle(HWND hwnd)
|
||||
{
|
||||
var capacity = PInvoke.GetWindowTextLength(hwnd) + 1;
|
||||
int length;
|
||||
Span<char> buffer = capacity < 1024 ? stackalloc char[capacity] : new char[capacity];
|
||||
fixed (char* pBuffer = buffer)
|
||||
{
|
||||
// If the window has no title bar or text, if the title bar is empty,
|
||||
// or if the window or control handle is invalid, the return value is zero.
|
||||
length = PInvoke.GetWindowText(hwnd, pBuffer, capacity);
|
||||
}
|
||||
|
||||
return buffer[..length].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all non-system processes whose file path matches the given processPath
|
||||
/// </summary>
|
||||
public IEnumerable<Process> GetSimilarProcesses(string processPath)
|
||||
{
|
||||
return Process.GetProcesses().Where(p => !IsSystemProcess(p) && TryGetProcessFilename(p) == processPath);
|
||||
return Process.GetProcesses().Where(p => !IsSystemProcessOrFlowLauncher(p) && TryGetProcessFilename(p) == processPath);
|
||||
}
|
||||
|
||||
public void TryKill(Process p)
|
||||
public void TryKill(PluginInitContext context, Process p)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -82,7 +131,7 @@ namespace Flow.Launcher.Plugin.ProcessKiller
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Exception($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
|
||||
context.API.LogException($"{nameof(ProcessHelper)}", $"Failed to kill process {p.ProcessName}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,27 @@
|
|||
using System.Diagnostics;
|
||||
using Flow.Launcher.Plugin.SharedModels;
|
||||
|
||||
namespace Flow.Launcher.Plugin.ProcessKiller
|
||||
{
|
||||
internal class ProcessResult
|
||||
{
|
||||
public ProcessResult(Process process, int score)
|
||||
public ProcessResult(Process process, int score, string title, MatchResult match, string tooltip)
|
||||
{
|
||||
Process = process;
|
||||
Score = score;
|
||||
Title = title;
|
||||
TitleMatch = match;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
|
||||
public Process Process { get; }
|
||||
|
||||
public int Score { get; }
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
public MatchResult TitleMatch { get; }
|
||||
|
||||
public string Tooltip { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
Normal file
7
Plugins/Flow.Launcher.Plugin.ProcessKiller/Settings.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Flow.Launcher.Plugin.ProcessKiller
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
public bool PutVisibleWindowProcessesTop { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
namespace Flow.Launcher.Plugin.ProcessKiller.ViewModels
|
||||
{
|
||||
public class SettingsViewModel
|
||||
{
|
||||
public Settings Settings { get; set; }
|
||||
|
||||
public SettingsViewModel(Settings settings)
|
||||
{
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public bool PutVisibleWindowProcessesTop
|
||||
{
|
||||
get => Settings.PutVisibleWindowProcessesTop;
|
||||
set => Settings.PutVisibleWindowProcessesTop = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<UserControl
|
||||
x:Class="Flow.Launcher.Plugin.ProcessKiller.Views.SettingsControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="500"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Margin="{StaticResource SettingPanelMargin}">
|
||||
<Grid.ColumnDefinitions />
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox
|
||||
Grid.Row="0"
|
||||
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
|
||||
Content="{DynamicResource flowlauncher_plugin_processkiller_put_visible_window_process_top}"
|
||||
IsChecked="{Binding PutVisibleWindowProcessesTop}" />
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using System.Windows.Controls;
|
||||
using Flow.Launcher.Plugin.ProcessKiller.ViewModels;
|
||||
|
||||
namespace Flow.Launcher.Plugin.ProcessKiller.Views;
|
||||
|
||||
public partial class SettingsControl : UserControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingsControl.xaml
|
||||
/// </summary>
|
||||
public SettingsControl(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea
|
|||
|
||||
## 📦 Plugins
|
||||
|
||||
- Support wide range of plugins. Visit [here](https://flowlauncher.com/docs/#/plugins) for our plugin portfolio.
|
||||
- Support wide range of plugins. Visit [here](https://www.flowlauncher.com/plugins/) for our plugin portfolio.
|
||||
- Publish your own plugin to flow! Create plugins in:
|
||||
|
||||
<p align="center">
|
||||
|
|
|
|||
Loading…
Reference in a new issue